Browse Source

initial commit

erinn 4 months ago
parent
commit
8a9ba6d154

+ 0 - 237
gcd.go

@@ -1,237 +0,0 @@
-package main
-
-import (
-	"cwtch.im/cwtch/model"
-	"encoding/base32"
-	"github.com/therecipe/qt/core"
-	"log"
-	"strings"
-	"time"
-	"fmt"
-)
-
-var TIME_FORMAT = "Mon 3:04pm"
-
-type GrandCentralDispatcher struct {
-	core.QObject
-
-	currentOpenConversation string `property:"currentOpenConversation"`
-	themeScale float32 `property:"themeScale"`
-
-	// messages pane stuff
-	_ func(from, message, displayname string, mID uint, ts, source string) `signal:"AppendMessage"`
-	_ func()                                          `signal:"ClearMessages"`
-	_ func()                                          `signal:"ResetMessagePane"`
-	_ func(uint)                                      `signal:"Acknowledged"`
-
-	// contact list stuff
-	_ func(onion string, num int)                          `signal:"SetUnread"`
-	_ func(onion string, status int)                       `signal:"SetConnectionStatus"`
-	_ func(name, onion, server, image, badge string, trusted bool) `signal:"AddContact"`
-	_ func(onion string)                                   `signal:"MarkTrusted"`
-
-	// profile-area stuff
-	_ func(name, onion, image string) `signal:"UpdateMyProfile"`
-	_ func(status int, str string)    `signal:"TorStatus"`
-
-	// other stuff i can't ontologize atm
-	_ func(str string) `signal:"InvokePopup"`
-
-	// exfiltrated signals (written in go, below)
-	_ func(message string, mid uint) `signal:"sendMessage,auto"`
-	_ func(onion string)             `signal:"loadMessagesPane,auto"`
-	_ func(signal string)            `signal:"broadcast,auto"` // convenience relay signal
-	_ func(str string)               `signal:"importString,auto"`
-	_ func(str string)               `signal:"popup,auto"`
-	_ func(nick string)              `signal:"updateNick,auto"`
-}
-
-func (this *GrandCentralDispatcher) sendMessage(message string, mID uint) {
-	if len(message) > 65530 {
-		gcd.InvokePopup("message is too long")
-		return
-	}
-
-	if gcd.currentOpenConversation == "" {
-		return
-	}
-
-	if len(gcd.currentOpenConversation) == 32 { // SEND TO GROUP
-		if !peer.GetGroup(gcd.currentOpenConversation).Accepted {
-			peer.GetGroup(gcd.currentOpenConversation).Accepted = true
-			peer.Save()
-		}
-
-		peer.SendMessageToGroup(gcd.currentOpenConversation, message)
-		return
-	}
-
-	// TODO: require explicit invite accept/reject instead of implicitly trusting on send
-	if !peer.GetContact(gcd.currentOpenConversation).Trusted {
-		peer.GetContact(gcd.currentOpenConversation).Trusted = true
-		peer.Save()
-		gcd.MarkTrusted(gcd.currentOpenConversation)
-	}
-
-	select { // 1 weird trick to do a non-blocking send. this means the user can only send a limited number of messages
-	// before the channel buffer fills. TODO: stop the user from sending if the buffer is full
-	case outgoingMessages <- Message{gcd.currentOpenConversation, message, true, mID, time.Now()}:
-	default:
-	}
-
-	DeliverMessageToUI(gcd.currentOpenConversation, gcd.currentOpenConversation, "", message,  mID, true, time.Now())
-}
-
-func (this *GrandCentralDispatcher) loadMessagesPane(onion string) {
-	gcd.ClearMessages()
-	gcd.currentOpenConversation = onion
-	gcd.SetUnread(onion, 0)
-
-	if len(onion) == 32 { // LOAD GROUP
-		log.Printf("LOADING GROUP %s", onion)
-		tl := peer.GetGroup(onion).GetTimeline()
-		log.Printf("messages: %d", len(tl))
-		for i := range tl {
-			var handle string
-			if tl[i].PeerID == peer.GetProfile().Onion {
-				handle = "me"
-			} else {
-				handle = tl[i].PeerID
-			}
-			var name string
-			var exists bool
-			name, exists = peer.GetProfile().GetCustomAttribute(tl[i].PeerID + "_name")
-			if !exists || name == "" {
-				name = tl[i].PeerID[:16] + "..."
-			}
-			gcd.AppendMessage(handle, tl[i].Message, name, 0, tl[i].Timestamp.Format(TIME_FORMAT), randomProfileImage(tl[i].PeerID))
-		}
-		return
-	} // ELSE LOAD CONTACT
-
-	_, exists := contactMgr[onion]
-	if exists { // (if not, they haven't been accepted as a contact yet)
-		contactMgr[onion].Unread = 0
-
-		messages := contactMgr[onion].Messages
-		for i := range messages {
-			from := messages[i].With
-			if messages[i].FromMe {
-				from = "me"
-			}
-			gcd.AppendMessage(from, messages[i].Message, "", messages[i].MessageID, messages[i].Timestamp.Format(TIME_FORMAT), randomProfileImage(onion))
-		}
-	}
-}
-
-func (this *GrandCentralDispatcher) broadcast(signal string) {
-	switch signal {
-	default:
-		log.Printf("unhandled broadcast signal: %v", signal)
-	case "ResetMessagePane":
-		gcd.ResetMessagePane()
-	}
-}
-
-func (this *GrandCentralDispatcher) importString(str string) {
-	if len(str) < 5 {
-		log.Printf("ignoring short string")
-		return
-	}
-
-	log.Printf("importing: %s\n", str)
-	onion := str
-	name := onion
-	str = strings.TrimSpace(str)
-
-	//eg: torv3JFDWkXExBsZLkjvfkkuAxHsiLGZBk0bvoeJID9ItYnU=EsEBCiBhOWJhZDU1OTQ0NWI3YmM2N2YxYTM5YjkzMTNmNTczNRIgpHeNaG+6jy750eDhwLO39UX4f2xs0irK/M3P6mDSYQIaOTJjM2ttb29ibnlnaGoyenc2cHd2N2Q1N3l6bGQ3NTNhdW8zdWdhdWV6enB2ZmFrM2FoYzRiZHlkCiJAdVSSVgsksceIfHe41OJu9ZFHO8Kwv3G6F5OK3Hw4qZ6hn6SiZjtmJlJezoBH0voZlCahOU7jCOg+dsENndZxAA==
-	if str[0:5] == "torv3" { // GROUP INVITE
-		groupID, err := peer.ImportGroup(str)
-		if err != nil {
-			gcd.InvokePopup("not a valid group invite")
-			return
-		}
-
-		group := peer.GetGroup(groupID)
-		peer.JoinServer(group.GroupServer)
-		peer.Save()
-		fmt.Printf("imported groupid=%s server=%s", groupID, group.GroupServer)
-
-		return
-	}
-
-	if strings.Contains(str, " ") { // usually people prepend spaces and we don't want it going into the name (use ~ for that)
-		parts := strings.Split(strings.TrimSpace(str), " ")
-		str = parts[len(parts)-1]
-	}
-
-	if strings.Contains(str, "~") {
-		parts := strings.Split(str, "~")
-		onion = parts[len(parts)-1]
-		name = strings.Join(parts[:len(parts)-1], " ")
-	}
-
-	if len(onion) != 56 {
-		gcd.InvokePopup("invalid format")
-		return
-	}
-
-	name = strings.TrimSpace(name)
-	if name == "" {
-		gcd.InvokePopup("empty name")
-		return
-	}
-
-	if len(name) > 32 {
-		name = name[:32] //TODO: better strategy for long names?
-	}
-
-	decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion[:56]))
-	if err != nil {
-		log.Printf("%v", err)
-		gcd.InvokePopup("bad format. missing characters?")
-		return
-	}
-
-	_, exists := peer.GetProfile().GetCustomAttribute(name + "_onion")
-	if exists {
-		gcd.InvokePopup("can't re-use names :(")
-		return
-	}
-
-	_, exists = peer.GetProfile().GetCustomAttribute(onion + "_name")
-	if exists {
-		gcd.InvokePopup("already have this contact")
-		return //TODO: bring them to the duplicate
-	}
-
-	pp := model.PublicProfile{
-		name,
-		decodedPub[:32],
-		false,
-		false,
-		onion}
-
-	if peer == nil {
-		log.Printf("[!!!] peer is nil?!?!?")
-	}
-
-	log.Printf("adding %v <%v>", name, onion)
-	peer.GetProfile().Contacts[onion] = &pp
-	peer.GetProfile().SetCustomAttribute(onion+"_name", name)
-	peer.GetProfile().SetCustomAttribute(name+"_onion", onion)
-	peer.GetProfile().TrustPeer(onion)
-	peer.Save()
-	go peer.PeerWithOnion(onion)
-	//contactMgr[onion] = &Contact{[]Message{}, 0, 0}
-	//gcd.AddContact(name, onion, randomProfileImage(onion), "0")
-}
-
-func (this *GrandCentralDispatcher) popup(str string) {
-	gcd.InvokePopup(str)
-}
-
-func (this *GrandCentralDispatcher) updateNick(nick string) {
-	peer.GetProfile().Name = nick
-	peer.Save()
-}

+ 39 - 0
go/characters/cwtchlistener.go

