Compare commits

...

156 commits

Author SHA1 Message Date
Natsu Kagami 7fa08d4b6d
Don't statically pin the size of the carousel 2024-05-19 02:01:52 +02:00
Natsu Kagami 97790d929e
Link to GtS settings when we know we are on GtS 2024-05-19 02:01:52 +02:00
Natsu Kagami 394bcb9cf8
Make main column bigger 2024-05-19 02:01:52 +02:00
Natsu Kagami 2f3adec3ea
Add a bit more touch 2024-05-19 02:01:52 +02:00
Natsu Kagami d25ab5b38f
Force display instance
Because I don't like this decision from Phanpy
2024-05-19 02:01:51 +02:00
Natsu Kagami 5324b69bc5
Automatically put people into DTTHDon login 2024-05-19 02:01:51 +02:00
Natsu Kagami f51b94f56b
Add some DTTH notice 2024-05-19 02:01:51 +02:00
Natsu Kagami b3d419b38a
Incorporate commit hash 2024-05-19 02:01:51 +02:00
Natsu Kagami 56fdd902f7
Get flakes to work 2024-05-19 02:01:50 +02:00
Chee Aun 6976191113
Merge pull request #532 from cheeaun/main
Update from main
2024-05-15 21:34:39 +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
Chee Aun e224bdc8c3
Merge pull request #525 from cheeaun/main
Update from main
2024-05-06 21:42:48 +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
Chee Aun 51457302f8
Merge pull request #506 from cheeaun/main
Update from main
2024-04-19 09:18:55 +08:00
Chee Aun b6cf53c221
Merge pull request #489 from cheeaun/main
Update from main
2024-04-17 20:21:00 +08:00
Chee Aun 65d51b077f
Merge pull request #475 from cheeaun/main
Update from main
2024-04-04 19:41:09 +08:00
Chee Aun ecd308ce39
Merge pull request #472 from cheeaun/main
Update from main
2024-03-28 20:55:24 +08:00
Chee Aun 8fc5e10be9
Merge pull request #465 from cheeaun/main
Update from main
2024-03-26 22:19:53 +08:00
Chee Aun c7c764a4f0
Merge pull request #459 from cheeaun/main
Update from main
2024-03-17 21:12:58 +08:00
Chee Aun e73ae563e6
Merge pull request #456 from cheeaun/main
Update from main
2024-03-12 14:57:02 +08:00
Chee Aun d34cce6d18
Merge pull request #450 from cheeaun/main
Update from main
2024-03-11 17:14:49 +08:00
Chee Aun aed84226a1
Merge pull request #446 from cheeaun/main
Update from main
2024-03-07 22:28:08 +08:00
Chee Aun 6827a3811e
Merge pull request #445 from cheeaun/main
Update from main
2024-03-06 19:24:58 +08:00
Chee Aun 83f9498b79
Merge pull request #429 from cheeaun/main
Update from main
2024-03-06 17:52:16 +08:00
Chee Aun 77818d3ac8
Merge pull request #412 from cheeaun/main
Update from main
2024-02-25 20:32:39 +08:00
Chee Aun 35a31e1613
Merge pull request #406 from cheeaun/main
Update from main
2024-02-04 00:40:45 +08:00
Chee Aun 247ed4a7e1
Merge pull request #404 from cheeaun/main
Update from main
2024-01-21 23:05:33 +08:00
Chee Aun 98e6a6e42e
Merge pull request #401 from cheeaun/main
Update from main
2024-01-18 10:49:55 +08:00
Chee Aun fe91c13703
Merge pull request #397 from cheeaun/main
Update from main
2024-01-16 18:36:41 +08:00
Chee Aun e791ea8015
Merge pull request #393 from cheeaun/main
Update from main
2024-01-11 21:49:26 +08:00
Chee Aun dbef125ee3
Merge pull request #387 from cheeaun/main
Update from main
2024-01-09 00:13:19 +08:00
Chee Aun 32c53b8c6e
Merge pull request #388 from cheeaun/hotfix/infinite-loop-intersection-observer
[Hotfix] Infinite loop bug in intersection observer
2024-01-06 03:24:21 +08:00
Lim Chee Aun bd112f19b1 Potential fix to infinite loop of intersection observer 2024-01-06 03:18:08 +08:00
Chee Aun adf0b351c1
Merge pull request #383 from cheeaun/main
Update from main
2024-01-05 10:16:09 +08:00
Chee Aun 8aa05422b1
Merge pull request #375 from cheeaun/main
Update from main
2023-12-31 18:21:25 +08:00
Chee Aun 1e21f519f3
Merge pull request #360 from cheeaun/main
Update from main
2023-12-22 23:34:15 +08:00
Chee Aun 19da64a787
Merge pull request #352 from cheeaun/main
Update from main
2023-12-13 23:18:23 +08:00
Chee Aun efca016d81
Merge pull request #337 from cheeaun/main
Update from main
2023-12-06 15:23:20 +08:00
Chee Aun 1d9d22a214
Merge pull request #334 from cheeaun/main
Update from main
2023-11-25 18:04:32 +08:00
Chee Aun e4dd76cb11
Merge pull request #333 from cheeaun/main
Update from main
2023-11-22 11:16:11 +08:00
Chee Aun 0f537667d8
Merge pull request #327 from cheeaun/main
Update from main
2023-11-20 13:29:33 +08:00
Chee Aun 3f575142f1
Merge pull request #321 from cheeaun/main
Update from main
2023-11-16 20:01:02 +08:00
Chee Aun 11407d0f3c
Merge pull request #311 from cheeaun/main
Update from main
2023-11-13 22:13:48 +08:00
Chee Aun 84b6368253
Merge pull request #310 from cheeaun/main
Update from main
2023-11-07 14:59:48 +08:00
Chee Aun a75b214999
Merge pull request #308 from cheeaun/main
Update from main
2023-11-07 00:52:17 +08:00
Chee Aun 33c3b63b6e
Merge pull request #307 from cheeaun/main
Update from main
2023-11-06 23:05:24 +08:00
Chee Aun 74991c326d
Merge pull request #286 from cheeaun/main
Update from main
2023-11-06 21:18:15 +08:00
Chee Aun f558a8cd32
Merge pull request #283 from cheeaun/main
Update from main
2023-10-27 01:04:55 +08:00
Chee Aun 87f1d17ce3
Merge pull request #272 from cheeaun/main
Update from main
2023-10-26 20:26:20 +08:00
Chee Aun 0cf7d683ee
Merge pull request #268 from cheeaun/main
Update from main
2023-10-20 00:44:53 +08:00
Chee Aun cb80057f21
Merge pull request #267 from cheeaun/main
Update from main
2023-10-19 07:17:54 +08:00
Chee Aun bf6ee572fb
Merge pull request #266 from cheeaun/main
Update from main
2023-10-18 16:36:11 +08:00
Chee Aun 7b049385f7
Merge pull request #264 from cheeaun/main
Update from main
2023-10-16 22:10:32 +08:00
Chee Aun 6feb2e7b41
Merge pull request #263 from cheeaun/main
Update from main
2023-10-16 20:06:11 +08:00
Chee Aun 4ea8e2c145
Merge pull request #261 from cheeaun/main
Update from main
2023-10-16 19:53:00 +08:00
Chee Aun cd68aee186
Merge pull request #260 from cheeaun/main
Update from main
2023-10-12 01:34:30 +08:00
Chee Aun 5ba2af0970
Merge pull request #259 from cheeaun/main
Update from main
2023-10-09 23:10:34 +08:00
Chee Aun 9b1800dc56
Merge pull request #254 from cheeaun/main
Update from main
2023-10-07 22:23:18 +08:00
Chee Aun 2e97f19133
Merge pull request #252 from cheeaun/main
Update from main
2023-10-06 21:19:29 +08:00
Chee Aun 3f23d42966
Merge pull request #251 from cheeaun/main
Update from main
2023-10-04 12:35:04 +08:00
Chee Aun e9c5025d31
Merge pull request #250 from cheeaun/main
Update from main
2023-10-04 00:00:27 +08:00
Chee Aun eb013645e7
Merge pull request #248 from cheeaun/main
Update from main
2023-10-03 19:21:44 +08:00
Chee Aun d5a3b48f0f
Merge pull request #228 from cheeaun/main
Update from main
2023-10-02 23:05:44 +08:00
Chee Aun b8d92bceb2
Merge pull request #223 from cheeaun/main
Update from main
2023-09-01 13:00:48 +08:00
Chee Aun def1e8d099
Merge pull request #206 from cheeaun/main
Update from main
2023-08-24 17:50:49 +08:00
Chee Aun 8e2099daa7
Merge pull request #200 from cheeaun/main
Update from main
2023-08-06 12:03:53 +08:00
Chee Aun ba81352844
Merge pull request #195 from cheeaun/main
Update from main
2023-07-30 15:23:09 +08:00
Chee Aun 650b71e9cc
Merge pull request #194 from cheeaun/main
Update from main
2023-07-23 23:29:16 +08:00
Chee Aun a6e369b1a8
Merge pull request #193 from cheeaun/main 2023-07-23 01:07:14 +08:00
Chee Aun f479feba65
Merge pull request #192 from cheeaun/main
Update from main
2023-07-22 21:06:22 +08:00
Chee Aun 38680aa6e7
Merge pull request #190 from cheeaun/main
Update from main
2023-07-22 10:04:50 +08:00
Chee Aun d1d606fa10
Merge pull request #182 from cheeaun/main
Update from main
2023-07-21 10:32:30 +08:00
Chee Aun 375da8d173
Merge pull request #173 from cheeaun/main
Update from main
2023-06-30 23:25:52 +08:00
Chee Aun 2c31e8e04c
Merge pull request #172 from cheeaun/main
Update from main
2023-06-24 09:15:26 +08:00
Chee Aun 534c4c97cd
Merge pull request #170 from cheeaun/main
Update from main
2023-06-20 21:38:11 +08:00
Chee Aun 482a64cfac
Merge pull request #166 from cheeaun/main
Update from main
2023-06-16 18:21:45 +08:00
Chee Aun 2dc1343f54
Merge pull request #156 from cheeaun/main
Update from main
2023-06-15 09:05:28 +08:00
Chee Aun 5e52fa87e0
Merge pull request #153 from cheeaun/main
Update from main
2023-05-30 09:46:02 +08:00
Chee Aun 6b03ae1fee
Merge pull request #152 from cheeaun/main
Update from main
2023-05-27 21:57:38 +08:00
Chee Aun c763d8b954
Merge pull request #151 from cheeaun/main
Update from main
2023-05-23 13:11:19 +08:00
Chee Aun 0a5d7267d5
Merge pull request #143 from cheeaun/main
Update from main
2023-05-22 23:40:44 +08:00
Chee Aun 5ee926481a
Merge pull request #142 from cheeaun/main
Update from main
2023-05-18 00:13:24 +08:00
Chee Aun 0cd9a2db6e
Merge pull request #141 from cheeaun/main
Update from main
2023-05-16 19:38:41 +08:00
Chee Aun 69f9b750c2
Merge pull request #139 from cheeaun/main
Update from main
2023-05-14 22:02:10 +08:00
Chee Aun f5955ef258
Merge pull request #133 from cheeaun/main
Update from main
2023-05-14 11:55:03 +08:00
Chee Aun 27a999f733
Merge pull request #122 from cheeaun/main
Update from main
2023-05-11 21:59:39 +08:00
Chee Aun 54271101c1
Merge pull request #119 from cheeaun/main
Update from main
2023-04-28 23:23:10 +08:00
Chee Aun d0cbb0812d
Merge pull request #117 from cheeaun/main
Update from main
2023-04-26 15:36:11 +08:00
Chee Aun ad45bf9d19
Merge pull request #97 from cheeaun/main
Update from main
2023-04-25 22:54:01 +08:00
Chee Aun 982f7b3ec4
Merge pull request #94 from cheeaun/main
Update from main
2023-04-07 21:57:29 +08:00
Chee Aun 4e50f227d8
Merge pull request #93 from cheeaun/main
Update from main
2023-04-03 09:28:40 +08:00
Chee Aun 546e77d3e1
Merge pull request #92 from cheeaun/main
Update from main
2023-04-03 01:25:32 +08:00
Chee Aun e29f14bbcf
Merge pull request #86 from cheeaun/main
Update from main
2023-03-31 23:21:27 +08:00
Chee Aun 05e87e084a
Merge pull request #84 from cheeaun/main
Update from main
2023-03-16 23:31:30 +08:00
Chee Aun 01f10d3daa
Merge pull request #83 from cheeaun/main
Update from main
2023-03-15 23:25:58 +08:00
Chee Aun fc615e0c0d
Merge pull request #82 from cheeaun/main
Update from main
2023-03-15 22:12:17 +08:00
Chee Aun 25e9771754
Merge pull request #80 from cheeaun/main
Update from main
2023-03-15 21:21:04 +08:00
Chee Aun 5e916559b3
Merge pull request #79 from cheeaun/main
Update from main
2023-03-15 20:49:59 +08:00
Chee Aun 883fe39b6c
Merge pull request #78 from cheeaun/main
Update from main
2023-03-03 13:08:28 +08:00
Chee Aun 9933d83846
Merge pull request #77 from cheeaun/main
Update from main
2023-03-02 22:56:43 +08:00
Chee Aun 7d806301f2
Merge pull request #74 from cheeaun/main
Update from main
2023-03-02 22:29:28 +08:00
Chee Aun faf9cbf23d
Merge pull request #73 from cheeaun/main
Update from main
2023-02-24 12:35:44 +08:00
Chee Aun a0f79e7eea
Merge pull request #71 from cheeaun/main
Update from main
2023-02-24 12:04:00 +08:00
Chee Aun 0b1974e94b
Merge pull request #70 from cheeaun/main
Update from main
2023-02-23 23:31:09 +08:00
Chee Aun b4a4615b9a
Merge pull request #68 from cheeaun/main
Update from main
2023-02-22 09:51:37 +08:00
Chee Aun dda14587c0
Merge pull request #67 from cheeaun/main
Update from main
2023-02-22 00:47:07 +08:00
Chee Aun ed9289d8c6
Merge pull request #66 from cheeaun/main
Update from main
2023-02-21 09:22:54 +08:00
Chee Aun 6274f2f24f
Merge pull request #63 from cheeaun/main
Update from main
2023-02-20 00:49:21 +08:00
Chee Aun b4e8ba820c
Merge pull request #62 from cheeaun/main
Update from main
2023-02-20 00:23:56 +08:00
Chee Aun 29896dfe0e
Merge pull request #54 from cheeaun/main
Update from main
2023-02-19 22:33:56 +08:00
Chee Aun 69c3f1a082
Merge pull request #53 from cheeaun/main
Update from main
2023-02-01 02:10:43 +08:00
Chee Aun 451dc57a69
Merge pull request #49 from cheeaun/main
Update from main
2023-02-01 01:27:15 +08:00
Chee Aun 4fbee9168d
Merge pull request #48 from cheeaun/main
Update from main
2023-01-17 21:34:52 +08:00
Chee Aun 6ecc015199
Merge pull request #47 from cheeaun/main
Update from main
2023-01-17 18:05:25 +08:00
Chee Aun a7a3d5605b
Merge pull request #42 from cheeaun/main
Update from main
2023-01-06 23:24:13 +08:00
Chee Aun ad4ed66cd6
Merge pull request #41 from cheeaun/main
Update from main
2023-01-01 19:26:30 +08:00
Chee Aun 4277992773
Merge pull request #39 from cheeaun/main
Update from main
2023-01-01 19:06:33 +08:00
Chee Aun 6bcf6b143c
Merge pull request #38 from cheeaun/main
Update from main
2022-12-28 20:57:56 +08:00
Chee Aun 9e9f7a6ea1
Merge pull request #37 from cheeaun/main
Update from main
2022-12-27 22:07:51 +08:00
Chee Aun f0014cb26a
Merge pull request #36 from cheeaun/main
Update from main
2022-12-27 19:59:16 +08:00
Chee Aun b0e118fcab
Merge pull request #34 from cheeaun/main
Update from main
2022-12-27 09:57:15 +08:00
Chee Aun f51201a787
Merge pull request #33 from cheeaun/main
Update from main
2022-12-27 01:18:41 +08:00
Chee Aun 5a035089ab
Merge pull request #31 from cheeaun/main
Update from main
2022-12-27 00:10:32 +08:00
Chee Aun 206f00af40
Merge pull request #27 from cheeaun/main
Update from main
2022-12-24 23:20:13 +08:00
Chee Aun 13de3d9263
Merge pull request #20 from cheeaun/main
Update from main
2022-12-24 23:06:13 +08:00
Chee Aun eb41ddf2de
Merge pull request #18 from cheeaun/main
Update from main
2022-12-22 09:04:05 +08:00
Chee Aun 940e8f5376
Merge pull request #16 from cheeaun/main
Update from main
2022-12-22 08:44:56 +08:00
Chee Aun 77ba42dba9
Merge pull request #13 from cheeaun/main
Update from main
2022-12-21 21:32:38 +08:00
Chee Aun 95e204c439
Merge pull request #12 from cheeaun/main
Update from main
2022-12-19 20:12:56 +08:00
Chee Aun 82770e8035
Merge pull request #11 from cheeaun/main 2022-12-19 18:44:01 +08:00
Chee Aun 818c8e61cd
Merge pull request #10 from cheeaun/main
Update from main
2022-12-18 13:07:45 +08:00
Chee Aun 3b8592e946
Merge pull request #9 from cheeaun/main
Update from main
2022-12-17 18:37:19 +08:00
Chee Aun c0c7d65034
Merge pull request #8 from cheeaun/main
Update from main
2022-12-16 13:59:24 +08:00
Chee Aun 5631126e8d
Merge pull request #7 from cheeaun/main
Update from main
2022-12-16 13:33:54 +08:00
Chee Aun bd2ed53f32
Merge pull request #6 from cheeaun/main
Update from main
2022-12-16 12:28:52 +08:00
Chee Aun 694fa22942
Merge pull request #5 from cheeaun/main
Update from main
2022-12-15 21:48:39 +08:00
Chee Aun 15c3979815
Merge pull request #4 from cheeaun/main
Update from main
2022-12-15 17:45:04 +08:00
Chee Aun ab5f53273f
Merge pull request #3 from cheeaun/main
Update from main
2022-12-15 14:48:02 +08:00
Chee Aun 19c2f9b048
Merge pull request #2 from cheeaun/main
Update from main
2022-12-15 13:17:46 +08:00
Chee Aun a45250ac96
Merge pull request #1 from cheeaun/main
Update from main
2022-12-15 12:02:26 +08:00
32 changed files with 2377 additions and 1704 deletions

