1
0
Fork 0

Compare commits

...

119 commits

Author SHA1 Message Date
Alexander Yakovlev 9d09e32f24 Merge remote-tracking branch 'upstream/main' 2024-03-23 12:48:02 +06:00
Lim Chee Aun 5695b3ca1e Fix alignment issues with the checkboxes 2024-03-21 08:59:07 +08:00
Lim Chee Aun 15c113ecb1 Reduce brightness
iOS seems to HDR-ify it and it's so annoyingly brighter
2024-03-20 14:30:07 +08:00
Lim Chee Aun 4a75d6f172 Fix flex issues 2024-03-20 11:18:56 +08:00
Lim Chee Aun 8f43099840 More conditional menu dividers
Srsly need better way to render these dividers
2024-03-20 11:04:38 +08:00
Lim Chee Aun a2743f9940 This got prettier-ed 2024-03-20 11:04:38 +08:00
Lim Chee Aun 4c2210c68b MVP-ish filtered notifications UI 2024-03-20 11:04:38 +08:00
Lim Chee Aun da909e4084 Fix wrong filtered counts due to grouped boosts 2024-03-20 11:04:38 +08:00
Lim Chee Aun 552ad249e5 Clean up the usernames 2024-03-20 11:04:38 +08:00
Chee Aun 9a5704ee95
Merge pull request #464 from snail-coupe/phanpy-crmbl-uk
Update README.md - adding another instance
2024-03-18 09:02:03 +08:00
snail-coupe c7f68c8971
Update README.md - adding another instance 2024-03-17 21:31:26 +00:00
Lim Chee Aun e8219e458d Try this font settings out.
Depends on system font's capabilities, so may not work.
2024-03-16 20:02:20 +08:00
Lim Chee Aun 6157ee105c Fix "hide"-filtered post bug again 2024-03-16 18:45:59 +08:00
Lim Chee Aun 4718ef36b0 Need one more detail: site version 2024-03-16 17:49:41 +08:00
Lim Chee Aun 2723ef4593 Attempt to fix wrong boosts count 2024-03-16 13:36:23 +08:00
Chee Aun d1965a84b5
Merge pull request #461 from Vinnl/ellipsis-tooltip
Add tooltip for truncated preview text
2024-03-16 13:33:28 +08:00
Lim Chee Aun c7762cc56f Upgrade dependencies 2024-03-16 10:12:34 +08:00
Vincent cf05568e0c
Add tooltip for truncated preview text
Expose the full content of preview text that might get truncated in
their tooltips.
2024-03-15 18:06:56 +01:00
Lim Chee Aun 69c47489e3 Fix some at-mentions not handled 2024-03-15 18:20:45 +08:00
Lim Chee Aun 861ad83423 More keyboard shortcuts for Catch-up 2024-03-15 18:06:52 +08:00
Lim Chee Aun cd3ed64e48 Show relative time if boosting/quoting old post 2024-03-15 16:02:33 +08:00
Lim Chee Aun 2e28c147b9 Scope the keyboard shortcuts in Catch-up 2024-03-15 09:05:05 +08:00
Lim Chee Aun fef033b282 Show relative time if replying to old post
Ref: https://blog.joinmastodon.org/2023/11/improving-the-quality-of-conversations-on-mastodon/
2024-03-13 13:30:58 +08:00
Lim Chee Aun 3dbbba0be2 Fix captioning turned on even when showCaption = false 2024-03-12 08:14:07 +08:00
Lim Chee Aun 0b8cbbef51 Consider the safe areas 2024-03-11 19:04:08 +08:00
Lim Chee Aun f72ec0aba5 Scroll up too if changing author 2024-03-11 12:21:15 +08:00
Lim Chee Aun d63e6c87c4 Potential perf improvements for canvas 2024-03-10 23:25:07 +08:00
Lim Chee Aun f5ea96a093 Merge dup boosts in Catch-up 2024-03-10 23:24:17 +08:00
Lim Chee Aun 0e1be5dbdc MVP-ish initial implementation of Quote
The menuExtras is hacky, I know.
2024-03-09 21:29:44 +08:00
Lim Chee Aun 4843970e1b Custom context menu if link has hash 2024-03-09 17:01:50 +08:00
Lim Chee Aun a0367f4860 Basic j/k/o/enter shortcuts for Notifications page 2024-03-08 16:25:23 +08:00
Lim Chee Aun 687a08b2a4 Forgot to add 'k' lol
Might as well add 'h' and 'l', & fix the selected author focusing issue
2024-03-08 14:53:38 +08:00
Lim Chee Aun ac07479edd Fix wrong account shown for multiple same-username links 2024-03-08 14:52:31 +08:00
Lim Chee Aun 306a96eec3 Need uppercase C,else it'll be true instead of false
🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️🤦‍♂️
2024-03-07 16:33:56 +08:00
Lim Chee Aun 061d769901 Test fix race-condition for new notifications 2024-03-07 16:06:08 +08:00
Lim Chee Aun cf1c10b338 Show text from poll too 2024-03-07 12:34:38 +08:00
Lim Chee Aun 7f6ef4ff96 Better copy for embed post 2024-03-07 09:05:52 +08:00
Lim Chee Aun ce190cbc50 Lock icon for locked profiles 2024-03-07 09:05:40 +08:00
Lim Chee Aun e7e4f15234 Need extra check on domain 2024-03-06 22:01:13 +08:00
Lim Chee Aun c005745ad0 Fix links layout in embed modal 2024-03-06 19:17:03 +08:00
Lim Chee Aun 0b81b5bfd2 Add menu item to copy handle 2024-03-06 16:51:13 +08:00
Lim Chee Aun b48d32e503 Fix spoiler not working for media 2024-03-06 14:26:01 +08:00
Lim Chee Aun ed309b289f Add categories 2024-03-06 14:25:46 +08:00
Lim Chee Aun ecc5fc5bbe Remove content-visibility, this crops some elements 2024-03-05 23:41:26 +08:00
Lim Chee Aun 7eb77f5d1b Larger separator even for mobile 2024-03-05 23:40:57 +08:00
Lim Chee Aun 3f4832965d Extracting stuff for now 2024-03-05 23:30:12 +08:00
Lim Chee Aun b7ed27ef70 Small catch-up adjustments 2024-03-05 20:56:37 +08:00
Lim Chee Aun c9a48cf482 New .plain6
I honestly need better naming sense
2024-03-05 19:11:50 +08:00
Lim Chee Aun c0ad216227 Merge sort order into sort buttons 2024-03-05 19:11:28 +08:00
Lim Chee Aun 8a9f1a3c25 Fix 2 history icons conflict 2024-03-05 16:23:16 +08:00
Lim Chee Aun 375c4b5d00 Upgrade vite 2024-03-05 16:22:55 +08:00
Lim Chee Aun f522d8e932 Basic j/k keyboard shortcuts for Catch-up 2024-03-05 15:05:26 +08:00
Lim Chee Aun bd46af6166 UI enhancements for Catch-up 2024-03-05 13:32:40 +08:00
Lim Chee Aun 29e9e15d3f Try split it out as another chunk 2024-03-05 00:51:53 +08:00
Lim Chee Aun 42dac0720f Revert "Conditional import polyfill"
This reverts commit 427207ae5a.
2024-03-04 23:41:21 +08:00
Lim Chee Aun d348c458b3 Blurred menu will be opt-in 2024-03-04 21:13:57 +08:00
Lim Chee Aun 427207ae5a Conditional import polyfill 2024-03-04 19:45:57 +08:00
Lim Chee Aun 531147cbc3 It's time for Intl.Segmenter
Remove runes2
2024-03-04 19:38:46 +08:00
Lim Chee Aun e0c2570875 Temporarily disable line to fix sub menu not opening 2024-03-04 17:29:28 +08:00
Lim Chee Aun 2b2f6c28a9 Time to re-organize this main menu
Will need to gather feedback
2024-03-04 16:41:06 +08:00
Lim Chee Aun 4a9cae9cb6 Experiment some Suspense
This splits code, lazy load the other less-critical components
2024-03-04 16:37:34 +08:00
Lim Chee Aun c578b41105 Only show setting if logged-in 2024-03-04 16:36:34 +08:00
Lim Chee Aun cfdbecc608 Better "back" buttons for Catch-up 2024-03-04 14:37:03 +08:00
Lim Chee Aun 7c81548320 Help section for Catch-up 2024-03-04 14:36:47 +08:00
Lim Chee Aun 8cab77415e Only show share and embed if public or unlisted
Also slight refactor
2024-03-04 09:56:38 +08:00
Lim Chee Aun 8b36cef510 Proper passing of props 2024-03-04 09:52:22 +08:00
Lim Chee Aun 4e67edac5e data-id was meant for debugging, removing it 2024-03-03 21:35:44 +08:00
Lim Chee Aun 0bf5ef52ac Only add more gap if there's enough space 2024-03-03 21:35:23 +08:00
Lim Chee Aun 7a7d51f56e Fix the post counts messed up in smaller viewports 2024-03-03 17:44:04 +08:00
Lim Chee Aun 48e1a0753a Make danger menu item more dangerous 2024-03-03 17:41:30 +08:00
Lim Chee Aun 195c2e2960 Turns out this was under the avatar, hmmmm 2024-03-03 17:37:34 +08:00
Lim Chee Aun 60c0d1cca0 Upgrade valtio 2024-03-03 17:31:37 +08:00
Lim Chee Aun 6292557bc9 Default modal to light, add solid class instead 2024-03-03 17:31:06 +08:00
Lim Chee Aun b79ce92aef Use acct instead of username 2024-03-03 17:16:58 +08:00
Lim Chee Aun 6bb6b9c350 Upgrade masto 2024-03-03 17:16:58 +08:00
Chee Aun 0b4c720153
Merge pull request #431 from cvennevik/perf-modal-backdrop-filter
(performance) Remove backdrop-filter blur and saturate effects from modals
2024-03-03 17:16:29 +08:00
Chee Aun 02d1339b29
Merge pull request #430 from cvennevik/perf-notification-icons
(performance) Remove backdrop-filter blur and saturate effect from .account-sub-icons
2024-03-03 17:16:20 +08:00
Alexander Yakovlev 2197dac514 Merge remote-tracking branch 'upstream/main' 2024-03-03 09:41:04 +06:00
Lim Chee Aun 93c871353a Fix status actions close when focused 2024-03-03 11:01:11 +08:00
Lim Chee Aun 641d22a7cc Default density sort to desc 2024-03-03 09:48:53 +08:00
Lim Chee Aun 0fd378811f Fix range order 2024-03-02 21:53:03 +08:00
Lim Chee Aun afb1f6d520 Perf fixes + 3d posts viz 2024-03-02 21:25:54 +08:00
Lim Chee Aun fcb0074f49 Experimental Embed post 2024-03-02 18:55:05 +08:00
Lim Chee Aun 8108151fb6 Fix getComputedStyle running on undefined/null element 2024-03-02 18:54:27 +08:00
Lim Chee Aun d8b0adfe97 Prevent embeds from playing inline 2024-03-02 18:53:35 +08:00
Lim Chee Aun cef4e6373e Add 404 page 2024-03-02 13:53:53 +08:00
Lim Chee Aun 4d138f5773 Upgrade dependencies 2024-03-02 11:23:23 +08:00
Lim Chee Aun 0db10bf7d0 More adaptive copy 2024-03-02 10:08:10 +08:00
Lim Chee Aun 7ab6da5e9b Relayout the previous catchups list 2024-03-02 10:01:22 +08:00
Lim Chee Aun beed3ca18c Fix cloak mode showing ghost text 2024-03-02 10:01:04 +08:00
Lim Chee Aun abd5031602 "What is this" section for Catch-up 2024-03-02 10:00:45 +08:00
Lim Chee Aun 346dba9ed7 Sort by density 2024-03-01 16:03:45 +08:00
Lim Chee Aun 0ceb6ffd06 Tooltip for authors showing display name and username 2024-03-01 16:03:07 +08:00
Lim Chee Aun 488aece050 Better z-indices for the media 2024-03-01 16:02:27 +08:00
Lim Chee Aun ecde88d6a1 Fix weird jump when height of list changes 2024-03-01 16:02:08 +08:00
Lim Chee Aun 94dcd1606a Make toast stay longer, due to longer text 2024-03-01 13:20:34 +08:00
Lim Chee Aun b479fa1f35 Don't scroll vertical 2024-03-01 13:20:12 +08:00
Lim Chee Aun ab0472de02 Fix some links not opening browser's context menu 2024-03-01 10:29:38 +08:00
Lim Chee Aun 1bf8616957 Auto-scroll to selected author 2024-02-29 21:01:31 +08:00
Lim Chee Aun 631333ba9e Cache custom emojis 2024-02-29 18:18:40 +08:00
Lim Chee Aun 69d77c368e Experiment longer captions for no-content single-media post 2024-02-29 13:25:30 +08:00
Lim Chee Aun bb3621e424 Make loader abrupt if >= 3 replies 2024-02-29 13:19:41 +08:00
Lim Chee Aun e1447053b3 Upgrade dependencies 2024-02-29 10:12:05 +08:00
Lim Chee Aun aaf64bbc34 More cloak fixes 2024-02-28 15:34:11 +08:00
Lim Chee Aun 52b60fa38b Respect filters for reply hints 2024-02-28 15:04:01 +08:00
Lim Chee Aun 3acfc00ec0 Don't show toast when not on results page 2024-02-28 11:49:07 +08:00
Lim Chee Aun f8b5e9563c Fix trend links not respecting set instance 2024-02-28 11:27:48 +08:00
Lim Chee Aun 6f3f83a620 Catching up with fixes and enhancements 2024-02-28 11:01:09 +08:00
Lim Chee Aun 315ce98511 Fix cloak for catch-up 2024-02-27 23:29:54 +08:00
Lim Chee Aun 3cfc35898b Slight adjustments 2024-02-27 21:53:08 +08:00
Lim Chee Aun ffc216cfed Fix account info not re-rendering correctly when id changed 2024-02-27 21:24:38 +08:00
Lim Chee Aun 35e34c0bc6 Remove space 2024-02-27 21:23:46 +08:00
Lim Chee Aun b023a43fee Fix weird rendering on Safari 2024-02-27 18:02:12 +08:00
Lim Chee Aun 44f6d9cda0 Remove unused code 2024-02-27 18:02:00 +08:00
Lim Chee Aun c466e0c279 Broken image fallbacks 2024-02-27 18:01:47 +08:00
cvennevik fa99debabd (performance) Remove backdrop-filter blur and saturate effects from modals 2024-02-26 19:37:14 +01:00
cvennevik 58778aba45 (perf) Remove backdrop-filter blur effect from .account-sub-icons 2024-02-26 19:14:29 +01:00
Lim Chee Aun b913c8817d Fix wrong icon size 2024-02-26 21:44:45 +08:00
Lim Chee Aun ffb7ce1c63 Quick style adjusts 2024-02-26 21:13:17 +08:00
52 changed files with 2992 additions and 735 deletions

