From 89f34d7942cb091144896373d4312f2e26eee6c2 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Mon, 26 Feb 2024 11:56:18 +0800 Subject: [PATCH 01/83] Use em, and hide if there's nothing in account "note" --- src/components/account-info.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/account-info.css b/src/components/account-info.css index 3f6cf3cd..8811c652 100644 --- a/src/components/account-info.css +++ b/src/components/account-info.css @@ -9,11 +9,15 @@ --original-color: var(--link-color); .note { - font-size: 95%; + font-size: 0.95em; line-height: 1.4; text-wrap: pretty; margin-bottom: 16px; + &:empty { + display: none; + } + > *:first-child { margin-top: 0; padding-top: 0; From c595b0ee312ef066a7a13544d5f8792abffb2a8a Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Mon, 26 Feb 2024 11:58:22 +0800 Subject: [PATCH 02/83] Fix toasts showing for unauthenticated interactions --- src/components/status.jsx | 133 +++++++++++++++++--------------------- 1 file changed, 59 insertions(+), 74 deletions(-) diff --git a/src/components/status.jsx b/src/components/status.jsx index f97f19bc..1d953b23 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -559,12 +559,11 @@ function Status({ if (reblogged) { const newStatus = await masto.v1.statuses.$select(id).unreblog(); saveStatus(newStatus, instance); - return true; } else { const newStatus = await masto.v1.statuses.$select(id).reblog(); saveStatus(newStatus, instance); - return true; } + return true; } catch (e) { console.error(e); // Revert optimistism @@ -575,7 +574,8 @@ function Status({ const favouriteStatus = async () => { if (!sameInstance || !authenticated) { - return alert(unauthInteractionErrorMessage); + alert(unauthInteractionErrorMessage); + return false; } try { // Optimistic @@ -591,16 +591,31 @@ function Status({ const newStatus = await masto.v1.statuses.$select(id).favourite(); saveStatus(newStatus, instance); } + return true; } catch (e) { console.error(e); // Revert optimistism states.statuses[sKey] = status; + return false; } }; + const favouriteStatusNotify = async () => { + try { + const done = await favouriteStatus(); + if (!isSizeLarge && done) { + showToast( + favourited + ? `Unliked @${username || acct}'s post` + : `Liked @${username || acct}'s post`, + ); + } + } catch (e) {} + }; const bookmarkStatus = async () => { if (!sameInstance || !authenticated) { - return alert(unauthInteractionErrorMessage); + alert(unauthInteractionErrorMessage); + return false; } try { // Optimistic @@ -615,12 +630,26 @@ function Status({ const newStatus = await masto.v1.statuses.$select(id).bookmark(); saveStatus(newStatus, instance); } + return true; } catch (e) { console.error(e); // Revert optimistism states.statuses[sKey] = status; + return false; } }; + const bookmarkStatusNotify = async () => { + try { + const done = await bookmarkStatus(); + if (!isSizeLarge && done) { + showToast( + bookmarked + ? `Unbookmarked @${username || acct}'s post` + : `Bookmarked @${username || acct}'s post`, + ); + } + } catch (e) {} + }; const differentLanguage = !!language && @@ -752,18 +781,7 @@ function Status({ { - try { - favouriteStatus(); - if (!isSizeLarge) { - showToast( - favourited - ? `Unliked @${username || acct}'s post` - : `Liked @${username || acct}'s post`, - ); - } - } catch (e) {} - }} + onClick={favouriteStatusNotify} className={`menu-favourite ${favourited ? 'checked' : ''}`} > @@ -776,18 +794,7 @@ function Status({ { - try { - bookmarkStatus(); - if (!isSizeLarge) { - showToast( - bookmarked - ? `Unbookmarked @${username || acct}'s post` - : `Bookmarked @${username || acct}'s post`, - ); - } - } catch (e) {} - }} + onClick={bookmarkStatusNotify} className={`menu-bookmark ${bookmarked ? 'checked' : ''}`} > @@ -1040,6 +1047,23 @@ function Status({ )} )} + {!isSelf && isSizeLarge && ( + <> + + { + states.showReportModal = { + account: status.account, + post: status, + }; + }} + > + + Report post… + + + )} ); @@ -1085,42 +1109,12 @@ function Status({ const rRef = useHotkeys('r, shift+r', replyStatus, { enabled: hotkeysEnabled, }); - const fRef = useHotkeys( - 'f, l', - () => { - try { - favouriteStatus(); - if (!isSizeLarge) { - showToast( - favourited - ? `Unliked @${username || acct}'s post` - : `Liked @${username || acct}'s post`, - ); - } - } catch (e) {} - }, - { - enabled: hotkeysEnabled, - }, - ); - const dRef = useHotkeys( - 'd', - () => { - try { - bookmarkStatus(); - if (!isSizeLarge) { - showToast( - bookmarked - ? `Unbookmarked @${username || acct}'s post` - : `Bookmarked @${username || acct}'s post`, - ); - } - } catch (e) {} - }, - { - enabled: hotkeysEnabled, - }, - ); + const fRef = useHotkeys('f, l', favouriteStatusNotify, { + enabled: hotkeysEnabled, + }); + const dRef = useHotkeys('d', bookmarkStatusNotify, { + enabled: hotkeysEnabled, + }); const bRef = useHotkeys( 'shift+b', () => { @@ -1420,16 +1414,7 @@ function Status({ icon="heart" iconSize="m" count={favouritesCount} - onClick={() => { - try { - favouriteStatus(); - showToast( - favourited - ? `Unliked @${username || acct}'s post` - : `Liked @${username || acct}'s post`, - ); - } catch (e) {} - }} + onClick={favouriteStatusNotify} /> + +
+
+ {post ? ( + + ) : ( + + )} +
+ {!!selectedCategory && + !CATEGORIES_INFO[selectedCategory].excludeStamp && ( + + )} +
{ + e.preventDefault(); + + const formData = new FormData(e.target); + const entries = Object.fromEntries(formData.entries()); + console.log('ENTRIES', entries); + + let { category, comment, forward } = entries; + if (!comment) comment = undefined; + if (forward === 'on') forward = true; + const ruleIds = + category === 'violation' + ? Object.entries(entries) + .filter(([key]) => key.startsWith('rule_ids')) + .map(([key, value]) => value) + : undefined; + + const params = { + category, + comment, + forward, + ruleIds, + }; + console.log('PARAMS', params); + + setUIState('loading'); + (async () => { + try { + await masto.v1.reports.create({ + accountId: account.id, + statusIds: post?.id ? [post.id] : undefined, + category, + comment, + ruleIds, + forward, + }); + setUIState('success'); + showToast(post ? 'Post reported' : 'Profile reported'); + onClose(); + } catch (error) { + console.error(error); + setUIState('error'); + showToast( + error?.message || + (post + ? 'Unable to report post' + : 'Unable to report profile'), + ); + } + })(); + }} + > +

