diff --git a/go/handlers/appHandler.go b/go/handlers/appHandler.go index 7baa03d6..f58a137a 100644 --- a/go/handlers/appHandler.go +++ b/go/handlers/appHandler.go @@ -109,13 +109,13 @@ func App(gcd *ui.GrandCentralDispatcher, subscribed chan bool, reloadingFirst bo } log.Infof("NewPeer for %v\n", onion) - gcd.UIManager.AddProfile(onion) + ui.AddProfile(gcd, onion) the.CwtchApp.AddPeerPlugin(onion, plugins.CONNECTIONRETRY) the.CwtchApp.AddPeerPlugin(onion, plugins.NETWORKCHECK) incSubscribed := make(chan bool) - go PeerHandler(onion, &gcd.UIManager, incSubscribed) + go PeerHandler(onion, gcd.GetUiManager(peer.GetProfile().Onion), incSubscribed) <-incSubscribed if e.Data[event.Status] != "running" { diff --git a/go/handlers/peerHandler.go b/go/handlers/peerHandler.go index a753db98..cefe3d23 100644 --- a/go/handlers/peerHandler.go +++ b/go/handlers/peerHandler.go @@ -9,7 +9,7 @@ import ( "time" ) -func PeerHandler(onion string, uiManager *ui.Manager, subscribed chan bool) { +func PeerHandler(onion string, uiManager ui.Manager, subscribed chan bool) { peer := the.CwtchApp.GetPeer(onion) eventBus := the.CwtchApp.GetEventBus(onion) q := event.NewQueue() diff --git a/go/ui/gcd.go b/go/ui/gcd.go index 1c3c8e7b..ca066b5f 100644 --- a/go/ui/gcd.go +++ b/go/ui/gcd.go @@ -5,6 +5,7 @@ import ( "cwtch.im/cwtch/protocol/connections" "cwtch.im/ui/go/constants" "github.com/therecipe/qt/qml" + "sync" "cwtch.im/ui/go/the" "encoding/base32" @@ -17,15 +18,23 @@ import ( type GrandCentralDispatcher struct { core.QObject - UIManager Manager QMLEngine *qml.QQmlApplicationEngine Translator *core.QTranslator + uIManagers map[string]Manager // profile-onion : Manager + + profileLock sync.Mutex + conversationLock sync.Mutex + + m_selectedProfile string + m_selectedConversation string + _ string `property:"os"` - _ string `property:"currentOpenConversation"` _ float32 `property:"themeScale"` _ string `property:"version"` _ string `property:"buildDate"` + _ string `property:"selectedProfile,auto"` + _ string `property:"selectedConversation,auto"` // profile management stuff _ func() `signal:"Loaded"` @@ -90,6 +99,83 @@ type GrandCentralDispatcher struct { _ func() `signal:"onActivate,auto"` _ func(password string) `signal:"unlockProfiles,auto"` _ func(handle string) `signal:"loadProfile,auto"` + + _ func() `constructor:"init"` +} + +func (this *GrandCentralDispatcher) init() { + this.uIManagers = make(map[string]Manager) +} + +// GetUiManager gets (and creates if required) a ui Manager for the supplied profile id +func (this *GrandCentralDispatcher) GetUiManager(profile string) Manager { + this.profileLock.Lock() + defer this.profileLock.Unlock() + + if manager, exists := this.uIManagers[profile]; exists { + return manager + } else { + this.uIManagers[profile] = NewManager(profile, this) + return this.uIManagers[profile] + } +} + +func (this *GrandCentralDispatcher) selectedProfile() string { + this.profileLock.Lock() + defer this.profileLock.Unlock() + + return this.m_selectedProfile +} + +func (this *GrandCentralDispatcher) setSelectedProfile(onion string) { + this.profileLock.Lock() + defer this.profileLock.Unlock() + + this.m_selectedProfile = onion +} + +func (this *GrandCentralDispatcher) selectedProfileChanged(onion string) { + this.SelectedProfileChanged(onion) +} + +// DoIfProfile performs a gcd action for a profile IF it is the currently selected profile in the UI +// otherwise it does nothing. it also locks profile switching for the duration of the action +func (this *GrandCentralDispatcher) DoIfProfile(profile string, fn func()) { + this.profileLock.Lock() + defer this.profileLock.Unlock() + + if this.m_selectedProfile == profile { + fn() + } +} + +func (this *GrandCentralDispatcher) selectedConversation() string { + this.conversationLock.Lock() + defer this.conversationLock.Unlock() + + return this.m_selectedConversation +} + +func (this *GrandCentralDispatcher) setSelectedConversation(handle string) { + this.conversationLock.Lock() + defer this.conversationLock.Unlock() + + this.m_selectedConversation = handle +} + +func (this *GrandCentralDispatcher) selectedConversationChanged(handle string) { + this.SelectedConversationChanged(handle) +} + +// DoIfConversation performs a gcd action for a conversation IF it is the currently selected conversation in the UI +// otherwise it does nothing. it also locks conversation switching for the duration of the action +func (this *GrandCentralDispatcher) DoIfConversation(conversation string, fn func()) { + this.conversationLock.Lock() + defer this.conversationLock.Unlock() + + if this.m_selectedConversation == conversation { + fn() + } } func (this *GrandCentralDispatcher) sendMessage(message string, mID string) { @@ -98,14 +184,14 @@ func (this *GrandCentralDispatcher) sendMessage(message string, mID string) { return } - if this.CurrentOpenConversation() == "" { + if this.SelectedConversation() == "" { this.InvokePopup("ui error") return } - if isGroup(this.CurrentOpenConversation()) { - if !the.Peer.GetGroup(this.CurrentOpenConversation()).Accepted { - err := the.Peer.AcceptInvite(this.CurrentOpenConversation()) + if isGroup(this.SelectedConversation()) { + if !the.Peer.GetGroup(this.SelectedConversation()).Accepted { + err := the.Peer.AcceptInvite(this.SelectedConversation()) if err != nil { log.Errorf("tried to mark a nonexistent group as existed. bad!") return @@ -113,19 +199,19 @@ func (this *GrandCentralDispatcher) sendMessage(message string, mID string) { } var err error - mID, err = the.Peer.SendMessageToGroupTracked(this.CurrentOpenConversation(), message) + mID, err = the.Peer.SendMessageToGroupTracked(this.SelectedConversation(), message) - this.UIManager.AddMessage(this.CurrentOpenConversation(), "me", message, true, mID, time.Now(), false) + this.GetUiManager(this.selectedProfile()).AddMessage(this.SelectedConversation(), "me", message, true, mID, time.Now(), false) if err != nil { this.InvokePopup("failed to send message " + err.Error()) return } } else { - to := this.CurrentOpenConversation() + to := this.SelectedConversation() mID = the.Peer.SendMessageToPeer(to, message) - this.UIManager.AddMessage(to, "me", message, true, mID, time.Now(), false) + this.GetUiManager(this.selectedProfile()).AddMessage(to, "me", message, true, mID, time.Now(), false) } } @@ -139,7 +225,7 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { return } this.ClearMessages() - this.SetCurrentOpenConversation(handle) + this.SetSelectedConversation(handle) if isGroup(handle) { // LOAD GROUP group := the.Peer.GetGroup(handle) @@ -245,17 +331,18 @@ func (this *GrandCentralDispatcher) saveSettings(zoom, locale string) { } func (this *GrandCentralDispatcher) requestPeerSettings() { - contact := the.Peer.GetContact(this.CurrentOpenConversation()) + contact := the.Peer.GetContact(this.SelectedConversation()) if contact == nil { - log.Errorf("error: requested settings for unknown contact %v?", this.CurrentOpenConversation()) - this.SupplyPeerSettings(this.CurrentOpenConversation(), this.CurrentOpenConversation(), false) + log.Errorf("error: requested settings for unknown contact %v?", this.SelectedConversation()) + this.SupplyPeerSettings(this.SelectedConversation(), this.SelectedConversation(), false) return } 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) + log.Errorf("error: couldn't find contact %v", this.SelectedConversation()) + this.SupplyPeerSettings(this.SelectedConversation(), this.SelectedConversation(), contact.Blocked) + this.SupplyPeerSettings(this.SelectedConversation(), this.SelectedConversation(), contact.Blocked) return } @@ -379,7 +466,7 @@ func (this *GrandCentralDispatcher) importString(str string) { the.Peer.PeerWithOnion(onion) } - this.UIManager.AddContact(onion) + this.GetUiManager(this.selectedProfile()).AddContact(onion) } func (this *GrandCentralDispatcher) popup(str string) { @@ -400,7 +487,7 @@ func (this *GrandCentralDispatcher) createGroup(server, groupName string) { return } - this.UIManager.AddContact(groupID) + this.GetUiManager(this.selectedProfile()).AddContact(groupID) the.Peer.SetGroupAttribute(groupID, constants.Nick, groupName) @@ -449,7 +536,7 @@ func (this *GrandCentralDispatcher) acceptGroup(groupID string) { func (this *GrandCentralDispatcher) setAttribute(onion, key, value string) { the.Peer.SetContactAttribute(onion, key, value) - this.UIManager.UpdateContactAttribute(onion, key, value) + this.GetUiManager(this.selectedProfile()).UpdateContactAttribute(onion, key, value) } func (this *GrandCentralDispatcher) blockUnknownPeers() { @@ -505,13 +592,13 @@ func (this *GrandCentralDispatcher) loadProfile(onion string) { contacts := the.Peer.GetContacts() for i := range contacts { - this.UIManager.AddContact(contacts[i]) + this.GetUiManager(this.selectedProfile()).AddContact(contacts[i]) } groups := the.Peer.GetGroups() for i := range groups { // Only join servers for active and explicitly accepted groups. - this.UIManager.AddContact(groups[i]) + this.GetUiManager(this.selectedProfile()).AddContact(groups[i]) } // load ui preferences diff --git a/go/ui/manager.go b/go/ui/manager.go index b2f0ad34..a23493dd 100644 --- a/go/ui/manager.go +++ b/go/ui/manager.go @@ -107,15 +107,8 @@ func countUnread(messages []model.Message, lastRead time.Time) int { return count } -type Manager struct { - gcd *GrandCentralDispatcher -} - -func NewManager(gcd *GrandCentralDispatcher) Manager { - return Manager{gcd} -} - -func (this *Manager) AddProfile(handle string) { +// AddProfile adds a new profile to the UI +func AddProfile(gcd *GrandCentralDispatcher, handle string) { peer := the.CwtchApp.GetPeer(handle) if peer != nil { nick := peer.GetProfile().Name @@ -130,12 +123,41 @@ func (this *Manager) AddProfile(handle string) { peer.SetAttribute(constants.Picture, pic) } log.Infof("AddProfile %v %v %v\n", handle, nick, pic) - this.gcd.AddProfile(handle, nick, pic) + gcd.AddProfile(handle, nick, pic) } } -func (this *Manager) Acknowledge(mID string) { - this.gcd.Acknowledged(mID) +type manager struct { + gcd *GrandCentralDispatcher + profile string +} + +// Manager is a middleware helper for entities like peer event listeners wishing to trigger ui changes (via the gcd) +// each manager is for one profile/peer +// manager takes minimal arguments and builds the full struct of data (usually pulled from a cwtch peer) required to call the GCD to perform the ui action +// 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) + 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) + + UpdateContactDisplayName(handle string, name string) + UpdateContactStatus(handle string, status int, loading bool) + UpdateContactAttribute(handle, key, value string) +} + +// NewManager returns a new Manager interface for a profile to the gcd +func NewManager(profile string, gcd *GrandCentralDispatcher) Manager { + return &manager{gcd: gcd, profile: profile} +} + +// Acknowledge acknowledges the given message id in the UI +func (this *manager) Acknowledge(mID string) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.Acknowledged(mID) + }) } func getLastMessageTime(tl *model.Timeline) int { @@ -146,77 +168,99 @@ func getLastMessageTime(tl *model.Timeline) int { return int(tl.Messages[len(tl.Messages)-1].Timestamp.Unix()) } -func (this *Manager) AddContact(Handle string) { - if isGroup(Handle) { - group := the.Peer.GetGroup(Handle) - if group != nil { - lastRead := initLastReadTime(group.GroupID) - unread := countUnread(group.Timeline.GetMessages(), lastRead) +// AddContact adds a new contact to the ui for this manager's profile +func (this *manager) AddContact(Handle string) { + this.gcd.DoIfProfile(this.profile, func() { + + if isGroup(Handle) { + group := the.Peer.GetGroup(Handle) + if group != nil { + lastRead := initLastReadTime(group.GroupID) + unread := countUnread(group.Timeline.GetMessages(), lastRead) + picture := initProfilePicture(Handle) + nick, exists := group.GetAttribute(constants.Nick) + if !exists { + nick = Handle + } + + this.gcd.AddContact(Handle, nick, picture, group.GroupServer, unread, int(connections.ConnectionStateToType[group.State]), false, false, getLastMessageTime(&group.Timeline)) + } + return + } else if !isPeer(Handle) { + log.Errorf("sorry, unable to handle AddContact(%v)", Handle) + debug.PrintStack() + return + } + + contact := the.Peer.GetContact(Handle) + if contact != nil { + lastRead := initLastReadTime(contact.Onion) + unread := countUnread(contact.Timeline.GetMessages(), lastRead) picture := initProfilePicture(Handle) - nick, exists := group.GetAttribute(constants.Nick) + nick, exists := contact.GetAttribute(constants.Nick) if !exists { nick = Handle } - this.gcd.AddContact(Handle, nick, picture, group.GroupServer, unread, int(connections.ConnectionStateToType[group.State]), false, false, getLastMessageTime(&group.Timeline)) + this.gcd.AddContact(Handle, nick, picture, "", unread, int(connections.ConnectionStateToType[contact.State]), contact.Blocked, false, getLastMessageTime(&contact.Timeline)) } - return - } else if !isPeer(Handle) { - log.Errorf("sorry, unable to handle AddContact(%v)", Handle) - debug.PrintStack() - return - } - - contact := the.Peer.GetContact(Handle) - if contact != nil { - lastRead := initLastReadTime(contact.Onion) - unread := countUnread(contact.Timeline.GetMessages(), lastRead) - picture := initProfilePicture(Handle) - nick, exists := contact.GetAttribute(constants.Nick) - if !exists { - nick = Handle - } - - this.gcd.AddContact(Handle, nick, picture, "", unread, int(connections.ConnectionStateToType[contact.State]), contact.Blocked, false, getLastMessageTime(&contact.Timeline)) - } + }) } -func (this *Manager) AddSendMessageError(peer string, signature string, err string) { - log.Debugf("Received Error Sending Message: %v", err) - // FIXME: Sometimes, for the first Peer message we send our error beats our message to the UI - time.Sleep(time.Second * 1) - this.gcd.GroupSendError(signature, err) +// AddSendMessageError adds an error not and icon to a message in a conversation in the ui for the message identified by the peer/sig combo +func (this *manager) AddSendMessageError(peer string, signature string, err string) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.DoIfConversation(peer, func() { + log.Debugf("Received Error Sending Message: %v", err) + // FIXME: Sometimes, for the first Peer message we send our error beats our message to the UI + time.Sleep(time.Second * 1) + this.gcd.GroupSendError(signature, err) + }) + }) } -func (this *Manager) AddMessage(handle string, from string, message string, fromMe bool, messageID string, timestamp time.Time, Acknowledged bool) { - nick := getOrDefault(handle, constants.Nick, handle) - image := getProfilePic(handle) +// 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) { + this.gcd.DoIfProfile(this.profile, func() { - // If we have this group loaded already - if this.gcd.CurrentOpenConversation() == handle { - updateLastReadTime(handle) - // If the message is not from the user then add it, otherwise, just acknowledge. - if !fromMe { - this.gcd.AppendMessage(handle, from, nick, message, image, messageID, fromMe, timestamp.Format(constants.TIME_FORMAT), false, false) - } else { - if !Acknowledged { + nick := getOrDefault(handle, constants.Nick, 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.AppendMessage(handle, from, nick, message, image, messageID, fromMe, timestamp.Format(constants.TIME_FORMAT), false, false) } else { - this.gcd.Acknowledged(messageID) + if !Acknowledged { + this.gcd.AppendMessage(handle, from, nick, message, image, messageID, fromMe, timestamp.Format(constants.TIME_FORMAT), false, false) + } else { + this.gcd.Acknowledged(messageID) + } } - } - } - this.gcd.IncContactUnreadCount(handle) + }) + this.gcd.IncContactUnreadCount(handle) + }) } -func (this *Manager) UpdateContactDisplayName(handle string, name string) { - this.gcd.UpdateContactDisplayName(handle, name) +// UpdateContactDisplayName updates a contact's display name in the contact list and conversations +func (this *manager) UpdateContactDisplayName(handle string, name string) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.UpdateContactDisplayName(handle, name) + }) } -func (this *Manager) UpdateContactStatus(handle string, status int, loading bool) { - this.gcd.UpdateContactStatus(handle, status, loading) +// UpdateContactStatus updates a contact's status in the ui +func (this *manager) UpdateContactStatus(handle string, status int, loading bool) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.UpdateContactStatus(handle, status, loading) + }) } -func (this *Manager) UpdateContactAttribute(handle, key, value string) { - this.gcd.UpdateContactAttribute(handle, key, value) +// UpdateContactAttribute update's a contacts attribute in the ui +func (this *manager) UpdateContactAttribute(handle, key, value string) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.UpdateContactAttribute(handle, key, value) + }) } diff --git a/main.go b/main.go index b6da2e69..2e5a587b 100644 --- a/main.go +++ b/main.go @@ -137,7 +137,6 @@ func mainUi(flagLocal bool, flagClientUI bool) { gcd.SetVersion("development") gcd.SetBuildDate("now") } - gcd.UIManager = ui.NewManager(gcd) //TODO: put theme stuff somewhere better gcd.SetThemeScale(1.0) diff --git a/qml/overlays/BulletinOverlay.qml b/qml/overlays/BulletinOverlay.qml index f914acb6..71e32c8d 100644 --- a/qml/overlays/BulletinOverlay.qml +++ b/qml/overlays/BulletinOverlay.qml @@ -93,7 +93,7 @@ ColumnLayout { } onUpdateContactStatus: function(_handle, _status, _loading) { - if (gcd.currentOpenConversation == _handle) { + if (gcd.selectedConversation == _handle) { if (_loading == true) { newposttitle.enabled = false newpostbody.enabled = false diff --git a/qml/overlays/ChatOverlay.qml b/qml/overlays/ChatOverlay.qml index b2c6d9ae..a6677e9a 100644 --- a/qml/overlays/ChatOverlay.qml +++ b/qml/overlays/ChatOverlay.qml @@ -111,7 +111,7 @@ ColumnLayout { } onUpdateContactStatus: function(_handle, _status, _loading) { - if (gcd.currentOpenConversation == _handle) { + if (gcd.selectedConversation == _handle) { // Group is Synced OR p2p is Authenticated if ( (_handle.length == 32 && _status == 4) || (_handle.length == 56 && _status == 3) ) { txtMessage.enabled = true diff --git a/qml/overlays/ListOverlay.qml b/qml/overlays/ListOverlay.qml index 6f983722..40c8acb8 100644 --- a/qml/overlays/ListOverlay.qml +++ b/qml/overlays/ListOverlay.qml @@ -97,7 +97,7 @@ ColumnLayout { } onUpdateContactStatus: function(_handle, _status, _loading) { - if (gcd.currentOpenConversation == _handle) { + if (gcd.selectedConversation == _handle) { if (_loading == true) { newposttitle.enabled = false btnSend.enabled = false diff --git a/qml/panes/OverlayPane.qml b/qml/panes/OverlayPane.qml index 6ac6f185..d56d3462 100644 --- a/qml/panes/OverlayPane.qml +++ b/qml/panes/OverlayPane.qml @@ -19,14 +19,14 @@ ColumnLayout { StackToolbar { id: toolbar - membership.visible: gcd.currentOpenConversation.length == 32 + membership.visible: gcd.selectedConversation.length == 32 membership.onClicked: overlayStack.overlay = overlayStack.membershipOverlay aux.onClicked: { - if (gcd.currentOpenConversation.length == 32) { + if (gcd.selectedConversation.length == 32) { theStack.pane = theStack.groupProfilePane - gcd.requestGroupSettings(gcd.currentOpenConversation) + gcd.requestGroupSettings(gcd.selectedConversation) } else { theStack.pane = theStack.userProfilePane gcd.requestPeerSettings() @@ -36,7 +36,7 @@ ColumnLayout { } RowLayout { - visible:!overlay.accepted && (gcd.currentOpenConversation.length == 32) + visible:!overlay.accepted && (gcd.selectedConversation.length == 32) Text { @@ -49,8 +49,8 @@ ColumnLayout { text: qsTr("accept-group-btn") icon: "regular/heart" onClicked: { - gcd.acceptGroup(gcd.currentOpenConversation) - gcd.requestGroupSettings(gcd.currentOpenConversation) + gcd.acceptGroup(gcd.selectedConversation) + gcd.requestGroupSettings(gcd.selectedConversation) } } @@ -59,7 +59,7 @@ ColumnLayout { text: qsTr("reject-group-btn") icon: "regular/trash-alt" onClicked: { - gcd.leaveGroup(gcd.currentOpenConversation) + gcd.leaveGroup(gcd.selectedConversation) theStack.pane = theStack.emptyPane } } diff --git a/qml/widgets/ContactRow.qml b/qml/widgets/ContactRow.qml index e6dd9e67..e5de3639 100644 --- a/qml/widgets/ContactRow.qml +++ b/qml/widgets/ContactRow.qml @@ -150,6 +150,7 @@ Item { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY } else if (type == "profile") { gcd.broadcast("ResetMessagePane") gcd.broadcast("ResetProfile") + gcd.selectedProfile = handle gcd.loadProfile(handle) parentStack.pane = parentStack.profilePane } @@ -191,7 +192,7 @@ Item { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY } onIncContactUnreadCount: function(handle) { - if (handle == _handle && gcd.currentOpenConversation != handle) { + if (handle == _handle && gcd.selectedConversation != handle) { badge++ } } diff --git a/qml/widgets/MyProfile.qml b/qml/widgets/MyProfile.qml index d634c69a..cdcd96e0 100644 --- a/qml/widgets/MyProfile.qml +++ b/qml/widgets/MyProfile.qml @@ -28,6 +28,7 @@ ColumnLayout { anchors.top: parent.top anchors.topMargin: 2 onClicked: function() { + gcd.selectedProfile = "none" parentStack.pane = parentStack.managementPane theStack.pane = theStack.emptyPane }