Porównaj commity

...

97 Commity

Autor SHA1 Wiadomość Data
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
59 zmienionych plików z 4806 dodań i 2533 usunięć

Wyświetl plik

@ -179,6 +179,9 @@ Available variables:
- 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) - 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 fallback instances hard-coded in `/.env`
- [↗️ List of lingva-translate instances](https://github.com/thedaviddelta/lingva-translate?tab=readme-ov-file#instances) - [↗️ 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): - `PHANPY_GIPHY_API_KEY` (optional, no defaults):
- API key for [GIPHY](https://developers.giphy.com/). See [API docs](https://developers.giphy.com/docs/api/). - 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. - If provided, a setting will appear for users to enable the GIF picker in the composer. Disabled by default.
@ -205,6 +208,7 @@ These are self-hosted by other wonderful folks.
- [phanpy.hear-me.social](https://phanpy.hear-me.social) by [@admin@hear-me.social](https://hear-me.social/@admin) - [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.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) - [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)
> Note: Add yours by creating a pull request. > Note: Add yours by creating a pull request.
@ -240,6 +244,8 @@ And here I am. Building a Mastodon web client.
## Alternative web clients ## Alternative web clients
- Phanpy forks ↓
- [Agora](https://agorasocial.app/)
- [Pinafore](https://pinafore.social/) ([retired](https://nolanlawson.com/2023/01/09/retiring-pinafore/)) - forks ↓ - [Pinafore](https://pinafore.social/) ([retired](https://nolanlawson.com/2023/01/09/retiring-pinafore/)) - forks ↓
- [Semaphore](https://semaphore.social/) - [Semaphore](https://semaphore.social/)
- [Enafore](https://enafore.social/) - [Enafore](https://enafore.social/)
@ -256,6 +262,7 @@ And here I am. Building a Mastodon web client.
- [Tusked](https://tusked.app/) - [Tusked](https://tusked.app/)
- [Mastodon Glitch Edition (standalone frontend)](https://iceshrimp.dev/iceshrimp/masto-fe-standalone) - [Mastodon Glitch Edition (standalone frontend)](https://iceshrimp.dev/iceshrimp/masto-fe-standalone)
- [Mangane](https://github.com/BDX-town/Mangane) - [Mangane](https://github.com/BDX-town/Mangane)
- [TheDesk](https://github.com/cutls/TheDesk)
- [More...](https://github.com/hueyy/awesome-mastodon/#clients) - [More...](https://github.com/hueyy/awesome-mastodon/#clients)
## 💁‍♂️ Notice to all other social media client developers ## 💁‍♂️ Notice to all other social media client developers

3203
package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

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

Wyświetl plik

@ -62,7 +62,7 @@ const iconsRoute = new Route(
cacheName: 'icons', cacheName: 'icons',
plugins: [ plugins: [
new ExpirationPlugin({ new ExpirationPlugin({
maxEntries: 50, maxEntries: 300,
maxAgeSeconds: 3 * 24 * 60 * 60, // 3 days maxAgeSeconds: 3 * 24 * 60 * 60, // 3 days
purgeOnQuotaError: true, purgeOnQuotaError: true,
}), }),

Wyświetl plik

@ -301,6 +301,41 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
} }
} }
} }
.deck-container-media-first {
.timeline {
> li:not(.timeline-item-carousel, .timeline-item-container) {
&:has(.status-media-first) {
@media (min-width: 40em) {
width: fit-content;
max-width: min(480px, 100%);
}
background-color: transparent !important;
border: 0 !important;
box-shadow: none !important;
margin-inline: auto !important;
&:not(:first-child) {
margin-block: 32px;
}
&:has(.skeleton) {
width: 100%;
}
}
&:has(.media[data-orientation='landscape']) {
max-width: 100%;
}
}
.status-link:has(.status-media-first):hover {
background-color: transparent;
}
}
}
.timeline.grow { .timeline.grow {
/* min-height: 100vh; /* min-height: 100vh;
min-height: 100dvh; */ min-height: 100dvh; */
@ -1544,8 +1579,8 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
0 10px 36px -4px var(--button-bg-blur-color); 0 10px 36px -4px var(--button-bg-blur-color);
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
} }
.deck-container:has(header[hidden]) ~ #compose-button, .deck-container:has(header[hidden]) ~ #compose-button:not(.loading),
#compose-button[hidden] { #compose-button[hidden]:not(.loading) {
transform: translateY(200%); transform: translateY(200%);
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
@ -1574,6 +1609,48 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
bottom: calc(16px + env(safe-area-inset-bottom) + 52px); bottom: calc(16px + env(safe-area-inset-bottom) + 52px);
} }
} }
#compose-button {
&.min {
outline: 2px solid var(--button-text-color);
z-index: 1001; /* Higher than modal */
&:after {
content: '';
display: block;
position: absolute;
top: 0;
right: 0;
width: 14px;
height: 14px;
border-radius: 50%;
background-color: var(--button-bg-color);
border: 2px solid var(--button-text-color);
box-shadow: 0 2px 8px var(--drop-shadow-color);
opacity: 0;
transition: opacity 0.2s ease-out 0.5s;
opacity: 1;
}
}
&.loading {
outline-color: var(--button-bg-blur-color);
&:before {
position: absolute;
inset: 0;
content: '';
border-radius: 50%;
animation: spin 5s linear infinite;
border: 2px dashed var(--button-text-color);
}
}
&.error {
&:after {
background-color: var(--red-color);
}
}
}
/* SHEET */ /* SHEET */
@ -1882,7 +1959,8 @@ body > .szh-menu-container {
/* two columns only */ /* two columns only */
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
.szh-menu .menu-horizontal:has(> .szh-menu__item:only-child) { .szh-menu .menu-horizontal:has(> .szh-menu__item:only-child),
.szh-menu .menu-horizontal:has(> .szh-menu__submenu:only-child) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.szh-menu .menu-horizontal > .szh-menu__item:not(:only-child):first-child, .szh-menu .menu-horizontal > .szh-menu__item:not(:only-child):first-child,
@ -1930,6 +2008,10 @@ body > .szh-menu-container {
.szh-menu .szh-menu
.szh-menu__item.danger:not(.szh-menu__item--disabled).szh-menu__item--hover { .szh-menu__item.danger:not(.szh-menu__item--disabled).szh-menu__item--hover {
background-color: var(--red-text-color); background-color: var(--red-text-color);
@media (prefers-color-scheme: dark) {
background-color: var(--red-color);
}
} }
.szh-menu .szh-menu
.szh-menu__item:not(.szh-menu__item--disabled):not( .szh-menu__item:not(.szh-menu__item--disabled):not(
@ -2143,6 +2225,8 @@ body > .szh-menu-container {
box-shadow: 0 3px 8px -1px var(--drop-shadow-color), box-shadow: 0 3px 8px -1px var(--drop-shadow-color),
0 10px 36px -4px var(--button-bg-blur-color); 0 10px 36px -4px var(--button-bg-blur-color);
text-align: center; text-align: center;
width: fit-content;
max-width: calc(100vw - 32px);
} }
.toastify-bottom { .toastify-bottom {
margin-bottom: env(safe-area-inset-bottom); margin-bottom: env(safe-area-inset-bottom);

Wyświetl plik

@ -53,7 +53,7 @@ import { getAccessToken } from './utils/auth';
import focusDeck from './utils/focus-deck'; import focusDeck from './utils/focus-deck';
import states, { initStates, statusKey } from './utils/states'; import states, { initStates, statusKey } from './utils/states';
import store from './utils/store'; import store from './utils/store';
import { getCurrentAccount } from './utils/store-utils'; import { getCurrentAccount, setCurrentAccountID } from './utils/store-utils';
import './utils/toast-alert'; import './utils/toast-alert';
window.__STATES__ = states; window.__STATES__ = states;
@ -338,7 +338,7 @@ function App() {
window.__IGNORE_GET_ACCOUNT_ERROR__ = true; window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
const account = getCurrentAccount(); const account = getCurrentAccount();
if (account) { if (account) {
store.session.set('currentAccount', account.info.id); setCurrentAccountID(account.info.id);
const { client } = api({ account }); const { client } = api({ account });
const { instance } = client; const { instance } = client;
// console.log('masto', masto); // console.log('masto', masto);

Wyświetl plik

@ -108,4 +108,5 @@ export const ICONS = {
settings: () => import('@iconify-icons/mingcute/settings-6-line'), settings: () => import('@iconify-icons/mingcute/settings-6-line'),
'heart-break': () => import('@iconify-icons/mingcute/heart-crack-line'), 'heart-break': () => import('@iconify-icons/mingcute/heart-crack-line'),
'user-x': () => import('@iconify-icons/mingcute/user-x-line'), 'user-x': () => import('@iconify-icons/mingcute/user-x-line'),
minimize: () => import('@iconify-icons/mingcute/arrows-down-line'),
}; };

Wyświetl plik

@ -133,21 +133,18 @@ function AccountBlock({
)} )}
</span> </span>
{showActivity && ( {showActivity && (
<> <div class="account-block-stats">
<br /> Posts: {shortenNumber(statusesCount)}
<small class="last-status-at insignificant"> {!!lastStatusAt && (
Posts: {statusesCount} <>
{!!lastStatusAt && ( {' '}
<> &middot; Last posted:{' '}
{' '} {niceDateTime(lastStatusAt, {
&middot; Last posted:{' '} hideTime: true,
{niceDateTime(lastStatusAt, { })}
hideTime: true, </>
})} )}
</> </div>
)}
</small>
</>
)} )}
{showStats && ( {showStats && (
<div class="account-block-stats"> <div class="account-block-stats">

Wyświetl plik

@ -19,10 +19,12 @@ import { getLists } from '../utils/lists';
import niceDateTime from '../utils/nice-date-time'; import niceDateTime from '../utils/nice-date-time';
import pmem from '../utils/pmem'; import pmem from '../utils/pmem';
import shortenNumber from '../utils/shorten-number'; import shortenNumber from '../utils/shorten-number';
import showCompose from '../utils/show-compose';
import showToast from '../utils/show-toast'; import showToast from '../utils/show-toast';
import states, { hideAllModals } from '../utils/states'; import states, { hideAllModals } from '../utils/states';
import store from '../utils/store'; import store from '../utils/store';
import { updateAccount } from '../utils/store-utils'; import { getCurrentAccountID, updateAccount } from '../utils/store-utils';
import supports from '../utils/supports';
import AccountBlock from './account-block'; import AccountBlock from './account-block';
import Avatar from './avatar'; import Avatar from './avatar';
@ -185,6 +187,7 @@ function AccountInfo({
memorial, memorial,
moved, moved,
roles, roles,
hideCollections,
} = info || {}; } = info || {};
let headerIsAvatar = false; let headerIsAvatar = false;
let { header, headerStatic } = info || {}; let { header, headerStatic } = info || {};
@ -198,10 +201,7 @@ function AccountInfo({
} }
} }
const isSelf = useMemo( const isSelf = useMemo(() => id === getCurrentAccountID(), [id]);
() => id === store.session.get('currentAccount'),
[id],
);
useEffect(() => { useEffect(() => {
const infoHasEssentials = !!( const infoHasEssentials = !!(
@ -254,12 +254,13 @@ function AccountInfo({
// On first load, fetch familiar followers, merge to top of results' `value` // On first load, fetch familiar followers, merge to top of results' `value`
// Remove dups on every fetch // Remove dups on every fetch
if (firstLoad) { if (firstLoad) {
const familiarFollowers = await masto.v1.accounts.familiarFollowers.fetch( let familiarFollowers = [];
{ try {
familiarFollowers = await masto.v1.accounts.familiarFollowers.fetch({
id: [id], id: [id],
}, });
); } catch (e) {}
familiarFollowersCache.current = familiarFollowers[0].accounts; familiarFollowersCache.current = familiarFollowers?.[0]?.accounts || [];
newValue = [ newValue = [
...familiarFollowersCache.current, ...familiarFollowersCache.current,
...value.filter( ...value.filter(
@ -678,6 +679,9 @@ function AccountInfo({
excludeRelationshipAttrs: isSelf excludeRelationshipAttrs: isSelf
? ['followedBy'] ? ['followedBy']
: [], : [],
blankCopy: hideCollections
? 'This user has chosen to not make this information available.'
: undefined,
}; };
}, 0); }, 0);
}} }}
@ -713,6 +717,9 @@ function AccountInfo({
fetchAccounts: fetchFollowing, fetchAccounts: fetchFollowing,
instance, instance,
excludeRelationshipAttrs: isSelf ? ['following'] : [], excludeRelationshipAttrs: isSelf ? ['following'] : [],
blankCopy: hideCollections
? 'This user has chosen to not make this information available.'
: undefined,
}; };
}, 0); }, 0);
}} }}
@ -919,7 +926,7 @@ function RelatedActions({
useEffect(() => { useEffect(() => {
if (info) { if (info) {
const currentAccount = store.session.get('currentAccount'); const currentAccount = getCurrentAccountID();
let currentID; let currentID;
(async () => { (async () => {
if (sameInstance && authenticated) { if (sameInstance && authenticated) {
@ -1075,11 +1082,11 @@ function RelatedActions({
<> <>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
states.showCompose = { showCompose({
draftStatus: { draftStatus: {
status: `@${currentInfo?.acct || acct} `, status: `@${currentInfo?.acct || acct} `,
}, },
}; });
}} }}
> >
<Icon icon="at" /> <Icon icon="at" />
@ -1093,16 +1100,18 @@ function RelatedActions({
<Icon icon="translate" /> <Icon icon="translate" />
<span>Translate bio</span> <span>Translate bio</span>
</MenuItem> </MenuItem>
<MenuItem {supports('@mastodon/profile-private-note') && (
onClick={() => { <MenuItem
setShowPrivateNoteModal(true); onClick={() => {
}} setShowPrivateNoteModal(true);
> }}
<Icon icon="pencil" /> >
<span> <Icon icon="pencil" />
{privateNote ? 'Edit private note' : 'Add private note'} <span>
</span> {privateNote ? 'Edit private note' : 'Add private note'}
</MenuItem> </span>
</MenuItem>
)}
{following && !!relationship && ( {following && !!relationship && (
<> <>
<MenuItem <MenuItem
@ -1451,19 +1460,22 @@ function RelatedActions({
</MenuItem> </MenuItem>
</> </>
)} )}
{currentAuthenticated && isSelf && standalone && ( {currentAuthenticated &&
<> isSelf &&
<MenuDivider /> standalone &&
<MenuItem supports('@mastodon/profile-edit') && (
onClick={() => { <>
setShowEditProfile(true); <MenuDivider />
}} <MenuItem
> onClick={() => {
<Icon icon="pencil" /> setShowEditProfile(true);
<span>Edit profile</span> }}
</MenuItem> >
</> <Icon icon="pencil" />
)} <span>Edit profile</span>
</MenuItem>
</>
)}
{import.meta.env.DEV && currentAuthenticated && isSelf && ( {import.meta.env.DEV && currentAuthenticated && isSelf && (
<> <>
<MenuDivider /> <MenuDivider />

Wyświetl plik

@ -1,4 +1,5 @@
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useSnapshot } from 'valtio';
import openCompose from '../utils/open-compose'; import openCompose from '../utils/open-compose';
import openOSK from '../utils/open-osk'; import openOSK from '../utils/open-osk';
@ -7,7 +8,15 @@ import states from '../utils/states';
import Icon from './icon'; import Icon from './icon';
export default function ComposeButton() { export default function ComposeButton() {
const snapStates = useSnapshot(states);
function handleButton(e) { function handleButton(e) {
if (snapStates.composerState.minimized) {
states.composerState.minimized = false;
openOSK();
return;
}
if (e.shiftKey) { if (e.shiftKey) {
const newWin = openCompose(); const newWin = openCompose();
@ -28,7 +37,14 @@ export default function ComposeButton() {
}); });
return ( return (
<button type="button" id="compose-button" onClick={handleButton}> <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" /> <Icon icon="quill" size="xl" alt="Compose" />
</button> </button>
); );

Wyświetl plik

@ -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 />;
}

Wyświetl plik

@ -298,19 +298,25 @@
height: 2.2em; height: 2.2em;
} }
#compose-container .text-expander-menu li:is(:hover, :focus, [aria-selected]) { #compose-container .text-expander-menu li:is(:hover, :focus, [aria-selected]) {
color: var(--bg-color); background-color: var(--link-bg-color);
background-color: var(--link-color);
}
#compose-container
.text-expander-menu:hover
li[aria-selected]:not(:hover, :focus) {
color: var(--text-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);
}
#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 { #compose-container .form-visibility-direct {
--yellow-stripes: repeating-linear-gradient( --yellow-stripes: repeating-linear-gradient(
-45deg, 135deg,
var(--reply-to-faded-color), var(--reply-to-faded-color),
var(--reply-to-faded-color) 10px, var(--reply-to-faded-color) 10px,
var(--reply-to-faded-color) 10px, var(--reply-to-faded-color) 10px,
@ -334,6 +340,21 @@
display: flex; display: flex;
gap: 8px; gap: 8px;
align-items: stretch; 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 { #compose-container .media-preview {
flex-shrink: 0; flex-shrink: 0;
@ -594,44 +615,194 @@
} */ } */
} }
#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(
to right,
transparent 75%,
var(--link-bg-color)
);
}
&.selected {
background-image: linear-gradient(
to right,
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;
left: 58px;
right: 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 { #custom-emojis-sheet {
max-height: 50vh; max-height: 50vh;
max-height: 50dvh; max-height: 50dvh;
}
#custom-emojis-sheet main { header {
mask-image: none; .loader-container {
} margin: 0;
#custom-emojis-sheet .custom-emojis-list .section-header { }
font-size: 80%;
text-transform: uppercase; form {
color: var(--text-insignificant-color); margin: 8px 0 0;
padding: 8px 0 4px;
position: sticky; input {
top: 0; width: 100%;
background-color: var(--bg-blur-color); min-width: 0;
backdrop-filter: blur(1px); }
} }
#custom-emojis-sheet .custom-emojis-list section { }
display: flex;
flex-wrap: wrap; main {
} mask-image: none;
#custom-emojis-sheet .custom-emojis-list button { min-height: 40vh;
border-radius: 8px; padding-bottom: 88px;
background-image: radial-gradient( }
closest-side,
var(--img-bg-color), .custom-emojis-matches {
transparent margin: 0;
); padding: 0;
} list-style: none;
#custom-emojis-sheet .custom-emojis-list button:is(:hover, :focus) { display: flex;
filter: none; flex-wrap: wrap;
background-color: var(--bg-faded-color); }
}
#custom-emojis-sheet .custom-emojis-list button img { .custom-emojis-list {
transition: transform 0.1s ease-out; .section-header {
} font-size: 80%;
#custom-emojis-sheet .custom-emojis-list button:is(:hover, :focus) img { text-transform: uppercase;
transform: scale(1.5); 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 { .compose-field-container {

Plik diff jest za duży Load Diff

Wyświetl plik

@ -17,6 +17,21 @@
); );
filter: saturate(0.5); 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 { .accounts-list {

Wyświetl plik

@ -11,6 +11,7 @@ import useLocationChange from '../utils/useLocationChange';
import AccountBlock from './account-block'; import AccountBlock from './account-block';
import Icon from './icon'; import Icon from './icon';
import Link from './link';
import Loader from './loader'; import Loader from './loader';
import Status from './status'; import Status from './status';
@ -19,6 +20,7 @@ export default function GenericAccounts({
excludeRelationshipAttrs = [], excludeRelationshipAttrs = [],
postID, postID,
onClose = () => {}, onClose = () => {},
blankCopy = 'Nothing to show',
}) { }) {
const { masto, instance: currentInstance } = api(); const { masto, instance: currentInstance } = api();
const isCurrentInstance = instance ? instance === currentInstance : true; const isCurrentInstance = instance ? instance === currentInstance : true;
@ -143,9 +145,12 @@ export default function GenericAccounts({
</header> </header>
<main> <main>
{post && ( {post && (
<div class="post-preview"> <Link
to={`/${instance || currentInstance}/s/${post.id}`}
class="post-preview"
>
<Status status={post} size="s" readOnly /> <Status status={post} size="s" readOnly />
</div> </Link>
)} )}
{accounts.length > 0 ? ( {accounts.length > 0 ? (
<> <>
@ -217,7 +222,7 @@ export default function GenericAccounts({
) : uiState === 'error' ? ( ) : uiState === 'error' ? (
<p class="ui-state">Error loading accounts</p> <p class="ui-state">Error loading accounts</p>
) : ( ) : (
<p class="ui-state insignificant">Nothing to show</p> <p class="ui-state insignificant">{blankCopy}</p>
)} )}
</main> </main>
</div> </div>

Wyświetl plik

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

Wyświetl plik

@ -8,6 +8,7 @@ import FilterContext from '../utils/filter-context';
import { isFiltered } from '../utils/filters'; import { isFiltered } from '../utils/filters';
import states, { statusKey } from '../utils/states'; import states, { statusKey } from '../utils/states';
import store from '../utils/store'; import store from '../utils/store';
import { getCurrentAccountID } from '../utils/store-utils';
import Media from './media'; import Media from './media';
@ -88,7 +89,7 @@ function MediaPost({
}; };
const currentAccount = useMemo(() => { const currentAccount = useMemo(() => {
return store.session.get('currentAccount'); return getCurrentAccountID();
}, []); }, []);
const isSelf = useMemo(() => { const isSelf = useMemo(() => {
return currentAccount && currentAccount === accountId; return currentAccount && currentAccount === accountId;

Wyświetl plik

@ -74,7 +74,7 @@ function Media({
altIndex, altIndex,
onClick = () => {}, onClick = () => {},
}) { }) {
const { let {
blurhash, blurhash,
description, description,
meta, meta,
@ -84,15 +84,27 @@ function Media({
url, url,
type, type,
} = media; } = media;
if (/no\-preview\./i.test(previewUrl)) {
previewUrl = null;
}
const { original = {}, small, focus } = meta || {}; const { original = {}, small, focus } = meta || {};
const width = showOriginal ? original?.width : small?.width; const width = showOriginal
const height = showOriginal ? original?.height : small?.height; ? original?.width
: small?.width || original?.width;
const height = showOriginal
? original?.height
: small?.height || original?.height;
const mediaURL = showOriginal ? url : previewUrl || url; const mediaURL = showOriginal ? url : previewUrl || url;
const remoteMediaURL = showOriginal const remoteMediaURL = showOriginal
? remoteUrl ? remoteUrl
: previewRemoteUrl || 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 rgbAverageColor = blurhash ? getBlurHashAverageColor(blurhash) : null;
@ -133,7 +145,8 @@ function Media({
enabled: pinchZoomEnabled, enabled: pinchZoomEnabled,
draggableUnZoomed: false, draggableUnZoomed: false,
inertiaFriction: 0.9, inertiaFriction: 0.9,
doubleTapZoomOutOnMaxScale: true, tapZoomFactor: 2,
doubleTapToggleZoom: true,
containerProps: { containerProps: {
className: 'media-zoom', className: 'media-zoom',
style: { style: {
@ -153,7 +166,7 @@ function Media({
[to], [to],
); );
const remoteMediaURLObj = remoteMediaURL ? new URL(remoteMediaURL) : null; const remoteMediaURLObj = remoteMediaURL ? getURLObj(remoteMediaURL) : null;
const isVideoMaybe = const isVideoMaybe =
type === 'unknown' && type === 'unknown' &&
remoteMediaURLObj && remoteMediaURLObj &&
@ -290,7 +303,11 @@ function Media({
}} }}
onError={(e) => { onError={(e) => {
const { src } = e.target; const { src } = e.target;
if (src === mediaURL && mediaURL !== remoteMediaURL) { if (
src === mediaURL &&
remoteMediaURL &&
mediaURL !== remoteMediaURL
) {
e.target.src = remoteMediaURL; e.target.src = remoteMediaURL;
} }
}} }}
@ -321,6 +338,20 @@ function Media({
onLoad={(e) => { onLoad={(e) => {
// e.target.closest('.media-image').style.backgroundImage = ''; // e.target.closest('.media-image').style.backgroundImage = '';
e.target.dataset.loaded = true; e.target.dataset.loaded = true;
if (!hasDimensions) {
const $media = e.target.closest('.media');
if ($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}`;
}
}
}} }}
onError={(e) => { onError={(e) => {
const { src } = e.target; const { src } = e.target;
@ -338,6 +369,7 @@ function Media({
</Figure> </Figure>
); );
} else if (type === 'gifv' || type === 'video' || isVideoMaybe) { } else if (type === 'gifv' || type === 'video' || isVideoMaybe) {
const hasDuration = original.duration > 0;
const shortDuration = original.duration < 31; const shortDuration = original.duration < 31;
const isGIF = type === 'gifv' && shortDuration; const isGIF = type === 'gifv' && shortDuration;
// If GIF is too long, treat it as a video // If GIF is too long, treat it as a video
@ -356,7 +388,7 @@ function Media({
data-orientation="${orientation}" data-orientation="${orientation}"
preload="auto" preload="auto"
autoplay autoplay
muted="${isGIF}" ${isGIF ? 'muted' : ''}
${isGIF ? '' : 'controls'} ${isGIF ? '' : 'controls'}
playsinline playsinline
loop="${loopable}" loop="${loopable}"
@ -473,14 +505,61 @@ function Media({
/> />
) : ( ) : (
<> <>
<img {previewUrl ? (
src={previewUrl} <img
alt={showInlineDesc ? '' : description} src={previewUrl}
width={width} alt={showInlineDesc ? '' : description}
height={height} width={width}
data-orientation={orientation} height={height}
loading="lazy" 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"> <div class="media-play">
<Icon icon="play" size="xl" /> <Icon icon="play" size="xl" />
</div> </div>
@ -539,4 +618,13 @@ function Media({
} }
} }
function getURLObj(url) {
try {
// Fake base URL if url doesn't have https:// prefix
return new URL(url, location.origin);
} catch (e) {
return null;
}
}
export default Media; export default Media;

Wyświetl plik

@ -10,17 +10,56 @@
align-items: center; align-items: center;
background-color: var(--backdrop-color); background-color: var(--backdrop-color);
animation: appear 0.5s var(--timing-function) both; animation: appear 0.5s var(--timing-function) both;
transition: all 0.5s var(--timing-function);
&.solid { &.solid {
background-color: var(--backdrop-solid-color); 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);
--right: max(
var(--compose-button-dimension-margin),
env(safe-area-inset-right)
);
--bottom: max(
var(--compose-button-dimension-margin),
env(safe-area-inset-bottom)
);
--origin-right: calc(
100% - var(--compose-button-dimension-half) - var(--right)
);
--origin-bottom: calc(
100% - var(--compose-button-dimension-half) - var(--bottom)
);
transform-origin: var(--origin-right) var(--origin-bottom);
}
.sheet { .sheet {
transition: transform 0.3s var(--timing-function); transition: transform 0.3s var(--timing-function);
transform-origin: center bottom; transform-origin: 80% 80%;
} }
&:has(~ div) .sheet { &:has(~ div) .sheet {
transform: scale(0.975); transform: scale(0.975);
} }
} }
@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
);
}
}

Wyświetl plik

@ -8,7 +8,7 @@ import useCloseWatcher from '../utils/useCloseWatcher';
const $modalContainer = document.getElementById('modal-container'); const $modalContainer = document.getElementById('modal-container');
function Modal({ children, onClose, onClick, class: className }) { function Modal({ children, onClose, onClick, class: className, minimized }) {
if (!children) return null; if (!children) return null;
const modalRef = useRef(); const modalRef = useRef();
@ -41,6 +41,33 @@ function Modal({ children, onClose, onClick, class: className }) {
); );
useCloseWatcher(onClose, [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 = ( const Modal = (
<div <div
ref={(node) => { ref={(node) => {
@ -54,7 +81,8 @@ function Modal({ children, onClose, onClick, class: className }) {
onClose?.(e); onClose?.(e);
} }
}} }}
tabIndex="-1" tabIndex={minimized ? 0 : '-1'}
inert={minimized}
onFocus={(e) => { onFocus={(e) => {
try { try {
if (e.target === e.currentTarget) { if (e.target === e.currentTarget) {

Wyświetl plik

@ -1,4 +1,4 @@
import { lazy } from 'preact/compat'; import { useEffect } from 'preact/hooks';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { subscribe, useSnapshot } from 'valtio'; import { subscribe, useSnapshot } from 'valtio';
@ -9,19 +9,16 @@ import showToast from '../utils/show-toast';
import states from '../utils/states'; import states from '../utils/states';
import AccountSheet from './account-sheet'; import AccountSheet from './account-sheet';
// import Compose from './compose'; import ComposeSuspense, { preload } from './compose-suspense';
import Drafts from './drafts'; import Drafts from './drafts';
import EmbedModal from './embed-modal'; import EmbedModal from './embed-modal';
import GenericAccounts from './generic-accounts'; import GenericAccounts from './generic-accounts';
import IntlSegmenterSuspense from './intl-segmenter-suspense';
import MediaAltModal from './media-alt-modal'; import MediaAltModal from './media-alt-modal';
import MediaModal from './media-modal'; import MediaModal from './media-modal';
import Modal from './modal'; import Modal from './modal';
import ReportModal from './report-modal'; import ReportModal from './report-modal';
import ShortcutsSettings from './shortcuts-settings'; import ShortcutsSettings from './shortcuts-settings';
const Compose = lazy(() => import('./compose'));
subscribe(states, (changes) => { subscribe(states, (changes) => {
for (const [action, path, value, prevValue] of changes) { for (const [action, path, value, prevValue] of changes) {
// When closing modal, focus on deck // When closing modal, focus on deck
@ -36,55 +33,60 @@ export default function Modals() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
useEffect(() => {
queueMicrotask(preload);
}, []);
return ( return (
<> <>
{!!snapStates.showCompose && ( {!!snapStates.showCompose && (
<Modal class="solid"> <Modal
<IntlSegmenterSuspense> class={`solid ${snapStates.composerState.minimized ? 'min' : ''}`}
<Compose minimized={!!snapStates.composerState.minimized}
replyToStatus={ >
typeof snapStates.showCompose !== 'boolean' <ComposeSuspense
? snapStates.showCompose.replyToStatus replyToStatus={
: window.__COMPOSE__?.replyToStatus || null 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}`,
);
},
});
} }
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}`,
);
},
});
}
}}
/>
</IntlSegmenterSuspense>
</Modal> </Modal>
)} )}
{!!snapStates.showSettings && ( {!!snapStates.showSettings && (
@ -187,6 +189,7 @@ export default function Modals() {
} }
postID={snapStates.showGenericAccounts.postID} postID={snapStates.showGenericAccounts.postID}
onClose={() => (states.showGenericAccounts = false)} onClose={() => (states.showGenericAccounts = false)}
blankCopy={snapStates.showGenericAccounts.blankCopy}
/> />
</Modal> </Modal>
)} )}

Wyświetl plik

@ -31,16 +31,17 @@ function NameText({
.replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1 .replace(/(\:(\w|\+|\-)+\:)(?=|[\!\.\?]|$)/g, '') // Remove shortcodes, regex from https://regex101.com/r/iE9uV0/1
.replace(/\s+/g, ''); // E.g. "My name" === "myname" .replace(/\s+/g, ''); // E.g. "My name" === "myname"
const shortenedAlphaNumericDisplayName = shortenedDisplayName.replace( const shortenedAlphaNumericDisplayName = shortenedDisplayName.replace(
/[^a-z0-9]/gi, /[^a-z0-9@\.]/gi,
'', '',
); // Remove non-alphanumeric characters ); // Remove non-alphanumeric characters
if ( if (
!short && (!short &&
(trimmedUsername === trimmedDisplayName || (trimmedUsername === trimmedDisplayName ||
trimmedUsername === shortenedDisplayName || trimmedUsername === shortenedDisplayName ||
trimmedUsername === shortenedAlphaNumericDisplayName || trimmedUsername === shortenedAlphaNumericDisplayName ||
nameCollator.compare(trimmedUsername, shortenedDisplayName) === 0) nameCollator.compare(trimmedUsername, shortenedDisplayName) === 0)) ||
shortenedAlphaNumericDisplayName === acct.toLowerCase()
) { ) {
username = null; username = null;
} }
@ -57,9 +58,15 @@ function NameText({
} }
onClick={(e) => { onClick={(e) => {
if (external) return; if (external) return;
if (e.shiftKey) return; // Save link? 🤷
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (onClick) return onClick(e); 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 = { states.showAccount = {
account, account,
instance, instance,

Wyświetl plik

@ -11,6 +11,8 @@ import { getLists } from '../utils/lists';
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding'; import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
import states from '../utils/states'; import states from '../utils/states';
import store from '../utils/store'; import store from '../utils/store';
import { getCurrentAccountID } from '../utils/store-utils';
import supports from '../utils/supports';
import Avatar from './avatar'; import Avatar from './avatar';
import Icon from './icon'; import Icon from './icon';
@ -24,9 +26,8 @@ function NavMenu(props) {
const [currentAccount, moreThanOneAccount] = useMemo(() => { const [currentAccount, moreThanOneAccount] = useMemo(() => {
const accounts = store.local.getJSON('accounts') || []; const accounts = store.local.getJSON('accounts') || [];
const acc = const acc =
accounts.find( accounts.find((account) => account.info.id === getCurrentAccountID()) ||
(account) => account.info.id === store.session.get('currentAccount'), accounts[0];
) || accounts[0];
return [acc, accounts.length > 1]; return [acc, accounts.length > 1];
}, []); }, []);
@ -83,8 +84,10 @@ function NavMenu(props) {
return results; return results;
} }
const supportsLists = supports('@mastodon/lists');
const [lists, setLists] = useState([]); const [lists, setLists] = useState([]);
useEffect(() => { useEffect(() => {
if (!supportsLists) return;
if (menuState === 'open') { if (menuState === 'open') {
getLists().then(setLists); getLists().then(setLists);
} }
@ -186,9 +189,11 @@ function NavMenu(props) {
<Icon icon="history2" size="l" /> <Icon icon="history2" size="l" />
<span>Catch-up</span> <span>Catch-up</span>
</MenuLink> </MenuLink>
<MenuLink to="/mentions"> {supports('@mastodon/mentions') && (
<Icon icon="at" size="l" /> <span>Mentions</span> <MenuLink to="/mentions">
</MenuLink> <Icon icon="at" size="l" /> <span>Mentions</span>
</MenuLink>
)}
<MenuLink to="/notifications"> <MenuLink to="/notifications">
<Icon icon="notification" size="l" /> <span>Notifications</span> <Icon icon="notification" size="l" /> <span>Notifications</span>
{snapStates.notificationsShowNew && ( {snapStates.notificationsShowNew && (
@ -232,10 +237,12 @@ function NavMenu(props) {
)} )}
</SubMenu2> </SubMenu2>
) : ( ) : (
<MenuLink to="/l"> supportsLists && (
<Icon icon="list" size="l" /> <MenuLink to="/l">
<span>Lists</span> <Icon icon="list" size="l" />
</MenuLink> <span>Lists</span>
</MenuLink>
)
)} )}
<MenuLink to="/b"> <MenuLink to="/b">
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span> <Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
@ -260,10 +267,12 @@ function NavMenu(props) {
<span>Followed Hashtags</span> <span>Followed Hashtags</span>
</MenuLink> </MenuLink>
<MenuDivider /> <MenuDivider />
<MenuLink to="/ft"> {supports('@mastodon/filters') && (
<Icon icon="filters" size="l" /> <MenuLink to="/ft">
Filters <Icon icon="filters" size="l" />
</MenuLink> Filters
</MenuLink>
)}
<MenuItem <MenuItem
onClick={() => { onClick={() => {
states.showGenericAccounts = { states.showGenericAccounts = {

Wyświetl plik

@ -4,6 +4,7 @@ import { memo } from 'preact/compat';
import shortenNumber from '../utils/shorten-number'; import shortenNumber from '../utils/shorten-number';
import states, { statusKey } from '../utils/states'; import states, { statusKey } from '../utils/states';
import store from '../utils/store'; import store from '../utils/store';
import { getCurrentAccountID } from '../utils/store-utils';
import useTruncated from '../utils/useTruncated'; import useTruncated from '../utils/useTruncated';
import Avatar from './avatar'; import Avatar from './avatar';
@ -27,6 +28,7 @@ const NOTIFICATION_ICONS = {
'admin.signup': 'account-edit', 'admin.signup': 'account-edit',
'admin.report': 'account-warning', 'admin.report': 'account-warning',
severed_relationships: 'heart-break', severed_relationships: 'heart-break',
moderation_warning: 'alert',
emoji_reaction: 'emoji2', emoji_reaction: 'emoji2',
'pleroma:emoji_reaction': 'emoji2', 'pleroma:emoji_reaction': 'emoji2',
}; };
@ -44,6 +46,8 @@ poll = A poll you have voted in or created has ended
update = A status you interacted with has been edited update = A status you interacted with has been edited
admin.sign_up = Someone signed up (optionally sent to admins) admin.sign_up = Someone signed up (optionally sent to admins)
admin.report = A new report has been filed admin.report = A new report has been filed
severed_relationships = Severed relationships
moderation_warning = Moderation warning
*/ */
function emojiText(emoji, emoji_url) { function emojiText(emoji, emoji_url) {
@ -90,6 +94,7 @@ const contentText = {
Lost connections with <i>{name}</i>. Lost connections with <i>{name}</i>.
</> </>
), ),
moderation_warning: <b>Moderation warning</b>,
emoji_reaction: emojiText, emoji_reaction: emojiText,
'pleroma:emoji_reaction': emojiText, 'pleroma:emoji_reaction': emojiText,
}; };
@ -116,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; const AVATARS_LIMIT = 50;
function Notification({ function Notification({
@ -124,15 +140,23 @@ function Notification({
isStatic, isStatic,
disableContextMenu, disableContextMenu,
}) { }) {
const { id, status, account, report, event, _accounts, _statuses } = const {
notification; id,
status,
account,
report,
event,
moderation_warning,
_accounts,
_statuses,
} = notification;
let { type } = notification; let { type } = notification;
// status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update // status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update
const actualStatus = status?.reblog || status; const actualStatus = status?.reblog || status;
const actualStatusID = actualStatus?.id; const actualStatusID = actualStatus?.id;
const currentAccount = store.session.get('currentAccount'); const currentAccount = getCurrentAccountID();
const isSelf = currentAccount === account?.id; const isSelf = currentAccount === account?.id;
const isVoted = status?.poll?.voted; const isVoted = status?.poll?.voted;
const isReplyToOthers = const isReplyToOthers =
@ -313,6 +337,20 @@ function Notification({
. .
</div> </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 && ( {_accounts?.length > 1 && (

Wyświetl plik

@ -21,6 +21,7 @@ export default function RelativeTime({ datetime, format }) {
const [renderCount, rerender] = useReducer((x) => x + 1, 0); const [renderCount, rerender] = useReducer((x) => x + 1, 0);
const date = useMemo(() => dayjs(datetime), [datetime]); const date = useMemo(() => dayjs(datetime), [datetime]);
const [dateStr, dt, title] = useMemo(() => { const [dateStr, dt, title] = useMemo(() => {
if (!date.isValid()) return ['' + datetime, '', ''];
let str; let str;
if (format === 'micro') { if (format === 'micro') {
// If date <= 1 day ago or day is within this year // If date <= 1 day ago or day is within this year
@ -37,6 +38,7 @@ export default function RelativeTime({ datetime, format }) {
}, [date, format, renderCount]); }, [date, format, renderCount]);
useEffect(() => { useEffect(() => {
if (!date.isValid()) return;
let timeout; let timeout;
let raf; let raf;
function rafRerender() { function rafRerender() {

Wyświetl plik

@ -19,6 +19,7 @@ import pmem from '../utils/pmem';
import showToast from '../utils/show-toast'; import showToast from '../utils/show-toast';
import states from '../utils/states'; import states from '../utils/states';
import store from '../utils/store'; import store from '../utils/store';
import { getCurrentAccountID } from '../utils/store-utils';
import AsyncText from './AsyncText'; import AsyncText from './AsyncText';
import Icon from './icon'; import Icon from './icon';
@ -787,7 +788,7 @@ function ImportExport({ shortcuts, onClose }) {
disabled={importUIState === 'cloud-downloading'} disabled={importUIState === 'cloud-downloading'}
onClick={async () => { onClick={async () => {
setImportUIState('cloud-downloading'); setImportUIState('cloud-downloading');
const currentAccount = store.session.get('currentAccount'); const currentAccount = getCurrentAccountID();
showToast( showToast(
'Downloading saved shortcuts from instance server…', 'Downloading saved shortcuts from instance server…',
); );
@ -1043,7 +1044,7 @@ function ImportExport({ shortcuts, onClose }) {
disabled={importUIState === 'cloud-uploading'} disabled={importUIState === 'cloud-uploading'}
onClick={async () => { onClick={async () => {
setImportUIState('cloud-uploading'); setImportUIState('cloud-uploading');
const currentAccount = store.session.get('currentAccount'); const currentAccount = getCurrentAccountID();
try { try {
const relationships = const relationships =
await masto.v1.accounts.relationships.fetch({ await masto.v1.accounts.relationships.fetch({

Wyświetl plik

@ -47,7 +47,7 @@
} }
.visibility-direct { .visibility-direct {
--yellow-stripes: repeating-linear-gradient( --yellow-stripes: repeating-linear-gradient(
-45deg, 135deg,
var(--reply-to-faded-color), var(--reply-to-faded-color),
var(--reply-to-faded-color) 10px, var(--reply-to-faded-color) 10px,
var(--reply-to-faded-color) 10px, var(--reply-to-faded-color) 10px,
@ -160,7 +160,7 @@
display: block; display: block;
position: relative; position: relative;
&:after { &[data-read-more]:after {
content: attr(data-read-more); content: attr(data-read-more);
line-height: 1; line-height: 1;
display: inline-block; display: inline-block;
@ -365,6 +365,10 @@
background-image: var(--yellow-stripes); background-image: var(--yellow-stripes);
} }
.status-pre-meta + & {
background-image: none;
}
> * { > * {
opacity: 0.65; opacity: 0.65;
transition: opacity 1s ease-out; transition: opacity 1s ease-out;
@ -565,8 +569,15 @@
font-weight: bold; font-weight: bold;
vertical-align: middle; vertical-align: middle;
display: inline-block; 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; display: inline-flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
@ -580,10 +591,10 @@
border-color: var(--text-color); border-color: var(--text-color);
background: var(--bg-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; white-space: nowrap;
} }
.status-filtered-badge.badge-meta > span + span { .status-filtered-badge:not(.horizontal).badge-meta > span + span {
display: block; display: block;
font-size: 9px; font-size: 9px;
font-weight: normal; font-weight: normal;
@ -597,6 +608,10 @@
left: 0; left: 0;
text-align: center; text-align: center;
} }
.status-filtered-badge.horizontal.badge-meta > span + span {
font-weight: normal;
text-transform: none;
}
.status.large > .container > .content-container { .status.large > .container > .content-container {
margin-left: calc(-50px - 16px); margin-left: calc(-50px - 16px);
@ -618,6 +633,7 @@
~ *:not( ~ *:not(
.content.truncated, .content.truncated,
.media-container, .media-container,
.media-first-container,
.card, .card,
.media-figure-multiple, .media-figure-multiple,
.spoiler-media-button .spoiler-media-button
@ -638,6 +654,7 @@
~ *:not( ~ *:not(
.media-container, .media-container,
.media-first-container,
.card, .card,
.media-figure-multiple, .media-figure-multiple,
.spoiler-media-button .spoiler-media-button
@ -708,11 +725,12 @@
} }
} }
~ :is(.media-container, .media-figure-multiple) .media { ~ :is(.media-container, .media-first-container, .media-figure-multiple)
.media {
background-image: radial-gradient( background-image: radial-gradient(
circle at 50% 50%, circle at 50% 50%,
var(--average-color, var(--bg-faded-color)), var(--average-color, var(--bg-faded-color)),
var(--bg-color) 20em var(--bg-color) 25em
); );
> *:not(.media-play, .alt-badge) { > *:not(.media-play, .alt-badge) {
@ -790,7 +808,9 @@
black 1.5em black 1.5em
); );
} }
.timeline-deck .status:not(.truncated .status) .content.truncated:after { .timeline-deck
.status:not(.truncated .status)
.content.truncated[data-read-more]:after {
content: attr(data-read-more); content: attr(data-read-more);
line-height: 1; line-height: 1;
display: inline-block; display: inline-block;
@ -816,6 +836,12 @@
.timeline-deck .status .content.truncated ~ .card { .timeline-deck .status .content.truncated ~ .card {
display: none; 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) { .status .content .inner-content a:not(.mention, .has-url-text) {
color: var(--link-text-color); color: var(--link-text-color);
} }
@ -1314,6 +1340,258 @@ body:has(#modal-container .carousel) .status .media img:hover {
background-blend-mode: multiply; background-blend-mode: multiply;
} }
.status.skeleton .media-first-container {
min-height: 320px;
background-color: var(--outline-color);
}
@keyframes media-carousel-slide {
0% {
transform: translateX(calc(var(--dots-count, 1) * 2.5px));
}
100% {
transform: translateX(calc(var(--dots-count, 1) * -2.5px));
}
}
.status-media-first {
timeline-scope: --media-carousel;
.meta-name {
opacity: 0.65;
transition: opacity 0.5s ease-in-out;
b + i {
opacity: 0;
transition: opacity 0.5s ease-in-out;
}
}
:is(:hover, :focus) > & .meta-name {
opacity: 1;
b + i {
opacity: 0.5;
}
}
.media-first-spoiler-content {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
transition: opacity 0.5s ease-in-out;
opacity: 0.5;
}
&:hover .media-first-spoiler-content {
opacity: 1;
}
.media-first-spoiler-button {
display: inline-flex !important;
}
.media-first-container {
position: relative;
margin-top: 8px;
margin-inline: -16px;
@media (min-width: 40em) {
margin-inline: 0;
}
.media-carousel-controls {
flex-shrink: 0;
position: absolute;
inset: 0;
pointer-events: none;
display: flex;
justify-content: space-between;
}
.carousel-indexer {
z-index: 1;
position: absolute;
top: 8px;
right: 8px;
color: var(--media-fg-color);
background-color: var(--media-bg-color);
padding: 2px 8px;
border-radius: 16px;
font-size: 0.8em;
font-variant-numeric: tabular-nums;
opacity: 0.6;
transition: opacity 1s ease-in-out 0.3s;
border: var(--hairline-width) solid var(--media-outline-color);
}
.media-carousel-button {
display: flex;
flex-shrink: 0;
padding-inline: 8px;
margin-block: 3em;
pointer-events: auto;
cursor: pointer;
align-items: center;
justify-content: center;
}
.carousel-button {
@media (pointer: coarse) {
display: none;
}
+ .carousel-button {
left: auto;
right: 8px;
}
}
@media (hover: hover) and (pointer: fine) {
.carousel-button {
filter: opacity(0);
}
&:hover .carousel-button {
filter: opacity(1);
}
}
}
.media-first-carousel {
display: flex;
max-height: 80vh;
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
user-select: none;
scrollbar-width: none;
/* border: var(--hairline-width) solid var(--outline-color);
border-inline-width: 0;
background-color: var(--bg-faded-color); */
box-shadow: 0 0 0 var(--hairline-width) var(--outline-color);
scroll-timeline: --media-carousel x;
@media (min-width: 40em) {
/* margin-inline: 0; */
/* border-radius: 4px; */
/* border-inline-width: var(--hairline-width); */
box-shadow: none;
}
&::-webkit-scrollbar {
display: none;
}
> .media-first-item {
scroll-snap-align: center;
scroll-snap-stop: always;
flex-shrink: 0;
display: flex;
width: 100%;
align-items: center;
justify-content: center;
&:not(:only-child) {
background-color: var(--bg-blur-color);
/* box-shadow: inset 0 0 0 var(--hairline-width) var(--outline-color); */
}
.media {
/* background-color: var(--average-color, var(--bg-faded-color)); */
width: var(--width, 100%);
max-width: 100%;
max-height: 100%;
min-height: var(--min-dimension);
/* max-height: min(var(--height), 80vh); */
&:has(img:not([data-loaded='true'])) {
min-height: 320px;
}
&:active {
transform: none;
filter: none;
}
img,
video {
object-fit: scale-down;
animation: none;
&:not([data-loaded='true']) {
background-color: var(--bg-color);
}
}
}
}
}
:is(:hover, :focus) > & .carousel-indexer {
opacity: 0;
}
.media-carousel-dots {
pointer-events: none;
display: flex;
gap: 5px;
justify-content: center;
margin-top: 8px;
padding: 8px;
@supports (animation-timeline: scroll()) {
animation: media-carousel-slide 1s linear both;
animation-timeline: --media-carousel;
}
.carousel-dot {
display: inline-block;
width: 5px;
height: 5px;
border-radius: 50%;
background-color: var(--text-color);
transition: all 0.3s ease-in-out;
opacity: 0.3;
flex-shrink: 0;
&.active {
opacity: 1;
background-color: var(--text-color);
transform: scale(1.5);
}
}
}
.media-first-content {
margin-top: 8px;
height: 1.75em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.9em;
mask-image: linear-gradient(to bottom, black 1.5em, transparent 1.75em);
opacity: 0.5;
transition: opacity 0.5s ease-in-out;
@media (min-width: 40em) {
margin-inline: 16px;
}
* {
text-align: center;
/* Brute force ellipsis */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap !important;
pointer-events: none;
}
a {
filter: grayscale(0.5);
}
}
:is(:hover, :focus) > & .media-first-content {
opacity: 1;
}
}
.status:not(.large) .hashtag-stuffing { .status:not(.large) .hashtag-stuffing {
opacity: 0.75; opacity: 0.75;
transition: opacity 0.2s ease-in-out; transition: opacity 0.2s ease-in-out;
@ -2119,8 +2397,8 @@ a.card:is(:hover, :focus):visited {
max-width: 100%; max-width: 100%;
height: 1.2em; height: 1.2em;
vertical-align: text-bottom; vertical-align: text-bottom;
object-fit: cover; object-fit: contain;
object-position: left; /* object-position: left; */
} }
/* EDIT HISTORY */ /* EDIT HISTORY */
@ -2289,7 +2567,7 @@ a.card:is(:hover, :focus):visited {
mask-image: linear-gradient(to bottom, #000 80px, transparent); mask-image: linear-gradient(to bottom, #000 80px, transparent);
} }
&:after { &[data-read-more]:after {
content: attr(data-read-more); content: attr(data-read-more);
line-height: 1; line-height: 1;
display: inline-block; display: inline-block;

Wyświetl plik

@ -11,6 +11,7 @@ import {
import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash'; import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash';
import { shallowEqual } from 'fast-equals'; import { shallowEqual } from 'fast-equals';
import prettify from 'html-prettify'; import prettify from 'html-prettify';
import { Fragment } from 'preact';
import { memo } from 'preact/compat'; import { memo } from 'preact/compat';
import { import {
useCallback, useCallback,
@ -50,11 +51,14 @@ import openCompose from '../utils/open-compose';
import pmem from '../utils/pmem'; import pmem from '../utils/pmem';
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding'; import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
import shortenNumber from '../utils/shorten-number'; import shortenNumber from '../utils/shorten-number';
import showCompose from '../utils/show-compose';
import showToast from '../utils/show-toast'; import showToast from '../utils/show-toast';
import { speak, supportsTTS } from '../utils/speech'; import { speak, supportsTTS } from '../utils/speech';
import states, { getStatus, saveStatus, statusKey } from '../utils/states'; import states, { getStatus, saveStatus, statusKey } from '../utils/states';
import statusPeek from '../utils/status-peek'; import statusPeek from '../utils/status-peek';
import store from '../utils/store'; import store from '../utils/store';
import { getCurrentAccountID } from '../utils/store-utils';
import supports from '../utils/supports';
import unfurlMastodonLink from '../utils/unfurl-link'; import unfurlMastodonLink from '../utils/unfurl-link';
import useTruncated from '../utils/useTruncated'; import useTruncated from '../utils/useTruncated';
import visibilityIconsMap from '../utils/visibility-icons-map'; import visibilityIconsMap from '../utils/visibility-icons-map';
@ -148,6 +152,12 @@ const PostContent = memo(
}, },
); );
const SIZE_CLASS = {
s: 'small',
m: 'medium',
l: 'large',
};
function Status({ function Status({
statusID, statusID,
status, status,
@ -169,15 +179,23 @@ function Status({
allowContextMenu, allowContextMenu,
showActionsBar, showActionsBar,
showReplyParent, showReplyParent,
mediaFirst,
}) { }) {
if (skeleton) { if (skeleton) {
return ( return (
<div class="status skeleton"> <div
<Avatar size="xxl" /> class={`status skeleton ${
mediaFirst ? 'status-media-first small' : ''
}`}
>
{!mediaFirst && <Avatar size="xxl" />}
<div class="container"> <div class="container">
<div class="meta"> </div> <div class="meta">
{(size === 's' || mediaFirst) && <Avatar size="m" />}
</div>
<div class="content-container"> <div class="content-container">
<div class="content"> {mediaFirst && <div class="media-first-container" />}
<div class={`content ${mediaFirst ? 'media-first-content' : ''}`}>
<p> </p> <p> </p>
</div> </div>
</div> </div>
@ -247,8 +265,12 @@ function Status({
emojiReactions, emojiReactions,
} = status; } = status;
// if (!mediaAttachments?.length) mediaFirst = false;
const hasMediaAttachments = !!mediaAttachments?.length;
if (mediaFirst && hasMediaAttachments) size = 's';
const currentAccount = useMemo(() => { const currentAccount = useMemo(() => {
return store.session.get('currentAccount'); return getCurrentAccountID();
}, []); }, []);
const isSelf = useMemo(() => { const isSelf = useMemo(() => {
return currentAccount && currentAccount === accountId; return currentAccount && currentAccount === accountId;
@ -282,6 +304,7 @@ function Status({
onMouseEnter: debugHover, onMouseEnter: debugHover,
}} }}
showFollowedTags showFollowedTags
quoted={quoted}
/> />
); );
} }
@ -354,6 +377,7 @@ function Status({
size={size} size={size}
contentTextWeight={contentTextWeight} contentTextWeight={contentTextWeight}
readOnly={readOnly} readOnly={readOnly}
mediaFirst={mediaFirst}
/> />
</div> </div>
); );
@ -378,14 +402,15 @@ function Status({
contentTextWeight={contentTextWeight} contentTextWeight={contentTextWeight}
readOnly={readOnly} readOnly={readOnly}
enableCommentHint enableCommentHint
mediaFirst={mediaFirst}
/> />
</div> </div>
); );
} }
// Check followedTags // Check followedTags
if (showFollowedTags && !!snapStates.statusFollowedTags[sKey]?.length) { const FollowedTagsParent = useCallback(
return ( ({ children }) => (
<div <div
data-state-post-id={sKey} data-state-post-id={sKey}
class="status-followed-tags" class="status-followed-tags"
@ -403,18 +428,15 @@ function Status({
</Link> </Link>
))} ))}
</div> </div>
<Status {children}
status={statusID ? null : status}
statusID={statusID ? status.id : null}
instance={instance}
size={size}
contentTextWeight={contentTextWeight}
readOnly={readOnly}
enableCommentHint
/>
</div> </div>
); ),
} [sKey, instance, snapStates.statusFollowedTags[sKey]],
);
const StatusParent =
showFollowedTags && !!snapStates.statusFollowedTags[sKey]?.length
? FollowedTagsParent
: Fragment;
const isSizeLarge = size === 'l'; const isSizeLarge = size === 'l';
@ -503,9 +525,9 @@ function Status({
}); });
if (newWin) return; if (newWin) return;
} }
states.showCompose = { showCompose({
replyToStatus: status, replyToStatus: status,
}; });
}; };
// Check if media has no descriptions // Check if media has no descriptions
@ -628,6 +650,7 @@ function Status({
}; };
const bookmarkStatus = async () => { const bookmarkStatus = async () => {
if (!supports('@mastodon/post-bookmark')) return;
if (!sameInstance || !authenticated) { if (!sameInstance || !authenticated) {
alert(unauthInteractionErrorMessage); alert(unauthInteractionErrorMessage);
return false; return false;
@ -749,11 +772,11 @@ function Status({
menuExtras={ menuExtras={
<MenuItem <MenuItem
onClick={() => { onClick={() => {
states.showCompose = { showCompose({
draftStatus: { draftStatus: {
status: `\n${url}`, status: `\n${url}`,
}, },
}; });
}} }}
> >
<Icon icon="quote" /> <Icon icon="quote" />
@ -815,13 +838,15 @@ function Status({
: 'Like'} : 'Like'}
</span> </span>
</MenuItem> </MenuItem>
<MenuItem {supports('@mastodon/post-bookmark') && (
onClick={bookmarkStatusNotify} <MenuItem
className={`menu-bookmark ${bookmarked ? 'checked' : ''}`} onClick={bookmarkStatusNotify}
> className={`menu-bookmark ${bookmarked ? 'checked' : ''}`}
<Icon icon="bookmark" /> >
<span>{bookmarked ? 'Unbookmark' : 'Bookmark'}</span> <Icon icon="bookmark" />
</MenuItem> <span>{bookmarked ? 'Unbookmark' : 'Bookmark'}</span>
</MenuItem>
)}
</div> </div>
</> </>
)} )}
@ -848,56 +873,62 @@ function Status({
</MenuItem> </MenuItem>
</> </>
)} )}
{(enableTranslate || !language || differentLanguage) && <MenuDivider />} {!mediaFirst && (
{enableTranslate ? ( <>
<div class={supportsTTS ? 'menu-horizontal' : ''}> {(enableTranslate || !language || differentLanguage) && (
<MenuItem <MenuDivider />
disabled={forceTranslate}
onClick={() => {
setForceTranslate(true);
}}
>
<Icon icon="translate" />
<span>Translate</span>
</MenuItem>
{supportsTTS && (
<MenuItem
onClick={() => {
const postText = getPostText(status);
if (postText) {
speak(postText, language);
}
}}
>
<Icon icon="speak" />
<span>Speak</span>
</MenuItem>
)} )}
</div> {enableTranslate ? (
) : ( <div class={supportsTTS ? 'menu-horizontal' : ''}>
(!language || differentLanguage) && (
<div class={supportsTTS ? 'menu-horizontal' : ''}>
<MenuLink
to={`${instance ? `/${instance}` : ''}/s/${id}?translate=1`}
>
<Icon icon="translate" />
<span>Translate</span>
</MenuLink>
{supportsTTS && (
<MenuItem <MenuItem
disabled={forceTranslate}
onClick={() => { onClick={() => {
const postText = getPostText(status); setForceTranslate(true);
if (postText) {
speak(postText, language);
}
}} }}
> >
<Icon icon="speak" /> <Icon icon="translate" />
<span>Speak</span> <span>Translate</span>
</MenuItem> </MenuItem>
)} {supportsTTS && (
</div> <MenuItem
) onClick={() => {
const postText = getPostText(status);
if (postText) {
speak(postText, language);
}
}}
>
<Icon icon="speak" />
<span>Speak</span>
</MenuItem>
)}
</div>
) : (
(!language || differentLanguage) && (
<div class={supportsTTS ? 'menu-horizontal' : ''}>
<MenuLink
to={`${instance ? `/${instance}` : ''}/s/${id}?translate=1`}
>
<Icon icon="translate" />
<span>Translate</span>
</MenuLink>
{supportsTTS && (
<MenuItem
onClick={() => {
const postText = getPostText(status);
if (postText) {
speak(postText, language);
}
}}
>
<Icon icon="speak" />
<span>Speak</span>
</MenuItem>
)}
</div>
)
)}
</>
)} )}
{((!isSizeLarge && sameInstance) || {((!isSizeLarge && sameInstance) ||
enableTranslate || enableTranslate ||
@ -1059,16 +1090,18 @@ function Status({
)} )}
{isSelf && ( {isSelf && (
<div class="menu-horizontal"> <div class="menu-horizontal">
<MenuItem {supports('@mastodon/post-edit') && (
onClick={() => { <MenuItem
states.showCompose = { onClick={() => {
editStatus: status, showCompose({
}; editStatus: status,
}} });
> }}
<Icon icon="pencil" /> >
<span>Edit</span> <Icon icon="pencil" />
</MenuItem> <span>Edit</span>
</MenuItem>
)}
{isSizeLarge && ( {isSizeLarge && (
<MenuConfirm <MenuConfirm
subMenu subMenu
@ -1349,7 +1382,7 @@ function Status({
]); ]);
return ( return (
<> <StatusParent>
{showReplyParent && !!(inReplyToId && inReplyToAccountId) && ( {showReplyParent && !!(inReplyToId && inReplyToAccountId) && (
<StatusCompact sKey={sKey} /> <StatusCompact sKey={sKey} />
)} )}
@ -1377,14 +1410,10 @@ function Status({
? 'status-reply-to' ? 'status-reply-to'
: '' : ''
} visibility-${visibility} ${_pinned ? 'status-pinned' : ''} ${ } visibility-${visibility} ${_pinned ? 'status-pinned' : ''} ${
{ SIZE_CLASS[size]
s: 'small',
m: 'medium',
l: 'large',
}[size]
} ${_deleted ? 'status-deleted' : ''} ${quoted ? 'status-card' : ''} ${ } ${_deleted ? 'status-deleted' : ''} ${quoted ? 'status-card' : ''} ${
isContextMenuOpen ? 'status-menu-open' : '' isContextMenuOpen ? 'status-menu-open' : ''
}`} } ${mediaFirst && hasMediaAttachments ? 'status-media-first' : ''}`}
onMouseEnter={debugHover} onMouseEnter={debugHover}
onContextMenu={(e) => { onContextMenu={(e) => {
if (!showContextMenu) return; if (!showContextMenu) return;
@ -1712,188 +1741,253 @@ function Status({
} }
} }
> >
{!!spoilerText && ( {mediaFirst && hasMediaAttachments ? (
<> <>
<div {(!!spoilerText || !!sensitive) && !readingExpandSpoilers && (
class="content spoiler-content" <>
lang={language} {!!spoilerText && (
dir="auto" <span
ref={spoilerContentRef} class="spoiler-content media-first-spoiler-content"
data-read-more={readMoreText} lang={language}
> dir="auto"
<p> ref={spoilerContentRef}
<EmojiText text={spoilerText} emojis={emojis} /> data-read-more={readMoreText}
</p> >
</div> <EmojiText text={spoilerText} emojis={emojis} />{' '}
{readingExpandSpoilers || previewMode ? ( </span>
<div class="spoiler-divider"> )}
<Icon icon="eye-open" /> Content warning <button
class={`light spoiler-button media-first-spoiler-button ${
showSpoiler ? 'spoiling' : ''
}`}
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (showSpoiler) {
delete states.spoilers[id];
if (!readingExpandSpoilers) {
delete states.spoilersMedia[id];
}
} else {
states.spoilers[id] = true;
if (!readingExpandSpoilers) {
states.spoilersMedia[id] = true;
}
}
}}
>
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
{showSpoiler ? 'Show less' : 'Show content'}
</button>
</>
)}
<MediaFirstContainer
mediaAttachments={mediaAttachments}
language={language}
postID={id}
instance={instance}
/>
{!!content && (
<div class="media-first-content content" ref={contentRef}>
<PostContent
post={status}
instance={instance}
previewMode={previewMode}
/>
</div> </div>
) : (
<button
class={`light spoiler-button ${
showSpoiler ? 'spoiling' : ''
}`}
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (showSpoiler) {
delete states.spoilers[id];
if (!readingExpandSpoilers) {
delete states.spoilersMedia[id];
}
} else {
states.spoilers[id] = true;
if (!readingExpandSpoilers) {
states.spoilersMedia[id] = true;
}
}
}}
>
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
{showSpoiler ? 'Show less' : 'Show content'}
</button>
)} )}
</> </>
)} ) : (
{!!content && ( <>
<div {!!spoilerText && (
class="content" <>
ref={contentRef} <div
data-read-more={readMoreText} class="content spoiler-content"
>
<PostContent
post={status}
instance={instance}
previewMode={previewMode}
/>
<QuoteStatuses id={id} instance={instance} level={quoted} />
</div>
)}
{!!poll && (
<Poll
lang={language}
poll={poll}
readOnly={readOnly || !sameInstance || !authenticated}
onUpdate={(newPoll) => {
states.statuses[sKey].poll = newPoll;
}}
refresh={() => {
return masto.v1.polls
.$select(poll.id)
.fetch()
.then((pollResponse) => {
states.statuses[sKey].poll = pollResponse;
})
.catch((e) => {}); // Silently fail
}}
votePoll={(choices) => {
return masto.v1.polls
.$select(poll.id)
.votes.create({
choices,
})
.then((pollResponse) => {
states.statuses[sKey].poll = pollResponse;
})
.catch((e) => {}); // Silently fail
}}
/>
)}
{(((enableTranslate || inlineTranslate) &&
!!content.trim() &&
!!getHTMLText(emojifyText(content, emojis)) &&
differentLanguage) ||
forceTranslate) && (
<TranslationBlock
forceTranslate={forceTranslate || inlineTranslate}
mini={!isSizeLarge && !withinContext}
sourceLanguage={language}
text={getPostText(status)}
/>
)}
{!previewMode &&
sensitive &&
!!mediaAttachments.length &&
readingExpandMedia !== 'show_all' && (
<button
class={`plain spoiler-media-button ${
showSpoilerMedia ? 'spoiling' : ''
}`}
type="button"
hidden={!readingExpandSpoilers && !!spoilerText}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (showSpoilerMedia) {
delete states.spoilersMedia[id];
} else {
states.spoilersMedia[id] = true;
}
}}
>
<Icon icon={showSpoilerMedia ? 'eye-open' : 'eye-close'} />{' '}
{showSpoilerMedia ? 'Show less' : 'Show media'}
</button>
)}
{!!mediaAttachments.length && (
<MultipleMediaFigure
lang={language}
enabled={showMultipleMediaCaptions}
captionChildren={captionChildren}
>
<div
ref={mediaContainerRef}
class={`media-container media-eq${mediaAttachments.length} ${
mediaAttachments.length > 2 ? 'media-gt2' : ''
} ${mediaAttachments.length > 4 ? 'media-gt4' : ''}`}
>
{displayedMediaAttachments.map((media, i) => (
<Media
key={media.id}
media={media}
autoAnimate={isSizeLarge}
showCaption={mediaAttachments.length === 1}
allowLongerCaption={
!content && mediaAttachments.length === 1
}
lang={language} lang={language}
altIndex={ dir="auto"
showMultipleMediaCaptions && ref={spoilerContentRef}
!!media.description && data-read-more={readMoreText}
i + 1 >
} <p>
to={`/${instance}/s/${id}?${ <EmojiText text={spoilerText} emojis={emojis} />
withinContext ? 'media' : 'media-only' </p>
}=${i + 1}`} </div>
onClick={ {readingExpandSpoilers || previewMode ? (
onMediaClick <div class="spoiler-divider">
? (e) => { <Icon icon="eye-open" /> Content warning
onMediaClick(e, i, media, status); </div>
) : (
<button
class={`light spoiler-button ${
showSpoiler ? 'spoiling' : ''
}`}
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (showSpoiler) {
delete states.spoilers[id];
if (!readingExpandSpoilers) {
delete states.spoilersMedia[id];
} }
: undefined } else {
} states.spoilers[id] = true;
if (!readingExpandSpoilers) {
states.spoilersMedia[id] = true;
}
}
}}
>
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
{showSpoiler ? 'Show less' : 'Show content'}
</button>
)}
</>
)}
{!!content && (
<div
class="content"
ref={contentRef}
data-read-more={readMoreText}
>
<PostContent
post={status}
instance={instance}
previewMode={previewMode}
/> />
))} <QuoteStatuses id={id} instance={instance} level={quoted} />
</div> </div>
</MultipleMediaFigure> )}
{!!poll && (
<Poll
lang={language}
poll={poll}
readOnly={readOnly || !sameInstance || !authenticated}
onUpdate={(newPoll) => {
states.statuses[sKey].poll = newPoll;
}}
refresh={() => {
return masto.v1.polls
.$select(poll.id)
.fetch()
.then((pollResponse) => {
states.statuses[sKey].poll = pollResponse;
})
.catch((e) => {}); // Silently fail
}}
votePoll={(choices) => {
return masto.v1.polls
.$select(poll.id)
.votes.create({
choices,
})
.then((pollResponse) => {
states.statuses[sKey].poll = pollResponse;
})
.catch((e) => {}); // Silently fail
}}
/>
)}
{(((enableTranslate || inlineTranslate) &&
!!content.trim() &&
!!getHTMLText(emojifyText(content, emojis)) &&
differentLanguage) ||
forceTranslate) && (
<TranslationBlock
forceTranslate={forceTranslate || inlineTranslate}
mini={!isSizeLarge && !withinContext}
sourceLanguage={language}
text={getPostText(status)}
/>
)}
{!previewMode &&
sensitive &&
!!mediaAttachments.length &&
readingExpandMedia !== 'show_all' && (
<button
class={`plain spoiler-media-button ${
showSpoilerMedia ? 'spoiling' : ''
}`}
type="button"
hidden={!readingExpandSpoilers && !!spoilerText}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (showSpoilerMedia) {
delete states.spoilersMedia[id];
} else {
states.spoilersMedia[id] = true;
}
}}
>
<Icon
icon={showSpoilerMedia ? 'eye-open' : 'eye-close'}
/>{' '}
{showSpoilerMedia ? 'Show less' : 'Show media'}
</button>
)}
{!!mediaAttachments.length && (
<MultipleMediaFigure
lang={language}
enabled={showMultipleMediaCaptions}
captionChildren={captionChildren}
>
<div
ref={mediaContainerRef}
class={`media-container media-eq${
mediaAttachments.length
} ${mediaAttachments.length > 2 ? 'media-gt2' : ''} ${
mediaAttachments.length > 4 ? 'media-gt4' : ''
}`}
>
{displayedMediaAttachments.map((media, i) => (
<Media
key={media.id}
media={media}
autoAnimate={isSizeLarge}
showCaption={mediaAttachments.length === 1}
allowLongerCaption={
!content && mediaAttachments.length === 1
}
lang={language}
altIndex={
showMultipleMediaCaptions &&
!!media.description &&
i + 1
}
to={`/${instance}/s/${id}?${
withinContext ? 'media' : 'media-only'
}=${i + 1}`}
onClick={
onMediaClick
? (e) => {
onMediaClick(e, i, media, status);
}
: undefined
}
/>
))}
</div>
</MultipleMediaFigure>
)}
{!!card &&
/^https/i.test(card?.url) &&
!sensitive &&
!spoilerText &&
!poll &&
!mediaAttachments.length &&
!snapStates.statusQuotes[sKey] && (
<Card
card={card}
selfReferential={
card?.url === status.url || card?.url === status.uri
}
instance={currentInstance}
/>
)}
</>
)} )}
{!!card &&
/^https/i.test(card?.url) &&
!sensitive &&
!spoilerText &&
!poll &&
!mediaAttachments.length &&
!snapStates.statusQuotes[sKey] && (
<Card
card={card}
selfReferential={
card?.url === status.url || card?.url === status.uri
}
instance={currentInstance}
/>
)}
</div> </div>
{!isSizeLarge && showCommentCount && ( {!isSizeLarge && showCommentCount && (
<div class="content-comment-hint insignificant"> <div class="content-comment-hint insignificant">
@ -2032,11 +2126,11 @@ function Status({
menuExtras={ menuExtras={
<MenuItem <MenuItem
onClick={() => { onClick={() => {
states.showCompose = { showCompose({
draftStatus: { draftStatus: {
status: `\n${url}`, status: `\n${url}`,
}, },
}; });
}} }}
> >
<Icon icon="quote" /> <Icon icon="quote" />
@ -2077,16 +2171,18 @@ function Status({
onClick={favouriteStatus} onClick={favouriteStatus}
/> />
</div> </div>
<div class="action"> {supports('@mastodon/post-bookmark') && (
<StatusButton <div class="action">
checked={bookmarked} <StatusButton
title={['Bookmark', 'Unbookmark']} checked={bookmarked}
alt={['Bookmark', 'Bookmarked']} title={['Bookmark', 'Unbookmark']}
class="bookmark-button" alt={['Bookmark', 'Bookmarked']}
icon="bookmark" class="bookmark-button"
onClick={bookmarkStatus} icon="bookmark"
/> onClick={bookmarkStatus}
</div> />
</div>
)}
<Menu2 <Menu2
portal={{ portal={{
target: target:
@ -2154,7 +2250,7 @@ function Status({
</Modal> </Modal>
)} )}
</article> </article>
</> </StatusParent>
); );
} }
@ -2171,6 +2267,108 @@ function MultipleMediaFigure(props) {
); );
} }
function MediaFirstContainer(props) {
const { mediaAttachments, language, postID, instance } = props;
const moreThanOne = mediaAttachments.length > 1;
const carouselRef = useRef();
const [currentIndex, setCurrentIndex] = useState(0);
useEffect(() => {
let handleScroll = () => {
const { clientWidth, scrollLeft } = carouselRef.current;
const index = Math.round(scrollLeft / clientWidth);
setCurrentIndex(index);
};
if (carouselRef.current) {
carouselRef.current.addEventListener('scroll', handleScroll, {
passive: true,
});
}
return () => {
if (carouselRef.current) {
carouselRef.current.removeEventListener('scroll', handleScroll);
}
};
}, []);
return (
<>
<div class="media-first-container">
<div class="media-first-carousel" ref={carouselRef}>
{mediaAttachments.map((media, i) => (
<div class="media-first-item" key={media.id}>
<Media
media={media}
lang={language}
to={`/${instance}/s/${postID}?media=${i + 1}`}
/>
</div>
))}
</div>
{moreThanOne && (
<div class="media-carousel-controls">
<div class="carousel-indexer">
{currentIndex + 1}/{mediaAttachments.length}
</div>
<label class="media-carousel-button">
<button
type="button"
class="carousel-button"
hidden={currentIndex === 0}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
carouselRef.current.focus();
carouselRef.current.scrollTo({
left: carouselRef.current.clientWidth * (currentIndex - 1),
behavior: 'smooth',
});
}}
>
<Icon icon="arrow-left" />
</button>
</label>
<label class="media-carousel-button">
<button
type="button"
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),
behavior: 'smooth',
});
}}
>
<Icon icon="arrow-right" />
</button>
</label>
</div>
)}
</div>
{moreThanOne && (
<div
class="media-carousel-dots"
style={{
'--dots-count': mediaAttachments.length,
}}
>
{mediaAttachments.map((media, i) => (
<span
key={media.id}
class={`carousel-dot ${i === currentIndex ? 'active' : ''}`}
/>
))}
</div>
)}
</>
);
}
function Card({ card, selfReferential, instance }) { function Card({ card, selfReferential, instance }) {
const snapStates = useSnapshot(states); const snapStates = useSnapshot(states);
const { const {
@ -2980,6 +3178,7 @@ function FilteredStatus({
instance, instance,
containerProps = {}, containerProps = {},
showFollowedTags, showFollowedTags,
quoted,
}) { }) {
const snapStates = useSnapshot(states); const snapStates = useSnapshot(states);
const { const {
@ -3024,7 +3223,9 @@ function FilteredStatus({
return ( return (
<div <div
class={ class={
isReblog quoted
? ''
: isReblog
? group ? group
? 'status-group' ? 'status-group'
: 'status-reblog' : 'status-reblog'
@ -3040,7 +3241,11 @@ function FilteredStatus({
}} }}
{...bindLongPressPeek()} {...bindLongPressPeek()}
> >
<article data-state-post-id={ssKey} class="status filtered" tabindex="-1"> <article
data-state-post-id={ssKey}
class={`status filtered ${quoted ? 'status-card' : ''}`}
tabindex="-1"
>
<b <b
class="status-filtered-badge clickable badge-meta" class="status-filtered-badge clickable badge-meta"
title={filterTitleStr} title={filterTitleStr}
@ -3141,7 +3346,7 @@ const QuoteStatuses = memo(({ id, instance, level = 0 }) => {
return uniqueQuotes.map((q) => { return uniqueQuotes.map((q) => {
return ( return (
<LazyShazam> <LazyShazam id={q.instance + q.id}>
<Link <Link
key={q.instance + q.id} key={q.instance + q.id}
to={`${q.instance ? `/${q.instance}` : ''}/s/${q.id}`} to={`${q.instance ? `/${q.instance}` : ''}/s/${q.id}`}

Wyświetl plik

@ -1,5 +1,11 @@
import { memo } from 'preact/compat'; import { memo } from 'preact/compat';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { InView } from 'react-intersection-observer'; import { InView } from 'react-intersection-observer';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
@ -9,6 +15,7 @@ import FilterContext from '../utils/filter-context';
import { filteredItems, isFiltered } from '../utils/filters'; import { filteredItems, isFiltered } from '../utils/filters';
import states, { statusKey } from '../utils/states'; import states, { statusKey } from '../utils/states';
import statusPeek from '../utils/status-peek'; import statusPeek from '../utils/status-peek';
import { isMediaFirstInstance } from '../utils/store-utils';
import { groupBoosts, groupContext } from '../utils/timeline-utils'; import { groupBoosts, groupContext } from '../utils/timeline-utils';
import useInterval from '../utils/useInterval'; import useInterval from '../utils/useInterval';
import usePageVisibility from '../utils/usePageVisibility'; import usePageVisibility from '../utils/usePageVisibility';
@ -59,6 +66,8 @@ function Timeline({
console.debug('RENDER Timeline', id, refresh); console.debug('RENDER Timeline', id, refresh);
const mediaFirst = useMemo(() => isMediaFirstInstance(), []);
const allowGrouping = view !== 'media'; const allowGrouping = view !== 'media';
const loadItems = useDebouncedCallback( const loadItems = useDebouncedCallback(
(firstLoad) => { (firstLoad) => {
@ -200,8 +209,8 @@ function Timeline({
const oRef = useHotkeys(['enter', 'o'], () => { const oRef = useHotkeys(['enter', 'o'], () => {
// open active status // open active status
const activeItem = document.activeElement.closest(itemsSelector); const activeItem = document.activeElement;
if (activeItem) { if (activeItem?.matches(itemsSelector)) {
activeItem.click(); activeItem.click();
} }
}); });
@ -355,7 +364,9 @@ function Timeline({
<FilterContext.Provider value={filterContext}> <FilterContext.Provider value={filterContext}>
<div <div
id={`${id}-page`} id={`${id}-page`}
class="deck-container" class={`deck-container ${
mediaFirst ? 'deck-container-media-first' : ''
}`}
ref={(node) => { ref={(node) => {
scrollableRef.current = node; scrollableRef.current = node;
jRef.current = node; jRef.current = node;
@ -432,6 +443,7 @@ function Timeline({
view={view} view={view}
showFollowedTags={showFollowedTags} showFollowedTags={showFollowedTags}
showReplyParent={showReplyParent} showReplyParent={showReplyParent}
mediaFirst={mediaFirst}
/> />
))} ))}
{showMore && {showMore &&
@ -443,14 +455,14 @@ function Timeline({
height: '20vh', height: '20vh',
}} }}
> >
<Status skeleton /> <Status skeleton mediaFirst={mediaFirst} />
</li> </li>
<li <li
style={{ style={{
height: '25vh', height: '25vh',
}} }}
> >
<Status skeleton /> <Status skeleton mediaFirst={mediaFirst} />
</li> </li>
</> </>
))} ))}
@ -490,7 +502,7 @@ function Timeline({
/> />
) : ( ) : (
<li key={i}> <li key={i}>
<Status skeleton /> <Status skeleton mediaFirst={mediaFirst} />
</li> </li>
), ),
)} )}
@ -525,6 +537,7 @@ const TimelineItem = memo(
view, view,
showFollowedTags, showFollowedTags,
showReplyParent, showReplyParent,
mediaFirst,
}) => { }) => {
console.debug('RENDER TimelineItem', status.id); console.debug('RENDER TimelineItem', status.id);
const { id: statusID, reblog, items, type, _pinned } = status; const { id: statusID, reblog, items, type, _pinned } = status;
@ -533,6 +546,7 @@ const TimelineItem = memo(
const url = instance const url = instance
? `/${instance}/s/${actualStatusID}` ? `/${instance}/s/${actualStatusID}`
: `/s/${actualStatusID}`; : `/s/${actualStatusID}`;
if (items) { if (items) {
const fItems = filteredItems(items, filterContext); const fItems = filteredItems(items, filterContext);
let title = ''; let title = '';
@ -585,6 +599,7 @@ const TimelineItem = memo(
contentTextWeight contentTextWeight
enableCommentHint enableCommentHint
// allowFilters={allowFilters} // allowFilters={allowFilters}
mediaFirst={mediaFirst}
/> />
) : ( ) : (
<Status <Status
@ -594,6 +609,7 @@ const TimelineItem = memo(
contentTextWeight contentTextWeight
enableCommentHint enableCommentHint
// allowFilters={allowFilters} // allowFilters={allowFilters}
mediaFirst={mediaFirst}
/> />
)} )}
</Link> </Link>
@ -630,7 +646,11 @@ const TimelineItem = memo(
> >
<Link class="status-link timeline-item" to={url}> <Link class="status-link timeline-item" to={url}>
{showCompact ? ( {showCompact ? (
<TimelineStatusCompact status={item} instance={instance} /> <TimelineStatusCompact
status={item}
instance={instance}
filterContext={filterContext}
/>
) : useItemID ? ( ) : useItemID ? (
<Status <Status
statusID={statusID} statusID={statusID}
@ -689,6 +709,7 @@ const TimelineItem = memo(
showFollowedTags={showFollowedTags} showFollowedTags={showFollowedTags}
showReplyParent={showReplyParent} showReplyParent={showReplyParent}
// allowFilters={allowFilters} // allowFilters={allowFilters}
mediaFirst={mediaFirst}
/> />
) : ( ) : (
<Status <Status
@ -698,6 +719,7 @@ const TimelineItem = memo(
showFollowedTags={showFollowedTags} showFollowedTags={showFollowedTags}
showReplyParent={showReplyParent} showReplyParent={showReplyParent}
// allowFilters={allowFilters} // allowFilters={allowFilters}
mediaFirst={mediaFirst}
/> />
)} )}
</Link> </Link>
@ -802,11 +824,12 @@ function StatusCarousel({ title, class: className, children }) {
); );
} }
function TimelineStatusCompact({ status, instance }) { function TimelineStatusCompact({ status, instance, filterContext }) {
const snapStates = useSnapshot(states); const snapStates = useSnapshot(states);
const { id, visibility, language } = status; const { id, visibility, language } = status;
const statusPeekText = statusPeek(status); const statusPeekText = statusPeek(status);
const sKey = statusKey(id, instance); const sKey = statusKey(id, instance);
const filterInfo = isFiltered(status.filtered, filterContext);
return ( return (
<article <article
class={`status compact-thread ${ class={`status compact-thread ${
@ -832,13 +855,24 @@ function TimelineStatusCompact({ status, instance }) {
lang={language} lang={language}
dir="auto" dir="auto"
> >
{statusPeekText} {!!filterInfo ? (
{status.sensitive && status.spoilerText && ( <b
class="status-filtered-badge badge-meta horizontal"
title={filterInfo?.titlesStr || ''}
>
<span>Filtered</span>: <span>{filterInfo?.titlesStr || ''}</span>
</b>
) : (
<> <>
{' '} {statusPeekText}
<span class="spoiler-badge"> {status.sensitive && status.spoilerText && (
<Icon icon="eye-close" size="s" /> <>
</span> {' '}
<span class="spoiler-badge">
<Icon icon="eye-close" size="s" />
</span>
</>
)}
</> </>
)} )}
</div> </div>

Wyświetl plik

@ -3,15 +3,12 @@ import './index.css';
import './app.css'; import './app.css';
import { render } from 'preact'; import { render } from 'preact';
import { lazy } from 'preact/compat';
import { useEffect, useState } from 'preact/hooks'; import { useEffect, useState } from 'preact/hooks';
import IntlSegmenterSuspense from './components/intl-segmenter-suspense'; import ComposeSuspense from './components/compose-suspense';
// import Compose from './components/compose'; import { initStates } from './utils/states';
import useTitle from './utils/useTitle'; import useTitle from './utils/useTitle';
const Compose = lazy(() => import('./components/compose'));
if (window.opener) { if (window.opener) {
console = window.opener.console; console = window.opener.console;
} }
@ -31,6 +28,10 @@ function App() {
: 'Compose', : 'Compose',
); );
useEffect(() => {
initStates();
}, []);
useEffect(() => { useEffect(() => {
if (uiState === 'closed') { if (uiState === 'closed') {
try { try {
@ -61,25 +62,23 @@ function App() {
console.debug('OPEN COMPOSE'); console.debug('OPEN COMPOSE');
return ( return (
<IntlSegmenterSuspense> <ComposeSuspense
<Compose editStatus={editStatus}
editStatus={editStatus} replyToStatus={replyToStatus}
replyToStatus={replyToStatus} draftStatus={draftStatus}
draftStatus={draftStatus} standalone
standalone hasOpener={window.opener}
hasOpener={window.opener} onClose={(results) => {
onClose={(results) => { const { newStatus, fn = () => {} } = results || {};
const { newStatus, fn = () => {} } = results || {}; try {
try { if (newStatus) {
if (newStatus) { window.opener.__STATES__.reloadStatusPage++;
window.opener.__STATES__.reloadStatusPage++; }
} fn();
fn(); setUIState('closed');
setUIState('closed'); } catch (e) {}
} catch (e) {} }}
}} />
/>
</IntlSegmenterSuspense>
); );
} }

Wyświetl plik

@ -547,3 +547,9 @@ kbd {
.shazam-container-horizontal[hidden] { .shazam-container-horizontal[hidden] {
grid-template-columns: 0fr; grid-template-columns: 0fr;
} }
@keyframes spin {
to {
transform: rotate(360deg);
}
}

Wyświetl plik

@ -21,6 +21,7 @@ import pmem from '../utils/pmem';
import showToast from '../utils/show-toast'; import showToast from '../utils/show-toast';
import states from '../utils/states'; import states from '../utils/states';
import { saveStatus } from '../utils/states'; import { saveStatus } from '../utils/states';
import { isMediaFirstInstance } from '../utils/store-utils';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
const LIMIT = 20; const LIMIT = 20;
@ -68,6 +69,8 @@ function AccountStatuses() {
searchOffsetRef.current = 0; searchOffsetRef.current = 0;
}, allSearchParams); }, allSearchParams);
const mediaFirst = useMemo(() => isMediaFirstInstance(), []);
const sameCurrentInstance = useMemo( const sameCurrentInstance = useMemo(
() => instance === currentInstance, () => instance === currentInstance,
[instance, currentInstance], [instance, currentInstance],
@ -151,7 +154,7 @@ function AccountStatuses() {
} }
} }
const results = []; let results = [];
if (firstLoad) { if (firstLoad) {
const { value } = await masto.v1.accounts const { value } = await masto.v1.accounts
.$select(id) .$select(id)
@ -186,12 +189,32 @@ function AccountStatuses() {
limit: LIMIT, limit: LIMIT,
exclude_replies: excludeReplies, exclude_replies: excludeReplies,
exclude_reblogs: excludeBoosts, exclude_reblogs: excludeBoosts,
only_media: media, only_media: media || undefined,
tagged, tagged,
}); });
} }
const { value, done } = await accountStatusesIterator.current.next(); const { value, done } = await accountStatusesIterator.current.next();
if (value?.length) { if (value?.length) {
// Check if value is same as pinned post (results)
// If the index for every post is the same, means API might not support pinned posts
if (results.length) {
let pinnedStatusesIds = [];
if (results[0]?.type === 'pinned') {
pinnedStatusesIds = results[0].id;
} else {
pinnedStatusesIds = results
.filter((status) => status._pinned)
.map((status) => status.id);
}
const containsAllPinned = pinnedStatusesIds.every((postId) =>
value.some((status) => status.id === postId),
);
if (containsAllPinned) {
// Remove pinned posts
results = [];
}
}
results.push(...value); results.push(...value);
value.forEach((item) => { value.forEach((item) => {
@ -250,17 +273,21 @@ function AccountStatuses() {
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
try { // No need, because the whole filter bar is hidden
const featuredTags = await masto.v1.accounts // TODO: Revisit this
.$select(id) if (!mediaFirst) {
.featuredTags.list(); try {
console.log({ featuredTags }); const featuredTags = await masto.v1.accounts
setFeaturedTags(featuredTags); .$select(id)
} catch (e) { .featuredTags.list();
console.error(e); console.log({ featuredTags });
setFeaturedTags(featuredTags);
} catch (e) {
console.error(e);
}
} }
})(); })();
}, [id]); }, [id, mediaFirst]);
const { displayName, acct, emojis } = account || {}; const { displayName, acct, emojis } = account || {};
@ -279,95 +306,126 @@ function AccountStatuses() {
authenticated={authenticated} authenticated={authenticated}
standalone standalone
/> />
<div {!mediaFirst && (
class="filter-bar" <div
ref={filterBarRef} class="filter-bar"
style={{ ref={filterBarRef}
position: 'relative', style={{
}} position: 'relative',
> }}
{filtered ? ( >
{filtered ? (
<Link
to={`/${instance}/a/${id}`}
class="insignificant filter-clear"
title="Clear filters"
key="clear-filters"
>
<Icon icon="x" size="l" />
</Link>
) : (
<Icon icon="filter" class="insignificant" size="l" />
)}
<Link <Link
to={`/${instance}/a/${id}`} to={`/${instance}/a/${id}${excludeReplies ? '?replies=1' : ''}`}
class="insignificant filter-clear"
title="Clear filters"
key="clear-filters"
>
<Icon icon="x" size="l" />
</Link>
) : (
<Icon icon="filter" class="insignificant" size="l" />
)}
<Link
to={`/${instance}/a/${id}${excludeReplies ? '?replies=1' : ''}`}
onClick={() => {
if (excludeReplies) {
showToast('Showing post with replies');
}
}}
class={excludeReplies ? '' : 'is-active'}
>
+ Replies
</Link>
<Link
to={`/${instance}/a/${id}${excludeBoosts ? '' : '?boosts=0'}`}
onClick={() => {
if (!excludeBoosts) {
showToast('Showing posts without boosts');
}
}}
class={!excludeBoosts ? '' : 'is-active'}
>
- Boosts
</Link>
<Link
to={`/${instance}/a/${id}${media ? '' : '?media=1'}`}
onClick={() => {
if (!media) {
showToast('Showing posts with media');
}
}}
class={media ? 'is-active' : ''}
>
Media
</Link>
{featuredTags.map((tag) => (
<Link
key={tag.id}
to={`/${instance}/a/${id}${
tagged === tag.name
? ''
: `?tagged=${encodeURIComponent(tag.name)}`
}`}
onClick={() => { onClick={() => {
if (tagged !== tag.name) { if (excludeReplies) {
showToast(`Showing posts tagged with #${tag.name}`); showToast('Showing post with replies');
} }
}} }}
class={tagged === tag.name ? 'is-active' : ''} class={excludeReplies ? '' : 'is-active'}
> >
<span> + Replies
<span class="more-insignificant">#</span>
{tag.name}
</span>
{
// The count differs based on instance 😅
}
{/* <span class="filter-count">{tag.statusesCount}</span> */}
</Link> </Link>
))} <Link
{searchEnabled && to={`/${instance}/a/${id}${excludeBoosts ? '' : '?boosts=0'}`}
(supportsInputMonth ? ( onClick={() => {
<label class={`filter-field ${month ? 'is-active' : ''}`}> if (!excludeBoosts) {
<Icon icon="month" size="l" /> showToast('Showing posts without boosts');
<input }
type="month" }}
class={!excludeBoosts ? '' : 'is-active'}
>
- Boosts
</Link>
<Link
to={`/${instance}/a/${id}${media ? '' : '?media=1'}`}
onClick={() => {
if (!media) {
showToast('Showing posts with media');
}
}}
class={media ? 'is-active' : ''}
>
Media
</Link>
{featuredTags.map((tag) => (
<Link
key={tag.id}
to={`/${instance}/a/${id}${
tagged === tag.name
? ''
: `?tagged=${encodeURIComponent(tag.name)}`
}`}
onClick={() => {
if (tagged !== tag.name) {
showToast(`Showing posts tagged with #${tag.name}`);
}
}}
class={tagged === tag.name ? 'is-active' : ''}
>
<span>
<span class="more-insignificant">#</span>
{tag.name}
</span>
{
// The count differs based on instance 😅
}
{/* <span class="filter-count">{tag.statusesCount}</span> */}
</Link>
))}
{searchEnabled &&
(supportsInputMonth ? (
<label class={`filter-field ${month ? 'is-active' : ''}`}>
<Icon icon="month" size="l" />
<input
type="month"
disabled={!account?.acct}
value={month || ''}
min={MIN_YEAR_MONTH}
max={new Date().toISOString().slice(0, 7)}
onInput={(e) => {
const { value, validity } = e.currentTarget;
if (!validity.valid) return;
setSearchParams(
value
? {
month: value,
}
: {},
);
const [year, month] = value.split('-');
const monthIndex = parseInt(month, 10) - 1;
const date = new Date(year, monthIndex);
showToast(
`Showing posts in ${date.toLocaleString('default', {
month: 'long',
year: 'numeric',
})}`,
);
}}
/>
</label>
) : (
// Fallback to <select> for month and <input type="number"> for year
<MonthPicker
class={`filter-field ${month ? 'is-active' : ''}`}
disabled={!account?.acct} disabled={!account?.acct}
value={month || ''} value={month || ''}
min={MIN_YEAR_MONTH} min={MIN_YEAR_MONTH}
max={new Date().toISOString().slice(0, 7)} max={new Date().toISOString().slice(0, 7)}
onInput={(e) => { onInput={(e) => {
const { value, validity } = e.currentTarget; const { value, validity } = e;
if (!validity.valid) return; if (!validity.valid) return;
setSearchParams( setSearchParams(
value value
@ -376,40 +434,11 @@ function AccountStatuses() {
} }
: {}, : {},
); );
const [year, month] = value.split('-');
const monthIndex = parseInt(month, 10) - 1;
const date = new Date(year, monthIndex);
showToast(
`Showing posts in ${date.toLocaleString('default', {
month: 'long',
year: 'numeric',
})}`,
);
}} }}
/> />
</label> ))}
) : ( </div>
// Fallback to <select> for month and <input type="number"> for year )}
<MonthPicker
class={`filter-field ${month ? 'is-active' : ''}`}
disabled={!account?.acct}
value={month || ''}
min={MIN_YEAR_MONTH}
max={new Date().toISOString().slice(0, 7)}
onInput={(e) => {
const { value, validity } = e;
if (!validity.valid) return;
setSearchParams(
value
? {
month: value,
}
: {},
);
}}
/>
))}
</div>
</> </>
); );
}, [ }, [
@ -472,7 +501,7 @@ function AccountStatuses() {
errorText="Unable to load posts" errorText="Unable to load posts"
fetchItems={fetchAccountStatuses} fetchItems={fetchAccountStatuses}
useItemID useItemID
view={media ? 'media' : undefined} view={media || mediaFirst ? 'media' : undefined}
boostsCarousel={snapStates.settings.boostsCarousel} boostsCarousel={snapStates.settings.boostsCarousel}
timelineStart={TimelineStart} timelineStart={TimelineStart}
refresh={[ refresh={[

Wyświetl plik

@ -13,12 +13,13 @@ import NameText from '../components/name-text';
import { api } from '../utils/api'; import { api } from '../utils/api';
import states from '../utils/states'; import states from '../utils/states';
import store from '../utils/store'; import store from '../utils/store';
import { getCurrentAccountID, setCurrentAccountID } from '../utils/store-utils';
function Accounts({ onClose }) { function Accounts({ onClose }) {
const { masto } = api(); const { masto } = api();
// Accounts // Accounts
const accounts = store.local.getJSON('accounts'); const accounts = store.local.getJSON('accounts');
const currentAccount = store.session.get('currentAccount'); const currentAccount = getCurrentAccountID();
const moreThanOneAccount = accounts.length > 1; const moreThanOneAccount = accounts.length > 1;
const [_, reload] = useReducer((x) => x + 1, 0); const [_, reload] = useReducer((x) => x + 1, 0);
@ -81,7 +82,7 @@ function Accounts({ onClose }) {
if (isCurrent) { if (isCurrent) {
states.showAccount = `${account.info.username}@${account.instanceURL}`; states.showAccount = `${account.info.username}@${account.instanceURL}`;
} else { } else {
store.session.set('currentAccount', account.info.id); setCurrentAccountID(account.info.id);
location.reload(); location.reload();
} }
}} }}

Wyświetl plik

@ -614,7 +614,7 @@
} }
&.visibility-direct { &.visibility-direct {
--yellow-stripes: repeating-linear-gradient( --yellow-stripes: repeating-linear-gradient(
-45deg, 135deg,
var(--reply-to-faded-color), var(--reply-to-faded-color),
var(--reply-to-faded-color) 10px, var(--reply-to-faded-color) 10px,
var(--reply-to-faded-color) 10px, var(--reply-to-faded-color) 10px,

Wyświetl plik

@ -40,7 +40,7 @@ import showToast from '../utils/show-toast';
import states, { statusKey } from '../utils/states'; import states, { statusKey } from '../utils/states';
import statusPeek from '../utils/status-peek'; import statusPeek from '../utils/status-peek';
import store from '../utils/store'; import store from '../utils/store';
import { getCurrentAccountNS } from '../utils/store-utils'; import { getCurrentAccountID, getCurrentAccountNS } from '../utils/store-utils';
import { assignFollowedTags } from '../utils/timeline-utils'; import { assignFollowedTags } from '../utils/timeline-utils';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
@ -112,7 +112,7 @@ function Catchup() {
const [showTopLinks, setShowTopLinks] = useState(false); const [showTopLinks, setShowTopLinks] = useState(false);
const currentAccount = useMemo(() => { const currentAccount = useMemo(() => {
return store.session.get('currentAccount'); return getCurrentAccountID();
}, []); }, []);
const isSelf = (accountID) => accountID === currentAccount; const isSelf = (accountID) => accountID === currentAccount;
@ -1677,63 +1677,70 @@ function PostPeek({ post, filterInfo }) {
} = post; } = post;
const isThread = const isThread =
(inReplyToId && inReplyToAccountId === account.id) || !!_thread; (inReplyToId && inReplyToAccountId === account.id) || !!_thread;
const showMedia = !spoilerText && !sensitive;
const readingExpandSpoilers = useMemo(() => {
const prefs = store.account.get('preferences') || {};
return !!prefs['reading:expand:spoilers'];
}, []);
// const readingExpandSpoilers = true;
const showMedia = readingExpandSpoilers || (!spoilerText && !sensitive);
const postText = content ? statusPeek(post) : ''; const postText = content ? statusPeek(post) : '';
const showPostContent = !spoilerText || readingExpandSpoilers;
return ( return (
<div class="post-peek" title={!spoilerText ? postText : ''}> <div class="post-peek" title={!spoilerText ? postText : ''}>
<span class="post-peek-content"> <span class="post-peek-content">
{isThread && !showPostContent && (
<>
<span class="post-peek-tag post-peek-thread">Thread</span>{' '}
</>
)}
{!!filterInfo ? ( {!!filterInfo ? (
<> <span class="post-peek-filtered">
{isThread && ( Filtered{filterInfo?.titlesStr ? `: ${filterInfo.titlesStr}` : ''}
<> </span>
<span class="post-peek-tag post-peek-thread">Thread</span>{' '}
</>
)}
<span class="post-peek-filtered">
Filtered{filterInfo?.titlesStr ? `: ${filterInfo.titlesStr}` : ''}
</span>
</>
) : !!spoilerText ? (
<>
{isThread && (
<>
<span class="post-peek-tag post-peek-thread">Thread</span>{' '}
</>
)}
<span class="post-peek-spoiler">
<Icon icon="eye-close" /> {spoilerText}
</span>
</>
) : ( ) : (
<div class="post-peek-html"> <>
{isThread && ( {!!spoilerText && (
<> <span class="post-peek-spoiler">
<span class="post-peek-tag post-peek-thread">Thread</span>{' '} <Icon
</> icon={`${readingExpandSpoilers ? 'eye-open' : 'eye-close'}`}
/>{' '}
{spoilerText}
</span>
)} )}
{!!content && ( {showPostContent && (
<div <div class="post-peek-html">
dangerouslySetInnerHTML={{ {isThread && (
__html: emojifyText(content, emojis), <>
}} <span class="post-peek-tag post-peek-thread">Thread</span>{' '}
/> </>
)}
{!!content && (
<div
dangerouslySetInnerHTML={{
__html: emojifyText(content, emojis),
}}
/>
)}
{!!poll?.options?.length &&
poll.options.map((o) => (
<div>
{poll.multiple ? '▪️' : '•'} {o.title}
</div>
))}
{!content &&
mediaAttachments?.length === 1 &&
mediaAttachments[0].description && (
<>
<span class="post-peek-tag post-peek-alt">ALT</span>{' '}
<div>{mediaAttachments[0].description}</div>
</>
)}
</div>
)} )}
{!!poll?.options?.length && </>
poll.options.map((o) => (
<div>
{poll.multiple ? '▪️' : '•'} {o.title}
</div>
))}
{!content &&
mediaAttachments?.length === 1 &&
mediaAttachments[0].description && (
<>
<span class="post-peek-tag post-peek-alt">ALT</span>{' '}
<div>{mediaAttachments[0].description}</div>
</>
)}
</div>
)} )}
</span> </span>
{!filterInfo && ( {!filterInfo && (

Wyświetl plik

@ -286,7 +286,13 @@ function FiltersAddEdit({ filter, onClose }) {
// Preserve existing expiry if not specified // Preserve existing expiry if not specified
// Seconds from now to expiresAtDate // Seconds from now to expiresAtDate
// Other clients don't do this // 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) { } else if (expiresIn === '0' || expiresIn === 0) {
// 0 = Never // 0 = Never
expiresIn = null; expiresIn = null;

Wyświetl plik

@ -71,7 +71,8 @@ function Following({ title, path, id, ...props }) {
.next(); .next();
let { value } = results; let { value } = results;
console.log('checkForUpdates', latestItem.current, value); console.log('checkForUpdates', latestItem.current, value);
if (value?.length) { const valueContainsLatestItem = value[0]?.id === latestItem.current; // since_id might not be supported
if (value?.length && !valueContainsLatestItem) {
latestItem.current = value[0].id; latestItem.current = value[0].id;
value = dedupeBoosts(value, instance); value = dedupeBoosts(value, instance);
value = filteredItems(value, 'home'); value = filteredItems(value, 'home');

Wyświetl plik

@ -5,7 +5,7 @@ import {
MenuHeader, MenuHeader,
MenuItem, MenuItem,
} from '@szhsin/react-menu'; } from '@szhsin/react-menu';
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import Icon from '../components/icon'; import Icon from '../components/icon';
@ -18,6 +18,7 @@ import { filteredItems } from '../utils/filters';
import showToast from '../utils/show-toast'; import showToast from '../utils/show-toast';
import states from '../utils/states'; import states from '../utils/states';
import { saveStatus } from '../utils/states'; import { saveStatus } from '../utils/states';
import { isMediaFirstInstance } from '../utils/store-utils';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
const LIMIT = 20; const LIMIT = 20;
@ -55,6 +56,8 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
useTitle(title, `/:instance?/t/:hashtag`); useTitle(title, `/:instance?/t/:hashtag`);
const latestItem = useRef(); const latestItem = useRef();
const mediaFirst = useMemo(() => isMediaFirstInstance(), []);
// const hashtagsIterator = useRef(); // const hashtagsIterator = useRef();
const maxID = useRef(undefined); const maxID = useRef(undefined);
async function fetchHashtags(firstLoad) { async function fetchHashtags(firstLoad) {
@ -73,7 +76,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
limit: LIMIT, limit: LIMIT,
any: hashtags.slice(1), any: hashtags.slice(1),
maxId: firstLoad ? undefined : maxID.current, maxId: firstLoad ? undefined : maxID.current,
onlyMedia: media, onlyMedia: media ? true : undefined,
}) })
.next(); .next();
let { value } = results; let { value } = results;
@ -85,7 +88,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
// value = filteredItems(value, 'public'); // value = filteredItems(value, 'public');
value.forEach((item) => { value.forEach((item) => {
saveStatus(item, instance, { saveStatus(item, instance, {
skipThreading: media, // If media view, no need to form threads skipThreading: media || mediaFirst, // If media view, no need to form threads
}); });
}); });
@ -109,8 +112,9 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
}) })
.next(); .next();
let { value } = results; let { value } = results;
value = filteredItems(value, 'public'); const valueContainsLatestItem = value[0]?.id === latestItem.current; // since_id might not be supported
if (value?.length) { if (value?.length && !valueContainsLatestItem) {
value = filteredItems(value, 'public');
return true; return true;
} }
return false; return false;
@ -155,7 +159,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
fetchItems={fetchHashtags} fetchItems={fetchHashtags}
checkForUpdates={checkForUpdates} checkForUpdates={checkForUpdates}
useItemID useItemID
view={media ? 'media' : undefined} view={media || mediaFirst ? 'media' : undefined}
refresh={media} refresh={media}
// allowFilters // allowFilters
filterContext="public" filterContext="public"
@ -232,23 +236,27 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
<MenuDivider /> <MenuDivider />
</> </>
)} )}
<MenuHeader className="plain">Filters</MenuHeader> {!mediaFirst && (
<MenuItem <>
type="checkbox" <MenuHeader className="plain">Filters</MenuHeader>
checked={!!media} <MenuItem
onClick={() => { type="checkbox"
if (media) { checked={!!media}
searchParams.delete('media'); onClick={() => {
} else { if (media) {
searchParams.set('media', '1'); searchParams.delete('media');
} } else {
setSearchParams(searchParams); searchParams.set('media', '1');
}} }
> setSearchParams(searchParams);
<Icon icon="check-circle" />{' '} }}
<span class="menu-grow">Media only</span> >
</MenuItem> <Icon icon="check-circle" />{' '}
<MenuDivider /> <span class="menu-grow">Media only</span>
</MenuItem>
<MenuDivider />
</>
)}
<FocusableItem className="menu-field" disabled={reachLimit}> <FocusableItem className="menu-field" disabled={reachLimit}>
{({ ref }) => ( {({ ref }) => (
<form <form

Wyświetl plik

@ -84,7 +84,7 @@ function NotificationsLink() {
); );
} }
const NOTIFICATIONS_LIMIT = 30; const NOTIFICATIONS_LIMIT = 80;
const NOTIFICATIONS_DISPLAY_LIMIT = 5; const NOTIFICATIONS_DISPLAY_LIMIT = 5;
function NotificationsMenu({ anchorRef, state, onClose }) { function NotificationsMenu({ anchorRef, state, onClose }) {
const { masto, instance } = api(); const { masto, instance } = api();

Wyświetl plik

@ -63,8 +63,9 @@ function List(props) {
since_id: latestItem.current, since_id: latestItem.current,
}); });
let { value } = results; let { value } = results;
value = filteredItems(value, 'home'); const valueContainsLatestItem = value[0]?.id === latestItem.current; // since_id might not be supported
if (value?.length) { if (value?.length && !valueContainsLatestItem) {
value = filteredItems(value, 'home');
return true; return true;
} }
return false; return false;

Wyświetl plik

@ -1,5 +1,6 @@
import './login.css'; import './login.css';
import Fuse from 'fuse.js';
import { useEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useRef, useState } from 'preact/hooks';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
@ -27,12 +28,14 @@ function Login() {
); );
const [instancesList, setInstancesList] = useState([]); const [instancesList, setInstancesList] = useState([]);
const searcher = useRef();
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
const res = await fetch(instancesListURL); const res = await fetch(instancesListURL);
const data = await res.json(); const data = await res.json();
setInstancesList(data); setInstancesList(data);
searcher.current = new Fuse(data);
} catch (e) { } catch (e) {
// Silently fail // Silently fail
console.error(e); console.error(e);
@ -90,21 +93,11 @@ function Login() {
!/[\s\/\\@]/.test(cleanInstanceText); !/[\s\/\\@]/.test(cleanInstanceText);
const instancesSuggestions = cleanInstanceText const instancesSuggestions = cleanInstanceText
? instancesList ? searcher.current
.filter((instance) => instance.includes(instanceText)) ?.search(cleanInstanceText, {
.sort((a, b) => { limit: 10,
// 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;
}) })
.slice(0, 10) ?.map((match) => match.item)
: []; : [];
const selectedInstanceText = instanceTextLooksLikeDomain const selectedInstanceText = instanceTextLooksLikeDomain

Wyświetl plik

@ -4,6 +4,7 @@ import { useSearchParams } from 'react-router-dom';
import Link from '../components/link'; import Link from '../components/link';
import Timeline from '../components/timeline'; import Timeline from '../components/timeline';
import { api } from '../utils/api'; import { api } from '../utils/api';
import { fixNotifications } from '../utils/group-notifications';
import { saveStatus } from '../utils/states'; import { saveStatus } from '../utils/states';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
@ -30,6 +31,8 @@ function Mentions({ columnMode, ...props }) {
const results = await mentionsIterator.current.next(); const results = await mentionsIterator.current.next();
let { value } = results; let { value } = results;
if (value?.length) { if (value?.length) {
value = fixNotifications(value);
if (firstLoad) { if (firstLoad) {
latestItem.current = value[0].id; latestItem.current = value[0].id;
console.log('First load', latestItem.current); console.log('First load', latestItem.current);
@ -95,7 +98,9 @@ function Mentions({ columnMode, ...props }) {
latestConversationItem.current, latestConversationItem.current,
value, value,
); );
if (value?.length) { const valueContainsLatestItem =
value[0]?.id === latestConversationItem.current; // since_id might not be supported
if (value?.length && !valueContainsLatestItem) {
latestConversationItem.current = value[0].lastStatus.id; latestConversationItem.current = value[0].lastStatus.id;
return true; return true;
} }

Wyświetl plik

@ -33,7 +33,7 @@ import usePageVisibility from '../utils/usePageVisibility';
import useScroll from '../utils/useScroll'; import useScroll from '../utils/useScroll';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
const LIMIT = 30; // 30 is the maximum limit :( const LIMIT = 80;
const emptySearchParams = new URLSearchParams(); const emptySearchParams = new URLSearchParams();
const scrollIntoViewOptions = { const scrollIntoViewOptions = {
@ -72,6 +72,13 @@ function Notifications({ columnMode }) {
excludeTypes: ['follow_request'], excludeTypes: ['follow_request'],
}); });
} }
if (/max_id=($|&)/i.test(notificationsIterator.current?.nextParams)) {
// Pixelfed returns next paginationed link with empty max_id
// I assume, it's done (end of list)
return {
done: true,
};
}
const allNotifications = await notificationsIterator.current.next(); const allNotifications = await notificationsIterator.current.next();
const notifications = allNotifications.value; const notifications = allNotifications.value;
@ -82,6 +89,32 @@ function Notifications({ columnMode }) {
}); });
}); });
// TEST: Slot in a fake notification to test 'severed_relationships'
// notifications.unshift({
// id: '123123',
// type: 'severed_relationships',
// createdAt: '2024-03-22T19:20:08.316Z',
// event: {
// type: 'account_suspension',
// targetName: 'mastodon.dev',
// followersCount: 0,
// followingCount: 0,
// },
// });
// 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); const groupedNotifications = groupNotifications(notifications);
if (firstLoad) { if (firstLoad) {
@ -247,7 +280,6 @@ function Notifications({ columnMode }) {
const lastHiddenTime = useRef(); const lastHiddenTime = useRef();
usePageVisibility((visible) => { usePageVisibility((visible) => {
let unsub;
if (visible) { if (visible) {
const timeDiff = Date.now() - lastHiddenTime.current; const timeDiff = Date.now() - lastHiddenTime.current;
if (!lastHiddenTime.current || timeDiff > 1000 * 3) { if (!lastHiddenTime.current || timeDiff > 1000 * 3) {
@ -258,20 +290,16 @@ function Notifications({ columnMode }) {
} else { } else {
lastHiddenTime.current = Date.now(); lastHiddenTime.current = Date.now();
} }
unsub = subscribeKey(states, 'notificationsShowNew', (v) => {
if (uiState === 'loading') {
return;
}
if (v) {
loadUpdates();
}
setShowNew(v);
});
} }
return () => {
unsub?.();
};
}); });
useEffect(() => {
let unsub = subscribeKey(states, 'notificationsShowNew', (v) => {
if (uiState === 'loading') return;
if (v) loadUpdates();
setShowNew(v);
});
return () => unsub?.();
}, []);
const todayDate = new Date(); const todayDate = new Date();
const yesterdayDate = new Date(todayDate - 24 * 60 * 60 * 1000); const yesterdayDate = new Date(todayDate - 24 * 60 * 60 * 1000);
@ -418,7 +446,7 @@ function Notifications({ columnMode }) {
{supportsFilteredNotifications && ( {supportsFilteredNotifications && (
<button <button
type="button" type="button"
class="button plain" class="button plain4"
onClick={() => { onClick={() => {
setShowNotificationsSettings(true); setShowNotificationsSettings(true);
}} }}
@ -613,7 +641,7 @@ function Notifications({ columnMode }) {
</label> </label>
</div> </div>
<h2 class="timeline-header">Today</h2> <h2 class="timeline-header">Today</h2>
{showTodayEmpty && !!snapStates.notifications.length && ( {showTodayEmpty && (
<p class="ui-state insignificant"> <p class="ui-state insignificant">
{uiState === 'default' ? "You're all caught up." : <>&hellip;</>} {uiState === 'default' ? "You're all caught up." : <>&hellip;</>}
</p> </p>

Wyświetl plik

@ -33,6 +33,7 @@ function Public({ local, columnMode, ...props }) {
publicIterator.current = masto.v1.timelines.public.list({ publicIterator.current = masto.v1.timelines.public.list({
limit: LIMIT, limit: LIMIT,
local: isLocal, local: isLocal,
remote: !isLocal, // Pixelfed
}); });
} }
const results = await publicIterator.current.next(); const results = await publicIterator.current.next();
@ -63,8 +64,9 @@ function Public({ local, columnMode, ...props }) {
}) })
.next(); .next();
let { value } = results; let { value } = results;
value = filteredItems(value, 'public'); const valueContainsLatestItem = value[0]?.id === latestItem.current; // since_id might not be supported
if (value?.length) { if (value?.length && !valueContainsLatestItem) {
value = filteredItems(value, 'public');
return true; return true;
} }
return false; return false;

Wyświetl plik

@ -23,12 +23,6 @@
} }
} }
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.hero-heading { .hero-heading {
font-size: var(--text-size); font-size: var(--text-size);
display: inline-block; display: inline-block;

Wyświetl plik

@ -153,6 +153,18 @@ function StatusPage(params) {
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [showMediaOnly]); }, [showMediaOnly]);
useEffect(() => {
const $deckContainers = document.querySelectorAll('.deck-container');
$deckContainers.forEach(($deckContainer) => {
$deckContainer.setAttribute('inert', '');
});
return () => {
$deckContainers.forEach(($deckContainer) => {
$deckContainer.removeAttribute('inert');
});
};
}, []);
return ( return (
<div class="deck-backdrop"> <div class="deck-backdrop">
{showMedia ? ( {showMedia ? (
@ -972,6 +984,18 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
[statuses, limit, renderStatus], [statuses, limit, renderStatus],
); );
// If there's spoiler in hero status, auto-expand it
useEffect(() => {
let timer = setTimeout(() => {
if (!heroStatusRef.current) return;
const spoilerButton = heroStatusRef.current.querySelector(
'.spoiler-button:not(.spoiling), .spoiler-media-button:not(.spoiling)',
);
if (spoilerButton) spoilerButton.click();
}, 1000);
return () => clearTimeout(timer);
}, [id]);
return ( return (
<div <div
tabIndex="-1" tabIndex="-1"

Wyświetl plik

@ -19,6 +19,7 @@ import pmem from '../utils/pmem';
import shortenNumber from '../utils/shorten-number'; import shortenNumber from '../utils/shorten-number';
import states from '../utils/states'; import states from '../utils/states';
import { saveStatus } from '../utils/states'; import { saveStatus } from '../utils/states';
import supports from '../utils/supports';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
const LIMIT = 20; const LIMIT = 20;
@ -33,6 +34,17 @@ const fetchLinks = pmem(
}, },
); );
function fetchTrends(masto) {
if (supports('@pixelfed/trending')) {
return masto.pixelfed.v2.discover.posts.trending.list({
range: 'daily',
});
}
return masto.v1.trends.statuses.list({
limit: LIMIT,
});
}
function Trending({ columnMode, ...props }) { function Trending({ columnMode, ...props }) {
const snapStates = useSnapshot(states); const snapStates = useSnapshot(states);
const params = columnMode ? {} : useParams(); const params = columnMode ? {} : useParams();
@ -48,36 +60,39 @@ function Trending({ columnMode, ...props }) {
const [hashtags, setHashtags] = useState([]); const [hashtags, setHashtags] = useState([]);
const [links, setLinks] = useState([]); const [links, setLinks] = useState([]);
const trendIterator = useRef(); const trendIterator = useRef();
async function fetchTrend(firstLoad) { async function fetchTrend(firstLoad) {
if (firstLoad || !trendIterator.current) { if (firstLoad || !trendIterator.current) {
trendIterator.current = masto.v1.trends.statuses.list({ trendIterator.current = fetchTrends(masto);
limit: LIMIT,
});
// Get hashtags // Get hashtags
try { if (supports('@mastodon/trending-hashtags')) {
const iterator = masto.v1.trends.tags.list(); try {
const { value: tags } = await iterator.next(); const iterator = masto.v1.trends.tags.list();
console.log('tags', tags); const { value: tags } = await iterator.next();
if (tags?.length) { console.log('tags', tags);
setHashtags(tags); if (tags?.length) {
setHashtags(tags);
}
} catch (e) {
console.error(e);
} }
} catch (e) {
console.error(e);
} }
// Get links // Get links
try { if (supports('@mastodon/trending-links')) {
const { value } = await fetchLinks(masto, instance); try {
// 4 types available: link, photo, video, rich const { value } = await fetchLinks(masto, instance);
// Only want links for now // 4 types available: link, photo, video, rich
const links = value?.filter?.((link) => link.type === 'link'); // Only want links for now
console.log('links', links); const links = value?.filter?.((link) => link.type === 'link');
if (links?.length) { console.log('links', links);
setLinks(links); if (links?.length) {
setLinks(links);
}
} catch (e) {
console.error(e);
} }
} catch (e) {
console.error(e);
} }
} }
const results = await trendIterator.current.next(); const results = await trendIterator.current.next();

Wyświetl plik

@ -7,6 +7,7 @@ import {
getAccountByInstance, getAccountByInstance,
getCurrentAccount, getCurrentAccount,
saveAccount, saveAccount,
setCurrentAccountID,
} from './store-utils'; } from './store-utils';
// Default *fallback* instance // Default *fallback* instance
@ -118,7 +119,7 @@ export async function initAccount(client, instance, accessToken, vapidKey) {
const mastoAccount = await masto.v1.accounts.verifyCredentials(); const mastoAccount = await masto.v1.accounts.verifyCredentials();
console.log('CURRENTACCOUNT SET', mastoAccount.id); console.log('CURRENTACCOUNT SET', mastoAccount.id);
store.session.set('currentAccount', mastoAccount.id); setCurrentAccountID(mastoAccount.id);
saveAccount({ saveAccount({
info: mastoAccount, info: mastoAccount,

Wyświetl plik

@ -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) { if (postEnhanceDOM) {
queueMicrotask(() => postEnhanceDOM(dom)); queueMicrotask(() => postEnhanceDOM(dom));
// postEnhanceDOM(dom); // mutate dom // postEnhanceDOM(dom); // mutate dom

Wyświetl plik

@ -1,5 +1,5 @@
import mem from './mem'; import mem from './mem';
import store from './store'; import { getCurrentAccountID } from './store-utils';
function _isFiltered(filtered, filterContext) { function _isFiltered(filtered, filterContext) {
if (!filtered?.length) return false; if (!filtered?.length) return false;
@ -43,7 +43,7 @@ export function filteredItem(item, filterContext, currentAccountID) {
export function filteredItems(items, filterContext) { export function filteredItems(items, filterContext) {
if (!items?.length) return []; if (!items?.length) return [];
if (!filterContext) return items; if (!filterContext) return items;
const currentAccountID = store.session.get('currentAccount'); const currentAccountID = getCurrentAccountID();
return items.filter((item) => return items.filter((item) =>
filteredItem(item, filterContext, currentAccountID), filteredItem(item, filterContext, currentAccountID),
); );

Wyświetl plik

@ -6,6 +6,7 @@ const statusPostRegexes = [
/\/notes\/([^\/]+)/i, // Misskey, Firefish /\/notes\/([^\/]+)/i, // Misskey, Firefish
/^\/(?:notice|objects)\/([a-z0-9-]+)/i, // Pleroma /^\/(?:notice|objects)\/([a-z0-9-]+)/i, // Pleroma
/\/@[^@\/]+@?[^\/]+?\/([^\/]+)/i, // Mastodon /\/@[^@\/]+@?[^\/]+?\/([^\/]+)/i, // Mastodon
/^\/p\/[^\/]+\/([^\/]+)/i, // Pixelfed
]; ];
export function getInstanceStatusObject(url) { export function getInstanceStatusObject(url) {

Wyświetl plik

@ -9,7 +9,7 @@ const notificationTypeKeys = {
poll: ['status'], poll: ['status'],
update: ['status'], update: ['status'],
}; };
function fixNotifications(notifications) { export function fixNotifications(notifications) {
return notifications.filter((notification) => { return notifications.filter((notification) => {
const { type, id, createdAt } = notification; const { type, id, createdAt } = notification;
if (!type) { if (!type) {

Wyświetl plik

@ -1,11 +1,11 @@
import { api } from './api'; import { api } from './api';
import store from './store'; import { getCurrentAccountID } from './store-utils';
export async function fetchRelationships(accounts, relationshipsMap = {}) { export async function fetchRelationships(accounts, relationshipsMap = {}) {
if (!accounts?.length) return; if (!accounts?.length) return;
const { masto } = api(); const { masto } = api();
const currentAccount = store.session.get('currentAccount'); const currentAccount = getCurrentAccountID();
const uniqueAccountIds = accounts.reduce((acc, a) => { const uniqueAccountIds = accounts.reduce((acc, a) => {
// 1. Ignore duplicate accounts // 1. Ignore duplicate accounts
// 2. Ignore accounts that are already inside relationshipsMap // 2. Ignore accounts that are already inside relationshipsMap

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,27 @@
import openOSK from './open-osk';
import showToast from './show-toast';
import states from './states';
const TOAST_DURATION = 5_000; // 5 seconds
export default function showCompose(opts) {
if (!opts) opts = true;
if (states.showCompose) {
if (states.composerState.minimized) {
showToast({
duration: TOAST_DURATION,
text: `A draft post is currently minimized. Post or discard it before creating a new one.`,
});
} else {
showToast({
duration: TOAST_DURATION,
text: `A post is currently open. Post or discard it before creating a new one.`,
});
}
return;
}
openOSK();
states.showCompose = opts;
}

Wyświetl plik

@ -40,6 +40,7 @@ const states = proxy({
statusReply: {}, statusReply: {},
accounts: {}, accounts: {},
routeNotification: null, routeNotification: null,
composerState: {},
// Modals // Modals
showCompose: false, showCompose: false,
showSettings: false, showSettings: false,

Wyświetl plik

@ -16,13 +16,40 @@ export function getAccountByInstance(instance) {
return accounts.find((a) => a.instanceURL === instance); return accounts.find((a) => a.instanceURL === instance);
} }
const standaloneMQ = window.matchMedia('(display-mode: standalone)');
export function getCurrentAccountID() {
try {
const id = store.session.get('currentAccount');
if (id) return id;
} catch (e) {}
if (standaloneMQ.matches) {
try {
const id = store.local.get('currentAccount');
if (id) return id;
} catch (e) {}
}
return null;
}
export function setCurrentAccountID(id) {
try {
store.session.set('currentAccount', id);
} catch (e) {}
if (standaloneMQ.matches) {
try {
store.local.set('currentAccount', id);
} catch (e) {}
}
}
export function getCurrentAccount() { export function getCurrentAccount() {
if (!window.__IGNORE_GET_ACCOUNT_ERROR__) { if (!window.__IGNORE_GET_ACCOUNT_ERROR__) {
// Track down getCurrentAccount() calls before account-based states are initialized // Track down getCurrentAccount() calls before account-based states are initialized
console.error('getCurrentAccount() called before states are initialized'); console.error('getCurrentAccount() called before states are initialized');
if (import.meta.env.DEV) console.trace(); if (import.meta.env.DEV) console.trace();
} }
const currentAccount = store.session.get('currentAccount'); const currentAccount = getCurrentAccountID();
const account = getAccount(currentAccount); const account = getAccount(currentAccount);
return account; return account;
} }
@ -48,7 +75,7 @@ export function saveAccount(account) {
accounts.push(account); accounts.push(account);
} }
store.local.setJSON('accounts', accounts); store.local.setJSON('accounts', accounts);
store.session.set('currentAccount', account.info.id); setCurrentAccountID(account.info.id);
} }
export function updateAccount(accountInfo) { export function updateAccount(accountInfo) {
@ -80,10 +107,10 @@ export function getCurrentInstance() {
return (currentInstance = instances[instance]); return (currentInstance = instances[instance]);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
alert(`Failed to load instance configuration. Please try again.\n\n${e}`); // alert(`Failed to load instance configuration. Please try again.\n\n${e}`);
// Temporary fix for corrupted data // Temporary fix for corrupted data
store.local.del('instances'); // store.local.del('instances');
location.reload(); // location.reload();
return {}; return {};
} }
} }
@ -126,3 +153,8 @@ export function getCurrentInstanceConfiguration() {
const instance = getCurrentInstance(); const instance = getCurrentInstance();
return getInstanceConfiguration(instance); return getInstanceConfiguration(instance);
} }
export function isMediaFirstInstance() {
const instance = getCurrentInstance();
return /pixelfed/i.test(instance?.version);
}

Wyświetl plik

@ -4,6 +4,21 @@ import features from '../data/features.json';
import { getCurrentInstance } from './store-utils'; import { getCurrentInstance } from './store-utils';
// Non-semver(?) UA string detection
const containPixelfed = /pixelfed/i;
const notContainPixelfed = /^(?!.*pixelfed).*$/i;
const platformFeatures = {
'@mastodon/lists': notContainPixelfed,
'@mastodon/filters': notContainPixelfed,
'@mastodon/mentions': notContainPixelfed,
'@mastodon/trending-hashtags': notContainPixelfed,
'@mastodon/trending-links': notContainPixelfed,
'@mastodon/post-bookmark': notContainPixelfed,
'@mastodon/post-edit': notContainPixelfed,
'@mastodon/profile-edit': notContainPixelfed,
'@mastodon/profile-private-note': notContainPixelfed,
'@pixelfed/trending': containPixelfed,
};
const supportsCache = {}; const supportsCache = {};
function supports(feature) { function supports(feature) {
@ -11,6 +26,11 @@ function supports(feature) {
const { version, domain } = getCurrentInstance(); const { version, domain } = getCurrentInstance();
const key = `${domain}-${feature}`; const key = `${domain}-${feature}`;
if (supportsCache[key]) return supportsCache[key]; if (supportsCache[key]) return supportsCache[key];
if (platformFeatures[feature]) {
return (supportsCache[key] = platformFeatures[feature].test(version));
}
const range = features[feature]; const range = features[feature];
if (!range) return false; if (!range) return false;
return (supportsCache[key] = satisfies(version, range, { return (supportsCache[key] = satisfies(version, range, {

Wyświetl plik

@ -9,6 +9,20 @@ export const throttle = pThrottle({
interval: 1000, interval: 1000,
}); });
const STATUS_ID_REGEXES = [
/\/@[^@\/]+@?[^\/]+?\/(\d+)$/i, // Mastodon
/\/notice\/(\w+)$/i, // Pleroma
];
function getStatusID(path) {
for (let i = 0; i < STATUS_ID_REGEXES.length; i++) {
const statusMatchID = path.match(STATUS_ID_REGEXES[i])?.[1];
if (statusMatchID) {
return statusMatchID;
}
}
return null;
}
const denylistDomains = /(twitter|github)\.com/i; const denylistDomains = /(twitter|github)\.com/i;
const failedUnfurls = {}; const failedUnfurls = {};
function _unfurlMastodonLink(instance, url) { function _unfurlMastodonLink(instance, url) {
@ -53,11 +67,11 @@ function _unfurlMastodonLink(instance, url) {
} }
const domain = urlObj.hostname; const domain = urlObj.hostname;
const path = urlObj.pathname; const path = urlObj.pathname;
// Regex /:username/:id, where username = @username or @username@domain, id = number // Regex /:username/:id, where username = @username or @username@domain, id = post ID
const statusRegex = /\/@([^@\/]+)@?([^\/]+)?\/(\d+)$/i; let statusMatchID = getStatusID(path);
const statusMatch = statusRegex.exec(path);
if (statusMatch) { if (statusMatchID) {
const id = statusMatch[3]; const id = statusMatchID;
const { masto } = api({ instance: domain }); const { masto } = api({ instance: domain });
remoteInstanceFetch = masto.v1.statuses remoteInstanceFetch = masto.v1.statuses
.$select(id) .$select(id)