forked from cwtch.im/ui
initial commit
This commit is contained in:
parent
690a71308e
commit
8a9ba6d154
237
gcd.go
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()
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package constants
|
||||
|
||||
var TIME_FORMAT = "Mon 3:04pm"
|
|
@ -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])
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package gobjects
|
||||
|
||||
type Contact struct {
|
||||
Handle string
|
||||
DisplayName string
|
||||
Image string
|
||||
Server string
|
||||
Badge int
|
||||
Status int
|
||||
Trusted bool
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
394
main.go
394
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 (this *Contact) AddMessage(m Message) {
|
||||
this.Messages = append(this.Messages, m)
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
With, Message string
|
||||
FromMe bool
|
||||
MessageID uint
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
GrandCentralDispatcher_QmlRegisterType2("CustomQmlTypes", 1, 0, "GrandCentralDispatcher")
|
||||
// make go-defined types available in qml
|
||||
gothings.GrandCentralDispatcher_QmlRegisterType2("CustomQmlTypes", 1, 0, "GrandCentralDispatcher")
|
||||
}
|
||||
|
||||
func main() {
|
||||
contactMgr = make(ContactManager)
|
||||
acknowledgementIDs = make(map[uint32]uint)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// window construction boilerplate
|
||||
view := initializeQtView()
|
||||
|
||||
// variables we want to access from inside qml
|
||||
view.RootContext().SetContextProperty("gcd", gcd)
|
||||
|
||||
// this actually loads the qml
|
||||
view.SetSource(qmlSource)
|
||||
|
||||
// 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)
|
||||
|
||||
// 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()
|
||||
return view
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
peer, err = libpeer.LoadCwtchPeer(filename, "be gay do crime")
|
||||
the.CwtchApp, err = libapp.NewApp(the.CwtchDir, "tor")
|
||||
if err != nil {
|
||||
fmt.Println("couldn't load your config file, attempting to create a new one now")
|
||||
|
||||
names, err := diceware.Generate(1)
|
||||
peer, err = libpeer.NewCwtchPeer(names[0], "be gay do crime", filename)
|
||||
if err != nil {
|
||||
fmt.Println("couldn't create one either :( exiting")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
peer.Save()
|
||||
//TODO no more fatalfs
|
||||
log.Fatalf("couldn't create cwtch app: %v", err)
|
||||
}
|
||||
|
||||
gcd.UpdateMyProfile(peer.GetProfile().Name, peer.GetProfile().Onion, randomProfileImage(peer.GetProfile().Onion))
|
||||
err = the.CwtchApp.LoadProfiles("be gay do crime")
|
||||
if err != nil {
|
||||
//TODO no more fatalfs
|
||||
log.Fatalf("couldn't load profiles: %v", err)
|
||||
}
|
||||
|
||||
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 {
|
||||
log.Fatalf("couldn't create one. is your cwtch folder writable?")
|
||||
}
|
||||
the.CwtchApp.SaveProfile(the.Peer)
|
||||
} else {
|
||||
the.Peer = the.CwtchApp.PrimaryIdentity()
|
||||
}
|
||||
|
||||
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)
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
return "qrc:/qml/images/servers/" + choices[int(barr[0])%len(choices)] + ".png"
|
||||
}
|
60
qml/main.qml
60
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Binary file not shown.
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Binary file not shown.
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue