fix: initial fix for marking posts as unread
8bb7fa03
1 file(s) · +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"> |
|