diff --git a/Dockerfile b/Dockerfile index 43f47640..413068ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,8 +28,7 @@ RUN npm install # Copy the source code to prevent invaliding cache whenever there is a change in the code COPY . . -RUN npm run build \ - && node scripts/write-build-version-json.mjs +RUN npm run build # Step 2: Final container with Nginx and embedded config FROM nginx:alpine diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 587f6ec3..e11688f9 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,9 +1,15 @@ #!/bin/sh # Runtime config for the SPA. NIP-66 monitor runs in a separate cron container; nsec is never sent to the client. # Optional: NIP66_MONITOR_NPUB (npub of the monitor) can be exposed so the relay info page shows who runs the monitor. +HTML=/usr/share/nginx/html +if [ ! -s "$HTML/health.json" ]; then + jq -n --arg t "$(date -Iseconds)" \ + '{status:"ok", name:"imwald", version:"unknown", gitTag:"unknown", gitCommit:"unknown", builtAt:$t}' \ + > "$HTML/health.json" +fi if [ -n "$NIP66_MONITOR_NPUB" ]; then - jq -n --arg npub "$NIP66_MONITOR_NPUB" '{NIP66_MONITOR_NPUB: $npub}' > /usr/share/nginx/html/config.json + jq -n --arg npub "$NIP66_MONITOR_NPUB" '{NIP66_MONITOR_NPUB: $npub}' > "$HTML/config.json" else - echo '{}' > /usr/share/nginx/html/config.json + echo '{}' > "$HTML/config.json" fi exec nginx -g "daemon off;" diff --git a/package.json b/package.json index d4204bda..e2af0d6e 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "docker:editor-tools": "bash scripts/ensure-libretranslate-dirs.sh && docker compose -f docker-compose.dev.yml --profile editor-tools up -d languagetool libretranslate", "docker:local-ancillary": "bash scripts/ensure-libretranslate-dirs.sh && docker compose -f docker-compose.dev.yml --profile editor-tools --profile local-tts build piper-tts-proxy && docker compose -f docker-compose.dev.yml --profile editor-tools --profile local-tts up -d og-proxy languagetool libretranslate piper-wyoming piper-tts-proxy", "piper-tts-proxy": "cross-env NODE_ENV=development tsx services/piper-tts-proxy/http.ts", - "build": "tsc -b && vite build", + "build": "tsc -b && vite build && node scripts/write-build-version-json.mjs", "lint": "eslint .", "knip": "knip", "format": "prettier --write .", diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index 0b114692..e43db808 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -911,8 +911,6 @@ const NoteList = forwardRef( const timelineEstablishedCloserRef = useRef<(() => void) | null>(null) /** Bumps on each timeline effect run so Strict Mode / fast remount does not stack subscribeTimeline waves. */ const timelineEffectGenerationRef = useRef(0) - /** Skip closing/reopening the live REQ when effect deps churn but subscription shape is unchanged. */ - const lastTimelineLiveIdentityKeyRef = useRef('') /** Session snapshot was written to state; log once after commit (see feed-paint layout effect). */ const feedPaintSessionPendingRef = useRef(false) /** Relay / one-shot data was written to state; log once after commit. */ @@ -1041,6 +1039,8 @@ const NoteList = forwardRef( useFilterAsIs ] ) + const mapLiveSubRequestsForTimelineRef = useRef(mapLiveSubRequestsForTimeline) + mapLiveSubRequestsForTimelineRef.current = mapLiveSubRequestsForTimeline /** Feed identity for scoping client filter state (timeline key minus unrelated churn where possible). */ const feedClientFilterScopeKey = useMemo( @@ -2021,35 +2021,48 @@ const NoteList = forwardRef( useImperativeHandle(ref, () => ({ scrollToTop, refresh }), [scrollToTop, refresh]) - useEffect(() => { - const timelineLiveIdentityKey = [ - pauseTimelineForPrimaryFreeze ? 'frozen' : 'live', + /** Single key for live timeline REQ identity — effect deps must not exceed this or subscriptions churn. */ + const timelineLiveIdentityKey = useMemo( + () => + [ + pauseTimelineForPrimaryFreeze ? 'frozen' : 'live', + timelineSubscriptionKey, + feedSubscriptionKey ?? '', + sessionSnapshotIdentityKey, + subRequestsKey, + timelineResubscribeKindKey, + seeAllFeedEvents ? '1' : '0', + useFilterAsIs ? '1' : '0', + areAlgoRelays ? '1' : '0', + allowKindlessRelayExplore ? '1' : '0', + clientSideKindFilter ? '1' : '0', + showAllKinds ? '1' : '0', + withKindFilter ? '1' : '0', + feedTimelineScopeKey ?? '', + String(refreshCount), + relayCapabilityReady ? '1' : '0' + ].join('\x1e'), + [ + pauseTimelineForPrimaryFreeze, timelineSubscriptionKey, - feedSubscriptionKey ?? '', + feedSubscriptionKey, sessionSnapshotIdentityKey, subRequestsKey, timelineResubscribeKindKey, - seeAllFeedEvents ? '1' : '0', - useFilterAsIs ? '1' : '0', - areAlgoRelays ? '1' : '0', - allowKindlessRelayExplore ? '1' : '0', - clientSideKindFilter ? '1' : '0', - showAllKinds ? '1' : '0', - withKindFilter ? '1' : '0', - feedTimelineScopeKey ?? '', - String(refreshCount), - relayCapabilityReady ? '1' : '0' - ].join('\x1e') - - if ( - !pauseTimelineForPrimaryFreeze && - lastTimelineLiveIdentityKeyRef.current === timelineLiveIdentityKey && - timelineEstablishedCloserRef.current - ) { - return () => {} - } - lastTimelineLiveIdentityKeyRef.current = timelineLiveIdentityKey + seeAllFeedEvents, + useFilterAsIs, + areAlgoRelays, + allowKindlessRelayExplore, + clientSideKindFilter, + showAllKinds, + withKindFilter, + feedTimelineScopeKey, + refreshCount, + relayCapabilityReady + ] + ) + useEffect(() => { const effectGen = ++timelineEffectGenerationRef.current const timelineEffectStale = () => effectGen !== timelineEffectGenerationRef.current @@ -2108,7 +2121,7 @@ const NoteList = forwardRef( try { const mapped = stripNostrLandAggrFromTimelineSubRequests( feedSubscriptionKey, - mapLiveSubRequestsForTimeline(subRequestsRef.current) + mapLiveSubRequestsForTimelineRef.current(subRequestsRef.current) ) .map((req) => isOfflineRef.current @@ -2201,7 +2214,7 @@ const NoteList = forwardRef( const mappedSubRequests = stripNostrLandAggrFromTimelineSubRequests( feedSubscriptionKey, - mapLiveSubRequestsForTimeline(subRequestsRef.current) + mapLiveSubRequestsForTimelineRef.current(subRequestsRef.current) ) .map((req) => isOfflineRef.current @@ -3457,7 +3470,6 @@ const NoteList = forwardRef( const promise = init() const snapshotKeyForCleanup = sessionSnapshotIdentityKey return () => { - lastTimelineLiveIdentityKeyRef.current = '' effectActive = false if (liveOnNewFlushTimerRef.current != null) { clearTimeout(liveOnNewFlushTimerRef.current) @@ -3490,38 +3502,25 @@ const NoteList = forwardRef( } }) } - }, [ - timelineSubscriptionKey, - feedSubscriptionKey, - sessionSnapshotIdentityKey, - subRequestsKey, - preserveTimelineOnSubRequestsChange, - mergeTimelineWhenSubRequestFiltersMatch, - feedTimelineScopeKey, - refreshCount, - timelineResubscribeKindKey, - seeAllFeedEvents, - useFilterAsIs, - areAlgoRelays, - relayCapabilityReady, - oneShotFetch, - oneShotMergedCap, - revealBatchSize, - oneShotDebugLabel, - oneShotGlobalTimeoutMs, - oneShotEoseTimeoutMs, - oneShotFirstRelayGraceMs, - clientSideKindFilter, - allowKindlessRelayExplore, - showAllKinds, - withKindFilter, - onSingleRelayKindlessEmpty, - mapLiveSubRequestsForTimeline, - progressiveWarmupQuery, - hostPrimaryPageName, - relayAuthoritativeFeedOnly, - pauseTimelineForPrimaryFreeze - ]) + }, [timelineLiveIdentityKey, pauseTimelineForPrimaryFreeze, oneShotFetch]) + + const followingFeedDeltaIdentityKey = useMemo( + () => + [ + followingFeedDeltaSubRequestsKey, + timelineKey ?? '', + feedSubscriptionKey ?? '', + areAlgoRelays ? '1' : '0', + pauseTimelineForPrimaryFreeze ? 'frozen' : 'live' + ].join('\x1e'), + [ + followingFeedDeltaSubRequestsKey, + timelineKey, + feedSubscriptionKey, + areAlgoRelays, + pauseTimelineForPrimaryFreeze + ] + ) useEffect(() => { if (oneShotFetch) return @@ -3542,7 +3541,7 @@ const NoteList = forwardRef( let deltaActive = true const mappedDelta = stripNostrLandAggrFromTimelineSubRequests( feedSubscriptionKey, - mapLiveSubRequestsForTimeline(deltas) + mapLiveSubRequestsForTimelineRef.current(deltas) ) const seeAllNoSpellDelta = seeAllFeedEventsRef.current && !useFilterAsIsRef.current const filterMissingKindsDelta = (f: Filter) => !f.kinds || f.kinds.length === 0 @@ -3745,24 +3744,7 @@ const NoteList = forwardRef( followingFeedDeltaCloserRef.current?.() followingFeedDeltaCloserRef.current = null } - }, [ - followingFeedDeltaSubRequestsKey, - timelineKey, - oneShotFetch, - feedSubscriptionKey, - mapLiveSubRequestsForTimeline, - areAlgoRelays, - allowKindlessRelayExplore, - useFilterAsIs, - clientSideKindFilter, - startLogin, - pubkey, - effectiveShowKinds, - showKind1OPs, - showKind1Replies, - showKind1111, - pauseTimelineForPrimaryFreeze - ]) + }, [followingFeedDeltaIdentityKey, oneShotFetch]) const oneShotDebugPrevLoadingRef = useRef(false) useEffect(() => { @@ -3941,7 +3923,7 @@ const NoteList = forwardRef( if (publicReadFallbackAttemptedRef.current) return const uiStatuses = relayOpTerminalRowsToTimelineRelayUiStatuses(feedSubscribeRelayOutcomes) - const mapped = mapLiveSubRequestsForTimeline(subRequestsRef.current) + const mapped = mapLiveSubRequestsForTimelineRef.current(subRequestsRef.current) if (!mapped.length) return // Skip fallback for d-tag / layered warmup feeds where the live REQ has no NIP-50 `search` @@ -4038,7 +4020,6 @@ const NoteList = forwardRef( progressiveWarmupQuery, feedFullSearchEvents, feedSubscribeRelayOutcomes, - mapLiveSubRequestsForTimeline, effectiveShowKinds, allowKindlessRelayExplore, timelineSubscriptionKey, diff --git a/src/components/ReplyNote/index.tsx b/src/components/ReplyNote/index.tsx index ab435e00..5e245a7d 100644 --- a/src/components/ReplyNote/index.tsx +++ b/src/components/ReplyNote/index.tsx @@ -100,12 +100,10 @@ export default function ReplyNote({ return true }, [showMuted, mutePubkeySet, event, hideContentMentioningMutedUsers]) - return (