From ad2bebec0e6de835756936f5f2473c0120168f18 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 20 Dec 2022 00:11:55 +0800 Subject: [PATCH 01/38] Bump notifications limit --- src/pages/notifications.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/notifications.jsx b/src/pages/notifications.jsx index 2820e8f4..23e5d772 100644 --- a/src/pages/notifications.jsx +++ b/src/pages/notifications.jsx @@ -41,7 +41,7 @@ const contentText = { update: 'A status you interacted with has been edited.', }; -const LIMIT = 20; +const LIMIT = 30; // 30 is the maximum limit :( function Notification({ notification }) { const { id, type, status, account, _accounts } = notification; From c116db79cc232dc6c55a8f91d3e7775b830fff24 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 20 Dec 2022 00:16:45 +0800 Subject: [PATCH 02/38] Have fun with welcome page --- src/pages/welcome.css | 57 ++++++++++++++++++++++--------------------- src/pages/welcome.jsx | 35 ++++++++++++++------------ 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/src/pages/welcome.css b/src/pages/welcome.css index 4bd7a456..578178bd 100644 --- a/src/pages/welcome.css +++ b/src/pages/welcome.css @@ -1,39 +1,40 @@ #welcome { text-align: center; + background-image: radial-gradient( + closest-side at 50% 50%, + var(--bg-faded-color), + transparent + ); } -#welcome img { - margin-top: 16px; - height: auto; +#welcome h1 { + margin: 0; + padding: 0; + font-size: 1.2em; } -@keyframes dance { +#welcome img { + vertical-align: top; +} + +#welcome h2 { + font-size: 3em; + letter-spacing: -0.05ex; + margin: 16px 0; + padding: 0; + /* gradiented text */ + background: linear-gradient(45deg, var(--purple-color), var(--red-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +@keyframes psychedelic { 0% { - transform: rotate(0deg); - } - 20% { - transform: rotate(5deg); - } - 40% { - transform: rotate(-5deg); - } - 60% { - transform: rotate(5deg); - } - 80% { - transform: rotate(-5deg); + filter: hue-rotate(0deg); } 100% { - transform: rotate(0deg); + filter: hue-rotate(360deg); } } -#welcome:hover img { - animation: dance 2s infinite 15s linear; +#welcome:hover h2 { + animation: psychedelic 60s infinite; } - -#welcome .warning { - font-weight: bold; - padding: 16px; - background: lemonchiffon; - color: chocolate; - border-radius: 16px; -} \ No newline at end of file diff --git a/src/pages/welcome.jsx b/src/pages/welcome.jsx index f9940a98..4ba7e7c2 100644 --- a/src/pages/welcome.jsx +++ b/src/pages/welcome.jsx @@ -6,22 +6,25 @@ import useTitle from '../utils/useTitle'; function Welcome() { useTitle(); return ( -
- -

Welcome

-

Phanpy is a minimalistic opinionated Mastodon web client.

-

- 🚧 This is an early ALPHA project. Many features are missing, many bugs - are present. Please report issues as detailed as possible. Thanks πŸ™ -

+
+

+ {' '} + Phanpy +

+

+ Trunk-tastic +
+ Mastodon Experience +

+

A minimalistic opinionated Mastodon web client.

