fix: improve channel selection hints and profile switching logic

This commit is contained in:
Sarto
2026-05-04 13:14:27 +03:30
parent 213dc86881
commit 4f59ec8270
2 changed files with 54 additions and 30 deletions
+23 -8
View File
@@ -3022,7 +3022,7 @@
ok: 'باشه',
cancel_media_msg: 'دانلود این رسانه لغو شود؟', dismiss: 'بستن',
write_message: 'پیام بنویسید...', configure_server: 'برای شروع یک سرور راه‌اندازی کنید',
set_up: 'راه‌اندازی', switching: 'در حال تغییر پروفایل...',
set_up: 'راه‌اندازی', switching: 'در حال تغییر پروفایل...', select_channel_hint: 'یک کانال را برای دیدن پیام‌ها انتخاب کنید',
font_size: 'اندازه قلم', debug_mode: 'حالت دیباگ', language: 'زبان',
next_fetch_info: 'زمان باقی‌مانده تا دریافت بعدی محتوا توسط سرور',
no_profiles: 'هنوز پروفایلی وجود ندارد', add_profile: '+ پروفایل جدید',
@@ -3232,7 +3232,7 @@
ok: 'OK',
cancel_media_msg: 'Cancel this download?', dismiss: 'Dismiss',
write_message: 'Write a message...', configure_server: 'Configure a server to start reading',
set_up: 'Set Up', switching: 'Switching profile...',
set_up: 'Set Up', switching: 'Switching profile...', select_channel_hint: 'Pick a channel to view its messages',
font_size: 'Font Size', debug_mode: 'Debug mode', language: 'Language',
next_fetch_info: 'Time until the server next fetches fresh channel content',
no_profiles: 'No profiles yet', add_profile: '+ Add Profile',
@@ -3549,7 +3549,11 @@
// Land on the channel list; don't auto-open the first channel.
// Users on mobile may be tapping while we load and the auto-open
// races with their touch.
if (!channels || channels.length === 0) { showInitProgress(); await doRefresh(); }
if (!channels || channels.length === 0) {
showInitProgress(); await doRefresh();
} else {
document.getElementById('messages').innerHTML = '<div class="empty-state"><p>' + (t('select_channel_hint') || '') + '</p></div>';
}
startAutoRefresh();
} catch (e) { }
}
@@ -4758,14 +4762,20 @@
var r = await fetch('/api/profiles/switch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: id, skipCheck: skipCheck }) });
if (!r.ok) return;
activeProfileId = id; selectedChannel = 0; channels = [];
refreshingChannels = {};
progressSilencedUntil = Date.now() + 5000;
// Reset the chat header so it doesn't keep showing the previous
// profile's channel name/handle while the user picks a new one.
document.getElementById('chatName').textContent = 'thefeed';
document.getElementById('chatSub').textContent = '';
document.getElementById('progressPanel').innerHTML = '';
document.getElementById('messages').innerHTML = '<div class="empty-state"><p>' + t('switching') + '</p></div>';
document.getElementById('messages').innerHTML = '<div class="empty-state"><p>' + (t('select_channel_hint') || t('switching')) + '</p></div>';
// On desktop the chat panel is always visible; collapse it back to
// the sidebar so the empty state doesn't sit next to a stale header.
openSidebar();
await loadProfiles(); closeProfiles();
// selectChannel races with in-flight touch on mobile, skip there.
var preLoadIsOnSidebar = !chatIsOpen;
await loadChannels();
if (channels.length === 0) { showInitProgress(); await doRefresh(); return; }
if (preLoadIsOnSidebar && !chatIsOpen && !mobileQuery.matches) await selectChannel(1);
if (channels.length === 0) { showInitProgress(); await doRefresh(); }
} catch (e) { }
}
@@ -6904,7 +6914,12 @@
}
}
var progressSilencedUntil = 0;
function updateProgressDisplay(line) {
// Silence briefly across a profile switch — log events from the
// previous profile's in-flight fetch keep arriving until the old
// fetcher's context cancellation lands.
if (Date.now() < progressSilencedUntil) return;
var match = line.match(/Channel\s+(\d+)/); if (!match) return;
var channelNum = parseInt(match[1]);
var fracMatch = line.match(/\((\d+)\/(\d+)\)/);
+31 -22
View File
@@ -2279,12 +2279,13 @@ func addToBank(pl *ProfileList, resolvers []string) int {
}
// persistResolverScores saves the current fetcher stats to profiles.json.
// Not serialised by profilesMu — initFetcher holds s.mu while calling this,
// and grabbing profilesMu here would risk AB-BA with handlers that take
// profilesMu first. The score map-merge is benign under last-writer-wins.
func (s *Server) persistResolverScores(stats map[string][3]int64) {
if len(stats) == 0 {
return
}
s.profilesMu.Lock()
defer s.profilesMu.Unlock()
pl, err := s.loadProfiles()
if err != nil || pl == nil {
return
@@ -2363,9 +2364,9 @@ func (s *Server) handleProfiles(w http.ResponseWriter, r *http.Request) {
return
}
s.profilesMu.Lock()
defer s.profilesMu.Unlock()
pl, err := s.loadProfilesExisting()
if err != nil {
s.profilesMu.Unlock()
http.Error(w, fmt.Sprintf("load: %v", err), 500)
return
}
@@ -2444,34 +2445,44 @@ func (s *Server) handleProfiles(w http.ResponseWriter, r *http.Request) {
}
default:
s.profilesMu.Unlock()
http.Error(w, "unknown action", 400)
return
}
if err := s.saveProfiles(pl); err != nil {
http.Error(w, fmt.Sprintf("save profiles: %v", err), 500)
return
}
// Only re-init the fetcher when the active profile's config was modified.
saveErr := s.saveProfiles(pl)
var activeConfig *Config
if needsReinit && pl.Active != "" {
for _, p := range pl.Profiles {
if p.ID == pl.Active {
_ = s.saveConfig(&p.Config)
s.mu.Lock()
s.config = &p.Config
s.mu.Unlock()
if err := s.initFetcher(); err != nil {
log.Printf("[web] re-init fetcher after profile change: %v", err)
} else if req.SkipCheck {
s.skipCheckerUseSaved()
} else {
s.startCheckerThenRefresh()
}
cfg := p.Config
activeConfig = &cfg
break
}
}
}
s.profilesMu.Unlock()
if saveErr != nil {
http.Error(w, fmt.Sprintf("save profiles: %v", saveErr), 500)
return
}
// initFetcher takes s.mu — call it OUTSIDE profilesMu so handlers
// that need both don't AB-BA against it.
if activeConfig != nil {
_ = s.saveConfig(activeConfig)
s.mu.Lock()
s.config = activeConfig
s.mu.Unlock()
if err := s.initFetcher(); err != nil {
log.Printf("[web] re-init fetcher after profile change: %v", err)
} else if req.SkipCheck {
s.skipCheckerUseSaved()
} else {
s.startCheckerThenRefresh()
}
}
writeJSON(w, map[string]any{"ok": true, "profiles": pl})
@@ -2976,8 +2987,6 @@ func (s *Server) persistLastScanToProfiles(resolvers []string) {
// a populated saved list was applied; false routes the caller to the
// last_scan.json / full-scan fallback chain.
func (s *Server) applySelectedList() bool {
s.profilesMu.Lock()
defer s.profilesMu.Unlock()
pl, err := s.loadProfiles()
if err != nil || pl == nil {
return false