diff --git a/.drone.yml b/.drone.yml index 9a72ab71..f48637a2 100644 --- a/.drone.yml +++ b/.drone.yml @@ -14,15 +14,16 @@ pipeline: - QT_DIR=/opt/Qt - QT_DOCKER='true' - QT_API=5.13.0 + - GO111MODULE=on commands: - - export GOPATH=$GOPATH:/media/sf_GOPATH1/ - export PATH=$PATH:/home/user/work/bin:/media/sf_GOPATH1/bin - apt-get -qq update && apt-get --no-install-recommends -qq -y install ca-certificates curl git openssh-client - - go get -d + - go mod download - $QT_DIR/$QT_API/gcc_64/bin/lrelease ui.pro - git fetch --tags - export VERSION=`git describe --tags` - export BUILDDATE=`date +%G-%m-%d-%H-%M` + - go mod vendor - qtdeploy -ldflags "-X main.buildVer=$VERSION -X main.buildDate=$BUILDDATE" build linux - cp README.md deploy/linux - export FILENAME=cwtch-linux-$BUILDDATE.tar.gz @@ -44,16 +45,18 @@ pipeline: - QT_API=5.13.0 - ANDROID_NDK_DIR=/home/user/android-ndk-r18b - ANDROID_SDK_DIR=/home/user/android-sdk-linux + - GO111MODULE=on commands: - - export GOPATH=$GOPATH:/media/sf_GOPATH1/ - export PATH=$PATH:/home/user/work/bin:/media/sf_GOPATH1/bin - apt-get -qq update && apt-get --no-install-recommends -qq -y install ca-certificates curl git - - find -iname 'moc*' | xargs rm - - find -iname 'rcc*' | xargs rm - - go get -d + - rm -r vendor/ + - make clean + - go mod download - export VERSION=`git describe --tags` - export BUILDDATE=`date +%G-%m-%d-%H-%M` - - qtdeploy -ldflags "-X main.buildVer=$VERSION -X main.buildDate=$BUILDDATE" build android + - go mod vendor + - qtsetup generate android + - qtdeploy -ldflags "-X main.buildVer=$VERSION -X main.buildDate=$BUILDDATE" build android - cd deploy - export FILENAME=cwtch-android-$BUILDDATE.apk - cp android/build-debug.apk $FILENAME @@ -69,15 +72,16 @@ pipeline: - QT_DIR=/opt/Qt - QT_DOCKER='true' - QT_API=5.13.0 + - GO111MODULE=on commands: - - export GOPATH=$GOPATH:/media/sf_GOPATH1/ - export PATH=$PATH:/home/user/work/bin:/media/sf_GOPATH1/bin - apt-get -qq update && apt-get --no-install-recommends -qq -y install ca-certificates curl git zip - - find -iname 'moc*' | xargs rm - - find -iname 'rcc*' | xargs rm - - go get -d + - rm -r vendor + - make clean + - go mod download - export VERSION=`git describe --tags` - export BUILDDATE=`date +%G-%m-%d-%H-%M` + - go mod vendor - qtdeploy -ldflags "-X main.buildVer=$VERSION -X main.buildDate=$BUILDDATE" build windows - cp README.md deploy/windows - cp -r windows/* deploy/windows diff --git a/Makefile b/Makefile index 5292ed7a..342345c0 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,9 @@ .PHONY: all clean linux android -all: - -default: all +all: clean linux android +default: linux clean: + rm -r vendor || true find -type f -iname "moc*" | xargs rm find -iname "rcc*" | xargs rm diff --git a/README.md b/README.md index 96870753..0979608e 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,11 @@ The UI is built using QT so you will need the development libraries and tools fo This code relies on [therecipe/qt](https://github.com/therecipe/qt) before getting started consult the [Installation](https://github.com/therecipe/qt/wiki/Installation) and [Getting Started](https://github.com/therecipe/qt/wiki/Getting-Started) documentation to get that up and running. It will make building this much easier. -We are aiming to use Go module support for versioning but it has some issues working with therecipe/qt so we aren't there yet. For now build with GO111MODULE=off using just the GOPATH for dependancies. +Cwtch UI uses the Go module system for dependancies ## Linux - go get -d + go mod vendor qtdeploy build linux ./deploy/linux/ui -local -debug 2>&1 | grep -v 'Detected anchors on an item that is managed by a layout.' @@ -33,7 +33,7 @@ The grep statement filters out some QML noise. We supply an arm-pie version of tor in `android/libs/armeabi-v7a` with the name `libtor.so` - go get -d + go mod vendor qtdeploy -docker build android adb install deploy/android/build-debug.apk @@ -52,7 +52,7 @@ We supply an arm-pie version of tor in `android/libs/armeabi-v7a` with the name If all that is done, then check out cwtch.im/ui - go get -d + go mod vendor qtdeploy deploy/windows/ui diff --git a/go.mod b/go.mod index aff5778e..7630ee7a 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,13 @@ module cwtch.im/ui go 1.12 require ( - cwtch.im/cwtch v0.3.0 - git.openprivacy.ca/openprivacy/libricochet-go v1.0.6 + cwtch.im/cwtch v0.3.8 + git.openprivacy.ca/openprivacy/libricochet-go v1.0.8 github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f // indirect github.com/kr/pretty v0.1.0 // indirect github.com/stretchr/testify v1.4.0 // indirect - github.com/therecipe/qt v0.0.0-20191221221430-5e239f03fa53 - github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200103041036-2b818d970888 // indirect - github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20191221221430-5e239f03fa53 // indirect + github.com/therecipe/qt v0.0.0-20191101232336-18864661ae4f + github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20191002095216-73192f6811d0 // indirect; Required - do not delete or `go tidy` away golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 // indirect golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect diff --git a/go.sum b/go.sum index a4280120..c623810b 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,10 @@ -cwtch.im/cwtch v0.3.0 h1:RFZyc2m9BowFNdngBs7GcQE41w75jMp3Ku5zEE92v9I= -cwtch.im/cwtch v0.3.0/go.mod h1:8tmtp3c7fccWw9H7s9u6E8GD2PKI3ar21i0fjN8pzd0= -cwtch.im/tapir v0.1.10 h1:V+TkmwXNd6gySZqlVw468wMYEkmDwMSyvhkkpOfUw7w= -cwtch.im/tapir v0.1.10/go.mod h1:EuRYdVrwijeaGBQ4OijDDRHf7R2MDSypqHkSl5DxI34= +cwtch.im/cwtch v0.3.8 h1:QxuDu+sH5VIcLQZGGfah3zuseq02Iyqhm7O2+ATtA9M= +cwtch.im/cwtch v0.3.8/go.mod h1:/CAGNdgidvJ0sOfsWeU2hxlYCXv8usf6kspsfhG8gtQ= +cwtch.im/tapir v0.1.14 h1:lg+reZNT998l++4Q4RQBLXYv3ukqWffhI0Wed9RSjuA= +cwtch.im/tapir v0.1.14/go.mod h1:QwERb982YIes9UOxDqIthm1HZ1xy0YQetD2+XxDbg9Y= git.openprivacy.ca/openprivacy/libricochet-go v1.0.4/go.mod h1:yMSG1gBaP4f1U+RMZXN85d29D39OK5s8aTpyVRoH5FY= -git.openprivacy.ca/openprivacy/libricochet-go v1.0.6 h1:5o4K2qn3otEE1InC5v5CzU0yL7Wl7DhVp4s8H3K6mXY= -git.openprivacy.ca/openprivacy/libricochet-go v1.0.6/go.mod h1:yMSG1gBaP4f1U+RMZXN85d29D39OK5s8aTpyVRoH5FY= +git.openprivacy.ca/openprivacy/libricochet-go v1.0.8 h1:HVoyxfivFaEtkfO5K3piD6oq6MQB1qGF5IB2EYXeCW8= +git.openprivacy.ca/openprivacy/libricochet-go v1.0.8/go.mod h1:6I+vO9Aagv3/yUWv+e7A57H8tgXgR67FCjfSj9Pp970= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= github.com/c-bata/go-prompt v0.2.3/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34= @@ -19,6 +19,10 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f h1:KMlcu9X58lhTA/KrfX8Bi1LQSO4pzoVjTiL3h4Jk+Zk= github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= +github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= +github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -30,6 +34,8 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -43,17 +49,11 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/struCoder/pidusage v0.1.2/go.mod h1:pWBlW3YuSwRl6h7R5KbvA4N8oOqe9LjaKW5CwT1SPjI= -github.com/therecipe/qt v0.0.0-20190824160953-615e084bab56 h1:CAFR/rHptsl8gEP6igtp6VbuQpPALEJ/B+gl9QkyFXU= -github.com/therecipe/qt v0.0.0-20190824160953-615e084bab56/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us= -github.com/therecipe/qt v0.0.0-20191221221430-5e239f03fa53 h1:vmHLq7TFJ0OQLZzJF0mnCQW6o7NKV319X4F9ImMcLv4= -github.com/therecipe/qt v0.0.0-20191221221430-5e239f03fa53/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us= -github.com/therecipe/qt v0.0.0-20200103041036-2b818d970888 h1:kwDtZGIbjPGYzvs4Dk/4O4E2nnJugQkccLyfFUHpHk0= -github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200103041036-2b818d970888 h1:GnH3hKsPT8vfSw6LQ6+gHoYdJw7Zd5bifdXzP/Z1tus= -github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200103041036-2b818d970888/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc= -github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20191221221430-5e239f03fa53 h1:nnH71oC0mqkEFEZT8hxykAV+7/yQXial8gbJuxNQNdY= -github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20191221221430-5e239f03fa53/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4= -github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200103041036-2b818d970888 h1:4tZ5WKqMm1U6iOM3kaRvD+mndR5flF61YMXr7gxQ9TU= -github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200103041036-2b818d970888/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4= +github.com/therecipe/qt v0.0.0-20191101232336-18864661ae4f h1:06ICDSmDOBUC9jwgv44ngvyHzwudJNLa5H+rbCyDFRY= +github.com/therecipe/qt v0.0.0-20191101232336-18864661ae4f/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us= +github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20191002095216-73192f6811d0/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4= +go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= @@ -69,6 +69,7 @@ golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -78,7 +79,6 @@ golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20190420181800-aa740d480789 h1:FF0rjo15h51+N6642mf5S3QuplmKo2aCrJUYkHTx85s= golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= diff --git a/go/characters/appEventListener.go b/go/characters/appEventListener.go deleted file mode 100644 index 0c328d90..00000000 --- a/go/characters/appEventListener.go +++ /dev/null @@ -1,135 +0,0 @@ -package characters - -import ( - "cwtch.im/cwtch/app/plugins" - "cwtch.im/cwtch/event" - "cwtch.im/ui/go/constants" - "cwtch.im/ui/go/cwutil" - "cwtch.im/ui/go/gobjects" - "cwtch.im/ui/go/gothings" - "cwtch.im/ui/go/the" - "git.openprivacy.ca/openprivacy/libricochet-go/log" - "os" - "strconv" -) - -func AppEventListener(gcd *gothings.GrandCentralDispatcher, subscribed chan bool) { - q := event.NewQueue() - the.AppBus.Subscribe(event.NewPeer, q) - the.AppBus.Subscribe(event.PeerError, q) - the.AppBus.Subscribe(event.AppError, q) - the.AppBus.Subscribe(event.ACNStatus, q) - the.AppBus.Subscribe(event.ReloadDone, q) - subscribed <- true - - for { - e := q.Next() - - switch e.EventType { - case event.ACNStatus: - progStr := e.Data[event.Progreess] - percent, _ := strconv.Atoi(progStr) - message := e.Data[event.Status] - var statuscode int - if percent >= 0 && percent <= 25 { - statuscode = 1 - message = "Connecting to network" - } else if percent < 100 { - statuscode = 2 - message = "Establishng Tor circut" - } else if percent == 100 { - statuscode = 3 - message = "tor appears to be running just fine!" - } else { - statuscode = 0 - message = "can't find tor. is it running? is the controlport configured?" - } - - gcd.TorStatus(statuscode, message) - case event.PeerError: - // current only case - log.Errorf("couldn't load profiles: %v", e.Data[event.Error]) - os.Exit(1) - - case event.AppError: - - if e.Data[event.Error] == event.AppErrLoaded0 { - // TODO: only an error if other profiles are not loaded - log.Infoln("couldn't load your config file. attempting to create one now") - - the.CwtchApp.CreatePeer("alice", the.AppPassword) - } - - case event.ReloadDone: - if the.Peer == nil { - the.CwtchApp.LoadProfiles(the.AppPassword) - } - case event.NewPeer: - if the.Peer != nil { - continue - } - onion := e.Data[event.Identity] - - the.CwtchApp.AddPeerPlugin(onion, plugins.CONTACTRETRY) - - the.Peer = the.CwtchApp.GetPeer(onion) - the.EventBus = the.CwtchApp.GetEventBus(onion) - - incSubscribed := make(chan bool) - go IncomingListener(&gcd.UIState, incSubscribed) - <-incSubscribed - - gcd.UpdateMyProfile(the.Peer.GetProfile().Name, the.Peer.GetProfile().Onion, cwutil.RandomProfileImage(the.Peer.GetProfile().Onion)) - - contacts := the.Peer.GetContacts() - for i := range contacts { - contact, _ := the.Peer.GetProfile().GetContact(contacts[i]) - displayName, _ := contact.GetAttribute("nick") - - gcd.UIState.AddContact(&gobjects.Contact{ - Handle: contacts[i], - DisplayName: displayName, - Image: cwutil.RandomProfileImage(contacts[i]), - Trusted: contact.Trusted, - Blocked: contact.Blocked, - Loading: false, - }) - } - - groups := the.Peer.GetGroups() - for i := range groups { - group := the.Peer.GetGroup(groups[i]) - nick, exists := group.GetAttribute("nick") - if !exists { - nick = group.GroupID[:12] - } - // Only join servers for active and explicitly accepted groups. - gcd.UIState.AddContact(&gobjects.Contact{ - Handle: group.GroupID, - DisplayName: nick, - Image: cwutil.RandomGroupImage(group.GroupID), - Server: group.GroupServer, - Trusted: group.Accepted, - Loading: false, - }) - } - - if e.Data[event.Status] != "running" { - the.CwtchApp.LaunchPeers() - } - - // load ui preferences - gcd.RequestSettings() - locale, exists := the.Peer.GetProfile().GetAttribute(constants.LocaleSetting) - if exists { - gcd.SetLocale_helper(locale) - } - - blockUnkownPeers, exists := the.Peer.GetProfile().GetAttribute(constants.BlockUnknownPeersSetting) - if exists && blockUnkownPeers == "true" { - the.EventBus.Publish(event.NewEvent(event.BlockUnknownPeers, map[event.Field]string{})) - } - } - } - -} diff --git a/go/characters/incominglistener.go b/go/characters/incominglistener.go deleted file mode 100644 index c2f6697d..00000000 --- a/go/characters/incominglistener.go +++ /dev/null @@ -1,154 +0,0 @@ -package characters - -import ( - "cwtch.im/cwtch/event" - "cwtch.im/cwtch/protocol/connections" - "cwtch.im/ui/go/cwutil" - "cwtch.im/ui/go/gobjects" - "cwtch.im/ui/go/gothings" - "cwtch.im/ui/go/the" - "git.openprivacy.ca/openprivacy/libricochet-go/log" - "time" -) - -func IncomingListener(uiState *gothings.InterfaceState, subscribed chan bool) { - q := event.NewQueue() - the.EventBus.Subscribe(event.NewMessageFromPeer, q) - the.EventBus.Subscribe(event.PeerAcknowledgement, q) - the.EventBus.Subscribe(event.NewMessageFromGroup, q) - the.EventBus.Subscribe(event.NewGroupInvite, q) - the.EventBus.Subscribe(event.SendMessageToGroupError, q) - the.EventBus.Subscribe(event.SendMessageToPeerError, q) - the.EventBus.Subscribe(event.ServerStateChange, q) - the.EventBus.Subscribe(event.PeerStateChange, q) - subscribed <- true - - for { - e := q.Next() - - switch e.EventType { - case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data - ts, _ := time.Parse(time.RFC3339Nano, e.Data[event.TimestampReceived]) - uiState.AddMessage(&gobjects.Message{ - Handle: e.Data[event.RemotePeer], - From: e.Data[event.RemotePeer], - Message: e.Data[event.Data], - Image: cwutil.RandomProfileImage(e.Data[event.RemotePeer]), - Timestamp: ts, - }) - if the.Peer.GetContact(e.Data[event.RemotePeer]) == nil { - the.Peer.AddContact(e.Data[event.RemotePeer], e.Data[event.RemotePeer], false) - } - - case event.PeerAcknowledgement: - ackI, ok := the.AcknowledgementIDs.Load(e.Data[event.EventID]) - if ok { - ack := ackI.(*the.AckId) - if ack.Peer == e.Data[event.RemotePeer] { - ack.Ack = true - uiState.Acknowledge(e.Data[event.EventID]) - continue - } - } - log.Debugf("Received Ack ID for unknown message or peer: %v", e) - case event.NewMessageFromGroup: //event.TimestampReceived, event.TimestampSent, event.Data, event.GroupID, event.RemotePeer - var name string - var exists bool - ctc := the.Peer.GetContact(e.Data[event.RemotePeer]) - if ctc != nil { - name, exists = ctc.GetAttribute("nick") - if !exists || name == "" { - name = e.Data[event.RemotePeer] - } - } else { - name = e.Data[event.RemotePeer] - } - - ts, _ := time.Parse(time.RFC3339Nano, e.Data[event.TimestampSent]) - uiState.AddMessage(&gobjects.Message{ - MessageID: e.Data[event.Signature], - Handle: e.Data[event.GroupID], - From: e.Data[event.RemotePeer], - Message: e.Data[event.Data], - Image: cwutil.RandomProfileImage(e.Data[event.RemotePeer]), - FromMe: e.Data[event.RemotePeer] == the.Peer.GetProfile().Onion, - Timestamp: ts, - Acknowledged: true, - DisplayName: name, - }) - case event.NewGroupInvite: - gid, err := the.Peer.GetProfile().ProcessInvite(e.Data[event.GroupInvite], e.Data[event.RemotePeer]) - group := the.Peer.GetGroup(gid) - if err == nil && group != nil { - uiState.AddContact(&gobjects.Contact{ - Handle: gid, - DisplayName: gid, - Image: cwutil.RandomGroupImage(gid), - Server: group.GroupServer, - Trusted: false, - Loading: false, - }) - } - case event.SendMessageToGroupError: - uiState.AddSendMessageError(e.Data[event.GroupServer], e.Data[event.Signature], e.Data[event.Error]) - case event.SendMessageToPeerError: - uiState.AddSendMessageError(e.Data[event.RemotePeer], e.Data[event.Signature], e.Data[event.Error]) - case event.PeerStateChange: - cxnState := connections.ConnectionStateToType[e.Data[event.ConnectionState]] - - // if it's not in the.Peer it's new. Only add once Authed - _, exists := the.Peer.GetProfile().Contacts[e.Data[event.RemotePeer]] - if !exists { - // Contact does not exist, we will add them but we won't know who they are until they are authenticated - // So if we get any other state from an unknown contact we do nothing - // (the next exists check will fail) - if cxnState == connections.AUTHENTICATED { - the.Peer.AddContact(e.Data[event.RemotePeer], e.Data[event.RemotePeer], false) - } - } - - // if it's in the.Peer its either existing and needs an update or not in the UI and needs to be added - if contact, exists := the.Peer.GetProfile().Contacts[e.Data[event.RemotePeer]]; exists { - contact.State = e.Data[event.ConnectionState] - uiContact := uiState.GetContact(contact.Onion) - if uiContact != nil { - if uiContact.Status != int(cxnState) { - uiContact.Status = int(cxnState) - uiState.UpdateContact(contact.Onion) - } - } else { - uiState.AddContact(&gobjects.Contact{ - e.Data[event.RemotePeer], - e.Data[event.RemotePeer], - cwutil.RandomProfileImage(e.Data[event.RemotePeer]), - "", - 0, - int(cxnState), - false, - false, - false, - }) - } - } - case event.ServerStateChange: - serverOnion := e.Data[event.GroupServer] - state := connections.ConnectionStateToType[e.Data[event.ConnectionState]] - groups := the.Peer.GetGroups() - for _, groupID := range groups { - group := the.Peer.GetGroup(groupID) - group.State = e.Data[event.ConnectionState] - if group != nil && group.GroupServer == serverOnion { - uiState.GetContact(group.GroupID).Status = int(state) - if state == connections.AUTHENTICATED { - uiState.GetContact(group.GroupID).Loading = true - } else { - uiState.GetContact(group.GroupID).Loading = false - } - uiState.UpdateContact(group.GroupID) - } else { - log.Errorf("found group that is nil :/") - } - } - } - } -} diff --git a/go/constants/attributes.go b/go/constants/attributes.go new file mode 100644 index 00000000..7471c265 --- /dev/null +++ b/go/constants/attributes.go @@ -0,0 +1,9 @@ +package constants + +const Nick = "nick" +const LastRead = "last-read" +const Picture = "picture" +const DefaultPassword = "default-password" + +const ProfileTypeV1DefaultPassword = "v1-defaultPassword" +const ProfileTypeV1Password = "v1-userPassword" diff --git a/go/gobjects/contact.go b/go/gobjects/contact.go deleted file mode 100644 index c038c1de..00000000 --- a/go/gobjects/contact.go +++ /dev/null @@ -1,13 +0,0 @@ -package gobjects - -type Contact struct { - Handle string - DisplayName string - Image string - Server string - Badge int - Status int - Trusted bool - Blocked bool - Loading bool -} diff --git a/go/gobjects/letter.go b/go/gobjects/letter.go deleted file mode 100644 index fc35c26f..00000000 --- a/go/gobjects/letter.go +++ /dev/null @@ -1,7 +0,0 @@ -package gobjects - -// a Letter is a very simple message object passed to us from the UI -type Letter struct { - To, Message string - MID string -} diff --git a/go/gobjects/message.go b/go/gobjects/message.go deleted file mode 100644 index 01284ffb..00000000 --- a/go/gobjects/message.go +++ /dev/null @@ -1,16 +0,0 @@ -package gobjects - -import "time" - -type Message struct { - Handle string - From string - DisplayName string - Message string - Image string - FromMe bool - MessageID string - Timestamp time.Time - Acknowledged bool - Error bool -} diff --git a/go/gothings/uistate.go b/go/gothings/uistate.go deleted file mode 100644 index 8f55c800..00000000 --- a/go/gothings/uistate.go +++ /dev/null @@ -1,195 +0,0 @@ -package gothings - -import ( - "cwtch.im/ui/go/constants" - "cwtch.im/ui/go/cwutil" - "cwtch.im/ui/go/gobjects" - "cwtch.im/ui/go/the" - "git.openprivacy.ca/openprivacy/libricochet-go/log" - "runtime/debug" - "sync" - "time" -) - -type InterfaceState struct { - parentGcd *GrandCentralDispatcher - contacts sync.Map // string : *gobjects.Contact - messages sync.Map -} - -func NewUIState(gcd *GrandCentralDispatcher) (uis InterfaceState) { - uis = InterfaceState{gcd, sync.Map{}, sync.Map{}} - return -} - -func (this *InterfaceState) Acknowledge(mID string) { - this.parentGcd.Acknowledged(mID) -} - -func (this *InterfaceState) AddContact(c *gobjects.Contact) { - if len(c.Handle) == 32 { // ADD GROUP - if _, found := this.contacts.Load(c.Handle); !found { - this.parentGcd.AddContact(c.Handle, c.DisplayName, c.Image, c.Server, c.Badge, c.Status, c.Trusted, c.Blocked, c.Loading) - this.contacts.Store(c.Handle, c) - } - return - } else if len(c.Handle) != 56 { - log.Errorf("sorry, unable to handle AddContact(%v)", c.Handle) - debug.PrintStack() - return - } - - if _, found := this.contacts.Load(c.Handle); !found { - this.contacts.Store(c.Handle, c) - this.parentGcd.AddContact(c.Handle, c.DisplayName, c.Image, c.Server, c.Badge, c.Status, c.Trusted, c.Blocked, false) - if the.Peer.GetContact(c.Handle) == nil { - the.Peer.AddContact(c.DisplayName, c.Handle, c.Trusted) - go the.Peer.PeerWithOnion(c.Handle) - } - } -} - -func (this *InterfaceState) DeleteContact(id string) { - this.contacts.Delete(id) -} - -func (this *InterfaceState) GetContact(handle string) *gobjects.Contact { - if _, found := this.contacts.Load(handle); !found { - if len(handle) == 32 { - group := the.Peer.GetGroup(handle) - if group != nil { - nick, exists := group.GetAttribute("nick") - if !exists { - nick = group.GroupID[:12] - } - this.AddContact(&gobjects.Contact{ - handle, - nick, - cwutil.RandomGroupImage(handle), - group.GroupServer, - 0, - 0, - group.Accepted, - false, - false, - }) - } else { - log.Errorf("Attempting to add non existent group to ui %v", handle) - } - } else { - contact := the.Peer.GetContact(handle) - if contact != nil && handle != contact.Onion { - nick, exists := contact.GetAttribute("name") - if !exists { - nick = contact.Onion - } - this.AddContact(&gobjects.Contact{ - handle, - nick, - cwutil.RandomProfileImage(handle), - "", - 0, - 0, - false, - contact.Blocked, - false, - }) - } else if contact == nil { - //log.Errorf("Attempting to add non existent contact to ui %v", handle) - } - } - } - - contactIntf, _ := this.contacts.Load(handle) - if contactIntf == nil { - return nil - } - contact := contactIntf.(*gobjects.Contact) - return contact -} - -func (this *InterfaceState) AddSendMessageError(peer string, signature string, err string) { - ackI, ok := the.AcknowledgementIDs.Load(signature) - if ok { - ack := ackI.(*the.AckId) - ack.Error = true - } - - messages, _ := this.messages.Load(peer) - messageList, _ := messages.([]*gobjects.Message) - - for _, message := range messageList { - if message.MessageID == signature { - message.Error = true - } - } - log.Debugf("Received Error Sending Message: %v", err) - // FIXME: Sometimes, for the first Peer message we send our error beats our message to the UI - time.Sleep(time.Second * 1) - this.parentGcd.GroupSendError(signature, err) -} - -func (this *InterfaceState) AddMessage(m *gobjects.Message) { - this.GetContact(m.From) - - _, found := this.messages.Load(m.Handle) - if !found { - this.messages.Store(m.Handle, make([]*gobjects.Message, 0)) - } - - // Ack message sent to group - this.parentGcd.Acknowledged(m.MessageID) - - messages, _ := this.messages.Load(m.Handle) - messageList, _ := messages.([]*gobjects.Message) - this.messages.Store(m.Handle, append(messageList, m)) - - // If we have this group loaded already - if this.parentGcd.CurrentOpenConversation() == m.Handle { - // If the message is not from the user then add it, otherwise, just acknowledge. - if !m.FromMe || !m.Acknowledged { - this.parentGcd.AppendMessage(m.Handle, m.From, m.DisplayName, m.Message, m.Image, m.MessageID, m.FromMe, m.Timestamp.Format(constants.TIME_FORMAT), m.Acknowledged, m.Error) - } else { - this.parentGcd.Acknowledged(m.MessageID) - } - } else { - c := this.GetContact(m.Handle) - if c != nil { - c.Badge++ - this.UpdateContact(c.Handle) - } - } - -} - -func (this *InterfaceState) GetMessages(handle string) []*gobjects.Message { - _, found := this.messages.Load(handle) - if !found { - this.messages.Store(handle, make([]*gobjects.Message, 0)) - } - messages, found := this.messages.Load(handle) - messageList, _ := messages.([]*gobjects.Message) - return messageList -} - -func (this *InterfaceState) UpdateContact(handle string) { - contact := the.Peer.GetContact(handle) - if contact != nil { - cif, found := this.contacts.Load(handle) - if found { - c := cif.(*gobjects.Contact) - c.Blocked = contact.Blocked - this.parentGcd.UpdateContact(c.Handle, c.DisplayName, c.Image, c.Server, c.Badge, c.Status, c.Trusted, c.Blocked, c.Loading) - } - } else { - cif, found := this.contacts.Load(handle) - if found { - c := cif.(*gobjects.Contact) - this.parentGcd.UpdateContact(c.Handle, c.DisplayName, c.Image, c.Server, c.Badge, c.Status, c.Trusted, c.Blocked, c.Loading) - } - } -} - -func (this *InterfaceState) UpdateContactAttribute(handle, key, value string) { - this.parentGcd.UpdateContactAttribute(handle, key, value) -} diff --git a/go/handlers/appHandler.go b/go/handlers/appHandler.go new file mode 100644 index 00000000..a2085a21 --- /dev/null +++ b/go/handlers/appHandler.go @@ -0,0 +1,123 @@ +package handlers + +import ( + "cwtch.im/cwtch/app" + "cwtch.im/cwtch/app/plugins" + "cwtch.im/cwtch/event" + "cwtch.im/ui/go/constants" + "cwtch.im/ui/go/the" + "cwtch.im/ui/go/ui" + "git.openprivacy.ca/openprivacy/libricochet-go/log" + "os" + "strconv" + "time" +) + +func App(gcd *ui.GrandCentralDispatcher, subscribed chan bool, reloadingAccounts bool) { + q := event.NewQueue() + the.AppBus.Subscribe(event.NewPeer, q) + the.AppBus.Subscribe(event.PeerError, q) + the.AppBus.Subscribe(event.AppError, q) + the.AppBus.Subscribe(event.ACNStatus, q) + the.AppBus.Subscribe(event.NetworkStatus, q) + the.AppBus.Subscribe(event.ReloadDone, q) + subscribed <- true + + networkOffline := false + timeSinceLastSuccess := time.Unix(0, 0) + + gcd.Loaded() + + for { + e := q.Next() + + switch e.EventType { + case event.NetworkStatus: + status := e.Data[event.Status] + if status == "Error" && !networkOffline { + networkOffline = true + // if it has been more that 5 minutes since we received any kind of success, then we should kill tor + // anything less that this i.e. transient networking failures, should allow us to reconnect without issue + if time.Now().Sub(timeSinceLastSuccess) > (time.Minute * 5) { + the.ACN.Restart() + } + } + + if status == "Success" && networkOffline { + timeSinceLastSuccess = time.Now() + networkOffline = false + } + + case event.ACNStatus: + progStr := e.Data[event.Progreess] + percent, _ := strconv.Atoi(progStr) + message := e.Data[event.Status] + var statuscode int + if percent >= 0 && percent <= 25 { + statuscode = 1 + message = "Connecting to network" + } else if percent < 100 { + statuscode = 2 + message = "Establishing Tor circuit" + } else if percent == 100 { + statuscode = 3 + message = "tor appears to be running just fine!" + } else { + statuscode = 0 + message = "can't find tor. is it running? is the controlport configured?" + } + + gcd.TorStatus(statuscode, message) + + case event.PeerError: + // current only case + log.Errorf("couldn't load profiles: %v", e.Data[event.Error]) + os.Exit(1) + + case event.AppError: + + if e.Data[event.Error] == event.AppErrLoaded0 { + if reloadingAccounts { + reloadingAccounts = false + } else { + gcd.ErrorLoaded0() + } + } + + case event.ReloadDone: + reloadingAccounts = false + if len(the.CwtchApp.ListPeers()) == 0 { + the.CwtchApp.LoadProfiles(the.AppPassword) + } + case event.NewPeer: + onion := e.Data[event.Identity] + peer := the.CwtchApp.GetPeer(onion) + + if tag, exists := peer.GetAttribute(app.AttributeTag); !exists || tag == "" { + peer.SetAttribute(app.AttributeTag, constants.ProfileTypeV1DefaultPassword) + } + + log.Infof("NewPeer for %v\n", onion) + ui.AddProfile(gcd, onion) + + the.CwtchApp.AddPeerPlugin(onion, plugins.CONNECTIONRETRY) + the.CwtchApp.AddPeerPlugin(onion, plugins.NETWORKCHECK) + + incSubscribed := make(chan bool) + go PeerHandler(onion, gcd.GetUiManager(peer.GetProfile().Onion), incSubscribed) + <-incSubscribed + + if e.Data[event.Status] != "running" { + peer.Listen() + peer.StartPeersConnections() + peer.StartGroupConnections() + } + + blockUnkownPeers, exists := peer.GetProfile().GetAttribute(constants.BlockUnknownPeersSetting) + if exists && blockUnkownPeers == "true" { + the.EventBus.Publish(event.NewEvent(event.BlockUnknownPeers, map[event.Field]string{})) + } + } + } + +} diff --git a/go/handlers/peerHandler.go b/go/handlers/peerHandler.go new file mode 100644 index 00000000..e64e6005 --- /dev/null +++ b/go/handlers/peerHandler.go @@ -0,0 +1,124 @@ +package handlers + +import ( + "cwtch.im/cwtch/app" + "cwtch.im/cwtch/event" + "cwtch.im/cwtch/protocol/connections" + "cwtch.im/ui/go/constants" + "cwtch.im/ui/go/the" + "cwtch.im/ui/go/ui" + "git.openprivacy.ca/openprivacy/libricochet-go/log" + "time" +) + +func PeerHandler(onion string, uiManager ui.Manager, subscribed chan bool) { + peer := the.CwtchApp.GetPeer(onion) + eventBus := the.CwtchApp.GetEventBus(onion) + q := event.NewQueue() + eventBus.Subscribe(event.NewMessageFromPeer, q) + eventBus.Subscribe(event.PeerAcknowledgement, q) + eventBus.Subscribe(event.NewMessageFromGroup, q) + eventBus.Subscribe(event.NewGroupInvite, q) + eventBus.Subscribe(event.SendMessageToGroupError, q) + eventBus.Subscribe(event.SendMessageToPeerError, q) + eventBus.Subscribe(event.ServerStateChange, q) + eventBus.Subscribe(event.PeerStateChange, q) + eventBus.Subscribe(event.PeerCreated, q) + eventBus.Subscribe(event.NetworkStatus, q) + eventBus.Subscribe(event.ChangePasswordSuccess, q) + eventBus.Subscribe(event.ChangePasswordError, q) + + subscribed <- true + + networkOffline := false + + for { + e := q.Next() + + switch e.EventType { + + case event.NetworkStatus: + the.AppBus.Publish(*e) + if e.Data["Status"] == "Success" && networkOffline { + networkOffline = false + // TODO we may have to reinitialize the peer + } else { + networkOffline = true + } + + case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data + ts, _ := time.Parse(time.RFC3339Nano, e.Data[event.TimestampReceived]) + uiManager.AddMessage(e.Data[event.RemotePeer], e.Data[event.RemotePeer], e.Data[event.Data], false, e.EventID, ts, true) + if peer.GetContact(e.Data[event.RemotePeer]) == nil { + peer.AddContact(e.Data[event.RemotePeer], e.Data[event.RemotePeer], false) + } + + case event.PeerAcknowledgement: + uiManager.Acknowledge(e.Data[event.EventID]) + + case event.NewMessageFromGroup: //event.TimestampReceived, event.TimestampSent, event.Data, event.GroupID, event.RemotePeer + ts, _ := time.Parse(time.RFC3339Nano, e.Data[event.TimestampSent]) + uiManager.AddMessage(e.Data[event.GroupID], e.Data[event.RemotePeer], e.Data[event.Data], e.Data[event.RemotePeer] == peer.GetProfile().Onion, e.Data[event.Signature], ts, true) + case event.NewGroupInvite: + gid, err := peer.GetProfile().ProcessInvite(e.Data[event.GroupInvite], e.Data[event.RemotePeer]) + group := peer.GetGroup(gid) + if err == nil && group != nil { + uiManager.AddContact(gid) + } + case event.PeerCreated: + onion := e.Data[event.RemotePeer] + uiManager.AddContact(onion) + case event.SendMessageToGroupError: + uiManager.AddSendMessageError(e.Data[event.GroupServer], e.Data[event.Signature], e.Data[event.Error]) + case event.SendMessageToPeerError: + uiManager.AddSendMessageError(e.Data[event.RemotePeer], e.Data[event.EventID], e.Data[event.Error]) + case event.PeerStateChange: + cxnState := connections.ConnectionStateToType[e.Data[event.ConnectionState]] + + // if it's not in the.PeerHandler it's new. Only add once Authed + if _, exists := peer.GetProfile().Contacts[e.Data[event.RemotePeer]]; !exists { + // Contact does not exist, we will add them but we won't know who they are until they are authenticated + // So if we get any other state from an unknown contact we do nothing + // (the next exists check will fail) + if cxnState == connections.AUTHENTICATED { + peer.AddContact(e.Data[event.RemotePeer], e.Data[event.RemotePeer], false) + uiManager.AddContact(e.Data[event.RemotePeer]) + } + } + + // if it's in the.PeerHandler its either existing and needs an update or not in the UI and needs to be added + if contact, exists := peer.GetProfile().Contacts[e.Data[event.RemotePeer]]; exists { + contact.State = e.Data[event.ConnectionState] + uiManager.UpdateContactStatus(contact.Onion, int(cxnState), false) + + } + case event.ServerStateChange: + serverOnion := e.Data[event.GroupServer] + state := connections.ConnectionStateToType[e.Data[event.ConnectionState]] + groups := peer.GetGroups() + for _, groupID := range groups { + group := peer.GetGroup(groupID) + if group != nil && group.GroupServer == serverOnion { + group.State = e.Data[event.ConnectionState] + loading := false + if state == connections.AUTHENTICATED { + loading = true + } + uiManager.UpdateContactStatus(group.GroupID, int(state), loading) + } else { + log.Errorf("found group that is nil :/") + } + } + case event.DeletePeer: + log.Infof("PeerHandler got DeletePeer, SHUTTING down!\n") + uiManager.ReloadProfiles() + return + case event.ChangePasswordSuccess: + peer.SetAttribute(app.AttributeTag, constants.ProfileTypeV1Password) + uiManager.ChangePasswordResponse(false) + case event.ChangePasswordError: + uiManager.ChangePasswordResponse(true) + } + + } +} diff --git a/go/the/globals.go b/go/the/globals.go index 013a76cb..b4813824 100644 --- a/go/the/globals.go +++ b/go/the/globals.go @@ -5,7 +5,6 @@ import ( "cwtch.im/cwtch/event" libPeer "cwtch.im/cwtch/peer" "git.openprivacy.ca/openprivacy/libricochet-go/connectivity" - "sync" ) // Terrible, to be replaced when proper profile/password management comes in ~ 0.2 @@ -19,12 +18,3 @@ var ACN connectivity.ACN var Peer libPeer.CwtchPeer var CwtchDir string var IPCBridge event.IPCBridge - -type AckId struct { - ID string - Peer string - Ack bool - Error bool -} - -var AcknowledgementIDs sync.Map diff --git a/go/gothings/android/CwtchActivity.go b/go/ui/android/CwtchActivity.go similarity index 100% rename from go/gothings/android/CwtchActivity.go rename to go/ui/android/CwtchActivity.go diff --git a/go/gothings/gcd.go b/go/ui/gcd.go similarity index 51% rename from go/gothings/gcd.go rename to go/ui/gcd.go index 9842ac05..40d54d1a 100644 --- a/go/gothings/gcd.go +++ b/go/ui/gcd.go @@ -1,12 +1,13 @@ -package gothings +package ui import ( + "cwtch.im/cwtch/app" "cwtch.im/cwtch/event" + "cwtch.im/cwtch/protocol/connections" "cwtch.im/ui/go/constants" - "cwtch.im/ui/go/cwutil" "github.com/therecipe/qt/qml" + "sync" - "cwtch.im/ui/go/gobjects" "cwtch.im/ui/go/the" "encoding/base32" "git.openprivacy.ca/openprivacy/libricochet-go/log" @@ -18,23 +19,41 @@ import ( type GrandCentralDispatcher struct { core.QObject - OutgoingMessages chan gobjects.Letter - UIState InterfaceState - QMLEngine *qml.QQmlApplicationEngine - Translator *core.QTranslator + QMLEngine *qml.QQmlApplicationEngine + Translator *core.QTranslator + + uIManagers map[string]Manager // profile-onion : Manager + + profileLock sync.Mutex + conversationLock sync.Mutex + + m_selectedProfile string + m_selectedConversation string _ string `property:"os"` - _ string `property:"currentOpenConversation"` _ float32 `property:"themeScale"` _ string `property:"version"` _ string `property:"buildDate"` _ string `property:"assetPath"` + _ string `property:"selectedProfile,auto"` + _ string `property:"selectedConversation,auto"` + + // profile management stuff + _ func() `signal:"Loaded"` + _ func(handle, displayname, image, tag string) `signal:"AddProfile"` + _ func() `signal:"ErrorLoaded0"` + _ func() `signal:"ResetProfile"` + _ func() `signal:"ResetProfileList"` + _ func(failed bool) `signal:"ChangePasswordResponse"` // contact list stuff - _ func(handle, displayName, image, server string, badge, status int, trusted bool, blocked bool, loading bool) `signal:"AddContact"` - _ func(handle, displayName, image, server string, badge, status int, trusted bool, blocked bool, loading bool) `signal:"UpdateContact"` - _ func(handle string) `signal:"RemoveContact"` - _ func(handle, key, value string) `signal:"UpdateContactAttribute"` + _ func(handle, displayName, image, server string, badge, status int, blocked bool, loading bool, lastMsgTime int) `signal:"AddContact"` + _ func(handle, displayName string) `signal:"UpdateContactDisplayName"` + _ func(handle string, status int, loading bool) `signal:"UpdateContactStatus"` + _ func(handle string, blocked bool) `signal:"UpdateContactBlocked"` + _ func(handle string) `signal:"IncContactUnreadCount"` + _ func(handle string) `signal:"RemoveContact"` + _ func(handle, key, value string) `signal:"UpdateContactAttribute"` // messages pane stuff _ func(handle, from, displayName, message, image string, mID string, fromMe bool, ts string, ackd bool, error bool) `signal:"AppendMessage"` @@ -57,14 +76,26 @@ type GrandCentralDispatcher struct { _ func(onion, nick string, blocked bool) `signal:"SupplyPeerSettings"` // signals emitted from the ui (written in go, below) + // ui + _ func() `signal:"onActivate,auto"` + _ func(locale string) `signal:"setLocale,auto"` + // profile managemenet + _ func(onion, nick string) `signal:"updateNick,auto"` + _ func(handle string) `signal:"loadProfile,auto"` + _ func(nick string, defaultPass bool, password string) `signal:"createProfile,auto"` + _ func(password string) `signal:"unlockProfiles,auto"` + _ func() `signal:"reloadProfileList,auto"` + _ func(onion string) `signal:"deleteProfile,auto"` + _ func(onion, currentPassword, newPassword string, defaultPass bool) `signal:"changePassword,auto""` + // operating a profile _ func(message string, mid string) `signal:"sendMessage,auto"` _ func(onion string) `signal:"blockPeer,auto"` _ func(onion string) `signal:"unblockPeer,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:"createContact,auto"` _ func(str string) `signal:"popup,auto"` - _ func(nick string) `signal:"updateNick,auto"` _ func(server, groupName string) `signal:"createGroup,auto"` _ func(groupID string) `signal:"leaveGroup,auto"` _ func(groupID string) `signal:"acceptGroup,auto"` @@ -77,9 +108,85 @@ type GrandCentralDispatcher struct { _ func(onion, groupID string) `signal:"inviteToGroup,auto"` _ func(onion, key, nick string) `signal:"setAttribute,auto"` _ func(onion string) `signal:"deleteContact,auto"` - _ func(locale string) `signal:"setLocale,auto"` _ func() `signal:"allowUnknownPeers,auto"` _ func() `signal:"blockUnknownPeers,auto"` + + _ func() `constructor:"init"` +} + +func (this *GrandCentralDispatcher) init() { + this.uIManagers = make(map[string]Manager) +} + +// GetUiManager gets (and creates if required) a ui Manager for the supplied profile id +func (this *GrandCentralDispatcher) GetUiManager(profile string) Manager { + this.profileLock.Lock() + defer this.profileLock.Unlock() + + if manager, exists := this.uIManagers[profile]; exists { + return manager + } else { + this.uIManagers[profile] = NewManager(profile, this) + return this.uIManagers[profile] + } +} + +func (this *GrandCentralDispatcher) selectedProfile() string { + this.profileLock.Lock() + defer this.profileLock.Unlock() + + return this.m_selectedProfile +} + +func (this *GrandCentralDispatcher) setSelectedProfile(onion string) { + this.profileLock.Lock() + defer this.profileLock.Unlock() + + this.m_selectedProfile = onion +} + +func (this *GrandCentralDispatcher) selectedProfileChanged(onion string) { + this.SelectedProfileChanged(onion) +} + +// DoIfProfile performs a gcd action for a profile IF it is the currently selected profile in the UI +// otherwise it does nothing. it also locks profile switching for the duration of the action +func (this *GrandCentralDispatcher) DoIfProfile(profile string, fn func()) { + this.profileLock.Lock() + defer this.profileLock.Unlock() + + if this.m_selectedProfile == profile { + fn() + } +} + +func (this *GrandCentralDispatcher) selectedConversation() string { + this.conversationLock.Lock() + defer this.conversationLock.Unlock() + + return this.m_selectedConversation +} + +func (this *GrandCentralDispatcher) setSelectedConversation(handle string) { + this.conversationLock.Lock() + defer this.conversationLock.Unlock() + + this.m_selectedConversation = handle +} + +func (this *GrandCentralDispatcher) selectedConversationChanged(handle string) { + this.SelectedConversationChanged(handle) +} + +// DoIfConversation performs a gcd action for a conversation IF it is the currently selected conversation in the UI +// otherwise it does nothing. it also locks conversation switching for the duration of the action +func (this *GrandCentralDispatcher) DoIfConversation(conversation string, fn func()) { + this.conversationLock.Lock() + defer this.conversationLock.Unlock() + + if this.m_selectedConversation == conversation { + fn() + } } func (this *GrandCentralDispatcher) sendMessage(message string, mID string) { @@ -88,72 +195,34 @@ func (this *GrandCentralDispatcher) sendMessage(message string, mID string) { return } - if this.CurrentOpenConversation() == "" { + if this.SelectedConversation() == "" { this.InvokePopup("ui error") return } - if len(this.CurrentOpenConversation()) == 32 { // SEND TO GROUP - if !the.Peer.GetGroup(this.CurrentOpenConversation()).Accepted { - err := the.Peer.AcceptInvite(this.CurrentOpenConversation()) + if isGroup(this.SelectedConversation()) { + if !the.Peer.GetGroup(this.SelectedConversation()).Accepted { + err := the.Peer.AcceptInvite(this.SelectedConversation()) if err != nil { log.Errorf("tried to mark a nonexistent group as existed. bad!") return } - c := this.UIState.GetContact(this.CurrentOpenConversation()) - c.Trusted = true - this.UIState.UpdateContact(c.Handle) } var err error - mID, err = the.Peer.SendMessageToGroupTracked(this.CurrentOpenConversation(), message) + mID, err = the.Peer.SendMessageToGroupTracked(this.SelectedConversation(), message) - this.UIState.AddMessage(&gobjects.Message{ - this.CurrentOpenConversation(), - "me", - "", - message, - "", - true, - mID, - time.Now(), - false, - false, - }) + this.GetUiManager(this.selectedProfile()).AddMessage(this.SelectedConversation(), "me", message, true, mID, time.Now(), false) if err != nil { this.InvokePopup("failed to send message " + err.Error()) return } } else { - - // 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()) - } - - to := this.CurrentOpenConversation() + to := this.SelectedConversation() mID = the.Peer.SendMessageToPeer(to, message) - this.UIState.AddMessage(&gobjects.Message{ - to, - "me", - "", - message, - "", - true, - mID, - time.Now(), - false, - false, - }) - - ackID := new(the.AckId) - ackID.ID = mID - ackID.Ack = false - ackID.Peer = to - the.AcknowledgementIDs.Store(mID, ackID) + this.GetUiManager(this.selectedProfile()).AddMessage(to, "me", message, true, mID, time.Now(), false) } } @@ -167,30 +236,21 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { return } this.ClearMessages() - this.SetCurrentOpenConversation(handle) - c := this.UIState.GetContact(handle) + this.SetSelectedConversation(handle) - if c == nil { - this.UIState.AddContact(&gobjects.Contact{ - handle, - handle, - cwutil.RandomProfileImage(handle), - "", - 0, - 0, - false, - false, - false, - }) - } else { - c.Badge = 0 - this.UIState.UpdateContact(handle) - } - - if len(handle) == 32 { // LOAD GROUP + if isGroup(handle) { // LOAD GROUP group := the.Peer.GetGroup(handle) + + loading := false + state := connections.ConnectionStateToType[group.State] + if state == connections.AUTHENTICATED { + loading = true + } + this.UpdateContactStatus(group.GroupID, int(state), loading) + tl := group.GetTimeline() - nick, _ := group.GetAttribute("nick") + nick, _ := group.GetAttribute(constants.Nick) + updateLastReadTime(group.GroupID) if nick == "" { this.SetToolbarTitle(handle) } else { @@ -203,24 +263,16 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { } else { handle = tl[i].PeerID } - var name string - var exists bool - ctc := the.Peer.GetContact(tl[i].PeerID) - if ctc != nil { - name, exists = ctc.GetAttribute("nick") - if !exists || name == "" { - name = tl[i].PeerID - } - } else { - name = tl[i].PeerID - } + + name := getOrDefault(tl[i].PeerID, constants.Nick, tl[i].PeerID) + image := getProfilePic(tl[i].PeerID) this.PrependMessage( handle, tl[i].PeerID, name, tl[i].Message, - cwutil.RandomProfileImage(tl[i].PeerID), + image, string(tl[i].Signature), tl[i].PeerID == the.Peer.GetProfile().Onion, tl[i].Timestamp.Format(constants.TIME_FORMAT), @@ -232,42 +284,43 @@ func (this *GrandCentralDispatcher) loadMessagesPaneHelper(handle string) { } // ELSE LOAD CONTACT contact, _ := the.Peer.GetProfile().GetContact(handle) + + this.UpdateContactStatus(handle, int(connections.ConnectionStateToType[contact.State]), false) + + var nick string if contact != nil { - nick, _ := contact.GetAttribute("nick") + nick, _ = contact.GetAttribute(constants.Nick) if nick == "" { - this.SetToolbarTitle(handle) - } else { - this.SetToolbarTitle(nick) + nick = handle } } + updateLastReadTime(contact.Onion) + this.SetToolbarTitle(nick) - messages := this.UIState.GetMessages(handle) + peer := the.Peer.GetContact(handle) + messages := peer.Timeline.GetMessages() for i := range messages { - from := messages[i].From - if messages[i].FromMe { + from := messages[i].PeerID + fromMe := messages[i].PeerID == the.Peer.GetProfile().Onion + if fromMe { from = "me" } - ackI, ok := the.AcknowledgementIDs.Load(messages[i].MessageID) - acked := false - if ok { - ack := ackI.(*the.AckId) - acked = ack.Ack - } + displayname := getOrDefault(messages[i].PeerID, constants.Nick, messages[i].PeerID) + image := getProfilePic(messages[i].PeerID) this.AppendMessage( - messages[i].Handle, from, - messages[i].DisplayName, + messages[i].PeerID, + displayname, messages[i].Message, - cwutil.RandomProfileImage(handle), - messages[i].MessageID, - messages[i].FromMe, + image, + string(messages[i].Signature), + fromMe, messages[i].Timestamp.Format(constants.TIME_FORMAT), - acked, - messages[i].Error, + messages[i].Acknowledged, + messages[i].Error != "", ) - } } @@ -285,25 +338,22 @@ func (this *GrandCentralDispatcher) saveSettings(zoom, locale string) { return } - the.EventBus.Publish(event.NewEvent(event.SetAttribute, map[event.Field]string{ - event.Key: constants.ZoomSetting, - event.Data: zoom, - })) - the.Peer.GetProfile().SetAttribute(constants.ZoomSetting, zoom) + the.Peer.SetAttribute(constants.ZoomSetting, zoom) } func (this *GrandCentralDispatcher) requestPeerSettings() { - contact := the.Peer.GetContact(this.CurrentOpenConversation()) + contact := the.Peer.GetContact(this.SelectedConversation()) if contact == nil { - log.Errorf("error: requested settings for unknown contact %v?", this.CurrentOpenConversation()) - this.SupplyPeerSettings(this.CurrentOpenConversation(), this.CurrentOpenConversation(), false) + log.Errorf("error: requested settings for unknown contact %v?", this.SelectedConversation()) + this.SupplyPeerSettings(this.SelectedConversation(), this.SelectedConversation(), false) return } - name, exists := contact.GetAttribute("nick") + name, exists := contact.GetAttribute(constants.Nick) if !exists { - log.Errorf("error: couldn't find contact %v", this.CurrentOpenConversation()) - this.SupplyPeerSettings(this.CurrentOpenConversation(), this.CurrentOpenConversation(), contact.Blocked) + log.Errorf("error: couldn't find contact %v", this.SelectedConversation()) + this.SupplyPeerSettings(this.SelectedConversation(), this.SelectedConversation(), contact.Blocked) + this.SupplyPeerSettings(this.SelectedConversation(), this.SelectedConversation(), contact.Blocked) return } @@ -311,23 +361,8 @@ func (this *GrandCentralDispatcher) requestPeerSettings() { } func (this *GrandCentralDispatcher) savePeerSettings(onion, nick string) { - contact := the.Peer.GetContact(onion) - if contact == nil { - log.Errorf("error: tried to save settings for unknown peer %v", onion) - return - } - - contact.SetAttribute("nick", nick) - the.EventBus.Publish(event.NewEvent(event.SetPeerAttribute, map[event.Field]string{ - event.RemotePeer: onion, - event.Key: "nick", - event.Data: nick, - })) - - cif, _ := this.UIState.contacts.Load(onion) - c := cif.(*gobjects.Contact) - c.DisplayName = nick - this.UIState.UpdateContact(onion) + the.Peer.SetContactAttribute(onion, constants.Nick, nick) + this.UpdateContactDisplayName(onion, nick) } func (this *GrandCentralDispatcher) requestGroupSettings(groupID string) { @@ -338,13 +373,13 @@ func (this *GrandCentralDispatcher) requestGroupSettings(groupID string) { return } - nick, _ := group.GetAttribute("nick") + nick, _ := group.GetAttribute(constants.Nick) invite, _ := the.Peer.ExportGroup(groupID) contactaddrs := the.Peer.GetContacts() contactnames := make([]string, len(contactaddrs)) for i, contact := range contactaddrs { - name, hasname := the.Peer.GetContact(contact).GetAttribute("nick") + name, hasname := the.Peer.GetContact(contact).GetAttribute(constants.Nick) if hasname { contactnames[i] = name } else { @@ -356,24 +391,8 @@ func (this *GrandCentralDispatcher) requestGroupSettings(groupID string) { } func (this *GrandCentralDispatcher) saveGroupSettings(groupID, nick string) { - group := the.Peer.GetGroup(groupID) - - if group == nil { - log.Errorf("couldn't find group %v", groupID) - return - } - - group.SetAttribute("nick", nick) - the.EventBus.Publish(event.NewEvent(event.SetGroupAttribute, map[event.Field]string{ - event.GroupID: groupID, - event.Key: "nick", - event.Data: nick, - })) - - cif, _ := this.UIState.contacts.Load(groupID) - c := cif.(*gobjects.Contact) - c.DisplayName = nick - this.UIState.UpdateContact(groupID) + the.Peer.SetGroupAttribute(groupID, constants.Nick, nick) + this.UpdateContactDisplayName(groupID, nick) } func (this *GrandCentralDispatcher) broadcast(signal string) { @@ -382,9 +401,19 @@ func (this *GrandCentralDispatcher) broadcast(signal string) { log.Debugf("unhandled broadcast signal: %v", signal) case "ResetMessagePane": this.ResetMessagePane() + case "ResetProfile": + this.ResetProfile() } } +func (this *GrandCentralDispatcher) createContact(onion string) { + if contact := the.Peer.GetContact(onion); contact != nil { + return + } + the.Peer.AddContact(onion, onion, false) + the.Peer.PeerWithOnion(onion) +} + func (this *GrandCentralDispatcher) importString(str string) { if len(str) < 5 { log.Debugf("ignoring short string") @@ -435,7 +464,7 @@ func (this *GrandCentralDispatcher) importString(str string) { _, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion[:56])) if err != nil { log.Debugln(err) - this.InvokePopup("bad format. missing characters?") + this.InvokePopup("bad format. missing handlers?") return } @@ -443,25 +472,26 @@ func (this *GrandCentralDispatcher) importString(str string) { if checkc != nil { this.InvokePopup("already have this contact") return //TODO: bring them to the duplicate + } else { + the.Peer.AddContact(name, onion, false) + the.Peer.PeerWithOnion(onion) } - this.UIState.AddContact(&gobjects.Contact{ - Handle: onion, - DisplayName: name, - Image: cwutil.RandomProfileImage(onion), - Trusted: true, - }) + this.GetUiManager(this.selectedProfile()).AddContact(onion) } func (this *GrandCentralDispatcher) popup(str string) { this.InvokePopup(str) } -func (this *GrandCentralDispatcher) updateNick(nick string) { - the.Peer.GetProfile().Name = nick - the.EventBus.Publish(event.NewEvent(event.SetProfileName, map[event.Field]string{ - event.ProfileName: nick, - })) +func (this *GrandCentralDispatcher) updateNick(onion, nick string) { + peer := the.CwtchApp.GetPeer(onion) + if peer != nil { + peer.GetProfile().Name = nick + the.CwtchApp.GetEventBus(onion).Publish(event.NewEvent(event.SetProfileName, map[event.Field]string{ + event.ProfileName: nick, + })) + } } func (this *GrandCentralDispatcher) createGroup(server, groupName string) { @@ -471,21 +501,9 @@ func (this *GrandCentralDispatcher) createGroup(server, groupName string) { return } - this.UIState.AddContact(&gobjects.Contact{ - Handle: groupID, - DisplayName: groupName, - Image: cwutil.RandomGroupImage(groupID), - Server: server, - Trusted: true, - }) + this.GetUiManager(this.selectedProfile()).AddContact(groupID) - group := the.Peer.GetGroup(groupID) - group.SetAttribute("nick", groupName) - the.EventBus.Publish(event.NewEvent(event.SetGroupAttribute, map[event.Field]string{ - event.GroupID: groupID, - event.Key: "nick", - event.Data: groupName, - })) + the.Peer.SetGroupAttribute(groupID, constants.Nick, groupName) the.Peer.JoinServer(server) } @@ -495,7 +513,7 @@ func (this *GrandCentralDispatcher) blockPeer(onion string) { if err != nil { this.InvokePopup("Error Blocking Peer: " + err.Error()) } - this.UIState.UpdateContact(onion) + this.UpdateContactBlocked(onion, true) } func (this *GrandCentralDispatcher) unblockPeer(onion string) { @@ -504,7 +522,7 @@ func (this *GrandCentralDispatcher) unblockPeer(onion string) { this.InvokePopup("Error Unblocking Peer: " + err.Error()) } the.Peer.PeerWithOnion(onion) - this.UIState.UpdateContact(onion) + this.UpdateContactBlocked(onion, false) } func (this *GrandCentralDispatcher) inviteToGroup(onion, groupID string) { @@ -527,54 +545,28 @@ func (this *GrandCentralDispatcher) deleteContact(onion string) { func (this *GrandCentralDispatcher) acceptGroup(groupID string) { if the.Peer.GetGroup(groupID) != nil { the.Peer.AcceptInvite(groupID) - this.UIState.UpdateContact(groupID) } } func (this *GrandCentralDispatcher) setAttribute(onion, key, value string) { - pp, _ := the.Peer.GetProfile().GetContact(onion) - if pp != nil { - pp.SetAttribute(key, value) - the.EventBus.Publish(event.NewEvent(event.SetPeerAttribute, map[event.Field]string{ - event.RemotePeer: onion, - event.Key: key, - event.Data: value, - })) - this.UIState.UpdateContactAttribute(onion, key, value) - } + the.Peer.SetContactAttribute(onion, key, value) + this.GetUiManager(this.selectedProfile()).UpdateContactAttribute(onion, key, value) } func (this *GrandCentralDispatcher) blockUnknownPeers() { - - // Save this setting - the.EventBus.Publish(event.NewEvent(event.SetAttribute, map[event.Field]string{ - event.Key: constants.BlockUnknownPeersSetting, - event.Data: "true", - })) - - the.Peer.GetProfile().SetAttribute(constants.BlockUnknownPeersSetting, "true") + the.Peer.SetAttribute(constants.BlockUnknownPeersSetting, "true") the.EventBus.Publish(event.NewEvent(event.BlockUnknownPeers, map[event.Field]string{})) } func (this *GrandCentralDispatcher) allowUnknownPeers() { - - // Save this setting - the.EventBus.Publish(event.NewEvent(event.SetAttribute, map[event.Field]string{ - event.Key: constants.BlockUnknownPeersSetting, - event.Data: "false", - })) - - the.Peer.GetProfile().SetAttribute(constants.BlockUnknownPeersSetting, "false") + the.Peer.SetAttribute(constants.BlockUnknownPeersSetting, "false") the.EventBus.Publish(event.NewEvent(event.AllowUnknownPeers, map[event.Field]string{})) } func (this *GrandCentralDispatcher) setLocale(locale string) { this.SetLocale_helper(locale) - the.EventBus.Publish(event.NewEvent(event.SetAttribute, map[event.Field]string{ - event.Key: constants.LocaleSetting, - event.Data: locale, - })) + the.Peer.SetAttribute(constants.LocaleSetting, locale) zoom, _ := the.Peer.GetProfile().GetAttribute(constants.ZoomSetting) blockunkownpeers, _ := the.Peer.GetProfile().GetAttribute(constants.BlockUnknownPeersSetting) @@ -582,6 +574,13 @@ func (this *GrandCentralDispatcher) setLocale(locale string) { } +func (this *GrandCentralDispatcher) onActivate() { + log.Debugln("onActivate") + if the.CwtchApp != nil { + the.CwtchApp.QueryACNStatus() + } +} + func (this *GrandCentralDispatcher) SetLocale_helper(locale string) { core.QCoreApplication_RemoveTranslator(this.Translator) this.Translator = core.NewQTranslator(nil) @@ -589,3 +588,72 @@ func (this *GrandCentralDispatcher) SetLocale_helper(locale string) { core.QCoreApplication_InstallTranslator(this.Translator) this.QMLEngine.Retranslate() } + +func (this *GrandCentralDispatcher) unlockProfiles(password string) { + the.CwtchApp.LoadProfiles(password) +} + +func (this *GrandCentralDispatcher) loadProfile(onion string) { + the.Peer = the.CwtchApp.GetPeer(onion) + the.EventBus = the.CwtchApp.GetEventBus(onion) + + pic, exists := the.Peer.GetAttribute(constants.Picture) + if !exists { + pic = RandomProfileImage(the.Peer.GetProfile().Onion) + the.Peer.SetAttribute(constants.Picture, pic) + } + this.UpdateMyProfile(the.Peer.GetProfile().Name, the.Peer.GetProfile().Onion, pic) + + contacts := the.Peer.GetContacts() + for i := range contacts { + this.GetUiManager(this.selectedProfile()).AddContact(contacts[i]) + } + + groups := the.Peer.GetGroups() + for i := range groups { + // Only join servers for active and explicitly accepted groups. + this.GetUiManager(this.selectedProfile()).AddContact(groups[i]) + } + + // load ui preferences + this.RequestSettings() + locale, exists := the.Peer.GetProfile().GetAttribute(constants.LocaleSetting) + if exists { + this.SetLocale_helper(locale) + } +} + +func (this *GrandCentralDispatcher) createProfile(nick string, defaultPass bool, password string) { + if defaultPass { + the.CwtchApp.CreateTaggedPeer(nick, the.AppPassword, constants.ProfileTypeV1DefaultPassword) + } else { + the.CwtchApp.CreateTaggedPeer(nick, password, constants.ProfileTypeV1Password) + } +} + +func (this *GrandCentralDispatcher) changePassword(onion, currentPassword, newPassword string, defaultPass bool) { + tag, _ := the.CwtchApp.GetPeer(onion).GetAttribute(app.AttributeTag) + + if tag == constants.ProfileTypeV1DefaultPassword { + currentPassword = the.AppPassword + } + + if defaultPass { + newPassword = the.AppPassword + } + + the.CwtchApp.ChangePeerPassword(onion, currentPassword, newPassword) +} + +func (this *GrandCentralDispatcher) reloadProfileList() { + this.ResetProfileList() + + for onion, _ := range the.CwtchApp.ListPeers() { + AddProfile(this, onion) + } +} + +func (this *GrandCentralDispatcher) deleteProfile(onion string) { + log.Infof("deleteProfile %v\n", onion) + the.CwtchApp.DeletePeer(onion) +} diff --git a/go/ui/manager.go b/go/ui/manager.go new file mode 100644 index 00000000..89ebc9ba --- /dev/null +++ b/go/ui/manager.go @@ -0,0 +1,281 @@ +package ui + +import ( + "cwtch.im/cwtch/app" + "cwtch.im/cwtch/model" + "cwtch.im/cwtch/protocol/connections" + "cwtch.im/ui/go/constants" + "cwtch.im/ui/go/the" + "git.openprivacy.ca/openprivacy/libricochet-go/log" + "runtime/debug" + "time" +) + +func isGroup(id string) bool { + return len(id) == 32 +} + +func isPeer(id string) bool { + return len(id) == 56 +} + +func getOrDefault(id, key, defaultVal string) string { + var val string + var ok bool + if isGroup(id) { + val, ok = the.Peer.GetGroupAttribute(id, key) + } else { + val, ok = the.Peer.GetContactAttribute(id, key) + } + if ok { + return val + } else { + return defaultVal + } +} + +func getWithSetDefault(id string, key, defaultVal string) string { + var val string + var ok bool + if isGroup(id) { + val, ok = the.Peer.GetGroupAttribute(id, key) + } else { + val, ok = the.Peer.GetContactAttribute(id, key) + } + if !ok { + val = defaultVal + if isGroup(id) { + the.Peer.SetGroupAttribute(id, key, defaultVal) + } else { + the.Peer.SetContactAttribute(id, key, defaultVal) + } + } + return val +} + +// initLastReadTime checks and gets the Attributable's LastRead time or sets it to now +func initLastReadTime(id string) time.Time { + nowStr, _ := time.Now().MarshalText() + lastReadStr := getWithSetDefault(id, constants.LastRead, string(nowStr)) + var lastRead time.Time + lastRead.UnmarshalText([]byte(lastReadStr)) + return lastRead +} + +func initProfilePicture(id string) string { + if isGroup(id) { + return getWithSetDefault(id, constants.Picture, RandomGroupImage(id)) + } else { + return getWithSetDefault(id, constants.Picture, RandomProfileImage(id)) + } +} + +// getProfilePic supplies a profile pic to use. In groups we may not have a contact so it will generate one +func getProfilePic(id string) string { + if isGroup(id) { + if pic, exists := the.Peer.GetGroupAttribute(id, constants.Picture); !exists { + return RandomGroupImage(id) + } else { + return pic + } + } else { + if pic, exists := the.Peer.GetContactAttribute(id, constants.Picture); !exists { + return RandomProfileImage(id) + } else { + return pic + } + } +} + +func updateLastReadTime(id string) { + lastRead, _ := time.Now().MarshalText() + if isGroup(id) { + the.Peer.SetGroupAttribute(id, constants.LastRead, string(lastRead)) + } else { + the.Peer.SetContactAttribute(id, constants.LastRead, string(lastRead)) + } +} + +func countUnread(messages []model.Message, lastRead time.Time) int { + count := 0 + for i := len(messages) - 1; i >= 0; i-- { + if messages[i].Timestamp.After(lastRead) || messages[i].Timestamp.Equal(lastRead) { + count++ + } else { + break + } + } + return count +} + +// AddProfile adds a new profile to the UI +func AddProfile(gcd *GrandCentralDispatcher, handle string) { + peer := the.CwtchApp.GetPeer(handle) + if peer != nil { + nick := peer.GetProfile().Name + if nick == "" { + nick = handle + peer.SetAttribute(constants.Nick, nick) + } + + pic, ok := peer.GetAttribute(constants.Picture) + if !ok { + pic = RandomProfileImage(handle) + peer.SetAttribute(constants.Picture, pic) + } + + tag, _ := peer.GetAttribute(app.AttributeTag) + log.Infof("AddProfile %v %v %v %v\n", handle, nick, pic, tag) + gcd.AddProfile(handle, nick, pic, tag) + } +} + +type manager struct { + gcd *GrandCentralDispatcher + profile string +} + +// Manager is a middleware helper for entities like peer event listeners wishing to trigger ui changes (via the gcd) +// each manager is for one profile/peer +// manager takes minimal arguments and builds the full struct of data (usually pulled from a cwtch peer) required to call the GCD to perform the ui action +// manager also performs call filtering based on UI state: users of manager can safely always call it on events and not have to worry about weather the relevant ui is active +// ie: you can always safely call AddMessage even if in the ui a different profile is selected. manager will check with gcd, and if the correct conditions are not met, it will not call on gcd to update the ui incorrectly +type Manager interface { + Acknowledge(mID string) + AddContact(Handle string) + AddSendMessageError(peer string, signature string, err string) + AddMessage(handle string, from string, message string, fromMe bool, messageID string, timestamp time.Time, Acknowledged bool) + + ReloadProfiles() + + UpdateContactDisplayName(handle string, name string) + UpdateContactStatus(handle string, status int, loading bool) + UpdateContactAttribute(handle, key, value string) + + ChangePasswordResponse(error bool) +} + +// NewManager returns a new Manager interface for a profile to the gcd +func NewManager(profile string, gcd *GrandCentralDispatcher) Manager { + return &manager{gcd: gcd, profile: profile} +} + +// Acknowledge acknowledges the given message id in the UI +func (this *manager) Acknowledge(mID string) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.Acknowledged(mID) + }) +} + +func getLastMessageTime(tl *model.Timeline) int { + if len(tl.Messages) == 0 { + return 0 + } + + return int(tl.Messages[len(tl.Messages)-1].Timestamp.Unix()) +} + +// AddContact adds a new contact to the ui for this manager's profile +func (this *manager) AddContact(Handle string) { + this.gcd.DoIfProfile(this.profile, func() { + + if isGroup(Handle) { + group := the.Peer.GetGroup(Handle) + if group != nil { + lastRead := initLastReadTime(group.GroupID) + unread := countUnread(group.Timeline.GetMessages(), lastRead) + picture := initProfilePicture(Handle) + nick, exists := group.GetAttribute(constants.Nick) + if !exists { + nick = Handle + } + + this.gcd.AddContact(Handle, nick, picture, group.GroupServer, unread, int(connections.ConnectionStateToType[group.State]), false, false, getLastMessageTime(&group.Timeline)) + } + return + } else if !isPeer(Handle) { + log.Errorf("sorry, unable to handle AddContact(%v)", Handle) + debug.PrintStack() + return + } + + contact := the.Peer.GetContact(Handle) + if contact != nil { + lastRead := initLastReadTime(contact.Onion) + unread := countUnread(contact.Timeline.GetMessages(), lastRead) + picture := initProfilePicture(Handle) + nick, exists := contact.GetAttribute(constants.Nick) + if !exists { + nick = Handle + } + + this.gcd.AddContact(Handle, nick, picture, "", unread, int(connections.ConnectionStateToType[contact.State]), contact.Blocked, false, getLastMessageTime(&contact.Timeline)) + } + }) +} + +// AddSendMessageError adds an error not and icon to a message in a conversation in the ui for the message identified by the peer/sig combo +func (this *manager) AddSendMessageError(peer string, signature string, err string) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.DoIfConversation(peer, func() { + log.Debugf("Received Error Sending Message: %v", err) + // FIXME: Sometimes, for the first Peer message we send our error beats our message to the UI + time.Sleep(time.Second * 1) + this.gcd.GroupSendError(signature, err) + }) + }) +} + +// AddMessage adds a message to the message pane for the supplied conversation if it is active +func (this *manager) AddMessage(handle string, from string, message string, fromMe bool, messageID string, timestamp time.Time, Acknowledged bool) { + this.gcd.DoIfProfile(this.profile, func() { + + nick := getOrDefault(handle, constants.Nick, handle) + image := getProfilePic(handle) + + // If we have this group loaded already + this.gcd.DoIfConversation(handle, func() { + updateLastReadTime(handle) + // If the message is not from the user then add it, otherwise, just acknowledge. + if !fromMe { + this.gcd.AppendMessage(handle, from, nick, message, image, messageID, fromMe, timestamp.Format(constants.TIME_FORMAT), false, false) + } else { + if !Acknowledged { + this.gcd.AppendMessage(handle, from, nick, message, image, messageID, fromMe, timestamp.Format(constants.TIME_FORMAT), false, false) + } else { + this.gcd.Acknowledged(messageID) + } + } + }) + this.gcd.IncContactUnreadCount(handle) + }) +} + +func (this *manager) ReloadProfiles() { + this.gcd.reloadProfileList() +} + +// UpdateContactDisplayName updates a contact's display name in the contact list and conversations +func (this *manager) UpdateContactDisplayName(handle string, name string) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.UpdateContactDisplayName(handle, name) + }) +} + +// UpdateContactStatus updates a contact's status in the ui +func (this *manager) UpdateContactStatus(handle string, status int, loading bool) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.UpdateContactStatus(handle, status, loading) + }) +} + +// UpdateContactAttribute update's a contacts attribute in the ui +func (this *manager) UpdateContactAttribute(handle, key, value string) { + this.gcd.DoIfProfile(this.profile, func() { + this.gcd.UpdateContactAttribute(handle, key, value) + }) +} + +func (this *manager) ChangePasswordResponse(error bool) { + this.gcd.ChangePasswordResponse(error) +} diff --git a/go/cwutil/utils.go b/go/ui/utils.go similarity index 97% rename from go/cwutil/utils.go rename to go/ui/utils.go index 157042b8..b0f39e8e 100644 --- a/go/cwutil/utils.go +++ b/go/ui/utils.go @@ -1,9 +1,8 @@ -package cwutil +package ui import ( "encoding/base32" "encoding/hex" - "fmt" "git.openprivacy.ca/openprivacy/libricochet-go/log" "strings" ) @@ -23,7 +22,7 @@ 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) + log.Errorf("error: %v %v %v\n", handle, err, barr) return "qrc:/qml/images/extra/openprivacy.png" } return "servers/" + choices[int(barr[0])%len(choices)] + ".png" diff --git a/i18n/translation_de.ts b/i18n/translation_de.ts index 1a8a443f..cd9416c4 100644 --- a/i18n/translation_de.ts +++ b/i18n/translation_de.ts @@ -4,239 +4,469 @@ AddGroupPane + create-group-title - Gruppe Anlegen + Gruppe Anlegen + server-label Server label - Server + Server + group-name-label Group name label - Gruppenname + Gruppenname + default-group-name default suggested group name - Tolle Gruppe + Tolle Gruppe + create-group-btn create group button - Anlegen + Anlegen BulletinOverlay + new-bulletin-label - Neue Meldung + Neue Meldung + post-new-bulletin-label Post a new Bulletin Post - Neue Meldung veröffentlichen + Neue Meldung veröffentlichen + title-placeholder title place holder text - Titel... + Titel... GroupSettingsPane + server-label - Server + Server + + copy-btn - Kopieren + Kopieren + invitation-label - Einladung + Einladung + group-name-label - Gruppenname + Gruppenname + save-btn - Speichern + Speichern + invite-to-group-label Invite someone to the group - In die Gruppe einladen + In die Gruppe einladen + invite-btn - Einladen + Einladen + delete-btn - Löschen + Löschen + + + + InplaceEditText + + + Update + ListOverlay + add-list-item Add a New List Item - Liste hinzufügen + Liste hinzufügen + add-new-item Add a new item to the list - Neues Listenelement hinzüfgen + Neues Listenelement hinzüfgen + todo-placeholder Todo... placeholder text - noch zu erledigen + noch zu erledigen MembershipOverlay + membership-description Below is a list of users who have sent messages to the group. This list may not reflect all users who have access to the group. - Unten steht eine Liste der Benutzer, die Nachrichten an die Gruppe gesendet haben. Möglicherweise enthält diese Benutzerzliste nicht alle, die Zugang zur Gruppe haben. + Unten steht eine Liste der Benutzer, die Nachrichten an die Gruppe gesendet haben. Möglicherweise enthält diese Benutzerzliste nicht alle, die Zugang zur Gruppe haben. Message + dm-tooltip Click to DM - Klicken, um DM zu senden + Klicken, um DM zu senden + could-not-send-msg-error Could not send this message - Nachricht konnte nicht gesendet werden + Nachricht konnte nicht gesendet werden + acknowledged-label - bestätigt + bestätigt + pending-label - Bestätigung ausstehend + Bestätigung ausstehend MyProfile + copy-btn Button for copying profile onion address to clipboard - Kopieren + Kopieren + copied-clipboard-notification Copied to clipboard - in die Zwischenablage kopiert + in die Zwischenablage kopiert + new-group-btn create new group button - Neue Gruppe anlegen + Neue Gruppe anlegen + paste-address-to-add-contact ex: "... paste an address here to add a contact ..." - Adresse hier hinzufügen, um einen Kontakt aufzunehmen + Adresse hier hinzufügen, um einen Kontakt aufzunehmen OverlayPane + accept-group-invite-label Do you want to accept the invitation to $GROUP - Möchtest Du die Einladung annehmen + Möchtest Du die Einladung annehmen + accept-group-btn Accept group invite button - Annehmen + Annehmen + reject-group-btn Reject Group invite button - Ablehnen + Ablehnen + chat-btn - Chat + Chat + lists-btn - Listen + Listen + bulletins-btn - Meldungen + Meldungen + puzzle-game-btn - Puzzlespiel + Puzzlespiel PeerSettingsPane + address-label - Adresse + Adresse + copy-btn - Kopieren + Kopieren + copied-to-clipboard-notification notification: copied to clipboard - in die Zwischenablage kopiert + in die Zwischenablage kopiert + display-name-label - Angezeigter Name + Angezeigter Name + save-btn - speichern + speichern + + block-btn + + + + + unblock-btn + + + + delete-btn - löschen + löschen + + + + ProfileAddEditPane + + + add-profile-title + + + + + edit-profile-title + + + + + profile-name + Display name + + + + + + default-profile-name + default suggested profile name + + + + + profile-onion-label + Onion + + + + + radio-use-password + Password + + + + + radio-no-password + Unencrypted (No password) + + + + + no-password-warning + Not using a password on this account means that all data stored locally will not be encrypted + + + + + current-password-label + Current Password + + + + + password1-label + Password + + + + + password2-label + Reenter password + + + + + create-profile-btn + Create Profile || Save Profile + + + + + save-profile-btn + + + + + password-error-match + Passwords do not match + + + + + password-change-error + Error changing password: Supplied password rejected + + + + + delete-profile-btn + Delete Profile + + + + + delete-confirm-label + Type DELETE to confirm + + + + + delete-profile-confirm-btn + Really Delete Profile + + + + + delete-confirm-text + DELETE + + + + + ProfileList + + + + add-new-profile-btn + + + + + ProfileManagerPane + + + enter-profile-password + Please enter password: + + + + + error-0-profiles-loaded-for-password + 0 profiles loaded with that password + + + + + unlock + Unlock + SettingsPane + cwtch-settings-title Cwtch Settings title - Cwtch Einstellungen + Cwtch Einstellungen + + version %1 builddate %2 + Version: %1 Built on: %2 + + + + zoom-label Interface zoom (mostly affects text and button sizes) - Benutzeroberflächen-Zoom (betriftt hauptsächlich Text- und Knopgrößen) + Benutzeroberflächen-Zoom (betriftt hauptsächlich Text- und Knopgrößen) + + block-unknown-label + + + + large-text-label - Groß + Groß + default-scaling-text "Default size text (scale factor: " - defaultmäßige Textgröße (Skalierungsfaktor: + defaultmäßige Textgröße (Skalierungsfaktor: + small-text-label - Klein + Klein + + + + StackToolbar + + + view-group-membership-tooltip + View Group Membership + diff --git a/i18n/translation_en.qm b/i18n/translation_en.qm index 9dad8dff..41cfb9e3 100644 Binary files a/i18n/translation_en.qm and b/i18n/translation_en.qm differ diff --git a/i18n/translation_en.ts b/i18n/translation_en.ts index 51319b63..9ec91004 100644 --- a/i18n/translation_en.ts +++ b/i18n/translation_en.ts @@ -4,264 +4,469 @@ AddGroupPane + create-group-title - Create Group + Create Group + server-label Server label - Server + Server + group-name-label Group name label - Group name + Group name + default-group-name default suggested group name - Awesome Group + Awesome Group + create-group-btn create group button - Create + Create BulletinOverlay + new-bulletin-label - New Bulletin + New Bulletin + post-new-bulletin-label Post a new Bulletin Post - Post new bulletin + Post new bulletin + title-placeholder title place holder text - title... + title... GroupSettingsPane + server-label - Server + Server + + copy-btn - Copy + Copy + invitation-label - Invitation + Invitation + group-name-label - Group Name + Group Name + save-btn - Save + Save + invite-to-group-label Invite someone to the group - Invite to group + Invite to group + invite-btn - Invite + Invite + delete-btn - Delete + Delete + + + + InplaceEditText + + + Update + Update ListOverlay + add-list-item Add a New List Item - Add a New List Item + Add a New List Item + add-new-item Add a new item to the list - Add a new item to the list + Add a new item to the list + todo-placeholder Todo... placeholder text - Todo... + Todo... MembershipOverlay + membership-description Below is a list of users who have sent messages to the group. This list may not reflect all users who have access to the group. - Below is a list of users who have sent messages to the group. This list may not reflect all users who have access to the group. + Below is a list of users who have sent messages to the group. This list may not reflect all users who have access to the group. Message + dm-tooltip Click to DM - Click to DM + Click to DM + could-not-send-msg-error Could not send this message - Could not send this message + Could not send this message + acknowledged-label - Acknowledged + Acknowledged + pending-label - Pending + Pending MyProfile + copy-btn Button for copying profile onion address to clipboard - Copy + Copy + copied-clipboard-notification Copied to clipboard - Copied to clipboard + Copied to clipboard + new-group-btn create new group button - Create new group + Create new group + paste-address-to-add-contact ex: "... paste an address here to add a contact ..." - ... paste an address here to add a contact... + ... paste an address here to add a contact... OverlayPane + accept-group-invite-label Do you want to accept the invitation to $GROUP - Do you want to accept the invitation to + Do you want to accept the invitation to + accept-group-btn Accept group invite button - Accept + Accept + reject-group-btn Reject Group invite button - Reject + Reject + chat-btn - Chat + Chat + lists-btn - Lists + Lists + bulletins-btn - Bulletins + Bulletins + puzzle-game-btn - Puzzle Game + Puzzle Game PeerSettingsPane + address-label - Address + Address + copy-btn - Copy + Copy + copied-to-clipboard-notification notification: copied to clipboard - Copied to Clipboard + Copied to Clipboard + display-name-label - Display Name + Display Name + save-btn - Save + Save + block-btn - Block Peer + Block Peer + unblock-btn - Unblock Peer + Unblock Peer + delete-btn - Delete + Delete + + + + ProfileAddEditPane + + + add-profile-title + Add new profile + + + + edit-profile-title + Edit Profile + + + + profile-name + Display name + Display name + + + + + default-profile-name + default suggested profile name + Alice + + + + profile-onion-label + Onion + Onion + + + + radio-use-password + Password + Password + + + + radio-no-password + Unencrypted (No password) + Unencrypted (No password) + + + + no-password-warning + Not using a password on this account means that all data stored locally will not be encrypted + Not using a password on this account means that all data stored locally will not be encrypted + + + + current-password-label + Current Password + Current Password + + + + password1-label + Password + Password + + + + password2-label + Reenter password + Reenter password + + + + create-profile-btn + Create Profile || Save Profile + Create Profile + + + + save-profile-btn + Save Profile + + + + password-error-match + Passwords do not match + Passwords do not match + + + + password-change-error + Error changing password: Supplied password rejected + Error changing password: Supplied password rejected + + + + delete-profile-btn + Delete Profile + Delete Profile + + + + delete-confirm-label + Type DELETE to confirm + Type DELETE to confirm + + + + delete-profile-confirm-btn + Really Delete Profile + Really Delete Profile + + + + delete-confirm-text + DELETE + DELETE + + + + ProfileList + + + + add-new-profile-btn + Add new profile + + + + ProfileManagerPane + + + enter-profile-password + Please enter password: + Please enter password + + + + error-0-profiles-loaded-for-password + 0 profiles loaded with that password + 0 profiles loaded with that password + + + + unlock + Unlock + Unlock SettingsPane + cwtch-settings-title Cwtch Settings title - Cwtch Settings + Cwtch Settings + version %1 builddate %2 Version: %1 Built on: %2 - Version: %1 Built on: %2 + Version: %1 Built on: %2 + zoom-label Interface zoom (mostly affects text and button sizes) - Interface zoom (mostly affects text and button sizes) + Interface zoom (mostly affects text and button sizes) + block-unknown-label - Block Unknown Peers + Block Unknown Peers + large-text-label - Large + Large + default-scaling-text "Default size text (scale factor: " - Default size text (scale factor: + Default size text (scale factor: + small-text-label - Small + Small StackToolbar + view-group-membership-tooltip View Group Membership - View Group Membership + View Group Membership diff --git a/i18n/translation_fr.ts b/i18n/translation_fr.ts index 39db9d6e..dd42812f 100644 --- a/i18n/translation_fr.ts +++ b/i18n/translation_fr.ts @@ -4,239 +4,469 @@ AddGroupPane + create-group-title - Créer un groupe + Créer un groupe + server-label Server label - Serveur + Serveur + group-name-label Group name label - Groupe + Groupe + default-group-name default suggested group name - Un super groupe + Un super groupe + create-group-btn create group button - Créer + Créer BulletinOverlay + new-bulletin-label - Nouveau bulletin + Nouveau bulletin + post-new-bulletin-label Post a new Bulletin Post - Envoyer un nouveau bulletin + Envoyer un nouveau bulletin + title-placeholder title place holder text - titre... + titre... GroupSettingsPane + server-label - Serveur + Serveur + + copy-btn - Copier + Copier + invitation-label - Invitation + Invitation + group-name-label - Nom du groupe + Nom du groupe + save-btn - Sauvegarder + Sauvegarder + invite-to-group-label Invite someone to the group - Inviter quelqu'un + Inviter quelqu'un + invite-btn - Invitation + Invitation + delete-btn - Effacer + Effacer + + + + InplaceEditText + + + Update + ListOverlay + add-list-item Add a New List Item - Ajouter un nouvel élément + Ajouter un nouvel élément + add-new-item Add a new item to the list - Ajouter un nouvel élément à la liste + Ajouter un nouvel élément à la liste + todo-placeholder Todo... placeholder text - A faire... + A faire... MembershipOverlay + membership-description Below is a list of users who have sent messages to the group. This list may not reflect all users who have access to the group. - Liste des utilisateurs ayant envoyés un ou plusieurs messages au groupe. Cette liste peut ne pas être representatives de l'ensemble des membres du groupe. + Liste des utilisateurs ayant envoyés un ou plusieurs messages au groupe. Cette liste peut ne pas être representatives de l'ensemble des membres du groupe. Message + dm-tooltip Click to DM - Envoyer un message privé + Envoyer un message privé + could-not-send-msg-error Could not send this message - Impossible d'envoyer ce message + Impossible d'envoyer ce message + acknowledged-label - Confirmé + Confirmé + pending-label - En attente + En attente MyProfile + copy-btn Button for copying profile onion address to clipboard - Copier + Copier + copied-clipboard-notification Copied to clipboard - Copié dans le presse-papier + Copié dans le presse-papier + new-group-btn create new group button - Créer un nouveau groupe + Créer un nouveau groupe + paste-address-to-add-contact ex: "... paste an address here to add a contact ..." - ... coller une adresse ici pour ajouter un contact... + ... coller une adresse ici pour ajouter un contact... OverlayPane + accept-group-invite-label Do you want to accept the invitation to $GROUP - Voulez-vous accepter l'invitation au groupe + Voulez-vous accepter l'invitation au groupe + accept-group-btn Accept group invite button - Accepter + Accepter + reject-group-btn Reject Group invite button - Refuser + Refuser + chat-btn - Discuter + Discuter + lists-btn - Listes + Listes + bulletins-btn - Bulletins + Bulletins + puzzle-game-btn - Puzzle + Puzzle PeerSettingsPane + address-label - Adresse + Adresse + copy-btn - Copier + Copier + copied-to-clipboard-notification notification: copied to clipboard - Copié dans le presse-papier + Copié dans le presse-papier + display-name-label - Pseudo + Pseudo + save-btn - Sauvegarder + Sauvegarder + + block-btn + + + + + unblock-btn + + + + delete-btn - Effacer + Effacer + + + + ProfileAddEditPane + + + add-profile-title + + + + + edit-profile-title + + + + + profile-name + Display name + + + + + + default-profile-name + default suggested profile name + + + + + profile-onion-label + Onion + + + + + radio-use-password + Password + + + + + radio-no-password + Unencrypted (No password) + + + + + no-password-warning + Not using a password on this account means that all data stored locally will not be encrypted + + + + + current-password-label + Current Password + + + + + password1-label + Password + + + + + password2-label + Reenter password + + + + + create-profile-btn + Create Profile || Save Profile + + + + + save-profile-btn + + + + + password-error-match + Passwords do not match + + + + + password-change-error + Error changing password: Supplied password rejected + + + + + delete-profile-btn + Delete Profile + + + + + delete-confirm-label + Type DELETE to confirm + + + + + delete-profile-confirm-btn + Really Delete Profile + + + + + delete-confirm-text + DELETE + + + + + ProfileList + + + + add-new-profile-btn + + + + + ProfileManagerPane + + + enter-profile-password + Please enter password: + + + + + error-0-profiles-loaded-for-password + 0 profiles loaded with that password + + + + + unlock + Unlock + SettingsPane + cwtch-settings-title Cwtch Settings title - Préférences Cwtch + Préférences Cwtch + + version %1 builddate %2 + Version: %1 Built on: %2 + + + + zoom-label Interface zoom (mostly affects text and button sizes) - Interface zoom (essentiellement la taille du texte et des composants de l'interface) + Interface zoom (essentiellement la taille du texte et des composants de l'interface) + + block-unknown-label + + + + large-text-label - Large + Large + default-scaling-text "Default size text (scale factor: " - Taille par défaut du texte (échelle: + Taille par défaut du texte (échelle: + small-text-label - Petit + Petit + + + + StackToolbar + + + view-group-membership-tooltip + View Group Membership + diff --git a/i18n/translation_pt.ts b/i18n/translation_pt.ts index b31eebdd..aa042b14 100644 --- a/i18n/translation_pt.ts +++ b/i18n/translation_pt.ts @@ -4,239 +4,469 @@ AddGroupPane + create-group-title - Criar Grupo + Criar Grupo + server-label Server label - Servidor + Servidor + group-name-label Group name label - Nome do grupo + Nome do grupo + default-group-name default suggested group name - Grupo incrível + Grupo incrível + create-group-btn create group button - Criar + Criar BulletinOverlay + new-bulletin-label - Novo Boletim + Novo Boletim + post-new-bulletin-label Post a new Bulletin Post - Postar novo boletim + Postar novo boletim + title-placeholder title place holder text - título… + título… GroupSettingsPane + server-label - Servidor + Servidor + + copy-btn - Copiar + Copiar + invitation-label - Convite + Convite + group-name-label - Nome do Grupo + Nome do Grupo + save-btn - Salvar + Salvar + invite-to-group-label Invite someone to the group - Convidar ao grupo + Convidar ao grupo + invite-btn - Convidar + Convidar + delete-btn - Deletar + Deletar + + + + InplaceEditText + + + Update + ListOverlay + add-list-item Add a New List Item - Adicionar Item à Lista + Adicionar Item à Lista + add-new-item Add a new item to the list - Adicionar novo item à lista + Adicionar novo item à lista + todo-placeholder Todo... placeholder text - Afazer… + Afazer… MembershipOverlay + membership-description Below is a list of users who have sent messages to the group. This list may not reflect all users who have access to the group. - A lista abaixo é de usuários que enviaram mensagens ao grupo. Essa lista pode não refletir todos os usuários que têm acesso ao grupo. + A lista abaixo é de usuários que enviaram mensagens ao grupo. Essa lista pode não refletir todos os usuários que têm acesso ao grupo. Message + dm-tooltip Click to DM - Clique para DM + Clique para DM + could-not-send-msg-error Could not send this message - Não deu para enviar esta mensagem + Não deu para enviar esta mensagem + acknowledged-label - Confirmada + Confirmada + pending-label - Pendente + Pendente MyProfile + copy-btn Button for copying profile onion address to clipboard - Copiar + Copiar + copied-clipboard-notification Copied to clipboard - Copiado + Copiado + new-group-btn create new group button - Criar novo grupo + Criar novo grupo + paste-address-to-add-contact ex: "... paste an address here to add a contact ..." - … cole um endereço aqui para adicionar um contato… + … cole um endereço aqui para adicionar um contato… OverlayPane + accept-group-invite-label Do you want to accept the invitation to $GROUP - Você quer aceitar o convite para + Você quer aceitar o convite para + accept-group-btn Accept group invite button - Aceitar + Aceitar + reject-group-btn Reject Group invite button - Recusar + Recusar + chat-btn - Chat + Chat + lists-btn - Listas + Listas + bulletins-btn - Boletins + Boletins + puzzle-game-btn - Jogo de Adivinhação + Jogo de Adivinhação PeerSettingsPane + address-label - Endereço + Endereço + copy-btn - Copiar + Copiar + copied-to-clipboard-notification notification: copied to clipboard - Copiado + Copiado + display-name-label - Nome de Exibição + Nome de Exibição + save-btn - Salvar + Salvar + + block-btn + + + + + unblock-btn + + + + delete-btn - Deletar + Deletar + + + + ProfileAddEditPane + + + add-profile-title + + + + + edit-profile-title + + + + + profile-name + Display name + + + + + + default-profile-name + default suggested profile name + + + + + profile-onion-label + Onion + + + + + radio-use-password + Password + + + + + radio-no-password + Unencrypted (No password) + + + + + no-password-warning + Not using a password on this account means that all data stored locally will not be encrypted + + + + + current-password-label + Current Password + + + + + password1-label + Password + + + + + password2-label + Reenter password + + + + + create-profile-btn + Create Profile || Save Profile + + + + + save-profile-btn + + + + + password-error-match + Passwords do not match + + + + + password-change-error + Error changing password: Supplied password rejected + + + + + delete-profile-btn + Delete Profile + + + + + delete-confirm-label + Type DELETE to confirm + + + + + delete-profile-confirm-btn + Really Delete Profile + + + + + delete-confirm-text + DELETE + + + + + ProfileList + + + + add-new-profile-btn + + + + + ProfileManagerPane + + + enter-profile-password + Please enter password: + + + + + error-0-profiles-loaded-for-password + 0 profiles loaded with that password + + + + + unlock + Unlock + SettingsPane + cwtch-settings-title Cwtch Settings title - Configurações do Cwtch + Configurações do Cwtch + + version %1 builddate %2 + Version: %1 Built on: %2 + + + + zoom-label Interface zoom (mostly affects text and button sizes) - Zoom da interface (afeta principalmente tamanho de texto e botões) + Zoom da interface (afeta principalmente tamanho de texto e botões) + + block-unknown-label + + + + large-text-label - Grande + Grande + default-scaling-text "Default size text (scale factor: " - Texto tamanho padrão (fator de escala: + Texto tamanho padrão (fator de escala: + small-text-label - Pequeno + Pequeno + + + + StackToolbar + + + view-group-membership-tooltip + View Group Membership + diff --git a/main.go b/main.go index 4f2e85fe..a7c5b87d 100644 --- a/main.go +++ b/main.go @@ -3,11 +3,10 @@ package main import ( libapp "cwtch.im/cwtch/app" "cwtch.im/cwtch/event/bridge" - "cwtch.im/ui/go/characters" - "cwtch.im/ui/go/gobjects" - "cwtch.im/ui/go/gothings" - "cwtch.im/ui/go/gothings/android" + "cwtch.im/ui/go/handlers" "cwtch.im/ui/go/the" + "cwtch.im/ui/go/ui" + "cwtch.im/ui/go/ui/android" "flag" "git.openprivacy.ca/openprivacy/libricochet-go/connectivity" "git.openprivacy.ca/openprivacy/libricochet-go/log" @@ -34,7 +33,7 @@ var ( func init() { // make go-defined types available in qml - gothings.GrandCentralDispatcher_QmlRegisterType2("CustomQmlTypes", 1, 0, "GrandCentralDispatcher") + ui.GrandCentralDispatcher_QmlRegisterType2("CustomQmlTypes", 1, 0, "GrandCentralDispatcher") } func main() { @@ -58,7 +57,9 @@ func main() { } // TESTING - //log.SetLevel(log.LevelDebug) + if buildVer == "" { + log.SetLevel(log.LevelDebug) + } //log.ExcludeFromPattern("connection/connection") //log.ExcludeFromPattern("outbound/3dhauthchannel") //log.AddNothingExceptFilter("event/eventmanager") @@ -75,6 +76,12 @@ func main() { } the.CwtchDir = path.Join(usr.HomeDir, ".cwtch") } + + if buildVer == "" && os.Getenv("CWTCH_FOLDER") == "" { + log.Infoln("Development build: using dev directory for dev profiles") + the.CwtchDir = path.Join(the.CwtchDir, "dev") + } + the.ACN = nil the.Peer = nil the.IPCBridge = nil @@ -111,6 +118,7 @@ func mainService() { log.Infoln("Making QGuiApplication...") app = gui.NewQGuiApplication(len(os.Args), os.Args) } + log.Infoln("Cwtch Service starting app.Exec") app.Exec() } @@ -121,7 +129,7 @@ func mainUi(flagLocal bool, flagClientUI bool) { //app := gui.NewQGuiApplication(len(os.Args), os.Args) app := widgets.NewQApplication(len(os.Args), os.Args) // our globals - gcd := gothings.NewGrandCentralDispatcher(nil) + gcd := ui.NewGrandCentralDispatcher(nil) gcd.SetOs(runtime.GOOS) ex, err := os.Executable() if err != nil { log.Infof("error getting path: %v", err) } @@ -139,8 +147,6 @@ func mainUi(flagLocal bool, flagClientUI bool) { gcd.SetVersion("development") gcd.SetBuildDate("now") } - gcd.UIState = gothings.NewUIState(gcd) - gcd.OutgoingMessages = make(chan gobjects.Letter, 1000) //TODO: put theme stuff somewhere better gcd.SetThemeScale(1.0) @@ -232,7 +238,7 @@ func loadACN() { } } -func loadNetworkingAndFiles(gcd *gothings.GrandCentralDispatcher, service bool, clientUI bool) { +func loadNetworkingAndFiles(gcd *ui.GrandCentralDispatcher, service bool, clientUI bool) { if service || clientUI || runtime.GOOS == "android" { clientIn := path.Join(the.CwtchDir, "clientIn") serviceIn := path.Join(the.CwtchDir, "serviceIn") @@ -255,7 +261,7 @@ func loadNetworkingAndFiles(gcd *gothings.GrandCentralDispatcher, service bool, if !service { the.AppBus = the.CwtchApp.GetPrimaryBus() subscribed := make(chan bool) - go characters.AppEventListener(gcd, subscribed) + go handlers.App(gcd, subscribed, clientUI) <-subscribed } diff --git a/qml/main.qml b/qml/main.qml index cd9cad18..756786b5 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -127,6 +127,13 @@ ApplicationWindow { currentIndex: 1 anchors.fill: parent + currentIndex: 0 + readonly property int splashPane: 0 + readonly property int managementPane: 1 + readonly property int addEditProfilePane: 2 + readonly property int profilePane: 3 + property alias pane: parentStack.currentIndex + Rectangle { // Splash pane color: "#FFFFFF" //Layout.fillHeight: true @@ -143,6 +150,28 @@ ApplicationWindow { } } + Rectangle { // Profile login/management pane + anchors.fill: parent + visible: false + color: "#D2C0DD" + + ProfileManagerPane { + id: profilesPane + anchors.fill: parent + } + } + + Rectangle { // Profile login/management pane + anchors.fill: parent + color: "#EEEEFF" + + + ProfileAddEditPane{ + id: profileAddEditPane + anchors.fill: parent + } + } + RowLayout { // CONTAINS EVERYTHING EXCEPT THE TOOLBAR /* anchors.left: ratio >= 0.92 ? parent.left : toolbar.right @@ -260,7 +289,23 @@ ApplicationWindow { onSetToolbarTitle: function(str) { theStack.title = str } + + onLoaded: function() { + parentStack.pane = parentStack.managementPane + splashPane.running = false + } } Component.onCompleted: Mutant.standard.imagePath = gcd.assetPath; + + Connections { + target: Qt.application + onStateChanged: function() { + // https://doc.qt.io/qt-5/qt.html#ApplicationState-enum + if (Qt.application.state == 4) { + // Active + gcd.onActivate() + } + } + } } diff --git a/qml/overlays/BulletinOverlay.qml b/qml/overlays/BulletinOverlay.qml index 1e1f3cd8..47a4a64a 100644 --- a/qml/overlays/BulletinOverlay.qml +++ b/qml/overlays/BulletinOverlay.qml @@ -6,8 +6,7 @@ import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 -import "../widgets" -import "../widgets/controls" as Awesome +import "../widgets" as Widgets import "../fonts/Twemoji.js" as T import "../utils.js" as Utils import "../styles" @@ -87,13 +86,13 @@ ColumnLayout { }) } - 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) { sv.contentY = sv.contentHeight - sv.height - } + }*/ } - onUpdateContact: function(_handle, _displayName, _image, _server, _badge, _status, _trusted, _blocked, _loading) { - if (gcd.currentOpenConversation == _handle) { + onUpdateContactStatus: function(_handle, _status, _loading) { + if (gcd.selectedConversation == _handle) { if (_loading == true) { newposttitle.enabled = false newpostbody.enabled = false @@ -176,7 +175,7 @@ ColumnLayout { width: parent.width - 50 } - SimpleButton { + Widgets.SimpleButton { id: replybtn visible: selected text: "reply" @@ -231,7 +230,7 @@ ColumnLayout { } - SimpleButton { // SEND MESSAGE BUTTON + Widgets.SimpleButton { // SEND MESSAGE BUTTON id: btnSend icon: "regular/paper-plane" text: "post" diff --git a/qml/overlays/ChatOverlay.qml b/qml/overlays/ChatOverlay.qml index 3b3ccb2e..ecc7c97b 100644 --- a/qml/overlays/ChatOverlay.qml +++ b/qml/overlays/ChatOverlay.qml @@ -112,10 +112,10 @@ Item { messagesListView.positionViewAtEnd() } - onUpdateContact: function(_handle, _displayName, _image, _server, _badge, _status, _trusted, _blocked, _loading) { - if (gcd.currentOpenConversation == _handle) { + onUpdateContactStatus: function(_handle, _status, _loading) { + if (gcd.selectedConversation == _handle) { // Group is Synced OR p2p is Authenticated - if ( (_handle.length == 32 && _status == 4) || _status == 3) { + if ( (_handle.length == 32 && _status == 4) || (_handle.length == 56 && _status == 3) ) { txtMessage.enabled = true btnSend.enabled = true } else { diff --git a/qml/overlays/ListOverlay.qml b/qml/overlays/ListOverlay.qml index 5304ac28..3bfeebbd 100644 --- a/qml/overlays/ListOverlay.qml +++ b/qml/overlays/ListOverlay.qml @@ -5,7 +5,7 @@ import QtQuick.Controls.Material 2.0 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 -import "../widgets" +import "../widgets" as Widgets import "../widgets/controls" as Awesome import "../fonts/Twemoji.js" as T import "../utils.js" as Utils @@ -87,17 +87,17 @@ ColumnLayout { }) } - if(msg.c != undefined) { + /*if(msg.c != undefined) { jsonModel4.get(msg.c).complete = true } if (sv.contentY + sv.height >= sv.contentHeight - colMessages.height && sv.contentHeight > sv.height) { sv.contentY = sv.contentHeight - sv.height - } + }*/ } - onUpdateContact: function(_handle, _displayName, _image, _server, _badge, _status, _trusted, _blocked, _loading) { - if (gcd.currentOpenConversation == _handle) { + onUpdateContactStatus: function(_handle, _status, _loading) { + if (gcd.selectedConversation == _handle) { if (_loading == true) { newposttitle.enabled = false btnSend.enabled = false @@ -204,7 +204,7 @@ ColumnLayout { style: CwtchTextFieldStyle{} } - SimpleButton { // SEND MESSAGE BUTTON + Widgets.SimpleButton { // SEND MESSAGE BUTTON id: btnSend icon: "regular/paper-plane" text: "add" diff --git a/qml/overlays/MembershipOverlay.qml b/qml/overlays/MembershipOverlay.qml index d35caef8..b9297717 100644 --- a/qml/overlays/MembershipOverlay.qml +++ b/qml/overlays/MembershipOverlay.qml @@ -105,7 +105,6 @@ ColumnLayout { handle: _handle displayName: _displayName image: _image - trusted: true blocked: false background: false } diff --git a/qml/panes/AddGroupPane.qml b/qml/panes/AddGroupPane.qml index 048e8aaa..4a0e45de 100644 --- a/qml/panes/AddGroupPane.qml +++ b/qml/panes/AddGroupPane.qml @@ -6,7 +6,7 @@ import QtQuick.Layouts 1.3 import QtQuick.Window 2.11 import QtQuick.Controls 1.4 -import "../widgets" +import "../widgets" as Widgets import "../styles" ColumnLayout { // settingsPane @@ -14,7 +14,7 @@ ColumnLayout { // settingsPane anchors.fill: parent - StackToolbar { + Widgets.StackToolbar { id: stb text: qsTr("create-group-title") aux.visible: false @@ -37,7 +37,7 @@ ColumnLayout { // settingsPane spacing: 5 width: root.width - ScalingLabel { + Widgets.ScalingLabel { //: Server label text: qsTr("server-label") + ":" } @@ -48,7 +48,7 @@ ColumnLayout { // settingsPane text: "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd" } - ScalingLabel{ + Widgets.ScalingLabel{ //: Group name label text: qsTr("group-name-label") + ":" } @@ -60,7 +60,7 @@ ColumnLayout { // settingsPane text: qsTr("default-group-name") } - SimpleButton { + Widgets.SimpleButton { //: create group button text: qsTr("create-group-btn") diff --git a/qml/panes/GroupSettingsPane.qml b/qml/panes/GroupSettingsPane.qml index 7616158d..bb0499af 100644 --- a/qml/panes/GroupSettingsPane.qml +++ b/qml/panes/GroupSettingsPane.qml @@ -6,7 +6,7 @@ import QtQuick.Layouts 1.3 import QtQuick.Window 2.11 import QtQuick.Controls 1.4 -import "../widgets" +import "../widgets" as Widgets import "../styles" import "../utils.js" as Utils @@ -16,7 +16,7 @@ ColumnLayout { // groupSettingsPane property string groupID property variant addrbook - StackToolbar { + Widgets.StackToolbar { id: toolbar aux.visible: false back.onClicked: theStack.pane = theStack.messagePane @@ -38,7 +38,7 @@ ColumnLayout { // groupSettingsPane leftPadding: 10 spacing: 5 - ScalingLabel { + Widgets.ScalingLabel { text: qsTr("server-label") + ":" } @@ -48,7 +48,7 @@ ColumnLayout { // groupSettingsPane readOnly: true } - SimpleButton { + Widgets.SimpleButton { icon: "regular/clipboard" text: qsTr("copy-btn") @@ -59,7 +59,7 @@ ColumnLayout { // groupSettingsPane } } - ScalingLabel { + Widgets.ScalingLabel { text: qsTr("invitation-label") + ":" } @@ -69,7 +69,7 @@ ColumnLayout { // groupSettingsPane readOnly: true } - SimpleButton { + Widgets.SimpleButton { icon: "regular/clipboard" text: qsTr("copy-btn") @@ -80,7 +80,7 @@ ColumnLayout { // groupSettingsPane } } - ScalingLabel{ + Widgets.ScalingLabel{ text: qsTr("group-name-label") + ":" } @@ -89,7 +89,7 @@ ColumnLayout { // groupSettingsPane style: CwtchTextFieldStyle{ width: tehcol.width * 0.8 } } - SimpleButton { + Widgets.SimpleButton { text: qsTr("save-btn") onClicked: { @@ -100,7 +100,7 @@ ColumnLayout { // groupSettingsPane } //: Invite someone to the group - ScalingLabel { text: qsTr("invite-to-group-label") } + Widgets.ScalingLabel { text: qsTr("invite-to-group-label") } ComboBox { id: cbInvite @@ -110,7 +110,7 @@ ColumnLayout { // groupSettingsPane style: CwtchComboBoxStyle{} } - SimpleButton { + Widgets.SimpleButton { text: qsTr("invite-btn") onClicked: { @@ -118,7 +118,7 @@ ColumnLayout { // groupSettingsPane } } - SimpleButton { + Widgets.SimpleButton { icon: "regular/trash-alt" text: qsTr("delete-btn") diff --git a/qml/panes/OverlayPane.qml b/qml/panes/OverlayPane.qml index 6ac6f185..d56d3462 100644 --- a/qml/panes/OverlayPane.qml +++ b/qml/panes/OverlayPane.qml @@ -19,14 +19,14 @@ ColumnLayout { StackToolbar { id: toolbar - membership.visible: gcd.currentOpenConversation.length == 32 + membership.visible: gcd.selectedConversation.length == 32 membership.onClicked: overlayStack.overlay = overlayStack.membershipOverlay aux.onClicked: { - if (gcd.currentOpenConversation.length == 32) { + if (gcd.selectedConversation.length == 32) { theStack.pane = theStack.groupProfilePane - gcd.requestGroupSettings(gcd.currentOpenConversation) + gcd.requestGroupSettings(gcd.selectedConversation) } else { theStack.pane = theStack.userProfilePane gcd.requestPeerSettings() @@ -36,7 +36,7 @@ ColumnLayout { } RowLayout { - visible:!overlay.accepted && (gcd.currentOpenConversation.length == 32) + visible:!overlay.accepted && (gcd.selectedConversation.length == 32) Text { @@ -49,8 +49,8 @@ ColumnLayout { text: qsTr("accept-group-btn") icon: "regular/heart" onClicked: { - gcd.acceptGroup(gcd.currentOpenConversation) - gcd.requestGroupSettings(gcd.currentOpenConversation) + gcd.acceptGroup(gcd.selectedConversation) + gcd.requestGroupSettings(gcd.selectedConversation) } } @@ -59,7 +59,7 @@ ColumnLayout { text: qsTr("reject-group-btn") icon: "regular/trash-alt" onClicked: { - gcd.leaveGroup(gcd.currentOpenConversation) + gcd.leaveGroup(gcd.selectedConversation) theStack.pane = theStack.emptyPane } } diff --git a/qml/panes/PeerSettingsPane.qml b/qml/panes/PeerSettingsPane.qml index a48b8f02..3b7927e5 100644 --- a/qml/panes/PeerSettingsPane.qml +++ b/qml/panes/PeerSettingsPane.qml @@ -6,7 +6,7 @@ import QtQuick.Layouts 1.3 import QtQuick.Window 2.11 import QtQuick.Controls 1.4 -import "../widgets" +import "../widgets" as Widgets import "../styles" ColumnLayout { // peerSettingsPane @@ -14,7 +14,7 @@ ColumnLayout { // peerSettingsPane anchors.fill: parent property bool blocked - StackToolbar { + Widgets.StackToolbar { id: toolbar aux.visible: false @@ -38,7 +38,7 @@ ColumnLayout { // peerSettingsPane leftPadding: 10 spacing: 5 - ScalingLabel { + Widgets.ScalingLabel { text: qsTr("address-label") } @@ -48,7 +48,7 @@ ColumnLayout { // peerSettingsPane readOnly: true } - SimpleButton { + Widgets.SimpleButton { icon: "regular/clipboard" text: qsTr("copy-btn") @@ -60,7 +60,7 @@ ColumnLayout { // peerSettingsPane } } - ScalingLabel{ + Widgets.ScalingLabel{ text: qsTr("display-name-label") } @@ -69,7 +69,7 @@ ColumnLayout { // peerSettingsPane style: CwtchTextFieldStyle{ width: tehcol.width * 0.8 } } - SimpleButton { + Widgets.SimpleButton { text: qsTr("save-btn") onClicked: { @@ -80,7 +80,7 @@ ColumnLayout { // peerSettingsPane } - SimpleButton { + Widgets.SimpleButton { icon: "solid/hand-paper" text: root.blocked ? qsTr("unblock-btn") : qsTr("block-btn") @@ -94,7 +94,7 @@ ColumnLayout { // peerSettingsPane } } - SimpleButton { + Widgets.SimpleButton { icon: "regular/trash-alt" text: qsTr("delete-btn") diff --git a/qml/panes/ProfileAddEditPane.qml b/qml/panes/ProfileAddEditPane.qml new file mode 100644 index 00000000..8939877d --- /dev/null +++ b/qml/panes/ProfileAddEditPane.qml @@ -0,0 +1,303 @@ +import QtGraphicalEffects 1.0 +import QtQuick 2.7 +import QtQuick.Controls 2.13 +import QtQuick.Controls.Material 2.0 +import QtQuick.Layouts 1.3 +import QtQuick.Window 2.11 + + +import "../widgets" as Widgets +// import "../styles" + +ColumnLayout { // Add Profile Pane + id: profileAddEditPane + anchors.fill: parent + + property string mode // edit or add + property string onion + property string tag + property bool deleting + property bool changingPassword + + Widgets.StackToolbar { + id: stb + text: mode == "add" ? qsTr("add-profile-title") : qsTr("edit-profile-title") + aux.visible: false + membership.visible: false + stack: "management" + } + + function reset() { + mode = "add" + txtProfileName.text = qsTr("default-profile-name") + changingPassword = false + txtPassword1.text = "" + txtPassword2.text = "" + deleting = false + deleteConfirmLabel.color = "black" + passwordErrorLabel.visible = false + txtCurrentPassword.text = "" + + tag = "" + confirmDeleteTxt.text = "" + radioUsePassword.checked = true + passwordChangeErrorLabel.visible = false + } + + function load(userOnion, name, userTag) { + reset() + + mode = "edit" + tag = userTag + onion = userOnion + txtPassword1.text = "" + txtPassword2.text = "" + onionLabel.text = onion + txtProfileName.text = name + + if (tag == "v1-defaultPassword" || tag == "v1-default-password") { + radioNoPassword.checked = true + } else { + radioUsePassword.checked = true + } + } + + + Flickable { + anchors.top: stb.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + boundsBehavior: Flickable.StopAtBounds + clip:true + contentWidth: tehcol.width + contentHeight: tehcol.height + + Column { + id: tehcol + leftPadding: 10 + spacing: 5 + width: profileAddEditPane.width + + Widgets.ScalingLabel { + //: Onion + text: qsTr("profile-onion-label") + ":" + visible: mode == "edit" + } + + Widgets.ScalingLabel { + id: onionLabel + visible: mode == "edit" + } + + Widgets.ScalingLabel { + //: Display name + text: qsTr("profile-name") + ":" + } + + Widgets.TextField { + id: txtProfileName + Layout.fillWidth: true + //style: CwtchTextFieldStyle{ width: tehcol.width * 0.8 } + //: default suggested profile name + text: qsTr("default-profile-name") + + + } + + RowLayout { + //id: radioButtons + + Widgets.RadioButton { + id: radioUsePassword + checked: true + //: Password + text: qsTr("radio-use-password") + visible: mode == "add" || tag == "v1-defaultPassword" + onClicked: { + changingPassword = true + } + } + + Widgets.RadioButton { + id: radioNoPassword + //: Unencrypted (No password) + text: qsTr("radio-no-password") + visible: mode == "add" || tag == "v1-defaultPassword" + onClicked: { + changingPassword = true + } + } + } + + Widgets.ScalingLabel { + id: noPasswordLabel + //: Not using a password on this account means that all data stored locally will not be encrypted + text: qsTr("no-password-warning") + visible: radioNoPassword.checked + } + + Widgets.ScalingLabel { + id: currentPasswordLabel + //: Current Password + text: qsTr("current-password-label") + ":" + visible: radioUsePassword.checked && mode == "edit" && tag != "v1-defaultPassword" + } + + Widgets.TextField { + id: txtCurrentPassword + Layout.fillWidth: true + echoMode: TextInput.Password + visible: radioUsePassword.checked && mode == "edit" && tag != "v1-defaultPassword" + } + + Widgets.ScalingLabel { + id: passwordLabel + //: Password + text: qsTr("password1-label") + ":" + visible: radioUsePassword.checked + } + + Widgets.TextField { + id: txtPassword1 + Layout.fillWidth: true + //style: CwtchTextFieldStyle{ width: tehcol.width * 0.8 } + echoMode: TextInput.Password + visible: radioUsePassword.checked + + onTextEdited: { + changingPassword = true + } + } + + + Widgets.ScalingLabel { + id: passwordReLabel + //: Reenter password + text: qsTr("password2-label") + ":" + visible: radioUsePassword.checked + } + + Widgets.TextField { + id: txtPassword2 + Layout.fillWidth: true + //style: CwtchTextFieldStyle{ width: tehcol.width * 0.8 } + echoMode: TextInput.Password + visible: radioUsePassword.checked + } + + Widgets.SimpleButton { // ADD or SAVE button + //: Create Profile || Save Profile + text: mode == "add" ? qsTr("create-profile-btn") : qsTr("save-profile-btn") + + onClicked: { + if (mode == "add") { + if (txtPassword1.text != txtPassword2.text) { + passwordErrorLabel.visible = true + } else { + gcd.createProfile(txtProfileName.text, radioNoPassword.checked, txtPassword1.text) + gcd.reloadProfileList() + parentStack.pane = parentStack.managementPane + } + } else { + gcd.updateNick(onion, txtProfileName.text) + + if (changingPassword) { + if (txtPassword1.text != txtPassword2.text) { + passwordErrorLabel.visible = true + } else { + gcd.changePassword(onion, txtCurrentPassword.text, txtPassword1.text, radioNoPassword.checked) + } + } else { + gcd.reloadProfileList() + parentStack.pane = parentStack.managementPane + } + } + + } + } + + Widgets.ScalingLabel { + id: passwordErrorLabel + //: Passwords do not match + text: qsTr("password-error-match") + visible: false + color: "red" + } + + Widgets.ScalingLabel { + id: passwordChangeErrorLabel + //: Error changing password: Supplied password rejected + text: qsTr("password-change-error") + visible: false + color: "red" + } + + // ***** Delete button and confirm flow ***** + + Widgets.SimpleButton { + //: Delete Profile + text: qsTr("delete-profile-btn") + icon: "regular/trash-alt" + visible: mode == "edit" + + + onClicked: { + deleting = true + } + } + + Widgets.ScalingLabel { + id: deleteConfirmLabel + //: Type DELETE to confirm + text: qsTr("delete-confirm-label")+ ":" + visible: deleting + } + + Widgets.TextField { + id: confirmDeleteTxt + Layout.fillWidth: true + //style: CwtchTextFieldStyle{ width: tehcol.width * 0.8 } + visible: deleting + } + + Widgets.SimpleButton { + id: confirmDeleteBtn + icon: "regular/trash-alt" + + //: Really Delete Profile + text: qsTr("delete-profile-confirm-btn") + color: "red" + visible: deleting + + onClicked: { + //: DELETE + if (confirmDeleteTxt.text == qsTr("delete-confirm-text")) { + deleteConfirmLabel.color = "black" + gcd.deleteProfile(onion) + gcd.reloadProfileList() + parentStack.pane = parentStack.managementPane + } else { + deleteConfirmLabel.color = "red" + } + + } + } + + + }//end of column with padding + }//end of flickable + + Connections { // UPDATE UNREAD MESSAGES COUNTER + target: gcd + + onChangePasswordResponse: function(error) { + if (!error) { + gcd.reloadProfileList() + parentStack.pane = parentStack.managementPane + } else { + passwordChangeErrorLabel.visible = true + } + } + } +} \ No newline at end of file diff --git a/qml/panes/ProfileManagerPane.qml b/qml/panes/ProfileManagerPane.qml new file mode 100644 index 00000000..c288b4a4 --- /dev/null +++ b/qml/panes/ProfileManagerPane.qml @@ -0,0 +1,85 @@ +import QtQuick 2.0 +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 QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +import "../widgets" as Widgets +import "../widgets/controls" +import "../styles" + + +ColumnLayout { + id: thecol + anchors.fill: parent + //leftPadding: 10 + //spacing: 5 + + Widgets.ScalingLabel { + anchors.horizontalCenter: parent.horizontalCenter + wrapMode: TextEdit.Wrap + //: Please enter password: + text: qsTr("enter-profile-password")+":" + } + + TextField { + id: txtPassword + anchors.horizontalCenter: parent.horizontalCenter + style: CwtchTextFieldStyle{ width: thecol.width * 0.8 } + echoMode: TextInput.Password + onAccepted: button.clicked() + } + + Widgets.ScalingLabel { + id: error + anchors.horizontalCenter: parent.horizontalCenter + color: "red" + //: 0 profiles loaded with that password + text: qsTr("error-0-profiles-loaded-for-password") + visible: false + } + + Widgets.SimpleButton { + id: "button" + anchors.horizontalCenter: parent.horizontalCenter + + icon: "solid/unlock-alt" + //: Unlock + text: qsTr("unlock") + + onClicked: { + gcd.unlockProfiles(txtPassword.text) + txtPassword.text = "" + error.visible = false + } + } + + Connections { // ADD/REMOVE CONTACT ENTRIES + target: gcd + + onErrorLoaded0: function() { + error.visible = true + } + } + + + Rectangle { // THE LEFT PANE WITH TOOLS AND CONTACTS + color: "#D2C0DD" + width: thecol.width + + Layout.fillHeight: true + Layout.fillWidth: true + Layout.minimumWidth: Layout.maximumWidth + //Layout.maximumWidth: theStack.pane == theStack.emptyPane ? parent.width : 450 + + Widgets.ProfileList { + anchors.fill: parent + } + } + + +} diff --git a/qml/widgets/ContactList.qml b/qml/widgets/ContactList.qml index d13fdd3d..1552475d 100644 --- a/qml/widgets/ContactList.qml +++ b/qml/widgets/ContactList.qml @@ -47,19 +47,36 @@ ColumnLayout { Connections { // ADD/REMOVE CONTACT ENTRIES target: gcd - onAddContact: function(handle, displayName, image, server, badge, status, trusted, blocked, loading) { - contactsModel.append({ - "_handle": handle, - "_displayName": displayName + (blocked ? " (blocked)" : "" ), - "_image": image, - "_server": server, - "_badge": badge, - "_status": status, - "_trusted": trusted, - "_blocked": blocked, - "_loading": loading, - "_loading": loading - }) + onAddContact: function(handle, displayName, image, server, badge, status, blocked, loading, lastMsgTs) { + + for (var i = 0; i < contactsModel.count; i++) { + if (contactsModel.get(i)["_handle"] == handle) { + return + } + } + + var index = contactsModel.count + for (var i = 0; i < contactsModel.count; i++) { + if (contactsModel.get(i)["_lastMsgTs"] < lastMsgTs) { + index = i + break + } + } + + var newContact = { + "_handle": handle, + "_displayName": displayName + (blocked ? " (blocked)" : "" ), + "_image": image, + "_server": server, + "_badge": badge, + "_status": status, + "_blocked": blocked, + "_loading": loading, + "_loading": loading, + "_lastMsgTs": lastMsgTs + } + + contactsModel.insert(index, newContact) } onRemoveContact: function(handle) { @@ -71,6 +88,22 @@ ColumnLayout { } } } + + onIncContactUnreadCount: function(handle) { + var ts = Math.round((new Date()).getTime() / 1000); + for(var i = 0; i < contactsModel.count; i++){ + if(contactsModel.get(i)["_handle"] == handle) { + var contact = contactsModel.get(i) + contact["_lastMsgTs"] = ts + console.log("Found at " + i + " contact: " + contact) + contactsModel.move(i, 0, 1) + } + } + } + + onResetProfile: function() { + contactsModel.clear() + } } ListModel { // CONTACT OBJECTS ARE STORED HERE ... @@ -86,11 +119,18 @@ ColumnLayout { server: _server badge: _badge status: _status - trusted: _trusted blocked: _blocked loading: _loading + type: "contact" } } + + } } + + } + + + diff --git a/qml/widgets/ContactPicture.qml b/qml/widgets/ContactPicture.qml index 33ffa270..03ad82b6 100644 --- a/qml/widgets/ContactPicture.qml +++ b/qml/widgets/ContactPicture.qml @@ -17,6 +17,7 @@ Item { property bool isGroup property bool showStatus property bool highlight + property bool button property real logscale: 4 * Math.log10(gcd.themeScale + 1) property int baseWidth: 48 * logscale @@ -30,7 +31,7 @@ Item { Rectangle { width: highlight ? baseWidth - 4 : baseWidth height: highlight ? baseWidth - 4 : baseWidth - color: "#FFFFFF" + color: button ? windowItem.cwtch_dark_color: "#FFFFFF" radius: width / 2 anchors.centerIn:parent @@ -69,7 +70,7 @@ Item { anchors.margins: 4 * logscale - Rectangle { //-2:WtfCodeError,-1:Untrusted,0:Disconnected,1:Connecting,2:Connected,3:Authenticated,4:Synced,5:Failed,6:Killed + Rectangle { //-2:WtfCodeError,-1:Error,0:Disconnected,1:Connecting,2:Connected,3:Authenticated,4:Synced,5:Failed,6:Killed color: status == 4 ? "green" : status == 3 ? "green" : status == -1 ? "blue" : status == 1 ? "orange" : status == 2 ? "orange" : "red" width: 5 * logscale height: 5 * logscale diff --git a/qml/widgets/ContactRow.qml b/qml/widgets/ContactRow.qml index 3b4f2661..f74d838d 100644 --- a/qml/widgets/ContactRow.qml +++ b/qml/widgets/ContactRow.qml @@ -10,6 +10,7 @@ import QtQuick.Controls.Styles 1.4 Item { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY + id: crItem anchors.left: parent.left anchors.right: parent.right height: 48 * logscale + 3 @@ -22,12 +23,18 @@ Item { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY property int badge property bool isActive property bool isHover - property bool trusted + property bool background: true + property string type // profile or contact or button + property string tag // profile version/type + + // Profile + property bool defaultPassword + + // Contact property bool blocked property bool loading - property alias status: imgProfile.status - property string server - property bool background: true + property alias status: imgProfile.status + property string server Rectangle { // CONTACT ENTRY BACKGROUND COLOR @@ -40,24 +47,43 @@ Item { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY ContactPicture { id: imgProfile - showStatus: true + showStatus: type == "contact" + button: type == "button" } - Label { // CONTACT NAME - id: cn - leftPadding: 10 - rightPadding: 10 - //wrapMode: Text.WordWrap + ColumnLayout { + anchors.left: imgProfile.right anchors.right: loading ? loadingProgress.left : rectUnread.left anchors.verticalCenter: parent.verticalCenter - font.pixelSize: 16 * gcd.themeScale - font.italic: !trusted - font.strikeout: blocked - textFormat: Text.PlainText - //fontSizeMode: Text.HorizontalFit - elide: Text.ElideRight - color: "#000000" + + + Label { // CONTACT NAME + id: cn + leftPadding: 10 + rightPadding: 10 + //wrapMode: Text.WordWrap + font.pixelSize: 16 * gcd.themeScale + font.strikeout: blocked + textFormat: Text.PlainText + //fontSizeMode: Text.HorizontalFit + elide: Text.ElideRight + color: "#000000" + } + + Label { // Onion + id: onion + text: handle + leftPadding: 10 + rightPadding: 10 + font.pixelSize: 10 * gcd.themeScale + font.strikeout: blocked + textFormat: Text.PlainText + //fontSizeMode: Text.HorizontalFit + elide: Text.ElideRight + color: "#000000" + } + } Rectangle { // UNREAD MESSAGES? @@ -87,6 +113,27 @@ Item { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY } } + // Profile + + Image {// Profle Type + id: profiletype + + source: tag == "v1-userPassword" ? "qrc:/qml/images/fontawesome/solid/lock.svg" : "qrc:/qml/images/fontawesome/solid/lock-open.svg" + + anchors.right: parent.right + + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 1 * gcd.themeScale + anchors.rightMargin: (20 * gcd.themeScale) + parent.height + height: parent.height * 0.5 + width: height + + visible: type == "profile" + + } + + + // Contact ProgressBar { // LOADING ? id: loadingProgress property bool running @@ -118,14 +165,26 @@ Item { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY hoverEnabled: true onClicked: { - if (displayName != "me") { - gcd.broadcast("ResetMessagePane") - isActive = true - theStack.pane = theStack.messagePane - gcd.loadMessagesPane(handle) - if (handle.length == 32) { - gcd.requestGroupSettings(handle) - } + if (type == "contact") { + if (displayName != "me") { + gcd.broadcast("ResetMessagePane") + isActive = true + theStack.pane = theStack.messagePane + gcd.loadMessagesPane(handle) + badge = 0 + if (handle.length == 32) { + gcd.requestGroupSettings(handle) + } + } + } else if (type == "profile") { + gcd.broadcast("ResetMessagePane") + gcd.broadcast("ResetProfile") + gcd.selectedProfile = handle + gcd.loadProfile(handle) + parentStack.pane = parentStack.profilePane + } else if (type == "button") { // Add profile button + profileAddEditPane.reset() + parentStack.pane = parentStack.addEditProfilePane } } @@ -138,6 +197,27 @@ Item { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY } } + SimpleButton {// Edit BUTTON + id: btnEdit + icon: "solid/user-edit" + + anchors.right: parent.right + + //rectUnread.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 1 * gcd.themeScale + anchors.rightMargin: 20 * gcd.themeScale + height: parent.height * 0.75 + + visible: type == "profile" + + + onClicked: { + profileAddEditPane.load(handle, displayName, tag) + parentStack.pane = parentStack.addEditProfilePane + } + } + Connections { // UPDATE UNREAD MESSAGES COUNTER target: gcd @@ -145,17 +225,29 @@ Item { // LOTS OF NESTING TO DEAL WITH QT WEIRDNESS, SORRY isActive = false } - onUpdateContact: function(_handle, _displayName, _image, _server, _badge, _status, _trusted, _blocked, _loading) { - if (handle == _handle) { - displayName = _displayName + (_blocked == true ? " (blocked)" : "") - image = _image - server = _server - badge = _badge - status = _status - trusted = _trusted - blocked = _blocked + onUpdateContactStatus: function(_handle, _status, _loading) { + if (handle == _handle) { + status = _status loadingProgress.visible = loadingProgress.running = loading = _loading - } + } + } + + onUpdateContactBlocked: function(_handle, _blocked) { + if (handle == _handle) { + blocked = _blocked + } + } + + onUpdateContactDisplayName: function(_handle, _displayName) { + if (handle == _handle) { + displayName = _displayName + (_blocked == true ? " (blocked)" : "") + } + } + + onIncContactUnreadCount: function(handle) { + if (handle == _handle && gcd.selectedConversation != handle) { + badge++ + } } } } diff --git a/qml/widgets/Message.qml b/qml/widgets/Message.qml index 104ee32e..7f437001 100644 --- a/qml/widgets/Message.qml +++ b/qml/widgets/Message.qml @@ -62,6 +62,7 @@ Item { onClicked: { + gcd.createContact(from) gcd.broadcast("ResetMessagePane") theStack.pane = theStack.messagePane gcd.loadMessagesPane(from) diff --git a/qml/widgets/MyProfile.qml b/qml/widgets/MyProfile.qml index 493b5a9b..8bb3ec0f 100644 --- a/qml/widgets/MyProfile.qml +++ b/qml/widgets/MyProfile.qml @@ -19,6 +19,22 @@ ColumnLayout { property string onion + SimpleButton {// BACK BUTTON + id: btnBack + icon: "solid/arrow-circle-left" + anchors.left: parent.left + //anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 2 + anchors.top: parent.top + anchors.topMargin: 2 + onClicked: function() { + gcd.selectedProfile = "none" + gcd.reloadProfileList() + parentStack.pane = parentStack.managementPane + theStack.pane = theStack.emptyPane + } + } + Item{ height: 6 } Item { // PROFILE IMAGE @@ -106,7 +122,7 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter onUpdated: { - gcd.updateNick(lblNick.text) + gcd.updateNick(onion, lblNick.text) } } @@ -230,8 +246,6 @@ ColumnLayout { lblNick.text = _nick onion = _onion image = _image - parentStack.currentIndex = 1 - splashPane.running = false } onTorStatus: function(code, str) { diff --git a/qml/widgets/ProfileList.qml b/qml/widgets/ProfileList.qml new file mode 100644 index 00000000..b7c1cc57 --- /dev/null +++ b/qml/widgets/ProfileList.qml @@ -0,0 +1,125 @@ +import QtGraphicalEffects 1.0 +import QtQuick 2.7 +import QtQuick.Controls 2.4 +import QtQuick.Controls.Material 2.0 +import QtQuick.Layouts 1.3 + +ColumnLayout { + id: root + + + MouseArea { + anchors.fill: parent + + onClicked: { + forceActiveFocus() + } + } + + Flickable { // Profile List + id: sv + clip: true + Layout.minimumHeight: 100 + Layout.fillHeight: true + Layout.minimumWidth: parent.width + Layout.maximumWidth: parent.width + contentWidth: colContacts.width + contentHeight: colContacts.height + boundsBehavior: Flickable.StopAtBounds + maximumFlickVelocity: 400 + + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AlwaysOn + } + + ColumnLayout { + id: colContacts + width: root.width + spacing: 0 + + Connections { // ADD/REMOVE CONTACT ENTRIES + target: gcd + + onAddProfile: function(handle, displayName, image, tag) { + + // don't add duplicates + console.log("ProfileList onAddProfile for: " + handle) + for (var i = 0; i < profilesModel.count; i++) { + if (profilesModel.get(i)["_handle"] == handle) { + return + } + } + + // find index for insert (sort by onion) + var index = profilesModel.count-1 + for (var i = 0; i < profilesModel.count-1; i++) { + if (profilesModel.get(i)["_handle"] > handle) { + index = i + break + } + } + + profilesModel.insert(index, + { + "_handle": handle, + "_displayName": displayName, + "_image": image, + "_type": "profile", + "_tag": tag + }) + } + + /* + onRemoveProfile: function(handle) { + for(var i = 0; i < profilesModel.count; i++){ + if(profilesModel.get(i)["_handle"] == handle) { + console.log("deleting contact " + profilesModel.get(i)["_handle"]) + profilesModel.remove(i) + return + } + } + }*/ + + onResetProfileList: function() { + profilesModel.clear() + profilesModel.append({ + _handle: "", + _displayName: qsTr("add-new-profile-btn"), + _image: "qrc:/qml/images/fontawesome/solid/user-plus.svg", + _type: "button", + _tag: "," + }) + } + } + + ListModel { // Profile OBJECTS ARE STORED HERE ... + id: profilesModel + + ListElement { + _handle: "" + _displayName: qsTr("add-new-profile-btn") + _image: "qrc:/qml/images/fontawesome/solid/user-plus.svg" + _type: "button" + _tag: "" + } + } + + Repeater { + model: profilesModel // ... AND DISPLAYED HERE + delegate: ContactRow { + handle: _handle + displayName: _displayName + image: _image + server: "" + badge: 0 + status: 0 + blocked: false + loading: false + type: _type + tag: _tag + } + } + } + } +} diff --git a/qml/widgets/RadioButton.qml b/qml/widgets/RadioButton.qml new file mode 100644 index 00000000..187822e6 --- /dev/null +++ b/qml/widgets/RadioButton.qml @@ -0,0 +1,27 @@ +import QtQuick 2.7 + +import QtQuick.Controls 2.13 + + +RadioButton { + id: control + + property real size: 12 + spacing: 0 + + indicator: Rectangle { + width: 16 * gcd.themeScale + height: 16 * gcd.themeScale + anchors.verticalCenter: parent.verticalCenter + radius: 9 + border.width: 1 + + Rectangle { + anchors.fill: parent + visible: control.checked + color: "black" + radius: 9 + anchors.margins: 4 + } + } +} \ No newline at end of file diff --git a/qml/widgets/SimpleButton.qml b/qml/widgets/SimpleButton.qml index fe953ce0..f5bce956 100644 --- a/qml/widgets/SimpleButton.qml +++ b/qml/widgets/SimpleButton.qml @@ -4,7 +4,6 @@ import QtQuick.Controls 2.4 import QtQuick.Controls.Material 2.0 import QtQuick.Layouts 1.3 -import "controls" as Awesome import "../fonts/Twemoji.js" as T Rectangle { diff --git a/qml/widgets/StackToolbar.qml b/qml/widgets/StackToolbar.qml index c3b917c0..05ddc52e 100644 --- a/qml/widgets/StackToolbar.qml +++ b/qml/widgets/StackToolbar.qml @@ -4,7 +4,6 @@ import QtQuick.Controls 2.4 import QtQuick.Controls.Material 2.0 import QtQuick.Layouts 1.3 -import "controls" as Awesome import "../fonts/Twemoji.js" as T Rectangle { // OVERHEAD BAR ON STACK PANE @@ -21,6 +20,7 @@ Rectangle { // OVERHEAD BAR ON STACK PANE property alias aux: btnAux property alias back: btnBack property alias membership: btnMembership + property string stack: "profile" // profile(theStack) or management(parentStack) SimpleButton {// BACK BUTTON @@ -29,7 +29,13 @@ Rectangle { // OVERHEAD BAR ON STACK PANE anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 6 - onClicked: theStack.pane = theStack.emptyPane + onClicked: { + if (stack == "profile") { + theStack.pane = theStack.emptyPane + } else { + parentStack.pane = parentStack.managementPane + } + } } ScalingLabel { // TEXT diff --git a/qml/widgets/TextField.qml b/qml/widgets/TextField.qml new file mode 100644 index 00000000..6486f9d0 --- /dev/null +++ b/qml/widgets/TextField.qml @@ -0,0 +1,16 @@ +import QtQuick 2.7 + +import QtQuick.Controls 2.13 + + +TextField { + color: "black" + font.pointSize: 10 * gcd.themeScale + width: 100 + + background: Rectangle { + radius: 2 + color: windowItem.cwtch_background_color + border.color: windowItem.cwtch_color + } +} \ No newline at end of file diff --git a/qml/widgets/controls/Button.qml b/qml/widgets/controls/Button.qml deleted file mode 100644 index 1c120a50..00000000 --- a/qml/widgets/controls/Button.qml +++ /dev/null @@ -1,81 +0,0 @@ -/**************************************************************************** -** -** -** Copyright (c) 2014 Ricardo do Valle Flores de Oliveira -** -** $BEGIN_LICENSE:MIT$ -** Permission is hereby granted, free of charge, to any person obtaining a copy -** of this software and associated documentation files (the "Software"), to deal -** in the Software without restriction, including without limitation the rights -** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -** copies of the Software, and to permit persons to whom the Software is -** furnished to do so, subject to the following conditions: -** -** The above copyright notice and this permission notice shall be included in -** all copies or substantial portions of the Software. -** -** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -** SOFTWARE. -** -** -****************************************************************************/ - -import QtQuick 2.0 -import QtQuick.Controls 1.0 -import QtQuick.Controls.Styles 1.0 -import QtQuick.Layouts 1.0 - -Button { - id: button - property string icon - property color color: "black" - property font font - - style: ButtonStyle { - id: buttonstyle - property font font: button.font - property color foregroundColor: button.color - - background: Item { - Rectangle { - id: baserect - anchors.fill: parent - color: "transparent" - } - } - - label: Item { - implicitWidth: row.implicitWidth - implicitHeight: row.implicitHeight - - RowLayout { - id: row - anchors.centerIn: parent - spacing: 15 - - Text { - color: buttonstyle.foregroundColor - font.pointSize: buttonstyle.font.pointSize * 2 - font.family: awesome.family - renderType: Text.NativeRendering - text: awesome.loaded ? icon : "" - visible: !(icon === "") - } - Text { - color: buttonstyle.foregroundColor - font: buttonstyle.font - renderType: Text.NativeRendering - text: control.text - visible: !(control.text === "") - - Layout.alignment: Qt.AlignBottom - } - } - } - } -} diff --git a/qml/widgets/controls/Text.qml b/qml/widgets/controls/Text.qml deleted file mode 100644 index 8931eb17..00000000 --- a/qml/widgets/controls/Text.qml +++ /dev/null @@ -1,61 +0,0 @@ -/**************************************************************************** -** -** The MIT License (MIT) -** -** Copyright (c) 2014 Ricardo do Valle Flores de Oliveira -** -** $BEGIN_LICENSE:MIT$ -** Permission is hereby granted, free of charge, to any person obtaining a copy -** of this software and associated documentation files (the "Software"), to deal -** in the Software without restriction, including without limitation the rights -** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -** copies of the Software, and to permit persons to whom the Software is -** furnished to do so, subject to the following conditions: -** -** The above copyright notice and this permission notice shall be included in -** all copies or substantial portions of the Software. -** -** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -** SOFTWARE. -** -** $END_LICENSE$ -** -****************************************************************************/ - -import QtQuick 2.0 -import QtQuick.Controls 1.0 -import QtQuick.Layouts 1.0 - -Text { - id: root - - property alias spacing: row.spacing - property alias text: content.text - property color color: "black" - property font font - property string icon - - RowLayout { - id: row - - Text { - color: root.color - font.pointSize: root.font.pointSize - font.family: awesome.family - renderType: Text.NativeRendering - text: awesome.loaded ? icon : "" - } - - Text { - id: content - color: root.color - font.pointSize: root.font.pointSize - renderType: Text.NativeRendering - } - } -}