stacker.news/components/switch-account.js

156 lines
4.9 KiB
JavaScript
Raw Normal View History

2023-11-21 00:43:26 +01:00
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
2023-11-17 05:00:53 +01:00
import AnonIcon from '../svgs/spy-fill.svg'
import { useRouter } from 'next/router'
import cookie from 'cookie'
import { useMe } from './me'
2023-11-17 05:00:53 +01:00
import Image from 'react-bootstrap/Image'
import Link from 'next/link'
2023-11-19 05:49:35 +01:00
import { SSR } from '../lib/constants'
2023-11-21 02:35:10 +01:00
import { USER } from '../fragments/users'
import { useQuery } from '@apollo/client'
2023-11-17 05:00:53 +01:00
const AccountContext = createContext()
const b64Decode = str => Buffer.from(str, 'base64').toString('utf-8')
2023-11-17 05:00:53 +01:00
export const AccountProvider = ({ children }) => {
const { me } = useMe()
const [accounts, setAccounts] = useState([])
2023-11-21 00:43:26 +01:00
const [isAnon, setIsAnon] = useState(true)
2023-11-17 05:00:53 +01:00
const updateAccountsFromCookie = useCallback(() => {
try {
const { multi_auth: multiAuthCookie } = cookie.parse(document.cookie)
const accounts = multiAuthCookie
? JSON.parse(b64Decode(multiAuthCookie))
: me ? [{ id: me.id, name: me.name, photoId: me.photoId }] : []
setAccounts(accounts)
} catch (err) {
console.error('error parsing cookies:', err)
}
}, [setAccounts])
useEffect(() => {
updateAccountsFromCookie()
2023-11-17 05:00:53 +01:00
}, [])
const addAccount = useCallback(user => {
setAccounts(accounts => [...accounts, user])
}, [setAccounts])
const removeAccount = useCallback(userId => {
setAccounts(accounts => accounts.filter(({ id }) => id !== userId))
}, [setAccounts])
const multiAuthSignout = useCallback(async () => {
// document.cookie = 'multi_auth.user-id='
// switch to next available account
const { status } = await fetch('/api/signout', { credentials: 'include' })
if (status === 201) updateAccountsFromCookie()
return status
}, [updateAccountsFromCookie])
2023-11-21 05:02:51 +01:00
2023-11-21 00:43:26 +01:00
useEffect(() => {
2023-11-19 05:49:35 +01:00
// document not defined on server
2023-11-21 00:43:26 +01:00
if (SSR) return
const { 'multi_auth.user-id': multiAuthUserIdCookie } = cookie.parse(document.cookie)
2023-11-21 00:43:26 +01:00
setIsAnon(multiAuthUserIdCookie === 'anonymous')
}, [])
return <AccountContext.Provider value={{ accounts, addAccount, removeAccount, isAnon, setIsAnon, multiAuthSignout }}>{children}</AccountContext.Provider>
2023-11-17 05:00:53 +01:00
}
export const useAccounts = () => useContext(AccountContext)
2023-11-17 05:00:53 +01:00
2023-11-21 00:43:26 +01:00
const AnonAccount = ({ selected, onClick }) => {
const { isAnon, setIsAnon } = useAccounts()
const { refreshMe } = useMe()
2023-11-17 05:00:53 +01:00
return (
<div
className='d-flex flex-column me-2 my-1 text-center'
>
<AnonIcon
className='fill-muted'
2023-11-21 00:43:26 +01:00
width='135' height='135' style={{ cursor: 'pointer' }} onClick={async () => {
2023-11-21 05:19:16 +01:00
document.cookie = 'multi_auth.user-id=anonymous; Path=/; Secure'
2023-11-21 00:43:26 +01:00
// order is important to prevent flashes of no session
setIsAnon(true)
await refreshMe()
2023-11-17 05:00:53 +01:00
}}
/>
<div className='fst-italic'>anonymous</div>
2023-11-21 00:43:26 +01:00
{isAnon && <div className='text-muted fst-italic'>selected</div>}
2023-11-17 05:00:53 +01:00
</div>
)
}
const Account = ({ account, className }) => {
const { me } = useMe()
2023-11-21 02:35:10 +01:00
const [name, setName] = useState(account.name)
const [src, setSrc] = useState(account.photoId || '/dorian400.jpg')
const { refreshMe } = useMe()
2023-11-21 00:43:26 +01:00
const { setIsAnon } = useAccounts()
2023-11-21 02:35:10 +01:00
useQuery(USER,
{
variables: { id: account.id },
onCompleted ({ user: { name, photoId } }) {
if (photoId) {
const src = `https://${process.env.NEXT_PUBLIC_MEDIA_DOMAIN}/${photoId}`
setSrc(src)
}
setName(name)
}
}
)
2023-11-17 05:00:53 +01:00
return (
<div
className='d-flex flex-column me-2 my-1 text-center'
>
<Image
2023-11-21 00:43:26 +01:00
width='135' height='135' src={src} style={{ cursor: 'pointer' }} onClick={async () => {
2023-11-21 05:19:16 +01:00
document.cookie = `multi_auth.user-id=${account.id}; Path=/; Secure`
2023-11-21 00:43:26 +01:00
await refreshMe()
// order is important to prevent flashes of inconsistent data in switch account dialog
setIsAnon(false)
2023-11-17 05:00:53 +01:00
}}
/>
2023-11-21 02:35:10 +01:00
<Link href={`/${account.name}`}>@{name}</Link>
2023-11-17 05:00:53 +01:00
{Number(me?.id) === Number(account.id) && <div className='text-muted fst-italic'>selected</div>}
</div>
)
}
const AddAccount = () => {
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={() => {
2023-11-17 05:00:53 +01:00
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()
return (
<>
<div className='my-2'>
<div className='d-flex flex-row flex-wrap'>
<AnonAccount />
{
accounts.map((account) => <Account key={account.id} account={account} />)
}
<AddAccount />
</div>
</div>
</>
)
}