diff --git a/brclient/appstate.go b/brclient/appstate.go index 507e34e2..2547247d 100644 --- a/brclient/appstate.go +++ b/brclient/appstate.go @@ -146,12 +146,13 @@ type appState struct { canPayServer bool canPayServerTestTime time.Time - postsMtx sync.Mutex - posts []clientdb.PostSummary - post *rpc.PostMetadata - postSumm clientdb.PostSummary - myComments []string // Unreplicated comments - postStatus []rpc.PostMetadataStatus + postsMtx sync.Mutex + posts []clientdb.PostSummary + post *rpc.PostMetadata + postSumm clientdb.PostSummary + myComments []string // Unreplicated comments + postStatus []rpc.PostMetadataStatus + unreadPosts map[clientintf.PostID]struct{} contentMtx sync.Mutex remoteFiles map[clientintf.UserID]map[clientdb.FileID]clientdb.RemoteFile @@ -233,6 +234,12 @@ type appStateErr struct { func (as *appState) run() error { as.wg.Add(1) var err error + + go func() { + // Initial loading of posts. + as.loadPosts() + }() + go func() { as.log.Infof("Starting %s version %s", appName, version.String()) err = as.c.Run(as.ctx) @@ -1867,7 +1874,6 @@ func (as *appState) requestRecv(amount, server, key string, caCert []byte) error } func (as *appState) createPost(post string) { - as.loadPosts() summ, err := as.c.CreatePost(post, "") if err != nil { as.cwHelpMsg("Unable to create post: %v", err) @@ -1875,6 +1881,7 @@ func (as *appState) createPost(post string) { as.cwHelpMsg("Created post %s", summ.ID) as.postsMtx.Lock() as.posts = append(as.posts, summ) + as.sortPosts() as.postsMtx.Unlock() as.sendMsg(summ) } @@ -1887,15 +1894,17 @@ func (as *appState) loadPosts() { } else { as.postsMtx.Lock() as.posts = posts + as.sortPosts() as.postsMtx.Unlock() } } -func (as *appState) allPosts() []clientdb.PostSummary { +func (as *appState) allPosts() ([]clientdb.PostSummary, map[clientintf.PostID]struct{}) { as.postsMtx.Lock() res := as.posts + m := maps.Clone(as.unreadPosts) as.postsMtx.Unlock() - return res + return res, m } func (as *appState) activePost() (rpc.PostMetadata, clientdb.PostSummary, @@ -1905,6 +1914,7 @@ func (as *appState) activePost() (rpc.PostMetadata, clientdb.PostSummary, as.postsMtx.Lock() if as.post != nil { res = *as.post + delete(as.unreadPosts, as.postSumm.ID) } summ := as.postSumm status := as.postStatus @@ -1929,6 +1939,7 @@ func (as *appState) activatePost(summ *clientdb.PostSummary) { as.post = &post as.postSumm = *summ as.postStatus = postStatus + delete(as.unreadPosts, summ.ID) as.postsMtx.Unlock() } @@ -1937,6 +1948,7 @@ func (as *appState) commentPost(from clientintf.UserID, pid clientintf.PostID, as.postsMtx.Lock() as.myComments = append(as.myComments, comment) + as.sortPosts() as.postsMtx.Unlock() as.sendMsg(sentPostComment{}) @@ -1946,6 +1958,22 @@ func (as *appState) commentPost(from clientintf.UserID, pid clientintf.PostID, } } +// sortPosts sorts the posts in as.posts. MUST be called with the postsMtx +// locked. +func (as *appState) sortPosts() { + sort.Slice(as.posts, func(i, j int) bool { + idt := as.posts[i].LastStatusTS + if idt.IsZero() { + idt = as.posts[i].Date + } + jdt := as.posts[j].LastStatusTS + if jdt.IsZero() { + jdt = as.posts[j].Date + } + return jdt.Before(idt) + }) +} + func (as *appState) getUserPost(cw *chatWindow, pid clientintf.PostID) { cw.newInternalMsg(fmt.Sprintf("Fetching post %s", pid)) as.repaintIfActive(cw) @@ -2573,7 +2601,11 @@ func newAppState(sendMsg func(tea.Msg), lndLogLines *sloglinesbuffer.Buffer, summ.Title, summ.ID, nick) // Store new post. - as.loadPosts() + as.postsMtx.Lock() + as.posts = append(as.posts, summ) + as.sortPosts() + as.unreadPosts[summ.ID] = struct{}{} + as.postsMtx.Unlock() // Signal updated feed window. as.chatWindowsMtx.Lock() @@ -2627,6 +2659,9 @@ func newAppState(sendMsg func(tea.Msg), lndLogLines *sloglinesbuffer.Buffer, as.postStatus = append(as.postStatus, status) } + as.unreadPosts[pid] = struct{}{} + as.sortPosts() + as.postsMtx.Unlock() if statusFrom != as.c.PublicID() { @@ -3552,6 +3587,8 @@ func newAppState(sendMsg func(tea.Msg), lndLogLines *sloglinesbuffer.Buffer, collator: collate.New(language.Und), + unreadPosts: make(map[clientintf.PostID]struct{}), + missingKXUsers: make(map[client.UserID]time.Time), inboundMsgs: &genericlist.List[inboundRemoteMsg]{}, diff --git a/brclient/commands.go b/brclient/commands.go index b8924b53..8bbd6668 100644 --- a/brclient/commands.go +++ b/brclient/commands.go @@ -3379,6 +3379,22 @@ var commands = []tuicmd{ return nil }, handler: subcmdNeededHandler, + }, { + cmd: "testcolor", + usableOffline: true, + descr: "Test a color spec", + usage: "::", + handler: func(args []string, as *appState) error { + if len(args) < 1 { + return usageError{msg: "color :: must be specified"} + } + style, err := colorDefnToLGStyle(args[0]) + if err != nil { + return err + } + as.diagMsg(style.Render("On sangen hauskaa, että polkupyörä on maanteiden jokapäiväinen ilmiö.")) + return nil + }, }, { cmd: "quit", usableOffline: true, diff --git a/brclient/feedwin.go b/brclient/feedwin.go index 6b988540..68498286 100644 --- a/brclient/feedwin.go +++ b/brclient/feedwin.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "sort" "strings" "github.com/charmbracelet/bubbles/viewport" @@ -16,9 +15,10 @@ import ( // feedWindow tracks what needs to be initialized before the app can // properly start. type feedWindow struct { - as *appState - posts []clientdb.PostSummary - idx int + as *appState + posts []clientdb.PostSummary + unread map[clientintf.PostID]struct{} + idx int viewport viewport.Model } @@ -56,6 +56,8 @@ func (fw *feedWindow) renderPostSumm(post clientdb.PostSummary, if fw.idx == i { b.WriteString(st.focused.Render(pf("%s %s by %s", date, title, author))) + } else if _, ok := fw.unread[post.ID]; ok { + b.WriteString(st.unreadPost.Render(pf("%s %s by %s", date, title, author))) } else { b.WriteString(st.timestamp.Render(date)) b.WriteString(" ") @@ -83,13 +85,17 @@ func (fw *feedWindow) renderPostSumm(post clientdb.PostSummary, } func (fw *feedWindow) listPosts() { - fw.posts = fw.as.allPosts() - sort.Slice(fw.posts, func(i, j int) bool { - return fw.posts[i].Date.Sub(fw.posts[j].Date) < 0 - }) - fw.idx = len(fw.posts) - 1 - if fw.idx < 0 { - fw.idx = 0 + fw.posts, fw.unread = fw.as.allPosts() + _, summ, _, _ := fw.as.activePost() + if fw.idx == -1 { + idx := 0 + for i := range fw.posts { + if fw.posts[i].ID == summ.ID { + idx = i + break + } + } + fw.idx = idx } } @@ -213,7 +219,7 @@ func (fw feedWindow) View() string { func newFeedWindow(as *appState, feedActiveIdx, yOffsetHint int) (feedWindow, tea.Cmd) { as.loadPosts() as.markWindowSeen(activeCWFeed) - fw := feedWindow{as: as} + fw := feedWindow{as: as, idx: -1} fw.listPosts() if feedActiveIdx > -1 && feedActiveIdx < len(fw.posts) { fw.idx = feedActiveIdx diff --git a/brclient/theme.go b/brclient/theme.go index d3c9f282..1bdbad3f 100644 --- a/brclient/theme.go +++ b/brclient/theme.go @@ -23,6 +23,7 @@ type theme struct { nick lipgloss.Style nickMe lipgloss.Style nickGC lipgloss.Style + unreadPost lipgloss.Style msg lipgloss.Style unsent lipgloss.Style online lipgloss.Style @@ -37,6 +38,10 @@ type theme struct { func textToColor(in string) (lipgloss.Color, error) { var c lipgloss.Color + if len(in) > 0 && in[0] == '#' { + return lipgloss.Color(in), nil + } + switch strings.ToLower(in) { case "na": case "black": @@ -80,6 +85,7 @@ func colorDefnToLGStyle(color string) (lipgloss.Style, error) { style = style.Underline(true) case "reverse": style = style.Reverse(true) + case "", "na": default: return style, fmt.Errorf("invalid attribute: %v", k) } @@ -87,15 +93,16 @@ func colorDefnToLGStyle(color string) (lipgloss.Style, error) { fg, err := textToColor(s[1]) if err != nil { - return style, err + // return style, err + return style, fmt.Errorf("invalid foreground color: %v", err) } - style.Foreground(fg) + style = style.Foreground(fg) bg, err := textToColor(s[2]) if err != nil { - return style, err + return style, fmt.Errorf("invalid background color: %v", err) } - style.Background(bg) + style = style.Background(bg) return style, nil } @@ -158,6 +165,8 @@ func newTheme(args *config) (*theme, error) { timestampHelp: lipgloss.NewStyle(). Bold(false). Foreground(lipgloss.Color("#6b6b6b")), + unreadPost: lipgloss.NewStyle(). + Foreground(lipgloss.Color("6")), /* nick: lipgloss.NewStyle().