diff --git a/go/handlers/peerHandler.go b/go/handlers/peerHandler.go index 4210616c..84038e77 100644 --- a/go/handlers/peerHandler.go +++ b/go/handlers/peerHandler.go @@ -10,6 +10,7 @@ import ( "cwtch.im/ui/go/constants" "cwtch.im/ui/go/the" "cwtch.im/ui/go/ui" + "encoding/hex" "git.openprivacy.ca/openprivacy/log" "strconv" "time" @@ -54,19 +55,15 @@ func PeerHandler(onion string, uiManager ui.Manager, subscribed chan bool) { case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data ts, _ := time.Parse(time.RFC3339Nano, e.Data[event.TimestampReceived]) - //uiManager.AddMessage(e.Data[event.RemotePeer], e.Data[event.RemotePeer], e.Data[event.Data], false, e.EventID, ts, true) - uiManager.AboutToAddMessage() - //time.Sleep(time.Millisecond) - peer.StoreMessage(e.Data[event.RemotePeer], e.Data[event.Data], ts) - uiManager.MessageJustAdded() - + uiManager.StoreAndNotify(peer, e.Data[event.RemotePeer], e.Data[event.Data], ts, onion) case event.PeerAcknowledgement: - uiManager.Acknowledge(e.Data[event.EventID]) + uiManager.Acknowledge(e.Data[event.RemotePeer], e.Data[event.EventID]) case event.NewMessageFromGroup: //event.TimestampReceived, event.TimestampSent, event.Data, event.GroupID, event.RemotePeer ts, _ := time.Parse(time.RFC3339Nano, e.Data[event.TimestampSent]) - uiManager.AddMessage(e.Data[event.GroupID], e.Data[event.RemotePeer], e.Data[event.Data], e.Data[event.RemotePeer] == peer.GetOnion(), e.Data[event.Signature], ts, true) + uiManager.AddMessage(e.Data[event.GroupID], e.Data[event.RemotePeer], e.Data[event.Data], e.Data[event.RemotePeer] == peer.GetOnion(), hex.EncodeToString([]byte(e.Data[event.Signature])), ts, true) + case event.NewGroupInvite: gid, err := peer.ProcessInvite(e.Data[event.GroupInvite], e.Data[event.RemotePeer]) group := peer.GetGroup(gid) diff --git a/go/ui/gcd.go b/go/ui/gcd.go index b317d85b..a507abbd 100644 --- a/go/ui/gcd.go +++ b/go/ui/gcd.go @@ -2,6 +2,7 @@ package ui import ( "encoding/base64" + "strconv" "sync" "cwtch.im/cwtch/app" @@ -12,13 +13,11 @@ import ( "cwtch.im/ui/go/constants" "github.com/therecipe/qt/qml" - "encoding/base32" - "strings" - "time" - "cwtch.im/ui/go/the" + "encoding/base32" "git.openprivacy.ca/openprivacy/log" "github.com/therecipe/qt/core" + "strings" ) type GrandCentralDispatcher struct { @@ -66,8 +65,6 @@ type GrandCentralDispatcher struct { _ func(handle, key, value string) `signal:"UpdateContactAttribute"` // messages pane stuff - _ func(handle, from, displayName, message, image string, mID string, fromMe bool, ts int64, ackd bool, error bool) `signal:"AppendMessage"` - _ func(handle, from, displayName, message, image string, mID string, fromMe bool, ts int64, ackd bool, error bool) `signal:"PrependMessage"` _ func() `signal:"ClearMessages"` _ func() `signal:"ResetMessagePane"` _ func(mID string) `signal:"Acknowledged"` @@ -120,6 +117,8 @@ type GrandCentralDispatcher struct { _ func() `signal:"blockUnknownPeers,auto"` _ func(onion string) `signal:"storeHistoryForPeer,auto"` _ func(onion string) `signal:"deleteHistoryForPeer,auto"` + // chat + _ func(mID string) `slot:"acktest,auto"` _ func(handle string) `signal:"requestServerSettings,auto"` @@ -175,6 +174,18 @@ func (this *GrandCentralDispatcher) DoIfProfile(profile string, fn func()) { } } +// Like DoIfProfile() but runs elseFn() if profile isn't the currently selected one in the UI +func (this *GrandCentralDispatcher) DoIfProfileElse(profile string, fn func(), elseFn func()) { + this.profileLock.Lock() + defer this.profileLock.Unlock() + + if this.m_selectedProfile == profile { + fn() + } else { + elseFn() + } +} + func (this *GrandCentralDispatcher) selectedConversation() string { this.conversationLock.Lock() defer this.conversationLock.Unlock() @@ -204,6 +215,18 @@ func (this *GrandCentralDispatcher) DoIfConversation(conversation string, fn fun } } +// like DoIfConversation() but +func (this *GrandCentralDispatcher) DoIfConversationElse(conversation string, fn func(), elseFn func()) { + this.conversationLock.Lock() + defer this.conversationLock.Unlock() + + if this.m_selectedConversation == conversation { + fn() + } else { + elseFn() + } +} + func (this *GrandCentralDispatcher) sendMessage(message string) { if len(message) > 65530 { this.InvokePopup("message is too long") @@ -224,22 +247,18 @@ func (this *GrandCentralDispatcher) sendMessage(message string) { } } - mID, err := the.Peer.SendMessageToGroupTracked(this.SelectedConversation(), message) - - this.GetUiManager(this.selectedProfile()).AddMessage(this.SelectedConversation(), "me", message, true, mID, time.Now(), false) + this.TimelineInterface.AddMessage(this.TimelineInterface.num()) + _, err := the.Peer.SendMessageToGroupTracked(this.SelectedConversation(), message) + this.TimelineInterface.RequestEIR() if err != nil { this.InvokePopup("failed to send message " + err.Error()) return } } else { - to := this.SelectedConversation() - prenum := this.TimelineInterface.num() - this.TimelineInterface.AddMessage(prenum) - /*mID := */the.Peer.SendMessageToPeer(to, message) + this.TimelineInterface.AddMessage(this.TimelineInterface.num()) + the.Peer.SendMessageToPeer(this.SelectedConversation(), message) this.TimelineInterface.RequestEIR() - - //this.GetUiManager(this.selectedProfile()).AddMessage(to, "me", message, true, mID, time.Now(), false) } } @@ -266,7 +285,6 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { this.UpdateContactStatus(group.GroupID, int(state), loading) this.requestGroupSettings(handle) - tl := group.GetTimeline() nick := GetNick(handle) updateLastReadTime(group.GroupID) if nick == "" { @@ -275,34 +293,6 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { this.SetToolbarTitle(nick) } - go func() { - // Janky hack to let the ui/qml respond to the status updates first before freezing under a deluge of new messages - time.Sleep(10 * time.Millisecond) - for i := len(tl) - 1; i >= 0; i-- { - if tl[i].PeerID == the.Peer.GetOnion() { - handle = "me" - } else { - handle = tl[i].PeerID - } - - name := GetNick(tl[i].PeerID) - image := GetProfilePic(tl[i].PeerID) - - this.PrependMessage( - handle, - tl[i].PeerID, - name, - tl[i].Message, - image, - string(tl[i].Signature), - tl[i].PeerID == the.Peer.GetOnion(), - tl[i].Timestamp.Unix(), - 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 - false, - ) - } - }() - return } // ELSE LOAD CONTACT @@ -317,32 +307,6 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { } updateLastReadTime(contact.Onion) this.SetToolbarTitle(nick) - - peer := the.Peer.GetContact(handle) - messages := peer.Timeline.GetMessages() - for i := range messages { - from := messages[i].PeerID - fromMe := messages[i].PeerID == the.Peer.GetOnion() - if fromMe { - from = "me" - } - - displayname := GetNick(messages[i].PeerID) - image := GetProfilePic(messages[i].PeerID) - - this.AppendMessage( - from, - messages[i].PeerID, - displayname, - messages[i].Message, - image, - string(messages[i].Signature), - fromMe, - messages[i].Timestamp.Unix(), - messages[i].Acknowledged, - messages[i].Error != "", - ) - } } func (this *GrandCentralDispatcher) requestSettings() { @@ -760,3 +724,8 @@ func (this *GrandCentralDispatcher) deleteProfile(onion string) { log.Infof("deleteProfile %v\n", onion) the.CwtchApp.DeletePeer(onion) } + +func (this *GrandCentralDispatcher) acktest(mID string) { + idx, _ := strconv.Atoi(mID) + this.TimelineInterface.EditMessage(idx) +} \ No newline at end of file diff --git a/go/ui/manager.go b/go/ui/manager.go index ff4db305..bf8f754a 100644 --- a/go/ui/manager.go +++ b/go/ui/manager.go @@ -4,6 +4,7 @@ import ( "cwtch.im/cwtch/app" "cwtch.im/cwtch/model" "cwtch.im/cwtch/model/attr" + "cwtch.im/cwtch/peer" "cwtch.im/cwtch/protocol/connections" "cwtch.im/ui/go/constants" "cwtch.im/ui/go/the" @@ -202,7 +203,7 @@ type manager struct { // manager also performs call filtering based on UI state: users of manager can safely always call it on events and not have to worry about weather the relevant ui is active // ie: you can always safely call AddMessage even if in the ui a different profile is selected. manager will check with gcd, and if the correct conditions are not met, it will not call on gcd to update the ui incorrectly type Manager interface { - Acknowledge(mID string) + Acknowledge(handle, mID string) AddContact(Handle string) AddSendMessageError(peer string, signature string, err string) AddMessage(handle string, from string, message string, fromMe bool, messageID string, timestamp time.Time, Acknowledged bool) @@ -218,6 +219,7 @@ type Manager interface { AboutToAddMessage() MessageJustAdded() + StoreAndNotify(peer.CwtchPeer, string, string, time.Time, string) } // NewManager returns a new Manager interface for a profile to the gcd @@ -226,9 +228,11 @@ func NewManager(profile string, gcd *GrandCentralDispatcher) Manager { } // Acknowledge acknowledges the given message id in the UI -func (this *manager) Acknowledge(mID string) { +func (this *manager) Acknowledge(handle, mID string) { this.gcd.DoIfProfile(this.profile, func() { - this.gcd.Acknowledged(mID) + this.gcd.DoIfConversation(handle, func(){ + this.gcd.Acktest(mID) + }) }) } @@ -291,28 +295,34 @@ func (this *manager) MessageJustAdded() { this.gcd.TimelineInterface.RequestEIR() } +func (this *manager) StoreAndNotify(pere peer.CwtchPeer, onion string, messageTxt string, sent time.Time, profileOnion string) { + this.gcd.DoIfProfileElse(this.profile, func() { + this.gcd.DoIfConversationElse(onion, func() { + updateLastReadTime(onion) + this.gcd.TimelineInterface.AddMessage(this.gcd.TimelineInterface.num()) + pere.StoreMessage(onion, messageTxt, sent) + this.gcd.TimelineInterface.RequestEIR() + }, func() { + updateLastReadTime(onion) + pere.StoreMessage(onion, messageTxt, sent) + }) + this.gcd.IncContactUnreadCount(onion) + }, func() { + the.CwtchApp.GetPeer(profileOnion).StoreMessage(onion, messageTxt, sent) + }) +} + // AddMessage adds a message to the message pane for the supplied conversation if it is active func (this *manager) AddMessage(handle string, from string, message string, fromMe bool, messageID string, timestamp time.Time, Acknowledged bool) { - log.Errorf("uiManager.AddMessage(...) uhhhh???? NO ZOOP ZEEP") this.gcd.DoIfProfile(this.profile, func() { - - //nick := GetNick(handle) - //image := GetProfilePic(handle) - - // If we have this group loaded already this.gcd.DoIfConversation(handle, func() { updateLastReadTime(handle) // If the message is not from the user then add it, otherwise, just acknowledge. - if !fromMe { - this.gcd.TimelineInterface.AddMessage(this.gcd.TimelineInterface.num()) - //this.gcd.AppendMessage(handle, from, nick, message, image, messageID, fromMe, timestamp.Unix(), false, false) + if !fromMe || !Acknowledged { + this.gcd.TimelineInterface.AddMessage(this.gcd.TimelineInterface.num()-1) + this.gcd.TimelineInterface.RequestEIR() } else { - if !Acknowledged { - this.gcd.TimelineInterface.AddMessage(this.gcd.TimelineInterface.num()) - //this.gcd.AppendMessage(handle, from, nick, message, image, messageID, fromMe, timestamp.Unix(), false, false) - } else { - this.gcd.Acknowledged(messageID) - } + this.gcd.Acknowledged(messageID) } }) this.gcd.IncContactUnreadCount(handle) diff --git a/go/ui/messagemodel.go b/go/ui/messagemodel.go index 5d0735d1..33faab1a 100644 --- a/go/ui/messagemodel.go +++ b/go/ui/messagemodel.go @@ -3,6 +3,7 @@ package ui import ( "cwtch.im/cwtch/model" "cwtch.im/ui/go/the" + "encoding/hex" "git.openprivacy.ca/openprivacy/log" "github.com/therecipe/qt/core" "reflect" @@ -12,16 +13,16 @@ import ( type MessageModel struct { core.QAbstractTableModel + ackIdx int handle string - //_ string `property:"handle,auto"` _ func(string) `signal:"setHandle,auto"` _ map[int]*core.QByteArray `property:"roles"` _ func() `constructor:"init"` _ func(int) `signal:"addMessage,auto"` - _ func(string) `signal:"createLocalFormEntry,auto"` - _ func() `signal:"requestEIR,auto"` // request this.EndInsertRecord() on gui thread + _ func(int) `signal:"editMessage,auto"` + _ func() `signal:"requestEIR,auto"` _ func(string) string `slot:"getNick,auto"` _ func(string) string `slot:"getImage,auto"` @@ -36,6 +37,8 @@ type MessageWrapper struct { Acknowledged bool RawMessage string Error string + Day string + Signature string _ bool `property:"ackd"` } @@ -49,10 +52,12 @@ func (this *MessageModel) setHandle(handle string) { func (this *MessageModel) init() { - //mdt := reflect.TypeOf([]model.Message{}).Elem() mdt := reflect.TypeOf([]MessageWrapper{}).Elem() roles := make(map[int]*core.QByteArray) for i := 0; i < mdt.NumField(); i++ { + if mdt.Field(i).Name == "Acknowledged" { + this.ackIdx = int(core.Qt__UserRole) + 1 + i + } roles[int(core.Qt__UserRole) + 1 + i] = core.NewQByteArray2(mdt.Field(i).Name, -1) } roles[int(core.Qt__DisplayRole)] = core.NewQByteArray2("display", -1) @@ -83,7 +88,7 @@ func (this *MessageModel) getImage(handle string) string { func (this *MessageModel) num() int { if this.Handle() == "" || the.Peer == nil { - log.Debugf("num: early returning 0") + log.Debugf("MessageModel.num: early returning 0") return 0 } @@ -95,19 +100,17 @@ func (this *MessageModel) num() int { } else { contact := the.Peer.GetContact(this.Handle()) if contact != nil { - log.Debugf("num: returning %v", len(contact.Timeline.Messages)) return len(contact.Timeline.Messages) } } - log.Warnf("group/contact was nil, returning 0") + log.Warnf("MessageModel.num: group/contact was nil, returning 0") return 0 } func (this *MessageModel) getMessage(idx int) *MessageWrapper { - log.Infof("MessageModel.getMessage(%v)", idx) - var modelmsg model.Message + var ackd bool if this.isGroup() { group := the.Peer.GetGroup(this.Handle()) @@ -115,6 +118,7 @@ func (this *MessageModel) getMessage(idx int) *MessageWrapper { modelmsg = group.UnacknowledgedMessages[idx - len(group.Timeline.Messages)] } else { modelmsg = group.Timeline.Messages[idx] + ackd = true } } else { contact := the.Peer.GetContact(this.Handle()) @@ -122,9 +126,9 @@ func (this *MessageModel) getMessage(idx int) *MessageWrapper { modelmsg = model.Message{Message:"oops test hi uhhhhh :/"} } else if idx >= len(contact.Timeline.Messages) { log.Errorf("this shouldnt happen") - //modelmsg = contact.UnacknowledgedMessages[idx-len(contact.Timeline.Messages)] } else { modelmsg = contact.Timeline.Messages[idx] + ackd = modelmsg.Acknowledged } } @@ -134,13 +138,13 @@ func (this *MessageModel) getMessage(idx int) *MessageWrapper { RawMessage: modelmsg.Message, PeerID: modelmsg.PeerID, Error: modelmsg.Error, - Acknowledged: modelmsg.Acknowledged, + Acknowledged: ackd, + Day: modelmsg.Timestamp.Format("January 2, 2006"), + Signature: hex.EncodeToString(modelmsg.Signature), } } func (this *MessageModel) data(index *core.QModelIndex, role int) *core.QVariant { - log.Infof("MessageModel.data(%v, %v)", index.Row(), role) - if !index.IsValid() { return core.NewQVariant() } @@ -173,7 +177,7 @@ func (this *MessageModel) headerData(section int, orientation core.Qt__Orientati return this.HeaderDataDefault(section, orientation, role) } - mdt := reflect.TypeOf([]model.Message{}).Elem() + mdt := reflect.TypeOf([]MessageWrapper{}).Elem() return core.NewQVariant12(mdt.Field(section).Name) } @@ -185,30 +189,29 @@ func (this *MessageModel) columnCount(parent *core.QModelIndex) int { return reflect.TypeOf(MessageWrapper{}).NumField() } +// perform this.BeginInsertRows() on the gui thread +// important: +// 1. idx MUST be set to this.num()'s value *before* calling addMessage() +// 2. insert the message yourself +// 3. this.RequestEIR() *must* be called afterward func (this *MessageModel) addMessage(idx int) { - log.Debugf("MessageModel.addMessage() ZOOP ZOOP %v", this.handle) - this.BeginInsertRows(core.NewQModelIndex(), idx, idx)//this.num(), this.num()) - //this.modelData = append(this.modelData, *fe) - //this.RequestEIR() + this.BeginInsertRows(core.NewQModelIndex(), idx, idx) } -// perform this.EndInsertRows() on the gui thread +// perform this.EndInsertRows() on the gui thread after an AddMessage() func (this *MessageModel) requestEIR() { - log.Debugf("MessageModel.requestEIR() ZEEP ZEEP %v", this.handle) this.EndInsertRows() } - -func (this *MessageModel) createLocalFormEntry(name string) { - go this.createLocalFormEntry_thread(name) -} - -func (this *MessageModel) createLocalFormEntry_thread(name string) { - log.Debugf("nyi #9779729343959699492726648294050382") - /* - fe := &model.Message{ - Message: "hi!", +// notify the gui that the message acknowledgement at index idx has been modified +func (this *MessageModel) editMessage(idx int) { + if idx < 0 || idx >= this.num() { + log.Errorf("cant edit message %v. probably fine", idx) + return } - this.addMessage(fe) - */ -} \ No newline at end of file + + log.Debugf("editMessage(%v, %v)", idx, this.ackIdx) + indexObject := this.Index(idx, 0, core.NewQModelIndex()) + // replace third param with []int{} to update all attributes instead + this.DataChanged(indexObject, indexObject, []int{this.ackIdx}) +} diff --git a/qml/overlays/ChatOverlay.qml b/qml/overlays/ChatOverlay.qml index 1629f699..686b62c0 100644 --- a/qml/overlays/ChatOverlay.qml +++ b/qml/overlays/ChatOverlay.qml @@ -16,11 +16,30 @@ W.Overlay { //horizontalPadding: 15 * gcd.themeScale - ListModel { // MESSAGE OBJECTS ARE STORED HERE ... - id: messagesModel + Connections { + target: mm + onRowsInserted: { + if (messagesListView.atYEnd) thymer.running = true + + //todo: this won't fire for non-active convos + windowItem.alert(0) + if (gcd.os == "android" && windowItem.activeFocusItem == null) { + androidCwtchActivity.notification = "New Content" + } + } } - + // onRowsInserted is firing after the model is updated but before the delegate is inflated + // causing positionViewAtEnd() to scroll to "just above the last message" + // so we use this timer to delay scrolling by a few milliseconds + Timer { + id: thymer + interval: 30 + onTriggered: { + thymer.running = false + messagesListView.positionViewAtEnd() + } + } contentItem: ListView { id: messagesListView @@ -28,22 +47,26 @@ W.Overlay { Layout.fillWidth: true width: parent.width - model: mm//messagesModel// + model: mm spacing: 6 clip: true ScrollBar.vertical: Opaque.ScrollBar {} maximumFlickVelocity: 1250 + section.delegate: sectionHeading + section.property: "Day" + + delegate: W.Message { handle: PeerID from: PeerID displayName: mm.getNick(PeerID) - message: JSON.parse(RawMessage).d + message: JSON.parse(RawMessage).d+"//"+Signature rawMessage: RawMessage image: mm.getImage(PeerID) - messageID: "-1"//_mid - fromMe: PeerID == gcd.SelectedProfile + messageID: Signature + fromMe: PeerID == gcd.selectedProfile timestamp: parseInt(Timestamp) ackd: Acknowledged error: Error @@ -53,119 +76,48 @@ W.Overlay { width: messagesListView.width } + Component { + id: sectionHeading + Rectangle {// ⟵ outer rect because anchors._Margin isnt supported here + // with qt 5.15+ this↓ can be changed to... + // required property string section + property string txt: section + color: Theme.backgroundMainColor + width: childrenRect.width + height: childrenRect.height + 12 + anchors.horizontalCenter: parent.horizontalCenter + + Rectangle { + opacity: 1 + width: childrenRect.width + 66 + height: childrenRect.height + 6 + color: Theme.messageFromOtherBackgroundColor + radius: 15 + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + + + Text { + // ... and this can be changed to + // text: parent.parent.section + text: parent.parent.txt + font.pixelSize: Theme.chatSize * gcd.themeScale + color: Theme.messageFromOtherTextColor + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + Connections { target: gcd onClearMessages: function() { - messagesModel.clear() messagesListView.model = null messagesListView.model = mm - } - - onAppendMessage: function(handle, from, displayName, message, image, mid, fromMe, ts, ackd, error) { - return - var msg - try { - msg = JSON.parse(message) - } catch (e) { - msg = {"o": 1, "d": "(legacy message type) " + message} - } - if (msg.o != 1) return - - var date = new Date(ts * 1000); - if (messagesModel.count != 0) { - var prevDate = new Date(messagesModel.get(messagesModel.count-1)["_ts"] * 1000); - if (prevDate.getFullYear() != date.getFullYear() - || prevDate.getMonth() != date.getMonth() - || prevDate.getUTCDate() != date.getUTCDate()) { - // new Day detected, Add Date message divider - messagesModel.append({ - "_handle": "calendar", - "_from": "calendar", - "_displayName": "calendar", - "_message": Qt.formatDateTime(date, "MMMM dd, yyyy"), - "_rawMessage": "", - "_image": "", - "_mid": "", - "_fromMe": false, - "_ts": ts, - "_ackd": true, - "_error": "", - }) - } - } - - messagesModel.append({ - "_handle": handle, - "_from": from, - "_displayName": displayName, - "_message": msg.d, - "_rawMessage":msg.d, - "_image": image, - "_mid": mid, - "_fromMe": fromMe, - "_ts": ts, - "_ackd": ackd, - "_error": error == true ? "this message failed to send" : "", - }) - - messagesListView.positionViewAtEnd() - - // If the window is out of focus, alert the user (makes taskbar light up) - windowItem.alert(0) - if (gcd.os == "android" && windowItem.activeFocusItem == null) { - androidCwtchActivity.notification = "New Content" - } - } - - onPrependMessage: function(handle, from, displayName, message, image, mid, fromMe, ts, ackd, error) { - return - var msg - try { - msg = JSON.parse(message) - } catch (e) { - msg = {"o": 1, "d": "(legacy message type) " + message} - } - if (msg.o != 1) return - - var date = new Date(ts * 1000); - if (messagesModel.count != 0) { - var prevDate = new Date(messagesModel.get(0)["_ts"] * 1000); - - if (prevDate.getFullYear() != date.getFullYear() - || prevDate.getMonth() != date.getMonth() - || prevDate.getUTCDate() != date.getUTCDate()) { - messagesModel.insert(0, { - "_handle": "calendar", - "_from": "calendar", - "_displayName": "calendar", - "_message": Qt.formatDateTime(prevDate, "MMMM dd, yyyy"), - "_rawMessage": "", - "_image": "", - "_mid": "", - "_fromMe": false, - "_ts": ts, - "_ackd": true, - "_error": "", - }) - } - } - - messagesModel.insert(0, { - "_handle": handle, - "_from": from, - "_displayName": displayName, - "_message": msg.d, - "_rawMessage":msg.d, - "_image": image, - "_mid": mid, - "_fromMe": fromMe, - "_ts": ts, - "_ackd": ackd, - "_error": error == true ? "this message failed to send" : "", - }) - - messagesListView.positionViewAtEnd() + messagesListView.positionViewAtEnd() + thymer.running = true } } }