4
.gitignore vendored
View file

@ -27,3 +27,7 @@ dist-ssr
.env.dev
phanpy-dist.zip
phanpy-dist.tar.gz
# Nix
.direnv
result

View file

@ -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.
@ -244,6 +244,8 @@ And here I am. Building a Mastodon web client.
## Alternative web clients
- 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/)

61
flake.lock Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1711703276,
"narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=",
"owner": "nixOS",
"repo": "nixpkgs",
"rev": "d8fe5e6c92d0d190646fb9f1056741a229980089",
"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
}

42
flake.nix Normal file
View file

@ -0,0 +1,42 @@
{
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;
in
rec {
packages.default = pkgs.buildNpmPackage {
pname = "dtth-phanpy";
version = "0.1.0";
nativeBuildInputs = with pkgs; [ git ];
ESBUILD_BINARY_PATH = lib.getExe pkgs.esbuild;
src = lib.cleanSource ./.;
npmFlags = [ "--legacy-peer-deps" ];
npmDepsHash = "sha256-pL/bqTNMtekD5GzTCNPOy60OD2MC36zlg1A6MP0Fn2M=";
# 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 ];
};
});
}

3074
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -12,33 +12,34 @@
"dependencies": {
"@formatjs/intl-localematcher": "~0.5.4",
"@formatjs/intl-segmenter": "~11.5.5",
"@formkit/auto-animate": "~0.8.1",
"@formkit/auto-animate": "~0.8.2",
"@github/text-expander-element": "~2.6.1",
"@iconify-icons/mingcute": "~1.2.9",
"@justinribeiro/lite-youtube": "~1.5.0",
"@szhsin/react-menu": "~4.1.0",
"@uidotdev/usehooks": "~2.4.1",
"compare-versions": "~6.1.0",
"dayjs": "~1.11.10",
"dayjs": "~1.11.11",
"dayjs-twitter": "~0.5.0",
"fast-blurhash": "~1.1.2",
"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",
"lz-string": "~1.5.0",
"masto": "~6.7.0",
"masto": "~6.7.7",
"moize": "~6.1.6",
"p-retry": "~6.2.0",
"p-throttle": "~6.1.0",
"preact": "~10.20.1",
"preact": "~10.21.0",
"punycode": "~2.3.1",
"react-hotkeys-hook": "~4.5.0",
"react-intersection-observer": "~9.8.1",
"react-intersection-observer": "~9.10.2",
"react-quick-pinch-zoom": "~5.1.0",
"react-router-dom": "6.6.2",
"string-length": "6.0.0",
"swiped-events": "~1.1.9",
"swiped-events": "~1.2.0",
"toastify-js": "~1.12.0",
"uid": "~2.0.2",
"use-debounce": "~10.0.0",
@ -50,18 +51,18 @@
"@preact/preset-vite": "~2.8.2",
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
"postcss": "~8.4.38",
"postcss-dark-theme-class": "~1.2.1",
"postcss-preset-env": "~9.5.4",
"postcss-dark-theme-class": "~1.3.0",
"postcss-preset-env": "~9.5.11",
"twitter-text": "~3.1.0",
"vite": "~5.2.8",
"vite": "~5.2.11",
"vite-plugin-generate-file": "~0.1.1",
"vite-plugin-html-config": "~1.0.11",
"vite-plugin-pwa": "~0.19.7",
"vite-plugin-pwa": "~0.20.0",
"vite-plugin-remove-console": "~2.2.0",
"workbox-cacheable-response": "~7.0.0",
"workbox-expiration": "~7.0.0",
"workbox-routing": "~7.0.0",
"workbox-strategies": "~7.0.0"
"workbox-cacheable-response": "~7.1.0",
"workbox-expiration": "~7.1.0",
"workbox-routing": "~7.1.0",
"workbox-strategies": "~7.1.0"
},
"postcss": {
"plugins": {

View file

@ -62,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,
}),

