better link rel attr handling

This commit is contained in:
keyan 2024-03-04 19:20:14 -06:00
parent 0a0bfbbb37
commit b16234630b
13 changed files with 50 additions and 37 deletions

View File

@ -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
},

View File

@ -123,6 +123,7 @@ export default gql`
parentOtsHash: String
forwards: [ItemForward]
imgproxyUrls: JSONObject
rel: String
}
input ItemForwardInput {

View File

@ -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}

View File

@ -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>)

View File

@ -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 }) {

View File

@ -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>
)}

View File

@ -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>}
/>

View File

@ -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'}

View File

@ -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
}}

View File

@ -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>

View File

@ -33,6 +33,7 @@ export const COMMENT_FIELDS = gql`
otsHash
ncomments
imgproxyUrls
rel
}
`

View File

@ -58,6 +58,7 @@ export const ITEM_FIELDS = gql`
uploadId
mine
imgproxyUrls
rel
}`
export const ITEM_FULL_FIELDS = gql`

View File

@ -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