From 9571271d83925e852237633a13fb9ae330ea9477 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun <cheeaun@gmail.com> Date: Fri, 15 Sep 2023 22:15:41 +0800 Subject: [PATCH] Experimental posting stats for non-following accounts Also recode+redesign the multiple metadata boxes in account info --- src/components/account-info.css | 105 +++++++++- src/components/account-info.jsx | 354 +++++++++++++++++++++----------- 2 files changed, 335 insertions(+), 124 deletions(-) diff --git a/src/components/account-info.css b/src/components/account-info.css index 168c4841..197a0579 100644 --- a/src/components/account-info.css +++ b/src/components/account-info.css @@ -139,13 +139,13 @@ /* flex-wrap: wrap; */ column-gap: 24px; row-gap: 8px; - opacity: 0.75; + /* opacity: 0.75; */ font-size: 90%; background-color: var(--bg-faded-color); padding: 12px; - border-radius: 16px; + /* border-radius: 16px; */ line-height: 1.25; - overflow-x: auto; + overflow-x: auto !important; justify-content: flex-start; position: relative; @@ -185,11 +185,33 @@ display: flex; } +.account-container .account-metadata-box { + overflow: hidden; + border-radius: 16px; + + & > * { + margin-bottom: 2px; + border-radius: 4px; + overflow: hidden; + } + + &:has(+ .account-metadata-box) { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + } + + + .account-metadata-box { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-left-radius: 16px; + border-bottom-right-radius: 16px; + } +} + .account-container .profile-metadata { display: flex; /* flex-wrap: wrap; */ gap: 2px; - border-radius: 16px; overflow: hidden; overflow-x: auto; } @@ -235,12 +257,11 @@ margin: 0; } -.account-container .common-followers p { +.account-container .common-followers { font-size: 90%; color: var(--text-insignificant-color); - border-top: 1px solid var(--outline-color); - border-bottom: 1px solid var(--outline-color); - padding: 8px 0; + background-color: var(--bg-faded-color); + padding: 8px 12px; margin: 0; } @@ -261,6 +282,74 @@ opacity: 0.5; } +@keyframes swoosh-bg-image { + 0% { + background-position: -320px 0; + opacity: 0.25; + } + 100% { + background-position: 0 0; + opacity: 1; + } +} +.account-container .posting-stats { + font-size: 90%; + color: var(--text-insignificant-color); + background-color: var(--bg-faded-color); + padding: 8px 12px; + --size: 8px; + --original-color: var(--link-color); + + .posting-stats-bar { + height: var(--size); + border-radius: var(--size); + overflow: hidden; + margin: 8px 0; + box-shadow: inset 0 0 0 1px var(--outline-color), + inset 0 0 0 1.5px var(--bg-blur-color); + background-color: var(--bg-color); + background-repeat: no-repeat; + animation: swoosh-bg-image 0.3s ease-in-out 0.3s both; + background-image: linear-gradient( + to right, + var(--original-color) 0%, + var(--original-color) var(--originals-percentage), + var(--reply-to-color) var(--originals-percentage), + var(--reply-to-color) var(--replies-percentage), + var(--reblog-color) var(--replies-percentage), + var(--reblog-color) 100% + ); + } + + .posting-stats-legends { + font-size: 12px; + text-transform: uppercase; + } + + .posting-stats-legend-item { + display: inline-block; + width: var(--size); + height: var(--size); + border-radius: var(--size); + background-color: var(--text-insignificant-color); + vertical-align: middle; + margin: 0 4px 2px; + /* border: 1px solid var(--outline-color); */ + box-shadow: inset 0 0 0 1px var(--outline-color), + inset 0 0 0 1.5px var(--bg-blur-color); + + &.posting-stats-legend-item-originals { + background-color: var(--original-color); + } + &.posting-stats-legend-item-replies { + background-color: var(--reply-to-color); + } + &.posting-stats-legend-item-boosts { + background-color: var(--reblog-color); + } + } +} + @keyframes shine { 0% { left: -100%; diff --git a/src/components/account-info.jsx b/src/components/account-info.jsx index 9a9ddb82..f7b04756 100644 --- a/src/components/account-info.jsx +++ b/src/components/account-info.jsx @@ -357,94 +357,99 @@ function AccountInfo({ __html: enhanceContent(note, { emojis }), }} /> - {fields?.length > 0 && ( - <div class="profile-metadata"> - {fields.map(({ name, value, verifiedAt }, i) => ( - <div - class={`profile-field ${ - verifiedAt ? 'profile-verified' : '' - }`} - key={name + i} - > - <b> - <EmojiText text={name} emojis={emojis} />{' '} - {!!verifiedAt && <Icon icon="check-circle" size="s" />} - </b> - <p - dangerouslySetInnerHTML={{ - __html: enhanceContent(value, { emojis }), - }} - /> - </div> - ))} - </div> - )} - <p class="stats"> - <LinkOrDiv - tabIndex={0} - to={accountLink} - onClick={() => { - states.showAccount = false; - states.showGenericAccounts = { - heading: 'Followers', - fetchAccounts: fetchFollowers, - }; - }} - > - <span title={followersCount}> - {shortenNumber(followersCount)} - </span>{' '} - Followers - </LinkOrDiv> - <LinkOrDiv - class="insignificant" - tabIndex={0} - to={accountLink} - onClick={() => { - states.showAccount = false; - states.showGenericAccounts = { - heading: 'Following', - fetchAccounts: fetchFollowing, - }; - }} - > - <span title={followingCount}> - {shortenNumber(followingCount)} - </span>{' '} - Following - <br /> - </LinkOrDiv> - <LinkOrDiv - class="insignificant" - to={accountLink} - onClick={ - standalone - ? undefined - : () => { - hideAllModals(); - } - } - > - <span title={statusesCount}> - {shortenNumber(statusesCount)} - </span>{' '} - Posts - </LinkOrDiv> - {!!createdAt && ( - <div class="insignificant"> - Joined{' '} - <time datetime={createdAt}> - {niceDateTime(createdAt, { - hideTime: true, - })} - </time> + <div class="account-metadata-box"> + {fields?.length > 0 && ( + <div class="profile-metadata"> + {fields.map(({ name, value, verifiedAt }, i) => ( + <div + class={`profile-field ${ + verifiedAt ? 'profile-verified' : '' + }`} + key={name + i} + > + <b> + <EmojiText text={name} emojis={emojis} />{' '} + {!!verifiedAt && ( + <Icon icon="check-circle" size="s" /> + )} + </b> + <p + dangerouslySetInnerHTML={{ + __html: enhanceContent(value, { emojis }), + }} + /> + </div> + ))} </div> )} - </p> + <div class="stats"> + <LinkOrDiv + tabIndex={0} + to={accountLink} + onClick={() => { + states.showAccount = false; + states.showGenericAccounts = { + heading: 'Followers', + fetchAccounts: fetchFollowers, + }; + }} + > + <span title={followersCount}> + {shortenNumber(followersCount)} + </span>{' '} + Followers + </LinkOrDiv> + <LinkOrDiv + class="insignificant" + tabIndex={0} + to={accountLink} + onClick={() => { + states.showAccount = false; + states.showGenericAccounts = { + heading: 'Following', + fetchAccounts: fetchFollowing, + }; + }} + > + <span title={followingCount}> + {shortenNumber(followingCount)} + </span>{' '} + Following + <br /> + </LinkOrDiv> + <LinkOrDiv + class="insignificant" + to={accountLink} + onClick={ + standalone + ? undefined + : () => { + hideAllModals(); + } + } + > + <span title={statusesCount}> + {shortenNumber(statusesCount)} + </span>{' '} + Posts + </LinkOrDiv> + {!!createdAt && ( + <div class="insignificant"> + Joined{' '} + <time datetime={createdAt}> + {niceDateTime(createdAt, { + hideTime: true, + })} + </time> + </div> + )} + </div> + </div> <RelatedActions info={info} instance={instance} authenticated={authenticated} + standalone={standalone} /> </main> </> @@ -454,7 +459,9 @@ function AccountInfo({ ); } -function RelatedActions({ info, instance, authenticated }) { +const FAMILIAR_FOLLOWERS_LIMIT = 10; + +function RelatedActions({ info, instance, authenticated, standalone }) { if (!info) return null; const { masto: currentMasto, @@ -466,6 +473,7 @@ function RelatedActions({ info, instance, authenticated }) { const [relationshipUIState, setRelationshipUIState] = useState('default'); const [relationship, setRelationship] = useState(null); const [familiarFollowers, setFamiliarFollowers] = useState([]); + const [postingStats, setPostingStats] = useState(); const { id, acct, url, username, locked, lastStatusAt, note, fields } = info; const accountID = useRef(id); @@ -526,12 +534,11 @@ function RelatedActions({ info, instance, authenticated }) { setRelationshipUIState('loading'); setFamiliarFollowers([]); + setPostingStats(null); const fetchRelationships = currentMasto.v1.accounts.fetchRelationships([ currentID, ]); - const fetchFamiliarFollowers = - currentMasto.v1.accounts.fetchFamiliarFollowers(currentID); try { const relationships = await fetchRelationships; @@ -542,9 +549,55 @@ function RelatedActions({ info, instance, authenticated }) { if (!relationship.following) { try { + const fetchFamiliarFollowers = + currentMasto.v1.accounts.fetchFamiliarFollowers(currentID); + const fetchStatuses = currentMasto.v1.accounts + .listStatuses(currentID, { + limit: 20, + }) + .next(); + const followers = await fetchFamiliarFollowers; console.log('fetched familiar followers', followers); - setFamiliarFollowers(followers[0].accounts.slice(0, 10)); + setFamiliarFollowers(followers[0].accounts); + + if (standalone) return; + + const { value: statuses } = await fetchStatuses; + console.log('fetched statuses', statuses); + const stats = { + total: statuses.length, + originals: 0, + replies: 0, + boosts: 0, + }; + // Categories statuses by type + // - Original posts (not replies to others) + // - Threads (self-replies + 1st original post) + // - Boosts (reblogs) + // - Replies (not-self replies) + statuses.forEach((status) => { + if (status.reblog) { + stats.boosts++; + } else if ( + status.inReplyToAccountId !== currentID && + !!status.inReplyToId + ) { + stats.replies++; + } else { + stats.originals++; + } + }); + + // Count days since last post + stats.daysSinceLastPost = Math.ceil( + (Date.now() - + new Date(statuses[statuses.length - 1].createdAt)) / + 86400000, + ); + + console.log('posting stats', stats); + setPostingStats(stats); } catch (e) { console.error(e); } @@ -571,40 +624,109 @@ function RelatedActions({ info, instance, authenticated }) { const [showTranslatedBio, setShowTranslatedBio] = useState(false); const [showAddRemoveLists, setShowAddRemoveLists] = useState(false); + const hasFamiliarFollowers = familiarFollowers?.length > 0; + const hasPostingStats = postingStats?.total >= 3; + return ( <> - <div - class="common-followers shazam-container no-animation" - hidden={!familiarFollowers?.length} - > - <div class="shazam-container-inner"> - <p> - Followed by{' '} - <span class="ib"> - {familiarFollowers.map((follower) => ( - <a - href={follower.url} - rel="noopener noreferrer" - onClick={(e) => { - e.preventDefault(); - states.showAccount = { - account: follower, - instance, - }; - }} - > - <Avatar - url={follower.avatarStatic} - size="l" - alt={`${follower.displayName} @${follower.acct}`} - squircle={follower?.bot} + {(hasFamiliarFollowers || hasPostingStats) && ( + <div class="account-metadata-box"> + {hasFamiliarFollowers && ( + <div class="shazam-container"> + <div class="shazam-container-inner"> + <p class="common-followers"> + Followed by{' '} + <span class="ib"> + {familiarFollowers + .slice(0, FAMILIAR_FOLLOWERS_LIMIT) + .map((follower) => ( + <a + href={follower.url} + rel="noopener noreferrer" + onClick={(e) => { + e.preventDefault(); + states.showAccount = { + account: follower, + instance, + }; + }} + > + <Avatar + url={follower.avatarStatic} + size="l" + alt={`${follower.displayName} @${follower.acct}`} + squircle={follower?.bot} + /> + </a> + ))} + {familiarFollowers.length > FAMILIAR_FOLLOWERS_LIMIT && ( + <button + type="button" + class="small plain4" + onClick={() => { + states.showGenericAccounts = { + heading: 'Followed by', + accounts: familiarFollowers, + }; + }} + > + +{familiarFollowers.length - FAMILIAR_FOLLOWERS_LIMIT} + <Icon icon="chevron-down" size="s" /> + </button> + )} + </span> + </p> + </div> + </div> + )} + {hasPostingStats && ( + <div class="shazam-container"> + <div class="shazam-container-inner"> + <div class="posting-stats"> + <div> + {postingStats.daysSinceLastPost < 365 + ? `Last ${postingStats.total} posts in the past + ${postingStats.daysSinceLastPost} day${ + postingStats.daysSinceLastPost > 1 ? 's' : '' + }` + : ` + Last ${postingStats.total} posts in the past year(s) + `} + </div> + <div + class="posting-stats-bar" + style={{ + // [originals | replies | boosts] + '--originals-percentage': `${ + (postingStats.originals / postingStats.total) * 100 + }%`, + '--replies-percentage': `${ + ((postingStats.originals + postingStats.replies) / + postingStats.total) * + 100 + }%`, + }} /> - </a> - ))} - </span> - </p> + <div class="posting-stats-legends"> + <span class="ib"> + <span class="posting-stats-legend-item posting-stats-legend-item-originals" />{' '} + Original + </span>{' '} + <span class="ib"> + <span class="posting-stats-legend-item posting-stats-legend-item-replies" />{' '} + Replies + </span>{' '} + <span class="ib"> + <span class="posting-stats-legend-item posting-stats-legend-item-boosts" />{' '} + Boosts + </span> + </div> + </div> + </div> + </div> + )} </div> - </div> + )} <p class="actions"> <span> {followedBy ? (