Compare commits
No commits in common. "15b038cd789e6ea79ad00047f5c4415b416016a3" and "6b27f5450287036ed37520167564dbb198d948e6" have entirely different histories.
15b038cd78
...
6b27f54502
@ -4,7 +4,6 @@ import { useAccordionButton } from 'react-bootstrap/AccordionButton'
|
|||||||
import ArrowRight from '@/svgs/arrow-right-s-fill.svg'
|
import ArrowRight from '@/svgs/arrow-right-s-fill.svg'
|
||||||
import ArrowDown from '@/svgs/arrow-down-s-fill.svg'
|
import ArrowDown from '@/svgs/arrow-down-s-fill.svg'
|
||||||
import { useContext, useEffect, useState } from 'react'
|
import { useContext, useEffect, useState } from 'react'
|
||||||
import classNames from 'classnames'
|
|
||||||
|
|
||||||
const KEY_ID = '0'
|
const KEY_ID = '0'
|
||||||
|
|
||||||
@ -31,7 +30,7 @@ function ContextAwareToggle ({ children, headerColor = 'var(--theme-grey)', even
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AccordianItem ({ header, body, className, headerColor = 'var(--theme-grey)', show }) {
|
export default function AccordianItem ({ header, body, headerColor = 'var(--theme-grey)', show }) {
|
||||||
const [activeKey, setActiveKey] = useState()
|
const [activeKey, setActiveKey] = useState()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -45,7 +44,7 @@ export default function AccordianItem ({ header, body, className, headerColor =
|
|||||||
return (
|
return (
|
||||||
<Accordion defaultActiveKey={activeKey} activeKey={activeKey} onSelect={handleOnSelect}>
|
<Accordion defaultActiveKey={activeKey} activeKey={activeKey} onSelect={handleOnSelect}>
|
||||||
<ContextAwareToggle show={show} eventKey={KEY_ID} headerColor={headerColor}><div style={{ color: headerColor }}>{header}</div></ContextAwareToggle>
|
<ContextAwareToggle show={show} eventKey={KEY_ID} headerColor={headerColor}><div style={{ color: headerColor }}>{header}</div></ContextAwareToggle>
|
||||||
<Accordion.Collapse eventKey={KEY_ID} className={classNames('mt-2', className)}>
|
<Accordion.Collapse eventKey={KEY_ID} className='mt-2'>
|
||||||
<div>{body}</div>
|
<div>{body}</div>
|
||||||
</Accordion.Collapse>
|
</Accordion.Collapse>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
@ -109,8 +109,7 @@ export default function Invoice ({ id, query = INVOICE, modal, onPayment, onCanc
|
|||||||
<div className='w-100'>
|
<div className='w-100'>
|
||||||
<AccordianItem
|
<AccordianItem
|
||||||
header='sender information'
|
header='sender information'
|
||||||
body={<PayerData data={lud18Data} className='text-muted ms-3' />}
|
body={<PayerData data={lud18Data} className='text-muted ms-3 mb-3' />}
|
||||||
className='mb-3'
|
|
||||||
/>
|
/>
|
||||||
</div>}
|
</div>}
|
||||||
{comment &&
|
{comment &&
|
||||||
@ -118,7 +117,6 @@ export default function Invoice ({ id, query = INVOICE, modal, onPayment, onCanc
|
|||||||
<AccordianItem
|
<AccordianItem
|
||||||
header='sender comments'
|
header='sender comments'
|
||||||
body={<span className='text-muted ms-3'>{comment}</span>}
|
body={<span className='text-muted ms-3'>{comment}</span>}
|
||||||
className='mb-3'
|
|
||||||
/>
|
/>
|
||||||
</div>}
|
</div>}
|
||||||
<Bolt11Info bolt11={bolt11} preimage={confirmedPreimage} />
|
<Bolt11Info bolt11={bolt11} preimage={confirmedPreimage} />
|
||||||
|
@ -9,7 +9,10 @@ import styles from '@/styles/item.module.css'
|
|||||||
import itemStyles from './item.module.css'
|
import itemStyles from './item.module.css'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import Button from 'react-bootstrap/Button'
|
import Button from 'react-bootstrap/Button'
|
||||||
import { useEffect } from 'react'
|
import { TwitterTweetEmbed } from 'react-twitter-embed'
|
||||||
|
import YouTube from 'react-youtube'
|
||||||
|
import useDarkMode from './dark-mode'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
import Poll from './poll'
|
import Poll from './poll'
|
||||||
import { commentsViewed } from '@/lib/new-comments'
|
import { commentsViewed } from '@/lib/new-comments'
|
||||||
import Related from './related'
|
import Related from './related'
|
||||||
@ -19,7 +22,7 @@ import Share from './share'
|
|||||||
import Toc from './table-of-contents'
|
import Toc from './table-of-contents'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { RootProvider } from './root'
|
import { RootProvider } from './root'
|
||||||
import { decodeProxyUrl, IMGPROXY_URL_REGEXP } from '@/lib/url'
|
import { IMGPROXY_URL_REGEXP, parseEmbedUrl } from '@/lib/url'
|
||||||
import { numWithUnits } from '@/lib/format'
|
import { numWithUnits } from '@/lib/format'
|
||||||
import { useQuoteReply } from './use-quote-reply'
|
import { useQuoteReply } from './use-quote-reply'
|
||||||
import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
||||||
@ -47,11 +50,91 @@ function BioItem ({ item, handleClick }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ItemEmbed ({ url, imgproxyUrls }) {
|
function TweetSkeleton () {
|
||||||
const src = IMGPROXY_URL_REGEXP.test(url) ? decodeProxyUrl(url) : url
|
return (
|
||||||
const srcSet = imgproxyUrls?.[url]
|
<div className={styles.tweetsSkeleton}>
|
||||||
|
<div className={styles.tweetSkeleton}>
|
||||||
|
<div className={`${styles.img} clouds`} />
|
||||||
|
<div className={styles.content1}>
|
||||||
|
<div className={`${styles.line} clouds`} />
|
||||||
|
<div className={`${styles.line} clouds`} />
|
||||||
|
<div className={`${styles.line} clouds`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return <MediaOrLink src={src} srcSet={srcSet} topLevel linkFallback={false} />
|
function ItemEmbed ({ item }) {
|
||||||
|
const [darkMode] = useDarkMode()
|
||||||
|
const [overflowing, setOverflowing] = useState(false)
|
||||||
|
const [show, setShow] = useState(false)
|
||||||
|
|
||||||
|
// This Twitter embed could use similar logic to the video embeds below
|
||||||
|
const twitter = item.url?.match(/^https?:\/\/(?:twitter|x)\.com\/(?:#!\/)?\w+\/status(?:es)?\/(?<id>\d+)/)
|
||||||
|
if (twitter?.groups?.id) {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.twitterContainer} ${show ? '' : styles.twitterContained}`}>
|
||||||
|
<TwitterTweetEmbed tweetId={twitter.groups.id} options={{ theme: darkMode ? 'dark' : 'light', width: '550px' }} key={darkMode ? '1' : '2'} placeholder={<TweetSkeleton />} onLoad={() => setOverflowing(true)} />
|
||||||
|
{overflowing && !show &&
|
||||||
|
<Button size='lg' variant='info' className={styles.twitterShowFull} onClick={() => setShow(true)}>
|
||||||
|
show full tweet
|
||||||
|
</Button>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { provider, id, meta } = parseEmbedUrl(item.url)
|
||||||
|
|
||||||
|
if (provider === 'youtube') {
|
||||||
|
return (
|
||||||
|
<div className={styles.videoWrapper}>
|
||||||
|
<YouTube
|
||||||
|
videoId={id} className={styles.videoContainer} opts={{
|
||||||
|
playerVars: {
|
||||||
|
start: meta?.start || 0
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider === 'rumble') {
|
||||||
|
return (
|
||||||
|
<div className={styles.videoWrapper}>
|
||||||
|
<div className={styles.videoContainer}>
|
||||||
|
<iframe
|
||||||
|
title='Rumble Video'
|
||||||
|
allowFullScreen
|
||||||
|
src={meta?.href}
|
||||||
|
sandbox='allow-scripts'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider === 'peertube') {
|
||||||
|
return (
|
||||||
|
<div className={styles.videoWrapper}>
|
||||||
|
<div className={styles.videoContainer}>
|
||||||
|
<iframe
|
||||||
|
title='PeerTube Video'
|
||||||
|
allowFullScreen
|
||||||
|
src={meta?.href}
|
||||||
|
sandbox='allow-scripts'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.url?.match(IMGPROXY_URL_REGEXP)) {
|
||||||
|
return <MediaOrLink src={item.url} rel={item.rel ?? UNKNOWN_LINK_REL} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function FwdUsers ({ forwards }) {
|
function FwdUsers ({ forwards }) {
|
||||||
@ -91,7 +174,7 @@ function TopLevelItem ({ item, noReply, ...props }) {
|
|||||||
>
|
>
|
||||||
<article className={styles.fullItemContainer} ref={textRef}>
|
<article className={styles.fullItemContainer} ref={textRef}>
|
||||||
{item.text && <ItemText item={item} />}
|
{item.text && <ItemText item={item} />}
|
||||||
{item.url && !item.outlawed && <ItemEmbed url={item.url} imgproxyUrls={item.imgproxyUrls} />}
|
{item.url && <ItemEmbed item={item} />}
|
||||||
{item.poll && <Poll item={item} />}
|
{item.poll && <Poll item={item} />}
|
||||||
{item.bounty &&
|
{item.bounty &&
|
||||||
<div className='fw-bold mt-2'>
|
<div className='fw-bold mt-2'>
|
||||||
|
@ -9,7 +9,6 @@ import PollIcon from '@/svgs/bar-chart-horizontal-fill.svg'
|
|||||||
import BountyIcon from '@/svgs/bounty-bag.svg'
|
import BountyIcon from '@/svgs/bounty-bag.svg'
|
||||||
import ActionTooltip from './action-tooltip'
|
import ActionTooltip from './action-tooltip'
|
||||||
import ImageIcon from '@/svgs/image-fill.svg'
|
import ImageIcon from '@/svgs/image-fill.svg'
|
||||||
import VideoIcon from '@/svgs/video-on-fill.svg'
|
|
||||||
import { numWithUnits } from '@/lib/format'
|
import { numWithUnits } from '@/lib/format'
|
||||||
import ItemInfo from './item-info'
|
import ItemInfo from './item-info'
|
||||||
import Prism from '@/svgs/prism.svg'
|
import Prism from '@/svgs/prism.svg'
|
||||||
@ -21,9 +20,6 @@ import { DownZap } from './dont-link-this'
|
|||||||
import { timeLeft } from '@/lib/time'
|
import { timeLeft } from '@/lib/time'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import removeMd from 'remove-markdown'
|
import removeMd from 'remove-markdown'
|
||||||
import { decodeProxyUrl, IMGPROXY_URL_REGEXP, parseInternalLinks } from '@/lib/url'
|
|
||||||
import ItemPopover from './item-popover'
|
|
||||||
import { useMe } from './me'
|
|
||||||
|
|
||||||
function onItemClick (e, router, item) {
|
function onItemClick (e, router, item) {
|
||||||
const viewedAt = commentsViewedAt(item)
|
const viewedAt = commentsViewedAt(item)
|
||||||
@ -49,41 +45,6 @@ export function SearchTitle ({ title }) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function mediaType ({ url, imgproxyUrls }) {
|
|
||||||
const me = useMe()
|
|
||||||
const src = IMGPROXY_URL_REGEXP.test(url) ? decodeProxyUrl(url) : url
|
|
||||||
if (!imgproxyUrls?.[src] ||
|
|
||||||
me?.privates?.showImagesAndVideos === false ||
|
|
||||||
// we don't proxy videos even if we have thumbnails
|
|
||||||
(me?.privates?.imgproxyOnly && imgproxyUrls?.[src]?.video)) return
|
|
||||||
return imgproxyUrls?.[src]?.video ? 'video' : 'image'
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemLink ({ url, rel }) {
|
|
||||||
try {
|
|
||||||
const { linkText } = parseInternalLinks(url)
|
|
||||||
if (linkText) {
|
|
||||||
return (
|
|
||||||
<ItemPopover id={linkText.replace('#', '').split('/')[0]}>
|
|
||||||
<Link href={url} className={styles.link}>{linkText}</Link>
|
|
||||||
</ItemPopover>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
// eslint-disable-next-line
|
|
||||||
<a
|
|
||||||
className={styles.link} target='_blank' href={url}
|
|
||||||
rel={rel ?? UNKNOWN_LINK_REL}
|
|
||||||
>
|
|
||||||
{url.replace(/(^https?:|^)\/\//, '')}
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Item ({
|
export default function Item ({
|
||||||
item, rank, belowTitle, right, full, children, itemClassName,
|
item, rank, belowTitle, right, full, children, itemClassName,
|
||||||
onQuoteReply, pinnable
|
onQuoteReply, pinnable
|
||||||
@ -91,8 +52,7 @@ export default function Item ({
|
|||||||
const titleRef = useRef()
|
const titleRef = useRef()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const media = mediaType({ url: item.url, imgproxyUrls: item.imgproxyUrls })
|
const image = item.url && item.url.startsWith(process.env.NEXT_PUBLIC_IMGPROXY_URL)
|
||||||
const MediaIcon = media === 'video' ? VideoIcon : ImageIcon
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -127,9 +87,16 @@ export default function Item ({
|
|||||||
</ActionTooltip>
|
</ActionTooltip>
|
||||||
</span>}
|
</span>}
|
||||||
{item.forwards?.length > 0 && <span className={styles.icon}><Prism className='fill-grey ms-1' height={14} width={14} /></span>}
|
{item.forwards?.length > 0 && <span className={styles.icon}><Prism className='fill-grey ms-1' height={14} width={14} /></span>}
|
||||||
{media && <span className={styles.icon}><MediaIcon className='fill-grey ms-2' height={16} width={16} /></span>}
|
{image && <span className={styles.icon}><ImageIcon className='fill-grey ms-2' height={16} width={16} /></span>}
|
||||||
</Link>
|
</Link>
|
||||||
{item.url && !media && <ItemLink url={item.url} rel={UNKNOWN_LINK_REL} />}
|
{item.url && !image &&
|
||||||
|
// eslint-disable-next-line
|
||||||
|
<a
|
||||||
|
className={styles.link} target='_blank' href={item.url}
|
||||||
|
rel={item.rel ?? UNKNOWN_LINK_REL}
|
||||||
|
>
|
||||||
|
{item.url.replace(/(^https?:|^)\/\//, '')}
|
||||||
|
</a>}
|
||||||
</div>
|
</div>
|
||||||
<ItemInfo
|
<ItemInfo
|
||||||
full={full} item={item}
|
full={full} item={item}
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
import styles from './text.module.css'
|
import styles from './text.module.css'
|
||||||
import { useState, useEffect, useMemo, useCallback, memo, useRef } from 'react'
|
import { useState, useEffect, useMemo, useCallback, memo } from 'react'
|
||||||
import { decodeProxyUrl, IMGPROXY_URL_REGEXP, MEDIA_DOMAIN_REGEXP, parseEmbedUrl } from '@/lib/url'
|
import { decodeProxyUrl, IMGPROXY_URL_REGEXP, MEDIA_DOMAIN_REGEXP } from '@/lib/url'
|
||||||
import { useShowModal } from './modal'
|
import { useShowModal } from './modal'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import { Button, Dropdown } from 'react-bootstrap'
|
import { Dropdown } from 'react-bootstrap'
|
||||||
import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
import { UNKNOWN_LINK_REL } from '@/lib/constants'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { TwitterTweetEmbed } from 'react-twitter-embed'
|
|
||||||
import YouTube from 'react-youtube'
|
|
||||||
import useDarkMode from './dark-mode'
|
|
||||||
|
|
||||||
function LinkRaw ({ href, children, src, rel }) {
|
function LinkRaw ({ href, children, src, rel }) {
|
||||||
const isRawURL = /^https?:\/\//.test(children?.[0])
|
const isRawURL = /^https?:\/\//.test(children?.[0])
|
||||||
@ -49,7 +46,7 @@ const Media = memo(function Media ({ src, bestResSrc, srcSet, sizes, width, heig
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default function MediaOrLink ({ linkFallback = true, ...props }) {
|
export default function MediaOrLink (props) {
|
||||||
const media = useMediaHelper(props)
|
const media = useMediaHelper(props)
|
||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
@ -81,45 +78,28 @@ export default function MediaOrLink ({ linkFallback = true, ...props }) {
|
|||||||
|
|
||||||
if (!media.src) return null
|
if (!media.src) return null
|
||||||
|
|
||||||
if (!error) {
|
if (!error && (media.image || media.video)) {
|
||||||
if (media.image || media.video) {
|
return (
|
||||||
return (
|
<Media
|
||||||
<Media
|
{...media} onClick={handleClick} onError={handleError}
|
||||||
{...media} onClick={handleClick} onError={handleError}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (media.embed) {
|
|
||||||
return (
|
|
||||||
<Embed
|
|
||||||
{...media.embed} src={media.src}
|
|
||||||
className={media.className} onError={handleError} topLevel={props.topLevel}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (linkFallback) {
|
return <LinkRaw {...props} />
|
||||||
return <LinkRaw {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 }) => {
|
const useMediaHelper = ({ src, srcSet: { dimensions, video, ...srcSetObj } = {}, 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(() => !!srcSetObj || IMGPROXY_URL_REGEXP.test(src) || MEDIA_DOMAIN_REGEXP.test(src), [!!srcSetObj, src])
|
||||||
const { dimensions, video, format, ...srcSetObj } = srcSetIntital || {}
|
|
||||||
const [isImage, setIsImage] = useState(!video && trusted)
|
const [isImage, setIsImage] = useState(!video && trusted)
|
||||||
const [isVideo, setIsVideo] = useState(video)
|
const [isVideo, setIsVideo] = useState(video)
|
||||||
const showMedia = useMemo(() => tab === 'preview' || me?.privates?.showImagesAndVideos !== false, [tab, me?.privates?.showImagesAndVideos])
|
const showMedia = useMemo(() => tab === 'preview' || me?.privates?.showImagesAndVideos !== false, [tab, me?.privates?.showImagesAndVideos])
|
||||||
const embed = useMemo(() => parseEmbedUrl(src), [src])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// don't load the video at all if user doesn't want these
|
// don't load the video at all if use doesn't want these
|
||||||
if (!showMedia || isVideo || isImage || embed) return
|
if (!showMedia || isVideo || isImage) return
|
||||||
// make sure it's not a false negative by trying to load URL as <img>
|
// make sure it's not a false negative by trying to load URL as <img>
|
||||||
const img = new window.Image()
|
const img = new window.Image()
|
||||||
img.onload = () => setIsImage(true)
|
img.onload = () => setIsImage(true)
|
||||||
@ -134,7 +114,7 @@ export const useMediaHelper = ({ src, srcSet: srcSetIntital, topLevel, tab }) =>
|
|||||||
video.onloadeddata = null
|
video.onloadeddata = null
|
||||||
video.src = ''
|
video.src = ''
|
||||||
}
|
}
|
||||||
}, [src, setIsImage, setIsVideo, showMedia, isVideo, embed])
|
}, [src, setIsImage, setIsVideo, showMedia, isVideo])
|
||||||
|
|
||||||
const srcSet = useMemo(() => {
|
const srcSet = useMemo(() => {
|
||||||
if (Object.keys(srcSetObj).length === 0) return undefined
|
if (Object.keys(srcSetObj).length === 0) return undefined
|
||||||
@ -184,177 +164,7 @@ export const useMediaHelper = ({ src, srcSet: srcSetIntital, topLevel, tab }) =>
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
className: classNames(topLevel && styles.topLevel),
|
className: classNames(topLevel && styles.topLevel),
|
||||||
image: (!me?.privates?.imgproxyOnly || trusted) && showMedia && isImage && !isVideo && !embed,
|
image: (!me?.privates?.imgproxyOnly || trusted) && showMedia && isImage && !isVideo,
|
||||||
video: !me?.privates?.imgproxyOnly && showMedia && isVideo && !embed,
|
video: !me?.privates?.imgproxyOnly && showMedia && isVideo
|
||||||
embed: !me?.privates?.imgproxyOnly && showMedia && embed
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function TweetSkeleton ({ className }) {
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.tweetsSkeleton, className)}>
|
|
||||||
<div className={styles.tweetSkeleton}>
|
|
||||||
<div className={`${styles.img} clouds`} />
|
|
||||||
<div className={styles.content1}>
|
|
||||||
<div className={`${styles.line} clouds`} />
|
|
||||||
<div className={`${styles.line} clouds`} />
|
|
||||||
<div className={`${styles.line} clouds`} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useFrameHeight = (
|
|
||||||
iframeRef
|
|
||||||
) => {
|
|
||||||
const [height, setHeight] = useState(0)
|
|
||||||
const iframeCurrent = iframeRef.current
|
|
||||||
useEffect(() => {
|
|
||||||
const setHeightFromIframe = (e) => {
|
|
||||||
if (e.origin !== 'https://njump.me' || !e?.data?.height) return
|
|
||||||
setHeight(e.data.height)
|
|
||||||
}
|
|
||||||
window?.addEventListener('message', setHeightFromIframe)
|
|
||||||
return () => {
|
|
||||||
window?.removeEventListener('message', setHeightFromIframe)
|
|
||||||
}
|
|
||||||
}, [iframeCurrent])
|
|
||||||
return height
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NostrEmbed = memo(function NostrEmbed ({ src, className, topLevel, id }) {
|
|
||||||
const [show, setShow] = useState(false)
|
|
||||||
const iframeRef = useRef(null)
|
|
||||||
const frameHeight = useFrameHeight(iframeRef)
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.nostrContainer, !show && styles.twitterContained, className)}>
|
|
||||||
<iframe
|
|
||||||
ref={iframeRef}
|
|
||||||
src={`https://njump.me/${id}?embed=yes`}
|
|
||||||
width={topLevel ? '550px' : '350px'}
|
|
||||||
style={{ maxWidth: '100%' }}
|
|
||||||
height={frameHeight ? `${frameHeight}px` : topLevel ? '200px' : '150px'}
|
|
||||||
frameBorder='0'
|
|
||||||
sandbox='allow-scripts allow-same-origin'
|
|
||||||
allow=''
|
|
||||||
/>
|
|
||||||
{!show &&
|
|
||||||
<Button size='md' variant='info' className={styles.twitterShowFull} onClick={() => setShow(true)}>
|
|
||||||
<div>show full note</div>
|
|
||||||
<small className='fw-normal fst-italic'>or other stuff</small>
|
|
||||||
</Button>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const Embed = memo(function Embed ({ src, provider, id, meta, className, topLevel, onError }) {
|
|
||||||
const [darkMode] = useDarkMode()
|
|
||||||
const [overflowing, setOverflowing] = useState(true)
|
|
||||||
const [show, setShow] = useState(false)
|
|
||||||
|
|
||||||
// This Twitter embed could use similar logic to the video embeds below
|
|
||||||
if (provider === 'twitter') {
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.twitterContainer, !show && styles.twitterContained, className)}>
|
|
||||||
<TwitterTweetEmbed
|
|
||||||
tweetId={id}
|
|
||||||
options={{ theme: darkMode ? 'dark' : 'light', width: topLevel ? '550px' : '350px' }}
|
|
||||||
key={darkMode ? '1' : '2'}
|
|
||||||
placeholder={<TweetSkeleton className={className} />}
|
|
||||||
onLoad={() => setOverflowing(true)}
|
|
||||||
/>
|
|
||||||
{overflowing && !show &&
|
|
||||||
<Button size='lg' variant='info' className={styles.twitterShowFull} onClick={() => setShow(true)}>
|
|
||||||
show full tweet
|
|
||||||
</Button>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider === 'nostr') {
|
|
||||||
return (
|
|
||||||
<NostrEmbed src={src} className={className} topLevel={topLevel} id={id} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider === 'wavlake') {
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.wavlakeWrapper, className)}>
|
|
||||||
<iframe
|
|
||||||
src={`https://embed.wavlake.com/track/${id}`} width='100%' height='380' frameBorder='0'
|
|
||||||
allow='encrypted-media'
|
|
||||||
sandbox='allow-scripts'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider === 'spotify') {
|
|
||||||
// https://open.spotify.com/track/1KFxcj3MZrpBGiGA8ZWriv?si=f024c3aa52294aa1
|
|
||||||
// Remove any additional path segments
|
|
||||||
const url = new URL(src)
|
|
||||||
url.pathname = url.pathname.replace(/\/intl-\w+\//, '/')
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.spotifyWrapper, className)}>
|
|
||||||
<iframe
|
|
||||||
title='Spotify Web Player'
|
|
||||||
src={`https://open.spotify.com/embed${url.pathname}`}
|
|
||||||
width='100%'
|
|
||||||
height='152'
|
|
||||||
allowfullscreen=''
|
|
||||||
frameBorder='0'
|
|
||||||
allow='encrypted-media; clipboard-write;'
|
|
||||||
style={{ borderRadius: '12px' }}
|
|
||||||
sandbox='allow-scripts'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider === 'youtube') {
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.videoWrapper, className)}>
|
|
||||||
<YouTube
|
|
||||||
videoId={id} className={styles.videoContainer} opts={{
|
|
||||||
playerVars: {
|
|
||||||
start: meta?.start || 0
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider === 'rumble') {
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.videoWrapper, className)}>
|
|
||||||
<div className={styles.videoContainer}>
|
|
||||||
<iframe
|
|
||||||
title='Rumble Video'
|
|
||||||
allowFullScreen
|
|
||||||
src={meta?.href}
|
|
||||||
sandbox='allow-scripts'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provider === 'peertube') {
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.videoWrapper, className)}>
|
|
||||||
<div className={styles.videoContainer}>
|
|
||||||
<iframe
|
|
||||||
title='PeerTube Video'
|
|
||||||
allowFullScreen
|
|
||||||
src={meta?.href}
|
|
||||||
sandbox='allow-scripts'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import styles from './text.module.css'
|
import styles from './text.module.css'
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
import YouTube from 'react-youtube'
|
||||||
import gfm from 'remark-gfm'
|
import gfm from 'remark-gfm'
|
||||||
import { LightAsync as SyntaxHighlighter } from 'react-syntax-highlighter'
|
import { LightAsync as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||||
import atomDark from 'react-syntax-highlighter/dist/cjs/styles/prism/atom-dark'
|
import atomDark from 'react-syntax-highlighter/dist/cjs/styles/prism/atom-dark'
|
||||||
@ -12,7 +13,7 @@ import Thumb from '@/svgs/thumb-up-fill.svg'
|
|||||||
import { toString } from 'mdast-util-to-string'
|
import { toString } from 'mdast-util-to-string'
|
||||||
import copy from 'clipboard-copy'
|
import copy from 'clipboard-copy'
|
||||||
import MediaOrLink from './media-or-link'
|
import MediaOrLink from './media-or-link'
|
||||||
import { IMGPROXY_URL_REGEXP, parseInternalLinks, decodeProxyUrl } from '@/lib/url'
|
import { IMGPROXY_URL_REGEXP, parseInternalLinks, parseEmbedUrl, decodeProxyUrl } from '@/lib/url'
|
||||||
import reactStringReplace from 'react-string-replace'
|
import reactStringReplace from 'react-string-replace'
|
||||||
import { rehypeInlineCodeProperty, rehypeStyler } from '@/lib/md'
|
import { rehypeInlineCodeProperty, rehypeStyler } from '@/lib/md'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
@ -254,11 +255,64 @@ export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, o
|
|||||||
// ignore errors like invalid URLs
|
// ignore errors like invalid URLs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const videoWrapperStyles = {
|
||||||
|
maxWidth: topLevel ? '640px' : '320px',
|
||||||
|
margin: '0.5rem 0',
|
||||||
|
paddingRight: '15px'
|
||||||
|
}
|
||||||
|
|
||||||
|
const { provider, id, meta } = parseEmbedUrl(href)
|
||||||
|
// Youtube video embed
|
||||||
|
if (provider === 'youtube') {
|
||||||
|
return (
|
||||||
|
<div style={videoWrapperStyles}>
|
||||||
|
<YouTube
|
||||||
|
videoId={id} className={styles.videoContainer} opts={{
|
||||||
|
playerVars: {
|
||||||
|
start: meta?.start || 0
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rumble video embed
|
||||||
|
if (provider === 'rumble') {
|
||||||
|
return (
|
||||||
|
<div style={videoWrapperStyles}>
|
||||||
|
<div className={styles.videoContainer}>
|
||||||
|
<iframe
|
||||||
|
title='Rumble Video'
|
||||||
|
allowFullScreen
|
||||||
|
src={meta?.href}
|
||||||
|
sandbox='allow-scripts'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider === 'peertube') {
|
||||||
|
return (
|
||||||
|
<div style={videoWrapperStyles}>
|
||||||
|
<div className={styles.videoContainer}>
|
||||||
|
<iframe
|
||||||
|
title='PeerTube Video'
|
||||||
|
allowFullScreen
|
||||||
|
src={meta?.href}
|
||||||
|
sandbox='allow-scripts'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// assume the link is an image which will fallback to link if it's not
|
// assume the link is an image which will fallback to link if it's not
|
||||||
return <TextMediaOrLink src={href} rel={rel ?? UNKNOWN_LINK_REL} {...props}>{children}</TextMediaOrLink>
|
return <TextMediaOrLink src={href} rel={rel ?? UNKNOWN_LINK_REL} {...props}>{children}</TextMediaOrLink>
|
||||||
},
|
},
|
||||||
img: TextMediaOrLink
|
img: TextMediaOrLink
|
||||||
}), [outlawed, rel, itemId, Code, P, Heading, Table, TextMediaOrLink])
|
}), [outlawed, rel, topLevel, itemId, Code, P, Heading, Table, TextMediaOrLink])
|
||||||
|
|
||||||
const remarkPlugins = useMemo(() => [gfm, mention, sub], [])
|
const remarkPlugins = useMemo(() => [gfm, mention, sub], [])
|
||||||
const rehypePlugins = useMemo(() => [rehypeInlineCodeProperty, rehypeSuperscript, rehypeSubscript], [])
|
const rehypePlugins = useMemo(() => [rehypeInlineCodeProperty, rehypeSuperscript, rehypeSubscript], [])
|
||||||
|
@ -246,28 +246,6 @@ img.fullScreen {
|
|||||||
font-size: .85rem;
|
font-size: .85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Utility classes used in rehype plugins in md.js */
|
|
||||||
.subscript {
|
|
||||||
vertical-align: sub;
|
|
||||||
font-size: smaller;
|
|
||||||
}
|
|
||||||
|
|
||||||
.superscript {
|
|
||||||
vertical-align: super;
|
|
||||||
font-size: smaller;
|
|
||||||
}
|
|
||||||
|
|
||||||
.videoWrapper {
|
|
||||||
max-width: 320px;
|
|
||||||
padding-right: 15px;
|
|
||||||
margin: '0.5rem 0',
|
|
||||||
}
|
|
||||||
|
|
||||||
.videoWrapper.topLevel {
|
|
||||||
max-width: 640px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.videoContainer {
|
.videoContainer {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -284,108 +262,14 @@ img.fullScreen {
|
|||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.twitterContainer, .nostrContainer {
|
|
||||||
margin-top: .25rem;
|
/* Utility classes used in rehype plugins in md.js */
|
||||||
position: relative;
|
.subscript {
|
||||||
overflow: hidden;
|
vertical-align: sub;
|
||||||
|
font-size: smaller;
|
||||||
}
|
}
|
||||||
|
|
||||||
.twitterContainer:not(:first-child), .nostrContainer:not(:first-child) {
|
.superscript {
|
||||||
margin-top: .75rem;
|
vertical-align: super;
|
||||||
}
|
font-size: smaller;
|
||||||
|
|
||||||
.twitterContainer iframe, .nostrContainer iframe {
|
|
||||||
border-radius: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nostrContainer {
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.videoWrapper:not(:first-child) {
|
|
||||||
margin-top: .75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.twitterContained {
|
|
||||||
height: 150px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.twitterContained.topLevel {
|
|
||||||
height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.twitterShowFull {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
line-height: 1.2;
|
|
||||||
padding: .75rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tweetsSkeleton {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row wrap;
|
|
||||||
max-width: 350px;
|
|
||||||
width: 100%;
|
|
||||||
padding-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tweetsSkeleton.topLevel {
|
|
||||||
max-width: 550px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tweetSkeleton {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tweetSkeleton {
|
|
||||||
border: 0.05rem solid var(--theme-borderColor);
|
|
||||||
border-radius: 12px;
|
|
||||||
height: 150px;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topLevel .tweetSkeleton {
|
|
||||||
height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tweetSkeleton .img {
|
|
||||||
height: 48px;
|
|
||||||
width: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tweetSkeleton .content1,
|
|
||||||
.tweetSkeleton .content2 {
|
|
||||||
height: 50%;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tweetSkeleton .line {
|
|
||||||
height: 25%;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: .4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tweetSkeleton .line:last-child {
|
|
||||||
width: 75%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wavlakeWrapper {
|
|
||||||
width: 100%;
|
|
||||||
height: 380px;
|
|
||||||
max-width: 480px;
|
|
||||||
border-radius: 1.65rem;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spotifyWrapper {
|
|
||||||
width: 100%;
|
|
||||||
height: 152px;
|
|
||||||
max-width: 480px;
|
|
||||||
border-radius: 13px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
@ -10,7 +10,7 @@ export const defaultCommentSort = (pinned, bio, createdAt) => {
|
|||||||
return 'hot'
|
return 'hot'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isJob = item => item.maxBid !== null && typeof item.maxBid !== 'undefined'
|
export const isJob = item => typeof item.maxBid !== 'undefined'
|
||||||
|
|
||||||
// a delete directive preceded by a non word character that isn't a backtick
|
// a delete directive preceded by a non word character that isn't a backtick
|
||||||
const deletePattern = /\B@delete\s+in\s+(\d+)\s+(second|minute|hour|day|week|month|year)s?/gi
|
const deletePattern = /\B@delete\s+in\s+(\d+)\s+(second|minute|hour|day|week|month|year)s?/gi
|
||||||
|
53
lib/url.js
53
lib/url.js
@ -1,6 +1,3 @@
|
|||||||
import { nip19 } from 'nostr-tools'
|
|
||||||
import { DEFAULT_CROSSPOSTING_RELAYS } from './nostr'
|
|
||||||
|
|
||||||
export function ensureProtocol (value) {
|
export function ensureProtocol (value) {
|
||||||
if (!value) return value
|
if (!value) return value
|
||||||
value = value.trim()
|
value = value.trim()
|
||||||
@ -79,52 +76,9 @@ export function parseInternalLinks (href) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parseEmbedUrl (href) {
|
export function parseEmbedUrl (href) {
|
||||||
if (!href) return null
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const twitter = href.match(/^https?:\/\/(?:twitter|x)\.com\/(?:#!\/)?\w+\/status(?:es)?\/(?<id>\d+)/)
|
|
||||||
if (twitter?.groups?.id) {
|
|
||||||
return {
|
|
||||||
provider: 'twitter',
|
|
||||||
id: twitter.groups.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { hostname, pathname, searchParams } = new URL(href)
|
const { hostname, pathname, searchParams } = new URL(href)
|
||||||
|
|
||||||
// nostr prefixes: [npub1, nevent1, nprofile1, note1]
|
|
||||||
const nostr = href.match(/(?<id>(?<type>npub1|nevent1|nprofile1|note1|naddr1)[02-9ac-hj-np-z]+)/)
|
|
||||||
if (nostr?.groups?.id) {
|
|
||||||
let id = nostr.groups.id
|
|
||||||
if (nostr.groups.type === 'npub1') {
|
|
||||||
const { data } = nip19.decode(id)
|
|
||||||
id = nip19.nprofileEncode({ pubkey: data })
|
|
||||||
}
|
|
||||||
if (nostr.groups.type === 'note1') {
|
|
||||||
const { data } = nip19.decode(id)
|
|
||||||
// njump needs relays to embed
|
|
||||||
id = nip19.neventEncode({ id: data, relays: DEFAULT_CROSSPOSTING_RELAYS, author: '' })
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
provider: 'nostr',
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://wavlake.com/track/c0aaeff8-5a26-49cf-8dad-2b6909e4aed1
|
|
||||||
if (hostname.endsWith('wavlake.com') && pathname.startsWith('/track')) {
|
|
||||||
return {
|
|
||||||
provider: 'wavlake',
|
|
||||||
id: pathname.split('/')?.[2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hostname.endsWith('spotify.com') && pathname.startsWith('/track')) {
|
|
||||||
return {
|
|
||||||
provider: 'spotify'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hostname.endsWith('youtube.com')) {
|
if (hostname.endsWith('youtube.com')) {
|
||||||
if (pathname.includes('/watch')) {
|
if (pathname.includes('/watch')) {
|
||||||
return {
|
return {
|
||||||
@ -176,11 +130,12 @@ export function parseEmbedUrl (href) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {
|
||||||
console.error('Error parsing embed URL:', err)
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
// Important to return empty object as default
|
||||||
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stripTrailingSlash (uri) {
|
export function stripTrailingSlash (uri) {
|
||||||
|
@ -95,7 +95,7 @@ export function middleware (request) {
|
|||||||
// unsafe-inline for styles is not ideal but okay if script-src is using nonces
|
// unsafe-inline for styles is not ideal but okay if script-src is using nonces
|
||||||
"style-src 'self' a.stacker.news 'unsafe-inline'",
|
"style-src 'self' a.stacker.news 'unsafe-inline'",
|
||||||
"manifest-src 'self'",
|
"manifest-src 'self'",
|
||||||
'frame-src www.youtube.com platform.twitter.com njump.me open.spotify.com rumble.com embed.wavlake.com bitcointv.com peertube.tv',
|
'frame-src www.youtube.com platform.twitter.com rumble.com bitcointv.com peertube.tv',
|
||||||
"connect-src 'self' https: wss:" + devSrc,
|
"connect-src 'self' https: wss:" + devSrc,
|
||||||
// disable dangerous plugins like Flash
|
// disable dangerous plugins like Flash
|
||||||
"object-src 'none'",
|
"object-src 'none'",
|
||||||
|
@ -18,7 +18,6 @@ import PageLoading from '@/components/page-loading'
|
|||||||
import PayerData from '@/components/payer-data'
|
import PayerData from '@/components/payer-data'
|
||||||
import { Badge } from 'react-bootstrap'
|
import { Badge } from 'react-bootstrap'
|
||||||
import navStyles from '../settings/settings.module.css'
|
import navStyles from '../settings/settings.module.css'
|
||||||
import classNames from 'classnames'
|
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps({ query: WALLET_HISTORY, authRequired: true })
|
export const getServerSideProps = getGetServerSideProps({ query: WALLET_HISTORY, authRequired: true })
|
||||||
|
|
||||||
@ -37,7 +36,7 @@ function satusClass (status) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Satus ({ status, className }) {
|
function Satus ({ status }) {
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -86,8 +85,8 @@ function Satus ({ status, className }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={classNames('d-inline-block', className)}>
|
<span className='d-inline-block'>
|
||||||
<Icon /><small className={`text-${color} fw-bold ms-1`}>{desc}</small>
|
<Icon /><small className={`text-${color} fw-bold ms-2`}>{desc}</small>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -142,8 +141,7 @@ function Detail ({ fact }) {
|
|||||||
(fact.description && <span className='d-block'>{fact.description}</span>)}
|
(fact.description && <span className='d-block'>{fact.description}</span>)}
|
||||||
<PayerData data={fact.invoicePayerData} className='text-muted' header />
|
<PayerData data={fact.invoicePayerData} className='text-muted' header />
|
||||||
{fact.invoiceComment && <small className='text-muted'><b>sender says:</b> {fact.invoiceComment}</small>}
|
{fact.invoiceComment && <small className='text-muted'><b>sender says:</b> {fact.invoiceComment}</small>}
|
||||||
<Satus className={fact.invoiceComment ? 'ms-1' : ''} status={fact.status} />
|
<Satus status={fact.status} />{fact.autoWithdraw && <Badge className={styles.badge} bg={null}>{fact.type === 'p2p' ? 'p2p' : 'autowithdraw'}</Badge>}
|
||||||
{fact.autoWithdraw && <Badge className={styles.badge} bg={null}>{fact.type === 'p2p' ? 'p2p' : 'autowithdraw'}</Badge>}
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -352,7 +352,7 @@ export default function Settings ({ ssrData }) {
|
|||||||
<li>use this to protect receiver privacy</li>
|
<li>use this to protect receiver privacy</li>
|
||||||
<li>applies retroactively, cannot be reversed</li>
|
<li>applies retroactively, cannot be reversed</li>
|
||||||
<li>withdrawal invoices are kept at least {INVOICE_RETENTION_DAYS} days for security and debugging purposes</li>
|
<li>withdrawal invoices are kept at least {INVOICE_RETENTION_DAYS} days for security and debugging purposes</li>
|
||||||
<li>autodeletions are run on a daily basis at night</li>
|
<li>autodeletions are run a daily basis at night</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Info>
|
</Info>
|
||||||
</div>
|
</div>
|
||||||
@ -448,12 +448,12 @@ export default function Settings ({ ssrData }) {
|
|||||||
/>}
|
/>}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={
|
label={
|
||||||
<div className='d-flex align-items-center'>do not load images, videos, or content from external sites
|
<div className='d-flex align-items-center'>only load images from proxy
|
||||||
<Info>
|
<Info>
|
||||||
<ul>
|
<ul className='fw-bold'>
|
||||||
<li>only load images and videos when we can proxy them</li>
|
<li>only load images from our image proxy automatically</li>
|
||||||
<li>this prevents IP address leaks to arbitrary sites</li>
|
<li>this prevents IP address leaks to arbitrary sites</li>
|
||||||
<li>if we can't, the raw link will be shown instead</li>
|
<li>if we fail to load an image, the raw link will be shown</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Info>
|
</Info>
|
||||||
</div>
|
</div>
|
||||||
@ -511,22 +511,9 @@ export default function Settings ({ ssrData }) {
|
|||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={
|
label={
|
||||||
<div className='d-flex align-items-center'>show images, video, and 3rd party embeds
|
<div className='d-flex align-items-center'>show images and video
|
||||||
<Info>
|
<Info>
|
||||||
<ul>
|
<p>disable to show images and videos as links instead of embedding them</p>
|
||||||
<li>if checked and a link is an image, video or can be embedded in another way, we will do it</li>
|
|
||||||
<li>we support embeds from following sites:</li>
|
|
||||||
<ul>
|
|
||||||
<li>njump.me</li>
|
|
||||||
<li>youtube.com</li>
|
|
||||||
<li>twitter.com</li>
|
|
||||||
<li>spotify.com</li>
|
|
||||||
<li>rumble.com</li>
|
|
||||||
<li>wavlake.com</li>
|
|
||||||
<li>bitcointv.com</li>
|
|
||||||
<li>peertube.tv</li>
|
|
||||||
</ul>
|
|
||||||
</ul>
|
|
||||||
</Info>
|
</Info>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,98 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.videoWrapper {
|
||||||
|
max-width: 640px;
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoContainer {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
padding-bottom: 56.25%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoContainer iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twitterContainer {
|
||||||
|
margin-top: .25rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twitterContainer:not(:first-child) {
|
||||||
|
margin-top: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoWrapper:not(:first-child) {
|
||||||
|
margin-top: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twitterContainer iframe {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twitterContained {
|
||||||
|
height: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twitterShowFull {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweetsSkeleton {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
max-width: 550px;
|
||||||
|
width: 100%;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweetSkeleton {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweetSkeleton {
|
||||||
|
border: 0.05rem solid var(--theme-borderColor);
|
||||||
|
border-radius: 12px;
|
||||||
|
height: 200px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweetSkeleton .img {
|
||||||
|
height: 48px;
|
||||||
|
width: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweetSkeleton .content1,
|
||||||
|
.tweetSkeleton .content2 {
|
||||||
|
height: 50%;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweetSkeleton .line {
|
||||||
|
height: 25%;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: .4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweetSkeleton .line:last-child {
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
.fullItemContainer {
|
.fullItemContainer {
|
||||||
margin-bottom: .5rem;
|
margin-bottom: .5rem;
|
||||||
}
|
}
|
||||||
|
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M17 9.2L22.2133 5.55071C22.4395 5.39235 22.7513 5.44737 22.9096 5.6736C22.9684 5.75764 23 5.85774 23 5.96033V18.0397C23 18.3158 22.7761 18.5397 22.5 18.5397C22.3974 18.5397 22.2973 18.5081 22.2133 18.4493L17 14.8V19C17 19.5523 16.5523 20 16 20H2C1.44772 20 1 19.5523 1 19V5C1 4.44772 1.44772 4 2 4H16C16.5523 4 17 4.44772 17 5V9.2Z"></path></svg>
|
|
Before Width: | Height: | Size: 435 B |
Loading…
x
Reference in New Issue
Block a user