improve dupes (#1356)
This commit is contained in:
		
							parent
							
								
									d9024ff837
								
							
						
					
					
						commit
						adcf048f4e
					
				@ -570,40 +570,34 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    dupes: async (parent, { url }, { me, models }) => {
 | 
			
		||||
      const urlObj = new URL(ensureProtocol(url))
 | 
			
		||||
      const { hostname, pathname } = urlObj
 | 
			
		||||
      let { hostname, pathname } = urlObj
 | 
			
		||||
 | 
			
		||||
      let hostnameRegex = hostname + '(:[0-9]+)?'
 | 
			
		||||
      // remove subdomain from hostname
 | 
			
		||||
      const parseResult = parse(urlObj.hostname)
 | 
			
		||||
      if (parseResult?.subdomain?.length) {
 | 
			
		||||
        const { subdomain } = parseResult
 | 
			
		||||
        hostnameRegex = hostnameRegex.replace(subdomain, '(%)?')
 | 
			
		||||
      } else {
 | 
			
		||||
        hostnameRegex = `(%.)?${hostnameRegex}`
 | 
			
		||||
      if (parseResult?.subdomain?.length > 0) {
 | 
			
		||||
        hostname = hostname.replace(`${parseResult.subdomain}.`, '')
 | 
			
		||||
      }
 | 
			
		||||
      // hostname with optional protocol, subdomain, and port
 | 
			
		||||
      const hostnameRegex = `^(http(s)?:\\/\\/)?(\\w+\\.)?${(hostname + '(:[0-9]+)?').replace(/\./g, '\\.')}`
 | 
			
		||||
      // pathname with trailing slash and escaped special characters
 | 
			
		||||
      const pathnameRegex = stripTrailingSlash(pathname).replace(/(\+|\.|\/)/g, '\\$1') + '\\/?'
 | 
			
		||||
      // url with optional trailing slash
 | 
			
		||||
      let similar = hostnameRegex + pathnameRegex
 | 
			
		||||
 | 
			
		||||
      // escape postgres regex meta characters
 | 
			
		||||
      let pathnameRegex = pathname.replace(/\+/g, '\\+')
 | 
			
		||||
      pathnameRegex = pathnameRegex.replace(/%/g, '\\%')
 | 
			
		||||
      pathnameRegex = pathnameRegex.replace(/_/g, '\\_')
 | 
			
		||||
 | 
			
		||||
      const uriRegex = stripTrailingSlash(hostnameRegex + pathnameRegex)
 | 
			
		||||
 | 
			
		||||
      let similar = `(http(s)?://)?${uriRegex}/?`
 | 
			
		||||
      const whitelist = ['news.ycombinator.com/item', 'bitcointalk.org/index.php']
 | 
			
		||||
      const youtube = ['www.youtube.com', 'youtube.com', 'm.youtube.com', 'youtu.be']
 | 
			
		||||
 | 
			
		||||
      const hostAndPath = stripTrailingSlash(urlObj.hostname + urlObj.pathname)
 | 
			
		||||
      if (whitelist.includes(hostAndPath)) {
 | 
			
		||||
        // make query string match for whitelist domains
 | 
			
		||||
        similar += `\\${urlObj.search}`
 | 
			
		||||
      } else if (youtube.includes(urlObj.hostname)) {
 | 
			
		||||
        // extract id and create both links
 | 
			
		||||
        const matches = url.match(/(https?:\/\/)?((www\.)?(youtube(-nocookie)?|youtube.googleapis)\.com.*(v\/|v=|vi=|vi\/|e\/|embed\/|user\/.*\/u\/\d+\/)|youtu\.be\/)(?<id>[_0-9a-z-]+)/i)
 | 
			
		||||
        similar = `(http(s)?://)?((www.|m.)?youtube.com/(watch\\?v=|v/|live/)${matches?.groups?.id}|youtu.be/${matches?.groups?.id})((\\?|&|#)%)?`
 | 
			
		||||
        similar = `^(http(s)?:\\/\\/)?((www\\.|m\\.)?youtube.com\\/(watch\\?v\\=|v\\/|live\\/)${matches?.groups?.id}|youtu\\.be\\/${matches?.groups?.id})&?`
 | 
			
		||||
      } else if (urlObj.hostname === 'yewtu.be') {
 | 
			
		||||
        const matches = url.match(/(https?:\/\/)?yewtu\.be.*(v=|embed\/)(?<id>[_0-9a-z-]+)/i)
 | 
			
		||||
        similar = `(http(s)?://)?yewtu.be/(watch\\?v=|embed/)${matches?.groups?.id}((\\?|&|#)%)?`
 | 
			
		||||
      } else {
 | 
			
		||||
        similar += '((\\?|#)%)?'
 | 
			
		||||
        similar = `^(http(s)?:\\/\\/)?yewtu\\.be\\/(watch\\?v\\=|embed\\/)${matches?.groups?.id}&?`
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return await itemQueryWithMeta({
 | 
			
		||||
@ -612,7 +606,7 @@ export default {
 | 
			
		||||
        query: `
 | 
			
		||||
          ${SELECT}
 | 
			
		||||
          FROM "Item"
 | 
			
		||||
          WHERE LOWER(url) SIMILAR TO LOWER($1)
 | 
			
		||||
          WHERE url ~* $1
 | 
			
		||||
          ORDER BY created_at DESC
 | 
			
		||||
          LIMIT 3`
 | 
			
		||||
      }, similar)
 | 
			
		||||
 | 
			
		||||
@ -43,7 +43,7 @@ export default function AccordianItem ({ header, body, headerColor = 'var(--them
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Accordion defaultActiveKey={activeKey} activeKey={activeKey} onSelect={handleOnSelect}>
 | 
			
		||||
      <ContextAwareToggle show={show} eventKey={KEY_ID}><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='mt-2'>
 | 
			
		||||
        <div>{body}</div>
 | 
			
		||||
      </Accordion.Collapse>
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ import { useMe } from './me'
 | 
			
		||||
import { ItemButtonBar } from './post'
 | 
			
		||||
import { UPSERT_LINK } from '@/fragments/paidAction'
 | 
			
		||||
import useItemSubmit from './use-item-submit'
 | 
			
		||||
import useDebounceCallback from './use-debounce-callback'
 | 
			
		||||
 | 
			
		||||
export function LinkForm ({ item, sub, editThreshold, children }) {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
@ -25,6 +26,8 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
 | 
			
		||||
  // if Web Share Target API was used
 | 
			
		||||
  const shareUrl = router.query.url
 | 
			
		||||
  const shareTitle = router.query.title
 | 
			
		||||
  // allows finer control over dupe accordian layout shift
 | 
			
		||||
  const [dupes, setDupes] = useState()
 | 
			
		||||
 | 
			
		||||
  const [getPageTitleAndUnshorted, { data }] = useLazyQuery(gql`
 | 
			
		||||
    query PageTitleAndUnshorted($url: String!) {
 | 
			
		||||
@ -39,9 +42,7 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
 | 
			
		||||
      dupes(url: $url) {
 | 
			
		||||
        ...ItemFields
 | 
			
		||||
      }
 | 
			
		||||
    }`, {
 | 
			
		||||
    onCompleted: () => setPostDisabled(false)
 | 
			
		||||
  })
 | 
			
		||||
    }`)
 | 
			
		||||
  const [getRelated, { data: relatedData }] = useLazyQuery(gql`
 | 
			
		||||
    ${ITEM_FIELDS}
 | 
			
		||||
    query related($title: String!) {
 | 
			
		||||
@ -69,19 +70,27 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
 | 
			
		||||
 | 
			
		||||
  const onSubmit = useItemSubmit(UPSERT_LINK, { item, sub })
 | 
			
		||||
 | 
			
		||||
  const getDupesDebounce = useDebounceCallback((...args) => getDupes(...args), 1000, [getDupes])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (data?.pageTitleAndUnshorted?.title) {
 | 
			
		||||
      setTitleOverride(data.pageTitleAndUnshorted.title)
 | 
			
		||||
    }
 | 
			
		||||
  }, [data?.pageTitleAndUnshorted?.title])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!dupesLoading) {
 | 
			
		||||
      setDupes(dupesData?.dupes)
 | 
			
		||||
    }
 | 
			
		||||
  }, [dupesLoading, dupesData, setDupes])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (data?.pageTitleAndUnshorted?.unshorted) {
 | 
			
		||||
      getDupes({
 | 
			
		||||
      getDupesDebounce({
 | 
			
		||||
        variables: { url: data?.pageTitleAndUnshorted?.unshorted }
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }, [data?.pageTitleAndUnshorted?.unshorted])
 | 
			
		||||
  }, [data?.pageTitleAndUnshorted?.unshorted, getDupesDebounce])
 | 
			
		||||
 | 
			
		||||
  const [postDisabled, setPostDisabled] = useState(false)
 | 
			
		||||
  const [titleOverride, setTitleOverride] = useState()
 | 
			
		||||
@ -147,8 +156,8 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
 | 
			
		||||
          }
 | 
			
		||||
          if (e.target.value) {
 | 
			
		||||
            setPostDisabled(true)
 | 
			
		||||
            setTimeout(() => setPostDisabled(false), 3000)
 | 
			
		||||
            getDupes({
 | 
			
		||||
            setTimeout(() => setPostDisabled(false), 2000)
 | 
			
		||||
            getDupesDebounce({
 | 
			
		||||
              variables: { url: e.target.value }
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
@ -164,15 +173,15 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
 | 
			
		||||
        />
 | 
			
		||||
      </AdvPostForm>
 | 
			
		||||
      <ItemButtonBar itemId={item?.id} disable={postDisabled}>
 | 
			
		||||
        {!item && dupesLoading &&
 | 
			
		||||
          <div className='d-flex justify-content-center'>
 | 
			
		||||
            <Moon className='spin fill-grey' />
 | 
			
		||||
            <div className='ms-2 text-muted' style={{ fontWeight: '600' }}>searching for dupes</div>
 | 
			
		||||
        {!item && postDisabled &&
 | 
			
		||||
          <div className='d-flex align-items-center small'>
 | 
			
		||||
            <Moon className='spin fill-grey' height={16} width={16} />
 | 
			
		||||
            <div className='ms-2 text-muted'>searching for dupes</div>
 | 
			
		||||
          </div>}
 | 
			
		||||
      </ItemButtonBar>
 | 
			
		||||
      {!item &&
 | 
			
		||||
        <>
 | 
			
		||||
          {dupesData?.dupes?.length > 0 &&
 | 
			
		||||
          {dupes?.length > 0 &&
 | 
			
		||||
            <div className='mt-3'>
 | 
			
		||||
              <AccordianItem
 | 
			
		||||
                show
 | 
			
		||||
@ -180,7 +189,7 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
 | 
			
		||||
                header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>dupes</div>}
 | 
			
		||||
                body={
 | 
			
		||||
                  <div>
 | 
			
		||||
                    {dupesData.dupes.map((item, i) => (
 | 
			
		||||
                    {dupes.map((item, i) => (
 | 
			
		||||
                      <Item item={item} key={item.id} />
 | 
			
		||||
                    ))}
 | 
			
		||||
                  </div>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								prisma/migrations/20240903180612_url_index/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								prisma/migrations/20240903180612_url_index/migration.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
-- CreateIndex
 | 
			
		||||
CREATE INDEX "Item_url_idx" ON "Item" ("url" COLLATE "C");
 | 
			
		||||
@ -529,6 +529,7 @@ model Item {
 | 
			
		||||
  @@index([invoiceId])
 | 
			
		||||
  @@index([invoiceActionState])
 | 
			
		||||
  @@index([cost])
 | 
			
		||||
  @@index([url])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// we use this to denormalize a user's aggregated interactions (zaps) with an item
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user