Add account info into Account statuses page

This commit is contained in:
Lim Chee Aun 2023-03-11 14:05:56 +08:00
parent b4f8f92431
commit 6fd9c106c6
8 changed files with 401 additions and 194 deletions

View file

@ -16,7 +16,7 @@ import {
} from 'react-router-dom';
import { useSnapshot } from 'valtio';
import Account from './components/account';
import AccountSheet from './components/account-sheet';
import Compose from './components/compose';
import Drafts from './components/drafts';
import Loader from './components/loader';
@ -409,7 +409,7 @@ function App() {
}
}}
>
<Account
<AccountSheet
account={snapStates.showAccount?.account || snapStates.showAccount}
instance={snapStates.showAccount?.instance}
onClose={() => {

View file

@ -1,5 +1,7 @@
import './account-block.css';
import { useNavigate } from 'react-router-dom';
import emojifyText from '../utils/emojify-text';
import niceDateTime from '../utils/nice-date-time';
import states from '../utils/states';
@ -12,6 +14,7 @@ function AccountBlock({
avatarSize = 'xl',
instance,
external,
internal,
onClick,
showActivity = false,
}) {
@ -22,13 +25,16 @@ function AccountBlock({
<span>
<b></b>
<br />
@
<span class="account-block-acct">@</span>
</span>
</div>
);
}
const navigate = useNavigate();
const {
id,
acct,
avatar,
avatarStatic,
@ -40,6 +46,7 @@ function AccountBlock({
lastStatusAt,
} = account;
const displayNameWithEmoji = emojifyText(displayName, emojis);
const [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct];
return (
<a
@ -51,10 +58,14 @@ function AccountBlock({
if (external) return;
e.preventDefault();
if (onClick) return onClick(e);
states.showAccount = {
account,
instance,
};
if (internal) {
navigate(`/${instance}/a/${id}`);
} else {
states.showAccount = {
account,
instance,
};
}
}}
>
<Avatar url={avatar} size={avatarSize} />
@ -68,7 +79,12 @@ function AccountBlock({
) : (
<b>{username}</b>
)}
<br />@{acct}
<br />
<span class="account-block-acct">
@{acct1}
<wbr />
{acct2}
</span>
{showActivity && (
<>
<br />

View file

@ -0,0 +1,225 @@
.account-container {
display: flex;
flex-direction: column;
overflow: hidden;
max-width: 100%;
}
.account-container.skeleton {
color: var(--outline-color);
}
.account-container .header-banner {
/* pointer-events: none; */
aspect-ratio: 6 / 1;
width: 100%;
height: auto;
object-fit: cover;
/* mask fade out bottom of banner */
mask-image: linear-gradient(
to bottom,
hsl(0, 0%, 0%) 0%,
hsla(0, 0%, 0%, 0.987) 14%,
hsla(0, 0%, 0%, 0.951) 26.2%,
hsla(0, 0%, 0%, 0.896) 36.8%,
hsla(0, 0%, 0%, 0.825) 45.9%,
hsla(0, 0%, 0%, 0.741) 53.7%,
hsla(0, 0%, 0%, 0.648) 60.4%,
hsla(0, 0%, 0%, 0.55) 66.2%,
hsla(0, 0%, 0%, 0.45) 71.2%,
hsla(0, 0%, 0%, 0.352) 75.6%,
hsla(0, 0%, 0%, 0.259) 79.6%,
hsla(0, 0%, 0%, 0.175) 83.4%,
hsla(0, 0%, 0%, 0.104) 87.2%,
hsla(0, 0%, 0%, 0.049) 91.1%,
hsla(0, 0%, 0%, 0.013) 95.3%,
hsla(0, 0%, 0%, 0) 100%
);
margin-bottom: -44px;
}
.account-container .header-banner:hover {
animation: position-object 5s ease-in-out 1s 5;
}
@media (min-height: 480px) {
.account-container .header-banner {
aspect-ratio: 3 / 1;
}
}
.account-container header {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 8px;
text-shadow: -8px 0 12px -6px var(--bg-color), 8px 0 12px -6px var(--bg-color),
-8px 0 24px var(--header-color-3, --bg-color),
8px 0 24px var(--header-color-4, --bg-color);
}
.account-container header .avatar {
box-shadow: -8px 0 24px var(--header-color-3, --bg-color),
8px 0 24px var(--header-color-4, --bg-color);
}
.account-container .note {
font-size: 95%;
line-height: 1.4;
}
.account-container .note:not(:has(p)):not(:empty) {
/* Some notes don't have <p> tags, so we need to add some padding */
padding: 1em 0;
}
.account-container .stats {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
gap: 16px;
opacity: 0.75;
font-size: 90%;
background-color: var(--bg-faded-color);
padding: 12px;
border-radius: 8px;
line-height: 1.25;
}
.account-container .stats > * {
text-align: center;
}
.account-container .stats a {
color: inherit;
}
.account-container .actions {
display: flex;
gap: 8px;
justify-content: space-between;
min-height: 2.5em;
}
.account-container .actions button {
align-self: flex-end;
}
.account-container .profile-metadata {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.account-container .profile-field {
min-width: 0;
flex-grow: 1;
font-size: 90%;
background-color: var(--bg-faded-color);
padding: 12px;
border-radius: 8px;
filter: saturate(0.75);
line-height: 1.25;
}
.account-container :is(.note, .profile-field) .invisible {
display: none;
}
.account-container :is(.note, .profile-field) .ellipsis::after {
content: '…';
}
.account-container .profile-field b {
font-size: 90%;
color: var(--text-insignificant-color);
text-transform: uppercase;
}
.account-container .profile-field b .icon {
color: var(--green-color);
}
.account-container .profile-field p {
margin: 0;
}
.account-container .common-followers {
border-top: 1px solid var(--outline-color);
border-bottom: 1px solid var(--outline-color);
padding: 8px 0;
font-size: 90%;
line-height: 1.5;
color: var(--text-insignificant-color);
}
.timeline-start .account-container {
border-bottom: 1px solid var(--outline-color);
}
.timeline-start .account-container :is(header, main) {
padding: 16px 16px 4px;
}
.timeline-start .account-container .account-block .account-block-acct {
opacity: 0.5;
}
.timeline-start .account-container .actions {
min-height: 0;
}
@keyframes shine {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
.timeline-start .account-container {
position: relative;
overflow: hidden;
}
.timeline-start .account-container:before {
content: '';
position: absolute;
z-index: 2;
width: 100%;
height: 100%;
background-image: linear-gradient(
100deg,
rgba(255, 255, 255, 0) 30%,
rgba(255, 255, 255, 0.25),
rgba(255, 255, 255, 0) 70%
);
top: 0;
left: -100%;
pointer-events: none;
}
.timeline-start .account-container:hover:before {
animation: shine 1s ease-in-out 1s;
}
@media (min-width: 40em) {
.timeline-start .account-container {
--item-radius: 16px;
border: 1px solid var(--divider-color);
margin: 16px 0;
background-color: var(--bg-color);
border-radius: var(--item-radius);
overflow: hidden;
/* box-shadow: 0px 1px var(--bg-blur-color), 0 0 64px var(--bg-color); */
--shadow-offset: 16px;
--shadow-blur: 32px;
--shadow-spread: calc(var(--shadow-blur) * -0.75);
box-shadow: calc(var(--shadow-offset) * -1) var(--shadow-offset)
var(--shadow-blur) var(--shadow-spread)
var(--header-color-1, var(--drop-shadow-color)),
var(--shadow-offset) var(--shadow-offset) var(--shadow-blur)
var(--shadow-spread) var(--header-color-2, var(--drop-shadow-color));
}
.timeline-start .account-container .header-banner {
margin-bottom: -77px;
}
.timeline-start .account-container header .account-block {
font-size: 175%;
margin-bottom: -8px;
line-height: 1.1;
letter-spacing: -1px;
mix-blend-mode: multiply;
gap: 12px;
}
.timeline-start .account-container header .account-block .avatar {
width: 112px !important;
height: 112px !important;
}
}

View file

@ -1,7 +1,6 @@
import './account.css';
import './account-info.css';
import { useEffect, useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import { api } from '../utils/api';
import emojifyText from '../utils/emojify-text';
@ -17,49 +16,32 @@ import Avatar from './avatar';
import Icon from './icon';
import Link from './link';
function Account({ account, instance: propInstance, onClose }) {
const { masto, instance, authenticated } = api({ instance: propInstance });
function AccountInfo({
account,
fetchAccount = () => {},
standalone,
instance,
authenticated,
}) {
const [uiState, setUIState] = useState('default');
const isString = typeof account === 'string';
const [info, setInfo] = useState(isString ? null : account);
useEffect(() => {
if (isString) {
setUIState('loading');
(async () => {
try {
const info = await masto.v1.accounts.lookup({
acct: account,
skip_webfinger: false,
});
setInfo(info);
setUIState('default');
} catch (e) {
try {
const result = await masto.v2.search({
q: account,
type: 'accounts',
limit: 1,
resolve: authenticated,
});
if (result.accounts.length) {
setInfo(result.accounts[0]);
setUIState('default');
return;
}
setInfo(null);
setUIState('error');
} catch (err) {
console.error(err);
setInfo(null);
setUIState('error');
}
}
})();
} else {
setInfo(account);
}
}, [account]);
if (!isString) return;
setUIState('loading');
(async () => {
try {
const info = await fetchAccount();
setInfo(info);
setUIState('default');
} catch (e) {
console.error(e);
setInfo(null);
setUIState('error');
}
})();
}, [isString, fetchAccount]);
const {
acct,
@ -84,13 +66,17 @@ function Account({ account, instance: propInstance, onClose }) {
username,
} = info || {};
const escRef = useHotkeys('esc', onClose, [onClose]);
const [headerCornerColors, setHeaderCornerColors] = useState([]);
return (
<div
ref={escRef}
id="account-container"
class={`sheet ${uiState === 'loading' ? 'skeleton' : ''}`}
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],
}}
>
{uiState === 'error' && (
<div class="ui-state">
@ -128,7 +114,47 @@ function Account({ account, instance: propInstance, onClose }) {
alt=""
class="header-banner"
onError={(e) => {
e.target.src = headerStatic;
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) => {
try {
// Get color from four corners of image
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = e.target.width;
canvas.height = e.target.height;
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,
];
const rgbColors = colors.map((color) => {
return `rgb(${color[0]}, ${color[1]}, ${color[2]}, 0.3)`;
});
setHeaderCornerColors(rgbColors);
console.log({ colors, rgbColors });
} catch (e) {
// Silently fail
}
}}
/>
)}
@ -137,7 +163,8 @@ function Account({ account, instance: propInstance, onClose }) {
account={info}
instance={instance}
avatarSize="xxxl"
external
external={standalone}
internal={!standalone}
/>
</header>
<main tabIndex="-1">
@ -429,4 +456,4 @@ function RelatedActions({ info, instance, authenticated }) {
);
}
export default Account;
export default AccountInfo;

View file

@ -0,0 +1,56 @@
import { useHotkeys } from 'react-hotkeys-hook';
import { api } from '../utils/api';
import AccountInfo from './account-info';
function AccountSheet({ account, instance: propInstance, onClose }) {
const { masto, instance, authenticated } = api({ instance: propInstance });
const isString = typeof account === 'string';
const escRef = useHotkeys('esc', onClose, [onClose]);
return (
<div
ref={escRef}
class="sheet"
onClick={(e) => {
const accountBlock = e.target.closest('.account-block');
if (accountBlock) {
onClose();
}
}}
>
<AccountInfo
instance={instance}
authenticated={authenticated}
account={account}
fetchAccount={async () => {
if (isString) {
try {
const info = await masto.v1.accounts.lookup({
acct: account,
skip_webfinger: false,
});
return info;
} catch (e) {
const result = await masto.v2.search({
q: account,
type: 'accounts',
limit: 1,
resolve: authenticated,
});
if (result.accounts.length) {
return result.accounts[0];
}
}
} else {
return account;
}
}}
/>
</div>
);
}
export default AccountSheet;

View file

@ -1,134 +0,0 @@
#account-container.skeleton {
color: var(--outline-color);
}
#account-container .header-banner {
/* pointer-events: none; */
aspect-ratio: 6 / 1;
width: 100%;
height: auto;
object-fit: cover;
/* mask fade out bottom of banner */
mask-image: linear-gradient(
to bottom,
hsl(0, 0%, 0%) 0%,
hsla(0, 0%, 0%, 0.987) 14%,
hsla(0, 0%, 0%, 0.951) 26.2%,
hsla(0, 0%, 0%, 0.896) 36.8%,
hsla(0, 0%, 0%, 0.825) 45.9%,
hsla(0, 0%, 0%, 0.741) 53.7%,
hsla(0, 0%, 0%, 0.648) 60.4%,
hsla(0, 0%, 0%, 0.55) 66.2%,
hsla(0, 0%, 0%, 0.45) 71.2%,
hsla(0, 0%, 0%, 0.352) 75.6%,
hsla(0, 0%, 0%, 0.259) 79.6%,
hsla(0, 0%, 0%, 0.175) 83.4%,
hsla(0, 0%, 0%, 0.104) 87.2%,
hsla(0, 0%, 0%, 0.049) 91.1%,
hsla(0, 0%, 0%, 0.013) 95.3%,
hsla(0, 0%, 0%, 0) 100%
);
margin-bottom: -44px;
}
#account-container .header-banner:hover {
animation: position-object 5s ease-in-out 1s 5;
}
@media (min-height: 480px) {
#account-container .header-banner {
aspect-ratio: 3 / 1;
}
}
#account-container header {
position: relative;
display: flex;
align-items: center;
gap: 8px;
text-shadow: 0 0 24px var(--bg-color);
}
#account-container header .avatar {
box-shadow: 0 0 24px var(--bg-color);
}
#account-container .note {
font-size: 95%;
line-height: 1.4;
}
#account-container .note:not(:has(p)):not(:empty) {
/* Some notes don't have <p> tags, so we need to add some padding */
padding: 1em 0;
}
#account-container .stats {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
gap: 16px;
opacity: 0.75;
font-size: 90%;
background-color: var(--bg-faded-color);
padding: 12px;
border-radius: 8px;
line-height: 1.25;
}
#account-container .stats > * {
text-align: center;
}
#account-container .stats a {
color: inherit;
}
#account-container .actions {
display: flex;
gap: 8px;
justify-content: space-between;
min-height: 2.5em;
}
#account-container .actions button {
align-self: flex-end;
}
#account-container .profile-metadata {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
#account-container .profile-field {
min-width: 0;
flex-grow: 1;
font-size: 90%;
background-color: var(--bg-faded-color);
padding: 12px;
border-radius: 8px;
filter: saturate(0.75);
line-height: 1.25;
}
#account-container :is(.note, .profile-field) .invisible {
display: none;
}
#account-container :is(.note, .profile-field) .ellipsis::after {
content: '…';
}
#account-container .profile-field b {
font-size: 90%;
color: var(--text-insignificant-color);
text-transform: uppercase;
}
#account-container .profile-field b .icon {
color: var(--green-color);
}
#account-container .profile-field p {
margin: 0;
}
#account-container .common-followers {
border-top: 1px solid var(--outline-color);
border-bottom: 1px solid var(--outline-color);
padding: 8px 0;
font-size: 90%;
line-height: 1.5;
color: var(--text-insignificant-color);
}

