Use list view to render accounts

This commit is contained in:
ekzyis 2023-12-05 05:06:02 +01:00
parent 0e04daebfb
commit 47dc05d285
3 changed files with 69 additions and 90 deletions

View File

@ -28,7 +28,7 @@ import { clearNotifications } from '../lib/badge'
import { useServiceWorker } from './serviceworker' import { useServiceWorker } from './serviceworker'
import SubSelect from './sub-select' import SubSelect from './sub-select'
import { useShowModal } from './modal' import { useShowModal } from './modal'
import SwitchAccountDialog, { useAccounts } from './switch-account' import SwitchAccountList, { useAccounts } from './switch-account'
function WalletSummary ({ me }) { function WalletSummary ({ me }) {
if (!me) return null if (!me) return null
@ -128,7 +128,7 @@ function NavProfileMenu ({ me, dropNavKey }) {
</Link> </Link>
</div> </div>
<Dropdown.Divider /> <Dropdown.Divider />
<Dropdown.Item onClick={() => showModal(onClose => <SwitchAccountDialog onClose={onClose} />)}>switch account</Dropdown.Item> <Dropdown.Item onClick={() => showModal(onClose => <SwitchAccountList onClose={onClose} />)}>switch account</Dropdown.Item>
<Dropdown.Item <Dropdown.Item
onClick={async () => { onClick={async () => {
const status = await multiAuthSignout() const status = await multiAuthSignout()
@ -209,7 +209,7 @@ function LurkerCorner ({ path }) {
</Nav.Link> </Nav.Link>
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu> <Dropdown.Menu>
<Dropdown.Item onClick={() => showModal(onClose => <SwitchAccountDialog onClose={onClose} />)}>switch account</Dropdown.Item> <Dropdown.Item onClick={() => showModal(onClose => <SwitchAccountList onClose={onClose} />)}>switch account</Dropdown.Item>
</Dropdown.Menu> </Dropdown.Menu>
</Dropdown> </Dropdown>
</div> </div>

View File

@ -1,13 +1,11 @@
import { createContext, useCallback, useContext, useEffect, useState } from 'react' import { createContext, useCallback, useContext, useEffect, useState } from 'react'
import AnonIcon from '../svgs/spy-fill.svg'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import cookie from 'cookie' import cookie from 'cookie'
import { useMe } from './me' import { useMe } from './me'
import Image from 'react-bootstrap/Image' import { ANON_USER_ID, SSR } from '../lib/constants'
import Link from 'next/link'
import { SSR } from '../lib/constants'
import { USER } from '../fragments/users' import { USER } from '../fragments/users'
import { useQuery } from '@apollo/client' import { useQuery } from '@apollo/client'
import { UserListRow } from './user-list'
const AccountContext = createContext() const AccountContext = createContext()
@ -62,92 +60,67 @@ export const AccountProvider = ({ children }) => {
export const useAccounts = () => useContext(AccountContext) export const useAccounts = () => useContext(AccountContext)
const AnonAccount = ({ selected, onClick }) => { const AccountListRow = ({ account, ...props }) => {
const { isAnon, setIsAnon } = useAccounts() const { isAnon, setIsAnon } = useAccounts()
const { refreshMe } = useMe() const { me, refreshMe } = useMe()
return ( const anonRow = account.id === ANON_USER_ID
<div const selected = (isAnon && anonRow) || Number(me?.id) === Number(account.id)
className='d-flex flex-column me-2 my-1 text-center'
>
<AnonIcon
className='fill-muted'
width='135' height='135' style={{ cursor: 'pointer' }} onClick={async () => {
document.cookie = 'multi_auth.user-id=anonymous; Path=/; Secure'
// order is important to prevent flashes of no session
setIsAnon(true)
await refreshMe()
}}
/>
<div className='fst-italic'>anonymous</div>
{isAnon && <div className='text-muted fst-italic'>selected</div>}
</div>
)
}
const Account = ({ account, className }) => { // fetch updated names and photo ids since they might have changed since we were issued the JWTs
const { me } = useMe()
const [name, setName] = useState(account.name) const [name, setName] = useState(account.name)
const [src, setSrc] = useState(account.photoId || '/dorian400.jpg') const [photoId, setPhotoId] = useState(account.photoId)
const { refreshMe } = useMe()
const { setIsAnon } = useAccounts()
useQuery(USER, useQuery(USER,
{ {
variables: { id: account.id }, variables: { id: account.id },
onCompleted ({ user: { name, photoId } }) { onCompleted ({ user: { name, photoId } }) {
if (photoId) { if (photoId) setPhotoId(photoId)
const src = `https://${process.env.NEXT_PUBLIC_MEDIA_DOMAIN}/${photoId}` if (name) setName(name)
setSrc(src)
}
setName(name)
} }
} }
) )
const onClick = async () => {
document.cookie = `multi_auth.user-id=${anonRow ? 'anonymous' : account.id}; Path=/; Secure`
if (anonRow) {
// order is important to prevent flashes of no session
setIsAnon(true)
await refreshMe()
} else {
await refreshMe()
// order is important to prevent flashes of inconsistent data in switch account dialog
setIsAnon(account.id === ANON_USER_ID)
}
}
// can't show hat since we don't have access to the streak from the data available in the cookies
return ( return (
<div <div className='d-flex flex-row'>
className='d-flex flex-column me-2 my-1 text-center' <UserListRow user={{ ...account, photoId, name }} className='d-flex align-items-center me-2' {...props} />
> <div className='me-2' style={{ cursor: 'pointer' }} onClick={onClick}>switch</div>
<Image {selected && <div className='text-muted fst-italic text-muted'>selected</div>}
width='135' height='135' src={src} style={{ cursor: 'pointer' }} onClick={async () => {
document.cookie = `multi_auth.user-id=${account.id}; Path=/; Secure`
await refreshMe()
// order is important to prevent flashes of inconsistent data in switch account dialog
setIsAnon(false)
}}
/>
<Link href={`/${account.name}`}>@{name}</Link>
{Number(me?.id) === Number(account.id) && <div className='text-muted fst-italic'>selected</div>}
</div> </div>
) )
} }
const AddAccount = () => { export default function SwitchAccountList () {
const router = useRouter()
return (
<div className='d-flex flex-column me-2 my-1 text-center'>
<Image
width='135' height='135' src='/Portrait_Placeholder.webp' style={{ cursor: 'pointer' }} onClick={() => {
router.push({
pathname: '/login',
query: { callbackUrl: window.location.origin + router.asPath, multiAuth: true }
})
}}
/>
<div className='fst-italic'>+ add account</div>
</div>
)
}
export default function SwitchAccountDialog () {
const { accounts } = useAccounts() const { accounts } = useAccounts()
const router = useRouter()
const addAccount = () => {
router.push({
pathname: '/login',
query: { callbackUrl: window.location.origin + router.asPath, multiAuth: true }
})
}
return ( return (
<> <>
<div className='my-2'> <div className='my-2'>
<div className='d-flex flex-row flex-wrap'> <div className='d-flex flex-column flex-wrap'>
<AnonAccount />
<AccountListRow account={{ id: ANON_USER_ID, name: 'anon' }} showHat={false} />
{ {
accounts.map((account) => <Account key={account.id} account={account} />)
accounts.map((account) => <AccountListRow key={account.id} account={account} showHat={false} />)
} }
<AddAccount /> <div style={{ cursor: 'pointer' }} onClick={addAccount}>+ add account</div>
</div> </div>
</div> </div>
</> </>

View File

@ -36,6 +36,29 @@ function seperate (arr, seperator) {
return arr.flatMap((x, i) => i < arr.length - 1 ? [x, seperator] : [x]) return arr.flatMap((x, i) => i < arr.length - 1 ? [x, seperator] : [x])
} }
export const UserListRow = ({ user, stats, className, showHat = true }) => {
return (
<div className={`${styles.item} mb-2`} key={user.name}>
<Link href={`/${user.name}`}>
<Image
src={user.photoId ? `https://${process.env.NEXT_PUBLIC_MEDIA_DOMAIN}/${user.photoId}` : '/dorian400.jpg'} width='32' height='32'
className={`${userStyles.userimg} me-2`}
/>
</Link>
<div className={`${styles.hunk} ${className}`}>
<Link href={`/${user.name}`} className={`${styles.title} d-inline-flex align-items-center text-reset`}>
@{user.name}{showHat && <Hat className='ms-1 fill-grey' height={14} width={14} user={user} />}
</Link>
{stats && (
<div className={styles.other}>
{stats.map((Comp, i) => <Comp key={i} user={user} />)}
</div>
)}
</div>
</div>
)
}
export default function UserList ({ ssrData, query, variables, destructureData }) { export default function UserList ({ ssrData, query, variables, destructureData }) {
const { data, fetchMore } = useQuery(query, { variables }) const { data, fetchMore } = useQuery(query, { variables })
const dat = useData(data, ssrData) const dat = useData(data, ssrData)
@ -62,24 +85,7 @@ export default function UserList ({ ssrData, query, variables, destructureData }
return ( return (
<> <>
{users?.map(user => ( {users?.map(user => <UserListRow key={user.id} user={user} stats={statComps} />)}
<div className={`${styles.item} mb-2`} key={user.name}>
<Link href={`/${user.name}`}>
<Image
src={user.photoId ? `https://${process.env.NEXT_PUBLIC_MEDIA_DOMAIN}/${user.photoId}` : '/dorian400.jpg'} width='32' height='32'
className={`${userStyles.userimg} me-2`}
/>
</Link>
<div className={styles.hunk}>
<Link href={`/${user.name}`} className={`${styles.title} d-inline-flex align-items-center text-reset`}>
@{user.name}<Hat className='ms-1 fill-grey' height={14} width={14} user={user} />
</Link>
<div className={styles.other}>
{statComps.map((Comp, i) => <Comp key={i} user={user} />)}
</div>
</div>
</div>
))}
<MoreFooter cursor={cursor} count={users?.length} fetchMore={fetchMore} Skeleton={UsersSkeleton} noMoreText='NO MORE' /> <MoreFooter cursor={cursor} count={users?.length} fetchMore={fetchMore} Skeleton={UsersSkeleton} noMoreText='NO MORE' />
</> </>
) )