summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--client/application.go9
-rw-r--r--client/channel_list.go17
-rw-r--r--client/channel_window.go9
-rw-r--r--server/channel/channel.go68
-rw-r--r--server/channel/command.go20
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()
}