@@ -0,0 +1,39 @@
+package characters
+
+import (
+	"cwtch.im/cwtch/model"
+		"bounce/go/the"
+	"bounce/go/gobjects"
+)
+
+func CwtchListener(callback func(message *gobjects.Message), groupID string, channel chan model.Message) {
+	for {
+		m := <-channel
+
+		name := m.PeerID
+		if name == the.Peer.GetProfile().Onion {
+			name = "me"
+		} else {
+			var exists bool // lol this is a golang antifeature
+			name, exists = the.Peer.GetContact(m.PeerID).GetAttribute("name")
+			if !exists {
+				name = ""
+			}
+		}
+
+		if name == "" {
+			name = m.PeerID[:16] + "..."
+		}
+
+		callback(&gobjects.Message{
+			groupID,
+			m.PeerID,
+			name,
+			m.Message,
+			"",
+			m.PeerID == the.Peer.GetProfile().Onion,
+			0,
+			m.Timestamp,
+		})
+	}
+}

+ 21 - 0
go/characters/grouppoller.go

@@ -0,0 +1,21 @@
+package characters
+
+import (
+	"time"
+	"bounce/go/the"
+	"bounce/go/gobjects"
+)
+
+func GroupPoller(getContact func(string) *gobjects.Contact, updateContact func(string)) {
+	for {
+		time.Sleep(time.Second * 4)
+
+		servers := the.Peer.GetServers()
+		groups := the.Peer.GetGroups()
+		for i := range groups {
+			group := the.Peer.GetGroup(groups[i])
+			getContact(group.GroupID).Status = int(servers[group.GroupServer])
+			updateContact(group.GroupID)
+		}
+	}
+}

+ 39 - 0
go/characters/postmanpat.go

@@ -0,0 +1,39 @@
+package characters
+
+import (
+	"bounce/go/the"
+	"git.openprivacy.ca/openprivacy/libricochet-go/channels"
+	"log"
+	"bounce/go/gobjects"
+)
+
+func PostmanPat(messages chan gobjects.Letter) {
+	postOffice := make(map[string]chan gobjects.Letter)
+
+	for {
+		m := <-messages
+
+		_, found := postOffice[m.To]
+		if !found {
+			postOffice[m.To] = make(chan gobjects.Letter, 100)
+			go andHisBlackAndWhiteCat(postOffice[m.To])
+		}
+
+		postOffice[m.To] <- m
+	}
+}
+
+func andHisBlackAndWhiteCat(incomingMessages chan gobjects.Letter) {
+	for {
+		m := <-incomingMessages
+		connection := the.Peer.PeerWithOnion(m.To)
+		connection.WaitTilAuthenticated()
+		connection.DoOnChannel("im.ricochet.chat", channels.Outbound, func(channel *channels.Channel) {
+			chatchannel, ok := channel.Handler.(*channels.ChatChannel)
+			if ok {
+				log.Printf("Sending packet")
+				the.AcknowledgementIDs[chatchannel.SendMessage(m.Message)] = m.MID
+			}
+		})
+	}
+}

+ 49 - 0
go/characters/presencepoller.go

@@ -0,0 +1,49 @@
+package characters
+
+import (
+	"time"
+	"bounce/go/the"
+		"bounce/go/cwutil"
+	"bounce/go/gobjects"
+)
+
+func PresencePoller(getContact func(string) *gobjects.Contact, addContact func(contact *gobjects.Contact), updateContact func(string)) { // TODO: make this subscribe-able in ricochet
+	time.Sleep(time.Second * 4)
+	for {
+		contacts := the.Peer.GetContacts()
+		for i := range contacts {
+			ct := getContact(contacts[i])
+			if ct == nil { // new contact has attempted to connect with us, treat it as an invite
+				toc := the.Peer.GetContact(contacts[i])
+				c, _ := the.Peer.GetProfile().GetContact(contacts[i])
+				addContact(&gobjects.Contact{
+					toc.Onion,
+					toc.Name,
+					cwutil.RandomProfileImage(toc.Onion),
+					"",
+					0,
+					0,
+					c.Trusted,
+				})
+				c.SetAttribute("name", c.Name)
+				the.CwtchApp.SaveProfile(the.Peer)
+			}
+
+			cxnState, found := the.Peer.GetPeers()[contacts[i]]
+			if !found {
+				c2 := getContact(contacts[i])
+				if c2 != nil && c2.Status != -2 {
+					c2.Status = -2
+					updateContact(contacts[i])
+				}
+			} else {
+				c2 := getContact(contacts[i])
+				if c2 != nil && c2.Status != int(cxnState) {
+					c2.Status = int(cxnState)
+					updateContact(contacts[i])
+				}
+			}
+		}
+		time.Sleep(time.Second * 4)
+	}
+}

+ 35 - 0
go/characters/torstatuspoller.go

@@ -0,0 +1,35 @@
+package characters
+
+import (
+		"git.openprivacy.ca/openprivacy/asaur"
+	"strconv"
+	"time"
+)
+
+func TorStatusPoller(setTorStatus func(int, string)) {
+	for {
+		time.Sleep(time.Second)
+		//todo: this should use a config manager
+		//todo: also, try dialing the proxy to differentiate tor not running vs control port not configured
+		rawStatus, err := asaur.GetInfo("localhost:9051", "tcp4", "", "status/bootstrap-phase")
+		if err != nil {
+			setTorStatus(0, "can't find tor. is it running? is the controlport configured?")
+			continue
+		}
+
+		status := asaur.ParseBootstrapPhase(rawStatus)
+		progress, _ := strconv.Atoi(status["PROGRESS"])
+
+		if status["TAG"] == "done" {
+			setTorStatus(3, "tor appears to be running just fine!")
+			continue
+		}
+
+		if progress == 0 {
+			setTorStatus(1, "tor is trying to start up")
+			continue
+		}
+
+		setTorStatus(2, status["SUMMARY"])
+	}
+}

+ 3 - 0
go/constants/style.go

@@ -0,0 +1,3 @@
+package constants
+
+var TIME_FORMAT = "Mon 3:04pm"

+ 24 - 9
chatchannellistener.go

@@ -1,20 +1,26 @@
-package main
+package cwtchthings
 
 import (
 	"git.openprivacy.ca/openprivacy/libricochet-go/application"
-		"git.openprivacy.ca/openprivacy/libricochet-go/channels"
+	"git.openprivacy.ca/openprivacy/libricochet-go/channels"
 	"time"
-	)
+	"bounce/go/the"
+	"bounce/go/cwutil"
+	"bounce/go/gobjects"
+)
 
 type ChatChannelListener struct {
 	rai *application.ApplicationInstance
 	ra  *application.RicochetApplication
+	addMessage func(*gobjects.Message)
+	acknowledged func(uint)
 }
 