View file

@ -10,6 +10,7 @@ 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**

View file

@ -200,6 +200,7 @@ These are self-hosted by other wonderful folks.
- [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)
> Note: Add yours by creating a pull request.

571
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -11,6 +11,7 @@
},
"dependencies": {
"@formatjs/intl-localematcher": "~0.5.4",
"@formatjs/intl-segmenter": "~11.5.5",
"@formkit/auto-animate": "~0.8.1",
"@github/text-expander-element": "~2.6.1",
"@iconify-icons/mingcute": "~1.2.9",
@ -22,10 +23,11 @@
"dayjs-twitter": "~0.5.0",
"fast-blurhash": "~1.1.2",
"fast-equals": "~5.0.1",
"html-prettify": "^1.0.7",
"idb-keyval": "~6.2.1",
"just-debounce-it": "~3.2.0",
"lz-string": "~1.5.0",
"masto": "~6.6.0",
"masto": "~6.6.4",
"moize": "~6.1.6",
"p-retry": "~6.2.0",
"p-throttle": "~6.1.0",
@ -34,27 +36,26 @@
"react-intersection-observer": "~9.8.1",
"react-quick-pinch-zoom": "~5.1.0",
"react-router-dom": "6.6.2",
"runes2": "~1.1.4",
"string-length": "5.0.1",
"string-length": "6.0.0",
"swiped-events": "~1.1.9",
"toastify-js": "~1.12.0",
"uid": "~2.0.2",
"use-debounce": "~10.0.0",
"use-long-press": "~3.2.0",
"use-resize-observer": "~9.1.0",
"valtio": "1.9.0"
"valtio": "1.13.2"
},
"devDependencies": {
"@preact/preset-vite": "~2.8.1",
"@preact/preset-vite": "~2.8.2",
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
"postcss": "~8.4.35",
"postcss-dark-theme-class": "~1.2.1",
"postcss-preset-env": "~9.4.0",
"postcss-preset-env": "~9.5.1",
"twitter-text": "~3.1.0",
"vite": "~5.1.4",
"vite": "~5.1.6",
"vite-plugin-generate-file": "~0.1.1",
"vite-plugin-html-config": "~1.0.11",
"vite-plugin-pwa": "~0.19.0",
"vite-plugin-pwa": "~0.19.4",
"vite-plugin-remove-console": "~2.2.0",
"workbox-cacheable-response": "~7.0.0",
"workbox-expiration": "~7.0.0",

32
public/404.html Normal file
View file

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover"
/>
<title>Page not found</title>
<meta name="color-scheme" content="dark light" />
<style>
body {
text-align: center;
font-family: ui-rounded, -apple-system, BlinkMacSystemFont, Segoe UI,
Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
}
h1 {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<h1>Page not found</h1>
<p><a href="/">Go home</a></p>
</body>
</html>

View file

@ -33,8 +33,9 @@ const imageRoute = new Route(
const isRemote = !sameOrigin;
const isImage = request.destination === 'image';
const isAvatar = request.url.includes('/avatars/');
const isCustomEmoji = request.url.includes('/custom/_emojis');
const isEmoji = request.url.includes('/emoji/');
return isRemote && isImage && (isAvatar || isEmoji);
return isRemote && isImage && (isAvatar || isCustomEmoji || isEmoji);
},
new CacheFirst({
cacheName: 'remote-images',

View file

@ -34,6 +34,8 @@ a.mention span {
text-decoration-color: inherit;
text-decoration-thickness: 2px;
text-underline-offset: 2px;
font-variant-numeric: slashed-zero;
font-feature-settings: 'ss01';
}
/* a.mention:has(span).hashtag {
color: var(--link-light-color);
@ -1757,7 +1759,7 @@ body > .szh-menu-container {
max-width: 90vw;
/* overflow: hidden; */
}
.szh-menu[aria-label='Submenu'] {
.szh-menu[aria-label='Submenu'].menu-blur {
background-color: var(--bg-blur-color);
backdrop-filter: blur(4px);
box-shadow: 0 3px 24px -3px var(--drop-shadow-color);
@ -1789,6 +1791,7 @@ body > .szh-menu-container {
animation-duration: 0.3s;
animation-timing-function: ease-in-out;
width: auto;
min-width: min(12em, 90vw);
}
.szh-menu .footer {
margin: 8px 0 -8px;
@ -1925,6 +1928,10 @@ body > .szh-menu-container {
).danger {
color: var(--red-color);
}
.szh-menu
.szh-menu__item.danger:not(.szh-menu__item--disabled).szh-menu__item--hover {
background-color: var(--red-color);
}
.szh-menu
.szh-menu__item:not(.szh-menu__item--disabled):not(
.szh-menu__item--hover
@ -2669,6 +2676,10 @@ ul.link-list li a .icon {
box-shadow: 0px 1px var(--bg-blur-color);
transition: transform 0.4s var(--timing-function);
--back-transition: transform 0.4s ease-out;
&:is(:empty, :has(> a:empty)) {
display: none;
}
}
.timeline:not(.flat) > li > a {
border-radius: inherit;

View file

@ -1,6 +1,7 @@
import './app.css';
import debounce from 'just-debounce-it';
import { lazy, Suspense } from 'preact/compat';
import {
useEffect,
useLayoutEffect,
@ -17,14 +18,14 @@ import ComposeButton from './components/compose-button';
import { ICONS } from './components/ICONS';
import KeyboardShortcutsHelp from './components/keyboard-shortcuts-help';
import Loader from './components/loader';
import Modals from './components/modals';
// import Modals from './components/modals';
import NotificationService from './components/notification-service';
import SearchCommand from './components/search-command';
import Shortcuts from './components/shortcuts';
import NotFound from './pages/404';
import AccountStatuses from './pages/account-statuses';
import Bookmarks from './pages/bookmarks';
import Catchup from './pages/catchup';
// import Catchup from './pages/catchup';
import Favourites from './pages/favourites';
import FollowedHashtags from './pages/followed-hashtags';
import Following from './pages/following';
@ -55,6 +56,9 @@ import store from './utils/store';
import { getCurrentAccount } from './utils/store-utils';
import './utils/toast-alert';
const Catchup = lazy(() => import('./pages/catchup'));
const Modals = lazy(() => import('./components/modals'));
window.__STATES__ = states;
window.__STATES_STATS__ = () => {
const keys = [
@ -382,7 +386,9 @@ function App() {
)}
{isLoggedIn && <ComposeButton />}
{isLoggedIn && <Shortcuts />}
<Modals />
<Suspense>
<Modals />
</Suspense>
{isLoggedIn && <NotificationService />}
<BackgroundService isLoggedIn={isLoggedIn} />
{uiState !== 'loading' && <SearchCommand onClose={focusDeck} />}
@ -458,7 +464,14 @@ function SecondaryRoutes({ isLoggedIn }) {
<Route path=":id" element={<List />} />
</Route>
<Route path="/ft" element={<FollowedHashtags />} />
<Route path="/catchup" element={<Catchup />} />
<Route
path="/catchup"
element={
<Suspense>
<Catchup />
</Suspense>
}
/>
</>
)}
<Route path="/:instance?/t/:hashtag" element={<Hashtag />} />

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -8,12 +8,14 @@ body.cloak,
.name-text *,
.status .content-container,
.status .content-container *,
.status .content-compact,
.status .content-compact > *,
.account-container :is(header, main > *:not(.actions)),
.account-container :is(header, main > *:not(.actions)) *,
.header-double-lines,
.account-block,
.post-peek-html * {
.catchup-filters .filter-author *,
.post-peek-html *,
.post-peek-content > * {
text-decoration-thickness: 1.1em;
text-decoration-line: line-through;
/* text-rendering: optimizeSpeed; */
@ -21,7 +23,8 @@ body.cloak,
}
.name-text *,
.status .content-container *,
.account-container :is(header, main > *:not(.actions)) * {
.account-container :is(header, main > *:not(.actions)) *,
.post-peek-content > * {
filter: none;
}
@ -39,7 +42,16 @@ body.cloak,
/* SPECIAL CASES */
@supports (display: -webkit-box) {
body.cloak .card :is(.title, .meta) {
background-color: var(--text-color) !important;
:is(body.cloak, .cloak) .card :is(.title, .meta) {
background-color: currentColor !important;
}
}
body.cloak,
.cloak {
.media-container figcaption,
.media-container figcaption > *,
.catchup-filters .filter-author * {
color: var(--text-color) !important;
}
}

View file

@ -98,7 +98,11 @@ export const ICONS = {
media: () => import('@iconify-icons/mingcute/photo-album-line'),
speak: () => import('@iconify-icons/mingcute/radar-line'),
building: () => import('@iconify-icons/mingcute/building-5-line'),
history: () => import('@iconify-icons/mingcute/history-2-line'),
history2: () => import('@iconify-icons/mingcute/history-2-line'),
document: () => import('@iconify-icons/mingcute/document-line'),
'arrows-right': () => import('@iconify-icons/mingcute/arrows-right-line'),
code: () => import('@iconify-icons/mingcute/code-line'),
copy: () => import('@iconify-icons/mingcute/copy-2-line'),
quote: () => import('@iconify-icons/mingcute/quote-left-line'),
settings: () => import('@iconify-icons/mingcute/settings-6-line'),
};

View file

@ -33,7 +33,7 @@ function AccountBlock({
<span>
<b></b>
<br />
<span class="account-block-acct">@</span>
<span class="account-block-acct"></span>
</span>
</div>
);
@ -62,6 +62,7 @@ function AccountBlock({
group,
followersCount,
createdAt,
locked,
} = account;
let [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct];
if (accountInstance) {
@ -86,7 +87,7 @@ function AccountBlock({
class="account-block"
href={url}
target={external ? '_blank' : null}
title={`@${acct}`}
title={acct2 ? acct : `@${acct}`}
onClick={(e) => {
if (external) return;
e.preventDefault();
@ -120,9 +121,16 @@ function AccountBlock({
</>
)}{' '}
<span class="account-block-acct">
@{acct1}
{acct2 ? '' : '@'}
{acct1}
<wbr />
{acct2}
{locked && (
<>
{' '}
<Icon icon="lock" size="s" alt="Locked" />
</>
)}
</span>
{showActivity && (
<>

View file

@ -343,7 +343,7 @@ function AccountInfo({
return (
<div
tabIndex="-1"
class={`account-container ${uiState === 'loading' ? 'skeleton' : ''}`}
class={`account-container ${uiState === 'loading' ? 'skeleton' : ''}`}
style={{
'--header-color-1': headerCornerColors[0],
'--header-color-2': headerCornerColors[1],
@ -453,12 +453,15 @@ function AccountInfo({
e.target.classList.add('loaded');
try {
// Get color from four corners of image
const canvas = document.createElement('canvas');
const canvas = window.OffscreenCanvas
? new OffscreenCanvas(1, 1)
: document.createElement('canvas');
const ctx = canvas.getContext('2d', {
willReadFrequently: true,
});
canvas.width = e.target.width;
canvas.height = e.target.height;
ctx.imageSmoothingEnabled = false;
ctx.drawImage(e.target, 0, 0);
// const colors = [
// ctx.getImageData(0, 0, 1, 1).data,
@ -1053,6 +1056,27 @@ function RelatedActions({
<MenuDivider />
</>
)}
<MenuItem
onClick={() => {
const handle = `@${currentInfo?.acct || acct}`;
try {
navigator.clipboard.writeText(handle);
showToast('Handle copied');
} catch (e) {
console.error(e);
showToast('Unable to copy handle');
}
}}
>
<Icon icon="copy" />
<small>
Copy handle
<br />
<span class="more-insignificant">
@{currentInfo?.acct || acct}
</span>
</small>
</MenuItem>
<MenuItem href={url} target="_blank">
<Icon icon="external" />
<small class="menu-double-lines">{niceAccountURL(url)}</small>
@ -1124,6 +1148,7 @@ function RelatedActions({
</MenuItem>
) : (
<SubMenu
menuClassName="menu-blur"
openTrigger="clickOnly"
direction="bottom"
overflow="auto"
@ -1352,7 +1377,6 @@ function RelatedActions({
</div>
{!!showTranslatedBio && (
<Modal
class="light"
onClose={() => {
setShowTranslatedBio(false);
}}
@ -1366,7 +1390,6 @@ function RelatedActions({
)}
{!!showAddRemoveLists && (
<Modal
class="light"
onClose={() => {
setShowAddRemoveLists(false);
}}
@ -1379,7 +1402,6 @@ function RelatedActions({
)}
{!!showPrivateNoteModal && (
<Modal
class="light"
onClose={() => {
setShowPrivateNoteModal(false);
}}
@ -1571,7 +1593,6 @@ function AddRemoveListsSheet({ accountID, onClose }) {
</main>
{showListAddEditModal && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowListAddEditModal(false);

View file

@ -21,6 +21,7 @@ const canvas = window.OffscreenCanvas
const ctx = canvas.getContext('2d', {
willReadFrequently: true,
});
ctx.imageSmoothingEnabled = false;
function Avatar({ url, size, alt = '', squircle, ...props }) {
size = SIZES[size] || size || SIZES.m;

View file

@ -95,6 +95,10 @@
0 1px 10px var(--bg-color), 0 1px 10px var(--bg-color),
0 1px 10px var(--bg-color);
z-index: 2;
strong {
color: var(--red-color);
}
}
#_compose-container .status-preview-legend.reply-to {
color: var(--reply-to-color);

View file

@ -6,7 +6,6 @@ import { deepEqual } from 'fast-equals';
import { forwardRef } from 'preact/compat';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import { substring } from 'runes2';
import stringLength from 'string-length';
import { uid } from 'uid/single';
import { useDebouncedCallback, useThrottledCallback } from 'use-debounce';
@ -131,6 +130,7 @@ const SCAN_RE = new RegExp(
'g',
);
const segmenter = new Intl.Segmenter();
function highlightText(text, { maxCharacters = Infinity }) {
// Accept text string, return formatted HTML string
// Escape all HTML special characters
@ -143,19 +143,25 @@ function highlightText(text, { maxCharacters = Infinity }) {
// Exceeded characters limit
const { composerCharacterCount } = states;
let leftoverHTML = '';
if (composerCharacterCount > maxCharacters) {
// NOTE: runes2 substring considers surrogate pairs
// const leftoverCount = composerCharacterCount - maxCharacters;
// Highlight exceeded characters
leftoverHTML =
'<mark class="compose-highlight-exceeded">' +
// html.slice(-leftoverCount) +
substring(html, maxCharacters) +
'</mark>';
// html = html.slice(0, -leftoverCount);
html = substring(html, 0, maxCharacters);
return html + leftoverHTML;
let withinLimitHTML = '',
exceedLimitHTML = '';
const htmlSegments = segmenter.segment(html);
for (const { segment, index } of htmlSegments) {
if (index < maxCharacters) {
withinLimitHTML += segment;
} else {
exceedLimitHTML += segment;
}
}
if (exceedLimitHTML) {
exceedLimitHTML =
'<mark class="compose-highlight-exceeded">' +
exceedLimitHTML +
'</mark>';
}
return withinLimitHTML + exceedLimitHTML;
}
return html
@ -168,6 +174,8 @@ function highlightText(text, { maxCharacters = Infinity }) {
); // Emoji shortcodes
}
const rtf = new Intl.RelativeTimeFormat();
function Compose({
onClose,
replyToStatus,
@ -229,6 +237,12 @@ function Compose({
};
const focusTextarea = () => {
setTimeout(() => {
if (!textareaRef.current) return;
// status starts with newline, focus on first position
if (draftStatus?.status?.startsWith?.('\n')) {
textareaRef.current.selectionStart = 0;
textareaRef.current.selectionEnd = 0;
}
console.debug('FOCUS textarea');
textareaRef.current?.focus();
}, 300);
@ -625,6 +639,16 @@ function Compose({
return [topLanguages, restLanguages];
}, [language]);
const replyToStatusMonthsAgo = useMemo(
() =>
!!replyToStatus?.createdAt &&
Math.floor(
(Date.now() - new Date(replyToStatus.createdAt)) /
(1000 * 60 * 60 * 24 * 30),
),
[replyToStatus],
);
return (
<div id="compose-container-outer">
<div id="compose-container" class={standalone ? 'standalone' : ''}>
@ -774,6 +798,16 @@ function Compose({
Replying to @
{replyToStatus.account.acct || replyToStatus.account.username}
&rsquo;s post
{replyToStatusMonthsAgo >= 3 && (
<>
{' '}
(
<strong>
{rtf.format(-replyToStatusMonthsAgo, 'month')}
</strong>
)
</>
)}
</div>
</div>
)}
@ -1254,7 +1288,6 @@ function Compose({
</div>
{showEmoji2Picker && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowEmoji2Picker(false);
@ -1768,7 +1801,6 @@ function MediaAttachment({
</div>
{showModal && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowModal(false);

View file

@ -32,7 +32,7 @@ export default memo(function KeyboardShortcutsHelp() {
return (
!!snapStates.showKeyboardShortcutsHelp && (
<Modal class="light" onClose={onClose}>
<Modal onClose={onClose}>
<div id="keyboard-shortcuts-help-container" class="sheet" tabindex="-1">
<button type="button" class="sheet-close" onClick={onClose}>
<Icon icon="x" />

View file

@ -54,6 +54,7 @@ const AltBadge = (props) => {
};
const MEDIA_CAPTION_LIMIT = 140;
const MEDIA_CAPTION_LIMIT_LONGER = 280;
export const isMediaCaptionLong = mem((caption) =>
caption?.length
? caption.length > MEDIA_CAPTION_LIMIT ||
@ -69,6 +70,7 @@ function Media({
showOriginal,
autoAnimate,
showCaption,
allowLongerCaption,
altIndex,
onClick = () => {},
}) {
@ -198,8 +200,15 @@ function Media({
};
const longDesc = isMediaCaptionLong(description);
const showInlineDesc =
let showInlineDesc =
!!showCaption && !showOriginal && !!description && !longDesc;
if (
allowLongerCaption &&
!showInlineDesc &&
description?.length <= MEDIA_CAPTION_LIMIT_LONGER
) {
showInlineDesc = true;
}
const Figure = !showInlineDesc
? Fragment
: (props) => {

View file

@ -1,4 +1,4 @@
import { Menu, MenuItem, SubMenu } from '@szhsin/react-menu';
import { MenuItem, SubMenu } from '@szhsin/react-menu';
import { cloneElement } from 'preact';
import { useRef } from 'preact/hooks';
@ -10,6 +10,7 @@ function MenuConfirm({
confirmLabel,
menuItemClassName,
menuFooter,
menuExtras,
...props
}) {
const { children, onClick, ...restProps } = props;
@ -53,6 +54,7 @@ function MenuConfirm({
<MenuItem className={menuItemClassName} onClick={onClick}>
{confirmLabel}
</MenuItem>
{menuExtras}
{menuFooter}
</Parent>
);

View file

@ -3,11 +3,12 @@ import { FocusableItem } from '@szhsin/react-menu';
import Link from './link';
function MenuLink(props) {
const { className, disabled, ...restProps } = props;
return (
<FocusableItem>
<FocusableItem className={className} disabled={disabled}>
{({ ref, closeMenu }) => (
<Link
{...props}
{...restProps}
ref={ref}
onClick={({ detail }) =>
closeMenu(detail === 0 ? 'Enter' : undefined)

View file

@ -9,17 +9,18 @@
justify-content: center;
align-items: center;
background-color: var(--backdrop-color);
backdrop-filter: blur(24px);
animation: appear 0.5s var(--timing-function) both;
}
#modal-container > div .sheet {
transition: transform 0.3s var(--timing-function);
transform-origin: center bottom;
}
#modal-container > div:has(~ div) .sheet {
transform: scale(0.975);
}
#modal-container > .light {
backdrop-filter: saturate(0.75);
&.solid {
background-color: var(--backdrop-solid-color);
}
.sheet {
transition: transform 0.3s var(--timing-function);
transform-origin: center bottom;
}
&:has(~ div) .sheet {
transform: scale(0.975);
}
}

View file

@ -61,6 +61,7 @@ function Modal({ children, onClose, onClick, class: className }) {
const focusElement =
modalRef.current?.querySelector('[tabindex="-1"]');
const isFocusable =
!!focusElement &&
getComputedStyle(focusElement)?.pointerEvents !== 'none';
if (focusElement && isFocusable) {
focusElement.focus();

View file

@ -35,7 +35,7 @@ export default function Modals() {
return (
<>
{!!snapStates.showCompose && (
<Modal>
<Modal class="solid">
<Compose
replyToStatus={
typeof snapStates.showCompose !== 'boolean'
@ -109,7 +109,6 @@ export default function Modals() {
)}
{!!snapStates.showAccount && (
<Modal
class="light"
onClose={() => {
states.showAccount = false;
}}
@ -160,7 +159,6 @@ export default function Modals() {
)}
{!!snapStates.showShortcutsSettings && (
<Modal
class="light"
onClose={() => {
states.showShortcutsSettings = false;
}}
@ -172,7 +170,6 @@ export default function Modals() {
)}
{!!snapStates.showGenericAccounts && (
<Modal
class="light"
onClose={() => {
states.showGenericAccounts = false;
}}
@ -188,7 +185,6 @@ export default function Modals() {
)}
{!!snapStates.showMediaAlt && (
<Modal
class="light"
onClose={(e) => {
states.showMediaAlt = false;
}}
@ -204,6 +200,7 @@ export default function Modals() {
)}
{!!snapStates.showEmbedModal && (
<Modal
class="solid"
onClose={() => {
states.showEmbedModal = false;
}}

View file

@ -8,6 +8,11 @@
font-weight: 500;
unicode-bidi: isolate;
}
i {
font-variant-numeric: slashed-zero;
font-feature-settings: 'ss01';
}
}
.name-text.show-acct {
display: inline-block;

View file

@ -50,7 +50,11 @@ function NameText({
class={`name-text ${showAcct ? 'show-acct' : ''} ${short ? 'short' : ''}`}
href={url}
target={external ? '_blank' : null}
title={`${displayName ? `${displayName} ` : ''}@${acct}`}
title={
displayName
? `${displayName} (${acct2 ? '' : '@'}${acct})`
: `${acct2 ? '' : '@'}${acct}`
}
onClick={(e) => {
if (external) return;
e.preventDefault();
@ -88,8 +92,9 @@ function NameText({
<>
<br />
<i>
@{acct1}
<span class="ib">{acct2}</span>
{acct2 ? '' : '@'}
{acct1}
{!!acct2 && <span class="ib">{acct2}</span>}
</i>
</>
)}

View file

@ -1,6 +1,11 @@
import './nav-menu.css';
import { ControlledMenu, MenuDivider, MenuItem } from '@szhsin/react-menu';
import {
ControlledMenu,
MenuDivider,
MenuItem,
SubMenu,
} from '@szhsin/react-menu';
import { memo } from 'preact/compat';
import { useEffect, useRef, useState } from 'preact/hooks';
import { useLongPress } from 'use-long-press';
@ -130,7 +135,7 @@ function NavMenu(props) {
if (Date.now() - buttonClickTS.current < 300) {
return;
}
setMenuState(undefined);
// setMenuState(undefined);
},
}}
portal={{
@ -169,7 +174,7 @@ function NavMenu(props) {
<MenuLink to="/">
<Icon icon="home" size="l" /> <span>Home</span>
</MenuLink>
{authenticated && (
{authenticated ? (
<>
{showFollowing && (
<MenuLink to="/following">
@ -177,7 +182,7 @@ function NavMenu(props) {
</MenuLink>
)}
<MenuLink to="/catchup">
<Icon icon="history" />
<Icon icon="history2" size="l" />
<span>Catch-up</span>
</MenuLink>
<MenuLink to="/mentions">
@ -192,44 +197,64 @@ function NavMenu(props) {
</sup>
)}
</MenuLink>
<MenuDivider />
<MenuLink to="/l">
<Icon icon="list" size="l" /> <span>Lists</span>
</MenuLink>
<MenuLink to="/ft">
<Icon icon="hashtag" size="l" /> <span>Followed Hashtags</span>
</MenuLink>
<MenuLink to="/b">
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
</MenuLink>
<MenuLink to="/f">
<Icon icon="heart" size="l" /> <span>Likes</span>
</MenuLink>
</>
)}
<MenuDivider />
<MenuLink to={`/search`}>
<Icon icon="search" size="l" /> <span>Search</span>
</MenuLink>
<MenuLink to={`/${instance}/p/l`}>
<Icon icon="building" size="l" /> <span>Local</span>
</MenuLink>
<MenuLink to={`/${instance}/p`}>
<Icon icon="earth" size="l" /> <span>Federated</span>
</MenuLink>
<MenuLink to={`/${instance}/trending`}>
<Icon icon="chart" size="l" /> <span>Trending</span>
</MenuLink>
</section>
<section>
{authenticated ? (
<>
<MenuDivider />
{currentAccount?.info?.id && (
<MenuLink to={`/${instance}/a/${currentAccount.info.id}`}>
<Icon icon="user" size="l" /> <span>Profile</span>
</MenuLink>
)}
<MenuLink to="/l">
<Icon icon="list" size="l" /> <span>Lists</span>
</MenuLink>
<MenuLink to="/b">
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
</MenuLink>
<SubMenu
overflow="auto"
gap={-8}
label={
<>
<Icon icon="more" size="l" />
<span class="menu-grow">More</span>
<Icon icon="chevron-right" />
</>
}
>
<MenuLink to="/f">
<Icon icon="heart" size="l" /> <span>Likes</span>
</MenuLink>
<MenuLink to="/ft">
<Icon icon="hashtag" size="l" />{' '}
<span>Followed Hashtags</span>
</MenuLink>
<MenuDivider />
<MenuItem
onClick={() => {
states.showGenericAccounts = {
id: 'mute',
heading: 'Muted users',
fetchAccounts: fetchMutes,
excludeRelationshipAttrs: ['muting'],
};
}}
>
<Icon icon="mute" size="l" /> Muted users&hellip;
</MenuItem>
<MenuItem
onClick={() => {
states.showGenericAccounts = {
id: 'block',
heading: 'Blocked users',
fetchAccounts: fetchBlocks,
excludeRelationshipAttrs: ['blocking'],
};
}}
>
<Icon icon="block" size="l" />
Blocked users&hellip;
</MenuItem>{' '}
</SubMenu>
<MenuDivider />
<MenuItem
onClick={() => {
states.showAccounts = true;
@ -237,31 +262,32 @@ function NavMenu(props) {
>
<Icon icon="group" size="l" /> <span>Accounts&hellip;</span>
</MenuItem>
<MenuItem
onClick={() => {
states.showGenericAccounts = {
id: 'mute',
heading: 'Muted users',
fetchAccounts: fetchMutes,
excludeRelationshipAttrs: ['muting'],
};
}}
>
<Icon icon="mute" size="l" /> Muted users&hellip;
</MenuItem>
<MenuItem
onClick={() => {
states.showGenericAccounts = {
id: 'block',
heading: 'Blocked users',
fetchAccounts: fetchBlocks,
excludeRelationshipAttrs: ['blocking'],
};
}}
>
<Icon icon="block" size="l" />
Blocked users&hellip;
</MenuItem>
</>
) : (
<>
<MenuDivider />
<MenuLink to="/login">
<Icon icon="user" size="l" /> <span>Log in</span>
</MenuLink>
</>
)}
</section>
<section>
<MenuDivider />
<MenuLink to={`/search`}>
<Icon icon="search" size="l" /> <span>Search</span>
</MenuLink>
<MenuLink to={`/${instance}/trending`}>
<Icon icon="chart" size="l" /> <span>Trending</span>
</MenuLink>
<MenuLink to={`/${instance}/p/l`}>
<Icon icon="building" size="l" /> <span>Local</span>
</MenuLink>
<MenuLink to={`/${instance}/p`}>
<Icon icon="earth" size="l" /> <span>Federated</span>
</MenuLink>
{authenticated ? (
<>
<MenuDivider className="divider-grow" />
<MenuItem
onClick={() => {
@ -290,9 +316,6 @@ function NavMenu(props) {
) : (
<>
<MenuDivider />
<MenuLink to="/login">
<Icon icon="user" size="l" /> <span>Log in</span>
</MenuLink>
<MenuItem
onClick={() => {
states.showSettings = true;

View file

@ -144,7 +144,6 @@ export default memo(function NotificationService() {
const { id, account, notification, sameInstance } = showNotificationSheet;
return (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
onClose();

View file

@ -26,6 +26,8 @@
background-color: var(--bg-blur-color);
backdrop-filter: blur(16px);
padding: 16px;
padding: calc(var(--sai-top, 0) + 16px) calc(var(--sai-right, 0) + 16px)
16px calc(var(--sai-left, 0) + 16px);
display: flex;
gap: 8px;
justify-content: space-between;
@ -41,6 +43,8 @@
main {
padding: 0 16px 16px;
padding: 0 calc(var(--sai-right, 0) + 16px)
calc(var(--sai-bottom, 0) + 16px) calc(var(--sai-left, 0) + 16px);
/* display: flex;
flex-direction: column;
gap: 16px; */

View file

@ -232,8 +232,8 @@ function ReportModal({ account, post, onClose }) {
disabled={uiState === 'loading'}
/>
</section>
<section>
{domain !== currentDomain && (
{!!domain && domain !== currentDomain && (
<section>
<p>
<label>
<input
@ -247,8 +247,8 @@ function ReportModal({ account, post, onClose }) {
</span>
</label>
</p>
)}
</section>
</section>
)}
<footer>
<button type="submit" disabled={uiState === 'loading'}>
Send Report

View file

@ -73,7 +73,7 @@ const SearchForm = forwardRef((props, ref) => {
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
spellCheck="false"
onSearch={(e) => {
if (!e.target.value) {
setSearchParams({});

View file

@ -392,7 +392,11 @@ function ShortcutsSettings({ onClose }) {
</>
) : (
<div class="ui-state insignificant">
<p>No shortcuts yet. Tap on the Add shortcut button.</p>
<p>
{snapStates.settings.shortcutsViewMode === 'multi-column'
? 'No columns yet. Tap on the Add column button.'
: 'No shortcuts yet. Tap on the Add shortcut button.'}
</p>
<p>
Not sure what to add?
<br />
@ -419,7 +423,9 @@ function ShortcutsSettings({ onClose }) {
)}
<p class="insignificant">
{shortcuts.length >= SHORTCUTS_LIMIT &&
`Max ${SHORTCUTS_LIMIT} shortcuts`}
(snapStates.settings.shortcutsViewMode === 'multi-column'
? `Max ${SHORTCUTS_LIMIT} columns`
: `Max ${SHORTCUTS_LIMIT} shortcuts`)}
</p>
<p
style={{
@ -451,7 +457,6 @@ function ShortcutsSettings({ onClose }) {
</main>
{showForm && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowForm(false);
@ -475,7 +480,6 @@ function ShortcutsSettings({ onClose }) {
)}
{showImportExport && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowImportExport(false);
@ -667,7 +671,7 @@ function ShortcutForm({
}
autocorrect="off"
autocapitalize="off"
spellcheck={false}
spellCheck={false}
pattern={pattern}
/>
{currentType === 'hashtag' &&

View file

@ -404,6 +404,24 @@
font-size: 90%;
line-height: var(--avatar-size);
}
.status-filtered-badge.badge-meta {
margin-top: 6px;
flex-direction: row;
gap: 0.5em;
color: var(--text-color);
border-color: var(--text-color);
background-color: var(--bg-blur-color);
max-width: 100%;
> span + span {
position: static;
&:empty {
display: none;
}
}
}
}
.status .container {
@ -661,7 +679,9 @@
animation: none !important; */
}
}
.status .content-container.has-spoiler:not(.show-media) .spoiler-media-button {
.status
.content-container.has-spoiler:not(.show-media)
:is(.spoiler-button, .spoiler-media-button) {
~ :is(.media-container, .media-figure-multiple) figcaption {
/* filter: blur(5px) invert(0.5);
image-rendering: crisp-edges;
@ -1591,6 +1611,11 @@ a.card:is(:hover, :focus):visited {
.card.video {
max-width: 320px;
max-height: 320px;
cursor: pointer;
lite\-youtube {
pointer-events: none;
}
}
.card.video iframe {
width: 100%;
@ -1971,6 +1996,7 @@ a.card:is(:hover, :focus):visited {
}
.status:focus &,
.status:focus-within &,
&.open {
opacity: 1;
pointer-events: auto;
@ -2112,6 +2138,97 @@ a.card:is(:hover, :focus):visited {
pointer-events: none;
}
/* EMBED */
#embed-post {
> main > section {
p {
margin-block: 0.5em;
}
ul {
margin: 0;
padding-inline: 1em;
}
p + ul {
margin-top: 0;
padding-top: 0;
}
}
.embed-code {
width: 100%;
resize: vertical;
min-height: 12em;
max-height: 40vh;
font-family: var(--monospace-font);
font-size: 0.8em;
border-color: var(--link-color);
/* background-color: var(--bg-faded-color); */
}
.links-list {
li > a {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
.embed-preview {
display: block;
max-height: 40vh;
overflow: auto;
font-size: 0.9em;
border: 2px dashed var(--link-light-color);
border-radius: 8px;
box-shadow: 0 4px 8px -4px var(--drop-shadow-color),
0 8px 32px -8px var(--drop-shadow-color);
padding: 16px;
/* Interactive elements */
button,
a,
video,
audio,
input,
select,
textarea,
iframe,
object,
embed {
pointer-events: none;
}
blockquote {
margin: 0 0 1em;
border-inline-start: 4px solid var(--outline-color);
padding-inline-start: 1em;
> p:first-child {
margin-top: 0;
}
}
ul,
ol {
margin-inline: 0;
padding-inline: 1em;
}
figure {
margin-inline: 0;
img,
video,
audio {
max-width: 100%;
height: auto;
}
}
}
}
/* DELETED */
.status-deleted {

View file

@ -10,6 +10,7 @@ import {
} from '@szhsin/react-menu';
import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash';
import { shallowEqual } from 'fast-equals';
import prettify from 'html-prettify';
import { memo } from 'preact/compat';
import {
useCallback,
@ -84,6 +85,8 @@ const isIOS =
window.ontouchstart !== undefined &&
/iPad|iPhone|iPod/.test(navigator.userAgent);
const rtf = new Intl.RelativeTimeFormat();
const REACTIONS_LIMIT = 80;
function getPollText(poll) {
@ -451,6 +454,7 @@ function Status({
]);
const [showEdited, setShowEdited] = useState(false);
const [showEmbed, setShowEmbed] = useState(false);
const spoilerContentRef = useTruncated();
const contentRef = useTruncated();
@ -506,6 +510,13 @@ function Status({
(attachment) => !attachment.description?.trim?.(),
);
}, [mediaAttachments]);
const statusMonthsAgo = useMemo(() => {
return Math.floor(
(new Date() - createdAtDate) / (1000 * 60 * 60 * 24 * 30),
);
}, [createdAtDate]);
const boostStatus = async () => {
if (!sameInstance || !authenticated) {
alert(unauthInteractionErrorMessage);
@ -709,6 +720,8 @@ function Status({
}
const actionsRef = useRef();
const isPublic = ['public', 'unlisted'].includes(visibility);
const isPinnable = ['public', 'unlisted', 'private'].includes(visibility);
const StatusMenuItems = (
<>
{isSizeLarge && (
@ -744,17 +757,41 @@ function Status({
confirmLabel={
<>
<Icon icon="rocket" />
<span>{reblogged ? 'Unboost?' : 'Boost to everyone?'}</span>
<span>{reblogged ? 'Unboost' : 'Boost'}</span>
</>
}
className={`menu-reblog ${reblogged ? 'checked' : ''}`}
menuExtras={
<MenuItem
onClick={() => {
states.showCompose = {
draftStatus: {
status: `\n${url}`,
},
};
}}
>
<Icon icon="quote" />
<span>Quote</span>
</MenuItem>
}
menuFooter={
mediaNoDesc &&
!reblogged && (
mediaNoDesc && !reblogged ? (
<div class="footer">
<Icon icon="alert" />
Some media have no descriptions.
</div>
) : (
statusMonthsAgo >= 3 && (
<div class="footer">
<Icon icon="info" />
<span>
Old post (
<strong>{rtf.format(-statusMonthsAgo, 'month')}</strong>
)
</span>
</div>
)
)
}
disabled={!canBoost}
@ -854,13 +891,12 @@ function Status({
</div>
)
)}
{!isSizeLarge ||
((enableTranslate || !language || differentLanguage) && (
<MenuDivider />
))}
{((!isSizeLarge && sameInstance) ||
enableTranslate ||
!language ||
differentLanguage) && <MenuDivider />}
{!isSizeLarge && (
<>
<MenuDivider />
<MenuLink
to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
onClick={(e) => {
@ -914,7 +950,8 @@ function Status({
<Icon icon="link" />
<span>Copy</span>
</MenuItem>
{navigator?.share &&
{isPublic &&
navigator?.share &&
navigator?.canShare?.({
url,
}) && (
@ -935,6 +972,16 @@ function Status({
</MenuItem>
)}
</div>
{isPublic && isSizeLarge && (
<MenuItem
onClick={() => {
setShowEmbed(true);
}}
>
<Icon icon="code" />
<span>Embed post</span>
</MenuItem>
)}
{(isSelf || mentionSelf) && <MenuDivider />}
{(isSelf || mentionSelf) && (
<MenuItem
@ -968,7 +1015,7 @@ function Status({
)}
</MenuItem>
)}
{isSelf && /(public|unlisted|private)/i.test(visibility) && (
{isSelf && isPinnable && (
<MenuItem
onClick={async () => {
try {
@ -1085,7 +1132,12 @@ function Status({
const { clientX, clientY } = e.touches?.[0] || e;
// link detection copied from onContextMenu because here it works
const link = e.target.closest('a');
if (link && /^https?:\/\//.test(link.getAttribute('href'))) return;
if (
link &&
statusRef.current.contains(link) &&
!link.getAttribute('href').startsWith('#')
)
return;
e.preventDefault();
setContextMenuProps({
anchorPoint: {
@ -1331,7 +1383,12 @@ function Status({
if (e.metaKey) return;
// console.log('context menu', e);
const link = e.target.closest('a');
if (link && /^https?:\/\//.test(link.getAttribute('href'))) return;
if (
link &&
statusRef.current.contains(link) &&
!link.getAttribute('href').startsWith('#')
)
return;
// If there's selected text, don't show custom context menu
const selection = window.getSelection?.();
@ -1783,6 +1840,9 @@ function Status({
media={media}
autoAnimate={isSizeLarge}
showCaption={mediaAttachments.length === 1}
allowLongerCaption={
!content && mediaAttachments.length === 1
}
lang={language}
altIndex={
showMultipleMediaCaptions &&
@ -1894,11 +1954,23 @@ function Status({
confirmLabel={
<>
<Icon icon="rocket" />
<span>
{reblogged ? 'Unboost?' : 'Boost to everyone?'}
</span>
<span>{reblogged ? 'Unboost' : 'Boost'}</span>
</>
}
menuExtras={
<MenuItem
onClick={() => {
states.showCompose = {
draftStatus: {
status: `\n${url}`,
},
};
}}
>
<Icon icon="quote" />
<span>Quote</span>
</MenuItem>
}
menuFooter={
mediaNoDesc &&
!reblogged && (
@ -1972,7 +2044,6 @@ function Status({
</div>
{!!showEdited && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowEdited(false);
@ -1993,6 +2064,23 @@ function Status({
/>
</Modal>
)}
{!!showEmbed && (
<Modal
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowEmbed(false);
}
}}
>
<EmbedModal
post={status}
instance={instance}
onClose={() => {
setShowEmbed(false);
}}
/>
</Modal>
)}
</article>
</>
);
@ -2099,10 +2187,13 @@ function Card({ card, selfReferential, instance }) {
const w = 44;
const h = 44;
const blurhashPixels = decodeBlurHash(blurhash, w, h);
const canvas = document.createElement('canvas');
const canvas = window.OffscreenCanvas
? new OffscreenCanvas(1, 1)
: document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
const imageData = ctx.createImageData(w, h);
imageData.data.set(blurhashPixels);
ctx.putImageData(imageData, 0, 0);
@ -2140,10 +2231,10 @@ function Card({ card, selfReferential, instance }) {
<p class="meta domain" dir="auto">
{domain}
</p>
<p class="title" dir="auto">
<p class="title" dir="auto" title={title}>
{title}
</p>
<p class="meta" dir="auto">
<p class="meta" dir="auto" title={description}>
{description ||
(!!publishedAt && (
<RelativeTime datetime={publishedAt} format="micro" />
@ -2180,7 +2271,11 @@ function Card({ card, selfReferential, instance }) {
// Get ID from e.g. https://www.youtube.com/watch?v=[VIDEO_ID]
const videoID = url.match(/watch\?v=([^&]+)/)?.[1];
if (videoID) {
return <lite-youtube videoid={videoID} nocookie></lite-youtube>;
return (
<a class="card video" onClick={handleClick}>
<lite-youtube videoid={videoID} nocookie></lite-youtube>
</a>
);
}
}
// return (
@ -2208,8 +2303,12 @@ function Card({ card, selfReferential, instance }) {
<p class="meta domain">
<Icon icon="link" size="s" /> <span>{domain}</span>
</p>
<p class="title">{title}</p>
<p class="meta">{description || providerName || authorName}</p>
<p class="title" title={title}>
{title}
</p>
<p class="meta" title={description || providerName || authorName}>
{description || providerName || authorName}
</p>
</div>
</a>
);
@ -2293,6 +2392,360 @@ function EditedAtModal({
);
}
function generateHTMLCode(post, instance, level = 0) {
const {
account: {
url: accountURL,
displayName,
acct,
username,
emojis: accountEmojis,
bot,
group,
},
id,
poll,
spoilerText,
language,
editedAt,
createdAt,
content,
mediaAttachments,
url,
emojis,
} = post;
const sKey = statusKey(id, instance);
const quotes = states.statusQuotes[sKey] || [];
const uniqueQuotes = quotes.filter(
(q, i, arr) => arr.findIndex((q2) => q2.url === q.url) === i,
);
const quoteStatusesHTML =
uniqueQuotes.length && level <= 2
? uniqueQuotes
.map((quote) => {
const { id, instance } = quote;
const sKey = statusKey(id, instance);
const s = states.statuses[sKey];
if (s) {
return generateHTMLCode(s, instance, ++level);
}
})
.join('')
: '';
const createdAtDate = new Date(createdAt);
// const editedAtDate = editedAt && new Date(editedAt);
const contentHTML =
emojifyText(content, emojis) +
'\n' +
quoteStatusesHTML +
'\n' +
(poll?.options?.length
? `
<p>📊:</p>
<ul>
${poll.options
.map(
(option) => `
<li>
${option.title}
${option.votesCount >= 0 ? ` (${option.votesCount})` : ''}
</li>
`,
)
.join('')}
</ul>`
: '') +
(mediaAttachments.length > 0
? '\n' +
mediaAttachments
.map((media) => {
const {
description,
meta,
previewRemoteUrl,
previewUrl,
remoteUrl,
url,
type,
} = media;
const { original = {}, small } = meta || {};
const width = small?.width || original?.width;
const height = small?.height || original?.height;
// Prefer remote over original
const sourceMediaURL = remoteUrl || url;
const previewMediaURL = previewRemoteUrl || previewUrl;
const mediaURL = previewMediaURL || sourceMediaURL;
const sourceMediaURLObj = sourceMediaURL
? new URL(sourceMediaURL)
: null;
const isVideoMaybe =
type === 'unknown' &&
sourceMediaURLObj &&
/\.(mp4|m4r|m4v|mov|webm)$/i.test(sourceMediaURLObj.pathname);
const isAudioMaybe =
type === 'unknown' &&
sourceMediaURLObj &&
/\.(mp3|ogg|wav|m4a|m4p|m4b)$/i.test(sourceMediaURLObj.pathname);
const isImage =
type === 'image' ||
(type === 'unknown' &&
previewMediaURL &&
!isVideoMaybe &&
!isAudioMaybe);
const isVideo = type === 'gifv' || type === 'video' || isVideoMaybe;
const isAudio = type === 'audio' || isAudioMaybe;
let mediaHTML = '';
if (isImage) {
mediaHTML = `<img src="${mediaURL}" width="${width}" height="${height}" alt="${description}" loading="lazy" />`;
} else if (isVideo) {
mediaHTML = `
<video src="${sourceMediaURL}" width="${width}" height="${height}" controls preload="auto" poster="${previewMediaURL}" loading="lazy"></video>
${description ? `<figcaption>${description}</figcaption>` : ''}
`;
} else if (isAudio) {
mediaHTML = `
<audio src="${sourceMediaURL}" controls preload="auto"></audio>
${description ? `<figcaption>${description}</figcaption>` : ''}
`;
} else {
mediaHTML = `
<a href="${sourceMediaURL}">📄 ${
description || sourceMediaURL
}</a>
`;
}
return `<figure>${mediaHTML}</figure>`;
})
.join('\n')
: '');
const htmlCode = `
<blockquote lang="${language}" cite="${url}">
${
spoilerText
? `
<details>
<summary>${spoilerText}</summary>
${contentHTML}
</details>
`
: contentHTML
}
<footer>
${emojifyText(
displayName,
accountEmojis,
)} (@${acct}) <a href="${url}"><time datetime="${createdAtDate.toISOString()}">${createdAtDate.toLocaleString()}</time></a>
</footer>
</blockquote>
`;
return prettify(htmlCode);
}
function EmbedModal({ post, instance, onClose }) {
const {
account: {
url: accountURL,
displayName,
username,
emojis: accountEmojis,
bot,
group,
},
id,
poll,
spoilerText,
language,
editedAt,
createdAt,
content,
mediaAttachments,
url,
emojis,
} = post;
const htmlCode = generateHTMLCode(post, instance);
return (
<div id="embed-post" class="sheet">
{!!onClose && (
<button type="button" class="sheet-close" onClick={onClose}>
<Icon icon="x" />
</button>
)}
<header>
<h2>Embed post</h2>
</header>
<main tabIndex="-1">
<h3>HTML Code</h3>
<textarea
class="embed-code"
readonly
onClick={(e) => {
e.target.select();
}}
>
{htmlCode}
</textarea>
<button
type="button"
onClick={() => {
try {
navigator.clipboard.writeText(htmlCode);
showToast('HTML code copied');
} catch (e) {
console.error(e);
showToast('Unable to copy HTML code');
}
}}
>
<Icon icon="clipboard" /> <span>Copy</span>
</button>
{!!mediaAttachments?.length && (
<section>
<p>Media attachments:</p>
<ol class="links-list">
{mediaAttachments.map((media) => {
return (
<li key={media.id}>
<a
href={media.remoteUrl || media.url}
target="_blank"
download
>
{media.remoteUrl || media.url}
</a>
</li>
);
})}
</ol>
</section>
)}
{!!accountEmojis?.length && (
<section>
<p>Account Emojis:</p>
<ul>
{accountEmojis.map((emoji) => {
return (
<li key={emoji.shortcode}>
<picture>
<source
srcset={emoji.staticUrl}
media="(prefers-reduced-motion: reduce)"
></source>
<img
class="shortcode-emoji emoji"
src={emoji.url}
alt={`:${emoji.shortcode}:`}
width="16"
height="16"
loading="lazy"
decoding="async"
/>
</picture>{' '}
<code>:{emoji.shortcode}:</code> (
<a href={emoji.url} target="_blank" download>
url
</a>
)
{emoji.staticUrl ? (
<>
{' '}
(
<a href={emoji.staticUrl} target="_blank" download>
static
</a>
)
</>
) : null}
</li>
);
})}
</ul>
</section>
)}
{!!emojis?.length && (
<section>
<p>Emojis:</p>
<ul>
{emojis.map((emoji) => {
return (
<li key={emoji.shortcode}>
<picture>
<source
srcset={emoji.staticUrl}
media="(prefers-reduced-motion: reduce)"
></source>
<img
class="shortcode-emoji emoji"
src={emoji.url}
alt={`:${emoji.shortcode}:`}
width="16"
height="16"
loading="lazy"
decoding="async"
/>
</picture>{' '}
<code>:{emoji.shortcode}:</code> (
<a href={emoji.url} target="_blank" download>
url
</a>
)
{emoji.staticUrl ? (
<>
{' '}
(
<a href={emoji.staticUrl} target="_blank" download>
static
</a>
)
</>
) : null}
</li>
);
})}
</ul>
</section>
)}
<section>
<small>
<p>Notes:</p>
<ul>
<li>
This is static, unstyled and scriptless. You may need to apply
your own styles and edit as needed.
</li>
<li>
Polls are not interactive, becomes a list with vote counts.
</li>
<li>
Media attachments can be images, videos, audios or any file
types.
</li>
<li>Post could be edited or deleted later.</li>
</ul>
</small>
</section>
<h3>Preview</h3>
<output
class="embed-preview"
dangerouslySetInnerHTML={{ __html: htmlCode }}
/>
<p>
<small>Note: This preview is lightly styled.</small>
</p>
</main>
</div>
);
}
function StatusButton({
checked,
count,
@ -2405,13 +2858,21 @@ function StatusCompact({ sKey }) {
visibility,
content,
language,
filtered,
} = status;
if (sensitive || spoilerText) return null;
if (!content) return null;
const srKey = statusKey(id, instance);
const statusPeekText = statusPeek(status);
const filterContext = useContext(FilterContext);
const filterInfo = isFiltered(filtered, filterContext);
if (filterInfo?.action === 'hide') return null;
const filterTitleStr = filterInfo?.titlesStr || '';
return (
<article
class={`status compact-reply ${
@ -2427,7 +2888,14 @@ function StatusCompact({ sKey }) {
lang={language}
dir="auto"
>
{statusPeekText}
{filterInfo ? (
<b class="status-filtered-badge badge-meta" title={filterTitleStr}>
<span>Filtered</span>
<span>{filterTitleStr}</span>
</b>
) : (
<span>{statusPeekText}</span>
)}
</div>
</article>
);
@ -2549,7 +3017,6 @@ function FilteredStatus({
</article>
{!!showPeek && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowPeek(false);

View file

@ -535,15 +535,15 @@ const TimelineItem = memo(
const url = instance
? `/${instance}/s/${actualStatusID}`
: `/s/${actualStatusID}`;
let title = '';
if (type === 'boosts') {
title = `${items.length} Boosts`;
} else if (type === 'pinned') {
title = 'Pinned posts';
}
const isCarousel = type === 'boosts' || type === 'pinned';
if (items) {
const fItems = filteredItems(items, filterContext);
let title = '';
if (type === 'boosts') {
title = `${fItems.length} Boosts`;
} else if (type === 'pinned') {
title = 'Pinned posts';
}
const isCarousel = type === 'boosts' || type === 'pinned';
if (isCarousel) {
// Here, we don't hide filtered posts, but we sort them last
fItems.sort((a, b) => {

View file

@ -1,4 +1,5 @@
{
"@mastodon/edit-media-attributes": ">=4.1",
"@mastodon/list-exclusive": ">=4.2"
"@mastodon/list-exclusive": ">=4.2",
"@mastodon/filtered-notifications": "~4.3 || >=4.3"
}

View file

@ -69,7 +69,7 @@
--outline-color: rgba(128, 128, 128, 0.2);
--outline-hover-color: rgba(128, 128, 128, 0.7);
--divider-color: rgba(0, 0, 0, 0.1);
--backdrop-color: rgba(0, 0, 0, 0.05);
--backdrop-color: rgba(0, 0, 0, 0.1);
--backdrop-darker-color: rgba(0, 0, 0, 0.25);
--backdrop-solid-color: #eee;
--img-bg-color: rgba(128, 128, 128, 0.2);
@ -227,7 +227,7 @@ button[hidden] {
}
:is(button, .button):not(:disabled, .disabled):is(:hover, :focus) {
cursor: pointer;
filter: brightness(1.2);
filter: brightness(1.05);
}
:is(button, .button):not(:disabled, .disabled):active {
filter: brightness(0.8);
@ -267,6 +267,14 @@ button[hidden] {
:is(button, .button).plain5:not(:disabled, .disabled):is(:hover, :focus) {
text-decoration: underline;
}
:is(button, .button).plain6 {
background-color: var(--bg-blur-color);
color: var(--link-color);
border: 1px solid var(--link-color);
}
:is(button, .button).plain6:not(:disabled, .disabled):is(:hover, :focus) {
background-color: var(--link-bg-color);
}
:is(button, .button).light {
background-color: var(--bg-faded-color);
color: var(--text-color);

View file

@ -2,6 +2,9 @@ import './index.css';
import './cloak-mode.css';
// Polyfill needed for Firefox < 122
// https://bugzilla.mozilla.org/show_bug.cgi?id=1423593
import '@formatjs/intl-segmenter/polyfill';
import { render } from 'preact';
import { HashRouter } from 'react-router-dom';

View file

@ -206,8 +206,12 @@ function AccountStatuses() {
const [featuredTags, setFeaturedTags] = useState([]);
useTitle(
account?.acct
? `${account?.displayName ? account.displayName + ' ' : ''}@${
account.acct
? `${
account?.displayName
? `${account.displayName} (${/@/.test(account.acct) ? '' : '@'}${
account.acct
})`
: `${/@/.test(account.acct) ? '' : '@'}${account.acct}`
}${
!excludeReplies
? ' (+ Replies)'
@ -259,27 +263,21 @@ function AccountStatuses() {
const { displayName, acct, emojis } = account || {};
const accountInfoMemo = useMemo(() => {
const cachedAccount = snapStates.accounts[`${id}@${instance}`];
return (
<AccountInfo
instance={instance}
account={cachedAccount || id}
fetchAccount={fetchAccount}
authenticated={authenticated}
standalone
/>
);
}, [id, instance, authenticated, fetchAccount]);
const filterBarRef = useRef();
const TimelineStart = useMemo(() => {
const filtered =
!excludeReplies || excludeBoosts || tagged || media || !!month;
const cachedAccount = snapStates.accounts[`${id}@${instance}`];
return (
<>
{accountInfoMemo}
<AccountInfo
instance={instance}
account={cachedAccount || id}
fetchAccount={fetchAccount}
authenticated={authenticated}
standalone
/>
<div
class="filter-bar"
ref={filterBarRef}
@ -418,6 +416,7 @@ function AccountStatuses() {
instance,
authenticated,
featuredTags,
fetchAccount,
searchEnabled,
...allSearchParams,
]);

View file

@ -32,6 +32,57 @@
max-width: 40em;
margin-inline: auto;
details {
border-radius: 16px;
text-wrap: balance;
color: var(--text-insignificant-color);
padding: 1em;
margin: -1em 0;
transition: all 0.3s var(--timing-function);
line-height: 1.4;
&[open] {
transform: translateY(-10vh);
color: var(--text-color);
background-color: var(--bg-color);
background-image: radial-gradient(
farthest-corner at 25% 0,
transparent 80%,
var(--bg-faded-color) 95%,
var(--bg-color)
),
radial-gradient(
farthest-corner at 100% 100%,
transparent 80%,
var(--bg-faded-blur-color)
);
outline: 1px solid var(--bg-color);
box-shadow: 0 16px 32px -16px var(--drop-shadow-color);
~ * {
opacity: 0;
pointer-events: none;
}
img {
width: 480px;
height: auto;
border-radius: 8px;
border: 1px solid var(--outline-color);
}
}
summary {
font-size: 0.9em;
cursor: pointer;
user-select: none;
&:hover {
color: var(--text-color);
}
}
}
.catchup-info {
animation: appear 0.3s ease-out;
display: flex;
@ -68,6 +119,13 @@
gap: 0.25em;
align-items: center;
}
> span {
display: flex;
align-items: center;
gap: 8px;
text-align: end;
}
}
}
}
@ -117,14 +175,14 @@
border-radius: 3px;
border: 1px solid var(--bg-color);
display: flex;
gap: 1px;
gap: var(--hairline-width);
pointer-events: none;
justify-content: stretch;
height: 3px;
&:has(.post-dot:nth-child(320)) {
/* &:has(.post-dot:nth-child(320)) {
gap: 0;
}
} */
.post-dot {
display: block;
@ -147,6 +205,42 @@
}
}
.catchup-posts-viz-time-bar {
margin: 0 16px;
padding: 1px;
display: flex;
row-gap: var(--hairline-width);
pointer-events: none;
justify-content: stretch;
background-image: linear-gradient(to bottom, transparent, var(--bg-color));
@media (min-width: 640px) {
column-gap: var(--hairline-width);
}
.posts-bin {
display: flex;
gap: var(--hairline-width);
flex-direction: column-reverse;
width: 100%;
.post-dot {
display: block;
width: 100%;
height: 2px;
opacity: 0.2;
background-color: var(--link-color);
transition: 0.25s ease-in-out;
transition-property: opacity, transform;
contain: none;
&.post-dot-highlight {
opacity: 1;
}
}
}
}
.catchup-filters {
padding: 8px 16px;
display: flex;
@ -179,6 +273,10 @@
color: var(--text-insignificant-color);
position: relative;
&:active {
transform: scale(0.95);
}
select {
/* appearance: none;
background-color: var(--bg-faded-color);
@ -262,6 +360,7 @@
img {
transition: filter 0.15s ease;
will-change: filter;
}
}
@ -315,11 +414,16 @@
}
}
&:has(.filter-author :checked)
.filter-author:not(:has(:checked)):not(:is(:hover, :focus)) {
&:has(.filter-author :checked) .filter-author:not(:has(:checked)) {
.avatar img {
filter: grayscale(1) contrast(2) opacity(0.5);
}
&:is(:hover, :focus) {
.avatar img {
filter: none;
}
}
}
.radio-field-group {
@ -360,6 +464,7 @@
}
.catchup-list {
min-height: 85vh;
margin: 0;
padding: 0;
list-style: none;
@ -385,18 +490,14 @@
}
> li {
margin: 0 0 1px;
margin: 0 0 var(--hairline-width);
padding: 0;
list-style: none;
/* border-bottom: var(--hairline-width) solid var(--outline-color); */
&.separator {
height: 16px;
height: 32px;
pointer-events: none;
@media (min-width: 40em) {
height: 32px;
}
}
@media (min-width: 40em) {
@ -424,10 +525,13 @@
background-color: var(--bg-faded-color);
box-shadow: 0 8px 16px -8px var(--drop-shadow-color),
inset 0 1px var(--bg-color);
outline: 1px solid var(--outline-color);
text-shadow: 0 1px var(--bg-color);
}
&:hover:not(:focus-visible) {
outline: 1px solid var(--outline-color);
}
&:active {
filter: brightness(0.95);
box-shadow: none;
@ -450,6 +554,7 @@
}
.post-line {
font-size: 0.95em;
border-radius: inherit;
animation: appear-smooth 0.3s ease-in-out both;
--pad: 16px;
@ -468,9 +573,9 @@
'content content';
/* align-items: center; */
background-image: linear-gradient(
160deg,
140deg,
var(--post-bg-color),
transparent min(80px, 50%)
transparent min(160px, 50%)
);
/* background-image: linear-gradient(
90deg,
@ -524,10 +629,24 @@
gap: 4px;
align-items: center;
flex-shrink: 0;
min-height: 24px;
.icon {
> .avatar {
outline: 1px solid var(--bg-blur-color);
}
> .avatar ~ .avatar {
margin-left: -8px;
}
> .icon {
color: var(--reblog-color);
}
> .name-text {
opacity: 0.75;
filter: grayscale(0.75);
}
}
.post-author {
@ -559,10 +678,38 @@
}
}
> li:first-child .post-line {
animation-duration: 0.1s;
}
> li:nth-child(2) .post-line {
animation-duration: 0.2s;
}
> li:nth-child(10) ~ li .post-line {
animation: none;
}
&:is(.catchup-group-account, .catchup-selected-author):is(
.catchup-filter-original,
.catchup-filter-reply
)
> li {
margin-bottom: 0;
&:first-child ~ li {
.post-author:not(:has(.post-reblog-avatar)) {
opacity: 0.25;
@media (min-width: 40em) {
opacity: 0;
}
}
&.separator + li .post-author {
opacity: 1;
}
}
}
.post-peek {
grid-area: content;
display: flex;
@ -573,7 +720,7 @@
/* align-items: center; */
/* margin-left: 24px; */
flex-direction: row-reverse;
flex-wrap: wrap;
/* flex-wrap: wrap; */
justify-content: flex-end;
/* CLOAK - uncomment when taking screenshots */
@ -597,7 +744,6 @@
overflow: hidden;
line-height: 1.3;
opacity: 0.9;
/* font-size: 0.9em; */
text-wrap: balance;
&:empty {
@ -647,7 +793,8 @@
}
}
br:after {
br:after,
:not(br, span, a) + :is(p, div, blockquote, ul, ol, pre):before {
font-size: 0.75em;
content: ' ↵ ';
opacity: 0.35;
@ -670,6 +817,7 @@
}
.post-peek-spoiler {
display: inline-block;
line-height: 1.5;
border-radius: 1em;
padding-inline: 0.5em;
@ -737,6 +885,47 @@
}
}
@media (max-width: 480px) {
.post-peek-media:not(:last-child) {
margin-right: -24px;
box-shadow: 0 0 0 2px var(--bg-blur-color);
}
/* Max 10, I'm not going to code more than this */
.post-peek-media:nth-child(1) {
z-index: 10;
}
.post-peek-media:nth-child(2) {
z-index: 9;
}
.post-peek-media:nth-child(3) {
z-index: 8;
}
.post-peek-media:nth-child(4) {
z-index: 7;
}
.post-peek-media:nth-child(5) {
z-index: 6;
}
.post-peek-media:nth-child(6) {
z-index: 5;
}
.post-peek-media:nth-child(7) {
z-index: 4;
}
.post-peek-media:nth-child(8) {
z-index: 3;
}
.post-peek-media:nth-child(9) {
z-index: 2;
}
.post-peek-media:nth-child(10) {
z-index: 1;
}
.post-peek-media:hover {
z-index: 11;
}
}
.post-peek-faux-media {
width: 48px;
height: 48px;
@ -807,20 +996,86 @@
}
.post-stats {
opacity: 0;
display: inline-flex;
gap: 2px;
gap: 4px;
align-items: center;
transform: translateX(4px);
/* transition: all 0.25s ease-out; */
&:empty {
display: none;
}
> span {
display: inline-flex;
gap: 2px;
align-items: center;
}
}
.post-line:hover .post-stats {
opacity: 1;
transform: translateX(0);
@media (hover: hover) {
.post-stats {
opacity: 0;
transform: translateX(4px);
}
.post-line:hover .post-stats {
opacity: 1;
transform: translateX(0);
}
}
&.catchup-sort-repliesCount {
.post-stats {
opacity: 1;
transform: translateX(0);
.post-stat-replies {
order: 2;
}
> *:not(.post-stat-replies) {
opacity: 0;
}
}
@media (hover: hover) {
.post-line:hover .post-stats > * {
opacity: 1;
}
}
}
&.catchup-sort-favouritesCount {
.post-stats {
opacity: 1;
transform: translateX(0);
.post-stat-likes {
order: 2;
}
> *:not(.post-stat-likes) {
opacity: 0;
}
}
@media (hover: hover) {
.post-line:hover .post-stats > * {
opacity: 1;
}
}
}
&.catchup-sort-reblogsCount {
.post-stats {
opacity: 1;
transform: translateX(0);
.post-stat-boosts {
order: 2;
}
> *:not(.post-stat-boosts) {
opacity: 0;
}
}
@media (hover: hover) {
.post-line:hover .post-stats > * {
opacity: 1;
}
}
}
+ footer {
@ -830,3 +1085,29 @@
text-align: center;
}
}
#catchup-help-sheet {
dl {
dt {
font-weight: bold;
}
dd {
margin-block-end: 1em;
margin-inline: 1em;
+ dd {
margin-block-start: -0.9em;
}
}
}
kbd {
border-radius: 4px;
display: inline-block;
padding: 0.2em 0.3em;
margin: 1px 0;
line-height: 1;
border: 1px solid var(--outline-color);
background-color: var(--bg-faded-color);
}
}

File diff suppressed because it is too large Load diff

View file

@ -285,7 +285,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
required
autocorrect="off"
autocapitalize="off"
spellcheck={false}
spellCheck={false}
// no spaces, no hashtags
pattern="[^#][^\s#]+[^#]"
disabled={reachLimit}

View file

@ -143,7 +143,6 @@ function List(props) {
/>
{showListAddEditModal && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowListAddEditModal(false);
@ -167,7 +166,6 @@ function List(props) {
)}
{showManageMembersModal && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowManageMembersModal(false);

View file

@ -108,7 +108,6 @@ function Lists() {
</div>
{showListAddEditModal && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowListAddEditModal(false);

View file

@ -160,7 +160,7 @@ function Login() {
autocorrect="off"
autocapitalize="off"
autocomplete="off"
spellcheck={false}
spellCheck={false}
placeholder="instance domain"
onInput={(e) => {
setInstanceText(e.target.value);

View file

@ -57,13 +57,14 @@
width: fit-content;
margin: -0.25em auto 0;
line-height: 1;
z-index: 1;
position: relative;
background-color: var(--bg-blur-color);
/* background-image: linear-gradient(
to bottom,
var(--bg-color),
var(--bg-blur-color)
); */
backdrop-filter: blur(16px) saturate(3);
padding: 2px 4px;
border-radius: 999px;
overflow: hidden;
@ -142,6 +143,7 @@
border-color: var(--reply-to-color);
box-shadow: 0 0 0 3px var(--reply-to-faded-color);
}
.notification:focus-visible .status-link,
.notification .status-link:is(:hover, :focus) {
background-color: var(--bg-blur-color);
filter: saturate(1);
@ -419,3 +421,145 @@
color: var(--text-color);
background-color: var(--link-faded-color);
}
/* FILTERED NOTIFICATIONS */
.filtered-notifications {
padding-block-end: 16px;
summary {
padding: 8px 16px;
cursor: pointer;
font-weight: 600;
user-select: none;
margin: 16px 0 0;
color: var(--text-insignificant-color);
&::marker,
&::-webkit-details-marker {
color: var(--text-insignificant-color);
}
}
details[open] summary {
color: var(--text-color);
}
summary + ul {
}
ul {
list-style: none;
padding: 0;
margin: 0;
max-height: 50vh;
max-height: 50dvh;
overflow: auto;
border-top: 1px solid var(--outline-color);
border-bottom: 1px solid var(--outline-color);
background-color: var(--bg-faded-color);
@media (min-width: 40em) {
background-color: var(--bg-color);
border-radius: 16px;
border-width: 0;
}
li {
display: flex;
padding: 16px;
row-gap: 8px;
column-gap: 16px;
border-bottom: 1px solid var(--outline-color);
}
li:last-child {
border-bottom: none;
}
.request-notifcations {
min-width: 0;
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
.last-post {
max-width: 100%;
> .status-link {
border-radius: 8px;
overflow: hidden;
--max-height: 160px;
max-height: var(--max-height);
border: 1px solid var(--outline-color);
&:is(:hover, :focus-visible) {
border-color: var(--outline-hover-color);
}
.status {
mask-image: linear-gradient(
to bottom,
black calc(var(--max-height) / 2),
transparent calc(var(--max-height) - 8px)
);
font-size: calc(var(--text-size) * 0.9);
.content-container {
pointer-events: none;
filter: saturate(0.5);
}
}
}
}
.request-notifications-account {
display: flex;
align-items: center;
gap: 4px;
}
}
.notification-request-buttons {
grid-area: buttons;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
button {
max-width: 30vw;
}
.notification-request-states {
min-height: 32px;
text-align: center;
vertical-align: middle;
.icon {
margin-inline: 8px;
&.notification-accepted {
color: var(--green-color);
}
&.notification-dismissed {
color: var(--red-color);
}
}
}
}
}
}
#notifications-settings {
label {
display: flex;
gap: 8px;
align-items: center;
input[type='checkbox'] {
flex-shrink: 0;
}
}
}

View file

@ -3,6 +3,7 @@ import './notifications.css';
import { Fragment } from 'preact';
import { memo } from 'preact/compat';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import { InView } from 'react-intersection-observer';
import { useSearchParams } from 'react-router-dom';
import { useSnapshot } from 'valtio';
@ -13,8 +14,10 @@ import FollowRequestButtons from '../components/follow-request-buttons';
import Icon from '../components/icon';
import Link from '../components/link';
import Loader from '../components/loader';
import Modal from '../components/modal';
import NavMenu from '../components/nav-menu';
import Notification from '../components/notification';
import Status from '../components/status';
import { api } from '../utils/api';
import enhanceContent from '../utils/enhance-content';
import groupNotifications from '../utils/group-notifications';
@ -22,8 +25,10 @@ import handleContentLinks from '../utils/handle-content-links';
import niceDateTime from '../utils/nice-date-time';
import { getRegistration } from '../utils/push-notifications';
import shortenNumber from '../utils/shorten-number';
import showToast from '../utils/show-toast';
import states, { saveStatus } from '../utils/states';
import { getCurrentInstance } from '../utils/store-utils';
import supports from '../utils/supports';
import usePageVisibility from '../utils/usePageVisibility';
import useScroll from '../utils/useScroll';
import useTitle from '../utils/useTitle';
@ -31,6 +36,12 @@ import useTitle from '../utils/useTitle';
const LIMIT = 30; // 30 is the maximum limit :(
const emptySearchParams = new URLSearchParams();
const scrollIntoViewOptions = {
block: 'center',
inline: 'center',
behavior: 'smooth',
};
function Notifications({ columnMode }) {
useTitle('Notifications', '/notifications');
const { masto, instance } = api();
@ -129,6 +140,28 @@ function Notifications({ columnMode }) {
}
}
const supportsFilteredNotifications = supports(
'@mastodon/filtered-notifications',
);
const [showNotificationsSettings, setShowNotificationsSettings] =
useState(false);
const [notificationsPolicy, setNotificationsPolicy] = useState({});
function fetchNotificationsPolicy() {
return masto.v1.notifications.policy.fetch().catch(() => {});
}
function loadNotificationsPolicy() {
fetchNotificationsPolicy()
.then((policy) => {
console.log('✨ Notifications policy', policy);
setNotificationsPolicy(policy);
})
.catch(() => {});
}
const [notificationsRequests, setNotificationsRequests] = useState(null);
function fetchNotificationsRequest() {
return masto.v1.notifications.requests.list();
}
const loadNotifications = (firstLoad) => {
setShowNew(false);
setUIState('loading');
@ -154,6 +187,10 @@ function Notifications({ columnMode }) {
setFollowRequests(requests);
})
.catch(() => {});
if (supportsFilteredNotifications) {
loadNotificationsPolicy();
}
}
const { done } = await fetchNotificationsPromise;
@ -221,6 +258,9 @@ function Notifications({ columnMode }) {
lastHiddenTime.current = Date.now();
}
unsub = subscribeKey(states, 'notificationsShowNew', (v) => {
if (uiState === 'loading') {
return;
}
if (v) {
loadUpdates();
}
@ -270,11 +310,84 @@ function Notifications({ columnMode }) {
// }
// }, [uiState]);
const itemsSelector = '.notification';
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 oRef = useHotkeys(['enter', 'o'], () => {
const activeItem = document.activeElement.closest(itemsSelector);
const statusLink = activeItem?.querySelector('.status-link');
if (statusLink) {
statusLink.click();
}
});
return (
<div
id="notifications-page"
class="deck-container"
ref={scrollableRef}
ref={(node) => {
scrollableRef.current = node;
jRef.current = node;
kRef.current = node;
oRef.current = node;
}}
tabIndex="-1"
>
<div class={`timeline-deck deck ${onlyMentions ? 'only-mentions' : ''}`}>
@ -301,7 +414,17 @@ function Notifications({ columnMode }) {
</div>
<h1>Notifications</h1>
<div class="header-side">
{/* <Loader hidden={uiState !== 'loading'} /> */}
{supportsFilteredNotifications && (
<button
type="button"
class="button plain"
onClick={() => {
setShowNotificationsSettings(true);
}}
>
<Icon icon="settings" size="l" alt="Notifications settings" />
</button>
)}
</div>
</div>
{showNew && uiState !== 'loading' && (
@ -406,6 +529,70 @@ function Notifications({ columnMode }) {
)}
</div>
)}
{supportsFilteredNotifications &&
notificationsPolicy?.summary?.pendingRequestsCount > 0 && (
<div class="filtered-notifications">
<details
onToggle={async (e) => {
const { open } = e.target;
if (open) {
const requests = await fetchNotificationsRequest();
setNotificationsRequests(requests);
console.log({ open, requests });
}
}}
>
<summary>
Filtered notifications from{' '}
{notificationsPolicy.summary.pendingRequestsCount} people
</summary>
{!notificationsRequests ? (
<p class="ui-state">
<Loader abrupt />
</p>
) : (
notificationsRequests?.length > 0 && (
<ul>
{notificationsRequests.map((request) => (
<li key={request.id}>
<div class="request-notifcations">
{!request.lastStatus?.id && (
<AccountBlock
useAvatarStatic
showStats
account={request.account}
/>
)}
{request.lastStatus?.id && (
<div class="last-post">
<Link
class="status-link"
to={`/${instance}/s/${request.lastStatus.id}`}
>
<Status
status={request.lastStatus}
size="s"
readOnly
/>
</Link>
</div>
)}
<NotificationRequestModalButton request={request} />
</div>
<NotificationRequestButtons
request={request}
onChange={() => {
loadNotifications(true);
}}
/>
</li>
))}
</ul>
)
)}
</details>
</div>
)}
<div id="mentions-option">
<label>
<input
@ -514,6 +701,109 @@ function Notifications({ columnMode }) {
</InView>
)}
</div>
{supportsFilteredNotifications && showNotificationsSettings && (
<Modal
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowNotificationsSettings(false);
}
}}
>
<div class="sheet" id="notifications-settings" tabIndex="-1">
<button
type="button"
class="sheet-close"
onClick={() => setShowNotificationsSettings(false)}
>
<Icon icon="x" />
</button>
<header>
<h2>Notifications settings</h2>
</header>
<main>
<form
onSubmit={(e) => {
e.preventDefault();
const {
filterNotFollowing,
filterNotFollowers,
filterNewAccounts,
filterPrivateMentions,
} = e.target;
const allFilters = {
filterNotFollowing: filterNotFollowing.checked,
filterNotFollowers: filterNotFollowers.checked,
filterNewAccounts: filterNewAccounts.checked,
filterPrivateMentions: filterPrivateMentions.checked,
};
setNotificationsPolicy({
...notificationsPolicy,
...allFilters,
});
setShowNotificationsSettings(false);
(async () => {
try {
await masto.v1.notifications.policy.update(allFilters);
showToast('Notifications settings updated');
} catch (e) {
console.error(e);
}
})();
}}
>
<p>Filter out notifications from people:</p>
<p>
<label>
<input
type="checkbox"
switch
defaultChecked={notificationsPolicy.filterNotFollowing}
name="filterNotFollowing"
/>{' '}
You don't follow
</label>
</p>
<p>
<label>
<input
type="checkbox"
switch
defaultChecked={notificationsPolicy.filterNotFollowers}
name="filterNotFollowers"
/>{' '}
Who don't follow you
</label>
</p>
<p>
<label>
<input
type="checkbox"
switch
defaultChecked={notificationsPolicy.filterNewAccounts}
name="filterNewAccounts"
/>{' '}
With a new account
</label>
</p>
<p>
<label>
<input
type="checkbox"
switch
defaultChecked={notificationsPolicy.filterPrivateMentions}
name="filterPrivateMentions"
/>{' '}
Who unsolicitedly private mention you
</label>
</p>
<p>
<button type="submit">Save</button>
</p>
</form>
</main>
</div>
</Modal>
)}
</div>
);
}
@ -596,4 +886,186 @@ function AnnouncementBlock({ announcement }) {
);
}
function fetchNotficationsByAccount(accountID) {
const { masto } = api();
return masto.v1.notifications.list({
accountID,
});
}
function NotificationRequestModalButton({ request }) {
const { instance } = api();
const [uiState, setUIState] = useState('loading');
const { account, lastStatus } = request;
const [showModal, setShowModal] = useState(false);
const [notifications, setNotifications] = useState([]);
function onClose() {
setShowModal(false);
}
useEffect(() => {
if (!request?.account?.id) return;
if (!showModal) return;
setUIState('loading');
(async () => {
const notifs = await fetchNotficationsByAccount(request.account.id);
setNotifications(notifs || []);
setUIState('default');
})();
}, [showModal, request?.account?.id]);
return (
<>
<button
type="button"
class="plain4 request-notifications-account"
onClick={() => {
setShowModal(true);
}}
>
<Icon icon="notification" class="more-insignificant" />{' '}
<small>View notifications from @{account.username}</small>{' '}
<Icon icon="chevron-down" />
</button>
{showModal && (
<Modal
onClick={(e) => {
if (e.target === e.currentTarget) {
onClose();
}
}}
>
<div class="sheet" tabIndex="-1">
<button type="button" class="sheet-close" onClick={onClose}>
<Icon icon="x" />
</button>
<header>
<b>Notifications from @{account.username}</b>
</header>
<main>
{uiState === 'loading' ? (
<p class="ui-state">
<Loader abrupt />
</p>
) : (
notifications.map((notification) => (
<div
class="notification-peek"
onClick={(e) => {
const { target } = e;
// If button or links
if (
e.target.tagName === 'BUTTON' ||
e.target.tagName === 'A'
) {
onClose();
}
}}
>
<Notification
instance={instance}
notification={notification}
isStatic
/>
</div>
))
)}
</main>
</div>
</Modal>
)}
</>
);
}
function NotificationRequestButtons({ request, onChange }) {
const { masto } = api();
const [uiState, setUIState] = useState('default');
const [requestState, setRequestState] = useState(null); // accept, dismiss
const hasRequestState = requestState !== null;
return (
<p class="notification-request-buttons">
<button
type="button"
disabled={uiState === 'loading' || hasRequestState}
onClick={() => {
setUIState('loading');
(async () => {
try {
await masto.v1.notifications.requests
.$select(request.id)
.accept();
setRequestState('accept');
setUIState('default');
onChange({
request,
state: 'accept',
});
showToast(
`Notifications from @${request.account.username} will not be filtered from now on.`,
);
} catch (error) {
setUIState('error');
console.error(error);
showToast(`Unable to accept notification request`);
}
})();
}}
>
Allow
</button>{' '}
<button
type="button"
disabled={uiState === 'loading' || hasRequestState}
class="light danger"
onClick={() => {
setUIState('loading');
(async () => {
try {
await masto.v1.notifications.requests
.$select(request.id)
.dismiss();
setRequestState('dismiss');
setUIState('default');
onChange({
request,
state: 'dismiss',
});
showToast(
`Notifications from @${request.account.username} will not show up in Filtered notifications from now on.`,
);
} catch (error) {
setUIState('error');
console.error(error);
showToast(`Unable to dismiss notification request`);
}
})();
}}
>
Dismiss
</button>
<span class="notification-request-states">
{uiState === 'loading' ? (
<Loader abrupt />
) : requestState === 'accept' ? (
<Icon
icon="check-circle"
alt="Accepted"
class="notification-accepted"
/>
) : (
requestState === 'dismiss' && (
<Icon
icon="x-circle"
alt="Dismissed"
class="notification-dismissed"
/>
)
)}
</span>
</p>
);
}
export default memo(Notifications);

View file

@ -433,7 +433,7 @@ function Settings({ onClose }) {
</div>
</div>
</li>
{!!IMG_ALT_API_URL && (
{!!IMG_ALT_API_URL && authenticated && (
<li>
<label>
<input

View file

@ -906,7 +906,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
!!heroStatus?.repliesCount &&
!hasDescendants && (
<div class="status-loading">
<Loader />
<Loader abrupt={heroStatus.repliesCount >= 3} />
</div>
)}
{uiState === 'error' &&

View file

@ -67,7 +67,7 @@ function Trending({ columnMode, ...props }) {
// Get links
try {
const { value } = await fetchLinks(masto);
const { value } = await fetchLinks(masto, instance);
// 4 types available: link, photo, video, rich
// Only want links for now
const links = value?.filter?.((link) => link.type === 'link');

View file

@ -16,7 +16,9 @@ function handleContentLinks(opts) {
const textBeforeLinkIsAt = prevText?.endsWith('@');
const textStartsWithAt = target.innerText.startsWith('@');
if (
(target.classList.contains('u-url') && textStartsWithAt) ||
((target.classList.contains('u-url') ||
target.classList.contains('mention')) &&
textStartsWithAt) ||
(textBeforeLinkIsAt && !textStartsWithAt)
) {
const targetText = (
@ -24,12 +26,14 @@ function handleContentLinks(opts) {
).innerText.trim();
const username = targetText.replace(/^@/, '');
const url = target.getAttribute('href');
const mention = mentions.find(
(mention) =>
mention.username === username ||
mention.acct === username ||
mention.url === url,
);
// Only fallback to acct/username check if url doesn't match
const mention =
mentions.find((mention) => mention.url === url) ||
mentions.find(
(mention) =>
mention.acct === username || mention.username === username,
);
console.warn('MENTION', mention, url);
if (mention) {
e.preventDefault();
e.stopPropagation();

View file

@ -9,8 +9,10 @@ function statusPeek(status) {
text += getHTMLText(content);
}
text = text.trim();
if (poll) {
text += ' 📊';
if (poll?.options?.length) {
text += `\n\n📊:\n${poll.options
.map((o) => `${poll.multiple ? '▪️' : '•'} ${o.title}`)
.join('\n')}`;
}
if (mediaAttachments?.length) {
text +=

View file

@ -37,6 +37,7 @@ const rollbarCode = fs.readFileSync(
export default defineConfig({
base: './',
envPrefix: allowedEnvPrefixes,
appType: 'mpa',
mode: NODE_ENV,
define: {
__BUILD_TIME__: JSON.stringify(now),
@ -93,6 +94,7 @@ export default defineConfig({
purpose: 'maskable',
},
],
categories: ['social', 'news'],
},
strategies: 'injectManifest',
injectRegister: 'inline',
@ -115,6 +117,9 @@ export default defineConfig({
compose: resolve(__dirname, 'compose/index.html'),
},
output: {
manualChunks: {
'intl-segmenter-polyfill': ['@formatjs/intl-segmenter/polyfill'],
},
chunkFileNames: (chunkInfo) => {
const { facadeModuleId } = chunkInfo;
if (facadeModuleId && facadeModuleId.includes('icon')) {