better link rel attr handling
This commit is contained in:
parent
0a0bfbbb37
commit
b16234630b
|
@ -9,7 +9,7 @@ import {
|
|||
ITEM_SPAM_INTERVAL, ITEM_FILTER_THRESHOLD,
|
||||
COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY,
|
||||
ANON_USER_ID, ANON_ITEM_SPAM_INTERVAL, POLL_COST,
|
||||
ITEM_ALLOW_EDITS, GLOBAL_SEED, ANON_FEE_MULTIPLIER
|
||||
ITEM_ALLOW_EDITS, GLOBAL_SEED, ANON_FEE_MULTIPLIER, NOFOLLOW_LIMIT, UNKNOWN_LINK_REL
|
||||
} from '../../lib/constants'
|
||||
import { msatsToSats } from '../../lib/format'
|
||||
import { parse } from 'tldts'
|
||||
|
@ -1146,6 +1146,11 @@ export default {
|
|||
}
|
||||
return item.outlawed || item.weightedVotes - item.weightedDownVotes <= -ITEM_FILTER_THRESHOLD
|
||||
},
|
||||
rel: async (item, args, { me, models }) => {
|
||||
const sats = item.msats ? msatsToSats(item.msats) : 0
|
||||
const boost = item.boost ?? 0
|
||||
return (sats + boost < NOFOLLOW_LIMIT) ? UNKNOWN_LINK_REL : 'noopener noreferrer'
|
||||
},
|
||||
mine: async (item, args, { me, models }) => {
|
||||
return me?.id === item.userId
|
||||
},
|
||||
|
|
|
@ -123,6 +123,7 @@ export default gql`
|
|||
parentOtsHash: String
|
||||
forwards: [ItemForward]
|
||||
imgproxyUrls: JSONObject
|
||||
rel: String
|
||||
}
|
||||
|
||||
input ItemForwardInput {
|
||||
|
|
|
@ -9,7 +9,7 @@ import Eye from '../svgs/eye-fill.svg'
|
|||
import EyeClose from '../svgs/eye-close-line.svg'
|
||||
import { useRouter } from 'next/router'
|
||||
import CommentEdit from './comment-edit'
|
||||
import { ANON_USER_ID, COMMENT_DEPTH_LIMIT, NOFOLLOW_LIMIT } from '../lib/constants'
|
||||
import { ANON_USER_ID, COMMENT_DEPTH_LIMIT, UNKNOWN_LINK_REL } from '../lib/constants'
|
||||
import { ignoreClick } from '../lib/clicks'
|
||||
import PayBounty from './pay-bounty'
|
||||
import BountyIcon from '../svgs/bounty-bag.svg'
|
||||
|
@ -212,7 +212,7 @@ export default function Comment ({
|
|||
{item.searchText
|
||||
? <SearchText text={item.searchText} />
|
||||
: (
|
||||
<Text itemId={item.id} topLevel={topLevel} nofollow={item.sats + item.boost < NOFOLLOW_LIMIT} imgproxyUrls={item.imgproxyUrls}>
|
||||
<Text itemId={item.id} topLevel={topLevel} rel={item.rel ?? UNKNOWN_LINK_REL} imgproxyUrls={item.imgproxyUrls}>
|
||||
{item.outlawed && !me?.privates?.wildWestMode
|
||||
? '*stackers have outlawed this. turn on wild west mode in your [settings](/settings) to see outlawed content.*'
|
||||
: truncate ? truncateString(item.text) : item.text}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { IMGPROXY_URL_REGEXP } from '../lib/url'
|
|||
import { useShowModal } from './modal'
|
||||
import { useMe } from './me'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import { UPLOAD_TYPES_ALLOW } from '../lib/constants'
|
||||
import { UNKNOWN_LINK_REL, UPLOAD_TYPES_ALLOW } from '../lib/constants'
|
||||
import { useToast } from './toast'
|
||||
import gql from 'graphql-tag'
|
||||
import { useMutation } from '@apollo/client'
|
||||
|
@ -19,7 +19,7 @@ export function decodeOriginalUrl (imgproxyUrl) {
|
|||
return originalUrl
|
||||
}
|
||||
|
||||
function ImageOriginal ({ src, topLevel, nofollow, tab, children, onClick, ...props }) {
|
||||
function ImageOriginal ({ src, topLevel, rel, tab, children, onClick, ...props }) {
|
||||
const me = useMe()
|
||||
const [showImage, setShowImage] = useState(false)
|
||||
|
||||
|
@ -52,9 +52,10 @@ function ImageOriginal ({ src, topLevel, nofollow, tab, children, onClick, ...pr
|
|||
// This will not be the case if [text](url) format is used. Then we will show what was chosen as text.
|
||||
const isRawURL = /^https?:\/\//.test(children?.[0])
|
||||
return (
|
||||
// eslint-disable-next-line
|
||||
<a
|
||||
target='_blank'
|
||||
rel={`noreferrer ${nofollow ? 'nofollow' : ''} noopener`}
|
||||
rel={rel ?? UNKNOWN_LINK_REL}
|
||||
href={src}
|
||||
>{isRawURL ? src : children}
|
||||
</a>
|
||||
|
@ -118,7 +119,7 @@ export default function ZoomableImage ({ src, srcSet, ...props }) {
|
|||
overflow: (
|
||||
<Dropdown.Item
|
||||
href={originalUrl} target='_blank'
|
||||
rel={`noreferrer ${props.nofollow ? 'nofollow' : ''} noopener`}
|
||||
rel={props.rel ?? UNKNOWN_LINK_REL}
|
||||
>
|
||||
open original
|
||||
</Dropdown.Item>)
|
||||
|
|
|
@ -7,7 +7,6 @@ import ZoomableImage from './image'
|
|||
import Comments from './comments'
|
||||
import styles from '../styles/item.module.css'
|
||||
import itemStyles from './item.module.css'
|
||||
import { NOFOLLOW_LIMIT } from '../lib/constants'
|
||||
import { useMe } from './me'
|
||||
import Button from 'react-bootstrap/Button'
|
||||
import { TwitterTweetEmbed } from 'react-twitter-embed'
|
||||
|
@ -26,6 +25,7 @@ import { RootProvider } from './root'
|
|||
import { IMGPROXY_URL_REGEXP } from '../lib/url'
|
||||
import { numWithUnits } from '../lib/format'
|
||||
import { useQuoteReply } from './use-quote-reply'
|
||||
import { UNKNOWN_LINK_REL } from '../lib/constants'
|
||||
|
||||
function BioItem ({ item, handleClick }) {
|
||||
const me = useMe()
|
||||
|
@ -99,7 +99,7 @@ function ItemEmbed ({ item }) {
|
|||
}
|
||||
|
||||
if (item.url?.match(IMGPROXY_URL_REGEXP)) {
|
||||
return <ZoomableImage src={item.url} />
|
||||
return <ZoomableImage src={item.url} rel={item.rel ?? UNKNOWN_LINK_REL} />
|
||||
}
|
||||
|
||||
return null
|
||||
|
@ -171,7 +171,7 @@ function TopLevelItem ({ item, noReply, ...props }) {
|
|||
function ItemText ({ item }) {
|
||||
return item.searchText
|
||||
? <SearchText text={item.searchText} />
|
||||
: <Text itemId={item.id} topLevel nofollow={item.sats + item.boost < NOFOLLOW_LIMIT} imgproxyUrls={item.imgproxyUrls}>{item.text}</Text>
|
||||
: <Text itemId={item.id} topLevel rel={item.rel ?? UNKNOWN_LINK_REL} imgproxyUrls={item.imgproxyUrls}>{item.text}</Text>
|
||||
}
|
||||
|
||||
export default function ItemFull ({ item, bio, rank, ...props }) {
|
||||
|
|
|
@ -25,7 +25,7 @@ import { MuteSubDropdownItem, PinSubDropdownItem } from './territory-header'
|
|||
export default function ItemInfo ({
|
||||
item, full, commentsText = 'comments',
|
||||
commentTextSingular = 'comment', className, embellishUser, extraInfo, onEdit, editText,
|
||||
onQuoteReply, nofollow, extraBadges, nested, pinnable
|
||||
onQuoteReply, extraBadges, nested, pinnable
|
||||
}) {
|
||||
const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
|
||||
const me = useMe()
|
||||
|
@ -79,7 +79,6 @@ export default function ItemInfo ({
|
|||
<span> \ </span>
|
||||
</>}
|
||||
<Link
|
||||
rel={nofollow}
|
||||
href={`/items/${item.id}`} onClick={(e) => {
|
||||
const viewedAt = commentsViewedAt(item)
|
||||
if (viewedAt) {
|
||||
|
@ -107,7 +106,7 @@ export default function ItemInfo ({
|
|||
{embellishUser}
|
||||
</Link>
|
||||
<span> </span>
|
||||
<Link rel={nofollow} 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))}
|
||||
</Link>
|
||||
{item.prior &&
|
||||
|
@ -161,7 +160,7 @@ export default function ItemInfo ({
|
|||
opentimestamp
|
||||
</Link>}
|
||||
{item?.noteId && (
|
||||
<Dropdown.Item onClick={() => window.open(`https://nostr.com/${item.noteId}`, '_blank', 'noopener')}>
|
||||
<Dropdown.Item onClick={() => window.open(`https://nostr.com/${item.noteId}`, '_blank', 'noopener,noreferrer,nofollow')}>
|
||||
nostr note
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
|
|
|
@ -2,7 +2,7 @@ import Link from 'next/link'
|
|||
import styles from './item.module.css'
|
||||
import UpVote from './upvote'
|
||||
import { useRef } from 'react'
|
||||
import { AD_USER_ID, NOFOLLOW_LIMIT } from '../lib/constants'
|
||||
import { AD_USER_ID, UNKNOWN_LINK_REL } from '../lib/constants'
|
||||
import Pin from '../svgs/pushpin-fill.svg'
|
||||
import reactStringReplace from 'react-string-replace'
|
||||
import PollIcon from '../svgs/bar-chart-horizontal-fill.svg'
|
||||
|
@ -29,7 +29,6 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
|
|||
const router = useRouter()
|
||||
|
||||
const image = item.url && item.url.startsWith(process.env.NEXT_PUBLIC_IMGPROXY_URL)
|
||||
const nofollow = item.sats + item.boost < NOFOLLOW_LIMIT && !item.position ? 'nofollow' : ''
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -50,7 +49,6 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
|
|||
<div className={styles.hunk}>
|
||||
<div className={`${styles.main} flex-wrap`}>
|
||||
<Link
|
||||
rel={nofollow}
|
||||
href={`/items/${item.id}`}
|
||||
onClick={(e) => {
|
||||
const viewedAt = commentsViewedAt(item)
|
||||
|
@ -82,19 +80,17 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
|
|||
{image && <span className={styles.icon}><ImageIcon className='fill-grey ms-2' height={16} width={16} /></span>}
|
||||
</Link>
|
||||
{item.url && !image &&
|
||||
<>
|
||||
<a
|
||||
className={styles.link} target='_blank' href={item.url}
|
||||
rel={`noreferrer ${nofollow} noopener`}
|
||||
>
|
||||
{item.url.replace(/(^https?:|^)\/\//, '')}
|
||||
</a>
|
||||
</>}
|
||||
// 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>
|
||||
<ItemInfo
|
||||
full={full} item={item}
|
||||
onQuoteReply={onQuoteReply}
|
||||
nofollow={nofollow}
|
||||
pinnable={pinnable}
|
||||
extraBadges={Number(item?.user?.id) === AD_USER_ID && <Badge className={styles.newComment} bg={null}>AD</Badge>}
|
||||
/>
|
||||
|
|
|
@ -11,7 +11,7 @@ import { dayMonthYear, timeSince } from '../lib/time'
|
|||
import Link from 'next/link'
|
||||
import Check from '../svgs/check-double-line.svg'
|
||||
import HandCoin from '../svgs/hand-coin-fill.svg'
|
||||
import { LOST_BLURBS, FOUND_BLURBS } from '../lib/constants'
|
||||
import { LOST_BLURBS, FOUND_BLURBS, UNKNOWN_LINK_REL } from '../lib/constants'
|
||||
import CowboyHatIcon from '../svgs/cowboy.svg'
|
||||
import BaldIcon from '../svgs/bald.svg'
|
||||
import { RootProvider } from './root'
|
||||
|
@ -230,12 +230,15 @@ function NostrZap ({ n }) {
|
|||
<>
|
||||
<div className='fw-bold text-nostr ms-2 py-1'>
|
||||
<NostrIcon width={24} height={24} className='fill-nostr me-1' />{numWithUnits(n.earnedSats)} zap from
|
||||
<Link className='mx-1 text-reset text-underline' target='_blank' href={`https://snort.social/p/${npub}`} rel='noreferrer'>
|
||||
{// eslint-disable-next-line
|
||||
<Link className='mx-1 text-reset text-underline' target='_blank' href={`https://snort.social/p/${npub}`} rel={UNKNOWN_LINK_REL}>
|
||||
{npub.slice(0, 10)}...
|
||||
</Link>
|
||||
}
|
||||
on {note
|
||||
? (
|
||||
<Link className='mx-1 text-reset text-underline' target='_blank' href={`https://snort.social/e/${note}`} rel='noreferrer'>
|
||||
// eslint-disable-next-line
|
||||
<Link className='mx-1 text-reset text-underline' target='_blank' href={`https://snort.social/e/${note}`} rel={UNKNOWN_LINK_REL}>
|
||||
{note.slice(0, 12)}...
|
||||
</Link>)
|
||||
: 'nostr'}
|
||||
|
|
|
@ -19,6 +19,7 @@ import { rehypeInlineCodeProperty } from '../lib/md'
|
|||
import { Button } from 'react-bootstrap'
|
||||
import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import { UNKNOWN_LINK_REL } from '../lib/constants'
|
||||
|
||||
export function SearchText ({ text }) {
|
||||
return (
|
||||
|
@ -33,7 +34,7 @@ export function SearchText ({ text }) {
|
|||
}
|
||||
|
||||
// this is one of the slowest components to render
|
||||
export default memo(function Text ({ nofollow, imgproxyUrls, children, tab, itemId, ...outerProps }) {
|
||||
export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, ...outerProps }) {
|
||||
const [overflowing, setOverflowing] = useState(false)
|
||||
const router = useRouter()
|
||||
const [show, setShow] = useState(false)
|
||||
|
@ -139,7 +140,7 @@ export default memo(function Text ({ nofollow, imgproxyUrls, children, tab, item
|
|||
const Img = useCallback(({ node, src, ...props }) => {
|
||||
const url = IMGPROXY_URL_REGEXP.test(src) ? decodeOriginalUrl(src) : src
|
||||
const srcSet = imgproxyUrls?.[url]
|
||||
return <ZoomableImage srcSet={srcSet} tab={tab} src={src} {...props} {...outerProps} />
|
||||
return <ZoomableImage srcSet={srcSet} tab={tab} src={src} rel={rel ?? UNKNOWN_LINK_REL} {...props} {...outerProps} />
|
||||
}, [imgproxyUrls, outerProps, tab])
|
||||
|
||||
return (
|
||||
|
@ -174,14 +175,14 @@ export default memo(function Text ({ nofollow, imgproxyUrls, children, tab, item
|
|||
<Link
|
||||
{...props}
|
||||
id={props.id && itemId ? `${props.id}-${itemId}` : props.id}
|
||||
rel={`noreferrer ${nofollow ? 'nofollow' : ''} noopener`}
|
||||
href={itemId ? `${href}-${itemId}` : href}
|
||||
>{text}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<a id={props.id} target='_blank' rel={`noreferrer ${nofollow ? 'nofollow' : ''} noopener`} href={href}>{text}</a>
|
||||
// eslint-disable-next-line
|
||||
<a id={props.id} target='_blank' rel={rel ?? UNKNOWN_LINK_REL} href={href}>{text}</a>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -211,7 +212,7 @@ export default memo(function Text ({ nofollow, imgproxyUrls, children, tab, item
|
|||
}
|
||||
|
||||
// assume the link is an image which will fallback to link if it's not
|
||||
return <Img src={href} nofollow={nofollow} {...props}>{children}</Img>
|
||||
return <Img src={href} rel={rel ?? UNKNOWN_LINK_REL} {...props}>{children}</Img>
|
||||
},
|
||||
img: Img
|
||||
}}
|
||||
|
|
|
@ -28,6 +28,7 @@ import { hexToBech32 } from '../lib/nostr'
|
|||
import NostrIcon from '../svgs/nostr.svg'
|
||||
import GithubIcon from '../svgs/github-fill.svg'
|
||||
import TwitterIcon from '../svgs/twitter-fill.svg'
|
||||
import { UNKNOWN_LINK_REL } from '../lib/constants'
|
||||
|
||||
export default function UserHeader ({ user }) {
|
||||
const router = useRouter()
|
||||
|
@ -206,21 +207,24 @@ function SocialLink ({ name, id }) {
|
|||
if (name === 'Nostr') {
|
||||
const npub = hexToBech32(id)
|
||||
return (
|
||||
<Link className={className} target='_blank' href={`https://nostr.com/${npub}`} rel='noreferrer'>
|
||||
// eslint-disable-next-line
|
||||
<Link className={className} target='_blank' href={`https://nostr.com/${npub}`} rel={UNKNOWN_LINK_REL}>
|
||||
<NostrIcon width={20} height={20} className='me-1' />
|
||||
{npub.slice(0, 10)}...{npub.slice(-10)}
|
||||
</Link>
|
||||
)
|
||||
} else if (name === 'Github') {
|
||||
return (
|
||||
<Link className={className} target='_blank' href={`https://github.com/${id}`} rel='noreferrer'>
|
||||
// eslint-disable-next-line
|
||||
<Link className={className} target='_blank' href={`https://github.com/${id}`} rel={UNKNOWN_LINK_REL}>
|
||||
<GithubIcon width={20} height={20} className='me-1' />
|
||||
{id}
|
||||
</Link>
|
||||
)
|
||||
} else if (name === 'Twitter') {
|
||||
return (
|
||||
<Link className={className} target='_blank' href={`https://twitter.com/${id}`} rel='noreferrer'>
|
||||
// eslint-disable-next-line
|
||||
<Link className={className} target='_blank' href={`https://twitter.com/${id}`} rel={UNKNOWN_LINK_REL}>
|
||||
<TwitterIcon width={20} height={20} className='me-1' />
|
||||
@{id}
|
||||
</Link>
|
||||
|
|
|
@ -33,6 +33,7 @@ export const COMMENT_FIELDS = gql`
|
|||
otsHash
|
||||
ncomments
|
||||
imgproxyUrls
|
||||
rel
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ export const ITEM_FIELDS = gql`
|
|||
uploadId
|
||||
mine
|
||||
imgproxyUrls
|
||||
rel
|
||||
}`
|
||||
|
||||
export const ITEM_FULL_FIELDS = gql`
|
||||
|
|
|
@ -4,6 +4,7 @@ export const DEFAULT_SUBS = ['bitcoin', 'nostr', 'tech', 'meta', 'jobs']
|
|||
export const DEFAULT_SUBS_NO_JOBS = DEFAULT_SUBS.filter(s => s !== 'jobs')
|
||||
|
||||
export const NOFOLLOW_LIMIT = 1000
|
||||
export const UNKNOWN_LINK_REL = 'noreferrer nofollow noopener'
|
||||
export const BOOST_MULT = 5000
|
||||
export const BOOST_MIN = BOOST_MULT * 5
|
||||
export const UPLOAD_SIZE_MAX = 25 * 1024 * 1024
|
||||
|
|
Loading…
Reference in New Issue