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 (
|
import (
|
||||||
"git.openprivacy.ca/openprivacy/libricochet-go/application"
|
"git.openprivacy.ca/openprivacy/libricochet-go/application"
|
||||||
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
|
"git.openprivacy.ca/openprivacy/libricochet-go/channels"
|
||||||
"time"
|
"time"
|
||||||
)
|
"bounce/go/the"
|
||||||
|
"bounce/go/cwutil"
|
||||||
|
"bounce/go/gobjects"
|
||||||
|
)
|
||||||
|
|
||||||
type ChatChannelListener struct {
|
type ChatChannelListener struct {
|
||||||
rai *application.ApplicationInstance
|
rai *application.ApplicationInstance
|
||||||
ra *application.RicochetApplication
|
ra *application.RicochetApplication
|
||||||
|
addMessage func(*gobjects.Message)
|
||||||
|
acknowledged func(uint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (this *ChatChannelListener) Init(rai *application.ApplicationInstance, ra *application.RicochetApplication, addMessage func(*gobjects.Message), acknowledged func(uint)) {
|
||||||
func (this *ChatChannelListener) Init(rai *application.ApplicationInstance, ra *application.RicochetApplication) {
|
|
||||||
this.rai = rai
|
this.rai = rai
|
||||||
this.ra = ra
|
this.ra = ra
|
||||||
|
this.addMessage = addMessage
|
||||||
|
this.acknowledged = acknowledged
|
||||||
}
|
}
|
||||||
|
|
||||||
// We always want bidirectional chat channels
|
// 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 {
|
func (this *ChatChannelListener) ChatMessage(messageID uint32, when time.Time, message string) bool {
|
||||||
DeliverMessageToUI(this.rai.RemoteHostname, this.rai.RemoteHostname, "", message, uint(messageID), false, when)
|
this.addMessage(&gobjects.Message{
|
||||||
go func() {
|
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)
|
time.Sleep(time.Second)
|
||||||
peer.Save()
|
the.CwtchApp.SaveProfile(the.Peer)
|
||||||
}()
|
}()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (this *ChatChannelListener) ChatMessageAck(messageID uint32, accepted bool) {
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
libpeer "cwtch.im/cwtch/peer"
|
"bounce/go/characters"
|
||||||
"cwtch.im/cwtch/peer/connections"
|
"bounce/go/cwutil"
|
||||||
"encoding/base32"
|
"bounce/go/the"
|
||||||
"fmt"
|
libapp "cwtch.im/cwtch/app"
|
||||||
"github.com/sethvargo/go-diceware/diceware"
|
"cwtch.im/cwtch/model"
|
||||||
"github.com/therecipe/qt/core"
|
"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/quick"
|
||||||
"github.com/therecipe/qt/quickcontrols2"
|
"github.com/therecipe/qt/quickcontrols2"
|
||||||
"github.com/therecipe/qt/widgets"
|
"github.com/therecipe/qt/widgets"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"bounce/go/cwtchthings"
|
||||||
"time"
|
"bounce/go/gothings"
|
||||||
"strconv"
|
"bounce/go/gobjects"
|
||||||
"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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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() {
|
func init() {
|
||||||
GrandCentralDispatcher_QmlRegisterType2("CustomQmlTypes", 1, 0, "GrandCentralDispatcher")
|
// make go-defined types available in qml
|
||||||
|
gothings.GrandCentralDispatcher_QmlRegisterType2("CustomQmlTypes", 1, 0, "GrandCentralDispatcher")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
contactMgr = make(ContactManager)
|
// our globals
|
||||||
acknowledgementIDs = make(map[uint32]uint)
|
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)
|
core.QCoreApplication_SetAttribute(core.Qt__AA_EnableHighDpiScaling, true)
|
||||||
widgets.NewQApplication(len(os.Args), os.Args)
|
widgets.NewQApplication(len(os.Args), os.Args)
|
||||||
quickcontrols2.QQuickStyle_SetStyle("Universe")
|
quickcontrols2.QQuickStyle_SetStyle("Universe")
|
||||||
|
@ -87,176 +76,23 @@ func main() {
|
||||||
view.SetMinimumHeight(280)
|
view.SetMinimumHeight(280)
|
||||||
view.SetMinimumWidth(300)
|
view.SetMinimumWidth(300)
|
||||||
view.SetTitle("cwtch")
|
view.SetTitle("cwtch")
|
||||||
gcd = NewGrandCentralDispatcher(nil)
|
|
||||||
view.RootContext().SetContextProperty("gcd", gcd)
|
|
||||||
|
|
||||||
if len(os.Args) == 2 && os.Args[1] == "local" {
|
return view
|
||||||
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() {
|
// this is mostly going to get factored out when we add profile support
|
||||||
for {
|
// for now, it loads a single peer and fills the ui with its data
|
||||||
time.Sleep(time.Second * 4)
|
func loadCwtchData(gcd *gothings.GrandCentralDispatcher) {
|
||||||
|
|
||||||
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) {
|
|
||||||
var err error
|
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") != "" {
|
if os.Getenv("CWTCH_FOLDER") != "" {
|
||||||
dirname = os.Getenv("CWTCH_FOLDER")
|
the.CwtchDir = os.Getenv("CWTCH_FOLDER")
|
||||||
filename = path.Join(dirname, "keep-this-file-private")
|
|
||||||
} else {
|
} else {
|
||||||
usr, err := user.Current()
|
usr, err := user.Current()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("\nerror: could not load current user: %v\n", err)
|
fmt.Printf("\nerror: could not load current user: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
dirname = path.Join(usr.HomeDir, ".cwtch")
|
the.CwtchDir = path.Join(usr.HomeDir, ".cwtch")
|
||||||
filename = path.Join(dirname, "keep-this-file-private")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*_, err := app2.NewApp(dirname, "/data/data/org.qtproject.example.go/lib/libtor.so")
|
/*_, 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)
|
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")
|
the.CwtchApp, err = libapp.NewApp(the.CwtchDir, "tor")
|
||||||
log.Printf("starting Swtch app: err: %v\n", err)
|
|
||||||
|
|
||||||
peer, err = libpeer.LoadCwtchPeer(filename, "be gay do crime")
|
|
||||||
if err != nil {
|
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 create cwtch app: %v", err)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 := application.ApplicationInstanceFactory{}
|
||||||
aif.Init()
|
aif.Init()
|
||||||
app := new(application.RicochetApplication)
|
app := new(application.RicochetApplication)
|
||||||
aif.AddHandler("im.ricochet.chat", func(rai *application.ApplicationInstance) func() channels.Handler {
|
aif.AddHandler("im.ricochet.chat", func(rai *application.ApplicationInstance) func() channels.Handler {
|
||||||
ccl := new(ChatChannelListener)
|
ccl := new(cwtchthings.ChatChannelListener)
|
||||||
ccl.Init(rai, app)
|
ccl.Init(rai, app, gcd.UIState.AddMessage, gcd.Acknowledged)
|
||||||
return func() channels.Handler {
|
return func() channels.Handler {
|
||||||
chat := new(channels.ChatChannel)
|
chat := new(channels.ChatChannel)
|
||||||
chat.Handler = ccl
|
chat.Handler = ccl
|
||||||
return chat
|
return chat
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
peer.SetApplicationInstanceFactory(aif)
|
the.Peer.SetApplicationInstanceFactory(aif)
|
||||||
|
the.CwtchApp.LaunchPeers()
|
||||||
|
|
||||||
contacts := peer.GetContacts()
|
|
||||||
|
contacts := the.Peer.GetContacts()
|
||||||
for i := range contacts {
|
for i := range contacts {
|
||||||
attr, _ := peer.GetProfile().GetCustomAttribute(contacts[i] + "_name")
|
contact, _ := the.Peer.GetProfile().GetContact(contacts[i])
|
||||||
contactMgr[contacts[i]] = &Contact{[]Message{}, 0, 0}
|
displayName, _ := contact.GetAttribute("name")
|
||||||
gcd.AddContact(attr, contacts[i], "", randomProfileImage(contacts[i]), "0", peer.GetContact(contacts[i]).Trusted)
|
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 {
|
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)
|
group.NewMessage = make(chan model.Message)
|
||||||
go cwtchListener(groups[i], group.NewMessage)
|
go characters.CwtchListener(gcd.UIState.AddMessage, groups[i], group.NewMessage)
|
||||||
peer.JoinServer(group.GroupServer)
|
the.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.UIState.AddContact(&gobjects.Contact{
|
||||||
gcd.AddContact(group.GroupID[:12], group.GroupID, group.GroupServer, randomGroupImage(groups[i]), "0", group.Accepted)
|
group.GroupID,
|
||||||
log.Printf("GROUP %s@%s", group.GroupID, group.GroupServer)
|
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 QtQuick.Window 2.11
|
||||||
|
|
||||||
import "fonts/Twemoji.js" as T
|
import "fonts/Twemoji.js" as T
|
||||||
|
import "panes"
|
||||||
import "widgets"
|
import "widgets"
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
@ -133,6 +134,7 @@ Item {
|
||||||
readonly property int settingsPane: 2
|
readonly property int settingsPane: 2
|
||||||
readonly property int userProfilePane: 3
|
readonly property int userProfilePane: 3
|
||||||
readonly property int groupProfilePane: 4
|
readonly property int groupProfilePane: 4
|
||||||
|
readonly property int addGroupPane: 5
|
||||||
|
|
||||||
|
|
||||||
Item {} // empty
|
Item {} // empty
|
||||||
|
@ -141,47 +143,7 @@ Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout { // settingsPane
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout { // userProfilePane
|
ColumnLayout { // userProfilePane
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
@ -197,21 +159,9 @@ Item {
|
||||||
Label { text: "per-user things like contact name and picture will be edited here" }
|
Label { text: "per-user things like contact name and picture will be edited here" }
|
||||||
}
|
}
|
||||||
|
|
||||||
Label { // groupProfilePane
|
GroupSettingsPane{}
|
||||||
font.pixelSize: 12
|
|
||||||
text: "invite new people or change the group name here"
|
|
||||||
|
|
||||||
|
AddGroupPane { anchors.fill: parent }
|
||||||
StackToolbar { text: "Group settings" }
|
|
||||||
}
|
|
||||||
|
|
||||||
Label { // addGroupPane
|
|
||||||
font.pixelSize: 12
|
|
||||||
text: "add a new group"
|
|
||||||
|
|
||||||
|
|
||||||
StackToolbar { text: "Create group" }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
id: root
|
||||||
|
|
||||||
|
|
||||||
MyProfile{
|
MyProfile{ // CURRENT PROFILE INFO AND CONTROL BAR
|
||||||
id: myprof
|
id: myprof
|
||||||
}
|
}
|
||||||
|
|
||||||
Flickable { // CONTACT LIST
|
Flickable { // THE ACTUAL CONTACT LIST
|
||||||
id: sv
|
id: sv
|
||||||
//Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
//Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||||
clip: true
|
clip: true
|
||||||
|
@ -36,18 +36,18 @@ ColumnLayout {
|
||||||
width: root.width
|
width: root.width
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
|
|
||||||
Connections { // ADD/REMOVE CONTACT ENTRIES
|
Connections { // ADD/REMOVE CONTACT ENTRIES
|
||||||
target: gcd
|
target: gcd
|
||||||
|
|
||||||
onAddContact: function(name, onion, server, image, badge, trusted) {
|
onAddContact: function(handle, displayName, image, server, badge, status, trusted) {
|
||||||
contactsModel.append({
|
contactsModel.append({
|
||||||
"n": name,
|
"_handle": handle,
|
||||||
"o": onion,
|
"_displayName": displayName,
|
||||||
"s": server,
|
"_image": image,
|
||||||
"i": image,
|
"_server": server,
|
||||||
"b": badge,
|
"_badge": badge,
|
||||||
"t": trusted
|
"_status": status,
|
||||||
|
"_trusted": trusted,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,16 +56,16 @@ ColumnLayout {
|
||||||
id: contactsModel
|
id: contactsModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: contactsModel // ... AND DISPLAYED HERE
|
model: contactsModel // ... AND DISPLAYED HERE
|
||||||
delegate: Contact{
|
delegate: ContactRow {
|
||||||
nick: n
|
handle: _handle
|
||||||
onion: o
|
displayName: _displayName
|
||||||
server: s
|
image: _image
|
||||||
image: i
|
server: _server
|
||||||
badge: b
|
badge: _badge
|
||||||
isTrusted: t
|
status: _status
|
||||||
|
trusted: _trusted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,15 +8,15 @@ import CustomQmlTypes 1.0
|
||||||
RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
|
RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
visible: nick != ""
|
visible: true
|
||||||
|
|
||||||
property alias nick: cn.text
|
property alias displayName: cn.text
|
||||||
property alias image: imgProfile.source
|
property alias image: imgProfile.source
|
||||||
property string onion
|
property string handle
|
||||||
property string badge
|
property int badge
|
||||||
property bool isActive
|
property bool isActive
|
||||||
property bool isHover
|
property bool isHover
|
||||||
property bool isTrusted
|
property bool trusted
|
||||||
property alias status: imgProfile.status
|
property alias status: imgProfile.status
|
||||||
property string server
|
property string server
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
|
||||||
anchors.left: imgProfile.right
|
anchors.left: imgProfile.right
|
||||||
anchors.right: rectUnread.left
|
anchors.right: rectUnread.left
|
||||||
font.pixelSize: 16
|
font.pixelSize: 16
|
||||||
font.italic: !isTrusted
|
font.italic: !trusted
|
||||||
textFormat: Text.PlainText
|
textFormat: Text.PlainText
|
||||||
//fontSizeMode: Text.HorizontalFit
|
//fontSizeMode: Text.HorizontalFit
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
|
@ -61,7 +61,7 @@ RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
|
||||||
width: lblUnread.width + 10
|
width: lblUnread.width + 10
|
||||||
radius: 8
|
radius: 8
|
||||||
color: "#4B3557"
|
color: "#4B3557"
|
||||||
visible: badge != "0"
|
visible: badge != 0
|
||||||
anchors.rightMargin: 9
|
anchors.rightMargin: 9
|
||||||
|
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
|
||||||
gcd.broadcast("ResetMessagePane")
|
gcd.broadcast("ResetMessagePane")
|
||||||
isActive = true
|
isActive = true
|
||||||
theStack.pane = theStack.messagePane
|
theStack.pane = theStack.messagePane
|
||||||
gcd.loadMessagesPane(onion)
|
gcd.loadMessagesPane(handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
onEntered: {
|
onEntered: {
|
||||||
|
@ -100,25 +100,18 @@ RowLayout { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY
|
||||||
Connections { // UPDATE UNREAD MESSAGES COUNTER
|
Connections { // UPDATE UNREAD MESSAGES COUNTER
|
||||||
target: gcd
|
target: gcd
|
||||||
|
|
||||||
onSetUnread: function(foronion, n) {
|
|
||||||
if (onion == foronion) {
|
|
||||||
badge = ""+n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onResetMessagePane: function() {
|
onResetMessagePane: function() {
|
||||||
isActive = false
|
isActive = false
|
||||||
}
|
}
|
||||||
|
|
||||||
onSetConnectionStatus: function(foronion, x) {
|
onUpdateContact: function(_handle, _displayName, _image, _server, _badge, _status, _trusted) {
|
||||||
if (onion == foronion) {
|
if (handle == _handle) {
|
||||||
status = x
|
displayName = _displayName
|
||||||
}
|
image = _image
|
||||||
}
|
server = _server
|
||||||
|
badge = _badge
|
||||||
onMarkTrusted: function(foronion) {
|
status = _status
|
||||||
if (onion == foronion) {
|
trusted = _trusted
|
||||||
isTrusted = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -15,10 +15,12 @@ RowLayout {
|
||||||
|
|
||||||
property alias message: lbl.text
|
property alias message: lbl.text
|
||||||
property string from
|
property string from
|
||||||
property string displayname
|
property string handle
|
||||||
|
property string displayName
|
||||||
property int messageID
|
property int messageID
|
||||||
|
property bool fromMe
|
||||||
property alias timestamp: ts.text
|
property alias timestamp: ts.text
|
||||||
property alias source: imgProfile.source
|
property alias image: imgProfile.source
|
||||||
property alias status: imgProfile.status
|
property alias status: imgProfile.status
|
||||||
|
|
||||||
|
|
||||||
|
@ -107,14 +109,14 @@ RowLayout {
|
||||||
color: "#FFFFFF"
|
color: "#FFFFFF"
|
||||||
font.pixelSize: 10
|
font.pixelSize: 10
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
text: displayname
|
text: displayName
|
||||||
visible: from != "me"
|
visible: from != "me"
|
||||||
}
|
}
|
||||||
|
|
||||||
Image { // ACKNOWLEDGEMENT ICON
|
Image { // ACKNOWLEDGEMENT ICON
|
||||||
id: ack
|
id: ack
|
||||||
anchors.right: parent.right
|
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
|
height: 10
|
||||||
sourceSize.height: 10
|
sourceSize.height: 10
|
||||||
visible: from == "me"
|
visible: from == "me"
|
||||||
|
|
|
@ -14,7 +14,10 @@ ColumnLayout {
|
||||||
StackToolbar {
|
StackToolbar {
|
||||||
text: "open privacy exec"
|
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
|
Flickable { // THE MESSAGE LIST ITSELF
|
||||||
|
@ -38,14 +41,16 @@ ColumnLayout {
|
||||||
messagesModel.clear()
|
messagesModel.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
onAppendMessage: function(from, message, displayname, mid, ts, source) {
|
onAppendMessage: function(handle, from, displayName, message, image, mid, fromMe, ts) {
|
||||||
messagesModel.append({
|
messagesModel.append({
|
||||||
"f": from,
|
"_handle": handle,
|
||||||
"m": parse(message, 12),
|
"_from": from,
|
||||||
"d": displayname,
|
"_displayName": displayName,
|
||||||
"i": mid,
|
"_message": parse(message, 12),
|
||||||
"t": ts,
|
"_image": image,
|
||||||
"src": source
|
"_mid": mid,
|
||||||
|
"_fromMe": fromMe,
|
||||||
|
"_ts": ts,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (sv.contentY + sv.height >= sv.contentHeight - colMessages.height && sv.contentHeight > sv.height) {
|
if (sv.contentY + sv.height >= sv.contentHeight - colMessages.height && sv.contentHeight > sv.height) {
|
||||||
|
@ -73,12 +78,14 @@ ColumnLayout {
|
||||||
Repeater { // ... AND DISPLAYED HERE
|
Repeater { // ... AND DISPLAYED HERE
|
||||||
model: messagesModel
|
model: messagesModel
|
||||||
delegate: Message {
|
delegate: Message {
|
||||||
from: f
|
handle: _handle
|
||||||
message: m
|
from: _from
|
||||||
displayname: d
|
displayName: _displayName
|
||||||
messageID: i
|
message: _message
|
||||||
timestamp: t
|
image: _image
|
||||||
source: src
|
messageID: _mid
|
||||||
|
fromMe: _fromMe
|
||||||
|
timestamp: _ts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,8 +95,8 @@ ColumnLayout {
|
||||||
Rectangle { // MESSAGE ENTRY TEXTFIELD
|
Rectangle { // MESSAGE ENTRY TEXTFIELD
|
||||||
id: rectMessage
|
id: rectMessage
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.minimumHeight: 40 * zoomSlider.value
|
Layout.minimumHeight: 40 * gcd.themeScale
|
||||||
Layout.maximumHeight: 40 * zoomSlider.value
|
Layout.maximumHeight: 40 * gcd.themeScale
|
||||||
color: "#EDEDED"
|
color: "#EDEDED"
|
||||||
border.color: "#AAAAAA"
|
border.color: "#AAAAAA"
|
||||||
radius: 10
|
radius: 10
|
||||||
|
|
|
@ -149,7 +149,7 @@ ColumnLayout {
|
||||||
|
|
||||||
Row { // TOOLS FOR EDITING PROFILE
|
Row { // TOOLS FOR EDITING PROFILE
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
spacing: zoomSlider.value * 2
|
spacing: gcd.themeScale * 2
|
||||||
|
|
||||||
|
|
||||||
TextEdit { // USED TO POWER THE COPY/PASTE BUTTON
|
TextEdit { // USED TO POWER THE COPY/PASTE BUTTON
|
||||||
|
@ -164,8 +164,8 @@ ColumnLayout {
|
||||||
onClicked: {
|
onClicked: {
|
||||||
gcd.popup("copied to clipboard!")
|
gcd.popup("copied to clipboard!")
|
||||||
txtHidden.text = nick.replace(" ", "~") + "~" + onion
|
txtHidden.text = nick.replace(" ", "~") + "~" + onion
|
||||||
txtHidden.selectAll();
|
txtHidden.selectAll()
|
||||||
txtHidden.copy();
|
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 {
|
RowLayout {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
|
Binary file not shown.
|
@ -7,7 +7,7 @@ import QtQuick.Window 2.11
|
||||||
|
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
font.pixelSize: zoomSlider.value * size
|
font.pixelSize: gcd.themeScale * size
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
|
||||||
property real size: 12
|
property real size: 12
|
||||||
|
|
|
@ -9,10 +9,10 @@ import "../fonts/Twemoji.js" as T
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: button
|
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.minimumWidth: width
|
||||||
Layout.maximumWidth: width
|
Layout.maximumWidth: width
|
||||||
height: 20 * zoomSlider.value
|
height: 20 * gcd.themeScale
|
||||||
Layout.minimumHeight: height
|
Layout.minimumHeight: height
|
||||||
Layout.maximumHeight: height
|
Layout.maximumHeight: height
|
||||||
color: mousedown ? "#B09CBC" : "#4B3557"
|
color: mousedown ? "#B09CBC" : "#4B3557"
|
||||||
|
|
Binary file not shown.
|
@ -12,7 +12,7 @@ Rectangle { // OVERHEAD BAR ON STACK PANE
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
height: 20 * zoomSlider.value + 4
|
height: 20 * gcd.themeScale + 4
|
||||||
Layout.minimumHeight: height
|
Layout.minimumHeight: height
|
||||||
Layout.maximumHeight: height
|
Layout.maximumHeight: height
|
||||||
color: "#EDEDED"
|
color: "#EDEDED"
|
||||||
|
|
Loading…
Reference in New Issue