Compare commits

...

8 Commits

Author SHA1 Message Date
Simone Cervino c88afc5aae
fix can't upload mp4 on safari (#1617) 2024-11-20 07:06:05 -06:00
ekzyis 6bae1f1a89
Fix account switching anon login (#1618)
* Always switch to user we just logged in as

If we're logged in and switch to anon and then use login to get into our previous account instead of using 'switch accounts', we only updated the JWT but we didn't switch to the user.

* Fix getToken unaware of multi-auth middleware

If we use login with new credentials while switched to anon (multi_auth.user-id === 'anonymous'), we updated the pubkey because getToken wasn't aware of the switch and thus believed we're logged in as a user.

This is fixed by applying the middleware before calling getToken.
2024-11-20 07:05:42 -06:00
ekzyis 82fead60f1
Fix mentions for pessimistic actions (#1615)
* Fix mentions for pessimistic actions

* (item)mentions should use tx not models

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-11-19 19:28:21 -06:00
k00b 5dac2f2ed0 fix #1566 2024-11-19 19:12:18 -06:00
Keyan 9179688abc
Merge pull request #1614 from stackernews/wallet-fixes
Wallet fixes
2024-11-19 15:49:12 -06:00
k00b 66ec5d5da8 dark/light mode images on wallet pages 2024-11-19 15:46:11 -06:00
k00b f3cc0f9e1d make recv optional 2024-11-19 15:38:27 -06:00
k00b aa4c448999 fix account switch disconnect 2024-11-19 14:58:48 -06:00
12 changed files with 90 additions and 57 deletions

View File

@ -2,11 +2,11 @@ import { USER_ID } from '@/lib/constants'
import { deleteReminders, getDeleteAt, getRemindAt } from '@/lib/item'
import { parseInternalLinks } from '@/lib/url'
export async function getMentions ({ text }, { me, models }) {
export async function getMentions ({ text }, { me, tx }) {
const mentionPattern = /\B@[\w_]+/gi
const names = text.match(mentionPattern)?.map(m => m.slice(1))
if (names?.length > 0) {
const users = await models.user.findMany({
const users = await tx.user.findMany({
where: {
name: {
in: names
@ -21,7 +21,7 @@ export async function getMentions ({ text }, { me, models }) {
return []
}
export const getItemMentions = async ({ text }, { me, models }) => {
export const getItemMentions = async ({ text }, { me, tx }) => {
const linkPattern = new RegExp(`${process.env.NEXT_PUBLIC_URL}/items/\\d+[a-zA-Z0-9/?=]*`, 'gi')
const refs = text.match(linkPattern)?.map(m => {
try {
@ -33,7 +33,7 @@ export const getItemMentions = async ({ text }, { me, models }) => {
}).filter(r => !!r)
if (refs?.length > 0) {
const referee = await models.item.findMany({
const referee = await tx.item.findMany({
where: {
id: { in: refs },
userId: { not: me?.id || USER_ID.anon }

View File

@ -2,7 +2,7 @@ import { useShowModal } from './modal'
import { useToast } from './toast'
import ItemAct from './item-act'
import AccordianItem from './accordian-item'
import { useMemo, useState } from 'react'
import { useMemo } from 'react'
import getColor from '@/lib/rainbow'
import BoostIcon from '@/svgs/arrow-up-double-line.svg'
import styles from './upvote.module.css'
@ -12,32 +12,29 @@ import classNames from 'classnames'
export default function Boost ({ item, className, ...props }) {
const { boost } = item
const [hover, setHover] = useState(false)
const [color, nextColor] = useMemo(() => [getColor(boost), getColor(boost + BOOST_MULT)], [boost])
const style = useMemo(() => (hover || boost
? {
fill: hover ? nextColor : color,
filter: `drop-shadow(0 0 6px ${hover ? nextColor : color}90)`
}
: undefined), [boost, hover])
const style = useMemo(() => ({
'--hover-fill': nextColor,
'--hover-filter': `drop-shadow(0 0 6px ${nextColor}90)`,
'--fill': color,
'--filter': `drop-shadow(0 0 6px ${color}90)`
}), [color, nextColor])
return (
<Booster
item={item} As={({ ...oprops }) =>
item={item} As={oprops =>
<div className='upvoteParent'>
<div
className={styles.upvoteWrapper}
>
<BoostIcon
{...props} {...oprops} style={style}
{...props}
{...oprops}
style={style}
width={26}
height={26}
onPointerEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
onTouchEnd={() => setHover(false)}
className={classNames(styles.boost, className, boost && styles.voted)}
className={classNames(styles.boost, className, boost && styles.boosted)}
/>
</div>
</div>}

View File

@ -78,6 +78,11 @@ export const FileUpload = forwardRef(({ children, className, onSelect, onUpload,
element.onerror = reject
element.src = window.URL.createObjectURL(file)
// iOS Force the video to load metadata
if (element.tagName === 'VIDEO') {
element.load()
}
})
}, [toaster, getSignedPOST])

View File

@ -109,7 +109,6 @@ export default function UpVote ({ item, className, collapsed }) {
const [tipShow, _setTipShow] = useState(false)
const ref = useRef()
const { me } = useMe()
const [hover, setHover] = useState(false)
const [setWalkthrough] = useMutation(
gql`
mutation setWalkthrough($upvotePopover: Boolean, $tipPopover: Boolean) {
@ -172,10 +171,6 @@ export default function UpVote ({ item, className, collapsed }) {
me, item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault,
me?.privates?.tipRandom, me?.privates?.tipRandomMin, me?.privates?.tipRandomMax, pending])
const handleModalClosed = () => {
setHover(false)
}
const handleLongPress = (e) => {
if (!item) return
@ -195,7 +190,7 @@ export default function UpVote ({ item, className, collapsed }) {
setController(c)
showModal(onClose =>
<ItemAct onClose={onClose} item={item} abortSignal={c.signal} />, { onClose: handleModalClosed })
<ItemAct onClose={onClose} item={item} abortSignal={c.signal} />)
}
const handleShortPress = async () => {
@ -223,19 +218,16 @@ export default function UpVote ({ item, className, collapsed }) {
await zap({ item, me, abortSignal: c.signal })
} else {
showModal(onClose => <ItemAct onClose={onClose} item={item} />, { onClose: handleModalClosed })
showModal(onClose => <ItemAct onClose={onClose} item={item} />)
}
}
const style = useMemo(() => {
const fillColor = pending || hover ? nextColor : color
return meSats || hover || pending
? {
fill: fillColor,
filter: `drop-shadow(0 0 6px ${fillColor}90)`
}
: undefined
}, [hover, pending, nextColor, color, meSats])
const style = useMemo(() => ({
'--hover-fill': nextColor,
'--hover-filter': `drop-shadow(0 0 6px ${nextColor}90)`,
'--fill': color,
'--filter': `drop-shadow(0 0 6px ${color}90)`
}), [color, nextColor])
return (
<div ref={ref} className='upvoteParent'>
@ -246,9 +238,6 @@ export default function UpVote ({ item, className, collapsed }) {
<ActionTooltip notForm disable={disabled} overlayText={overlayText}>
<div className={classNames(disabled && styles.noSelfTips, styles.upvoteWrapper)}>
<UpBolt
onPointerEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
onTouchEnd={() => setHover(false)}
width={26}
height={26}
className={classNames(styles.upvote,

View File

@ -5,6 +5,11 @@
-webkit-touch-callout: none;
}
.upvote:hover {
fill: var(--hover-fill) !important;
filter: var(--hover-filter) !important;
}
.boost {
fill: var(--theme-clickToContextColor);
user-select: none;
@ -12,6 +17,16 @@
-webkit-touch-callout: none;
}
.boost:hover {
fill: var(--hover-fill) !important;
filter: var(--hover-filter) !important;
}
.boost.boosted {
fill: var(--fill);
filter: var(--filter);
}
.upvoteWrapper {
position: relative;
padding-right: .2rem;
@ -28,8 +43,8 @@
}
.upvote.voted {
fill: #F6911D;
filter: drop-shadow(0 0 6px #f6911d90);
fill: var(--fill);
filter: var(--filter);
}
.cover {
@ -43,6 +58,7 @@
}
.pending {
fill: var(--hover-fill);
animation-name: pulse;
animation-iteration-count: infinite;
animation-timing-function: linear;

View File

@ -27,12 +27,17 @@ function useIndexedDB ({ dbName, storeName, options = DEFAULT_OPTIONS, indices =
db.transaction(storeName)
while (operationQueue.current.length > 0) {
const operation = operationQueue.current.shift()
operation(db)
// if the db is the same as the one we're processing, run the operation
// else, we'll just clear the operation queue
// XXX this is a consquence of using a ref to store the queue and should be fixed
if (dbName === db.name) {
operation(db)
}
}
} catch (error) {
handleError(error)
}
}, [storeName, handleError, operationQueue])
}, [dbName, storeName, handleError, operationQueue])
useEffect(() => {
let isMounted = true

View File

@ -14,6 +14,7 @@ import { schnorr } from '@noble/curves/secp256k1'
import { notifyReferral } from '@/lib/webPush'
import { hashEmail } from '@/lib/crypto'
import * as cookie from 'cookie'
import { multiAuthMiddleware } from '@/pages/api/graphql'
/**
* Stores userIds in user table
@ -132,6 +133,9 @@ function setMultiAuthCookies (req, res, { id, jwt, name, photoId }) {
// add JWT to **httpOnly** cookie
res.appendHeader('Set-Cookie', cookie.serialize(`multi_auth.${id}`, jwt, cookieOptions))
// switch to user we just added
res.appendHeader('Set-Cookie', cookie.serialize('multi_auth.user-id', id, { ...cookieOptions, httpOnly: false }))
let newMultiAuth = [{ id, name, photoId }]
if (req.cookies.multi_auth) {
const oldMultiAuth = b64Decode(req.cookies.multi_auth)
@ -140,9 +144,6 @@ function setMultiAuthCookies (req, res, { id, jwt, name, photoId }) {
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) {
@ -165,6 +166,7 @@ async function pubkeyAuth (credentials, req, res, pubkeyColumnName) {
let user = await prisma.user.findUnique({ where: { [pubkeyColumnName]: pubkey } })
// get token if it exists
req = multiAuthMiddleware(req)
const token = await getToken({ req })
if (!user) {
// we have not seen this pubkey before

View File

@ -82,7 +82,7 @@ export default startServerAndCreateNextHandler(apolloServer, {
}
})
function multiAuthMiddleware (request) {
export function multiAuthMiddleware (request) {
// switch next-auth session cookie with multi_auth cookie if cookie pointer present
// is there a cookie pointer?

View File

@ -13,11 +13,12 @@ import { canReceive, canSend, isConfigured } from '@/wallets/common'
import { SSR } from '@/lib/constants'
import WalletButtonBar from '@/components/wallet-buttonbar'
import { useWalletConfigurator } from '@/wallets/config'
import { useCallback, useMemo } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useMe } from '@/components/me'
import validateWallet from '@/wallets/validate'
import { ValidationError } from 'yup'
import { useFormikContext } from 'formik'
import useDarkMode from '@/components/dark-mode'
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
@ -28,6 +29,8 @@ export default function WalletSettings () {
const wallet = useWallet(name)
const { me } = useMe()
const { save, detach } = useWalletConfigurator(wallet)
const [dark] = useDarkMode()
const [imgSrc, setImgSrc] = useState(wallet?.def.card?.image?.src)
const initial = useMemo(() => {
const initial = wallet?.def.fields.reduce((acc, field) => {
@ -69,12 +72,16 @@ export default function WalletSettings () {
const { card: { image, title, subtitle } } = wallet?.def || { card: {} }
useEffect(() => {
if (!imgSrc) return
// wallet.png <-> wallet-dark.png
setImgSrc(dark ? image?.src.replace(/\.([a-z]{3})$/, '-dark.$1') : image?.src)
}, [dark])
return (
<CenterLayout>
{image
? typeof image === 'object'
? <img {...image} alt={title} className='pb-2' />
: <img src={image} width='33%' alt={title} className='pb-2' />
? <img alt={title} {...image} src={imgSrc} className='pb-3 px-2 mw-100' />
: <h2 className='pb-2'>{title}</h2>}
<h6 className='text-muted text-center pb-3'><Text>{subtitle}</Text></h6>
<Form

View File

@ -0,0 +1,11 @@
-- AlterTable
ALTER TABLE "WalletBlink" ALTER COLUMN "apiKeyRecv" DROP NOT NULL;
-- AlterTable
ALTER TABLE "WalletLNbits" ALTER COLUMN "invoiceKey" DROP NOT NULL;
-- AlterTable
ALTER TABLE "WalletNWC" ALTER COLUMN "nwcUrlRecv" DROP NOT NULL;
-- AlterTable
ALTER TABLE "WalletPhoenixd" ALTER COLUMN "secondaryPassword" DROP NOT NULL;

View File

@ -289,7 +289,7 @@ model WalletLNbits {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
url String
invoiceKey String
invoiceKey String?
}
model WalletNWC {
@ -298,7 +298,7 @@ model WalletNWC {
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
nwcUrlRecv String
nwcUrlRecv String?
}
model WalletBlink {
@ -307,7 +307,7 @@ model WalletBlink {
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
apiKeyRecv String
apiKeyRecv String?
currencyRecv String?
}
@ -318,7 +318,7 @@ model WalletPhoenixd {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
url String
secondaryPassword String
secondaryPassword String?
}
model Mute {

View File

@ -119,11 +119,12 @@ async function performPessimisticAction ({ lndInvoice, dbInvoice, tx, models, ln
const context = {
tx,
cost: BigInt(lndInvoice.received_mtokens),
me: dbInvoice.user,
sybilFeePercent: await paidActions[dbInvoice.actionType].getSybilFeePercent?.()
me: dbInvoice.user
}
const result = await paidActions[dbInvoice.actionType].perform(args, context)
const sybilFeePercent = await paidActions[dbInvoice.actionType].getSybilFeePercent?.(args, context)
const result = await paidActions[dbInvoice.actionType].perform(args, { ...context, sybilFeePercent })
await tx.invoice.update({
where: { id: dbInvoice.id },
data: {