@@ -2,7 +2,7 @@ import React, { useState, useEffect, memo, useCallback, useRef, useMemo, useSync
22import styled , { keyframes , css } from 'styled-components' ;
33import { useNoteActions } from '../contexts/NoteActionsContext' ; // Use stable actions context
44import { selectionModeRef } from '../contexts/NoteSelectionContext' ;
5- import { tagsApi , getServerUrl } from '../services/api' ;
5+ import { tagsApi } from '../services/api' ;
66import NoteTagPicker , { TagPicker } from './NoteTagPicker' ;
77import { ColorPicker as SharedColorPicker } from './ColorPicker' ; // Import the new ColorPicker
88import Icon from './Icons' ;
@@ -11,6 +11,7 @@ import ImageGallery from './ImageGallery';
1111import ProgressFlyout from './ProgressFlyout' ;
1212import CompactBookCard from './CompactBookCard' ;
1313import CompactImdbCard from './CompactImdbCard' ;
14+ import LinkPreviewCard from './LinkPreviewCard' ;
1415import { loadNoteImages } from '../services/imageManager' ;
1516import { useInlineImageResolution } from '../hooks/useInlineImageResolution' ;
1617
@@ -93,7 +94,7 @@ const Card = styled.div`
9394 padding: 16px;
9495 padding-bottom: 8px;
9596 height: fit-content; /* Key for masonry layout: use fit-content instead of min-height */
96- max-height: ${ props => props . $hasImages ? '620px' : '460px' } ;
97+ max-height: ${ props => props . $hasImages ? '620px' : props . $hasLinks ? '600px' : '460px' } ;
9798 display: flex;
9899 flex-direction: column;
99100 transition: box-shadow 0.2s ease, transform 0.2s ease, opacity 0.3s ease;
@@ -121,7 +122,7 @@ const Card = styled.div`
121122 @media (max-width: 600px) {
122123 padding: 12px;
123124 font-size: 0.9em;
124- max-height: ${ props => props . $hasImages ? '440px' : props . $hasTags ? '380px' : '250px' } ;
125+ max-height: ${ props => props . $hasImages ? '440px' : props . $hasLinks ? '440px' : props . $ hasTags ? '380px' : '250px' } ;
125126 /* Use CSS variable for mobile background */
126127 background-color: var(--card-bg-color, var(--mobile-background-color));
127128 border: ${ props => props . $isDirectIdMatch
@@ -526,6 +527,30 @@ const TagText = styled.span`
526527 min-width: 0;
527528` ;
528529
530+ // Holds the stack of link preview cards. A flex column with a bounded height: when
531+ // several (image-heavy) previews would overflow, the cards themselves shrink to fit
532+ // (their images compress) rather than the card spilling or the note text getting
533+ // starved. flex-shrink:999 + min-height:0 makes THIS section yield first under
534+ // pressure. Collapses to no margin when it renders empty (previews toggled off).
535+ const LinkPreviewList = styled . div `
536+ display: flex;
537+ flex-direction: column;
538+ gap: 6px;
539+ min-height: 0;
540+ flex-shrink: 999;
541+ max-height: 300px;
542+ margin: 8px 0 10px;
543+
544+ &:empty {
545+ margin: 0;
546+ }
547+
548+ /* When the note already has images/sketches, only the first preview is shown */
549+ ${ props => props . $compact && `
550+ > *:nth-child(n+2) { display: none; }
551+ ` }
552+ ` ;
553+
529554const UrlLink = styled ( Tag ) `
530555
531556` ;
@@ -670,7 +695,6 @@ const extractNoteReferences = (text, urlsToExclude = []) => {
670695} ;
671696
672697const NoteCard = memo ( function NoteCard ( { note, searchQuery, layoutView, onPickerOpen, isSelected, onToggleSelect } ) {
673- console . log ( '[NoteCard] Rendering note:' , note . id ) ;
674698 const [ justEdited , setJustEdited ] = useState ( false ) ;
675699 const [ showActions , setShowActions ] = useState ( false ) ;
676700 const [ showColorPicker , setShowColorPicker ] = useState ( false ) ;
@@ -1040,6 +1064,17 @@ const NoteCard = memo(function NoteCard({ note, searchQuery, layoutView, onPicke
10401064 return noteTags . some ( tag => tag . visible === false ) ;
10411065 } , [ noteTags ] ) ;
10421066
1067+ // Renderable link previews (have a fetched title/image), capped at 3.
1068+ const previewLinks = useMemo (
1069+ ( ) => ( note . links || [ ] ) . filter ( l => l . title || l . image_url ) . slice ( 0 , 3 ) ,
1070+ [ note . links ]
1071+ ) ;
1072+ const hasLinkPreviews = previewLinks . length > 0 ;
1073+ // Images get cramped once there are three cards, so only show them for 1-2.
1074+ const showLinkImages = previewLinks . length <= 2 ;
1075+ // With more than one card, clamp each title/description to a single line.
1076+ const oneLinePreviewText = previewLinks . length > 1 ;
1077+
10431078 // Compute inline style for color CSS variables - avoids styled-components class regeneration
10441079 const cardColorStyle = useMemo ( ( ) => {
10451080 const color = note . color || 'default' ;
@@ -1060,6 +1095,7 @@ const NoteCard = memo(function NoteCard({ note, searchQuery, layoutView, onPicke
10601095 style = { cardColorStyle }
10611096 $isPinned = { note . is_pinned }
10621097 $hasImages = { images . length > 0 }
1098+ $hasLinks = { hasLinkPreviews }
10631099 $hasTags = { visibleTags . length > 0 } // Use visibleTags for layout check
10641100 $justEdited = { justEdited }
10651101 $isSelected = { isSelected }
@@ -1113,6 +1149,15 @@ const NoteCard = memo(function NoteCard({ note, searchQuery, layoutView, onPicke
11131149 </ div >
11141150 ) }
11151151
1152+ { /* Link preview cards (max 3; images hidden once there are 3) */ }
1153+ { hasLinkPreviews && (
1154+ < LinkPreviewList $compact = { images . length > 0 || sketches . length > 0 } >
1155+ { previewLinks . map ( link => (
1156+ < LinkPreviewCard key = { link . id } link = { link } isDark = { isDarkTheme } showImage = { showLinkImages } oneLine = { oneLinePreviewText } />
1157+ ) ) }
1158+ </ LinkPreviewList >
1159+ ) }
1160+
11161161 { /* Display tags, URLs, book references, note references, and objects if any */ }
11171162 { ( noteTags . length > 0 || noteUrls . length > 0 || bookReferences . length > 0 || noteReferences . length > 0 || noteObjects . length > 0 ) && (
11181163 < TagsContainer style = { {
@@ -1443,6 +1488,18 @@ const NoteCard = memo(function NoteCard({ note, searchQuery, layoutView, onPicke
14431488 }
14441489 }
14451490
1491+ // Check for link preview updates. Preview fetches (title/image/favicon) update
1492+ // the note_links table WITHOUT bumping note.updated_at, so this must be compared
1493+ // explicitly — otherwise a card whose preview just arrived over the socket won't
1494+ // re-render until a full refresh.
1495+ const prevLinks = prevProps . note . links || [ ] ;
1496+ const nextLinks = nextProps . note . links || [ ] ;
1497+ if ( prevLinks . length !== nextLinks . length ) return false ;
1498+ for ( let i = 0 ; i < prevLinks . length ; i ++ ) {
1499+ if ( prevLinks [ i ] . id !== nextLinks [ i ] . id ) return false ;
1500+ if ( prevLinks [ i ] . fetched_at !== nextLinks [ i ] . fetched_at ) return false ;
1501+ }
1502+
14461503 // For large content, only check length to avoid expensive comparison
14471504 const prevContent = prevProps . note . content || '' ;
14481505 const nextContent = nextProps . note . content || '' ;
0 commit comments