Compare commits
6 Commits
7090ea3b70
...
c83ff02a85
Author | SHA1 | Date | |
---|---|---|---|
|
c83ff02a85 | ||
|
85a4839538 | ||
|
471888563e | ||
|
691818e779 | ||
|
c6ab776091 | ||
|
47bfa24b57 |
@ -151,3 +151,6 @@ SKIP_SSL_CERT_DOWNLOAD=1
|
|||||||
|
|
||||||
# tor
|
# 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 cache = new Map()
|
||||||
const expiresIn = 1000 * 30 // 30 seconds in milliseconds
|
const expiresIn = 1000 * 30 // 30 seconds in milliseconds
|
||||||
|
|
||||||
async function fetchChainFeeRate () {
|
async function fetchChainFeeRate () {
|
||||||
let chainFee = 0
|
const url = 'https://mempool.space/api/v1/fees/recommended'
|
||||||
try {
|
const chainFee = await fetch(url)
|
||||||
const fee = await lndService.getChainFeeRate({ lnd })
|
.then((res) => res.json())
|
||||||
chainFee = fee.tokens_per_vbyte
|
.then((body) => body.hourFee)
|
||||||
} catch (err) {
|
.catch((err) => {
|
||||||
console.error('fetchChainFee', err)
|
console.error('fetchChainFee', err)
|
||||||
}
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
cache.set('fee', { fee: chainFee, createdAt: Date.now() })
|
cache.set('fee', { fee: chainFee, createdAt: Date.now() })
|
||||||
return chainFee
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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,???
|
abhiShandy,pr,#1123,#624,good-first-issue,,,,20k,abhishandy@stacker.news,???
|
||||||
benalleng,pr,#1129,#491,good-first-issue,,,,20k,benalleng@mutiny.plus,???
|
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)
|
border: 1px solid var(--theme-toolbarActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
.userPopBody {
|
.hoverablePopBody {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
@ -26,7 +26,7 @@ import UserPopover from './user-popover'
|
|||||||
export default function ItemInfo ({
|
export default function ItemInfo ({
|
||||||
item, full, commentsText = 'comments',
|
item, full, commentsText = 'comments',
|
||||||
commentTextSingular = 'comment', className, embellishUser, extraInfo, onEdit, editText,
|
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 editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
@ -102,12 +102,13 @@ export default function ItemInfo ({
|
|||||||
</Link>
|
</Link>
|
||||||
<span> \ </span>
|
<span> \ </span>
|
||||||
<span>
|
<span>
|
||||||
|
{showUser &&
|
||||||
<UserPopover name={item.user.name}>
|
<UserPopover name={item.user.name}>
|
||||||
<Link href={`/${item.user.name}`}>
|
<Link href={`/${item.user.name}`}>
|
||||||
@{item.user.name}<span> </span><Hat className='fill-grey' user={item.user} height={12} width={12} />
|
@{item.user.name}<span> </span><Hat className='fill-grey' user={item.user} height={12} width={12} />
|
||||||
{embellishUser}
|
{embellishUser}
|
||||||
</Link>
|
</Link>
|
||||||
</UserPopover>
|
</UserPopover>}
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<Link href={`/items/${item.id}`} title={item.createdAt} className='text-reset' suppressHydrationWarning>
|
<Link href={`/items/${item.id}`} title={item.createdAt} className='text-reset' suppressHydrationWarning>
|
||||||
{timeSince(new Date(item.createdAt))}
|
{timeSince(new Date(item.createdAt))}
|
||||||
@ -152,6 +153,8 @@ export default function ItemInfo ({
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</>}
|
</>}
|
||||||
|
{
|
||||||
|
showActionDropdown &&
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
<CopyLinkDropdownItem item={item} />
|
<CopyLinkDropdownItem item={item} />
|
||||||
{(item.parentId || item.text) && onQuoteReply &&
|
{(item.parentId || item.text) && onQuoteReply &&
|
||||||
@ -200,6 +203,7 @@ export default function ItemInfo ({
|
|||||||
<MuteDropdownItem user={item.user} />
|
<MuteDropdownItem user={item.user} />
|
||||||
</>}
|
</>}
|
||||||
</ActionDropdown>
|
</ActionDropdown>
|
||||||
|
}
|
||||||
{extraInfo}
|
{extraInfo}
|
||||||
</div>
|
</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 AdIcon from '@/svgs/advertisement-fill.svg'
|
||||||
import { DownZap } from './dont-link-this'
|
import { DownZap } from './dont-link-this'
|
||||||
import { timeLeft } from '@/lib/time'
|
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 }) {
|
export function SearchTitle ({ title }) {
|
||||||
return reactStringReplace(title, /\*\*\*([^*]+)\*\*\*/g, (match, i) => {
|
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`}>
|
<div className={`${styles.main} flex-wrap`}>
|
||||||
<Link
|
<Link
|
||||||
href={`/items/${item.id}`}
|
href={`/items/${item.id}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => onItemClick(e, router, item)}
|
||||||
const viewedAt = commentsViewedAt(item)
|
ref={titleRef}
|
||||||
if (viewedAt) {
|
className={`${styles.title} text-reset me-2`}
|
||||||
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`}
|
|
||||||
>
|
>
|
||||||
{item.searchTitle ? <SearchTitle title={item.searchTitle} /> : item.title}
|
{item.searchTitle ? <SearchTitle title={item.searchTitle} /> : item.title}
|
||||||
{item.pollCost && <PollIndicator item={item} />}
|
{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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{rank
|
{rank
|
||||||
@ -118,7 +165,7 @@ export function ItemSkeleton ({ rank, children }) {
|
|||||||
</div>)
|
</div>)
|
||||||
: <div />}
|
: <div />}
|
||||||
<div className={`${styles.item} ${styles.skeleton}`}>
|
<div className={`${styles.item} ${styles.skeleton}`}>
|
||||||
<UpVote className={styles.upvote} />
|
{showUpvote && <UpVote className={styles.upvote} />}
|
||||||
<div className={styles.hunk}>
|
<div className={styles.hunk}>
|
||||||
<div className={`${styles.main} flex-wrap flex-md-nowrap`}>
|
<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`} />
|
<span className={`${styles.title} clouds text-reset flex-md-fill flex-md-shrink-0 me-2`} />
|
||||||
@ -149,8 +196,7 @@ function PollIndicator ({ item }) {
|
|||||||
return (
|
return (
|
||||||
<span className={styles.icon} title={isActive ? 'active' : 'results in'}>
|
<span className={styles.icon} title={isActive ? 'active' : 'results in'}>
|
||||||
<PollIcon
|
<PollIcon
|
||||||
className={`${
|
className={`${isActive
|
||||||
isActive
|
|
||||||
? 'fill-success'
|
? 'fill-success'
|
||||||
: 'fill-grey'
|
: 'fill-grey'
|
||||||
} ms-1`} height={14} width={14}
|
} ms-1`} height={14} width={14}
|
||||||
|
@ -6,6 +6,14 @@
|
|||||||
margin-bottom: .15rem;
|
margin-bottom: .15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.summaryText {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
.notification {
|
.notification {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: 3px;
|
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 }) {
|
export default function Price ({ className }) {
|
||||||
const [asSats, setAsSats] = useState(undefined)
|
const [asSats, setAsSats] = useState(undefined)
|
||||||
|
const [pos, setPos] = useState(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const satSelection = window.localStorage.getItem('asSats')
|
const selection = window.localStorage.getItem(STORAGE_KEY) ?? DEFAULT_SELECTION
|
||||||
setAsSats(satSelection ?? 'fiat')
|
setAsSats(selection)
|
||||||
|
setPos(carousel.findIndex((item) => item === selection))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { price, fiatSymbol } = usePrice()
|
const { price, fiatSymbol } = usePrice()
|
||||||
const { height: blockHeight, halving } = useBlockHeight()
|
const { height: blockHeight, halving } = useBlockHeight()
|
||||||
const { fee: chainFee } = useChainFee()
|
const { fee: chainFee } = useChainFee()
|
||||||
|
|
||||||
// Options: yep, 1btc, blockHeight, undefined
|
|
||||||
// yep -> 1btc -> blockHeight -> chainFee -> undefined -> yep
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (asSats === 'yep') {
|
const nextPos = (pos + 1) % carousel.length
|
||||||
window.localStorage.setItem('asSats', '1btc')
|
|
||||||
setAsSats('1btc')
|
window.localStorage.setItem(STORAGE_KEY, carousel[nextPos])
|
||||||
} else if (asSats === '1btc') {
|
setAsSats(carousel[nextPos])
|
||||||
window.localStorage.setItem('asSats', 'blockHeight')
|
setPos(nextPos)
|
||||||
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 compClassName = (className || '') + ' text-reset pointer'
|
const compClassName = (className || '') + ' text-reset pointer'
|
||||||
|
@ -22,6 +22,7 @@ import Link from 'next/link'
|
|||||||
import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
||||||
import isEqual from 'lodash/isEqual'
|
import isEqual from 'lodash/isEqual'
|
||||||
import UserPopover from './user-popover'
|
import UserPopover from './user-popover'
|
||||||
|
import ItemPopover from './item-popover'
|
||||||
|
|
||||||
export function SearchText ({ text }) {
|
export function SearchText ({ text }) {
|
||||||
return (
|
return (
|
||||||
@ -227,7 +228,11 @@ export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, o
|
|||||||
try {
|
try {
|
||||||
const linkText = parseInternalLinks(href)
|
const linkText = parseInternalLinks(href)
|
||||||
if (linkText) {
|
if (linkText) {
|
||||||
return <Link href={href}>{linkText}</Link>
|
return (
|
||||||
|
<ItemPopover id={linkText.replace('#', '').split('/')[0]}>
|
||||||
|
<Link href={href}>{linkText}</Link>
|
||||||
|
</ItemPopover>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore errors like invalid URLs
|
// ignore errors like invalid URLs
|
||||||
|
@ -1,28 +1,28 @@
|
|||||||
import { USER } from '@/fragments/users'
|
import { USER } from '@/fragments/users'
|
||||||
import errorStyles from '@/styles/error.module.css'
|
import errorStyles from '@/styles/error.module.css'
|
||||||
import { useLazyQuery } from '@apollo/client'
|
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 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 }) {
|
function StackingSince ({ since }) {
|
||||||
return (
|
return (
|
||||||
<small className='text-muted d-flex-inline'>
|
<small className='text-muted d-flex-inline'>
|
||||||
stacking since:{' '}
|
stacking since:{' '}
|
||||||
{since
|
{since
|
||||||
? <Link href={`/items/${since}`}>#{since}</Link>
|
? (
|
||||||
|
<ItemPopover id={since}>
|
||||||
|
<Link href={`/items/${since}`}>#{since}</Link>
|
||||||
|
</ItemPopover>
|
||||||
|
)
|
||||||
: <span>never</span>}
|
: <span>never</span>}
|
||||||
</small>
|
</small>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UserPopover ({ name, children }) {
|
export default function UserPopover ({ name, children }) {
|
||||||
const [showOverlay, setShowOverlay] = useState(false)
|
|
||||||
|
|
||||||
const [getUser, { loading, data }] = useLazyQuery(
|
const [getUser, { loading, data }] = useLazyQuery(
|
||||||
USER,
|
USER,
|
||||||
{
|
{
|
||||||
@ -31,34 +31,11 @@ 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 (
|
return (
|
||||||
<OverlayTrigger
|
<HoverablePopover
|
||||||
show={showOverlay}
|
onShow={getUser}
|
||||||
placement='bottom'
|
trigger={children}
|
||||||
onHide={handleMouseLeave}
|
body={!data || loading
|
||||||
overlay={
|
|
||||||
<Popover
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
className={styles.userPopover}
|
|
||||||
>
|
|
||||||
<Popover.Body className={styles.userPopBody}>
|
|
||||||
{!data || loading
|
|
||||||
? <UserSkeleton />
|
? <UserSkeleton />
|
||||||
: !data.user
|
: !data.user
|
||||||
? <h1 className={classNames(errorStyles.status, errorStyles.describe)}>USER NOT FOUND</h1>
|
? <h1 className={classNames(errorStyles.status, errorStyles.describe)}>USER NOT FOUND</h1>
|
||||||
@ -67,16 +44,6 @@ export default function UserPopover ({ name, children }) {
|
|||||||
<StackingSince since={data.user.since} />
|
<StackingSince since={data.user.since} />
|
||||||
</UserBase>
|
</UserBase>
|
||||||
)}
|
)}
|
||||||
</Popover.Body>
|
/>
|
||||||
</Popover>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
</OverlayTrigger>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -471,6 +471,51 @@ services:
|
|||||||
- app
|
- app
|
||||||
labels:
|
labels:
|
||||||
CONNECT: "localhost:8025"
|
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:
|
volumes:
|
||||||
db:
|
db:
|
||||||
os:
|
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