diff --git a/go/characters/appEventListener.go b/go/characters/appEventListener.go index 8685ad97..2ea4fec7 100644 --- a/go/characters/appEventListener.go +++ b/go/characters/appEventListener.go @@ -87,7 +87,7 @@ func AppEventListener(gcd *gothings.GrandCentralDispatcher, subscribed chan bool contacts := the.Peer.GetContacts() for i := range contacts { contact, _ := the.Peer.GetProfile().GetContact(contacts[i]) - displayName, _ := contact.GetAttribute("nick") + displayName, _ := contact.GetAttribute(constants.Nick) gcd.UIState.AddContact(&gobjects.Contact{ Handle: contacts[i], @@ -102,10 +102,11 @@ func AppEventListener(gcd *gothings.GrandCentralDispatcher, subscribed chan bool groups := the.Peer.GetGroups() for i := range groups { group := the.Peer.GetGroup(groups[i]) - nick, exists := group.GetAttribute("nick") + nick, exists := group.GetAttribute(constants.Nick) if !exists { nick = group.GroupID[:12] } + // Only join servers for active and explicitly accepted groups. gcd.UIState.AddContact(&gobjects.Contact{ Handle: group.GroupID, diff --git a/go/characters/incominglistener.go b/go/characters/incominglistener.go index 55ada538..0a639075 100644 --- a/go/characters/incominglistener.go +++ b/go/characters/incominglistener.go @@ -3,6 +3,7 @@ package characters import ( "cwtch.im/cwtch/event" "cwtch.im/cwtch/protocol/connections" + "cwtch.im/ui/go/constants" "cwtch.im/ui/go/cwutil" "cwtch.im/ui/go/gobjects" "cwtch.im/ui/go/gothings" @@ -48,7 +49,7 @@ func IncomingListener(uiState *gothings.InterfaceState, subscribed chan bool) { var exists bool ctc := the.Peer.GetContact(e.Data[event.RemotePeer]) if ctc != nil { - name, exists = ctc.GetAttribute("nick") + name, exists = ctc.GetAttribute(constants.Nick) if !exists || name == "" { name = e.Data[event.RemotePeer] } @@ -114,7 +115,6 @@ func IncomingListener(uiState *gothings.InterfaceState, subscribed chan bool) { e.Data[event.RemotePeer], cwutil.RandomProfileImage(e.Data[event.RemotePeer]), "", - 0, int(cxnState), false, false, diff --git a/go/gobjects/contact.go b/go/gobjects/contact.go index 933ca39b..75807d56 100644 --- a/go/gobjects/contact.go +++ b/go/gobjects/contact.go @@ -5,7 +5,6 @@ type Contact struct { DisplayName string Image string Server string - Badge int // # of unread Status int Trusted bool Blocked bool diff --git a/go/gothings/gcd.go b/go/gothings/gcd.go index a06bfc56..22802c1d 100644 --- a/go/gothings/gcd.go +++ b/go/gothings/gcd.go @@ -31,7 +31,8 @@ type GrandCentralDispatcher struct { // contact list stuff _ func(handle, displayName, image, server string, badge, status int, trusted bool, blocked bool, loading bool) `signal:"AddContact"` - _ func(handle, displayName, image, server string, badge, status int, trusted bool, blocked bool, loading bool) `signal:"UpdateContact"` + _ func(handle, displayName, image, server string, status int, trusted bool, blocked bool, loading bool) `signal:"UpdateContact"` + _ func(handle string) `signal:"IncContactUnreadCount"` _ func(handle string) `signal:"RemoveContact"` _ func(handle, key, value string) `signal:"UpdateContactAttribute"` @@ -171,20 +172,18 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { cwutil.RandomProfileImage(handle), "", 0, - 0, false, false, false, }) } else { - c.Badge = 0 this.UIState.UpdateContact(handle) } if len(handle) == 32 { // LOAD GROUP group := the.Peer.GetGroup(handle) tl := group.GetTimeline() - nick, _ := group.GetAttribute("nick") + nick, _ := group.GetAttribute(constants.Nick) if nick == "" { this.SetToolbarTitle(handle) } else { @@ -201,7 +200,7 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { var exists bool ctc := the.Peer.GetContact(tl[i].PeerID) if ctc != nil { - name, exists = ctc.GetAttribute("nick") + name, exists = ctc.GetAttribute(constants.Nick) if !exists || name == "" { name = tl[i].PeerID } @@ -228,7 +227,7 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { contact, _ := the.Peer.GetProfile().GetContact(handle) var nick string if contact != nil { - nick, _ = contact.GetAttribute("nick") + nick, _ = contact.GetAttribute(constants.Nick) if nick == "" { nick = handle } @@ -248,7 +247,7 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { ctc := the.Peer.GetContact(messages[i].PeerID) if ctc != nil { var exists bool - displayname, exists = ctc.GetAttribute("nick") + displayname, exists = ctc.GetAttribute(constants.Nick) if !exists || displayname == "" { displayname = messages[i].PeerID } @@ -300,7 +299,7 @@ func (this *GrandCentralDispatcher) requestPeerSettings() { return } - name, exists := contact.GetAttribute("nick") + name, exists := contact.GetAttribute(constants.Nick) if !exists { log.Errorf("error: couldn't find contact %v", this.CurrentOpenConversation()) this.SupplyPeerSettings(this.CurrentOpenConversation(), this.CurrentOpenConversation(), contact.Blocked) @@ -317,10 +316,10 @@ func (this *GrandCentralDispatcher) savePeerSettings(onion, nick string) { return } - contact.SetAttribute("nick", nick) + contact.SetAttribute(constants.Nick, nick) the.EventBus.Publish(event.NewEvent(event.SetPeerAttribute, map[event.Field]string{ event.RemotePeer: onion, - event.Key: "nick", + event.Key: constants.Nick, event.Data: nick, })) @@ -338,13 +337,13 @@ func (this *GrandCentralDispatcher) requestGroupSettings(groupID string) { return } - nick, _ := group.GetAttribute("nick") + nick, _ := group.GetAttribute(constants.Nick) invite, _ := the.Peer.ExportGroup(groupID) contactaddrs := the.Peer.GetContacts() contactnames := make([]string, len(contactaddrs)) for i, contact := range contactaddrs { - name, hasname := the.Peer.GetContact(contact).GetAttribute("nick") + name, hasname := the.Peer.GetContact(contact).GetAttribute(constants.Nick) if hasname { contactnames[i] = name } else { @@ -363,10 +362,10 @@ func (this *GrandCentralDispatcher) saveGroupSettings(groupID, nick string) { return } - group.SetAttribute("nick", nick) + group.SetAttribute(constants.Nick, nick) the.EventBus.Publish(event.NewEvent(event.SetGroupAttribute, map[event.Field]string{ event.GroupID: groupID, - event.Key: "nick", + event.Key: constants.Nick, event.Data: nick, })) @@ -480,10 +479,10 @@ func (this *GrandCentralDispatcher) createGroup(server, groupName string) { }) group := the.Peer.GetGroup(groupID) - group.SetAttribute("nick", groupName) + group.SetAttribute(constants.Nick, groupName) the.EventBus.Publish(event.NewEvent(event.SetGroupAttribute, map[event.Field]string{ event.GroupID: groupID, - event.Key: "nick", + event.Key: constants.Nick, event.Data: groupName, })) diff --git a/go/gothings/uistate.go b/go/gothings/uistate.go index 30d284a0..2bac7a83 100644 --- a/go/gothings/uistate.go +++ b/go/gothings/uistate.go @@ -1,6 +1,7 @@ package gothings import ( + "cwtch.im/cwtch/model" "cwtch.im/ui/go/constants" "cwtch.im/ui/go/cwutil" "cwtch.im/ui/go/gobjects" @@ -11,6 +12,39 @@ import ( "time" ) +type Attributable interface { + GetAttribute(string) (string, bool) + SetAttribute(string, string) +} + +// initLastReadTime checks and gets the Attributable's LastRead time or sets it to now +func initLastReadTime(attr Attributable) time.Time { + lastRead := time.Now() + lastReadVal, ok := attr.GetAttribute(constants.LastRead) + if ok { + err := lastRead.UnmarshalText([]byte(lastReadVal)) + if err != nil { + lastRead = time.Now() + } + } else { + lastReadVal, _ := lastRead.MarshalText() + attr.SetAttribute(constants.LastRead, string(lastReadVal)) + } + return lastRead +} + +func countUnread(messages []model.Message, lastRead time.Time) int { + count := 0 + for i := len(messages) - 1; i >= 0; i-- { + if messages[i].Timestamp.After(lastRead) { + count++ + } else { + break + } + } + return count +} + type InterfaceState struct { parentGcd *GrandCentralDispatcher contacts sync.Map // string : *gobjects.Contact @@ -27,8 +61,14 @@ func (this *InterfaceState) Acknowledge(mID string) { func (this *InterfaceState) AddContact(c *gobjects.Contact) { if len(c.Handle) == 32 { // ADD GROUP + unread := 0 + group := the.Peer.GetGroup(c.Handle) + if group != nil { + lastRead := initLastReadTime(group) + unread = countUnread(group.Timeline.GetMessages(), lastRead) + } if _, found := this.contacts.Load(c.Handle); !found { - this.parentGcd.AddContact(c.Handle, c.DisplayName, c.Image, c.Server, c.Badge, c.Status, c.Trusted, c.Blocked, c.Loading) + this.parentGcd.AddContact(c.Handle, c.DisplayName, c.Image, c.Server, unread, c.Status, c.Trusted, c.Blocked, c.Loading) this.contacts.Store(c.Handle, c) } return @@ -38,9 +78,15 @@ func (this *InterfaceState) AddContact(c *gobjects.Contact) { return } + unread := 0 + contact := the.Peer.GetContact(c.Handle) + if contact != nil { + lastRead := initLastReadTime(contact) + unread = countUnread(contact.Timeline.GetMessages(), lastRead) + } if _, found := this.contacts.Load(c.Handle); !found { this.contacts.Store(c.Handle, c) - this.parentGcd.AddContact(c.Handle, c.DisplayName, c.Image, c.Server, c.Badge, c.Status, c.Trusted, c.Blocked, false) + this.parentGcd.AddContact(c.Handle, c.DisplayName, c.Image, c.Server, unread, c.Status, c.Trusted, c.Blocked, false) if the.Peer.GetContact(c.Handle) == nil { the.Peer.AddContact(c.DisplayName, c.Handle, c.Trusted) go the.Peer.PeerWithOnion(c.Handle) @@ -57,7 +103,7 @@ func (this *InterfaceState) GetContact(handle string) *gobjects.Contact { if len(handle) == 32 { group := the.Peer.GetGroup(handle) if group != nil { - nick, exists := group.GetAttribute("nick") + nick, exists := group.GetAttribute(constants.Nick) if !exists { nick = group.GroupID[:12] } @@ -67,7 +113,6 @@ func (this *InterfaceState) GetContact(handle string) *gobjects.Contact { cwutil.RandomGroupImage(handle), group.GroupServer, 0, - 0, group.Accepted, false, false, @@ -88,7 +133,6 @@ func (this *InterfaceState) GetContact(handle string) *gobjects.Contact { cwutil.RandomProfileImage(handle), "", 0, - 0, false, contact.Blocked, false, @@ -126,11 +170,7 @@ func (this *InterfaceState) AddMessage(m *gobjects.Message) { this.parentGcd.Acknowledged(m.MessageID) } } else { - c := this.GetContact(m.Handle) - if c != nil { - c.Badge++ - this.UpdateContact(c.Handle) - } + this.parentGcd.IncContactUnreadCount(m.Handle) } } @@ -142,7 +182,7 @@ func (this *InterfaceState) UpdateContact(handle string) { if contact != nil { c.Blocked = contact.Blocked } - this.parentGcd.UpdateContact(c.Handle, c.DisplayName, c.Image, c.Server, c.Badge, c.Status, c.Trusted, c.Blocked, c.Loading) + this.parentGcd.UpdateContact(c.Handle, c.DisplayName, c.Image, c.Server, c.Status, c.Trusted, c.Blocked, c.Loading) } } diff --git a/qml/overlays/BulletinOverlay.qml b/qml/overlays/BulletinOverlay.qml index 1e1f3cd8..c8abe9a6 100644 --- a/qml/overlays/BulletinOverlay.qml +++ b/qml/overlays/BulletinOverlay.qml @@ -92,7 +92,7 @@ ColumnLayout { } } - onUpdateContact: function(_handle, _displayName, _image, _server, _badge, _status, _trusted, _blocked, _loading) { + onUpdateContact: function(_handle, _displayName, _image, _server, _status, _trusted, _blocked, _loading) { if (gcd.currentOpenConversation == _handle) { if (_loading == true) { newposttitle.enabled = false diff --git a/qml/overlays/ChatOverlay.qml b/qml/overlays/ChatOverlay.qml index 061e14b5..59dfefa4 100644 --- a/qml/overlays/ChatOverlay.qml +++ b/qml/overlays/ChatOverlay.qml @@ -110,7 +110,7 @@ ColumnLayout { messagesListView.positionViewAtEnd() } - onUpdateContact: function(_handle, _displayName, _image, _server, _badge, _status, _trusted, _blocked, _loading) { + onUpdateContact: function(_handle, _displayName, _image, _server, _status, _trusted, _blocked, _loading) { if (gcd.currentOpenConversation == _handle) { // Group is Synced OR p2p is Authenticated if ( (_handle.length == 32 && _status == 4) || _status == 3) { diff --git a/qml/overlays/ListOverlay.qml b/qml/overlays/ListOverlay.qml index 5304ac28..1515a4f1 100644 --- a/qml/overlays/ListOverlay.qml +++ b/qml/overlays/ListOverlay.qml @@ -96,7 +96,7 @@ ColumnLayout { } } - onUpdateContact: function(_handle, _displayName, _image, _server, _badge, _status, _trusted, _blocked, _loading) { + onUpdateContact: function(_handle, _displayName, _image, _server, _status, _trusted, _blocked, _loading) { if (gcd.currentOpenConversation == _handle) { if (_loading == true) { newposttitle.enabled = false diff --git a/qml/widgets/ContactRow.qml b/qml/widgets/ContactRow.qml index 3b4f2661..a3f3f9e9 100644 --- a/qml/widgets/ContactRow.qml +++ b/qml/widgets/ContactRow.qml @@ -123,6 +123,7 @@ Item { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY isActive = true theStack.pane = theStack.messagePane gcd.loadMessagesPane(handle) + badge = 0 if (handle.length == 32) { gcd.requestGroupSettings(handle) } @@ -145,17 +146,22 @@ Item { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY isActive = false } - onUpdateContact: function(_handle, _displayName, _image, _server, _badge, _status, _trusted, _blocked, _loading) { + onUpdateContact: function(_handle, _displayName, _image, _server, _status, _trusted, _blocked, _loading) { if (handle == _handle) { displayName = _displayName + (_blocked == true ? " (blocked)" : "") image = _image server = _server - badge = _badge status = _status trusted = _trusted blocked = _blocked loadingProgress.visible = loadingProgress.running = loading = _loading } } + + onIncContactUnreadCount: function(handle) { + if (handle == _handle) { + badge++ + } + } } }