diff --git a/components/hoverable-popover.js b/components/hoverable-popover.js new file mode 100644 index 00000000..0a0ca4ba --- /dev/null +++ b/components/hoverable-popover.js @@ -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 ( + + + {body} + + + } + > + + {trigger} + + + ) +} diff --git a/components/user-popover.module.css b/components/hoverable-popover.module.css similarity index 71% rename from components/user-popover.module.css rename to components/hoverable-popover.module.css index 20d8ca76..a1601726 100644 --- a/components/user-popover.module.css +++ b/components/hoverable-popover.module.css @@ -1,8 +1,8 @@ -.userPopover { +.hoverablePopover { border: 1px solid var(--theme-toolbarActive) } -.userPopBody { +.hoverablePopBody { font-weight: 500; font-size: 0.9rem; } diff --git a/components/item-info.js b/components/item-info.js index a0422e82..0ca302f4 100644 --- a/components/item-info.js +++ b/components/item-info.js @@ -26,7 +26,7 @@ import UserPopover from './user-popover' export default function ItemInfo ({ item, full, commentsText = 'comments', commentTextSingular = 'comment', className, embellishUser, extraInfo, onEdit, editText, - onQuoteReply, extraBadges, nested, pinnable + onQuoteReply, extraBadges, nested, pinnable, showActionDropdown = true, showUser = true }) { const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000 const me = useMe() @@ -61,10 +61,10 @@ export default function ItemInfo ({ {!(item.position && (pinnable || !item.subName)) && !(!item.parentId && Number(item.user?.id) === AD_USER_ID) && <> \ - - - @{item.user.name} - {embellishUser} - - + {showUser && + + + @{item.user.name} + {embellishUser} + + } {timeSince(new Date(item.createdAt))} @@ -152,54 +153,57 @@ export default function ItemInfo ({ /> } - - - {(item.parentId || item.text) && onQuoteReply && - quote reply} - {me && } - {me && } - {item.otsHash && - - opentimestamp - } - {item?.noteId && ( - window.open(`https://njump.me/${item.noteId}`, '_blank', 'noopener,noreferrer,nofollow')}> - nostr note - - )} - {item && item.mine && !item.noteId && !item.isJob && !item.parentId && - } - {me && !item.position && - !item.mine && !item.deletedAt && - (item.meDontLikeSats > meTotalSats - ? - : )} - {me && sub && !item.mine && !item.outlawed && Number(me.id) === Number(sub.userId) && sub.moderated && - <> -
- - } - {me && !nested && !item.mine && sub && Number(me.id) !== Number(sub.userId) && - <> -
- - } - {canPin && - <> -
- - } - {item.mine && !item.position && !item.deletedAt && !item.bio && - <> -
- - } - {me && !item.mine && - <> -
- - } -
+ { + showActionDropdown && + + + {(item.parentId || item.text) && onQuoteReply && + quote reply} + {me && } + {me && } + {item.otsHash && + + opentimestamp + } + {item?.noteId && ( + window.open(`https://njump.me/${item.noteId}`, '_blank', 'noopener,noreferrer,nofollow')}> + nostr note + + )} + {item && item.mine && !item.noteId && !item.isJob && !item.parentId && + } + {me && !item.position && + !item.mine && !item.deletedAt && + (item.meDontLikeSats > meTotalSats + ? + : )} + {me && sub && !item.mine && !item.outlawed && Number(me.id) === Number(sub.userId) && sub.moderated && + <> +
+ + } + {me && !nested && !item.mine && sub && Number(me.id) !== Number(sub.userId) && + <> +
+ + } + {canPin && + <> +
+ + } + {item.mine && !item.position && !item.deletedAt && !item.bio && + <> +
+ + } + {me && !item.mine && + <> +
+ + } +
+ } {extraInfo} ) diff --git a/components/item-popover.js b/components/item-popover.js new file mode 100644 index 00000000..bf7de5da --- /dev/null +++ b/components/item-popover.js @@ -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 ( + + : !data.item + ?

ITEM NOT FOUND

+ : } + /> + ) +} diff --git a/components/item.js b/components/item.js index d859ff31..ea801fcf 100644 --- a/components/item.js +++ b/components/item.js @@ -18,6 +18,26 @@ import { Badge } from 'react-bootstrap' import AdIcon from '@/svgs/advertisement-fill.svg' import { DownZap } from './dont-link-this' import { timeLeft } from '@/lib/time' +import classNames from 'classnames' +import removeMd from 'remove-markdown' + +function onItemClick (e, router, item) { + const viewedAt = commentsViewedAt(item) + if (viewedAt) { + e.preventDefault() + if (e.ctrlKey || e.metaKey) { + window.open( + `/items/${item.id}`, + '_blank', + 'noopener,noreferrer' + ) + } else { + router.push( + `/items/${item.id}?commentsViewedAt=${viewedAt}`, + `/items/${item.id}`) + } + } +} export function SearchTitle ({ title }) { return reactStringReplace(title, /\*\*\*([^*]+)\*\*\*/g, (match, i) => { @@ -51,23 +71,9 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
{ - const viewedAt = commentsViewedAt(item) - if (viewedAt) { - e.preventDefault() - if (e.ctrlKey || e.metaKey) { - window.open( - `/items/${item.id}`, - '_blank', - 'noopener,noreferrer' - ) - } else { - router.push( - `/items/${item.id}?commentsViewedAt=${viewedAt}`, - `/items/${item.id}`) - } - } - }} ref={titleRef} className={`${styles.title} text-reset me-2`} + onClick={(e) => onItemClick(e, router, item)} + ref={titleRef} + className={`${styles.title} text-reset me-2`} > {item.searchTitle ? : item.title} {item.pollCost && } @@ -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 = ( + onItemClick(e, router, item)} + className={`${item.title && styles.title} ${styles.summaryText} text-reset me-2`} + > + {item.title ?? removeMd(item.text)} + + ) + const info = ( + AD} + /> + ) + + return ( +
+
+ {item.title + ? ( + <> + {link} + {info} + + ) + : ( + <> + {info} + {link} + + )} +
+
+ ) +} + +export function ItemSkeleton ({ rank, children, showUpvote = true }) { return ( <> {rank @@ -118,7 +165,7 @@ export function ItemSkeleton ({ rank, children }) {
) :
}
- + {showUpvote && }
@@ -149,11 +196,10 @@ function PollIndicator ({ item }) { return ( ) diff --git a/components/item.module.css b/components/item.module.css index 816d83c3..5d83b494 100644 --- a/components/item.module.css +++ b/components/item.module.css @@ -6,6 +6,14 @@ margin-bottom: .15rem; } +.summaryText { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + .notification { position: absolute; padding: 3px; diff --git a/components/text.js b/components/text.js index 0908baa1..1dc9d445 100644 --- a/components/text.js +++ b/components/text.js @@ -22,6 +22,7 @@ import Link from 'next/link' import { UNKNOWN_LINK_REL } from '@/lib/constants' import isEqual from 'lodash/isEqual' import UserPopover from './user-popover' +import ItemPopover from './item-popover' export function SearchText ({ text }) { return ( @@ -227,7 +228,11 @@ export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, o try { const linkText = parseInternalLinks(href) if (linkText) { - return {linkText} + return ( + + {linkText} + + ) } } catch { // ignore errors like invalid URLs diff --git a/components/user-popover.js b/components/user-popover.js index 445ec2c7..11d7d65b 100644 --- a/components/user-popover.js +++ b/components/user-popover.js @@ -1,28 +1,28 @@ import { USER } from '@/fragments/users' import errorStyles from '@/styles/error.module.css' import { useLazyQuery } from '@apollo/client' -import Link from 'next/link' -import { useRef, useState } from 'react' -import { Popover } from 'react-bootstrap' -import OverlayTrigger from 'react-bootstrap/OverlayTrigger' -import { UserBase, UserSkeleton } from './user-list' -import styles from './user-popover.module.css' import classNames from 'classnames' +import Link from 'next/link' +import HoverablePopover from './hoverable-popover' +import ItemPopover from './item-popover' +import { UserBase, UserSkeleton } from './user-list' function StackingSince ({ since }) { return ( stacking since:{' '} {since - ? #{since} + ? ( + + #{since} + + ) : never} ) } export default function UserPopover ({ name, children }) { - const [showOverlay, setShowOverlay] = useState(false) - const [getUser, { loading, data }] = useLazyQuery( USER, { @@ -31,52 +31,19 @@ export default function UserPopover ({ name, children }) { } ) - const timeoutId = useRef(null) - - const handleMouseEnter = () => { - clearTimeout(timeoutId.current) - getUser() - timeoutId.current = setTimeout(() => { - setShowOverlay(true) - }, 500) - } - - const handleMouseLeave = () => { - clearTimeout(timeoutId.current) - timeoutId.current = setTimeout(() => setShowOverlay(false), 100) - } - return ( - - - {!data || loading - ? - : !data.user - ?

USER NOT FOUND

- : ( - - - - )} -
- - } - > - - {children} - -
+ + : !data.user + ?

USER NOT FOUND

+ : ( + + + + )} + /> ) }