diff options
| author | raven <citrons@mondecitronne.com> | 2026-02-10 17:22:58 -0600 |
|---|---|---|
| committer | raven <citrons@mondecitronne.com> | 2026-02-10 17:22:58 -0600 |
| commit | 8024722bd5db50bc3ec602b807819a87bd65035e (patch) | |
| tree | 0aa9585878c5f4a3fa8f16ec059314661aa1578d | |
| parent | 159773c277a4067f42037d1cbac31659de776382 (diff) | |
channel read status
| -rw-r--r-- | client/application.go | 9 | ||||
| -rw-r--r-- | client/channel_list.go | 17 | ||||
| -rw-r--r-- | client/channel_window.go | 9 | ||||
| -rw-r--r-- | server/channel/channel.go | 68 | ||||
| -rw-r--r-- | server/channel/command.go | 20 |
5 files changed, 119 insertions, 4 deletions
diff --git a/client/application.go b/client/application.go index 3b627b1..e1d0956 100644 --- a/client/application.go +++ b/client/application.go @@ -93,6 +93,15 @@ func (a *application) OnEvent(cmd proto.Command) { a.newChannel(*ch) } }) + case "unread": + switch cl := a.currentWindow.(type) { + case channelLocation: + if cl.id != cmd.Target { + a.channelList.setUnread(channelLocation {id: cmd.Target}, true) + } + } + case "read": + a.channelList.setUnread(channelLocation {id: cmd.Target}, false) } } diff --git a/client/channel_list.go b/client/channel_list.go index bc89dba..c202d39 100644 --- a/client/channel_list.go +++ b/client/channel_list.go @@ -13,6 +13,7 @@ type channelListEntry struct { name string location channelLocation clicked bool + unread bool } func (cl *channelList) Len() int { @@ -33,7 +34,8 @@ func (cl *channelList) setChannels(cs []proto.Object) { *cl = nil for _, c := range cs { *cl = append(*cl, channelListEntry { - c.Fields[""], channelLocation {id: c.Id}, false, + name: c.Fields[""], location: channelLocation {id: c.Id}, + unread: c.Fields["unread"] == "yes", }) } sort.Sort(cl) @@ -48,6 +50,14 @@ func (cl *channelList) contains(location channelLocation) bool { return false } +func (cl *channelList) setUnread(location channelLocation, unread bool) { + for i := 0; i < len(*cl); i++ { + if (*cl)[i].location == location { + (*cl)[i].unread = unread + } + } +} + func (cl *channelList) findName(name string) channelLocation { for i := 0; i < len(*cl); i++ { if validate.Fold((*cl)[i].name) == validate.Fold(name) { @@ -97,7 +107,7 @@ func (cl *channelList) traverse(direction int) int { func (cl *channelList) add(name string, location channelLocation) { cl.remove(location) - entry := channelListEntry {name, location, false} + entry := channelListEntry {name: name, location: location} *cl = append([]channelListEntry {entry}, *cl...) } @@ -107,12 +117,13 @@ func (cl *channelList) show(scroll *tui.ScrollState) { }) scroll.ByMouse(mouse, false) for i, entry := range *cl { - var style *tui.Style + var style *tui.Style = &tui.Style {Fg: tui.White, Bg: tui.Black} if entry.location == globalApp.currentWindow { style = &tui.Style {Fg: tui.Black, Bg: tui.White} } else if entry.clicked { style = &tui.Style {Fg: tui.Black, Bg: tui.BrightBlack} } + style.Bold = entry.unread mouse := tui.Push("channel list." + entry.location.id, tui.Box { Width: tui.Fill, Height: 1, NoWrap: true, Style: style, diff --git a/client/channel_window.go b/client/channel_window.go index 50b7165..f7832e1 100644 --- a/client/channel_window.go +++ b/client/channel_window.go @@ -95,6 +95,9 @@ func (cw *channelWindow) put(msg proto.Object) { } } cw.addMessage(msg, true) + if globalApp.currentWindow == cw.location { + cw.setRead() + } } func (cw *channelWindow) Location() window.Location { @@ -146,6 +149,11 @@ func (cw *channelWindow) Send(text string) { globalApp.goTo(channelLocation {id: cw.location.id}) } +func (cw *channelWindow) setRead() { + globalApp.Request(proto.NewCmd("read", cw.location.id), nil) + globalApp.channelList.setUnread(cw.location, false) +} + func (cw *channelWindow) replyTo(id string) { cw.replyingTo = id } @@ -393,6 +401,7 @@ func (cw *channelWindow) OnNavigate() { if cw.jumpedTo != nil { cw.Buf.ScrollTo(cw.jumpedTo.Id()) } + cw.setRead() } func (cw *channelWindow) goToMessage(id string) { diff --git a/server/channel/channel.go b/server/channel/channel.go index e2a18c5..d907bc0 100644 --- a/server/channel/channel.go +++ b/server/channel/channel.go @@ -179,6 +179,14 @@ func (c *Channel) Rename(name string) *proto.Fail { } func (c *Channel) Put(m proto.Object, From *session.Session) proto.Object { + for uid := range c.Members() { + switch u := c.kind.world.GetCachedObject(uid).(type) { + case (*user.User): + if len(u.PrivateStream.Subscribers()) != 0 && !c.Unread(uid) { + u.PrivateStream.Event(proto.NewCmd("unread", c.id)) + } + } + } m.Id = proto.GenId() m.Fields["t"] = proto.Timestamp() err := c.kind.db.Update(func(tx *bolt.Tx) error { @@ -283,6 +291,56 @@ func (c *Channel) History(min, max int) []proto.Object { return result } +func (c *Channel) Unread(uid string) bool { + lastIndex := c.HistorySize() - 1 + if (lastIndex < 0) { + return false + } + latest := c.History(lastIndex, lastIndex + 1)[0].Id + + unread := true + err := c.kind.db.View(func(tx *bolt.Tx) error { + udata := tx.Bucket([]byte("user data")) + if udata == nil { + return nil + } + user := udata.Bucket([]byte(uid)) + if user == nil { + return nil + } + readStatus := user.Bucket([]byte("read status")) + if readStatus == nil { + return nil + } + unread = string(readStatus.Get([]byte(c.id))) != latest + return nil + }) + if err != nil { + log.Fatal("error reading database: ", err) + } + + return unread +} + +func (c *Channel) SetRead(uid string) { + lastIndex := c.HistorySize() - 1 + if (lastIndex < 0) { + return + } + latest := c.History(lastIndex, lastIndex + 1)[0].Id + + err := c.kind.db.Update(func(tx *bolt.Tx) error { + udata, _ := tx.CreateBucketIfNotExists([]byte("user data")) + user, _ := udata.CreateBucketIfNotExists([]byte(uid)) + readStatus, _ := user.CreateBucketIfNotExists([]byte("read status")) + readStatus.Put([]byte(c.id), []byte(latest)) + return nil + }) + if err != nil { + log.Fatal("error updating database: ", err) + } +} + func (c *Channel) Join(u *user.User) *proto.Fail { if c.isDirect { return &proto.Fail {"invalid", "", nil} @@ -427,7 +485,15 @@ func (c *Channel) Kind() string { } func (c *Channel) InfoFor(uid string) proto.Object { - return proto.Object { + i := proto.Object { c.Kind(), c.id, map[string]string {"": c.NameFor(uid)}, } + if uid != "" { + if c.Unread(uid) { + i.Fields["unread"] = "yes" + } else { + i.Fields["unread"] = "no" + } + } + return i } diff --git a/server/channel/command.go b/server/channel/command.go index 92ae2cc..23acccb 100644 --- a/server/channel/command.go +++ b/server/channel/command.go @@ -260,6 +260,26 @@ func (c *Channel) SendRequest(r session.Request) { r.ReplyInvalid() } + case "read": + if len(r.Cmd.Args) != 0 { + r.ReplyInvalid() + return + } + if !c.GetMembership(r.From.UserId).See { + r.Reply(proto.Fail{"forbidden", "", nil}.Cmd()) + return + } + c.SetRead(r.From.UserId) + + u := c.kind.world.GetObject(r.From.UserId).(*user.User) + for s := range u.PrivateStream.Subscribers { + if s != r.From { + s.Event(proto.NewCmd("read", c.id)) + } + } + + r.ReplyOk() + default: r.ReplyInvalid() } |
