282 lines
8.6 KiB
Go
282 lines
8.6 KiB
Go
package ui
|
|
|
|
import (
|
|
"cwtch.im/cwtch/app"
|
|
"cwtch.im/cwtch/model"
|
|
"cwtch.im/cwtch/protocol/connections"
|
|
"cwtch.im/ui/go/constants"
|
|
"cwtch.im/ui/go/the"
|
|
"git.openprivacy.ca/openprivacy/libricochet-go/log"
|
|
"runtime/debug"
|
|
"time"
|
|
)
|
|
|
|
func isGroup(id string) bool {
|
|
return len(id) == 32
|
|
}
|
|
|
|
func isPeer(id string) bool {
|
|
return len(id) == 56
|
|
}
|
|
|
|
func getOrDefault(id, key, 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, 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
|
|
}
|
|
|
|
// initLastReadTime checks and gets the Attributable's LastRead time or sets it to now
|
|
func initLastReadTime(id string) time.Time {
|
|
nowStr, _ := time.Now().MarshalText()
|
|
lastReadStr := getWithSetDefault(id, constants.LastRead, string(nowStr))
|
|
var lastRead time.Time
|
|
lastRead.UnmarshalText([]byte(lastReadStr))
|
|
return lastRead
|
|
}
|
|
|
|
func initProfilePicture(id string) string {
|
|
if isGroup(id) {
|
|
return getWithSetDefault(id, constants.Picture, RandomGroupImage(id))
|
|
} else {
|
|
return getWithSetDefault(id, constants.Picture, RandomProfileImage(id))
|
|
}
|
|
}
|
|
|
|
// getProfilePic supplies a profile pic to use. In groups we may not have a contact so it will generate one
|
|
func getProfilePic(id string) string {
|
|
if isGroup(id) {
|
|
if pic, exists := the.Peer.GetGroupAttribute(id, constants.Picture); !exists {
|
|
return RandomGroupImage(id)
|
|
} else {
|
|
return pic
|
|
}
|
|
} else {
|
|
if pic, exists := the.Peer.GetContactAttribute(id, constants.Picture); !exists {
|
|
return RandomProfileImage(id)
|
|
} else {
|
|
return pic
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateLastReadTime(id string) {
|
|
lastRead, _ := time.Now().MarshalText()
|
|
if isGroup(id) {
|
|
the.Peer.SetGroupAttribute(id, constants.LastRead, string(lastRead))
|
|
} else {
|
|
the.Peer.SetContactAttribute(id, 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) {
|
|
peer := the.CwtchApp.GetPeer(handle)
|
|
if peer != nil {
|
|
nick := peer.GetProfile().Name
|
|
if nick == "" {
|
|
nick = handle
|
|
peer.SetAttribute(constants.Nick, nick)
|
|
}
|
|
|
|
pic, ok := peer.GetAttribute(constants.Picture)
|
|
if !ok {
|
|
pic = RandomProfileImage(handle)
|
|
peer.SetAttribute(constants.Picture, pic)
|
|
}
|
|
|
|
tag, _ := peer.GetAttribute(app.AttributeTag)
|
|
log.Infof("AddProfile %v %v %v %v\n", handle, nick, pic, tag)
|
|
gcd.AddProfile(handle, nick, pic, tag)
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
ReloadProfiles()
|
|
|
|
UpdateContactDisplayName(handle string, name string)
|
|
UpdateContactStatus(handle string, status int, loading bool)
|
|
UpdateContactAttribute(handle, key, value string)
|
|
|
|
ChangePasswordResponse(error 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(mID string) {
|
|
this.gcd.DoIfProfile(this.profile, func() {
|
|
this.gcd.Acknowledged(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 := 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 := 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))
|
|
}
|
|
})
|
|
}
|
|
|
|
// 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)
|
|
})
|
|
})
|
|
}
|
|
|
|
// 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() {
|
|
|
|
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 {
|
|
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)
|
|
})
|
|
}
|
|
|
|
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, name string) {
|
|
this.gcd.DoIfProfile(this.profile, func() {
|
|
this.gcd.UpdateContactDisplayName(handle, name)
|
|
})
|
|
}
|
|
|
|
// 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)
|
|
}
|