View file

@ -27,6 +27,7 @@ function Timeline({
checkForUpdatesInterval = 60_000, // 1 minute
headerStart,
headerEnd,
timelineStart,
}) {
const [items, setItems] = useState([]);
const [uiState, setUIState] = useState('default');
@ -292,6 +293,7 @@ function Timeline({
</button>
)}
</header>
{!!timelineStart && <div class="timeline-start">{timelineStart}</div>}
{!!items.length ? (
<>
<ul class="timeline">

View file

@ -1,7 +1,8 @@
import { useEffect, useRef, useState } from 'preact/hooks';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useParams } from 'react-router-dom';
import { useSnapshot } from 'valtio';
import AccountInfo from '../components/account-info';
import Timeline from '../components/timeline';
import { api } from '../utils/api';
import emojifyText from '../utils/emojify-text';
@ -13,7 +14,7 @@ const LIMIT = 20;
function AccountStatuses() {
const snapStates = useSnapshot(states);
const { id, ...params } = useParams();
const { masto, instance } = api({ instance: params.instance });
const { masto, instance, authenticated } = api({ instance: params.instance });
const accountStatusesIterator = useRef();
async function fetchAccountStatuses(firstLoad) {
const results = [];
@ -27,7 +28,7 @@ function AccountStatuses() {
pinnedStatuses.forEach((status) => {
status._pinned = true;
});
if (pinnedStatuses.length > 1) {
if (pinnedStatuses.length >= 3) {
const pinnedStatusesIds = pinnedStatuses.map((status) => status.id);
results.push({
id: pinnedStatusesIds,
@ -54,7 +55,7 @@ function AccountStatuses() {
};
}
const [account, setAccount] = useState({});
const [account, setAccount] = useState();
useTitle(
`${account?.acct ? '@' + account.acct : 'Posts'}`,
'/:instance?/a/:id',
@ -71,7 +72,20 @@ function AccountStatuses() {
})();
}, [id]);
const { displayName, acct, emojis } = account;
const { displayName, acct, emojis } = account || {};
const TimelineStart = useMemo(
() => (
<AccountInfo
instance={instance}
account={id}
fetchAccount={() => masto.v1.accounts.fetch(id)}
authenticated={authenticated}
standalone
/>
),
[id, instance, authenticated],
);
return (
<Timeline
@ -103,6 +117,7 @@ function AccountStatuses() {
errorText="Unable to load statuses"
fetchItems={fetchAccountStatuses}
boostsCarousel={snapStates.settings.boostsCarousel}
timelineStart={TimelineStart}
/>
);
}