fix(ui): synchronize favorites sidebar notices
- commit
- 79c0bb88eae9cba56a4783c64fa05e3dd7eec48e
- author
- brice · · 2026-07-14 07:02 UTC
- parent
- 68e8ab6
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +30 -11
M internal/ui/component/favorites_sidebar.go +15 -4
| @@ -55,6 +55,7 @@ type FavoritesSidebar struct { | |||
| 55 | 55 | displayRows []favoriteSidebarDisplayRow | |
| 56 | 56 | currentQuery string | |
| 57 | 57 | notice string | |
| 58 | + | noticeLoadGen uint64 | |
| 58 | 59 | loadGen uint64 | |
| 59 | 60 | destroyed bool | |
| 60 | 61 | visible bool | |
| @@ -263,6 +264,7 @@ func (fs *FavoritesSidebar) startLoad() { | |||
| 263 | 264 | ctx := fs.ctx | |
| 264 | 265 | visible := fs.visible | |
| 265 | 266 | fs.notice = "Loading favorites..." | |
| 267 | + | fs.noticeLoadGen = gen | |
| 266 | 268 | fs.mu.Unlock() | |
| 267 | 269 | if visible { | |
| 268 | 270 | fs.scheduleIdle(glib.SourceFunc(func(uintptr) bool { | |
| @@ -311,10 +313,13 @@ func (fs *FavoritesSidebar) applyLoadedData(favorites []*entity.Favorite, tags [ | |||
| 311 | 313 | } | |
| 312 | 314 | fs.allFavorites = favorites | |
| 313 | 315 | fs.allTags = tags | |
| 314 | - | if err != nil { | |
| 315 | - | fs.notice = err.Error() | |
| 316 | - | } else { | |
| 317 | - | fs.notice = "" | |
| 316 | + | if fs.noticeLoadGen == gen { | |
| 317 | + | fs.noticeLoadGen = 0 | |
| 318 | + | if err != nil { | |
| 319 | + | fs.notice = err.Error() | |
| 320 | + | } else { | |
| 321 | + | fs.notice = "" | |
| 322 | + | } | |
| 318 | 323 | } | |
| 319 | 324 | fs.rebuildDisplayRowsLocked() | |
| 320 | 325 | fs.mu.Unlock() | |
| @@ -363,6 +368,12 @@ func (fs *FavoritesSidebar) rebuildDisplayRowsLocked() { | |||
| 363 | 368 | fs.displayRows = buildFavoriteSidebarDisplayRows(filtered) | |
| 364 | 369 | } | |
| 365 | 370 | ||
| 371 | + | // setNoticeLocked records a notice not owned by an in-flight load. fs.mu must be held. | |
| 372 | + | func (fs *FavoritesSidebar) setNoticeLocked(notice string) { | |
| 373 | + | fs.notice = notice | |
| 374 | + | fs.noticeLoadGen = 0 | |
| 375 | + | } | |
| 376 | + | ||
| 366 | 377 | func (fs *FavoritesSidebar) scheduleIdle(cb glib.SourceFunc) { | |
| 367 | 378 | if fs != nil && fs.idleScheduler != nil { | |
| 368 | 379 | fs.idleScheduler(cb) | |
| @@ -55,6 +55,7 @@ type FavoritesSidebar struct { | |||||
| 55 | displayRows []favoriteSidebarDisplayRow | 55 | displayRows []favoriteSidebarDisplayRow | ||
| 56 | currentQuery string | 56 | currentQuery string | ||
| 57 | notice string | 57 | notice string | ||
| 58 | + | noticeLoadGen uint64 | |||
| 58 | loadGen uint64 | 59 | loadGen uint64 | ||
| 59 | destroyed bool | 60 | destroyed bool | ||
| 60 | visible bool | 61 | visible bool | ||
| @@ -263,6 +264,7 @@ func (fs *FavoritesSidebar) startLoad() { | |||||
| 263 | ctx := fs.ctx | 264 | ctx := fs.ctx | ||
| 264 | visible := fs.visible | 265 | visible := fs.visible | ||
| 265 | fs.notice = "Loading favorites..." | 266 | fs.notice = "Loading favorites..." | ||
| 267 | + | fs.noticeLoadGen = gen | |||
| 266 | fs.mu.Unlock() | 268 | fs.mu.Unlock() | ||
| 267 | if visible { | 269 | if visible { | ||
| 268 | fs.scheduleIdle(glib.SourceFunc(func(uintptr) bool { | 270 | fs.scheduleIdle(glib.SourceFunc(func(uintptr) bool { | ||
| @@ -311,10 +313,13 @@ func (fs *FavoritesSidebar) applyLoadedData(favorites []*entity.Favorite, tags [ | |||||
| 311 | } | 313 | } | ||
| 312 | fs.allFavorites = favorites | 314 | fs.allFavorites = favorites | ||
| 313 | fs.allTags = tags | 315 | fs.allTags = tags | ||
| 314 | - | if err != nil { | 316 | + | if fs.noticeLoadGen == gen { |
| 315 | - | fs.notice = err.Error() | 317 | + | fs.noticeLoadGen = 0 |
| 316 | - | } else { | 318 | + | if err != nil { |
| 317 | - | fs.notice = "" | 319 | + | fs.notice = err.Error() |
| 320 | + | } else { | |||
| 321 | + | fs.notice = "" | |||
| 322 | + | } | |||
| 318 | } | 323 | } | ||
| 319 | fs.rebuildDisplayRowsLocked() | 324 | fs.rebuildDisplayRowsLocked() | ||
| 320 | fs.mu.Unlock() | 325 | fs.mu.Unlock() | ||
| @@ -363,6 +368,12 @@ func (fs *FavoritesSidebar) rebuildDisplayRowsLocked() { | |||||
| 363 | fs.displayRows = buildFavoriteSidebarDisplayRows(filtered) | 368 | fs.displayRows = buildFavoriteSidebarDisplayRows(filtered) | ||
| 364 | } | 369 | } | ||
| 365 | 370 | ||||
| 371 | + | // setNoticeLocked records a notice not owned by an in-flight load. fs.mu must be held. | |||
| 372 | + | func (fs *FavoritesSidebar) setNoticeLocked(notice string) { | |||
| 373 | + | fs.notice = notice | |||
| 374 | + | fs.noticeLoadGen = 0 | |||
| 375 | + | } | |||
| 376 | + | ||||
| 366 | func (fs *FavoritesSidebar) scheduleIdle(cb glib.SourceFunc) { | 377 | func (fs *FavoritesSidebar) scheduleIdle(cb glib.SourceFunc) { | ||
| 367 | if fs != nil && fs.idleScheduler != nil { | 378 | if fs != nil && fs.idleScheduler != nil { | ||
| 368 | fs.idleScheduler(cb) | 379 | fs.idleScheduler(cb) | ||
M internal/ui/component/favorites_sidebar_forms.go +6 -6
| @@ -42,7 +42,7 @@ func (fs *FavoritesSidebar) beginAddForm() { | |||
| 42 | 42 | fs.editingID = 0 | |
| 43 | 43 | fs.confirmDelete = false | |
| 44 | 44 | fs.confirmDeleteID = 0 | |
| 45 | - | fs.notice = "Add favorite: URL, title, comma-separated tag IDs, shortcut 1-9. Press Ctrl+Enter or Save." | |
| 45 | + | fs.setNoticeLocked("Add favorite: URL, title, comma-separated tag IDs, shortcut 1-9. Press Ctrl+Enter or Save.") | |
| 46 | 46 | fs.mu.Unlock() | |
| 47 | 47 | fs.renderForm(nil) | |
| 48 | 48 | fs.rebuildList() | |
| @@ -63,7 +63,7 @@ func (fs *FavoritesSidebar) beginEditForm() { | |||
| 63 | 63 | fs.editingID = fav.ID | |
| 64 | 64 | fs.confirmDelete = false | |
| 65 | 65 | fs.confirmDeleteID = 0 | |
| 66 | - | fs.notice = "Edit favorite: URL is read-only; use tag mode for tags. Press Ctrl+Enter or Save." | |
| 66 | + | fs.setNoticeLocked("Edit favorite: URL is read-only; use tag mode for tags. Press Ctrl+Enter or Save.") | |
| 67 | 67 | fs.mu.Unlock() | |
| 68 | 68 | fs.renderForm(fav) | |
| 69 | 69 | fs.rebuildList() | |
| @@ -170,7 +170,7 @@ func (fs *FavoritesSidebar) cancelManagement() bool { | |||
| 170 | 170 | fs.confirmDelete = false | |
| 171 | 171 | fs.confirmDeleteID = 0 | |
| 172 | 172 | if active { | |
| 173 | - | fs.notice = "" | |
| 173 | + | fs.setNoticeLocked("") | |
| 174 | 174 | } | |
| 175 | 175 | fs.mu.Unlock() | |
| 176 | 176 | if fs.formBox != nil { | |
| @@ -250,7 +250,7 @@ func (fs *FavoritesSidebar) setModeNotice(mode favoritesSidebarMode, notice stri | |||
| 250 | 250 | fs.mode = mode | |
| 251 | 251 | fs.confirmDelete = false | |
| 252 | 252 | fs.confirmDeleteID = 0 | |
| 253 | - | fs.notice = notice | |
| 253 | + | fs.setNoticeLocked(notice) | |
| 254 | 254 | } | |
| 255 | 255 | fs.mu.Unlock() | |
| 256 | 256 | fs.rebuildList() | |
| @@ -324,7 +324,7 @@ func (fs *FavoritesSidebar) requestDeleteConfirmation() bool { | |||
| 324 | 324 | if !fs.destroyed { | |
| 325 | 325 | fs.confirmDelete = true | |
| 326 | 326 | fs.confirmDeleteID = fav.ID | |
| 327 | - | fs.notice = "Delete selected favorite? Press Delete again to confirm, Esc to cancel." | |
| 327 | + | fs.setNoticeLocked("Delete selected favorite? Press Delete again to confirm, Esc to cancel.") | |
| 328 | 328 | } | |
| 329 | 329 | fs.mu.Unlock() | |
| 330 | 330 | fs.rebuildList() | |
| @@ -396,7 +396,7 @@ func (fs *FavoritesSidebar) selectedFavorite() *entity.Favorite { | |||
| 396 | 396 | func (fs *FavoritesSidebar) setNotice(notice string) { | |
| 397 | 397 | fs.mu.Lock() | |
| 398 | 398 | if !fs.destroyed { | |
| 399 | - | fs.notice = notice | |
| 399 | + | fs.setNoticeLocked(notice) | |
| 400 | 400 | } | |
| 401 | 401 | fs.mu.Unlock() | |
| 402 | 402 | fs.rebuildList() | |
| @@ -42,7 +42,7 @@ func (fs *FavoritesSidebar) beginAddForm() { | |||||
| 42 | fs.editingID = 0 | 42 | fs.editingID = 0 | ||
| 43 | fs.confirmDelete = false | 43 | fs.confirmDelete = false | ||
| 44 | fs.confirmDeleteID = 0 | 44 | fs.confirmDeleteID = 0 | ||
| 45 | - | fs.notice = "Add favorite: URL, title, comma-separated tag IDs, shortcut 1-9. Press Ctrl+Enter or Save." | 45 | + | fs.setNoticeLocked("Add favorite: URL, title, comma-separated tag IDs, shortcut 1-9. Press Ctrl+Enter or Save.") |
| 46 | fs.mu.Unlock() | 46 | fs.mu.Unlock() | ||
| 47 | fs.renderForm(nil) | 47 | fs.renderForm(nil) | ||
| 48 | fs.rebuildList() | 48 | fs.rebuildList() | ||
| @@ -63,7 +63,7 @@ func (fs *FavoritesSidebar) beginEditForm() { | |||||
| 63 | fs.editingID = fav.ID | 63 | fs.editingID = fav.ID | ||
| 64 | fs.confirmDelete = false | 64 | fs.confirmDelete = false | ||
| 65 | fs.confirmDeleteID = 0 | 65 | fs.confirmDeleteID = 0 | ||
| 66 | - | fs.notice = "Edit favorite: URL is read-only; use tag mode for tags. Press Ctrl+Enter or Save." | 66 | + | fs.setNoticeLocked("Edit favorite: URL is read-only; use tag mode for tags. Press Ctrl+Enter or Save.") |
| 67 | fs.mu.Unlock() | 67 | fs.mu.Unlock() | ||
| 68 | fs.renderForm(fav) | 68 | fs.renderForm(fav) | ||
| 69 | fs.rebuildList() | 69 | fs.rebuildList() | ||
| @@ -170,7 +170,7 @@ func (fs *FavoritesSidebar) cancelManagement() bool { | |||||
| 170 | fs.confirmDelete = false | 170 | fs.confirmDelete = false | ||
| 171 | fs.confirmDeleteID = 0 | 171 | fs.confirmDeleteID = 0 | ||
| 172 | if active { | 172 | if active { | ||
| 173 | - | fs.notice = "" | 173 | + | fs.setNoticeLocked("") |
| 174 | } | 174 | } | ||
| 175 | fs.mu.Unlock() | 175 | fs.mu.Unlock() | ||
| 176 | if fs.formBox != nil { | 176 | if fs.formBox != nil { | ||
| @@ -250,7 +250,7 @@ func (fs *FavoritesSidebar) setModeNotice(mode favoritesSidebarMode, notice stri | |||||
| 250 | fs.mode = mode | 250 | fs.mode = mode | ||
| 251 | fs.confirmDelete = false | 251 | fs.confirmDelete = false | ||
| 252 | fs.confirmDeleteID = 0 | 252 | fs.confirmDeleteID = 0 | ||
| 253 | - | fs.notice = notice | 253 | + | fs.setNoticeLocked(notice) |
| 254 | } | 254 | } | ||
| 255 | fs.mu.Unlock() | 255 | fs.mu.Unlock() | ||
| 256 | fs.rebuildList() | 256 | fs.rebuildList() | ||
| @@ -324,7 +324,7 @@ func (fs *FavoritesSidebar) requestDeleteConfirmation() bool { | |||||
| 324 | if !fs.destroyed { | 324 | if !fs.destroyed { | ||
| 325 | fs.confirmDelete = true | 325 | fs.confirmDelete = true | ||
| 326 | fs.confirmDeleteID = fav.ID | 326 | fs.confirmDeleteID = fav.ID | ||
| 327 | - | fs.notice = "Delete selected favorite? Press Delete again to confirm, Esc to cancel." | 327 | + | fs.setNoticeLocked("Delete selected favorite? Press Delete again to confirm, Esc to cancel.") |
| 328 | } | 328 | } | ||
| 329 | fs.mu.Unlock() | 329 | fs.mu.Unlock() | ||
| 330 | fs.rebuildList() | 330 | fs.rebuildList() | ||
| @@ -396,7 +396,7 @@ func (fs *FavoritesSidebar) selectedFavorite() *entity.Favorite { | |||||
| 396 | func (fs *FavoritesSidebar) setNotice(notice string) { | 396 | func (fs *FavoritesSidebar) setNotice(notice string) { | ||
| 397 | fs.mu.Lock() | 397 | fs.mu.Lock() | ||
| 398 | if !fs.destroyed { | 398 | if !fs.destroyed { | ||
| 399 | - | fs.notice = notice | 399 | + | fs.setNoticeLocked(notice) |
| 400 | } | 400 | } | ||
| 401 | fs.mu.Unlock() | 401 | fs.mu.Unlock() | ||
| 402 | fs.rebuildList() | 402 | fs.rebuildList() | ||
M internal/ui/component/favorites_sidebar_keyboard.go +1 -1
| @@ -463,7 +463,7 @@ func (fs *FavoritesSidebar) handleNavigationError(err error) { | |||
| 463 | 463 | fs.mu.Unlock() | |
| 464 | 464 | return | |
| 465 | 465 | } | |
| 466 | - | fs.notice = err.Error() | |
| 466 | + | fs.setNoticeLocked(err.Error()) | |
| 467 | 467 | fs.mu.Unlock() | |
| 468 | 468 | fs.rebuildList() | |
| 469 | 469 | } | |
| @@ -463,7 +463,7 @@ func (fs *FavoritesSidebar) handleNavigationError(err error) { | |||||
| 463 | fs.mu.Unlock() | 463 | fs.mu.Unlock() | ||
| 464 | return | 464 | return | ||
| 465 | } | 465 | } | ||
| 466 | - | fs.notice = err.Error() | 466 | + | fs.setNoticeLocked(err.Error()) |
| 467 | fs.mu.Unlock() | 467 | fs.mu.Unlock() | ||
| 468 | fs.rebuildList() | 468 | fs.rebuildList() | ||
| 469 | } | 469 | } | ||
M internal/ui/component/favorites_sidebar_test.go +8 -0
| @@ -486,6 +486,8 @@ func TestFavoritesSidebarAddShortcutFailureClosesFormAfterCreate(t *testing.T) { | |||
| 486 | 486 | uc := &fakeFavoritesSidebarUC{errOnSetShortcut: errors.New("shortcut failed")} | |
| 487 | 487 | fs := newFavoritesSidebarHarness(nil, nil) | |
| 488 | 488 | fs.favoritesUC = uc | |
| 489 | + | loaded := make(chan glib.SourceFunc, 1) | |
| 490 | + | fs.idleScheduler = func(cb glib.SourceFunc) { loaded <- cb } | |
| 489 | 491 | fs.formURL = "https://add.test" | |
| 490 | 492 | fs.formTitle = "Added" | |
| 491 | 493 | fs.formShortcut = "3" | |
| @@ -493,6 +495,12 @@ func TestFavoritesSidebarAddShortcutFailureClosesFormAfterCreate(t *testing.T) { | |||
| 493 | 495 | ||
| 494 | 496 | assert.True(t, fs.submitForm()) | |
| 495 | 497 | require.Len(t, uc.addInputs, 1) | |
| 498 | + | select { | |
| 499 | + | case cb := <-loaded: | |
| 500 | + | cb(0) | |
| 501 | + | case <-time.After(time.Second): | |
| 502 | + | t.Fatal("timed out waiting for favorites reload") | |
| 503 | + | } | |
| 496 | 504 | assert.Equal(t, favoritesSidebarModeNone, fs.mode) | |
| 497 | 505 | assert.Contains(t, fs.notice, "failed to set shortcut") | |
| 498 | 506 | } | |
| @@ -486,6 +486,8 @@ func TestFavoritesSidebarAddShortcutFailureClosesFormAfterCreate(t *testing.T) { | |||||
| 486 | uc := &fakeFavoritesSidebarUC{errOnSetShortcut: errors.New("shortcut failed")} | 486 | uc := &fakeFavoritesSidebarUC{errOnSetShortcut: errors.New("shortcut failed")} | ||
| 487 | fs := newFavoritesSidebarHarness(nil, nil) | 487 | fs := newFavoritesSidebarHarness(nil, nil) | ||
| 488 | fs.favoritesUC = uc | 488 | fs.favoritesUC = uc | ||
| 489 | + | loaded := make(chan glib.SourceFunc, 1) | |||
| 490 | + | fs.idleScheduler = func(cb glib.SourceFunc) { loaded <- cb } | |||
| 489 | fs.formURL = "https://add.test" | 491 | fs.formURL = "https://add.test" | ||
| 490 | fs.formTitle = "Added" | 492 | fs.formTitle = "Added" | ||
| 491 | fs.formShortcut = "3" | 493 | fs.formShortcut = "3" | ||
| @@ -493,6 +495,12 @@ func TestFavoritesSidebarAddShortcutFailureClosesFormAfterCreate(t *testing.T) { | |||||
| 493 | 495 | ||||
| 494 | assert.True(t, fs.submitForm()) | 496 | assert.True(t, fs.submitForm()) | ||
| 495 | require.Len(t, uc.addInputs, 1) | 497 | require.Len(t, uc.addInputs, 1) | ||
| 498 | + | select { | |||
| 499 | + | case cb := <-loaded: | |||
| 500 | + | cb(0) | |||
| 501 | + | case <-time.After(time.Second): | |||
| 502 | + | t.Fatal("timed out waiting for favorites reload") | |||
| 503 | + | } | |||
| 496 | assert.Equal(t, favoritesSidebarModeNone, fs.mode) | 504 | assert.Equal(t, favoritesSidebarModeNone, fs.mode) | ||
| 497 | assert.Contains(t, fs.notice, "failed to set shortcut") | 505 | assert.Contains(t, fs.notice, "failed to set shortcut") | ||
| 498 | } | 506 | } | ||