384 lines
12 KiB
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)
|
|
}
|