View file

@ -1966,6 +1966,10 @@ body > .szh-menu-container {
.szh-menu
.szh-menu__item.danger:not(.szh-menu__item--disabled).szh-menu__item--hover {
background-color: var(--red-text-color);
@media (prefers-color-scheme: dark) {
background-color: var(--red-color);
}
}
.szh-menu
.szh-menu__item:not(.szh-menu__item--disabled):not(
@ -2780,8 +2784,8 @@ ul.link-list li a .icon {
padding: 32px;
} */
li.timeline-item-carousel {
width: 95vw;
max-width: calc(320px * 3.3);
/* width: 95vw;
max-width: calc(320px * 3.3); */
transform: translateX(calc(-50% + var(--main-width) / 2));
}
}

View file

@ -108,4 +108,5 @@ export const ICONS = {
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'),
};

View file

@ -597,41 +597,123 @@
#custom-emojis-sheet {
max-height: 50vh;
max-height: 50dvh;
}
#custom-emojis-sheet main {
mask-image: none;
}
#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);
}
#custom-emojis-sheet .custom-emojis-list section {
display: flex;
flex-wrap: wrap;
}
#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);
header {
.loader-container {
margin: 0;
}
form {
margin: 8px 0 0;
input {
width: 100%;
min-width: 0;
font-size: 0.8em;
}
}
}
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;
}
}
}
}
.compose-field-container {

View file

@ -3,8 +3,16 @@ import './compose.css';
import '@github/text-expander-element';
import { MenuItem } from '@szhsin/react-menu';
import { deepEqual } from 'fast-equals';
import Fuse from 'fuse.js';
import { memo } from 'preact/compat';
import { forwardRef } from 'preact/compat';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import stringLength from 'string-length';
import { uid } from 'uid/single';
@ -21,6 +29,7 @@ import db from '../utils/db';
import emojifyText from '../utils/emojify-text';
import localeMatch from '../utils/locale-match';
import openCompose from '../utils/open-compose';
import pmem from '../utils/pmem';
import shortenNumber from '../utils/shorten-number';
import showToast from '../utils/show-toast';
import states, { saveStatus } from '../utils/states';
@ -181,6 +190,8 @@ function highlightText(text, { maxCharacters = Infinity }) {
const rtf = new Intl.RelativeTimeFormat();
const CUSTOM_EMOJIS_COUNT = 100;
function Compose({
onClose,
replyToStatus,
@ -1423,25 +1434,40 @@ function autoResizeTextarea(textarea) {
}
}
async function _getCustomEmojis(instance, masto) {
const emojis = await masto.v1.customEmojis.list();
const visibleEmojis = emojis.filter((e) => e.visibleInPicker);
const searcher = new Fuse(visibleEmojis, {
keys: ['shortcode'],
findAllMatches: true,
});
return [visibleEmojis, searcher];
}
const getCustomEmojis = pmem(_getCustomEmojis, {
// Limit by time to reduce memory usage
// Cached by instance
matchesArg: (cacheKeyArg, keyArg) => cacheKeyArg.instance === keyArg.instance,
maxAge: 30 * 60 * 1000, // 30 minutes
});
const Textarea = forwardRef((props, ref) => {
const { masto } = api();
const { masto, instance } = api();
const [text, setText] = useState(ref.current?.value || '');
const { maxCharacters, performSearch = () => {}, ...textareaProps } = props;
// const snapStates = useSnapshot(states);
// const charCount = snapStates.composerCharacterCount;
const customEmojis = useRef();
// const customEmojis = useRef();
const searcherRef = useRef();
useEffect(() => {
(async () => {
try {
const emojis = await masto.v1.customEmojis.list();
console.log({ emojis });
customEmojis.current = emojis;
} catch (e) {
// silent fail
getCustomEmojis(instance, masto)
.then((r) => {
const [emojis, searcher] = r;
searcherRef.current = searcher;
})
.catch((e) => {
console.error(e);
}
})();
});
}, []);
const textExpanderRef = useRef();
@ -1467,23 +1493,26 @@ const Textarea = forwardRef((props, ref) => {
// const emojis = customEmojis.current.filter((emoji) =>
// emoji.shortcode.startsWith(text),
// );
const emojis = filterShortcodes(customEmojis.current, text);
// const emojis = filterShortcodes(customEmojis.current, text);
const results = searcherRef.current?.search(text, {
limit: 5,
});
let html = '';
emojis.forEach((emoji) => {
results.forEach(({ item: emoji }) => {
const { shortcode, url } = emoji;
html += `
<li role="option" data-value="${encodeHTML(shortcode)}">
<img src="${encodeHTML(
url,
)}" width="16" height="16" alt="" loading="lazy" />
:${encodeHTML(shortcode)}:
${encodeHTML(shortcode)}
</li>`;
});
// console.log({ emojis, html });
menu.innerHTML = html;
provide(
Promise.resolve({
matched: emojis.length > 0,
matched: results.length > 0,
fragment: menu,
}),
);
@ -2185,38 +2214,19 @@ function CustomEmojisModal({
}) {
const [uiState, setUIState] = useState('default');
const customEmojisList = useRef([]);
const [customEmojis, setCustomEmojis] = useState({});
const [customEmojis, setCustomEmojis] = useState([]);
const recentlyUsedCustomEmojis = useMemo(
() => store.account.get('recentlyUsedCustomEmojis') || [],
);
const searcherRef = useRef();
useEffect(() => {
setUIState('loading');
(async () => {
try {
const emojis = await masto.v1.customEmojis.list();
// Group emojis by category
const emojisCat = {
'--recent--': recentlyUsedCustomEmojis.filter((emoji) =>
emojis.find((e) => e.shortcode === emoji.shortcode),
),
};
const othersCat = [];
emojis.forEach((emoji) => {
if (!emoji.visibleInPicker) return;
customEmojisList.current?.push?.(emoji);
if (!emoji.category) {
othersCat.push(emoji);
return;
}
if (!emojisCat[emoji.category]) {
emojisCat[emoji.category] = [];
}
emojisCat[emoji.category].push(emoji);
});
if (othersCat.length) {
emojisCat['--others--'] = othersCat;
}
setCustomEmojis(emojisCat);
const [emojis, searcher] = await getCustomEmojis(instance, masto);
console.log('emojis', emojis);
searcherRef.current = searcher;
setCustomEmojis(emojis);
setUIState('default');
} catch (e) {
setUIState('error');
@ -2225,6 +2235,83 @@ function CustomEmojisModal({
})();
}, []);
const customEmojisCatList = useMemo(() => {
// Group emojis by category
const emojisCat = {
'--recent--': recentlyUsedCustomEmojis.filter((emoji) =>
customEmojis.find((e) => e.shortcode === emoji.shortcode),
),
};
const othersCat = [];
customEmojis.forEach((emoji) => {
customEmojisList.current?.push?.(emoji);
if (!emoji.category) {
othersCat.push(emoji);
return;
}
if (!emojisCat[emoji.category]) {
emojisCat[emoji.category] = [];
}
emojisCat[emoji.category].push(emoji);
});
if (othersCat.length) {
emojisCat['--others--'] = othersCat;
}
return emojisCat;
}, [customEmojis]);
const scrollableRef = useRef();
const [matches, setMatches] = useState(null);
const onFind = useCallback(
(e) => {
const { value } = e.target;
if (value) {
const results = searcherRef.current?.search(value, {
limit: CUSTOM_EMOJIS_COUNT,
});
setMatches(results.map((r) => r.item));
scrollableRef.current?.scrollTo?.(0, 0);
} else {
setMatches(null);
}
},
[customEmojis],
);
const onSelectEmoji = useCallback(
(emoji) => {
onSelect?.(emoji);
onClose?.();
queueMicrotask(() => {
let recentlyUsedCustomEmojis =
store.account.get('recentlyUsedCustomEmojis') || [];
const recentlyUsedEmojiIndex = recentlyUsedCustomEmojis.findIndex(
(e) => e.shortcode === emoji.shortcode,
);
if (recentlyUsedEmojiIndex !== -1) {
// Move emoji to index 0
recentlyUsedCustomEmojis.splice(recentlyUsedEmojiIndex, 1);
recentlyUsedCustomEmojis.unshift(emoji);
} else {
recentlyUsedCustomEmojis.unshift(emoji);
// Remove unavailable ones
recentlyUsedCustomEmojis = recentlyUsedCustomEmojis.filter((e) =>
customEmojisList.current?.find?.(
(emoji) => emoji.shortcode === e.shortcode,
),
);
// Limit to 10
recentlyUsedCustomEmojis = recentlyUsedCustomEmojis.slice(0, 10);
}
// Store back
store.account.set('recentlyUsedCustomEmojis', recentlyUsedCustomEmojis);
});
},
[onSelect],
);
return (
<div id="custom-emojis-sheet" class="sheet">
{!!onClose && (
@ -2233,107 +2320,167 @@ function CustomEmojisModal({
</button>
)}
<header>
<b>Custom emojis</b>{' '}
{uiState === 'loading' ? (
<Loader />
) : (
<small class="insignificant"> {instance}</small>
)}
</header>
<main>
<div class="custom-emojis-list">
{uiState === 'error' && (
<div class="ui-state">
<p>Error loading custom emojis</p>
</div>
<div>
<b>Custom emojis</b>{' '}
{uiState === 'loading' ? (
<Loader />
) : (
<small class="insignificant"> {instance}</small>
)}
{uiState === 'default' &&
Object.entries(customEmojis).map(
([category, emojis]) =>
!!emojis?.length && (
<>
<div class="section-header">
{{
'--recent--': 'Recently used',
'--others--': 'Others',
}[category] || category}
</div>
<section>
{emojis.map((emoji) => (
<button
key={emoji}
type="button"
class="plain4"
onClick={() => {
onClose();
requestAnimationFrame(() => {
onSelect(`:${emoji.shortcode}:`);
});
let recentlyUsedCustomEmojis =
store.account.get('recentlyUsedCustomEmojis') ||
[];
const recentlyUsedEmojiIndex =
recentlyUsedCustomEmojis.findIndex(
(e) => e.shortcode === emoji.shortcode,
);
if (recentlyUsedEmojiIndex !== -1) {
// Move emoji to index 0
recentlyUsedCustomEmojis.splice(
recentlyUsedEmojiIndex,
1,
);
recentlyUsedCustomEmojis.unshift(emoji);
} else {
recentlyUsedCustomEmojis.unshift(emoji);
// Remove unavailable ones
recentlyUsedCustomEmojis =
recentlyUsedCustomEmojis.filter((e) =>
customEmojisList.current?.find?.(
(emoji) => emoji.shortcode === e.shortcode,
),
);
// Limit to 10
recentlyUsedCustomEmojis =
recentlyUsedCustomEmojis.slice(0, 10);
}
// Store back
store.account.set(
'recentlyUsedCustomEmojis',
recentlyUsedCustomEmojis,
);
}}
title={`:${emoji.shortcode}:`}
>
<picture>
{!!emoji.staticUrl && (
<source
srcset={emoji.staticUrl}
media="(prefers-reduced-motion: reduce)"
/>
)}
<img
class="shortcode-emoji"
src={emoji.url || emoji.staticUrl}
alt={emoji.shortcode}
width="16"
height="16"
loading="lazy"
decoding="async"
/>
</picture>
</button>
))}
</section>
</>
),
)}
</div>
<form
onSubmit={(e) => {
e.preventDefault();
const emoji = matches[0];
if (emoji) {
onSelectEmoji(`:${emoji.shortcode}:`);
}
}}
>
<input
type="search"
placeholder="Search emoji"
onInput={onFind}
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellCheck="false"
dir="auto"
/>
</form>
</header>
<main ref={scrollableRef}>
{matches !== null ? (
<ul class="custom-emojis-matches custom-emojis-list">
{matches.map((emoji) => (
<li key={emoji.shortcode} class="custom-emojis-match">
<CustomEmojiButton
emoji={emoji}
onClick={() => {
onSelectEmoji(`:${emoji.shortcode}:`);
}}
showCode
/>
</li>
))}
</ul>
) : (
<div class="custom-emojis-list">
{uiState === 'error' && (
<div class="ui-state">
<p>Error loading custom emojis</p>
</div>
)}
{uiState === 'default' &&
Object.entries(customEmojisCatList).map(
([category, emojis]) =>
!!emojis?.length && (
<>
<div class="section-header">
{{
'--recent--': 'Recently used',
'--others--': 'Others',
}[category] || category}
</div>
<CustomEmojisList
emojis={emojis}
onSelect={onSelectEmoji}
/>
</>
),
)}
</div>
)}
</main>
</div>
);
}
const CustomEmojisList = memo(({ emojis, onSelect }) => {
const [max, setMax] = useState(CUSTOM_EMOJIS_COUNT);
const showMore = emojis.length > max;
return (
<section>
{emojis.slice(0, max).map((emoji) => (
<CustomEmojiButton
key={emoji.shortcode}
emoji={emoji}
onClick={() => {
onSelect(`:${emoji.shortcode}:`);
}}
/>
))}
{showMore && (
<button
type="button"
class="plain small"
onClick={() => setMax(max + CUSTOM_EMOJIS_COUNT)}
>
{(emojis.length - max).toLocaleString()} more
</button>
)}
</section>
);
});
const CustomEmojiButton = memo(({ emoji, onClick, showCode }) => {
const addEdges = (e) => {
// Add edge-left or edge-right class based on self position relative to scrollable parent
// If near left edge, add edge-left, if near right edge, add edge-right
const buffer = 88;
const parent = e.currentTarget.closest('main');
if (parent) {
const rect = parent.getBoundingClientRect();
const selfRect = e.currentTarget.getBoundingClientRect();
const targetClassList = e.currentTarget.classList;
if (selfRect.left < rect.left + buffer) {
targetClassList.add('edge-left');
targetClassList.remove('edge-right');
} else if (selfRect.right > rect.right - buffer) {
targetClassList.add('edge-right');
targetClassList.remove('edge-left');
} else {
targetClassList.remove('edge-left', 'edge-right');
}
}
};
return (
<button
type="button"
className="plain4"
onClick={onClick}
data-title={showCode ? undefined : emoji.shortcode}
onPointerEnter={addEdges}
onFocus={addEdges}
>
<picture>
{!!emoji.staticUrl && (
<source
srcSet={emoji.staticUrl}
media="(prefers-reduced-motion: reduce)"
/>
)}
<img
className="shortcode-emoji"
src={emoji.url || emoji.staticUrl}
alt={emoji.shortcode}
width="24"
height="24"
loading="lazy"
decoding="async"
/>
</picture>
{showCode && (
<>
{' '}
<code>{emoji.shortcode}</code>
</>
)}
</button>
);
});
const GIFS_PER_PAGE = 20;
function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
const [uiState, setUIState] = useState('default');

View file

@ -17,6 +17,21 @@
);
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 {

View file

@ -11,6 +11,7 @@ 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';
@ -143,9 +144,12 @@ export default function GenericAccounts({
</header>
<main>
{post && (
<div class="post-preview">
<Link
to={`/${instance || currentInstance}/s/${post.id}`}
class="post-preview"
>
<Status status={post} size="s" readOnly />
</div>
</Link>
)}
{accounts.length > 0 ? (
<>

View file

@ -7,10 +7,13 @@ import { useInView } from 'react-intersection-observer';
// The sticky header, usually at the top
const TOP = 48;
export default function LazyShazam({ children }) {
const shazamIDs = {};
export default function LazyShazam({ id, children }) {
const containerRef = useRef();
const hasID = !!shazamIDs[id];
const [visible, setVisible] = useState(false);
const [visibleStart, setVisibleStart] = useState(false);
const [visibleStart, setVisibleStart] = useState(hasID || false);
const { ref } = useInView({
root: null,
@ -20,6 +23,7 @@ export default function LazyShazam({ children }) {
onChange: (inView) => {
if (inView) {
setVisible(true);
if (id) shazamIDs[id] = true;
}
},
triggerOnce: true,
@ -35,6 +39,7 @@ export default function LazyShazam({ children }) {
} else {
setVisibleStart(true);
}
if (id) shazamIDs[id] = true;
}
}, []);

View file

@ -26,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

@ -76,12 +76,13 @@ function NameText({
<b>
<EmojiText text={displayName} emojis={emojis} />
</b>
{!showAcct && username && (
{!showAcct && username ? (
<>
{' '}
<i>@{username}</i>
</>
)}
) : ' '}
<i class="instance">{acct2}</i>
</>
) : short ? (
<i>{username}</i>

View file

@ -1,6 +1,6 @@
import './nav-menu.css';
import { ControlledMenu, MenuDivider, MenuItem } from '@szhsin/react-menu';
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';
@ -18,6 +18,7 @@ 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);
@ -209,6 +210,10 @@ function NavMenu(props) {
<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"

View file

@ -28,6 +28,7 @@ const NOTIFICATION_ICONS = {
'admin.signup': 'account-edit',
'admin.report': 'account-warning',
severed_relationships: 'heart-break',
moderation_warning: 'alert',
emoji_reaction: 'emoji2',
'pleroma:emoji_reaction': 'emoji2',
};
@ -45,6 +46,8 @@ 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) {
@ -91,6 +94,7 @@ const contentText = {
Lost connections with <i>{name}</i>.
</>
),
moderation_warning: <b>Moderation warning</b>,
emoji_reaction: emojiText,
'pleroma:emoji_reaction': emojiText,
};
@ -117,6 +121,17 @@ const SEVERED_RELATIONSHIPS_TEXT = {
),
};
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 = 50;
function Notification({
@ -125,8 +140,16 @@ function Notification({
isStatic,
disableContextMenu,
}) {
const { id, status, account, report, event, _accounts, _statuses } =
notification;
const {
id,
status,
account,
report,
event,
moderation_warning,
_accounts,
_statuses,
} = notification;
let { type } = notification;
// status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update
@ -314,6 +337,20 @@ function Notification({
.
</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 && (

View file

@ -569,8 +569,15 @@
font-weight: bold;
vertical-align: middle;
display: inline-block;
&.horizontal {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 100%;
}
}
.status-filtered-badge.badge-meta {
.status-filtered-badge:not(.horizontal).badge-meta {
display: inline-flex;
flex-direction: column;
position: relative;
@ -584,10 +591,10 @@
border-color: var(--text-color);
background: var(--bg-color);
}
.status-filtered-badge.badge-meta > span:first-child {
.status-filtered-badge:not(.horizontal).badge-meta > span:first-child {
white-space: nowrap;
}
.status-filtered-badge.badge-meta > span + span {
.status-filtered-badge:not(.horizontal).badge-meta > span + span {
display: block;
font-size: 9px;
font-weight: normal;
@ -601,6 +608,10 @@
left: 0;
text-align: center;
}
.status-filtered-badge.horizontal.badge-meta > span + span {
font-weight: normal;
text-transform: none;
}
.status.large > .container > .content-container {
margin-left: calc(-50px - 16px);
@ -825,6 +836,12 @@
.timeline-deck .status .content.truncated ~ .card {
display: none;
}
.status .content .inner-content {
> img[height] {
height: auto;
aspect-ratio: var(--original-aspect-ratio);
}
}
.status .content .inner-content a:not(.mention, .has-url-text) {
color: var(--link-text-color);
}
@ -2380,8 +2397,8 @@ a.card:is(:hover, :focus):visited {
max-width: 100%;
height: 1.2em;
vertical-align: text-bottom;
object-fit: cover;
object-position: left;
object-fit: contain;
/* object-position: left; */
}
/* EDIT HISTORY */

View file

@ -3337,7 +3337,7 @@ const QuoteStatuses = memo(({ id, instance, level = 0 }) => {
return uniqueQuotes.map((q) => {
return (
<LazyShazam>
<LazyShazam id={q.instance + q.id}>
<Link
key={q.instance + q.id}
to={`${q.instance ? `/${q.instance}` : ''}/s/${q.id}`}

View file

@ -209,8 +209,8 @@ function Timeline({
const oRef = useHotkeys(['enter', 'o'], () => {
// open active status
const activeItem = document.activeElement.closest(itemsSelector);
if (activeItem) {
const activeItem = document.activeElement;
if (activeItem?.matches(itemsSelector)) {
activeItem.click();
}
});
@ -646,7 +646,11 @@ const TimelineItem = memo(
>
<Link class="status-link timeline-item" to={url}>
{showCompact ? (
<TimelineStatusCompact status={item} instance={instance} />
<TimelineStatusCompact
status={item}
instance={instance}
filterContext={filterContext}
/>
) : useItemID ? (
<Status
statusID={statusID}
@ -820,11 +824,12 @@ function StatusCarousel({ title, class: className, children }) {
);
}
function TimelineStatusCompact({ status, instance }) {
function TimelineStatusCompact({ status, instance, filterContext }) {
const snapStates = useSnapshot(states);
const { id, visibility, language } = status;
const statusPeekText = statusPeek(status);
const sKey = statusKey(id, instance);
const filterInfo = isFiltered(status.filtered, filterContext);
return (
<article
class={`status compact-thread ${
@ -850,13 +855,24 @@ function TimelineStatusCompact({ status, instance }) {
lang={language}
dir="auto"
>
{statusPeekText}
{status.sensitive && status.spoilerText && (
{!!filterInfo ? (
<b
class="status-filtered-badge badge-meta horizontal"
title={filterInfo?.titlesStr || ''}
>
<span>Filtered</span>: <span>{filterInfo?.titlesStr || ''}</span>
</b>
) : (
<>
{' '}
<span class="spoiler-badge">
<Icon icon="eye-close" size="s" />
</span>
{statusPeekText}
{status.sensitive && status.spoilerText && (
<>
{' '}
<span class="spoiler-badge">
<Icon icon="eye-close" size="s" />
</span>
</>
)}
</>
)}
</div>

View file

@ -7,6 +7,7 @@ import { lazy } from 'preact/compat';
import { useEffect, useState } from 'preact/hooks';
import IntlSegmenterSuspense from './components/intl-segmenter-suspense';
import { initStates } from './utils/states';
// import Compose from './components/compose';
import useTitle from './utils/useTitle';
@ -31,6 +32,10 @@ function App() {
: 'Compose',
);
useEffect(() => {
initStates();
}, []);
useEffect(() => {
if (uiState === 'closed') {
try {

View file

@ -8,7 +8,7 @@
--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',

View file

@ -286,7 +286,13 @@ function FiltersAddEdit({ filter, onClose }) {
// Preserve existing expiry if not specified
// Seconds from now to expiresAtDate
// Other clients don't do this
expiresIn = Math.floor((expiresAtDate - new Date()) / 1000);
if (hasExpiry) {
expiresIn = Math.floor(
(expiresAtDate - new Date()) / 1000,
);
} else {
expiresIn = null;
}
} else if (expiresIn === '0' || expiresIn === 0) {
// 0 = Never
expiresIn = null;

View file

@ -1,5 +1,6 @@
import './login.css';
import Fuse from 'fuse.js';
import { useEffect, useRef, useState } from 'preact/hooks';
import { useSearchParams } from 'react-router-dom';
@ -11,6 +12,7 @@ import instancesListURL from '../data/instances.json?url';
import { getAuthorizationURL, registerApplication } from '../utils/auth';
import store from '../utils/store';
import useTitle from '../utils/useTitle';
import { gtsDtth } from '../utils/dtth';
const { PHANPY_DEFAULT_INSTANCE: DEFAULT_INSTANCE } = import.meta.env;
@ -23,16 +25,18 @@ function Login() {
const instance = searchParams.get('instance');
const submit = searchParams.get('submit');
const [instanceText, setInstanceText] = useState(
instance || cachedInstanceURL?.toLowerCase() || '',
instance || cachedInstanceURL?.toLowerCase() || gtsDtth,
);
const [instancesList, setInstancesList] = useState([]);
const searcher = useRef();
useEffect(() => {
(async () => {
try {
const res = await fetch(instancesListURL);
const data = await res.json();
setInstancesList(data);
searcher.current = new Fuse(data);
} catch (e) {
// Silently fail
console.error(e);
@ -90,21 +94,11 @@ function Login() {
!/[\s\/\\@]/.test(cleanInstanceText);
const instancesSuggestions = cleanInstanceText
? instancesList
.filter((instance) => instance.includes(instanceText))
.sort((a, b) => {
// Move text that starts with instanceText to the start
const aStartsWith = a
.toLowerCase()
.startsWith(instanceText.toLowerCase());
const bStartsWith = b
.toLowerCase()
.startsWith(instanceText.toLowerCase());
if (aStartsWith && !bStartsWith) return -1;
if (!aStartsWith && bStartsWith) return 1;
return 0;
? searcher.current
?.search(cleanInstanceText, {
limit: 10,
})
.slice(0, 10)
?.map((match) => match.item)
: [];
const selectedInstanceText = instanceTextLooksLikeDomain

View file

@ -102,6 +102,17 @@ function Notifications({ columnMode }) {
// },
// });
// TEST: Slot in a fake notification to test 'moderation_warning'
// notifications.unshift({
// id: '123123',
// type: 'moderation_warning',
// createdAt: new Date().toISOString(),
// moderation_warning: {
// id: '1231234',
// action: 'mark_statuses_as_sensitive',
// },
// });
// console.log({ notifications });
const groupedNotifications = groupNotifications(notifications);

View file

@ -603,14 +603,18 @@ function Settings({ onClose }) {
}}
>
@phanpy
</a>
</a> (
<a href="https://git.dtth.ch/nki/phanpy" target="_blank">
DTTH Fork
</a>
)
<br />
<a
href="https://github.com/cheeaun/phanpy"
target="_blank"
rel="noopener noreferrer"
>
Built
Original
</a>{' '}
by{' '}
<a
@ -665,10 +669,10 @@ function Settings({ onClose }) {
type="text"
class="version-string"
readOnly
size="18" // Manually calculated here
size={10 /* build time */ + (1+8) /* commit hash */ + '-dtth'.length}
value={`${__BUILD_TIME__.slice(0, 10).replace(/-/g, '.')}${
__COMMIT_HASH__ ? `.${__COMMIT_HASH__}` : ''
}`}
__COMMIT_HASH__ ? `.${__COMMIT_HASH__.slice(0, 8)}` : ''
}-dtth`}
onClick={(e) => {
e.target.select();
// Copy to clipboard
@ -685,7 +689,7 @@ function Settings({ onClose }) {
<span class="ib insignificant">
(
<a
href={`https://github.com/cheeaun/phanpy/commit/${__COMMIT_HASH__}`}
href={`https://git.dtth.ch/nki/phanpy/commit/${__COMMIT_HASH__}`}
target="_blank"
rel="noopener noreferrer"
>

View file

@ -104,6 +104,7 @@ function Welcome() {
</a>
.
</p>
<p class="desc">A minimalistic opinionated DTTHDon web client.</p>
</div>
<div id="why-container">
<div class="sections">
@ -162,6 +163,38 @@ function Welcome() {
</section>
</div>
</div>
<footer>
<hr />
<p>
<a href="https://git.dtth.ch/nki/phanpy" target="_blank">
DTTHDon Fork
</a>
</p>
<p>
<a href="https://github.com/cheeaun/phanpy" target="_blank">
Original built
</a>{' '}
by{' '}
<a
href="https://mastodon.social/@cheeaun"
target="_blank"
onClick={(e) => {
e.preventDefault();
states.showAccount = 'cheeaun@mastodon.social';
}}
>
@cheeaun
</a>
.{' '}
<a
href="https://github.com/cheeaun/phanpy/blob/main/PRIVACY.MD"
target="_blank"
>
Privacy Policy
</a>
.
</p>
</footer>
</main>
);
}

13
src/utils/dtth.js Normal file
View file

@ -0,0 +1,13 @@
export function accountsIsDtth(account) {
return (
account.info &&
typeof account.info.url === 'string' &&
account.info.url.startsWith(gtsDtth)
);
}
/** URL to DTTHDon */
export const gtsDtth = 'https://gts.dtth.ch';
/** URL to DTTHDon settings */
export const gtsDtthSettings = 'https://gts.dtth.ch/settings';

View file

@ -242,6 +242,17 @@ function _enhanceContent(content, opts = {}) {
}
}
// ADD ASPECT RATIO TO ALL IMAGES
if (enhancedContent.includes('<img')) {
dom.querySelectorAll('img').forEach((img) => {
const width = img.getAttribute('width') || img.naturalWidth;
const height = img.getAttribute('height') || img.naturalHeight;
if (width && height) {
img.style.setProperty('--original-aspect-ratio', `${width}/${height}`);
}
});
}
if (postEnhanceDOM) {
queueMicrotask(() => postEnhanceDOM(dom));
// postEnhanceDOM(dom); // mutate dom

View file

@ -1,5 +1,6 @@
const { locale } = Intl.NumberFormat().resolvedOptions();
const shortenNumber = Intl.NumberFormat(locale, {
notation: 'compact',
roundingMode: 'floor',
}).format;
export default shortenNumber;

View file

@ -24,8 +24,12 @@ try {
} catch (error) {
// If error, means git is not installed or not a git repo (could be downloaded instead of git cloned)
// Fallback to random hash which should be different on every build run 🤞
commitHash = uid();
fakeCommitHash = true;
if (process.env.PHANPY_COMMIT_HASH) {
commitHash = process.env.PHANPY_COMMIT_HASH;
} else {
commitHash = uid();
fakeCommitHash = true;
}
}
const rollbarCode = fs.readFileSync(