Compare commits
6 Commits
7090ea3b70
...
c83ff02a85
Author | SHA1 | Date | |
---|---|---|---|
|
c83ff02a85 | ||
|
85a4839538 | ||
|
471888563e | ||
|
691818e779 | ||
|
c6ab776091 | ||
|
47bfa24b57 |
@ -150,4 +150,7 @@ PERSISTENCE=1
|
||||
SKIP_SSL_CERT_DOWNLOAD=1
|
||||
|
||||
# tor
|
||||
TOR_PROXY=http://127.0.0.1:7050/
|
||||
TOR_PROXY=http://127.0.0.1:7050/
|
||||
|
||||
# lnbits
|
||||
LNBITS_WEB_PORT=5000
|
@ -1,17 +1,16 @@
|
||||
import lndService from 'ln-service'
|
||||
import lnd from '@/api/lnd'
|
||||
|
||||
const cache = new Map()
|
||||
const expiresIn = 1000 * 30 // 30 seconds in milliseconds
|
||||
|
||||
async function fetchChainFeeRate () {
|
||||
let chainFee = 0
|
||||
try {
|
||||
const fee = await lndService.getChainFeeRate({ lnd })
|
||||
chainFee = fee.tokens_per_vbyte
|
||||
} catch (err) {
|
||||
console.error('fetchChainFee', err)
|
||||
}
|
||||
const url = 'https://mempool.space/api/v1/fees/recommended'
|
||||
const chainFee = await fetch(url)
|
||||
.then((res) => res.json())
|
||||
.then((body) => body.hourFee)
|
||||
.catch((err) => {
|
||||
console.error('fetchChainFee', err)
|
||||
return 0
|
||||
})
|
||||
|
||||
cache.set('fee', { fee: chainFee, createdAt: Date.now() })
|
||||
return chainFee
|
||||
}
|
||||
|
18
awards.csv
18
awards.csv
@ -74,11 +74,23 @@ SatsAllDay,issue,#1137,#1125,good-first-issue,,,,2k,weareallsatoshi@getalby.com,
|
||||
SatsAllDay,helpfulness,#1137,#1125,good-first-issue,,,,2k,weareallsatoshi@getalby.com,2024-05-04
|
||||
itsrealfake,pr,#1138,#995,good-first-issue,,,,20k,itsrealfake2@stacker.news,2024-05-06
|
||||
SouthKoreaLN,issue,#1138,#995,good-first-issue,,,,2k,south_korea_ln@stacker.news,2024-05-04
|
||||
mateusdeap,helpfulness,#1138,#995,good-first-issue,,,,1k,???,???
|
||||
mateusdeap,helpfulness,#1138,#995,good-first-issue,,,,1k,mateusdeap@stacker.news,???
|
||||
felipebueno,pr,#1094,,,,2,,80k,felipebueno@getalby.com,2024-05-06
|
||||
benalleng,helpfulness,#1127,#927,good-first-issue,,,,2k,benalleng@mutiny.plus,2024-05-04
|
||||
itsrealfake,pr,#1135,#1016,good-first-issue,,,nonideal solution,10k,itsrealfake2@stacker.news,2024-05-06
|
||||
SatsAllDay,issue,#1135,#1016,good-first-issue,,,,1k,weareallsatoshi@getalby.com,2024-05-04
|
||||
s373nZ,issue,#1136,#1107,medium,high,,,50k,se7enz@minibits.cash,2024-05-05
|
||||
benalleng,pr,#1129,#1045,good-first-issue,,,paid for advice out of band,20k,benalleng@mutiny.plus,???
|
||||
benalleng,pr,#1129,#491,good-first-issue,,,,20k,benalleng@mutiny.plus,???
|
||||
abhiShandy,pr,#1123,#624,good-first-issue,,,,20k,abhishandy@stacker.news,???
|
||||
hkarani,pr,#1147,#1143,good-first-issue,,,,20k,asterisk32@stacker.news,???
|
||||
benalleng,helpfulness,#1147,#1143,good-first-issue,,,,2k,benalleng@mutiny.plus,???
|
||||
abhiShandy,pr,#1157,#1148,good-first-issue,,,,20k,abhishandy@stacker.news,???
|
||||
SatsAllDay,issue,#1157,#1148,good-first-issue,,,,2k,weareallsatoshi@getalby.com,???
|
||||
abhiShandy,pr,#1158,#1139,good-first-issue,,,,20k,abhishandy@stacker.news,???
|
||||
SatsAllDay,issue,#1158,#1139,good-first-issue,,,,2k,weareallsatoshi@getalby.com,???
|
||||
SatsAllDay,pr,#1145,#717,medium,,,,250k,weareallsatoshi@getalby.com,???
|
||||
benalleng,pr,#1129,#491,good-first-issue,,,paid for advice out of band,20k,benalleng@mutiny.plus,???
|
||||
benalleng,pr,#1129,#1045,easy,,2,post-humously upgraded to easy,80k,benalleng@mutiny.plus,???
|
||||
SouthKoreaLN,issue,#1129,#1045,easy,,,,8k,south_korea_ln@stacker.news,???
|
||||
tsmith123,pr,#1171,#1124,good-first-issue,,,bonus for refactor,40k,stickymarch60@walletofsatoshi.com,???
|
||||
SatsAllDay,issue,#1171,#1124,good-first-issue,,,,4k,weareallsatoshi@getalby.com,???
|
||||
felipebueno,pr,#1162,,,,2,,200k,felipebueno@getalby.com,???
|
||||
|
|
49
components/hoverable-popover.js
Normal file
49
components/hoverable-popover.js
Normal file
@ -0,0 +1,49 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { Popover } from 'react-bootstrap'
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger'
|
||||
import styles from './hoverable-popover.module.css'
|
||||
|
||||
export default function HoverablePopover ({ id, trigger, body, onShow }) {
|
||||
const [showOverlay, setShowOverlay] = useState(false)
|
||||
|
||||
const timeoutId = useRef(null)
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
clearTimeout(timeoutId.current)
|
||||
onShow && onShow()
|
||||
timeoutId.current = setTimeout(() => {
|
||||
setShowOverlay(true)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
clearTimeout(timeoutId.current)
|
||||
timeoutId.current = setTimeout(() => setShowOverlay(false), 100)
|
||||
}
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
show={showOverlay}
|
||||
placement='bottom'
|
||||
onHide={handleMouseLeave}
|
||||
overlay={
|
||||
<Popover
|
||||
onPointerEnter={handleMouseEnter}
|
||||
onPointerLeave={handleMouseLeave}
|
||||
className={styles.HoverablePopover}
|
||||
>
|
||||
<Popover.Body className={styles.HoverablePopover}>
|
||||
{body}
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<span
|
||||
onPointerEnter={handleMouseEnter}
|
||||
onPointerLeave={handleMouseLeave}
|
||||
>
|
||||
{trigger}
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
)
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
.userPopover {
|
||||
.hoverablePopover {
|
||||
border: 1px solid var(--theme-toolbarActive)
|
||||
}
|
||||
|
||||
.userPopBody {
|
||||
.hoverablePopBody {
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
@ -26,7 +26,7 @@ import UserPopover from './user-popover'
|
||||
export default function ItemInfo ({
|
||||
item, full, commentsText = 'comments',
|
||||
commentTextSingular = 'comment', className, embellishUser, extraInfo, onEdit, editText,
|
||||
onQuoteReply, extraBadges, nested, pinnable
|
||||
onQuoteReply, extraBadges, nested, pinnable, showActionDropdown = true, showUser = true
|
||||
}) {
|
||||
const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
|
||||
const me = useMe()
|
||||
@ -61,10 +61,10 @@ export default function ItemInfo ({
|
||||
{!(item.position && (pinnable || !item.subName)) && !(!item.parentId && Number(item.user?.id) === AD_USER_ID) &&
|
||||
<>
|
||||
<span title={`from ${numWithUnits(item.upvotes, {
|
||||
abbreviate: false,
|
||||
unitSingular: 'stacker',
|
||||
unitPlural: 'stackers'
|
||||
})} ${item.mine
|
||||
abbreviate: false,
|
||||
unitSingular: 'stacker',
|
||||
unitPlural: 'stackers'
|
||||
})} ${item.mine
|
||||
? `\\ ${numWithUnits(item.meSats, { abbreviate: false })} to post`
|
||||
: `(${numWithUnits(meTotalSats, { abbreviate: false })}${item.meDontLikeSats
|
||||
? ` & ${numWithUnits(item.meDontLikeSats, { abbreviate: false, unitSingular: 'downsat', unitPlural: 'downsats' })}`
|
||||
@ -102,12 +102,13 @@ export default function ItemInfo ({
|
||||
</Link>
|
||||
<span> \ </span>
|
||||
<span>
|
||||
<UserPopover name={item.user.name}>
|
||||
<Link href={`/${item.user.name}`}>
|
||||
@{item.user.name}<span> </span><Hat className='fill-grey' user={item.user} height={12} width={12} />
|
||||
{embellishUser}
|
||||
</Link>
|
||||
</UserPopover>
|
||||
{showUser &&
|
||||
<UserPopover name={item.user.name}>
|
||||
<Link href={`/${item.user.name}`}>
|
||||
@{item.user.name}<span> </span><Hat className='fill-grey' user={item.user} height={12} width={12} />
|
||||
{embellishUser}
|
||||
</Link>
|
||||
</UserPopover>}
|
||||
<span> </span>
|
||||
<Link href={`/items/${item.id}`} title={item.createdAt} className='text-reset' suppressHydrationWarning>
|
||||
{timeSince(new Date(item.createdAt))}
|
||||
@ -152,54 +153,57 @@ export default function ItemInfo ({
|
||||
/>
|
||||
</span>
|
||||
</>}
|
||||
<ActionDropdown>
|
||||
<CopyLinkDropdownItem item={item} />
|
||||
{(item.parentId || item.text) && onQuoteReply &&
|
||||
<Dropdown.Item onClick={onQuoteReply}>quote reply</Dropdown.Item>}
|
||||
{me && <BookmarkDropdownItem item={item} />}
|
||||
{me && <SubscribeDropdownItem item={item} />}
|
||||
{item.otsHash &&
|
||||
<Link href={`/items/${item.id}/ots`} className='text-reset dropdown-item'>
|
||||
opentimestamp
|
||||
</Link>}
|
||||
{item?.noteId && (
|
||||
<Dropdown.Item onClick={() => window.open(`https://njump.me/${item.noteId}`, '_blank', 'noopener,noreferrer,nofollow')}>
|
||||
nostr note
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
{item && item.mine && !item.noteId && !item.isJob && !item.parentId &&
|
||||
<CrosspostDropdownItem item={item} />}
|
||||
{me && !item.position &&
|
||||
!item.mine && !item.deletedAt &&
|
||||
(item.meDontLikeSats > meTotalSats
|
||||
? <DropdownItemUpVote item={item} />
|
||||
: <DontLikeThisDropdownItem id={item.id} />)}
|
||||
{me && sub && !item.mine && !item.outlawed && Number(me.id) === Number(sub.userId) && sub.moderated &&
|
||||
<>
|
||||
<hr className='dropdown-divider' />
|
||||
<OutlawDropdownItem item={item} />
|
||||
</>}
|
||||
{me && !nested && !item.mine && sub && Number(me.id) !== Number(sub.userId) &&
|
||||
<>
|
||||
<hr className='dropdown-divider' />
|
||||
<MuteSubDropdownItem item={item} sub={sub} />
|
||||
</>}
|
||||
{canPin &&
|
||||
<>
|
||||
<hr className='dropdown-divider' />
|
||||
<PinSubDropdownItem item={item} />
|
||||
</>}
|
||||
{item.mine && !item.position && !item.deletedAt && !item.bio &&
|
||||
<>
|
||||
<hr className='dropdown-divider' />
|
||||
<DeleteDropdownItem itemId={item.id} type={item.title ? 'post' : 'comment'} />
|
||||
</>}
|
||||
{me && !item.mine &&
|
||||
<>
|
||||
<hr className='dropdown-divider' />
|
||||
<MuteDropdownItem user={item.user} />
|
||||
</>}
|
||||
</ActionDropdown>
|
||||
{
|
||||
showActionDropdown &&
|
||||
<ActionDropdown>
|
||||
<CopyLinkDropdownItem item={item} />
|
||||
{(item.parentId || item.text) && onQuoteReply &&
|
||||
<Dropdown.Item onClick={onQuoteReply}>quote reply</Dropdown.Item>}
|
||||
{me && <BookmarkDropdownItem item={item} />}
|
||||
{me && <SubscribeDropdownItem item={item} />}
|
||||
{item.otsHash &&
|
||||
<Link href={`/items/${item.id}/ots`} className='text-reset dropdown-item'>
|
||||
opentimestamp
|
||||
</Link>}
|
||||
{item?.noteId && (
|
||||
<Dropdown.Item onClick={() => window.open(`https://njump.me/${item.noteId}`, '_blank', 'noopener,noreferrer,nofollow')}>
|
||||
nostr note
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
{item && item.mine && !item.noteId && !item.isJob && !item.parentId &&
|
||||
<CrosspostDropdownItem item={item} />}
|
||||
{me && !item.position &&
|
||||
!item.mine && !item.deletedAt &&
|
||||
(item.meDontLikeSats > meTotalSats
|
||||
? <DropdownItemUpVote item={item} />
|
||||
: <DontLikeThisDropdownItem id={item.id} />)}
|
||||
{me && sub && !item.mine && !item.outlawed && Number(me.id) === Number(sub.userId) && sub.moderated &&
|
||||
<>
|
||||
<hr className='dropdown-divider' />
|
||||
<OutlawDropdownItem item={item} />
|
||||
</>}
|
||||
{me && !nested && !item.mine && sub && Number(me.id) !== Number(sub.userId) &&
|
||||
<>
|
||||
<hr className='dropdown-divider' />
|
||||
<MuteSubDropdownItem item={item} sub={sub} />
|
||||
</>}
|
||||
{canPin &&
|
||||
<>
|
||||
<hr className='dropdown-divider' />
|
||||
<PinSubDropdownItem item={item} />
|
||||
</>}
|
||||
{item.mine && !item.position && !item.deletedAt && !item.bio &&
|
||||
<>
|
||||
<hr className='dropdown-divider' />
|
||||
<DeleteDropdownItem itemId={item.id} type={item.title ? 'post' : 'comment'} />
|
||||
</>}
|
||||
{me && !item.mine &&
|
||||
<>
|
||||
<hr className='dropdown-divider' />
|
||||
<MuteDropdownItem user={item.user} />
|
||||
</>}
|
||||
</ActionDropdown>
|
||||
}
|
||||
{extraInfo}
|
||||
</div>
|
||||
)
|
||||
|
28
components/item-popover.js
Normal file
28
components/item-popover.js
Normal file
@ -0,0 +1,28 @@
|
||||
import { ITEM } from '@/fragments/items'
|
||||
import errorStyles from '@/styles/error.module.css'
|
||||
import { useLazyQuery } from '@apollo/client'
|
||||
import classNames from 'classnames'
|
||||
import HoverablePopover from './hoverable-popover'
|
||||
import { ItemSkeleton, ItemSummary } from './item'
|
||||
|
||||
export default function ItemPopover ({ id, children }) {
|
||||
const [getItem, { loading, data }] = useLazyQuery(
|
||||
ITEM,
|
||||
{
|
||||
variables: { id },
|
||||
fetchPolicy: 'cache-first'
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<HoverablePopover
|
||||
onShow={getItem}
|
||||
trigger={children}
|
||||
body={!data || loading
|
||||
? <ItemSkeleton showUpvote={false} />
|
||||
: !data.item
|
||||
? <h1 className={classNames(errorStyles.status, errorStyles.describe)}>ITEM NOT FOUND</h1>
|
||||
: <ItemSummary item={data.item} />}
|
||||
/>
|
||||
)
|
||||
}
|
@ -18,6 +18,26 @@ import { Badge } from 'react-bootstrap'
|
||||
import AdIcon from '@/svgs/advertisement-fill.svg'
|
||||
import { DownZap } from './dont-link-this'
|
||||
import { timeLeft } from '@/lib/time'
|
||||
import classNames from 'classnames'
|
||||
import removeMd from 'remove-markdown'
|
||||
|
||||
function onItemClick (e, router, item) {
|
||||
const viewedAt = commentsViewedAt(item)
|
||||
if (viewedAt) {
|
||||
e.preventDefault()
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
window.open(
|
||||
`/items/${item.id}`,
|
||||
'_blank',
|
||||
'noopener,noreferrer'
|
||||
)
|
||||
} else {
|
||||
router.push(
|
||||
`/items/${item.id}?commentsViewedAt=${viewedAt}`,
|
||||
`/items/${item.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function SearchTitle ({ title }) {
|
||||
return reactStringReplace(title, /\*\*\*([^*]+)\*\*\*/g, (match, i) => {
|
||||
@ -51,23 +71,9 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
|
||||
<div className={`${styles.main} flex-wrap`}>
|
||||
<Link
|
||||
href={`/items/${item.id}`}
|
||||
onClick={(e) => {
|
||||
const viewedAt = commentsViewedAt(item)
|
||||
if (viewedAt) {
|
||||
e.preventDefault()
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
window.open(
|
||||
`/items/${item.id}`,
|
||||
'_blank',
|
||||
'noopener,noreferrer'
|
||||
)
|
||||
} else {
|
||||
router.push(
|
||||
`/items/${item.id}?commentsViewedAt=${viewedAt}`,
|
||||
`/items/${item.id}`)
|
||||
}
|
||||
}
|
||||
}} ref={titleRef} className={`${styles.title} text-reset me-2`}
|
||||
onClick={(e) => onItemClick(e, router, item)}
|
||||
ref={titleRef}
|
||||
className={`${styles.title} text-reset me-2`}
|
||||
>
|
||||
{item.searchTitle ? <SearchTitle title={item.searchTitle} /> : item.title}
|
||||
{item.pollCost && <PollIndicator item={item} />}
|
||||
@ -108,7 +114,48 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
|
||||
)
|
||||
}
|
||||
|
||||
export function ItemSkeleton ({ rank, children }) {
|
||||
export function ItemSummary ({ item }) {
|
||||
const router = useRouter()
|
||||
const link = (
|
||||
<Link
|
||||
href={`/items/${item.id}`}
|
||||
onClick={(e) => onItemClick(e, router, item)}
|
||||
className={`${item.title && styles.title} ${styles.summaryText} text-reset me-2`}
|
||||
>
|
||||
{item.title ?? removeMd(item.text)}
|
||||
</Link>
|
||||
)
|
||||
const info = (
|
||||
<ItemInfo
|
||||
item={item}
|
||||
showUser={false}
|
||||
showActionDropdown={false}
|
||||
extraBadges={item.title && Number(item?.user?.id) === AD_USER_ID && <Badge className={styles.newComment} bg={null}>AD</Badge>}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.item, 'mb-0 pb-0')}>
|
||||
<div className={styles.hunk}>
|
||||
{item.title
|
||||
? (
|
||||
<>
|
||||
{link}
|
||||
{info}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{info}
|
||||
{link}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ItemSkeleton ({ rank, children, showUpvote = true }) {
|
||||
return (
|
||||
<>
|
||||
{rank
|
||||
@ -118,7 +165,7 @@ export function ItemSkeleton ({ rank, children }) {
|
||||
</div>)
|
||||
: <div />}
|
||||
<div className={`${styles.item} ${styles.skeleton}`}>
|
||||
<UpVote className={styles.upvote} />
|
||||
{showUpvote && <UpVote className={styles.upvote} />}
|
||||
<div className={styles.hunk}>
|
||||
<div className={`${styles.main} flex-wrap flex-md-nowrap`}>
|
||||
<span className={`${styles.title} clouds text-reset flex-md-fill flex-md-shrink-0 me-2`} />
|
||||
@ -149,11 +196,10 @@ function PollIndicator ({ item }) {
|
||||
return (
|
||||
<span className={styles.icon} title={isActive ? 'active' : 'results in'}>
|
||||
<PollIcon
|
||||
className={`${
|
||||
isActive
|
||||
? 'fill-success'
|
||||
className={`${isActive
|
||||
? 'fill-success'
|
||||
: 'fill-grey'
|
||||
} ms-1`} height={14} width={14}
|
||||
} ms-1`} height={14} width={14}
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
|
@ -6,6 +6,14 @@
|
||||
margin-bottom: .15rem;
|
||||
}
|
||||
|
||||
.summaryText {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.notification {
|
||||
position: absolute;
|
||||
padding: 3px;
|
||||
|
@ -43,39 +43,38 @@ export function PriceProvider ({ price, children }) {
|
||||
)
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'asSats'
|
||||
const DEFAULT_SELECTION = 'fiat'
|
||||
|
||||
const carousel = [
|
||||
'fiat',
|
||||
'yep',
|
||||
'1btc',
|
||||
'blockHeight',
|
||||
'chainFee',
|
||||
'halving'
|
||||
]
|
||||
|
||||
export default function Price ({ className }) {
|
||||
const [asSats, setAsSats] = useState(undefined)
|
||||
const [pos, setPos] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const satSelection = window.localStorage.getItem('asSats')
|
||||
setAsSats(satSelection ?? 'fiat')
|
||||
const selection = window.localStorage.getItem(STORAGE_KEY) ?? DEFAULT_SELECTION
|
||||
setAsSats(selection)
|
||||
setPos(carousel.findIndex((item) => item === selection))
|
||||
}, [])
|
||||
|
||||
const { price, fiatSymbol } = usePrice()
|
||||
const { height: blockHeight, halving } = useBlockHeight()
|
||||
const { fee: chainFee } = useChainFee()
|
||||
|
||||
// Options: yep, 1btc, blockHeight, undefined
|
||||
// yep -> 1btc -> blockHeight -> chainFee -> undefined -> yep
|
||||
const handleClick = () => {
|
||||
if (asSats === 'yep') {
|
||||
window.localStorage.setItem('asSats', '1btc')
|
||||
setAsSats('1btc')
|
||||
} else if (asSats === '1btc') {
|
||||
window.localStorage.setItem('asSats', 'blockHeight')
|
||||
setAsSats('blockHeight')
|
||||
} else if (asSats === 'blockHeight') {
|
||||
window.localStorage.setItem('asSats', 'chainFee')
|
||||
setAsSats('chainFee')
|
||||
} else if (asSats === 'chainFee') {
|
||||
window.localStorage.setItem('asSats', 'halving')
|
||||
setAsSats('halving')
|
||||
} else if (asSats === 'halving') {
|
||||
window.localStorage.removeItem('asSats')
|
||||
setAsSats('fiat')
|
||||
} else {
|
||||
window.localStorage.setItem('asSats', 'yep')
|
||||
setAsSats('yep')
|
||||
}
|
||||
const nextPos = (pos + 1) % carousel.length
|
||||
|
||||
window.localStorage.setItem(STORAGE_KEY, carousel[nextPos])
|
||||
setAsSats(carousel[nextPos])
|
||||
setPos(nextPos)
|
||||
}
|
||||
|
||||
const compClassName = (className || '') + ' text-reset pointer'
|
||||
|
@ -22,6 +22,7 @@ import Link from 'next/link'
|
||||
import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
||||
import isEqual from 'lodash/isEqual'
|
||||
import UserPopover from './user-popover'
|
||||
import ItemPopover from './item-popover'
|
||||
|
||||
export function SearchText ({ text }) {
|
||||
return (
|
||||
@ -227,7 +228,11 @@ export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, o
|
||||
try {
|
||||
const linkText = parseInternalLinks(href)
|
||||
if (linkText) {
|
||||
return <Link href={href}>{linkText}</Link>
|
||||
return (
|
||||
<ItemPopover id={linkText.replace('#', '').split('/')[0]}>
|
||||
<Link href={href}>{linkText}</Link>
|
||||
</ItemPopover>
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// ignore errors like invalid URLs
|
||||
|
@ -1,28 +1,28 @@
|
||||
import { USER } from '@/fragments/users'
|
||||
import errorStyles from '@/styles/error.module.css'
|
||||
import { useLazyQuery } from '@apollo/client'
|
||||
import Link from 'next/link'
|
||||
import { useRef, useState } from 'react'
|
||||
import { Popover } from 'react-bootstrap'
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger'
|
||||
import { UserBase, UserSkeleton } from './user-list'
|
||||
import styles from './user-popover.module.css'
|
||||
import classNames from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import HoverablePopover from './hoverable-popover'
|
||||
import ItemPopover from './item-popover'
|
||||
import { UserBase, UserSkeleton } from './user-list'
|
||||
|
||||
function StackingSince ({ since }) {
|
||||
return (
|
||||
<small className='text-muted d-flex-inline'>
|
||||
stacking since:{' '}
|
||||
{since
|
||||
? <Link href={`/items/${since}`}>#{since}</Link>
|
||||
? (
|
||||
<ItemPopover id={since}>
|
||||
<Link href={`/items/${since}`}>#{since}</Link>
|
||||
</ItemPopover>
|
||||
)
|
||||
: <span>never</span>}
|
||||
</small>
|
||||
)
|
||||
}
|
||||
|
||||
export default function UserPopover ({ name, children }) {
|
||||
const [showOverlay, setShowOverlay] = useState(false)
|
||||
|
||||
const [getUser, { loading, data }] = useLazyQuery(
|
||||
USER,
|
||||
{
|
||||
@ -31,52 +31,19 @@ export default function UserPopover ({ name, children }) {
|
||||
}
|
||||
)
|
||||
|
||||
const timeoutId = useRef(null)
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
clearTimeout(timeoutId.current)
|
||||
getUser()
|
||||
timeoutId.current = setTimeout(() => {
|
||||
setShowOverlay(true)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
clearTimeout(timeoutId.current)
|
||||
timeoutId.current = setTimeout(() => setShowOverlay(false), 100)
|
||||
}
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
show={showOverlay}
|
||||
placement='bottom'
|
||||
onHide={handleMouseLeave}
|
||||
overlay={
|
||||
<Popover
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
className={styles.userPopover}
|
||||
>
|
||||
<Popover.Body className={styles.userPopBody}>
|
||||
{!data || loading
|
||||
? <UserSkeleton />
|
||||
: !data.user
|
||||
? <h1 className={classNames(errorStyles.status, errorStyles.describe)}>USER NOT FOUND</h1>
|
||||
: (
|
||||
<UserBase user={data.user} className='mb-0 pb-0'>
|
||||
<StackingSince since={data.user.since} />
|
||||
</UserBase>
|
||||
)}
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<span
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
<HoverablePopover
|
||||
onShow={getUser}
|
||||
trigger={children}
|
||||
body={!data || loading
|
||||
? <UserSkeleton />
|
||||
: !data.user
|
||||
? <h1 className={classNames(errorStyles.status, errorStyles.describe)}>USER NOT FOUND</h1>
|
||||
: (
|
||||
<UserBase user={data.user} className='mb-0 pb-0'>
|
||||
<StackingSince since={data.user.since} />
|
||||
</UserBase>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -471,6 +471,51 @@ services:
|
||||
- app
|
||||
labels:
|
||||
CONNECT: "localhost:8025"
|
||||
nwc:
|
||||
build:
|
||||
context: ./docker/nwc
|
||||
container_name: nwc
|
||||
profiles:
|
||||
- payments
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
stacker_lnd:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
volumes:
|
||||
- ./docker/lnd/stacker:/root/.lnd
|
||||
environment:
|
||||
- RUST_LOG=info
|
||||
entrypoint:
|
||||
- 'nostr-wallet-connect-lnd'
|
||||
- '--relay'
|
||||
- 'wss://relay.damus.io'
|
||||
- '--macaroon-file'
|
||||
- '/root/.lnd/regtest/admin.macaroon'
|
||||
- '--cert-file'
|
||||
- '/root/.lnd/tls.cert'
|
||||
- '--lnd-host'
|
||||
- 'stacker_lnd'
|
||||
- '--lnd-port'
|
||||
- '10009'
|
||||
lnbits:
|
||||
image: lnbits/lnbits:0.12.5
|
||||
container_name: lnbits
|
||||
profiles:
|
||||
- payments
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${LNBITS_WEB_PORT}:5000"
|
||||
depends_on:
|
||||
- stacker_lnd
|
||||
environment:
|
||||
- LNBITS_BACKEND_WALLET_CLASS=LndWallet
|
||||
- LND_GRPC_ENDPOINT=stacker_lnd
|
||||
- LND_GRPC_PORT=10009
|
||||
- LND_GRPC_CERT=/app/.lnd/tls.cert
|
||||
- LND_GRPC_MACAROON=/app/.lnd/regtest/admin.macaroon
|
||||
volumes:
|
||||
- ./docker/lnd/stacker:/app/.lnd
|
||||
volumes:
|
||||
db:
|
||||
os:
|
||||
|
12
docker/nwc/Dockerfile
Normal file
12
docker/nwc/Dockerfile
Normal file
@ -0,0 +1,12 @@
|
||||
FROM rust:1.78
|
||||
|
||||
RUN wget https://github.com/benthecarman/nostr-wallet-connect-lnd/archive/9d53490f0a0cf655030e4ef4d32b478d7f29af5b.zip \
|
||||
&& unzip 9d53490f0a0cf655030e4ef4d32b478d7f29af5b.zip
|
||||
|
||||
WORKDIR nostr-wallet-connect-lnd-9d53490f0a0cf655030e4ef4d32b478d7f29af5b
|
||||
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install -y cmake \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
RUN cargo build --release && cargo install --path .
|
Loading…
x
Reference in New Issue
Block a user