From fc3bdb76f6a2fe38dd16389fa47bba80ee54755a Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Fri, 6 Mar 2020 23:41:38 -0800 Subject: [PATCH] New getVal/retVal message handling, and new attribute storage schema --- go.mod | 5 +- go.sum | 17 ++++ go/constants/attributes.go | 5 +- go/constants/settings.go | 6 +- go/handlers/appHandler.go | 16 ++-- go/handlers/peerHandler.go | 100 +++++++++++++++++++-- go/ui/gcd.go | 94 ++++++++++---------- go/ui/imageType.go | 34 +++++++ go/ui/manager.go | 156 ++++++++++++++++++++++----------- qml/widgets/ContactPicture.qml | 4 +- qml/widgets/ContactRow.qml | 6 ++ 11 files changed, 319 insertions(+), 124 deletions(-) create mode 100644 go/ui/imageType.go diff --git a/go.mod b/go.mod index f39a8dca..b16ef9c7 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,10 @@ module cwtch.im/ui go 1.12 require ( - cwtch.im/cwtch v0.3.10 + cwtch.im/cwtch v0.3.11 git.openprivacy.ca/openprivacy/connectivity v1.1.1 git.openprivacy.ca/openprivacy/log v1.0.0 github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d // indirect github.com/therecipe/qt v0.0.0-20191101232336-18864661ae4f - github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20191002095216-73192f6811d0 // indirect; Required - do not delete or `go tidy` away - golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678 // indirect + github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20191002095216-73192f6811d0 // indirect; Required - do not delete or `go tidy` away ) diff --git a/go.sum b/go.sum index 798f0f79..59d6ad7f 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,11 @@ cwtch.im/cwtch v0.3.10 h1:akrIwsc1KnLbT3K6ZIFkhmA7kI62L03EWna7Ul1vszU= cwtch.im/cwtch v0.3.10/go.mod h1:tmYeI2v0IEeBMbqzhcndXWZ2oyflhK4Afcf27+49rKU= +cwtch.im/cwtch v0.3.11 h1:2+W2w9HDQowKwEGx4oRLywmn0NzQ0Sg9JEyBdR/V1mA= +cwtch.im/cwtch v0.3.11/go.mod h1:PnMJb9CyzdrdbYjmL99pl6Nu34s6+lmeENVnGaY0hzk= cwtch.im/tapir v0.1.15 h1:XSCWOvjmNkzMT2IceFgTBXWGKtYfr3a8o+La1s10OhE= cwtch.im/tapir v0.1.15/go.mod h1:HzezugpEx+nZ3LdyDsl0w6n45IJYnOt8uqldkLWmaqs= +cwtch.im/tapir v0.1.17 h1:2jVZUe1a88tMI4aJPvRTO4Id3NN3PsM62cT5lntEChk= +cwtch.im/tapir v0.1.17/go.mod h1:HzezugpEx+nZ3LdyDsl0w6n45IJYnOt8uqldkLWmaqs= git.openprivacy.ca/openprivacy/connectivity v1.1.0/go.mod h1:4P8mirZZslKbo2zBrXXVjgEdqGwHo/6qoFBwFQW6d6E= git.openprivacy.ca/openprivacy/connectivity v1.1.1 h1:hKxBOmxP7Jdu3K1BJ93mRtKNiWUoP6YHt/o2snE2Z0w= git.openprivacy.ca/openprivacy/connectivity v1.1.1/go.mod h1:4P8mirZZslKbo2zBrXXVjgEdqGwHo/6qoFBwFQW6d6E= @@ -12,6 +16,7 @@ git.openprivacy.ca/openprivacy/log v1.0.0/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQN github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= github.com/c-bata/go-prompt v0.2.3/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca h1:Q2r7AxHdJwWfLtBZwvW621M3sPqxPc6ITv2j1FGsYpw= github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -36,6 +41,7 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= @@ -44,6 +50,7 @@ github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0= github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -57,6 +64,7 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/struCoder/pidusage v0.1.3/go.mod h1:pWBlW3YuSwRl6h7R5KbvA4N8oOqe9LjaKW5CwT1SPjI= github.com/therecipe/qt v0.0.0-20191101232336-18864661ae4f h1:06ICDSmDOBUC9jwgv44ngvyHzwudJNLa5H+rbCyDFRY= github.com/therecipe/qt v0.0.0-20191101232336-18864661ae4f/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us= @@ -71,6 +79,8 @@ github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200126204426 github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200126204426-5074eb6d8c41/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4= go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= +go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72 h1:+ELyKg6m8UBf0nPFSqD0mi7zUfwPyXo23HNjMnXPz7w= @@ -80,11 +90,15 @@ golang.org/x/crypto v0.0.0-20200206161412-a0c6ece9d31a/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200210191831-6ca56c2f2e2b/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678 h1:wCWoJcFExDgyYx2m2hpHgwz8W3+FPdfldvIgzqDIhyg= golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200320181102-891825fb96df h1:lDWgvUvNnaTnNBc/dwOty86cFeKoKWbwy2wQj0gIxbU= +golang.org/x/crypto v0.0.0-20200320181102-891825fb96df/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200320181208-1c781a10960a h1:KaxWXSFrOaE2ptiOotI+zFdzHxBsg9MW6XfCv497IRo= +golang.org/x/net v0.0.0-20200320181208-1c781a10960a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -98,12 +112,15 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200320181252-af34d8274f85 h1:fD99hd4ciR6T3oPhr2EkmuKe9oHixHx9Hj/hND89j3g= +golang.org/x/sys v0.0.0-20200320181252-af34d8274f85/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190420181800-aa740d480789 h1:FF0rjo15h51+N6642mf5S3QuplmKo2aCrJUYkHTx85s= golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/go/constants/attributes.go b/go/constants/attributes.go index 7471c265..940e3041 100644 --- a/go/constants/attributes.go +++ b/go/constants/attributes.go @@ -1,9 +1,10 @@ package constants -const Nick = "nick" +const SchemaVersion = "schemaVersion" + +const Name = "name" const LastRead = "last-read" const Picture = "picture" -const DefaultPassword = "default-password" const ProfileTypeV1DefaultPassword = "v1-defaultPassword" const ProfileTypeV1Password = "v1-userPassword" diff --git a/go/constants/settings.go b/go/constants/settings.go index 210ad84b..6e148d14 100644 --- a/go/constants/settings.go +++ b/go/constants/settings.go @@ -1,5 +1,5 @@ package constants -const BlockUnknownPeersSetting = "settings.blockunknownpeers" -const LocaleSetting = "settings.locale" -const ZoomSetting = "settings.zoom" +const BlockUnknownPeersSetting = ".blockunknownpeers" +const LocaleSetting = ".locale" +const ZoomSetting = ".zoom" diff --git a/go/handlers/appHandler.go b/go/handlers/appHandler.go index ab74b764..c7d5cb9e 100644 --- a/go/handlers/appHandler.go +++ b/go/handlers/appHandler.go @@ -91,10 +91,10 @@ func App(gcd *ui.GrandCentralDispatcher, subscribed chan bool, reloadingAccounts } case event.NewPeer: onion := e.Data[event.Identity] - peer := the.CwtchApp.GetPeer(onion) + p := the.CwtchApp.GetPeer(onion) - if tag, exists := peer.GetAttribute(app.AttributeTag); !exists || tag == "" { - peer.SetAttribute(app.AttributeTag, constants.ProfileTypeV1DefaultPassword) + if tag, exists := p.GetAttribute(app.AttributeTag); !exists || tag == "" { + p.SetAttribute(app.AttributeTag, constants.ProfileTypeV1DefaultPassword) } log.Infof("NewPeer for %v\n", onion) @@ -104,16 +104,16 @@ func App(gcd *ui.GrandCentralDispatcher, subscribed chan bool, reloadingAccounts the.CwtchApp.AddPeerPlugin(onion, plugins.NETWORKCHECK) incSubscribed := make(chan bool) - go PeerHandler(onion, gcd.GetUiManager(peer.GetOnion()), incSubscribed) + go PeerHandler(onion, gcd.GetUiManager(p.GetOnion()), incSubscribed) <-incSubscribed if e.Data[event.Status] != "running" { - peer.Listen() - peer.StartPeersConnections() - peer.StartGroupConnections() + p.Listen() + p.StartPeersConnections() + p.StartGroupConnections() } - blockUnkownPeers, exists := peer.GetAttribute(constants.BlockUnknownPeersSetting) + blockUnkownPeers, exists := p.GetAttribute(constants.BlockUnknownPeersSetting) if exists && blockUnkownPeers == "true" { the.EventBus.Publish(event.NewEvent(event.BlockUnknownPeers, map[event.Field]string{})) } diff --git a/go/handlers/peerHandler.go b/go/handlers/peerHandler.go index 0e4475dc..3799b594 100644 --- a/go/handlers/peerHandler.go +++ b/go/handlers/peerHandler.go @@ -3,16 +3,21 @@ package handlers import ( "cwtch.im/cwtch/app" "cwtch.im/cwtch/event" + "cwtch.im/cwtch/model/attr" + peerC "cwtch.im/cwtch/peer" "cwtch.im/cwtch/protocol/connections" "cwtch.im/ui/go/constants" "cwtch.im/ui/go/the" "cwtch.im/ui/go/ui" "git.openprivacy.ca/openprivacy/log" + "strconv" "time" ) func PeerHandler(onion string, uiManager ui.Manager, subscribed chan bool) { peer := the.CwtchApp.GetPeer(onion) + upgradeSchema(peer) + eventBus := the.CwtchApp.GetEventBus(onion) q := event.NewQueue() eventBus.Subscribe(event.NewMessageFromPeer, q) @@ -27,6 +32,7 @@ func PeerHandler(onion string, uiManager ui.Manager, subscribed chan bool) { eventBus.Subscribe(event.NetworkStatus, q) eventBus.Subscribe(event.ChangePasswordSuccess, q) eventBus.Subscribe(event.ChangePasswordError, q) + eventBus.Subscribe(event.NewRetValMessageFromPeer, q) subscribed <- true @@ -49,9 +55,6 @@ 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) - if peer.GetContact(e.Data[event.RemotePeer]) == nil { - peer.AddContact(e.Data[event.RemotePeer], e.Data[event.RemotePeer], false) - } case event.PeerAcknowledgement: uiManager.Acknowledge(e.Data[event.EventID]) @@ -76,7 +79,7 @@ func PeerHandler(onion string, uiManager ui.Manager, subscribed chan bool) { cxnState := connections.ConnectionStateToType[e.Data[event.ConnectionState]] // if it's not in the.PeerHandler it's new. Only add once Authed - if contact := peer.GetContact(e.Data[event.RemotePeer]); contact != nil { + if contact := peer.GetContact(e.Data[event.RemotePeer]); contact == nil { // Contact does not exist, we will add them but we won't know who they are until they are authenticated // So if we get any other state from an unknown contact we do nothing // (the next exists check will fail) @@ -90,8 +93,27 @@ func PeerHandler(onion string, uiManager ui.Manager, subscribed chan bool) { if contact := peer.GetContact(e.Data[event.RemotePeer]); contact != nil { contact.State = e.Data[event.ConnectionState] uiManager.UpdateContactStatus(contact.Onion, int(cxnState), false) - + if cxnState == connections.AUTHENTICATED { + peer.SendGetValToPeer(e.Data[event.RemotePeer], attr.PublicScope, constants.Name) + peer.SendGetValToPeer(e.Data[event.RemotePeer], attr.PublicScope, constants.Picture) + } } + case event.NewRetValMessageFromPeer: + onion := e.Data[event.RemotePeer] + scope := e.Data[event.Scope] + path := e.Data[event.Path] + val := e.Data[event.Data] + exists, _ := strconv.ParseBool(e.Data[event.Exists]) + + if exists && scope == attr.PublicScope { + switch path { + case constants.Name: + uiManager.UpdateContactDisplayName(onion, val) + case constants.Picture: + uiManager.UpdateContactPicture(onion, val) + } + } + case event.ServerStateChange: serverOnion := e.Data[event.GroupServer] state := connections.ConnectionStateToType[e.Data[event.ConnectionState]] @@ -119,6 +141,72 @@ func PeerHandler(onion string, uiManager ui.Manager, subscribed chan bool) { case event.ChangePasswordError: uiManager.ChangePasswordResponse(true) } - } } + +func upgradeSchema(p peerC.CwtchPeer) { + schemaVerVal, exists := p.GetAttribute(constants.SchemaVersion) + if !exists { + schemaVerVal = "0" + } + schemaVer, err := strconv.Atoi(schemaVerVal) + if err != nil { + schemaVer = 0 + } + + if schemaVer < 1 { + upgradeSchema1(p) + } +} + +func upgradeSchema1(p peerC.CwtchPeer) { + log.Infof("UpgradeSchema 1\n") + p.SetAttribute(attr.GetPublicScope(constants.Name), p.GetName()) + + if picture, exists := p.GetAttribute("picture"); exists { + p.SetAttribute(attr.GetPublicScope(constants.Picture), ui.ImageToString(ui.NewImage(picture, ui.TypeImageDistro))) + } + + if locale, exists := p.GetAttribute("settings.locale"); exists { + p.SetAttribute(attr.GetSettingsScope(constants.LocaleSetting), locale) + } + + if zoom, exists := p.GetAttribute("settings.zoom"); exists { + p.SetAttribute(attr.GetSettingsScope(constants.ZoomSetting), zoom) + + } + + if blockunknown, exists := p.GetAttribute("settings.blockunknownpeers"); exists { + p.SetAttribute(attr.GetSettingsScope(constants.BlockUnknownPeersSetting), blockunknown) + } + + for _, contactID := range p.GetContacts() { + if nick, exists := p.GetContactAttribute(contactID, "nick"); exists { + p.SetContactAttribute(contactID, attr.GetLocalScope(constants.Name), nick) + } + + if picture, exists := p.GetContactAttribute(contactID, "picture"); exists { + p.SetContactAttribute(contactID, attr.GetLocalScope(constants.Picture), ui.ImageToString(ui.NewImage(picture, ui.TypeImageDistro))) + } + + if lastRead, exists := p.GetContactAttribute(contactID, "last-read"); exists { + p.SetContactAttribute(contactID, attr.GetLocalScope(constants.LastRead), lastRead) + } + } + + for _, gID := range p.GetGroups() { + if nick, exists := p.GetGroupAttribute(gID, "nick"); exists { + p.SetGroupAttribute(gID, attr.GetLocalScope(constants.Name), nick) + } + + if picture, exists := p.GetGroupAttribute(gID, "picture"); exists { + p.SetGroupAttribute(gID, attr.GetLocalScope(constants.Picture), ui.ImageToString(ui.NewImage(picture, ui.TypeImageDistro))) + } + + if lastRead, exists := p.GetGroupAttribute(gID, "last-read"); exists { + p.SetGroupAttribute(gID, attr.GetLocalScope(constants.LastRead), lastRead) + } + } + + p.SetAttribute(constants.SchemaVersion, "1") +} diff --git a/go/ui/gcd.go b/go/ui/gcd.go index 93ff28f0..7114b3ec 100644 --- a/go/ui/gcd.go +++ b/go/ui/gcd.go @@ -3,6 +3,7 @@ package ui import ( "cwtch.im/cwtch/app" "cwtch.im/cwtch/event" + "cwtch.im/cwtch/model/attr" "cwtch.im/cwtch/protocol/connections" "cwtch.im/ui/go/constants" "github.com/therecipe/qt/qml" @@ -49,6 +50,7 @@ type GrandCentralDispatcher struct { // contact list stuff _ func(handle, displayName, image, server string, badge, status int, blocked bool, loading bool, lastMsgTime int) `signal:"AddContact"` _ func(handle, displayName string) `signal:"UpdateContactDisplayName"` + _ func(handle, image string) `signal:"UpdateContactPicture"` _ func(handle string, status int, loading bool) `signal:"UpdateContactStatus"` _ func(handle string, blocked bool) `signal:"UpdateContactBlocked"` _ func(handle string) `signal:"IncContactUnreadCount"` @@ -106,7 +108,6 @@ type GrandCentralDispatcher struct { _ func() `signal:"requestPeerSettings,auto"` _ func(onion, nick string) `signal:"savePeerSettings,auto"` _ func(onion, groupID string) `signal:"inviteToGroup,auto"` - _ func(onion, key, nick string) `signal:"setAttribute,auto"` _ func(onion string) `signal:"deleteContact,auto"` _ func() `signal:"allowUnknownPeers,auto"` _ func() `signal:"blockUnknownPeers,auto"` @@ -249,7 +250,7 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { this.UpdateContactStatus(group.GroupID, int(state), loading) tl := group.GetTimeline() - nick, _ := group.GetAttribute(constants.Nick) + nick := getNick(handle) updateLastReadTime(group.GroupID) if nick == "" { this.SetToolbarTitle(handle) @@ -264,7 +265,7 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { handle = tl[i].PeerID } - name := getOrDefault(tl[i].PeerID, constants.Nick, tl[i].PeerID) + name := getNick(tl[i].PeerID) image := getProfilePic(tl[i].PeerID) this.PrependMessage( @@ -289,10 +290,7 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { var nick string if contact != nil { - nick, _ = contact.GetAttribute(constants.Nick) - if nick == "" { - nick = handle - } + nick = getNick(contact.Onion) } updateLastReadTime(contact.Onion) this.SetToolbarTitle(nick) @@ -306,7 +304,7 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { from = "me" } - displayname := getOrDefault(messages[i].PeerID, constants.Nick, messages[i].PeerID) + displayname := getNick(messages[i].PeerID) image := getProfilePic(messages[i].PeerID) this.AppendMessage( @@ -325,9 +323,20 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { } func (this *GrandCentralDispatcher) requestSettings() { - zoom, _ := the.Peer.GetAttribute(constants.ZoomSetting) - locale, _ := the.Peer.GetAttribute(constants.LocaleSetting) - blockunkownpeers, _ := the.Peer.GetAttribute(constants.BlockUnknownPeersSetting) + zoom, exists := the.Peer.GetAttribute(attr.GetSettingsScope(constants.ZoomSetting)) + if !exists { + zoom = "1.0" + } + locale, exists := the.Peer.GetAttribute(attr.GetSettingsScope(constants.LocaleSetting)) + if !exists { + // TODO: pull env locale + locale = "" + } + + blockunkownpeers, exists := the.Peer.GetAttribute(attr.GetSettingsScope(constants.BlockUnknownPeersSetting)) + if !exists { + blockunkownpeers = "false" + } this.SupplySettings(zoom, locale, blockunkownpeers == "true") } @@ -338,7 +347,7 @@ func (this *GrandCentralDispatcher) saveSettings(zoom, locale string) { return } - the.Peer.SetAttribute(constants.ZoomSetting, zoom) + the.Peer.SetAttribute(attr.GetSettingsScope(constants.ZoomSetting), zoom) } func (this *GrandCentralDispatcher) requestPeerSettings() { @@ -349,19 +358,13 @@ func (this *GrandCentralDispatcher) requestPeerSettings() { return } - name, exists := contact.GetAttribute(constants.Nick) - if !exists { - 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 - } + name := getNick(contact.Onion) this.SupplyPeerSettings(contact.Onion, name, contact.Blocked) } func (this *GrandCentralDispatcher) savePeerSettings(onion, nick string) { - the.Peer.SetContactAttribute(onion, constants.Nick, nick) + the.Peer.SetContactAttribute(onion, attr.GetLocalScope(constants.Name), nick) this.UpdateContactDisplayName(onion, nick) } @@ -373,25 +376,21 @@ func (this *GrandCentralDispatcher) requestGroupSettings(groupID string) { return } - nick, _ := group.GetAttribute(constants.Nick) + nick := getNick(groupID) invite, _ := the.Peer.ExportGroup(groupID) contactaddrs := the.Peer.GetContacts() contactnames := make([]string, len(contactaddrs)) for i, contact := range contactaddrs { - name, hasname := the.Peer.GetContact(contact).GetAttribute(constants.Nick) - if hasname { - contactnames[i] = name - } else { - contactnames[i] = contact - } + contactnames[i] = getNick(contact) + } this.SupplyGroupSettings(group.GroupID, nick, group.GroupServer, invite, group.Accepted, contactnames, contactaddrs) } func (this *GrandCentralDispatcher) saveGroupSettings(groupID, nick string) { - the.Peer.SetGroupAttribute(groupID, constants.Nick, nick) + the.Peer.SetGroupAttribute(groupID, attr.GetLocalScope(constants.Name), nick) this.UpdateContactDisplayName(groupID, nick) } @@ -485,9 +484,10 @@ func (this *GrandCentralDispatcher) popup(str string) { } func (this *GrandCentralDispatcher) updateNick(onion, nick string) { - peer := the.CwtchApp.GetPeer(onion) - if peer != nil { - peer.SetName(nick) + p := the.CwtchApp.GetPeer(onion) + if p != nil { + p.SetName(nick) + p.SetAttribute(attr.GetPublicScope(constants.Name), nick) the.CwtchApp.GetEventBus(onion).Publish(event.NewEvent(event.SetProfileName, map[event.Field]string{ event.ProfileName: nick, })) @@ -503,7 +503,7 @@ func (this *GrandCentralDispatcher) createGroup(server, groupName string) { this.GetUiManager(this.selectedProfile()).AddContact(groupID) - the.Peer.SetGroupAttribute(groupID, constants.Nick, groupName) + the.Peer.SetGroupAttribute(groupID, attr.GetLocalScope(constants.Name), groupName) the.Peer.JoinServer(server) } @@ -548,28 +548,23 @@ func (this *GrandCentralDispatcher) acceptGroup(groupID string) { } } -func (this *GrandCentralDispatcher) setAttribute(onion, key, value string) { - the.Peer.SetContactAttribute(onion, key, value) - this.GetUiManager(this.selectedProfile()).UpdateContactAttribute(onion, key, value) -} - func (this *GrandCentralDispatcher) blockUnknownPeers() { - the.Peer.SetAttribute(constants.BlockUnknownPeersSetting, "true") + the.Peer.SetAttribute(attr.GetSettingsScope(constants.BlockUnknownPeersSetting), "true") the.EventBus.Publish(event.NewEvent(event.BlockUnknownPeers, map[event.Field]string{})) } func (this *GrandCentralDispatcher) allowUnknownPeers() { - the.Peer.SetAttribute(constants.BlockUnknownPeersSetting, "false") + the.Peer.SetAttribute(attr.GetSettingsScope(constants.BlockUnknownPeersSetting), "false") the.EventBus.Publish(event.NewEvent(event.AllowUnknownPeers, map[event.Field]string{})) } func (this *GrandCentralDispatcher) setLocale(locale string) { this.SetLocale_helper(locale) - the.Peer.SetAttribute(constants.LocaleSetting, locale) + the.Peer.SetAttribute(attr.GetSettingsScope(constants.LocaleSetting), locale) - zoom, _ := the.Peer.GetAttribute(constants.ZoomSetting) - blockunkownpeers, _ := the.Peer.GetAttribute(constants.BlockUnknownPeersSetting) + zoom, _ := the.Peer.GetAttribute(attr.GetSettingsScope(constants.ZoomSetting)) + blockunkownpeers, _ := the.Peer.GetAttribute(attr.GetPeerScope(constants.BlockUnknownPeersSetting)) this.SupplySettings(zoom, locale, blockunkownpeers == "true") } @@ -597,12 +592,17 @@ func (this *GrandCentralDispatcher) loadProfile(onion string) { the.Peer = the.CwtchApp.GetPeer(onion) the.EventBus = the.CwtchApp.GetEventBus(onion) - pic, exists := the.Peer.GetAttribute(constants.Picture) + picVal, exists := the.Peer.GetAttribute(attr.GetPublicScope(constants.Picture)) if !exists { - pic = RandomProfileImage(the.Peer.GetOnion()) - the.Peer.SetAttribute(constants.Picture, pic) + picVal = ImageToString(NewImage(RandomProfileImage(onion), TypeImageDistro)) + the.Peer.SetAttribute(attr.GetPublicScope(constants.Picture), picVal) } - this.UpdateMyProfile(the.Peer.GetName(), the.Peer.GetOnion(), profilePicRelativize(pic)) + pic, err := StringToImage(picVal) + if err != nil { + pic = NewImage(RandomProfileImage(onion), TypeImageDistro) + the.Peer.SetAttribute(attr.GetPublicScope(constants.Picture), ImageToString(pic)) + } + this.UpdateMyProfile(the.Peer.GetName(), the.Peer.GetOnion(), getPicturePath(pic)) contacts := the.Peer.GetContacts() for i := range contacts { @@ -617,7 +617,7 @@ func (this *GrandCentralDispatcher) loadProfile(onion string) { // load ui preferences this.RequestSettings() - locale, exists := the.Peer.GetAttribute(constants.LocaleSetting) + locale, exists := the.Peer.GetAttribute(attr.GetSettingsScope(constants.LocaleSetting)) if exists { this.SetLocale_helper(locale) } diff --git a/go/ui/imageType.go b/go/ui/imageType.go new file mode 100644 index 00000000..366ea95b --- /dev/null +++ b/go/ui/imageType.go @@ -0,0 +1,34 @@ +package ui + +import "encoding/json" + +// Image types we support +const ( + // TypeImageDistro is a reletive path to any of the distributed images in cwtch/ui in the assets folder + TypeImageDistro = "distro" + // TypeImageComposition will be an face image composed of a recipe of parts like faceType, eyeType, etc + TypeImageComposition = "composition" +) + +type image struct { + Val string + T string +} + +func NewImage(val, t string) *image { + return &image{val, t} +} + +func StringToImage(str string) (*image, error) { + var img image + err := json.Unmarshal([]byte(str), &img) + if err != nil { + return nil, err + } + return &img, nil +} + +func ImageToString(img *image) string { + bytes, _ := json.Marshal(img) + return string(bytes) +} diff --git a/go/ui/manager.go b/go/ui/manager.go index f68650a6..d5355c4d 100644 --- a/go/ui/manager.go +++ b/go/ui/manager.go @@ -3,6 +3,7 @@ package ui import ( "cwtch.im/cwtch/app" "cwtch.im/cwtch/model" + "cwtch.im/cwtch/model/attr" "cwtch.im/cwtch/protocol/connections" "cwtch.im/ui/go/constants" "cwtch.im/ui/go/the" @@ -20,7 +21,7 @@ func isPeer(id string) bool { return len(id) == 56 } -func getOrDefault(id, key, defaultVal string) string { +func getOrDefault(id, key string, defaultVal string) string { var val string var ok bool if isGroup(id) { @@ -35,7 +36,7 @@ func getOrDefault(id, key, defaultVal string) string { } } -func getWithSetDefault(id string, key, defaultVal string) string { +func getWithSetDefault(id string, key string, defaultVal string) string { var val string var ok bool if isGroup(id) { @@ -54,12 +55,34 @@ func getWithSetDefault(id string, key, defaultVal string) string { return val } +func getNick(id string) string { + if isGroup(id) { + nick, exists := the.Peer.GetGroupAttribute(id, attr.GetLocalScope(constants.Name)) + if !exists || nick == "" { + 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, 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() - lastReadStr := getWithSetDefault(id, constants.LastRead, string(nowStr)) + lastReadAttr := getWithSetDefault(id, attr.GetLocalScope(constants.LastRead), string(nowStr)) var lastRead time.Time - lastRead.UnmarshalText([]byte(lastReadStr)) + lastRead.UnmarshalText([]byte(lastReadAttr)) return lastRead } @@ -70,37 +93,57 @@ func profilePicRelativize(filename string) string { return parts[len(parts)-1] } -func initProfilePicture(id string) string { +// getProfilePic returns a string path to an image to display for hte given peer/group id +func getProfilePic(id string) string { + log.Debugf("getProfilePic for %v\n", id) if isGroup(id) { - return profilePicRelativize(getWithSetDefault(id, constants.Picture, RandomGroupImage(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 { - return profilePicRelativize(getWithSetDefault(id, constants.Picture, RandomProfileImage(id))) + 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 getPicturePath(NewImage("fontawesome/regular/user.svg", TypeImageDistro)) } } -// 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 profilePicRelativize(pic) - } - } else { - if pic, exists := the.Peer.GetContactAttribute(id, constants.Picture); !exists { - return profilePicRelativize(RandomProfileImage(id)) - } else { - return pic - } +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, constants.LastRead, string(lastRead)) + the.Peer.SetGroupAttribute(id, attr.GetLocalScope(constants.LastRead), string(lastRead)) } else { - the.Peer.SetContactAttribute(id, constants.LastRead, string(lastRead)) + the.Peer.SetContactAttribute(id, attr.GetLocalScope(constants.LastRead), string(lastRead)) } } @@ -118,23 +161,27 @@ func countUnread(messages []model.Message, lastRead time.Time) int { // AddProfile adds a new profile to the UI func AddProfile(gcd *GrandCentralDispatcher, handle string) { - peer := the.CwtchApp.GetPeer(handle) - if peer != nil { - nick := peer.GetName() - if nick == "" { + p := the.CwtchApp.GetPeer(handle) + if p != nil { + nick, exists := p.GetAttribute(attr.GetPublicScope(constants.Name)) + if !exists { nick = handle - peer.SetAttribute(constants.Nick, nick) } - pic, ok := peer.GetAttribute(constants.Picture) + picVal, ok := p.GetAttribute(attr.GetPublicScope(constants.Picture)) if !ok { - pic = RandomProfileImage(handle) - peer.SetAttribute(constants.Picture, pic) + picVal = ImageToString(NewImage(RandomProfileImage(handle), TypeImageDistro)) } + pic, err := StringToImage(picVal) + if err != nil { + pic = NewImage(RandomProfileImage(handle), TypeImageDistro) + } + picPath := getPicturePath(pic) - tag, _ := peer.GetAttribute(app.AttributeTag) - log.Infof("AddProfile %v %v %v %v\n", handle, nick, profilePicRelativize(pic), tag) - gcd.AddProfile(handle, nick, profilePicRelativize(pic), tag) + tag, _ := p.GetAttribute(app.AttributeTag) + + log.Infof("AddProfile %v %v %v %v\n", handle, nick, picPath, tag) + gcd.AddProfile(handle, nick, picPath, tag) } } @@ -157,6 +204,7 @@ type Manager interface { ReloadProfiles() UpdateContactDisplayName(handle string, name string) + UpdateContactPicture(handle string, picVal string) UpdateContactStatus(handle string, status int, loading bool) UpdateContactAttribute(handle, key, value string) @@ -184,40 +232,32 @@ func getLastMessageTime(tl *model.Timeline) int { } // AddContact adds a new contact to the ui for this manager's profile -func (this *manager) AddContact(Handle string) { +func (this *manager) AddContact(handle string) { this.gcd.DoIfProfile(this.profile, func() { - if isGroup(Handle) { - group := the.Peer.GetGroup(Handle) + 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 - } + picture := getProfilePic(handle) - this.gcd.AddContact(Handle, nick, picture, group.GroupServer, unread, int(connections.ConnectionStateToType[group.State]), false, false, getLastMessageTime(&group.Timeline)) + this.gcd.AddContact(handle, getNick(handle), 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) + } else if !isPeer(handle) { + log.Errorf("sorry, unable to handle AddContact(%v)", handle) debug.PrintStack() return } - contact := the.Peer.GetContact(Handle) + 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 - } + picture := getProfilePic(handle) - this.gcd.AddContact(Handle, nick, picture, "", unread, int(connections.ConnectionStateToType[contact.State]), contact.Blocked, false, getLastMessageTime(&contact.Timeline)) + this.gcd.AddContact(handle, getNick(handle), picture, "", unread, int(connections.ConnectionStateToType[contact.State]), contact.Blocked, false, getLastMessageTime(&contact.Timeline)) } }) } @@ -238,7 +278,7 @@ func (this *manager) AddSendMessageError(peer string, signature string, err stri 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) + nick := getNick(handle) image := getProfilePic(handle) // If we have this group loaded already @@ -270,6 +310,16 @@ func (this *manager) UpdateContactDisplayName(handle string, name string) { }) } +// UpdateContactPicture updates a contact's picture in the contact list and conversations +func (this *manager) UpdateContactPicture(handle string, picVal string) { + this.gcd.DoIfProfile(this.profile, func() { + pic, err := StringToImage(picVal) + if err == nil { + this.gcd.UpdateContactPicture(handle, getPicturePath(pic)) + } + }) +} + // 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() { diff --git a/qml/widgets/ContactPicture.qml b/qml/widgets/ContactPicture.qml index 03ad82b6..ca7b9925 100644 --- a/qml/widgets/ContactPicture.qml +++ b/qml/widgets/ContactPicture.qml @@ -25,13 +25,13 @@ Item { id: mainImage width: baseWidth height: baseWidth - color: highlight ? windowItem.cwtch_dark_color: "#FFFFFF" + color: "#350052" //: "#FFFFFF" //windowItem.cwtch_dark_color: "#FFFFFF" radius: width / 2 Rectangle { width: highlight ? baseWidth - 4 : baseWidth height: highlight ? baseWidth - 4 : baseWidth - color: button ? windowItem.cwtch_dark_color: "#FFFFFF" + color: "#350052" //: "#FFFFFF" // windowItem.cwtch_dark_color: "#FFFFFF" radius: width / 2 anchors.centerIn:parent diff --git a/qml/widgets/ContactRow.qml b/qml/widgets/ContactRow.qml index f25f21e8..e35dd7a3 100644 --- a/qml/widgets/ContactRow.qml +++ b/qml/widgets/ContactRow.qml @@ -244,6 +244,12 @@ Item { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY } } + onUpdateContactPicture: function(_handle, _image) { + if (handle == _handle) { + image = _image + } + } + onIncContactUnreadCount: function(handle) { if (handle == _handle && gcd.selectedConversation != handle) { badge++ -- 2.25.1