-
-func (this *ChatChannelListener) Init(rai *application.ApplicationInstance, ra *application.RicochetApplication) {
+func (this *ChatChannelListener) Init(rai *application.ApplicationInstance, ra *application.RicochetApplication, addMessage func(*gobjects.Message), acknowledged func(uint)) {
 	this.rai = rai
 	this.ra = ra
+	this.addMessage = addMessage
+	this.acknowledged = acknowledged
 }
 
 // We always want bidirectional chat channels
@@ -32,14 +38,23 @@ func (this *ChatChannelListener) OpenInbound() {
 }
 
 func (this *ChatChannelListener) ChatMessage(messageID uint32, when time.Time, message string) bool {
-	DeliverMessageToUI(this.rai.RemoteHostname, this.rai.RemoteHostname, "", message, uint(messageID), false, when)
-	go func() {
+	this.addMessage(&gobjects.Message{
+		this.rai.RemoteHostname,
+		this.rai.RemoteHostname,
+		"",
+		message,
+		cwutil.RandomProfileImage(this.rai.RemoteHostname),
+		false,
+		int(messageID),
+		when,
+	})
+	go func() { // TODO: this is probably no longer necessary. check later
 		time.Sleep(time.Second)
-		peer.Save()
+		the.CwtchApp.SaveProfile(the.Peer)
 	}()
 	return true
 }
 
 func (this *ChatChannelListener) ChatMessageAck(messageID uint32, accepted bool) {
-	gcd.Acknowledged(acknowledgementIDs[messageID])
+	this.acknowledged(the.AcknowledgementIDs[messageID])
 }

+ 29 - 0
go/cwutil/utils.go

@@ -0,0 +1,29 @@
+package cwutil
+
+import (
+	"encoding/base32"
+	"encoding/hex"
+	"fmt"
+	"strings"
+)
+
+// temporary until we do real picture selection
+func RandomProfileImage(onion string) string {
+	choices := []string{"001-centaur", "002-kraken", "003-dinosaur", "004-tree-1", "005-hand", "006-echidna", "007-robot", "008-mushroom", "009-harpy", "010-phoenix", "011-dragon-1", "012-devil", "013-troll", "014-alien", "015-minotaur", "016-madre-monte", "017-satyr", "018-karakasakozou", "019-pirate", "020-werewolf", "021-scarecrow", "022-valkyrie", "023-curupira", "024-loch-ness-monster", "025-tree", "026-cerberus", "027-gryphon", "028-mermaid", "029-vampire", "030-goblin", "031-yeti", "032-leprechaun", "033-medusa", "034-chimera", "035-elf", "036-hydra", "037-cyclops", "038-pegasus", "039-narwhal", "040-woodcutter", "041-zombie", "042-dragon", "043-frankenstein", "044-witch", "045-fairy", "046-genie", "047-pinocchio", "048-ghost", "049-wizard", "050-unicorn"}
+	barr, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
+	if err != nil || len(barr) != 35 {
+		fmt.Printf("error: %v %v %v\n", onion, err, barr)
+		return "qrc:/qml/images/extra/openprivacy.png"
+	}
+	return "qrc:/qml/images/profiles/" + choices[int(barr[33])%len(choices)] + ".png"
+}
+
+func RandomGroupImage(handle string) string {
+	choices := []string{"001-borobudur", "002-opera-house", "003-burj-al-arab", "004-chrysler", "005-acropolis", "006-empire-state-building", "007-temple", "008-indonesia-1", "009-new-zealand", "010-notre-dame", "011-space-needle", "012-seoul", "013-mosque", "014-milan", "015-statue", "016-pyramid", "017-cologne", "018-brandenburg-gate", "019-berlin-cathedral", "020-hungarian-parliament", "021-buckingham", "022-thailand", "023-independence", "024-angkor-wat", "025-vaticano", "026-christ-the-redeemer", "027-colosseum", "028-golden-gate-bridge", "029-sphinx", "030-statue-of-liberty", "031-cradle-of-humankind", "032-istanbul", "033-london-eye", "034-sagrada-familia", "035-tower-bridge", "036-burj-khalifa", "037-washington", "038-big-ben", "039-stonehenge", "040-white-house", "041-ahu-tongariki", "042-capitol", "043-eiffel-tower", "044-church-of-the-savior-on-spilled-blood", "045-arc-de-triomphe", "046-windmill", "047-louvre", "048-torii-gate", "049-petronas", "050-matsumoto-castle", "051-fuji", "052-temple-of-heaven", "053-pagoda", "054-chichen-itza", "055-forbidden-city", "056-merlion", "057-great-wall-of-china", "058-taj-mahal", "059-pisa", "060-indonesia"}
+	barr, err := hex.DecodeString(handle)
+	if err != nil || len(barr) == 0 {
+		fmt.Printf("error: %v %v %v\n", handle, err, barr)
+		return "qrc:/qml/images/extra/openprivacy.png"
+	}
+	return "qrc:/qml/images/servers/" + choices[int(barr[0])%len(choices)] + ".png"
+}

+ 11 - 0
go/gobjects/contact.go

@@ -0,0 +1,11 @@
+package gobjects
+
+type Contact struct {
+	Handle      string
+	DisplayName string
+	Image       string
+	Server      string
+	Badge       int
+	Status      int
+	Trusted     bool
+}

+ 8 - 0
go/gobjects/letter.go

@@ -0,0 +1,8 @@
+package gobjects
+
+// a Letter is a very simple message object passed to us from the UI
+type Letter struct {
+	To, Message string
+	MID         uint
+}
+

+ 14 - 0
go/gobjects/message.go

@@ -0,0 +1,14 @@
+package gobjects
+
+import "time"
+
+type Message struct {
+	Handle      string
+	From        string
+	DisplayName string
+	Message     string
+	Image       string
+	FromMe      bool
+	MessageID   int
+	Timestamp   time.Time
+}

+ 303 - 0
go/gothings/gcd.go

@@ -0,0 +1,303 @@
+package gothings
+
+import (
+	"bounce/go/constants"
+	"bounce/go/cwutil"
+
+	"encoding/base32"
+	"fmt"
+	"github.com/therecipe/qt/core"
+	"log"
+	"strings"
+	"time"
+			"bounce/go/the"
+	"cwtch.im/cwtch/model"
+	"bounce/go/characters"
+	"bounce/go/gobjects"
+)
+
+type GrandCentralDispatcher struct {
+	core.QObject
+
+	OutgoingMessages chan gobjects.Letter
+	UIState InterfaceState
+
+	_ string  `property:"currentOpenConversation"`
+	_ float32 `property:"themeScale"`
+
+	// contact list stuff
+	_ func(handle, displayName, image, server string, badge, status int, trusted bool) `signal:"AddContact"`
+	_ func(handle, displayName, image, server string, badge, status int, trusted bool) `signal:"UpdateContact"`
+
+	// messages pane stuff
+	_ func(handle, from, displayName, message, image string, mID uint, fromMe bool, ts string) `signal:"AppendMessage"`
+	_ func()                                                               `signal:"ClearMessages"`
+	_ func()                                                               `signal:"ResetMessagePane"`
+	_ func(mID uint)                                                       `signal:"Acknowledged"`
+
+	// profile-area stuff
+	_ func(name, onion, image string) `signal:"UpdateMyProfile"`
+	_ func(status int, str string)    `signal:"TorStatus"`
+
+	// other stuff i can't ontologize atm
+	_ func(str string) `signal:"InvokePopup"`
+	_ func(name, server, invitation string) `signal:"SupplyGroupSettings"`
+
+	// signals emitted from the ui (written in go, below)
+	_ func(message string, mid uint) `signal:"sendMessage,auto"`
+	_ func(onion string)             `signal:"loadMessagesPane,auto"`
+	_ func(signal string)            `signal:"broadcast,auto"` // convenience relay signal
+	_ func(str string)               `signal:"importString,auto"`
+	_ func(str string)               `signal:"popup,auto"`
+	_ func(nick string)              `signal:"updateNick,auto"`
+	_ func(server, groupName string) `signal:"createGroup,auto"`
+	_ func()                         `signal:"requestGroupSettings,auto"`
+}
+
+func (this *GrandCentralDispatcher) sendMessage(message string, mID uint) {
+	if len(message) > 65530 {
+		this.InvokePopup("message is too long")
+		return
+	}
+
+	if this.CurrentOpenConversation() == "" {
+		this.InvokePopup("ui error")
+		return
+	}
+
+	if len(this.CurrentOpenConversation()) == 32 { // SEND TO GROUP
+		if !the.Peer.GetGroup(this.CurrentOpenConversation()).Accepted {
+			the.Peer.GetGroup(this.CurrentOpenConversation()).Accepted = true
+			the.CwtchApp.SaveProfile(the.Peer)
+			c := this.UIState.GetContact(this.CurrentOpenConversation())
+			c.Trusted = true
+			this.UIState.UpdateContact(c.Handle)
+		}
+
+		the.Peer.SendMessageToGroup(this.CurrentOpenConversation(), message)
+		return
+	}
+
+	// TODO: require explicit invite accept/reject instead of implicitly trusting on send
+	if !this.UIState.GetContact(this.CurrentOpenConversation()).Trusted {
+		this.UIState.GetContact(this.CurrentOpenConversation()).Trusted = true
+		this.UIState.UpdateContact(this.CurrentOpenConversation())
+	}
+
+	select { // 1 weird trick to do a non-blocking send. this means the user can only send a limited number of messages
+	// before the channel buffer fills. TODO: stop the user from sending if the buffer is full
+	case this.OutgoingMessages <- gobjects.Letter{this.CurrentOpenConversation(), message, mID}:
+	default:
+	}
+
+	this.UIState.AddMessage(&gobjects.Message{
+		this.CurrentOpenConversation(),
+		"me",
+		"",
+		message,
+		"",
+		true,
+		int(mID),
+		time.Now(),
+	})
+}
+
+func (this *GrandCentralDispatcher) loadMessagesPane(handle string) {
+	this.ClearMessages()
+	this.SetCurrentOpenConversation(handle)
+	c := this.UIState.GetContact(handle)
+	c.Badge = 0
+	this.UIState.UpdateContact(handle)
+
+	if len(handle) == 32 { // LOAD GROUP
+		log.Printf("LOADING GROUP %s", handle)
+		tl := the.Peer.GetGroup(handle).GetTimeline()
+		log.Printf("messages: %d", len(tl))
+		for i := range tl {
+			if tl[i].PeerID == the.Peer.GetProfile().Onion {
+				handle = "me"
+			} else {
+				handle = tl[i].PeerID
+			}
+			var name string
+			var exists bool
+			name, exists = the.Peer.GetProfile().GetCustomAttribute(tl[i].PeerID + "_name")
+			if !exists || name == "" {
+				name = tl[i].PeerID[:16] + "..."
+			}
+			this.AppendMessage(
+				handle,
+				tl[i].PeerID,
+				name,
+				tl[i].Message,
+				cwutil.RandomProfileImage(tl[i].PeerID),
+				0,
+				tl[i].PeerID == the.Peer.GetProfile().Onion,
+				tl[i].Timestamp.Format(constants.TIME_FORMAT),
+			)
+		}
+		return
+	} // ELSE LOAD CONTACT
+
+	messages := this.UIState.GetMessages(handle)
+	for i := range messages {
+		from := messages[i].From
+		if messages[i].FromMe {
+			from = "me"
+		}
+		this.AppendMessage(
+			messages[i].Handle,
+			from,
+			messages[i].DisplayName,
+			messages[i].Message,
+			cwutil.RandomProfileImage(handle),
+			uint(messages[i].MessageID),
+			messages[i].FromMe,
+			messages[i].Timestamp.Format(constants.TIME_FORMAT),
+		)
+	}
+}
+
+func (this *GrandCentralDispatcher) requestGroupSettings() {
+	log.Printf("requestGroupSettings()")
+	group := the.Peer.GetGroup(this.CurrentOpenConversation())
+	nick, _ := group.GetAttribute("nick")
+	invite, _ := the.Peer.ExportGroup(this.CurrentOpenConversation())
+	this.SupplyGroupSettings(nick, group.GroupServer, invite)
+}
+
+func (this *GrandCentralDispatcher) broadcast(signal string) {
+	switch signal {
+	default:
+		log.Printf("unhandled broadcast signal: %v", signal)
+	case "ResetMessagePane":
+		this.ResetMessagePane()
+	}
+}
+
+func (this *GrandCentralDispatcher) importString(str string) {
+	if len(str) < 5 {
+		log.Printf("ignoring short string")
+		return
+	}
+
+	log.Printf("importing: %s\n", str)
+	onion := str
+	name := onion
+	str = strings.TrimSpace(str)
+
+	//eg: torv3JFDWkXExBsZLkjvfkkuAxHsiLGZBk0bvoeJID9ItYnU=EsEBCiBhOWJhZDU1OTQ0NWI3YmM2N2YxYTM5YjkzMTNmNTczNRIgpHeNaG+6jy750eDhwLO39UX4f2xs0irK/M3P6mDSYQIaOTJjM2ttb29ibnlnaGoyenc2cHd2N2Q1N3l6bGQ3NTNhdW8zdWdhdWV6enB2ZmFrM2FoYzRiZHlkCiJAdVSSVgsksceIfHe41OJu9ZFHO8Kwv3G6F5OK3Hw4qZ6hn6SiZjtmJlJezoBH0voZlCahOU7jCOg+dsENndZxAA==
+	if str[0:5] == "torv3" { // GROUP INVITE
+		groupID, err := the.Peer.ImportGroup(str)
+		if err != nil {
+			this.InvokePopup("not a valid group invite")
+			return
+		}
+
+		group := the.Peer.GetGroup(groupID)
+		the.Peer.JoinServer(group.GroupServer)
+		the.CwtchApp.SaveProfile(the.Peer)
+		this.UIState.AddContact(&gobjects.Contact{
+			groupID[:12],
+			groupID,
+			cwutil.RandomGroupImage(groupID),
+			group.GroupServer,
+			0,
+			0,
+			true,
+		})
+		fmt.Printf("imported groupid=%s server=%s", groupID, group.GroupServer)
+
+		return
+	}
+
+	if strings.Contains(str, " ") { // usually people prepend spaces and we don't want it going into the name (use ~ for that)
+		parts := strings.Split(strings.TrimSpace(str), " ")
+		str = parts[len(parts)-1]
+	}
+
+	if strings.Contains(str, "~") {
+		parts := strings.Split(str, "~")
+		onion = parts[len(parts)-1]
+		name = strings.Join(parts[:len(parts)-1], " ")
+	}
+
+	if len(onion) != 56 {
+		this.InvokePopup("invalid format")
+		return
+	}
+
+	name = strings.TrimSpace(name)
+	if name == "" {
+		this.InvokePopup("empty name")
+		return
+	}
+
+	if len(name) > 32 {
+		name = name[:32] //TODO: better strategy for long names?
+	}
+
+	_, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion[:56]))
+	if err != nil {
+		log.Printf("%v", err)
+		this.InvokePopup("bad format. missing characters?")
+		return
+	}
+
+	_, exists := the.Peer.GetProfile().GetCustomAttribute(name + "_onion")
+	if exists {
+		this.InvokePopup("can't re-use names :(")
+		return
+	}
+
+	_, exists = the.Peer.GetProfile().GetCustomAttribute(onion + "_name")
+	if exists {
+		this.InvokePopup("already have this contact")
+		return //TODO: bring them to the duplicate
+	}
+
+	this.UIState.AddContact(&gobjects.Contact{
+		onion,
+		name,
+		cwutil.RandomProfileImage(onion),
+		"",
+		0,
+		0,
+		true,
+	})
+}
+
+func (this *GrandCentralDispatcher) popup(str string) {
+	this.InvokePopup(str)
+}
+
+func (this *GrandCentralDispatcher) updateNick(nick string) {
+	the.Peer.GetProfile().Name = nick
+	the.CwtchApp.SaveProfile(the.Peer)
+}
+
+func (this *GrandCentralDispatcher) createGroup(server, groupName string) {
+	groupID, _, err := the.Peer.StartGroup(server)
+	if err != nil {
+		this.popup("group creation failed :(")
+		return
+	}
+
+	this.UIState.AddContact(&gobjects.Contact{
+		groupID,
+		groupName,
+		cwutil.RandomGroupImage(groupID),
+		server,
+		0,
+		0,
+		true,
+	})
+
+	group := the.Peer.GetGroup(groupID)
+	group.SetAttribute("nick", groupName)
+	the.CwtchApp.SaveProfile(the.Peer)
+
+	the.Peer.JoinServer(server)
+	group.NewMessage = make(chan model.Message)
+	go characters.CwtchListener(this.UIState.AddMessage, group.GroupID, group.NewMessage)
+}

