diff --git a/go/characters/cwtchlistener.go b/go/characters/cwtchlistener.go index e246f459..d6abc36b 100644 --- a/go/characters/cwtchlistener.go +++ b/go/characters/cwtchlistener.go @@ -29,8 +29,9 @@ func CwtchListener(callback func(message *gobjects.Message), groupID string, cha m.Message, "", m.PeerID == the.Peer.GetProfile().Onion, - 0, + "0", m.Timestamp, + false, }) } } diff --git a/go/characters/incominglistener.go b/go/characters/incominglistener.go index 4ca73711..76532ec8 100644 --- a/go/characters/incominglistener.go +++ b/go/characters/incominglistener.go @@ -41,12 +41,14 @@ func IncomingListener(callback func(*gobjects.Message)) { case event.NewMessageFromGroup://event.TimestampReceived, event.TimestampSent, event.Data, event.GroupID, event.RemotePeer ts, _ := time.Parse(time.RFC3339Nano, e.Data[event.TimestampSent]) callback(&gobjects.Message{ + MessageID: e.Data[event.Signature], Handle: e.Data[event.GroupID], From: e.Data[event.RemotePeer], Message: e.Data[event.Data], Image: cwutil.RandomProfileImage(e.Data[event.RemotePeer]), FromMe: e.Data[event.RemotePeer] == the.Peer.GetProfile().Onion, Timestamp: ts, + Acknowledged: true, }) case event.NewGroupInvite: log.Debugf("got a group invite!") diff --git a/go/gobjects/letter.go b/go/gobjects/letter.go index aae65e73..fc35c26f 100644 --- a/go/gobjects/letter.go +++ b/go/gobjects/letter.go @@ -3,5 +3,5 @@ package gobjects // a Letter is a very simple message object passed to us from the UI type Letter struct { To, Message string - MID uint + MID string } diff --git a/go/gobjects/message.go b/go/gobjects/message.go index 8d4d7799..f7321f6e 100644 --- a/go/gobjects/message.go +++ b/go/gobjects/message.go @@ -9,6 +9,7 @@ type Message struct { Message string Image string FromMe bool - MessageID int + MessageID string Timestamp time.Time + Acknowledged bool } diff --git a/go/gothings/gcd.go b/go/gothings/gcd.go index 10c6de3a..e02bfe36 100644 --- a/go/gothings/gcd.go +++ b/go/gothings/gcd.go @@ -5,13 +5,11 @@ import ( "cwtch.im/ui/go/constants" "cwtch.im/ui/go/cwutil" - "cwtch.im/cwtch/model" - "cwtch.im/ui/go/characters" "cwtch.im/ui/go/gobjects" "cwtch.im/ui/go/the" "encoding/base32" - "github.com/therecipe/qt/core" "git.openprivacy.ca/openprivacy/libricochet-go/log" + "github.com/therecipe/qt/core" "strings" "time" ) @@ -31,10 +29,10 @@ type GrandCentralDispatcher struct { _ func(handle, key, value string) `signal:"UpdateContactAttribute"` // messages pane stuff - _ func(handle, from, displayName, message, image string, mID uint, fromMe bool, ts string) `signal:"AppendMessage"` + _ func(handle, from, displayName, message, image string, mID string, fromMe bool, ts string, ackd bool) `signal:"AppendMessage"` _ func() `signal:"ClearMessages"` _ func() `signal:"ResetMessagePane"` - _ func(mID uint) `signal:"Acknowledged"` + _ func(mID string) `signal:"Acknowledged"` _ func(title string) `signal:"SetToolbarTitle"` // profile-area stuff @@ -47,7 +45,7 @@ type GrandCentralDispatcher struct { _ func(onion, nick string) `signal:"SupplyPeerSettings"` // signals emitted from the ui (written in go, below) - _ func(message string, mid uint) `signal:"sendMessage,auto"` + _ func(message string, mid string) `signal:"sendMessage,auto"` _ func(onion string) `signal:"loadMessagesPane,auto"` _ func(signal string) `signal:"broadcast,auto"` // convenience relay signal _ func(str string) `signal:"importString,auto"` @@ -64,7 +62,7 @@ type GrandCentralDispatcher struct { _ func(onion, key, nick string) `signal:"setAttribute,auto"` } -func (this *GrandCentralDispatcher) sendMessage(message string, mID uint) { +func (this *GrandCentralDispatcher) sendMessage(message string, mID string) { if len(message) > 65530 { this.InvokePopup("message is too long") return @@ -87,20 +85,25 @@ func (this *GrandCentralDispatcher) sendMessage(message string, mID uint) { this.UIState.UpdateContact(c.Handle) } - the.Peer.SendMessageToGroup(this.CurrentOpenConversation(), message) - return - } + var err error + mID,err = the.Peer.SendMessageToGroupTracked(this.CurrentOpenConversation(), message) + if err != nil { + this.InvokePopup("failed to send message " +err.Error()) + return + } + } else { - // TODO: require explicit invite accept/reject instead of implicitly trusting on send - if !this.UIState.GetContact(this.CurrentOpenConversation()).Trusted { - this.UIState.GetContact(this.CurrentOpenConversation()).Trusted = true - this.UIState.UpdateContact(this.CurrentOpenConversation()) - } + // TODO: require explicit invite accept/reject instead of implicitly trusting on send + if !this.UIState.GetContact(this.CurrentOpenConversation()).Trusted { + this.UIState.GetContact(this.CurrentOpenConversation()).Trusted = true + this.UIState.UpdateContact(this.CurrentOpenConversation()) + } - select { // 1 weird trick to do a non-blocking send. this means the user can only send a limited number of messages - // before the channel buffer fills. TODO: stop the user from sending if the buffer is full - case this.OutgoingMessages <- gobjects.Letter{this.CurrentOpenConversation(), message, mID}: - default: + select { // 1 weird trick to do a non-blocking send. this means the user can only send a limited number of messages + // before the channel buffer fills. TODO: stop the user from sending if the buffer is full + case this.OutgoingMessages <- gobjects.Letter{this.CurrentOpenConversation(), message, mID}: + default: + } } this.UIState.AddMessage(&gobjects.Message{ @@ -110,8 +113,9 @@ func (this *GrandCentralDispatcher) sendMessage(message string, mID uint) { message, "", true, - int(mID), + mID, time.Now(), + false, }) } @@ -174,9 +178,10 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { name, tl[i].Message, cwutil.RandomProfileImage(tl[i].PeerID), - 0, + string(tl[i].Signature), tl[i].PeerID == the.Peer.GetProfile().Onion, tl[i].Timestamp.Format(constants.TIME_FORMAT), + tl[i].Received.Equal(time.Unix(0,0)) == false, // If the received timestamp is epoch, we have not yet received this message through an active server ) } return @@ -204,12 +209,13 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { messages[i].DisplayName, messages[i].Message, cwutil.RandomProfileImage(handle), - uint(messages[i].MessageID), + messages[i].MessageID, messages[i].FromMe, messages[i].Timestamp.Format(constants.TIME_FORMAT), + true, ) for _,id := range the.AcknowledgementIDs[messages[i].Handle] { - if int(id.ID) == messages[i].MessageID && id.Ack{ + if id.ID == messages[i].MessageID && id.Ack{ this.Acknowledged(id.ID) } } @@ -412,8 +418,6 @@ func (this *GrandCentralDispatcher) createGroup(server, groupName string) { })) the.Peer.JoinServer(server) - group.NewMessage = make(chan model.Message) - go characters.CwtchListener(this.UIState.AddMessage, group.GroupID, group.NewMessage) } func (this *GrandCentralDispatcher) inviteToGroup(onion, groupID string) { diff --git a/go/gothings/uistate.go b/go/gothings/uistate.go index 4405d6a6..6442ff18 100644 --- a/go/gothings/uistate.go +++ b/go/gothings/uistate.go @@ -94,6 +94,11 @@ func (this *InterfaceState) AddMessage(m *gobjects.Message) { this.messages[m.Handle] = make([]*gobjects.Message, 0) } + // Ack message sent to group + this.parentGcd.Acknowledged(m.MessageID) + + // Ack personal messages + // TODO: unify this with the above signature based approach if m.Message == "ack" { // If an ack, swallow the message and ack from the list. acklist := the.AcknowledgementIDs[m.From] @@ -104,8 +109,14 @@ func (this *InterfaceState) AddMessage(m *gobjects.Message) { } else { this.messages[m.Handle] = append(this.messages[m.Handle], m) + // If we have this group loaded already if this.parentGcd.CurrentOpenConversation() == m.Handle { - this.parentGcd.AppendMessage(m.Handle, m.From, m.DisplayName, m.Message, m.Image, uint(m.MessageID), m.FromMe, m.Timestamp.Format(constants.TIME_FORMAT)) + // If the message is not from the user then add it, otherwise, just acknowledge. + if !m.FromMe || !m.Acknowledged { + this.parentGcd.AppendMessage(m.Handle, m.From, m.DisplayName, m.Message, m.Image, m.MessageID, m.FromMe, m.Timestamp.Format(constants.TIME_FORMAT), m.Acknowledged) + } else { + this.parentGcd.Acknowledged(m.MessageID) + } } else { c := this.GetContact(m.Handle) if c != nil { diff --git a/go/the/globals.go b/go/the/globals.go index afaef4cf..f55547f1 100644 --- a/go/the/globals.go +++ b/go/the/globals.go @@ -10,7 +10,7 @@ var Peer libPeer.CwtchPeer var CwtchDir string type AckId struct { - ID uint + ID string Ack bool } diff --git a/qml/overlays/ChatOverlay.qml b/qml/overlays/ChatOverlay.qml index b4b980a8..ffc9f769 100644 --- a/qml/overlays/ChatOverlay.qml +++ b/qml/overlays/ChatOverlay.qml @@ -32,7 +32,7 @@ ColumnLayout { txtMessage.text = "" } - onAppendMessage: function(handle, from, displayName, message, image, mid, fromMe, ts) { + onAppendMessage: function(handle, from, displayName, message, image, mid, fromMe, ts, ackd) { var msg try { msg = JSON.parse(message) @@ -41,6 +41,7 @@ ColumnLayout { } if (msg.o != 1) return + messagesModel.append({ "_handle": handle, "_from": from, @@ -50,8 +51,11 @@ ColumnLayout { "_mid": mid, "_fromMe": fromMe, "_ts": ts, + "_ackd": ackd, }) + + // If the window is out of focus, alert the user (makes taskbar light up) windowItem.alert(0) @@ -88,6 +92,7 @@ ColumnLayout { messageID: _mid fromMe: _fromMe timestamp: _ts + ackd: _ackd } } } diff --git a/qml/widgets/Message.qml b/qml/widgets/Message.qml index 1309dfe7..153a001e 100644 --- a/qml/widgets/Message.qml +++ b/qml/widgets/Message.qml @@ -17,8 +17,9 @@ RowLayout { property string from property string handle property string displayName - property int messageID + property string messageID property bool fromMe + property bool ackd property alias timestamp: ts.text property alias image: imgProfile.source property alias status: imgProfile.status @@ -28,7 +29,7 @@ RowLayout { onAcknowledged: function(mid) { if (mid == messageID) { - ack.source = "qrc:/qml/images/fontawesome/regular/check-circle.svg" + root.ackd = true } } @@ -129,7 +130,7 @@ RowLayout { Image { // ACKNOWLEDGEMENT ICON id: ack anchors.right: parent.right - source: from == "me" ? "qrc:/qml/images/fontawesome/regular/hourglass.svg" : "qrc:/qml/images/fontawesome/regular/check-circle.svg" + source: root.ackd ? "qrc:/qml/images/fontawesome/regular/check-circle.svg" : "qrc:/qml/images/fontawesome/regular/hourglass.svg" height: 10 sourceSize.height: 10 visible: fromMe