Compare commits

...

1320 commits

Author SHA1 Message Date
Natsu Kagami c72bd47bbd
Don't statically pin the size of the carousel 2024-08-12 15:42:44 +02:00
Natsu Kagami bc0f856d72
Link to GtS settings when we know we are on GtS 2024-08-12 15:42:44 +02:00
Natsu Kagami 0e4dd6ee39
Make main column bigger 2024-08-12 15:42:44 +02:00
Natsu Kagami 0fa57fc0aa
Add a bit more touch 2024-08-12 15:42:43 +02:00
Natsu Kagami bb440c5d28
Force display instance
Because I don't like this decision from Phanpy
2024-08-12 15:42:43 +02:00
Natsu Kagami 66078a1867
Automatically put people into DTTHDon login 2024-08-12 15:42:43 +02:00
Natsu Kagami 3ff14d942e
Add some DTTH notice 2024-08-12 15:42:43 +02:00
Natsu Kagami 109b919c6c
Incorporate commit hash 2024-08-12 15:42:43 +02:00
Natsu Kagami 28fb3e4102
Get flakes to work 2024-08-12 15:42:28 +02:00
Chee Aun 9bf50615cb
Merge pull request #615 from verymilan/phanpy.social.tchncs.de
add phanpy.social.tchncs.de deployment to readme
2024-08-07 09:51:17 +08:00
Chee Aun dfa1123ac3
Merge pull request #616 from Fastidious/patch-2
Update README.md
2024-08-07 09:50:39 +08:00
Fastidious a0f2eb7305
Update README.md
Moved servers. This updates to the new one.
2024-08-06 10:47:13 -04:00
Milan 1bd9ceb4fc
add phanpy.social.tchncs.de deployment to readme 2024-08-06 12:39:12 +02:00
Chee Aun 082409a09f
Merge pull request #613 from illfygli/main
Filter out languages that aren't RFC5646-shaped
2024-08-06 08:58:16 +08:00
owl 225eaf4a2d
Pass undefined to Intl.DisplayNames, so '*' doesn't break it 2024-08-05 14:14:03 +02:00
Lim Chee Aun 60289cdb29 Upgrade dependencies 2024-08-04 19:09:46 +08:00
Lim Chee Aun a1c419b675 Try fix select field bug on Windows again
Previously: b47c043699
2024-08-04 19:01:21 +08:00
Lim Chee Aun 89e8bdf77b Use pinned instead of _pinned 2024-08-04 18:06:26 +08:00
Lim Chee Aun b3681a93ee Workbox expiration plugin not working as expected 2024-08-04 18:05:03 +08:00
Lim Chee Aun ad7193d067 Fix notifications popover not close-able on iPad 2024-08-04 13:53:06 +08:00
Lim Chee Aun f05e3012e3 Preliminary step for RTL 2024-08-04 13:32:46 +08:00
Lim Chee Aun 2aff1dc1fd Try switch to 20s interval 2024-08-04 13:32:46 +08:00
Lim Chee Aun 99ee6c3979 Don't reuse var for both timeout and interval 2024-08-04 13:32:46 +08:00
Lim Chee Aun 4ebfb544aa This caching seems still buggy
Revert to SWR with 1-min expiry
2024-08-04 13:32:46 +08:00
Lim Chee Aun cf2461add5 Better checks 2024-08-04 13:32:46 +08:00
Chee Aun 4937c5f77e
Merge pull request #610 from fhemberger/patch-1
fix(shortcuts-settings): `settingsJSON` must be defined if note doesn't exist
2024-08-04 10:11:42 +08:00
Frederic Hemberger 0febcacb93
fix(shortcuts-settings): settingsJSON must be defined if note doesn't exist 2024-08-03 13:30:22 +02:00
Lim Chee Aun 818f58b460 Fix profile URLs not working for http route 2024-08-01 20:18:44 +08:00
Lim Chee Aun 57db8778a4 Adapt to new changes in group notifications API
Reference: https://github.com/mastodon/mastodon/pull/31214
2024-08-01 20:18:10 +08:00
Chee Aun 9806d8ae9d
Merge pull request #607 from kizu/fix-overflow
Fix overflow for the columns wrapper
2024-08-01 09:56:05 +08:00
Roman Komarov 522a324b0d
Fix overflow for the columns wrapper 2024-07-31 13:59:35 +02:00
Lim Chee Aun 5be30e0c80 Upgrade dependencies 2024-07-29 20:05:03 +08:00
Lim Chee Aun 379ef7cc11 Random unused IntersectionView
Keeping this for future use
2024-07-28 16:09:44 +08:00
Lim Chee Aun 2d23b15c8d Assume title is the author for .card-post 2024-07-28 16:09:03 +08:00
Lim Chee Aun fa3a0e23cc Unhide some text for posts inside Edit History
Every char matters when looking at post edit history
2024-07-28 16:08:18 +08:00
Lim Chee Aun 631730f2f2 Replace SWR with CacheFirst
This SWR strategy is sometimes too stale, possibly a bug with Workbox
2024-07-28 16:07:22 +08:00
Lim Chee Aun f1822d54af Fix poll radio button position on Safari
Plus a color
2024-07-25 18:39:14 +08:00
Lim Chee Aun 4c0bc62ad0 Group filtered carousel items 2024-07-22 14:31:52 +08:00
Lim Chee Aun 84b3106f50 Undo font size inherit for card posts 2024-07-22 14:19:25 +08:00
Lim Chee Aun a2b88f1cdd Distinct both implementation of grouped notifications 2024-07-21 20:31:10 +08:00
Lim Chee Aun b88376569e Test this out for bridgy fed links 2024-07-21 19:06:38 +08:00
Lim Chee Aun 00e2ba0b34 Fix notification markers not working
Also the ids are getting confusing, so need to clean this up.
2024-07-21 18:59:38 +08:00
Lim Chee Aun a0d75e7e83 Upgrade dependencies 2024-07-20 17:45:43 +08:00
Lim Chee Aun 4b2ec14dcd Try set default sort and group when choosing Boosts 2024-07-19 20:00:10 +08:00
Chee Aun 808c6262d8
Merge pull request #597 from graue/graue/copy-handle-with-instance
Include domain when copying local user's handle
2024-07-18 17:08:51 +08:00
Scott Feeney 44d440649f Include domain when copying local user's handle
Fixes #596
2024-07-13 01:15:01 -07:00
Lim Chee Aun a2f7638257 Experimental opt-in server-side grouped notifications 2024-07-12 18:57:48 +08:00
Lim Chee Aun 57d6889826 Test memoize Media 2024-07-12 13:35:43 +08:00
Lim Chee Aun 2a91c005a1 Test fix self-recursive quote posts 2024-07-12 13:34:57 +08:00
Lim Chee Aun 418895e1c3 Another attempt: upgrade dependencies 2024-07-08 17:40:16 +08:00
Lim Chee Aun 180a23f116 Fix wrong exceeded chars highlighting 2024-07-07 22:56:24 +08:00
Lim Chee Aun 9ea7a1f4db Use onClose for this 2024-07-06 09:47:42 +08:00
Lim Chee Aun f26dbeb79a Fix more cloaking business 2024-07-06 09:47:28 +08:00
Lim Chee Aun f0872e79fb Revert "Upgrade dependencies"
This reverts commit cb9848fe8c.
2024-07-05 18:56:52 +08:00
Lim Chee Aun a72400febf Test support Hollo 2024-07-05 16:19:04 +08:00
Lim Chee Aun cb9848fe8c Upgrade dependencies 2024-07-03 20:02:47 +08:00
Lim Chee Aun c950a6552c Experiment: unhide header when clicking on timeline items 2024-07-03 20:01:11 +08:00
Lim Chee Aun 95bf9e183e Replace trivago/ with ianvs/prettier-plugin-sort-imports 2024-07-01 17:41:21 +08:00
Lim Chee Aun e6e884f1cb Refactor + make card post work for no-image cards 2024-06-28 07:49:30 +08:00
Lim Chee Aun b6a25f5939 MVP-ish add/remove featured tags 2024-06-27 22:05:16 +08:00
Lim Chee Aun 71823fbad2 Fix typo 2024-06-27 22:05:16 +08:00
Lim Chee Aun 046d3d323a Enable unfurling when fetching reply hints 2024-06-27 22:05:16 +08:00
Lim Chee Aun f7024f7723 Only allow trending link posts for current instance, not remote instance
For this to work on remote instance, will need to fetch its version and check first
2024-06-27 22:05:16 +08:00
Lim Chee Aun 1b3938f3d2 Add bundle-visualizer 2024-06-27 22:05:16 +08:00
Lim Chee Aun 5ab0ea1b59 Remove usehooks dep
In the end, only used one hook out of so many hooks
2024-06-27 22:05:16 +08:00
Lim Chee Aun 09745e3078 Don't show account if notification = severed_relationships 2024-06-27 22:05:16 +08:00
Chee Aun 87be0cad16
Merge pull request #584 from coxde/patch-1
fix: enable/disable boosts button logic
2024-06-27 22:00:01 +08:00
COxDE 04588874c7
fix: enable/disable boosts button logic 2024-06-27 13:38:55 +01:00
Lim Chee Aun 5d6a43e5d2 Bump up to 600 2024-06-23 15:41:00 +08:00
Lim Chee Aun 7f5f01b118 Further extend quote post dimensions 2024-06-22 17:48:14 +08:00
Lim Chee Aun f4a4913889 Don't animate for small-dimension images 2024-06-22 17:47:56 +08:00
Lim Chee Aun 7fb4aad089 Upgrade vite 2024-06-22 12:42:43 +08:00
Lim Chee Aun f8e72d1808 Prevent miscalculated width in large media container 2024-06-22 12:42:23 +08:00
Lim Chee Aun 527a1551cf The math here is quite forgiving 2024-06-19 23:34:19 +08:00
Lim Chee Aun a6e6a7d741 Simplify natural aspect ratio math 2024-06-19 18:29:20 +08:00
Lim Chee Aun 21bdb6afc1 Posts timeline for trending links
Timeline logic changed slightly, so might be buggy.
2024-06-19 12:22:17 +08:00
Lim Chee Aun 4be88da1d6 Test slight fade out 2024-06-19 12:19:48 +08:00
Lim Chee Aun 93bb1da7c9 Fix undefined in account link when ctrl-clicking 2024-06-18 20:14:56 +08:00
Lim Chee Aun 497ede1a3d Use state to set natural aspect ratio instead
And fix all other data attributes
2024-06-15 10:25:10 +08:00
Lim Chee Aun 8a1fda5a85 Prevent flash of post page 2024-06-15 10:24:33 +08:00
Lim Chee Aun 83164c321f Apply anim-duration for card image too
And also media images in Catch-up
2024-06-15 08:36:12 +08:00
Lim Chee Aun 15ebf628f8 Give status cards same treatment as media attachments 2024-06-14 18:13:59 +08:00
Lim Chee Aun fbe540ca7f Upgrade dependencies 2024-06-14 08:36:23 +08:00
Lim Chee Aun 1f8a8f8928 Use URL.parse with polyfill 2024-06-14 08:34:50 +08:00
Lim Chee Aun febd04dd54 Try use dangerouslySetInnerHTML again
And… fix the loop attribute value
2024-06-11 23:43:55 +08:00
Lim Chee Aun 983dd6623f Try autoPlay instead of autoplay
Fixing Mobile Safari bug
2024-06-11 18:17:19 +08:00
Lim Chee Aun a79d0613ec One more experimental magic 2024-06-11 14:53:12 +08:00
Lim Chee Aun c0c7fdd6e1 Handle tiny images & fix layout
Honestly there's just too many possibilities
2024-06-11 14:46:29 +08:00
Lim Chee Aun 17a3939061 Use data attr instead
The JSX className modification classes with this DOM-based modification
2024-06-10 20:50:21 +08:00
Lim Chee Aun 8a10a81fec Experiment immersive media render on large-size post 2024-06-10 20:42:38 +08:00
Lim Chee Aun 17230fc690 Experiment reduce radius for uncropped images 2024-06-10 20:41:43 +08:00
Lim Chee Aun 88e36183c6 Experiment different card preview style 2024-06-10 20:40:35 +08:00
Lim Chee Aun d0bb0c04db Small style adjustments to composer 2024-06-10 20:39:03 +08:00
Lim Chee Aun 42d761e747 Chunk tinyld out 2024-06-10 20:38:41 +08:00
Lim Chee Aun 901725793b Try resolve threads' links if they work one day 2024-06-08 21:36:09 +08:00
Lim Chee Aun 3fbecb2f0d Fix NameText not showing username when short 2024-06-08 21:35:14 +08:00
Lim Chee Aun ef1abbc25c Wait I need a slash here? 2024-06-08 21:34:50 +08:00
Lim Chee Aun 2f75dfd9e4 Prefs need to be awaited 2024-06-07 18:41:04 +08:00
Lim Chee Aun 8d91bfb0a3 Throttle account fetches 2024-06-07 18:38:26 +08:00
Lim Chee Aun 04e1d60e54 Check vapidKey 2024-06-06 17:47:44 +08:00
Lim Chee Aun 1c01e1b0f4 Fix federated feed only showing remote posts
There's a mismatch parameter between Mastodon's and Pixelfed's APIs
2024-06-06 17:47:44 +08:00
Chee Aun dea3507053
Merge pull request #561 from zkreml/add-my-instance
Added phanpy.cz to self-hosted instances
2024-06-05 09:02:29 +08:00
archos 9b35119f99 Added phanpy.cz to self-hosted instances 2024-06-04 20:04:04 +02:00
Chee Aun 6d7eddc568
Merge pull request #557 from kevquirk/patch-1
Added social.qrk.one to self-hosted instances
2024-06-03 22:37:31 +08:00
Kev Quirk dac2af4334
Added social.qrk.one to self-hosted instances 2024-06-03 14:54:09 +01:00
Lim Chee Aun 2099953b68 Remove spaces between buttons 2024-06-03 18:01:49 +08:00
Lim Chee Aun 5931ebb8fc Reduce visual clutter for grouped notification
30 instead of 50 as limit. No more tiny avatars as they don't help much.
2024-06-02 22:52:47 +08:00
Lim Chee Aun adcb87679b Upgrade dependencies
Try bump text-expander too as it might have fixed its bugs
2024-06-01 16:33:13 +08:00
Lim Chee Aun 5ead17a093 Disable GIF button if exceed max media limit or has poll 2024-06-01 11:51:58 +08:00
Lim Chee Aun 224cad4d7f Utilise the new batch fetch on Mastodon v4.3 2024-05-31 17:11:40 +08:00
Lim Chee Aun e08817d611 Attempt to rewrite this part 2024-05-31 16:56:13 +08:00
Lim Chee Aun 1ffc1c257a Use setTimeout instead 2024-05-29 18:46:42 +08:00
Lim Chee Aun 098014a109 Fix possible error 2024-05-29 18:46:14 +08:00
Lim Chee Aun 7546b42c7c Further improve lang detection perf 2024-05-29 15:26:58 +08:00
Lim Chee Aun f9a73777e7 Perf over function 2024-05-29 10:23:46 +08:00
Lim Chee Aun d5584f8dd4 Delay preload 2024-05-29 08:58:17 +08:00
Lim Chee Aun 563b06e680 Break the tasks 2024-05-28 22:22:14 +08:00
Lim Chee Aun b6a64b66c7 Fix wrong logic for highlighting Languages select 2024-05-28 21:03:05 +08:00
Lim Chee Aun 0a4aae51b7 It's time for MVP-ish language auto-detection 2024-05-28 17:59:17 +08:00
Lim Chee Aun d16221e296 Test fix Pixelfed home timeline not showing reblogs 2024-05-28 13:44:24 +08:00
Lim Chee Aun ed712d15f1 Test fix notification toast appearing after loaded 2024-05-28 13:44:02 +08:00
Lim Chee Aun bd8817e61b Show warning if exceed file size or matrix limit 2024-05-27 19:19:34 +08:00
Lim Chee Aun ef712c62a9 Add one more username ≈ display name logic 2024-05-27 19:02:19 +08:00
Lim Chee Aun 9aa2bac685 Try fix toast width again 2024-05-27 19:01:41 +08:00
Lim Chee Aun 34077e8467 Don't show 'More…' for hashtag autosuggest 2024-05-26 18:15:37 +08:00
Lim Chee Aun b473061845 Show compose button above post modal when minimized 2024-05-26 00:13:20 +08:00
Lim Chee Aun 64c7b5b4f0 Rewrite polyfill suspense for Composer with preload
Hopefully this works
2024-05-25 20:43:15 +08:00
Lim Chee Aun c11bbbb2b3 Handle modifiers when clicking on account links 2024-05-25 13:52:25 +08:00
Lim Chee Aun 2c1a6c8cb5 Restyle the composer controls UI 2024-05-25 13:39:11 +08:00
Lim Chee Aun 67a85e1eef Forgot Mobile Safari always need 16px for input fields 2024-05-25 13:16:22 +08:00
Lim Chee Aun 2e0ef6494b Extend at-mentions with dedicated UI 2024-05-25 11:06:58 +08:00
Lim Chee Aun 012b86d7ce Try not hide compose button if loading 2024-05-25 11:06:03 +08:00
Lim Chee Aun 0c45f515f0 Don't add space if empty string 2024-05-25 09:16:03 +08:00
Lim Chee Aun 9cc590be1b Extra check if the composer is publishing 2024-05-25 09:15:43 +08:00
Lim Chee Aun 7589ec8803 Downgrade text-expander
Possibly might fix autosuggest position bug on Mobile Safari
2024-05-25 09:15:13 +08:00
Lim Chee Aun cd17ca0b42 Experiment: allow minimize composer 2024-05-24 12:30:20 +08:00
Lim Chee Aun 8aab997900 Upgrade dependencies 2024-05-23 20:25:14 +08:00
Lim Chee Aun 96c44ed485 Fix composer not overwritten by restored composer window 2024-05-23 14:14:23 +08:00
Lim Chee Aun 7053fcc96a Experimental 'More…' for custom emojis suggestions
Also includes small fixes and improvements
2024-05-22 19:12:13 +08:00
Lim Chee Aun ad7cb46547 Experiment auto-expand spoiler in hero status 2024-05-19 18:46:27 +08:00
Lim Chee Aun 1b1af67064 Experiment non-English description generation 2024-05-19 16:27:59 +08:00
Lim Chee Aun bdd238de0e Test using inert to control text searchability 2024-05-19 16:26:15 +08:00
Lim Chee Aun ced4dc86aa Forgot passing blankCopy 2024-05-19 16:24:29 +08:00
Lim Chee Aun 7be1e589ab Support Pleroma's /notice unfurl 2024-05-19 16:23:12 +08:00
Lim Chee Aun 7da1745cca Respect expand spoiler setting in Catchup 2024-05-19 16:22:18 +08:00
Lim Chee Aun 025a5429cc Set limit to 80 for notifications 2024-05-17 18:32:12 +08:00
Lim Chee Aun 62f843b4dc Fix crash when media url doesn't have http prefix 2024-05-17 17:10:54 +08:00
Lim Chee Aun b0a53b7fa1 Handle hideCollections 2024-05-16 21:11:51 +08:00
Lim Chee Aun 9934daeb4d Handle filtered quote posts 2024-05-16 13:00:23 +08:00
Lim Chee Aun d4a0a080b5 Bump up max entries for icons 2024-05-15 19:38:28 +08:00
Lim Chee Aun bc4e3b0f72 Fix red too faint in dark mode 2024-05-14 23:39:48 +08:00
Lim Chee Aun ac760265da Fix post preview internals becoming clickable 2024-05-11 13:09:08 +08:00
Lim Chee Aun 98b0ccf032 Default to floor rounding mode 2024-05-10 12:11:57 +08:00
Lim Chee Aun 90f06c511a Test allow linking to post from generic accounts modal 2024-05-08 10:29:00 +08:00
Lim Chee Aun e7aad03279 Preliminary implementation of moderation_warning notifications 2024-05-08 10:28:34 +08:00
Lim Chee Aun 1c6b0aa0d7 Upgrade dependencies 2024-05-06 12:48:55 +08:00
Lim Chee Aun 3e1b9ff53d Apply filter context in compact status too 2024-05-02 23:29:01 +08:00
Lim Chee Aun 5c9a47c31e Might as well re-use it for instances search 2024-05-02 00:14:48 +08:00
Lim Chee Aun 65a4c3441c Add search for custom emojis 2024-05-02 00:14:25 +08:00
Lim Chee Aun 77bc06545c Handle inline images 2024-05-01 15:05:29 +08:00
Lim Chee Aun 11e64a2cc4 Fix filter expiry wrongly set if there's no expiry 2024-04-28 08:30:52 +08:00
Lim Chee Aun 5433e4e119 initStates needed for standalone compose page 2024-04-28 08:30:52 +08:00
Lim Chee Aun c8dc32b884 Test caching shazam states 2024-04-28 08:30:52 +08:00
Lim Chee Aun 1f29aee26e Upgrade dependencies 2024-04-28 08:30:52 +08:00
Lim Chee Aun daae055f4d List out forks 2024-04-28 08:30:52 +08:00
Chee Aun 044f754d7e
Merge pull request #522 from mickaobrien/timeline-enter-keyboard-shortcut-fix
Fix `enter` keyboard shortcut on timeline
2024-04-28 08:29:47 +08:00
Mick O'Brien 5ae2058c07 Fix enter keyboard shortcut on timeline
Currently pressing `enter` opens the active status if the status or any
focusable child of the status is focused e.g. the avatar or a link.

I think it should only open the post details when the post itself is
focused.
2024-04-26 12:23:53 +01:00
Lim Chee Aun 7376cb1e99 Fix muted="false" means still muted 🤦‍♂️🤦‍♂️🤦‍♂️ 2024-04-19 08:46:10 +08:00
Lim Chee Aun ffbae70178 Remove newline from regex for shortcode 2024-04-19 08:41:16 +08:00
Lim Chee Aun 9235d2c800 Hide poll button if maxOptions <= 1
It's not a poll if there's only 1 option
2024-04-18 23:12:29 +08:00
Lim Chee Aun 6ccefaebe1 Handle invalid date
Ugly solution for now, but it's already ugly
2024-04-18 23:11:18 +08:00
Lim Chee Aun 5a448c8049 Fix infinite reloading
Comment these out because this used to fix an old bug with instances not loaded properly
2024-04-18 23:10:26 +08:00
Lim Chee Aun 9bf77fa97a Mentions also need fixNotifications
It's also from notifications API
2024-04-18 17:15:51 +08:00
Lim Chee Aun b9058c6e3d Debounced auto-submit for GIF search field 2024-04-17 08:26:35 +08:00
Lim Chee Aun 55ad6500bc Fix margins 2024-04-16 23:21:46 +08:00
Lim Chee Aun f4b95d254c Maybe this helps? 2024-04-16 20:18:18 +08:00
Lim Chee Aun effbe189e1 Revert "Test upgrade react-hotkeys-hook for the keys fix"
This reverts commit 9285a0ba9a.
2024-04-16 00:09:53 +08:00
Lim Chee Aun 44e910b8c9 Fix wrong carousel math 2024-04-15 23:34:58 +08:00
Lim Chee Aun a68dccd7cf Fix rerender bug with followed hashtag parent
And… somehow memoize it?
2024-04-15 21:37:03 +08:00
Lim Chee Aun 9a6364a674 Obviously got to flex my scroll-driven animation CSSkillz 2024-04-15 19:59:57 +08:00
Lim Chee Aun e2f39596f0 Might as well add more supports 2024-04-15 19:58:59 +08:00
Lim Chee Aun 701b9e99b3 More media-first styling changes 2024-04-15 17:07:34 +08:00
Lim Chee Aun 294ab2bf00 Just put in this commented test notification
Good for reference in the future
2024-04-15 17:07:20 +08:00
Lim Chee Aun 304ce5a3e8 Experiment dynamic change of parent
This might prevent double renders
2024-04-15 17:06:44 +08:00
Lim Chee Aun 57390a291b No need background if there's pre-meta before it 2024-04-15 10:10:49 +08:00
Lim Chee Aun cd5920114f Undo back to -45deg, not everything need 135deg 2024-04-15 07:26:45 +08:00
Lim Chee Aun 06c6360cae More support for Pixelfed 2024-04-14 17:20:18 +08:00
Lim Chee Aun afdfdb86da Media-first style adjustments 2024-04-14 17:18:52 +08:00
Lim Chee Aun 6f8f3e4fd0 Change -35deg to 145deg prevents stripes animation
When dynamically changing dimension (height), repeating linear gradient seems to animate. This prevents it.
https://stackoverflow.com/a/76285775/20838
2024-04-14 14:08:50 +08:00
Lim Chee Aun 342ff20986 Document PHANPY_IMG_ALT_API_URL 2024-04-14 08:14:34 +08:00
Lim Chee Aun 94996d098e Fix width issue 2024-04-13 23:08:25 +08:00
Lim Chee Aun c286562ee8 Media-first style adjustments 2024-04-13 19:21:48 +08:00
Lim Chee Aun 5babdc9d63 Fix width/height not set 2024-04-13 19:21:20 +08:00
Lim Chee Aun 260bb8746d More media-first adjustments 2024-04-13 17:10:13 +08:00
Lim Chee Aun 7be620808f Fix notifications for Pixelfed 2024-04-13 17:09:56 +08:00
Lim Chee Aun df3aca70fa Open media + post view for wider viewports 2024-04-13 17:09:00 +08:00
Lim Chee Aun ec65163c89 More breathing space 2024-04-13 17:08:39 +08:00
Lim Chee Aun 6f22ec3842 Fix missing idempotency key 2024-04-13 17:07:28 +08:00
Lim Chee Aun 2faf9b4c20 Pixelfed needs remote which is opposite of local lol 2024-04-13 00:11:00 +08:00
Lim Chee Aun 501e43207b Don't set onlyMedia if not set
This defaults to false for Mastodon, but true for Pixelfed
2024-04-13 00:11:00 +08:00
Lim Chee Aun e782cc0dde Refactor set/get current account ID
And add fallback for standalone mode where session storage is not enough
2024-04-13 00:11:00 +08:00
Lim Chee Aun aefda31c2a Temporary quick fix, remove dash from hashtag regex 2024-04-13 00:11:00 +08:00
Lim Chee Aun 9285a0ba9a Test upgrade react-hotkeys-hook for the keys fix 2024-04-13 00:11:00 +08:00
Chee Aun 7fb56d9f6c
Merge pull request #493 from ultramookie/ultramookie-patch-1
Adding new self-hosted instance of Phanpy
2024-04-12 17:41:55 +08:00
steve mookie kong f7c69e56e9
Adding new self-hosted instance of Phanpy
Added new self-hosted instance of Phanpy, halo.mookiesplace.com
2024-04-11 21:28:38 -07:00
Lim Chee Aun c3bcf3d595 Try make Safari show video preview 2024-04-11 18:24:51 +08:00
Lim Chee Aun 0efa39b825 Sometimes it returns a preview image without dimenions 2024-04-11 17:45:19 +08:00
Lim Chee Aun a0d2037007 Early implementation of media-first UI experience 2024-04-11 17:18:17 +08:00
Lim Chee Aun 6e73728e2b Only show data-read-more when it's available 2024-04-11 17:16:04 +08:00
Lim Chee Aun 60920966d6 Special fallback handling when media object doesn't have enough info 2024-04-11 17:15:16 +08:00
Lim Chee Aun 5083463942 Show empty copy when no notifications at all 2024-04-11 17:13:34 +08:00
Lim Chee Aun 8b5fee3dfd Just sub it once 2024-04-10 17:31:26 +08:00
Lim Chee Aun c9124bf150 Change double-tap zoom to match mobile expectations 2024-04-10 15:03:02 +08:00
Lim Chee Aun b85174155c Make notifications settings icon less significant 2024-04-10 14:21:05 +08:00
Lim Chee Aun 5c9f6bae3c Fix followers list failing if familiar followers fail 2024-04-10 14:19:35 +08:00
Lim Chee Aun 4e5940900e Pixelfed-related fixes 2024-04-09 23:35:17 +08:00
Lim Chee Aun 7fa0b4f076 Upgrade dependencies 2024-04-05 17:13:29 +08:00
Lim Chee Aun ecfcc68f15 Add TheDesk 2024-04-05 17:13:13 +08:00
Lim Chee Aun 015ed5e7eb Further expand usage of SubMenu2 2024-04-04 17:03:30 +08:00
Lim Chee Aun 2ad9706304 Further utilize lazy shazam 2024-04-04 14:34:28 +08:00
Lim Chee Aun 30382d088b Possible fix for menus again 2024-04-04 14:34:04 +08:00
Lim Chee Aun 80196f83ca Revert "Test if this fixes submenu not opening"
This reverts commit 49fa48bd28.
2024-04-04 14:29:46 +08:00
Lim Chee Aun 419ad34250 Revert "Test another fix for submenus not opening"
This reverts commit a7cc0785f9.
2024-04-04 14:29:35 +08:00
Lim Chee Aun ed0d714cf2 Just a little spacing fix 2024-04-03 22:51:29 +08:00
Lim Chee Aun 708976a9e9 Anything Intl always need to extract out
and memoized
2024-04-03 19:48:18 +08:00
Lim Chee Aun d77ba19308 Handle another kind of emojiReaction response
Can't everyone just standardize the responses?
2024-04-03 17:58:37 +08:00
Lim Chee Aun b10e22a9a2 Better fallbacks 2024-04-03 17:57:15 +08:00
Lim Chee Aun 36d8b62e1e Height adjustments when switching between poll form and results 2024-04-03 16:14:59 +08:00
Lim Chee Aun 989e788d8e Slight delay is needed 2024-04-03 16:06:37 +08:00
Lim Chee Aun ebd9f05f69 Preload IntlSegmenter polyfill if needed 2024-04-03 14:33:53 +08:00
Lim Chee Aun 5246af4ae9 Undo lazy component experiment
Doesn't make much difference
2024-04-03 14:33:19 +08:00
Lim Chee Aun e6ba72f4c8 'Remove follower' menu item 2024-04-03 11:54:46 +08:00
Lim Chee Aun 960dff8b9e Make lazy shazam ignore top sticky header 2024-04-03 11:53:03 +08:00
Lim Chee Aun e3c25d25ee Add menus to view profile image and header 2024-04-03 09:29:23 +08:00
Lim Chee Aun 090320150a Select text too when pressing / 2024-04-03 09:28:59 +08:00
Lim Chee Aun 7100937e79 Higher gif picker sheet 2024-04-02 19:44:22 +08:00
Lim Chee Aun c18efef7b6 GIF picker 2024-04-02 17:51:48 +08:00
Lim Chee Aun ff336628f8 Fix media description not recognized if programmatically entered 2024-04-02 17:45:14 +08:00
Lim Chee Aun 28882d98d9 Add different UI state than default for start 2024-04-02 17:42:51 +08:00
Lim Chee Aun f6ad22e58f Fix bug: media attachments not updated when edited 2024-04-02 13:12:52 +08:00
Lim Chee Aun aa664e15f6 Convert all the punycodes
Surprising that this is still not built into browsers
2024-04-02 09:03:13 +08:00
Chee Aun f2f203c9d8
Merge pull request #478 from snail-coupe/doc/fix_build_example
Update README.md
2024-04-02 07:49:11 +08:00
snail-coupe ae0e4a0792
Update README.md
Build examples: PHANPY_APP_TITLE -> PHANPY_CLIENT_NAME
2024-04-01 23:26:15 +01:00
Lim Chee Aun 4def6eef5a Refactor this out for no particular reason 2024-03-31 20:53:08 +08:00
Lim Chee Aun 1004a5f176 Revert back to 88px 2024-03-31 20:47:43 +08:00
Lim Chee Aun 2b6beee875 More logic to prevent recursive/wrong quote posts 2024-03-31 20:35:24 +08:00
Lim Chee Aun e35e02593a If beyond 12 hours, allow last catch up's end timing 2024-03-31 20:34:01 +08:00
Lim Chee Aun 5e56ba9fb9 Bring back auto-updating relative time
This time, more optimized re-render
2024-03-30 17:21:31 +08:00
Lim Chee Aun a7cc0785f9 Test another fix for submenus not opening 2024-03-30 14:44:48 +08:00
Lim Chee Aun bb5d34c94c Still need to request relationship for moved accounts
Instead hide specific elements if moved.
2024-03-29 21:27:46 +08:00
Lim Chee Aun 671d2c9bb1 Less wider submenu 2024-03-28 18:22:29 +08:00
Lim Chee Aun 49fa48bd28 Test if this fixes submenu not opening 2024-03-28 18:22:03 +08:00
Lim Chee Aun 32fb406629 Better shift, but not dynamic 2024-03-28 12:18:25 +08:00
Lim Chee Aun 6950698935 Color space works differently in different browsers 2024-03-28 12:13:38 +08:00
Lim Chee Aun fd9d8059bc Handle info with menu dropdown for profile page 2024-03-28 00:25:10 +08:00
Lim Chee Aun 3b975e899b Try use smaller dimension for fine pointers 2024-03-28 00:23:31 +08:00
Lim Chee Aun b1950046d4 Better alignment for poll radios/checkboxes 2024-03-27 22:08:56 +08:00
Lim Chee Aun d2af509eaf Hacky way to show on-screen keyboard
Doesn't work some of the time.
2024-03-27 21:22:47 +08:00
Lim Chee Aun 311160983f Experiment hide some visibility icons 2024-03-27 19:09:01 +08:00
Lim Chee Aun 9d7d5df7f2 Fix sudden Chrome CSS bug with text-shadow affecting underlines 2024-03-27 16:17:09 +08:00
Lim Chee Aun 927430853a Fix CW-ed images from QPs not cloaked 2024-03-27 16:03:15 +08:00
Lim Chee Aun 1692637e22 Possibly fix weird race conditions
No idea how this happen at all
2024-03-27 14:58:32 +08:00
Lim Chee Aun 2bc24cc495 Pass in postID for Boosted/Liked sheet here too 2024-03-27 10:19:01 +08:00
Lim Chee Aun 66e58c74ef Shazam the filtered notifications 2024-03-27 10:18:34 +08:00
Lim Chee Aun e3591514a1 Use acct instead of username 2024-03-27 10:18:12 +08:00
Lim Chee Aun 4abb1aeaed Fix poll got false value 2024-03-27 09:46:37 +08:00
Lim Chee Aun 7cac17a043 Need Loader fallbacks 2024-03-27 08:09:24 +08:00
Lim Chee Aun 7049166b40 Finally facing the consequences of hacky code
By fixing it with more hacky code
2024-03-26 23:45:22 +08:00
Lim Chee Aun 0a695410d9 Cloak the buttons in filtered notifications 2024-03-26 23:44:18 +08:00
Lim Chee Aun d671178c02 Update copies for severed relationships
Ref: https://github.com/mastodon/mastodon/pull/29731
2024-03-26 19:47:03 +08:00
Lim Chee Aun 67a05450cf Test this lazy shazam 2024-03-26 16:35:02 +08:00
Lim Chee Aun 438b520970 Fix sudden weird underline bug 2024-03-26 13:49:14 +08:00
Lim Chee Aun c8c96f08ac Another attempt to conditional load Intl.Segmenter polyfill 2024-03-25 19:31:25 +08:00
Lim Chee Aun c9bbca9e11 Might as well go further into custom emoji reactions
But still MVP-ish. Misskey emoji shortcodes ain't going to work tho'
2024-03-25 17:58:56 +08:00
Lim Chee Aun 39800e771c Add Mangane 2024-03-25 12:06:03 +08:00
Lim Chee Aun b1c81f7d71 Preliminary support for emoji reaction notifications
Note: pleroma:emoji_reaction is not tested.
2024-03-25 12:05:49 +08:00
Lim Chee Aun 53e9aac14f Show chevron to hint dropdown 2024-03-25 10:26:37 +08:00
Lim Chee Aun cc268019a0 Upgrade dependencies 2024-03-25 10:13:42 +08:00
Lim Chee Aun 9d16c6c12a Fix policy change not working for push notifications
1. Turns out `policy` needs to be inside `data` hash
2. namedItem(policy) → namedItem('policy')

Super embarrassed that these bugs exist for 7 months since push notifications release.
2024-03-25 09:20:51 +08:00
Lim Chee Aun 27a7bc7627 Edit profile now includes extra fields 2024-03-24 23:39:45 +08:00
Lim Chee Aun 1a2914362f Very, very simple Edit Profile sheet. 2024-03-24 20:49:02 +08:00
Lim Chee Aun 9c8aff6d32 Show post preview inside Boosted/Liked by modal
And show the menu in more places
2024-03-24 17:24:47 +08:00
Lim Chee Aun 6816a4b64a Port the tooltip stuff to other link cards 2024-03-24 16:53:33 +08:00
Lim Chee Aun 13f5621488 Fix char counter not showing properly on Firefox 2024-03-24 16:37:58 +08:00
Lim Chee Aun fd59a39021 Preliminary support for severed relationships notifications
Reference: https://github.com/mastodon/mastodon/pull/27511

This is done purely based on the above codebase without real testing.
2024-03-24 14:13:58 +08:00
Lim Chee Aun c19096ab1b Try no split CSS 2024-03-24 10:13:51 +08:00
Lim Chee Aun 0fbc566454 Fix this somehow-partially implemented dot shortcut 2024-03-24 00:21:41 +08:00
Lim Chee Aun f6a9f7807e Allow Lists to be in Shortcuts (except columns)
…and all various Lists-related improvements
2024-03-23 23:52:05 +08:00
Lim Chee Aun 8378d6fc1d Upgrade dependencies 2024-03-23 15:00:13 +08:00
Lim Chee Aun 5ccf8b6842 Show published dates for cards 2024-03-23 12:26:50 +08:00
Lim Chee Aun d6b65d0413 Better red color for danger menus 2024-03-23 12:26:22 +08:00
Lim Chee Aun 8eb67f469c Add Enable/Disable notifications/boosts for accounts 2024-03-23 12:26:01 +08:00
Lim Chee Aun 717633e422 Filters, finally. 2024-03-23 01:07:24 +08:00
Lim Chee Aun f6c2097a89 Fix beyond to date range formatting 2024-03-22 09:33:32 +08: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
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
Lim Chee Aun 707b51a1a0 Don't trigger auto list if meta/ctrl+enter 2024-02-26 14:57:09 +08:00
Lim Chee Aun 201ca6ce4a Catch-up (beta) 2024-02-26 14:02:58 +08:00
Lim Chee Aun a419bb9b61 Fix small typo 2024-02-26 14:02:12 +08:00
Lim Chee Aun a8b5c8cd64 Experimental "cloud" shortcuts settings import/export 2024-02-26 14:00:53 +08:00
Lim Chee Aun a3236ea0f0 Report post/profile 2024-02-26 13:59:26 +08:00
Lim Chee Aun c595b0ee31 Fix toasts showing for unauthenticated interactions 2024-02-26 11:58:22 +08:00
Lim Chee Aun 89f34d7942 Use em, and hide if there's nothing in account "note" 2024-02-26 11:56:18 +08:00
Lim Chee Aun f23e4b0dd9 Just in case, probably not needed 2024-02-25 13:37:50 +08:00
Lim Chee Aun e7d2d088ba Super weird fix for clicks "leaked" to the container 2024-02-25 13:37:29 +08:00
Lim Chee Aun bf609b979e Upgrade masto 2024-02-24 14:00:28 +08:00
Lim Chee Aun 6a6162ec6e Use readOnly, respect CWs for statuses in notifications 2024-02-23 18:07:42 +08:00
Lim Chee Aun 03e5c3ff54 Disable text-rendering: optimizeSpeed
It causes text to jump. It has different kerning when optimized for speed
2024-02-23 18:05:39 +08:00
Lim Chee Aun d8e824b548 Upgrade dependencies 2024-02-23 18:03:26 +08:00
Lim Chee Aun e5d36b82bb Fix search suggestion sort 2024-02-23 18:00:30 +08:00
Lim Chee Aun b6721fc58f Change pin icon color
It gets easily confused with heart icon
2024-02-22 14:21:47 +08:00
Lim Chee Aun 246862e0a4 Upgrade other dependencies 2024-02-21 09:56:55 +08:00
Lim Chee Aun 65e048be17 Downgrade preact due to some weird bugs 2024-02-21 09:56:55 +08:00
Lim Chee Aun cd96ba0c59 Isolate bidi for name text 2024-02-21 09:56:55 +08:00
Lim Chee Aun 9803d18185 Speed up the fade 2024-02-21 09:56:55 +08:00
Chee Aun fefc121b11
Merge pull request #422 from Ganneff/Ganneff-add-fulda.social
Add another phanpy instance url
2024-02-19 17:59:22 +08:00
Joerg Jaspert 3d08349851
Add another phanpy instance url
Adding fulda.social to the list of self-hosted instances.
2024-02-18 21:48:23 +01:00
Lim Chee Aun 1478aca7a5 Need the stripes for PMs 2024-02-18 09:38:54 +08:00
Lim Chee Aun dab0d61ac8 Allow double-click to refresh on Notifications page 2024-02-17 16:50:13 +08:00
Lim Chee Aun 14b92f3f98 Switch to the list from joinmastodon.org/servers 2024-02-17 16:49:50 +08:00
Lim Chee Aun 49cdba2652 Upgrade dependencies again. Last Preact version was causing weird bugs. 2024-02-17 00:27:21 +08:00
Lim Chee Aun 2f94cb34f6 Fix post content not updating when changed 2024-02-16 17:36:46 +08:00
Lim Chee Aun b7a79c8fdd Better memo for Notification 2024-02-15 18:07:17 +08:00
Lim Chee Aun 2f0d04eca4 Update instances list, fix script bug 2024-02-15 17:53:35 +08:00
Lim Chee Aun daabd85273 Upgrade dependencies 2024-02-15 14:59:11 +08:00
Lim Chee Aun c84ad73d0d More memoization 2024-02-14 17:17:15 +08:00
Lim Chee Aun 3295b1ab96 Remove the need for setStates 2024-02-14 17:16:53 +08:00
Chee Aun 5d7b67a410
Merge pull request #417 from Fastidious/patch-1
Update README.md
2024-02-14 01:28:12 +08:00
Fastidious 4e85f92f4b
Update README.md
Changed URL of my self hosted instance.
2024-02-13 10:08:40 -05:00
Lim Chee Aun 9d80647b11 Upgrade dependencies 2024-02-13 22:05:26 +08:00
Lim Chee Aun 24a481b782 Back to end 2024-02-12 18:59:04 +08:00
Lim Chee Aun 97cce8a828 Slightly faster bg transition 2024-02-12 11:54:47 +08:00
Lim Chee Aun 3c31c56306 Fine-tuning status actions styles 2024-02-12 11:53:59 +08:00
Lim Chee Aun 92f4371041 More granular hover/focus state for status actions 2024-02-11 22:46:21 +08:00
Lim Chee Aun a9d0100087 Stripes if PM 2024-02-11 21:04:30 +08:00
Lim Chee Aun 3fbe11295f Don't use dvh for this 2024-02-10 22:22:25 +08:00
Lim Chee Aun 98f018913d Test change to :focus 2024-02-10 20:21:03 +08:00
Lim Chee Aun 60ca577f9b Slight adjustments to status actions 2024-02-10 12:01:51 +08:00
Lim Chee Aun 1d0d02f39b Different alignment for status action menu 2024-02-10 12:00:40 +08:00
Lim Chee Aun fbd448c152 Add one more smaller text size option 2024-02-09 20:07:16 +08:00
Lim Chee Aun 038b2b2e6b Upgrade vite and dependencies 2024-02-09 20:07:06 +08:00
Lim Chee Aun 169aa2d3d3 Fix boost icon color in new status menu 2024-02-08 01:12:02 +08:00
Lim Chee Aun 9a9667d824 Redesign the context menu 2024-02-06 17:34:26 +08:00
Lim Chee Aun afd9d2cf97 Slight style adjustments 2024-02-06 17:32:17 +08:00
Lim Chee Aun b9c287b29e Don't show icon, just show text for visibility.
Icon, in the end, ain't descriptive enough.
2024-02-06 17:30:58 +08:00
Lim Chee Aun 436277c6b4 Prevent re-render dangerouslySetInnerHTML 2024-02-06 17:30:10 +08:00
Lim Chee Aun 4f28d3cc6d Less bolder bold 2024-02-06 17:28:18 +08:00
Lim Chee Aun 46415b87a6 Show lists containing the account in the menu 2024-02-05 10:17:49 +08:00
Lim Chee Aun 913d923877 Make grouped subsequent hashtag pre-meta more seamless 2024-02-04 19:38:22 +08:00
Lim Chee Aun 36f38230c4 Attempt to shorten links if not shortened
This usually comes from non-Mastodon instances
2024-02-03 20:36:25 +08:00
Lim Chee Aun a66a4e238e More subtle style change to reply parent 2024-02-02 13:20:55 +08:00
Lim Chee Aun aa7fb4441f Subtle style change to reply parent 2024-02-02 12:58:35 +08:00
Lim Chee Aun f1dbb9ec42 Further delay filtered status peek, remove tooltip 2024-02-02 00:27:12 +08:00
Lim Chee Aun a59668ea9a Slight adjustment to carousel colors 2024-02-01 22:49:16 +08:00
Lim Chee Aun 6581bc2881 Prevent reply parent hint from being GC-ed 2024-01-31 13:45:34 +08:00
Lim Chee Aun 28bb66f185 Show total at end of list 2024-01-31 09:03:33 +08:00
Lim Chee Aun 46d7cba1ea Show join date if there's nothing to show 2024-01-30 22:46:18 +08:00
Lim Chee Aun ff35c458c3 Don't return 2024-01-30 18:57:28 +08:00
Lim Chee Aun 26d445af7d Fix reply parent hint not appearing
Also respect language
2024-01-30 17:43:44 +08:00
Lim Chee Aun 3470b9adec Fix forgot to opt-in new experiment 2024-01-30 15:22:01 +08:00
Lim Chee Aun f3d77dd04e Experimental reply parent hint 2024-01-30 14:34:54 +08:00
Lim Chee Aun 14f5c37721 Don't show comment hint for timeline item container 2024-01-30 14:28:28 +08:00
Lim Chee Aun 94c59c47d1 Upgrade dependencies 2024-01-29 21:11:19 +08:00
Lim Chee Aun a66307b757 Fixes + improvements to search UI 2024-01-29 21:11:08 +08:00
Lim Chee Aun 9792700f30 Fix wrong CSS
Add more checks
2024-01-29 01:38:53 +08:00
Lim Chee Aun 36e852bebb Fix weird overflow: clip bug on Chrome 2024-01-28 00:49:11 +08:00
Lim Chee Aun 6075542071 Exclude the JS-injected hashtag stuffing class 2024-01-26 16:09:21 +08:00
Lim Chee Aun 0386357688 Fix weird bug with wrong cache of icon 2024-01-26 00:28:03 +08:00
Lim Chee Aun 9cac63c37d Experimental more-harsh hashtag stuffing collapsing 2024-01-25 22:13:38 +08:00
Lim Chee Aun 5cfcfdc98b Squeeze all the micro-perf 2024-01-25 21:28:41 +08:00
Lim Chee Aun a2d995ec07 Support unofficial status.quote 2024-01-25 12:59:53 +08:00
Lim Chee Aun 4ca9a802e3 Remove console.log 2024-01-25 08:00:55 +08:00
Lim Chee Aun 990f2b2e29 Handle unknown audio attachments 2024-01-24 13:08:54 +08:00
Lim Chee Aun 725da37063 Slight adjustments to post actions bar 2024-01-21 13:10:57 +08:00
Lim Chee Aun 1b41d39032 Stretch svg dimensions 2024-01-20 10:26:01 +08:00
Lim Chee Aun 23dd7f5a7a Extract ICONS out 2024-01-20 10:25:47 +08:00
Lim Chee Aun 7d95c50c7a Remove width/height in svg 2024-01-20 01:45:54 +08:00
Lim Chee Aun a352f94c2c Use more beautiful quotes 2024-01-20 01:45:36 +08:00
Lim Chee Aun 38e2b176bc Make embeds larger 2024-01-19 20:31:05 +08:00
Lim Chee Aun 6b4c1c8505 Change menu alignment 2024-01-19 20:29:46 +08:00
Lim Chee Aun 46dfd9aab0 MVP-ish pin/unpin post 2024-01-18 19:05:12 +08:00
Lim Chee Aun 59d0138ca8 If there's selected text, don't show custom context menu 2024-01-17 13:42:46 +08:00
Lim Chee Aun 3fbd5b8622 s/allowNofitications/allowNotifications
Also very embarrassing
2024-01-17 11:32:16 +08:00
Lim Chee Aun b6c4045cb4 Escape HTML chars in composer highlights
This is very embarrassing, I know
2024-01-17 11:31:33 +08:00
Lim Chee Aun 37c784dad2 Make refresh button more prominent 2024-01-16 15:47:10 +08:00
Lim Chee Aun 04d431cf71 Add more conditions 2024-01-15 22:05:18 +08:00
Lim Chee Aun 97458b66eb Update languages list 2024-01-15 20:39:29 +08:00
Lim Chee Aun fadfc6052d Only show for coarse pointer 2024-01-15 00:31:42 +08:00
Lim Chee Aun 0ca92e7509 Fix icon alignment in shortcut settings 2024-01-14 23:04:14 +08:00
Lim Chee Aun b8484eff79 Differentiate menu open from right-click vs actions bar
Kinda hacky for now
2024-01-14 21:34:21 +08:00
Lim Chee Aun 1017d1d270 Style changes for focused more button 2024-01-14 21:33:52 +08:00
Lim Chee Aun 04179340f6 Further enhance actions bar
- Focus color when context menu is open
- Focus color for more button when context menu is open
- Reuse menu instead of creating another menu
- Show like toast when liked/unliked
2024-01-14 19:36:14 +08:00
Lim Chee Aun 9b0889fe23 Test show refresh button after a minute 2024-01-14 18:31:53 +08:00
Lim Chee Aun 79e87b7d89 A little transition when expanding replies 2024-01-14 18:29:11 +08:00
Lim Chee Aun 0ebc0fa64c First step in introducing actions bar 2024-01-14 00:32:08 +08:00
Lim Chee Aun 35974cc89c Show more consistent icon for "comment" 2024-01-14 00:30:12 +08:00
Lim Chee Aun 00675c827f Upgrade react-hotkeys-hook 2024-01-14 00:29:30 +08:00
Lim Chee Aun 2b3f65f28c Fix wrong account shown
Need the hostname to be more accurate
2024-01-12 14:47:59 +08:00
Lim Chee Aun 500f877d4b Fix error when r is undefined 2024-01-11 10:44:37 +08:00
Lim Chee Aun 4b9ff0ca5b Hide "more" icon for posts in notifications 2024-01-11 10:44:24 +08:00
Lim Chee Aun 07f927d4ff Add notice if there's only 1 shortcut 2024-01-10 14:48:29 +08:00
Lim Chee Aun 8c6563a671 More contextual copy 2024-01-10 14:48:08 +08:00
Lim Chee Aun ffabd6188d Truncate URLs 2024-01-10 01:48:20 +08:00
Lim Chee Aun d71b1a7e36 Test add "more" icon near timestamp 2024-01-10 01:47:50 +08:00
Lim Chee Aun c47687e2e4 Fix / and ? key shortcuts suddenly not working 2024-01-10 00:03:36 +08:00
Lim Chee Aun 5b0d6dd58b Upgrade dependencies 2024-01-09 23:47:21 +08:00
Lim Chee Aun ecd5c7b91e . (period) keyboard shortcut = load new posts 2024-01-09 23:47:21 +08:00
Chee Aun 96387c8abb
Merge pull request #392 from JerryLerman/main
Update README.md to add hear-me.social
2024-01-08 13:43:55 +08:00
Jerry Lerman 35958d429d
Update README.md to add hear-me.social
Added phanpy.hear-me.social to the list of deployed sites
2024-01-07 21:53:54 -05:00
Lim Chee Aun 99b0b7c096 Test disable viewScroll=close for hashtag page menu
Possible fix for self-auto-closing when focusing on the
input field to add hashtag and the software keyboard resizes
the page, causing scroll event to fire and close the menu itself
2024-01-07 12:30:51 +08:00
Lim Chee Aun e44ac16396 Fix flash of unscrolled position
Due to statuses being memo-ed, need to speed up the scroll position setup
2024-01-06 19:15:48 +08:00
Lim Chee Aun 147a12cbcb Handle cards with iframe embeds 2024-01-06 16:46:45 +08:00
Lim Chee Aun 16e2ac9bce Test better equal checks 2024-01-06 12:31:25 +08:00
Lim Chee Aun 1574be2b35 Test content-visibility: auto on off-screen columns 2024-01-06 12:23:43 +08:00
Lim Chee Aun 7223baaaad Better error handling for image desc generator
400 doesn't throw error
2024-01-06 12:23:15 +08:00
Lim Chee Aun 9cffd429b0 Potential fix to infinite loop of intersection observer 2024-01-06 03:15:24 +08:00
Lim Chee Aun 9a5d749b8d Better search suggestion styles
Lighter style and fifferentiate between hover and focus
2024-01-06 01:04:14 +08:00
Lim Chee Aun e43f2283dd Resolve account URLs too 2024-01-06 01:03:30 +08:00
Lim Chee Aun be5fcc35ac Comment line extended if there's status pre-meta 2024-01-05 19:18:05 +08:00
Lim Chee Aun 54314de976 Experiment unlinked replies (again)
But still show link to the post's "thread"
2024-01-05 19:15:22 +08:00
Lim Chee Aun bc2886f7e2 Ancestor indicator animates smoother with spring 2024-01-05 19:13:51 +08:00
Lim Chee Aun 2bc1b8387e Fix missing name & short_name inside webmanifest
Need to pass env prefix to loadEnv too
2024-01-05 09:14:09 +08:00
Lim Chee Aun 3989b218d0 Need to encode the query 2024-01-04 22:00:27 +08:00
Lim Chee Aun a8331375ba Double make sure header change doesn't block scrolling 2024-01-04 19:09:30 +08:00
Lim Chee Aun 6919975c6d Remove unneeded .inview 2024-01-04 19:08:51 +08:00
Lim Chee Aun c0987209a8 Only threadify & unfurl non-reblog post object 2024-01-04 18:56:11 +08:00
Lim Chee Aun d25c2df392 Warn if icon not found 2024-01-04 18:55:21 +08:00
Lim Chee Aun 848433365d Don't limit 80px if more than 2 media 2024-01-04 18:55:14 +08:00
Lim Chee Aun 3d4ebb8abe Adjust rootMargin 2024-01-03 10:54:55 +08:00
Lim Chee Aun 72dc4cc81b Test disable menu animation 2024-01-03 09:53:08 +08:00
Lim Chee Aun 92c0a8b4f0 Test memoize svg icon 2024-01-03 09:49:48 +08:00
Lim Chee Aun 1adcca5666 Fix destructure error 2024-01-03 07:27:39 +08:00
Lim Chee Aun b4d4c61128 Experiment delay render items in carousel 2024-01-02 19:56:54 +08:00
Lim Chee Aun 764125e6b9 Test replace scroll-based to inview 2024-01-02 19:26:05 +08:00
Lim Chee Aun 098df0ad2c Test move this out of component mount
It needs to run faster
2024-01-02 17:45:58 +08:00
Lim Chee Aun e41e49884f Less paragraph margins for status cards 2024-01-02 17:45:21 +08:00
Lim Chee Aun 852f7090f6 Status card style changes 2024-01-02 12:27:39 +08:00
Lim Chee Aun d54511aa10 Test a bunch of perf-related style changes 2024-01-02 12:27:22 +08:00
Lim Chee Aun d8ceb03d74 Throttle scroll events 2024-01-02 12:25:25 +08:00
Lim Chee Aun df393ae959 Use InView to replace nearReachStart 2024-01-02 12:25:01 +08:00
Lim Chee Aun 0ebbc5b34e Don't need nearReachEnd, use InView more 2024-01-02 12:24:03 +08:00
Lim Chee Aun cf52e0776e Don't need reachStart from useScroll 2024-01-02 12:20:36 +08:00
Lim Chee Aun b168707c14 Revert "Remove DEV check"
This reverts commit d2fb86036c.
2024-01-01 18:31:59 +08:00
Lim Chee Aun d2fb86036c Remove DEV check
It refers to local dev, not the dev site
2024-01-01 18:29:21 +08:00
Lim Chee Aun 62c8a51307 Test another temp color 2023-12-31 09:39:07 +08:00
Lim Chee Aun f056d7407a Attempt to fix iOS status bar color 2023-12-31 08:02:32 +08:00
Lim Chee Aun c3e40297e0 Add a little delay 2023-12-30 21:51:10 +08:00
Lim Chee Aun d6099df51b Experiment unindenting deep single replies 2023-12-30 21:16:30 +08:00
Lim Chee Aun 096bc69584 Fix child replies accidentally got GC-ed 2023-12-30 21:03:10 +08:00
Lim Chee Aun 32d32b72f4 Less radius for animated media 2023-12-30 20:29:21 +08:00
Lim Chee Aun 796b365fd8 Disable animation if hidden 2023-12-30 20:17:34 +08:00
Lim Chee Aun bd38122f1b Extract unfurling out of status component 2023-12-30 18:13:56 +08:00
Lim Chee Aun d7d838ebf8 Rebuild useScroll, less states 2023-12-29 18:29:08 +08:00
Lim Chee Aun de3787209e Make bold less bold 2023-12-29 18:16:19 +08:00
Lim Chee Aun 6500be2782 Disable hotkeys in quote posts 2023-12-29 18:16:08 +08:00
Lim Chee Aun 2240380f68 Fix wrong month shown for different system date formats 2023-12-29 14:27:43 +08:00
Lim Chee Aun f21a65da9a Micro optimizations 2023-12-29 11:27:01 +08:00
Lim Chee Aun a97478097b Queue all the microtasks 2023-12-29 08:25:58 +08:00
Lim Chee Aun 71d2db31e0 Fix undefined sKey 2023-12-29 08:25:41 +08:00
Lim Chee Aun 88547fa403 Fix slow code blocking whole component render 2023-12-28 18:39:56 +08:00
Lim Chee Aun 1765defa56 Remove dup regex, add another GTS url pattern 2023-12-28 15:42:27 +08:00
Lim Chee Aun 437d721c26 Safari needs this on every element 2023-12-28 15:23:47 +08:00
Lim Chee Aun e13a2feec8 Prioritise local instance unfurl over remote 2023-12-28 11:58:50 +08:00
Lim Chee Aun 39bcb01894 Differentiate icon for group vs local 2023-12-28 11:57:48 +08:00
Lim Chee Aun 7fb0044471 More queueMicrotask 2023-12-28 10:50:54 +08:00
Lim Chee Aun f645815b84 Add small note on usage 2023-12-28 08:29:12 +08:00
Lim Chee Aun f5b1b924a5 More queueMicrotask 2023-12-27 23:44:27 +08:00
Lim Chee Aun fe54eb11a7 Experimental opt-in description generator 2023-12-27 23:33:59 +08:00
Lim Chee Aun cfe41cb802 Test queueMicrotask 2023-12-27 23:32:52 +08:00
Lim Chee Aun 53b1755e51 Update copy, add lingva-api 2023-12-27 16:00:42 +08:00
Lim Chee Aun ef8dda2dbb Special styling for .content.truncated 2023-12-27 12:28:01 +08:00
Lim Chee Aun 66a519f4dc s/Following/Follows 2023-12-27 10:33:29 +08:00
Lim Chee Aun ce6d14fa04 Finally can allow this padding 2023-12-27 10:33:19 +08:00
Lim Chee Aun bc5a4eaf3c Inherit line-through color too 2023-12-27 08:50:23 +08:00
Lim Chee Aun b89463d412 Various adjustments for spoilers and media
- No more blur effects. Performance is more important
- Add background color for all media
2023-12-26 17:06:52 +08:00
Lim Chee Aun 72c5411347 Also show comment icon when not a thread 2023-12-26 17:04:46 +08:00
Lim Chee Aun d59ee9169f Potentially fix unwieldy margins once and for all 2023-12-26 16:00:39 +08:00
Lim Chee Aun 3a3858bd72 Add note on node.js and branches 2023-12-26 10:38:10 +08:00
Lim Chee Aun 69571bf817 Fix "Show all sensitive content" button not working 2023-12-26 10:37:49 +08:00
Lim Chee Aun a539cfea0a Don't link to github if fake commit hash 2023-12-25 20:05:56 +08:00
Lim Chee Aun 976f0a5592 s/APP_TITLE/CLIENT_NAME 2023-12-25 19:53:07 +08:00
Lim Chee Aun f520e30858 Extend self-hosting variables 2023-12-25 19:25:48 +08:00
Lim Chee Aun 563a7bf03b Can't really use important 2023-12-25 01:08:40 +08:00
Lim Chee Aun ae57d95045 Upgrade p-retry 2023-12-24 23:58:05 +08:00
Lim Chee Aun 2923c23672 Test remove theme_color 2023-12-24 23:43:18 +08:00
Lim Chee Aun 7cfa839e1c Perf fixes 2023-12-24 22:49:23 +08:00
Lim Chee Aun 94075086ce Make media post respect reading:expand:media 2023-12-24 21:20:12 +08:00
Lim Chee Aun 60fdd3f522 Fix button display specificity 2023-12-24 21:19:46 +08:00
Lim Chee Aun 6dd54633e0 Finally revisiting this CW thing
Respect reading:expand:spoilers and reading:expand:media but differently than Mastodon's logic
2023-12-24 21:07:46 +08:00
Lim Chee Aun 088d795595 This got repositioned 2023-12-24 21:06:26 +08:00
Lim Chee Aun c54a15de11 Disable memo for Icon
Too many memoization going on here
2023-12-24 21:05:43 +08:00
Lim Chee Aun 8ca768b957 Apply auth for remote-instance API calls 2023-12-23 23:07:08 +08:00
Lim Chee Aun 6703b27bfb No idea why this requires so much code 2023-12-23 23:06:03 +08:00
Lim Chee Aun 3cab36f24c Fix icon doesn't refresh when changed 2023-12-23 18:05:30 +08:00
Lim Chee Aun 30403f835c Update privacy policy regarding translations 2023-12-23 15:35:55 +08:00
Lim Chee Aun 486a707f49 Fix small-width media indentation in status carousels
Center-ize it, add a background color
2023-12-23 15:34:25 +08:00
Lim Chee Aun 5d95d602a7 Skip useEffect if icon already loaded 2023-12-23 12:14:11 +08:00
Lim Chee Aun b00033129f Reset search query if really empty 2023-12-22 23:03:05 +08:00
Lim Chee Aun 768477ea6c Handle cases when account is undefined 2023-12-22 23:02:44 +08:00
Lim Chee Aun da58336285 Experiment: allow Search in Shortcuts 2023-12-22 18:01:41 +08:00
Lim Chee Aun 6bcee318e4 Change intervals 2023-12-22 10:19:06 +08:00
Lim Chee Aun 49fd8a5dc9 Further rate limit this threadify calls
Every post calls threadify and clogs the RAF
2023-12-22 09:54:50 +08:00
Lim Chee Aun 5f48f92c11 Improve perf due to slow localeCompare 2023-12-22 00:26:29 +08:00
Lim Chee Aun 3e4e4d179b Test better temp color for refreshing theme-color 2023-12-21 22:07:31 +08:00
Lim Chee Aun 92d6fe7ebe Let's add speech 2023-12-21 18:17:14 +08:00
Lim Chee Aun 33b55c937b Fix hide-filtered items appearing in boost carousel 2023-12-21 13:32:32 +08:00
Chee Aun cdc19f83b0
Merge pull request #373 from b4ux1t3/b4ux1t3-self-hosted
Added my own self-hosted instance to the list
2023-12-21 10:02:28 +08:00
Lim Chee Aun 22b9a33d64 Only exclude relationship attrs for self 2023-12-21 09:59:08 +08:00
Chris P c1fe8c2b0e
Update README.md 2023-12-20 12:17:35 -05:00
Lim Chee Aun a2189bf44b Make card aware of self-reference
Prevent unfurl if self-referential
2023-12-21 00:37:40 +08:00
Lim Chee Aun ccecc16a2c Fix undefined statusObject 2023-12-21 00:36:55 +08:00
Lim Chee Aun 7b246fc660 It's time to use CloseWatcher
It shipped since Chrome 120 https://chromestatus.com/feature/4722261258928128
2023-12-20 21:02:22 +08:00
Lim Chee Aun dfe727b702 Replace onClick with onClose 2023-12-20 20:59:59 +08:00
Lim Chee Aun bee32cc781 Add 'x' for expanding content warning 2023-12-20 16:42:36 +08:00
Lim Chee Aun 9983c8086c Only show followed hashtags for non-followings 2023-12-20 16:04:37 +08:00
Lim Chee Aun 8ce720f305 Add all the relationships 2023-12-20 13:55:56 +08:00
Lim Chee Aun c16532d4c2 Fix wrong mute durations
This bug exists for 9 months. It's seconds, not ms.
2023-12-19 11:50:01 +08:00
Lim Chee Aun ac60890c9a Revert "Don't highlight mention when it's prepended by a dot"
This reverts commit 5fef0b3fb5.
2023-12-17 18:25:58 +08:00
Lim Chee Aun ec4320d53e Slightly more accurate content length 2023-12-17 18:25:58 +08:00
Lim Chee Aun 4c7c518d4d Disable context menu inside notifications popover
Popovers over popovers ain't easy
2023-12-17 18:25:58 +08:00
Lim Chee Aun 86a362c619 Upgrade dependencies 2023-12-17 18:25:58 +08:00
Chee Aun 4344a4fa82
Merge pull request #368 from owu-one/phanpy-instance
Add another self-hosted instance
2023-12-17 18:21:44 +08:00
CDN 2921d098b3
Add another self-hosted instance 2023-12-17 07:05:11 +00:00
Lim Chee Aun fede3b5f0c Add another self-hosted instance
And add note to encourage PRs
2023-12-16 09:00:23 +08:00
Lim Chee Aun e0c2e41755 Fix typo 2023-12-15 23:32:24 +08:00
Lim Chee Aun df16cabec5 Need tooltip of the timestamp 2023-12-15 23:30:09 +08:00
Lim Chee Aun 60e86c1eaf Use clip more 2023-12-15 23:29:48 +08:00
Lim Chee Aun 2eea5ae053 Add community deployments 2023-12-15 23:29:28 +08:00
Lim Chee Aun aa8cbe046c New experiment: followed tag indicator 2023-12-15 01:58:44 +08:00
Lim Chee Aun b34ef09411 Document the pre-built releases 2023-12-15 01:58:44 +08:00
Chee Aun 9985ffa5b2
Update feature_request.md
Auto-tag it as enhancement
2023-12-13 23:31:20 +08:00
Chee Aun 5ac0d413cb
Update issue templates 2023-12-13 23:30:36 +08:00
Chee Aun aefbfd59e5
Merge pull request #356 from rakoo/dot-mention
Don't highlight mention when it's prepended by a dot
2023-12-13 08:49:27 +08:00
Matthieu Rakotojaona 5fef0b3fb5 Don't highlight mention when it's prepended by a dot 2023-12-12 19:18:56 +01:00
Lim Chee Aun f213a8e094 Fix subfolder hosting not working
Fix wrong `location` used
2023-12-12 08:34:43 +08:00
Lim Chee Aun 82195a8db0 Debug loop break 2023-12-12 08:34:06 +08:00
Chee Aun a9c624dc59
Merge pull request #353 from rakoo/fix-remote-media
Remote media: avoid never-ending loops if it won't work
2023-12-11 19:28:13 +08:00
Matthieu Rakotojaona 71454d40a9 Remote media: avoid never-ending loops if it won't work 2023-12-11 11:51:22 +01:00
Lim Chee Aun 433d8b3bcc Adjustments to welcome and login pages 2023-12-10 19:16:34 +08:00
Lim Chee Aun 7dd0b0a4fb Fix for smaller images 2023-12-10 19:13:11 +08:00
Lim Chee Aun a039f84c9d Don't 100% the select
Suppose to be max-width but not working for select(s)
2023-12-09 15:04:21 +08:00
Lim Chee Aun ceb92a4bfc Fix media widening applied to status cards 2023-12-09 09:35:39 +08:00
Lim Chee Aun 8009a8d743 What's with all this math 2023-12-05 19:28:42 +08:00
Lim Chee Aun 5be3e22467 Reduce the widening
It gets kinda distracting when it's widen too far to left
2023-12-05 19:15:08 +08:00
Lim Chee Aun 94c2f43c38 Add basic unicode awareness to mention highlighting 2023-12-05 18:30:15 +08:00
Lim Chee Aun 92a109d25e Upgrade dependencies 2023-12-05 13:59:30 +08:00
Lim Chee Aun 66746eb579 Potential fix for weird carousel bug on Firefox 2023-12-05 13:02:52 +08:00
Lim Chee Aun 222786f202 Exclude wide media for status cards 2023-12-05 13:01:35 +08:00
Lim Chee Aun 99b4842586 Apply grid to specific classes instead
Due to some extensions inject their own components here and conflicting
2023-12-05 11:06:36 +08:00
Lim Chee Aun 2563b23a31 Prevent scrolling inside status carousel link 2023-12-04 15:25:19 +08:00
Lim Chee Aun ac05fabf05 Experiment widen multiple-media figure 2023-12-04 15:11:14 +08:00
Lim Chee Aun 902149e9a7 Upgrade dependencies 2023-12-04 15:10:34 +08:00
Lim Chee Aun f7e755d0f0 Running npm i rearranged these 2023-12-03 20:42:06 +08:00
Lim Chee Aun cbb7378601 Guard against invalid URLs 2023-12-03 20:40:00 +08:00
Lim Chee Aun 012e944a53 Slight style realignment to post carousels 2023-12-03 20:27:49 +08:00
Lim Chee Aun f98306ed18 No need render div if no content 2023-12-03 20:26:42 +08:00
Lim Chee Aun 810596b7cf Fix history key might be undefined in hashtag object
And some other fixes
2023-12-03 14:21:39 +08:00
Lim Chee Aun 2ad72a667d In case they're undefined 2023-12-03 14:21:39 +08:00
Chee Aun bfcb314324
Merge pull request #341 from natsukagami/fix-pkg-lock-integrity
Update integrity information in `package-lock.json`
2023-12-03 14:21:07 +08:00
Natsu Kagami d1bfbf1ef2
Update integrity information in the package lock
This was done with https://github.com/jeslie0/npm-lockfile-fix
2023-12-02 00:20:44 +01:00
Lim Chee Aun 34e2fe320d Attempt to fix theme-color bug 2023-12-02 00:07:13 +08:00
Lim Chee Aun af503ac865 Upgrade dependencies 2023-12-01 12:08:52 +08:00
Lim Chee Aun 89fb1bbc07 Experiment show replies count for questions 2023-11-30 23:47:58 +08:00
Lim Chee Aun d27de2337a Disable highlighting if slow perf 2023-11-30 23:46:55 +08:00
Lim Chee Aun 910b72ba8c Make settings page work for very small viewports or super large text sizes 2023-11-27 19:01:39 +08:00
Lim Chee Aun cbf4ea5060 Add 1 option for smaller text size 2023-11-27 19:01:09 +08:00
Lim Chee Aun a579f27d55 Upgrade vite-plugin-pwa 2023-11-26 22:55:24 +08:00
Lim Chee Aun 4f41646000 Multiple fixes on composer highlighting
- Hide scrollbar for the faux highlight div
- Use unicode-aware split for highlighting exceeded characters
- Disable highlight of mentions, hashtags, etc if exceeded max characters
- Sync scroll as often as possible
2023-11-26 18:25:29 +08:00
Lim Chee Aun 7019c09e5b Better resolving of links 2023-11-25 21:26:27 +08:00
Lim Chee Aun 1422c5da33 Disable Switch post menu if no post instance yet 2023-11-25 21:25:01 +08:00
Lim Chee Aun 25e13144a3 s/Calckey/Firefish 2023-11-25 21:22:51 +08:00
Lim Chee Aun b7a0d4fe28 Still need tilde
Because there can be spaces around it
2023-11-24 18:49:23 +08:00
Lim Chee Aun 7967194b89 Experiment show play progress for longer GIFs 2023-11-23 22:59:27 +08:00
Lim Chee Aun 4b617b7b9a Upgrade other dependencies 2023-11-23 16:59:39 +08:00
Lim Chee Aun b74f6b3168 Upgrade to Vite 5
Also overrides rollup version to 4.5.1
2023-11-23 16:57:16 +08:00
Lim Chee Aun 6553ae0b6e Use different icon for comment hint 2023-11-23 16:50:14 +08:00
Lim Chee Aun b22e7c06a7 Test new instance of Lingva Translate 2023-11-23 14:21:18 +08:00
Lim Chee Aun fecebc24a8 Fix missing posts due to GC
Hidden/collapsed comments are not mounted so they got accidentally GC-ed
2023-11-23 09:25:29 +08:00
Lim Chee Aun b269d9d660 Fix menu blocking everything for Boost button 2023-11-22 08:47:49 +08:00
Lim Chee Aun 1383296861 Fix null style 2023-11-19 12:06:39 +08:00
Lim Chee Aun eb203a0498 Replace lookbehind regex
because older Safari doesn't support it
2023-11-19 12:06:03 +08:00
Lim Chee Aun 85bdaace58 Replace all Menu to Menu2
Need the default unmountOnClose so don't need the :has() hack
2023-11-18 21:11:07 +08:00
Lim Chee Aun d0e0248dd6 Upgrade text expander element 2023-11-16 10:40:49 +08:00
Lim Chee Aun d87f60665a Enable comment hint for end of thread/conversation 2023-11-15 00:42:19 +08:00
Lim Chee Aun 19ed85f298 Make comment hint opt-in 2023-11-14 22:45:13 +08:00
Lim Chee Aun d6afb473ee Experiment show replies hint 2023-11-14 16:52:47 +08:00
Lim Chee Aun fc842e776e Upgrade dependencies again 2023-11-14 16:46:27 +08:00
Lim Chee Aun 7248095a92 Disable touch-action 2023-11-14 13:49:13 +08:00
Lim Chee Aun e049539b91 Upgrade dependencies 2023-11-14 13:49:06 +08:00
Lim Chee Aun 770f4d9205 Prevent pinned posts from being grouped 2023-11-13 16:57:15 +08:00
Lim Chee Aun 3a326194ad Use static avatar in composer 2023-11-12 11:01:44 +08:00
Lim Chee Aun 911ee288df Adjustments for hidden select inside toolbar button 2023-11-12 10:57:49 +08:00
Lim Chee Aun 91f6efe736 Adjustments for the compose field 2023-11-12 10:57:22 +08:00
Lim Chee Aun b40357c54e Upgrade dependencies 2023-11-09 23:59:20 +08:00
Lim Chee Aun 97188391df Slight adjustments to carousel modal
- Gap between media
- Gradiented backgrounds
2023-11-09 22:38:52 +08:00
Lim Chee Aun 82a9a7212d Fix highlight bugs & maybe some perf issues 2023-11-09 19:11:00 +08:00
Lim Chee Aun dc2eb1163f Slow down polling if scrolled down 2023-11-09 00:16:16 +08:00
Lim Chee Aun 469ce0df6b Add Masto FE standalone 2023-11-08 23:27:33 +08:00
Lim Chee Aun 1dc397c066 Generate tar.gz file too 2023-11-08 23:24:07 +08:00
Lim Chee Aun 1882338078 Basic text highlighting for composer
This will probably be very buggy
2023-11-08 23:16:16 +08:00
Lim Chee Aun 51ddf9b030 Fix link color 2023-11-08 23:03:43 +08:00
Lim Chee Aun 98d1f44244 Also 3s 2023-11-07 11:19:49 +08:00
Lim Chee Aun d16cd501d4 Quick fix for pinned post not showing pin
_pinned no longer stored with post, so pinned posts now can't be reactive
2023-11-07 07:59:59 +08:00
Lim Chee Aun 6d5b2ef9a6 Test fix for uncloseable 'New notifications' bug 2023-11-07 07:58:32 +08:00
Lim Chee Aun a1b0d6e3bd Better keys 2023-11-06 23:58:44 +08:00
Lim Chee Aun a8cf7879a2 Fix promise error with fetching followed hashtags
Remove memoization for now
2023-11-06 23:31:00 +08:00
Lim Chee Aun b027967168 Reduce buffer time between page visibilities 2023-11-06 23:27:58 +08:00
Lim Chee Aun bca205182e Quick fix rendering bug when switching media filter 2023-11-06 22:48:20 +08:00
Lim Chee Aun ea660f9146 New keyboard shortcuts 2023-11-06 20:15:13 +08:00
Lim Chee Aun 8f34d98f47 Fix disappearing filter/spoiler text when hover 2023-11-06 19:47:49 +08:00
Lim Chee Aun 180466160b Slight relayout for Welcome page 2023-11-06 17:17:56 +08:00
Lim Chee Aun 90df455d6e Prevent GC posts from notifications 2023-11-06 16:47:35 +08:00
Lim Chee Aun 6e3494488a Reduce interval to 15s 2023-11-06 09:44:46 +08:00
Lim Chee Aun f73a942b61 Auto-update self account info
And fix isSelf not working in some cases
2023-11-06 00:49:45 +08:00
Lim Chee Aun 8d41ff6884 Fix alpha avatars 2023-11-05 20:09:57 +08:00
Lim Chee Aun 540b9a15a4 Fix noob mistake
And also make announcements and follow requests fetch more non-blocking
2023-11-05 17:57:49 +08:00
Lim Chee Aun 678fc100c8 Allow shifts to open composer in new window 2023-11-05 17:41:29 +08:00
Lim Chee Aun 305710fa8c Fix collapsed peek status with wrong url 2023-11-05 17:40:58 +08:00
Lim Chee Aun 83bdc82049 Add more unfurling
- Fix regex
- Handle trunks.social and Phanpy links too
2023-11-05 16:13:00 +08:00
Lim Chee Aun 7c8d310ed9 Some debugging if this actually runs 2023-11-05 14:31:20 +08:00
Lim Chee Aun 5a4f1fb686 Fix 'account moved' banner wrongly placed 2023-11-05 14:29:18 +08:00
Lim Chee Aun b461823d60 Garbage collect status quotes & unfurled links too
Make this less destructive by setting to 15min interval
Ignore whatever errors inside
2023-11-05 10:12:52 +08:00
Lim Chee Aun 986187141e Make text inside replies button bolder 2023-11-05 09:10:36 +08:00
Lim Chee Aun d0890e3633 Bunch these avatars too 2023-11-05 09:10:12 +08:00
Lim Chee Aun 42df8e62c5 Experiment using touch-action 2023-11-05 09:09:55 +08:00
Lim Chee Aun 87d0b86ecb Only run when idle 2023-11-05 08:26:51 +08:00
Lim Chee Aun e5d5025299 Quick fix disappearing posts bug 2023-11-05 08:21:43 +08:00
Lim Chee Aun 2c6d18bcfc Reduce to 50 2023-11-04 19:19:42 +08:00
Lim Chee Aun 9f31cc8e07 Some sort of "garbage collection" 2023-11-04 19:18:12 +08:00
Lim Chee Aun 660cbebbc4 Move iOS check outside 2023-11-04 19:05:14 +08:00
Lim Chee Aun f8674963b3 Prevent the extra call if posts = 0 2023-11-04 18:02:03 +08:00
Lim Chee Aun fbfb5e5441 Add menu to quick switch to current logged-in instance 2023-11-04 17:51:36 +08:00
Lim Chee Aun 5038e1988d Show 'View post' if 1 media in modal 2023-11-04 15:36:51 +08:00
Lim Chee Aun 5f50df1721 Replace provider/author fallback with published date 2023-11-04 15:36:13 +08:00
Lim Chee Aun 7ad6151637 Port domain format from Trending 2023-11-04 15:35:28 +08:00
Lim Chee Aun 8c8ff72e53 s/See/View 2023-11-04 15:23:56 +08:00
Lim Chee Aun e42d660756 Remove luminosity, makes the alt text hard to read 2023-11-04 15:23:43 +08:00
Lim Chee Aun 674e1fd1ff Fix textarea styles leaked to other textareas 2023-11-04 12:02:41 +08:00
Lim Chee Aun 44ffd69941 Make textarea wider for small viewport 2023-11-04 11:46:32 +08:00
Lim Chee Aun 21007e0a4d Make Try Again button more noticeable 2023-11-04 09:56:06 +08:00
Lim Chee Aun a53be08b3a Reduce hero height 2023-11-04 09:55:52 +08:00
Lim Chee Aun 8e341ff7ed Maybe this logic work better 2023-11-04 01:12:28 +08:00
Lim Chee Aun e0cf2e22fd Make fetches on-demand
Also, cache them
2023-11-04 01:11:29 +08:00
Lim Chee Aun f726f47fcb Slight adjustments 2023-11-04 01:09:25 +08:00
Lim Chee Aun dc1452ab30 Experiment quick open Shortcuts Settings 2023-11-03 22:08:44 +08:00
Lim Chee Aun 1f039a4d73 Upgrade dependencies 2023-11-03 21:53:00 +08:00
Lim Chee Aun 0bc1b598c3 Breaking: rewrote filters implementation 2023-11-03 21:45:31 +08:00
Lim Chee Aun 1cdc4ebbe8 Apply "public" filters for hashtag timeline 2023-11-03 11:27:16 +08:00
Lim Chee Aun e1434e15d9 Fix wrong attr() being used lolol 2023-11-03 11:26:20 +08:00
Lim Chee Aun bd798865d8 Fix .media class can clash with carousel's .media 2023-11-03 00:41:28 +08:00
Lim Chee Aun fa9e0059c0 Hmm, need a flow chart for this srsly 2023-11-02 20:24:52 +08:00
Lim Chee Aun 89f82707d6 Let's try this out
Sometimes the logic gets confusing
2023-11-02 20:13:18 +08:00
Lim Chee Aun 7f327e5980 Fix same key bug 2023-11-02 19:44:53 +08:00
Lim Chee Aun 05ab42684b Another scroll-driven initiative 2023-11-02 17:39:42 +08:00
Lim Chee Aun 131b91e2c1 Clamp 3 lines 2023-11-02 17:38:55 +08:00
Lim Chee Aun 490d776a70 Remove unused variable 2023-11-02 17:38:22 +08:00
Lim Chee Aun 6b3602c6ae Remove debugging console logs 2023-11-02 13:44:32 +08:00
Lim Chee Aun ab5a115084 Replace semver with compare-versions
Also, semver wasn't even in package.json, it worked because a lot of deps use it
2023-11-02 13:38:39 +08:00
Lim Chee Aun fd7caca039 text-wrap: pretty attempt again 2023-11-02 13:00:07 +08:00
Lim Chee Aun 48b505b382 Fix old columns mode setting wrongly applied
Deprecate it more now
2023-11-02 12:59:52 +08:00
Lim Chee Aun 0c2d79c159 Make edited timestamp tab-able 2023-11-02 10:50:21 +08:00
Lim Chee Aun 93e19f549d No need scroll back 2023-11-02 10:50:01 +08:00
Lim Chee Aun 38ee094405 Make edited modal lighter 2023-11-02 10:49:52 +08:00
Lim Chee Aun a9c3c6fdb4 Scroll-driven avatar shrinking 2023-11-02 09:36:30 +08:00
Lim Chee Aun bf7acb6eab Add more conditions for binding longpress
Should be same condition as contextmenu
2023-11-02 08:00:00 +08:00
Lim Chee Aun 030728bc93 Fix .header-account used wrongly
Obviously confused by my own code
2023-11-02 00:14:01 +08:00
Lim Chee Aun 706f3f0cc8 Subtle peekaboo header for the scroll-driven 2023-11-01 23:41:30 +08:00
Lim Chee Aun d9dab6b5ee Ok need to check if navigation is undefined 2023-11-01 23:14:13 +08:00
Lim Chee Aun d35d0cbe18 Fix active filter scrolling to wrong position on larger viewport 2023-11-01 22:56:30 +08:00
Lim Chee Aun ff7db6212d Bye to shine effect
It was fun.
2023-11-01 22:56:10 +08:00
Lim Chee Aun 0c3449aba4 Rearrange/code this part again
- Streaming wasn't UNSUBscribed due to the forever-stuck loop
- Make streaming start later
2023-11-01 22:26:21 +08:00
Lim Chee Aun 3361ffc366 Further make use of Navigation API
history.length is seriously not reliable
2023-11-01 21:56:37 +08:00
Lim Chee Aun 616b9fcf02 Skip if meta/ctrl/shift/alt/middle-click 2023-11-01 21:56:06 +08:00
Lim Chee Aun 7119a78711 Fallback to polling if streaming fails 2023-11-01 21:31:43 +08:00
Lim Chee Aun 33f807de73 More reliable back button
Uses new Navigation API
2023-11-01 19:11:54 +08:00
Lim Chee Aun caeeffaa72 Extra check if container if not clickable 2023-11-01 18:12:22 +08:00
Lim Chee Aun ecb1be5776 Reduce extraneous fetch calls 2023-11-01 18:02:54 +08:00
Lim Chee Aun 0cc956b8c0 Fix initial authenticated: false bug 2023-11-01 17:02:52 +08:00
Lim Chee Aun e6ef2f9064 Better range for header banner 2023-11-01 14:10:56 +08:00
Lim Chee Aun 478271348e Make selected view mode more glowy 2023-11-01 10:00:28 +08:00
Lim Chee Aun 6ec7073151 Fix bypass shortcuts limit 2023-11-01 10:00:05 +08:00
Lim Chee Aun 22abc2fb31 Need @supports check before doing scroll-driven animation 2023-10-31 22:25:08 +08:00
Lim Chee Aun bc0197a5f1 Add a little Fragment here 2023-10-31 22:22:57 +08:00
Lim Chee Aun f3dcd9f4ee Position nav bar to top if there's hover 2023-10-31 20:55:34 +08:00
Lim Chee Aun f5808b6f3b Add keyboard shortcut to toggle cloak mode 2023-10-31 20:50:27 +08:00
Lim Chee Aun 5cb0621f34 Remove unused useSnapshot 2023-10-31 20:21:49 +08:00
Lim Chee Aun 3f6402349c Rearrange code 2023-10-31 20:21:37 +08:00
Lim Chee Aun b17977a5c7 First production-stage scroll-driven animation 2023-10-31 15:43:56 +08:00
Lim Chee Aun afb80d3dc6 Hide "new notifications" button early 2023-10-31 15:41:56 +08:00
Lim Chee Aun 1f78bb9c09 Fix check updates don't use media param 2023-10-31 15:41:39 +08:00
Lim Chee Aun c67192bb81 Show toast when picking month for account statuses 2023-10-31 08:47:19 +08:00
Lim Chee Aun 33b989fffc Loosen the scrollTop check 2023-10-31 00:42:24 +08:00
Lim Chee Aun 39d97a51c5 Make idle state switch faster 2023-10-31 00:38:59 +08:00
Lim Chee Aun d5b257b130 Change the update check logic on Notifications page 2023-10-30 23:53:43 +08:00
Lim Chee Aun 3c790ebff4 Better segmentation of languages with <hr> 2023-10-30 23:50:15 +08:00
Lim Chee Aun 99f81c49c4 Revert "Debounce checks, less noisy"
This reverts commit 9c4252315a.
2023-10-30 20:45:30 +08:00
Lim Chee Aun 5f64553d17 Single column media posts for very small viewport 2023-10-30 19:54:20 +08:00
Lim Chee Aun 75bca8ed3a Upgrade dependencies 2023-10-30 19:31:42 +08:00
Lim Chee Aun 290243df0a Make document titles better 2023-10-30 18:04:17 +08:00
Lim Chee Aun 5fae5d8cf5 Adjustments to media post 2023-10-30 16:45:19 +08:00
Lim Chee Aun 85f966bfc9 Remove this fit-content fix
It makes content jumpy
2023-10-30 09:38:41 +08:00
Lim Chee Aun 3760b52860 This autoAnimate is SO GOOD 2023-10-30 09:24:36 +08:00
Lim Chee Aun 3092a8bba1 Show hashtag usage total counts 2023-10-30 09:22:39 +08:00
Lim Chee Aun 146e5d1a7e Filter out invalid notifications 2023-10-29 23:27:01 +08:00
Lim Chee Aun b28d2d590f Fix media audio squashed 2023-10-29 23:09:56 +08:00
Lim Chee Aun ab29c8c89e Pushing my CSS skills to the limit 2023-10-29 22:06:46 +08:00
Lim Chee Aun 77312f3fb2 Use current instance for links on account info 2023-10-29 21:43:03 +08:00
Lim Chee Aun b40bbb32c2 Alrighty, this is media-view layout 2023-10-29 21:41:03 +08:00
Lim Chee Aun 35f7cae01f Fix moved account styles 2023-10-29 21:21:09 +08:00
Lim Chee Aun 8180cc357e Fix loadAcounts firing twice 2023-10-29 11:47:20 +08:00
Lim Chee Aun 25ff0d7835 Fix toggle show control firing unnecessarily 2023-10-29 10:14:35 +08:00
Lim Chee Aun 173728536a Extract out the fancy selector string 2023-10-28 16:21:32 +08:00
Lim Chee Aun 0599c0d2c9 Fix typo 2023-10-28 13:01:31 +08:00
Lim Chee Aun a1021e1aee Add __STATES_STATS__ for debugging 2023-10-28 11:07:35 +08:00
Lim Chee Aun 087e282677 Show/hide the switch-view button based on viewport width 2023-10-27 23:03:07 +08:00
Lim Chee Aun 4efc922b7b Remove dup key 2023-10-27 18:51:10 +08:00
Lim Chee Aun 372e86415b Test fix scroll position when opening media on the side 2023-10-27 14:16:38 +08:00
Lim Chee Aun 6dd6e0e77c Refactor some components/callbacks 2023-10-27 14:15:29 +08:00
Lim Chee Aun c022e2fd00 Make filter bar expandable 2023-10-27 00:58:42 +08:00
Lim Chee Aun 713865a094 Revert "Let's prettify all paragraphs"
This reverts commit 4897847601.
2023-10-27 00:58:12 +08:00
Lim Chee Aun 0678366566 Fix focus style not working for news' images 2023-10-27 00:01:32 +08:00
Lim Chee Aun b6d8c46e2c Remove console log 2023-10-26 21:29:39 +08:00
Lim Chee Aun 065add5575 Fix account sheet's <main> uses safe-area padding bottom 2023-10-26 21:28:25 +08:00
Lim Chee Aun 35dced8eaf Disable search results pagination if not authenticated 2023-10-26 17:39:10 +08:00
Lim Chee Aun 2310664065 Make nested status link background more consistent 2023-10-26 17:14:43 +08:00
Lim Chee Aun 8858ce3e89 Prevent accidental browser history nav
When scrolling inside deeply nested comments
2023-10-26 17:05:37 +08:00
Lim Chee Aun 1c87dd6e41 Check for reload cases too
Prevent status page from going full width when reloading itself

This checks tab's history length, so opening status page on a new tab means
length = 1.

BUT this will fail if someone copies the link and paste to another
**existing** tab with existing pre-populated history.
2023-10-26 11:42:44 +08:00
Lim Chee Aun 0038c2225b Fix 1 more esc clash 2023-10-26 11:16:34 +08:00
Lim Chee Aun c35f4bb161 Replace old media alt modal with global one 2023-10-26 02:49:03 +08:00
Lim Chee Aun 8426a011b0 Also fix esc handling media alt modal in media modal 2023-10-26 02:48:36 +08:00
Lim Chee Aun 81644e67bb Fix 'esc' closes both modal and status page 2023-10-26 02:19:01 +08:00
Lim Chee Aun c03f39b10c Fix media alt modal not esc-able 2023-10-26 02:18:39 +08:00
Lim Chee Aun a1b81562db Prevent undefined css variables 2023-10-25 20:22:58 +08:00
Lim Chee Aun c82ccf5957 Has to be darker 2023-10-25 20:01:02 +08:00
Lim Chee Aun 8ee1c3a2e3 Reuse color utils for media modal background 2023-10-25 19:19:07 +08:00
Lim Chee Aun 5d5ab906ba Further robustify trending news
- Convert back to RGB for max compat
- Better variable names
- Add fallback if there's no blurhash
- Refactor color utils
- Use alpha instead of light/dark colors
2023-10-25 19:18:47 +08:00
Lim Chee Aun 3a32cbf974 Test full width for first load of status page 2023-10-25 17:07:00 +08:00
Lim Chee Aun b9afe4fb66 s/Favourite/Like
Poll: https://mastodon.social/@cheeaun/111272668719225402
2023-10-25 13:55:12 +08:00
Lim Chee Aun a192554b8b Test overflow-anchor: auto 2023-10-25 13:35:20 +08:00
Lim Chee Aun c2ba149563 Fix undefined variable 2023-10-25 11:17:02 +08:00
Lim Chee Aun 44a20b42a7 Upgrade dependencies 2023-10-25 11:05:39 +08:00
Lim Chee Aun 163ef4ce91 Use 'none' instead
Never thought 'none' would actually do something instead of nothing
2023-10-25 01:14:15 +08:00
Lim Chee Aun beff01c976 Rearrange lingva translate instances
Seems like a lot are 500-ing. May need to find alternatives soon.
2023-10-25 00:14:26 +08:00
Lim Chee Aun f19326528b Fix fetch doesn't throw error when 500 2023-10-25 00:13:42 +08:00
Lim Chee Aun 05ee27e045 Time to remove this auto-shrink text effect in composer
It was fun…
2023-10-24 23:23:51 +08:00
Lim Chee Aun 576dcf7701 Realign some code and UI for account info/sheet
I know, the code is still messy
2023-10-24 23:19:14 +08:00
Lim Chee Aun 0247c041f2 Fix composer not opening for Pleroma instances
Pleroma doesn't have `configuration` in instance API response
2023-10-24 14:30:50 +08:00
Lim Chee Aun 7555bda8e9 Waited wayy too long for Firefox to support :has 2023-10-24 09:58:41 +08:00
Lim Chee Aun 61756fac1d Fix unneccesary re-renders in Notifications 2023-10-23 16:24:30 +08:00
Lim Chee Aun 4897847601 Let's prettify all paragraphs 2023-10-23 16:23:45 +08:00
Lim Chee Aun 8bf3f31056 Slight rewrite, possibly breaking 2023-10-23 16:23:33 +08:00
Lim Chee Aun f2c2983663 Comment out test code 2023-10-23 11:12:28 +08:00
Lim Chee Aun 2c4dd0cdb7 Add lang & dir to trending news 2023-10-23 11:12:15 +08:00
Lim Chee Aun 58d36d2403 Filter links by type 2023-10-23 08:55:22 +08:00
Lim Chee Aun 72842c663a Change from "Build" to "Version"
Also make version string copy-able
2023-10-23 08:43:27 +08:00
Lim Chee Aun 8d694ecf1b Experiment useAutoAnimate 2023-10-23 08:42:40 +08:00
Lim Chee Aun cafadd0980 More fixes for Trending news 2023-10-23 01:36:32 +08:00
Lim Chee Aun 3a1341fb17 Always sort list of Lists 2023-10-22 23:25:25 +08:00
Lim Chee Aun ced30a9602 Fix default tag always wrong location
Feel so dumb looking back at this code lol
2023-10-22 23:09:38 +08:00
Lim Chee Aun 4e53b1e17f Need lazy loading 2023-10-22 20:08:19 +08:00
Lim Chee Aun 1c5453cfb6 Trending news carousel 2023-10-22 19:40:46 +08:00
Lim Chee Aun e7ef20f265 Reuse context menu component for Status
This might be buggy
2023-10-22 19:27:15 +08:00
Lim Chee Aun 9c4252315a Debounce checks, less noisy 2023-10-22 19:26:41 +08:00
Lim Chee Aun 2149c4c35a Toast need centered text 2023-10-22 19:25:36 +08:00
Lim Chee Aun 18b00f7b28 Fix link useTitle showing errors 2023-10-22 19:25:22 +08:00
Lim Chee Aun a6cdd0a01a Memo for shorcuts 2023-10-22 19:24:59 +08:00
Lim Chee Aun ddc8c1e9d9 Compact status need private-mention style if it is 2023-10-21 23:05:32 +08:00
Lim Chee Aun 0d4303861a Auto-set new notification as false
It means it's read from other tabs or devices

So, so cool.
2023-10-21 18:49:39 +08:00
Lim Chee Aun a222828306 Need InView for show more button in Notifications page 2023-10-21 17:54:10 +08:00
Lim Chee Aun 5850485207 Refactor some code 2023-10-21 17:40:03 +08:00
Lim Chee Aun 839647bee7 Better contrast for outer close button 2023-10-21 15:30:38 +08:00
Lim Chee Aun 749d6880b8 Experiment making idle detection global
Hooks are nice but it makes component re-render unnecessarily

Also, idle detection doesn't need to be per-component.
2023-10-21 12:26:28 +08:00
Lim Chee Aun 5a616633c6 Make sure month params don't run if invalid 2023-10-21 12:21:51 +08:00
Lim Chee Aun 47c2efacfb Experiment memoizing avatars 2023-10-21 12:21:05 +08:00
Lim Chee Aun 4c4e89ac9d Contain the overscroll behavior in notifications popover 2023-10-20 23:11:26 +08:00
Lim Chee Aun 4da968df2e Fix avatars not bunching properly 2023-10-20 22:10:55 +08:00
Lim Chee Aun c6f368ac0b Make sure the calendar picker works in dark mode 2023-10-20 22:04:56 +08:00
Lim Chee Aun 87e243ea58 Make scrolling work inside filter bar 2023-10-20 22:00:56 +08:00
Lim Chee Aun 66f9c3b918 Fix async/await 2023-10-20 20:54:24 +08:00
Lim Chee Aun 137ad7f4dd Cache search enabled check 2023-10-20 20:48:30 +08:00
Lim Chee Aun 8ddc44fba6 Mobile Safari need this
Else it'll be almost zero width
2023-10-20 19:46:47 +08:00
Lim Chee Aun 3721acf3d3 Attempt to make month picker better 2023-10-20 19:24:01 +08:00
Lim Chee Aun ab7df0f66c Experiment: month filter for account statuses 2023-10-20 18:11:13 +08:00
Lim Chee Aun d1aedcaef2 Fix unneeded id passed here 2023-10-20 17:11:10 +08:00
Lim Chee Aun 691aea3389 Update loading state of account info 2023-10-20 13:07:31 +08:00
Lim Chee Aun 72f204771f Minor adjustments for search page 2023-10-20 12:53:23 +08:00
Lim Chee Aun dba921a3fd Add key 2023-10-20 12:52:56 +08:00
Lim Chee Aun 4646859177 Fix text shadows applied to search popover 2023-10-20 00:11:14 +08:00
Lim Chee Aun 66fa6fbe52 Memoize getHTMLText 2023-10-19 22:57:56 +08:00
Lim Chee Aun 861619ce57 Fix max-width of nav menu 2023-10-19 22:10:20 +08:00
Lim Chee Aun 71bf8608e6 Relayout the menu items in nav menu again 2023-10-19 21:07:00 +08:00
Lim Chee Aun 2916d1146b Adjust the <p> out 2023-10-19 20:50:32 +08:00
Lim Chee Aun d62712d587 double-tap zoom out once reach max scale 2023-10-19 20:47:11 +08:00
Lim Chee Aun a37c3d6081 Sneak in a slight copy change 2023-10-19 20:19:55 +08:00
Lim Chee Aun 73e995f494 s/for/about 2023-10-19 20:04:07 +08:00
Lim Chee Aun 1dc0069cdc More descriptive toasts copy 2023-10-19 20:02:31 +08:00
Lim Chee Aun a5532488aa Bunch these avatars too 2023-10-19 17:45:37 +08:00
Lim Chee Aun c9545cdc34 Try focus first, then postMessage 2023-10-19 17:45:27 +08:00
Lim Chee Aun e9075906f8 Fix refresh key not unique enough
JS converted these to numbers, much fail
2023-10-19 17:25:17 +08:00
Lim Chee Aun 3339c5c1d6 Change div to span 2023-10-19 16:07:02 +08:00
Lim Chee Aun 965f948899 Recode some nested modal closing logic
Seems more robust
2023-10-19 16:06:55 +08:00
Lim Chee Aun c0c2bb45fe Auto-close account sheet when location path changes
Test this on account sheet first, probably useful for other sheets too
2023-10-19 10:15:54 +08:00
Lim Chee Aun 106cd16e41 Add loading state to filter bar 2023-10-19 10:13:53 +08:00
Lim Chee Aun 7145c20136 Fix wonky filter bar button transitions 2023-10-19 10:13:26 +08:00
Lim Chee Aun e4b6637680 Ok, hopefully fix messed up tag_name
Seems working but need better tag name
2023-10-19 07:43:54 +08:00
Lim Chee Aun cd57e97e2b Fix Preact wrongly rearrange the elements 2023-10-19 01:14:23 +08:00
Lim Chee Aun c1588322aa Bunch the avatars 2023-10-19 01:13:37 +08:00
Lim Chee Aun 3eda1e2267 Fix familiarFollowers call not working 2023-10-19 01:13:12 +08:00
Lim Chee Aun 3617bdc9cb Try tag_name
Why this action so complicated
2023-10-19 01:12:54 +08:00
Lim Chee Aun 26cf40dcea Break the words 2023-10-17 23:23:58 +08:00
Lim Chee Aun 8ae9131543 Private notes 2023-10-17 20:20:26 +08:00
Lim Chee Aun 1b0a77dfae Pluralization for post(s)
Srsly need a i18n lib soon
2023-10-17 14:56:57 +08:00
Lim Chee Aun e3f58442aa Move release creation to prodtag
There is a limitation of workflow: An action in a workflow run can’t trigger a new workflow run.
https://github.com/orgs/community/discussions/27028#discussioncomment-3254360
2023-10-16 23:23:39 +08:00
Lim Chee Aun 119dae29ca Try move this down 2023-10-16 21:38:14 +08:00
Lim Chee Aun c538cfeaaa Add AbortSignal.timeout polyfill 2023-10-16 21:35:56 +08:00
Lim Chee Aun e153f9f541 Prevent undefined class name lol 2023-10-16 20:21:09 +08:00
Lim Chee Aun 42db913b22 Need permissions 2023-10-16 20:14:15 +08:00
Lim Chee Aun 834b1fe1e1 Test fix push seems to not trigger after tag push but branch push instead
Also allow manual trigger
2023-10-16 20:02:27 +08:00
Lim Chee Aun 809b7cc2d2 Micro perf optimizations maybe 2023-10-16 17:01:16 +08:00
Lim Chee Aun 673001e4e0 Fix captions got squashed 2023-10-16 01:55:11 +08:00
Lim Chee Aun 54e69ed23b Perhaps need to be inside waitUntil block? 2023-10-15 23:50:37 +08:00
Lim Chee Aun 7e1bb08b1b Show contributors image in README 2023-10-15 22:55:41 +08:00
Lim Chee Aun 32b72f9297 Prevent time link from overlapping too much 2023-10-15 19:52:33 +08:00
Lim Chee Aun 57dead7960 Slight contrast bump for shiny pills 2023-10-15 19:52:17 +08:00
Lim Chee Aun 9786752a4f Group similar captions
Some folks really just copy/paste same desc for multiple media's
2023-10-15 18:28:04 +08:00
Lim Chee Aun ed8c9e994b Upgrade preact/preset-vite 2023-10-15 16:25:18 +08:00
Lim Chee Aun 8cf30773ce Close notification early
Not sure if this would make a difference but possibly fix some bugs
2023-10-15 16:25:04 +08:00
Lim Chee Aun 6540dd5642 Only set CW if there's spoiler text
Some posts have sensitive media but no spoiler text
2023-10-15 11:24:44 +08:00
Lim Chee Aun c80c8b3294 Need id as dependency too
- inner functions are not reading the updated id
- probably need to rewrite this as this code looks prone to errors
2023-10-15 10:50:33 +08:00
Lim Chee Aun e1ae89b00e Contextually highlight related caption when hovering over image
For multiple-media figures
2023-10-15 09:00:35 +08:00
Lim Chee Aun f9299ac15c Try generate more legit 'Release' 2023-10-15 08:45:11 +08:00
Lim Chee Aun df9eeeb0b3 Don't have to memoize unfurl
It already has caching
2023-10-15 01:42:24 +08:00
Lim Chee Aun 32bf258bbf Test memoize enhanceContent 2023-10-15 01:19:21 +08:00
Lim Chee Aun f56a44ac97 Complete transition from mem to moize 2023-10-14 20:33:40 +08:00
Lim Chee Aun 0a7f158b70 Memoize translated results
First step in migrating to moize
2023-10-14 20:10:34 +08:00
Lim Chee Aun ab1b34d4d2 Fix handling of admin.report notification
This is untested, may break.
2023-10-14 17:59:18 +08:00
Lim Chee Aun f2f7b7fe1f Fix admin.sign_up typo 2023-10-14 17:58:46 +08:00
Lim Chee Aun 7264f543bd Change p to div here too 2023-10-13 23:39:59 +08:00
Lim Chee Aun 66e4ba4991 Upgrade dependencies 2023-10-13 17:23:42 +08:00
Lim Chee Aun f6864f96bd Change p to div 2023-10-13 15:46:43 +08:00
Lim Chee Aun f67d4fd916 Fix id may not be available yet 2023-10-13 15:46:31 +08:00
Lim Chee Aun cd403fe605 Fix error with zero posts 2023-10-13 15:31:04 +08:00
Lim Chee Aun 5481aa12be Cache account info fetches for 10mins 2023-10-13 15:27:24 +08:00
Lim Chee Aun 806ad2c6a2 Fix media re-rendering due to url object keep being recreated 2023-10-12 23:19:48 +08:00
Lim Chee Aun d1b8d737cc Enable on-demand posting stats
- Slight refactor
- Make sure stats also work when switching instances
- Make sure zero stats fallback
2023-10-12 23:11:20 +08:00
Lim Chee Aun a095a30500 Breaking news: upgrade to masto v6
Expecting bugs!

Also include some fixes for states init.
2023-10-12 12:48:09 +08:00
Lim Chee Aun 5de7eec2ca Only show hover styles for tab bar when has hover
The hover delays the tap a little
2023-10-11 19:13:02 +08:00
Lim Chee Aun b8767f3618 Fix load wrong account's stuff when adding new account
Some account-based calls were called before states are initialized
2023-10-11 19:07:36 +08:00
Lim Chee Aun 68759e64d1 Silence errors for follow requests & announcements 2023-10-09 21:53:58 +08:00
Lim Chee Aun 78a6f13380 Fix leaked follow requests from Notifications popover to page 2023-10-09 19:46:07 +08:00
Lim Chee Aun a697fb04df Disable follow request buttons once has relationship 2023-10-09 19:44:54 +08:00
Lim Chee Aun 39f7d4e00d Fix familiar followers leaked to other profiles
Mistake for using global state when it should be per-profile
2023-10-07 17:13:55 +08:00
Lim Chee Aun 12d0e6aed8 Fix media caption and index not synced 2023-10-07 09:41:38 +08:00
Lim Chee Aun 769a5cb099 Change caption display logic for multiple media
- Show all of them or none of them
- If there's at least one caption < 140 chars, show all of them
- Fix potential bug when there are > 4 media
2023-10-06 23:57:12 +08:00
Lim Chee Aun d6d10d091e Slight adjustments to tab bar styles 2023-10-06 18:13:10 +08:00
Lim Chee Aun 5c6e9756d0 Upgrade dependencies 2023-10-06 18:12:37 +08:00
Lim Chee Aun eace6c4d9b Slight adjustments to media alt edit sheet 2023-10-05 18:07:36 +08:00
Lim Chee Aun 4723358d2d Fix borked image when restore from draft 2023-10-05 18:01:18 +08:00
Lim Chee Aun aad855cafc Try to use the additional new props for card
Only use imageDescription for now
2023-10-05 08:54:59 +08:00
Lim Chee Aun 643b6bce07 Try to use the additional new props for card
Only use imageDescription for now
2023-10-04 22:40:34 +08:00
Lim Chee Aun 5faf911b17 Replace scrollIntoViewIfNeeded with scrollIntoView
Because non-standard and not supported on Firefox
2023-10-04 21:24:48 +08:00
Lim Chee Aun ddd1ec5819 Compare accents and diacritics too 2023-10-04 21:23:21 +08:00
Lim Chee Aun 8cd3e38f22 Move this up, Intl stuff seems to run slow sometimes 2023-10-04 10:19:28 +08:00
Lim Chee Aun be964f933c Better throttle instead of debounce 2023-10-04 10:05:21 +08:00
Lim Chee Aun d429ef9161 Don't compact spoiler post if from different author 2023-10-04 08:31:40 +08:00
Lim Chee Aun 9885c8f388 Better contrast for visited links in dark mode 2023-10-04 00:09:32 +08:00
Lim Chee Aun 8be2c738df Make figcaption self align to bottom
This is in case the image height is smaller than the figcaption.
Could be possible for text in other languages.
Flexbox is so cool.
2023-10-03 22:15:15 +08:00
Lim Chee Aun faa7ffc310 Slight adjustments to carousel top buttons 2023-10-03 22:10:32 +08:00
Lim Chee Aun 4ac2e4aa7b Possibly fix rendering issue in Vanadium 2023-10-03 20:38:55 +08:00
Lim Chee Aun 60d55d45c2 Maybe need —tags 2023-10-03 19:45:42 +08:00
Lim Chee Aun 4436c337dd Cleanup 2023-10-03 15:07:47 +08:00
Lim Chee Aun c335655896 Link to Mingcute icons 2023-10-03 15:07:19 +08:00
Lim Chee Aun 48f1527cc6 Robustify useTruncated
Also attempt to fix weird scrollHeight bug again
2023-10-03 13:03:03 +08:00
Lim Chee Aun fcbf99f121 Got to dir=auto all the things 2023-10-03 10:29:28 +08:00
Lim Chee Aun 028b30a334 Possibly fix this tagging thing 2023-10-02 23:22:14 +08:00
Lim Chee Aun 5793476223 Change icons for muted/blocked users
It's not consistent with the icons on the menu for muting/blocking.
There's no "user" in these icons but at least more recognizable. The text should give sufficient context despite less contextual icons.
2023-10-02 21:20:47 +08:00
Lim Chee Aun 715357c8c9 Show synced icon & link to instance for more settings
Context: some users were confused why some settings are not on Phanpy when it can be set on their own instance's web UI
2023-10-02 21:13:56 +08:00
Lim Chee Aun 56365ebc39 Fix duplicate alt badges 2023-10-02 20:55:15 +08:00
Lim Chee Aun a1a78370cc Remove 'Media {i}:'
It'll look weird when description is not English
2023-10-02 19:57:19 +08:00
Lim Chee Aun 7e993704cc More conditions for show/hide captions
- Remove unused code
- Refactor and memoize the long/short calculation too
2023-10-02 18:58:42 +08:00
Lim Chee Aun f05267b216 MVP implementation of listing muted/blocked users 2023-10-02 17:51:36 +08:00
Lim Chee Aun 634e81e9d0 Show roles in account info 2023-10-02 16:55:13 +08:00
Lim Chee Aun 52c63690a3 More noopener noreferrer 2023-10-02 15:58:59 +08:00
Lim Chee Aun 348efe0069 Experiment figcaption for *multiple* media's 2023-10-02 12:21:26 +08:00
Lim Chee Aun 9f6236762d Place captions to right side of media when there's enough space 2023-10-02 09:30:35 +08:00
Lim Chee Aun 8a4ab1bdb9 Rewrite to be slightly more readable
Also, try to fix openWindow not working for Safari PWA
2023-10-01 23:20:48 +08:00
Lim Chee Aun a32a264159 Upgrade preact 2023-10-01 17:53:04 +08:00
Lim Chee Aun a364488895 Test only use longpress for iOS 2023-10-01 17:14:32 +08:00
Lim Chee Aun d05f0a4f23 Remove unused import 2023-10-01 17:14:18 +08:00
Lim Chee Aun 49fdcf7837 Show Translate button when different lang inside alt modal 2023-10-01 14:39:44 +08:00
Lim Chee Aun baa2605d27 Fix navigate not working 2023-10-01 14:38:28 +08:00
Lim Chee Aun 359fd92ae0 Little adjustments, show more captions 2023-10-01 13:18:31 +08:00
Lim Chee Aun 6a16b25722 Show tooltips for the tiny buttons on poll UI 2023-09-30 23:23:52 +08:00
Lim Chee Aun 4dd706ff96 Pass lang into media description
- Assume status lang applies to media description
- Allow RTL for media description
2023-09-30 23:23:34 +08:00
Lim Chee Aun 30f6d50a68 Let's further reduce cancelOnMovement 2023-09-30 00:26:51 +08:00
Lim Chee Aun 3042dea886 Allow GIFs play on focus/blur too 2023-09-29 21:02:29 +08:00
Lim Chee Aun ac14e61b6d Upgrade deps, fix warnings 2023-09-29 21:02:09 +08:00
Lim Chee Aun 27b0813e49 Fix flickering text bug
Font size changes when truncated class is added/removed, thus making it flickering
2023-09-29 09:38:14 +08:00
Lim Chee Aun 99d7525436 Fix name text becomes too easily clickable 2023-09-29 08:58:31 +08:00
Lim Chee Aun f9cb9502b1 Extract alt badge styles out from tag
- Differentiate clickable version vs non-clickable version
- Also differentiate alt badge vs the other "tags" on media
2023-09-28 23:48:01 +08:00
Lim Chee Aun 01c90150a8 Allow show more figcaption 2023-09-28 19:46:44 +08:00
Lim Chee Aun c1da6b8767 Remove previous experimental code 2023-09-28 18:08:36 +08:00
Lim Chee Aun dc06508aa5 Replace Info icon with ALT badge
This will be the "icon" as most users are already used to it
2023-09-28 16:25:13 +08:00
Lim Chee Aun 8c4a88b333 Fade out yellow more 2023-09-28 16:08:24 +08:00
Lim Chee Aun 8a10ffd477 Have to use media-fg/bg for alt badges 2023-09-28 15:59:10 +08:00
Lim Chee Aun b6c59d4ee1 Use luminosity for aesthetics 2023-09-28 15:48:55 +08:00
Lim Chee Aun 13cf7b3f92 It's time for global media alt modal 2023-09-28 15:48:32 +08:00
Lim Chee Aun fd1b45900d Different copy for toast when replying or editing 2023-09-28 15:45:38 +08:00
Lim Chee Aun 0f5edef199 Miss one here 2023-09-28 11:22:05 +08:00
Lim Chee Aun 4dfc0d0b41 Don't show 'Read more' if parent is already truncated 2023-09-28 11:21:40 +08:00
Lim Chee Aun b7416bc17d Handle Takahe links 2023-09-28 11:19:24 +08:00
Lim Chee Aun 173cad2275 So all this while been using the wrong API for autocomplete mentions
🫣🫣🫣
2023-09-27 13:37:12 +08:00
Lim Chee Aun 0403fc35f4 Update enafore link + slight README change 2023-09-27 13:36:14 +08:00
Lim Chee Aun 077b655c44 Don't translate posts with only custom emojis 2023-09-26 16:23:41 +08:00
Lim Chee Aun eeb89212d2 noopener noreferrer all the links 2023-09-26 10:55:36 +08:00
Lim Chee Aun cb04659ab1 Allow filters for posts in carousels 2023-09-25 10:20:32 +08:00
Lim Chee Aun d478dbddba Remove new lines from newline-separated hashtag stuffing
Uses even less vertical space
2023-09-24 18:33:08 +08:00
Lim Chee Aun cb36308790 Collapse grouped conversations too 2023-09-24 18:11:23 +08:00
Lim Chee Aun d4dca0e81f Support non-rectangular custom emojis 😩
Platforms like Misskey have irregularly-shaped custom emojis (emojos?)

- So far this handles horizontally-wide emojis, not tall ones (haven't seen any)
- text-overflow: ellipsis is not used because it can't ellipsis-fy wide emoji images
2023-09-24 15:45:01 +08:00
Lim Chee Aun f8fc24aca4 Fix Read More wrongly positioned on Safari 2023-09-24 10:18:01 +08:00
Lim Chee Aun 7ba5ee5fe2 Don't call familiar_followers if not same instance as logged-in instance 2023-09-23 22:38:29 +08:00
Lim Chee Aun 4c3666df6a Remove isHovering 2023-09-23 19:51:53 +08:00
Lim Chee Aun e3b0c31798 Add Post Translations to Privacy Policy 2023-09-23 19:46:14 +08:00
Lim Chee Aun da03de4115 Add multiple translation instances as fallbacks with retries 2023-09-23 19:45:54 +08:00
Lim Chee Aun 34fcf5e8bd Fix result undefined 2023-09-23 19:45:18 +08:00
Lim Chee Aun d6499cf7fd Subtle text shadowing 2023-09-23 19:16:44 +08:00
Lim Chee Aun 1e9f0bdf39 Slight restyle for shiny pill 2023-09-23 19:16:32 +08:00
Lim Chee Aun cd3ab50a18 Make 'Read more' buttons look more consistent everywhere
Too many cooks spoil the broth
2023-09-23 19:14:11 +08:00
Lim Chee Aun f6ab5e9afa Thicker badge icon
And somehow the old one is too pixelated
2023-09-23 15:59:41 +08:00
Lim Chee Aun b1dec8810b Change video icon style again, might as well make it more consistent this time 2023-09-23 14:39:05 +08:00
Lim Chee Aun a10e2804ba Allow RTL for text inside cards 2023-09-23 12:58:12 +08:00
Lim Chee Aun bd7e099f6e Larger status card inside large status 2023-09-23 12:57:19 +08:00
Lim Chee Aun 3d06662559 Prevent nested 'Read more's 2023-09-23 12:56:55 +08:00
Lim Chee Aun 1f584f945a Disable all the auto*** in search field 2023-09-22 20:39:05 +08:00
Lim Chee Aun a816b69ee9 Remove the @ if short or empty display name
Experimental as the '@' seems superfluous
2023-09-22 20:38:36 +08:00
Lim Chee Aun 85a4b382da Beautify play icon a bit 2023-09-22 00:15:17 +08:00
Lim Chee Aun 7ec1cd1e3d Add a span 2023-09-22 00:15:03 +08:00
Lim Chee Aun 5661729748 Select input text whenever open global search command UI 2023-09-21 22:31:12 +08:00
Lim Chee Aun 551de5a37c Embrace :visited because it's the web 2023-09-21 22:01:00 +08:00
Lim Chee Aun 38bd5c0b5d A bit more aesthetic touches for 'Read more' buttons 2023-09-21 21:56:04 +08:00
Lim Chee Aun 9387e37baa Lower contrast for shiny pill, higher contrast for toasts
Maybe shouldn't call it shiny pill anymore lol
2023-09-21 21:55:30 +08:00
Lim Chee Aun baca2b5851 For debugging 2023-09-21 19:44:26 +08:00
Lim Chee Aun 7e01b4a33a Ignore cmd/ctrl/shift/alt keys + middle clicks 2023-09-21 13:03:16 +08:00
Lim Chee Aun 674c99a05d Fix Lemmy post links not working
Because it's self-referential
2023-09-21 13:02:40 +08:00
Lim Chee Aun b3501d158f Fix push notification badge showing white box on Android 2023-09-20 17:28:42 +08:00
Lim Chee Aun c955427d8f Handle moved account cases 2023-09-20 17:28:08 +08:00
Lim Chee Aun 56e846bec6 Add more data-read-more UIs 2023-09-20 17:27:54 +08:00
Lim Chee Aun 4acfb2a1cf Use useTruncated for notification items 2023-09-19 21:53:59 +08:00
Lim Chee Aun f9b2ab3b94 Refactor truncated class
Also removed the hack fix, not sure why/how it's even fixed.
Don't even know how to explain the logic.
Will revisit and investigate more if the bug happens.

This `useTruncated` can now be reusable.
2023-09-19 16:27:22 +08:00
Lim Chee Aun 42f9483491 Test propagate contextmenu event
No long press yet
2023-09-19 00:46:14 +08:00
Lim Chee Aun fe80215325 Prevent repeated description for alt+figcaption 2023-09-19 00:45:43 +08:00
Lim Chee Aun f7ffce1b46 Add tooltip to show percentage values of posting stats 2023-09-18 19:23:49 +08:00
Lim Chee Aun 64db69af63 Add small gaps between bars 2023-09-18 19:23:29 +08:00
Lim Chee Aun 59dae782b2 Fix typo 🙈🙈🙈 2023-09-17 12:54:48 +08:00
Lim Chee Aun dafff4b635 Show remaining count if exceed the avatars limit 2023-09-16 23:42:49 +08:00
Lim Chee Aun 887503e40b Auto-list composing
Automatically create lists like "- " or "12. " when press Enter
2023-09-16 22:57:35 +08:00
Lim Chee Aun 1a714d214b Fix not all classes removed
This is due to DomTokenList being dynamic, looping it while removing items from it cause wrong indices
2023-09-16 15:45:09 +08:00
Lim Chee Aun 941d2efeb1 Convert posting stats box into a link to account page 2023-09-16 14:48:31 +08:00
Lim Chee Aun 908efb17ff Use onClose 2023-09-16 14:47:55 +08:00
Lim Chee Aun 7d28744234 Fix some links have same class names from the app itself
Srsly need to sanitize the HTML one day
2023-09-16 14:47:35 +08:00
Lim Chee Aun 679fba4f66 Make relationship ui state update faster 2023-09-16 09:43:26 +08:00
Lim Chee Aun ad831fae35 Fix disabled follow button 2023-09-16 08:52:24 +08:00
Lim Chee Aun e102a9f925 Combine familiar followers into followers section 2023-09-15 23:59:27 +08:00
Lim Chee Aun 9571271d83 Experimental posting stats for non-following accounts
Also recode+redesign the multiple metadata boxes in account info
2023-09-15 22:15:41 +08:00
Lim Chee Aun b116cbfe8c Only set data attr if there are shortcuts 2023-09-15 21:12:04 +08:00
Lim Chee Aun b1030cb38a Make figcaption blur too if under content warning 2023-09-15 18:06:55 +08:00
Lim Chee Aun 72438bbf06 Search results pagination not allowed when not authed 2023-09-15 13:08:34 +08:00
Lim Chee Aun f3b81bc540 Fix focus gone wrong 2023-09-15 01:10:58 +08:00
Lim Chee Aun 020d8e3631 Allow settings for unauthenticated sessions 2023-09-15 00:28:20 +08:00
Lim Chee Aun dac07a35d8 Remove unneeded import 2023-09-14 23:28:01 +08:00
Lim Chee Aun 6db40d7d3e Fix ref not defined 2023-09-14 23:23:22 +08:00
Lim Chee Aun 0b5693ae27 First step in caching assets 2023-09-14 23:21:43 +08:00
Lim Chee Aun 7a30cc4b12 Clear badge when onmount too 2023-09-14 22:31:16 +08:00
Lim Chee Aun d18db56032 Experiment show inline desc for videos in timelines
Reason: a video takes more time & effort to watch, so a quick desc would be helpful
2023-09-14 20:41:03 +08:00
Lim Chee Aun 27274eeab1 Rework the modal close + focus logic
- 'Esc' a modal will focus on "behind" nested modal
- All modals will have 'esc'
2023-09-14 20:39:23 +08:00
Lim Chee Aun fce5e45bc9 Respect 'reading:expand:spoilers' pref
Note this doesn't follow 'reading:expand:media' pref separately, so media will be spoiled too
2023-09-14 11:23:41 +08:00
Lim Chee Aun fa145d3ed0 Subtle blockquote styling 2023-09-14 00:25:04 +08:00
Lim Chee Aun 244f3325ae Always tilde 2023-09-13 18:47:11 +08:00
Lim Chee Aun ec57c75fa0 Upgrade p-retry 2023-09-13 18:46:16 +08:00
Lim Chee Aun 5ac255f808 If self, don't need to get familiar followers 2023-09-13 18:43:46 +08:00
Lim Chee Aun 62201b0250 Use _types as key too 2023-09-13 18:43:25 +08:00
Lim Chee Aun f02cd50d7b Fix unknown media not working 2023-09-13 18:10:20 +08:00
Lim Chee Aun 61e1a5042f Fix location invocation bug 2023-09-13 16:38:55 +08:00
Lim Chee Aun 2145f761b5 Fix wrong API call when switch to account's instance 2023-09-12 23:56:01 +08:00
Lim Chee Aun 979c3b1498 Add this to hideAllModals 2023-09-12 23:55:41 +08:00
Lim Chee Aun c4961b26bb Upgrade intl-localematcher 2023-09-12 20:54:40 +08:00
Lim Chee Aun aa3033b4ff Fix bugs with fetching followers/followings 2023-09-12 19:20:22 +08:00
Lim Chee Aun 641d274d7b Handle very-popular cases
- Shorten number
- Limit avatars to 50 since we have the Accounts sheet now
2023-09-12 18:50:46 +08:00
Lim Chee Aun 3fc3641437 Prevent infinite overlapping of Account & Accounts sheets 2023-09-12 18:00:19 +08:00
Lim Chee Aun b57d8adf18 Add Generic Accounts modal
Also refactored whole bunch of stuff
2023-09-12 11:27:54 +08:00
Lim Chee Aun dd2ca7bf35 Animate ancestor indicator 2023-09-12 11:22:01 +08:00
Lim Chee Aun f5184bd608 Prevent propagation from nested links 2023-09-12 11:21:31 +08:00
Lim Chee Aun 671c68b8f8 Experiment use markers for notifications 2023-09-10 19:22:14 +08:00
Lim Chee Aun fcfc61c93b Use different tag format 2023-09-10 19:19:32 +08:00
Lim Chee Aun 98e82a68fd Use useCallback for this 2023-09-10 15:31:51 +08:00
Lim Chee Aun 71f177bebe Memoize isModalPage 2023-09-10 15:30:04 +08:00
Lim Chee Aun a0f16057a0 Make this more readable 2023-09-10 15:29:52 +08:00
Lim Chee Aun 2d94f229c3 Fix weird textarea height on first render 2023-09-10 15:29:25 +08:00
Lim Chee Aun 33698c91cc Add one more account resolver fallback 2023-09-10 09:13:00 +08:00
Lim Chee Aun f4ce2e8367 Better style for jagged timeline items 2023-09-09 23:55:11 +08:00
Lim Chee Aun 059fed4b84 Upgrade dependencies 2023-09-09 17:36:15 +08:00
Lim Chee Aun 886d78bde8 Additional ? check 2023-09-09 17:20:31 +08:00
Lim Chee Aun 6b5a98ebb3 Prevent all the re-renders
Srsly this took me hours to debug
2023-09-09 17:00:51 +08:00
Lim Chee Aun 696a46311d Try willReadFrequently 2023-09-09 14:26:08 +08:00
Lim Chee Aun fea1d77342 Possible small optimization for name-text 2023-09-09 14:25:53 +08:00
Lim Chee Aun 8018d06cdf Another (better) way of updating safe area insets
Hopefully this works
2023-09-09 14:10:52 +08:00
Lim Chee Aun 5147efd123 memo all the things
Somehow things got slower on local dev
2023-09-09 14:09:50 +08:00
Lim Chee Aun d4fc54eaf4 Make the floating account block cooler in composer 2023-09-08 21:14:23 +08:00
Lim Chee Aun c82edd2778 Add r, f, shift+b, d 2023-09-08 15:32:55 +08:00
Lim Chee Aun 301b2576c0 Have more fun styling the keys 2023-09-08 15:32:31 +08:00
Lim Chee Aun 5969ce7d06 Test auto-create tag on push to prod 2023-09-08 14:49:58 +08:00
Lim Chee Aun 10288411be Add Costs to README 2023-09-07 23:31:55 +08:00
Lim Chee Aun 7c09485e26 Fix focusDeck not working on initial page load 2023-09-07 18:44:12 +08:00
Lim Chee Aun 3ce8b75e3f Add shortcut help for focusing columns in multi-column mode 2023-09-07 16:17:52 +08:00
Lim Chee Aun 61f2132abd Fix getNotifications is not a function 2023-09-07 12:17:31 +08:00
Lim Chee Aun 1c295c585b Try this tap UI feedback, idea from Threads 2023-09-07 12:01:26 +08:00
Lim Chee Aun aa12010b80 Try this out, box sizing will be slightly off to the naked eye 2023-09-07 12:00:50 +08:00
Lim Chee Aun 10471090f5 More accurate isActive 2023-09-07 12:00:13 +08:00
Lim Chee Aun 6e4110714c 44px is too small, especially when there's labels inside like GIF or video timestamp 2023-09-07 11:59:40 +08:00
Lim Chee Aun 67fb1a9b19 It's time to double down on scale-down 2023-09-07 11:58:17 +08:00
Lim Chee Aun 0d090eb555 Keyboard shortcuts help sheet 2023-09-06 22:54:05 +08:00
Lim Chee Aun 167fa70fd5 Fix search command not disappearing 2023-09-05 23:30:11 +08:00
Lim Chee Aun e4174b49d5 c for opening composer, shift+c for opening it in new window 2023-09-05 21:44:38 +08:00
Lim Chee Aun 2540135962 Extract compose button to file 2023-09-05 18:49:16 +08:00
Lim Chee Aun e7833d5b8c Grammar 2023-09-05 13:26:30 +08:00
Lim Chee Aun 20c80adfc6 New languages 2023-09-05 09:23:10 +08:00
Lim Chee Aun 4fede554e4 Handle admin notifications & unhandled ones 2023-09-05 09:19:11 +08:00
Lim Chee Aun 20dd843409 Why some posts have inReplyToAccountId but doesn't have inReplyToId?
Not sure if this will cause other bugs
2023-09-05 02:50:58 +08:00
Lim Chee Aun b472e496d1 Fix bug: hashtags opening account sheet 2023-09-04 20:10:08 +08:00
Lim Chee Aun 17a289ac22 Close notification sheet when click "View all notifications" 2023-09-04 19:40:56 +08:00
Lim Chee Aun eed9b70a7d Fix search bugs 2023-09-04 17:01:06 +08:00
Lim Chee Aun 0fd719d3e7 Global search command trigger 2023-09-04 14:49:39 +08:00
Lim Chee Aun 3511ba760a Try autofocus on search field
I commented this out for some reason that I forgot
2023-09-04 00:07:11 +08:00
Lim Chee Aun bb1850a330 Add note to npm install first before npm run build 2023-09-03 20:46:10 +08:00
Lim Chee Aun a9109f4839 Show account block in Composer 2023-09-03 19:48:36 +08:00
Lim Chee Aun c5766e431c Fix error when opts is null/undefined 2023-09-03 19:44:26 +08:00
Lim Chee Aun 6c3a700f01 Expand "New update available…" menu row
Somehow 2nd section position: sticky stops working
2023-09-03 18:41:36 +08:00
Lim Chee Aun 8cc85ecb1a First attempt of CSS container query 2023-09-03 18:10:47 +08:00
Lim Chee Aun 6cbbd0aa1b More reliable badge clearing
Should be when page visible, not on render

Possibly super effective, but badges can be annoying if not cleared easily.
2023-09-03 13:41:37 +08:00
Lim Chee Aun d4dce2fa45 Differentiate username displays
When there're mentions of multiple same username + different instances in a post
2023-09-03 10:07:06 +08:00
Lim Chee Aun 39d96f22a0 Make code blocks focusable 2023-09-02 20:49:25 +08:00
Lim Chee Aun 3ac05d8cdd Refactor code to files 2023-09-02 18:19:09 +08:00
Lim Chee Aun 1257ce8636 Handle memorial accounts 2023-09-02 15:06:15 +08:00
Lim Chee Aun 062f42a05d Fix missing useLayoutEffect 2023-09-02 02:25:44 +08:00
Lim Chee Aun 852bb27e81 Clear app badge when view Notifications page 2023-09-02 01:35:24 +08:00
Lim Chee Aun 0e745663f0 Yes, push notifications (beta).
Heck this feature is tough.
2023-09-01 15:40:00 +08:00
Lim Chee Aun 0b04e01d60 Try out another style for 2nd-pass grouped notifications 2023-08-30 20:16:34 +08:00
Lim Chee Aun 5461b06130 Safeguard deconstruct 2023-08-30 17:47:17 +08:00
Lim Chee Aun 91419b3243 Enable relative path hosting 2023-08-30 17:46:22 +08:00
Lim Chee Aun a5865825da Init states again after login to new account 2023-08-30 17:42:33 +08:00
Lim Chee Aun 4e47d679bc Card object has language now. Use it.
Also have other additional keys but later
2023-08-30 00:45:18 +08:00
Lim Chee Aun 2ebf421140 Remove "votes" text from poll items translation
Making it language-agnostic.
2023-08-29 20:41:48 +08:00
Lim Chee Aun 8bfc9892ed Blind fix for submenus bug 2023-08-29 15:23:58 +08:00
Lim Chee Aun d64bbb7acb Fix oops 2023-08-28 05:49:12 +08:00
Lim Chee Aun aae74aa476 Experiment show avatars instead
Add a bit of tooltips too
2023-08-28 00:21:49 +08:00
Lim Chee Aun 12b8651d18 Use 1px instead of hairline
The more visible border width is needed for the buttons overlaying on top of media
2023-08-27 13:07:06 +08:00
Lim Chee Aun 1fae2f3208 2nd pass grouping of 1-account-many-statuses fav/boost 2023-08-27 13:06:26 +08:00
Lim Chee Aun 1b3112de1b Don't apply max-height to statuses in carousel 2023-08-25 15:41:03 +08:00
Lim Chee Aun 0792df1adb dir=auto all the things 2023-08-24 09:12:00 +08:00
Lim Chee Aun de7248fbfd Link to Enafore and Tusked 2023-08-23 23:34:17 +08:00
Lim Chee Aun e8cc26fe2b bidi fixes 2023-08-23 18:34:11 +08:00
Lim Chee Aun b22ea39e6c Ignore instances-full.json 2023-08-22 23:36:16 +08:00
Lim Chee Aun 4aaf308d6e Don't show list of instances by default
Very basic sorting too
2023-08-22 20:16:09 +08:00
Lim Chee Aun e88b24fe6f Refresh instances list 2023-08-22 20:11:28 +08:00
Lim Chee Aun aede10d71e Better copy for interactions on replies 2023-08-20 14:22:47 +08:00
Lim Chee Aun 95f71115d4 First attempt of CSS nesting
This'll be "un-nested" by PostCSS anyway
2023-08-20 12:17:11 +08:00
Lim Chee Aun e0c2a5aed1 Prevent hero container from expanding too tall 2023-08-20 10:55:11 +08:00
Lim Chee Aun ccd79e5348 Further polish hashtag stuffing logic 2023-08-20 10:17:56 +08:00
Lim Chee Aun a325630c20 Upgrade dependencies 2023-08-19 19:24:15 +08:00
Lim Chee Aun 1559052361 Fix Flash of Loader (FOL) 2023-08-19 19:21:51 +08:00
Lim Chee Aun d2e417eaa4 Rendering bug seems fixed in Preact v10.17.1 2023-08-19 19:21:07 +08:00
Lim Chee Aun 4a423b134d Fix link style affecting status cards 2023-08-19 17:07:16 +08:00
Lim Chee Aun ff3ef9fa45 Restyle play icon 2023-08-19 14:39:45 +08:00
Lim Chee Aun bce8456ac6 Fix radius 2023-08-19 14:32:21 +08:00
Lim Chee Aun de10faee88 Further apply text color to links 2023-08-18 13:48:45 +08:00
Lim Chee Aun d64a363d60 Stretch the content for boosts in boosts carousel 2023-08-17 22:05:55 +08:00
Lim Chee Aun 6755626259 Show votes count in translated text 2023-08-17 14:08:26 +08:00
Lim Chee Aun 6ddbcacd76 Downgrade preact, v10.17.0 is causing rendering issues 2023-08-17 13:45:51 +08:00
Lim Chee Aun 271601dc2c Update the loading placeholder 2023-08-17 13:36:03 +08:00
Lim Chee Aun f7343fd4fd Check for no shortcuts cases 2023-08-16 16:39:22 +08:00
Lim Chee Aun 932e66f330 Update copy for import/export 2023-08-15 22:40:58 +08:00
Lim Chee Aun b922c2f096 Add fancy screenshot 2023-08-15 20:29:24 +08:00
Lim Chee Aun 8790b20354 Experimental Shortcuts settings import/export 2023-08-15 20:14:09 +08:00
Lim Chee Aun 4817eddc2a Get rid of system-ui 2023-08-14 22:45:57 +08:00
Lim Chee Aun c1f947a9c3 Sometimes, have to think out of the box
Focus first, then scroll
2023-08-14 21:56:44 +08:00
Lim Chee Aun e3c77cb516 Increase timeout, but such hacky sadly 2023-08-14 21:39:53 +08:00
Lim Chee Aun fe8eb74242 Another attempt 2023-08-14 21:09:14 +08:00
Lim Chee Aun d0bd257a8e Attempt to fix media modal next/prev buttons not working in Safari 2023-08-14 20:55:21 +08:00
Lim Chee Aun 8141513fa9 Spruce up buttons in media modal 2023-08-14 20:32:09 +08:00
Lim Chee Aun ac8a4c7fbf Instead of return false, return the default locale arg 2023-08-14 18:03:05 +08:00
Lim Chee Aun a382efee5b Document hashtag stuffing collapsing implementation 2023-08-14 15:56:46 +08:00
Lim Chee Aun 635f4c1b0d s/setUiState/setUIState 2023-08-14 11:22:42 +08:00
Lim Chee Aun d237fb8320 Experimental preload icons 2023-08-13 17:15:49 +08:00
Lim Chee Aun 2ba2696e9e Small radius fix 2023-08-13 12:38:03 +08:00
Lim Chee Aun 31d7016bd9 Default show chars-left donut 2023-08-13 12:00:33 +08:00
Lim Chee Aun 8b74a32168 Fix race conditions when accept/rejecting many follow requests
- No longer reload the whole list of follow requests and notifications for every accept/reject action
- Notifications list now exclude follow requests (experimental)
2023-08-11 18:00:36 +08:00
Lim Chee Aun 37ce48ae6e Update supported languages 2023-08-11 12:07:40 +08:00
Lim Chee Aun 5b8744ac55 Replace bull with round icon 2023-08-10 23:52:29 +08:00
Lim Chee Aun 339b66f42f Attempt to fix Firefox keyboard shortcuts bug on navigating media carousel 2023-08-10 21:58:11 +08:00
Lim Chee Aun 84d1500331 Fix menu items not stretching when it's only one 2023-08-09 19:59:06 +08:00
Lim Chee Aun 889fdc87a1 Fix weird styles in Safari 2023-08-09 19:34:37 +08:00
Lim Chee Aun 1ecd568c29 Preliminary support for exclusive list
Only for Mastodon v4.2+
2023-08-09 19:08:42 +08:00
Lim Chee Aun bf39f9eafc Add (more visible) show/hide poll results
+ small UI polish and fixes
2023-08-09 16:26:29 +08:00
Lim Chee Aun 79aa3faf51 Fix wrong height set for single media inside carousel status 2023-08-09 13:29:31 +08:00
Lim Chee Aun 0ca29cb181 Fix wrong color for filtered group post 2023-08-08 20:21:09 +08:00
Lim Chee Aun 3d458826cf Fix http route not working 2023-08-08 17:29:04 +08:00
Lim Chee Aun 58c6b6349c Time to embrace prefers-reduced-motion with picture 2023-08-08 15:34:24 +08:00
Lim Chee Aun fb798ce895 Recode EmojiText, fix bug for some emojis not being replaced 2023-08-08 14:04:12 +08:00
Lim Chee Aun c3f80cec9b Show displayName too 2023-08-08 14:03:27 +08:00
Lim Chee Aun 9a44dfafa6 Show group tag in search results 2023-08-07 21:26:56 +08:00
Lim Chee Aun a8c7e08f3f Treat posts from groups differently from boosts 2023-08-07 21:26:43 +08:00
Lim Chee Aun e53f0efde9 Test fix: Prevent pull-to-refresh on Chrome PWA 2023-08-07 16:11:11 +08:00
Lim Chee Aun 794ee3cb74 More accurate border radius 2023-08-07 16:00:12 +08:00
Lim Chee Aun 9b23e051e2 Still need this length check, this "done" is not reliable 2023-08-07 11:39:42 +08:00
Lim Chee Aun 0b3875c2cf Only focus when menu item is clicked 2023-08-06 16:54:13 +08:00
Lim Chee Aun c13e148b36 How did I even code this 2023-08-05 00:16:18 +08:00
Lim Chee Aun 6b8ae97d98 Add small link icon for imageless link cards 2023-08-05 00:15:57 +08:00
Lim Chee Aun d36ea02a02 Undo "Experiment: make replies container not whole-clickable" 2023-08-03 02:10:59 +08:00
Lim Chee Aun 76823b8497 Don't propagate large styles to status cards 2023-08-03 02:02:00 +08:00
Lim Chee Aun 1887a34fc5 Another aspect ratio style fix 2023-08-02 17:41:00 +08:00
Lim Chee Aun 88accb2a78 Reduce code for spoiler styles 2023-08-02 17:40:28 +08:00
Lim Chee Aun c91cda1a2c Ok the math was too advanced for CSS 2023-08-01 23:54:28 +08:00
Lim Chee Aun dc7083a11d Pushing the limits of my math 2023-08-01 23:44:28 +08:00
Lim Chee Aun b0ed0be47d Allow keyboard nav after clicking on buttons in media carousel 2023-08-01 19:43:52 +08:00
Lim Chee Aun 75cfd02134 Need link for ancestors too 2023-08-01 19:24:12 +08:00
Chee Aun e7f624c33c
Merge pull request #202 from natsukagami/transform-by-main-width
Use `--main-width` for transform calculation
2023-08-01 18:56:57 +08:00
Natsu Kagami 509efd2ce0
Use --main-width for transform calculation
... instead of hard-coding. Make it easier on the eyes for forks with `--main-width` modified :P
2023-08-01 17:36:43 +07:00
Lim Chee Aun c30eaee4e2 Somehow this kinda works 2023-08-01 18:20:54 +08:00
Lim Chee Aun 72ff229dfb Upgrade dependencies 2023-08-01 14:27:11 +08:00
Lim Chee Aun 30d532c2e3 Allow user-selection on hero post in status page 2023-08-01 14:26:59 +08:00
Lim Chee Aun b1b1ed0f3f Adjustments to prevent layout shift 2023-08-01 14:26:22 +08:00
Lim Chee Aun 48a5fc6327 One more fix for preventing callout 2023-08-01 09:43:25 +08:00
Lim Chee Aun c28bae7708 Try prevent touch callout when long-press 2023-08-01 09:12:43 +08:00
Lim Chee Aun 031bdc0a88 Forgot to commit these 2023-08-01 00:59:58 +08:00
Lim Chee Aun 8cd00a053c Experiment: make replies container not whole-clickable
Except for "thread" statuses
2023-08-01 00:15:07 +08:00
Lim Chee Aun 3fe99050e0 Small fixes 2023-08-01 00:12:01 +08:00
Lim Chee Aun ba9cf70f44 Unproxy the proxy 2023-07-31 20:30:29 +08:00
Lim Chee Aun 507d8f449a Safari seems really confused with this 2023-07-31 09:31:34 +08:00
Lim Chee Aun cf59b9dda1 Definitely need to recode this one day
Or at least split the code for single media vs multiple media
2023-07-31 00:37:57 +08:00
Lim Chee Aun 760fdb66db Quick fix for Safari 2023-07-31 00:24:45 +08:00
Lim Chee Aun c003724108 Few changes to how media rendering
1. Try respect aspect when only 1 media
2. Distance-based image inner-scroll animation
3. Small inner radius between media when >=2 media
2023-07-30 21:28:17 +08:00
Lim Chee Aun fad286e617 Some posts have nested lists 2023-07-26 11:25:57 +08:00
Lim Chee Aun 14091fbc7b It's time to widen carousel for Firefox users
Srsly take too long time waiting for Firefox to support :has()
2023-07-25 17:23:22 +08:00
Lim Chee Aun 6fe182a7a3 Shazam the mini translation block 2023-07-24 22:27:30 +08:00
Lim Chee Aun 871fe11d0f Add safe min-width for poll 2023-07-23 16:57:43 +08:00
Lim Chee Aun b0808305ab Fix poll meta not showing 2023-07-23 16:57:20 +08:00
Lim Chee Aun 4bf6b00b94 Shorten shortenNumber code 2023-07-23 14:09:39 +08:00
Lim Chee Aun 5fa02f9cc4 Fix max-width bug for profile field 2023-07-23 01:00:22 +08:00
Lim Chee Aun 32a853ecc0 Make auto inline translation as a setting, turned off by default 2023-07-22 20:59:07 +08:00
Lim Chee Aun d8b385a742 Fix logic not checking different language 2023-07-22 20:50:53 +08:00
Lim Chee Aun bc3e946f61 lol why need to keep checking the text 2023-07-22 20:48:01 +08:00
Lim Chee Aun eb13fe8ce0 Fix logic again
I really need to rename these variables to be less confusing
2023-07-22 20:31:13 +08:00
Lim Chee Aun 28ad18bd0b Show pronunciation text in tooltip 2023-07-22 20:30:32 +08:00
Lim Chee Aun 9869c9dc5b If translated text is same as original text, don't show it
This means language detection messed up
2023-07-22 20:30:18 +08:00
Lim Chee Aun ac9962b051 Don't show inline translation if has card 2023-07-22 10:10:41 +08:00
Lim Chee Aun 075c729807 Fix logic again 2023-07-22 00:06:15 +08:00
Lim Chee Aun 587864893c Getting confused with the logic
Also more accurate content length calc
2023-07-21 23:54:03 +08:00
Lim Chee Aun 658872cbd9 Fix logic again 2023-07-21 23:00:58 +08:00
Lim Chee Aun 5502d08d28 Fix typo and logic 2023-07-21 22:52:53 +08:00
Lim Chee Aun 58bf8e16c2 Persist auto-inline-translation to the large size status too 2023-07-21 13:25:18 +08:00
Lim Chee Aun 4aab2d39cc Set max width for very long profile metadata 2023-07-21 00:55:37 +08:00
Lim Chee Aun 6f28db2532 Make "tabs" work for Mentions page in Columns mode 2023-07-20 20:06:07 +08:00
Lim Chee Aun 8112f0a9d6 Upgrade dependencies 2023-07-20 19:29:49 +08:00
Lim Chee Aun 9b0e63d289 Handle elk links 2023-07-19 15:51:00 +08:00
Lim Chee Aun da425b4a70 Fix wrong url cached 2023-07-19 15:46:00 +08:00
Lim Chee Aun 7286a4e03b Attempt to fix menu confirm not opening 2023-07-19 15:19:03 +08:00
Lim Chee Aun 1f0d2eebe6 Having fun with multi-stacking modals 2023-07-18 20:40:10 +08:00
Lim Chee Aun 38a13b07c5 Fix boost menu bug 2023-07-18 18:45:38 +08:00
Lim Chee Aun 92a4f502a0 Experimental Auto Inline Translation (AIT)
For short posts for now and throttled API calls
2023-07-18 13:31:26 +08:00
Lim Chee Aun ff41cd3563 Replace (most) alert/confirms with alternative UI
Everything might break lol
2023-07-17 21:01:00 +08:00
Lim Chee Aun 10fa537a56 Make instance text wrap on its own 2023-07-16 10:36:33 +08:00
Lim Chee Aun 473dac1fde Fix layout regression in Settings sheet
My laziness in separating the styles between Settings and Accounts sheets bit back
2023-07-16 10:35:54 +08:00
Lim Chee Aun 18a5742bfc Make it shrink for profile page 2023-07-16 09:05:46 +08:00
Lim Chee Aun df047131bb Show instance URL in accounts list
When logged-in, acct doesn't show @instance
2023-07-14 14:46:57 +08:00
Lim Chee Aun 3192c319ee Experiment more minimalistic account sheet 2023-07-14 14:36:13 +08:00
Lim Chee Aun 42633f87ea Recode some parts in search page
Still very messy, I know
2023-07-14 13:16:41 +08:00
Lim Chee Aun 1ef9613358 Need more gap 2023-07-14 10:43:35 +08:00
Lim Chee Aun 48b21ec42d lol, totally wrong logic 2023-07-13 23:12:05 +08:00
Lim Chee Aun afc13c0d7e Fix fn not refreshed in useInterval 2023-07-13 20:11:23 +08:00
Lim Chee Aun 5791338393 Use svh 2023-07-13 20:10:53 +08:00
Lim Chee Aun 1e28efd9bb Fix search offset not working when first time load with 'type' 2023-07-13 20:10:39 +08:00
Lim Chee Aun fa21eec06a Try useIdle 2023-07-12 17:32:05 +08:00
Lim Chee Aun e26473f607 Replace import.meta.glob, it actually generates imports for *all* icons
Change to manually import icons
2023-07-12 16:42:58 +08:00
Chee Aun 0c86416489
Ask for which instance in bug report 2023-07-12 07:41:21 +08:00
Lim Chee Aun fd1fc9c5fc Let's flip things around 2023-07-11 19:20:01 +08:00
Lim Chee Aun 4dbc26dbb6 lol name is not unique 2023-07-11 15:06:30 +08:00
Lim Chee Aun 7fa7276a43 Prevent list numbers from being shrinked 2023-07-11 10:48:26 +08:00
Lim Chee Aun 32f2a6d99b Upgrade dependencies 2023-07-10 10:17:46 +08:00
Lim Chee Aun 4bfd36fa9b Fix useState undefined
Blame myself working on multiple features at the same time
2023-07-09 16:51:05 +08:00
Lim Chee Aun 6956628369 Add posting visibility setting
Also respect visibility setting when replying *if* replied-to post is public
2023-07-09 16:32:09 +08:00
Lim Chee Aun 470f7aa353 Experimental back button for status page 2023-07-09 09:12:29 +08:00
Lim Chee Aun db0261f8dd Not needed due to header-grid-2 2023-07-09 09:11:11 +08:00
Lim Chee Aun 44eef9ee3b Update instances list 2023-07-09 08:53:37 +08:00
Lim Chee Aun 41d1956ae5 Fix jumpy hero container height 2023-07-09 08:31:41 +08:00
Lim Chee Aun b02cae4967 Try use more system locale
Hopefully locale doesn't change half way
2023-07-08 13:43:25 +08:00
Lim Chee Aun 41fa08536e Link to non-alpha version of Trunks 2023-07-08 13:42:25 +08:00
Lim Chee Aun 7d793f19b3 Possible fix for 2-finger swipe-back not working 2023-07-08 13:42:09 +08:00
Lim Chee Aun 06c533cd47 Upgrade vite 2023-07-08 00:49:39 +08:00
Lim Chee Aun 471ab69182 Dangerously upgrade dependencies 2023-07-06 20:39:49 +08:00
Lim Chee Aun 5f67a29e1a Collapse follow requests if > 5 2023-07-06 20:32:21 +08:00
Lim Chee Aun d7a46ba0d6 Add link to Elk fork 2023-07-05 17:00:01 +08:00
Lim Chee Aun 2eba4eaf59 Prevent re-render timeline in multi-column mode 2023-07-05 16:59:28 +08:00
Lim Chee Aun e6880859ee Styles for search accounts results 2023-07-05 16:57:33 +08:00
Lim Chee Aun 97f7a066e2 Fix items not updating when items count = 0 2023-07-05 16:54:33 +08:00
Lim Chee Aun f67fdd5759 Show additional stats for accounts in search results 2023-07-02 18:02:30 +08:00
187 changed files with 32688 additions and 14290 deletions

7
.env
View file

@ -1,3 +1,4 @@
VITE_CLIENT_NAME=Phanpy
VITE_CLIENT_ID=social.phanpy
VITE_WEBSITE=https://phanpy.social
PHANPY_CLIENT_NAME=Phanpy
PHANPY_WEBSITE=https://phanpy.social
PHANPY_LINGVA_INSTANCES="lingva.phanpy.social lingva.lunar.icu lingva.garudalinux.org translate.plausibility.cloud"
PHANPY_PRIVACY_POLICY_URL="https://github.com/cheeaun/phanpy/blob/main/PRIVACY.MD"

View file

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

View file

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

32
.github/workflows/prodtag.yml vendored Normal file
View file

@ -0,0 +1,32 @@
name: Auto-create tag/release on every push to `production`
on:
push:
branches:
- production
jobs:
tag:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: production
# - run: git tag "`date +%Y.%m.%d`.`git rev-parse --short HEAD`" $(git rev-parse HEAD)
# - run: git push --tags
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm ci && npm run build
- run: cd dist && zip -r ../phanpy-dist.zip . && tar -czf ../phanpy-dist.tar.gz . && cd ..
- id: tag_name
run: echo ::set-output name=tag_name::$(date +%Y.%m.%d).$(git rev-parse --short HEAD)
- uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.tag_name.outputs.tag_name }}
generate_release_notes: true
files: |
phanpy-dist.zip
phanpy-dist.tar.gz

8
.gitignore vendored
View file

@ -24,4 +24,10 @@ dist-ssr
*.sw?
# Custom
.env.dev
.env.dev
phanpy-dist.zip
phanpy-dist.tar.gz
# Nix
.direnv
result

View file

@ -3,16 +3,20 @@
"useTabs": false,
"singleQuote": true,
"trailingComma": "all",
"plugins": ["@ianvs/prettier-plugin-sort-imports"],
"importOrder": [
"^[^.].*.css$",
"index.css$",
".css$",
"",
"./polyfills",
"",
"<THIRD_PARTY_MODULES>",
"",
"/assets/",
"",
"^../",
"",
"^[./]"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"importOrderGroupNamespaceSpecifiers": true,
"importOrderCaseInsensitive": true
]
}

View file

@ -6,6 +6,10 @@ Phanpy does not collect or process any personal information from its users. The
Phanpy is hosted on [Cloudflare Pages](https://pages.cloudflare.com/) as a static website. Read more about [Cloudflare's privacy policy](https://www.cloudflare.com/privacypolicy/).
## Translations
Phanpy uses [Lingva API](https://github.com/cheeaun/lingva-api) and [Lingva Translate](https://github.com/thedaviddelta/lingva-translate) as fallbacks for translating post content, profile bio and media description.
## Error logging
Phanpy dev site (*dev.phanpy.social*) uses [Rollbar](https://rollbar.com/) to log errors for debugging purposes. Read more about [Rollbar's privacy policy](https://rollbar.com/privacy/). The production site (*phanpy.social*) does not use error logging.

153
README.md
View file

@ -7,7 +7,7 @@ Phanpy
**Minimalistic opinionated Mastodon web client.**
</div>
<br>
![Fancy screenshot](readme-assets/fancy-screenshot.jpg)
**🗣️ Pronunciation**: [`/fænpi/`](https://ythi.net/how-do-you-pronounce/phanpy/english/) ([`FAN-pee`](https://www.smogon.com/forums/threads/the-official-name-pronunciation-guide.3474941/)) [🔊 Listen](https://www.youtube.com/watch?v=DIUbWe-ysJI)
@ -43,7 +43,7 @@ Everything is designed and engineered following my taste and vision. This is a p
- **Status actions (reply, boost, favourite, bookmark, etc) are hidden by default**.<br>They only appear in individual status page. This is to reduce clutter and distraction. It may result in lower engagement, but we're not chasing numbers here.
- **Boost is represented with the rocket icon**.<br>The green double arrow icon (retweet for Twitter) doesn't look right for the term "boost". Green rocket looks weird, so I use purple.
- **Short usernames (`@username`) are displayed in timelines, instead of the full account username (`@username@instance`)**.<br>Despite the [guideline](https://docs.joinmastodon.org/api/guidelines/#username) mentioned that "Decentralization must be transparent to the user", I don't think we should shove it to the face every single time. There are also some [screen-reader-related accessibility concerns](https://twitter.com/lifeofablindgrl/status/1595864647554502656) with the full username, though this web app is unfortunately not accessible yet.
- ~~**Short usernames (`@username`) are displayed in timelines, instead of the full account username (`@username@instance`)**.<br>Despite the [guideline](https://docs.joinmastodon.org/api/guidelines/#username) mentioned that "Decentralization must be transparent to the user", I don't think we should shove it to the face every single time. There are also some [screen-reader-related accessibility concerns](https://twitter.com/lifeofablindgrl/status/1595864647554502656) with the full username, though this web app is unfortunately not accessible yet.~~ DTTHDon fork displays the full username by default.
- **No autoplay for video/GIF/whatever in timeline**.<br>The timeline is already a huge mess with lots of people, brands, news and media trying to grab your attention. Let's not make it worse. (Current exception now would be animated emojis.)
- **Hash-based URLs**.<br>This web app is not meant to be a full-fledged replacement to Mastodon's existing front-end. There's no SEO, database, serverless or any long-running servers. I could be wrong one day.
@ -74,6 +74,18 @@ Everything is designed and engineered following my taste and vision. This is a p
- Limit up to 3 API requests as the root post may be very old or the thread is super long.
- If index number couldn't be found, badge will fallback to showing `Thread` without the number.
### Hashtag stuffing collapsing
![Hashtag stuffing collapsing](readme-assets/hashtag-stuffing-collapsing.jpg)
- First paragraph of post content with more than 3 hashtags will be collapsed to max 3 lines.
- Subsequent paragraphs after first paragraph with more than 3 hashtags will be collapsed to 1 line.
- Adjacent paragraphs with more than 1 hashtag after collapsed paragraphs will be collapsed to 1 line.
- If there are text around or between the hashtags, they will not be collapsed.
- Collapsed hashtags will be appended with `...` at the end.
- They are also slightly faded out to reduce visual noise.
- Opening the post view will reveal the hashtags uncollapsed.
### Filtered posts
- "Hide completely"-filtered posts will be hidden, with no UI to reveal it.
@ -81,7 +93,7 @@ Everything is designed and engineered following my taste and vision. This is a p
- Content can be partially revealed by hovering over the post, with tooltip showing the post text.
- Clicking it will open the Post page.
- Long-pressing or right-clicking it will "peek" the post with a bottom sheet UI.
- On boosts carousel, they are not partially hidden, but sorted to the end of the carousel.
- On boosts carousel, they are sorted to the end of the carousel.
## Development
@ -91,16 +103,9 @@ Prerequisites: Node.js 18+
- `npm run dev` - Start development server
- `npm run build` - Build for production
- `npm run preview` - Preview the production build
- `npm run fetch-instances` - Fetch instances list from [instances.social](https://instances.social/), save it to `src/data/instances.json`
- requires `.env.dev` file with `INSTANCES_SOCIAL_SECRET_TOKEN` variable set
- `npm run fetch-instances` - Fetch instances list from [joinmastodon.org/servers](https://joinmastodon.org/servers), save it to `src/data/instances.json`
- `npm run sourcemap` - Run `source-map-explorer` on the production build
## Self-hosting
This is a **pure static web app**. You can host it anywhere you want. Build it by running `npm run build` and serve the `dist` folder.
Try search for "how to self-host static sites" as there are many ways to do it.
## Tech stack
- [Vite](https://vitejs.dev/) - Build tool
@ -109,18 +114,125 @@ Try search for "how to self-host static sites" as there are many ways to do it.
- [React Router](https://reactrouter.com/) - Routing
- [masto.js](https://github.com/neet/masto.js/) - Mastodon API client
- [Iconify](https://iconify.design/) - Icon library
- Vanilla CSS - *Yes, I'm old school.*
- [MingCute icons](https://www.mingcute.com/)
- Vanilla CSS - _Yes, I'm old school._
Some of these may change in the future. The front-end world is ever-changing.
## Self-hosting
This is a **pure static web app**. You can host it anywhere you want.
Two ways (choose one):
### Easy way
Go to [Releases](https://github.com/cheeaun/phanpy/releases) and download the latest `phanpy-dist.zip` or `phanpy-dist.tar.gz`. It's pre-built so don't need to run any install/build commands. Extract it. Serve the folder of extracted files.
### Custom-build way
Requires [Node.js](https://nodejs.org/).
Download or `git clone` this repository. Use `production` branch for *stable* releases, `main` for *latest*. Build it by running `npm run build` (after `npm install`). Serve the `dist` folder.
Customization can be done by passing environment variables to the build command. Examples:
```bash
PHANPY_CLIENT_NAME="Phanpy Dev" \
PHANPY_WEBSITE="https://dev.phanpy.social" \
npm run build
```
```bash
PHANPY_DEFAULT_INSTANCE=hachyderm.io \
PHANPY_DEFAULT_INSTANCE_REGISTRATION_URL=https://hachyderm.io/auth/sign_up \
PHANPY_PRIVACY_POLICY_URL=https://hachyderm.io/privacy-policy \
npm run build
```
It's also possible to set them in the `.env` file.
Available variables:
- `PHANPY_CLIENT_NAME` (optional, default: `Phanpy`) affects:
- Web page title, shown in the browser window or tab title
- App title, when installed as PWA, shown in the Home screen, macOS dock, Windows taskbar, etc
- OpenGraph card title, when shared on social networks
- Client name, when [registering the app for authentication](https://docs.joinmastodon.org/client/token/#app) and shown as client used on posts in some apps/clients
- `PHANPY_WEBSITE` (optional but recommended, default: `https://phanpy.social`) affects:
- Canonical URL of the website
- OpenGraph card URL, when shared on social networks
- Root path for the OpenGraph card image
- Client URL, when [registering the app for authentication](https://docs.joinmastodon.org/client/token/#app) and shown as client used on posts in some apps/clients
- `PHANPY_DEFAULT_INSTANCE` (optional, no defaults):
- e.g. 'mastodon.social', without `https://`
- Default instance for log-in
- When logging in, the user will be redirected instantly to the instance's authentication page instead of having to manually type the instance URL and submit
- `PHANPY_DEFAULT_INSTANCE_REGISTRATION_URL` (optional, no defaults):
- URL of the instance registration page
- E.g. `https://mastodon.social/auth/sign_up`
- `PHANPY_PRIVACY_POLICY_URL` (optional, default to official instance's privacy policy):
- URL of the privacy policy page
- May specify the instance's own privacy policy
- `PHANPY_LINGVA_INSTANCES` (optional, space-separated list, default: `lingva.phanpy.social [...hard-coded list of fallback instances]`):
- Specify a space-separated list of instances. First will be used as default before falling back to the subsequent instances. If there's only 1 instance, means no fallback.
- May specify a self-hosted Lingva instance, powered by either [lingva-translate](https://github.com/thedaviddelta/lingva-translate) or [lingva-api](https://github.com/cheeaun/lingva-api)
- List of fallback instances hard-coded in `/.env`
- [↗️ List of lingva-translate instances](https://github.com/thedaviddelta/lingva-translate?tab=readme-ov-file#instances)
- `PHANPY_IMG_ALT_API_URL` (optional, no defaults):
- API endpoint for self-hosted instance of [img-alt-api](https://github.com/cheeaun/img-alt-api).
- If provided, a setting will appear for users to enable the image description generator in the composer. Disabled by default.
- `PHANPY_GIPHY_API_KEY` (optional, no defaults):
- API key for [GIPHY](https://developers.giphy.com/). See [API docs](https://developers.giphy.com/docs/api/).
- If provided, a setting will appear for users to enable the GIF picker in the composer. Disabled by default.
- This is not self-hosted.
### Static site hosting
Try online search for "how to self-host static sites" as there are many ways to do it.
#### Lingva-translate or lingva-api hosting
See documentation for [lingva-translate](https://github.com/thedaviddelta/lingva-translate) or [lingva-api](https://github.com/cheeaun/lingva-api).
## Community deployments
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.cz](https://phanpy.cz) by [@zdendys@mamutovo.cz](https://mamutovo.cz/@zdendys)
- [phanpy.social.tchncs.de](https://phanpy.social.tchncs.de) by [@milan@social.tchncs.de](https://social.tchncs.de/@milan)
> Note: Add yours by creating a pull request.
## Costs
Costs involved in running and developing this web app:
- Domain name (.social): **USD$23.18/year** (USD$6.87 1st year)
- Hosting: Free
- Development, design, maintenance: "Free" (My precious time)
## Mascot
[Phanpy](https://bulbapedia.bulbagarden.net/wiki/Phanpy_(Pok%C3%A9mon)) is a Ground-type Pokémon.
## Maintainers
## Maintainers + contributors
- [Chee Aun](https://github.com/cheeaun) ([Mastodon](https://mastodon.social/@cheeaun)) ([Twitter](https://twitter.com/cheeaun))
[![Contributors](https://contrib.rocks/image?repo=cheeaun/phanpy)](https://github.com/cheeaun/phanpy/graphs/contributors)
## Backstory
I am one of the earliest users of Twitter. Twitter was launched on [15 July 2006](https://en.wikipedia.org/wiki/Twitter). I joined on December 2006 and my [first tweet](https://twitter.com/cheeaun/status/1298723) was posted on 18 December 2006.
@ -135,16 +247,25 @@ And here I am. Building a Mastodon web client.
## Alternative web clients
- [Pinafore](https://pinafore.social/) ([retired](https://nolanlawson.com/2023/01/09/retiring-pinafore/)) → [Semaphore](https://semaphore.social/)
- Phanpy forks ↓
- [Agora](https://agorasocial.app/)
- [Pinafore](https://pinafore.social/) ([retired](https://nolanlawson.com/2023/01/09/retiring-pinafore/)) - forks ↓
- [Semaphore](https://semaphore.social/)
- [Enafore](https://enafore.social/)
- [Cuckoo+](https://www.cuckoo.social/)
- [Sengi](https://nicolasconstant.github.io/sengi/)
- [Soapbox](https://fe.soapbox.pub/)
- [Elk](https://elk.zone/)
- [Elk](https://elk.zone/) - forks ↓
- [elk.fedified.com](https://elk.fedified.com/)
- [Mastodeck](https://mastodeck.com/)
- [Trunks (alpha)](https://alpha.trunks.social/)
- [Trunks](https://trunks.social/)
- [Tooty](https://github.com/n1k0/tooty)
- [Litterbox](https://litterbox.koyu.space/)
- [Statuzer](https://statuzer.com/)
- [Tusked](https://tusked.app/)
- [Mastodon Glitch Edition (standalone frontend)](https://iceshrimp.dev/iceshrimp/masto-fe-standalone)
- [Mangane](https://github.com/BDX-town/Mangane)
- [TheDesk](https://github.com/cutls/TheDesk)
- [More...](https://github.com/hueyy/awesome-mastodon/#clients)
## 💁‍♂️ Notice to all other social media client developers

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Compose / %VITE_CLIENT_NAME%</title>
<title>Compose / %PHANPY_CLIENT_NAME%</title>
<meta name="color-scheme" content="dark light" />
<meta name="google" content="notranslate" />
</head>

Binary file not shown.

61
flake.lock Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1723175592,
"narHash": "sha256-M0xJ3FbDUc4fRZ84dPGx5VvgFsOzds77KiBMW/mMTnI=",
"owner": "nixOS",
"repo": "nixpkgs",
"rev": "5e0ca22929f3342b19569b21b2f3462f053e497b",
"type": "github"
},
"original": {
"owner": "nixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

60
flake.nix Normal file
View file

@ -0,0 +1,60 @@
{
inputs.nixpkgs.url = github:nixOS/nixpkgs/nixos-unstable;
inputs.flake-utils.url = github:numtide/flake-utils;
outputs = { self, nixpkgs, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
lib = pkgs.lib;
esbuild = pkgs.buildGoModule rec {
pname = "esbuild";
version = "0.21.5";
src = pkgs.fetchFromGitHub {
owner = "evanw";
repo = "esbuild";
rev = "v${version}";
hash = "sha256-FpvXWIlt67G8w3pBKZo/mcp57LunxDmRUaCU/Ne89B8=";
};
vendorHash = "sha256-+BfxCyg0KkDQpHt/wycy/8CTG6YBA/VJvJFhhzUnSiQ=";
subPackages = [ "cmd/esbuild" ];
ldflags = [ "-s" "-w" ];
meta.mainProgram = "esbuild";
};
in
rec {
packages.default = pkgs.buildNpmPackage {
pname = "dtth-phanpy";
version = "0.1.0";
nativeBuildInputs = with pkgs; [ git ];
ESBUILD_BINARY_PATH = lib.getExe esbuild;
src = lib.cleanSource ./.;
npmFlags = [ "--legacy-peer-deps" ];
npmDepsHash = "sha256-VROK9Emxi+jFqwidA/CUxQwxitKf7Y6mx0yuOCUwrzI=";
# npmDepsHash = lib.fakeHash;
# DTTH-specific env variables
PHANPY_CLIENT_NAME = "DTTH Phanpy";
PHANPY_CLIENT_ID = "ch.dtth.phanpy";
PHANPY_WEBSITE = "https://social.dtth.ch";
installPhase = ''
runHook preInstall
mkdir -p $out/lib
cp -r dist $out/lib/phanpy
runHook postInstall
'';
};
devShells.default = pkgs.mkShell {
inputsFrom = [ packages.default ];
buildInputs = with pkgs; [ nodejs ];
};
});
}

View file

@ -6,7 +6,7 @@
name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover"
/>
<title>%VITE_CLIENT_NAME%</title>
<title>%PHANPY_CLIENT_NAME%</title>
<meta
name="description"
content="Minimalistic opinionated Mastodon web client"
@ -14,18 +14,33 @@
<meta name="color-scheme" content="dark light" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="%VITE_CLIENT_NAME%" />
<meta name="apple-mobile-web-app-title" content="%PHANPY_CLIENT_NAME%" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="canonical" href="%VITE_WEBSITE%" />
<link rel="canonical" href="%PHANPY_WEBSITE%" />
<meta
name=""
data-theme-setting="manual"
content="#242526"
data-theme-light-color="#fff"
data-theme-light-color-temp="#ffff"
data-theme-dark-color="#242526"
data-theme-dark-color-temp="#242526ff"
/>
<meta
name="theme-color"
data-theme-setting="auto"
content="#fff"
data-content="#fff"
data-content-temp="#fffa"
media="(prefers-color-scheme: light)"
/>
<meta
name="theme-color"
data-theme-setting="auto"
content="#242526"
data-content="#242526"
data-content-temp="#242526aa"
media="(prefers-color-scheme: dark)"
/>
<meta name="google" content="notranslate" />
@ -33,13 +48,13 @@
<!-- Metacrap https://broken-links.com/2015/12/01/little-less-metacrap/ -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="og:url" content="%VITE_WEBSITE%" />
<meta property="og:title" content="%VITE_CLIENT_NAME%" />
<meta property="og:url" content="%PHANPY_WEBSITE%" />
<meta property="og:title" content="%PHANPY_CLIENT_NAME%" />
<meta
property="og:description"
content="Minimalistic opinionated Mastodon web client"
/>
<meta property="og:image" content="%VITE_WEBSITE%/og-image-2.jpg" />
<meta property="og:image" content="%PHANPY_WEBSITE%/og-image-2.jpg" />
</head>
<body>
<div id="app"></div>

11275
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,56 +6,64 @@
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"fetch-instances": "env $(cat .env.dev | grep -v \"#\" | xargs) node scripts/fetch-instances-list.js",
"sourcemap": "npx source-map-explorer dist/assets/*.js"
"fetch-instances": "env $(cat .env.local | grep -v \"#\" | xargs) node scripts/fetch-instances-list.js",
"sourcemap": "npx source-map-explorer dist/assets/*.js",
"bundle-visualizer": "npx vite-bundle-visualizer"
},
"dependencies": {
"@formatjs/intl-localematcher": "~0.4.0",
"@github/text-expander-element": "~2.5.0",
"@iconify-icons/mingcute": "~1.2.5",
"@formatjs/intl-localematcher": "~0.5.4",
"@formatjs/intl-segmenter": "~11.5.7",
"@formkit/auto-animate": "~0.8.2",
"@github/text-expander-element": "~2.7.1",
"@iconify-icons/mingcute": "~1.2.9",
"@justinribeiro/lite-youtube": "~1.5.0",
"@szhsin/react-menu": "~4.0.0",
"@uidotdev/usehooks": "~2.0.1",
"dayjs": "~1.11.8",
"@szhsin/react-menu": "~4.2.1",
"compare-versions": "~6.1.1",
"dayjs": "~1.11.12",
"dayjs-twitter": "~0.5.0",
"fast-blurhash": "~1.1.2",
"fast-deep-equal": "~3.1.3",
"fast-blurhash": "~1.1.4",
"fast-equals": "~5.0.1",
"fuse.js": "~7.0.0",
"html-prettify": "~1.0.7",
"idb-keyval": "~6.2.1",
"just-debounce-it": "~3.2.0",
"masto": "~5.11.3",
"mem": "~9.0.2",
"p-retry": "~5.1.2",
"p-throttle": "~5.1.0",
"preact": "~10.15.1",
"react-hotkeys-hook": "~4.4.0",
"react-intersection-observer": "~9.4.4",
"react-quick-pinch-zoom": "~4.9.0",
"lz-string": "~1.5.0",
"masto": "~6.8.0",
"moize": "~6.1.6",
"p-retry": "~6.2.0",
"p-throttle": "~6.1.0",
"preact": "~10.23.1",
"punycode": "~2.3.1",
"react-hotkeys-hook": "~4.5.0",
"react-intersection-observer": "~9.13.0",
"react-quick-pinch-zoom": "~5.1.0",
"react-router-dom": "6.6.2",
"string-length": "5.0.1",
"swiped-events": "~1.1.7",
"string-length": "6.0.0",
"swiped-events": "~1.2.0",
"tinyld": "~1.3.4",
"toastify-js": "~1.12.0",
"uid": "~2.0.2",
"use-debounce": "~9.0.4",
"use-long-press": "~3.1.5",
"use-debounce": "~10.0.2",
"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.5.0",
"@trivago/prettier-plugin-sort-imports": "~4.1.1",
"postcss": "~8.4.24",
"postcss-dark-theme-class": "~0.7.3",
"postcss-preset-env": "~8.5.0",
"@ianvs/prettier-plugin-sort-imports": "~4.3.1",
"@preact/preset-vite": "~2.9.0",
"postcss": "~8.4.40",
"postcss-dark-theme-class": "~1.3.0",
"postcss-preset-env": "~10.0.0",
"twitter-text": "~3.1.0",
"vite": "~4.3.9",
"vite-plugin-generate-file": "~0.0.4",
"vite": "~5.3.5",
"vite-plugin-generate-file": "~0.2.0",
"vite-plugin-html-config": "~1.0.11",
"vite-plugin-pwa": "~0.16.4",
"vite-plugin-remove-console": "~2.1.1",
"workbox-cacheable-response": "~7.0.0",
"workbox-expiration": "~7.0.0",
"workbox-routing": "~7.0.0",
"workbox-strategies": "~7.0.0"
"vite-plugin-pwa": "~0.20.1",
"vite-plugin-remove-console": "~2.2.0",
"workbox-cacheable-response": "~7.1.0",
"workbox-expiration": "~7.1.0",
"workbox-routing": "~7.1.0",
"workbox-strategies": "~7.1.0"
},
"postcss": {
"plugins": {
@ -67,6 +75,11 @@
}
}
},
"overrides": {
"vite": {
"rollup": ">=4.5.1"
}
},
"browserslist": [
"defaults",
"android >= 4"

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>

BIN
public/logo-badge-72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -9,13 +9,33 @@ import {
self.__WB_DISABLE_DEV_LOGS = true;
const assetsRoute = new Route(
({ request, sameOrigin }) => {
const isAsset =
request.destination === 'style' || request.destination === 'script';
const hasHash = /-[0-9a-f]{4,}\./i.test(request.url);
return sameOrigin && isAsset && hasHash;
},
new NetworkFirst({
cacheName: 'assets',
networkTimeoutSeconds: 5,
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
],
}),
);
registerRoute(assetsRoute);
const imageRoute = new Route(
({ request, sameOrigin }) => {
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',
@ -42,7 +62,7 @@ const iconsRoute = new Route(
cacheName: 'icons',
plugins: [
new ExpirationPlugin({
maxEntries: 50,
maxEntries: 300,
maxAgeSeconds: 3 * 24 * 60 * 60, // 3 days
purgeOnQuotaError: true,
}),
@ -76,6 +96,28 @@ const apiExtendedRoute = new RegExpRoute(
);
registerRoute(apiExtendedRoute);
// Note: expiration is not working as expected
// https://github.com/GoogleChrome/workbox/issues/3316
//
// const apiIntermediateRoute = new RegExpRoute(
// // Matches:
// // - trends/*
// // - timelines/link
// /^https?:\/\/[^\/]+\/api\/v\d+\/(trends|timelines\/link)/,
// new StaleWhileRevalidate({
// cacheName: 'api-intermediate',
// plugins: [
// new ExpirationPlugin({
// maxAgeSeconds: 1 * 60, // 1min
// }),
// new CacheableResponsePlugin({
// statuses: [0, 200],
// }),
// ],
// }),
// );
// registerRoute(apiIntermediateRoute);
const apiRoute = new RegExpRoute(
// Matches:
// - statuses/:id/context - some contexts are really huge
@ -94,3 +136,88 @@ const apiRoute = new RegExpRoute(
}),
);
registerRoute(apiRoute);
// PUSH NOTIFICATIONS
// ==================
self.addEventListener('push', (event) => {
const { data } = event;
if (data) {
const payload = data.json();
console.log('PUSH payload', payload);
const {
access_token,
title,
body,
icon,
notification_id,
notification_type,
preferred_locale,
} = payload;
if (!!navigator.setAppBadge) {
if (notification_type === 'mention') {
navigator.setAppBadge(1);
}
}
event.waitUntil(
self.registration.showNotification(title, {
body,
icon,
dir: 'auto',
badge: '/logo-badge-72.png',
lang: preferred_locale,
tag: notification_id,
timestamp: Date.now(),
data: {
access_token,
notification_type,
},
}),
);
}
});
self.addEventListener('notificationclick', (event) => {
const payload = event.notification;
console.log('NOTIFICATION CLICK payload', payload);
const { badge, body, data, dir, icon, lang, tag, timestamp, title } = payload;
const { access_token, notification_type } = data;
const url = `/#/notifications?id=${tag}&access_token=${btoa(access_token)}`;
event.waitUntil(
(async () => {
const clients = await self.clients.matchAll({
type: 'window',
includeUncontrolled: true,
});
console.log('NOTIFICATION CLICK clients 1', clients);
if (clients.length && 'navigate' in clients[0]) {
console.log('NOTIFICATION CLICK clients 2', clients);
const bestClient =
clients.find(
(client) => client.focused || client.visibilityState === 'visible',
) || clients[0];
console.log('NOTIFICATION CLICK navigate', url);
if (bestClient) {
console.log('NOTIFICATION CLICK postMessage', bestClient);
bestClient.focus();
bestClient.postMessage?.({
type: 'notification',
id: tag,
accessToken: access_token,
});
} else {
console.log('NOTIFICATION CLICK openWindow', url);
await self.clients.openWindow(url);
}
// }
} else {
console.log('NOTIFICATION CLICK openWindow', url);
await self.clients.openWindow(url);
}
await event.notification.close();
})(),
);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -1,24 +1,12 @@
import fs from 'fs';
const { INSTANCES_SOCIAL_SECRET_TOKEN } = process.env;
const params = new URLSearchParams({
count: 0,
min_users: 500,
sort_by: 'active_users',
sort_order: 'desc',
});
const url = `https://instances.social/api/1.0/instances/list?${params.toString()}`;
const results = await fetch(url, {
headers: {
Authorization: `Bearer ${INSTANCES_SOCIAL_SECRET_TOKEN}`,
},
});
const url = 'https://api.joinmastodon.org/servers';
const results = await fetch(url);
const json = await results.json();
const names = json.instances.map((instance) => instance.name);
const domains = json.map((instance) => instance.domain);
// Write to file
const path = './src/data/instances.json';
fs.writeFileSync(path, JSON.stringify(names, null, '\t'), 'utf8');
fs.writeFileSync(path, JSON.stringify(domains, null, '\t'), 'utf8');

View file

@ -1,7 +1,6 @@
// Fetch https://lingva.ml/api/v1/languages/{source|target}
import fs from 'fs';
fetch('https://lingva.ml/api/v1/languages/source')
fetch('https://lingva.phanpy.social/api/v1/languages/source')
.then((response) => response.json())
.then((json) => {
const file = './src/data/lingva-source-languages.json';
@ -9,7 +8,7 @@ fetch('https://lingva.ml/api/v1/languages/source')
fs.writeFileSync(file, JSON.stringify(json.languages, null, '\t'), 'utf8');
});
fetch('https://lingva.ml/api/v1/languages/target')
fetch('https://lingva.phanpy.social/api/v1/languages/target')
.then((response) => response.json())
.then((json) => {
const file = './src/data/lingva-target-languages.json';

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
import './app.css';
import debounce from 'just-debounce-it';
import {
useEffect,
useLayoutEffect,
@ -7,36 +8,32 @@ import {
useRef,
useState,
} from 'preact/hooks';
import {
matchPath,
Route,
Routes,
useLocation,
useNavigate,
useParams,
} from 'react-router-dom';
import 'swiped-events';
import { useSnapshot } from 'valtio';
import { matchPath, Route, Routes, useLocation } from 'react-router-dom';
import AccountSheet from './components/account-sheet';
import Compose from './components/compose';
import Drafts from './components/drafts';
import Icon from './components/icon';
import 'swiped-events';
import { subscribe } from 'valtio';
import BackgroundService from './components/background-service';
import ComposeButton from './components/compose-button';
import { ICONS } from './components/ICONS';
import KeyboardShortcutsHelp from './components/keyboard-shortcuts-help';
import Loader from './components/loader';
import MediaModal from './components/media-modal';
import Modal from './components/modal';
import Modals from './components/modals';
import NotificationService from './components/notification-service';
import SearchCommand from './components/search-command';
import Shortcuts from './components/shortcuts';
import ShortcutsSettings from './components/shortcuts-settings';
import NotFound from './pages/404';
import AccountStatuses from './pages/account-statuses';
import Accounts from './pages/accounts';
import Bookmarks from './pages/bookmarks';
import Catchup from './pages/catchup';
import Favourites from './pages/favourites';
import Filters from './pages/filters';
import FollowedHashtags from './pages/followed-hashtags';
import Following from './pages/following';
import Hashtag from './pages/hashtag';
import Home from './pages/home';
import HttpRoute from './pages/HttpRoute';
import HttpRoute from './pages/http-route';
import List from './pages/list';
import Lists from './pages/lists';
import Login from './pages/login';
@ -44,8 +41,7 @@ import Mentions from './pages/mentions';
import Notifications from './pages/notifications';
import Public from './pages/public';
import Search from './pages/search';
import Settings from './pages/settings';
import Status from './pages/status';
import StatusRoute from './pages/status-route';
import Trending from './pages/trending';
import Welcome from './pages/welcome';
import {
@ -56,38 +52,253 @@ import {
initPreferences,
} from './utils/api';
import { getAccessToken } from './utils/auth';
import openCompose from './utils/open-compose';
import showToast from './utils/show-toast';
import states, { getStatus, saveStatus } from './utils/states';
import focusDeck from './utils/focus-deck';
import states, { initStates, statusKey } from './utils/states';
import store from './utils/store';
import { getCurrentAccount } from './utils/store-utils';
import useInterval from './utils/useInterval';
import usePageVisibility from './utils/usePageVisibility';
import { getCurrentAccount, setCurrentAccountID } from './utils/store-utils';
import './utils/toast-alert';
window.__STATES__ = states;
window.__STATES_STATS__ = () => {
const keys = [
'statuses',
'accounts',
'spoilers',
'unfurledLinks',
'statusQuotes',
];
const counts = {};
keys.forEach((key) => {
counts[key] = Object.keys(states[key]).length;
});
console.warn('STATE stats', counts);
const { statuses } = states;
const unmountedPosts = [];
for (const key in statuses) {
const $post = document.querySelector(
`[data-state-post-id~="${key}"], [data-state-post-ids~="${key}"]`,
);
if (!$post) {
unmountedPosts.push(key);
}
}
console.warn('Unmounted posts', unmountedPosts.length, unmountedPosts);
};
// Experimental "garbage collection" for states
// Every 15 minutes
// Only posts for now
setInterval(() => {
if (!window.__IDLE__) return;
const { statuses, unfurledLinks, notifications } = states;
let keysCount = 0;
const { instance } = api();
for (const key in statuses) {
if (!window.__IDLE__) break;
try {
const $post = document.querySelector(
`[data-state-post-id~="${key}"], [data-state-post-ids~="${key}"]`,
);
const postInNotifications = notifications.some(
(n) => key === statusKey(n.status?.id, instance),
);
if (!$post && !postInNotifications) {
delete states.statuses[key];
delete states.statusQuotes[key];
for (const link in unfurledLinks) {
const unfurled = unfurledLinks[link];
const sKey = statusKey(unfurled.id, unfurled.instance);
if (sKey === key) {
delete states.unfurledLinks[link];
break;
}
}
keysCount++;
}
} catch (e) {}
}
if (keysCount) {
console.info(`GC: Removed ${keysCount} keys`);
}
}, 15 * 60 * 1000);
// Preload icons
// There's probably a better way to do this
// Related: https://github.com/vitejs/vite/issues/10600
setTimeout(() => {
for (const icon in ICONS) {
setTimeout(() => {
if (Array.isArray(ICONS[icon])) {
ICONS[icon][0]?.();
} else if (typeof ICONS[icon] === 'object') {
ICONS[icon].module?.();
} else {
ICONS[icon]?.();
}
}, 1);
}
}, 5000);
(() => {
window.__IDLE__ = true;
const nonIdleEvents = [
'mousemove',
'mousedown',
'resize',
'keydown',
'touchstart',
'pointerdown',
'pointermove',
'wheel',
];
const setIdle = () => {
window.__IDLE__ = true;
};
const IDLE_TIME = 3_000; // 3 seconds
const debouncedSetIdle = debounce(setIdle, IDLE_TIME);
const onNonIdle = () => {
window.__IDLE__ = false;
debouncedSetIdle();
};
nonIdleEvents.forEach((event) => {
window.addEventListener(event, onNonIdle, {
passive: true,
capture: true,
});
});
window.addEventListener('blur', setIdle, {
passive: true,
});
// When cursor leaves the window, set idle
document.documentElement.addEventListener(
'mouseleave',
(e) => {
if (!e.relatedTarget && !e.toElement) {
setIdle();
}
},
{
passive: true,
},
);
// document.addEventListener(
// 'visibilitychange',
// () => {
// if (document.visibilityState === 'visible') {
// onNonIdle();
// }
// },
// {
// passive: true,
// },
// );
})();
// Possible fix for iOS PWA theme-color bug
// It changes when loading web pages in "webview"
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
if (isIOS) {
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
const theme = store.local.get('theme');
let $meta;
if (theme) {
// Get current meta
$meta = document.querySelector(
`meta[name="theme-color"][data-theme-setting="manual"]`,
);
if ($meta) {
const color = $meta.content;
const tempColor =
theme === 'light'
? $meta.dataset.themeLightColorTemp
: $meta.dataset.themeDarkColorTemp;
$meta.content = tempColor || '';
setTimeout(() => {
$meta.content = color;
}, 10);
}
} else {
// Get current color scheme
const colorScheme = window.matchMedia('(prefers-color-scheme: dark)')
.matches
? 'dark'
: 'light';
// Get current theme-color
$meta = document.querySelector(
`meta[name="theme-color"][media*="${colorScheme}"]`,
);
if ($meta) {
const color = $meta.dataset.content;
const tempColor = $meta.dataset.contentTemp;
$meta.content = tempColor || '';
setTimeout(() => {
$meta.content = color;
}, 10);
}
}
}
});
}
{
const theme = store.local.get('theme');
// If there's a theme, it's NOT auto
if (theme) {
// dark | light
document.documentElement.classList.add(`is-${theme}`);
document
.querySelector('meta[name="color-scheme"]')
.setAttribute('content', theme || 'dark light');
// Enable manual theme <meta>
const $manualMeta = document.querySelector(
'meta[data-theme-setting="manual"]',
);
if ($manualMeta) {
$manualMeta.name = 'theme-color';
$manualMeta.content =
theme === 'light'
? $manualMeta.dataset.themeLightColor
: $manualMeta.dataset.themeDarkColor;
}
// Disable auto theme <meta>s
const $autoMetas = document.querySelectorAll(
'meta[data-theme-setting="auto"]',
);
$autoMetas.forEach((m) => {
m.name = '';
});
}
const textSize = store.local.get('textSize');
if (textSize) {
document.documentElement.style.setProperty('--text-size', `${textSize}px`);
}
}
subscribe(states, (changes) => {
for (const [action, path, value, prevValue] of changes) {
// Change #app dataset based on settings.shortcutsViewMode
if (path.join('.') === 'settings.shortcutsViewMode') {
const $app = document.getElementById('app');
if ($app) {
$app.dataset.shortcutsViewMode = states.shortcuts?.length ? value : '';
}
}
// Add/Remove cloak class to body
if (path.join('.') === 'settings.cloakMode') {
const $body = document.body;
$body.classList.toggle('cloak', value);
}
}
});
function App() {
const snapStates = useSnapshot(states);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [uiState, setUIState] = useState('loading');
const navigate = useNavigate();
useLayoutEffect(() => {
const theme = store.local.get('theme');
if (theme) {
document.documentElement.classList.add(`is-${theme}`);
document
.querySelector('meta[name="color-scheme"]')
.setAttribute('content', theme === 'auto' ? 'dark light' : theme);
}
const textSize = store.local.get('textSize');
if (textSize) {
document.documentElement.style.setProperty(
'--text-size',
`${textSize}px`,
);
}
}, []);
useEffect(() => {
const instanceURL = store.local.get('instanceURL');
@ -98,10 +309,15 @@ function App() {
if (code) {
console.log({ code });
// Clear the code from the URL
window.history.replaceState({}, document.title, '/');
window.history.replaceState(
{},
document.title,
window.location.pathname || '/',
);
const clientID = store.session.get('clientID');
const clientSecret = store.session.get('clientSecret');
const vapidKey = store.session.get('vapidKey');
(async () => {
setUIState('loading');
@ -112,27 +328,31 @@ function App() {
code,
});
const masto = initClient({ instance: instanceURL, accessToken });
const client = initClient({ instance: instanceURL, accessToken });
await Promise.allSettled([
initInstance(masto, instanceURL),
initAccount(masto, instanceURL, accessToken),
initPreferences(client),
initInstance(client, instanceURL),
initAccount(client, instanceURL, accessToken, vapidKey),
]);
initPreferences(masto);
initStates();
setIsLoggedIn(true);
setUIState('default');
})();
} else {
window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
const account = getCurrentAccount();
if (account) {
store.session.set('currentAccount', account.info.id);
const { masto, instance } = api({ account });
console.log('masto', masto);
initPreferences(masto);
setCurrentAccountID(account.info.id);
const { client } = api({ account });
const { instance } = client;
// console.log('masto', masto);
initStates();
setUIState('loading');
(async () => {
try {
await initInstance(masto, instance);
await initPreferences(client);
await initInstance(client, instance);
} catch (e) {
} finally {
setIsLoggedIn(true);
@ -147,47 +367,81 @@ function App() {
let location = useLocation();
states.currentLocation = location.pathname;
// useLayoutEffect(() => {
// states.currentLocation = location.pathname;
// }, [location.pathname]);
const focusDeck = () => {
let timer = setTimeout(() => {
const columns = document.getElementById('columns');
if (columns) {
// Focus first column
// columns.querySelector('.deck-container')?.focus?.();
} else {
const backDrop = document.querySelector('.deck-backdrop');
if (backDrop) return;
// Focus last deck
const pages = document.querySelectorAll('.deck-container');
const page = pages[pages.length - 1]; // last one
if (page && page.tabIndex === -1) {
console.log('FOCUS', page);
page.focus();
useEffect(focusDeck, [location, isLoggedIn]);
if (/\/https?:/.test(location.pathname)) {
return <HttpRoute />;
}
return (
<>
<PrimaryRoutes isLoggedIn={isLoggedIn} loading={uiState === 'loading'} />
<SecondaryRoutes isLoggedIn={isLoggedIn} />
{uiState === 'default' && (
<Routes>
<Route path="/:instance?/s/:id" element={<StatusRoute />} />
</Routes>
)}
{isLoggedIn && <ComposeButton />}
{isLoggedIn && <Shortcuts />}
<Modals />
{isLoggedIn && <NotificationService />}
<BackgroundService isLoggedIn={isLoggedIn} />
{uiState !== 'loading' && <SearchCommand onClose={focusDeck} />}
<KeyboardShortcutsHelp />
</>
);
}
function PrimaryRoutes({ isLoggedIn, loading }) {
const location = useLocation();
const nonRootLocation = useMemo(() => {
const { pathname } = location;
return !/^\/(login|welcome)/i.test(pathname);
}, [location]);
return (
<Routes location={nonRootLocation || location}>
<Route
path="/"
element={
isLoggedIn ? (
<Home />
) : loading ? (
<Loader id="loader-root" />
) : (
<Welcome />
)
}
}
}, 100);
return () => clearTimeout(timer);
};
useEffect(focusDeck, [location]);
const showModal =
snapStates.showCompose ||
snapStates.showSettings ||
snapStates.showAccounts ||
snapStates.showAccount ||
snapStates.showDrafts ||
snapStates.showMediaModal ||
snapStates.showShortcutsSettings;
useEffect(() => {
if (!showModal) focusDeck();
}, [showModal]);
/>
<Route path="/login" element={<Login />} />
<Route path="/welcome" element={<Welcome />} />
</Routes>
);
}
const { prevLocation } = snapStates;
const backgroundLocation = useRef(prevLocation || null);
const isModalPage =
matchPath('/:instance/s/:id', location.pathname) ||
matchPath('/s/:id', location.pathname);
function getPrevLocation() {
return states.prevLocation || null;
}
function SecondaryRoutes({ isLoggedIn }) {
// const snapStates = useSnapshot(states);
const location = useLocation();
// const prevLocation = snapStates.prevLocation;
const backgroundLocation = useRef(getPrevLocation());
const isModalPage = useMemo(() => {
return (
matchPath('/:instance/s/:id', location.pathname) ||
matchPath('/s/:id', location.pathname)
);
}, [location.pathname, matchPath]);
if (isModalPage) {
if (!backgroundLocation.current) backgroundLocation.current = prevLocation;
if (!backgroundLocation.current)
backgroundLocation.current = getPrevLocation();
} else {
backgroundLocation.current = null;
}
@ -196,337 +450,35 @@ function App() {
location,
});
if (/\/https?:/.test(location.pathname)) {
return <HttpRoute />;
}
const nonRootLocation = useMemo(() => {
const { pathname } = location;
return !/^\/(login|welcome)/.test(pathname);
}, [location]);
// Change #app dataset based on snapStates.settings.shortcutsViewMode
useEffect(() => {
const $app = document.getElementById('app');
if ($app) {
$app.dataset.shortcutsViewMode = snapStates.settings.shortcutsViewMode;
}
}, [snapStates.settings.shortcutsViewMode]);
// Add/Remove cloak class to body
useEffect(() => {
const $body = document.body;
$body.classList.toggle('cloak', snapStates.settings.cloakMode);
}, [snapStates.settings.cloakMode]);
return (
<>
<Routes location={nonRootLocation || location}>
<Route
path="/"
element={
isLoggedIn ? (
<Home />
) : uiState === 'loading' ? (
<Loader />
) : (
<Welcome />
)
}
/>
<Route path="/login" element={<Login />} />
<Route path="/welcome" element={<Welcome />} />
</Routes>
<Routes location={backgroundLocation.current || location}>
{isLoggedIn && (
<Routes location={backgroundLocation.current || location}>
{isLoggedIn && (
<>
<Route path="/notifications" element={<Notifications />} />
)}
{isLoggedIn && <Route path="/mentions" element={<Mentions />} />}
{isLoggedIn && <Route path="/following" element={<Following />} />}
{isLoggedIn && <Route path="/b" element={<Bookmarks />} />}
{isLoggedIn && <Route path="/f" element={<Favourites />} />}
{isLoggedIn && (
<Route path="/mentions" element={<Mentions />} />
<Route path="/following" element={<Following />} />
<Route path="/b" element={<Bookmarks />} />
<Route path="/f" element={<Favourites />} />
<Route path="/l">
<Route index element={<Lists />} />
<Route path=":id" element={<List />} />
</Route>
)}
{isLoggedIn && <Route path="/ft" element={<FollowedHashtags />} />}
<Route path="/:instance?/t/:hashtag" element={<Hashtag />} />
<Route path="/:instance?/a/:id" element={<AccountStatuses />} />
<Route path="/:instance?/p">
<Route index element={<Public />} />
<Route path="l" element={<Public local />} />
</Route>
<Route path="/:instance?/trending" element={<Trending />} />
<Route path="/:instance?/search" element={<Search />} />
{/* <Route path="/:anything" element={<NotFound />} /> */}
</Routes>
{uiState === 'default' && (
<Routes>
<Route path="/:instance?/s/:id" element={<StatusRoute />} />
</Routes>
<Route path="/fh" element={<FollowedHashtags />} />
<Route path="/ft" element={<Filters />} />
<Route path="/catchup" element={<Catchup />} />
</>
)}
{isLoggedIn && (
<button
type="button"
id="compose-button"
onClick={(e) => {
if (e.shiftKey) {
const newWin = openCompose();
if (!newWin) {
alert('Looks like your browser is blocking popups.');
states.showCompose = true;
}
} else {
states.showCompose = true;
}
}}
>
<Icon icon="quill" size="xl" alt="Compose" />
</button>
)}
{isLoggedIn &&
!snapStates.settings.shortcutsColumnsMode &&
snapStates.settings.shortcutsViewMode !== 'multi-column' && (
<Shortcuts />
)}
{!!snapStates.showCompose && (
<Modal>
<Compose
replyToStatus={
typeof snapStates.showCompose !== 'boolean'
? snapStates.showCompose.replyToStatus
: window.__COMPOSE__?.replyToStatus || null
}
editStatus={
states.showCompose?.editStatus ||
window.__COMPOSE__?.editStatus ||
null
}
draftStatus={
states.showCompose?.draftStatus ||
window.__COMPOSE__?.draftStatus ||
null
}
onClose={(results) => {
const { newStatus, instance } = results || {};
states.showCompose = false;
window.__COMPOSE__ = null;
if (newStatus) {
states.reloadStatusPage++;
showToast({
text: 'Post published. Check it out.',
delay: 1000,
duration: 10_000, // 10 seconds
onClick: (toast) => {
toast.hideToast();
states.prevLocation = location;
navigate(
instance
? `/${instance}/s/${newStatus.id}`
: `/s/${newStatus.id}`,
);
},
});
}
}}
/>
</Modal>
)}
{!!snapStates.showSettings && (
<Modal
onClick={(e) => {
if (e.target === e.currentTarget) {
states.showSettings = false;
}
}}
>
<Settings
onClose={() => {
states.showSettings = false;
}}
/>
</Modal>
)}
{!!snapStates.showAccounts && (
<Modal
onClick={(e) => {
if (e.target === e.currentTarget) {
states.showAccounts = false;
}
}}
>
<Accounts
onClose={() => {
states.showAccounts = false;
}}
/>
</Modal>
)}
{!!snapStates.showAccount && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
states.showAccount = false;
}
}}
>
<AccountSheet
account={snapStates.showAccount?.account || snapStates.showAccount}
instance={snapStates.showAccount?.instance}
onClose={({ destination }) => {
states.showAccount = false;
if (destination) {
states.showAccounts = false;
}
}}
/>
</Modal>
)}
{!!snapStates.showDrafts && (
<Modal
onClick={(e) => {
if (e.target === e.currentTarget) {
states.showDrafts = false;
}
}}
>
<Drafts onClose={() => (states.showDrafts = false)} />
</Modal>
)}
{!!snapStates.showMediaModal && (
<Modal
onClick={(e) => {
if (
e.target === e.currentTarget ||
e.target.classList.contains('media')
) {
states.showMediaModal = false;
}
}}
>
<MediaModal
mediaAttachments={snapStates.showMediaModal.mediaAttachments}
instance={snapStates.showMediaModal.instance}
index={snapStates.showMediaModal.index}
statusID={snapStates.showMediaModal.statusID}
onClose={() => {
states.showMediaModal = false;
}}
/>
</Modal>
)}
{!!snapStates.showShortcutsSettings && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
states.showShortcutsSettings = false;
}
}}
>
<ShortcutsSettings
onClose={() => (states.showShortcutsSettings = false)}
/>
</Modal>
)}
<BackgroundService isLoggedIn={isLoggedIn} />
</>
<Route path="/:instance?/t/:hashtag" element={<Hashtag />} />
<Route path="/:instance?/a/:id" element={<AccountStatuses />} />
<Route path="/:instance?/p">
<Route index element={<Public />} />
<Route path="l" element={<Public local />} />
</Route>
<Route path="/:instance?/trending" element={<Trending />} />
<Route path="/:instance?/search" element={<Search />} />
{/* <Route path="/:anything" element={<NotFound />} /> */}
</Routes>
);
}
function BackgroundService({ isLoggedIn }) {
// Notifications service
// - WebSocket to receive notifications when page is visible
const [visible, setVisible] = useState(true);
usePageVisibility(setVisible);
const notificationStream = useRef();
useEffect(() => {
if (isLoggedIn && visible) {
const { masto, instance } = api();
(async () => {
// 1. Get the latest notification
if (states.notificationsLast) {
const notificationsIterator = masto.v1.notifications.list({
limit: 1,
since_id: states.notificationsLast.id,
});
const { value: notifications } = await notificationsIterator.next();
if (notifications?.length) {
states.notificationsShowNew = true;
}
}
// 2. Start streaming
notificationStream.current = await masto.ws.stream(
'/api/v1/streaming',
{
stream: 'user:notification',
},
);
console.log('🎏 Streaming notification', notificationStream.current);
notificationStream.current.on('notification', (notification) => {
console.log('🔔🔔 Notification', notification);
if (notification.status) {
saveStatus(notification.status, instance, {
skipThreading: true,
});
}
states.notificationsShowNew = true;
});
notificationStream.current.ws.onclose = () => {
console.log('🔔🔔 Notification stream closed');
};
})();
}
return () => {
if (notificationStream.current) {
notificationStream.current.ws.close();
notificationStream.current = null;
}
};
}, [visible, isLoggedIn]);
// Check for updates service
const lastCheckDate = useRef();
const checkForUpdates = () => {
lastCheckDate.current = Date.now();
console.log('✨ Check app update');
fetch('./version.json')
.then((r) => r.json())
.then((info) => {
if (info) states.appVersion = info;
})
.catch((e) => {
console.error(e);
});
};
useInterval(checkForUpdates, visible && 1000 * 60 * 30); // 30 minutes
usePageVisibility((visible) => {
if (visible) {
if (!lastCheckDate.current) {
checkForUpdates();
} else {
const diff = Date.now() - lastCheckDate.current;
if (diff > 1000 * 60 * 60) {
// 1 hour
checkForUpdates();
}
}
}
});
return null;
}
function StatusRoute() {
const params = useParams();
const { id, instance } = params;
return <Status id={id} instance={instance} />;
}
export { App };

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 641 223">
<path fill="#aaa" d="M86 214c-9-1-17-4-24-8l-6-3-5-5-5-4-4-6-4-6-3-8-2-8v-27l2-9 3-9 4-6 4-6 5-5 5-5 7-3 6-4 7-2 7-2 12-1h12l7 1 8 2 7 4 7 3 5 5 5 4-10 10-10 9-4-3-10-5-5-1H88l-5 2-6 3-3 4-4 4-2 5-2 6v6l-1 7 1 7 2 7 3 5 2 4 4 3 4 3 5 2 6 2h9l10-1 5-2 6-3v-16H91v-27h59v54l-1 3-2 3-5 4-4 4-5 3-5 2-8 2-8 2-10 1H92l-6-1zm266-62V91h34v46h44V91h34v121h-34v-46h-44v46h-34v-61zm-182-1V90h34v121h-34v-60zm59-1V90h35l36 1 5 2c3 0 8 2 10 4l5 2 4 5 5 4 3 7 3 7 1 13v13l-4 6-3 7-4 4-5 5-5 2-5 3-6 2-5 1-18 1h-18v32h-34v-61zm67-2 3-2 2-4 2-5v-5l-2-4-2-4-3-2-3-3h-30v31h30l3-2zm226 39v-24l-8-12-18-28a1751 1751 0 0 0-20-31v-2h39l7 12 12 21 6 9 13-21 13-21h38v2l-41 61-7 10v48h-34v-24zM109 66l-4-1-5-5-5-4-1-5-3-9v-5l1-5c2-7 3-10 8-15l4-4 7-2 7-2h7l6 1 5 2 5 2 3 4 4 3 2 6 2 5v13l-2 5-2 6-4 4-3 3-5 2-4 2-9 1h-9l-5-2zm22-11 4-2 3-4 2-5V34l-2-4-2-4-3-2-4-3-5-1h-6l-4 2-5 2-2 4-3 5-1 3v4l1 5 2 5 2 2 5 3 4 2h10l4-2zM37 39V11h33l3 1 3 2 4 3 3 3 1 5 1 4v5l-1 4-3 4-3 5-4 1-3 2-11 1H49v16H37V39zm31 0 3-2 1-2 1-2v-4l-1-3-3-2-2-2H49v18h15l4-1zm107 25a512 512 0 0 0-19-53h14l4 14 6 19 1 4 1-1 7-19 5-17h9l6 19 7 18v-1l2-6 5-17 4-13h14v1l-4 12-16 41v2h-5l-5-1-6-15-6-15-1 1-3 7-6 15-2 8h-11l-1-3zm74-25V11h42v11h-29v2l-1 5v4h29v11h-28v11h2l15 1h13v11h-43V39zm55 0V11h33l5 3 5 2 2 4 2 5v10l-2 3-1 4-5 3-5 3 5 5 8 10 3 4h-14l-7-9-8-10h-9v19h-12V39zm33-3 2-3v-6l-3-3-2-3h-18v16h1v1h17l2-2zm26 3V11h42v11h-29l-1 6v5h29v11h-28v5l-1 5 1 1v1h30v11h-43V39zm54 0V11h17l18 1 4 2 5 3 2 4 3 4 2 6 1 6v5c-1 6-3 12-6 15l-3 4-5 3-5 2-17 1h-16V39zm33 14 5-5 2-3v-6l-1-6-1-3-1-3-4-3-3-2h-5l-6-1-3 1h-3v34h9l8-1 3-2zm50-14V11h34l5 2 4 2 2 3 2 3v9l-2 2-3 4-1 1 3 3 3 4 1 3 1 4-1 4-1 4-3 3-3 3-5 1-5 1h-31V39zm34 15 2-1v-6l-2-2-2-2h-20v13h20l2-2zm-3-22 4-2v-6l-2-1-2-2h-19v12h16l4-1zm42 24V45l-6-9-11-17-5-8h15l4 8 7 11 2 3 7-11 7-11h14l-11 16-11 17v23h-12V56z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -1,39 +1,71 @@
body.cloak a {
text-decoration-color: var(--link-color);
}
body.cloak,
.cloak {
a {
text-decoration-color: var(--link-color);
}
body.cloak .name-text,
body.cloak .name-text *,
body.cloak .status .content-container,
body.cloak .status .content-container *,
body.cloak .status .content-compact,
body.cloak .account-container :is(header, main > *:not(.actions)),
body.cloak .account-container :is(header, main > *:not(.actions)) *,
body.cloak .header-account,
body.cloak .account-block {
text-decoration-thickness: 1.1em;
text-decoration-line: line-through;
text-rendering: optimizeSpeed;
filter: opacity(0.5);
}
body.cloak .name-text *,
body.cloak .status .content-container *,
body.cloak .account-container :is(header, main > *:not(.actions)) * {
filter: none;
}
.name-text,
.name-text *,
.status .content-container,
.status .content-container *,
.status .content-compact > *,
.account-container .actions small,
.account-container :is(header, main > *:not(.actions)),
.account-container :is(header, main > *:not(.actions)) *,
.header-double-lines *,
.account-block,
.catchup-filters .filter-author *,
.post-peek-html *,
.post-peek-content > *,
.request-notifications-account *,
.status.compact-thread *,
.status .content-compact {
text-decoration-thickness: 1.1em;
text-decoration-line: line-through;
/* text-rendering: optimizeSpeed; */
filter: opacity(0.5);
}
.name-text *,
.status .content-container *,
.account-container :is(header, main > *:not(.actions)) *,
.post-peek-content > * {
filter: none;
}
body.cloak .status :is(img, video, audio),
body.cloak .avatar,
body.cloak .emoji,
body.cloak .header-banner {
filter: contrast(0) !important;
background-color: #000 !important;
.status :is(img, video, audio),
.media-post .media,
.avatar *,
.emoji,
.header-banner,
.post-peek-media {
filter: contrast(0) !important;
background-color: #000 !important;
}
}
/* 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 {
.header-double-lines *,
.account-container .profile-metadata b,
.account-container .actions small,
.account-container .stats *,
.media-container figcaption,
.media-container figcaption > *,
.catchup-filters .filter-author *,
.request-notifications-account * {
color: var(--text-color) !important;
}
.account-container .actions small,
.status .content-compact {
background-color: currentColor !important;
}
}

178
src/components/ICONS.jsx Normal file
View file

@ -0,0 +1,178 @@
export const ICONS = {
x: () => import('@iconify-icons/mingcute/close-line'),
heart: () => import('@iconify-icons/mingcute/heart-line'),
bookmark: () => import('@iconify-icons/mingcute/bookmark-line'),
'check-circle': () => import('@iconify-icons/mingcute/check-circle-line'),
'x-circle': () => import('@iconify-icons/mingcute/close-circle-line'),
transfer: () => import('@iconify-icons/mingcute/transfer-4-line'),
rocket: () => import('@iconify-icons/mingcute/rocket-line'),
'arrow-left': {
module: () => import('@iconify-icons/mingcute/arrow-left-line'),
rtl: true,
},
'arrow-right': {
module: () => import('@iconify-icons/mingcute/arrow-right-line'),
rtl: true,
},
'arrow-up': () => import('@iconify-icons/mingcute/arrow-up-line'),
'arrow-down': () => import('@iconify-icons/mingcute/arrow-down-line'),
earth: () => import('@iconify-icons/mingcute/earth-line'),
lock: () => import('@iconify-icons/mingcute/lock-line'),
unlock: () => import('@iconify-icons/mingcute/unlock-line'),
'eye-close': () => import('@iconify-icons/mingcute/eye-close-line'),
'eye-open': () => import('@iconify-icons/mingcute/eye-2-line'),
message: () => import('@iconify-icons/mingcute/mail-line'),
comment: {
module: () => import('@iconify-icons/mingcute/chat-3-line'),
rtl: true,
},
comment2: {
module: () => import('@iconify-icons/mingcute/comment-2-line'),
rtl: true,
},
home: () => import('@iconify-icons/mingcute/home-3-line'),
notification: () => import('@iconify-icons/mingcute/notification-line'),
follow: () => import('@iconify-icons/mingcute/user-follow-line'),
'follow-add': () => import('@iconify-icons/mingcute/user-add-line'),
poll: [() => import('@iconify-icons/mingcute/chart-bar-line'), '90deg'],
pencil: () => import('@iconify-icons/mingcute/pencil-line'),
quill: () => import('@iconify-icons/mingcute/quill-pen-line'),
at: () => import('@iconify-icons/mingcute/at-line'),
attachment: () => import('@iconify-icons/mingcute/attachment-line'),
upload: () => import('@iconify-icons/mingcute/upload-3-line'),
gear: () => import('@iconify-icons/mingcute/settings-3-line'),
more: () => import('@iconify-icons/mingcute/more-3-line'),
more2: () => import('@iconify-icons/mingcute/more-1-fill'),
external: {
module: () => import('@iconify-icons/mingcute/external-link-line'),
rtl: true,
},
popout: {
module: () => import('@iconify-icons/mingcute/external-link-line'),
rtl: true,
},
popin: {
module: () => import('@iconify-icons/mingcute/external-link-line'),
rotate: '180deg',
rtl: true,
},
plus: () => import('@iconify-icons/mingcute/add-circle-line'),
'chevron-left': {
module: () => import('@iconify-icons/mingcute/left-line'),
rtl: true,
},
'chevron-right': {
module: () => import('@iconify-icons/mingcute/right-line'),
rtl: true,
},
'chevron-down': () => import('@iconify-icons/mingcute/down-line'),
reply: {
module: () => import('@iconify-icons/mingcute/share-forward-line'),
rotate: '180deg',
flip: 'horizontal',
rtl: true,
},
thread: () => import('@iconify-icons/mingcute/route-line'),
group: {
module: () => import('@iconify-icons/mingcute/group-line'),
rtl: true,
},
bot: () => import('@iconify-icons/mingcute/android-2-line'),
menu: () => import('@iconify-icons/mingcute/rows-4-line'),
list: {
module: () => import('@iconify-icons/mingcute/list-check-line'),
rtl: true,
},
search: () => import('@iconify-icons/mingcute/search-2-line'),
hashtag: () => import('@iconify-icons/mingcute/hashtag-line'),
info: () => import('@iconify-icons/mingcute/information-line'),
shortcut: () => import('@iconify-icons/mingcute/lightning-line'),
user: () => import('@iconify-icons/mingcute/user-4-line'),
following: () => import('@iconify-icons/mingcute/walk-line'),
pin: () => import('@iconify-icons/mingcute/pin-line'),
unpin: [() => import('@iconify-icons/mingcute/pin-line'), '180deg'],
bus: () => import('@iconify-icons/mingcute/bus-2-line'),
link: () => import('@iconify-icons/mingcute/link-2-line'),
history: () => import('@iconify-icons/mingcute/history-line'),
share: () => import('@iconify-icons/mingcute/share-2-line'),
sparkles: () => import('@iconify-icons/mingcute/sparkles-line'),
sparkles2: () => import('@iconify-icons/mingcute/sparkles-2-line'),
exit: {
module: () => import('@iconify-icons/mingcute/exit-line'),
rtl: true,
},
translate: () => import('@iconify-icons/mingcute/translate-line'),
play: () => import('@iconify-icons/mingcute/play-fill'),
trash: () => import('@iconify-icons/mingcute/delete-2-line'),
mute: {
module: () => import('@iconify-icons/mingcute/volume-mute-line'),
rtl: true,
},
unmute: {
module: () => import('@iconify-icons/mingcute/volume-line'),
rtl: true,
},
block: () => import('@iconify-icons/mingcute/forbid-circle-line'),
unblock: [
() => import('@iconify-icons/mingcute/forbid-circle-line'),
'180deg',
],
flag: () => import('@iconify-icons/mingcute/flag-1-line'),
time: () => import('@iconify-icons/mingcute/time-line'),
refresh: () => import('@iconify-icons/mingcute/refresh-2-line'),
emoji2: () => import('@iconify-icons/mingcute/emoji-2-line'),
filter: () => import('@iconify-icons/mingcute/filter-2-line'),
filters: () => import('@iconify-icons/mingcute/filter-line'),
chart: () => import('@iconify-icons/mingcute/chart-line-line'),
react: () => import('@iconify-icons/mingcute/react-line'),
layout4: {
module: () => import('@iconify-icons/mingcute/layout-4-line'),
rtl: true,
},
layout5: () => import('@iconify-icons/mingcute/layout-5-line'),
announce: {
module: () => import('@iconify-icons/mingcute/announcement-line'),
rtl: true,
},
alert: () => import('@iconify-icons/mingcute/alert-line'),
round: () => import('@iconify-icons/mingcute/round-fill'),
'arrow-up-circle': () =>
import('@iconify-icons/mingcute/arrow-up-circle-line'),
'arrow-down-circle': () =>
import('@iconify-icons/mingcute/arrow-down-circle-line'),
clipboard: {
module: () => import('@iconify-icons/mingcute/clipboard-line'),
rtl: true,
},
'account-edit': () => import('@iconify-icons/mingcute/user-edit-line'),
'account-warning': () => import('@iconify-icons/mingcute/user-warning-line'),
keyboard: () => import('@iconify-icons/mingcute/keyboard-line'),
cloud: () => import('@iconify-icons/mingcute/cloud-line'),
month: {
module: () => import('@iconify-icons/mingcute/calendar-month-line'),
rtl: true,
},
media: () => import('@iconify-icons/mingcute/photo-album-line'),
speak: () => import('@iconify-icons/mingcute/radar-line'),
building: () => import('@iconify-icons/mingcute/building-5-line'),
history2: {
module: () => import('@iconify-icons/mingcute/history-2-line'),
rtl: true,
},
document: () => import('@iconify-icons/mingcute/document-line'),
'arrows-right': {
module: () => import('@iconify-icons/mingcute/arrows-right-line'),
rtl: true,
},
code: () => import('@iconify-icons/mingcute/code-line'),
copy: () => import('@iconify-icons/mingcute/copy-2-line'),
quote: {
module: () => import('@iconify-icons/mingcute/quote-left-line'),
rtl: true,
},
settings: () => import('@iconify-icons/mingcute/settings-6-line'),
'heart-break': () => import('@iconify-icons/mingcute/heart-crack-line'),
'user-x': () => import('@iconify-icons/mingcute/user-x-line'),
'user-setting': () => import('@iconify-icons/mingcute/user-setting-line'),
minimize: () => import('@iconify-icons/mingcute/arrows-down-line'),
};

View file

@ -4,6 +4,10 @@
gap: 8px;
color: var(--text-color);
text-decoration: none;
.account-block-acct {
display: inline-block;
}
}
.account-block:hover b {
text-decoration: underline;
@ -12,3 +16,57 @@
.account-block.skeleton {
color: var(--bg-faded-color);
}
.account-block .verified-field {
display: inline-flex;
align-items: baseline;
gap: 2px;
* {
-webkit-box-orient: vertical;
display: -webkit-box;
-webkit-line-clamp: 1;
line-clamp: 1;
text-overflow: ellipsis;
overflow: hidden;
unicode-bidi: isolate;
direction: initial;
}
a {
pointer-events: none;
color: color-mix(
in lch,
var(--green-color) 20%,
var(--text-insignificant-color) 80%
) !important;
}
.icon {
color: var(--green-color);
transform: translateY(1px);
}
.invisible {
display: none;
}
.ellipsis:after {
content: '…';
}
}
.account-block .account-block-stats {
line-height: 1.25;
margin-top: 2px;
font-size: 0.9em;
color: var(--text-insignificant-color);
display: flex;
flex-wrap: wrap;
align-items: center;
column-gap: 4px;
a {
color: inherit;
text-decoration: none;
}
}

View file

@ -1,22 +1,30 @@
import './account-block.css';
import { useNavigate } from 'react-router-dom';
// import { useNavigate } from 'react-router-dom';
import enhanceContent from '../utils/enhance-content';
import niceDateTime from '../utils/nice-date-time';
import shortenNumber from '../utils/shorten-number';
import states from '../utils/states';
import Avatar from './avatar';
import EmojiText from './emoji-text';
import Icon from './icon';
function AccountBlock({
skeleton,
account,
avatarSize = 'xl',
useAvatarStatic = false,
instance,
external,
internal,
onClick,
showActivity = false,
showStats = false,
accountInstance,
hideDisplayName = false,
relationship = {},
excludeRelationshipAttrs = [],
}) {
if (skeleton) {
return (
@ -25,13 +33,17 @@ function AccountBlock({
<span>
<b></b>
<br />
<span class="account-block-acct">@</span>
<span class="account-block-acct"></span>
</span>
</div>
);
}
const navigate = useNavigate();
if (!account) {
return null;
}
// const navigate = useNavigate();
const {
id,
@ -45,21 +57,44 @@ function AccountBlock({
statusesCount,
lastStatusAt,
bot,
fields,
note,
group,
followersCount,
createdAt,
locked,
} = account;
const [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct];
let [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct];
if (accountInstance) {
acct2 = `@${accountInstance}`;
}
const verifiedField = fields?.find((f) => !!f.verifiedAt && !!f.value);
const excludedRelationship = {};
for (const r in relationship) {
if (!excludeRelationshipAttrs.includes(r)) {
excludedRelationship[r] = relationship[r];
}
}
const hasRelationship =
excludedRelationship.following ||
excludedRelationship.followedBy ||
excludedRelationship.requested;
return (
<a
class="account-block"
href={url}
target={external ? '_blank' : null}
title={`@${acct}`}
title={acct2 ? acct : `@${acct}`}
onClick={(e) => {
if (external) return;
e.preventDefault();
if (onClick) return onClick(e);
if (internal) {
navigate(`/${instance}/a/${id}`);
// navigate(`/${instance}/a/${id}`);
location.hash = `/${instance}/a/${id}`;
} else {
states.showAccount = {
account,
@ -68,37 +103,113 @@ function AccountBlock({
}
}}
>
<Avatar url={avatar} size={avatarSize} squircle={bot} />
<span>
{displayName ? (
<b>
<EmojiText text={displayName} emojis={emojis} />
</b>
) : (
<b>{username}</b>
)}
<br />
<span class="account-block-acct">
@{acct1}
<Avatar
url={useAvatarStatic ? avatarStatic : avatar || avatarStatic}
size={avatarSize}
squircle={bot}
/>
<span class="account-block-content">
{!hideDisplayName && (
<>
{displayName ? (
<b>
<EmojiText text={displayName} emojis={emojis} />
</b>
) : (
<b>{username}</b>
)}
</>
)}{' '}
<span class="account-block-acct bidi-isolate">
{acct2 ? '' : '@'}
{acct1}
<wbr />
{acct2}
{locked && (
<>
{' '}
<Icon icon="lock" size="s" alt="Locked" />
</>
)}
</span>
{showActivity && (
<>
<br />
<small class="last-status-at insignificant">
Posts: {statusesCount}
{!!lastStatusAt && (
<>
{' '}
&middot; Last posted:{' '}
{niceDateTime(lastStatusAt, {
hideTime: true,
})}
</>
<div class="account-block-stats">
Posts: {shortenNumber(statusesCount)}
{!!lastStatusAt && (
<>
{' '}
&middot; Last posted:{' '}
{niceDateTime(lastStatusAt, {
hideTime: true,
})}
</>
)}
</div>
)}
{showStats && (
<div class="account-block-stats">
{bot && (
<>
<span class="tag collapsed">
<Icon icon="bot" /> Automated
</span>
</>
)}
{!!group && (
<>
<span class="tag collapsed">
<Icon icon="group" /> Group
</span>
</>
)}
{hasRelationship && (
<div key={relationship.id} class="shazam-container-horizontal">
<div class="shazam-container-inner">
{excludedRelationship.following &&
excludedRelationship.followedBy ? (
<span class="tag minimal">Mutual</span>
) : excludedRelationship.requested ? (
<span class="tag minimal">Requested</span>
) : excludedRelationship.following ? (
<span class="tag minimal">Following</span>
) : excludedRelationship.followedBy ? (
<span class="tag minimal">Follows you</span>
) : null}
</div>
</div>
)}
{!!followersCount && (
<span class="ib">
{shortenNumber(followersCount)}{' '}
{followersCount === 1 ? 'follower' : 'followers'}
</span>
)}
{!!verifiedField && (
<span class="verified-field">
<Icon icon="check-circle" size="s" />{' '}
<span
dangerouslySetInnerHTML={{
__html: enhanceContent(verifiedField.value, { emojis }),
}}
/>
</span>
)}
{!bot &&
!group &&
!hasRelationship &&
!followersCount &&
!verifiedField &&
!!createdAt && (
<span class="created-at">
Joined{' '}
<time datetime={createdAt}>
{niceDateTime(createdAt, {
hideTime: true,
})}
</time>
</span>
)}
</small>
</>
</div>
)}
</span>
</a>

View file

@ -1,16 +1,152 @@
.account-container {
display: flex;
flex-direction: column;
overflow: hidden;
/* display: flex; */
/* flex-direction: column; */
/* overflow: hidden; */
overflow-y: auto;
max-width: 100%;
--banner-overlap: 44px;
--posting-stats-size: 8px;
--original-color: var(--link-color);
.note {
font-size: 0.95em;
line-height: 1.4;
text-wrap: pretty;
margin-bottom: 16px;
&:empty {
display: none;
}
> *:first-child {
margin-top: 0;
padding-top: 0;
}
> *:last-child {
margin-bottom: 0;
padding-bottom: 0;
}
&:not(:has(p)):not(:empty) {
/* Some notes don't have <p> tags, so we need to add some padding */
padding: 1em 0;
}
}
.posting-stats {
font-size: 90%;
color: var(--text-insignificant-color);
background-color: var(--bg-faded-color);
padding: 8px 12px;
&:is(:hover, :focus-within) {
background-color: var(--link-bg-hover-color);
}
}
.posting-stats-bar {
--gap: 0.5px;
--gap-color: var(--outline-color);
height: var(--posting-stats-size);
border-radius: var(--posting-stats-size);
overflow: hidden;
margin: 8px 0;
box-shadow: inset 0 0 0 1px var(--outline-color),
inset 0 0 0 1.5px var(--bg-blur-color);
background-color: var(--bg-color);
background-repeat: no-repeat;
animation: swoosh-bg-image 0.3s ease-in-out 0.3s both;
background-image: linear-gradient(
var(--to-forward),
var(--original-color) 0%,
var(--original-color) calc(var(--originals-percentage) - var(--gap)),
var(--gap-color) calc(var(--originals-percentage) - var(--gap)),
var(--gap-color) calc(var(--originals-percentage) + var(--gap)),
var(--reply-to-color) calc(var(--originals-percentage) + var(--gap)),
var(--reply-to-color) calc(var(--replies-percentage) - var(--gap)),
var(--gap-color) calc(var(--replies-percentage) - var(--gap)),
var(--gap-color) calc(var(--replies-percentage) + var(--gap)),
var(--reblog-color) calc(var(--replies-percentage) + var(--gap)),
var(--reblog-color) 100%
);
}
.posting-stats-legends {
font-size: 12px;
text-transform: uppercase;
}
.posting-stats-legend-item {
display: inline-block;
width: var(--posting-stats-size);
height: var(--posting-stats-size);
border-radius: var(--posting-stats-size);
background-color: var(--text-insignificant-color);
vertical-align: middle;
margin: 0 4px 2px;
/* border: 1px solid var(--outline-color); */
box-shadow: inset 0 0 0 1px var(--outline-color),
inset 0 0 0 1.5px var(--bg-blur-color);
&.posting-stats-legend-item-originals {
background-color: var(--original-color);
}
&.posting-stats-legend-item-replies {
background-color: var(--reply-to-color);
}
&.posting-stats-legend-item-boosts {
background-color: var(--reblog-color);
}
}
}
.account-container.skeleton {
color: var(--outline-color);
}
.account-container .account-moved {
animation: fade-in 0.3s both ease-in-out 0.3s;
padding: 16px;
background-color: var(--bg-color);
position: absolute;
top: 8px;
inset-inline: 8px;
z-index: 3;
border: 1px solid var(--outline-color);
box-shadow: 0 8px 16px var(--drop-shadow-color);
border-radius: calc(16px - 8px);
overflow: hidden;
p {
margin: 0 0 8px;
padding: 0;
}
.account-block {
background-color: var(--bg-faded-color);
padding: 8px;
border-radius: 8px;
border: 1px solid var(--link-faded-color);
&:hover {
background-color: var(--link-bg-hover-color);
border-color: var(--link-color);
}
b {
color: var(--link-color);
}
}
~ * {
/* pointer-events: none; */
filter: grayscale(0.75) opacity(0.75);
}
}
.account-container .header-banner {
/* pointer-events: none; */
vertical-align: top;
aspect-ratio: 6 / 1;
width: 100%;
height: auto;
@ -35,7 +171,7 @@
hsla(0, 0%, 0%, 0.013) 95.3%,
hsla(0, 0%, 0%, 0) 100%
);
margin-bottom: -44px;
margin-bottom: calc(-1 * var(--banner-overlap));
user-select: none;
-webkit-user-drag: none;
opacity: 0;
@ -45,8 +181,8 @@
opacity: 1;
}
.sheet .account-container .header-banner {
border-top-left-radius: 16px;
border-top-right-radius: 16px;
border-start-start-radius: 16px;
border-start-end-radius: 16px;
}
.account-container .header-banner.header-is-avatar {
mask-image: linear-gradient(
@ -76,18 +212,26 @@
}
.account-container .header-banner:active {
mask-image: none;
}
.account-container .header-banner:active + header .avatar + * {
transition: opacity 0.3s ease-in-out;
opacity: 0 !important;
}
.account-container .header-banner:active + header .avatar {
transition: filter 0.3s ease-in-out;
filter: none !important;
}
.account-container .header-banner:active + header .avatar img {
transition: border-radius 0.3s ease-in-out;
border-radius: 8px;
& + header {
background-image: none;
}
& + header .avatar + * {
transition: opacity 0.3s ease-in-out;
opacity: 0 !important;
}
&,
& + header .avatar {
transition: filter 0.3s ease-in-out;
filter: none !important;
}
& + header .avatar img {
transition: border-radius 0.3s ease-in-out;
border-radius: 8px;
}
}
@media (min-height: 480px) {
@ -125,35 +269,140 @@
animation: fade-in 0.3s both ease-in-out 0.2s;
}
.account-container .note {
font-size: 95%;
line-height: 1.4;
.account-container .account-block .account-block-acct {
display: block;
opacity: 0.7;
}
.account-container .note:not(:has(p)):not(:empty) {
/* Some notes don't have <p> tags, so we need to add some padding */
padding: 1em 0;
.private-note-tag {
z-index: 1;
appearance: none;
display: inline-block;
color: var(--private-note-text-color);
background-color: var(--private-note-bg-color);
border: 1px solid var(--private-note-border-color);
padding: 4px;
line-height: normal;
font-size: smaller;
border-radius: 0;
align-self: center !important;
/* clip a dog ear on top right */
clip-path: polygon(0 0, calc(100% - 4px) 0, 100% 4px, 100% 100%, 0 100%);
&:dir(rtl) {
/* top left */
clip-path: polygon(4px 0, 100% 0, 100% 100%, 0 100%, 0 4px);
}
/* 4x4px square on top right */
background-size: 4px 4px;
background-repeat: no-repeat;
background-position: top right;
&:dir(rtl) {
background-position: top left;
}
background-image: linear-gradient(
to bottom,
var(--private-note-border-color),
var(--private-note-border-color)
);
transition: transform 0.15s ease-in-out;
overflow-wrap: anywhere;
span {
color: inherit;
opacity: 0.75;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
display: box;
-webkit-box-orient: vertical;
box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
text-align: start;
}
&:hover:not(:active) {
filter: none !important;
transform: rotate(-0.5deg) scale(1.05);
span {
opacity: 1;
}
}
}
.account-container .private-note {
font-size: 90%;
color: var(--text-insignificant-color);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
padding: 12px;
background-color: var(--bg-faded-color);
display: flex;
gap: 0.5em;
align-items: center;
b {
font-size: 90%;
text-transform: uppercase;
}
p {
margin: 0;
padding: 0;
}
}
.account-container .stats {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
gap: 16px;
opacity: 0.75;
/* flex-wrap: wrap; */
column-gap: 24px;
row-gap: 8px;
/* opacity: 0.75; */
font-size: 90%;
background-color: var(--bg-faded-color);
padding: 12px;
border-radius: 16px;
/* border-radius: 16px; */
line-height: 1.25;
overflow-x: auto !important;
justify-content: flex-start;
position: relative;
[tabindex='0']:is(:hover, :focus) {
color: var(--text-color);
cursor: pointer;
text-decoration-color: var(--text-insignificant-color);
}
.stats-avatars-bunch {
animation: appear 1s both ease-in-out;
> *:not(:first-child) {
margin: 0;
margin-inline-start: -4px;
}
}
}
.timeline-start .account-container .stats {
flex-wrap: wrap;
}
.account-container .stats > * {
text-align: center;
/* text-align: center; */
flex-shrink: 0;
display: flex;
gap: 0.5em;
}
.account-container .stats a {
.account-container .stats a:not(.insignificant) {
color: inherit;
}
.account-container .stats a:hover {
color: inherit;
}
.account-container footer {
padding: 0 16px 16px;
}
.account-container .actions {
/* margin-block: 8px; */
display: flex;
gap: 8px;
justify-content: space-between;
@ -161,18 +410,47 @@
align-items: center;
}
.account-container .actions button {
align-self: flex-end;
/* align-self: flex-end; */
}
.account-container .actions .buttons {
display: flex;
align-items: center;
}
.account-container .account-metadata-box {
overflow: hidden;
border-radius: 16px;
display: block;
text-decoration: none;
& > * {
margin-bottom: 2px;
border-radius: 4px;
overflow: hidden;
}
&:has(+ .account-metadata-box) {
border-end-start-radius: 4px;
border-end-end-radius: 4px;
}
+ .account-metadata-box {
border-start-start-radius: 4px;
border-start-end-radius: 4px;
border-end-start-radius: 16px;
border-end-end-radius: 16px;
}
}
.account-container .profile-metadata {
display: flex;
flex-wrap: wrap;
/* flex-wrap: wrap; */
gap: 2px;
border-radius: 16px;
overflow: hidden;
overflow-x: auto;
}
.timeline-start .account-container .profile-metadata {
flex-wrap: wrap;
}
.account-container .profile-field {
min-width: 0;
@ -183,6 +461,15 @@
border-radius: 4px;
filter: saturate(0.75);
line-height: 1.25;
flex-shrink: 0;
max-width: calc(100% - 12px - 2em);
}
.account-container .profile-field:only-child {
max-width: 100%;
}
.timeline-start .account-container .profile-field {
flex-shrink: 1;
max-width: 100%;
}
.account-container :is(.note, .profile-field) .invisible {
@ -204,67 +491,210 @@
margin: 0;
}
.account-container .common-followers p {
.account-container .common-followers {
font-size: 90%;
color: var(--text-insignificant-color);
border-top: 1px solid var(--outline-color);
border-bottom: 1px solid var(--outline-color);
padding: 8px 0;
background-color: var(--bg-faded-color);
padding: 8px 12px;
margin: 0;
}
.timeline-start .account-container {
border-bottom: 1px solid var(--outline-color);
position: relative;
}
.timeline-start .account-container header {
padding: 16px 16px 1px;
padding: 16px;
animation: none;
}
.timeline-start .account-container main {
padding: 1px 16px 1px;
padding: 1px 16px 16px;
}
.timeline-start .account-container main > * {
animation: none;
}
.timeline-start .account-container .account-block .account-block-acct {
opacity: 0.5;
.faux-header-bg {
display: none;
}
@keyframes shine {
0% {
left: -100%;
@keyframes bye-banner {
20% {
filter: blur(0) opacity(1);
}
100% {
left: 100%;
filter: blur(16px) opacity(0.2);
}
}
.timeline-start .account-container {
position: relative;
overflow: hidden;
@keyframes surface-header {
0% {
border-bottom-color: transparent;
box-shadow: none;
}
100% {
border-bottom-color: var(--outline-color);
box-shadow: 0 8px 16px -8px var(--drop-shadow-color);
}
}
.timeline-start .account-container:before {
content: '';
position: absolute;
z-index: 2;
width: 100%;
height: 100%;
background-image: linear-gradient(
100deg,
rgba(255, 255, 255, 0) 30%,
rgba(255, 255, 255, 0.25),
rgba(255, 255, 255, 0) 70%
);
top: 0;
left: -100%;
pointer-events: none;
@keyframes shrink-avatar {
0% {
width: 64px;
height: 64px;
}
100% {
width: 2.5em;
height: 2.5em;
}
}
@media (prefers-color-scheme: dark) {
.timeline-start .account-container:before {
.sheet .account-container {
border-radius: 16px 16px 0 0;
overflow-x: hidden;
max-height: 75vh;
overscroll-behavior: none;
scroll-timeline: --account-scroll;
header {
padding-bottom: 16px;
position: sticky;
top: 0;
z-index: 2;
background-image: linear-gradient(
to bottom,
transparent 30%,
var(--bg-color) var(--banner-overlap),
var(--bg-color) calc(100% - 8px),
transparent
);
.account-block-content {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
line-clamp: 3;
-webkit-line-clamp: 3;
}
}
.faux-header-bg {
display: block;
height: var(--banner-overlap);
position: sticky;
top: 0;
z-index: 1;
background-color: var(--bg-color);
margin-top: calc(-1 * var(--banner-overlap));
}
@supports (animation-timeline: scroll()) {
.header-banner:not(.header-is-avatar):not(:hover):not(:active) {
animation: bye-banner 1s linear both;
animation-timeline: view();
animation-range: contain 100% cover 100%;
}
header {
background-image: linear-gradient(
to bottom,
transparent 30%,
var(--bg-color) var(--banner-overlap)
);
border-bottom: 1px solid transparent;
animation: surface-header 1s linear both;
animation-timeline: --account-scroll;
animation-range: 0 150px;
}
header .avatar {
animation: shrink-avatar 1s linear both;
animation-timeline: --account-scroll;
animation-range: 0 150px;
}
}
main {
/* margin-top: -8px; */
padding-top: 1px;
padding-bottom: 16px;
}
footer {
min-height: calc(40px + 16px);
animation: slide-up 0.3s ease-out 0.3s both;
position: sticky;
bottom: 0;
background-color: var(--bg-faded-blur-color);
backdrop-filter: blur(16px) saturate(3);
padding: 8px 16px;
border-top: var(--hairline-width) solid var(--outline-color);
padding-bottom: max(8px, env(safe-area-inset-bottom));
box-shadow: 0 -8px 16px -8px var(--drop-shadow-color);
}
}
@keyframes swoosh-bg-image {
0% {
background-position: -320px 0;
opacity: 0.25;
}
100% {
background-position: 0 0;
opacity: 1;
}
}
.timeline-start .account-container:hover:before {
animation: shine 1s ease-in-out 1s;
.account-container .posting-stats-button {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
color: inherit;
background-color: var(--bg-faded-color);
padding: 8px 12px;
font-size: 90%;
color: var(--text-insignificant-color);
line-height: 1;
vertical-align: text-top;
border-radius: 4px;
&:is(:hover, :focus-within) {
color: var(--text-color);
background-color: var(--link-bg-hover-color);
filter: none !important;
}
.loader-container {
margin: 0;
opacity: 0.5;
transform: scale(0.75);
}
}
@keyframes wobble {
0% {
transform: rotate(-4deg);
}
100% {
transform: rotate(4deg);
}
}
@keyframes loading-spin {
0% {
transform: rotate(0deg) scale(0.75);
}
100% {
transform: rotate(360deg) scale(0.75);
}
}
.posting-stats-icon {
display: inline-block;
width: 24px;
height: 8px;
filter: opacity(0.75);
animation: wobble 2s linear both infinite alternate !important;
&.loading {
animation: loading-spin 0.35s linear both infinite !important;
}
}
#list-add-remove-container .list-add-remove {
@ -297,6 +727,7 @@
@media (min-width: 40em) {
.timeline-start .account-container {
--banner-overlap: 77px;
--item-radius: 16px;
border: 1px solid var(--divider-color);
margin: 16px 0;
@ -313,12 +744,12 @@
var(--shadow-offset) var(--shadow-offset) var(--shadow-blur)
var(--shadow-spread) var(--header-color-2, var(--drop-shadow-color));
}
.timeline-start .account-container .header-banner {
/* .timeline-start .account-container .header-banner {
margin-bottom: -77px;
}
} */
.timeline-start .account-container header .account-block {
font-size: 175%;
margin-bottom: -8px;
/* margin-bottom: -8px; */
line-height: 1.1;
letter-spacing: -0.5px;
mix-blend-mode: multiply;
@ -331,3 +762,135 @@
drop-shadow(8px 0 8px var(--header-color-4, --bg-color));
}
}
#private-note-container {
textarea {
margin-top: 8px;
width: 100%;
resize: vertical;
height: 33vh;
min-height: 25vh;
max-height: 50vh;
color: var(--private-note-text-color);
background-color: var(--private-note-bg-color);
border: 1px solid var(--private-note-border-color);
box-shadow: 0 2px 8px var(--drop-shadow-color);
border-radius: 0;
padding: 16px;
}
footer {
display: flex;
justify-content: space-between;
padding: 8px 0;
* {
vertical-align: middle;
}
}
}
#edit-profile-container {
p {
margin-block: 8px;
}
label {
input,
textarea {
display: block;
width: 100%;
}
textarea {
resize: vertical;
min-height: 5em;
max-height: 50vh;
}
}
table {
width: 100%;
th {
text-align: start;
color: var(--text-insignificant-color);
font-weight: normal;
font-size: 0.8em;
text-transform: uppercase;
}
tbody tr td:first-child {
width: 40%;
}
input {
width: 100%;
}
}
footer {
display: flex;
justify-content: space-between;
padding: 8px 0;
* {
vertical-align: middle;
}
}
}
.handle-info {
.handle-handle {
display: inline-block;
margin-block: 5px;
b {
font-weight: 600;
padding: 2px 4px;
border-radius: 4px;
display: inline-block;
box-shadow: 0 0 0 5px var(--bg-blur-color);
&.handle-username {
color: var(--orange-fg-color);
background-color: var(--orange-bg-color);
}
&.handle-server {
color: var(--purple-fg-color);
background-color: var(--purple-bg-color);
}
}
}
.handle-at {
display: inline-block;
margin-inline: -3px;
position: relative;
z-index: 1;
}
.handle-legend {
margin-top: 0.25em;
}
.handle-legend-icon {
overflow: hidden;
display: inline-block;
width: 14px;
height: 14px;
border: 4px solid transparent;
border-radius: 8px;
background-clip: padding-box;
&.username {
background-color: var(--orange-fg-color);
border-color: var(--orange-bg-color);
}
&.server {
background-color: var(--purple-fg-color);
border-color: var(--purple-bg-color);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
import { useEffect } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import { api } from '../utils/api';
import states from '../utils/states';
import useLocationChange from '../utils/useLocationChange';
import AccountInfo from './account-info';
import Icon from './icon';
@ -11,26 +11,25 @@ function AccountSheet({ account, instance: propInstance, onClose }) {
const { masto, instance, authenticated } = api({ instance: propInstance });
const isString = typeof account === 'string';
const escRef = useHotkeys('esc', onClose, [onClose]);
useEffect(() => {
if (!isString) {
states.accounts[`${account.id}@${instance}`] = account;
}
}, [account]);
useLocationChange(onClose);
return (
<div
ref={escRef}
class="sheet"
onClick={(e) => {
const accountBlock = e.target.closest('.account-block');
if (accountBlock) {
onClose({
destination: 'account-statuses',
});
}
}}
// onClick={(e) => {
// const accountBlock = e.target.closest('.account-block');
// if (accountBlock) {
// onClose({
// destination: 'account-statuses',
// });
// }
// }}
>
{!!onClose && (
<button type="button" class="sheet-close outer" onClick={onClose}>
@ -50,7 +49,7 @@ function AccountSheet({ account, instance: propInstance, onClose }) {
});
return info;
} catch (e) {
const result = await masto.v2.search({
const result = await masto.v2.search.fetch({
q: account,
type: 'accounts',
limit: 1,
@ -58,6 +57,22 @@ function AccountSheet({ account, instance: propInstance, onClose }) {
});
if (result.accounts.length) {
return result.accounts[0];
} else if (/https?:\/\/[^/]+\/@/.test(account)) {
const accountURL = URL.parse(account);
const { hostname, pathname } = accountURL;
const acct =
pathname.replace(/^\//, '').replace(/\/$/, '') +
'@' +
hostname;
const result = await masto.v2.search.fetch({
q: acct,
type: 'accounts',
limit: 1,
resolve: authenticated,
});
if (result.accounts.length) {
return result.accounts[0];
}
}
}
} else {

View file

@ -8,23 +8,31 @@
box-shadow: 0 0 0 1px var(--bg-blur-color);
flex-shrink: 0;
vertical-align: middle;
}
.avatar.has-alpha {
border-radius: 0;
}
.avatar:not(.has-alpha).squircle {
border-radius: 25%;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
background-color: var(--img-bg-color);
}
&.has-alpha {
border-radius: 0;
background-color: transparent;
box-shadow: none;
.avatar[data-loaded],
.avatar[data-loaded] img {
box-shadow: none;
background-color: transparent;
img {
background-color: transparent;
}
}
&:not(.has-alpha).squircle {
border-radius: 25%;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
background-color: var(--img-bg-color);
contain: none;
}
&[data-loaded],
&[data-loaded] img {
box-shadow: none;
background-color: transparent;
}
}

View file

@ -2,6 +2,8 @@ import './avatar.css';
import { useRef } from 'preact/hooks';
import mem from '../utils/mem';
const SIZES = {
s: 16,
m: 20,
@ -16,7 +18,10 @@ const alphaCache = {};
const canvas = window.OffscreenCanvas
? new OffscreenCanvas(1, 1)
: document.createElement('canvas');
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext('2d', {
willReadFrequently: true,
});
ctx.imageSmoothingEnabled = false;
function Avatar({ url, size, alt = '', squircle, ...props }) {
size = SIZES[size] || size || SIZES.m;
@ -58,29 +63,32 @@ function Avatar({ url, size, alt = '', squircle, ...props }) {
if (avatarRef.current) avatarRef.current.dataset.loaded = true;
if (alphaCache[url] !== undefined) return;
if (isMissing) return;
try {
// Check if image has alpha channel
const { width, height } = e.target;
if (canvas.width !== width) canvas.width = width;
if (canvas.height !== height) canvas.height = height;
ctx.drawImage(e.target, 0, 0);
const allPixels = ctx.getImageData(0, 0, width, height);
// At least 10% of pixels have alpha <= 128
const hasAlpha =
allPixels.data.filter((pixel, i) => i % 4 === 3 && pixel <= 128)
.length /
(allPixels.data.length / 4) >
0.1;
if (hasAlpha) {
// console.log('hasAlpha', hasAlpha, allPixels.data);
avatarRef.current.classList.add('has-alpha');
setTimeout(() => {
try {
// Check if image has alpha channel
const { width, height } = e.target;
if (canvas.width !== width) canvas.width = width;
if (canvas.height !== height) canvas.height = height;
ctx.drawImage(e.target, 0, 0);
const allPixels = ctx.getImageData(0, 0, width, height);
// At least 10% of pixels have alpha <= 128
const hasAlpha =
allPixels.data.filter(
(pixel, i) => i % 4 === 3 && pixel <= 128,
).length /
(allPixels.data.length / 4) >
0.1;
if (hasAlpha) {
// console.log('hasAlpha', hasAlpha, allPixels.data);
avatarRef.current.classList.add('has-alpha');
}
alphaCache[url] = hasAlpha;
ctx.clearRect(0, 0, width, height);
} catch (e) {
// Silent fail
alphaCache[url] = false;
}
alphaCache[url] = hasAlpha;
ctx.clearRect(0, 0, width, height);
} catch (e) {
// Silent fail
alphaCache[url] = false;
}
}, 1);
}}
/>
)}
@ -88,4 +96,4 @@ function Avatar({ url, size, alt = '', squircle, ...props }) {
);
}
export default Avatar;
export default mem(Avatar);

View file

@ -0,0 +1,142 @@
import { memo } from 'preact/compat';
import { useEffect, useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import { api } from '../utils/api';
import showToast from '../utils/show-toast';
import states, { saveStatus } from '../utils/states';
import useInterval from '../utils/useInterval';
import usePageVisibility from '../utils/usePageVisibility';
const STREAMING_TIMEOUT = 1000 * 3; // 3 seconds
const POLL_INTERVAL = 20_000; // 20 seconds
export default memo(function BackgroundService({ isLoggedIn }) {
// Notifications service
// - WebSocket to receive notifications when page is visible
const [visible, setVisible] = useState(true);
usePageVisibility(setVisible);
const checkLatestNotification = async (masto, instance, skipCheckMarkers) => {
if (states.notificationsLast) {
const notificationsIterator = masto.v1.notifications.list({
limit: 1,
sinceId: states.notificationsLast.id,
});
const { value: notifications } = await notificationsIterator.next();
if (notifications?.length) {
if (skipCheckMarkers) {
states.notificationsShowNew = true;
} else {
let lastReadId;
try {
const markers = await masto.v1.markers.fetch({
timeline: 'notifications',
});
lastReadId = markers?.notifications?.lastReadId;
} catch (e) {}
if (lastReadId) {
states.notificationsShowNew = notifications[0].id !== lastReadId;
} else {
states.notificationsShowNew = true;
}
}
}
}
};
useEffect(() => {
let sub;
let streamTimeout;
let pollNotifications;
if (isLoggedIn && visible) {
const { masto, streaming, instance } = api();
(async () => {
// 1. Get the latest notification
await checkLatestNotification(masto, instance);
let hasStreaming = false;
// 2. Start streaming
if (streaming) {
streamTimeout = setTimeout(() => {
(async () => {
try {
hasStreaming = true;
sub = streaming.user.notification.subscribe();
console.log('🎏 Streaming notification', sub);
for await (const entry of sub) {
if (!sub) break;
if (!visible) break;
console.log('🔔🔔 Notification entry', entry);
if (entry.event === 'notification') {
console.log('🔔🔔 Notification', entry);
saveStatus(entry.payload, instance, {
skipThreading: true,
});
}
states.notificationsShowNew = true;
}
console.log('💥 Streaming notification loop STOPPED');
} catch (e) {
hasStreaming = false;
console.error(e);
}
if (!hasStreaming) {
console.log('🎏 Streaming failed, fallback to polling');
pollNotifications = setInterval(() => {
checkLatestNotification(masto, instance, true);
}, POLL_INTERVAL);
}
})();
}, STREAMING_TIMEOUT);
}
})();
}
return () => {
sub?.unsubscribe?.();
sub = null;
clearTimeout(streamTimeout);
clearInterval(pollNotifications);
};
}, [visible, isLoggedIn]);
// Check for updates service
const lastCheckDate = useRef();
const checkForUpdates = () => {
lastCheckDate.current = Date.now();
console.log('✨ Check app update');
fetch('./version.json')
.then((r) => r.json())
.then((info) => {
if (info) states.appVersion = info;
})
.catch((e) => {
console.error(e);
});
};
useInterval(checkForUpdates, visible && 1000 * 60 * 30); // 30 minutes
usePageVisibility((visible) => {
if (visible) {
if (!lastCheckDate.current) {
checkForUpdates();
} else {
const diff = Date.now() - lastCheckDate.current;
if (diff > 1000 * 60 * 60) {
// 1 hour
checkForUpdates();
}
}
}
});
// Global keyboard shortcuts "service"
useHotkeys('shift+alt+k', () => {
const currentCloakMode = states.settings.cloakMode;
states.settings.cloakMode = !currentCloakMode;
showToast({
text: `Cloak mode ${currentCloakMode ? 'disabled' : 'enabled'}`,
});
});
return null;
});

View file

@ -9,6 +9,7 @@ import List from '../pages/list';
import Mentions from '../pages/mentions';
import Notifications from '../pages/notifications';
import Public from '../pages/public';
import Search from '../pages/search';
import Trending from '../pages/trending';
import states from '../utils/states';
import useTitle from '../utils/useTitle';
@ -18,6 +19,8 @@ function Columns() {
const snapStates = useSnapshot(states);
const { shortcuts } = snapStates;
console.debug('RENDER Columns', shortcuts);
const components = shortcuts.map((shortcut) => {
if (!shortcut) return null;
const { type, ...params } = shortcut;
@ -31,9 +34,16 @@ function Columns() {
hashtag: Hashtag,
mentions: Mentions,
trending: Trending,
search: Search,
}[type];
if (!Component) return null;
return <Component {...params} />;
// Don't show Search column with no query, for now
if (type === 'search' && !params.query) return null;
// Don't show List column with no list, for now
if (type === 'list' && !params.id) return null;
return (
<Component key={type + JSON.stringify(params)} {...params} columnMode />
);
});
useHotkeys(['1', '2', '3', '4', '5', '6', '7', '8', '9'], (e, handler) => {
@ -45,7 +55,24 @@ function Columns() {
}
});
return <div id="columns">{components}</div>;
return (
<div
id="columns"
onContextMenu={(e) => {
// If right-click on header, but not links or buttons
if (
e.target.closest('.deck > header') &&
!e.target.closest('a') &&
!e.target.closest('button')
) {
e.preventDefault();
states.showShortcutsSettings = true;
}
}}
>
{components}
</div>
);
}
export default Columns;

View file

@ -0,0 +1,51 @@
import { useHotkeys } from 'react-hotkeys-hook';
import { useSnapshot } from 'valtio';
import openCompose from '../utils/open-compose';
import openOSK from '../utils/open-osk';
import states from '../utils/states';
import Icon from './icon';
export default function ComposeButton() {
const snapStates = useSnapshot(states);
function handleButton(e) {
if (snapStates.composerState.minimized) {
states.composerState.minimized = false;
openOSK();
return;
}
if (e.shiftKey) {
const newWin = openCompose();
if (!newWin) {
states.showCompose = true;
}
} else {
openOSK();
states.showCompose = true;
}
}
useHotkeys('c, shift+c', handleButton, {
ignoreEventWhen: (e) => {
const hasModal = !!document.querySelector('#modal-container > *');
return hasModal;
},
});
return (
<button
type="button"
id="compose-button"
onClick={handleButton}
class={`${snapStates.composerState.minimized ? 'min' : ''} ${
snapStates.composerState.publishing ? 'loading' : ''
} ${snapStates.composerState.publishingError ? 'error' : ''}`}
>
<Icon icon="quill" size="xl" alt="Compose" />
</button>
);
}

View file

@ -0,0 +1,48 @@
import { shouldPolyfill } from '@formatjs/intl-segmenter/should-polyfill';
import { useEffect, useState } from 'preact/hooks';
import Loader from './loader';
const supportsIntlSegmenter = !shouldPolyfill();
function importIntlSegmenter() {
if (!supportsIntlSegmenter) {
return import('@formatjs/intl-segmenter/polyfill-force').catch(() => {});
}
}
function importCompose() {
return import('./compose');
}
export async function preload() {
try {
await importIntlSegmenter();
importCompose();
} catch (e) {
console.error(e);
}
}
export default function ComposeSuspense(props) {
const [Compose, setCompose] = useState(null);
useEffect(() => {
(async () => {
try {
if (supportsIntlSegmenter) {
const component = await importCompose();
setCompose(component);
} else {
await importIntlSegmenter();
const component = await importCompose();
setCompose(component);
}
} catch (e) {
console.error(e);
}
})();
}, []);
return Compose?.default ? <Compose.default {...props} /> : <Loader />;
}

View file

@ -16,7 +16,6 @@
}
#compose-container .compose-top {
text-align: right;
display: flex;
justify-content: space-between;
gap: 8px;
@ -25,28 +24,19 @@
position: sticky;
top: 0;
z-index: 100;
white-space: nowrap;
}
#compose-container textarea {
width: 100%;
max-width: 100%;
height: 5em;
min-height: 5em;
max-height: 50vh;
resize: vertical;
line-height: 1.4;
border-color: transparent;
}
#compose-container textarea:hover {
border-color: var(--divider-color);
}
@media (min-width: 40em) {
#compose-container textarea {
font-size: 150%;
font-size: calc(100% + 50% / var(--text-weight));
max-height: 65vh;
}
#compose-container .compose-top .account-block {
text-align: start;
pointer-events: none;
overflow: hidden;
color: var(--text-insignificant-color);
line-height: 1.1;
font-size: 90%;
background-color: var(--bg-faded-blur-color);
backdrop-filter: blur(16px);
padding-inline-end: 1em;
border-radius: 9999px;
}
@keyframes appear-up {
@ -71,7 +61,7 @@
box-shadow: 0 -3px 12px -3px var(--drop-shadow-color);
}
#compose-container .status-preview:has(.status-badge:not(:empty)) {
border-top-right-radius: 8px;
border-start-end-radius: 8px;
}
#compose-container .status-preview :is(.content-container, .time) {
pointer-events: none;
@ -104,6 +94,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);
@ -116,19 +110,45 @@
}
#compose-container form {
border-radius: 16px;
padding: 4px 12px;
--form-padding-inline: 8px;
--form-padding-block: 0;
/* border-radius: 16px; */
padding: var(--form-padding-block) var(--form-padding-inline);
background-color: var(--bg-blur-color);
/* background-image: linear-gradient(var(--bg-color) 85%, transparent); */
position: relative;
z-index: 2;
--drop-shadow: 0 3px 6px -3px var(--drop-shadow-color);
box-shadow: var(--drop-shadow);
@media (min-width: 40em) {
border-radius: 16px;
}
}
#compose-container .status-preview ~ form {
box-shadow: var(--drop-shadow), 0 -3px 6px -3px var(--drop-shadow-color);
}
#compose-container textarea {
width: 100%;
max-width: 100%;
height: 5em;
min-height: 5em;
max-height: 50vh;
resize: vertical;
line-height: 1.4;
border-color: transparent;
&.compose-field {
@media (min-width: 40em) {
max-height: 65vh;
}
}
}
#compose-container textarea:hover {
border-color: var(--divider-color);
}
#compose-container .toolbar {
display: flex;
justify-content: space-between;
@ -192,10 +212,11 @@
padding: 0 0 0 8px;
margin: 0;
appearance: none;
line-height: 1em;
}
#compose-container .toolbar-button:not(.show-field) select {
right: 0;
left: auto !important;
inset-inline-end: 0;
inset-inline-start: auto !important;
}
#compose-container
.toolbar-button:not(:disabled):is(
@ -256,6 +277,15 @@
gap: 8px;
align-items: center;
font-size: 90%;
.grow {
flex-grow: 1;
}
.count {
font-size: 80%;
opacity: 0.5;
}
}
#compose-container .text-expander-menu li b img {
/* The shortcode emojis */
@ -267,19 +297,28 @@
height: 2.2em;
}
#compose-container .text-expander-menu li:is(:hover, :focus, [aria-selected]) {
color: var(--bg-color);
background-color: var(--link-color);
}
#compose-container
.text-expander-menu:hover
li[aria-selected]:not(:hover, :focus) {
background-color: var(--link-bg-color);
color: var(--text-color);
background-color: var(--bg-color);
}
#compose-container .text-expander-menu li[aria-selected] {
box-shadow: inset 4px 0 0 0 var(--button-bg-color);
:dir(rtl) & {
box-shadow: inset -4px 0 0 0 var(--button-bg-color);
}
}
#compose-container .text-expander-menu li[data-more] {
&:not(:hover, :focus, [aria-selected]) {
color: var(--text-insignificant-color);
background-color: var(--bg-faded-color);
}
font-size: 0.8em;
justify-content: center;
}
#compose-container .form-visibility-direct {
--yellow-stripes: repeating-linear-gradient(
-45deg,
135deg,
var(--reply-to-faded-color),
var(--reply-to-faded-color) 10px,
var(--reply-to-faded-color) 10px,
@ -303,6 +342,21 @@
display: flex;
gap: 8px;
align-items: stretch;
.media-error {
padding: 2px;
color: var(--orange-fg-color);
background-color: transparent;
border: 1.5px dashed transparent;
line-height: 1;
border-radius: 4px;
display: flex;
&:is(:hover, :focus) {
background-color: var(--bg-color);
border-color: var(--orange-fg-color);
}
}
}
#compose-container .media-preview {
flex-shrink: 0;
@ -326,7 +380,7 @@
#compose-container .media-preview > * {
width: 80px;
height: 80px;
object-fit: contain;
object-fit: scale-down;
vertical-align: middle;
pointer-events: none;
}
@ -442,14 +496,14 @@
display: flex;
gap: 4px;
align-items: center;
border-left: 1px solid var(--outline-color);
padding-left: 8px;
border-inline-start: 1px solid var(--outline-color);
padding-inline-start: 8px;
}
#compose-container .expires-in {
flex-grow: 1;
border-left: 1px solid var(--outline-color);
padding-left: 8px;
border-inline-start: 1px solid var(--outline-color);
padding-inline-start: 8px;
display: flex;
gap: 4px;
flex-wrap: wrap;
@ -469,12 +523,51 @@
}
}
@media (min-width: 480px) {
#compose-container button[type='submit'] {
#compose-container button[type='submit'] {
border-radius: 8px;
@media (min-width: 480px) {
padding-inline: 24px;
}
}
@keyframes breathe {
0% {
opacity: 1;
}
40% {
opacity: 0.4;
}
100% {
opacity: 1;
}
}
#media-sheet {
.media-form {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
min-height: 50vh;
textarea {
flex-grow: 1;
resize: none;
width: 100%;
/* height: 10em; */
&.loading {
animation: skeleton-breathe 1.5s linear infinite;
}
}
footer {
display: flex;
justify-content: space-between;
align-items: center;
}
}
}
#media-sheet main {
padding-top: 8px;
display: flex;
@ -482,10 +575,6 @@
flex: 1;
gap: 8px;
}
#media-sheet textarea {
width: 100%;
height: 10em;
}
#media-sheet .media-preview {
border: 2px solid var(--outline-color);
border-radius: 8px;
@ -502,12 +591,13 @@
linear-gradient(-45deg, transparent 75%, var(--img-bg-color) 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
flex: 0.8;
}
#media-sheet .media-preview > * {
width: 100%;
height: 100%;
max-height: 50vh;
object-fit: contain;
object-fit: scale-down;
vertical-align: middle;
}
@ -521,49 +611,455 @@
#media-sheet .media-preview > * {
max-height: none;
}
#media-sheet textarea {
/* #media-sheet textarea {
flex: 1;
min-height: 100%;
height: auto;
} */
}
#mention-sheet {
height: 50vh;
.accounts-list {
--list-gap: 1px;
list-style: none;
margin: 0;
padding: 8px 0;
display: flex;
flex-direction: column;
row-gap: var(--list-gap);
&.loading {
opacity: 0.5;
}
li {
display: flex;
flex-grow: 1;
/* align-items: center; */
margin: 0 -8px;
padding: 8px;
gap: 8px;
position: relative;
justify-content: space-between;
border-radius: 8px;
/* align-items: center; */
&:hover {
background-image: linear-gradient(
var(--to-forward),
transparent 75%,
var(--link-bg-color)
);
}
&.selected {
background-image: linear-gradient(
var(--to-forward),
var(--bg-faded-color) 75%,
var(--link-bg-color)
);
}
&:before {
content: '';
display: block;
border-top: var(--hairline-width) solid var(--divider-color);
position: absolute;
bottom: 0;
inset-inline-start: 58px;
inset-inline-end: 0;
}
&:has(+ li:is(.selected, :hover)):before,
&:is(.selected, :hover):before {
opacity: 0;
}
> button {
border-radius: 4px;
&:hover {
outline: 2px solid var(--button-bg-blur-color);
}
}
}
}
}
#custom-emojis-sheet {
max-height: 50vh;
max-height: 50dvh;
header {
.loader-container {
margin: 0;
}
form {
margin: 8px 0 0;
input {
width: 100%;
min-width: 0;
}
}
}
main {
mask-image: none;
min-height: 40vh;
padding-bottom: 88px;
}
.custom-emojis-matches {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-wrap: wrap;
}
.custom-emojis-list {
.section-header {
font-size: 80%;
text-transform: uppercase;
color: var(--text-insignificant-color);
padding: 8px 0 4px;
position: sticky;
top: 0;
background-color: var(--bg-color);
z-index: 1;
}
section {
display: flex;
flex-wrap: wrap;
}
button {
color: var(--text-color);
border-radius: 8px;
background-image: radial-gradient(
closest-side,
var(--img-bg-color),
transparent
);
text-shadow: 0 1px 0 var(--bg-color);
position: relative;
min-width: 44px;
min-height: 44px;
font-variant-numeric: slashed-zero;
font-feature-settings: 'ss01';
&[data-title]:after {
max-width: 50vw;
pointer-events: none;
position: absolute;
content: attr(data-title);
left: 50%;
top: 0;
background-color: var(--bg-color);
padding: 2px 4px;
border-radius: 4px;
font-size: 12px;
border: 1px solid var(--text-color);
transform: translate(-50%, -110%);
opacity: 0;
transition: opacity 0.1s ease-out 0.1s;
font-family: var(--monospace-font);
line-height: 1;
}
&.edge-left[data-title]:after {
left: 0;
transform: translate(0, -110%);
}
&.edge-right[data-title]:after {
left: 100%;
transform: translate(-100%, -110%);
}
&:is(:hover, :focus) {
z-index: 1;
filter: none;
background-color: var(--bg-faded-color);
&[data-title]:after {
opacity: 1;
}
}
img {
transition: transform 0.1s ease-out;
}
&:is(:hover, :focus) img {
transform: scale(2);
}
&.edge-left img {
transform-origin: left center;
}
&.edge-right img {
transform-origin: right center;
}
code {
font-size: 0.8em;
}
}
}
}
#custom-emojis-sheet main {
mask-image: none;
.compose-field-container {
display: grid !important;
@media (width < 30em) {
margin-inline: calc(-1 * var(--form-padding-inline));
width: 100vw !important;
max-width: 100vw;
.compose-field {
border-radius: 0;
outline-offset: -2px;
}
}
&.debug {
grid-template-columns: 1fr 1fr;
}
> .compose-field,
> .compose-highlight {
grid-area: 1 / 1 / 2 / 2;
}
.compose-highlight {
user-drag: none;
user-select: none;
pointer-events: none;
touch-action: none;
padding: 8px;
color: transparent;
background-color: transparent;
border: 2px solid transparent;
line-height: 1.4;
overflow: auto;
unicode-bidi: plaintext;
-webkit-rtl-ordering: logical;
rtl-ordering: logical;
overflow-wrap: break-word;
white-space: pre-wrap;
min-height: 5em;
max-height: 50vh;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
/* Follow textarea styles */
@media (min-width: 40em) {
max-height: 65vh;
}
mark {
color: inherit;
}
.compose-highlight-url,
.compose-highlight-hashtag {
background-color: transparent;
text-decoration: underline;
text-decoration-color: var(--link-faded-color);
text-decoration-thickness: 2px;
text-underline-offset: 2px;
}
.compose-highlight-mention,
.compose-highlight-emoji-shortcode,
.compose-highlight-exceeded {
mix-blend-mode: multiply;
border-radius: 4px;
box-shadow: 0 0 0 1px;
}
.compose-highlight-mention {
background-color: var(--orange-light-bg-color);
box-shadow-color: var(--orange-light-bg-color);
}
.compose-highlight-emoji-shortcode {
background-color: var(--bg-faded-color);
box-shadow-color: var(--bg-faded-color);
}
.compose-highlight-exceeded {
background-color: var(--red-bg-color);
box-shadow-color: var(--red-bg-color);
}
@media (prefers-color-scheme: dark) {
.compose-highlight-mention,
.compose-highlight-emoji-shortcode,
.compose-highlight-exceeded {
mix-blend-mode: screen;
}
}
}
}
#custom-emojis-sheet .custom-emojis-list .section-header {
font-size: 80%;
text-transform: uppercase;
color: var(--text-insignificant-color);
padding: 8px 0 4px;
position: sticky;
top: 0;
background-color: var(--bg-blur-color);
backdrop-filter: blur(1px);
@keyframes gif-shake {
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(5deg);
}
50% {
transform: rotate(0deg);
}
75% {
transform: rotate(-5deg);
}
100% {
transform: rotate(0deg);
}
}
#custom-emojis-sheet .custom-emojis-list section {
display: flex;
flex-wrap: wrap;
.gif-picker-button {
span {
font-weight: bold;
font-size: 11.5px;
display: block;
}
&:is(:hover, :focus) {
span {
animation: gif-shake 0.3s 3;
}
}
}
#custom-emojis-sheet .custom-emojis-list button {
border-radius: 8px;
background-image: radial-gradient(
closest-side,
var(--img-bg-color),
transparent
);
}
#custom-emojis-sheet .custom-emojis-list button:is(:hover, :focus) {
filter: none;
background-color: var(--bg-faded-color);
}
#custom-emojis-sheet .custom-emojis-list button img {
transition: transform 0.1s ease-out;
}
#custom-emojis-sheet .custom-emojis-list button:is(:hover, :focus) img {
transform: scale(1.5);
#gif-picker-sheet {
height: 50vh;
form {
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
input[type='search'] {
flex-grow: 1;
min-width: 0;
}
}
main {
overflow-x: auto;
overflow-y: hidden;
mask-image: linear-gradient(
var(--to-forward),
transparent 2px,
black 16px,
black calc(100% - 16px),
transparent calc(100% - 2px)
);
@media (min-height: 480px) {
overflow-y: auto;
max-height: 50vh;
}
&.loading {
opacity: 0.25;
}
.ui-state {
min-height: 100px;
}
ul {
min-height: 100px;
display: flex;
gap: 4px;
list-style: none;
padding: 8px 2px;
margin: 0;
@media (min-height: 480px) {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
grid-auto-rows: 1fr;
}
li {
list-style: none;
padding: 0;
margin: 0;
max-width: 100%;
display: flex;
button {
padding: 4px;
margin: 0;
border: none;
background-color: transparent;
color: inherit;
cursor: pointer;
border-radius: 8px;
background-color: var(--bg-faded-color);
@media (min-height: 480px) {
width: 100%;
text-align: center;
}
&:is(:hover, :focus) {
background-color: var(--link-bg-color);
box-shadow: 0 0 0 2px var(--link-light-color);
filter: none;
}
}
figure {
margin: 0;
padding: 0;
width: var(--figure-width);
max-width: 100%;
@media (min-height: 480px) {
width: 100%;
text-align: center;
}
figcaption {
font-size: 0.8em;
padding: 2px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--text-insignificant-color);
}
}
img {
background-color: var(--img-bg-color);
border-radius: 4px;
vertical-align: top;
object-fit: contain;
}
}
}
.pagination {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 0;
margin: 0;
position: sticky;
bottom: 0;
left: 0;
right: 0;
@media (min-height: 480px) {
position: static;
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
export default function CustomEmoji({ staticUrl, alt, url }) {
return (
<picture>
{staticUrl && (
<source srcset={staticUrl} media="(prefers-reduced-motion: reduce)" />
)}
<img
key={alt || url}
src={url}
alt={alt}
class="shortcode-emoji emoji"
width="16"
height="16"
loading="lazy"
decoding="async"
/>
</picture>
);
}

View file

@ -27,7 +27,7 @@ button.draft-item {
background-color: var(--bg-color);
color: var(--text-color);
border: 1px solid var(--link-faded-color);
text-align: left;
text-align: start;
padding: 0;
}
button.draft-item:is(:hover, :focus) {

View file

@ -10,6 +10,7 @@ import { getCurrentAccountNS } from '../utils/store-utils';
import Icon from './icon';
import Loader from './loader';
import MenuConfirm from './menu-confirm';
function Drafts({ onClose }) {
const { masto } = api();
@ -89,26 +90,33 @@ function Drafts({ onClose }) {
{niceDateTime(updatedAtDate)}
</time>
</b>
<button
type="button"
class="small light"
<MenuConfirm
confirmLabel={<span>Delete this draft?</span>}
menuItemClassName="danger"
align="end"
disabled={uiState === 'loading'}
onClick={() => {
(async () => {
try {
const yes = confirm('Delete this draft?');
if (yes) {
await db.drafts.del(key);
reload();
}
// const yes = confirm('Delete this draft?');
// if (yes) {
await db.drafts.del(key);
reload();
// }
} catch (e) {
alert('Error deleting draft! Please try again.');
}
})();
}}
>
Delete&hellip;
</button>
<button
type="button"
class="small light"
disabled={uiState === 'loading'}
>
Delete&hellip;
</button>
</MenuConfirm>
</div>
<button
type="button"
@ -120,9 +128,9 @@ function Drafts({ onClose }) {
if (replyTo) {
setUIState('loading');
try {
replyToStatus = await masto.v1.statuses.fetch(
replyTo.id,
);
replyToStatus = await masto.v1.statuses
.$select(replyTo.id)
.fetch();
} catch (e) {
console.error(e);
alert('Error fetching reply-to status!');
@ -145,15 +153,16 @@ function Drafts({ onClose }) {
);
})}
</ul>
<p>
<button
type="button"
class="light danger"
disabled={uiState === 'loading'}
onClick={() => {
(async () => {
const yes = confirm('Delete all drafts?');
if (yes) {
{drafts.length > 1 && (
<p>
<MenuConfirm
confirmLabel={<span>Delete all drafts?</span>}
menuItemClassName="danger"
disabled={uiState === 'loading'}
onClick={() => {
(async () => {
// const yes = confirm('Delete all drafts?');
// if (yes) {
setUIState('loading');
try {
await db.drafts.delMany(
@ -166,13 +175,20 @@ function Drafts({ onClose }) {
alert('Error deleting drafts! Please try again.');
setUIState('error');
}
}
})();
}}
>
Delete all drafts&hellip;
</button>
</p>
// }
})();
}}
>
<button
type="button"
class="light danger"
disabled={uiState === 'loading'}
>
Delete all&hellip;
</button>
</MenuConfirm>
</p>
)}
</>
) : (
<p>No drafts found.</p>

View file

@ -0,0 +1,31 @@
.embed-modal-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
pointer-events: none;
.top-controls {
padding: 16px;
display: flex;
gap: 8px;
justify-content: space-between;
pointer-events: auto;
}
.embed-content {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
iframe {
pointer-events: auto;
max-width: 100%;
max-height: 100%;
width: max(var(--width), 480px);
height: auto;
aspect-ratio: var(--aspect-ratio);
}
}
}

View file

@ -0,0 +1,36 @@
import './embed-modal.css';
import Icon from './icon';
function EmbedModal({ html, url, width, height, onClose = () => {} }) {
return (
<div class="embed-modal-container">
<div class="top-controls">
<button type="button" class="light" onClick={() => onClose()}>
<Icon icon="x" />
</button>
{url && (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
class="button plain"
>
<span>Open link</span> <Icon icon="external" />
</a>
)}
</div>
<div
class="embed-content"
dangerouslySetInnerHTML={{ __html: html }}
style={{
'--width': width + 'px',
'--height': height + 'px',
'--aspect-ratio': `${width}/${height}`,
}}
/>
</div>
);
}
export default EmbedModal;

View file

@ -1,42 +1,29 @@
import { memo } from 'preact/compat';
import CustomEmoji from './custom-emoji';
function EmojiText({ text, emojis }) {
if (!text) return '';
if (!emojis?.length) return text;
if (text.indexOf(':') === -1) return text;
const components = [];
let lastIndex = 0;
emojis.forEach((shortcodeObj) => {
const { shortcode, staticUrl, url } = shortcodeObj;
const regex = new RegExp(`:${shortcode}:`, 'g');
let match;
while ((match = regex.exec(text))) {
const beforeText = text.substring(lastIndex, match.index);
if (beforeText) {
components.push(beforeText);
}
components.push(
<img
src={url}
alt={shortcode}
class="shortcode-emoji emoji"
width="12"
height="12"
loading="lazy"
decoding="async"
/>,
);
lastIndex = match.index + match[0].length;
const regex = new RegExp(
`:(${emojis.map((e) => e.shortcode).join('|')}):`,
'g',
);
const elements = text.split(regex).map((word) => {
const emoji = emojis.find((e) => e.shortcode === word);
if (emoji) {
const { url, staticUrl } = emoji;
return <CustomEmoji staticUrl={staticUrl} alt={word} url={url} />;
}
return word;
});
const afterText = text.substring(lastIndex);
if (afterText) {
components.push(afterText);
}
return components;
return elements;
}
export default EmojiText;
export default memo(
EmojiText,
(oldProps, newProps) =>
oldProps.text === newProps.text &&
oldProps.emojis?.length === newProps.emojis?.length,
);

View file

@ -2,26 +2,39 @@ import { useState } from 'preact/hooks';
import { api } from '../utils/api';
import Icon from './icon';
import Loader from './loader';
function FollowRequestButtons({ accountID, onChange }) {
const { masto } = api();
const [uiState, setUIState] = useState('default');
const [requestState, setRequestState] = useState(null); // accept, reject
const [relationship, setRelationship] = useState(null);
const hasRelationship = relationship !== null;
return (
<p class="follow-request-buttons">
<button
type="button"
disabled={uiState === 'loading'}
disabled={uiState === 'loading' || hasRelationship}
onClick={() => {
setUIState('loading');
setRequestState('accept');
(async () => {
try {
await masto.v1.followRequests.authorize(accountID);
const rel = await masto.v1.followRequests
.$select(accountID)
.authorize();
if (!rel?.followedBy) {
throw new Error('Follow request not accepted');
}
setRelationship(rel);
onChange();
} catch (e) {
console.error(e);
setUIState('default');
}
setUIState('default');
})();
}}
>
@ -29,13 +42,20 @@ function FollowRequestButtons({ accountID, onChange }) {
</button>{' '}
<button
type="button"
disabled={uiState === 'loading'}
disabled={uiState === 'loading' || hasRelationship}
class="light danger"
onClick={() => {
setUIState('loading');
setRequestState('reject');
(async () => {
try {
await masto.v1.followRequests.reject(accountID);
const rel = await masto.v1.followRequests
.$select(accountID)
.reject();
if (rel?.followedBy) {
throw new Error('Follow request not rejected');
}
setRelationship(rel);
onChange();
} catch (e) {
console.error(e);
@ -46,7 +66,17 @@ function FollowRequestButtons({ accountID, onChange }) {
>
Reject
</button>
<Loader hidden={uiState !== 'loading'} />
<span class="follow-request-states">
{hasRelationship && requestState ? (
requestState === 'accept' ? (
<Icon icon="check-circle" alt="Accepted" class="follow-accepted" />
) : (
<Icon icon="x-circle" alt="Rejected" class="follow-rejected" />
)
) : (
<Loader hidden={uiState !== 'loading'} />
)}
</span>
</p>
);
}

View file

@ -0,0 +1,111 @@
#generic-accounts-container {
.post-preview {
--max-height: 120px;
max-height: var(--max-height);
overflow: hidden;
margin-block: 8px;
border: 1px solid var(--outline-color);
border-radius: 8px;
pointer-events: none;
.status {
font-size: calc(var(--text-size) * 0.9);
mask-image: linear-gradient(
to bottom,
black calc(var(--max-height) / 2),
transparent calc(var(--max-height) - 8px)
);
filter: saturate(0.5);
}
&:is(a) {
pointer-events: auto;
display: block;
text-decoration: none;
color: inherit;
&:hover {
border-color: var(--outline-hover-color);
}
> * {
pointer-events: none;
}
}
}
.accounts-list {
--list-gap: 16px;
list-style: none;
margin: 0;
padding: 8px 0;
display: flex;
flex-wrap: wrap;
flex-direction: row;
column-gap: 1.5em;
row-gap: var(--list-gap);
li {
display: flex;
flex-grow: 1;
flex-basis: 16em;
/* align-items: center; */
margin: 0;
padding: 0;
gap: 8px;
position: relative;
&:before {
content: '';
display: block;
border-top: var(--hairline-width) solid var(--divider-color);
position: absolute;
bottom: calc(-1 * var(--list-gap) / 2);
inset-inline-start: 40px;
inset-inline-end: 0;
}
&:has(.reactions-block):before {
/* avatar + reactions + gap */
inset-inline-start: calc(40px + 16px + 8px);
}
}
.account-block-acct {
font-size: 0.9em;
color: var(--text-insignificant-color);
/* display: block; */
}
}
.reactions-block {
display: flex;
flex-direction: column;
/* align-self: center; */
.favourite-icon {
color: var(--favourite-color);
}
.reblog-icon {
color: var(--reblog-color);
}
> .icon:only-child {
margin-top: 8px; /* half of icon dimension */
}
}
.account-relationships {
flex-grow: 1;
.tag {
animation: appear 0.3s ease-out;
}
}
.account-block {
align-items: flex-start;
}
}

View file

@ -0,0 +1,230 @@
import './generic-accounts.css';
import { useEffect, useRef, useState } from 'preact/hooks';
import { InView } from 'react-intersection-observer';
import { useSnapshot } from 'valtio';
import { api } from '../utils/api';
import { fetchRelationships } from '../utils/relationships';
import states from '../utils/states';
import useLocationChange from '../utils/useLocationChange';
import AccountBlock from './account-block';
import Icon from './icon';
import Link from './link';
import Loader from './loader';
import Status from './status';
export default function GenericAccounts({
instance,
excludeRelationshipAttrs = [],
postID,
onClose = () => {},
blankCopy = 'Nothing to show',
}) {
const { masto, instance: currentInstance } = api();
const isCurrentInstance = instance ? instance === currentInstance : true;
const snapStates = useSnapshot(states);
``;
const [uiState, setUIState] = useState('default');
const [accounts, setAccounts] = useState([]);
const [showMore, setShowMore] = useState(false);
useLocationChange(onClose);
if (!snapStates.showGenericAccounts) {
return null;
}
const {
id,
heading,
fetchAccounts,
accounts: staticAccounts,
showReactions,
} = snapStates.showGenericAccounts;
const [relationshipsMap, setRelationshipsMap] = useState({});
const loadRelationships = async (accounts) => {
if (!accounts?.length) return;
if (!isCurrentInstance) return;
const relationships = await fetchRelationships(accounts, relationshipsMap);
if (relationships) {
setRelationshipsMap({
...relationshipsMap,
...relationships,
});
}
};
const loadAccounts = (firstLoad) => {
if (!fetchAccounts) return;
if (firstLoad) setAccounts([]);
setUIState('loading');
(async () => {
try {
const { done, value } = await fetchAccounts(firstLoad);
if (Array.isArray(value)) {
if (firstLoad) {
const accounts = [];
for (let i = 0; i < value.length; i++) {
const account = value[i];
const theAccount = accounts.find(
(a, j) => a.id === account.id && i !== j,
);
if (!theAccount) {
accounts.push({
_types: [],
...account,
});
} else {
theAccount._types.push(...account._types);
}
}
setAccounts(accounts);
} else {
// setAccounts((prev) => [...prev, ...value]);
// Merge accounts by id and _types
setAccounts((prev) => {
const newAccounts = prev;
for (const account of value) {
const theAccount = newAccounts.find((a) => a.id === account.id);
if (!theAccount) {
newAccounts.push(account);
} else {
theAccount._types.push(...account._types);
}
}
return newAccounts;
});
}
setShowMore(!done);
loadRelationships(value);
} else {
setShowMore(false);
}
setUIState('default');
} catch (e) {
console.error(e);
setUIState('error');
}
})();
};
const firstLoad = useRef(true);
useEffect(() => {
if (staticAccounts?.length > 0) {
setAccounts(staticAccounts);
loadRelationships(staticAccounts);
} else {
loadAccounts(true);
firstLoad.current = false;
}
}, [staticAccounts, fetchAccounts]);
useEffect(() => {
if (firstLoad.current) return;
// reloadGenericAccounts contains value like {id: 'mute', counter: 1}
// We only need to reload if the id matches
if (snapStates.reloadGenericAccounts?.id === id) {
loadAccounts(true);
}
}, [snapStates.reloadGenericAccounts.counter]);
const post = states.statuses[postID];
return (
<div id="generic-accounts-container" class="sheet" tabindex="-1">
<button type="button" class="sheet-close" onClick={onClose}>
<Icon icon="x" />
</button>
<header>
<h2>{heading || 'Accounts'}</h2>
</header>
<main>
{post && (
<Link
to={`/${instance || currentInstance}/s/${post.id}`}
class="post-preview"
>
<Status status={post} size="s" readOnly />
</Link>
)}
{accounts.length > 0 ? (
<>
<ul class="accounts-list">
{accounts.map((account) => {
const relationship = relationshipsMap[account.id];
const key = `${account.id}-${account._types?.length || ''}`;
return (
<li key={key}>
{showReactions && account._types?.length > 0 && (
<div class="reactions-block">
{account._types.map((type) => (
<Icon
icon={
{
reblog: 'rocket',
favourite: 'heart',
}[type]
}
class={`${type}-icon`}
/>
))}
</div>
)}
<div class="account-relationships">
<AccountBlock
account={account}
showStats
relationship={relationship}
excludeRelationshipAttrs={excludeRelationshipAttrs}
/>
</div>
</li>
);
})}
</ul>
{uiState === 'default' ? (
showMore ? (
<InView
onChange={(inView) => {
if (inView) {
loadAccounts();
}
}}
>
<button
type="button"
class="plain block"
onClick={() => loadAccounts()}
>
Show more&hellip;
</button>
</InView>
) : (
<p class="ui-state insignificant">The end.</p>
)
) : (
uiState === 'loading' && (
<p class="ui-state">
<Loader abrupt />
</p>
)
)}
</>
) : uiState === 'loading' ? (
<p class="ui-state">
<Loader abrupt />
</p>
) : uiState === 'error' ? (
<p class="ui-state">Error loading accounts</p>
) : (
<p class="ui-state insignificant">{blankCopy}</p>
)}
</main>
</div>
);
}

View file

@ -1,4 +1,7 @@
import { useEffect, useState } from 'preact/hooks';
import moize from 'moize';
import { useEffect, useRef, useState } from 'preact/hooks';
import { ICONS } from './ICONS';
const SIZES = {
s: 12,
@ -8,81 +11,30 @@ const SIZES = {
xxl: 32,
};
const ICONS = {
x: 'mingcute:close-line',
heart: 'mingcute:heart-line',
bookmark: 'mingcute:bookmark-line',
'check-circle': 'mingcute:check-circle-line',
transfer: 'mingcute:transfer-4-line',
rocket: 'mingcute:rocket-line',
'arrow-left': 'mingcute:arrow-left-line',
'arrow-right': 'mingcute:arrow-right-line',
'arrow-up': 'mingcute:arrow-up-line',
'arrow-down': 'mingcute:arrow-down-line',
earth: 'mingcute:earth-line',
lock: 'mingcute:lock-line',
unlock: 'mingcute:unlock-line',
'eye-close': 'mingcute:eye-close-line',
'eye-open': 'mingcute:eye-2-line',
message: 'mingcute:mail-line',
comment: 'mingcute:chat-3-line',
home: 'mingcute:home-3-line',
notification: 'mingcute:notification-line',
follow: 'mingcute:user-follow-line',
'follow-add': 'mingcute:user-add-line',
poll: ['mingcute:chart-bar-line', '90deg'],
pencil: 'mingcute:pencil-line',
quill: 'mingcute:quill-pen-line',
at: 'mingcute:at-line',
attachment: 'mingcute:attachment-line',
upload: 'mingcute:upload-3-line',
gear: 'mingcute:settings-3-line',
more: 'mingcute:more-3-line',
external: 'mingcute:external-link-line',
popout: 'mingcute:external-link-line',
popin: ['mingcute:external-link-line', '180deg'],
plus: 'mingcute:add-circle-line',
'chevron-left': 'mingcute:left-line',
'chevron-right': 'mingcute:right-line',
reply: ['mingcute:share-forward-line', '180deg', 'horizontal'],
thread: 'mingcute:route-line',
group: 'mingcute:group-line',
bot: 'mingcute:android-2-line',
menu: 'mingcute:rows-4-line',
list: 'mingcute:list-check-line',
search: 'mingcute:search-2-line',
hashtag: 'mingcute:hashtag-line',
info: 'mingcute:information-line',
shortcut: 'mingcute:lightning-line',
user: 'mingcute:user-4-line',
following: 'mingcute:walk-line',
pin: 'mingcute:pin-line',
bus: 'mingcute:bus-2-line',
link: 'mingcute:link-2-line',
history: 'mingcute:history-line',
share: 'mingcute:share-2-line',
sparkles: 'mingcute:sparkles-line',
exit: 'mingcute:exit-line',
translate: 'mingcute:translate-line',
play: 'mingcute:play-fill',
trash: 'mingcute:delete-2-line',
mute: 'mingcute:volume-mute-line',
unmute: 'mingcute:volume-line',
block: 'mingcute:forbid-circle-line',
unblock: ['mingcute:forbid-circle-line', '180deg'],
flag: 'mingcute:flag-4-line',
time: 'mingcute:time-line',
refresh: 'mingcute:refresh-2-line',
emoji2: 'mingcute:emoji-2-line',
filter: 'mingcute:filter-2-line',
chart: 'mingcute:chart-line-line',
react: 'mingcute:react-line',
layout4: 'mingcute:layout-4-line',
layout5: 'mingcute:layout-5-line',
announce: 'mingcute:announcement-line',
};
const ICONDATA = {};
const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js');
// Memoize the dangerouslySetInnerHTML of the SVGs
const SVGICon = moize(
function ({ width, height, body, rotate, flip }) {
return (
<svg
viewBox={`0 0 ${width} ${height}`}
dangerouslySetInnerHTML={{ __html: body }}
style={{
transform: `${rotate ? `rotate(${rotate})` : ''} ${
flip ? `scaleX(-1)` : ''
}`,
}}
/>
);
},
{
isShallowEqual: true,
maxSize: Object.keys(ICONS).length,
matchesArg: (cacheKeyArg, keyArg) =>
cacheKeyArg.icon === keyArg.icon && cacheKeyArg.body === keyArg.body,
},
);
function Icon({
icon,
@ -95,48 +47,67 @@ function Icon({
if (!icon) return null;
const iconSize = SIZES[size];
let iconName = ICONS[icon];
let rotate, flip;
if (Array.isArray(iconName)) {
[iconName, rotate, flip] = iconName;
let iconBlock = ICONS[icon];
if (!iconBlock) {
console.warn(`Icon ${icon} not found`);
return null;
}
const [iconData, setIconData] = useState(null);
useEffect(async () => {
const name = iconName.replace('mingcute:', '');
const icon = await modules[
`/node_modules/@iconify-icons/mingcute/${name}.js`
]();
setIconData(icon.default);
}, [iconName]);
let rotate,
flip,
rtl = false;
if (Array.isArray(iconBlock)) {
[iconBlock, rotate, flip] = iconBlock;
} else if (typeof iconBlock === 'object') {
({ rotate, flip, rtl } = iconBlock);
iconBlock = iconBlock.module;
}
const [iconData, setIconData] = useState(ICONDATA[icon]);
const currentIcon = useRef(icon);
useEffect(() => {
if (iconData && currentIcon.current === icon) return;
(async () => {
const iconB = await iconBlock();
setIconData(iconB.default);
ICONDATA[icon] = iconB.default;
})();
currentIcon.current = icon;
}, [icon]);
return (
<div
class={`icon ${className}`}
<span
class={`icon ${className} ${rtl ? 'rtl-flip' : ''}`}
title={title || alt}
style={{
width: `${iconSize}px`,
height: `${iconSize}px`,
display: 'inline-block',
overflow: 'hidden',
lineHeight: 0,
...style,
}}
data-icon={icon}
>
{iconData && (
<svg
width={iconSize}
height={iconSize}
viewBox={`0 0 ${iconData.width} ${iconData.height}`}
dangerouslySetInnerHTML={{ __html: iconData.body }}
style={{
transform: `${rotate ? `rotate(${rotate})` : ''} ${
flip ? `scaleX(-1)` : ''
}`,
}}
// <svg
// width={iconSize}
// height={iconSize}
// viewBox={`0 0 ${iconData.width} ${iconData.height}`}
// dangerouslySetInnerHTML={{ __html: iconData.body }}
// style={{
// transform: `${rotate ? `rotate(${rotate})` : ''} ${
// flip ? `scaleX(-1)` : ''
// }`,
// }}
// />
<SVGICon
icon={icon}
width={iconData.width}
height={iconData.height}
body={iconData.body}
rotate={rotate}
flip={flip}
/>
)}
</div>
</span>
);
}

View file

@ -0,0 +1,29 @@
import { useLayoutEffect, useRef, useState } from 'preact/hooks';
const IntersectionView = ({ children, root = null, fallback = null }) => {
const ref = useRef();
const [show, setShow] = useState(false);
useLayoutEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (entry.isIntersecting) {
setShow(true);
observer.unobserve(ref.current);
}
},
{
root,
rootMargin: `${screen.height}px`,
},
);
if (ref.current) observer.observe(ref.current);
return () => {
if (ref.current) observer.unobserve(ref.current);
};
}, []);
return show ? children : <div ref={ref}>{fallback}</div>;
};
export default IntersectionView;

View file

@ -0,0 +1,36 @@
import { shouldPolyfill } from '@formatjs/intl-segmenter/should-polyfill';
import { Suspense } from 'preact/compat';
import { useEffect, useState } from 'preact/hooks';
import Loader from './loader';
const supportsIntlSegmenter = !shouldPolyfill();
// Preload IntlSegmenter
setTimeout(() => {
queueMicrotask(() => {
if (!supportsIntlSegmenter) {
import('@formatjs/intl-segmenter/polyfill-force').catch(() => {});
}
});
}, 1000);
export default function IntlSegmenterSuspense({ children }) {
if (supportsIntlSegmenter) {
return <Suspense fallback={<Loader />}>{children}</Suspense>;
}
const [polyfillLoaded, setPolyfillLoaded] = useState(false);
useEffect(() => {
(async () => {
await import('@formatjs/intl-segmenter/polyfill-force');
setPolyfillLoaded(true);
})();
}, []);
return polyfillLoaded ? (
<Suspense fallback={<Loader />}>{children}</Suspense>
) : (
<Loader />
);
}

View file

@ -0,0 +1,44 @@
#keyboard-shortcuts-help-container {
table {
tr > * {
border-top: 1px solid var(--outline-color);
vertical-align: middle;
}
th {
font-weight: normal;
text-align: start;
padding: 0.25em 0;
line-height: 1;
width: 60%;
}
td {
padding: 0.25em 1em;
}
}
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);
background-image: linear-gradient(
to top,
var(--bg-blur-color),
transparent
);
text-shadow: 0 1px var(--bg-color);
box-shadow: 0 1px var(--drop-shadow-color),
0 1px 1px var(--drop-shadow-color), 0 1px 8px var(--drop-shadow-color),
inset 0 1px var(--bg-blur-color);
&:active {
box-shadow: 0 1px 4px var(--drop-shadow-color),
inset 0 1px var(--bg-blur-color);
transform: translateY(1px);
filter: brightness(0.95);
}
}
}

View file

@ -0,0 +1,191 @@
import './keyboard-shortcuts-help.css';
import { memo } from 'preact/compat';
import { useHotkeys } from 'react-hotkeys-hook';
import { useSnapshot } from 'valtio';
import states from '../utils/states';
import Icon from './icon';
import Modal from './modal';
export default memo(function KeyboardShortcutsHelp() {
const snapStates = useSnapshot(states);
function onClose() {
states.showKeyboardShortcutsHelp = false;
}
useHotkeys(
'?, shift+?, shift+slash',
(e) => {
console.log('help');
states.showKeyboardShortcutsHelp = true;
},
{
ignoreEventWhen: (e) => {
const hasModal = !!document.querySelector('#modal-container > *');
return hasModal;
},
},
);
return (
!!snapStates.showKeyboardShortcutsHelp && (
<Modal onClose={onClose}>
<div id="keyboard-shortcuts-help-container" class="sheet" tabindex="-1">
<button type="button" class="sheet-close" onClick={onClose}>
<Icon icon="x" />
</button>
<header>
<h2>Keyboard shortcuts</h2>
</header>
<main>
<table>
{[
{
action: 'Keyboard shortcuts help',
keys: <kbd>?</kbd>,
},
{
action: 'Next post',
keys: <kbd>j</kbd>,
},
{
action: 'Previous post',
keys: <kbd>k</kbd>,
},
{
action: 'Skip carousel to next post',
keys: (
<>
<kbd>Shift</kbd> + <kbd>j</kbd>
</>
),
},
{
action: 'Skip carousel to previous post',
keys: (
<>
<kbd>Shift</kbd> + <kbd>k</kbd>
</>
),
},
{
action: 'Load new posts',
keys: <kbd>.</kbd>,
},
{
action: 'Open post details',
keys: (
<>
<kbd>Enter</kbd> or <kbd>o</kbd>
</>
),
},
{
action: (
<>
Expand content warning or
<br />
toggle expanded/collapsed thread
</>
),
keys: <kbd>x</kbd>,
},
{
action: 'Close post or dialogs',
keys: (
<>
<kbd>Esc</kbd> or <kbd>Backspace</kbd>
</>
),
},
{
action: 'Focus column in multi-column mode',
keys: (
<>
<kbd>1</kbd> to <kbd>9</kbd>
</>
),
},
{
action: 'Compose new post',
keys: <kbd>c</kbd>,
},
{
action: 'Compose new post (new window)',
className: 'insignificant',
keys: (
<>
<kbd>Shift</kbd> + <kbd>c</kbd>
</>
),
},
{
action: 'Send post',
keys: (
<>
<kbd>Ctrl</kbd> + <kbd>Enter</kbd> or <kbd></kbd> +{' '}
<kbd>Enter</kbd>
</>
),
},
{
action: 'Search',
keys: <kbd>/</kbd>,
},
{
action: 'Reply',
keys: <kbd>r</kbd>,
},
{
action: 'Reply (new window)',
className: 'insignificant',
keys: (
<>
<kbd>Shift</kbd> + <kbd>r</kbd>
</>
),
},
{
action: 'Like (favourite)',
keys: (
<>
<kbd>l</kbd> or <kbd>f</kbd>
</>
),
},
{
action: 'Boost',
keys: (
<>
<kbd>Shift</kbd> + <kbd>b</kbd>
</>
),
},
{
action: 'Bookmark',
keys: <kbd>d</kbd>,
},
{
action: 'Toggle Cloak mode',
keys: (
<>
<kbd>Shift</kbd> + <kbd>Alt</kbd> + <kbd>k</kbd>
</>
),
},
].map(({ action, className, keys }) => (
<tr key={action}>
<th class={className}>{action}</th>
<td>{keys}</td>
</tr>
))}
</table>
</main>
</div>
</Modal>
)
);
});

View file

@ -0,0 +1,59 @@
/*
Rendered but hidden. Only show when visible
*/
import { useEffect, useRef, useState } from 'preact/hooks';
import { useInView } from 'react-intersection-observer';
// The sticky header, usually at the top
const TOP = 48;
const shazamIDs = {};
export default function LazyShazam({ id, children }) {
const containerRef = useRef();
const hasID = !!shazamIDs[id];
const [visible, setVisible] = useState(false);
const [visibleStart, setVisibleStart] = useState(hasID || false);
const { ref } = useInView({
root: null,
rootMargin: `-${TOP}px 0px 0px 0px`,
trackVisibility: true,
delay: 1000,
onChange: (inView) => {
if (inView) {
setVisible(true);
if (id) shazamIDs[id] = true;
}
},
triggerOnce: true,
skip: visibleStart || visible,
});
useEffect(() => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
if (rect.bottom > TOP) {
if (rect.top < window.innerHeight) {
setVisible(true);
} else {
setVisibleStart(true);
}
if (id) shazamIDs[id] = true;
}
}, []);
if (visibleStart) return children;
return (
<div
ref={containerRef}
class="shazam-container no-animation"
hidden={!visible}
>
<div ref={ref} class="shazam-container-inner">
{children}
</div>
</div>
);
}

View file

@ -19,7 +19,19 @@ const Link = forwardRef((props, ref) => {
let hash = (location.hash || '').replace(/^#/, '').trim();
if (hash === '') hash = '/';
const { to, ...restProps } = props;
const isActive = decodeURIComponent(hash) === to;
// Handle encodeURIComponent of searchParams values
if (!!hash && hash !== '/' && hash.includes('?')) {
const parsedHash = URL.parse(hash, location.origin); // Fake base URL
if (parsedHash?.searchParams?.size) {
const searchParamsStr = Array.from(parsedHash.searchParams.entries())
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&');
hash = parsedHash.pathname + '?' + searchParamsStr;
}
}
const isActive = hash === to || decodeURIComponent(hash) === to;
return (
<a
ref={ref}
@ -27,6 +39,10 @@ const Link = forwardRef((props, ref) => {
{...restProps}
class={`${props.class || ''} ${isActive ? 'is-active' : ''}`}
onClick={(e) => {
if (e.currentTarget?.parentNode?.closest('a')) {
// If this <a> is nested inside another <a>
e.stopPropagation();
}
if (routerLocation) states.prevLocation = routerLocation;
props.onClick?.(e);
}}

View file

@ -0,0 +1,226 @@
.links-bar {
position: relative;
display: flex;
padding: 16px 16px 20px 16px;
gap: 16px;
overflow-x: auto;
background-color: var(--bg-faded-color);
mask-image: linear-gradient(
var(--to-forward),
transparent,
black 16px,
black calc(100% - 16px),
transparent
);
text-shadow: 0 1px var(--bg-blur-color);
transition: opacity 0.3s ease-out;
#trending-page &:not(#columns &) {
@media (min-width: 40em) {
width: 95vw;
max-width: calc(320px * 3.3);
transform: translateX(calc(-50% + var(--main-width) / 2));
&:dir(rtl) {
transform: translateX(calc(50% - var(--main-width) / 2));
}
}
}
& > header {
width: 1.2em;
white-space: nowrap;
position: relative;
flex-shrink: 0;
h3 {
font-size: 90%;
font-style: italic;
margin: 0;
padding: 0;
text-transform: uppercase;
color: var(--text-insignificant-color);
position: absolute;
top: 8px;
inset-inline-start: 0;
transform-origin: top left;
transform: rotate(-90deg) translateX(-100%);
&:dir(rtl) {
transform-origin: top right;
transform: rotate(90deg) translateX(100%);
}
user-select: none;
background-image: linear-gradient(
var(--to-backward),
var(--text-color),
var(--link-color)
);
background-clip: text;
text-fill-color: transparent;
-webkit-text-fill-color: transparent;
}
}
a {
min-width: 240px;
flex-grow: 1;
max-width: 320px;
text-decoration: none;
color: inherit;
border-radius: 16px;
overflow: hidden;
background-color: var(--accent-alpha-color);
border: 4px solid transparent;
box-shadow: 0 4px 8px -2px var(--drop-shadow-color);
transition: all 0.15s ease-out;
display: flex;
background-image: linear-gradient(
to bottom,
var(--accent-color, var(--link-text-color)) -50%,
transparent
);
background-clip: border-box;
background-origin: border-box;
min-height: 160px;
height: 320px;
max-height: 50vh;
&:not(:active):is(:hover, :focus-visible) {
border-color: var(--accent-color, var(--link-light-color));
box-shadow: 0 4px 8px var(--drop-shadow-color),
0 8px 16px var(--drop-shadow-color);
transform-origin: center bottom;
transform: scale(1.02);
img {
animation: position-object 5s ease-in-out 1s 5;
}
}
&:active {
transition: none;
transform: scale(1.015);
filter: brightness(0.8);
}
figure {
transition: 1s ease-out;
transition-property: opacity, mix-blend-mode;
}
&.inactive:not(:active, :hover) {
figure {
transition-duration: 0.3s;
opacity: 0.5;
mix-blend-mode: luminosity;
}
}
&.active {
border-color: var(--accent-color, var(--link-light-color));
height: 100%;
max-height: 100%;
+ button[disabled] {
display: none;
}
}
article {
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
background-color: var(--bg-color);
background-repeat: no-repeat;
background-image: linear-gradient(
to bottom,
var(--accent-alpha-color) 70%,
var(--bg-color) 100%
);
transition: background-position-y 0.15s ease-out;
figure {
flex-grow: 1;
margin: 0 0 -16px;
padding: 0;
position: relative;
img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
vertical-align: top;
mask-image: linear-gradient(
to bottom,
hsl(0, 0%, 0%) 0%,
hsla(0, 0%, 0%, 0.987) 14%,
hsla(0, 0%, 0%, 0.951) 26.2%,
hsla(0, 0%, 0%, 0.896) 36.8%,
hsla(0, 0%, 0%, 0.825) 45.9%,
hsla(0, 0%, 0%, 0.741) 53.7%,
hsla(0, 0%, 0%, 0.648) 60.4%,
hsla(0, 0%, 0%, 0.55) 66.2%,
hsla(0, 0%, 0%, 0.45) 71.2%,
hsla(0, 0%, 0%, 0.352) 75.6%,
hsla(0, 0%, 0%, 0.259) 79.6%,
hsla(0, 0%, 0%, 0.175) 83.4%,
hsla(0, 0%, 0%, 0.104) 87.2%,
hsla(0, 0%, 0%, 0.049) 91.1%,
hsla(0, 0%, 0%, 0.013) 95.3%,
hsla(0, 0%, 0%, 0) 100%
);
}
}
}
&:is(:hover, :focus-visible) article {
background-position-y: -40px;
}
.article-body {
padding: 0 8px 8px;
line-height: 1.3;
flex-shrink: 0;
}
.article-meta {
color: var(--text-insignificant-color);
font-size: 90%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&:hover .domain {
color: var(--link-text-color);
}
h1 {
font-weight: normal;
font-size: inherit;
margin: 0;
padding: 0;
text-wrap: balance;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
p {
color: var(--text-insignificant-color);
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
font-size: 90%;
}
hr {
margin: 4px 0;
}
}
}

View file

@ -1,21 +1,30 @@
import { useEffect, useRef, useState } from 'preact/hooks';
import { api } from '../utils/api';
import { addListStore, deleteListStore, updateListStore } from '../utils/lists';
import supports from '../utils/supports';
import Icon from './icon';
import MenuConfirm from './menu-confirm';
function ListAddEdit({ list, onClose }) {
const { masto } = api();
const [uiState, setUiState] = useState('default');
const [uiState, setUIState] = useState('default');
const editMode = !!list;
const nameFieldRef = useRef();
const repliesPolicyFieldRef = useRef();
const exclusiveFieldRef = useRef();
useEffect(() => {
if (editMode) {
nameFieldRef.current.value = list.title;
repliesPolicyFieldRef.current.value = list.repliesPolicy;
if (exclusiveFieldRef.current) {
exclusiveFieldRef.current.checked = list.exclusive;
}
}
}, [editMode]);
const supportsExclusive = supports('@mastodon/list-exclusive');
return (
<div class="sheet">
{!!onClose && (
@ -35,37 +44,49 @@ function ListAddEdit({ list, onClose }) {
const formData = new FormData(e.target);
const title = formData.get('title');
const repliesPolicy = formData.get('replies_policy');
const exclusive = formData.get('exclusive') === 'on';
console.log({
title,
repliesPolicy,
exclusive,
});
setUiState('loading');
setUIState('loading');
(async () => {
try {
let listResult;
if (editMode) {
listResult = await masto.v1.lists.update(list.id, {
listResult = await masto.v1.lists.$select(list.id).update({
title,
replies_policy: repliesPolicy,
exclusive,
});
} else {
listResult = await masto.v1.lists.create({
title,
replies_policy: repliesPolicy,
exclusive,
});
}
console.log(listResult);
setUiState('default');
setUIState('default');
onClose?.({
state: 'success',
list: listResult,
});
setTimeout(() => {
if (editMode) {
updateListStore(listResult);
} else {
addListStore(listResult);
}
}, 1);
} catch (e) {
console.error(e);
setUiState('error');
setUIState('error');
alert(
editMode ? 'Unable to edit list.' : 'Unable to create list.',
);
@ -83,6 +104,7 @@ function ListAddEdit({ list, onClose }) {
name="title"
required
disabled={uiState === 'loading'}
dir="auto"
/>
</label>
</div>
@ -98,37 +120,60 @@ function ListAddEdit({ list, onClose }) {
<option value="none">Don't show replies</option>
</select>
</div>
{supportsExclusive && (
<div class="list-form-row">
<label class="label-block">
<input
ref={exclusiveFieldRef}
type="checkbox"
name="exclusive"
disabled={uiState === 'loading'}
/>{' '}
Hide posts on this list from Home/Following
</label>
</div>
)}
<div class="list-form-footer">
<button type="submit" disabled={uiState === 'loading'}>
{editMode ? 'Save' : 'Create'}
</button>
{editMode && (
<button
type="button"
class="light danger"
<MenuConfirm
disabled={uiState === 'loading'}
align="end"
menuItemClassName="danger"
confirmLabel="Delete this list?"
onClick={() => {
const yes = confirm('Delete this list?');
if (!yes) return;
setUiState('loading');
// const yes = confirm('Delete this list?');
// if (!yes) return;
setUIState('loading');
(async () => {
try {
await masto.v1.lists.remove(list.id);
setUiState('default');
await masto.v1.lists.$select(list.id).remove();
setUIState('default');
onClose?.({
state: 'deleted',
});
setTimeout(() => {
deleteListStore(list.id);
}, 1);
} catch (e) {
console.error(e);
setUiState('error');
setUIState('error');
alert('Unable to delete list.');
}
})();
}}
>
Delete
</button>
<button
type="button"
class="light danger"
disabled={uiState === 'loading'}
>
Delete
</button>
</MenuConfirm>
)}
</div>
</form>

View file

@ -1,14 +1,15 @@
import './loader.css';
function Loader({ abrupt, hidden }) {
function Loader({ abrupt, hidden, ...props }) {
return (
<div
<span
{...props}
class={`loader-container ${abrupt ? 'abrupt' : ''} ${
hidden ? 'hidden' : ''
}`}
>
<div class="loader" />
</div>
<span class="loader" />
</span>
);
}

View file

@ -0,0 +1,87 @@
import { Menu, MenuItem } from '@szhsin/react-menu';
import { useState } from 'preact/hooks';
import { useSnapshot } from 'valtio';
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
import localeMatch from '../utils/locale-match';
import { speak, supportsTTS } from '../utils/speech';
import states from '../utils/states';
import Icon from './icon';
import Menu2 from './menu2';
import TranslationBlock from './translation-block';
export default function MediaAltModal({ alt, lang, onClose }) {
const snapStates = useSnapshot(states);
const [forceTranslate, setForceTranslate] = useState(false);
const targetLanguage = getTranslateTargetLanguage(true);
const contentTranslationHideLanguages =
snapStates.settings.contentTranslationHideLanguages || [];
const differentLanguage =
!!lang &&
lang !== targetLanguage &&
!localeMatch([lang], [targetLanguage]) &&
!contentTranslationHideLanguages.find(
(l) => lang === l || localeMatch([lang], [l]),
);
return (
<div class="sheet" tabindex="-1">
{!!onClose && (
<button type="button" class="sheet-close outer" onClick={onClose}>
<Icon icon="x" />
</button>
)}
<header class="header-grid">
<h2>Media description</h2>
<div class="header-side">
<Menu2
align="end"
menuButton={
<button type="button" class="plain4">
<Icon icon="more" alt="More" size="xl" />
</button>
}
>
<MenuItem
disabled={forceTranslate}
onClick={() => {
setForceTranslate(true);
}}
>
<Icon icon="translate" />
<span>Translate</span>
</MenuItem>
{supportsTTS && (
<MenuItem
onClick={() => {
speak(alt, lang);
}}
>
<Icon icon="speak" />
<span>Speak</span>
</MenuItem>
)}
</Menu2>
</div>
</header>
<main lang={lang} dir="auto">
<p
style={{
whiteSpace: 'pre-wrap',
textWrap: 'pretty',
}}
>
{alt}
</p>
{(differentLanguage || forceTranslate) && (
<TranslationBlock
forceTranslate={forceTranslate}
sourceLanguage={lang}
text={alt}
/>
)}
</main>
</div>
);
}

View file

@ -1,22 +1,36 @@
import { Menu, MenuItem } from '@szhsin/react-menu';
import { MenuDivider, MenuItem } from '@szhsin/react-menu';
import { getBlurHashAverageColor } from 'fast-blurhash';
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
import {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
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 Icon from './icon';
import Link from './link';
import Media from './media';
import MenuLink from './menu-link';
import Modal from './modal';
import TranslationBlock from './translation-block';
import Menu2 from './menu2';
const { PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL } = import.meta.env;
function MediaModal({
mediaAttachments,
statusID,
instance,
lang,
index = 0,
onClose = () => {},
}) {
const [uiState, setUIState] = useState('default');
const carouselRef = useRef(null);
const [currentIndex, setCurrentIndex] = useState(index);
@ -41,9 +55,10 @@ function MediaModal({
const differentStatusID = prevStatusID.current !== statusID;
if (differentStatusID) prevStatusID.current = statusID;
carouselRef.current.scrollTo({
left: scrollLeft,
left: scrollLeft * (isRTL() ? -1 : 1),
behavior: differentStatusID ? 'auto' : 'smooth',
});
carouselRef.current.focus();
}, [index, statusID]);
const [showControls, setShowControls] = useState(true);
@ -62,14 +77,22 @@ function MediaModal({
};
}, []);
useHotkeys('esc', onClose, [onClose]);
const [showMediaAlt, setShowMediaAlt] = useState(false);
useHotkeys(
'esc',
onClose,
{
ignoreEventWhen: (e) => {
const hasModal = !!document.querySelector('#modal-container > *');
return hasModal;
},
},
[onClose],
);
useEffect(() => {
let handleScroll = () => {
const { clientWidth, scrollLeft } = carouselRef.current;
const index = Math.round(scrollLeft / clientWidth);
const index = Math.round(Math.abs(scrollLeft) / clientWidth);
setCurrentIndex(index);
};
if (carouselRef.current) {
@ -91,11 +114,55 @@ function MediaModal({
return () => clearTimeout(timer);
}, []);
const mediaAccentColors = 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 null;
});
}, [mediaAttachments]);
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]);
let toastRef = useRef(null);
useEffect(() => {
return () => {
toastRef.current?.hideToast?.();
};
}, []);
return (
<div class="media-modal-container">
<div
class={`media-modal-container media-modal-count-${mediaAttachments?.length}`}
>
<div
ref={carouselRef}
tabIndex="-1"
tabIndex="0"
data-swipe-threshold="44"
class="carousel"
onClick={(e) => {
@ -107,26 +174,41 @@ function MediaModal({
onClose();
}
}}
style={
mediaAttachments.length > 1
? {
backgroundAttachment: 'local',
backgroundImage: `linear-gradient(
to ${isRTL() ? 'left' : 'right'}, ${mediaAccentGradient})`,
}
: {}
}
>
{mediaAttachments?.map((media, i) => {
const { blurhash } = media;
const rgbAverageColor = blurhash
? getBlurHashAverageColor(blurhash)
: null;
const accentColor =
mediaAttachments.length === 1 ? mediaAccentColors[i] : null;
return (
<div
class="carousel-item"
style={{
'--average-color': `rgb(${rgbAverageColor?.join(',')})`,
'--average-color-alpha': `rgba(${rgbAverageColor?.join(
',',
)}, .5)`,
}}
style={
accentColor
? {
'--accent-color': `rgb(${accentColor?.join(',')})`,
'--accent-alpha-color': `rgba(${accentColor?.join(
',',
)}, 0.4)`,
}
: {}
}
tabindex="0"
key={media.id}
ref={i === currentIndex ? carouselFocusItem : null}
onClick={(e) => {
if (e.target !== e.currentTarget) {
// console.log(e);
// if (e.target !== e.currentTarget) {
// setShowControls(!showControls);
// }
if (!e.target.classList.contains('media')) {
setShowControls(!showControls);
}
}}
@ -134,17 +216,22 @@ function MediaModal({
{!!media.description && (
<button
type="button"
class="plain2 media-alt"
class="media-alt"
hidden={!showControls}
onClick={() => {
setShowMediaAlt(media.description);
states.showMediaAlt = {
alt: media.description,
lang,
};
}}
>
<Icon icon="info" />
<span class="media-alt-desc">{media.description}</span>
<span class="alt-badge">ALT</span>
<span class="media-alt-desc" lang={lang} dir="auto">
{media.description}
</span>
</button>
)}
<Media media={media} showOriginal />
<Media media={media} showOriginal lang={lang} />
</div>
);
})}
@ -153,7 +240,7 @@ function MediaModal({
<span>
<button
type="button"
class="carousel-button plain3"
class="carousel-button"
onClick={() => onClose()}
>
<Icon icon="x" />
@ -166,19 +253,19 @@ function MediaModal({
key={media.id}
type="button"
disabled={i === currentIndex}
class={`plain3 carousel-dot ${
i === currentIndex ? 'active' : ''
}`}
class={`carousel-dot ${i === currentIndex ? 'active' : ''}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
carouselRef.current.scrollTo({
left: carouselRef.current.clientWidth * i,
left:
carouselRef.current.clientWidth * i * (isRTL() ? -1 : 1),
behavior: 'smooth',
});
carouselRef.current.focus();
}}
>
&bull;
<Icon icon="round" size="s" />
</button>
))}
</span>
@ -186,15 +273,14 @@ function MediaModal({
<span />
)}
<span>
<Menu
<Menu2
overflow="auto"
align="end"
position="anchor"
boundingBoxPadding="8 8 8 8"
gap={4}
menuClassName="glass-menu"
menuButton={
<button type="button" class="carousel-button plain3">
<button type="button" class="carousel-button">
<Icon icon="more" alt="More" />
</button>
}
@ -204,21 +290,62 @@ function MediaModal({
mediaAttachments[currentIndex]?.remoteUrl ||
mediaAttachments[currentIndex]?.url
}
class="carousel-button plain3"
class="carousel-button"
target="_blank"
title="Open original media in new window"
>
<Icon icon="popout" />
<span>Open original media</span>
</MenuLink>
</Menu>{' '}
{import.meta.env.DEV && // Only dev for now
!!states.settings.mediaAltGenerator &&
!!IMG_ALT_API_URL &&
!!mediaAttachments[currentIndex]?.url &&
!mediaAttachments[currentIndex]?.description &&
mediaAttachments[currentIndex]?.type === 'image' && (
<>
<MenuDivider />
<MenuItem
disabled={uiState === 'loading'}
onClick={() => {
setUIState('loading');
toastRef.current = showToast({
text: 'Attempting to describe image. Please wait...',
duration: -1,
});
(async function () {
try {
const response = await fetch(
`${IMG_ALT_API_URL}?image=${encodeURIComponent(
mediaAttachments[currentIndex]?.url,
)}`,
).then((r) => r.json());
states.showMediaAlt = {
alt: response.description,
};
} catch (e) {
console.error(e);
showToast('Failed to describe image');
} finally {
setUIState('default');
toastRef.current?.hideToast?.();
}
})();
}}
>
<Icon icon="sparkles2" />
<span>Describe image</span>
</MenuItem>
</>
)}
</Menu2>{' '}
<Link
to={`${instance ? `/${instance}` : ''}/s/${statusID}${
window.matchMedia('(min-width: calc(40em + 350px))').matches
? `?media=${currentIndex + 1}`
: ''
}`}
class="button carousel-button media-post-link plain3"
class="button carousel-button media-post-link"
// onClick={() => {
// // if small screen (not media query min-width 40em + 350px), run onClose
// if (
@ -228,7 +355,7 @@ function MediaModal({
// }
// }}
>
<span class="button-label">See post </span>&raquo;
<span class="button-label">View post </span>&raquo;
</Link>
</span>
</div>
@ -236,13 +363,17 @@ function MediaModal({
<div class="carousel-controls" hidden={!showControls}>
<button
type="button"
class="carousel-button plain3"
class="carousel-button"
hidden={currentIndex === 0}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
carouselRef.current.focus();
carouselRef.current.scrollTo({
left: carouselRef.current.clientWidth * (currentIndex - 1),
left:
carouselRef.current.clientWidth *
(currentIndex - 1) *
(isRTL() ? -1 : 1),
behavior: 'smooth',
});
}}
@ -251,13 +382,17 @@ function MediaModal({
</button>
<button
type="button"
class="carousel-button plain3"
class="carousel-button"
hidden={currentIndex === mediaAttachments.length - 1}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
carouselRef.current.focus();
carouselRef.current.scrollTo({
left: carouselRef.current.clientWidth * (currentIndex + 1),
left:
carouselRef.current.clientWidth *
(currentIndex + 1) *
(isRTL() ? -1 : 1),
behavior: 'smooth',
});
}}
@ -266,69 +401,6 @@ function MediaModal({
</button>
</div>
)}
{!!showMediaAlt && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowMediaAlt(false);
}
}}
>
<MediaAltModal
alt={showMediaAlt}
onClose={() => setShowMediaAlt(false)}
/>
</Modal>
)}
</div>
);
}
function MediaAltModal({ alt, onClose }) {
const [forceTranslate, setForceTranslate] = useState(false);
return (
<div class="sheet">
{!!onClose && (
<button type="button" class="sheet-close outer" onClick={onClose}>
<Icon icon="x" />
</button>
)}
<header class="header-grid">
<h2>Media description</h2>
<div class="header-side">
<Menu
align="end"
menuButton={
<button type="button" class="plain4">
<Icon icon="more" alt="More" size="xl" />
</button>
}
>
<MenuItem
disabled={forceTranslate}
onClick={() => {
setForceTranslate(true);
}}
>
<Icon icon="translate" />
<span>Translate</span>
</MenuItem>
</Menu>
</div>
</header>
<main>
<p
style={{
whiteSpace: 'pre-wrap',
}}
>
{alt}
</p>
{forceTranslate && (
<TranslationBlock forceTranslate={forceTranslate} text={alt} />
)}
</main>
</div>
);
}

View file

@ -0,0 +1,122 @@
.media-post {
--item-radius: 16px;
position: relative;
animation: appear-smooth 1s ease-out;
&:is(.filtered, .has-spoiler:not(.show-media)) :is(img, video) {
/* filter: blur(32px);
image-rendering: crisp-edges;
image-rendering: pixelated; */
opacity: 0;
animation: none !important;
}
&.filtered[data-filtered-text]:before {
content: attr(data-filtered-text);
}
&.has-spoiler[data-spoiler-text]:before {
content: attr(data-spoiler-text);
}
&.filtered[data-filtered-text]:before,
&.has-spoiler[data-spoiler-text]:before {
pointer-events: none;
position: absolute;
top: 0;
inset-inline-start: 0;
z-index: 1;
background-color: var(--bg-blur-color);
margin: 8px;
padding: 4px 6px;
border-radius: calc(var(--item-radius) / 2);
font-size: 90%;
border: var(--hairline-width) dashed var(--bg-color);
word-break: break-word;
word-wrap: break-word;
overflow-wrap: break-word;
/* mix-blend-mode: luminosity; */
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
box-orient: vertical;
display: -webkit-box;
display: box;
overflow: hidden;
z-index: 2;
> * {
pointer-events: none;
}
}
&.has-spoiler.show-media[data-spoiler-text]:before {
mix-blend-mode: normal;
backdrop-filter: blur(4px);
}
.media {
border-radius: var(--item-radius);
overflow: hidden;
position: relative;
display: block;
aspect-ratio: 1 !important;
&:before {
position: absolute;
inset: 0;
content: '';
border: 1px solid var(--outline-color);
border-radius: inherit;
}
&:not(.media-audio) {
background-color: var(--average-color, var(--media-bg-color));
background-clip: padding-box;
}
@media (hover: hover) {
&:hover {
--drop-shadow: var(--drop-shadow-color);
position: relative;
z-index: 1;
box-shadow: 0 8px 16px -4px var(--drop-shadow),
0 4px 8px var(--drop-shadow);
@media (prefers-color-scheme: dark) {
--drop-shadow: var(--link-color);
}
}
}
&:active:not(:has(button:active)) {
box-shadow: none;
filter: brightness(0.8);
transform: scale(0.99);
}
video,
img,
audio {
border-radius: 16px;
/* object-fit: scale-down; */
object-fit: cover;
width: 100%;
height: 100%;
vertical-align: top;
}
:not(.filtered, .has-spoiler) &:is(:hover, :focus) img {
/* Less delay here to make it feel more responsive */
animation: position-object 5s ease-in-out 0.1s 5;
animation-duration: var(--anim-duration, 5s);
}
}
&.has-spoiler .media:not(.media-audio) {
background-image: radial-gradient(
circle at 50% 50%,
var(--average-color, var(--bg-faded-color)),
var(--bg-color) 20em
);
}
}

View file

@ -0,0 +1,154 @@
import './media-post.css';
import { memo } from 'preact/compat';
import { useContext, useMemo } from 'preact/hooks';
import { useSnapshot } from 'valtio';
import FilterContext from '../utils/filter-context';
import { isFiltered } from '../utils/filters';
import states, { statusKey } from '../utils/states';
import store from '../utils/store';
import { getCurrentAccountID } from '../utils/store-utils';
import Media from './media';
function MediaPost({
class: className,
statusID,
status,
instance,
parent,
// allowFilters,
onMediaClick,
}) {
let sKey = statusKey(statusID, instance);
const snapStates = useSnapshot(states);
if (!status) {
status = snapStates.statuses[sKey] || snapStates.statuses[statusID];
sKey = statusKey(status?.id, instance);
}
if (!status) {
return null;
}
const {
account: {
acct,
avatar,
avatarStatic,
id: accountId,
url: accountURL,
displayName,
username,
emojis: accountEmojis,
bot,
group,
},
id,
repliesCount,
reblogged,
reblogsCount,
favourited,
favouritesCount,
bookmarked,
poll,
muted,
sensitive,
spoilerText,
visibility, // public, unlisted, private, direct
language,
editedAt,
filtered,
card,
createdAt,
inReplyToId,
inReplyToAccountId,
content,
mentions,
mediaAttachments,
reblog,
uri,
url,
emojis,
// Non-API props
_deleted,
_pinned,
// _filtered,
} = status;
if (!mediaAttachments?.length) {
return null;
}
const debugHover = (e) => {
if (e.shiftKey) {
console.log({
...status,
});
}
};
const currentAccount = useMemo(() => {
return getCurrentAccountID();
}, []);
const isSelf = useMemo(() => {
return currentAccount && currentAccount === accountId;
}, [accountId, currentAccount]);
const filterContext = useContext(FilterContext);
const filterInfo = !isSelf && isFiltered(filtered, filterContext);
if (filterInfo?.action === 'hide') {
return null;
}
console.debug('RENDER Media post', id, status?.account.displayName);
const hasSpoiler = sensitive;
const readingExpandMedia = useMemo(() => {
// default | show_all | hide_all
const prefs = store.account.get('preferences') || {};
return prefs['reading:expand:media'] || 'default';
}, []);
const showSpoilerMedia = readingExpandMedia === 'show_all';
const Parent = parent || 'div';
return mediaAttachments.map((media, i) => {
const mediaKey = `${sKey}-${media.id}`;
const filterTitleStr = filterInfo?.titlesStr;
return (
<Parent
data-state-post-id={sKey}
onMouseEnter={debugHover}
key={mediaKey}
data-spoiler-text={
spoilerText || (sensitive ? 'Sensitive media' : undefined)
}
data-filtered-text={
filterInfo
? `Filtered${filterTitleStr ? `: ${filterTitleStr}` : ''}`
: undefined
}
class={`
media-post
${filterInfo ? 'filtered' : ''}
${hasSpoiler ? 'has-spoiler' : ''}
${showSpoilerMedia ? 'show-media' : ''}
`}
>
<Media
class={className}
media={media}
lang={language}
to={`/${instance}/s/${id}?media-only=${i + 1}`}
onClick={
onMediaClick ? (e) => onMediaClick(e, i, media, status) : undefined
}
/>
</Parent>
);
});
}
export default memo(MediaPost);

View file

@ -1,4 +1,6 @@
import { getBlurHashAverageColor } from 'fast-blurhash';
import { Fragment } from 'preact';
import { memo } from 'preact/compat';
import {
useCallback,
useLayoutEffect,
@ -8,9 +10,12 @@ import {
} from 'preact/hooks';
import QuickPinchZoom, { make3dTransformValue } from 'react-quick-pinch-zoom';
import formatDuration from '../utils/format-duration';
import mem from '../utils/mem';
import states from '../utils/states';
import Icon from './icon';
import Link from './link';
import { formatDuration } from './status';
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); // https://stackoverflow.com/a/23522755
@ -24,8 +29,53 @@ video = Video clip
audio = Audio track
*/
function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
const {
const dataAltLabel = 'ALT';
const AltBadge = (props) => {
const { alt, lang, index, ...rest } = props;
if (!alt || !alt.trim()) return null;
return (
<button
type="button"
class="alt-badge clickable"
{...rest}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
states.showMediaAlt = {
alt,
lang,
};
}}
title="Media description"
>
{dataAltLabel}
{!!index && <sup>{index}</sup>}
</button>
);
};
const MEDIA_CAPTION_LIMIT = 140;
const MEDIA_CAPTION_LIMIT_LONGER = 280;
export const isMediaCaptionLong = mem((caption) =>
caption?.length
? caption.length > MEDIA_CAPTION_LIMIT ||
/[\n\r].*[\n\r]/.test(caption.trim())
: false,
);
function Media({
class: className = '',
media,
to,
lang,
showOriginal,
autoAnimate,
showCaption,
allowLongerCaption,
altIndex,
onClick = () => {},
}) {
let {
blurhash,
description,
meta,
@ -35,21 +85,33 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
url,
type,
} = media;
if (/no\-preview\./i.test(previewUrl)) {
previewUrl = null;
}
const { original = {}, small, focus } = meta || {};
const width = showOriginal ? original?.width : small?.width;
const height = showOriginal ? original?.height : small?.height;
const width = showOriginal
? original?.width
: small?.width || original?.width;
const height = showOriginal
? original?.height
: small?.height || original?.height;
const mediaURL = showOriginal ? url : previewUrl || url;
const remoteMediaURL = showOriginal
? remoteUrl
: previewRemoteUrl || remoteUrl;
const orientation = width >= height ? 'landscape' : 'portrait';
const hasDimensions = width && height;
const orientation = hasDimensions
? width > height
? 'landscape'
: 'portrait'
: null;
const rgbAverageColor = blurhash ? getBlurHashAverageColor(blurhash) : null;
const videoRef = useRef();
let focalBackgroundPosition;
let focalPosition;
if (focus) {
// Convert focal point to CSS background position
// Formula from jquery-focuspoint
@ -58,7 +120,7 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
// x = 1, y = -1 => 100% 100%
const x = ((focus.x + 1) / 2) * 100;
const y = ((1 - focus.y) / 2) * 100;
focalBackgroundPosition = `${x.toFixed(0)}% ${y.toFixed(0)}%`;
focalPosition = `${x.toFixed(0)}% ${y.toFixed(0)}%`;
}
const mediaRef = useRef();
@ -84,6 +146,8 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
enabled: pinchZoomEnabled,
draggableUnZoomed: false,
inertiaFriction: 0.9,
tapZoomFactor: 2,
doubleTapToggleZoom: true,
containerProps: {
className: 'media-zoom',
style: {
@ -103,7 +167,18 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
[to],
);
const isImage = type === 'image' || (type === 'unknown' && previewUrl);
const remoteMediaURLObj = remoteMediaURL ? getURLObj(remoteMediaURL) : null;
const isVideoMaybe =
type === 'unknown' &&
remoteMediaURLObj &&
/\.(mp4|m4r|m4v|mov|webm)$/i.test(remoteMediaURLObj.pathname);
const isAudioMaybe =
type === 'unknown' &&
remoteMediaURLObj &&
/\.(mp3|ogg|wav|m4a|m4p|m4b)$/i.test(remoteMediaURLObj.pathname);
const isImage =
type === 'image' ||
(type === 'unknown' && previewUrl && !isVideoMaybe && !isAudioMaybe);
const parentRef = useRef();
const [imageSmallerThanParent, setImageSmallerThanParent] = useState(false);
@ -116,6 +191,66 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
if (smaller) setImageSmallerThanParent(smaller);
}, [width, height]);
const maxAspectHeight =
window.innerHeight * (orientation === 'portrait' ? 0.45 : 0.33);
const maxHeight = orientation === 'portrait' ? 0 : 160;
const averageColorStyle = {
'--average-color': rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
};
const mediaStyles =
width && height
? {
'--width': `${width}px`,
'--height': `${height}px`,
// Calculate '--aspectWidth' based on aspect ratio calculated from '--width' and '--height', max height has to be 160px
'--aspectWidth': `${
(width / height) * Math.max(maxHeight, maxAspectHeight)
}px`,
aspectRatio: `${width} / ${height}`,
...averageColorStyle,
}
: {
...averageColorStyle,
};
const longDesc = isMediaCaptionLong(description);
let showInlineDesc =
!!showCaption && !showOriginal && !!description && !longDesc;
if (
allowLongerCaption &&
!showInlineDesc &&
description?.length <= MEDIA_CAPTION_LIMIT_LONGER
) {
showInlineDesc = true;
}
const Figure = !showInlineDesc
? Fragment
: (props) => {
const { children, ...restProps } = props;
return (
<figure {...restProps}>
{children}
<figcaption
class="media-caption"
lang={lang}
dir="auto"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
states.showMediaAlt = {
alt: description,
lang,
};
}}
>
{description}
</figcaption>
</figure>
);
};
const [hasNaturalAspectRatio, setHasNaturalAspectRatio] = useState(undefined);
if (isImage) {
// Note: type: unknown might not have width/height
quickPinchZoomProps.containerProps.style.display = 'inherit';
@ -134,71 +269,139 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
}, [mediaURL]);
return (
<Parent
ref={parentRef}
class={`media media-image`}
onClick={onClick}
style={
showOriginal && {
backgroundImage: `url(${previewUrl})`,
backgroundSize: imageSmallerThanParent
? `${width}px ${height}px`
: undefined,
}
}
>
{showOriginal ? (
<QuickPinchZoom {...quickPinchZoomProps}>
<img
ref={mediaRef}
src={mediaURL}
alt={description}
width={width}
height={height}
data-orientation={orientation}
loading="eager"
decoding="sync"
onLoad={(e) => {
e.target.closest('.media-image').style.backgroundImage = '';
e.target.closest('.media-zoom').style.display = '';
setPinchZoomEnabled(true);
}}
onError={(e) => {
const { src } = e.target;
if (src === mediaURL) {
e.target.src = remoteMediaURL;
<Figure>
<Parent
ref={parentRef}
class={`media media-image ${className}`}
onClick={onClick}
data-orientation={orientation}
data-has-alt={!showInlineDesc || undefined}
data-has-natural-aspect-ratio={hasNaturalAspectRatio || undefined}
style={
showOriginal
? {
backgroundImage: `url(${previewUrl})`,
backgroundSize: imageSmallerThanParent
? `${width}px ${height}px`
: undefined,
...averageColorStyle,
}
}}
/>
</QuickPinchZoom>
) : (
<img
src={mediaURL}
alt={description}
width={width}
height={height}
data-orientation={orientation}
loading="lazy"
style={{
backgroundColor:
rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
backgroundPosition: focalBackgroundPosition || 'center',
}}
onLoad={(e) => {
e.target.closest('.media-image').style.backgroundImage = '';
e.target.dataset.loaded = true;
}}
onError={(e) => {
const { src } = e.target;
if (src === mediaURL) {
e.target.src = remoteMediaURL;
}
}}
/>
)}
</Parent>
: mediaStyles
}
>
{showOriginal ? (
<QuickPinchZoom {...quickPinchZoomProps}>
<img
ref={mediaRef}
src={mediaURL}
alt={description}
width={width}
height={height}
data-orientation={orientation}
loading="eager"
decoding="sync"
onLoad={(e) => {
e.target.closest('.media-image').style.backgroundImage = '';
e.target.closest('.media-zoom').style.display = '';
setPinchZoomEnabled(true);
}}
onError={(e) => {
const { src } = e.target;
if (
src === mediaURL &&
remoteMediaURL &&
mediaURL !== remoteMediaURL
) {
e.target.src = remoteMediaURL;
}
}}
/>
</QuickPinchZoom>
) : (
<>
<img
src={mediaURL}
alt={showInlineDesc ? '' : description}
width={width}
height={height}
data-orientation={orientation}
loading="lazy"
style={{
// backgroundColor:
// rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
// backgroundPosition: focalBackgroundPosition || 'center',
// Duration based on width or height in pixels
objectPosition: focalPosition || 'center',
// 100px per second (rough estimate)
// Clamp between 5s and 120s
'--anim-duration': `${Math.min(
Math.max(Math.max(width, height) / 100, 5),
120,
)}s`,
}}
onLoad={(e) => {
// e.target.closest('.media-image').style.backgroundImage = '';
e.target.dataset.loaded = true;
const $media = e.target.closest('.media');
if (!hasDimensions && $media) {
const { naturalWidth, naturalHeight } = e.target;
$media.dataset.orientation =
naturalWidth > naturalHeight ? 'landscape' : 'portrait';
$media.style.setProperty('--width', `${naturalWidth}px`);
$media.style.setProperty('--height', `${naturalHeight}px`);
$media.style.aspectRatio = `${naturalWidth}/${naturalHeight}`;
}
// Check natural aspect ratio vs display aspect ratio
if ($media) {
const {
clientWidth,
clientHeight,
naturalWidth,
naturalHeight,
} = e.target;
if (
clientWidth &&
clientHeight &&
naturalWidth &&
naturalHeight
) {
const minDimension = 88;
if (
naturalWidth < minDimension ||
naturalHeight < minDimension
) {
$media.dataset.hasSmallDimension = true;
} else {
const displayNaturalHeight =
(naturalHeight * clientWidth) / naturalWidth;
const almostSimilarHeight =
Math.abs(displayNaturalHeight - clientHeight) < 5;
if (almostSimilarHeight) {
setHasNaturalAspectRatio(true);
}
}
}
}
}}
onError={(e) => {
const { src } = e.target;
if (src === mediaURL && mediaURL !== remoteMediaURL) {
e.target.src = remoteMediaURL;
}
}}
/>
{!showInlineDesc && (
<AltBadge alt={description} lang={lang} index={altIndex} />
)}
</>
)}
</Parent>
</Figure>
);
} else if (type === 'gifv' || type === 'video') {
} else if (type === 'gifv' || type === 'video' || isVideoMaybe) {
const hasDuration = original.duration > 0;
const shortDuration = original.duration < 31;
const isGIF = type === 'gifv' && shortDuration;
// If GIF is too long, treat it as a video
@ -206,135 +409,282 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
const formattedDuration = formatDuration(original.duration);
const hoverAnimate = !showOriginal && !autoAnimate && isGIF;
const autoGIFAnimate = !showOriginal && autoAnimate && isGIF;
const showProgress = original.duration > 5;
const videoHTML = `
<video
src="${url}"
poster="${previewUrl}"
width="${width}"
height="${height}"
data-orientation="${orientation}"
preload="auto"
autoplay
muted="${isGIF}"
${isGIF ? '' : 'controls'}
playsinline
loop="${loopable}"
${isGIF ? 'ondblclick="this.paused ? this.play() : this.pause()"' : ''}
></video>
// This string is only for autoplay + muted to work on Mobile Safari
const gifHTML = `
<video
src="${url}"
poster="${previewUrl}"
width="${width}"
height="${height}"
data-orientation="${orientation}"
preload="auto"
autoplay
muted
playsinline
${loopable ? 'loop' : ''}
ondblclick="this.paused ? this.play() : this.pause()"
${
showProgress
? "ontimeupdate=\"this.closest('.media-gif') && this.closest('.media-gif').style.setProperty('--progress', `${~~((this.currentTime / this.duration) * 100)}%`)\""
: ''
}
></video>
`;
const videoHTML = `
<video
src="${url}"
poster="${previewUrl}"
width="${width}"
height="${height}"
data-orientation="${orientation}"
preload="auto"
autoplay
playsinline
${loopable ? 'loop' : ''}
controls
></video>
`;
return (
<Parent
class={`media media-${isGIF ? 'gif' : 'video'} ${
autoGIFAnimate ? 'media-contain' : ''
}`}
data-formatted-duration={formattedDuration}
data-label={isGIF && !showOriginal && !autoGIFAnimate ? 'GIF' : ''}
// style={{
// backgroundColor:
// rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
// }}
onClick={(e) => {
if (hoverAnimate) {
try {
videoRef.current.pause();
} catch (e) {}
<Figure>
<Parent
class={`media ${className} media-${isGIF ? 'gif' : 'video'} ${
autoGIFAnimate ? 'media-contain' : ''
} ${hoverAnimate ? 'media-hover-animate' : ''}`}
data-orientation={orientation}
data-formatted-duration={
!showOriginal ? formattedDuration : undefined
}
onClick(e);
}}
onMouseEnter={() => {
if (hoverAnimate) {
try {
videoRef.current.play();
} catch (e) {}
data-label={
isGIF && !showOriginal && !autoGIFAnimate ? 'GIF' : undefined
}
}}
onMouseLeave={() => {
if (hoverAnimate) {
try {
videoRef.current.pause();
} catch (e) {}
}
}}
>
{showOriginal || autoGIFAnimate ? (
isGIF && showOriginal ? (
<QuickPinchZoom {...quickPinchZoomProps} enabled>
data-has-alt={!showInlineDesc || undefined}
// style={{
// backgroundColor:
// rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
// }}
style={!showOriginal && mediaStyles}
onClick={(e) => {
if (hoverAnimate) {
try {
videoRef.current.pause();
} catch (e) {}
}
onClick(e);
}}
onMouseEnter={() => {
if (hoverAnimate) {
try {
videoRef.current.play();
} catch (e) {}
}
}}
onMouseLeave={() => {
if (hoverAnimate) {
try {
videoRef.current.pause();
} catch (e) {}
}
}}
onFocus={() => {
if (hoverAnimate) {
try {
videoRef.current.play();
} catch (e) {}
}
}}
onBlur={() => {
if (hoverAnimate) {
try {
videoRef.current.pause();
} catch (e) {}
}
}}
>
{showOriginal || autoGIFAnimate ? (
isGIF && showOriginal ? (
<QuickPinchZoom {...quickPinchZoomProps} enabled>
<div
ref={mediaRef}
dangerouslySetInnerHTML={{
__html: gifHTML,
}}
/>
</QuickPinchZoom>
) : isGIF ? (
<div
ref={mediaRef}
class="video-container"
dangerouslySetInnerHTML={{
__html: videoHTML,
__html: gifHTML,
}}
/>
</QuickPinchZoom>
) : (
<div
class="video-container"
dangerouslySetInnerHTML={{
__html: videoHTML,
}}
) : (
<div
class="video-container"
dangerouslySetInnerHTML={{ __html: videoHTML }}
/>
)
) : isGIF ? (
<video
ref={videoRef}
src={url}
poster={previewUrl}
width={width}
height={height}
data-orientation={orientation}
preload="auto"
// controls
playsinline
loop
muted
onTimeUpdate={
showProgress
? (e) => {
const { target } = e;
const container = target?.closest('.media-gif');
if (container) {
const percentage =
(target.currentTime / target.duration) * 100;
container.style.setProperty(
'--progress',
`${percentage}%`,
);
}
}
: undefined
}
/>
)
) : isGIF ? (
<video
ref={videoRef}
src={url}
poster={previewUrl}
width={width}
height={height}
data-orientation={orientation}
preload="auto"
// controls
playsinline
loop
muted
/>
) : (
<>
) : (
<>
{previewUrl ? (
<img
src={previewUrl}
alt={showInlineDesc ? '' : description}
width={width}
height={height}
data-orientation={orientation}
loading="lazy"
decoding="async"
onLoad={(e) => {
if (!hasDimensions) {
const $media = e.target.closest('.media');
if ($media) {
const { naturalHeight, naturalWidth } = e.target;
$media.dataset.orientation =
naturalWidth > naturalHeight
? 'landscape'
: 'portrait';
$media.style.setProperty(
'--width',
`${naturalWidth}px`,
);
$media.style.setProperty(
'--height',
`${naturalHeight}px`,
);
$media.style.aspectRatio = `${naturalWidth}/${naturalHeight}`;
}
}
}}
/>
) : (
<video
src={url + '#t=0.1'} // Make Safari show 1st-frame preview
width={width}
height={height}
data-orientation={orientation}
preload="metadata"
muted
disablePictureInPicture
onLoadedMetadata={(e) => {
if (!hasDuration) {
const { duration } = e.target;
if (duration) {
const formattedDuration = formatDuration(duration);
const container = e.target.closest('.media-video');
if (container) {
container.dataset.formattedDuration =
formattedDuration;
}
}
}
}}
/>
)}
<div class="media-play">
<Icon icon="play" size="xl" />
</div>
</>
)}
{!showOriginal && !showInlineDesc && (
<AltBadge alt={description} lang={lang} index={altIndex} />
)}
</Parent>
</Figure>
);
} else if (type === 'audio' || isAudioMaybe) {
const formattedDuration = formatDuration(original.duration);
return (
<Figure>
<Parent
class={`media media-audio ${className}`}
data-formatted-duration={
!showOriginal ? formattedDuration : undefined
}
data-has-alt={!showInlineDesc || undefined}
onClick={onClick}
style={!showOriginal && mediaStyles}
>
{showOriginal ? (
<audio src={remoteUrl || url} preload="none" controls autoPlay />
) : previewUrl ? (
<img
src={previewUrl}
alt={description}
alt={showInlineDesc ? '' : description}
width={width}
height={height}
data-orientation={orientation}
loading="lazy"
onError={(e) => {
try {
// Remove self if broken
e.target?.remove?.();
} catch (e) {}
}}
/>
<div class="media-play">
<Icon icon="play" size="xxl" />
</div>
</>
)}
</Parent>
);
} else if (type === 'audio') {
const formattedDuration = formatDuration(original.duration);
return (
<Parent
class="media media-audio"
data-formatted-duration={formattedDuration}
onClick={onClick}
>
{showOriginal ? (
<audio src={remoteUrl || url} preload="none" controls autoplay />
) : previewUrl ? (
<img
src={previewUrl}
alt={description}
width={width}
height={height}
data-orientation={orientation}
loading="lazy"
/>
) : null}
{!showOriginal && (
<div class="media-play">
<Icon icon="play" size="xxl" />
</div>
)}
</Parent>
) : null}
{!showOriginal && (
<>
<div class="media-play">
<Icon icon="play" size="xl" />
</div>
{!showInlineDesc && (
<AltBadge alt={description} lang={lang} index={altIndex} />
)}
</>
)}
</Parent>
</Figure>
);
}
}
export default Media;
function getURLObj(url) {
// Fake base URL if url doesn't have https:// prefix
return URL.parse(url, location.origin);
}
export default memo(Media, (oldProps, newProps) => {
const oldMedia = oldProps.media || {};
const newMedia = newProps.media || {};
return (
oldMedia?.id === newMedia?.id &&
oldMedia.url === newMedia.url &&
oldProps.to === newProps.to &&
oldProps.class === newProps.class
);
});

View file

@ -0,0 +1,48 @@
import { MenuItem } from '@szhsin/react-menu';
import { cloneElement } from 'preact';
import Menu2 from './menu2';
import SubMenu2 from './submenu2';
function MenuConfirm({
subMenu = false,
confirm = true,
confirmLabel,
menuItemClassName,
menuFooter,
menuExtras,
...props
}) {
const { children, onClick, ...restProps } = props;
if (!confirm) {
if (subMenu) return <MenuItem {...props} />;
if (onClick) {
return cloneElement(children, {
onClick,
});
}
return children;
}
const Parent = subMenu ? SubMenu2 : Menu2;
return (
<Parent
openTrigger="clickOnly"
direction="bottom"
overflow="auto"
gap={-8}
shift={8}
menuClassName="menu-emphasized"
{...restProps}
menuButton={subMenu ? undefined : children}
label={subMenu ? children : undefined}
>
<MenuItem className={menuItemClassName} onClick={onClick}>
{confirmLabel}
</MenuItem>
{menuExtras}
{menuFooter}
</Parent>
);
}
export default MenuConfirm;

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

@ -1,19 +1,33 @@
import { Menu } from '@szhsin/react-menu';
import { useWindowSize } from '@uidotdev/usehooks';
import { useRef } from 'preact/hooks';
import isRTL from '../utils/is-rtl';
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
import useWindowSize from '../utils/useWindowSize';
// It's like Menu but with sensible defaults, bug fixes and improvements.
function Menu2(props) {
const { containerProps } = props;
const { containerProps, instanceRef: _instanceRef, align } = props;
const size = useWindowSize();
const instanceRef = useRef();
const instanceRef = _instanceRef?.current ? _instanceRef : useRef();
// Values: start, end, center
// Note: don't mess with 'center'
const rtlAlign = isRTL()
? align === 'end'
? 'start'
: align === 'start'
? 'end'
: align
: align;
return (
<Menu
boundingBoxPadding={safeBoundingBoxPadding()}
repositionFlag={`${size.width}x${size.height}`}
unmountOnClose
{...props}
align={rtlAlign}
instanceRef={instanceRef}
containerProps={{
onClick: (e) => {

View file

@ -1,7 +1,7 @@
#modal-container > div {
position: fixed;
top: 0;
right: 0;
inset-inline-end: 0;
height: 100%;
width: 100%;
z-index: 1000;
@ -9,10 +9,66 @@
justify-content: center;
align-items: center;
background-color: var(--backdrop-color);
backdrop-filter: blur(24px);
animation: appear 0.5s var(--timing-function) both;
transition: all 0.5s var(--timing-function);
&.solid {
background-color: var(--backdrop-solid-color);
}
--compose-button-dimension: 56px;
--compose-button-dimension-half: calc(var(--compose-button-dimension) / 2);
--compose-button-dimension-margin: 16px;
&.min {
/* Minimized */
pointer-events: none;
user-select: none;
overflow: hidden;
transform: scale(0);
--end: max(
var(--compose-button-dimension-margin),
env(safe-area-inset-right)
);
:dir(rtl) & {
--end: max(
var(--compose-button-dimension-margin),
env(safe-area-inset-left)
);
}
--bottom: max(
var(--compose-button-dimension-margin),
env(safe-area-inset-bottom)
);
--origin-end: calc(
100% - var(--compose-button-dimension-half) - var(--end)
);
:dir(rtl) & {
--origin-end: calc(var(--compose-button-dimension-half) + var(--end));
}
--origin-bottom: calc(
100% - var(--compose-button-dimension-half) - var(--bottom)
);
transform-origin: var(--origin-end) var(--origin-bottom);
}
.sheet {
transition: transform 0.3s var(--timing-function);
transform-origin: 80% 80%;
}
&:has(~ div) .sheet {
transform: scale(0.975);
}
}
#modal-container > .light {
backdrop-filter: saturate(0.75);
@media (max-width: calc(40em - 1px)) {
#app[data-shortcuts-view-mode='tab-menu-bar'] ~ #modal-container > div.min {
border: 2px solid red;
--bottom: calc(
var(--compose-button-dimension-margin) + env(safe-area-inset-bottom) +
52px
);
}
}

View file

@ -2,10 +2,13 @@ import './modal.css';
import { createPortal } from 'preact/compat';
import { useEffect, useRef } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import useCloseWatcher from '../utils/useCloseWatcher';
const $modalContainer = document.getElementById('modal-container');
function Modal({ children, onClick, class: className }) {
function Modal({ children, onClose, onClick, class: className, minimized }) {
if (!children) return null;
const modalRef = useRef();
@ -19,8 +22,84 @@ function Modal({ children, onClick, class: className }) {
return () => clearTimeout(timer);
}, []);
const supportsCloseWatcher = window.CloseWatcher;
const escRef = useHotkeys(
'esc',
() => {
setTimeout(() => {
onClose?.();
}, 0);
},
{
enabled: !supportsCloseWatcher && !!onClose,
// Using keyup and setTimeout above
// This will run "later" to prevent clash with esc handlers from other components
keydown: false,
keyup: true,
},
[onClose],
);
useCloseWatcher(onClose, [onClose]);
useEffect(() => {
const $deckContainers = document.querySelectorAll('.deck-container');
if (minimized) {
// Similar to focusDeck in focus-deck.jsx
// Focus last deck
const page = $deckContainers[$deckContainers.length - 1]; // last one
if (page && page.tabIndex === -1) {
page.focus();
}
} else {
if (children) {
$deckContainers.forEach(($deckContainer) => {
$deckContainer.setAttribute('inert', '');
});
} else {
$deckContainers.forEach(($deckContainer) => {
$deckContainer.removeAttribute('inert');
});
}
}
return () => {
$deckContainers.forEach(($deckContainer) => {
$deckContainer.removeAttribute('inert');
});
};
}, [children, minimized]);
const Modal = (
<div ref={modalRef} className={className} onClick={onClick}>
<div
ref={(node) => {
modalRef.current = node;
escRef.current = node?.querySelector?.('[tabindex="-1"]') || node;
}}
className={className}
onClick={(e) => {
onClick?.(e);
if (e.target === e.currentTarget) {
onClose?.(e);
}
}}
tabIndex={minimized ? 0 : '-1'}
inert={minimized}
onFocus={(e) => {
try {
if (e.target === e.currentTarget) {
const focusElement =
modalRef.current?.querySelector('[tabindex="-1"]');
const isFocusable =
!!focusElement &&
getComputedStyle(focusElement)?.pointerEvents !== 'none';
if (focusElement && isFocusable) {
focusElement.focus();
}
}
} catch (err) {
console.error(err);
}
}}
>
{children}
</div>
);

246
src/components/modals.jsx Normal file
View file

@ -0,0 +1,246 @@
import { useEffect } from 'preact/hooks';
import { useLocation, useNavigate } from 'react-router-dom';
import { subscribe, useSnapshot } from 'valtio';
import Accounts from '../pages/accounts';
import Settings from '../pages/settings';
import focusDeck from '../utils/focus-deck';
import showToast from '../utils/show-toast';
import states from '../utils/states';
import AccountSheet from './account-sheet';
import ComposeSuspense, { preload } from './compose-suspense';
import Drafts from './drafts';
import EmbedModal from './embed-modal';
import GenericAccounts from './generic-accounts';
import MediaAltModal from './media-alt-modal';
import MediaModal from './media-modal';
import Modal from './modal';
import ReportModal from './report-modal';
import ShortcutsSettings from './shortcuts-settings';
subscribe(states, (changes) => {
for (const [action, path, value, prevValue] of changes) {
// When closing modal, focus on deck
if (/^show/i.test(path) && !value) {
focusDeck();
}
}
});
export default function Modals() {
const snapStates = useSnapshot(states);
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
setTimeout(preload, 1000);
}, []);
return (
<>
{!!snapStates.showCompose && (
<Modal
class={`solid ${snapStates.composerState.minimized ? 'min' : ''}`}
minimized={!!snapStates.composerState.minimized}
>
<ComposeSuspense
replyToStatus={
typeof snapStates.showCompose !== 'boolean'
? snapStates.showCompose.replyToStatus
: window.__COMPOSE__?.replyToStatus || null
}
editStatus={
states.showCompose?.editStatus ||
window.__COMPOSE__?.editStatus ||
null
}
draftStatus={
states.showCompose?.draftStatus ||
window.__COMPOSE__?.draftStatus ||
null
}
onClose={(results) => {
const { newStatus, instance, type } = results || {};
states.showCompose = false;
window.__COMPOSE__ = null;
if (newStatus) {
states.reloadStatusPage++;
showToast({
text: {
post: 'Post published. Check it out.',
reply: 'Reply posted. Check it out.',
edit: 'Post updated. Check it out.',
}[type || 'post'],
delay: 1000,
duration: 10_000, // 10 seconds
onClick: (toast) => {
toast.hideToast();
states.prevLocation = location;
navigate(
instance
? `/${instance}/s/${newStatus.id}`
: `/s/${newStatus.id}`,
);
},
});
}
}}
/>
</Modal>
)}
{!!snapStates.showSettings && (
<Modal
onClose={() => {
states.showSettings = false;
}}
>
<Settings
onClose={() => {
states.showSettings = false;
}}
/>
</Modal>
)}
{!!snapStates.showAccounts && (
<Modal
onClose={() => {
states.showAccounts = false;
}}
>
<Accounts
onClose={() => {
states.showAccounts = false;
}}
/>
</Modal>
)}
{!!snapStates.showAccount && (
<Modal
onClose={() => {
states.showAccount = false;
}}
>
<AccountSheet
account={snapStates.showAccount?.account || snapStates.showAccount}
instance={snapStates.showAccount?.instance}
onClose={({ destination } = {}) => {
states.showAccount = false;
// states.showGenericAccounts = false;
// if (destination) {
// states.showAccounts = false;
// }
}}
/>
</Modal>
)}
{!!snapStates.showDrafts && (
<Modal
onClose={() => {
states.showDrafts = false;
}}
>
<Drafts onClose={() => (states.showDrafts = false)} />
</Modal>
)}
{!!snapStates.showMediaModal && (
<Modal
onClick={(e) => {
if (
e.target === e.currentTarget ||
e.target.classList.contains('media')
) {
states.showMediaModal = false;
}
}}
>
<MediaModal
mediaAttachments={snapStates.showMediaModal.mediaAttachments}
instance={snapStates.showMediaModal.instance}
index={snapStates.showMediaModal.index}
statusID={snapStates.showMediaModal.statusID}
onClose={() => {
states.showMediaModal = false;
}}
/>
</Modal>
)}
{!!snapStates.showShortcutsSettings && (
<Modal
onClose={() => {
states.showShortcutsSettings = false;
}}
>
<ShortcutsSettings
onClose={() => (states.showShortcutsSettings = false)}
/>
</Modal>
)}
{!!snapStates.showGenericAccounts && (
<Modal
onClose={() => {
states.showGenericAccounts = false;
}}
>
<GenericAccounts
instance={snapStates.showGenericAccounts.instance}
excludeRelationshipAttrs={
snapStates.showGenericAccounts.excludeRelationshipAttrs
}
postID={snapStates.showGenericAccounts.postID}
onClose={() => (states.showGenericAccounts = false)}
blankCopy={snapStates.showGenericAccounts.blankCopy}
/>
</Modal>
)}
{!!snapStates.showMediaAlt && (
<Modal
onClose={(e) => {
states.showMediaAlt = false;
}}
>
<MediaAltModal
alt={snapStates.showMediaAlt.alt || snapStates.showMediaAlt}
lang={snapStates.showMediaAlt?.lang}
onClose={() => {
states.showMediaAlt = false;
}}
/>
</Modal>
)}
{!!snapStates.showEmbedModal && (
<Modal
class="solid"
onClose={() => {
states.showEmbedModal = false;
}}
>
<EmbedModal
html={snapStates.showEmbedModal.html}
url={snapStates.showEmbedModal.url}
width={snapStates.showEmbedModal.width}
height={snapStates.showEmbedModal.height}
onClose={() => {
states.showEmbedModal = false;
}}
/>
</Modal>
)}
{!!snapStates.showReportModal && (
<Modal
onClose={() => {
states.showReportModal = false;
}}
>
<ReportModal
account={snapStates.showReportModal.account}
post={snapStates.showReportModal.post}
onClose={() => {
states.showReportModal = false;
}}
/>
</Modal>
)}
</>
);
}

View file

@ -2,6 +2,17 @@
color: inherit;
text-decoration: none;
display: inline;
unicode-bidi: isolate;
b {
font-weight: 600;
unicode-bidi: isolate;
}
i {
font-variant-numeric: slashed-zero;
font-feature-settings: 'ss01';
}
}
.name-text.show-acct {
display: inline-block;
@ -15,6 +26,9 @@ a.name-text.short:is(:hover, :focus) i {
font-style: normal;
opacity: 0.75;
}
.name-text i.instance {
opacity: 0.35;
}
.name-text .avatar {
vertical-align: middle;

View file

@ -1,10 +1,17 @@
import './name-text.css';
import { memo } from 'preact/compat';
import { api } from '../utils/api';
import states from '../utils/states';
import Avatar from './avatar';
import EmojiText from './emoji-text';
const nameCollator = new Intl.Collator('en', {
sensitivity: 'base',
});
function NameText({
account,
instance,
@ -14,35 +21,60 @@ function NameText({
external,
onClick,
}) {
const { acct, avatar, avatarStatic, id, url, displayName, emojis, bot } =
account;
let { username } = account;
const {
acct,
avatar,
avatarStatic,
id,
url,
displayName,
emojis,
bot,
username,
} = account;
const [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct];
if (!instance) instance = api().instance;
const trimmedUsername = username.toLowerCase().trim();
const trimmedDisplayName = (displayName || '').toLowerCase().trim();
const shortenedDisplayName = trimmedDisplayName
.replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1
.replace(/\s+/g, '') // E.g. "My name" === "myname"
.replace(/[^a-z0-9]/gi, ''); // Remove non-alphanumeric characters
.replace(/\s+/g, ''); // E.g. "My name" === "myname"
const shortenedAlphaNumericDisplayName = shortenedDisplayName.replace(
/[^a-z0-9@\.]/gi,
'',
); // Remove non-alphanumeric characters
if (
!short &&
(trimmedUsername === trimmedDisplayName ||
trimmedUsername === shortenedDisplayName)
) {
username = null;
}
const hideUsername =
(!short &&
(trimmedUsername === trimmedDisplayName ||
trimmedUsername === shortenedDisplayName ||
trimmedUsername === shortenedAlphaNumericDisplayName ||
nameCollator.compare(trimmedUsername, shortenedDisplayName) === 0)) ||
shortenedAlphaNumericDisplayName === acct.toLowerCase();
return (
<a
class={`name-text ${showAcct ? 'show-acct' : ''} ${short ? 'short' : ''}`}
href={url}
target={external ? '_blank' : null}
title={`@${acct}`}
title={
displayName
? `${displayName} (${acct2 ? '' : '@'}${acct})`
: `${acct2 ? '' : '@'}${acct}`
}
onClick={(e) => {
if (external) return;
if (e.shiftKey) return; // Save link? 🤷
e.preventDefault();
e.stopPropagation();
if (onClick) return onClick(e);
if (e.metaKey || e.ctrlKey || e.shiftKey || e.which === 2) {
const internalURL = `#/${instance}/a/${id}`;
window.open(internalURL, '_blank');
return;
}
states.showAccount = {
account,
instance,
@ -56,29 +88,39 @@ function NameText({
)}
{displayName && !short ? (
<>
<b>
<b dir="auto">
<EmojiText text={displayName} emojis={emojis} />
</b>
{!showAcct && username && (
{!showAcct && !hideUsername ? (
<>
{' '}
<i>@{username}</i>
<i class="bidi-isolate">@{username}</i>
</>
)}
) : ' '}
<i class="instance">{acct2}</i>
</>
) : short ? (
<i>@{username}</i>
<i>{username}</i>
) : (
<b>@{username}</b>
<b>{username}</b>
)}
{showAcct && (
<>
<br />
<i>@{acct}</i>
<i class="bidi-isolate">
{acct2 ? '' : '@'}
{acct1}
{!!acct2 && <span class="ib">{acct2}</span>}
</i>
</>
)}
</a>
);
}
export default NameText;
export default memo(NameText, (oldProps, newProps) => {
// Only care about account.id, the other props usually don't change
const { account } = oldProps;
const { account: newAccount } = newProps;
return account?.acct === newAccount?.acct;
});

View file

@ -1,12 +1,29 @@
.nav-menu section:last-child {
background-color: var(--bg-faded-color);
margin-bottom: -8px;
padding-bottom: 8px;
}
@media (min-width: 23em) {
.nav-menu {
display: flex;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto 1fr;
grid-template-areas:
'top top'
'left right';
padding: 0;
width: 22em;
max-width: calc(100vw - 16px);
}
.nav-menu .top-menu {
grid-area: top;
padding-top: 8px;
margin-bottom: -8px;
}
.nav-menu section {
padding: 8px 0;
width: 50%;
/* width: 50%; */
}
@keyframes phanpying {
0% {
@ -17,13 +34,16 @@
}
}
.nav-menu section:last-child {
background-color: var(--bg-faded-color);
background-image: linear-gradient(
to right,
var(--to-forward),
var(--divider-color) 1px,
transparent 1px
),
linear-gradient(to bottom left, var(--bg-blur-color), transparent),
linear-gradient(
to bottom var(--backward),
var(--bg-blur-color),
transparent
),
url(../assets/phanpy-bg.svg);
background-repeat: no-repeat;
/* background-size: auto, auto, 200%; */
@ -33,6 +53,17 @@
position: sticky;
top: 0;
animation: phanpying 0.2s ease-in-out both;
border-start-end-radius: inherit;
border-end-end-radius: inherit;
margin-bottom: 0;
display: flex;
flex-direction: column;
.divider-grow {
flex-grow: 1;
height: auto;
background-color: transparent;
}
}
.nav-menu section:last-child > .szh-menu__divider:first-child {
display: none;
@ -47,3 +78,21 @@
width: 28em;
}
}
@keyframes sparkle-icon {
0% {
transform: scale(1);
color: var(--red-color);
}
100% {
transform: scale(1.2);
color: var(--orange-color);
}
}
.sparkle-icon {
animation: sparkle-icon 0.3s ease-in-out infinite alternate;
}
.nav-submenu {
max-width: 14em;
}

View file

@ -1,33 +1,35 @@
import './nav-menu.css';
import { ControlledMenu, MenuDivider, MenuItem } from '@szhsin/react-menu';
import { useEffect, useRef, useState } from 'preact/hooks';
import { ControlledMenu, FocusableItem, MenuDivider, MenuItem } from '@szhsin/react-menu';
import { memo } from 'preact/compat';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useLongPress } from 'use-long-press';
import { useSnapshot } from 'valtio';
import { api } from '../utils/api';
import { getLists } from '../utils/lists';
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
import states from '../utils/states';
import store from '../utils/store';
import { getCurrentAccountID } from '../utils/store-utils';
import supports from '../utils/supports';
import Avatar from './avatar';
import Icon from './icon';
import MenuLink from './menu-link';
import SubMenu2 from './submenu2';
import { accountsIsDtth, gtsDtthSettings } from '../utils/dtth';
function NavMenu(props) {
const snapStates = useSnapshot(states);
const { instance, authenticated } = api();
const { masto, instance, authenticated } = api();
const [currentAccount, setCurrentAccount] = useState();
const [moreThanOneAccount, setMoreThanOneAccount] = useState(false);
useEffect(() => {
const [currentAccount, moreThanOneAccount] = useMemo(() => {
const accounts = store.local.getJSON('accounts') || [];
const acc = accounts.find(
(account) => account.info.id === store.session.get('currentAccount'),
);
if (acc) setCurrentAccount(acc);
setMoreThanOneAccount(accounts.length > 1);
const acc =
accounts.find((account) => account.info.id === getCurrentAccountID()) ||
accounts[0];
return [acc, accounts.length > 1];
}, []);
// Home = Following
@ -35,8 +37,9 @@ function NavMenu(props) {
// User may choose pin or not to pin Following
// If user doesn't pin Following, we show it in the menu
const showFollowing =
(snapStates.settings.shortcutsColumnsMode ||
snapStates.settings.shortcutsViewMode === 'multi-column') &&
(snapStates.settings.shortcutsViewMode === 'multi-column' ||
(!snapStates.settings.shortcutsViewMode &&
snapStates.settings.shortcutsColumnsMode)) &&
!snapStates.shortcuts.find((pin) => pin.type === 'following');
const bindLongPress = useLongPress(
@ -60,6 +63,38 @@ function NavMenu(props) {
0,
]);
const mutesIterator = useRef();
async function fetchMutes(firstLoad) {
if (firstLoad || !mutesIterator.current) {
mutesIterator.current = masto.v1.mutes.list({
limit: 80,
});
}
const results = await mutesIterator.current.next();
return results;
}
const blocksIterator = useRef();
async function fetchBlocks(firstLoad) {
if (firstLoad || !blocksIterator.current) {
blocksIterator.current = masto.v1.blocks.list({
limit: 80,
});
}
const results = await blocksIterator.current.next();
return results;
}
const supportsLists = supports('@mastodon/lists');
const [lists, setLists] = useState([]);
useEffect(() => {
if (!supportsLists) return;
if (menuState === 'open') {
getLists().then(setLists);
}
}, [menuState === 'open']);
const buttonClickTS = useRef();
return (
<>
<button
@ -67,9 +102,10 @@ function NavMenu(props) {
type="button"
class={`button plain nav-menu-button ${
moreThanOneAccount ? 'with-avatar' : ''
} ${open ? 'active' : ''}`}
} ${menuState === 'open' ? 'active' : ''}`}
style={{ position: 'relative' }}
onClick={() => {
buttonClickTS.current = Date.now();
setMenuState((state) => (!state ? 'open' : undefined));
}}
onContextMenu={(e) => {
@ -101,7 +137,10 @@ function NavMenu(props) {
zIndex: 10,
},
onClick: () => {
setMenuState(undefined);
if (Date.now() - buttonClickTS.current < 300) {
return;
}
// setMenuState(undefined);
},
}}
portal={{
@ -115,41 +154,47 @@ function NavMenu(props) {
boundingBoxPadding={boundingBoxPadding}
unmountOnClose
>
{!!snapStates.appVersion?.commitHash &&
__COMMIT_HASH__ !== snapStates.appVersion.commitHash && (
<div class="top-menu">
<MenuItem
onClick={() => {
const yes = confirm('Reload page now to update?');
if (yes) {
(async () => {
try {
location.reload();
} catch (e) {}
})();
}
}}
>
<Icon icon="sparkles" class="sparkle-icon" size="l" />{' '}
<span>New update available</span>
</MenuItem>
<MenuDivider />
</div>
)}
<section>
{!!snapStates.appVersion?.commitHash &&
__COMMIT_HASH__ !== snapStates.appVersion.commitHash && (
<>
<MenuItem
onClick={() => {
const yes = confirm('Reload page now to update?');
if (yes) {
(async () => {
try {
location.reload();
} catch (e) {}
})();
}
}}
>
<Icon icon="sparkles" size="l" />{' '}
<span>New update available</span>
</MenuItem>
<MenuDivider />
</>
)}
<MenuLink to="/">
<Icon icon="home" size="l" /> <span>Home</span>
</MenuLink>
{authenticated && (
{authenticated ? (
<>
{showFollowing && (
<MenuLink to="/following">
<Icon icon="following" size="l" /> <span>Following</span>
</MenuLink>
)}
<MenuLink to="/mentions">
<Icon icon="at" size="l" /> <span>Mentions</span>
<MenuLink to="/catchup">
<Icon icon="history2" size="l" />
<span>Catch-up</span>
</MenuLink>
{supports('@mastodon/mentions') && (
<MenuLink to="/mentions">
<Icon icon="at" size="l" /> <span>Mentions</span>
</MenuLink>
)}
<MenuLink to="/notifications">
<Icon icon="notification" size="l" /> <span>Notifications</span>
{snapStates.notificationsShowNew && (
@ -159,44 +204,107 @@ 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>Favourites</span>
</MenuLink>
</>
)}
<MenuDivider />
<MenuLink to={`/search`}>
<Icon icon="search" size="l" /> <span>Search</span>
</MenuLink>
<MenuLink to={`/${instance}/p/l`}>
<Icon icon="group" 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>
)}
{currentAccount && accountsIsDtth(currentAccount) &&
<FocusableItem title="Takes you to DTTHDon settings">
<a href={gtsDtthSettings} target='_blank'><Icon icon="user-setting" size="l" /> <span>User Settings&hellip;</span></a>
</FocusableItem>}
{lists?.length > 0 ? (
<SubMenu2
menuClassName="nav-submenu"
overflow="auto"
gap={-8}
label={
<>
<Icon icon="list" size="l" />
<span class="menu-grow">Lists</span>
<Icon icon="chevron-right" />
</>
}
>
<MenuLink to="/l">
<span>All Lists</span>
</MenuLink>
{lists?.length > 0 && (
<>
<MenuDivider />
{lists.map((list) => (
<MenuLink key={list.id} to={`/l/${list.id}`}>
<span>{list.title}</span>
</MenuLink>
))}
</>
)}
</SubMenu2>
) : (
supportsLists && (
<MenuLink to="/l">
<Icon icon="list" size="l" />
<span>Lists</span>
</MenuLink>
)
)}
<MenuLink to="/b">
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
</MenuLink>
<SubMenu2
menuClassName="nav-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="/fh">
<Icon icon="hashtag" size="l" />{' '}
<span>Followed Hashtags</span>
</MenuLink>
<MenuDivider />
{supports('@mastodon/filters') && (
<MenuLink to="/ft">
<Icon icon="filters" size="l" />
Filters
</MenuLink>
)}
<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>{' '}
</SubMenu2>
<MenuDivider />
<MenuItem
onClick={() => {
states.showAccounts = true;
@ -204,13 +312,48 @@ function NavMenu(props) {
>
<Icon icon="group" size="l" /> <span>Accounts&hellip;</span>
</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={() => {
states.showKeyboardShortcutsHelp = true;
}}
>
<Icon icon="keyboard" size="l" />{' '}
<span>Keyboard shortcuts</span>
</MenuItem>
<MenuItem
onClick={() => {
states.showShortcutsSettings = true;
}}
>
<Icon icon="shortcut" size="l" />{' '}
<span>Shortcuts Settings&hellip;</span>
<span>Shortcuts / Columns&hellip;</span>
</MenuItem>
<MenuItem
onClick={() => {
@ -223,9 +366,13 @@ function NavMenu(props) {
) : (
<>
<MenuDivider />
<MenuLink to="/login">
<Icon icon="user" size="l" /> <span>Log in</span>
</MenuLink>
<MenuItem
onClick={() => {
states.showSettings = true;
}}
>
<Icon icon="gear" size="l" /> <span>Settings&hellip;</span>
</MenuItem>
</>
)}
</section>
@ -234,4 +381,4 @@ function NavMenu(props) {
);
}
export default NavMenu;
export default memo(NavMenu);

View file

@ -0,0 +1,199 @@
import { memo } from 'preact/compat';
import { useLayoutEffect, useState } from 'preact/hooks';
import { useSnapshot } from 'valtio';
import { api } from '../utils/api';
import states from '../utils/states';
import {
getAccountByAccessToken,
getCurrentAccount,
} from '../utils/store-utils';
import usePageVisibility from '../utils/usePageVisibility';
import Icon from './icon';
import Link from './link';
import Modal from './modal';
import Notification from './notification';
{
if ('serviceWorker' in navigator) {
console.log('👂👂👂 Listen to message');
navigator.serviceWorker.addEventListener('message', (event) => {
console.log('💥💥💥 Message event', event);
const { type, id, accessToken } = event?.data || {};
if (type === 'notification') {
states.routeNotification = {
id,
accessToken,
};
}
});
}
}
export default memo(function NotificationService() {
if (!('serviceWorker' in navigator)) return null;
const snapStates = useSnapshot(states);
const { routeNotification } = snapStates;
console.log('🛎️ Notification service', routeNotification);
const { id, accessToken } = routeNotification || {};
const [showNotificationSheet, setShowNotificationSheet] = useState(false);
useLayoutEffect(() => {
if (!id || !accessToken) return;
const { instance: currentInstance } = api();
const { masto, instance } = api({
accessToken,
});
console.log('API', { accessToken, currentInstance, instance });
const sameInstance = currentInstance === instance;
const account = accessToken
? getAccountByAccessToken(accessToken)
: getCurrentAccount();
(async () => {
const notification = await masto.v1.notifications.$select(id).fetch();
if (notification && account) {
console.log('🛎️ Notification', { id, notification, account });
const accountInstance = account.instanceURL;
const { type, status, account: notificationAccount } = notification;
const hasModal = !!document.querySelector('#modal-container > *');
const isFollow = type === 'follow' && !!notificationAccount?.id;
const hasAccount = !!notificationAccount?.id;
const hasStatus = !!status?.id;
if (isFollow && sameInstance) {
// Show account sheet, can handle different instances
states.showAccount = {
account: notificationAccount,
instance: accountInstance,
};
} else if (hasModal || !sameInstance || (hasAccount && hasStatus)) {
// Show sheet of notification, if
// - there is a modal open
// - the notification is from another instance
// - the notification has both account and status, gives choice for users to go to account or status
setShowNotificationSheet({
id,
account,
notification,
sameInstance,
});
} else {
if (hasStatus) {
// Go to status page
location.hash = `/${currentInstance}/s/${status.id}`;
} else if (isFollow) {
// Go to profile page
location.hash = `/${currentInstance}/a/${notificationAccount.id}`;
} else {
// Go to notifications page
location.hash = '/notifications';
}
}
} else {
console.warn('🛎️ Notification not found', id);
}
})();
}, [id, accessToken]);
// useLayoutEffect(() => {
// // Listen to message from service worker
// const handleMessage = (event) => {
// console.log('💥💥💥 Message event', event);
// const { type, id, accessToken } = event?.data || {};
// if (type === 'notification') {
// states.routeNotification = {
// id,
// accessToken,
// };
// }
// };
// console.log('👂👂👂 Listen to message');
// navigator.serviceWorker.addEventListener('message', handleMessage);
// return () => {
// console.log('👂👂👂 Remove listen to message');
// navigator.serviceWorker.removeEventListener('message', handleMessage);
// };
// }, []);
useLayoutEffect(() => {
if (navigator?.clearAppBadge) {
navigator.clearAppBadge();
}
}, []);
usePageVisibility((visible) => {
if (visible && navigator?.clearAppBadge) {
console.log('🔰 Clear app badge');
navigator.clearAppBadge();
}
});
const onClose = () => {
setShowNotificationSheet(false);
states.routeNotification = null;
// If url is #/notifications?id=123, go to #/notifications
if (/\/notifications\?id=/i.test(location.hash)) {
location.hash = '/notifications';
}
};
if (showNotificationSheet) {
const { id, account, notification, sameInstance } = showNotificationSheet;
return (
<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>Notification</b>
</header>
<main>
{!sameInstance && (
<p>This notification is from your other account.</p>
)}
<div
class="notification-peek"
// style={{
// pointerEvents: sameInstance ? '' : 'none',
// }}
onClick={(e) => {
const { target } = e;
// If button or links
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A') {
onClose();
}
}}
>
<Notification
instance={account.instanceURL}
notification={notification}
isStatic
/>
</div>
<div
style={{
textAlign: 'end',
}}
>
<Link to="/notifications" class="button light" onClick={onClose}>
<span>View all notifications</span> <Icon icon="arrow-right" />
</Link>
</div>
</main>
</div>
</Modal>
);
}
return null;
});

View file

@ -1,7 +1,14 @@
import states from '../utils/states';
import { Fragment } from 'preact';
import { memo } from 'preact/compat';
import shortenNumber from '../utils/shorten-number';
import states, { statusKey } from '../utils/states';
import store from '../utils/store';
import { getCurrentAccountID } from '../utils/store-utils';
import useTruncated from '../utils/useTruncated';
import Avatar from './avatar';
import CustomEmoji from './custom-emoji';
import FollowRequestButtons from './follow-request-buttons';
import Icon from './icon';
import Link from './link';
@ -18,6 +25,12 @@ const NOTIFICATION_ICONS = {
favourite: 'heart',
poll: 'poll',
update: 'pencil',
'admin.signup': 'account-edit',
'admin.report': 'account-warning',
severed_relationships: 'heart-break',
moderation_warning: 'alert',
emoji_reaction: 'emoji2',
'pleroma:emoji_reaction': 'emoji2',
};
/*
@ -33,63 +46,234 @@ poll = A poll you have voted in or created has ended
update = A status you interacted with has been edited
admin.sign_up = Someone signed up (optionally sent to admins)
admin.report = A new report has been filed
severed_relationships = Severed relationships
moderation_warning = Moderation warning
*/
function emojiText(emoji, emoji_url) {
let url;
let staticUrl;
if (typeof emoji_url === 'string') {
url = emoji_url;
} else {
url = emoji_url?.url;
staticUrl = emoji_url?.staticUrl;
}
return url ? (
<>
reacted to your post with{' '}
<CustomEmoji url={url} staticUrl={staticUrl} alt={emoji} />
</>
) : (
`reacted to your post with ${emoji}.`
);
}
const contentText = {
mention: 'mentioned you in their post.',
status: 'published a post.',
reblog: 'boosted your post.',
'reblog+account': (count) => `boosted ${count} of your posts.`,
reblog_reply: 'boosted your reply.',
follow: 'followed you.',
follow_request: 'requested to follow you.',
favourite: 'favourited your post.',
favourite: 'liked your post.',
'favourite+account': (count) => `liked ${count} of your posts.`,
favourite_reply: 'liked your reply.',
poll: 'A poll you have voted in or created has ended.',
'poll-self': 'A poll you have created has ended.',
'poll-voted': 'A poll you have voted in has ended.',
update: 'A post you interacted with has been edited.',
'favourite+reblog': 'boosted & favourited your post.',
'favourite+reblog': 'boosted & liked your post.',
'favourite+reblog+account': (count) =>
`boosted & liked ${count} of your posts.`,
'favourite+reblog_reply': 'boosted & liked your reply.',
'admin.sign_up': 'signed up.',
'admin.report': (targetAccount) => <>reported {targetAccount}</>,
severed_relationships: (name) => (
<>
Lost connections with <i>{name}</i>.
</>
),
moderation_warning: <b>Moderation warning</b>,
emoji_reaction: emojiText,
'pleroma:emoji_reaction': emojiText,
};
function Notification({ notification, instance, reload }) {
const { id, status, account, _accounts } = notification;
// account_suspension, domain_block, user_domain_block
const SEVERED_RELATIONSHIPS_TEXT = {
account_suspension: ({ from, targetName }) => (
<>
An admin from <i>{from}</i> has suspended <i>{targetName}</i>, which means
you can no longer receive updates from them or interact with them.
</>
),
domain_block: ({ from, targetName, followersCount, followingCount }) => (
<>
An admin from <i>{from}</i> has blocked <i>{targetName}</i>. Affected
followers: {followersCount}, followings: {followingCount}.
</>
),
user_domain_block: ({ targetName, followersCount, followingCount }) => (
<>
You have blocked <i>{targetName}</i>. Removed followers: {followersCount},
followings: {followingCount}.
</>
),
};
const MODERATION_WARNING_TEXT = {
none: 'Your account has received a moderation warning.',
disable: 'Your account has been disabled.',
mark_statuses_as_sensitive:
'Some of your posts have been marked as sensitive.',
delete_statuses: 'Some of your posts have been deleted.',
sensitive: 'Your posts will be marked as sensitive from now on.',
silence: 'Your account has been limited.',
suspend: 'Your account has been suspended.',
};
const AVATARS_LIMIT = 30;
function Notification({
notification,
instance,
isStatic,
disableContextMenu,
}) {
const {
id,
status,
account,
report,
event,
moderation_warning,
// Client-side grouped notification
_ids,
_accounts,
_statuses,
// Server-side grouped notification
sampleAccounts,
notificationsCount,
} = notification;
let { type } = notification;
// status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update
const actualStatusID = status?.reblog?.id || status?.id;
const actualStatus = status?.reblog || status;
const actualStatusID = actualStatus?.id;
const currentAccount = store.session.get('currentAccount');
const currentAccount = getCurrentAccountID();
const isSelf = currentAccount === account?.id;
const isVoted = status?.poll?.voted;
const isReplyToOthers =
!!status?.inReplyToAccountId &&
status?.inReplyToAccountId !== currentAccount &&
status?.account?.id === currentAccount;
let favsCount = 0;
let reblogsCount = 0;
if (type === 'favourite+reblog') {
for (const account of _accounts) {
if (account._types?.includes('favourite')) {
favsCount++;
}
if (account._types?.includes('reblog')) {
reblogsCount++;
if (_accounts) {
for (const account of _accounts) {
if (account._types?.includes('favourite')) {
favsCount++;
}
if (account._types?.includes('reblog')) {
reblogsCount++;
}
}
}
if (!reblogsCount && favsCount) type = 'favourite';
if (!favsCount && reblogsCount) type = 'reblog';
}
const text =
type === 'poll'
? contentText[isSelf ? 'poll-self' : isVoted ? 'poll-voted' : 'poll']
: contentText[type];
let text;
if (type === 'poll') {
text = contentText[isSelf ? 'poll-self' : isVoted ? 'poll-voted' : 'poll'];
} else if (
type === 'reblog' ||
type === 'favourite' ||
type === 'favourite+reblog'
) {
if (_statuses?.length > 1) {
text = contentText[`${type}+account`];
} else if (isReplyToOthers) {
text = contentText[`${type}_reply`];
} else {
text = contentText[type];
}
} else if (contentText[type]) {
text = contentText[type];
} else {
// Anticipate unhandled notification types, possibly from Mastodon forks or non-Mastodon instances
// This surfaces the error to the user, hoping that users will report it
text = `[Unknown notification type: ${type}]`;
}
if (typeof text === 'function') {
const count = _statuses?.length || _accounts?.length;
if (type === 'admin.report') {
const targetAccount = report?.targetAccount;
if (targetAccount) {
text = text(<NameText account={targetAccount} showAvatar />);
}
} else if (type === 'severed_relationships') {
const targetName = event?.targetName;
if (targetName) {
text = text(targetName);
}
} else if (
(type === 'emoji_reaction' || type === 'pleroma:emoji_reaction') &&
notification.emoji
) {
const emojiURL =
notification.emoji_url || // This is string
status?.emojis?.find?.(
(emoji) =>
emoji?.shortcode ===
notification.emoji.replace(/^:/, '').replace(/:$/, ''),
); // Emoji object instead of string
text = text(notification.emoji, emojiURL);
} else if (count) {
text = text(count);
}
}
if (type === 'mention' && !status) {
// Could be deleted
return null;
}
const formattedCreatedAt =
notification.createdAt && new Date(notification.createdAt).toLocaleString();
const genericAccountsHeading =
{
'favourite+reblog': 'Boosted/Liked by…',
favourite: 'Liked by…',
reblog: 'Boosted by…',
follow: 'Followed by…',
}[type] || 'Accounts';
const handleOpenGenericAccounts = () => {
states.showGenericAccounts = {
heading: genericAccountsHeading,
accounts: _accounts,
showReactions: type === 'favourite+reblog',
excludeRelationshipAttrs: type === 'follow' ? ['followedBy'] : [],
postID: statusKey(actualStatusID, instance),
};
};
console.debug('RENDER Notification', notification.id);
return (
<div class={`notification notification-${type}`} tabIndex="0">
<div
class={`notification notification-${type}`}
data-notification-id={_ids || id}
tabIndex="0"
>
<div
class={`notification-type notification-${type}`}
title={new Date(notification.createdAt).toLocaleString()}
title={formattedCreatedAt}
>
{type === 'favourite+reblog' ? (
<>
@ -108,16 +292,32 @@ function Notification({ notification, instance, reload }) {
{type !== 'mention' && (
<>
<p>
{!/poll|update/i.test(type) && (
{!/poll|update|severed_relationships/i.test(type) && (
<>
{_accounts?.length > 1 ? (
<>
<b>{_accounts.length} people</b>{' '}
<b tabIndex="0" onClick={handleOpenGenericAccounts}>
<span title={_accounts.length}>
{shortenNumber(_accounts.length)}
</span>{' '}
people
</b>{' '}
</>
) : notificationsCount > 1 ? (
<>
<b>
<span title={notificationsCount}>
{shortenNumber(notificationsCount)}
</span>{' '}
people
</b>{' '}
</>
) : (
<>
<NameText account={account} showAvatar />{' '}
</>
account && (
<>
<NameText account={account} showAvatar />{' '}
</>
)
)}
</>
)}
@ -134,20 +334,47 @@ function Notification({ notification, instance, reload }) {
)}
</p>
{type === 'follow_request' && (
<FollowRequestButtons
accountID={account.id}
onChange={() => {
reload();
}}
/>
<FollowRequestButtons accountID={account.id} />
)}
{type === 'severed_relationships' && (
<div>
{SEVERED_RELATIONSHIPS_TEXT[event.type]({
from: instance,
...event,
})}
<br />
<a
href={`https://${instance}/severed_relationships`}
target="_blank"
rel="noopener noreferrer"
>
Learn more <Icon icon="external" size="s" />
</a>
.
</div>
)}
{type === 'moderation_warning' && !!moderation_warning && (
<div>
{MODERATION_WARNING_TEXT[moderation_warning.action]}
<br />
<a
href={`/disputes/strikes/${moderation_warning.id}`}
target="_blank"
rel="noopener noreferrer"
>
Learn more <Icon icon="external" size="s" />
</a>
.
</div>
)}
</>
)}
{_accounts?.length > 1 && (
<p class="avatars-stack">
{_accounts.map((account, i) => (
<>
{_accounts.slice(0, AVATARS_LIMIT).map((account) => (
<Fragment key={account.id}>
<a
key={account.id}
href={account.url}
rel="noopener noreferrer"
class="account-avatar-stack"
@ -161,13 +388,9 @@ function Notification({ notification, instance, reload }) {
size={
_accounts.length <= 10
? 'xxl'
: _accounts.length < 100
: _accounts.length < 20
? 'xl'
: _accounts.length < 1000
? 'l'
: _accounts.length < 2000
? 'm'
: 's' // My god, this person is popular!
: 'l'
}
key={account.id}
alt={`${account.displayName} @${account.acct}`}
@ -185,25 +408,142 @@ function Notification({ notification, instance, reload }) {
</div>
)}
</a>{' '}
</>
</Fragment>
))}
<button
type="button"
class="small plain"
onClick={handleOpenGenericAccounts}
>
{_accounts.length > AVATARS_LIMIT &&
`+${_accounts.length - AVATARS_LIMIT}`}
<Icon icon="chevron-down" />
</button>
</p>
)}
{status && (
<Link
{!_accounts?.length && sampleAccounts?.length > 1 && (
<p class="avatars-stack">
{sampleAccounts.map((account) => (
<Fragment key={account.id}>
<a
key={account.id}
href={account.url}
rel="noopener noreferrer"
class="account-avatar-stack"
onClick={(e) => {
e.preventDefault();
states.showAccount = account;
}}
>
<Avatar
url={account.avatarStatic}
size="xxl"
key={account.id}
alt={`${account.displayName} @${account.acct}`}
squircle={account?.bot}
/>
{/* {type === 'favourite+reblog' && (
<div class="account-sub-icons">
{account._types.map((type) => (
<Icon
icon={NOTIFICATION_ICONS[type]}
size="s"
class={`${type}-icon`}
/>
))}
</div>
)} */}
</a>{' '}
</Fragment>
))}
{notificationsCount > sampleAccounts.length && (
<Link
to={
instance ? `/${instance}/s/${status.id}` : `/s/${status.id}`
}
class="button small plain centered"
>
+{notificationsCount - sampleAccounts.length}
<Icon icon="chevron-right" />
</Link>
)}
</p>
)}
{_statuses?.length > 1 && (
<ul class="notification-group-statuses">
{_statuses.map((status) => (
<li key={status.id}>
<TruncatedLink
class={`status-link status-type-${type}`}
to={
instance ? `/${instance}/s/${status.id}` : `/s/${status.id}`
}
>
<Status
status={status}
size="s"
previewMode
allowContextMenu
/>
</TruncatedLink>
</li>
))}
</ul>
)}
{status && (!_statuses?.length || _statuses?.length <= 1) && (
<TruncatedLink
class={`status-link status-type-${type}`}
to={
instance
? `/${instance}/s/${actualStatusID}`
: `/s/${actualStatusID}`
}
onContextMenu={
!disableContextMenu
? (e) => {
const post = e.target.querySelector('.status');
if (post) {
// Fire a custom event to open the context menu
if (e.metaKey) return;
e.preventDefault();
post.dispatchEvent(
new MouseEvent('contextmenu', {
clientX: e.clientX,
clientY: e.clientY,
}),
);
}
}
: undefined
}
>
<Status statusID={actualStatusID} size="s" />
</Link>
{isStatic ? (
<Status
status={actualStatus}
size="s"
readOnly
allowContextMenu
/>
) : (
<Status
statusID={actualStatusID}
size="s"
readOnly
allowContextMenu
/>
)}
</TruncatedLink>
)}
</div>
</div>
);
}
export default Notification;
function TruncatedLink(props) {
const ref = useTruncated();
return <Link {...props} data-read-more="Read more →" ref={ref} />;
}
export default memo(Notification, (oldProps, newProps) => {
return oldProps.notification?.id === newProps.notification?.id;
});

View file

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'preact/hooks';
import { useState } from 'preact/hooks';
import shortenNumber from '../utils/shorten-number';
@ -62,29 +62,13 @@ export default function Poll({
const [showResults, setShowResults] = useState(false);
const optionsHaveVoteCounts = options.every((o) => o.votesCount !== null);
const pollRef = useRef();
useEffect(() => {
const handleSwipe = () => {
console.log('swiped left');
setShowResults(!showResults);
};
pollRef.current?.addEventListener?.('swiped-left', handleSwipe);
return () => {
pollRef.current?.removeEventListener?.('swiped-left', handleSwipe);
};
}, [showResults]);
return (
<div
ref={pollRef}
lang={lang}
dir="auto"
class={`poll ${readOnly ? 'read-only' : ''} ${
uiState === 'loading' ? 'loading' : ''
}`}
onDblClick={() => {
setShowResults(!showResults);
}}
>
{(showResults && optionsHaveVoteCounts) || voted || expired ? (
<>
@ -138,11 +122,12 @@ export default function Poll({
<button
class="poll-vote-button plain2"
disabled={uiState === 'loading'}
onClick={() => {
onClick={(e) => {
e.preventDefault();
setShowResults(false);
}}
>
<Icon icon="arrow-left" /> Hide results
<Icon icon="arrow-left" size="s" /> Hide results
</button>
)}
</>
@ -196,44 +181,59 @@ export default function Poll({
)}
</form>
)}
{!readOnly && (
<p class="poll-meta">
{!expired && (
<>
<button
type="button"
class="textual"
disabled={uiState === 'loading'}
onClick={(e) => {
e.preventDefault();
setUIState('loading');
<p class="poll-meta">
{!expired && !readOnly && (
<button
type="button"
class="plain small"
disabled={uiState === 'loading'}
onClick={(e) => {
e.preventDefault();
setUIState('loading');
(async () => {
await refresh();
setUIState('default');
})();
}}
>
Refresh
</button>{' '}
&bull;{' '}
</>
)}
<span title={votesCount}>{shortenNumber(votesCount)}</span> vote
{votesCount === 1 ? '' : 's'}
{!!votersCount && votersCount !== votesCount && (
<>
{' '}
&bull;{' '}
<span title={votersCount}>{shortenNumber(votersCount)}</span>{' '}
voter
{votersCount === 1 ? '' : 's'}
</>
)}{' '}
&bull; {expired ? 'Ended' : 'Ending'}{' '}
{!!expiresAtDate && <RelativeTime datetime={expiresAtDate} />}
</p>
)}
(async () => {
await refresh();
setUIState('default');
})();
}}
title="Refresh"
>
<Icon icon="refresh" alt="Refresh" />
</button>
)}
{!voted && !expired && !readOnly && optionsHaveVoteCounts && (
<button
type="button"
class="plain small"
disabled={uiState === 'loading'}
onClick={(e) => {
e.preventDefault();
setShowResults(!showResults);
}}
title={showResults ? 'Hide results' : 'Show results'}
>
<Icon
icon={showResults ? 'eye-open' : 'eye-close'}
alt={showResults ? 'Hide results' : 'Show results'}
/>{' '}
</button>
)}
{!expired && !readOnly && ' '}
<span title={votesCount}>{shortenNumber(votesCount)}</span> vote
{votesCount === 1 ? '' : 's'}
{!!votersCount && votersCount !== votesCount && (
<>
{' '}
&bull; <span title={votersCount}>
{shortenNumber(votersCount)}
</span>{' '}
voter
{votersCount === 1 ? '' : 's'}
</>
)}{' '}
&bull; {expired ? 'Ended' : 'Ending'}{' '}
{!!expiresAtDate && <RelativeTime datetime={expiresAtDate} />}
</p>{' '}
</div>
);
}

View file

@ -8,7 +8,7 @@ import dayjs from 'dayjs';
import dayjsTwitter from 'dayjs-twitter';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import relativeTime from 'dayjs/plugin/relativeTime';
import { useEffect, useState } from 'preact/hooks';
import { useEffect, useMemo, useReducer } from 'preact/hooks';
dayjs.extend(dayjsTwitter);
dayjs.extend(localizedFormat);
@ -18,40 +18,54 @@ const dtf = new Intl.DateTimeFormat();
export default function RelativeTime({ datetime, format }) {
if (!datetime) return null;
const date = dayjs(datetime);
const [dateStr, setDateStr] = useState('');
const [renderCount, rerender] = useReducer((x) => x + 1, 0);
const date = useMemo(() => dayjs(datetime), [datetime]);
const [dateStr, dt, title] = useMemo(() => {
if (!date.isValid()) return ['' + datetime, '', ''];
let str;
if (format === 'micro') {
// If date <= 1 day ago or day is within this year
const now = dayjs();
const dayDiff = now.diff(date, 'day');
if (dayDiff <= 1 || now.year() === date.year()) {
str = date.twitter();
} else {
str = dtf.format(date.toDate());
}
}
if (!str) str = date.fromNow();
return [str, date.toISOString(), date.format('LLLL')];
}, [date, format, renderCount]);
useEffect(() => {
let timer, raf;
const update = () => {
if (!date.isValid()) return;
let timeout;
let raf;
function rafRerender() {
raf = requestAnimationFrame(() => {
let str;
if (format === 'micro') {
// If date <= 1 day ago or day is within this year
const now = dayjs();
const dayDiff = now.diff(date, 'day');
if (dayDiff <= 1 || now.year() === date.year()) {
str = date.twitter();
} else {
str = dtf.format(date.toDate());
}
} else {
str = date.fromNow();
}
setDateStr(str);
timer = setTimeout(update, 30_000);
rerender();
scheduleRerender();
});
};
raf = requestAnimationFrame(update);
}
function scheduleRerender() {
// If less than 1 minute, rerender every 10s
// If less than 1 hour rerender every 1m
// Else, don't need to rerender
if (date.diff(dayjs(), 'minute', true) < 1) {
timeout = setTimeout(rafRerender, 10_000);
} else if (date.diff(dayjs(), 'hour', true) < 1) {
timeout = setTimeout(rafRerender, 60_000);
}
}
scheduleRerender();
return () => {
clearTimeout(timer);
clearTimeout(timeout);
cancelAnimationFrame(raf);
};
}, [date]);
}, []);
return (
<time datetime={date.toISOString()} title={date.format('LLLL')}>
<time datetime={dt} title={title}>
{dateStr}
</time>
);

View file

@ -0,0 +1,200 @@
.report-modal-container {
width: 100%;
max-height: 100%;
display: flex;
flex-direction: column;
max-width: 40em;
background-color: var(--bg-color);
box-shadow: 0 16px 32px -8px var(--drop-shadow-color);
overflow-y: auto;
animation: slide-up-smooth 0.3s ease-in-out;
position: relative;
@media (min-width: 40em) {
max-height: calc(100% - 32px);
}
h1 {
margin: 0;
padding: 0;
}
.top-controls {
position: sticky;
top: var(--sai-top, 0);
z-index: 1;
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;
pointer-events: auto;
align-items: center;
h1 {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
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; */
}
form {
/* display: flex; */
/* flex-direction: column; */
/* gap: 16px; */
text-wrap: pretty;
input {
margin-inline: 0;
}
}
.report-preview {
background-color: var(--bg-color);
border-radius: 8px;
border: 2px dashed var(--red-color);
box-shadow: inset 0 0 16px -4px var(--red-bg-color);
overflow: auto;
max-height: 33vh;
.status {
font-size: 90%;
user-select: none;
pointer-events: none;
-webkit-touch-callout: none;
-webkit-user-drag: none;
filter: grayscale(0.5);
}
.account-block {
margin: 16px;
user-select: none;
pointer-events: none;
-webkit-touch-callout: none;
-webkit-user-drag: none;
filter: grayscale(0.5);
}
}
.rubber-stamp {
pointer-events: none;
user-select: none;
position: absolute;
inset-inline-end: 32px;
margin-top: -48px;
animation: rubber-stamp 0.3s ease-in both;
position: absolute;
font-weight: bold;
color: var(--red-color);
text-transform: uppercase;
letter-spacing: -0.5px;
font-size: 2em;
line-height: 1;
padding: 0.1em;
border: 0.15em solid var(--red-color);
border-radius: 0.3em;
background-color: var(--bg-blur-color);
text-align: center;
/* Noise pattern - https://css-tricks.com/making-static-noise-from-a-weird-css-gradient-bug/ */
mask-image: repeating-conic-gradient(
#000 0 0.01%,
rgba(0, 0, 0, 0.45) 0 0.02%
);
small {
display: block;
font-size: 11px;
}
}
p {
margin-block: 0.5em;
}
section {
label {
display: flex;
gap: 8px;
align-items: center;
cursor: pointer;
margin-bottom: 8px;
&:has(:checked) {
.insignificant {
color: var(--text-color);
}
}
}
> label:last-child {
margin-bottom: 0;
}
}
.report-categories {
label {
align-items: flex-start;
}
.report-rules {
margin-inline-start: 1.75em;
}
}
.report-comment {
display: flex;
gap: 8px;
align-items: flex-start;
margin-top: 2em;
flex-wrap: wrap;
p {
margin: 0;
padding: 8px 0 0;
flex-shrink: 0;
label {
margin-bottom: 0;
}
}
textarea {
flex-grow: 1;
resize: vertical;
}
}
footer {
margin-top: 2em;
display: flex;
gap: 8px;
align-items: center;
button {
border-radius: 8px !important;
align-self: stretch;
}
}
}
@keyframes rubber-stamp {
0% {
transform: rotate(-20deg) scale(5);
opacity: 0;
}
100% {
transform: rotate(-20deg) scale(1);
opacity: 1;
}
}

View file

@ -0,0 +1,298 @@
import './report-modal.css';
import { Fragment } from 'preact';
import { useMemo, useRef, useState } from 'preact/hooks';
import { api } from '../utils/api';
import showToast from '../utils/show-toast';
import { getCurrentInstance } from '../utils/store-utils';
import AccountBlock from './account-block';
import Icon from './icon';
import Loader from './loader';
import Status from './status';
// NOTE: `dislike` hidden for now, it's actually not used for reporting
// Mastodon shows another screen for unfollowing, muting or blocking instead of reporting
const CATEGORIES = [, /*'dislike'*/ 'spam', 'legal', 'violation', 'other'];
// `violation` will be set if there are `rule_ids[]`
const CATEGORIES_INFO = {
// dislike: {
// label: 'Dislike',
// description: 'Not something you want to see',
// },
spam: {
label: 'Spam',
description: 'Malicious links, fake engagement, or repetitive replies',
},
legal: {
label: 'Illegal',
description: "Violates the law of your or the server's country",
},
violation: {
label: 'Server rule violation',
description: 'Breaks specific server rules',
stampLabel: 'Violation',
},
other: {
label: 'Other',
description: "Issue doesn't fit other categories",
excludeStamp: true,
},
};
function ReportModal({ account, post, onClose }) {
const { masto } = api();
const [uiState, setUIState] = useState('default');
const [username, domain] = account.acct.split('@');
const [rules, currentDomain] = useMemo(() => {
const { rules, domain } = getCurrentInstance();
return [rules || [], domain];
});
const [selectedCategory, setSelectedCategory] = useState(null);
const [showRules, setShowRules] = useState(false);
const rulesRef = useRef(null);
const [hasRules, setHasRules] = useState(false);
return (
<div class="report-modal-container">
<div class="top-controls">
<h1>{post ? 'Report Post' : `Report @${username}`}</h1>
<button
type="button"
class="plain4 small"
disabled={uiState === 'loading'}
onClick={() => onClose()}
>
<Icon icon="x" size="xl" />
</button>
</div>
<main>
<div class="report-preview">
{post ? (
<Status status={post} size="s" previewMode />
) : (
<AccountBlock
account={account}
avatarSize="xxl"
useAvatarStatic
showStats
showActivity
/>
)}
</div>
{!!selectedCategory &&
!CATEGORIES_INFO[selectedCategory].excludeStamp && (
<span
class="rubber-stamp"
key={selectedCategory}
aria-hidden="true"
>
{CATEGORIES_INFO[selectedCategory].stampLabel ||
CATEGORIES_INFO[selectedCategory].label}
<small>Pending review</small>
</span>
)}
<form
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.target);
const entries = Object.fromEntries(formData.entries());
console.log('ENTRIES', entries);
let { category, comment, forward } = entries;
if (!comment) comment = undefined;
if (forward === 'on') forward = true;
const ruleIds =
category === 'violation'
? Object.entries(entries)
.filter(([key]) => key.startsWith('rule_ids'))
.map(([key, value]) => value)
: undefined;
const params = {
category,
comment,
forward,
ruleIds,
};
console.log('PARAMS', params);
setUIState('loading');
(async () => {
try {
await masto.v1.reports.create({
accountId: account.id,
statusIds: post?.id ? [post.id] : undefined,
category,
comment,
ruleIds,
forward,
});
setUIState('success');
showToast(post ? 'Post reported' : 'Profile reported');
onClose();
} catch (error) {
console.error(error);
setUIState('error');
showToast(
error?.message ||
(post
? 'Unable to report post'
: 'Unable to report profile'),
);
}
})();
}}
>
<p>
{post
? `What's the issue with this post?`
: `What's the issue with this profile?`}
</p>
<section class="report-categories">
{CATEGORIES.map((category) =>
category === 'violation' && !rules?.length ? null : (
<Fragment key={category}>
<label class="report-category">
<input
type="radio"
name="category"
value={category}
required
disabled={uiState === 'loading'}
onChange={(e) => {
setSelectedCategory(e.target.value);
setShowRules(e.target.value === 'violation');
}}
/>
<span>
{CATEGORIES_INFO[category].label} &nbsp;
<small class="ib insignificant">
{CATEGORIES_INFO[category].description}
</small>
</span>
</label>
{category === 'violation' && !!rules?.length && (
<div
class="shazam-container no-animation"
hidden={!showRules}
>
<div class="shazam-container-inner">
<div class="report-rules" ref={rulesRef}>
{rules.map((rule, i) => (
<label class="report-rule" key={rule.id}>
<input
type="checkbox"
name={`rule_ids[${i}]`}
value={rule.id}
required={showRules && !hasRules}
disabled={uiState === 'loading'}
onChange={(e) => {
const { checked } = e.target;
if (checked) {
setHasRules(true);
} else {
const checkedInputs =
rulesRef.current.querySelectorAll(
'input:checked',
);
if (!checkedInputs.length) {
setHasRules(false);
}
}
}}
/>
<span>{rule.text}</span>
</label>
))}
</div>
</div>
</div>
)}
</Fragment>
),
)}
</section>
<section class="report-comment">
<p>
<label for="report-comment">Additional info</label>
</p>
<textarea
maxlength="1000"
rows="1"
name="comment"
id="report-comment"
disabled={uiState === 'loading'}
/>
</section>
{!!domain && domain !== currentDomain && (
<section>
<p>
<label>
<input
type="checkbox"
switch
name="forward"
disabled={uiState === 'loading'}
/>{' '}
<span>
Forward to <i>{domain}</i>
</span>
</label>
</p>
</section>
)}
<footer>
<button type="submit" disabled={uiState === 'loading'}>
Send Report
</button>{' '}
<button
type="submit"
class="plain2"
disabled={uiState === 'loading'}
onClick={async () => {
try {
await masto.v1.accounts.$select(account.id).mute(); // Infinite duration
showToast(`Muted ${username}`);
} catch (e) {
console.error(e);
showToast(`Unable to mute ${username}`);
}
// onSubmit will still run
}}
>
Send Report <small class="ib">+ Mute profile</small>
</button>{' '}
<button
type="submit"
class="plain2"
disabled={uiState === 'loading'}
onClick={async () => {
try {
await masto.v1.accounts.$select(account.id).block();
showToast(`Blocked ${username}`);
} catch (e) {
console.error(e);
showToast(`Unable to block ${username}`);
}
// onSubmit will still run
}}
>
Send Report <small class="ib">+ Block profile</small>
</button>
<Loader hidden={uiState !== 'loading'} />
</footer>
</form>
</main>
</div>
);
}
export default ReportModal;

View file

@ -0,0 +1,54 @@
#search-command-container {
position: fixed;
inset: 0;
z-index: 1002;
background-color: var(--backdrop-darker-color);
background-image: radial-gradient(
farthest-corner at top,
var(--backdrop-color),
transparent
);
display: flex;
justify-content: center;
align-items: flex-start;
padding: 16px;
transition: opacity 0.1s ease-in-out;
}
#search-command-container[hidden] {
opacity: 0;
pointer-events: none;
}
#search-command-container form {
width: calc(40em - 32px);
max-width: 100%;
transition: transform 0.1s ease-in-out;
}
#search-command-container[hidden] form {
transform: translateY(-64px) scale(0.9);
}
#search-command-container input {
width: 100%;
padding: 16px;
border-radius: 999px;
background-color: var(--bg-faded-color);
border: 2px solid var(--outline-color);
box-shadow: 0 2px 16px var(--drop-shadow-color),
0 32px 64px var(--drop-shadow-color);
}
#search-command-container input:focus {
outline: 0;
background-color: var(--bg-color);
border-color: var(--link-color);
}
@media (min-width: 40em) {
#search-command-container {
align-items: center;
background-image: radial-gradient(
closest-side,
var(--backdrop-color),
transparent
);
}
}

View file

@ -0,0 +1,69 @@
import './search-command.css';
import { memo } from 'preact/compat';
import { useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import SearchForm from './search-form';
export default memo(function SearchCommand({ onClose = () => {} }) {
const [showSearch, setShowSearch] = useState(false);
const searchFormRef = useRef(null);
useHotkeys(
['Slash', '/'],
(e) => {
setShowSearch(true);
setTimeout(() => {
searchFormRef.current?.focus?.();
searchFormRef.current?.select?.();
}, 0);
},
{
preventDefault: true,
ignoreEventWhen: (e) => {
const isSearchPage = /\/search/.test(location.hash);
const hasModal = !!document.querySelector('#modal-container > *');
return isSearchPage || hasModal;
},
},
);
const closeSearch = () => {
setShowSearch(false);
onClose();
};
useHotkeys(
'esc',
(e) => {
searchFormRef.current?.blur?.();
closeSearch();
},
{
enabled: showSearch,
enableOnFormTags: true,
preventDefault: true,
},
);
return (
<div
id="search-command-container"
hidden={!showSearch}
onClick={(e) => {
console.log(e);
if (e.target === e.currentTarget) {
closeSearch();
}
}}
>
<SearchForm
ref={searchFormRef}
onSubmit={() => {
closeSearch();
}}
/>
</div>
);
});

View file

@ -0,0 +1,292 @@
import { forwardRef } from 'preact/compat';
import { useImperativeHandle, useRef, useState } from 'preact/hooks';
import { useSearchParams } from 'react-router-dom';
import { api } from '../utils/api';
import Icon from './icon';
import Link from './link';
const SearchForm = forwardRef((props, ref) => {
const { instance } = api();
const [searchParams, setSearchParams] = useSearchParams();
const [searchMenuOpen, setSearchMenuOpen] = useState(false);
const [query, setQuery] = useState(searchParams.get('q') || '');
const type = searchParams.get('type');
const formRef = useRef(null);
const searchFieldRef = useRef(null);
useImperativeHandle(ref, () => ({
setValue: (value) => {
setQuery(value);
},
focus: () => {
searchFieldRef.current.focus();
},
select: () => {
searchFieldRef.current.select();
},
blur: () => {
searchFieldRef.current.blur();
},
}));
return (
<form
ref={formRef}
class="search-popover-container"
onSubmit={(e) => {
e.preventDefault();
const isSearchPage = /\/search/.test(location.hash);
if (isSearchPage) {
if (query) {
const params = {
q: query,
};
if (type) params.type = type; // Preserve type
setSearchParams(params);
} else {
setSearchParams({});
}
} else {
if (query) {
location.hash = `/search?q=${encodeURIComponent(query)}${
type ? `&type=${type}` : ''
}`;
} else {
location.hash = `/search`;
}
}
props?.onSubmit?.(e);
}}
>
<input
ref={searchFieldRef}
value={query}
name="q"
type="search"
// autofocus
placeholder="Search"
dir="auto"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellCheck="false"
onSearch={(e) => {
if (!e.target.value) {
setSearchParams({});
}
}}
onInput={(e) => {
setQuery(e.target.value);
setSearchMenuOpen(true);
}}
onFocus={() => {
setSearchMenuOpen(true);
formRef.current
?.querySelector('.search-popover-item')
?.classList.add('focus');
}}
onBlur={() => {
setTimeout(() => {
setSearchMenuOpen(false);
}, 100);
formRef.current
?.querySelector('.search-popover-item.focus')
?.classList.remove('focus');
}}
onKeyDown={(e) => {
const { key } = e;
switch (key) {
case 'Escape':
setSearchMenuOpen(false);
break;
case 'Down':
case 'ArrowDown':
e.preventDefault();
if (searchMenuOpen) {
const focusItem = formRef.current.querySelector(
'.search-popover-item.focus',
);
if (focusItem) {
let nextItem = focusItem.nextElementSibling;
while (nextItem && nextItem.hidden) {
nextItem = nextItem.nextElementSibling;
}
if (nextItem) {
nextItem.classList.add('focus');
const siblings = Array.from(
nextItem.parentElement.children,
).filter((el) => el !== nextItem);
siblings.forEach((el) => {
el.classList.remove('focus');
});
}
} else {
const firstItem = formRef.current.querySelector(
'.search-popover-item',
);
if (firstItem) {
firstItem.classList.add('focus');
}
}
}
break;
case 'Up':
case 'ArrowUp':
e.preventDefault();
if (searchMenuOpen) {
const focusItem = document.querySelector(
'.search-popover-item.focus',
);
if (focusItem) {
let prevItem = focusItem.previousElementSibling;
while (prevItem && prevItem.hidden) {
prevItem = prevItem.previousElementSibling;
}
if (prevItem) {
prevItem.classList.add('focus');
const siblings = Array.from(
prevItem.parentElement.children,
).filter((el) => el !== prevItem);
siblings.forEach((el) => {
el.classList.remove('focus');
});
}
} else {
const lastItem = document.querySelector(
'.search-popover-item:last-child',
);
if (lastItem) {
lastItem.classList.add('focus');
}
}
}
break;
case 'Enter':
if (searchMenuOpen) {
const focusItem = document.querySelector(
'.search-popover-item.focus',
);
if (focusItem) {
e.preventDefault();
focusItem.click();
}
setSearchMenuOpen(false);
props?.onSubmit?.(e);
}
break;
}
}}
/>
<div class="search-popover" hidden={!searchMenuOpen || !query}>
{/* {!!query && (
<Link
to={`/search?q=${encodeURIComponent(query)}`}
class="search-popover-item focus"
onClick={(e) => {
props?.onSubmit?.(e);
}}
>
<Icon icon="search" />
<span>{query}</span>
</Link>
)} */}
{!!query &&
[
{
label: (
<>
{query}{' '}
<small class="insignificant">
accounts, hashtags &amp; posts
</small>
</>
),
to: `/search?q=${encodeURIComponent(query)}`,
top: !type && !/\s/.test(query),
hidden: !!type,
},
{
label: (
<>
Posts with <q>{query}</q>
</>
),
to: `/search?q=${encodeURIComponent(query)}&type=statuses`,
hidden: /^https?:/.test(query),
top: /\s/.test(query),
icon: 'document',
queryType: 'statuses',
},
{
label: (
<>
Posts tagged with <mark>#{query.replace(/^#/, '')}</mark>
</>
),
to: `/${instance}/t/${query.replace(/^#/, '')}`,
hidden:
/^@/.test(query) || /^https?:/.test(query) || /\s/.test(query),
top: /^#/.test(query),
type: 'link',
icon: 'hashtag',
queryType: 'hashtags',
},
{
label: (
<>
Look up <mark>{query}</mark>
</>
),
to: `/${query}`,
hidden: !/^https?:/.test(query),
top: /^https?:/.test(query),
type: 'link',
},
{
label: (
<>
Accounts with <q>{query}</q>
</>
),
to: `/search?q=${encodeURIComponent(query)}&type=accounts`,
icon: 'group',
queryType: 'accounts',
},
]
.sort((a, b) => {
if (type) {
if (a.queryType === type) return -1;
if (b.queryType === type) return 1;
}
if (a.top && !b.top) return -1;
if (!a.top && b.top) return 1;
return 0;
})
.filter(({ hidden }) => !hidden)
.map(({ label, to, icon, type }, i) => (
<Link
to={to}
class={`search-popover-item ${i === 0 ? 'focus' : ''}`}
// hidden={hidden}
onClick={(e) => {
console.log('onClick', e);
props?.onSubmit?.(e);
}}
>
<Icon
icon={icon || (type === 'link' ? 'arrow-right' : 'search')}
class="more-insignificant"
/>
<span>{label}</span>{' '}
</Link>
))}
</div>
</form>
);
});
export default SearchForm;

View file

@ -18,10 +18,11 @@
counter-increment: index;
display: inline-block;
width: 1.2em;
text-align: right;
margin-right: 8px;
text-align: end;
margin-inline-end: 8px;
color: var(--text-insignificant-color);
font-size: 90%;
flex-shrink: 0;
}
#shortcuts-settings-container .shortcuts-list li .shortcut-text {
flex-grow: 1;
@ -35,7 +36,7 @@
#shortcuts-settings-container .shortcuts-view-mode {
display: flex;
align-items: center;
align-items: stretch;
gap: 2px;
margin: 8px 0 0;
}
@ -51,14 +52,15 @@
gap: 8px;
flex-direction: column;
align-items: center;
justify-content: center;
}
#shortcuts-settings-container .shortcuts-view-mode label:first-child {
border-top-left-radius: 16px;
border-bottom-left-radius: 16px;
border-start-start-radius: 16px;
border-end-start-radius: 16px;
}
#shortcuts-settings-container .shortcuts-view-mode label:last-child {
border-top-right-radius: 16px;
border-bottom-right-radius: 16px;
border-start-end-radius: 16px;
border-end-end-radius: 16px;
}
#shortcuts-settings-container .shortcuts-view-mode label img {
max-height: 64px;
@ -84,8 +86,9 @@
transform: scale(0.975);
transition: all 0.2s ease-out;
}
#shortcuts-settings-container .shortcuts-view-mode label:has(input:checked) {
box-shadow: inset 0 0 0 3px var(--link-color);
#shortcuts-settings-container .shortcuts-view-mode label.checked {
box-shadow: inset 0 0 0 3px var(--link-color),
inset 0 0 32px var(--link-faded-color);
}
#shortcuts-settings-container
.shortcuts-view-mode
@ -111,7 +114,7 @@
}
#shortcut-settings-form label > span:first-child {
flex-basis: 5em;
text-align: right;
text-align: end;
}
#shortcut-settings-form :is(input[type='text'], select) {
flex-grow: 1;
@ -121,7 +124,81 @@
min-width: 0;
max-width: 320px;
}
#shortcut-settings-form .form-note {
display: flex;
gap: 6px;
align-items: center;
}
#shortcut-settings-form form footer {
display: flex;
gap: 16px;
}
/* Import/Export */
#import-export-container input[type='text'] {
font-family: var(--monospace-font);
}
#import-export-container section {
margin: 8px 0;
background-color: var(--bg-faded-color);
border-radius: 16px;
padding: 8px;
}
#import-export-container section h3 {
margin: 0 0 8px;
}
#import-export-container section h3 * {
vertical-align: middle;
}
#import-export-container section p {
margin: 8px 0;
&.field-button {
display: flex;
gap: 8px;
button {
flex-shrink: 0;
}
}
}
#import-export-container section details > summary {
cursor: pointer;
}
#import-export-container .import-settings-list {
border-radius: 8px;
overflow: hidden;
margin: 8px 0 0;
padding: 0;
counter-reset: index;
}
#import-export-container .import-settings-list li {
background-color: var(--bg-blur-color);
margin: 0 0 2px;
padding: 8px 4px;
display: flex;
gap: 4px;
}
#import-export-container .import-settings-list li::before {
content: counter(index);
counter-increment: index;
display: inline-block;
width: 1.2em;
text-align: end;
margin-inline-end: 8px;
color: var(--text-insignificant-color);
font-size: 90%;
flex-shrink: 0;
}
#import-export-container {
footer {
font-size: 90%;
color: var(--text-insignificant-color);
.icon {
vertical-align: text-bottom;
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -2,8 +2,8 @@
position: fixed;
bottom: 16px;
bottom: max(16px, env(safe-area-inset-bottom));
left: 16px;
left: max(16px, env(safe-area-inset-left));
inset-inline-start: 16px;
inset-inline-start: max(16px, env(safe-area-inset-left));
padding: 16px;
background-color: var(--bg-faded-blur-color);
z-index: 101;
@ -34,9 +34,9 @@
@media (min-width: calc(40em + 56px + 8px)) {
#shortcuts-button {
right: 16px;
right: max(16px, env(safe-area-inset-right));
left: auto;
inset-inline-end: 16px;
inset-inline-end: max(16px, env(safe-area-inset-right));
inset-inline-start: auto;
top: 16px;
top: max(16px, env(safe-area-inset-top));
bottom: auto;
@ -59,8 +59,9 @@
left: 0;
right: 0;
z-index: 100;
background-color: var(--bg-blur-color);
backdrop-filter: blur(16px) saturate(3);
background-color: var(--bg-color);
/* background-color: var(--bg-blur-color);
backdrop-filter: blur(16px) saturate(3); */
border-top: var(--hairline-width) solid var(--outline-color);
box-shadow: 0 -8px 16px -8px var(--drop-shadow-color);
overflow: auto;
@ -82,6 +83,8 @@
list-style: none;
display: flex;
justify-content: center;
min-width: 20vw;
flex-basis: 20vw;
}
#shortcuts .tab-bar li a {
-webkit-tap-highlight-color: transparent;
@ -95,7 +98,13 @@
padding: 8px;
text-decoration: none;
text-shadow: 0 var(--hairline-width) var(--bg-color);
width: 20vw;
width: 100%;
@media (hover: hover) {
&:is(:hover, :focus) {
color: var(--text-color);
}
}
}
#shortcuts .tab-bar li a:active {
transform: scale(0.95);
@ -144,7 +153,7 @@ shortcuts .tab-bar[hidden] {
}
}
@media (min-width: 40em) {
@media (min-width: 40em) and (hover: hover) {
#app[data-shortcuts-view-mode='tab-menu-bar'] .timeline-deck {
margin-top: 44px;
}
@ -157,6 +166,7 @@ shortcuts .tab-bar[hidden] {
padding: env(safe-area-inset-top) env(safe-area-inset-right) 0
env(safe-area-inset-left);
background-color: var(--bg-faded-blur-color);
backdrop-filter: blur(16px);
border: 0;
box-shadow: none;
border-bottom: var(--hairline-width) solid var(--bg-faded-color);
@ -171,6 +181,8 @@ shortcuts .tab-bar[hidden] {
}
#shortcuts .tab-bar li {
flex-grow: 0;
min-width: auto;
flex-basis: auto;
}
#shortcuts .tab-bar li a {
padding: 0 16px;

View file

@ -1,72 +1,83 @@
import './shortcuts.css';
import { Menu, MenuItem } from '@szhsin/react-menu';
import { useMemo, useRef } from 'preact/hooks';
import { MenuDivider } from '@szhsin/react-menu';
import { memo } from 'preact/compat';
import { useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import { useNavigate } from 'react-router-dom';
import { useSnapshot } from 'valtio';
import { SHORTCUTS_META } from '../components/shortcuts-settings';
import { api } from '../utils/api';
import { getLists } from '../utils/lists';
import states from '../utils/states';
import AsyncText from './AsyncText';
import Icon from './icon';
import Link from './link';
import MenuLink from './menu-link';
import Menu2 from './menu2';
import SubMenu2 from './submenu2';
function Shortcuts() {
const { instance } = api();
const snapStates = useSnapshot(states);
const { shortcuts } = snapStates;
const { shortcuts, settings } = snapStates;
if (!shortcuts.length) {
return null;
}
if (
settings.shortcutsViewMode === 'multi-column' ||
(!settings.shortcutsViewMode && settings.shortcutsColumnsMode)
) {
return null;
}
const menuRef = useRef();
const formattedShortcuts = useMemo(
() =>
shortcuts
.map((pin, i) => {
const { type, ...data } = pin;
if (!SHORTCUTS_META[type]) return null;
let { id, path, title, subtitle, icon } = SHORTCUTS_META[type];
const hasLists = useRef(false);
const formattedShortcuts = shortcuts
.map((pin, i) => {
const { type, ...data } = pin;
if (!SHORTCUTS_META[type]) return null;
let { id, path, title, subtitle, icon } = SHORTCUTS_META[type];
if (typeof id === 'function') {
id = id(data, i);
}
if (typeof path === 'function') {
path = path(
{
...data,
instance: data.instance || instance,
},
i,
);
}
if (typeof title === 'function') {
title = title(data, i);
}
if (typeof subtitle === 'function') {
subtitle = subtitle(data, i);
}
if (typeof icon === 'function') {
icon = icon(data, i);
}
if (typeof id === 'function') {
id = id(data, i);
}
if (typeof path === 'function') {
path = path(
{
...data,
instance: data.instance || instance,
},
i,
);
}
if (typeof title === 'function') {
title = title(data, i);
}
if (typeof subtitle === 'function') {
subtitle = subtitle(data, i);
}
if (typeof icon === 'function') {
icon = icon(data, i);
}
return {
id,
path,
title,
subtitle,
icon,
};
})
.filter(Boolean),
[shortcuts],
);
if (id === 'lists') {
hasLists.current = true;
}
return {
id,
path,
title,
subtitle,
icon,
};
})
.filter(Boolean);
const navigate = useNavigate();
useHotkeys(['1', '2', '3', '4', '5', '6', '7', '8', '9'], (e, handler) => {
@ -80,15 +91,23 @@ function Shortcuts() {
}
});
const [lists, setLists] = useState([]);
return (
<div id="shortcuts">
{snapStates.settings.shortcutsViewMode === 'tab-menu-bar' ? (
<nav class="tab-bar">
<nav
class="tab-bar"
onContextMenu={(e) => {
e.preventDefault();
states.showShortcutsSettings = true;
}}
>
<ul>
{formattedShortcuts.map(
({ id, path, title, subtitle, icon }, i) => {
return (
<li key={i + title}>
<li key={`${i}-${id}-${title}-${subtitle}-${path}`}>
<Link
class={subtitle ? 'has-subtitle' : ''}
to={path}
@ -126,19 +145,27 @@ function Shortcuts() {
</ul>
</nav>
) : (
<Menu
<Menu2
instanceRef={menuRef}
overflow="auto"
viewScroll="close"
boundingBoxPadding="8 8 8 8"
menuClassName="glass-menu shortcuts-menu"
gap={8}
position="anchor"
onMenuChange={(e) => {
if (e.open && hasLists.current) {
getLists().then(setLists);
}
}}
menuButton={
<button
type="button"
id="shortcuts-button"
class="plain"
onContextMenu={(e) => {
e.preventDefault();
states.showShortcutsSettings = true;
}}
onTransitionStart={(e) => {
// Close menu if the button disappears
try {
@ -153,9 +180,42 @@ function Shortcuts() {
</button>
}
>
{formattedShortcuts.map(({ path, title, subtitle, icon }, i) => {
{formattedShortcuts.map(({ id, path, title, subtitle, icon }, i) => {
if (id === 'lists') {
return (
<SubMenu2
menuClassName="glass-menu"
overflow="auto"
gap={-8}
label={
<>
<Icon icon={icon} size="l" />
<span class="menu-grow">
<AsyncText>{title}</AsyncText>
</span>
<Icon icon="chevron-right" />
</>
}
>
<MenuLink to="/l">
<span>All Lists</span>
</MenuLink>
<MenuDivider />
{lists?.map((list) => (
<MenuLink key={list.id} to={`/l/${list.id}`}>
<span>{list.title}</span>
</MenuLink>
))}
</SubMenu2>
);
}
return (
<MenuLink to={path} key={i + title} class="glass-menu-item">
<MenuLink
to={path}
key={`${i}-${id}-${title}-${subtitle}-${path}`}
class="glass-menu-item"
>
<Icon icon={icon} size="l" />{' '}
<span class="menu-grow">
<span>
@ -174,10 +234,10 @@ function Shortcuts() {
</MenuLink>
);
})}
</Menu>
</Menu2>
)}
</div>
);
}
export default Shortcuts;
export default memo(Shortcuts);

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,25 @@
import { SubMenu } from '@szhsin/react-menu';
import { useRef } from 'preact/hooks';
export default function SubMenu2(props) {
const menuRef = useRef();
return (
<SubMenu
{...props}
instanceRef={menuRef}
// Test fix for bug; submenus not opening on Android
itemProps={{
onPointerMove: (e) => {
if (e.pointerType === 'touch') {
menuRef.current?.openMenu?.();
}
},
onPointerLeave: (e) => {
if (e.pointerType === 'touch') {
menuRef.current?.openMenu?.();
}
},
}}
/>
);
}

File diff suppressed because it is too large Load diff

View file

@ -35,7 +35,7 @@
border-bottom: 0;
margin-bottom: -1px;
background-image: linear-gradient(
to top left,
to top var(--backward),
var(--bg-color) 50%,
var(--bg-faded-blur-color)
);
@ -44,12 +44,13 @@
.status-translation-block .translated-block {
border: 1px solid var(--outline-color);
line-height: 1.3;
border-radius: 0 8px 8px 8px;
border-radius: 8px;
border-start-start-radius: 0;
margin: 0;
padding: 8px;
background-color: var(--bg-color);
background-image: linear-gradient(
to bottom right,
to bottom var(--forward),
var(--bg-color),
var(--bg-faded-blur-color)
);
@ -83,6 +84,7 @@
.status-translation-block .translated-block output {
display: block;
margin-top: 0.75em;
text-wrap: pretty;
}
.status-translation-block
.translated-block
@ -105,3 +107,22 @@
overflow: visible;
mask-image: none;
}
/* MINI */
.status-translation-block-mini {
display: flex;
margin: 8px 0 0;
padding: 8px 0 0;
font-size: 90%;
border-top: var(--hairline-width) solid var(--outline-color);
color: var(--text-insignificant-color);
gap: 8px;
transition: color 0.3s ease-in-out;
}
.status-translation-block-mini .icon {
margin-top: 2px;
}
.status:is(:hover, :active) .status-translation-block-mini {
color: var(--text-color);
}

View file

@ -1,19 +1,83 @@
import './translation-block.css';
import pRetry from 'p-retry';
import pThrottle from 'p-throttle';
import { useEffect, useRef, useState } from 'preact/hooks';
import sourceLanguages from '../data/lingva-source-languages';
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
import localeCode2Text from '../utils/localeCode2Text';
import pmem from '../utils/pmem';
import Icon from './icon';
import LazyShazam from './lazy-shazam';
import Loader from './loader';
const { PHANPY_LINGVA_INSTANCES } = import.meta.env;
const LINGVA_INSTANCES = PHANPY_LINGVA_INSTANCES
? PHANPY_LINGVA_INSTANCES.split(/\s+/)
: [];
const throttle = pThrottle({
limit: 1,
interval: 2000,
});
let currentLingvaInstance = 0;
function _lingvaTranslate(text, source, target) {
console.log('TRANSLATE', text, source, target);
const fetchCall = () => {
let instance = LINGVA_INSTANCES[currentLingvaInstance];
return fetch(
`https://${instance}/api/v1/${source}/${target}/${encodeURIComponent(
text,
)}`,
)
.then((res) => {
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.then((res) => {
return {
provider: 'lingva',
content: res.translation,
detectedSourceLanguage: res.info?.detectedSource,
info: res.info,
};
});
};
return pRetry(fetchCall, {
retries: 3,
onFailedAttempt: (e) => {
currentLingvaInstance =
(currentLingvaInstance + 1) % LINGVA_INSTANCES.length;
console.log(
'Retrying translation with another instance',
currentLingvaInstance,
);
},
});
// return masto.v1.statuses.$select(id).translate({
// lang: DEFAULT_LANG,
// });
}
const TRANSLATED_MAX_AGE = 1000 * 60 * 60; // 1 hour
const lingvaTranslate = pmem(_lingvaTranslate, {
maxAge: TRANSLATED_MAX_AGE,
});
const throttledLingvaTranslate = pmem(throttle(lingvaTranslate), {
// I know, this is double-layered memoization
maxAge: TRANSLATED_MAX_AGE,
});
function TranslationBlock({
forceTranslate,
sourceLanguage,
onTranslate,
text = '',
mini,
autoDetected,
}) {
const targetLang = getTranslateTargetLanguage(true);
const [uiState, setUIState] = useState('default');
@ -28,35 +92,15 @@ function TranslationBlock({
const targetLangText = localeCode2Text(targetLang);
const apiSourceLang = useRef('auto');
if (!onTranslate)
onTranslate = (source, target) => {
console.log('TRANSLATE', source, target, text);
// Using another API instance instead of lingva.ml because of this bug (slashes don't work):
// https://github.com/thedaviddelta/lingva-translate/issues/68
return fetch(
`https://lingva.garudalinux.org/api/v1/${source}/${target}/${encodeURIComponent(
text,
)}`,
)
.then((res) => res.json())
.then((res) => {
return {
provider: 'lingva',
content: res.translation,
detectedSourceLanguage: res.info?.detectedSource,
info: res.info,
};
});
// return masto.v1.statuses.translate(id, {
// lang: DEFAULT_LANG,
// });
};
if (!onTranslate) {
onTranslate = mini ? throttledLingvaTranslate : lingvaTranslate;
}
const translate = async () => {
setUIState('loading');
try {
const { content, detectedSourceLanguage, provider, ...props } =
await onTranslate(apiSourceLang.current, targetLang);
const { content, detectedSourceLanguage, provider, error, ...props } =
await onTranslate(text, apiSourceLang.current, targetLang);
if (content) {
if (detectedSourceLanguage) {
const detectedLangText = localeCode2Text(detectedSourceLanguage);
@ -70,13 +114,15 @@ function TranslationBlock({
}
setTranslatedContent(content);
setUIState('default');
detailsRef.current.open = true;
detailsRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
if (!mini && content.trim() !== text.trim()) {
detailsRef.current.open = true;
detailsRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
}
} else {
console.error(result);
if (error) console.error(error);
setUIState('error');
}
} catch (e) {
@ -91,6 +137,33 @@ function TranslationBlock({
}
}, [forceTranslate]);
if (mini) {
if (
!!translatedContent &&
translatedContent.trim() !== text.trim() &&
detectedLang !== targetLangText
) {
return (
<LazyShazam>
<div class="status-translation-block-mini">
<Icon
icon="translate"
alt={`Auto-translated from ${sourceLangText}`}
/>
<output
lang={targetLang}
dir="auto"
title={pronunciationContent || ''}
>
{translatedContent}
</output>
</div>
</LazyShazam>
);
}
return null;
}
return (
<div
class="status-translation-block"
@ -115,7 +188,9 @@ function TranslationBlock({
{uiState === 'loading'
? 'Translating…'
: sourceLanguage && sourceLangText && !detectedLang
? `Translate from ${sourceLangText}`
? autoDetected
? `Translate from ${sourceLangText} (auto-detected)`
: `Translate from ${sourceLangText}`
: `Translate`}
</span>
</button>
@ -167,4 +242,4 @@ function TranslationBlock({
);
}
export default TranslationBlock;
export default LINGVA_INSTANCES?.length ? TranslationBlock : () => null;

View file

@ -1,11 +1,12 @@
import './index.css';
import './app.css';
import './polyfills';
import { render } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import Compose from './components/compose';
import ComposeSuspense from './components/compose-suspense';
import { initStates } from './utils/states';
import useTitle from './utils/useTitle';
if (window.opener) {
@ -27,6 +28,10 @@ function App() {
: 'Compose',
);
useEffect(() => {
initStates();
}, []);
useEffect(() => {
if (uiState === 'closed') {
try {
@ -57,7 +62,7 @@ function App() {
console.debug('OPEN COMPOSE');
return (
<Compose
<ComposeSuspense
editStatus={editStatus}
replyToStatus={replyToStatus}
draftStatus={draftStatus}

View file

@ -1,3 +1,8 @@
{
"@mastodon/edit-media-attributes": ">=4.1"
"@mastodon/edit-media-attributes": ">=4.1",
"@mastodon/list-exclusive": ">=4.2",
"@mastodon/filtered-notifications": "~4.3 || >=4.3",
"@mastodon/fetch-multiple-statuses": "~4.3 || >=4.3",
"@mastodon/trending-link-posts": "~4.3 || >=4.3",
"@mastodon/grouped-notifications": "~4.3 || >=4.3"
}

View file

@ -1,638 +1,384 @@
[
"daystorm.netz.org",
"mastodon.social",
"pawoo.net",
"mstdn.social",
"mstdn.jp",
"mstdn.social",
"mas.to",
"mastodon.online",
"mastodon.world",
"infosec.exchange",
"fosstodon.org",
"hachyderm.io",
"troet.cafe",
"m.cmx.im",
"fedibird.com",
"techhub.social",
"mastodonapp.uk",
"universeodon.com",
"mastodon.uno",
"chaos.social",
"mastodon.gamedev.place",
"m.cmx.im",
"techhub.social",
"piaille.fr",
"mastodon.gamedev.place",
"mastodonapp.uk",
"mastodon.nl",
"mastodon.art",
"mastodon.cloud",
"mastodon.sdf.org",
"kolektiva.social",
"thu.closed.social",
"mstdn.ca",
"masto.ai",
"c.im",
"alive.bar",
"sfba.social",
"o3o.ca",
"social.vivaldi.net",
"universeodon.com",
"mastodon.sdf.org",
"c.im",
"mstdn.ca",
"kolektiva.social",
"mastodon-japan.net",
"norden.social",
"social.tchncs.de",
"noagendasocial.com",
"det.social",
"wxw.moe",
"mstdn.party",
"aus.social",
"o3o.ca",
"sfba.social",
"nrw.social",
"home.social",
"mastodon.scot",
"tech.lgbt",
"newsie.social",
"mastodon.scot",
"mstdn.party",
"occm.cc",
"aus.social",
"mathstodon.xyz",
"toot.community",
"ohai.social",
"mastodon.top",
"mastodon.ie",
"mamot.fr",
"sueden.social",
"mindly.social",
"mathstodon.xyz",
"meow.social",
"botsin.space",
"mastodon.ie",
"mastodon.top",
"mastodontech.de",
"loforo.com",
"dice.camp",
"ro-mastodon.puyo.jp",
"twit.social",
"planet.moe",
"ioc.exchange",
"mastodon.au",
"mastodon.nu",
"masto.es",
"freemasonry.social",
"ioc.exchange",
"mindly.social",
"hessen.social",
"ruhr.social",
"mastodon.green",
"social.cologne",
"mastodon.nz",
"nerdculture.de",
"muenchen.social",
"mastouille.fr",
"qoto.org",
"social.anoxinon.de",
"twingyeo.kr",
"mastodon.xyz",
"fediscience.org",
"framapiaf.org",
"akamdon.com",
"indieweb.social",
"social.linux.pizza",
"wandering.shop",
"me.dm",
"sigmoid.social",
"aethy.com",
"eldritch.cafe",
"zirk.us",
"ruby.social",
"mastodon-japan.net",
"mstdn.science",
"defcon.social",
"noc.social",
"ravenation.club",
"social.librem.one",
"mstdn.guru",
"mastodont.cat",
"g0v.social",
"ecoevo.social",
"tkz.one",
"social.anoxinon.de",
"mastodon.green",
"mastouille.fr",
"social.linux.pizza",
"social.cologne",
"indieweb.social",
"livellosegreto.it",
"masto.nu",
"med-mastodon.com",
"toot.wales",
"ruby.social",
"ieji.de",
"bildung.social",
"octodon.social",
"urbanists.social",
"pouet.chapril.org",
"mastodon.com.tr",
"social.dev-wiki.de",
"mastodon.nz",
"toot.io",
"digitalcourage.social",
"econtwitter.net",
"climatejustice.social",
"kinky.business",
"mastodontti.fi",
"mastodon.radio",
"metalhead.club",
"tkz.one",
"mastodont.cat",
"social.tchncs.de",
"mastodon.com.tr",
"noc.social",
"sciences.social",
"mastodon.bida.im",
"toot.wales",
"masto.nu",
"phpc.social",
"mastodon.fun",
"berlin.social",
"mstdn.plus",
"mastodon.iriseden.eu",
"101010.pl",
"woof.group",
"obo.sh",
"geekdom.social",
"androiddev.social",
"rollenspiel.social",
"social.lol",
"genomic.social",
"socel.net",
"best-friends.chat",
"mastodonczech.cz",
"wien.rocks",
"mastodon.me.uk",
"scholar.social",
"swiss.social",
"dresden.network",
"yiff.life",
"social.dev-wiki.de",
"cyberplace.social",
"mastodontti.fi",
"climatejustice.social",
"urbanists.social",
"mstdn.plus",
"metalhead.club",
"ravenation.club",
"mastodon.ml",
"fairy.id",
"feuerwehr.social",
"dresden.network",
"stranger.social",
"mastodon.iriseden.eu",
"rollenspiel.social",
"pol.social",
"mstdn.business",
"mstdn.games",
"wien.rocks",
"h4.io",
"socel.net",
"mastodon.eus",
"wehavecookies.social",
"glasgow.social",
"masto.pt",
"sself.co",
"mastodon.me.uk",
"uri.life",
"hostux.social",
"theblower.au",
"openbiblio.social",
"mstdn.games",
"todon.eu",
"typo.social",
"floss.social",
"tabletop.social",
"mastodon.ml",
"freiburg.social",
"writing.exchange",
"rubber.social",
"mstdn.io",
"paquita.masto.host",
"hci.social",
"snabelen.no",
"astrodon.social",
"mastodon-belgium.be",
"queer.party",
"journa.host",
"hcommons.social",
"mastodonners.nl",
"toot.aquilenet.fr",
"mastodon-uk.net",
"masto.pt",
"awscommunity.social",
"mastodon.zaclys.com",
"dju.social",
"ursal.zone",
"stranger.social",
"tooot.im",
"muenster.im",
"social.coop",
"tooting.ch",
"abdl.link",
"toad.social",
"rheinneckar.social",
"social.treehouse.systems",
"shakedown.social",
"peoplemaking.games",
"lor.sh",
"union.place",
"witches.live",
"vis.social",
"equestria.social",
"tilde.zone",
"lewdieheaven.com",
"wetdry.world",
"nofan.xyz",
"h4.io",
"photog.social",
"discuss.systems",
"mastoturk.org",
"bonn.social",
"vmst.io",
"spore.social",
"pol.social",
"flipboard.social",
"imastodon.net",
"cybre.space",
"bsd.network",
"mstdn.maud.io",
"girlcock.club",
"pettingzoo.co",
"mast.lat",
"cupoftea.social",
"bark.lgbt",
"moth.social",
"toot.cat",
"furry.engineer",
"qdon.space",
"otadon.com",
"gruene.social",
"historians.social",
"mapstodon.space",
"douchi.space",
"vocalodon.net",
"layer8.space",
"todon.nl",
"types.pl",
"ludosphere.fr",
"merveilles.town",
"iosdev.space",
"feuerwehr.social",
"mast.dragon-fly.club",
"kemonodon.club",
"macaw.social",
"oldbytes.space",
"medibubble.org",
"expressional.social",
"disabled.social",
"bolha.us",
"freeradical.zone",
"scicomm.xyz",
"graphics.social",
"mona.do",
"toot.blue",
"emacs.ch",
"lile.cl",
"social.sciences.re",
"ai.wiki",
"linuxrocks.online",
"jorts.horse",
"persiansmastodon.com",
"mastodon.berlin",
"liker.social",
"literatur.social",
"masto.bike",
"retro.pizza",
"climatejustice.rocks",
"neurodifferent.me",
"post.lurk.org",
"mastodon.coffee",
"mastodon.gal",
"oslo.town",
"neuromatch.social",
"ika.queloud.net",
"mstdn.beer",
"graz.social",
"libretooth.gr",
"mastodonbooks.net",
"xoxo.zone",
"mastodon.design",
"convo.casa",
"bitbang.social",
"freeatlantis.com",
"masto.nobigtech.es",
"eupolicy.social",
"sociale.network",
"famichiki.jp",
"pkm.social",
"4bear.com",
"freak.university",
"opalstack.social",
"chitter.xyz",
"sciencemastodon.com",
"lgbtqia.space",
"ffxiv-mastodon.com",
"mental.social",
"iztasocial.site",
"artisan.chat",
"vulpine.club",
"musician.social",
"freiburg.social",
"snabelen.no",
"mastodon.zaclys.com",
"muenster.im",
"mastodon-belgium.be",
"geekdom.social",
"hcommons.social",
"tooot.im",
"tooting.ch",
"rheinneckar.social",
"discuss.systems",
"sunny.garden",
"dizl.de",
"glammr.us",
"mastodo.fi",
"kirche.social",
"mastodon.energy",
"kind.social",
"shelter.moe",
"computerfairi.es",
"mastodon.la",
"mastodon.org.uk",
"awoo.space",
"fulda.social",
"witter.cz",
"freemasonry.social",
"jawns.club",
"mao.mastodonhub.com",
"trpg.cloud",
"ramen-fsm.eu.org",
"toot.cafe",
"darmstadt.social",
"mstdn.mx",
"pokemon.mastportal.info",
"toot.lv",
"romancelandia.club",
"better.boston",
"pnw.zone",
"mastodon.content.town",
"rivals.space",
"thecanadian.social",
"cr8r.gg",
"plural.cafe",
"xarxa.cloud",
"esperanto.masto.host",
"federated.press",
"nnia.space",
"digipres.club",
"h5q.net",
"kinkyelephant.com",
"pawb.fun",
"data-folks.masto.host",
"mastodon.uy",
"worldkey.io",
"mastorol.es",
"zeroes.ca",
"mastodon.arch-linux.cz",
"mastodon.acm.org",
"social.bau-ha.us",
"bbq.snoot.com",
"akademienl.social",
"toot.bike",
"vtdon.com",
"uri.life",
"machteburch.social",
"mas.town",
"vkl.world",
"vt.social",
"mastodon.cat",
"podcastindex.social",
"artsio.com",
"dotnet.social",
"oc.todon.fr",
"functional.cafe",
"halifaxsocial.ca",
"babka.social",
"ichiji.social",
"ura-mstdn.com",
"eightpoint.app",
"liberdon.com",
"toot.portes-imaginaire.org",
"mograph.social",
"kirakiratter.com",
"mstdn.tokyocameraclub.com",
"gearheads.social",
"est.social",
"mastodon.mim-libre.fr",
"swiss-talk.net",
"donphan.social",
"masto.nyc",
"mapstodon.space",
"toad.social",
"lor.sh",
"peoplemaking.games",
"union.place",
"bark.lgbt",
"bonn.social",
"tilde.zone",
"vmst.io",
"mastodon.berlin",
"emacs.ch",
"blorbo.social",
"qubit-social.xyz",
"en.osm.town",
"gulp.cafe",
"assemblag.es",
"mstdn.kemono-friends.info",
"tyrol.social",
"social.seattle.wa.us",
"toot.kif.rocks",
"twiukraine.com",
"social.politicaconciencia.org",
"icosahedron.website",
"toot.si",
"mastodon.in.th",
"norcal.social",
"warhammer.social",
"bookwor.ms",
"kanoa.de",
"furry.engineer",
"rivals.space",
"cupoftea.social",
"qdon.space",
"graphics.social",
"veganism.social",
"cryptodon.lol",
"jasette.facil.services",
"is.nota.live",
"epicure.social",
"sauropods.win",
"kurry.social",
"hometech.social",
"kopiti.am",
"biplus.date",
"spacey.space",
"photodn.net",
"blabber.lu-rp.net",
"im-in.space",
"wargamers.social",
"toot.berlin",
"archaeo.social",
"col.social",
"h-net.social",
"social.kyiv.dcomm.net.ua",
"dobbs.town",
"mastodon.com.br",
"ludosphere.fr",
"4bear.com",
"famichiki.jp",
"expressional.social",
"convo.casa",
"historians.social",
"mastorol.es",
"retro.pizza",
"shelter.moe",
"mast.dragon-fly.club",
"sakurajima.moe",
"mastodon.arch-linux.cz",
"squawk.mytransponder.com",
"mastodon.gal",
"disabled.social",
"vkl.world",
"eupolicy.social",
"fandom.ink",
"toot.funami.tech",
"nafo.uk",
"arsenalfc.social",
"social.edu.nl",
"sunbeam.city",
"federate.social",
"hello.2heng.xin",
"gensokyo.town",
"mastodon.tetaneutral.net",
"tablegame.mstdn.cloud",
"elekk.xyz",
"blacktwitter.io",
"burma.social",
"osna.social",
"seocommunity.social",
"otogamer.me",
"mstdn.fr",
"toki.social",
"colearn.social",
"cloud-native.social",
"mstdn-bike.net",
"mastodon.hypnoguys.com",
"lounge.town",
"guitar.rodeo",
"mastodon.mit.edu",
"hispagatos.space",
"mstdn.id",
"flower.afn.social",
"parfait.day",
"nederland.online",
"ani.work",
"mastodon.education",
"mastodon.gougere.fr",
"cztwitter.cz",
"uwu.social",
"mastodon.bayern",
"gameliberty.club",
"sukebe.hostdon.ne.jp",
"social.veraciousnetwork.com",
"mastodon.vlaanderen",
"earthstream.social",
"xn--lofll-1sat.is",
"social.datalabour.com",
"gametoots.de",
"mastodon.com.py",
"outdoors.lgbt",
"arvr.social",
"loðfíll.is",
"social.yesterweb.org",
"9kb.me",
"mstdn.dk",
"occitania.social",
"apobangpo.space",
"dingdash.com",
"mastodon.chasem.dev",
"oulipo.social",
"digforfire.org",
"mastodon.partipirate.org",
"mastodon.hk",
"mastoot.fr",
"eigadon.net",
"irsoluciones.social",
"maly.io",
"birds.town",
"kfem.cat",
"beekeeping.ninja",
"mastodon.juggler.jp",
"oransns.com",
"anticapitalist.party",
"deadinsi.de",
"gardenstate.social",
"mastodon.cc",
"piano.masto.host",
"eletusk.club",
"lewacki.space",
"mastodon.pirateparty.be",
"anarchism.space",
"mastodon.cisti.org",
"metalverse.social",
"truthsocial.co.in",
"baraag.net",
"yakyudon.net",
"lou.lt",
"social.slat.org",
"gensokyo.social",
"social.chinwag.org",
"tribe.net",
"lgbt.io",
"toots.social",
"pravda.me",
"aleph.land",
"poweredbygay.social",
"masto.yttrx.com",
"yttrx.com",
"toot.pizza",
"drumstodon.net",
"acg.mn",
"kpop.social",
"toolboxtalk.tech",
"bear.community",
"otoya.space",
"mastodon.triggerphra.se",
"mastodon.free-solutions.org",
"rcsocial.net",
"kith.kitchen",
"vocalounge.cafe",
"pieville.net",
"mstdn.osaka",
"mastodon.mnetwork.co.kr",
"mstdn.es",
"seo.chat",
"mastodol.jp",
"renkontu.com",
"mastodon.cipherbliss.com",
"toot.turbo.chat",
"catdon.life",
"social.coletivos.org",
"toot.thoughtworks.com",
"mastodonbooks.net",
"lgbtqia.space",
"witter.cz",
"planetearth.social",
"oslo.town",
"mastodon.com.pl",
"pawb.fun",
"darmstadt.social",
"masto.nobigtech.es",
"cr8r.gg",
"pnw.zone",
"hear-me.social",
"furries.club",
"gaygeek.social",
"birdon.social",
"mastodon.energy",
"mastodon-swiss.org",
"social.targaryen.house",
"moe.cat",
"bologna.one",
"toot.site",
"e.fo",
"mastodon.holeyfox.co",
"m.rthome.me",
"stereodon.social",
"social.opendesktop.org",
"bgme.me",
"social.caa-ins.org",
"nojack.easydns.ca",
"mastodon.oeru.org",
"mastodon.elte.hu",
"nasface.cz",
"lilymagic.com",
"mast.moe",
"mastodon.librelabucm.org",
"fetswing.org",
"mastodon.cosmicanimal.jp",
"todon.ploud.fr",
"ephemeral.glitch.social",
"mikumikudance.cloud",
"summoners-riftodon.jp",
"kinbaku.club",
"www.mstddntfdn.online",
"dev.brighteon.social",
"jaxbeach.social",
"animalliberation.social",
"onmasto.com",
"pet123.club",
"ostatus.ikeji.ma",
"counter.social",
"the.resize.club",
"social.outsourcedmath.com",
"nerdculture.de",
"pewtix.com",
"med-mammoth.com",
"ping-pong-sandbox.herokuapp.com",
"id.cc",
"freespeechextremist.com",
"cawfee.club",
"1234.as",
"fedi.absturztau.be",
"fsmi.social",
"go5.dev",
"poa.st",
"patriot.online",
"stereophonic.space",
"kazv.moe",
"seaofog.com",
"libranet.de",
"tea.codes",
"pixelfed.social",
"shitposter.club",
"squeet.me",
"shared.graphics",
"glindr.org",
"devs.live",
"pxlmo.com",
"pixel.tchncs.de",
"pythondevs.social",
"pleroma.pibvt.net",
"books.theunseen.city",
"love.alicecomplex.com",
"dizl.de",
"libretooth.gr",
"mustard.blog",
"machteburch.social",
"fulda.social",
"muri.network",
"babka.social",
"archaeo.social",
"mastodon.uy",
"xarxa.cloud",
"corteximplant.com",
"mastodon.london",
"greenish.red",
"pixelfed.sdf.org",
"anar.chi.st",
"friendica.eskimo.com",
"meatbag.app",
"dudu.best",
"pix.diaspodon.fr",
"shpposter.club",
"pix.toot.wales",
"pleroma.noellabo.jp",
"fgc.network",
"bookrastinating.com",
"pixey.org",
"fe.disroot.org",
"pixelfed.tokyo",
"urusai.social",
"thecanadian.social",
"federated.press",
"kanoa.de",
"opalstack.social",
"bahn.social",
"mograph.social",
"dmv.community",
"social.bau-ha.us",
"mastodon.free-solutions.org",
"masto.nyc",
"tyrol.social",
"burma.social",
"toot.kif.rocks",
"donphan.social",
"mast.hpc.social",
"musicians.today",
"drupal.community",
"hometech.social",
"norcal.social",
"social.politicaconciencia.org",
"social.seattle.wa.us",
"is.nota.live",
"genealysis.social",
"wargamers.social",
"guitar.rodeo",
"bookstodon.com",
"mstdn.dk",
"elizur.me",
"irsoluciones.social",
"h-net.social",
"mastoot.fr",
"qaf.men",
"est.social",
"kurry.social",
"mastodon.pnpde.social",
"ani.work",
"nederland.online",
"epicure.social",
"occitania.social",
"lgbt.io",
"mountains.social",
"persiansmastodon.com",
"seocommunity.social",
"cyberfurz.social",
"fedi.at",
"gamepad.club",
"augsburg.social",
"mastodon.education",
"toot.re",
"linux.social",
"neovibe.app",
"musician.social",
"esq.social",
"social.veraciousnetwork.com",
"datasci.social",
"tooters.org",
"ciberlandia.pt",
"cloud-native.social",
"social.silicon.moe",
"cosocial.ca",
"arvr.social",
"hispagatos.space",
"friendsofdesoto.social",
"musicworld.social",
"aut.social",
"masto.yttrx.com",
"mastodon.wien",
"448c.net",
"freeframe.masto.host",
"pixelfed.photos",
"varishangout.net",
"pixelfed.fr",
"friendica.vrije-mens.org",
"mastodon.tech",
"bae.st",
"brighteon.social",
"pixelfed.nz",
"hayu.sh",
"pixelfed.uno",
"pixelfed.au",
"helladoge.com",
"miniwa.moe",
"genserver.social",
"bookwyrm.social",
"spinster.xyz",
"pixelfed.de",
"metapixl.com",
"neenster.org",
"venera.social",
"outerheaven.club",
"gleasonator.com",
"pixelfed.fi",
"blob.cat",
"pxl.roflcopter.fr",
"gc2.jp",
"kids.0px.io"
"colorid.es",
"arsenalfc.social",
"allthingstech.social",
"mastodon.vlaanderen",
"mastodon.com.py",
"tooter.social",
"lounge.town",
"puntarella.party",
"earthstream.social",
"apobangpo.space",
"opencoaster.net",
"frikiverse.zone",
"airwaves.social",
"toot.garden",
"lewacki.space",
"gardenstate.social",
"theatl.social",
"maly.io",
"library.love",
"kfem.cat",
"ruhrpott.social",
"techtoots.com",
"furry.energy",
"mastodon.pirateparty.be",
"metalverse.social",
"indieauthors.social",
"tuiter.rocks",
"mastodon.africa",
"jvm.social",
"poweredbygay.social",
"fikaverse.club",
"gametoots.de",
"mastodon.cr",
"hoosier.social",
"khiar.net",
"seo.chat",
"drumstodon.net",
"raphus.social",
"toots.nu",
"k8s.social",
"mastodon.holeyfox.co",
"fribygda.no",
"x0r.be",
"fpl.social",
"toot.pizza",
"mastodon.cipherbliss.com",
"burningboard.net",
"synapse.cafe",
"cultur.social",
"vermont.masto.host",
"mastodon.bot",
"bologna.one",
"mastodon.sg",
"tchafia.be",
"rail.chat",
"mastodon.hosnet.fr",
"leipzig.town",
"wayne.social",
"rheinhessen.social",
"rap.social",
"cwb.social",
"mastodon.bachgau.social",
"cville.online",
"bzh.social",
"mastodon.escepticos.es",
"zenzone.social",
"mastodon.ee",
"lsbt.me",
"neurodiversity-in.au",
"fairmove.net",
"stereodon.social",
"mcr.wtf",
"mastodon.frl",
"mikumikudance.cloud",
"okla.social",
"camp.smolnet.org",
"ailbhean.co-shaoghal.net",
"clj.social",
"tu.social",
"nomanssky.social",
"mastodon.iow.social",
"frontrange.co",
"episcodon.net",
"devianze.city",
"paktodon.asia",
"travelpandas.fr",
"silversword.online",
"nwb.social",
"skastodon.com",
"kcmo.social",
"balkan.fedive.rs",
"openedtech.social",
"mastodon.ph",
"enshittification.social",
"spojnik.works",
"mastodon.conquestuniverse.com",
"nutmeg.social",
"social.sndevs.com",
"social.diva.exchange",
"growers.social",
"pdx.sh",
"nfld.me",
"cartersville.social",
"voi.social",
"mastodon.babb.no",
"kzoo.to",
"mastodon.vanlife.is",
"toot.works",
"sanjuans.life",
"dariox.club",
"xreality.social",
"social.ferrocarril.net",
"pool.social",
"polsci.social",
"mastodon.mg",
"23.illuminati.org",
"apotheke.social",
"jaxbeach.social",
"ceilidh.online",
"netsphere.one",
"biplus.social",
"bvb.social",
"ms.maritime.social",
"darticulate.com",
"persia.social",
"streamerchat.social",
"troet.fediverse.at",
"publishing.social",
"finsup.social",
"kjas.no",
"wxw.moe",
"learningdisability.social",
"mastodon.bida.im",
"computerfairi.es",
"tea.codes"
]

View file

@ -909,11 +909,36 @@
"Zulu",
"isiZulu"
],
[
"zh-CN",
"Chinese (China)",
"简体中文"
],
[
"zh-HK",
"Chinese (Hong Kong)",
"繁體中文(香港)"
],
[
"zh-TW",
"Chinese (Taiwan)",
"繁體中文(臺灣)"
],
[
"zh-YUE",
"Cantonese",
"廣東話"
],
[
"ast",
"Asturian",
"Asturianu"
],
[
"chr",
"Cherokee",
"ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ"
],
[
"ckb",
"Sorani (Kurdish)",
@ -934,11 +959,6 @@
"Kabyle",
"Taqbaylit"
],
[
"kmr",
"Kurmanji (Kurdish)",
"Kurmancî"
],
[
"ldn",
"Láadan",
@ -974,6 +994,11 @@
"Toki Pona",
"toki pona"
],
[
"xal",
"Kalmyk",
"Хальмг келн"
],
[
"zba",
"Balaibalan",

View file

@ -8,15 +8,40 @@
--sai-left: env(safe-area-inset-left);
--text-size: 16px;
--main-width: 40em;
--main-width: max(60dvw, 40em);
text-size-adjust: none;
--hairline-width: 1px;
--monospace-font: ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono',
Menlo, Courier, monospace;
--blue-color: royalblue;
--purple-color: blueviolet;
--purple-fg-color: color-mix(
in srgb-linear,
var(--purple-color) 60%,
var(--text-color) 40%
);
--purple-bg-color: color-mix(in srgb, var(--purple-color) 10%, transparent);
--green-color: darkgreen;
--orange-color: darkorange;
--orange-light-bg-color: color-mix(
in srgb,
var(--orange-color) 20%,
transparent
);
--orange-fg-color: color-mix(
in srgb-linear,
var(--orange-color) 60%,
var(--text-color) 40%
);
--orange-bg-color: color-mix(in srgb, var(--orange-color) 10%, transparent);
--red-color: orangered;
--red-text-color: color-mix(
in srgb-linear,
var(--red-color) 60%,
var(--text-color) 40%
);
--red-bg-color: color-mix(in lch, var(--red-color) 40%, transparent);
--bg-color: #fff;
--bg-faded-color: #f0f2f5;
--bg-blur-color: #fff9;
@ -24,10 +49,16 @@
--text-color: #1c1e21;
--text-insignificant-color: #1c1e2199;
--link-color: var(--blue-color);
--link-bg-color: #4169e122;
--link-light-color: #4169e199;
--link-faded-color: #4169e155;
--link-bg-hover-color: #f0f2f599;
--link-visited-color: mediumslateblue;
--link-text-color: color-mix(
in lch,
var(--link-color) 60%,
var(--text-color) 40%
);
--focus-ring-color: var(--link-color);
--button-bg-color: var(--blue-color);
--button-bg-blur-color: #4169e1aa;
@ -35,15 +66,29 @@
--button-plain-bg-hover-color: rgba(128, 128, 128, 0.1);
--reblog-color: var(--purple-color);
--reblog-faded-color: #892be220;
--group-color: var(--green-color);
--group-faded-color: #00640020;
--reply-to-color: var(--orange-color);
--reply-to-text-color: #b36200;
--favourite-color: var(--red-color);
--reply-to-faded-color: #ffa60030;
--reply-to-faded-color: #ffa60020;
--hashtag-color: LightSeaGreen;
--hashtag-faded-color: color-mix(
in srgb,
var(--hashtag-color) 15%,
transparent
);
--hashtag-text-color: color-mix(
in lch,
var(--hashtag-color) 40%,
var(--text-color) 60%
);
--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-solid-color: #ccc;
--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);
--loader-color: #1c1e2199;
--comment-line-color: #e5e5e5;
@ -52,8 +97,30 @@
--close-button-bg-active-color: rgba(0, 0, 0, 0.2);
--close-button-color: rgba(0, 0, 0, 0.5);
--close-button-hover-color: rgba(0, 0, 0, 1);
--private-note-text-color: var(--text-color);
--private-note-bg-color: color-mix(in srgb, yellow 20%, var(--bg-color));
--private-note-border-color: rgba(0, 0, 0, 0.2);
/* Media colors won't change based on color scheme */
--media-fg-color: #f0f2f5;
--media-bg-color: #242526;
--media-outline-color: color-mix(in lch, var(--media-fg-color), transparent);
--timing-function: cubic-bezier(0.3, 0.5, 0, 1);
--spring-timing-funtion: cubic-bezier(0.175, 0.885, 0.32, 1.275);
--min-dimension: 88px;
--forward: right;
--backward: left;
--to-forward: to right;
--to-backward: to left;
&:dir(rtl) {
--forward: left;
--backward: right;
--to-forward: to left;
--to-backward: to right;
}
}
@media (min-resolution: 2dppx) {
@ -77,13 +144,18 @@
--link-light-color: #6494ed99;
--link-faded-color: #6494ed88;
--link-bg-hover-color: #34353799;
--link-visited-color: color-mix(
in lch,
mediumslateblue 70%,
var(--text-color) 30%
);
--reblog-faded-color: #b190f141;
--reply-to-text-color: var(--reply-to-color);
--reply-to-faded-color: #ffa60027;
--reply-to-faded-color: #ffa60017;
--divider-color: rgba(255, 255, 255, 0.1);
--bg-blur-color: #24252699;
--backdrop-color: rgba(0, 0, 0, 0.5);
--backdrop-solid-color: #333;
--backdrop-solid-color: #111;
--loader-color: #f0f2f599;
--comment-line-color: #565656;
--drop-shadow-color: rgba(0, 0, 0, 0.5);
@ -91,6 +163,7 @@
--close-button-bg-active-color: rgba(255, 255, 255, 0.15);
--close-button-color: rgba(255, 255, 255, 0.5);
--close-button-hover-color: rgba(255, 255, 255, 1);
--private-note-border-color: rgba(255, 255, 255, 0.2);
}
}
@ -109,12 +182,21 @@ html {
}
body {
font-family: ui-rounded, system-ui;
font-family: ui-rounded, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Ubuntu, Cantarell, Noto Sans, sans-serif;
font-size: var(--text-size);
word-wrap: break-word;
overflow-wrap: break-word;
}
/* Prevent pull-to-refresh on Chrome PWA */
@media (display-mode: standalone) {
html,
body {
overscroll-behavior-y: none;
}
}
a {
color: var(--link-color);
text-decoration-color: var(--link-faded-color);
@ -166,13 +248,16 @@ button,
text-decoration: none;
user-select: none;
}
button[hidden] {
display: none;
}
:is(button, .button) > * {
vertical-align: middle;
pointer-events: none;
}
: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);
@ -203,6 +288,23 @@ button,
:is(button, .button).plain4:not(:disabled, .disabled):is(:hover, :focus) {
color: var(--text-color);
}
:is(button, .button).plain5 {
background-color: transparent;
color: var(--link-color);
text-decoration: underline;
text-decoration-color: var(--link-faded-color);
}
: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);
@ -256,6 +358,7 @@ button,
}
input[type='text'],
input[type='search'],
textarea,
select {
color: var(--text-color);
@ -265,6 +368,7 @@ select {
border-radius: 4px;
}
input[type='text']:focus,
input[type='search']:focus,
textarea:focus,
select:focus {
border-color: var(--outline-color);
@ -276,12 +380,26 @@ button.large {
font-size: 125%;
padding: 12px;
}
textarea:disabled {
background-color: var(--bg-faded-color);
}
button.small {
:is(input[type='text'], input[type='search'], textarea, select).block {
display: block;
width: 100%;
}
:is(button, .button).small {
font-size: 90%;
padding: 4px 8px;
}
.button.centered {
display: inline-flex;
justify-content: center;
align-items: center;
}
select.plain {
border: 0;
background-color: transparent;
@ -291,10 +409,10 @@ pre {
tab-size: 2;
}
pre code,
code {
code,
kbd {
font-size: 90%;
font-family: ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono',
Menlo, Courier, monospace;
font-family: var(--monospace-font);
}
@media (prefers-color-scheme: dark) {
@ -335,6 +453,11 @@ code {
display: initial;
}
.bidi-isolate {
direction: initial;
unicode-bidi: isolate;
}
/* KEYFRAMES */
@keyframes appear {
@ -377,6 +500,17 @@ code {
}
}
@keyframes slide-up-smooth {
0% {
opacity: 0;
transform: translateY(100%);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes position-object {
0% {
object-position: 50% 50%;
@ -414,3 +548,30 @@ code {
.shazam-container-inner {
overflow: hidden;
}
@keyframes shazam-horizontal {
0% {
grid-template-columns: 0fr;
}
100% {
grid-template-columns: 1fr;
}
}
.shazam-container-horizontal {
display: grid;
grid-template-columns: 1fr;
transition: grid-template-columns 0.5s ease-in-out;
white-space: nowrap;
}
.shazam-container-horizontal:not(.no-animation) {
animation: shazam-horizontal 0.5s ease-in-out both !important;
}
.shazam-container-horizontal[hidden] {
grid-template-columns: 0fr;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}

View file

@ -1,7 +1,10 @@
import './index.css';
import './cloak-mode.css';
import './polyfills';
// 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';

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