Experimental posting stats for non-following accounts

Also recode+redesign the multiple metadata boxes in account info
This commit is contained in:
Lim Chee Aun 2023-09-15 22:15:41 +08:00
parent b116cbfe8c
commit 9571271d83
2 changed files with 335 additions and 124 deletions

View file

@ -139,13 +139,13 @@
/* flex-wrap: wrap; */ /* flex-wrap: wrap; */
column-gap: 24px; column-gap: 24px;
row-gap: 8px; row-gap: 8px;
opacity: 0.75; /* opacity: 0.75; */
font-size: 90%; font-size: 90%;
background-color: var(--bg-faded-color); background-color: var(--bg-faded-color);
padding: 12px; padding: 12px;
border-radius: 16px; /* border-radius: 16px; */
line-height: 1.25; line-height: 1.25;
overflow-x: auto; overflow-x: auto !important;
justify-content: flex-start; justify-content: flex-start;
position: relative; position: relative;
@ -185,11 +185,33 @@
display: flex; 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 { .account-container .profile-metadata {
display: flex; display: flex;
/* flex-wrap: wrap; */ /* flex-wrap: wrap; */
gap: 2px; gap: 2px;
border-radius: 16px;
overflow: hidden; overflow: hidden;
overflow-x: auto; overflow-x: auto;
} }
@ -235,12 +257,11 @@
margin: 0; margin: 0;
} }
.account-container .common-followers p { .account-container .common-followers {
font-size: 90%; font-size: 90%;
color: var(--text-insignificant-color); color: var(--text-insignificant-color);
border-top: 1px solid var(--outline-color); background-color: var(--bg-faded-color);
border-bottom: 1px solid var(--outline-color); padding: 8px 12px;
padding: 8px 0;
margin: 0; margin: 0;
} }
@ -261,6 +282,74 @@
opacity: 0.5; 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 { @keyframes shine {
0% { 0% {
left: -100%; left: -100%;

View file

@ -357,94 +357,99 @@ function AccountInfo({
__html: enhanceContent(note, { emojis }), __html: enhanceContent(note, { emojis }),
}} }}
/> />
{fields?.length > 0 && ( <div class="account-metadata-box">
<div class="profile-metadata"> {fields?.length > 0 && (
{fields.map(({ name, value, verifiedAt }, i) => ( <div class="profile-metadata">
<div {fields.map(({ name, value, verifiedAt }, i) => (
class={`profile-field ${ <div
verifiedAt ? 'profile-verified' : '' class={`profile-field ${
}`} verifiedAt ? 'profile-verified' : ''
key={name + i} }`}
> key={name + i}
<b> >
<EmojiText text={name} emojis={emojis} />{' '} <b>
{!!verifiedAt && <Icon icon="check-circle" size="s" />} <EmojiText text={name} emojis={emojis} />{' '}
</b> {!!verifiedAt && (
<p <Icon icon="check-circle" size="s" />
dangerouslySetInnerHTML={{ )}
__html: enhanceContent(value, { emojis }), </b>
}} <p
/> dangerouslySetInnerHTML={{
</div> __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> </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 <RelatedActions
info={info} info={info}
instance={instance} instance={instance}
authenticated={authenticated} authenticated={authenticated}
standalone={standalone}
/> />
</main> </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; if (!info) return null;
const { const {
masto: currentMasto, masto: currentMasto,
@ -466,6 +473,7 @@ function RelatedActions({ info, instance, authenticated }) {
const [relationshipUIState, setRelationshipUIState] = useState('default'); const [relationshipUIState, setRelationshipUIState] = useState('default');
const [relationship, setRelationship] = useState(null); const [relationship, setRelationship] = useState(null);
const [familiarFollowers, setFamiliarFollowers] = useState([]); const [familiarFollowers, setFamiliarFollowers] = useState([]);
const [postingStats, setPostingStats] = useState();
const { id, acct, url, username, locked, lastStatusAt, note, fields } = info; const { id, acct, url, username, locked, lastStatusAt, note, fields } = info;
const accountID = useRef(id); const accountID = useRef(id);
@ -526,12 +534,11 @@ function RelatedActions({ info, instance, authenticated }) {
setRelationshipUIState('loading'); setRelationshipUIState('loading');
setFamiliarFollowers([]); setFamiliarFollowers([]);
setPostingStats(null);
const fetchRelationships = currentMasto.v1.accounts.fetchRelationships([ const fetchRelationships = currentMasto.v1.accounts.fetchRelationships([
currentID, currentID,
]); ]);
const fetchFamiliarFollowers =
currentMasto.v1.accounts.fetchFamiliarFollowers(currentID);
try { try {
const relationships = await fetchRelationships; const relationships = await fetchRelationships;
@ -542,9 +549,55 @@ function RelatedActions({ info, instance, authenticated }) {
if (!relationship.following) { if (!relationship.following) {
try { try {
const fetchFamiliarFollowers =
currentMasto.v1.accounts.fetchFamiliarFollowers(currentID);
const fetchStatuses = currentMasto.v1.accounts
.listStatuses(currentID, {
limit: 20,
})
.next();
const followers = await fetchFamiliarFollowers; const followers = await fetchFamiliarFollowers;
console.log('fetched familiar followers', followers); 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) { } catch (e) {
console.error(e); console.error(e);
} }
@ -571,40 +624,109 @@ function RelatedActions({ info, instance, authenticated }) {
const [showTranslatedBio, setShowTranslatedBio] = useState(false); const [showTranslatedBio, setShowTranslatedBio] = useState(false);
const [showAddRemoveLists, setShowAddRemoveLists] = useState(false); const [showAddRemoveLists, setShowAddRemoveLists] = useState(false);
const hasFamiliarFollowers = familiarFollowers?.length > 0;
const hasPostingStats = postingStats?.total >= 3;
return ( return (
<> <>
<div {(hasFamiliarFollowers || hasPostingStats) && (
class="common-followers shazam-container no-animation" <div class="account-metadata-box">
hidden={!familiarFollowers?.length} {hasFamiliarFollowers && (
> <div class="shazam-container">
<div class="shazam-container-inner"> <div class="shazam-container-inner">
<p> <p class="common-followers">
Followed by{' '} Followed by{' '}
<span class="ib"> <span class="ib">
{familiarFollowers.map((follower) => ( {familiarFollowers
<a .slice(0, FAMILIAR_FOLLOWERS_LIMIT)
href={follower.url} .map((follower) => (
rel="noopener noreferrer" <a
onClick={(e) => { href={follower.url}
e.preventDefault(); rel="noopener noreferrer"
states.showAccount = { onClick={(e) => {
account: follower, e.preventDefault();
instance, states.showAccount = {
}; account: follower,
}} instance,
> };
<Avatar }}
url={follower.avatarStatic} >
size="l" <Avatar
alt={`${follower.displayName} @${follower.acct}`} url={follower.avatarStatic}
squircle={follower?.bot} 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> <div class="posting-stats-legends">
))} <span class="ib">
</span> <span class="posting-stats-legend-item posting-stats-legend-item-originals" />{' '}
</p> 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>
</div> )}
<p class="actions"> <p class="actions">
<span> <span>
{followedBy ? ( {followedBy ? (