import './account-info.css';
import { msg, plural, t, Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { MenuDivider, MenuItem } from '@szhsin/react-menu';
import {
useCallback,
useEffect,
useMemo,
useReducer,
useRef,
useState,
} from 'preact/hooks';
import punycode from 'punycode/';
import { api } from '../utils/api';
import enhanceContent from '../utils/enhance-content';
import getHTMLText from '../utils/getHTMLText';
import handleContentLinks from '../utils/handle-content-links';
import i18nDuration from '../utils/i18n-duration';
import { getLists } from '../utils/lists';
import niceDateTime from '../utils/nice-date-time';
import pmem from '../utils/pmem';
import shortenNumber from '../utils/shorten-number';
import showCompose from '../utils/show-compose';
import showToast from '../utils/show-toast';
import states, { hideAllModals } from '../utils/states';
import store from '../utils/store';
import { getCurrentAccountID, updateAccount } from '../utils/store-utils';
import supports from '../utils/supports';
import AccountBlock from './account-block';
import Avatar from './avatar';
import EmojiText from './emoji-text';
import Icon from './icon';
import Link from './link';
import ListAddEdit from './list-add-edit';
import Loader from './loader';
import MenuConfirm from './menu-confirm';
import MenuLink from './menu-link';
import Menu2 from './menu2';
import Modal from './modal';
import SubMenu2 from './submenu2';
import TranslationBlock from './translation-block';
const MUTE_DURATIONS = [
60 * 5, // 5 minutes
60 * 30, // 30 minutes
60 * 60, // 1 hour
60 * 60 * 6, // 6 hours
60 * 60 * 24, // 1 day
60 * 60 * 24 * 3, // 3 days
60 * 60 * 24 * 7, // 1 week
0, // forever
];
const MUTE_DURATIONS_LABELS = {
0: msg`Forever`,
300: i18nDuration(5, 'minute'),
1_800: i18nDuration(30, 'minute'),
3_600: i18nDuration(1, 'hour'),
21_600: i18nDuration(6, 'hour'),
86_400: i18nDuration(1, 'day'),
259_200: i18nDuration(3, 'day'),
604_800: i18nDuration(1, 'week'),
};
const LIMIT = 80;
const ACCOUNT_INFO_MAX_AGE = 1000 * 60 * 10; // 10 mins
function fetchFamiliarFollowers(currentID, masto) {
return masto.v1.accounts.familiarFollowers.fetch({
id: [currentID],
});
}
const memFetchFamiliarFollowers = pmem(fetchFamiliarFollowers, {
maxAge: ACCOUNT_INFO_MAX_AGE,
});
async function fetchPostingStats(accountID, masto) {
const fetchStatuses = masto.v1.accounts
.$select(accountID)
.statuses.list({
limit: 20,
})
.next();
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.inReplyToId &&
status.inReplyToAccountId !== status.account.id // Not self-reply
) {
stats.replies++;
} else {
stats.originals++;
}
});
// Count days since last post
if (statuses.length) {
stats.daysSinceLastPost = Math.ceil(
(Date.now() - new Date(statuses[statuses.length - 1].createdAt)) /
86400000,
);
}
console.log('posting stats', stats);
return stats;
}
const memFetchPostingStats = pmem(fetchPostingStats, {
maxAge: ACCOUNT_INFO_MAX_AGE,
});
function AccountInfo({
account,
fetchAccount = () => {},
standalone,
instance,
authenticated,
}) {
const { i18n } = useLingui();
const { masto } = api({
instance,
});
const { masto: currentMasto, instance: currentInstance } = api();
const [uiState, setUIState] = useState('default');
const isString = typeof account === 'string';
const [info, setInfo] = useState(isString ? null : account);
const sameCurrentInstance = useMemo(
() => instance === currentInstance,
[instance, currentInstance],
);
useEffect(() => {
if (!isString) {
setInfo(account);
return;
}
setUIState('loading');
(async () => {
try {
const info = await fetchAccount();
states.accounts[`${info.id}@${instance}`] = info;
setInfo(info);
setUIState('default');
} catch (e) {
console.error(e);
setInfo(null);
setUIState('error');
}
})();
}, [isString, account, fetchAccount]);
const {
acct,
avatar,
avatarStatic,
bot,
createdAt,
displayName,
emojis,
fields,
followersCount,
followingCount,
group,
// header,
// headerStatic,
id,
lastStatusAt,
locked,
note,
statusesCount,
url,
username,
memorial,
moved,
roles,
hideCollections,
} = info || {};
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;
}
}
}
const isSelf = useMemo(() => id === getCurrentAccountID(), [id]);
useEffect(() => {
const infoHasEssentials = !!(
info?.id &&
info?.username &&
info?.acct &&
info?.avatar &&
info?.avatarStatic &&
info?.displayName &&
info?.url
);
if (isSelf && instance && infoHasEssentials) {
const accounts = store.local.getJSON('accounts');
let updated = false;
accounts.forEach((account) => {
if (account.info.id === info.id && account.instanceURL === instance) {
account.info = info;
updated = true;
}
});
if (updated) {
console.log('Updated account info', info);
store.local.setJSON('accounts', accounts);
}
}
}, [isSelf, info, instance]);
const accountInstance = useMemo(() => {
if (!url) return null;
const domain = punycode.toUnicode(URL.parse(url).hostname);
return domain;
}, [url]);
const [headerCornerColors, setHeaderCornerColors] = useState([]);
const followersIterator = useRef();
const familiarFollowersCache = useRef([]);
async function fetchFollowers(firstLoad) {
if (firstLoad || !followersIterator.current) {
followersIterator.current = masto.v1.accounts.$select(id).followers.list({
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) {
let familiarFollowers = [];
try {
familiarFollowers = await masto.v1.accounts.familiarFollowers.fetch({
id: [id],
});
} catch (e) {}
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.$select(id).following.list({
limit: LIMIT,
});
}
const results = await followingIterator.current.next();
return results;
}
const LinkOrDiv = standalone ? 'div' : Link;
const accountLink = instance ? `/${instance}/a/${id}` : `/a/${id}`;
const [familiarFollowers, setFamiliarFollowers] = useState([]);
const [postingStats, setPostingStats] = useState();
const [postingStatsUIState, setPostingStatsUIState] = useState('default');
const hasPostingStats = !!postingStats?.total;
const renderFamiliarFollowers = async (currentID) => {
try {
const followers = await memFetchFamiliarFollowers(
currentID,
currentMasto,
);
console.log('fetched familiar followers', followers);
setFamiliarFollowers(
followers[0].accounts.slice(0, FAMILIAR_FOLLOWERS_LIMIT),
);
} catch (e) {
console.error(e);
}
};
const renderPostingStats = async () => {
if (!id) return;
setPostingStatsUIState('loading');
try {
const stats = await memFetchPostingStats(id, masto);
setPostingStats(stats);
setPostingStatsUIState('default');
} catch (e) {
console.error(e);
setPostingStatsUIState('error');
}
};
const onRelationshipChange = useCallback(
({ relationship, currentID }) => {
if (!relationship.following) {
renderFamiliarFollowers(currentID);
if (!standalone && statusesCount > 0) {
// Only render posting stats if not standalone and has posts
renderPostingStats();
}
}
},
[standalone, id, statusesCount],
);
const onProfileUpdate = useCallback(
(newAccount) => {
if (newAccount.id === id) {
console.log('Updated account info', newAccount);
setInfo(newAccount);
states.accounts[`${newAccount.id}@${instance}`] = newAccount;
}
},
[id, instance],
);
return (
{uiState === 'error' && (
)}
{uiState === 'loading' ? (
<>
███████ ████ ████
████ ████████ ██████ █████████ ████ ██
>
) : (
info && (
<>
{!!moved && (
{displayName} has indicated that their new account is
now:
{
e.stopPropagation();
states.showAccount = moved;
}}
/>
)}
{!!header && !/missing\.png$/.test(header) && (
{
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 = window.OffscreenCanvas
? new OffscreenCanvas(1, 1)
: document.createElement('canvas');
const ctx = canvas.getContext('2d', {
willReadFrequently: true,
});
canvas.width = e.target.width;
canvas.height = e.target.height;
ctx.imageSmoothingEnabled = false;
ctx.drawImage(e.target, 0, 0);
// 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 = [
ctx.getImageData(0, 0, pixelDimension, pixelDimension)
.data,
ctx.getImageData(
e.target.width - pixelDimension,
0,
pixelDimension,
pixelDimension,
).data,
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) => {
const [r, g, b, a] = lightenRGB(color);
return `rgba(${r}, ${g}, ${b}, ${a})`;
});
setHeaderCornerColors(rgbColors);
console.log({ colors, rgbColors });
} catch (e) {
// Silently fail
}
}}
/>
)}
}
>
{
const handleWithInstance = acct.includes('@')
? `@${acct}`
: `@${acct}@${instance}`;
try {
navigator.clipboard.writeText(handleWithInstance);
showToast(t`Handle copied`);
} catch (e) {
console.error(e);
showToast(t`Unable to copy handle`);
}
}}
>
Copy handle
Go to original profile page
View profile image
View profile header
) : (
)}
{!!memorial && (
In Memoriam
)}
{!!bot && (
Automated
)}
{!!group && (
Group
)}
{roles?.map((role) => (
{role.name}
{!!accountInstance && (
<>
{' '}
{accountInstance}
>
)}
))}
{!!postingStats && (
{
// states.showAccount = false;
// }}
>
{hasPostingStats ? (
{postingStats.daysSinceLastPost < 365
? plural(postingStats.total, {
one: plural(postingStats.daysSinceLastPost, {
one: `Last 1 post in the past 1 day`,
other: `Last 1 post in the past ${postingStats.daysSinceLastPost} days`,
}),
other: plural(
postingStats.daysSinceLastPost,
{
one: `Last ${postingStats.total} posts in the past 1 day`,
other: `Last ${postingStats.total} posts in the past ${postingStats.daysSinceLastPost} days`,
},
),
})
: plural(postingStats.total, {
one: 'Last 1 post in the past year(s)',
other: `Last ${postingStats.total} posts in the past year(s)`,
})}
{' '}
Original
{' '}
{' '}
Replies
{' '}
{' '}
Boosts
) : (
Post stats unavailable.
)}
)}
{!moved && (
)}
>
)
)}
);
}
const FAMILIAR_FOLLOWERS_LIMIT = 3;
function RelatedActions({
info,
instance,
standalone,
authenticated,
onRelationshipChange = () => {},
onProfileUpdate = () => {},
}) {
if (!info) return null;
const { _ } = useLingui();
const {
masto: currentMasto,
instance: currentInstance,
authenticated: currentAuthenticated,
} = api();
const sameInstance = instance === currentInstance;
const [relationshipUIState, setRelationshipUIState] = useState('default');
const [relationship, setRelationship] = useState(null);
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,
note: privateNote,
} = relationship || {};
const [currentInfo, setCurrentInfo] = useState(null);
const [isSelf, setIsSelf] = useState(false);
const acctWithInstance = acct.includes('@') ? acct : `${acct}@${instance}`;
useEffect(() => {
if (info) {
const currentAccount = getCurrentAccountID();
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.fetch({
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!
setIsSelf(true);
return;
}
accountID.current = currentID;
// if (moved) return;
setRelationshipUIState('loading');
const fetchRelationships = currentMasto.v1.accounts.relationships.fetch(
{
id: [currentID],
},
);
try {
const relationships = await fetchRelationships;
console.log('fetched relationship', relationships);
setRelationshipUIState('default');
if (relationships.length) {
const relationship = relationships[0];
setRelationship(relationship);
onRelationshipChange({ relationship, currentID });
}
} catch (e) {
console.error(e);
setRelationshipUIState('error');
}
})();
}
}, [info, authenticated]);
useEffect(() => {
if (info && isSelf) {
updateAccount(info);
}
}, [info, isSelf]);
const loading = relationshipUIState === 'loading';
const [showTranslatedBio, setShowTranslatedBio] = useState(false);
const [showAddRemoveLists, setShowAddRemoveLists] = useState(false);
const [showPrivateNoteModal, setShowPrivateNoteModal] = useState(false);
const [showEditProfile, setShowEditProfile] = useState(false);
const [lists, setLists] = useState([]);
return (
<>
{followedBy ? (
Follows you
) : !!lastStatusAt ? (
Last post:{' '}
{niceDateTime(lastStatusAt, {
hideTime: true,
})}
) : (
)}
{muting && (
Muted
)}
{blocking && (
Blocked
)}
{' '}
{!!privateNote && (
{
setShowPrivateNoteModal(true);
}}
dir="auto"
>
{privateNote}
)}
}
onMenuChange={(e) => {
if (following && e.open) {
// Fetch lists that have this account
(async () => {
try {
const lists = await currentMasto.v1.accounts
.$select(accountID.current)
.lists.list();
console.log('fetched account lists', lists);
setLists(lists);
} catch (e) {
console.error(e);
}
})();
}
}}
>
{currentAuthenticated && !isSelf && (
<>
{
showCompose({
draftStatus: {
status: `@${currentInfo?.acct || acct} `,
},
});
}}
>
Mention @{username}
{
setShowTranslatedBio(true);
}}
>
Translate bio
{supports('@mastodon/profile-private-note') && (
{
setShowPrivateNoteModal(true);
}}
>
{privateNote ? t`Edit private note` : t`Add private note`}
)}
{following && !!relationship && (
<>
{
setRelationshipUIState('loading');
(async () => {
try {
const rel = await currentMasto.v1.accounts
.$select(accountID.current)
.follow({
notify: !notifying,
});
if (rel) setRelationship(rel);
setRelationshipUIState('default');
showToast(
rel.notifying
? t`Notifications enabled for @${username}'s posts.`
: t` Notifications disabled for @${username}'s posts.`,
);
} catch (e) {
alert(e);
setRelationshipUIState('error');
}
})();
}}
>
{notifying
? t`Disable notifications`
: t`Enable notifications`}
{
setRelationshipUIState('loading');
(async () => {
try {
const rel = await currentMasto.v1.accounts
.$select(accountID.current)
.follow({
reblogs: !showingReblogs,
});
if (rel) setRelationship(rel);
setRelationshipUIState('default');
showToast(
rel.showingReblogs
? t`Boosts from @${username} enabled.`
: t`Boosts from @${username} disabled.`,
);
} catch (e) {
alert(e);
setRelationshipUIState('error');
}
})();
}}
>
{showingReblogs ? t`Disable boosts` : t`Enable boosts`}
>
)}
{/* Add/remove from lists is only possible if following the account */}
{following && (
{
setShowAddRemoveLists(true);
}}
>
{lists.length ? (
<>
{lists.length}
>
) : (
Add/Remove from Lists
)}
)}
>
)}
{
const handle = `@${currentInfo?.acct || acctWithInstance}`;
try {
navigator.clipboard.writeText(handle);
showToast(t`Handle copied`);
} catch (e) {
console.error(e);
showToast(t`Unable to copy handle`);
}
}}
>
Copy handle
@{currentInfo?.acct || acctWithInstance}
{!!relationship && (
<>
{muting ? (
{
setRelationshipUIState('loading');
(async () => {
try {
const newRelationship = await currentMasto.v1.accounts
.$select(currentInfo?.id || id)
.unmute();
console.log('unmuting', newRelationship);
setRelationship(newRelationship);
setRelationshipUIState('default');
showToast(t`Unmuted @${username}`);
states.reloadGenericAccounts.id = 'mute';
states.reloadGenericAccounts.counter++;
} catch (e) {
console.error(e);
setRelationshipUIState('error');
}
})();
}}
>
Unmute @{username}
) : (
>
}
>
)}
{followedBy && (
Remove @{username} {' '}
from followers?
>
}
onClick={() => {
setRelationshipUIState('loading');
(async () => {
try {
const newRelationship = await currentMasto.v1.accounts
.$select(currentInfo?.id || id)
.removeFromFollowers();
console.log(
'removing from followers',
newRelationship,
);
setRelationship(newRelationship);
setRelationshipUIState('default');
showToast(t`@${username} removed from followers`);
states.reloadGenericAccounts.id = 'followers';
states.reloadGenericAccounts.counter++;
} catch (e) {
console.error(e);
setRelationshipUIState('error');
}
})();
}}
>
Remove follower…
)}
Block @{username} ?
>
}
menuItemClassName="danger"
onClick={() => {
// if (!blocking && !confirm(`Block @${username}?`)) {
// return;
// }
setRelationshipUIState('loading');
(async () => {
try {
if (blocking) {
const newRelationship = await currentMasto.v1.accounts
.$select(currentInfo?.id || id)
.unblock();
console.log('unblocking', newRelationship);
setRelationship(newRelationship);
setRelationshipUIState('default');
showToast(t`Unblocked @${username}`);
} else {
const newRelationship = await currentMasto.v1.accounts
.$select(currentInfo?.id || id)
.block();
console.log('blocking', newRelationship);
setRelationship(newRelationship);
setRelationshipUIState('default');
showToast(t`Blocked @${username}`);
}
states.reloadGenericAccounts.id = 'block';
states.reloadGenericAccounts.counter++;
} catch (e) {
console.error(e);
setRelationshipUIState('error');
if (blocking) {
showToast(t`Unable to unblock @${username}`);
} else {
showToast(t`Unable to block @${username}`);
}
}
})();
}}
>
{blocking ? (
<>
Unblock @{username}
>
) : (
<>
Block @{username} …
>
)}
{
states.showReportModal = {
account: currentInfo || info,
};
}}
>
Report @{username} …
>
)}
{currentAuthenticated &&
isSelf &&
standalone &&
supports('@mastodon/profile-edit') && (
<>
{
setShowEditProfile(true);
}}
>
Edit profile
>
)}
{import.meta.env.DEV && currentAuthenticated && isSelf && (
<>
{
const relationships =
await currentMasto.v1.accounts.relationships.fetch({
id: [accountID.current],
});
const { note } = relationships[0] || {};
if (note) {
alert(note);
console.log(note);
}
}}
>
See note
>
)}
{!relationship && relationshipUIState === 'loading' && (
)}
{!!relationship && !moved && (
{requested
? t`Withdraw follow request?`
: t`Unfollow @${info.acct || info.username}?`}
}
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
.$select(accountID.current)
.unfollow();
// }
} else {
newRelationship = await currentMasto.v1.accounts
.$select(accountID.current)
.follow();
}
if (newRelationship) setRelationship(newRelationship);
setRelationshipUIState('default');
} catch (e) {
alert(e);
setRelationshipUIState('error');
}
})();
}}
>
{following ? (
<>
Following
Unfollow…
>
) : requested ? (
<>
Requested
Withdraw…
>
) : locked ? (
<>
{' '}
Follow
>
) : (
t`Follow`
)}
)}
{!!showTranslatedBio && (
{
setShowTranslatedBio(false);
}}
>
setShowTranslatedBio(false)}
/>
)}
{!!showAddRemoveLists && (
{
setShowAddRemoveLists(false);
}}
>
setShowAddRemoveLists(false)}
/>
)}
{!!showPrivateNoteModal && (
{
setShowPrivateNoteModal(false);
}}
>
{
setRelationship(relationship);
// onRelationshipChange({ relationship, currentID: accountID.current });
}}
onClose={() => setShowPrivateNoteModal(false)}
/>
)}
{!!showEditProfile && (
{
setShowEditProfile(false);
}}
>
{
setShowEditProfile(false);
if (state === 'success' && account) {
onProfileUpdate(account);
}
}}
/>
)}
>
);
}
// 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);
let alpha;
if (luminence >= 220) {
alpha = 1;
} else if (luminence <= 50) {
alpha = 0.1;
} else {
alpha = luminence / 255;
}
alpha = Math.min(1, alpha);
return [r, g, b, alpha];
}
function niceAccountURL(url) {
if (!url) return;
const urlObj = URL.parse(url);
const { host, pathname } = urlObj;
const path = pathname.replace(/\/$/, '').replace(/^\//, '');
return (
<>
{punycode.toUnicode(host)}/
{path}
>
);
}
function TranslatedBioSheet({ note, fields, onClose }) {
const fieldsText =
fields
?.map(({ name, value }) => `${name}\n${getHTMLText(value)}`)
.join('\n\n') || '';
const text = getHTMLText(note) + (fieldsText ? `\n\n${fieldsText}` : '');
return (
{!!onClose && (
)}
{text}
);
}
function AddRemoveListsSheet({ accountID, onClose }) {
const { masto } = api();
const [uiState, setUIState] = useState('default');
const [lists, setLists] = useState([]);
const [listsContainingAccount, setListsContainingAccount] = useState([]);
const [reloadCount, reload] = useReducer((c) => c + 1, 0);
useEffect(() => {
setUIState('loading');
(async () => {
try {
const lists = await getLists();
setLists(lists);
const listsContainingAccount = await masto.v1.accounts
.$select(accountID)
.lists.list();
console.log({ lists, listsContainingAccount });
setListsContainingAccount(listsContainingAccount);
setUIState('default');
} catch (e) {
console.error(e);
setUIState('error');
}
})();
}, [reloadCount]);
const [showListAddEditModal, setShowListAddEditModal] = useState(false);
return (
{!!onClose && (
)}
{lists.length > 0 ? (
{lists.map((list) => {
const inList = listsContainingAccount.some(
(l) => l.id === list.id,
);
return (
{
setUIState('loading');
(async () => {
try {
if (inList) {
await masto.v1.lists
.$select(list.id)
.accounts.remove({
accountIds: [accountID],
});
} else {
await masto.v1.lists
.$select(list.id)
.accounts.create({
accountIds: [accountID],
});
}
// setUIState('default');
reload();
} catch (e) {
console.error(e);
setUIState('error');
alert(
inList
? t`Unable to remove from list.`
: t`Unable to add to list.`,
);
}
})();
}}
>
{list.title}
);
})}
) : uiState === 'loading' ? (
) : uiState === 'error' ? (
Unable to load lists.
) : (
No lists.
)}
setShowListAddEditModal(true)}
disabled={uiState !== 'default'}
>
{' '}
New list
{showListAddEditModal && (
{
if (e.target === e.currentTarget) {
setShowListAddEditModal(false);
}
}}
>
{
if (result.state === 'success') {
reload();
}
setShowListAddEditModal(false);
}}
/>
)}
);
}
function PrivateNoteSheet({
account,
note: initialNote,
onRelationshipChange = () => {},
onClose = () => {},
}) {
const { masto } = api();
const [uiState, setUIState] = useState('default');
const textareaRef = useRef(null);
useEffect(() => {
let timer;
if (textareaRef.current && !initialNote) {
timer = setTimeout(() => {
textareaRef.current.focus?.();
}, 100);
}
return () => {
clearTimeout(timer);
};
}, []);
return (
{!!onClose && (
)}
Private note about{' '}
@{account?.username || account?.acct}
);
}
function EditProfileSheet({ onClose = () => {} }) {
const { masto } = api();
const [uiState, setUIState] = useState('loading');
const [account, setAccount] = useState(null);
useEffect(() => {
(async () => {
try {
const acc = await masto.v1.accounts.verifyCredentials();
setAccount(acc);
setUIState('default');
} catch (e) {
console.error(e);
setUIState('error');
}
})();
}, []);
console.log('EditProfileSheet', account);
const { displayName, source } = account || {};
const { note, fields } = source || {};
const fieldsAttributesRef = useRef(null);
return (
{!!onClose && (
)}
{uiState === 'loading' ? (
) : (
{
e.preventDefault();
const formData = new FormData(e.target);
const displayName = formData.get('display_name');
const note = formData.get('note');
const fieldsAttributesFields =
fieldsAttributesRef.current.querySelectorAll(
'input[name^="fields_attributes"]',
);
const fieldsAttributes = [];
fieldsAttributesFields.forEach((field) => {
const name = field.name;
const [_, index, key] =
name.match(/fields_attributes\[(\d+)\]\[(.+)\]/) || [];
const value = field.value ? field.value.trim() : '';
if (index && key && value) {
if (!fieldsAttributes[index]) fieldsAttributes[index] = {};
fieldsAttributes[index][key] = value;
}
});
// Fill in the blanks
fieldsAttributes.forEach((field) => {
if (field.name && !field.value) {
field.value = '';
}
});
(async () => {
try {
const newAccount = await masto.v1.accounts.updateCredentials({
displayName,
note,
fieldsAttributes,
});
console.log('updated account', newAccount);
onClose?.({
state: 'success',
account: newAccount,
});
} catch (e) {
console.error(e);
alert(e?.message || t`Unable to update profile.`);
}
})();
}}
>
Name {' '}
Bio
{/* Table for fields; name and values are in fields, min 4 rows */}
Extra fields
Label
Content
{Array.from({ length: Math.max(4, fields.length) }).map(
(_, i) => {
const { name = '', value = '' } = fields[i] || {};
return (
);
},
)}
{
onClose?.();
}}
>
Cancel
Save
)}
);
}
function FieldsAttributesRow({ name, value, disabled, index: i }) {
const [hasValue, setHasValue] = useState(!!value);
return (
setHasValue(!!e.currentTarget.value)}
dir="auto"
/>
);
}
function AccountHandleInfo({ acct, instance }) {
// acct = username or username@server
let [username, server] = acct.split('@');
if (!server) server = instance;
return (
{username}
@
{server}
username
{' '}
{' '}
server domain name
);
}
export default AccountInfo;