+ {post + ? `What's the issue with this post?` + : `What's the issue with this profile?`} +

+
+ {CATEGORIES.map((category) => + category === 'violation' && !rules?.length ? null : ( + + + {category === 'violation' && !!rules?.length && ( + + )} + + ), + )} +
+
+

+ +

+ + + {!!mediaAttachments?.length && ( +
+

Media attachments:

+ +
+ )} + {!!accountEmojis?.length && ( +
+

Account Emojis:

+ +
+ )} + {!!emojis?.length && ( +
+

Emojis:

+ +
+ )} +
+ +

Notes:

+
    +
  • + This is static, unstyled and scriptless. You may need to apply + your own styles and edit as needed. +
  • +
  • + Polls are not interactive, becomes a list with vote counts. +
  • +
  • + Media attachments can be images, videos, audios or any file + types. +
  • +
  • Post could be edited or deleted later.
  • +
+
+
+

Preview

+ +

+ Note: This preview is lightly styled. +

+
+ + ); +} + function StatusButton({ checked, count, From afb1f6d520275d5fdcc1ebaf449dcfa97690a53b Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 2 Mar 2024 21:25:54 +0800 Subject: [PATCH 45/83] Perf fixes + 3d posts viz --- src/pages/catchup.css | 37 +++++- src/pages/catchup.jsx | 265 +++++++++++++++++++++++++++--------------- 2 files changed, 204 insertions(+), 98 deletions(-) diff --git a/src/pages/catchup.css b/src/pages/catchup.css index fc89c03c..b8b0a5cb 100644 --- a/src/pages/catchup.css +++ b/src/pages/catchup.css @@ -168,14 +168,14 @@ border-radius: 3px; border: 1px solid var(--bg-color); display: flex; - gap: 1px; + gap: var(--hairline-width); pointer-events: none; justify-content: stretch; height: 3px; - &:has(.post-dot:nth-child(320)) { + /* &:has(.post-dot:nth-child(320)) { gap: 0; - } + } */ .post-dot { display: block; @@ -198,6 +198,37 @@ } } +.catchup-posts-viz-time-bar { + margin: 0 16px; + padding: 1px; + display: flex; + gap: var(--hairline-width); + pointer-events: none; + justify-content: stretch; + background-image: linear-gradient(to bottom, transparent, var(--bg-color)); + + .posts-bin { + display: flex; + gap: var(--hairline-width); + flex-direction: column-reverse; + width: 100%; + + .post-dot { + display: block; + width: 100%; + height: 2px; + opacity: 0.2; + background-color: var(--link-color); + transition: 0.25s ease-in-out; + transition-property: opacity, transform; + + &.post-dot-highlight { + opacity: 1; + } + } + } +} + .catchup-filters { padding: 8px 16px; display: flex; diff --git a/src/pages/catchup.jsx b/src/pages/catchup.jsx index db4258c9..d8f7e9b3 100644 --- a/src/pages/catchup.jsx +++ b/src/pages/catchup.jsx @@ -5,7 +5,14 @@ import autoAnimate from '@formkit/auto-animate'; import { getBlurHashAverageColor } from 'fast-blurhash'; import { Fragment } from 'preact'; import { memo } from 'preact/compat'; -import { useEffect, useMemo, useReducer, useRef, useState } from 'preact/hooks'; +import { + useCallback, + useEffect, + useMemo, + useReducer, + useRef, + useState, +} from 'preact/hooks'; import { useSearchParams } from 'react-router-dom'; import { uid } from 'uid/single'; @@ -36,6 +43,47 @@ import useTitle from '../utils/useTitle'; const FILTER_CONTEXT = 'home'; +const RANGES = [ + { label: 'last 1 hour', value: 1 }, + { label: 'last 2 hours', value: 2 }, + { label: 'last 3 hours', value: 3 }, + { label: 'last 4 hours', value: 4 }, + { label: 'last 5 hours', value: 5 }, + { label: 'last 6 hours', value: 6 }, + { label: 'last 7 hours', value: 7 }, + { label: 'last 8 hours', value: 8 }, + { label: 'last 9 hours', value: 9 }, + { label: 'last 10 hours', value: 10 }, + { label: 'last 11 hours', value: 11 }, + { label: 'last 12 hours', value: 12 }, + { label: 'beyond 12 hours', value: 13 }, +]; + +const FILTER_VALUES = { + Filtered: 'filtered', + Groups: 'group', + Boosts: 'boost', + Replies: 'reply', + 'Followed tags': 'followedTags', + Original: 'original', +}; +const FILTER_CATEGORY_TEXT = { + Filtered: 'filtered posts', + Groups: 'group posts', + Boosts: 'boosts', + Replies: 'replies', + 'Followed tags': 'followed-tag posts', + Original: 'original posts', +}; +const SORT_BY_TEXT = { + // asc, desc + createdAt: ['oldest', 'latest'], + repliesCount: ['fewest replies', 'most replies'], + favouritesCount: ['fewest likes', 'most likes'], + reblogsCount: ['fewest boosts', 'most boosts'], + density: ['least dense', 'most dense'], +}; + function Catchup() { useTitle('Catch-up', '/catchup'); const { masto, instance } = api(); @@ -125,15 +173,15 @@ function Catchup() { const [posts, setPosts] = useState([]); const catchupRangeRef = useRef(); - async function handleCatchupClick({ duration } = {}) { + const NS = useMemo(() => getCurrentAccountNS(), []); + const handleCatchupClick = useCallback(async ({ duration } = {}) => { const now = Date.now(); const maxCreatedAt = duration ? now - duration : null; setUIState('loading'); const results = await fetchHome({ maxCreatedAt }); // Namespaced by account ID // Possible conflict if ID matches between different accounts from different instances - const ns = getCurrentAccountNS(); - const catchupID = `${ns}-${uid()}`; + const catchupID = `${NS}-${uid()}`; try { await db.catchup.set(catchupID, { id: catchupID, @@ -145,17 +193,15 @@ function Catchup() { setSearchParams({ id: catchupID }); } catch (e) { console.error(e, results); - // setUIState('error'); } - // setPosts(results); - // setUIState('results'); - } + }, []); useEffect(() => { if (id) { (async () => { const catchup = await db.catchup.get(id); if (catchup) { + catchup.posts.sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1)); setPosts(catchup.posts); setUIState('results'); } @@ -340,65 +386,48 @@ function Catchup() { const [selectedAuthor, setSelectedAuthor] = useState(null); const [range, setRange] = useState(1); - const ranges = [ - { label: 'last 1 hour', value: 1 }, - { label: 'last 2 hours', value: 2 }, - { label: 'last 3 hours', value: 3 }, - { label: 'last 4 hours', value: 4 }, - { label: 'last 5 hours', value: 5 }, - { label: 'last 6 hours', value: 6 }, - { label: 'last 7 hours', value: 7 }, - { label: 'last 8 hours', value: 8 }, - { label: 'last 9 hours', value: 9 }, - { label: 'last 10 hours', value: 10 }, - { label: 'last 11 hours', value: 11 }, - { label: 'last 12 hours', value: 12 }, - { label: 'beyond 12 hours', value: 13 }, - ]; const [sortBy, setSortBy] = useState('createdAt'); const [sortOrder, setSortOrder] = useState('asc'); const [groupBy, setGroupBy] = useState(null); const [filteredPosts, authors, authorCounts] = useMemo(() => { - let authors = []; - const authorCounts = {}; + const authorsHash = {}; + const authorCountsMap = new Map(); + let filteredPosts = posts.filter((post) => { - return ( + const postFilterMatches = selectedFilterCategory === 'All' || - post.__FILTER === - { - Filtered: 'filtered', - Groups: 'group', - Boosts: 'boost', - Replies: 'reply', - 'Followed tags': 'followedTags', - Original: 'original', - }[selectedFilterCategory] - ); - }); + post.__FILTER === FILTER_VALUES[selectedFilterCategory]; - filteredPosts.forEach((post) => { - if (!authors.find((a) => a.id === post.account.id)) { - authors.push(post.account); + if (postFilterMatches) { + authorsHash[post.account.id] = post.account; + authorCountsMap.set( + post.account.id, + (authorCountsMap.get(post.account.id) || 0) + 1, + ); } - authorCounts[post.account.id] = (authorCounts[post.account.id] || 0) + 1; + + return postFilterMatches; }); - if (selectedAuthor && authorCounts[selectedAuthor]) { + if (selectedAuthor && authorCountsMap.has(selectedAuthor)) { filteredPosts = filteredPosts.filter( (post) => post.account.id === selectedAuthor, ); } - const authorsHash = {}; - for (const author of authors) { - authorsHash[author.id] = author; - } - - return [filteredPosts, authorsHash, authorCounts]; + return [filteredPosts, authorsHash, Object.fromEntries(authorCountsMap)]; }, [selectedFilterCategory, selectedAuthor, posts]); + const filteredPostsMap = useMemo(() => { + const map = {}; + filteredPosts.forEach((post) => { + map[post.id] = post; + }); + return map; + }, [filteredPosts]); + const authorCountsList = useMemo( () => Object.keys(authorCounts).sort( @@ -450,26 +479,55 @@ function Catchup() { const prevGroup = useRef(null); const authorsListParent = useRef(null); + const autoAnimated = useRef(false); useEffect(() => { - if (authorsListParent.current && authorCountsList.length < 30) { + if (posts.length > 100 || autoAnimated.current) return; + if (authorsListParent.current) { autoAnimate(authorsListParent.current, { duration: 200, }); + autoAnimated.current = true; } - }, [selectedFilterCategory, authorCountsList, authorsListParent]); + }, [posts, authorsListParent]); + + const postsBarType = posts.length > 160 ? '3d' : '2d'; const postsBar = useMemo(() => { + if (postsBarType !== '2d') return null; return posts.map((post) => { // If part of filteredPosts - const isFiltered = filteredPosts.find((p) => p.id === post.id); + const isFiltered = filteredPostsMap[post.id]; return ( ); }); - }, [posts, filteredPosts]); + }, [filteredPostsMap]); + + const postsBins = useMemo(() => { + if (postsBarType !== '3d') return null; + if (!posts?.length) return null; + const bins = binByTime(posts, 'createdAt', 320); + return bins.map((posts, i) => { + return ( +
+ {posts.map((post) => { + const isFiltered = filteredPostsMap[post.id]; + return ( + + ); + })} +
+ ); + }); + }, [filteredPostsMap]); const scrollableRef = useRef(null); @@ -482,36 +540,20 @@ function Catchup() { useEffect(() => { if (uiState !== 'results') return; - const filterCategoryText = { - Filtered: 'filtered posts', - Groups: 'group posts', - Boosts: 'boosts', - Replies: 'replies', - 'Followed tags': 'followed-tag posts', - Original: 'original posts', - }; const authorUsername = selectedAuthor && authors[selectedAuthor] ? authors[selectedAuthor].username : ''; const sortOrderIndex = sortOrder === 'asc' ? 0 : 1; - const sortByText = { - // asc, desc - createdAt: ['oldest', 'latest'], - repliesCount: ['fewest replies', 'most replies'], - favouritesCount: ['fewest likes', 'most likes'], - reblogsCount: ['fewest boosts', 'most boosts'], - density: ['least dense', 'most dense'], - }; const groupByText = { account: 'authors', }; let toast = showToast({ duration: 5_000, // 5 seconds text: `Showing ${ - filterCategoryText[selectedFilterCategory] || 'all posts' + FILTER_CATEGORY_TEXT[selectedFilterCategory] || 'all posts' }${authorUsername ? ` by @${authorUsername}` : ''}, ${ - sortByText[sortBy][sortOrderIndex] + SORT_BY_TEXT[sortBy][sortOrderIndex] } first${ !!groupBy ? `, grouped by ${groupBy === 'account' ? groupByText[groupBy] : ''}` @@ -533,11 +575,11 @@ function Catchup() { const prevSelectedAuthorMissing = useRef(false); useEffect(() => { - console.log({ - prevSelectedAuthorMissing, - selectedAuthor, - authors, - }); + // console.log({ + // prevSelectedAuthorMissing, + // selectedAuthor, + // authors, + // }); let timer; if (selectedAuthor) { if (authors[selectedAuthor]) { @@ -649,8 +691,8 @@ function Catchup() { ref={catchupRangeRef} type="range" value={range} - min={ranges[0].value} - max={ranges[ranges.length - 1].value} + min={RANGES[0].value} + max={RANGES[RANGES.length - 1].value} step="1" list="catchup-ranges" onChange={(e) => setRange(+e.target.value)} @@ -660,10 +702,10 @@ function Catchup() { width: '8em', }} > - {ranges[range - 1].label} + {RANGES[range - 1].label}
- {range == ranges[ranges.length - 1].value + {range == RANGES[RANGES.length - 1].value ? 'until the max' : niceDateTime( new Date(Date.now() - range * 60 * 60 * 1000), @@ -671,14 +713,14 @@ function Catchup() {
- {ranges.map(({ label, value }) => ( + {RANGES.map(({ label, value }) => ( {' '} )} @@ -1215,6 +1218,42 @@ function Catchup() { )} + {showHelp && ( + setShowHelp(false)}> +
+ +
+

Help

+
+
+
+
Top links
+
+ Links shared by followings, sorted by shared counts, boosts + and likes. +
+
Sort: Density
+
+ Posts are sorted by information density or depth. Shorter + posts are "lighter" while longer posts are "heavier". Posts + with photos are "heavier" than posts without photos. +
+
Group: Authors
+
+ Posts are grouped by authors, sorted by posts count per + author. +
+
+
+
+
+ )} ); } From cfdbecc608b03bf439947194dc3270a499df5a4b Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Mon, 4 Mar 2024 14:37:03 +0800 Subject: [PATCH 61/83] Better "back" buttons for Catch-up --- src/pages/catchup.jsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/pages/catchup.jsx b/src/pages/catchup.jsx index d7a7fba7..a1773885 100644 --- a/src/pages/catchup.jsx +++ b/src/pages/catchup.jsx @@ -627,9 +627,16 @@ function Catchup() {
- - - + {uiState === 'results' && ( + + + + )} + {uiState === 'start' && ( + + + + )}

{uiState !== 'start' && ( From c578b411053752614e8298af1edc45ab9743ed7a Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Mon, 4 Mar 2024 16:36:34 +0800 Subject: [PATCH 62/83] Only show setting if logged-in --- src/pages/settings.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings.jsx b/src/pages/settings.jsx index b3f9af4b..14d961c3 100644 --- a/src/pages/settings.jsx +++ b/src/pages/settings.jsx @@ -433,7 +433,7 @@ function Settings({ onClose }) {

- {!!IMG_ALT_API_URL && ( + {!!IMG_ALT_API_URL && authenticated && (