Compare commits
3 Commits
36e9f3f16f
...
b3d9eb0eba
Author | SHA1 | Date | |
---|---|---|---|
|
b3d9eb0eba | ||
|
24aacd8839 | ||
|
a6713f9793 |
@ -130,8 +130,9 @@ export default {
|
|||||||
|
|
||||||
return await models.user.findUnique({ where: { id: me.id } })
|
return await models.user.findUnique({ where: { id: me.id } })
|
||||||
},
|
},
|
||||||
user: async (parent, { name }, { models }) => {
|
user: async (parent, { id, name }, { models }) => {
|
||||||
return await models.user.findUnique({ where: { name } })
|
if (id) id = Number(id)
|
||||||
|
return await models.user.findUnique({ where: { id, name } })
|
||||||
},
|
},
|
||||||
users: async (parent, args, { models }) =>
|
users: async (parent, args, { models }) =>
|
||||||
await models.user.findMany(),
|
await models.user.findMany(),
|
||||||
|
@ -139,7 +139,13 @@ export function getGetServerSideProps (
|
|||||||
|
|
||||||
const client = await getSSRApolloClient({ req, res })
|
const client = await getSSRApolloClient({ req, res })
|
||||||
|
|
||||||
const { data: { me } } = await client.query({ query: ME })
|
let { data: { me } } = await client.query({ query: ME })
|
||||||
|
|
||||||
|
// required to redirect to /signup on page reload
|
||||||
|
// if we switched to anon and authentication is required
|
||||||
|
if (req.cookies['multi_auth.user-id'] === 'anonymous') {
|
||||||
|
me = null
|
||||||
|
}
|
||||||
|
|
||||||
if (authRequired && !me) {
|
if (authRequired && !me) {
|
||||||
let callback = process.env.NEXT_PUBLIC_URL + req.url
|
let callback = process.env.NEXT_PUBLIC_URL + req.url
|
||||||
|
@ -4,7 +4,7 @@ export default gql`
|
|||||||
extend type Query {
|
extend type Query {
|
||||||
me: User
|
me: User
|
||||||
settings: User
|
settings: User
|
||||||
user(name: String!): User
|
user(id: ID, name: String): User
|
||||||
users: [User!]
|
users: [User!]
|
||||||
nameAvailable(name: String!): Boolean!
|
nameAvailable(name: String!): Boolean!
|
||||||
topUsers(cursor: String, when: String, from: String, to: String, by: String, limit: Limit): UsersNullable!
|
topUsers(cursor: String, when: String, from: String, to: String, by: String, limit: Limit): UsersNullable!
|
||||||
|
158
components/account.js
Normal file
158
components/account.js
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import cookie from 'cookie'
|
||||||
|
import { useMe } from '@/components/me'
|
||||||
|
import { USER_ID, SSR } from '@/lib/constants'
|
||||||
|
import { USER } from '@/fragments/users'
|
||||||
|
import { useQuery } from '@apollo/client'
|
||||||
|
import { UserListRow } from '@/components/user-list'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import AddIcon from '@/svgs/add-fill.svg'
|
||||||
|
|
||||||
|
const AccountContext = createContext()
|
||||||
|
|
||||||
|
const b64Decode = str => Buffer.from(str, 'base64').toString('utf-8')
|
||||||
|
const b64Encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64')
|
||||||
|
|
||||||
|
const maybeSecureCookie = cookie => {
|
||||||
|
return window.location.protocol === 'https:' ? cookie + '; Secure' : cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AccountProvider = ({ children }) => {
|
||||||
|
const { me } = useMe()
|
||||||
|
const [accounts, setAccounts] = useState([])
|
||||||
|
const [meAnon, setMeAnon] = useState(true)
|
||||||
|
|
||||||
|
const updateAccountsFromCookie = useCallback(() => {
|
||||||
|
try {
|
||||||
|
const { multi_auth: multiAuthCookie } = cookie.parse(document.cookie)
|
||||||
|
const accounts = multiAuthCookie
|
||||||
|
? JSON.parse(b64Decode(multiAuthCookie))
|
||||||
|
: me ? [{ id: Number(me.id), name: me.name, photoId: me.photoId }] : []
|
||||||
|
setAccounts(accounts)
|
||||||
|
// required for backwards compatibility: sync cookie with accounts if no multi auth cookie exists
|
||||||
|
// this is the case for sessions that existed before we deployed account switching
|
||||||
|
if (!multiAuthCookie && !!me) {
|
||||||
|
document.cookie = maybeSecureCookie(`multi_auth=${b64Encode(accounts)}; Path=/`)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('error parsing cookies:', err)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(updateAccountsFromCookie, [])
|
||||||
|
|
||||||
|
const addAccount = useCallback(user => {
|
||||||
|
setAccounts(accounts => [...accounts, user])
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const removeAccount = useCallback(userId => {
|
||||||
|
setAccounts(accounts => accounts.filter(({ id }) => id !== userId))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const multiAuthSignout = useCallback(async () => {
|
||||||
|
const { status } = await fetch('/api/signout', { credentials: 'include' })
|
||||||
|
// if status is 302, this means the server was able to switch us to the next available account
|
||||||
|
// and the current account was simply removed from the list of available accounts including the corresponding JWT.
|
||||||
|
const switchSuccess = status === 302
|
||||||
|
if (switchSuccess) updateAccountsFromCookie()
|
||||||
|
return switchSuccess
|
||||||
|
}, [updateAccountsFromCookie])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (SSR) return
|
||||||
|
const { 'multi_auth.user-id': multiAuthUserIdCookie } = cookie.parse(document.cookie)
|
||||||
|
setMeAnon(multiAuthUserIdCookie === 'anonymous')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({ accounts, addAccount, removeAccount, meAnon, setMeAnon, multiAuthSignout }),
|
||||||
|
[accounts, addAccount, removeAccount, meAnon, setMeAnon, multiAuthSignout])
|
||||||
|
return <AccountContext.Provider value={value}>{children}</AccountContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAccounts = () => useContext(AccountContext)
|
||||||
|
|
||||||
|
const AccountListRow = ({ account, ...props }) => {
|
||||||
|
const { meAnon, setMeAnon } = useAccounts()
|
||||||
|
const { me, refreshMe } = useMe()
|
||||||
|
const anonRow = account.id === USER_ID.anon
|
||||||
|
const selected = (meAnon && anonRow) || Number(me?.id) === Number(account.id)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// fetch updated names and photo ids since they might have changed since we were issued the JWTs
|
||||||
|
const [name, setName] = useState(account.name)
|
||||||
|
const [photoId, setPhotoId] = useState(account.photoId)
|
||||||
|
useQuery(USER,
|
||||||
|
{
|
||||||
|
variables: { id: account.id },
|
||||||
|
onCompleted ({ user: { name, photoId } }) {
|
||||||
|
if (photoId) setPhotoId(photoId)
|
||||||
|
if (name) setName(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const onClick = async (e) => {
|
||||||
|
// prevent navigation
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// update pointer cookie
|
||||||
|
document.cookie = maybeSecureCookie(`multi_auth.user-id=${anonRow ? 'anonymous' : account.id}; Path=/`)
|
||||||
|
|
||||||
|
// update state
|
||||||
|
if (anonRow) {
|
||||||
|
// order is important to prevent flashes of no session
|
||||||
|
setMeAnon(true)
|
||||||
|
await refreshMe()
|
||||||
|
} else {
|
||||||
|
await refreshMe()
|
||||||
|
// order is important to prevent flashes of inconsistent data in switch account dialog
|
||||||
|
setMeAnon(account.id === USER_ID.anon)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reload whatever page we're on to avoid any bugs due to missing authorization etc.
|
||||||
|
router.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='d-flex flex-row'>
|
||||||
|
<UserListRow
|
||||||
|
user={{ ...account, photoId, name }}
|
||||||
|
className='d-flex align-items-center me-2'
|
||||||
|
{...props}
|
||||||
|
onNymClick={onClick}
|
||||||
|
selected={selected}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SwitchAccountList () {
|
||||||
|
const { accounts } = useAccounts()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// can't show hat since the streak is not included in the JWT payload
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='my-2'>
|
||||||
|
<div className='d-flex flex-column flex-wrap mt-2 mb-3'>
|
||||||
|
<h4 className='text-muted'>Accounts</h4>
|
||||||
|
<AccountListRow account={{ id: USER_ID.anon, name: 'anon' }} showHat={false} />
|
||||||
|
{
|
||||||
|
accounts.map((account) => <AccountListRow key={account.id} account={account} showHat={false} />)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: '/login',
|
||||||
|
query: { callbackUrl: window.location.origin + router.asPath, multiAuth: true }
|
||||||
|
}}
|
||||||
|
className='text-reset fw-bold'
|
||||||
|
>
|
||||||
|
<AddIcon height={20} width={20} /> another account
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -27,7 +27,7 @@ const FormStatus = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AdvPostForm ({ children, item, storageKeyPrefix }) {
|
export default function AdvPostForm ({ children, item, storageKeyPrefix }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const { merge } = useFeeButton()
|
const { merge } = useFeeButton()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [itemType, setItemType] = useState()
|
const [itemType, setItemType] = useState()
|
||||||
|
@ -17,7 +17,7 @@ export function autowithdrawInitial ({ me }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AutowithdrawSettings ({ wallet }) {
|
export function AutowithdrawSettings ({ wallet }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const threshold = autoWithdrawThreshold({ me })
|
const threshold = autoWithdrawThreshold({ me })
|
||||||
|
|
||||||
const [sendThreshold, setSendThreshold] = useState(Math.max(Math.floor(threshold / 10), 1))
|
const [sendThreshold, setSendThreshold] = useState(Math.max(Math.floor(threshold / 10), 1))
|
||||||
|
@ -9,7 +9,7 @@ import { BALANCE_LIMIT_MSATS } from '@/lib/constants'
|
|||||||
import { msatsToSats, numWithUnits } from '@/lib/format'
|
import { msatsToSats, numWithUnits } from '@/lib/format'
|
||||||
|
|
||||||
export function WelcomeBanner ({ Banner }) {
|
export function WelcomeBanner ({ Banner }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const [hidden, setHidden] = useState(true)
|
const [hidden, setHidden] = useState(true)
|
||||||
const handleClose = async () => {
|
const handleClose = async () => {
|
||||||
@ -70,7 +70,7 @@ export function WelcomeBanner ({ Banner }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MadnessBanner ({ handleClose }) {
|
export function MadnessBanner ({ handleClose }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
return (
|
return (
|
||||||
<Alert className={styles.banner} key='info' variant='info' onClose={handleClose} dismissible>
|
<Alert className={styles.banner} key='info' variant='info' onClose={handleClose} dismissible>
|
||||||
<Alert.Heading>
|
<Alert.Heading>
|
||||||
@ -102,7 +102,7 @@ export function MadnessBanner ({ handleClose }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function WalletLimitBanner () {
|
export function WalletLimitBanner () {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const limitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
|
const limitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
|
||||||
if (!me || !limitReached) return
|
if (!me || !limitReached) return
|
||||||
|
@ -23,7 +23,7 @@ export function BountyForm ({
|
|||||||
children
|
children
|
||||||
}) {
|
}) {
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const schema = bountySchema({ client, me, existingBoost: item?.boost })
|
const schema = bountySchema({ client, me, existingBoost: item?.boost })
|
||||||
|
|
||||||
const onSubmit = useItemSubmit(UPSERT_BOUNTY, { item, sub })
|
const onSubmit = useItemSubmit(UPSERT_BOUNTY, { item, sub })
|
||||||
|
@ -96,7 +96,7 @@ export default function Comment ({
|
|||||||
rootText, noComments, noReply, truncate, depth, pin
|
rootText, noComments, noReply, truncate, depth, pin
|
||||||
}) {
|
}) {
|
||||||
const [edit, setEdit] = useState()
|
const [edit, setEdit] = useState()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const isHiddenFreebie = me?.privates?.satsFilter !== 0 && !item.mine && item.freebie && !item.freedFreebie
|
const isHiddenFreebie = me?.privates?.satsFilter !== 0 && !item.mine && item.freebie && !item.freedFreebie
|
||||||
const [collapse, setCollapse] = useState(
|
const [collapse, setCollapse] = useState(
|
||||||
(isHiddenFreebie || item?.user?.meMute || (item?.outlawed && !me?.privates?.wildWestMode)) && !includeParent
|
(isHiddenFreebie || item?.user?.meMute || (item?.outlawed && !me?.privates?.wildWestMode)) && !includeParent
|
||||||
|
@ -22,7 +22,7 @@ export function DiscussionForm ({
|
|||||||
}) {
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const onSubmit = useItemSubmit(UPSERT_DISCUSSION, { item, sub })
|
const onSubmit = useItemSubmit(UPSERT_DISCUSSION, { item, sub })
|
||||||
const schema = discussionSchema({ client, me, existingBoost: item?.boost })
|
const schema = discussionSchema({ client, me, existingBoost: item?.boost })
|
||||||
// if Web Share Target API was used
|
// if Web Share Target API was used
|
||||||
|
@ -64,7 +64,7 @@ export function postCommentUseRemoteLineItems ({ parentId } = {}) {
|
|||||||
export function FeeButtonProvider ({ baseLineItems = {}, useRemoteLineItems = () => null, children }) {
|
export function FeeButtonProvider ({ baseLineItems = {}, useRemoteLineItems = () => null, children }) {
|
||||||
const [lineItems, setLineItems] = useState({})
|
const [lineItems, setLineItems] = useState({})
|
||||||
const [disabled, setDisabled] = useState(false)
|
const [disabled, setDisabled] = useState(false)
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const remoteLineItems = useRemoteLineItems()
|
const remoteLineItems = useRemoteLineItems()
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ function FreebieDialog () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function FeeButton ({ ChildButton = SubmitButton, variant, text, disabled }) {
|
export default function FeeButton ({ ChildButton = SubmitButton, variant, text, disabled }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const { lines, total, disabled: ctxDisabled, free } = useFeeButton()
|
const { lines, total, disabled: ctxDisabled, free } = useFeeButton()
|
||||||
const feeText = free
|
const feeText = free
|
||||||
? 'free'
|
? 'free'
|
||||||
|
@ -808,7 +808,7 @@ export function Form ({
|
|||||||
}) {
|
}) {
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const initialErrorToasted = useRef(false)
|
const initialErrorToasted = useRef(false)
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialError && !initialErrorToasted.current) {
|
if (initialError && !initialErrorToasted.current) {
|
||||||
|
@ -3,7 +3,7 @@ import { abbrNum, numWithUnits } from '@/lib/format'
|
|||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
|
|
||||||
export default function HiddenWalletSummary ({ abbreviate, fixedWidth }) {
|
export default function HiddenWalletSummary ({ abbreviate, fixedWidth }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [hover, setHover] = useState(false)
|
const [hover, setHover] = useState(false)
|
||||||
|
|
||||||
const fixedWidthAbbrSats = useMemo(() => {
|
const fixedWidthAbbrSats = useMemo(() => {
|
||||||
|
@ -11,6 +11,7 @@ import { nextTip, defaultTipIncludingRandom } from './upvote'
|
|||||||
import { ZAP_UNDO_DELAY_MS } from '@/lib/constants'
|
import { ZAP_UNDO_DELAY_MS } from '@/lib/constants'
|
||||||
import { usePaidMutation } from './use-paid-mutation'
|
import { usePaidMutation } from './use-paid-mutation'
|
||||||
import { ACT_MUTATION } from '@/fragments/paidAction'
|
import { ACT_MUTATION } from '@/fragments/paidAction'
|
||||||
|
import { meAnonSats } from '@/lib/apollo'
|
||||||
|
|
||||||
const defaultTips = [100, 1000, 10_000, 100_000]
|
const defaultTips = [100, 1000, 10_000, 100_000]
|
||||||
|
|
||||||
@ -43,14 +44,18 @@ const addCustomTip = (amount) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setItemMeAnonSats = ({ id, amount }) => {
|
const setItemMeAnonSats = ({ id, amount }) => {
|
||||||
|
const reactiveVar = meAnonSats[id]
|
||||||
|
const existingAmount = reactiveVar()
|
||||||
|
reactiveVar(existingAmount + amount)
|
||||||
|
|
||||||
|
// save for next page load
|
||||||
const storageKey = `TIP-item:${id}`
|
const storageKey = `TIP-item:${id}`
|
||||||
const existingAmount = Number(window.localStorage.getItem(storageKey) || '0')
|
|
||||||
window.localStorage.setItem(storageKey, existingAmount + amount)
|
window.localStorage.setItem(storageKey, existingAmount + amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ItemAct ({ onClose, item, down, children, abortSignal }) {
|
export default function ItemAct ({ onClose, item, down, children, abortSignal }) {
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null)
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [oValue, setOValue] = useState()
|
const [oValue, setOValue] = useState()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -203,7 +208,7 @@ export function useAct ({ query = ACT_MUTATION, ...options } = {}) {
|
|||||||
|
|
||||||
export function useZap () {
|
export function useZap () {
|
||||||
const act = useAct()
|
const act = useAct()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const strike = useLightning()
|
const strike = useLightning()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ import { useQuoteReply } from './use-quote-reply'
|
|||||||
import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
||||||
|
|
||||||
function BioItem ({ item, handleClick }) {
|
function BioItem ({ item, handleClick }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
if (!item.text) {
|
if (!item.text) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -33,13 +33,12 @@ export default function ItemInfo ({
|
|||||||
onQuoteReply, extraBadges, nested, pinnable, showActionDropdown = true, showUser = true
|
onQuoteReply, extraBadges, nested, pinnable, showActionDropdown = true, showUser = true
|
||||||
}) {
|
}) {
|
||||||
const editThreshold = new Date(item.invoice?.confirmedAt ?? item.createdAt).getTime() + 10 * 60000
|
const editThreshold = new Date(item.invoice?.confirmedAt ?? item.createdAt).getTime() + 10 * 60000
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [canEdit, setCanEdit] =
|
const [canEdit, setCanEdit] =
|
||||||
useState(item.mine && (Date.now() < editThreshold))
|
useState(item.mine && (Date.now() < editThreshold))
|
||||||
const [hasNewComments, setHasNewComments] = useState(false)
|
const [hasNewComments, setHasNewComments] = useState(false)
|
||||||
const [meTotalSats, setMeTotalSats] = useState(0)
|
|
||||||
const root = useRoot()
|
const root = useRoot()
|
||||||
const retryCreateItem = useRetryCreateItem({ id: item.id })
|
const retryCreateItem = useRetryCreateItem({ id: item.id })
|
||||||
const sub = item?.sub || root?.sub
|
const sub = item?.sub || root?.sub
|
||||||
@ -54,10 +53,6 @@ export default function ItemInfo ({
|
|||||||
setCanEdit(item.mine && (Date.now() < editThreshold))
|
setCanEdit(item.mine && (Date.now() < editThreshold))
|
||||||
}, [item.mine, editThreshold])
|
}, [item.mine, editThreshold])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (item) setMeTotalSats(item.meSats || item.meAnonSats || 0)
|
|
||||||
}, [item?.meSats, item?.meAnonSats])
|
|
||||||
|
|
||||||
// territory founders can pin any post in their territory
|
// territory founders can pin any post in their territory
|
||||||
// and OPs can pin any root reply in their post
|
// and OPs can pin any root reply in their post
|
||||||
const isPost = !item.parentId
|
const isPost = !item.parentId
|
||||||
@ -65,6 +60,7 @@ export default function ItemInfo ({
|
|||||||
const myPost = (me && root && Number(me.id) === Number(root.user.id))
|
const myPost = (me && root && Number(me.id) === Number(root.user.id))
|
||||||
const rootReply = item.path.split('.').length === 2
|
const rootReply = item.path.split('.').length === 2
|
||||||
const canPin = (isPost && mySub) || (myPost && rootReply)
|
const canPin = (isPost && mySub) || (myPost && rootReply)
|
||||||
|
const meSats = (me ? item.meSats : item.meAnonSats) || 0
|
||||||
|
|
||||||
const EditInfo = () => {
|
const EditInfo = () => {
|
||||||
const waitForQrPayment = useQrPayment()
|
const waitForQrPayment = useQrPayment()
|
||||||
@ -131,7 +127,7 @@ export default function ItemInfo ({
|
|||||||
unitPlural: 'stackers'
|
unitPlural: 'stackers'
|
||||||
})} ${item.mine
|
})} ${item.mine
|
||||||
? `\\ ${numWithUnits(item.meSats, { abbreviate: false })} to post`
|
? `\\ ${numWithUnits(item.meSats, { abbreviate: false })} to post`
|
||||||
: `(${numWithUnits(meTotalSats, { abbreviate: false })}${item.meDontLikeSats
|
: `(${numWithUnits(meSats, { abbreviate: false })}${item.meDontLikeSats
|
||||||
? ` & ${numWithUnits(item.meDontLikeSats, { abbreviate: false, unitSingular: 'downsat', unitPlural: 'downsats' })}`
|
? ` & ${numWithUnits(item.meDontLikeSats, { abbreviate: false, unitSingular: 'downsat', unitPlural: 'downsats' })}`
|
||||||
: ''} from me)`} `}
|
: ''} from me)`} `}
|
||||||
>
|
>
|
||||||
@ -229,7 +225,7 @@ export default function ItemInfo ({
|
|||||||
<CrosspostDropdownItem item={item} />}
|
<CrosspostDropdownItem item={item} />}
|
||||||
{me && !item.position &&
|
{me && !item.position &&
|
||||||
!item.mine && !item.deletedAt &&
|
!item.mine && !item.deletedAt &&
|
||||||
(item.meDontLikeSats > meTotalSats
|
(item.meDontLikeSats > meSats
|
||||||
? <DropdownItemUpVote item={item} />
|
? <DropdownItemUpVote item={item} />
|
||||||
: <DontLikeThisDropdownItem item={item} />)}
|
: <DontLikeThisDropdownItem item={item} />)}
|
||||||
{me && sub && !item.mine && !item.outlawed && Number(me.id) === Number(sub.userId) && sub.moderated &&
|
{me && sub && !item.mine && !item.outlawed && Number(me.id) === Number(sub.userId) && sub.moderated &&
|
||||||
@ -273,7 +269,7 @@ export default function ItemInfo ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function InfoDropdownItem ({ item }) {
|
function InfoDropdownItem ({ item }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
|
@ -50,7 +50,7 @@ export function SearchTitle ({ title }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mediaType ({ url, imgproxyUrls }) {
|
function mediaType ({ url, imgproxyUrls }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const src = IMGPROXY_URL_REGEXP.test(url) ? decodeProxyUrl(url) : url
|
const src = IMGPROXY_URL_REGEXP.test(url) ? decodeProxyUrl(url) : url
|
||||||
if (!imgproxyUrls?.[src] ||
|
if (!imgproxyUrls?.[src] ||
|
||||||
me?.privates?.showImagesAndVideos === false ||
|
me?.privates?.showImagesAndVideos === false ||
|
||||||
|
@ -11,7 +11,7 @@ import BackIcon from '@/svgs/arrow-left-line.svg'
|
|||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
|
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
|
||||||
|
|
||||||
function QrAuth ({ k1, encodedUrl, callbackUrl }) {
|
function QrAuth ({ k1, encodedUrl, callbackUrl, multiAuth }) {
|
||||||
const query = gql`
|
const query = gql`
|
||||||
{
|
{
|
||||||
lnAuth(k1: "${k1}") {
|
lnAuth(k1: "${k1}") {
|
||||||
@ -23,7 +23,7 @@ function QrAuth ({ k1, encodedUrl, callbackUrl }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.lnAuth?.pubkey) {
|
if (data?.lnAuth?.pubkey) {
|
||||||
signIn('lightning', { ...data.lnAuth, callbackUrl })
|
signIn('lightning', { ...data.lnAuth, callbackUrl, multiAuth })
|
||||||
}
|
}
|
||||||
}, [data?.lnAuth])
|
}, [data?.lnAuth])
|
||||||
|
|
||||||
@ -101,15 +101,15 @@ function LightningExplainer ({ text, children }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LightningAuthWithExplainer ({ text, callbackUrl }) {
|
export function LightningAuthWithExplainer ({ text, callbackUrl, multiAuth }) {
|
||||||
return (
|
return (
|
||||||
<LightningExplainer text={text}>
|
<LightningExplainer text={text}>
|
||||||
<LightningAuth callbackUrl={callbackUrl} />
|
<LightningAuth callbackUrl={callbackUrl} multiAuth={multiAuth} />
|
||||||
</LightningExplainer>
|
</LightningExplainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LightningAuth ({ callbackUrl }) {
|
export function LightningAuth ({ callbackUrl, multiAuth }) {
|
||||||
// query for challenge
|
// query for challenge
|
||||||
const [createAuth, { data, error }] = useMutation(gql`
|
const [createAuth, { data, error }] = useMutation(gql`
|
||||||
mutation createAuth {
|
mutation createAuth {
|
||||||
@ -125,5 +125,5 @@ export function LightningAuth ({ callbackUrl }) {
|
|||||||
|
|
||||||
if (error) return <div>error</div>
|
if (error) return <div>error</div>
|
||||||
|
|
||||||
return data ? <QrAuth {...data.createAuth} callbackUrl={callbackUrl} /> : <QrSkeleton status='generating' />
|
return data ? <QrAuth {...data.createAuth} callbackUrl={callbackUrl} multiAuth={multiAuth} /> : <QrSkeleton status='generating' />
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ import useDebounceCallback from './use-debounce-callback'
|
|||||||
export function LinkForm ({ item, sub, editThreshold, children }) {
|
export function LinkForm ({ item, sub, editThreshold, children }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const schema = linkSchema({ client, me, existingBoost: item?.boost })
|
const schema = linkSchema({ client, me, existingBoost: item?.boost })
|
||||||
// if Web Share Target API was used
|
// if Web Share Target API was used
|
||||||
const shareUrl = router.query.url
|
const shareUrl = router.query.url
|
||||||
|
@ -49,7 +49,7 @@ export const LoggerProvider = ({ children }) => {
|
|||||||
const ServiceWorkerLoggerContext = createContext()
|
const ServiceWorkerLoggerContext = createContext()
|
||||||
|
|
||||||
function ServiceWorkerLoggerProvider ({ children }) {
|
function ServiceWorkerLoggerProvider ({ children }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [name, setName] = useState()
|
const [name, setName] = useState()
|
||||||
const [os, setOS] = useState()
|
const [os, setOS] = useState()
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import LightningIcon from '@/svgs/bolt.svg'
|
|||||||
import NostrIcon from '@/svgs/nostr.svg'
|
import NostrIcon from '@/svgs/nostr.svg'
|
||||||
import Button from 'react-bootstrap/Button'
|
import Button from 'react-bootstrap/Button'
|
||||||
|
|
||||||
export default function LoginButton ({ text, type, className, onClick }) {
|
export default function LoginButton ({ text, type, className, onClick, disabled }) {
|
||||||
let Icon, variant
|
let Icon, variant
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'twitter':
|
case 'twitter':
|
||||||
@ -29,7 +29,7 @@ export default function LoginButton ({ text, type, className, onClick }) {
|
|||||||
const name = type.charAt(0).toUpperCase() + type.substr(1).toLowerCase()
|
const name = type.charAt(0).toUpperCase() + type.substr(1).toLowerCase()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button className={className} variant={variant} onClick={onClick}>
|
<Button className={className} variant={variant} onClick={onClick} disabled={disabled}>
|
||||||
<Icon
|
<Icon
|
||||||
width={20}
|
width={20}
|
||||||
height={20} className='me-3'
|
height={20} className='me-3'
|
||||||
|
@ -5,11 +5,14 @@ import { useState } from 'react'
|
|||||||
import Alert from 'react-bootstrap/Alert'
|
import Alert from 'react-bootstrap/Alert'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { LightningAuthWithExplainer } from './lightning-auth'
|
import { LightningAuthWithExplainer } from './lightning-auth'
|
||||||
import NostrAuth from './nostr-auth'
|
import { NostrAuthWithExplainer } from './nostr-auth'
|
||||||
import LoginButton from './login-button'
|
import LoginButton from './login-button'
|
||||||
import { emailSchema } from '@/lib/validate'
|
import { emailSchema } from '@/lib/validate'
|
||||||
|
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||||
|
|
||||||
|
export function EmailLoginForm ({ text, callbackUrl, multiAuth }) {
|
||||||
|
const disabled = multiAuth
|
||||||
|
|
||||||
export function EmailLoginForm ({ text, callbackUrl }) {
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
initial={{
|
initial={{
|
||||||
@ -17,7 +20,7 @@ export function EmailLoginForm ({ text, callbackUrl }) {
|
|||||||
}}
|
}}
|
||||||
schema={emailSchema}
|
schema={emailSchema}
|
||||||
onSubmit={async ({ email }) => {
|
onSubmit={async ({ email }) => {
|
||||||
signIn('email', { email, callbackUrl })
|
signIn('email', { email, callbackUrl, multiAuth })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
@ -26,8 +29,9 @@ export function EmailLoginForm ({ text, callbackUrl }) {
|
|||||||
placeholder='email@example.com'
|
placeholder='email@example.com'
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<SubmitButton variant='secondary' className={styles.providerButton}>{text || 'Login'} with Email</SubmitButton>
|
<SubmitButton disabled={disabled} variant='secondary' className={styles.providerButton}>{text || 'Login'} with Email</SubmitButton>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -48,16 +52,16 @@ export function authErrorMessage (error) {
|
|||||||
return error && (authErrorMessages[error] ?? authErrorMessages.default)
|
return error && (authErrorMessages[error] ?? authErrorMessages.default)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Login ({ providers, callbackUrl, error, text, Header, Footer }) {
|
export default function Login ({ providers, callbackUrl, multiAuth, error, text, Header, Footer }) {
|
||||||
const [errorMessage, setErrorMessage] = useState(authErrorMessage(error))
|
const [errorMessage, setErrorMessage] = useState(authErrorMessage(error))
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
if (router.query.type === 'lightning') {
|
if (router.query.type === 'lightning') {
|
||||||
return <LightningAuthWithExplainer callbackUrl={callbackUrl} text={text} />
|
return <LightningAuthWithExplainer callbackUrl={callbackUrl} text={text} multiAuth={multiAuth} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (router.query.type === 'nostr') {
|
if (router.query.type === 'nostr') {
|
||||||
return <NostrAuth callbackUrl={callbackUrl} text={text} />
|
return <NostrAuthWithExplainer callbackUrl={callbackUrl} text={text} multiAuth={multiAuth} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -74,10 +78,16 @@ export default function Login ({ providers, callbackUrl, error, text, Header, Fo
|
|||||||
switch (provider.name) {
|
switch (provider.name) {
|
||||||
case 'Email':
|
case 'Email':
|
||||||
return (
|
return (
|
||||||
|
<OverlayTrigger
|
||||||
|
placement='bottom'
|
||||||
|
overlay={multiAuth ? <Tooltip>not available for account switching yet</Tooltip> : <></>}
|
||||||
|
trigger={['hover', 'focus']}
|
||||||
|
>
|
||||||
<div className='w-100' key={provider.id}>
|
<div className='w-100' key={provider.id}>
|
||||||
<div className='mt-2 text-center text-muted fw-bold'>or</div>
|
<div className='mt-2 text-center text-muted fw-bold'>or</div>
|
||||||
<EmailLoginForm text={text} callbackUrl={callbackUrl} />
|
<EmailLoginForm text={text} callbackUrl={callbackUrl} multiAuth={multiAuth} />
|
||||||
</div>
|
</div>
|
||||||
|
</OverlayTrigger>
|
||||||
)
|
)
|
||||||
case 'Lightning':
|
case 'Lightning':
|
||||||
case 'Slashtags':
|
case 'Slashtags':
|
||||||
@ -99,13 +109,22 @@ export default function Login ({ providers, callbackUrl, error, text, Header, Fo
|
|||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
|
<OverlayTrigger
|
||||||
|
placement='bottom'
|
||||||
|
overlay={multiAuth ? <Tooltip>not available for account switching yet</Tooltip> : <></>}
|
||||||
|
trigger={['hover', 'focus']}
|
||||||
|
>
|
||||||
|
<div className='w-100'>
|
||||||
<LoginButton
|
<LoginButton
|
||||||
className={`mt-2 ${styles.providerButton}`}
|
className={`mt-2 ${styles.providerButton}`}
|
||||||
key={provider.id}
|
key={provider.id}
|
||||||
type={provider.id.toLowerCase()}
|
type={provider.id.toLowerCase()}
|
||||||
onClick={() => signIn(provider.id, { callbackUrl })}
|
onClick={() => signIn(provider.id, { callbackUrl, multiAuth })}
|
||||||
text={`${text || 'Login'} with`}
|
text={`${text || 'Login'} with`}
|
||||||
|
disabled={multiAuth}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</OverlayTrigger>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
@ -8,10 +8,14 @@ export const MeContext = React.createContext({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export function MeProvider ({ me, children }) {
|
export function MeProvider ({ me, children }) {
|
||||||
const { data } = useQuery(ME, SSR ? {} : { pollInterval: FAST_POLL_INTERVAL, nextFetchPolicy: 'cache-and-network' })
|
const { data, refetch } = useQuery(ME, SSR ? {} : { pollInterval: FAST_POLL_INTERVAL, nextFetchPolicy: 'cache-and-network' })
|
||||||
|
// this makes sure that we always use the fetched data if it's null.
|
||||||
|
// without this, we would always fallback to the `me` object
|
||||||
|
// which was passed during page load which (visually) breaks switching to anon
|
||||||
|
const futureMe = data?.me ?? (data?.me === null ? null : me)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MeContext.Provider value={data?.me || me}>
|
<MeContext.Provider value={{ me: futureMe, refreshMe: refetch }}>
|
||||||
{children}
|
{children}
|
||||||
</MeContext.Provider>
|
</MeContext.Provider>
|
||||||
)
|
)
|
||||||
|
@ -109,7 +109,7 @@ export default function MediaOrLink ({ linkFallback = true, ...props }) {
|
|||||||
|
|
||||||
// determines how the media should be displayed given the params, me settings, and editor tab
|
// determines how the media should be displayed given the params, me settings, and editor tab
|
||||||
export const useMediaHelper = ({ src, srcSet: srcSetIntital, topLevel, tab }) => {
|
export const useMediaHelper = ({ src, srcSet: srcSetIntital, topLevel, tab }) => {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const trusted = useMemo(() => !!srcSetIntital || IMGPROXY_URL_REGEXP.test(src) || MEDIA_DOMAIN_REGEXP.test(src), [!!srcSetIntital, src])
|
const trusted = useMemo(() => !!srcSetIntital || IMGPROXY_URL_REGEXP.test(src) || MEDIA_DOMAIN_REGEXP.test(src), [!!srcSetIntital, src])
|
||||||
const { dimensions, video, format, ...srcSetObj } = srcSetIntital || {}
|
const { dimensions, video, format, ...srcSetObj } = srcSetIntital || {}
|
||||||
const [isImage, setIsImage] = useState(!video && trusted)
|
const [isImage, setIsImage] = useState(!video && trusted)
|
||||||
|
@ -23,6 +23,8 @@ import classNames from 'classnames'
|
|||||||
import SnIcon from '@/svgs/sn.svg'
|
import SnIcon from '@/svgs/sn.svg'
|
||||||
import { useHasNewNotes } from '../use-has-new-notes'
|
import { useHasNewNotes } from '../use-has-new-notes'
|
||||||
import { useWallets } from 'wallets'
|
import { useWallets } from 'wallets'
|
||||||
|
import SwitchAccountList, { useAccounts } from '@/components/account'
|
||||||
|
import { useShowModal } from '@/components/modal'
|
||||||
|
|
||||||
export function Brand ({ className }) {
|
export function Brand ({ className }) {
|
||||||
return (
|
return (
|
||||||
@ -137,7 +139,7 @@ export function NavNotifications ({ className }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function WalletSummary () {
|
export function WalletSummary () {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
if (!me) return null
|
if (!me) return null
|
||||||
if (me.privates?.hideWalletBalance) {
|
if (me.privates?.hideWalletBalance) {
|
||||||
return <HiddenWalletSummary abbreviate fixedWidth />
|
return <HiddenWalletSummary abbreviate fixedWidth />
|
||||||
@ -146,7 +148,7 @@ export function WalletSummary () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NavWalletSummary ({ className }) {
|
export function NavWalletSummary ({ className }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const walletLimitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
|
const walletLimitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -211,7 +213,7 @@ export function MeDropdown ({ me, dropNavKey }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SignUpButton ({ className = 'py-0' }) {
|
export function SignUpButton ({ className = 'py-0', width }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const handleLogin = useCallback(async pathname => await router.push({
|
const handleLogin = useCallback(async pathname => await router.push({
|
||||||
pathname,
|
pathname,
|
||||||
@ -220,8 +222,8 @@ export function SignUpButton ({ className = 'py-0' }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={classNames('align-items-center ps-2 pe-3', className)}
|
className={classNames('align-items-center ps-2 py-1 pe-3', className)}
|
||||||
style={{ borderWidth: '2px', width: '112px' }}
|
style={{ borderWidth: '2px', width: width || '150px' }}
|
||||||
id='signup'
|
id='signup'
|
||||||
onClick={() => handleLogin('/signup')}
|
onClick={() => handleLogin('/signup')}
|
||||||
>
|
>
|
||||||
@ -234,7 +236,7 @@ export function SignUpButton ({ className = 'py-0' }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LoginButton ({ className }) {
|
export default function LoginButton () {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const handleLogin = useCallback(async pathname => await router.push({
|
const handleLogin = useCallback(async pathname => await router.push({
|
||||||
pathname,
|
pathname,
|
||||||
@ -243,9 +245,9 @@ export default function LoginButton ({ className }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className='align-items-center px-3 py-1 mb-2'
|
className='align-items-center px-3 py-1'
|
||||||
id='login'
|
id='login'
|
||||||
style={{ borderWidth: '2px', width: '112px' }}
|
style={{ borderWidth: '2px', width: '150px' }}
|
||||||
variant='outline-grey-darkmode'
|
variant='outline-grey-darkmode'
|
||||||
onClick={() => handleLogin('/login')}
|
onClick={() => handleLogin('/login')}
|
||||||
>
|
>
|
||||||
@ -254,12 +256,31 @@ export default function LoginButton ({ className }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LogoutDropdownItem () {
|
function LogoutObstacle ({ onClose }) {
|
||||||
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
||||||
const wallets = useWallets()
|
const wallets = useWallets()
|
||||||
|
const { multiAuthSignout } = useAccounts()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown.Item
|
<div className='d-flex m-auto flex-column w-fit-content'>
|
||||||
|
<h4 className='mb-3'>I reckon you want to logout?</h4>
|
||||||
|
<div className='mt-2 d-flex justify-content-between'>
|
||||||
|
<Button
|
||||||
|
className='me-2'
|
||||||
|
variant='grey-medium'
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
const switchSuccess = await multiAuthSignout()
|
||||||
|
// only signout if multiAuth did not find a next available account
|
||||||
|
if (switchSuccess) {
|
||||||
|
onClose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// order is important because we need to be logged in to delete push subscription on server
|
// order is important because we need to be logged in to delete push subscription on server
|
||||||
const pushSubscription = await swRegistration?.pushManager.getSubscription()
|
const pushSubscription = await swRegistration?.pushManager.getSubscription()
|
||||||
if (pushSubscription) {
|
if (pushSubscription) {
|
||||||
@ -270,16 +291,70 @@ export function LogoutDropdownItem () {
|
|||||||
|
|
||||||
await signOut({ callbackUrl: '/' })
|
await signOut({ callbackUrl: '/' })
|
||||||
}}
|
}}
|
||||||
>logout
|
>
|
||||||
</Dropdown.Item>
|
logout
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LoginButtons () {
|
export function LogoutDropdownItem ({ handleClose }) {
|
||||||
|
const showModal = useShowModal()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Dropdown.Item onClick={() => {
|
||||||
|
handleClose?.()
|
||||||
|
showModal(onClose => <SwitchAccountList onClose={onClose} />)
|
||||||
|
}}
|
||||||
|
>switch account
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item
|
||||||
|
onClick={async () => {
|
||||||
|
showModal(onClose => (<LogoutObstacle onClose={onClose} />))
|
||||||
|
}}
|
||||||
|
>logout
|
||||||
|
</Dropdown.Item>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SwitchAccountButton ({ handleClose }) {
|
||||||
|
const showModal = useShowModal()
|
||||||
|
const { accounts } = useAccounts()
|
||||||
|
|
||||||
|
if (accounts.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className='align-items-center px-3 py-1'
|
||||||
|
variant='outline-grey-darkmode'
|
||||||
|
style={{ borderWidth: '2px', width: '150px' }}
|
||||||
|
onClick={() => {
|
||||||
|
// login buttons rendered in offcanvas aren't wrapped inside <Dropdown>
|
||||||
|
// so we manually close the offcanvas in that case by passing down handleClose here
|
||||||
|
handleClose?.()
|
||||||
|
showModal(onClose => <SwitchAccountList onClose={onClose} />)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
switch account
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoginButtons ({ handleClose }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dropdown.Item className='py-1'>
|
||||||
<LoginButton />
|
<LoginButton />
|
||||||
<SignUpButton className='py-1' />
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item className='py-1'>
|
||||||
|
<SignUpButton />
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item className='py-1'>
|
||||||
|
<SwitchAccountButton handleClose={handleClose} />
|
||||||
|
</Dropdown.Item>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -299,7 +374,7 @@ export function AnonDropdown ({ path }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='position-relative'>
|
<div className='position-relative'>
|
||||||
<Dropdown className={styles.dropdown} align='end'>
|
<Dropdown className={styles.dropdown} align='end' autoClose>
|
||||||
<Dropdown.Toggle className='nav-link nav-item' id='profile' variant='custom'>
|
<Dropdown.Toggle className='nav-link nav-item' id='profile' variant='custom'>
|
||||||
<Nav.Link eventKey='anon' as='span' className='p-0 fw-normal'>
|
<Nav.Link eventKey='anon' as='span' className='p-0 fw-normal'>
|
||||||
@anon<Hat user={{ id: USER_ID.anon }} />
|
@anon<Hat user={{ id: USER_ID.anon }} />
|
||||||
|
@ -4,7 +4,7 @@ import { AnonCorner, Back, Brand, MeCorner, NavPrice, SearchItem } from '../comm
|
|||||||
import { useMe } from '../../me'
|
import { useMe } from '../../me'
|
||||||
|
|
||||||
export default function TopBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
|
export default function TopBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
return (
|
return (
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Nav
|
<Nav
|
||||||
|
@ -30,7 +30,7 @@ function useDetectKeyboardOpen (minKeyboardHeight = 300, defaultValue) {
|
|||||||
|
|
||||||
export default function BottomBar ({ sub }) {
|
export default function BottomBar ({ sub }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const isKeyboardOpen = useDetectKeyboardOpen(200, false)
|
const isKeyboardOpen = useDetectKeyboardOpen(200, false)
|
||||||
|
|
||||||
if (isKeyboardOpen) {
|
if (isKeyboardOpen) {
|
||||||
|
@ -75,10 +75,10 @@ export default function OffCanvas ({ me, dropNavKey }) {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Dropdown.Divider />
|
<Dropdown.Divider />
|
||||||
<LogoutDropdownItem />
|
<LogoutDropdownItem handleClose={handleClose} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: <LoginButtons />}
|
: <LoginButtons handleClose={handleClose} />}
|
||||||
<div className={classNames(styles.footerPadding, 'mt-auto')}>
|
<div className={classNames(styles.footerPadding, 'mt-auto')}>
|
||||||
<Navbar className={classNames('container d-flex flex-row px-0 text-muted')}>
|
<Navbar className={classNames('container d-flex flex-row px-0 text-muted')}>
|
||||||
<Nav>
|
<Nav>
|
||||||
|
@ -4,7 +4,7 @@ import styles from '../../header.module.css'
|
|||||||
import { useMe } from '@/components/me'
|
import { useMe } from '@/components/me'
|
||||||
|
|
||||||
export default function SecondBar (props) {
|
export default function SecondBar (props) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const { topNavKey } = props
|
const { topNavKey } = props
|
||||||
if (!hasNavSelect(props)) return null
|
if (!hasNavSelect(props)) return null
|
||||||
return (
|
return (
|
||||||
|
@ -4,7 +4,7 @@ import { Back, NavPrice, NavSelect, NavWalletSummary, SignUpButton, hasNavSelect
|
|||||||
import { useMe } from '@/components/me'
|
import { useMe } from '@/components/me'
|
||||||
|
|
||||||
export default function TopBar ({ prefix, sub, path, pathname, topNavKey, dropNavKey }) {
|
export default function TopBar ({ prefix, sub, path, pathname, topNavKey, dropNavKey }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
return (
|
return (
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Nav
|
<Nav
|
||||||
@ -17,7 +17,7 @@ export default function TopBar ({ prefix, sub, path, pathname, topNavKey, dropNa
|
|||||||
: (
|
: (
|
||||||
<>
|
<>
|
||||||
<NavPrice className='flex-shrink-1' />
|
<NavPrice className='flex-shrink-1' />
|
||||||
{me ? <NavWalletSummary /> : <SignUpButton />}
|
{me ? <NavWalletSummary /> : <SignUpButton width='fit-content' />}
|
||||||
</>)}
|
</>)}
|
||||||
</Nav>
|
</Nav>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
@ -7,7 +7,7 @@ import classNames from 'classnames'
|
|||||||
|
|
||||||
export default function StickyBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
|
export default function StickyBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
|
||||||
const ref = useRef()
|
const ref = useRef()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const stick = () => {
|
const stick = () => {
|
||||||
@ -49,7 +49,7 @@ export default function StickyBar ({ prefix, sub, path, topNavKey, dropNavKey })
|
|||||||
>
|
>
|
||||||
<Back />
|
<Back />
|
||||||
<NavPrice className='flex-shrink-1 flex-grow-0' />
|
<NavPrice className='flex-shrink-1 flex-grow-0' />
|
||||||
{me ? <NavWalletSummary className='px-2' /> : <SignUpButton />}
|
{me ? <NavWalletSummary className='px-2' /> : <SignUpButton width='fit-content' />}
|
||||||
</Nav>
|
</Nav>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -64,7 +64,7 @@ function NostrExplainer ({ text }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NostrAuth ({ text, callbackUrl }) {
|
export function NostrAuth ({ text, callbackUrl, multiAuth }) {
|
||||||
const [createAuth, { data, error }] = useMutation(gql`
|
const [createAuth, { data, error }] = useMutation(gql`
|
||||||
mutation createAuth {
|
mutation createAuth {
|
||||||
createAuth {
|
createAuth {
|
||||||
@ -112,7 +112,8 @@ export function NostrAuth ({ text, callbackUrl }) {
|
|||||||
try {
|
try {
|
||||||
await signIn('nostr', {
|
await signIn('nostr', {
|
||||||
event: JSON.stringify(event),
|
event: JSON.stringify(event),
|
||||||
callbackUrl
|
callbackUrl,
|
||||||
|
multiAuth
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error('authorization failed', e)
|
throw new Error('authorization failed', e)
|
||||||
@ -141,14 +142,14 @@ export function NostrAuth ({ text, callbackUrl }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NostrAuthWithExplainer ({ text, callbackUrl }) {
|
export function NostrAuthWithExplainer ({ text, callbackUrl, multiAuth }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<div className={styles.login}>
|
<div className={styles.login}>
|
||||||
<div className='w-100 mb-3 text-muted pointer' onClick={() => router.back()}><BackIcon /></div>
|
<div className='w-100 mb-3 text-muted pointer' onClick={() => router.back()}><BackIcon /></div>
|
||||||
<h3 className='w-100 pb-2'>{text || 'Login'} with Nostr</h3>
|
<h3 className='w-100 pb-2'>{text || 'Login'} with Nostr</h3>
|
||||||
<NostrAuth text={text} callbackUrl={callbackUrl} />
|
<NostrAuth text={text} callbackUrl={callbackUrl} multiAuth={multiAuth} />
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
@ -42,7 +42,7 @@ export const payBountyCacheMods = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function PayBounty ({ children, item }) {
|
export default function PayBounty ({ children, item }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const root = useRoot()
|
const root = useRoot()
|
||||||
const strike = useLightning()
|
const strike = useLightning()
|
||||||
|
@ -201,7 +201,7 @@ export const useQrPayment = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const usePayment = () => {
|
export const usePayment = () => {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const feeButton = useFeeButton()
|
const feeButton = useFeeButton()
|
||||||
const invoice = useInvoice()
|
const invoice = useInvoice()
|
||||||
const waitForWalletPayment = useWalletPayment()
|
const waitForWalletPayment = useWalletPayment()
|
||||||
|
@ -14,7 +14,7 @@ import useItemSubmit from './use-item-submit'
|
|||||||
|
|
||||||
export function PollForm ({ item, sub, editThreshold, children }) {
|
export function PollForm ({ item, sub, editThreshold, children }) {
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const schema = pollSchema({ client, me, existingBoost: item?.boost })
|
const schema = pollSchema({ client, me, existingBoost: item?.boost })
|
||||||
|
|
||||||
const onSubmit = useItemSubmit(UPSERT_POLL, { item, sub })
|
const onSubmit = useItemSubmit(UPSERT_POLL, { item, sub })
|
||||||
|
@ -11,7 +11,7 @@ import { usePaidMutation } from './use-paid-mutation'
|
|||||||
import { POLL_VOTE, RETRY_PAID_ACTION } from '@/fragments/paidAction'
|
import { POLL_VOTE, RETRY_PAID_ACTION } from '@/fragments/paidAction'
|
||||||
|
|
||||||
export default function Poll ({ item }) {
|
export default function Poll ({ item }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const pollVote = usePollVote({ query: POLL_VOTE, itemId: item.id })
|
const pollVote = usePollVote({ query: POLL_VOTE, itemId: item.id })
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ import CancelButton from './cancel-button'
|
|||||||
import { TerritoryInfo } from './territory-header'
|
import { TerritoryInfo } from './territory-header'
|
||||||
|
|
||||||
export function PostForm ({ type, sub, children }) {
|
export function PostForm ({ type, sub, children }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [errorMessage, setErrorMessage] = useState()
|
const [errorMessage, setErrorMessage] = useState()
|
||||||
|
|
||||||
const prefix = sub?.name ? `/~${sub.name}` : ''
|
const prefix = sub?.name ? `/~${sub.name}` : ''
|
||||||
@ -150,7 +150,7 @@ export function PostForm ({ type, sub, children }) {
|
|||||||
return (
|
return (
|
||||||
<FeeButtonProvider
|
<FeeButtonProvider
|
||||||
baseLineItems={sub ? postCommentBaseLineItems({ baseCost: sub.baseCost, me: !!me }) : undefined}
|
baseLineItems={sub ? postCommentBaseLineItems({ baseCost: sub.baseCost, me: !!me }) : undefined}
|
||||||
useRemoteLineItems={postCommentUseRemoteLineItems({ me: !!me })}
|
useRemoteLineItems={postCommentUseRemoteLineItems()}
|
||||||
>
|
>
|
||||||
<FormType sub={sub}>{children}</FormType>
|
<FormType sub={sub}>{children}</FormType>
|
||||||
</FeeButtonProvider>
|
</FeeButtonProvider>
|
||||||
|
@ -19,7 +19,7 @@ export function usePrice () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function PriceProvider ({ price, children }) {
|
export function PriceProvider ({ price, children }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const fiatCurrency = me?.privates?.fiatCurrency
|
const fiatCurrency = me?.privates?.fiatCurrency
|
||||||
const { data } = useQuery(PRICE, {
|
const { data } = useQuery(PRICE, {
|
||||||
variables: { fiatCurrency },
|
variables: { fiatCurrency },
|
||||||
|
@ -40,7 +40,7 @@ export default forwardRef(function Reply ({
|
|||||||
quote
|
quote
|
||||||
}, ref) {
|
}, ref) {
|
||||||
const [reply, setReply] = useState(replyOpen || quote)
|
const [reply, setReply] = useState(replyOpen || quote)
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const parentId = item.id
|
const parentId = item.id
|
||||||
const replyInput = useRef(null)
|
const replyInput = useRef(null)
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
|
@ -11,7 +11,7 @@ export default function Search ({ sub }) {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [q, setQ] = useState(router.query.q || '')
|
const [q, setQ] = useState(router.query.q || '')
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null)
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
|
@ -38,7 +38,7 @@ async function share (title, url, toaster) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Share ({ path, title = '', className = '' }) {
|
export default function Share ({ path, title = '', className = '' }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const url = referrurl(path, me)
|
const url = referrurl(path, me)
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ export default function Share ({ path, title = '', className = '' }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CopyLinkDropdownItem ({ item }) {
|
export function CopyLinkDropdownItem ({ item }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
let url = referrurl(`/items/${item.id}`, me)
|
let url = referrurl(`/items/${item.id}`, me)
|
||||||
|
@ -18,7 +18,7 @@ import { UNARCHIVE_TERRITORY, UPSERT_SUB } from '@/fragments/paidAction'
|
|||||||
export default function TerritoryForm ({ sub }) {
|
export default function TerritoryForm ({ sub }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [upsertSub] = usePaidMutation(UPSERT_SUB)
|
const [upsertSub] = usePaidMutation(UPSERT_SUB)
|
||||||
const [unarchiveTerritory] = usePaidMutation(UNARCHIVE_TERRITORY)
|
const [unarchiveTerritory] = usePaidMutation(UNARCHIVE_TERRITORY)
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ export function TerritoryInfo ({ sub }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TerritoryHeader ({ sub }) {
|
export default function TerritoryHeader ({ sub }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
|
||||||
const [toggleMuteSub] = useMutation(
|
const [toggleMuteSub] = useMutation(
|
||||||
|
@ -12,7 +12,7 @@ import { usePaidMutation } from './use-paid-mutation'
|
|||||||
import { SUB_PAY } from '@/fragments/paidAction'
|
import { SUB_PAY } from '@/fragments/paidAction'
|
||||||
|
|
||||||
export default function TerritoryPaymentDue ({ sub }) {
|
export default function TerritoryPaymentDue ({ sub }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const [paySub] = usePaidMutation(SUB_PAY)
|
const [paySub] = usePaidMutation(SUB_PAY)
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ export default function TerritoryPaymentDue ({ sub }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TerritoryBillingLine ({ sub }) {
|
export function TerritoryBillingLine ({ sub }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
if (!sub || sub.userId !== Number(me?.id)) return null
|
if (!sub || sub.userId !== Number(me?.id)) return null
|
||||||
|
|
||||||
const dueDate = sub.billPaidUntil && new Date(sub.billPaidUntil)
|
const dueDate = sub.billPaidUntil && new Date(sub.billPaidUntil)
|
||||||
|
@ -57,7 +57,7 @@ function TransferObstacle ({ sub, onClose, userName }) {
|
|||||||
function TerritoryTransferForm ({ sub, onClose }) {
|
function TerritoryTransferForm ({ sub, onClose }) {
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const schema = territoryTransferSchema({ me, client })
|
const schema = territoryTransferSchema({ me, client })
|
||||||
|
|
||||||
const onSubmit = useCallback(async (values) => {
|
const onSubmit = useCallback(async (values) => {
|
||||||
|
@ -145,6 +145,10 @@
|
|||||||
aspect-ratio: var(--aspect-ratio);
|
aspect-ratio: var(--aspect-ratio);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p:has(> .mediaContainer) {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.p:has(> .mediaContainer:only-child) ~ .p:has(> .mediaContainer:only-child) {
|
.p:has(> .mediaContainer:only-child) ~ .p:has(> .mediaContainer:only-child) {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: min-content;
|
width: min-content;
|
||||||
|
@ -14,7 +14,7 @@ import { numWithUnits } from '@/lib/format'
|
|||||||
import { Dropdown } from 'react-bootstrap'
|
import { Dropdown } from 'react-bootstrap'
|
||||||
|
|
||||||
const UpvotePopover = ({ target, show, handleClose }) => {
|
const UpvotePopover = ({ target, show, handleClose }) => {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<Overlay
|
||||||
show={show}
|
show={show}
|
||||||
@ -107,7 +107,7 @@ export default function UpVote ({ item, className }) {
|
|||||||
const [voteShow, _setVoteShow] = useState(false)
|
const [voteShow, _setVoteShow] = useState(false)
|
||||||
const [tipShow, _setTipShow] = useState(false)
|
const [tipShow, _setTipShow] = useState(false)
|
||||||
const ref = useRef()
|
const ref = useRef()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [hover, setHover] = useState(false)
|
const [hover, setHover] = useState(false)
|
||||||
const [setWalkthrough] = useMutation(
|
const [setWalkthrough] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
@ -153,7 +153,7 @@ export default function UpVote ({ item, className }) {
|
|||||||
[item?.mine, item?.meForward, item?.deletedAt])
|
[item?.mine, item?.meForward, item?.deletedAt])
|
||||||
|
|
||||||
const [meSats, overlayText, color, nextColor] = useMemo(() => {
|
const [meSats, overlayText, color, nextColor] = useMemo(() => {
|
||||||
const meSats = (item?.meSats || item?.meAnonSats || 0)
|
const meSats = (me ? item?.meSats : item?.meAnonSats) || 0
|
||||||
|
|
||||||
// what should our next tip be?
|
// what should our next tip be?
|
||||||
const sats = pending || nextTip(meSats, { ...me?.privates })
|
const sats = pending || nextTip(meSats, { ...me?.privates })
|
||||||
@ -168,7 +168,7 @@ export default function UpVote ({ item, className }) {
|
|||||||
meSats, overlayTextContent,
|
meSats, overlayTextContent,
|
||||||
getColor(meSats), getColor(meSats + sats)]
|
getColor(meSats), getColor(meSats + sats)]
|
||||||
}, [
|
}, [
|
||||||
item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault,
|
me, item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault,
|
||||||
me?.privates?.tipRandom, me?.privates?.tipRandomMin, me?.privates?.tipRandomMax, pending])
|
me?.privates?.tipRandom, me?.privates?.tipRandomMin, me?.privates?.tipRandomMax, pending])
|
||||||
|
|
||||||
const handleModalClosed = () => {
|
const handleModalClosed = () => {
|
||||||
|
@ -175,7 +175,7 @@ function NymEdit ({ user, setEditting }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function NymView ({ user, isMe, setEditting }) {
|
function NymView ({ user, isMe, setEditting }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
return (
|
return (
|
||||||
<div className='d-flex align-items-center mb-2'>
|
<div className='d-flex align-items-center mb-2'>
|
||||||
<div className={styles.username}>@{user.name}<Hat className='' user={user} badge /></div>
|
<div className={styles.username}>@{user.name}<Hat className='' user={user} badge /></div>
|
||||||
@ -237,7 +237,7 @@ function SocialLink ({ name, id }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function HeaderHeader ({ user }) {
|
function HeaderHeader ({ user }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
@ -12,6 +12,7 @@ import { useMe } from './me'
|
|||||||
import { MEDIA_URL } from '@/lib/constants'
|
import { MEDIA_URL } from '@/lib/constants'
|
||||||
import { NymActionDropdown } from '@/components/user-header'
|
import { NymActionDropdown } from '@/components/user-header'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
import CheckCircle from '@/svgs/checkbox-circle-fill.svg'
|
||||||
|
|
||||||
// all of this nonsense is to show the stat we are sorting by first
|
// all of this nonsense is to show the stat we are sorting by first
|
||||||
const Stacked = ({ user }) => (user.optional.stacked !== null && <span>{abbrNum(user.optional.stacked)} stacked</span>)
|
const Stacked = ({ user }) => (user.optional.stacked !== null && <span>{abbrNum(user.optional.stacked)} stacked</span>)
|
||||||
@ -40,6 +41,34 @@ 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 function UserListRow ({ user, stats, className, onNymClick, showHat = true, selected }) {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.item} mb-2`} key={user.name}>
|
||||||
|
<Link href={`/${user.name}`}>
|
||||||
|
<Image
|
||||||
|
src={user.photoId ? `${MEDIA_URL}/${user.photoId}` : '/dorian400.jpg'} width='32' height='32'
|
||||||
|
className={`${userStyles.userimg} me-2`}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
<div className={`${styles.hunk} ${className}`}>
|
||||||
|
<Link
|
||||||
|
href={`/${user.name}`}
|
||||||
|
className={`d-inline-flex align-items-center text-reset ${selected ? 'fw-bold text-underline' : 'text-muted'}`}
|
||||||
|
style={{ textUnderlineOffset: '0.25em' }}
|
||||||
|
onClick={onNymClick}
|
||||||
|
>
|
||||||
|
@{user.name}{showHat && <Hat className='ms-1 fill-grey' height={14} width={14} user={user} />}{selected && <CheckCircle className='ms-3 fill-primary' height={14} width={14} />}
|
||||||
|
</Link>
|
||||||
|
{stats && (
|
||||||
|
<div className={styles.other}>
|
||||||
|
{stats.map((Comp, i) => <Comp key={i} user={user} />)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function UserBase ({ user, className, children, nymActionDropdown }) {
|
export function UserBase ({ user, className, children, nymActionDropdown }) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.item, className)}>
|
<div className={classNames(styles.item, className)}>
|
||||||
@ -63,7 +92,7 @@ export function UserBase ({ user, className, children, nymActionDropdown }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function User ({ user, rank, statComps, className = 'mb-2', Embellish, nymActionDropdown = false }) {
|
export function User ({ user, rank, statComps, className = 'mb-2', Embellish, nymActionDropdown = false }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const showStatComps = statComps && statComps.length > 0
|
const showStatComps = statComps && statComps.length > 0
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -112,7 +112,7 @@ const initIndexedDB = async (dbName, storeName) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const WalletLoggerProvider = ({ children }) => {
|
export const WalletLoggerProvider = ({ children }) => {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [logs, setLogs] = useState([])
|
const [logs, setLogs] = useState([])
|
||||||
let dbName = 'app:storage'
|
let dbName = 'app:storage'
|
||||||
if (me) {
|
if (me) {
|
||||||
@ -209,8 +209,8 @@ export const WalletLoggerProvider = ({ children }) => {
|
|||||||
setLogs((prevLogs) => [log, ...prevLogs])
|
setLogs((prevLogs) => [log, ...prevLogs])
|
||||||
}, [saveLog])
|
}, [saveLog])
|
||||||
|
|
||||||
const deleteLogs = useCallback(async (wallet) => {
|
const deleteLogs = useCallback(async (wallet, options) => {
|
||||||
if (!wallet || wallet.walletType) {
|
if ((!wallet || wallet.walletType) && !options?.clientOnly) {
|
||||||
await deleteServerWalletLogs({ variables: { wallet: wallet?.walletType } })
|
await deleteServerWalletLogs({ variables: { wallet: wallet?.walletType } })
|
||||||
}
|
}
|
||||||
if (!wallet || wallet.sendPayment) {
|
if (!wallet || wallet.sendPayment) {
|
||||||
@ -262,7 +262,9 @@ export function useWalletLogger (wallet) {
|
|||||||
error: (...message) => log('error')(message.join(' '))
|
error: (...message) => log('error')(message.join(' '))
|
||||||
}), [log, wallet?.name])
|
}), [log, wallet?.name])
|
||||||
|
|
||||||
const deleteLogs = useCallback((w) => innerDeleteLogs(w || wallet), [innerDeleteLogs, wallet])
|
const deleteLogs = useCallback((options) => {
|
||||||
|
return innerDeleteLogs(wallet, options)
|
||||||
|
}, [innerDeleteLogs, wallet])
|
||||||
|
|
||||||
return { logger, deleteLogs }
|
return { logger, deleteLogs }
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@ export const SETTINGS_FIELDS = gql`
|
|||||||
|
|
||||||
export const SETTINGS = gql`
|
export const SETTINGS = gql`
|
||||||
${SETTINGS_FIELDS}
|
${SETTINGS_FIELDS}
|
||||||
{
|
query Settings {
|
||||||
settings {
|
settings {
|
||||||
...SettingsFields
|
...SettingsFields
|
||||||
}
|
}
|
||||||
@ -320,8 +320,8 @@ export const USER_FULL = gql`
|
|||||||
|
|
||||||
export const USER = gql`
|
export const USER = gql`
|
||||||
${USER_FIELDS}
|
${USER_FIELDS}
|
||||||
query User($name: String!) {
|
query User($id: ID, $name: String) {
|
||||||
user(name: $name) {
|
user(id: $id, name: $name) {
|
||||||
...UserFields
|
...UserFields
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
|
import { ApolloClient, InMemoryCache, HttpLink, makeVar } from '@apollo/client'
|
||||||
import { decodeCursor, LIMIT } from './cursor'
|
import { decodeCursor, LIMIT } from './cursor'
|
||||||
import { SSR } from './constants'
|
import { SSR } from './constants'
|
||||||
|
|
||||||
@ -25,6 +25,8 @@ export default function getApolloClient () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const meAnonSats = {}
|
||||||
|
|
||||||
function getClient (uri) {
|
function getClient (uri) {
|
||||||
return new ApolloClient({
|
return new ApolloClient({
|
||||||
link: new HttpLink({ uri }),
|
link: new HttpLink({ uri }),
|
||||||
@ -259,10 +261,23 @@ function getClient (uri) {
|
|||||||
Item: {
|
Item: {
|
||||||
fields: {
|
fields: {
|
||||||
meAnonSats: {
|
meAnonSats: {
|
||||||
read (meAnonSats, { readField }) {
|
read (existingAmount, { readField }) {
|
||||||
if (typeof window === 'undefined') return null
|
if (SSR) return null
|
||||||
|
|
||||||
const itemId = readField('id')
|
const itemId = readField('id')
|
||||||
return meAnonSats ?? Number(window.localStorage.getItem(`TIP-item:${itemId}`) || '0')
|
|
||||||
|
// we need to use reactive variables such that updates
|
||||||
|
// to local state propagate correctly
|
||||||
|
// see https://www.apollographql.com/docs/react/local-state/reactive-variables
|
||||||
|
let reactiveVar = meAnonSats[itemId]
|
||||||
|
if (!reactiveVar) {
|
||||||
|
const storageKey = `TIP-item:${itemId}`
|
||||||
|
const existingAmount = Number(window.localStorage.getItem(storageKey) || '0')
|
||||||
|
reactiveVar = makeVar(existingAmount || 0)
|
||||||
|
meAnonSats[itemId] = reactiveVar
|
||||||
|
}
|
||||||
|
|
||||||
|
return reactiveVar()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
35
package-lock.json
generated
35
package-lock.json
generated
@ -31,6 +31,7 @@
|
|||||||
"canonical-json": "0.0.4",
|
"canonical-json": "0.0.4",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
|
"cookie": "^0.6.0",
|
||||||
"cross-fetch": "^4.0.0",
|
"cross-fetch": "^4.0.0",
|
||||||
"csv-parser": "^3.0.0",
|
"csv-parser": "^3.0.0",
|
||||||
"domino": "^2.1.6",
|
"domino": "^2.1.6",
|
||||||
@ -530,6 +531,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@auth/core/node_modules/cookie": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@auth/prisma-adapter": {
|
"node_modules/@auth/prisma-adapter": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-1.0.3.tgz",
|
||||||
@ -7267,9 +7277,10 @@
|
|||||||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
|
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@ -9006,6 +9017,15 @@
|
|||||||
"node": ">= 0.10.0"
|
"node": ">= 0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express/node_modules/cookie": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express/node_modules/debug": {
|
"node_modules/express/node_modules/debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
@ -14752,6 +14772,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-auth/node_modules/cookie": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next-plausible": {
|
"node_modules/next-plausible": {
|
||||||
"version": "3.11.1",
|
"version": "3.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/next-plausible/-/next-plausible-3.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/next-plausible/-/next-plausible-3.11.1.tgz",
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
"canonical-json": "0.0.4",
|
"canonical-json": "0.0.4",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
|
"cookie": "^0.6.0",
|
||||||
"cross-fetch": "^4.0.0",
|
"cross-fetch": "^4.0.0",
|
||||||
"csv-parser": "^3.0.0",
|
"csv-parser": "^3.0.0",
|
||||||
"domino": "^2.1.6",
|
"domino": "^2.1.6",
|
||||||
|
@ -86,7 +86,7 @@ export default function User ({ ssrData }) {
|
|||||||
const [create, setCreate] = useState(false)
|
const [create, setCreate] = useState(false)
|
||||||
const [edit, setEdit] = useState(false)
|
const [edit, setEdit] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const { data } = useQuery(USER_FULL, { variables: { ...router.query } })
|
const { data } = useQuery(USER_FULL, { variables: { ...router.query } })
|
||||||
if (!data && !ssrData) return <PageLoading />
|
if (!data && !ssrData) return <PageLoading />
|
||||||
|
@ -22,6 +22,7 @@ import { ChainFeeProvider } from '@/components/chain-fee.js'
|
|||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import { HasNewNotesProvider } from '@/components/use-has-new-notes'
|
import { HasNewNotesProvider } from '@/components/use-has-new-notes'
|
||||||
import WebLnProvider from '@/wallets/webln'
|
import WebLnProvider from '@/wallets/webln'
|
||||||
|
import { AccountProvider } from '@/components/account'
|
||||||
|
|
||||||
const PWAPrompt = dynamic(() => import('react-ios-pwa-prompt'), { ssr: false })
|
const PWAPrompt = dynamic(() => import('react-ios-pwa-prompt'), { ssr: false })
|
||||||
|
|
||||||
@ -109,6 +110,7 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
|
|||||||
<WalletLoggerProvider>
|
<WalletLoggerProvider>
|
||||||
<WebLnProvider>
|
<WebLnProvider>
|
||||||
<ServiceWorkerProvider>
|
<ServiceWorkerProvider>
|
||||||
|
<AccountProvider>
|
||||||
<PriceProvider price={price}>
|
<PriceProvider price={price}>
|
||||||
<LightningProvider>
|
<LightningProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
@ -125,6 +127,7 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
|
|||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</LightningProvider>
|
</LightningProvider>
|
||||||
</PriceProvider>
|
</PriceProvider>
|
||||||
|
</AccountProvider>
|
||||||
</ServiceWorkerProvider>
|
</ServiceWorkerProvider>
|
||||||
</WebLnProvider>
|
</WebLnProvider>
|
||||||
</WalletLoggerProvider>
|
</WalletLoggerProvider>
|
||||||
|
@ -7,11 +7,13 @@ import EmailProvider from 'next-auth/providers/email'
|
|||||||
import prisma from '@/api/models'
|
import prisma from '@/api/models'
|
||||||
import nodemailer from 'nodemailer'
|
import nodemailer from 'nodemailer'
|
||||||
import { PrismaAdapter } from '@auth/prisma-adapter'
|
import { PrismaAdapter } from '@auth/prisma-adapter'
|
||||||
import { getToken } from 'next-auth/jwt'
|
import { NodeNextRequest, NodeNextResponse } from 'next/dist/server/base-http/node'
|
||||||
import { NodeNextRequest } from 'next/dist/server/base-http/node'
|
import { getToken, encode as encodeJWT } from 'next-auth/jwt'
|
||||||
|
import { datePivot } from '@/lib/time'
|
||||||
import { schnorr } from '@noble/curves/secp256k1'
|
import { schnorr } from '@noble/curves/secp256k1'
|
||||||
import { notifyReferral } from '@/lib/webPush'
|
import { notifyReferral } from '@/lib/webPush'
|
||||||
import { hashEmail } from '@/lib/crypto'
|
import { hashEmail } from '@/lib/crypto'
|
||||||
|
import cookie from 'cookie'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores userIds in user table
|
* Stores userIds in user table
|
||||||
@ -53,7 +55,7 @@ async function getReferrerId (referrer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @returns {Partial<import('next-auth').CallbacksOptions>} */
|
/** @returns {Partial<import('next-auth').CallbacksOptions>} */
|
||||||
function getCallbacks (req) {
|
function getCallbacks (req, res) {
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
* @param {object} token Decrypted JSON Web Token
|
* @param {object} token Decrypted JSON Web Token
|
||||||
@ -88,6 +90,16 @@ function getCallbacks (req) {
|
|||||||
token.sub = Number(token.id)
|
token.sub = Number(token.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// response is only defined during signup/login
|
||||||
|
if (req && res) {
|
||||||
|
req = new NodeNextRequest(req)
|
||||||
|
res = new NodeNextResponse(res)
|
||||||
|
const secret = process.env.NEXTAUTH_SECRET
|
||||||
|
const jwt = await encodeJWT({ token, secret })
|
||||||
|
const me = await prisma.user.findUnique({ where: { id: token.id } })
|
||||||
|
setMultiAuthCookies(req, res, { ...me, jwt })
|
||||||
|
}
|
||||||
|
|
||||||
return token
|
return token
|
||||||
},
|
},
|
||||||
async session ({ session, token }) {
|
async session ({ session, token }) {
|
||||||
@ -100,23 +112,78 @@ function getCallbacks (req) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pubkeyAuth (credentials, req, pubkeyColumnName) {
|
function setMultiAuthCookies (req, res, { id, jwt, name, photoId }) {
|
||||||
|
const b64Encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64')
|
||||||
|
const b64Decode = s => JSON.parse(Buffer.from(s, 'base64'))
|
||||||
|
|
||||||
|
// default expiration for next-auth JWTs is in 1 month
|
||||||
|
const expiresAt = datePivot(new Date(), { months: 1 })
|
||||||
|
const cookieOptions = {
|
||||||
|
path: '/',
|
||||||
|
httpOnly: true,
|
||||||
|
secure: req.secure,
|
||||||
|
sameSite: 'lax',
|
||||||
|
expires: expiresAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// add JWT to **httpOnly** cookie
|
||||||
|
res.appendHeader('Set-Cookie', cookie.serialize(`multi_auth.${id}`, jwt, cookieOptions))
|
||||||
|
|
||||||
|
let newMultiAuth = [{ id, name, photoId }]
|
||||||
|
if (req.cookies.multi_auth) {
|
||||||
|
const oldMultiAuth = b64Decode(req.cookies.multi_auth)
|
||||||
|
// make sure we don't add duplicates
|
||||||
|
if (oldMultiAuth.some(({ id: id_ }) => id_ === id)) return
|
||||||
|
newMultiAuth = [...oldMultiAuth, ...newMultiAuth]
|
||||||
|
}
|
||||||
|
res.appendHeader('Set-Cookie', cookie.serialize('multi_auth', b64Encode(newMultiAuth), { ...cookieOptions, httpOnly: false }))
|
||||||
|
|
||||||
|
// switch to user we just added
|
||||||
|
res.appendHeader('Set-Cookie', cookie.serialize('multi_auth.user-id', id, { ...cookieOptions, httpOnly: false }))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pubkeyAuth (credentials, req, res, pubkeyColumnName) {
|
||||||
const { k1, pubkey } = credentials
|
const { k1, pubkey } = credentials
|
||||||
|
|
||||||
|
// are we trying to add a new account for switching between?
|
||||||
|
const { body } = req.body
|
||||||
|
const multiAuth = typeof body.multiAuth === 'string' ? body.multiAuth === 'true' : !!body.multiAuth
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// does the given challenge (k1) exist in our db?
|
||||||
const lnauth = await prisma.lnAuth.findUnique({ where: { k1 } })
|
const lnauth = await prisma.lnAuth.findUnique({ where: { k1 } })
|
||||||
|
|
||||||
|
// delete challenge to prevent replay attacks
|
||||||
await prisma.lnAuth.delete({ where: { k1 } })
|
await prisma.lnAuth.delete({ where: { k1 } })
|
||||||
|
|
||||||
|
// does the given pubkey match the one for which we verified the signature?
|
||||||
if (lnauth.pubkey === pubkey) {
|
if (lnauth.pubkey === pubkey) {
|
||||||
|
// does the pubkey already exist in our db?
|
||||||
let user = await prisma.user.findUnique({ where: { [pubkeyColumnName]: pubkey } })
|
let user = await prisma.user.findUnique({ where: { [pubkeyColumnName]: pubkey } })
|
||||||
|
|
||||||
|
// get token if it exists
|
||||||
const token = await getToken({ req })
|
const token = await getToken({ req })
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// if we are logged in, update rather than create
|
// we have not seen this pubkey before
|
||||||
if (token?.id) {
|
|
||||||
|
// only update our pubkey if we're not currently trying to add a new account
|
||||||
|
if (token?.id && !multiAuth) {
|
||||||
user = await prisma.user.update({ where: { id: token.id }, data: { [pubkeyColumnName]: pubkey } })
|
user = await prisma.user.update({ where: { id: token.id }, data: { [pubkeyColumnName]: pubkey } })
|
||||||
} else {
|
} else {
|
||||||
|
// we're not logged in: create new user with that pubkey
|
||||||
user = await prisma.user.create({ data: { name: pubkey.slice(0, 10), [pubkeyColumnName]: pubkey } })
|
user = await prisma.user.create({ data: { name: pubkey.slice(0, 10), [pubkeyColumnName]: pubkey } })
|
||||||
}
|
}
|
||||||
} else if (token && token?.id !== user.id) {
|
}
|
||||||
return null
|
|
||||||
|
if (token && token?.id !== user.id && multiAuth) {
|
||||||
|
// we're logged in as a different user than the one we're authenticating as
|
||||||
|
// and we want to add a new account. this means we want to add this account
|
||||||
|
// to our list of accounts for switching between so we issue a new JWT and
|
||||||
|
// update the cookies for multi-authentication.
|
||||||
|
const secret = process.env.NEXTAUTH_SECRET
|
||||||
|
const userJWT = await encodeJWT({ token: { id: user.id, name: user.name, email: user.email }, secret })
|
||||||
|
setMultiAuthCookies(req, res, { ...user, jwt: userJWT })
|
||||||
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
return user
|
return user
|
||||||
@ -160,7 +227,7 @@ async function nostrEventAuth (event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @type {import('next-auth/providers').Provider[]} */
|
/** @type {import('next-auth/providers').Provider[]} */
|
||||||
const providers = [
|
const getProviders = res => [
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
id: 'lightning',
|
id: 'lightning',
|
||||||
name: 'Lightning',
|
name: 'Lightning',
|
||||||
@ -168,7 +235,9 @@ const providers = [
|
|||||||
pubkey: { label: 'publickey', type: 'text' },
|
pubkey: { label: 'publickey', type: 'text' },
|
||||||
k1: { label: 'k1', type: 'text' }
|
k1: { label: 'k1', type: 'text' }
|
||||||
},
|
},
|
||||||
authorize: async (credentials, req) => await pubkeyAuth(credentials, new NodeNextRequest(req), 'pubkey')
|
authorize: async (credentials, req) => {
|
||||||
|
return await pubkeyAuth(credentials, new NodeNextRequest(req), new NodeNextResponse(res), 'pubkey')
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
id: 'nostr',
|
id: 'nostr',
|
||||||
@ -178,7 +247,7 @@ const providers = [
|
|||||||
},
|
},
|
||||||
authorize: async ({ event }, req) => {
|
authorize: async ({ event }, req) => {
|
||||||
const credentials = await nostrEventAuth(event)
|
const credentials = await nostrEventAuth(event)
|
||||||
return await pubkeyAuth(credentials, new NodeNextRequest(req), 'nostrAuthPubkey')
|
return await pubkeyAuth(credentials, new NodeNextRequest(req), new NodeNextResponse(res), 'nostrAuthPubkey')
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
GitHubProvider({
|
GitHubProvider({
|
||||||
@ -213,9 +282,9 @@ const providers = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
/** @returns {import('next-auth').AuthOptions} */
|
/** @returns {import('next-auth').AuthOptions} */
|
||||||
export const getAuthOptions = req => ({
|
export const getAuthOptions = (req, res) => ({
|
||||||
callbacks: getCallbacks(req),
|
callbacks: getCallbacks(req, res),
|
||||||
providers,
|
providers: getProviders(res),
|
||||||
adapter: {
|
adapter: {
|
||||||
...PrismaAdapter(prisma),
|
...PrismaAdapter(prisma),
|
||||||
createUser: data => {
|
createUser: data => {
|
||||||
@ -299,7 +368,7 @@ async function enrollInNewsletter ({ email }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await NextAuth(req, res, getAuthOptions(req))
|
await NextAuth(req, res, getAuthOptions(req, res))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendVerificationRequest ({
|
async function sendVerificationRequest ({
|
||||||
|
@ -66,6 +66,7 @@ export default startServerAndCreateNextHandler(apolloServer, {
|
|||||||
session = { user: { ...sessionFields, apiKey: true } }
|
session = { user: { ...sessionFields, apiKey: true } }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
req = multiAuthMiddleware(req)
|
||||||
session = await getServerSession(req, res, getAuthOptions(req))
|
session = await getServerSession(req, res, getAuthOptions(req))
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@ -79,3 +80,41 @@ export default startServerAndCreateNextHandler(apolloServer, {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function multiAuthMiddleware (request) {
|
||||||
|
// switch next-auth session cookie with multi_auth cookie if cookie pointer present
|
||||||
|
|
||||||
|
// is there a cookie pointer?
|
||||||
|
const cookiePointerName = 'multi_auth.user-id'
|
||||||
|
const hasCookiePointer = !!request.cookies[cookiePointerName]
|
||||||
|
|
||||||
|
// is there a session?
|
||||||
|
const sessionCookieName = request.secure ? '__Secure-next-auth.session-token' : 'next-auth.session-token'
|
||||||
|
const hasSession = !!request.cookies[sessionCookieName]
|
||||||
|
|
||||||
|
if (!hasCookiePointer || !hasSession) {
|
||||||
|
// no session or no cookie pointer. do nothing.
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = request.cookies[cookiePointerName]
|
||||||
|
if (userId === 'anonymous') {
|
||||||
|
// user switched to anon. only delete session cookie.
|
||||||
|
delete request.cookies[sessionCookieName]
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
const userJWT = request.cookies[`multi_auth.${userId}`]
|
||||||
|
if (!userJWT) {
|
||||||
|
// no JWT for account switching found
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userJWT) {
|
||||||
|
// use JWT found in cookie pointed to by cookie pointer
|
||||||
|
request.cookies[sessionCookieName] = userJWT
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
61
pages/api/signout.js
Normal file
61
pages/api/signout.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import cookie from 'cookie'
|
||||||
|
import { datePivot } from '../../lib/time'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {NextApiRequest} req
|
||||||
|
* @param {NextApiResponse} res
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
export default (req, res) => {
|
||||||
|
// is there a cookie pointer?
|
||||||
|
const cookiePointerName = 'multi_auth.user-id'
|
||||||
|
const userId = req.cookies[cookiePointerName]
|
||||||
|
|
||||||
|
// is there a session?
|
||||||
|
const sessionCookieName = req.secure ? '__Secure-next-auth.session-token' : 'next-auth.session-token'
|
||||||
|
const sessionJWT = req.cookies[sessionCookieName]
|
||||||
|
|
||||||
|
if (!userId && !sessionJWT) {
|
||||||
|
// no cookie pointer and no session cookie present. nothing to do.
|
||||||
|
res.status(404).end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = []
|
||||||
|
|
||||||
|
const cookieOptions = {
|
||||||
|
path: '/',
|
||||||
|
secure: req.secure,
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
expires: datePivot(new Date(), { months: 1 })
|
||||||
|
}
|
||||||
|
// remove JWT pointed to by cookie pointer
|
||||||
|
cookies.push(cookie.serialize(`multi_auth.${userId}`, '', { ...cookieOptions, expires: 0, maxAge: 0 }))
|
||||||
|
|
||||||
|
// update multi_auth cookie and check if there are more accounts available
|
||||||
|
const oldMultiAuth = b64Decode(req.cookies.multi_auth)
|
||||||
|
const newMultiAuth = oldMultiAuth.filter(({ id }) => id !== Number(userId))
|
||||||
|
if (newMultiAuth.length === 0) {
|
||||||
|
// no next account available. cleanup: remove multi_auth + pointer cookie
|
||||||
|
cookies.push(cookie.serialize('multi_auth', '', { ...cookieOptions, httpOnly: false, expires: 0, maxAge: 0 }))
|
||||||
|
cookies.push(cookie.serialize('multi_auth.user-id', '', { ...cookieOptions, httpOnly: false, expires: 0, maxAge: 0 }))
|
||||||
|
res.setHeader('Set-Cookie', cookies)
|
||||||
|
res.status(204).end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cookies.push(cookie.serialize('multi_auth', b64Encode(newMultiAuth), { ...cookieOptions, httpOnly: false }))
|
||||||
|
|
||||||
|
const newUserId = newMultiAuth[0].id
|
||||||
|
const newUserJWT = req.cookies[`multi_auth.${newUserId}`]
|
||||||
|
res.setHeader('Set-Cookie', [
|
||||||
|
...cookies,
|
||||||
|
cookie.serialize(cookiePointerName, newUserId, { ...cookieOptions, httpOnly: false }),
|
||||||
|
cookie.serialize(sessionCookieName, newUserJWT, cookieOptions)
|
||||||
|
])
|
||||||
|
|
||||||
|
res.status(302).end()
|
||||||
|
}
|
||||||
|
|
||||||
|
const b64Encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64')
|
||||||
|
const b64Decode = s => JSON.parse(Buffer.from(s, 'base64'))
|
@ -6,8 +6,15 @@ import { StaticLayout } from '@/components/layout'
|
|||||||
import Login from '@/components/login'
|
import Login from '@/components/login'
|
||||||
import { isExternal } from '@/lib/url'
|
import { isExternal } from '@/lib/url'
|
||||||
|
|
||||||
export async function getServerSideProps ({ req, res, query: { callbackUrl, error = null } }) {
|
export async function getServerSideProps ({ req, res, query: { callbackUrl, multiAuth = false, error = null } }) {
|
||||||
const session = await getServerSession(req, res, getAuthOptions(req))
|
let session = await getServerSession(req, res, getAuthOptions(req))
|
||||||
|
|
||||||
|
// required to prevent infinite redirect loops if we switch to anon
|
||||||
|
// but are on a page that would redirect us to /signup.
|
||||||
|
// without this code, /signup would redirect us back to the callbackUrl.
|
||||||
|
if (req.cookies['multi_auth.user-id'] === 'anonymous') {
|
||||||
|
session = null
|
||||||
|
}
|
||||||
|
|
||||||
// prevent open redirects. See https://github.com/stackernews/stacker.news/issues/264
|
// prevent open redirects. See https://github.com/stackernews/stacker.news/issues/264
|
||||||
// let undefined urls through without redirect ... otherwise this interferes with multiple auth linking
|
// let undefined urls through without redirect ... otherwise this interferes with multiple auth linking
|
||||||
@ -22,9 +29,9 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, erro
|
|||||||
callbackUrl = '/'
|
callbackUrl = '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session && callbackUrl) {
|
if (session && callbackUrl && !multiAuth) {
|
||||||
// in the cause of auth linking we want to pass the error back to
|
// in the case of auth linking we want to pass the error back to settings
|
||||||
// settings
|
// in the case of multi auth, don't redirect if there is already a session
|
||||||
if (error) {
|
if (error) {
|
||||||
const url = new URL(callbackUrl, process.env.NEXT_PUBLIC_URL)
|
const url = new URL(callbackUrl, process.env.NEXT_PUBLIC_URL)
|
||||||
url.searchParams.set('error', error)
|
url.searchParams.set('error', error)
|
||||||
@ -39,11 +46,14 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, erro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const providers = await getProviders()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
providers: await getProviders(),
|
providers,
|
||||||
callbackUrl,
|
callbackUrl,
|
||||||
error
|
error,
|
||||||
|
multiAuth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ export const getServerSideProps = getGetServerSideProps({ query: REFERRALS, auth
|
|||||||
|
|
||||||
export default function Referrals ({ ssrData }) {
|
export default function Referrals ({ ssrData }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const select = async values => {
|
const select = async values => {
|
||||||
const { when, ...query } = values
|
const { when, ...query } = values
|
||||||
|
@ -84,7 +84,7 @@ export function SettingsHeader () {
|
|||||||
|
|
||||||
export default function Settings ({ ssrData }) {
|
export default function Settings ({ ssrData }) {
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [setSettings] = useMutation(SET_SETTINGS, {
|
const [setSettings] = useMutation(SET_SETTINGS, {
|
||||||
update (cache, { data: { setSettings } }) {
|
update (cache, { data: { setSettings } }) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
@ -96,13 +96,14 @@ export default function Settings ({ ssrData }) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
const logger = useServiceWorkerLogger()
|
const logger = useServiceWorkerLogger()
|
||||||
|
|
||||||
const { data } = useQuery(SETTINGS)
|
const { data } = useQuery(SETTINGS)
|
||||||
const { settings: { privates: settings } } = useMemo(() => data ?? ssrData, [data, ssrData])
|
const { settings: { privates: settings } } = useMemo(() => data ?? ssrData, [data, ssrData])
|
||||||
if (!data && !ssrData) return <PageLoading />
|
|
||||||
|
// if we switched to anon, me is null before the page is reloaded
|
||||||
|
if ((!data && !ssrData) || !me) return <PageLoading />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
@ -110,6 +111,7 @@ export default function Settings ({ ssrData }) {
|
|||||||
<SettingsHeader />
|
<SettingsHeader />
|
||||||
{hasOnlyOneAuthMethod(settings?.authMethods) && <AuthBanner />}
|
{hasOnlyOneAuthMethod(settings?.authMethods) && <AuthBanner />}
|
||||||
<Form
|
<Form
|
||||||
|
enableReinitialize
|
||||||
initial={{
|
initial={{
|
||||||
tipDefault: settings?.tipDefault || 21,
|
tipDefault: settings?.tipDefault || 21,
|
||||||
tipRandom: settings?.tipRandom,
|
tipRandom: settings?.tipRandom,
|
||||||
@ -870,7 +872,7 @@ export function EmailLinkForm ({ callbackUrl }) {
|
|||||||
|
|
||||||
function ApiKey ({ enabled, apiKey }) {
|
function ApiKey ({ enabled, apiKey }) {
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [generateApiKey] = useMutation(
|
const [generateApiKey] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation generateApiKey($id: ID!) {
|
mutation generateApiKey($id: ID!) {
|
||||||
@ -996,7 +998,7 @@ function ApiKeyModal ({ apiKey }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ApiKeyDeleteObstacle ({ onClose }) {
|
function ApiKeyDeleteObstacle ({ onClose }) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [deleteApiKey] = useMutation(
|
const [deleteApiKey] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation deleteApiKey($id: ID!) {
|
mutation deleteApiKey($id: ID!) {
|
||||||
|
@ -61,7 +61,7 @@ export default function Wallet () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function YouHaveSats () {
|
function YouHaveSats () {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const limitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
|
const limitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
|
||||||
return (
|
return (
|
||||||
<h2 className={`${me ? 'visible' : 'invisible'} ${limitReached ? 'text-warning' : 'text-success'}`}>
|
<h2 className={`${me ? 'visible' : 'invisible'} ${limitReached ? 'text-warning' : 'text-success'}`}>
|
||||||
@ -108,7 +108,7 @@ export function WalletForm () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FundForm () {
|
export function FundForm () {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const [showAlert, setShowAlert] = useState(true)
|
const [showAlert, setShowAlert] = useState(true)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [createInvoice, { called, error }] = useMutation(gql`
|
const [createInvoice, { called, error }] = useMutation(gql`
|
||||||
@ -211,7 +211,7 @@ export function SelectedWithdrawalForm () {
|
|||||||
|
|
||||||
export function InvWithdrawal () {
|
export function InvWithdrawal () {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const [createWithdrawl, { called, error }] = useMutation(CREATE_WITHDRAWL)
|
const [createWithdrawl, { called, error }] = useMutation(CREATE_WITHDRAWL)
|
||||||
|
|
||||||
@ -358,7 +358,7 @@ export function LnWithdrawal () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LnAddrWithdrawal () {
|
export function LnAddrWithdrawal () {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [sendToLnAddr, { called, error }] = useMutation(SEND_TO_LNADDR)
|
const [sendToLnAddr, { called, error }] = useMutation(SEND_TO_LNADDR)
|
||||||
const defaultOptions = { min: 1 }
|
const defaultOptions = { min: 1 }
|
||||||
|
@ -123,7 +123,7 @@ function LoadWithdrawl () {
|
|||||||
function PrivacyOption ({ wd }) {
|
function PrivacyOption ({ wd }) {
|
||||||
if (!wd.bolt11) return
|
if (!wd.bolt11) return
|
||||||
|
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const keepUntil = datePivot(new Date(wd.createdAt), { days: INVOICE_RETENTION_DAYS })
|
const keepUntil = datePivot(new Date(wd.createdAt), { days: INVOICE_RETENTION_DAYS })
|
||||||
const oldEnough = new Date() >= keepUntil
|
const oldEnough = new Date() >= keepUntil
|
||||||
if (!oldEnough) {
|
if (!oldEnough) {
|
||||||
|
@ -241,6 +241,10 @@ $zindex-sticky: 900;
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-fit-content {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
@media (display-mode: standalone) {
|
@media (display-mode: standalone) {
|
||||||
.standalone {
|
.standalone {
|
||||||
display: flex
|
display: flex
|
||||||
@ -251,6 +255,14 @@ $zindex-sticky: 900;
|
|||||||
justify-self: center;
|
justify-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--bs-body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-primary {
|
||||||
|
fill: var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.text-primary svg {
|
.text-primary svg {
|
||||||
fill: var(--bs-primary);
|
fill: var(--bs-primary);
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ export const Status = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useWallet (name) {
|
export function useWallet (name) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const [disableFreebies] = useMutation(gql`mutation { disableFreebies }`)
|
const [disableFreebies] = useMutation(gql`mutation { disableFreebies }`)
|
||||||
@ -77,9 +77,9 @@ export function useWallet (name) {
|
|||||||
}, [saveConfig, me, logger])
|
}, [saveConfig, me, logger])
|
||||||
|
|
||||||
// delete is a reserved keyword
|
// delete is a reserved keyword
|
||||||
const delete_ = useCallback(async () => {
|
const delete_ = useCallback(async (options) => {
|
||||||
try {
|
try {
|
||||||
await clearConfig({ logger })
|
await clearConfig({ logger, ...options })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err.message || err.toString?.()
|
const message = err.message || err.toString?.()
|
||||||
logger.error(message)
|
logger.error(message)
|
||||||
@ -87,6 +87,11 @@ export function useWallet (name) {
|
|||||||
}
|
}
|
||||||
}, [clearConfig, logger, disablePayments])
|
}, [clearConfig, logger, disablePayments])
|
||||||
|
|
||||||
|
const deleteLogs_ = useCallback(async (options) => {
|
||||||
|
// first argument is to override the wallet
|
||||||
|
return await deleteLogs(options)
|
||||||
|
}, [deleteLogs])
|
||||||
|
|
||||||
if (!wallet) return null
|
if (!wallet) return null
|
||||||
|
|
||||||
// Assign everything to wallet object so every function that is passed this wallet object in this
|
// Assign everything to wallet object so every function that is passed this wallet object in this
|
||||||
@ -102,7 +107,7 @@ export function useWallet (name) {
|
|||||||
wallet.config = config
|
wallet.config = config
|
||||||
wallet.save = save
|
wallet.save = save
|
||||||
wallet.delete = delete_
|
wallet.delete = delete_
|
||||||
wallet.deleteLogs = deleteLogs
|
wallet.deleteLogs = deleteLogs_
|
||||||
wallet.setPriority = setPriority
|
wallet.setPriority = setPriority
|
||||||
wallet.hasConfig = hasConfig
|
wallet.hasConfig = hasConfig
|
||||||
wallet.status = status
|
wallet.status = status
|
||||||
@ -151,7 +156,7 @@ function extractServerConfig (fields, config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useConfig (wallet) {
|
function useConfig (wallet) {
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const storageKey = getStorageKey(wallet?.name, me)
|
const storageKey = getStorageKey(wallet?.name, me)
|
||||||
const [clientConfig, setClientConfig, clearClientConfig] = useClientConfig(storageKey, {})
|
const [clientConfig, setClientConfig, clearClientConfig] = useClientConfig(storageKey, {})
|
||||||
@ -237,13 +242,13 @@ function useConfig (wallet) {
|
|||||||
}
|
}
|
||||||
}, [hasClientConfig, hasServerConfig, setClientConfig, setServerConfig, wallet])
|
}, [hasClientConfig, hasServerConfig, setClientConfig, setServerConfig, wallet])
|
||||||
|
|
||||||
const clearConfig = useCallback(async ({ logger }) => {
|
const clearConfig = useCallback(async ({ logger, clientOnly }) => {
|
||||||
if (hasClientConfig) {
|
if (hasClientConfig) {
|
||||||
clearClientConfig()
|
clearClientConfig()
|
||||||
wallet.disablePayments()
|
wallet.disablePayments()
|
||||||
logger.ok('wallet detached for payments')
|
logger.ok('wallet detached for payments')
|
||||||
}
|
}
|
||||||
if (hasServerConfig) await clearServerConfig()
|
if (hasServerConfig && !clientOnly) await clearServerConfig()
|
||||||
}, [hasClientConfig, hasServerConfig, clearClientConfig, clearServerConfig, wallet])
|
}, [hasClientConfig, hasServerConfig, clearClientConfig, clearServerConfig, wallet])
|
||||||
|
|
||||||
return [config, saveConfig, clearConfig]
|
return [config, saveConfig, clearConfig]
|
||||||
@ -268,7 +273,7 @@ function isConfigured ({ fields, config }) {
|
|||||||
|
|
||||||
function useServerConfig (wallet) {
|
function useServerConfig (wallet) {
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const me = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
const { data, refetch: refetchConfig } = useQuery(WALLET_BY_TYPE, { variables: { type: wallet?.walletType }, skip: !wallet?.walletType })
|
const { data, refetch: refetchConfig } = useQuery(WALLET_BY_TYPE, { variables: { type: wallet?.walletType }, skip: !wallet?.walletType })
|
||||||
|
|
||||||
@ -404,9 +409,9 @@ export function useWallets () {
|
|||||||
const resetClient = useCallback(async (wallet) => {
|
const resetClient = useCallback(async (wallet) => {
|
||||||
for (const w of wallets) {
|
for (const w of wallets) {
|
||||||
if (w.canSend) {
|
if (w.canSend) {
|
||||||
await w.delete()
|
await w.delete({ clientOnly: true })
|
||||||
}
|
}
|
||||||
await w.deleteLogs()
|
await w.deleteLogs({ clientOnly: true })
|
||||||
}
|
}
|
||||||
}, [wallets])
|
}, [wallets])
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user