phanpy/src/components/account-info.jsx

1317 lines
42 KiB
React
Raw Normal View History

import './account-info.css';
2022-12-10 12:14:48 +03:00
2023-04-05 18:30:26 +03:00
import { Menu, MenuDivider, MenuItem, SubMenu } from '@szhsin/react-menu';
import { useEffect, useMemo, useReducer, useRef, useState } from 'preact/hooks';
import { proxy, useSnapshot } from 'valtio';
2022-12-10 12:14:48 +03:00
import { api } from '../utils/api';
import enhanceContent from '../utils/enhance-content';
2023-03-28 20:12:59 +03:00
import getHTMLText from '../utils/getHTMLText';
import handleContentLinks from '../utils/handle-content-links';
2023-03-01 15:07:22 +03:00
import niceDateTime from '../utils/nice-date-time';
2022-12-10 12:14:48 +03:00
import shortenNumber from '../utils/shorten-number';
import showToast from '../utils/show-toast';
2023-02-02 05:30:16 +03:00
import states, { hideAllModals } from '../utils/states';
import store from '../utils/store';
import { updateAccount } from '../utils/store-utils';
2022-12-10 12:14:48 +03:00
2023-02-12 14:29:03 +03:00
import AccountBlock from './account-block';
2022-12-10 12:14:48 +03:00
import Avatar from './avatar';
import EmojiText from './emoji-text';
import Icon from './icon';
import Link from './link';
2023-04-05 18:30:26 +03:00
import ListAddEdit from './list-add-edit';
import Loader from './loader';
import MenuConfirm from './menu-confirm';
2023-03-28 20:12:59 +03:00
import Modal from './modal';
import TranslationBlock from './translation-block';
2022-12-10 12:14:48 +03:00
const MUTE_DURATIONS = [
1000 * 60 * 5, // 5 minutes
1000 * 60 * 30, // 30 minutes
1000 * 60 * 60, // 1 hour
1000 * 60 * 60 * 6, // 6 hours
1000 * 60 * 60 * 24, // 1 day
1000 * 60 * 60 * 24 * 3, // 3 days
1000 * 60 * 60 * 24 * 7, // 1 week
0, // forever
];
const MUTE_DURATIONS_LABELS = {
0: 'Forever',
300_000: '5 minutes',
1_800_000: '30 minutes',
3_600_000: '1 hour',
21_600_000: '6 hours',
86_400_000: '1 day',
259_200_000: '3 days',
604_800_000: '1 week',
};
const LIMIT = 80;
const accountInfoStates = proxy({
familiarFollowers: [],
});
function AccountInfo({
account,
fetchAccount = () => {},
standalone,
instance,
authenticated,
}) {
const { masto } = api({
instance,
});
2022-12-10 12:14:48 +03:00
const [uiState, setUIState] = useState('default');
const isString = typeof account === 'string';
const [info, setInfo] = useState(isString ? null : account);
const snapAccountInfoStates = useSnapshot(accountInfoStates);
2022-12-10 12:14:48 +03:00
const isSelf = useMemo(
() => account.id === store.session.get('currentAccount'),
[account?.id],
);
const sameCurrentInstance = useMemo(
() => instance === api().instance,
[instance],
);
2022-12-10 12:14:48 +03:00
useEffect(() => {
if (!isString) {
setInfo(account);
return;
}
setUIState('loading');
(async () => {
try {
const info = await fetchAccount();
2023-03-11 17:07:20 +03:00
states.accounts[`${info.id}@${instance}`] = info;
setInfo(info);
setUIState('default');
} catch (e) {
console.error(e);
setInfo(null);
setUIState('error');
}
})();
}, [isString, account, fetchAccount]);
2022-12-10 12:14:48 +03:00
const {
acct,
avatar,
avatarStatic,
bot,
createdAt,
displayName,
emojis,
fields,
followersCount,
followingCount,
group,
2023-03-14 06:50:27 +03:00
// header,
// headerStatic,
2022-12-10 12:14:48 +03:00
id,
lastStatusAt,
locked,
note,
statusesCount,
url,
username,
2023-09-02 10:06:15 +03:00
memorial,
2023-09-20 12:28:08 +03:00
moved,
2022-12-10 12:14:48 +03:00
} = info || {};
2023-03-14 06:50:27 +03:00
let headerIsAvatar = false;
let { header, headerStatic } = info || {};
if (!header || /missing\.png$/.test(header)) {
if (avatar && !/missing\.png$/.test(avatar)) {
header = avatar;
headerIsAvatar = true;
if (avatarStatic && !/missing\.png$/.test(avatarStatic)) {
headerStatic = avatarStatic;
}
}
}
2022-12-10 12:14:48 +03:00
const [headerCornerColors, setHeaderCornerColors] = useState([]);
2023-02-22 08:28:01 +03:00
const followersIterator = useRef();
const familiarFollowersCache = useRef([]);
async function fetchFollowers(firstLoad) {
if (firstLoad || !followersIterator.current) {
followersIterator.current = masto.v1.accounts.listFollowers(id, {
limit: LIMIT,
});
}
const results = await followersIterator.current.next();
if (isSelf) return results;
if (!sameCurrentInstance) return results;
const { value } = results;
let newValue = [];
// On first load, fetch familiar followers, merge to top of results' `value`
// Remove dups on every fetch
if (firstLoad) {
const familiarFollowers = await masto.v1.accounts.fetchFamiliarFollowers(
id,
);
familiarFollowersCache.current = familiarFollowers[0].accounts;
newValue = [
...familiarFollowersCache.current,
...value.filter(
(account) =>
!familiarFollowersCache.current.some(
(familiar) => familiar.id === account.id,
),
),
];
} else if (value?.length) {
newValue = value.filter(
(account) =>
!familiarFollowersCache.current.some(
(familiar) => familiar.id === account.id,
),
);
}
return {
...results,
value: newValue,
};
}
const followingIterator = useRef();
async function fetchFollowing(firstLoad) {
if (firstLoad || !followingIterator.current) {
followingIterator.current = masto.v1.accounts.listFollowing(id, {
limit: LIMIT,
});
}
const results = await followingIterator.current.next();
return results;
}
const LinkOrDiv = standalone ? 'div' : Link;
const accountLink = instance ? `/${instance}/a/${id}` : `/a/${id}`;
2022-12-10 12:14:48 +03:00
return (
<div
class={`account-container ${uiState === 'loading' ? 'skeleton' : ''}`}
style={{
'--header-color-1': headerCornerColors[0],
'--header-color-2': headerCornerColors[1],
'--header-color-3': headerCornerColors[2],
'--header-color-4': headerCornerColors[3],
}}
2022-12-10 12:14:48 +03:00
>
{uiState === 'error' && (
<div class="ui-state">
<p>Unable to load account.</p>
<p>
<a href={account} target="_blank">
Go to account page <Icon icon="external" />
</a>
</p>
</div>
)}
{uiState === 'loading' ? (
2022-12-10 12:14:48 +03:00
<>
<header>
2023-02-12 14:29:03 +03:00
<AccountBlock avatarSize="xxxl" skeleton />
2022-12-10 12:14:48 +03:00
</header>
<main>
<div class="note">
<p> </p>
<p> </p>
</div>
<p class="stats">
2023-08-17 08:36:03 +03:00
<div>
<span></span> Followers
</div>
<div>
<span></span> Following
</div>
<div>
<span></span> Posts
</div>
<div>Joined </div>
</p>
</main>
2022-12-10 12:14:48 +03:00
</>
) : (
info && (
<>
2023-09-20 12:28:08 +03:00
{!!moved && (
<div class="account-moved">
<p>
<b>{displayName}</b> has indicated that their new account is
now:
</p>
<AccountBlock
account={moved}
instance={instance}
onClick={(e) => {
e.stopPropagation();
states.showAccount = moved;
}}
/>
</div>
)}
2023-03-10 12:36:42 +03:00
{header && !/missing\.png$/.test(header) && (
<img
src={header}
alt=""
2023-03-14 06:50:27 +03:00
class={`header-banner ${
headerIsAvatar ? 'header-is-avatar' : ''
}`}
onError={(e) => {
if (e.target.crossOrigin) {
if (e.target.src !== headerStatic) {
e.target.src = headerStatic;
} else {
e.target.removeAttribute('crossorigin');
e.target.src = header;
}
} else if (e.target.src !== headerStatic) {
e.target.src = headerStatic;
} else {
e.target.remove();
}
}}
crossOrigin="anonymous"
onLoad={(e) => {
e.target.classList.add('loaded');
try {
// Get color from four corners of image
const canvas = document.createElement('canvas');
2023-09-09 09:26:08 +03:00
const ctx = canvas.getContext('2d', {
willReadFrequently: true,
});
canvas.width = e.target.width;
canvas.height = e.target.height;
ctx.drawImage(e.target, 0, 0);
2023-03-13 05:42:34 +03:00
// const colors = [
// ctx.getImageData(0, 0, 1, 1).data,
// ctx.getImageData(e.target.width - 1, 0, 1, 1).data,
// ctx.getImageData(0, e.target.height - 1, 1, 1).data,
// ctx.getImageData(
// e.target.width - 1,
// e.target.height - 1,
// 1,
// 1,
// ).data,
// ];
// Get 10x10 pixels from corners, get average color from each
const pixelDimension = 10;
const colors = [
2023-03-13 05:42:34 +03:00
ctx.getImageData(0, 0, pixelDimension, pixelDimension)
.data,
ctx.getImageData(
2023-03-13 05:42:34 +03:00
e.target.width - pixelDimension,
0,
pixelDimension,
pixelDimension,
).data,
2023-03-13 05:42:34 +03:00
ctx.getImageData(
0,
e.target.height - pixelDimension,
pixelDimension,
pixelDimension,
).data,
ctx.getImageData(
e.target.width - pixelDimension,
e.target.height - pixelDimension,
pixelDimension,
pixelDimension,
).data,
].map((data) => {
let r = 0;
let g = 0;
let b = 0;
let a = 0;
for (let i = 0; i < data.length; i += 4) {
r += data[i];
g += data[i + 1];
b += data[i + 2];
a += data[i + 3];
}
const dataLength = data.length / 4;
return [
r / dataLength,
g / dataLength,
b / dataLength,
a / dataLength,
];
});
const rgbColors = colors.map((color) => {
2023-03-13 05:10:21 +03:00
const [r, g, b, a] = lightenRGB(color);
return `rgba(${r}, ${g}, ${b}, ${a})`;
});
setHeaderCornerColors(rgbColors);
console.log({ colors, rgbColors });
} catch (e) {
// Silently fail
}
}}
/>
2023-03-10 12:36:42 +03:00
)}
<header>
2023-02-12 14:29:03 +03:00
<AccountBlock
account={info}
instance={instance}
avatarSize="xxxl"
external={standalone}
internal={!standalone}
2023-02-12 14:29:03 +03:00
/>
</header>
<main tabIndex="-1">
2023-09-02 10:06:15 +03:00
{!!memorial && <span class="tag">In Memoriam</span>}
{!!bot && (
<span class="tag">
<Icon icon="bot" /> Automated
</span>
)}
2023-09-02 10:06:15 +03:00
{!!group && (
<span class="tag">
<Icon icon="group" /> Group
</span>
2023-06-14 01:00:06 +03:00
)}
<div
class="note"
onClick={handleContentLinks({
instance,
})}
dangerouslySetInnerHTML={{
__html: enhanceContent(note, { emojis }),
}}
/>
<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>
)}
<div class="stats">
<LinkOrDiv
tabIndex={0}
to={accountLink}
onClick={() => {
states.showAccount = false;
states.showGenericAccounts = {
heading: 'Followers',
fetchAccounts: fetchFollowers,
};
}}
>
{!!snapAccountInfoStates.familiarFollowers.length && (
<span class="shazam-container-horizontal">
<span class="shazam-container-inner stats-avatars-bunch">
{(snapAccountInfoStates.familiarFollowers || []).map(
(follower) => (
<Avatar
url={follower.avatarStatic}
size="s"
alt={`${follower.displayName} @${follower.acct}`}
squircle={follower?.bot}
/>
),
)}
</span>
</span>
)}
<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>
</>
)
2022-12-10 12:14:48 +03:00
)}
</div>
);
2022-12-16 08:27:04 +03:00
}
const FAMILIAR_FOLLOWERS_LIMIT = 3;
function RelatedActions({ info, instance, authenticated, standalone }) {
if (!info) return null;
const {
masto: currentMasto,
instance: currentInstance,
authenticated: currentAuthenticated,
} = api();
const sameInstance = instance === currentInstance;
const [relationshipUIState, setRelationshipUIState] = useState('default');
const [relationship, setRelationship] = useState(null);
const [postingStats, setPostingStats] = useState();
2023-09-20 12:28:08 +03:00
const { id, acct, url, username, locked, lastStatusAt, note, fields, moved } =
info;
const accountID = useRef(id);
const {
following,
showingReblogs,
notifying,
followedBy,
blocking,
blockedBy,
muting,
mutingNotifications,
requested,
domainBlocking,
endorsed,
} = relationship || {};
const [currentInfo, setCurrentInfo] = useState(null);
2023-03-18 15:40:16 +03:00
const [isSelf, setIsSelf] = useState(false);
useEffect(() => {
if (info) {
const currentAccount = store.session.get('currentAccount');
let currentID;
(async () => {
if (sameInstance && authenticated) {
currentID = id;
} else if (!sameInstance && currentAuthenticated) {
// Grab this account from my logged-in instance
const acctHasInstance = info.acct.includes('@');
try {
const results = await currentMasto.v2.search({
q: acctHasInstance ? info.acct : `${info.username}@${instance}`,
type: 'accounts',
limit: 1,
resolve: true,
});
console.log('🥏 Fetched account from logged-in instance', results);
if (results.accounts.length) {
currentID = results.accounts[0].id;
setCurrentInfo(results.accounts[0]);
}
} catch (e) {
console.error(e);
}
}
if (!currentID) return;
if (currentAccount === currentID) {
// It's myself!
2023-03-18 15:40:16 +03:00
setIsSelf(true);
return;
}
accountID.current = currentID;
2023-09-20 12:28:08 +03:00
if (moved) return;
setRelationshipUIState('loading');
accountInfoStates.familiarFollowers = [];
setPostingStats(null);
const fetchRelationships = currentMasto.v1.accounts.fetchRelationships([
currentID,
]);
try {
const relationships = await fetchRelationships;
console.log('fetched relationship', relationships);
setRelationshipUIState('default');
if (relationships.length) {
const relationship = relationships[0];
setRelationship(relationship);
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);
accountInfoStates.familiarFollowers =
followers[0].accounts.slice(0, FAMILIAR_FOLLOWERS_LIMIT);
2023-09-16 03:52:24 +03:00
if (!standalone) {
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);
}
}
}
} catch (e) {
console.error(e);
setRelationshipUIState('error');
}
})();
}
}, [info, authenticated]);
useEffect(() => {
if (info && isSelf) {
updateAccount(info);
}
}, [info, isSelf]);
const loading = relationshipUIState === 'loading';
2023-03-18 17:59:19 +03:00
const menuInstanceRef = useRef(null);
2023-03-28 20:12:59 +03:00
const [showTranslatedBio, setShowTranslatedBio] = useState(false);
2023-04-05 18:30:26 +03:00
const [showAddRemoveLists, setShowAddRemoveLists] = useState(false);
2023-03-28 20:12:59 +03:00
const hasPostingStats = postingStats?.total >= 3;
const accountLink = instance ? `/${instance}/a/${id}` : `/a/${id}`;
return (
<>
{hasPostingStats && (
<Link
to={accountLink}
class="account-metadata-box"
onClick={() => {
states.showAccount = false;
}}
>
<div class="shazam-container">
<div class="shazam-container-inner">
<div
class="posting-stats"
title={`${Math.round(
(postingStats.originals / postingStats.total) * 100,
)}% original posts, ${Math.round(
(postingStats.replies / postingStats.total) * 100,
)}% replies, ${Math.round(
(postingStats.boosts / postingStats.total) * 100,
)}% boosts`}
>
<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
}%`,
}}
/>
<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>
</Link>
)}
<p class="actions">
<span>
{followedBy ? (
<span class="tag">Following you</span>
) : !!lastStatusAt ? (
<small class="insignificant">
Last post:{' '}
{niceDateTime(lastStatusAt, {
hideTime: true,
})}
</small>
) : (
<span />
)}
{muting && <span class="tag danger">Muted</span>}
{blocking && <span class="tag danger">Blocked</span>}
</span>{' '}
<span class="buttons">
<Menu
2023-03-18 17:59:19 +03:00
instanceRef={menuInstanceRef}
portal={{
target: document.body,
}}
containerProps={{
style: {
// Higher than the backdrop
zIndex: 1001,
},
2023-03-22 09:11:38 +03:00
onClick: (e) => {
if (e.target === e.currentTarget) {
menuInstanceRef.current?.closeMenu?.();
}
2023-03-18 17:59:19 +03:00
},
}}
align="center"
position="anchor"
overflow="auto"
boundingBoxPadding="8 8 8 8"
menuButton={
<button
type="button"
title="More"
class="plain"
disabled={loading}
>
<Icon icon="more" size="l" alt="More" />
</button>
}
>
2023-03-18 15:40:16 +03:00
{currentAuthenticated && !isSelf && (
<>
<MenuItem
onClick={() => {
states.showCompose = {
draftStatus: {
status: `@${currentInfo?.acct || acct} `,
},
};
}}
>
<Icon icon="at" />
<span>Mention @{username}</span>
</MenuItem>
2023-03-28 20:12:59 +03:00
<MenuItem
onClick={() => {
setShowTranslatedBio(true);
}}
>
<Icon icon="translate" />
<span>Translate bio</span>
</MenuItem>
2023-04-05 18:30:26 +03:00
{/* Add/remove from lists is only possible if following the account */}
{following && (
<MenuItem
onClick={() => {
setShowAddRemoveLists(true);
}}
>
<Icon icon="list" />
<span>Add/remove from Lists</span>
</MenuItem>
)}
<MenuDivider />
</>
)}
<MenuItem href={url} target="_blank">
<Icon icon="external" />
<small class="menu-double-lines">{niceAccountURL(url)}</small>
</MenuItem>
<div class="menu-horizontal">
<MenuItem
onClick={() => {
// Copy url to clipboard
try {
navigator.clipboard.writeText(url);
showToast('Link copied');
} catch (e) {
console.error(e);
showToast('Unable to copy link');
}
}}
>
<Icon icon="link" />
<span>Copy</span>
</MenuItem>
{navigator?.share &&
navigator?.canShare?.({
url,
}) && (
<MenuItem
onClick={() => {
try {
navigator.share({
url,
});
} catch (e) {
console.error(e);
alert("Sharing doesn't seem to work.");
}
}}
>
<Icon icon="share" />
<span>Share</span>
</MenuItem>
)}
</div>
{!!relationship && (
<>
<MenuDivider />
{muting ? (
<MenuItem
onClick={() => {
setRelationshipUIState('loading');
(async () => {
try {
const newRelationship =
await currentMasto.v1.accounts.unmute(
currentInfo?.id || id,
);
console.log('unmuting', newRelationship);
setRelationship(newRelationship);
setRelationshipUIState('default');
showToast(`Unmuted @${username}`);
} catch (e) {
console.error(e);
setRelationshipUIState('error');
}
})();
}}
>
<Icon icon="unmute" />
<span>Unmute @{username}</span>
</MenuItem>
) : (
<SubMenu
openTrigger="clickOnly"
direction="bottom"
overflow="auto"
2023-06-14 01:00:06 +03:00
shift={16}
label={
<>
<Icon icon="mute" />
<span class="menu-grow">Mute @{username}</span>
2023-03-24 10:05:16 +03:00
<span
style={{
textOverflow: 'clip',
}}
>
<Icon icon="time" />
<Icon icon="chevron-right" />
</span>
</>
}
>
<div class="menu-wrap">
{MUTE_DURATIONS.map((duration) => (
<MenuItem
onClick={() => {
setRelationshipUIState('loading');
(async () => {
try {
const newRelationship =
await currentMasto.v1.accounts.mute(
currentInfo?.id || id,
{
duration,
},
);
console.log('muting', newRelationship);
setRelationship(newRelationship);
setRelationshipUIState('default');
showToast(
`Muted @${username} for ${MUTE_DURATIONS_LABELS[duration]}`,
);
} catch (e) {
console.error(e);
setRelationshipUIState('error');
showToast(`Unable to mute @${username}`);
}
})();
}}
>
{MUTE_DURATIONS_LABELS[duration]}
</MenuItem>
))}
</div>
</SubMenu>
)}
<MenuConfirm
subMenu
confirm={!blocking}
confirmLabel={
<>
<Icon icon="block" />
<span>Block @{username}?</span>
</>
}
menuItemClassName="danger"
onClick={() => {
// if (!blocking && !confirm(`Block @${username}?`)) {
// return;
// }
setRelationshipUIState('loading');
(async () => {
try {
if (blocking) {
const newRelationship =
await currentMasto.v1.accounts.unblock(
currentInfo?.id || id,
);
console.log('unblocking', newRelationship);
setRelationship(newRelationship);
setRelationshipUIState('default');
showToast(`Unblocked @${username}`);
} else {
const newRelationship =
await currentMasto.v1.accounts.block(
currentInfo?.id || id,
);
console.log('blocking', newRelationship);
setRelationship(newRelationship);
setRelationshipUIState('default');
showToast(`Blocked @${username}`);
}
} catch (e) {
console.error(e);
setRelationshipUIState('error');
if (blocking) {
showToast(`Unable to unblock @${username}`);
} else {
showToast(`Unable to block @${username}`);
}
}
})();
}}
>
{blocking ? (
<>
<Icon icon="unblock" />
<span>Unblock @{username}</span>
</>
) : (
<>
<Icon icon="block" />
<span>Block @{username}</span>
</>
)}
</MenuConfirm>
{/* <MenuItem>
<Icon icon="flag" />
<span>Report @{username}</span>
</MenuItem> */}
</>
)}
</Menu>
{!relationship && relationshipUIState === 'loading' && (
<Loader abrupt />
)}
{!!relationship && (
<MenuConfirm
confirm={following || requested}
confirmLabel={
<span>
{requested
? 'Withdraw follow request?'
: `Unfollow @${info.acct || info.username}?`}
</span>
}
menuItemClassName="danger"
align="end"
disabled={loading}
onClick={() => {
setRelationshipUIState('loading');
(async () => {
try {
let newRelationship;
if (following || requested) {
// const yes = confirm(
// requested
// ? 'Withdraw follow request?'
// : `Unfollow @${info.acct || info.username}?`,
// );
// if (yes) {
newRelationship = await currentMasto.v1.accounts.unfollow(
accountID.current,
);
// }
} else {
newRelationship = await currentMasto.v1.accounts.follow(
accountID.current,
);
}
if (newRelationship) setRelationship(newRelationship);
setRelationshipUIState('default');
} catch (e) {
alert(e);
setRelationshipUIState('error');
}
})();
}}
>
<button
type="button"
class={`${following || requested ? 'light swap' : ''}`}
data-swap-state={following || requested ? 'danger' : ''}
disabled={loading}
>
{following ? (
<>
<span>Following</span>
<span>Unfollow</span>
</>
) : requested ? (
<>
<span>Requested</span>
<span>Withdraw</span>
</>
) : locked ? (
<>
<Icon icon="lock" /> <span>Follow</span>
</>
) : (
'Follow'
)}
</button>
</MenuConfirm>
)}
</span>
</p>
2023-03-28 20:12:59 +03:00
{!!showTranslatedBio && (
<Modal
class="light"
2023-09-16 09:47:55 +03:00
onClose={() => {
setShowTranslatedBio(false);
2023-03-28 20:12:59 +03:00
}}
>
2023-04-20 11:10:57 +03:00
<TranslatedBioSheet
note={note}
fields={fields}
onClose={() => setShowTranslatedBio(false)}
/>
2023-03-28 20:12:59 +03:00
</Modal>
)}
2023-04-05 18:30:26 +03:00
{!!showAddRemoveLists && (
<Modal
class="light"
2023-09-16 09:47:55 +03:00
onClose={() => {
setShowAddRemoveLists(false);
2023-04-05 18:30:26 +03:00
}}
>
2023-04-20 11:10:57 +03:00
<AddRemoveListsSheet
accountID={accountID.current}
onClose={() => setShowAddRemoveLists(false)}
/>
2023-04-05 18:30:26 +03:00
</Modal>
)}
</>
);
}
2023-03-13 05:10:21 +03:00
// Apply more alpha if high luminence
function lightenRGB([r, g, b]) {
const luminence = 0.2126 * r + 0.7152 * g + 0.0722 * b;
console.log('luminence', luminence);
2023-03-13 05:42:34 +03:00
let alpha;
if (luminence >= 220) {
alpha = 1;
} else if (luminence <= 50) {
alpha = 0.1;
2023-03-13 05:42:34 +03:00
} else {
alpha = luminence / 255;
2023-03-13 05:42:34 +03:00
}
alpha = Math.min(1, alpha);
2023-03-13 05:10:21 +03:00
return [r, g, b, alpha];
}
function niceAccountURL(url) {
if (!url) return;
const urlObj = new URL(url);
const { host, pathname } = urlObj;
const path = pathname.replace(/\/$/, '').replace(/^\//, '');
return (
<>
<span class="more-insignificant">{host}/</span>
<wbr />
<span>{path}</span>
</>
);
}
2023-04-20 11:10:57 +03:00
function TranslatedBioSheet({ note, fields, onClose }) {
2023-03-28 20:12:59 +03:00
const fieldsText =
fields
?.map(({ name, value }) => `${name}\n${getHTMLText(value)}`)
.join('\n\n') || '';
const text = getHTMLText(note) + (fieldsText ? `\n\n${fieldsText}` : '');
return (
<div class="sheet">
2023-04-20 11:10:57 +03:00
{!!onClose && (
<button type="button" class="sheet-close" onClick={onClose}>
<Icon icon="x" />
</button>
)}
2023-03-28 20:12:59 +03:00
<header>
2023-04-24 07:27:24 +03:00
<h2>Translated Bio</h2>
2023-03-28 20:12:59 +03:00
</header>
<main>
<p
style={{
whiteSpace: 'pre-wrap',
}}
>
{text}
</p>
<TranslationBlock forceTranslate text={text} />
</main>
</div>
);
}
2023-04-05 18:30:26 +03:00
2023-04-20 11:10:57 +03:00
function AddRemoveListsSheet({ accountID, onClose }) {
2023-04-05 18:30:26 +03:00
const { masto } = api();
2023-08-14 06:22:42 +03:00
const [uiState, setUIState] = useState('default');
2023-04-05 18:30:26 +03:00
const [lists, setLists] = useState([]);
const [listsContainingAccount, setListsContainingAccount] = useState([]);
const [reloadCount, reload] = useReducer((c) => c + 1, 0);
useEffect(() => {
2023-08-14 06:22:42 +03:00
setUIState('loading');
2023-04-05 18:30:26 +03:00
(async () => {
try {
const lists = await masto.v1.lists.list();
const listsContainingAccount = await masto.v1.accounts.listLists(
accountID,
);
console.log({ lists, listsContainingAccount });
setLists(lists);
setListsContainingAccount(listsContainingAccount);
2023-08-14 06:22:42 +03:00
setUIState('default');
2023-04-05 18:30:26 +03:00
} catch (e) {
console.error(e);
2023-08-14 06:22:42 +03:00
setUIState('error');
2023-04-05 18:30:26 +03:00
}
})();
}, [reloadCount]);
const [showListAddEditModal, setShowListAddEditModal] = useState(false);
return (
<div class="sheet" id="list-add-remove-container">
2023-04-20 11:10:57 +03:00
{!!onClose && (
<button type="button" class="sheet-close" onClick={onClose}>
<Icon icon="x" />
</button>
)}
2023-04-05 18:30:26 +03:00
<header>
<h2>Add/Remove from Lists</h2>
</header>
<main>
{lists.length > 0 ? (
<ul class="list-add-remove">
{lists.map((list) => {
const inList = listsContainingAccount.some(
(l) => l.id === list.id,
);
return (
<li>
<button
type="button"
class={`light ${inList ? 'checked' : ''}`}
disabled={uiState === 'loading'}
onClick={() => {
2023-08-14 06:22:42 +03:00
setUIState('loading');
2023-04-05 18:30:26 +03:00
(async () => {
try {
if (inList) {
await masto.v1.lists.removeAccount(list.id, {
accountIds: [accountID],
});
} else {
await masto.v1.lists.addAccount(list.id, {
accountIds: [accountID],
});
}
2023-08-14 06:22:42 +03:00
// setUIState('default');
2023-04-05 18:30:26 +03:00
reload();
} catch (e) {
console.error(e);
2023-08-14 06:22:42 +03:00
setUIState('error');
2023-04-05 18:30:26 +03:00
alert(
inList
? 'Unable to remove from list.'
: 'Unable to add to list.',
);
}
})();
}}
>
<Icon icon="check-circle" />
<span>{list.title}</span>
</button>
</li>
);
})}
</ul>
) : uiState === 'loading' ? (
<p class="ui-state">
<Loader abrupt />
</p>
) : uiState === 'error' ? (
<p class="ui-state">Unable to load lists.</p>
) : (
<p class="ui-state">No lists.</p>
)}
<button
type="button"
class="plain2"
onClick={() => setShowListAddEditModal(true)}
disabled={uiState !== 'default'}
>
<Icon icon="plus" size="l" /> <span>New list</span>
</button>
</main>
{showListAddEditModal && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowListAddEditModal(false);
}
}}
>
<ListAddEdit
list={showListAddEditModal?.list}
onClose={(result) => {
if (result.state === 'success') {
reload();
}
setShowListAddEditModal(false);
}}
/>
</Modal>
)}
</div>
);
}
export default AccountInfo;