+ 116 - 0
go/gothings/uistate.go

@@ -0,0 +1,116 @@
+package gothings
+
+import (
+	"cwtch.im/cwtch/model"
+	"encoding/base32"
+	"strings"
+	"log"
+	"bounce/go/constants"
+	"bounce/go/the"
+	"bounce/go/gobjects"
+)
+
+type InterfaceState struct {
+	parentGcd *GrandCentralDispatcher
+	contacts map[string]*gobjects.Contact
+	messages map[string][]*gobjects.Message
+}
+
+func NewUIState(gcd *GrandCentralDispatcher) (uis InterfaceState) {
+	uis = InterfaceState{gcd, make(map[string]*gobjects.Contact), make(map[string][]*gobjects.Message)}
+	return
+}
+
+func (this *InterfaceState) AddContact(c *gobjects.Contact) {
+	if len(c.Handle) == 32 { // ADD GROUP
+		//TODO: we should handle group creation here too probably? the code for groups vs individuals is weird right now ^ea
+
+		if _, found := this.contacts[c.Handle]; !found {
+			this.contacts[c.Handle] = c
+			this.parentGcd.AddContact(c.Handle, c.DisplayName, c.Image, c.Server, c.Badge, c.Status, c.Trusted)
+		}
+
+		return
+	} else if len(c.Handle) != 56 {
+		log.Printf("sorry, unable to handle AddContact(%v)", c.Handle)
+		return
+	}
+
+	if the.Peer.GetContact(c.Handle) == nil {
+		decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(c.Handle))
+
+		pp := &model.PublicProfile{Name: c.DisplayName, Ed25519PublicKey: decodedPub[:32], Trusted: c.Trusted, Blocked:false, Onion:c.Handle, Attributes:make(map[string]string)}
+		pp.SetAttribute("name", c.DisplayName)
+		the.Peer.GetProfile().Contacts[c.Handle] = pp
+		if c.Trusted {
+			the.Peer.GetProfile().TrustPeer(c.Handle)
+		}
+		the.CwtchApp.SaveProfile(the.Peer)
+		go the.Peer.PeerWithOnion(c.Handle)
+	}
+
+	if _, found := this.contacts[c.Handle]; !found {
+		this.contacts[c.Handle] = c
+		this.parentGcd.AddContact(c.Handle, c.DisplayName, c.Image, c.Server, c.Badge, c.Status, c.Trusted)
+	}
+
+	the.CwtchApp.SaveProfile(the.Peer)
+}
+
+func (this *InterfaceState) GetContact(handle string) *gobjects.Contact {
+	return this.contacts[handle]
+}
+
+func (this *InterfaceState) AddMessage(m *gobjects.Message) {
+	_, found := this.contacts[m.Handle]
+	if !found {
+		this.AddContact(&gobjects.Contact{
+			m.DisplayName,
+			m.Image,
+			m.Handle,
+			"",
+			1,
+			0,
+			false,
+		})
+	}
+
+	c := the.Peer.GetContact(m.Handle)
+	if c == nil {
+
+	}
+
+	_, found = this.messages[m.Handle]
+	if !found {
+		this.messages[m.Handle] = make([]*gobjects.Message, 0)
+	}
+
+	this.messages[m.Handle] = append(this.messages[m.Handle], m)
+
+	if this.parentGcd.CurrentOpenConversation() == m.Handle {
+		if m.FromMe {
+			m.From = "me"
+		}
+
+		this.parentGcd.AppendMessage(m.Handle, m.From, m.DisplayName, m.Message, m.Image, uint(m.MessageID), m.FromMe, m.Timestamp.Format(constants.TIME_FORMAT))
+	} else {
+		c := this.GetContact(m.Handle)
+		c.Badge++
+		this.UpdateContact(c.Handle)
+	}
+}
+
+func (this *InterfaceState) GetMessages(handle string) []*gobjects.Message {
+	_, found := this.messages[handle]
+	if !found {
+		this.messages[handle] = make([]*gobjects.Message, 0)
+	}
+	return this.messages[handle]
+}
+
+func (this *InterfaceState) UpdateContact(handle string) {
+	c, found := this.contacts[handle]
+	if found {
+		this.parentGcd.UpdateContact(c.Handle, c.DisplayName, c.Image, c.Server, c.Badge, c.Status, c.Trusted)
+	}
+}

+ 11 - 0
go/the/globals.go

@@ -0,0 +1,11 @@
+package the
+
+import (
+	libPeer "cwtch.im/cwtch/peer"
+	"cwtch.im/cwtch/app"
+	)
+
+var CwtchApp app.Application
+var Peer libPeer.CwtchPeer
+var CwtchDir string
+var AcknowledgementIDs map[uint32]uint

+ 106 - 274
main.go