From 6397b2d67ba43add8d4a4dc5f3d0ee459f566975 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 20 Dec 2022 00:32:45 +0800 Subject: [PATCH 03/38] viewport-fit=cover is needed for safe area CSS to work --- index.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index ae280bd9..3581003a 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,10 @@ - + Phanpy Date: Tue, 20 Dec 2022 09:22:26 +0800 Subject: [PATCH 04/38] Make everything inside buttons un-pointer-able Gets annoying when getting e.target from interacting with buttons --- src/index.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.css b/src/index.css index d7fe7760..ba505d82 100644 --- a/src/index.css +++ b/src/index.css @@ -118,8 +118,9 @@ button, vertical-align: middle; text-decoration: none; } -button > * { +:is(button, .button) > * { vertical-align: middle; + pointer-events: none; } :is(button, .button):not(:disabled, .disabled):hover { cursor: pointer; From 7d7473da15a6c5e7f73a5a87387d501e71b5cc8c Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 20 Dec 2022 09:27:59 +0800 Subject: [PATCH 05/38] Possible quick fix for menu popovers not working on iOS --- src/app.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.css b/src/app.css index a725080d..3c22ba42 100644 --- a/src/app.css +++ b/src/app.css @@ -638,7 +638,7 @@ button.carousel-dot[disabled].active { padding: 0; list-style: none; } -.menu-container > button:is(:active, :focus) + menu, +.menu-container > button:is(:hover, :active, :focus) + menu, .menu-container menu:is(:hover, :active) { opacity: 1; pointer-events: auto; From 6561f14d8b130043c3d0d43feba0f63fac4aa965 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 20 Dec 2022 09:28:12 +0800 Subject: [PATCH 06/38] Menu popover need a little soft shadows --- src/app.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app.css b/src/app.css index 3c22ba42..e4ebba4b 100644 --- a/src/app.css +++ b/src/app.css @@ -632,6 +632,8 @@ button.carousel-dot[disabled].active { border: 1px solid var(--outline-color); border-radius: 8px; transition: all 0.2s ease-in-out; + box-shadow: 0 0 8px var(--bg-faded-color), 0 4px 8px var(--bg-faded-color), + 0 2px 4px var(--bg-faded-color); } .menu-container menu li { margin: 0; From bf907abc17188127dc1e56cc88ab013ecdd689db Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 20 Dec 2022 09:37:29 +0800 Subject: [PATCH 07/38] Disable this small font sizing --- src/components/status.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/status.css b/src/components/status.css index 226b1701..e87a8cd7 100644 --- a/src/components/status.css +++ b/src/components/status.css @@ -81,7 +81,7 @@ padding-top: 8px; } .status.small { - font-size: 95%; + /* font-size: 95%; */ } .status.skeleton { color: var(--outline-color); From 1538400dc0fa744d4942becf4788de02f5f2dc57 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 20 Dec 2022 10:09:05 +0800 Subject: [PATCH 08/38] Oops, forgot to put confirm dialog before boosting --- src/components/status.jsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/status.jsx b/src/components/status.jsx index 23b76b57..0bdd79c1 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -455,6 +455,14 @@ function Status({ count={reblogsCount} onClick={async () => { try { + if (!reblogged) { + const yes = confirm( + 'Are you sure that you want to boost this post?', + ); + if (!yes) { + return; + } + } // Optimistic states.statuses.set(id, { ...status, From bbccb8a79b76de94ea162e0211d9fd98eeb2a0a6 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 20 Dec 2022 13:21:08 +0800 Subject: [PATCH 09/38] Only cache avatars and emojis No point caching all the images for a week Also they take up A LOT of space --- public/sw.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/sw.js b/public/sw.js index 329386cd..b44db070 100644 --- a/public/sw.js +++ b/public/sw.js @@ -5,12 +5,17 @@ import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies'; const imageRoute = new Route( ({ request, sameOrigin }) => { - return !sameOrigin && request.destination === 'image'; + const isRemote = !sameOrigin; + const isImage = request.destination === 'image'; + const isAvatar = request.url.includes('/avatars/'); + const isEmoji = request.url.includes('/emoji/'); + return isRemote && isImage && (isAvatar || isEmoji); }, new CacheFirst({ cacheName: 'remote-images', plugins: [ new ExpirationPlugin({ + maxEntries: 100, maxAgeSeconds: 7 * 24 * 60 * 60, // 7 days purgeOnQuotaError: true, }), From bbec6f2de9b197d25a073211d2e5d94b967f0e84 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 20 Dec 2022 13:21:53 +0800 Subject: [PATCH 10/38] Fix small-height videos too small When it's too short, the native video player UI is cramped --- src/app.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app.css b/src/app.css index e4ebba4b..daa1b9cb 100644 --- a/src/app.css +++ b/src/app.css @@ -463,6 +463,9 @@ a.mention span { max-height: 100vh; max-height: 100dvh; } +.carousel > * video { + min-height: 80px; +} .carousel-top-controls { top: 0; From 3921f8a6f98e11dd61d5a49b000b9ce284c3528e Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 20 Dec 2022 13:24:56 +0800 Subject: [PATCH 11/38] Debugging --- src/components/compose.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/compose.jsx b/src/components/compose.jsx index aca29fde..7b501fe6 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -554,6 +554,7 @@ function Compose({ // Alert all the reasons results.forEach((result) => { if (result.status === 'rejected') { + console.error(result); alert(result.reason || `Attachment #${i} failed`); } }); From e2749503248e9df67229d2a4608a17455454e58d Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 20 Dec 2022 13:26:45 +0800 Subject: [PATCH 12/38] Disable popping-in/out and closing when loading --- src/components/compose.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/compose.jsx b/src/components/compose.jsx index 7b501fe6..73cf046b 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -342,6 +342,7 @@ function Compose({ {' '} -

+

From f6e3c979af25a75489138bd71cc8f365e1362a52 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 21 Dec 2022 01:18:37 +0800 Subject: [PATCH 30/38] Fix onClick not a function --- src/components/status.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/status.jsx b/src/components/status.jsx index c0909a7b..7001db05 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -630,7 +630,7 @@ video = Video clip audio = Audio track */ -function Media({ media, showOriginal, onClick }) { +function Media({ media, showOriginal, onClick = () => {} }) { const { blurhash, description, meta, previewUrl, remoteUrl, url, type } = media; const { original, small, focus } = meta || {}; From 35894385562d9e1f89c5ee1360022a13ed9d6514 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 21 Dec 2022 07:42:48 +0800 Subject: [PATCH 31/38] Fix clash of styles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .status got overriden πŸ˜‚ (maybe I should scope the CSS or something) --- src/pages/notifications.css | 8 ++++---- src/pages/notifications.jsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/notifications.css b/src/pages/notifications.css index 9b2c4179..199f272e 100644 --- a/src/pages/notifications.css +++ b/src/pages/notifications.css @@ -17,14 +17,14 @@ opacity: 0.75; color: var(--text-insignificant-color); } -.notification-type.favourite { +.notification-type.notification-favourite { color: var(--favourite-color); } -.notification-type.reblog { +.notification-type.notification-reblog { color: var(--reblog-color); } -.notification-type.poll, -.notification-type.mention { +.notification-type.notification-poll, +.notification-type.notification-mention { color: var(--link-color); } diff --git a/src/pages/notifications.jsx b/src/pages/notifications.jsx index 23e5d772..e58f332f 100644 --- a/src/pages/notifications.jsx +++ b/src/pages/notifications.jsx @@ -61,7 +61,7 @@ function Notification({ notification }) { return ( <>

-

- {!/poll|update/i.test(type) && ( - <> - {_accounts?.length > 1 ? ( - <> - {_accounts.length} people{' '} - - ) : ( - <> - {' '} - - )} - - )} - {text} - {type === 'mention' && ( - - {' '} - β€’{' '} - - - )} -

+ {type !== 'mention' && ( +

+ {!/poll|update/i.test(type) && ( + <> + {_accounts?.length > 1 ? ( + <> + {_accounts.length} people{' '} + + ) : ( + <> + {' '} + + )} + + )} + {text} + {type === 'mention' && ( + + {' '} + β€’{' '} + + + )} +

+ )} {_accounts?.length > 1 && (

{_accounts.map((account, i) => ( @@ -142,7 +144,10 @@ function Notification({ notification }) {

)} {status && ( - + )} From 237ceae356a32846b37a7b8b595847f0768395c1 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 21 Dec 2022 08:54:39 +0800 Subject: [PATCH 33/38] Visual indicator that it tries to get new updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Probably too subtle. Loader also only appears after 1s delay πŸ˜† --- src/pages/home.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/home.jsx b/src/pages/home.jsx index 4c910dd6..8edd4bfa 100644 --- a/src/pages/home.jsx +++ b/src/pages/home.jsx @@ -82,6 +82,7 @@ function Home({ hidden }) { const diffMins = Math.round(diff / 1000 / 60); if (diffMins > 1) { console.log('visible', { lastHidden, diffMins }); + setUIState('loading'); setTimeout(() => { (async () => { const newStatus = await masto.timelines.fetchHome({ @@ -91,6 +92,7 @@ function Home({ hidden }) { if (newStatus.length && newStatus[0].id !== states.home[0].id) { states.homeNew = newStatus; } + setUIState('default'); })(); // loadStatuses(true); // states.homeNew = []; @@ -101,6 +103,7 @@ function Home({ hidden }) { document.addEventListener('visibilitychange', handleVisibilityChange); return () => { document.removeEventListener('visibilitychange', handleVisibilityChange); + setUIState('default'); }; }, []); From 3b6f0f277e19642173e754d1050890c721a90278 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 21 Dec 2022 18:02:13 +0800 Subject: [PATCH 34/38] Rewrite whole scroll logic for Status page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handle 3 cases, all written down in comments. Crossing my fingers 🀞🀞🀞 --- package-lock.json | 11 ++ package.json | 1 + src/app.css | 9 +- src/pages/status.jsx | 238 +++++++++++++++++++++++++++---------------- src/utils/states.js | 1 + 5 files changed, 166 insertions(+), 94 deletions(-) diff --git a/package-lock.json b/package-lock.json index 975b0ed9..63db590f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "fast-blurhash": "~1.1.2", "history": "~5.3.0", "iconify-icon": "~1.0.2", + "just-debounce-it": "^3.2.0", "masto": "~4.10.1", "mem": "~9.0.2", "preact": "~10.11.3", @@ -4046,6 +4047,11 @@ "node": ">=0.10.0" } }, + "node_modules/just-debounce-it": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/just-debounce-it/-/just-debounce-it-3.2.0.tgz", + "integrity": "sha512-WXzwLL0745uNuedrCsCs3rpmfD6DBaf7uuVwaq98/8dafURfgQaBsSpjiPp5+CW6Vjltwy9cOGI6qE71b3T8iQ==" + }, "node_modules/kolorist": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.6.0.tgz", @@ -8562,6 +8568,11 @@ "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", "dev": true }, + "just-debounce-it": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/just-debounce-it/-/just-debounce-it-3.2.0.tgz", + "integrity": "sha512-WXzwLL0745uNuedrCsCs3rpmfD6DBaf7uuVwaq98/8dafURfgQaBsSpjiPp5+CW6Vjltwy9cOGI6qE71b3T8iQ==" + }, "kolorist": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.6.0.tgz", diff --git a/package.json b/package.json index f0d1bb19..191757ff 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "fast-blurhash": "~1.1.2", "history": "~5.3.0", "iconify-icon": "~1.0.2", + "just-debounce-it": "^3.2.0", "masto": "~4.10.1", "mem": "~9.0.2", "preact": "~10.11.3", diff --git a/src/app.css b/src/app.css index 4e7cd710..bb889de9 100644 --- a/src/app.css +++ b/src/app.css @@ -137,6 +137,7 @@ a.mention span { transparent ); background-repeat: no-repeat; + transition: opacity 0.3s ease-in-out; } .timeline.contextual > li:first-child { background-position: 0 16px; @@ -274,13 +275,13 @@ a.mention span { left: calc(50px + 16px + 16px); } .timeline.contextual.loading > li:not(.hero) { - opacity: 0.2; + opacity: 0.5; pointer-events: none; - background-image: none !important; + /* background-image: none !important; */ } -.timeline.contextual.loading > li:not(.hero):before { +/* .timeline.contextual.loading > li:not(.hero):before { content: none !important; -} +} */ .timeline-deck.compact .status { max-height: max(25vh, 160px); diff --git a/src/pages/status.jsx b/src/pages/status.jsx index 71026c19..7d6f4dd8 100644 --- a/src/pages/status.jsx +++ b/src/pages/status.jsx @@ -1,3 +1,4 @@ +import debounce from 'just-debounce-it'; import { Link } from 'preact-router/match'; import { useEffect, @@ -18,117 +19,163 @@ import useTitle from '../utils/useTitle'; function StatusPage({ id }) { const snapStates = useSnapshot(states); - const cachedStatuses = store.session.getJSON('statuses-' + id); - const [statuses, setStatuses] = useState(cachedStatuses || [{ id }]); + const [statuses, setStatuses] = useState([]); const [uiState, setUIState] = useState('default'); + const userInitiated = useRef(true); // Initial open is user-initiated const heroStatusRef = useRef(); + const scrollableRef = useRef(); useEffect(() => { + const onScroll = debounce(() => { + // console.log('onScroll'); + const { scrollTop } = scrollableRef.current; + states.scrollPositions.set(id, scrollTop); + }, 100); + scrollableRef.current.addEventListener('scroll', onScroll, { + passive: true, + }); + onScroll(); + return () => { + scrollableRef.current?.removeEventListener('scroll', onScroll); + }; + }, [id]); + + useEffect(() => { + setUIState('loading'); + const containsStatus = statuses.find((s) => s.id === id); if (!containsStatus) { + // Case 1: On first load, or when navigating to a status that's not cached at all setStatuses([{ id }]); } else { const cachedStatuses = store.session.getJSON('statuses-' + id); if (cachedStatuses) { - setStatuses(cachedStatuses); + // Case 2: Looks like we've cached this status before, let's restore them to make it snappy + const reallyCachedStatuses = cachedStatuses.filter( + (s) => snapStates.statuses.has(s.id), + // Some are not cached in the global state, so we need to filter them out + ); + setStatuses(reallyCachedStatuses); + } else { + // Case 3: Unknown state, could be a sub-comment. Let's slice off all descendant statuses after the hero status to be safe because they are custom-rendered with sub-comments etc + const heroIndex = statuses.findIndex((s) => s.id === id); + const slicedStatuses = statuses.slice(0, heroIndex + 1); + setStatuses(slicedStatuses); } } - }, [id]); - useEffect(async () => { - setUIState('loading'); - - const hasStatus = snapStates.statuses.has(id); - let heroStatus = snapStates.statuses.get(id); - try { - heroStatus = await masto.statuses.fetch(id); - states.statuses.set(id, heroStatus); - } catch (e) { - // Silent fail if status is cached - if (!hasStatus) { - setUIState('error'); - alert('Error fetching status'); - } - return; - } - - try { - const context = await masto.statuses.fetchContext(id); - const { ancestors, descendants } = context; - - ancestors.forEach((status) => { - states.statuses.set(status.id, status); - }); - const nestedDescendants = []; - descendants.forEach((status) => { - states.statuses.set(status.id, status); - if (status.inReplyToAccountId === status.account.id) { - // If replying to self, it's part of the thread, level 1 - nestedDescendants.push(status); - } else if (status.inReplyToId === heroStatus.id) { - // If replying to the hero status, it's a reply, level 1 - nestedDescendants.push(status); - } else { - // If replying to someone else, it's a reply to a reply, level 2 - const parent = descendants.find((s) => s.id === status.inReplyToId); - if (parent) { - if (!parent.__replies) { - parent.__replies = []; - } - parent.__replies.push(status); - } else { - // If no parent, it's probably a reply to a reply to a reply, level 3 - console.warn('[LEVEL 3] No parent found for', status); - } + (async () => { + const hasStatus = snapStates.statuses.has(id); + let heroStatus = snapStates.statuses.get(id); + try { + heroStatus = await masto.statuses.fetch(id); + states.statuses.set(id, heroStatus); + } catch (e) { + // Silent fail if status is cached + if (!hasStatus) { + setUIState('error'); + alert('Error fetching status'); } - }); + return; + } - console.log({ ancestors, descendants, nestedDescendants }); + try { + const context = await masto.statuses.fetchContext(id); + const { ancestors, descendants } = context; - const allStatuses = [ - ...ancestors.map((s) => ({ - id: s.id, - ancestor: true, - accountID: s.account.id, - })), - { id, accountID: heroStatus.account.id }, - ...nestedDescendants.map((s) => ({ - id: s.id, - accountID: s.account.id, - descendant: true, - thread: s.account.id === heroStatus.account.id, - replies: s.__replies?.map((r) => r.id), - })), - ]; - console.log({ allStatuses }); - setStatuses(allStatuses); - store.session.setJSON('statuses-' + id, allStatuses); - } catch (e) { - console.error(e); - setUIState('error'); - } + ancestors.forEach((status) => { + states.statuses.set(status.id, status); + }); + const nestedDescendants = []; + descendants.forEach((status) => { + states.statuses.set(status.id, status); + if (status.inReplyToAccountId === status.account.id) { + // If replying to self, it's part of the thread, level 1 + nestedDescendants.push(status); + } else if (status.inReplyToId === heroStatus.id) { + // If replying to the hero status, it's a reply, level 1 + nestedDescendants.push(status); + } else { + // If replying to someone else, it's a reply to a reply, level 2 + const parent = descendants.find((s) => s.id === status.inReplyToId); + if (parent) { + if (!parent.__replies) { + parent.__replies = []; + } + parent.__replies.push(status); + } else { + // If no parent, it's probably a reply to a reply to a reply, level 3 + console.warn('[LEVEL 3] No parent found for', status); + } + } + }); - setUIState('default'); + console.log({ ancestors, descendants, nestedDescendants }); + + const allStatuses = [ + ...ancestors.map((s) => ({ + id: s.id, + ancestor: true, + accountID: s.account.id, + })), + { id, accountID: heroStatus.account.id }, + ...nestedDescendants.map((s) => ({ + id: s.id, + accountID: s.account.id, + descendant: true, + thread: s.account.id === heroStatus.account.id, + replies: s.__replies?.map((r) => r.id), + })), + ]; + + setUIState('default'); + console.log({ allStatuses }); + setStatuses(allStatuses); + store.session.setJSON('statuses-' + id, allStatuses); + } catch (e) { + console.error(e); + setUIState('error'); + } + })(); }, [id, snapStates.reloadStatusPage]); useLayoutEffect(() => { - if (heroStatusRef.current && statuses.length > 1) { - heroStatusRef.current.scrollIntoView({ - behavior: 'smooth', - block: 'start', - }); + if (!statuses.length) return; + const isLoading = uiState === 'loading'; + if (userInitiated.current) { + const hasAncestors = statuses.findIndex((s) => s.id === id) > 0; // Cannot use `ancestor` key because the hero state is dynamic + if (!isLoading && hasAncestors) { + // Case 1: User initiated, has ancestors, after statuses are loaded, SNAP to hero status + console.log('Case 1'); + heroStatusRef.current?.scrollIntoView(); + } else if (isLoading && statuses.length > 1) { + // Case 2: User initiated, while statuses are loading, SMOOTH-SCROLL to hero status + console.log('Case 2'); + heroStatusRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + } + } else { + const scrollPosition = states.scrollPositions.get(id); + if (scrollPosition && scrollableRef.current) { + // Case 3: Not user initiated (e.g. back/forward button), restore to saved scroll position + console.log('Case 3'); + scrollableRef.current.scrollTop = scrollPosition; + } } - }, [id]); + console.log('No case', { + isLoading, + userInitiated: userInitiated.current, + statusesLength: statuses.length, + // scrollPosition, + }); - useLayoutEffect(() => { - const hasAncestor = statuses.some((s) => s.ancestor); - if (hasAncestor) { - heroStatusRef.current?.scrollIntoView({ - // behavior: 'smooth', - block: 'start', - }); + if (!isLoading) { + // Reset user initiated flag after statuses are loaded + userInitiated.current = false; } - }, [statuses]); + }, [statuses, uiState]); const heroStatus = snapStates.statuses.get(id); const heroDisplayName = useMemo(() => { @@ -175,11 +222,13 @@ function StatusPage({ id }) { }, [statuses.length, limit]); const hasManyStatuses = statuses.length > 40; + const hasDescendants = statuses.some((s) => s.descendant); return (
1 ? 'padded-bottom' : '' }`} @@ -228,6 +277,9 @@ function StatusPage({ id }) { status-link " href={`#/s/${statusID}`} + onClick={() => { + userInitiated.current = true; + }} > {replies.map((replyID) => (
  • - + { + userInitiated.current = true; + }} + >
  • @@ -258,7 +316,7 @@ function StatusPage({ id }) { {uiState === 'loading' && isHero && !!heroStatus?.repliesCount && - statuses.length === 1 && ( + !hasDescendants && (
    diff --git a/src/utils/states.js b/src/utils/states.js index 613e52ba..cc3f6b3a 100644 --- a/src/utils/states.js +++ b/src/utils/states.js @@ -13,6 +13,7 @@ export default proxy({ accounts: new Map(), reloadStatusPage: 0, spoilers: proxyMap([]), + scrollPositions: new Map(), // Modals showCompose: false, showSettings: false, From 23745d0683194288be5bc1205a859c51dcb25c5f Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 21 Dec 2022 19:29:37 +0800 Subject: [PATCH 35/38] Update poll reactively --- src/components/status.jsx | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/status.jsx b/src/components/status.jsx index 7001db05..28f75ced 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -359,7 +359,15 @@ function Status({ }), }} /> - {!!poll && } + {!!poll && ( + { + states.statuses.get(id).poll = newPoll; + }} + /> + )} {!spoilerText && sensitive && !!mediaAttachments.length && ( {' '} + •{' '} + + )} {shortenNumber(votersCount)}{' '} {votersCount === 1 ? 'voter' : 'voters'} {votersCount !== votesCount && ( diff --git a/src/index.css b/src/index.css index 286ebafc..b799879b 100644 --- a/src/index.css +++ b/src/index.css @@ -157,6 +157,15 @@ button, padding: 12px; } +:is(button, .button).textual { + padding: 0; + margin: 0; + vertical-align: baseline; + color: var(--link-color); + background-color: transparent; + border-radius: 0; +} + input[type='text'], textarea, select { From eba78e3f07598ee0bf34756a134301ef5a58239b Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 21 Dec 2022 20:00:45 +0800 Subject: [PATCH 37/38] Time to embrace sheets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .box class is just… weird… --- src/app.css | 25 ------------------------- src/components/status.jsx | 6 +++--- src/pages/settings.css | 13 ++++--------- src/pages/settings.jsx | 6 +++--- 4 files changed, 10 insertions(+), 40 deletions(-) diff --git a/src/app.css b/src/app.css index bb889de9..5612910a 100644 --- a/src/app.css +++ b/src/app.css @@ -414,31 +414,6 @@ a.mention span { width: 40em; max-width: 100vw; padding: 16px; - background-color: var(--bg-color); - border-radius: 8px; - border: 1px solid var(--divider-color); - overflow: auto; - max-height: 90vh; - max-height: 90dvh; - position: relative; -} -.box > :is(h1, h2, h3):first-of-type { - margin-top: 0; -} -.box .close-button { - position: sticky; - top: 0; - float: right; - margin: -16px -8px 0 0; - transform: translate(0, -8px); -} - -.box-shadow { - box-shadow: 0px 36px 89px rgb(0 0 0 / 4%), - 0px 23.3333px 52.1227px rgb(0 0 0 / 3%), - 0px 13.8667px 28.3481px rgb(0 0 0 / 2%), 0px 7.2px 14.4625px rgb(0 0 0 / 2%), - 0px 2.93333px 7.25185px rgb(0 0 0 / 2%), - 0px 0.666667px 3.50231px rgb(0 0 0 / 1%); } /* CAROUSEL */ diff --git a/src/components/status.jsx b/src/components/status.jsx index f0afec59..26240078 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -1063,10 +1063,10 @@ function EditedAtModal({ statusID, onClose = () => {} }) { const currentYear = new Date().getFullYear(); return ( -
    - + */}

    Edit History

    {uiState === 'error' &&

    Failed to load history

    } {uiState === 'loading' && ( diff --git a/src/pages/settings.css b/src/pages/settings.css index 2f7812ca..1d11098c 100644 --- a/src/pages/settings.css +++ b/src/pages/settings.css @@ -1,10 +1,5 @@ -#settings-container { - padding-bottom: 3em; - animation: fade-in 0.2s ease-out; -} - #settings-container h2 { - font-size: .9em; + font-size: 0.9em; text-transform: uppercase; color: var(--text-insignificant-color); } @@ -48,7 +43,7 @@ text-align: right; } #settings-container div, -#settings-container div > *{ +#settings-container div > * { vertical-align: middle; } #settings-container .avatar { @@ -63,7 +58,7 @@ overflow: hidden; padding: 1px; } -#settings-container .radio-group input[type="radio"] { +#settings-container .radio-group input[type='radio'] { opacity: 0; position: absolute; pointer-events: none; @@ -87,4 +82,4 @@ } #settings-container .radio-group label:has(input:checked) input:checked + span { color: inherit; -} \ No newline at end of file +} diff --git a/src/pages/settings.jsx b/src/pages/settings.jsx index 925d6a25..90e5f4c7 100644 --- a/src/pages/settings.jsx +++ b/src/pages/settings.jsx @@ -23,10 +23,10 @@ function Settings({ onClose }) { const [currentDefault, setCurrentDefault] = useState(0); return ( -
    - + */}

    Accounts

      {accounts.map((account, i) => { From c828f53f09b868935170be548498513ba2521fbd Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 21 Dec 2022 20:34:24 +0800 Subject: [PATCH 38/38] Lame fix for Flash of Welcome Page (FoWP) useEffect runs after mounted so Welcome component appears for a split second --- src/app.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app.jsx b/src/app.jsx index 53e6c177..27268c59 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -104,7 +104,7 @@ async function startStream() { export function App() { const snapStates = useSnapshot(states); const [isLoggedIn, setIsLoggedIn] = useState(false); - const [uiState, setUIState] = useState('default'); + const [uiState, setUIState] = useState('loading'); useLayoutEffect(() => { const theme = store.local.get('theme'); @@ -194,6 +194,8 @@ export function App() { } setUIState('default'); })(); + } else { + setUIState('default'); } }, []);