This repository has been archived on 2021-06-24. You can view files and clone it, but cannot push or open issues or pull requests.
ui/go/ui/manager.go

384 lines
12 KiB
Go

package ui
import (
"cwtch.im/cwtch/app"
"cwtch.im/cwtch/event"
"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"
"git.openprivacy.ca/openprivacy/log"
"runtime/debug"
"strings"
"time"
)
func isGroup(id string) bool {
return len(id) == 32 && !isServer(id)
}
func isPeer(id string) bool {
return len(id) == 56 && !isServer(id)
}
// Check if the id is associated with a contact with a KeyTypeServerOnion attribute (which indicates that this
// is a server, not a regular contact or a group
func isServer(id string) bool {
_, ok := the.Peer.GetContactAttribute(id, string(model.KeyTypeServerOnion))
return ok
}
func getOrDefault(id, key string, defaultVal string) string {
var val string
var ok bool
if isGroup(id) {
val, ok = the.Peer.GetGroupAttribute(id, key)
} else {
val, ok = the.Peer.GetContactAttribute(id, key)
}
if ok {
return val
} else {
return defaultVal
}
}
func getWithSetDefault(id string, key string, defaultVal string) string {
var val string
var ok bool
if isGroup(id) {
val, ok = the.Peer.GetGroupAttribute(id, key)
} else {
val, ok = the.Peer.GetContactAttribute(id, key)
}
if !ok {
val = defaultVal
if isGroup(id) {
the.Peer.SetGroupAttribute(id, key, defaultVal)
} else {
the.Peer.SetContactAttribute(id, key, defaultVal)
}
}
return val
}
func GetNick(id string) string {
if isGroup(id) {
nick, exists := the.Peer.GetGroupAttribute(id, attr.GetLocalScope(constants.Name))
if !exists || nick == "" || nick == id {
nick, exists = the.Peer.GetGroupAttribute(id, attr.GetPeerScope(constants.Name))
if !exists {
nick = "[" + id + "]"
}
}
return nick
} else {
nick, exists := the.Peer.GetContactAttribute(id, attr.GetLocalScope(constants.Name))
if !exists || nick == "" || nick == id {
nick, exists = the.Peer.GetContactAttribute(id, attr.GetPeerScope(constants.Name))
if !exists {
nick = "[" + id + "]"
}
}
return nick
}
}
// initLastReadTime checks and gets the Attributable's LastRead time or sets it to now
func initLastReadTime(id string) time.Time {
nowStr, _ := time.Now().MarshalText()
lastReadAttr := getWithSetDefault(id, attr.GetLocalScope(constants.LastRead), string(nowStr))
var lastRead time.Time
lastRead.UnmarshalText([]byte(lastReadAttr))
return lastRead
}
// a lot of pics were stored full path + uri. remove all this to the relative path in images/
// fix for storing full paths introduced 2019.12
func profilePicRelativize(filename string) string {
parts := strings.Split(filename, "qml/images")
return parts[len(parts)-1]
}
// GetProfilePic returns a string path to an image to display for hte given peer/group id
func GetProfilePic(id string) string {
if isGroup(id) {
if picVal, exists := the.Peer.GetGroupAttribute(id, attr.GetLocalScope(constants.Picture)); exists {
pic, err := StringToImage(picVal)
if err == nil {
return getPicturePath(pic)
}
}
if picVal, exists := the.Peer.GetGroupAttribute(id, attr.GetPeerScope(constants.Picture)); exists {
pic, err := StringToImage(picVal)
if err == nil {
return getPicturePath(pic)
}
}
return getPicturePath(NewImage(RandomGroupImage(id), TypeImageDistro))
} else {
if picVal, exists := the.Peer.GetContactAttribute(id, attr.GetLocalScope(constants.Picture)); exists {
pic, err := StringToImage(picVal)
if err == nil {
return getPicturePath(pic)
}
}
if picVal, exists := the.Peer.GetContactAttribute(id, attr.GetPeerScope(constants.Picture)); exists {
pic, err := StringToImage(picVal)
if err == nil {
return getPicturePath(pic)
}
}
return RandomProfileImage(id)
}
}
func getPicturePath(pic *image) string {
switch pic.T {
case TypeImageDistro:
return profilePicRelativize(pic.Val)
default:
log.Errorf("Unhandled profile picture type of %v\n", pic.T)
return ""
}
}
func updateLastReadTime(id string) {
lastRead, _ := time.Now().MarshalText()
if isGroup(id) {
the.Peer.SetGroupAttribute(id, attr.GetLocalScope(constants.LastRead), string(lastRead))
} else {
the.Peer.SetContactAttribute(id, attr.GetLocalScope(constants.LastRead), string(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) || messages[i].Timestamp.Equal(lastRead) {
count++
} else {
break
}
}
return count
}
// AddProfile adds a new profile to the UI
func AddProfile(gcd *GrandCentralDispatcher, handle string) {
p := the.CwtchApp.GetPeer(handle)
if p != nil {
nick, exists := p.GetAttribute(attr.GetPublicScope(constants.Name))
if !exists {
nick = handle
}
picVal, ok := p.GetAttribute(attr.GetPublicScope(constants.Picture))
if !ok {
picVal = ImageToString(NewImage(RandomProfileImage(handle), TypeImageDistro))
}
pic, err := StringToImage(picVal)
if err != nil {
pic = NewImage(RandomProfileImage(handle), TypeImageDistro)
}
picPath := getPicturePath(pic)
tag, _ := p.GetAttribute(app.AttributeTag)
online, _ := p.GetAttribute(attr.GetLocalScope(constants.PeerOnline))
log.Debugf("AddProfile %v %v %v %v %v\n", handle, nick, picPath, tag, online)
gcd.AddProfile(handle, nick, picPath, tag, online == event.True)
}
}
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(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)
ReloadProfiles()
UpdateContactDisplayName(handle string)
UpdateContactPicture(handle string)
UpdateContactStatus(handle string, status int, loading bool)
UpdateContactAttribute(handle, key, value string)
ChangePasswordResponse(error bool)
AboutToAddMessage()
MessageJustAdded()
StoreAndNotify(peer.CwtchPeer, string, string, time.Time, string)
UpdateNetworkStatus(online bool)
}
// 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(handle, mID string) {
this.gcd.DoIfProfile(this.profile, func() {
this.gcd.DoIfConversation(handle, func() {
this.gcd.PeerAckAlert(mID)
})
})
}
func getLastMessageTime(tl *model.Timeline) int {
if len(tl.Messages) == 0 {
return 0
}
return int(tl.Messages[len(tl.Messages)-1].Timestamp.Unix())
}
// 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 := GetProfilePic(handle)
this.gcd.AddContact(handle, GetNick(handle), picture, unread, int(connections.ConnectionStateToType[group.State]), string(model.AuthApproved), 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 := GetProfilePic(handle)
this.gcd.AddContact(handle, GetNick(handle), picture, unread, int(connections.ConnectionStateToType[contact.State]), string(contact.Authorization), false, getLastMessageTime(&contact.Timeline))
}
})
}
// 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) AboutToAddMessage() {
this.gcd.TimelineInterface.AddMessage(this.gcd.TimelineInterface.num())
}
func (this *manager) MessageJustAdded() {
this.gcd.TimelineInterface.RequestEIR()
}
func (this *manager) StoreAndNotify(pere peer.CwtchPeer, onion string, messageTxt string, sent time.Time, profileOnion string) {
// Send a New Message from Peer Notification
this.gcd.AndroidCwtchActivity.SetChannel(onion)
this.gcd.AndroidCwtchActivity.NotificationChanged("New Message from Peer")
this.gcd.DoIfProfileElse(this.profile, func() {
this.gcd.DoIfConversationElse(onion, func() {
this.gcd.TimelineInterface.AddMessage(this.gcd.TimelineInterface.num())
pere.StoreMessage(onion, messageTxt, sent)
this.gcd.TimelineInterface.RequestEIR()
updateLastReadTime(onion)
}, func() {
pere.StoreMessage(onion, messageTxt, sent)
})
this.gcd.IncContactUnreadCount(onion)
}, func() {
the.CwtchApp.GetPeer(profileOnion).StoreMessage(onion, messageTxt, sent)
})
this.gcd.Notify(onion)
}
// 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() {
this.gcd.DoIfConversation(handle, func() {
updateLastReadTime(handle)
// If the message is not from the user then add it, otherwise, just acknowledge.
if !fromMe || !Acknowledged {
this.gcd.TimelineInterface.AddMessage(this.gcd.TimelineInterface.num() - 1)
this.gcd.TimelineInterface.RequestEIR()
} else {
this.gcd.Acknowledged(messageID)
}
})
this.gcd.IncContactUnreadCount(handle)
})
if !fromMe {
this.gcd.Notify(handle)
}
}
func (this *manager) ReloadProfiles() {
this.gcd.reloadProfileList()
}
// UpdateContactDisplayName updates a contact's display name in the contact list and conversations
func (this *manager) UpdateContactDisplayName(handle string) {
this.gcd.DoIfProfile(this.profile, func() {
this.gcd.UpdateContactDisplayName(handle, GetNick(handle))
})
}
// UpdateContactPicture updates a contact's picture in the contact list and conversations
func (this *manager) UpdateContactPicture(handle string) {
this.gcd.DoIfProfile(this.profile, func() {
this.gcd.UpdateContactPicture(handle, GetProfilePic(handle))
})
}
// 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)
})
}
// 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)
})
}
func (this *manager) ChangePasswordResponse(error bool) {
this.gcd.ChangePasswordResponse(error)
}
func (this *manager) UpdateNetworkStatus(online bool) {
this.gcd.UpdateProfileNetworkStatus(this.profile, online)
}