@@ -1,84 +1,73 @@
 package main
 
 import (
-	libpeer "cwtch.im/cwtch/peer"
-	"cwtch.im/cwtch/peer/connections"
-	"encoding/base32"
-	"fmt"
-	"github.com/sethvargo/go-diceware/diceware"
-	"github.com/therecipe/qt/core"
+	"bounce/go/characters"
+	"bounce/go/cwutil"
+	"bounce/go/the"
+	libapp "cwtch.im/cwtch/app"
+	"cwtch.im/cwtch/model"
+		"fmt"
+	"git.openprivacy.ca/openprivacy/libricochet-go/application"
+	"git.openprivacy.ca/openprivacy/libricochet-go/channels"
+		"github.com/therecipe/qt/core"
 	"github.com/therecipe/qt/quick"
 	"github.com/therecipe/qt/quickcontrols2"
 	"github.com/therecipe/qt/widgets"
+	"log"
 	"os"
 	"os/user"
 	"path"
-	"strings"
-	"time"
-	"strconv"
-	"git.openprivacy.ca/openprivacy/asaur"
-	"git.openprivacy.ca/openprivacy/libricochet-go/application"
-	"git.openprivacy.ca/openprivacy/libricochet-go/channels"
-	"log"
-	"cwtch.im/cwtch/model"
-	"encoding/hex"
-	"cwtch.im/cwtch/app"
+	"bounce/go/cwtchthings"
+	"bounce/go/gothings"
+	"bounce/go/gobjects"
 )
 
