fix: initial fix for marking posts as unread 8bb7fa03
Steve · 2025-12-05 20:01 1 file(s) · +71 −17
src/components/dashboard.tsx +71 −17
37 37
		null,
38 38
	);
39 39
	const mainContentRef = React.useRef<HTMLDivElement>(null);
40 +
	const hasUserInteracted = React.useRef(false);
41 +
	const manualToggleTimestamp = React.useRef<number>(0);
42 +
	const lastAutoReadPostId = React.useRef<string | null>(null);
40 43
41 44
	const evolu = useEvolu();
42 45
	const allFeeds = useQuery(allFeedsQuery);
70 73
	const [selectedPostId, setSelectedPostId] = React.useState<string | null>(
71 74
		firstPostId,
72 75
	);
76 +
	const initialPostId = React.useRef(firstPostId);
73 77
74 78
	const selectedFeed = selectedFeedId
75 79
		? allFeeds.find((f) => f.id === selectedFeedId)
137 141
	const toggleReadStatus = React.useCallback(() => {
138 142
		if (!selectedPostId || !selectedPost) return;
139 143
144 +
		// Record timestamp of manual toggle to prevent auto-read from overriding
145 +
		manualToggleTimestamp.current = Date.now();
146 +
147 +
		// Reset the auto-read tracking so if user marks as unread, it won't auto-mark again
148 +
		if (lastAutoReadPostId.current === selectedPostId) {
149 +
			lastAutoReadPostId.current = null;
150 +
		}
151 +
140 152
		const existingStatus = allReadStatusesWithUnread.find(
141 153
			(status) => status.postId === selectedPostId,
142 154
		);
167 179
		}
168 180
	}, [selectedPostId]);
169 181
170 -
	// Mark post as read when selected
182 +
	// Mark post as read when selected (with debounce to allow arrow key navigation)
171 183
	React.useEffect(() => {
172 184
		if (!selectedPostId || !selectedPost) return;
173 185
174 -
		const existingStatus = allReadStatusesWithUnread.find(
175 -
			(status) => status.postId === selectedPostId,
176 -
		);
186 +
		// Don't auto-mark the initial post that was selected on mount
187 +
		// This allows users to mark the first post as unread without it auto-marking as read
188 +
		if (
189 +
			selectedPostId === initialPostId.current &&
190 +
			!hasUserInteracted.current
191 +
		) {
192 +
			hasUserInteracted.current = true;
193 +
			return;
194 +
		}
177 195
178 -
		if (existingStatus && existingStatus.isRead === 0) {
179 -
			// Update existing status to read
180 -
			evolu.update("readStatus", {
181 -
				id: existingStatus.id as any,
182 -
				isRead: 1,
183 -
			});
184 -
		} else if (!existingStatus && selectedPost.feedId) {
185 -
			// Create new read status
186 -
			evolu.insert("readStatus", {
187 -
				postId: selectedPostId,
188 -
				feedId: selectedPost.feedId,
189 -
				isRead: 1,
190 -
			});
196 +
		// Don't even start the timeout if user manually toggled recently
197 +
		// This prevents the effect from marking posts as read after bulk operations
198 +
		const timeSinceManualToggle = Date.now() - manualToggleTimestamp.current;
199 +
		if (timeSinceManualToggle < 3000) {
200 +
			return;
191 201
		}
202 +
203 +
		// Don't auto-mark if we already processed this post
204 +
		// This prevents duplicate auto-reads when dependencies update
205 +
		if (lastAutoReadPostId.current === selectedPostId) {
206 +
			return;
207 +
		}
208 +
209 +
		// Debounce the auto-mark as read by 1.5 seconds
210 +
		// This allows users to navigate with arrow keys without marking everything as read
211 +
		const timeoutId = setTimeout(() => {
212 +
			// Double-check timestamp hasn't changed during the delay
213 +
			// This catches bulk operations that happen during the delay period
214 +
			const timeSinceManualToggle = Date.now() - manualToggleTimestamp.current;
215 +
			if (timeSinceManualToggle < 3000) {
216 +
				return;
217 +
			}
218 +
219 +
			const existingStatus = allReadStatusesWithUnread.find(
220 +
				(status) => status.postId === selectedPostId,
221 +
			);
222 +
223 +
			if (existingStatus && existingStatus.isRead === 0) {
224 +
				// Update existing status to read
225 +
				evolu.update("readStatus", {
226 +
					id: existingStatus.id as any,
227 +
					isRead: 1,
228 +
				});
229 +
				lastAutoReadPostId.current = selectedPostId;
230 +
			} else if (!existingStatus && selectedPost.feedId) {
231 +
				// Create new read status
232 +
				evolu.insert("readStatus", {
233 +
					postId: selectedPostId,
234 +
					feedId: selectedPost.feedId,
235 +
					isRead: 1,
236 +
				});
237 +
				lastAutoReadPostId.current = selectedPostId;
238 +
			}
239 +
		}, 1500); // 1.5 second delay
240 +
241 +
		// Cleanup timeout if user navigates away before the delay
242 +
		return () => clearTimeout(timeoutId);
192 243
	}, [selectedPostId, selectedPost, allReadStatusesWithUnread, evolu]);
193 244
194 245
	// Keyboard navigation for posts
261 312
					onFeedSelect={setSelectedFeedId}
262 313
					selectedPostId={selectedPostId}
263 314
					onPostSelect={setSelectedPostId}
315 +
					onBulkStatusChange={() => {
316 +
						manualToggleTimestamp.current = Date.now();
317 +
					}}
264 318
				/>
265 319
				<SidebarInset className="flex flex-col h-screen overflow-hidden">
266 320
					<header className="bg-background flex shrink-0 items-center gap-2 border-b p-4">