-var gcd *GrandCentralDispatcher
-
-type ContactManager map[string]*Contact
-
-var contactMgr ContactManager
-var peer libpeer.CwtchPeer
-var outgoingMessages chan Message
-
-// need this to translate from the message IDs the UI uses to the ones the acknowledgement system uses on the wire
-var acknowledgementIDs map[uint32]uint
-
-type Contact struct {
-	Messages []Message
-	Unread   int
-	Status   connections.ConnectionState
+func init() {
+	// make go-defined types available in qml
+	gothings.GrandCentralDispatcher_QmlRegisterType2("CustomQmlTypes", 1, 0, "GrandCentralDispatcher")
 }
 
-func (this *Contact) AddMessage(m Message) {
-	this.Messages = append(this.Messages, m)
-}
+func main() {
+	// our globals
+	gcd := gothings.NewGrandCentralDispatcher(nil)
+	gcd.UIState = gothings.NewUIState(gcd)
+	the.AcknowledgementIDs = make(map[uint32]uint)
+	gcd.OutgoingMessages = make(chan gobjects.Letter, 1000)
+
+	//TODO: put theme stuff somewhere better
+	gcd.SetThemeScale(1.0)
+
+	// this is to load local qml files quickly when developing
+	var qmlSource *core.QUrl
+	if len(os.Args) == 2 && os.Args[1] == "-local" {
+		qmlSource = core.QUrl_FromLocalFile("./qml/main.qml")
+	} else {
+		qmlSource = core.NewQUrl3("qrc:/qml/main.qml", 0)
+	}
 
-type Message struct {
-	With, Message string
-	FromMe        bool
-	MessageID     uint
-	Timestamp     time.Time
-}
+	// window construction boilerplate
+	view := initializeQtView()
 
-func DeliverMessageToUI(handle, from, displayname, message string, mID uint, fromMe bool, ts time.Time) {
-	_, found := contactMgr[handle]
-	if !found {
-		contactMgr[handle] = &Contact{[]Message{}, 0, 0}
-	}
+	// variables we want to access from inside qml
+	view.RootContext().SetContextProperty("gcd", gcd)
 
-	contactMgr[handle].AddMessage(Message{from, message, fromMe, mID, ts})
-	if gcd.currentOpenConversation == handle {
-		if fromMe {
-			from = "me"
-		}
-		gcd.AppendMessage(from, message, displayname, mID, ts.Format(TIME_FORMAT), randomProfileImage(from))
-	} else {
-		contactMgr[handle].Unread++
-		gcd.SetUnread(handle, contactMgr[handle].Unread)
-	}
-}
+	// this actually loads the qml
+	view.SetSource(qmlSource)
 
-func init() {
-	GrandCentralDispatcher_QmlRegisterType2("CustomQmlTypes", 1, 0, "GrandCentralDispatcher")
-}
+	// these are long-lived pollers/listeners for incoming messages and status changes
+	loadCwtchData(gcd)
+	go characters.PostmanPat(gcd.OutgoingMessages)
+	go characters.TorStatusPoller(gcd.TorStatus)
+	go characters.PresencePoller(gcd.UIState.GetContact, gcd.UIState.AddContact, gcd.UIState.UpdateContact)
+	go characters.GroupPoller(gcd.UIState.GetContact, gcd.UIState.UpdateContact)
 
-func main() {
-	contactMgr = make(ContactManager)
-	acknowledgementIDs = make(map[uint32]uint)
+	// here we go!
+	view.Show()
+	widgets.QApplication_Exec()
+}
 
+// window construction boilerplate
+func initializeQtView() *quick.QQuickView {
 	core.QCoreApplication_SetAttribute(core.Qt__AA_EnableHighDpiScaling, true)
 	widgets.NewQApplication(len(os.Args), os.Args)
 	quickcontrols2.QQuickStyle_SetStyle("Universe")
@@ -87,176 +76,23 @@ func main() {
 	view.SetMinimumHeight(280)
 	view.SetMinimumWidth(300)
 	view.SetTitle("cwtch")
-	gcd = NewGrandCentralDispatcher(nil)
-	view.RootContext().SetContextProperty("gcd", gcd)
-
-	if len(os.Args) == 2 && os.Args[1] == "local" {
-		view.SetSource(core.QUrl_FromLocalFile("./qml/main.qml"))
-	} else {
-		view.SetSource(core.NewQUrl3("qrc:/qml/main.qml", 0))
-	}
-
-	outgoingMessages = make(chan Message, 1000)
-	go postmanPat()
-
-	initialize(view)
-	view.Show()
-	go torStatusPoller()
-	go presencePoller()
-	go groupPoller()
-	go ricochetListener()
-	widgets.QApplication_Exec()
-}
-
-func groupPoller() {
-	for {
-		time.Sleep(time.Second * 4)
-
-		servers := peer.GetServers()
-		groups := peer.GetGroups()
-		for i := range groups {
-			group := peer.GetGroup(groups[i])
-			//log.Printf("setting group %s to status %d", groups[i], int(servers[group.GroupServer]))
-			gcd.SetConnectionStatus(groups[i], int(servers[group.GroupServer]))
-		}
-	}
-}
-
-func presencePoller() { // TODO: make this subscribe-able in ricochet
-	time.Sleep(time.Second * 4)
-	for {
-		contacts := peer.GetContacts()
-		for i := range contacts {
-			_, found := contactMgr[contacts[i]]
-			if !found { // new contact has attempted to connect with us, treat it as an invite
-				contactMgr[contacts[i]] = &Contact{[]Message{}, 0, -1}
-				c, _ := peer.GetProfile().GetContact(contacts[i])
-				peer.GetProfile().SetCustomAttribute(contacts[i]+"_name", c.Name)
-				peer.GetProfile().SetCustomAttribute(c.Name+"_onion", contacts[i])
-				peer.Save()
-				gcd.AddContact(c.Name, contacts[i], "", randomProfileImage(contacts[i]), "0", c.Trusted)
-			}
-
-			c, found := peer.GetPeers()[contacts[i]]
-			if !found && contactMgr[contacts[i]].Status != -2 {
-				//log.Printf("setting %v to -2", contacts[i])
-				contactMgr[contacts[i]].Status = -2
-				gcd.SetConnectionStatus(contacts[i], -2)
-			} else if contactMgr[contacts[i]].Status != c {
-				//log.Printf("was: %v", contactMgr[contacts[i]].Status)
-				contactMgr[contacts[i]].Status = c
-				//log.Printf("setting %v to status %v", contacts[i], c)
-				gcd.SetConnectionStatus(contacts[i], int(c))
-				//log.Printf("now: %v", contactMgr[contacts[i]].Status)
-			}
-		}
-		time.Sleep(time.Second * 4)
-	}
-}
-
-func torStatusPoller() {
-	for {
-		time.Sleep(time.Second)
-		//todo: this should use a config manager
-		//todo: also, try dialing the proxy to differentiate tor not running vs control port not configured
-		rawStatus, err := asaur.GetInfo("localhost:9051", "tcp4", "", "status/bootstrap-phase")
-		if err != nil {
-			gcd.TorStatus(0, "can't find tor. is it running? is the controlport configured?")
-			continue
-		}
-
-		status := asaur.ParseBootstrapPhase(rawStatus)
-		progress, _ := strconv.Atoi(status["PROGRESS"])
-
-		if status["TAG"] == "done" {
-			gcd.TorStatus(3, "tor appears to be running just fine!")
-			continue
-		}
-
-		if progress == 0 {
-			gcd.TorStatus(1, "tor is trying to start up")
-			continue
-		}
-
-		gcd.TorStatus(2, status["SUMMARY"])
-	}
-}
-
-func cwtchListener(groupID string, channel chan model.Message) {
-	for {
-		m := <-channel
-		log.Printf("GROUPMSG %s %s", m.Message, m.PeerID)
-		name := m.PeerID
-		if name == peer.GetProfile().Onion {
-			name = "me"
-		} else {
-			var exists bool // lol this is a golang antifeature
-			name, exists = peer.GetProfile().GetCustomAttribute(m.PeerID + "_name")
-			if !exists  {
-				name = ""
-			}
-		}
-		if name == "" {
-			name = m.PeerID[:16] + "..."
-		}
-		DeliverMessageToUI(groupID, m.PeerID, name, m.Message, 0, m.PeerID == peer.GetProfile().Onion, m.Timestamp)
-		peer.Save()
-	}
-}
-
-func ricochetListener() {
-	err := peer.Listen()
-	if err != nil {
-		fmt.Printf("error listening for connections: %v\n", err)
-		gcd.InvokePopup("error handling network connection")
-	}
-}
-
-func postmanPat() {
-	postOffice := make(map[string]chan Message)
-
-	for {
-		m := <-outgoingMessages
-
-		_, found := postOffice[m.With]
-		if !found {
-			postOffice[m.With] = make(chan Message, 100)
-			go andHisBlackAndWhiteCat(postOffice[m.With])
-		}
-
-		postOffice[m.With] <- m
-	}
-}
 
-func andHisBlackAndWhiteCat(incomingMessages chan Message) {
-	for {
-		m := <-incomingMessages
-		connection := peer.PeerWithOnion(m.With)
-		connection.DoOnChannel("im.ricochet.chat", channels.Outbound, func(channel *channels.Channel) {
-			chatchannel, ok := channel.Handler.(*channels.ChatChannel)
-			if ok {
-				log.Printf("Sending packet")
-				acknowledgementIDs[chatchannel.SendMessage(m.Message)] = m.MessageID
-			}
-		})
-	}
+	return view
 }
 
-func initialize(view *quick.QQuickView) {
+// this is mostly going to get factored out when we add profile support
+// for now, it loads a single peer and fills the ui with its data
+func loadCwtchData(gcd *gothings.GrandCentralDispatcher) {
 	var err error
-	//TODO: this section is ported over and has a lot of printf errors, need to show them in the ui
-	var dirname, filename string
 	if os.Getenv("CWTCH_FOLDER") != "" {
-		dirname = os.Getenv("CWTCH_FOLDER")
-		filename = path.Join(dirname, "keep-this-file-private")
+		the.CwtchDir = os.Getenv("CWTCH_FOLDER")
 	} else {
 		usr, err := user.Current()
 		if err != nil {
 			fmt.Printf("\nerror: could not load current user: %v\n", err)
 			os.Exit(1)
 		}
-		dirname = path.Join(usr.HomeDir, ".cwtch")
-		filename = path.Join(dirname, "keep-this-file-private")
+		the.CwtchDir = path.Join(usr.HomeDir, ".cwtch")
 	}
 
 	/*_, err := app2.NewApp(dirname, "/data/data/org.qtproject.example.go/lib/libtor.so")
@@ -265,83 +101,79 @@ func initialize(view *quick.QQuickView) {
 	}
 	time.Sleep(time.Second * 10)
 	*/
-	os.MkdirAll(dirname, 0700)
+	os.MkdirAll(the.CwtchDir, 0700)
 
-	_, err = app.NewApp("/data/data/org.qtproject.example.go/files/", "/data/data/org.qtproject.example.go/lib/libtor.so")
-	log.Printf("starting Swtch app: err: %v\n", err)
+	the.CwtchApp, err = libapp.NewApp(the.CwtchDir, "tor")
+	if err != nil {
+		//TODO no more fatalfs
+		log.Fatalf("couldn't create cwtch app: %v", err)
+	}
 
-	peer, err = libpeer.LoadCwtchPeer(filename, "be gay do crime")
+	err = the.CwtchApp.LoadProfiles("be gay do crime")
 	if err != nil {
-		fmt.Println("couldn't load your config file, attempting to create a new one now")
+		//TODO no more fatalfs
+		log.Fatalf("couldn't load profiles: %v", err)
+	}
 
-		names, err := diceware.Generate(1)
-		peer, err = libpeer.NewCwtchPeer(names[0], "be gay do crime", filename)
+	if len(the.CwtchApp.ListPeers()) == 0 {
+		log.Printf("couldn't load your config file. attempting to create one now")
+		the.Peer, err = the.CwtchApp.CreatePeer("alice", "be gay do crime")
 		if err != nil {
-			fmt.Println("couldn't create one either :( exiting")
-			os.Exit(1)
+			log.Fatalf("couldn't create one. is your cwtch folder writable?")
 		}
-
-		peer.Save()
+		the.CwtchApp.SaveProfile(the.Peer)
+	} else {
+		the.Peer = the.CwtchApp.PrimaryIdentity()
 	}
 
-	gcd.UpdateMyProfile(peer.GetProfile().Name, peer.GetProfile().Onion, randomProfileImage(peer.GetProfile().Onion))
+	gcd.UpdateMyProfile(the.Peer.GetProfile().Name, the.Peer.GetProfile().Onion, cwutil.RandomProfileImage(the.Peer.GetProfile().Onion))
 
 	aif := application.ApplicationInstanceFactory{}
 	aif.Init()
 	app := new(application.RicochetApplication)
 	aif.AddHandler("im.ricochet.chat", func(rai *application.ApplicationInstance) func() channels.Handler {
-		ccl := new(ChatChannelListener)
-		ccl.Init(rai, app)
+		ccl := new(cwtchthings.ChatChannelListener)
+		ccl.Init(rai, app, gcd.UIState.AddMessage, gcd.Acknowledged)
 		return func() channels.Handler {
 			chat := new(channels.ChatChannel)
 			chat.Handler = ccl
 			return chat
 		}
 	})
-	peer.SetApplicationInstanceFactory(aif)
+	the.Peer.SetApplicationInstanceFactory(aif)
+	the.CwtchApp.LaunchPeers()
 
-	contacts := peer.GetContacts()
+
+	contacts := the.Peer.GetContacts()
 	for i := range contacts {
-		attr, _ := peer.GetProfile().GetCustomAttribute(contacts[i] + "_name")
-		contactMgr[contacts[i]] = &Contact{[]Message{}, 0, 0}
-		gcd.AddContact(attr, contacts[i], "", randomProfileImage(contacts[i]), "0", peer.GetContact(contacts[i]).Trusted)
+		contact, _ := the.Peer.GetProfile().GetContact(contacts[i])
+		displayName, _ := contact.GetAttribute("name")
+		gcd.UIState.AddContact(&gobjects.Contact{
+			contacts[i],
+			displayName,
+			cwutil.RandomProfileImage(contacts[i]),
+			"",
+			0,
+			0,
+			contact.Trusted,
+		})
 	}
 
-	groups := peer.GetGroups()
+	groups := the.Peer.GetGroups()
 	for i := range groups {
-		group := peer.GetGroup(groups[i])
+		log.Printf("adding saved group %v", groups[i])
+		group := the.Peer.GetGroup(groups[i])
 		group.NewMessage = make(chan model.Message)
-		go cwtchListener(groups[i], group.NewMessage)
-		peer.JoinServer(group.GroupServer)
-		//TODO: base the profileImage off the groupid, not the server. probably by decoding it and then re-encoding it b32
-		gcd.AddContact(group.GroupID[:12], group.GroupID, group.GroupServer, randomGroupImage(groups[i]), "0", group.Accepted)
-		log.Printf("GROUP %s@%s", group.GroupID, group.GroupServer)
-	}
-
-}
-
-// temporary until we do real picture selection
-func randomProfileImage(onion string) string {
-	//TODO: this is a hack, fix ever passing this in
-	if onion == "me" {
-		onion = peer.GetProfile().Onion
-	}
-
-	choices := []string{"001-centaur", "002-kraken", "003-dinosaur", "004-tree-1", "005-hand", "006-echidna", "007-robot", "008-mushroom", "009-harpy", "010-phoenix", "011-dragon-1", "012-devil", "013-troll", "014-alien", "015-minotaur", "016-madre-monte", "017-satyr", "018-karakasakozou", "019-pirate", "020-werewolf", "021-scarecrow", "022-valkyrie", "023-curupira", "024-loch-ness-monster", "025-tree", "026-cerberus", "027-gryphon", "028-mermaid", "029-vampire", "030-goblin", "031-yeti", "032-leprechaun", "033-medusa", "034-chimera", "035-elf", "036-hydra", "037-cyclops", "038-pegasus", "039-narwhal", "040-woodcutter", "041-zombie", "042-dragon", "043-frankenstein", "044-witch", "045-fairy", "046-genie", "047-pinocchio", "048-ghost", "049-wizard", "050-unicorn"}
-	barr, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
-	if err != nil || len(barr) != 35 {
-		fmt.Printf("error: %v %v %v\n", onion, err, barr)
-		return "qrc:/qml/images/extra/openprivacy.png"
-	}
-	return "qrc:/qml/images/profiles/" + choices[int(barr[33])%len(choices)] + ".png"
-}
-
-func randomGroupImage(onion string) string {
-	choices := []string{"001-borobudur","002-opera-house","003-burj-al-arab","004-chrysler","005-acropolis","006-empire-state-building","007-temple","008-indonesia-1","009-new-zealand","010-notre-dame","011-space-needle","012-seoul","013-mosque","014-milan","015-statue","016-pyramid","017-cologne","018-brandenburg-gate","019-berlin-cathedral","020-hungarian-parliament","021-buckingham","022-thailand","023-independence","024-angkor-wat","025-vaticano","026-christ-the-redeemer","027-colosseum","028-golden-gate-bridge","029-sphinx","030-statue-of-liberty","031-cradle-of-humankind","032-istanbul","033-london-eye","034-sagrada-familia","035-tower-bridge","036-burj-khalifa","037-washington","038-big-ben","039-stonehenge","040-white-house","041-ahu-tongariki","042-capitol","043-eiffel-tower","044-church-of-the-savior-on-spilled-blood","045-arc-de-triomphe","046-windmill","047-louvre","048-torii-gate","049-petronas","050-matsumoto-castle","051-fuji","052-temple-of-heaven","053-pagoda","054-chichen-itza","055-forbidden-city","056-merlion","057-great-wall-of-china","058-taj-mahal","059-pisa","060-indonesia"}
-	barr, err := hex.DecodeString(onion)
-	if err != nil || len(barr) == 0 {
-		fmt.Printf("error: %v %v %v\n", onion, err, barr)
-		return "qrc:/qml/images/extra/openprivacy.png"
+		go characters.CwtchListener(gcd.UIState.AddMessage, groups[i], group.NewMessage)
+		the.Peer.JoinServer(group.GroupServer)
+		gcd.UIState.AddContact(&gobjects.Contact{
+			group.GroupID,
+			group.GroupID[:12],
+			cwutil.RandomGroupImage(group.GroupID),
+			group.GroupServer,
+			0,
+			0,
+			group.Accepted,
+		})
 	}
-	return "qrc:/qml/images/servers/" + choices[int(barr[0])%len(choices)] + ".png"
 }

+ 5 - 55
qml/main.qml

@@ -6,6 +6,7 @@ import QtQuick.Layouts 1.3
 import QtQuick.Window 2.11
 
 import "fonts/Twemoji.js" as T
+import "panes"
 import "widgets"
 
 Item {
@@ -133,6 +134,7 @@ Item {
 				readonly property int settingsPane: 2
 				readonly property int userProfilePane: 3
 				readonly property int groupProfilePane: 4
+				readonly property int addGroupPane: 5
 
 
 				Item {} // empty
@@ -141,47 +143,7 @@ Item {
 					anchors.fill: parent
 				}
 
-				ColumnLayout { // settingsPane
-					anchors.fill: parent
-
-
-					StackToolbar {
-						text: "Cwtch Settings"
-						aux.visible: false
-					}
-
-					ScalingLabel {
-						Layout.maximumWidth: parent.width
-						text: "welcome to the global app settings page!"
-					}
-
-					Slider {
-						id: zoomSlider
-						from: 0.5
-						to: 2.4
-					}
-
-					ScalingLabel {
-						text: "Large text"
-						size: 20
-					}
-
-					ScalingLabel{
-						text: "Default size text, scale factor: " + zoomSlider.value
-					}
-
-					ScalingLabel {
-						text: "Small text"
-						size: 8
-					}
-
-
-					Component.onCompleted: {
-						zoomSlider.value = Screen.pixelDensity / 3.2 // artistic license. set by erinn. fight me before changing
-						if (zoomSlider.value < zoomSlider.from) zoomSlider.value = zoomSlider.from
-						if (zoomSlider.value > zoomSlider.to) zoomSlider.value = zoomSlider.to
-					}
-				}
+				SettingsPane{}
 
 				ColumnLayout { // userProfilePane
 					anchors.fill: parent
@@ -197,21 +159,9 @@ Item {
 					Label { text: "per-user things like contact name and picture will be edited here" }
 				}
 
-				Label { // groupProfilePane
-					font.pixelSize: 12
-					text: "invite new people or change the group name here"
+				GroupSettingsPane{}
 
-
-					StackToolbar { text: "Group settings" }
-				}
-
-				Label { // addGroupPane
-					font.pixelSize: 12
-					text: "add a new group"
-
-
-					StackToolbar { text: "Create group" }
-				}
+				AddGroupPane { anchors.fill: parent }
 			}
 		}
 	}

+ 53 - 0
qml/panes/AddGroupPane.qml

@@ -0,0 +1,53 @@
+import QtGraphicalEffects 1.0
+import QtQuick 2.7
+import QtQuick.Controls 2.4
+import QtQuick.Controls.Material 2.0
+import QtQuick.Layouts 1.3
+import QtQuick.Window 2.11
+
+import "../widgets"
+
+ColumnLayout { // settingsPane
+	anchors.fill: parent
+
+
+	StackToolbar {
+		text: "Create a new group"
+		aux.visible: false
+	}
+
+	ScalingLabel {
+		text: "Server:"
+	}
+
+	TextEdit {
+		id: txtServer
+		width: 100
+		text: "6xnl4rlc5gq5crnyki6wp6qy2dy2brsjtfixwhgujfycsu7jly7pmxad"
+	}
+
+	ScalingLabel{
+		text: "Group name:"
+	}
+
+	TextEdit {
+		id: txtGroupName
+		width: 100
+		text: "my awesome group"
+	}
+
+	SimpleButton {
+		text: "create"
+
+		onClicked: {
+			gcd.createGroup(txtServer.text, txtGroupName.text)
+		}
+	}
+
+
+	Component.onCompleted: {
+		zoomSlider.value = Screen.pixelDensity / 3.2 // artistic license. set by erinn. fight me before changing
+		if (zoomSlider.value < zoomSlider.from) zoomSlider.value = zoomSlider.from
+		if (zoomSlider.value > zoomSlider.to) zoomSlider.value = zoomSlider.to
+	}
+}

+ 68 - 0
qml/panes/GroupSettingsPane.qml

@@ -0,0 +1,68 @@
+import QtGraphicalEffects 1.0
+import QtQuick 2.7
+import QtQuick.Controls 2.4
+import QtQuick.Controls.Material 2.0
+import QtQuick.Layouts 1.3
+import QtQuick.Window 2.11
+
+import "../widgets"
+
+ColumnLayout { // groupSettingsPane
+	anchors.fill: parent
+
+
+	StackToolbar {
+		id: toolbar
+		aux.visible: false
+	}
+
+	ScalingLabel {
+		text: "Server:"
+	}
+
+	TextEdit {
+		id: txtServer
+		width: 100
+	}
+
+	ScalingLabel{
+		text: "Group name:"
+	}
+
+	TextEdit {
+		id: txtGroupName
+		width: 100
+	}
+
+	ScalingLabel {
+		text: "Invitation:"
+	}
+
+	TextEdit {
+		id: txtInvitation
+		width: 200
+	}
+
+	SimpleButton {
+		icon: "regular/clipboard"
+		text: "copy"
+
+		onClicked: {
+			gcd.popup("copied to clipboard!")
+			txtInvitation.selectAll()
+			txtInvitation.copy()
+		}
+	}
+
+
+	Connections {
+		target: gcd
+
+		onSupplyGroupSettings: function(name, server, invite) {
+			toolbar.text = name
+			txtGroupName.text = name
+			txtServer.text = server
+			txtInvitation.text = invite
+		}
+	}
+}

+ 50 - 0
qml/panes/SettingsPane.qml

@@ -0,0 +1,50 @@
+import QtGraphicalEffects 1.0
+import QtQuick 2.7
+import QtQuick.Controls 2.4
+import QtQuick.Controls.Material 2.0
+import QtQuick.Layouts 1.3
+import QtQuick.Window 2.11
+
+import "../widgets"
+
+ColumnLayout { // settingsPane
+	anchors.fill: parent
+
+
+	StackToolbar {
+		text: "Cwtch Settings"
+		aux.visible: false
+	}
+
+	ScalingLabel {
+		Layout.maximumWidth: parent.width
+		text: "welcome to the global app settings page!"
+	}
+
+	Slider {
+		id: zoomSlider
+		from: 0.5
+		to: 2.4
+	}
+
+	ScalingLabel {
+		text: "Large text"
+		size: 20
+	}
+
+	ScalingLabel{
+		text: "Default size text, scale factor: " + zoomSlider.value
+	}
+
+	ScalingLabel {
+		text: "Small text"
+		size: 8
+	}
+
+
+	Component.onCompleted: {
+		zoomSlider.value = Screen.pixelDensity / 3.2 // artistic license. set by erinn. fight me before changing
+		if (zoomSlider.value < zoomSlider.from) zoomSlider.value = zoomSlider.from
+		if (zoomSlider.value > zoomSlider.to) zoomSlider.value = zoomSlider.to
+	}
+}

+ 18 - 18
qml/widgets/ContactList.qml

@@ -8,11 +8,11 @@ ColumnLayout {
 	id: root
 
 
-	MyProfile{
+	MyProfile{ // CURRENT PROFILE INFO AND CONTROL BAR
 		id: myprof
 	}
 
-	Flickable { // CONTACT LIST
+	Flickable { // THE ACTUAL CONTACT LIST
 		id: sv
 		//Layout.alignment: Qt.AlignLeft | Qt.AlignTop
 		clip: true
@@ -36,18 +36,18 @@ ColumnLayout {
 			width: root.width
 			spacing: 0
 
-
 			Connections { // ADD/REMOVE CONTACT ENTRIES
 				target: gcd
 
-				onAddContact: function(name, onion, server, image, badge, trusted) {
+				onAddContact: function(handle, displayName, image, server, badge, status, trusted) {
 					contactsModel.append({
-						"n": name,
-						"o": onion,
-						"s": server,
-						"i": image,
-						"b": badge,
-						"t": trusted
+						"_handle": handle,
+						"_displayName": displayName,
+						"_image": image,
+						"_server": server,
+						"_badge": badge,
+						"_status": status,
+						"_trusted": trusted,
 					})
 				}
 			}
@@ -56,16 +56,16 @@ ColumnLayout {
 				id: contactsModel
 			}
 
-
 			Repeater {
 				model: contactsModel // ... AND DISPLAYED HERE
-				delegate: Contact{
-					nick: n
-					onion: o
-					server: s
-					image: i
-					badge: b
-					isTrusted: t
+				delegate: ContactRow {
+					handle: _handle
+					displayName: _displayName
+					image: _image
+					server: _server
+					badge: _badge
+					status: _status
+					trusted: _trusted
 				}
 			}
 		}

+ 16 - 23
qml/widgets/Contact.qml

@@ -8,15 +8,15 @@ import CustomQmlTypes 1.0
 RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
 	anchors.left: parent.left
 	anchors.right: parent.right
-	visible: nick != ""
+	visible: true
 
-	property alias nick: cn.text
+	property alias displayName: cn.text
 	property alias image: imgProfile.source
-	property string onion
-	property string badge
+	property string handle
+	property int badge
 	property bool isActive
 	property bool isHover
-	property bool isTrusted
+	property bool trusted
 	property alias status: imgProfile.status
 	property string server
 
@@ -48,7 +48,7 @@ RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
 				anchors.left: imgProfile.right
 				anchors.right: rectUnread.left
 				font.pixelSize: 16
-				font.italic: !isTrusted
+				font.italic: !trusted
 				textFormat: Text.PlainText
 				//fontSizeMode: Text.HorizontalFit
 				elide: Text.ElideRight
@@ -61,7 +61,7 @@ RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
 				width: lblUnread.width + 10
 				radius: 8
 				color: "#4B3557"
-				visible: badge != "0"
+				visible: badge != 0
 				anchors.rightMargin: 9
 
 
@@ -85,7 +85,7 @@ RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
 			gcd.broadcast("ResetMessagePane")
 			isActive = true
 			theStack.pane = theStack.messagePane
-			gcd.loadMessagesPane(onion)
+			gcd.loadMessagesPane(handle)
 		}
 
 		onEntered: {
@@ -100,25 +100,18 @@ RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
 	Connections { // UPDATE UNREAD MESSAGES COUNTER
 		target: gcd
 
-		onSetUnread: function(foronion, n) {
-			if (onion == foronion) {
-				badge = ""+n
-			}
-		}
-
 		onResetMessagePane: function() {
 			isActive = false
 		}
 
-		onSetConnectionStatus: function(foronion, x) {
-			if (onion == foronion) {
-				status = x
-			}
-		}
-
-		onMarkTrusted: function(foronion) {
-			if (onion == foronion) {
-				isTrusted = true
+		onUpdateContact: function(_handle, _displayName, _image, _server, _badge, _status, _trusted) {
+			if (handle == _handle) {
+				displayName = _displayName
+				image = _image
+				server = _server
+				badge = _badge
+				status = _status
+				trusted = _trusted
 			}
 		}
 	}

+ 6 - 4
qml/widgets/Message.qml

@@ -15,10 +15,12 @@ RowLayout {
 
 	property alias message: lbl.text
 	property string from
-	property string displayname
+	property string handle
+	property string displayName
 	property int messageID
+	property bool fromMe
 	property alias timestamp: ts.text
-	property alias source: imgProfile.source
+	property alias image: imgProfile.source
 	property alias status: imgProfile.status
 
 
@@ -107,14 +109,14 @@ RowLayout {
 					color: "#FFFFFF"
 					font.pixelSize: 10
 					anchors.right: parent.right
-					text: displayname
+					text: displayName
 					visible: from != "me"
 				}
 
 				Image { // ACKNOWLEDGEMENT ICON
 					id: ack
 					anchors.right: parent.right
-					source: displayname == "" ? "qrc:/qml/images/fontawesome/regular/hourglass.svg" : "qrc:/qml/images/fontawesome/regular/check-circle.svg"
+					source: displayName == "" ? "qrc:/qml/images/fontawesome/regular/hourglass.svg" : "qrc:/qml/images/fontawesome/regular/check-circle.svg"
 					height: 10
 					sourceSize.height: 10
 					visible: from == "me"

+ 23 - 16
qml/widgets/MessageList.qml

@@ -14,7 +14,10 @@ ColumnLayout {
 	StackToolbar {
 		text: "open privacy exec"
 
-		aux.onClicked: theStack.pane = theStack.userProfilePane
+		aux.onClicked: {
+			theStack.pane = gcd.currentOpenConversation.length == 32 ? theStack.groupProfilePane : theStack.userProfilePane
+			gcd.requestGroupSettings()
+		}
 	}
 
 	Flickable { // THE MESSAGE LIST ITSELF
@@ -38,14 +41,16 @@ ColumnLayout {
 				messagesModel.clear()
 			}
 
-			onAppendMessage: function(from, message, displayname, mid, ts, source) {
+			onAppendMessage: function(handle, from, displayName, message, image, mid, fromMe, ts) {
 				messagesModel.append({
-					"f": from,
-					"m": parse(message, 12),
-					"d": displayname,
-					"i": mid,
-					"t": ts,
-					"src": source
+					"_handle": handle,
+					"_from": from,
+					"_displayName": displayName,
+					"_message": parse(message, 12),
+					"_image": image,
+					"_mid": mid,
+					"_fromMe": fromMe,
+					"_ts": ts,
 				})
 
 				if (sv.contentY + sv.height >= sv.contentHeight - colMessages.height && sv.contentHeight > sv.height) {
@@ -73,12 +78,14 @@ ColumnLayout {
 			Repeater { // ... AND DISPLAYED HERE
 				model: messagesModel
 				delegate: Message {
-					from: f
-					message: m
-					displayname: d
-					messageID: i
-					timestamp: t
-					source: src
+					handle: _handle
+					from: _from
+					displayName: _displayName
+					message: _message
+					image: _image
+					messageID: _mid
+					fromMe: _fromMe
+					timestamp: _ts
 				}
 			}
 		}
@@ -88,8 +95,8 @@ ColumnLayout {
 		Rectangle { // MESSAGE ENTRY TEXTFIELD
 			id: rectMessage
 			Layout.fillWidth: true
-			Layout.minimumHeight: 40 * zoomSlider.value
-			Layout.maximumHeight: 40 * zoomSlider.value
+			Layout.minimumHeight: 40 * gcd.themeScale
+			Layout.maximumHeight: 40 * gcd.themeScale
 			color: "#EDEDED"
 			border.color: "#AAAAAA"
 			radius: 10

+ 16 - 3
qml/widgets/MyProfile.qml

@@ -149,7 +149,7 @@ ColumnLayout {
 
 	Row { // TOOLS FOR EDITING PROFILE
 		anchors.horizontalCenter: parent.horizontalCenter
-		spacing: zoomSlider.value * 2
+		spacing: gcd.themeScale * 2
 
 
 		TextEdit { // USED TO POWER THE COPY/PASTE BUTTON
@@ -164,8 +164,8 @@ ColumnLayout {
 			onClicked: {
 				gcd.popup("copied to clipboard!")
 				txtHidden.text = nick.replace(" ", "~") + "~" + onion
-				txtHidden.selectAll();
-				txtHidden.copy();
+				txtHidden.selectAll()
+				txtHidden.copy()
 			}
 		}
 
@@ -184,6 +184,19 @@ ColumnLayout {
 		}
 	}
 
+	Row {
+		anchors.horizontalCenter: parent.horizontalCenter
+		spacing: gcd.themeScale * 2
+
+
+		SimpleButton { // CREATE GROUP BUTTON
+			icon: "regular/clipboard"
+			text: "new group"
+
+			onClicked: theStack.pane = theStack.addGroupPane
+		}
+	}
+
 	RowLayout {
 		anchors.left: parent.left
 		anchors.right: parent.right

BIN
qml/widgets/MyProfile.qmlc


+ 1 - 1
qml/widgets/ScalingLabel.qml

@@ -7,7 +7,7 @@ import QtQuick.Window 2.11
 
 
 Label {
-	font.pixelSize: zoomSlider.value * size
+	font.pixelSize: gcd.themeScale * size
 	wrapMode: Text.WordWrap
 
 	property real size: 12

+ 2 - 2
qml/widgets/SimpleButton.qml

@@ -9,10 +9,10 @@ import "../fonts/Twemoji.js" as T
 
 Rectangle {
     id: button
-    width: (text == undefined || text == "" ? 0 : buttonText.width) + (icon == undefined || icon == "" ? 0 : ico.width) + 24 * zoomSlider.value
+    width: (text == undefined || text == "" ? 0 : buttonText.width) + (icon == undefined || icon == "" ? 0 : ico.width) + 24 * gcd.themeScale
     Layout.minimumWidth: width
     Layout.maximumWidth: width
-    height: 20 * zoomSlider.value
+    height: 20 * gcd.themeScale
     Layout.minimumHeight: height
     Layout.maximumHeight: height
 	color: mousedown ? "#B09CBC" : "#4B3557"

BIN
qml/widgets/SimpleButton.qmlc


+ 1 - 1
qml/widgets/StackToolbar.qml

@@ -12,7 +12,7 @@ Rectangle { // OVERHEAD BAR ON STACK PANE
 	anchors.left: parent.left
 	anchors.right: parent.right
 	anchors.top: parent.top
-	height: 20 * zoomSlider.value + 4
+	height: 20 * gcd.themeScale + 4
 	Layout.minimumHeight: height
 	Layout.maximumHeight: height
 	color: "#EDEDED"