diff --git a/.gitignore b/.gitignore index d516050d..c5816869 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,6 @@ .*.sw? *~ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -vendor - # Test files godog.test debug.test @@ -17,17 +14,12 @@ coverage.html # Run files mem.pprof -# Auto generated frontend -internal/frontend/qml/BridgeUI/*.qmlc -internal/frontend/qml/ImportExportUI/*.qmlc -internal/frontend/qml/ProtonUI/*.qmlc -internal/frontend/qml/ProtonUI/fontawesome.ttf -internal/frontend/qml/ProtonUI/images -internal/frontend/qml/ImportExportUI/images -frontend/qml/*.qmlc - -# Credits files (generated). +# Auto generated internal/**/credits.go +vendor +vendor-cache +/main.go + # Build files /launcher-* @@ -37,18 +29,3 @@ internal/**/credits.go /hasher cmd/Desktop-Bridge/deploy cmd/Import-Export/deploy -internal/frontend/qt*/moc.cpp -internal/frontend/qt*/moc.go -internal/frontend/qt*/moc.h -internal/frontend/qt*/moc_cgo_*.go -internal/frontend/qt*/moc_moc.h -internal/frontend/qt*/rcc.cpp -internal/frontend/qt*/rcc.qrc -internal/frontend/qt*/rcc_cgo_*.go - -internal/frontend/rcc.cpp -internal/frontend/rcc.qrc -internal/frontend/rcc_cgo_*.go -vendor-cache/ - -/main.go diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 48b6aea2..3c54b00b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -126,6 +126,7 @@ build-linux-qa: extends: .build-base only: - web + - branches script: - BUILD_TAGS="build_qa" make build artifacts: @@ -161,6 +162,7 @@ build-darwin-qa: extends: .build-darwin-base only: - web + - branches script: - BUILD_TAGS="build_qa" make build artifacts: diff --git a/.golangci.yml b/.golangci.yml index 799d2645..ebaab17f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,8 +1,6 @@ --- run: timeout: 10m - build-tags: - - nogui skip-dirs: - pkg/mime diff --git a/Makefile b/Makefile index 288eead8..6e2b2ff6 100644 --- a/Makefile +++ b/Makefile @@ -88,7 +88,7 @@ ${TGZ_TARGET}: ${DEPLOY_DIR}/${TARGET_OS} cd ${DEPLOY_DIR}/${TARGET_OS} && tar czf ../../../../$@ . ${DEPLOY_DIR}/linux: ${EXE_TARGET} - cp -pf ./internal/frontend/share/icons/${SRC_SVG} ${DEPLOY_DIR}/linux/logo.svg + cp -pf ./internal/frontend/share/${SRC_SVG} ${DEPLOY_DIR}/linux/logo.svg cp -pf ./LICENSE ${DEPLOY_DIR}/linux/ cp -pf ./Changelog.md ${DEPLOY_DIR}/linux/ cp -pf ./dist/${EXE_NAME}.desktop ${DEPLOY_DIR}/linux/ @@ -98,7 +98,7 @@ ${DEPLOY_DIR}/darwin: ${EXE_TARGET} mv ${EXE_TARGET}/Contents/MacOS/{${DIRNAME},${EXE_NAME}}; \ perl -i -pe"s/>${DIRNAME}/>${EXE_NAME}/g" ${EXE_TARGET}/Contents/Info.plist; \ fi - cp ./internal/frontend/share/icons/${SRC_ICNS} ${DARWINAPP_CONTENTS}/Resources/${SRC_ICNS} + cp ./internal/frontend/share/${SRC_ICNS} ${DARWINAPP_CONTENTS}/Resources/${SRC_ICNS} cp LICENSE ${DARWINAPP_CONTENTS}/Resources/ rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebEngine.framework" rm -rf "${DARWINAPP_CONTENTS}/Frameworks/QtWebView.framework" @@ -106,7 +106,7 @@ ${DEPLOY_DIR}/darwin: ${EXE_TARGET} ./utils/remove_non_relative_links_darwin.sh "${EXE_TARGET}${EXE_BINARY_DARWIN}" ${DEPLOY_DIR}/windows: ${EXE_TARGET} - cp ./internal/frontend/share/icons/${SRC_ICO} ${DEPLOY_DIR}/windows/logo.ico + cp ./internal/frontend/share/${SRC_ICO} ${DEPLOY_DIR}/windows/logo.ico cp LICENSE ${DEPLOY_DIR}/windows/ QT_BUILD_TARGET:=build desktop @@ -127,9 +127,9 @@ ${EXE_TARGET}: check-has-go gofiles ${RESOURCE_FILE} ${VENDOR_TARGET} WINDRES_YEAR:=$(shell date +%Y) APP_VERSION_COMMA:=$(shell echo "${APP_VERSION}" | sed -e 's/[^0-9,.]*//g' -e 's/\./,/g') -resource.syso: ./internal/frontend/share/info.rc ./internal/frontend/share/icons/${SRC_ICO} .FORCE +resource.syso: ./internal/frontend/share/info.rc ./internal/frontend/share/${SRC_ICO} .FORCE rm -f ./*.syso - windres --target=pe-x86-64 -I ./internal/frontend/share/icons/ -D ICO_FILE=${SRC_ICO} -D EXE_NAME="${EXE_NAME}" -D FILE_VERSION="${APP_VERSION}" -D ORIGINAL_FILE_NAME="${EXE}" -D PRODUCT_VERSION="${APP_VERSION}" -D FILE_VERSION_COMMA=${APP_VERSION_COMMA} -D YEAR=${WINDRES_YEAR} -o $@ $< + windres --target=pe-x86-64 -I ./internal/frontend/share/ -D ICO_FILE=${SRC_ICO} -D EXE_NAME="${EXE_NAME}" -D FILE_VERSION="${APP_VERSION}" -D ORIGINAL_FILE_NAME="${EXE}" -D PRODUCT_VERSION="${APP_VERSION}" -D FILE_VERSION_COMMA=${APP_VERSION_COMMA} -D YEAR=${WINDRES_YEAR} -o $@ $< ## Rules for therecipe/qt .PHONY: prepare-vendor update-vendor update-qt-docs @@ -278,7 +278,7 @@ RUN_FLAGS?=-m -l=${LOG} --log-imap=${LOG_IMAP} ${LOG_SMTP} run: run-nogui-cli run-qt: ${EXE_TARGET} - PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} | tee last.log + PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} 2>&1 | tee last.log run-qt-cli: ${EXE_TARGET} PROTONMAIL_ENV=dev ./$< ${RUN_FLAGS} -c @@ -296,9 +296,7 @@ run-qml-preview: clean-frontend-qt: - # TODO: $(MAKE) -C internal/frontend/qt -f Makefile.local clean -clean-frontend-qt-common: - # TODO: $(MAKE) -C internal/frontend/qt-common -f Makefile.local clean + $(MAKE) -C internal/frontend -f Makefile.local clean clean-vendor: clean-frontend-qt clean-frontend-qt-common rm -rf ./vendor diff --git a/go.mod b/go.mod index d1a5df19..fc5873c4 100644 --- a/go.mod +++ b/go.mod @@ -56,8 +56,10 @@ require ( github.com/ricochet2200/go-disk-usage/du v0.0.0-20210707232629-ac9918953285 github.com/sirupsen/logrus v1.7.0 github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect - github.com/stretchr/objx v0.2.0 // indirect github.com/stretchr/testify v1.7.0 + github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e + github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d // indirect + github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d // indirect github.com/urfave/cli/v2 v2.2.0 github.com/vmihailenco/msgpack/v5 v5.1.3 go.etcd.io/bbolt v1.3.6 diff --git a/go.sum b/go.sum index a4c5597d..22599e10 100644 --- a/go.sum +++ b/go.sum @@ -42,8 +42,6 @@ github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1 github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4= github.com/ProtonMail/go-rfc5322 v0.8.0 h1:7emrf75n3CDIduQflx7aT1nJa5h/kGsiFKUYX/+IAkU= github.com/ProtonMail/go-rfc5322 v0.8.0/go.mod h1:BwpTbkJxkMGkc+pC84AXZnwuWOisEULBpfPIyIKS/Us= -github.com/ProtonMail/go-srp v0.0.0-20210910093455-a843a0b9adff h1:eiue56XAPSkOpsy5Fwnyz4+Vd7i2cN5D4orc++Irt1g= -github.com/ProtonMail/go-srp v0.0.0-20210910093455-a843a0b9adff/go.mod h1:Uvv5cqSGCs8MTZ8sbKiCkBnaB6/OA3eq2mc77tl2VVA= github.com/ProtonMail/go-srp v0.0.1 h1:J0O9Zb5XTC6iDrB7feH41cu+TUEB+l7uHctXIK6oS2o= github.com/ProtonMail/go-srp v0.0.1/go.mod h1:Uvv5cqSGCs8MTZ8sbKiCkBnaB6/OA3eq2mc77tl2VVA= github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ= @@ -195,6 +193,8 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e h1:XWcjeEtTFTOVA9Fs1w7n2XBftk5ib4oZrhzWk0B+3eA= +github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -264,6 +264,7 @@ github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0 github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 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/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= @@ -361,9 +362,11 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -393,6 +396,12 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk= +github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us= +github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d h1:hAZyEG2swPRWjF0kqqdGERXUazYnRJdAk4a58f14z7Y= +github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc= +github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d h1:AJRoBel/g9cDS+yE8BcN3E+TDD/xNAguG21aoR8DAIE= +github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= @@ -430,6 +439,7 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/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= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -473,6 +483,7 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -505,7 +516,9 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h 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= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -539,6 +552,7 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= diff --git a/internal/frontend/.gitignore b/internal/frontend/.gitignore new file mode 100644 index 00000000..e2c7a202 --- /dev/null +++ b/internal/frontend/.gitignore @@ -0,0 +1,11 @@ +# Auto generated +moc.cpp +moc.go +moc.h +moc_cgo_*.go +moc_moc.h +rcc.cpp +rcc.qrc +rcc_cgo_*.go +*.qmlc + diff --git a/internal/frontend/Makefile.local b/internal/frontend/Makefile.local new file mode 100644 index 00000000..0bd5369f --- /dev/null +++ b/internal/frontend/Makefile.local @@ -0,0 +1,14 @@ +FILES=$(shell find . -iname 'rcc.qrc') +FILES+=$(shell find . -iname 'rcc.cpp') +FILES+=$(shell find . -iname 'rcc_cgo*.go') + +FILES+=$(shell find . -iname 'moc.go') +FILES+=$(shell find . -iname 'moc.cpp') +FILES+=$(shell find . -iname 'moc.h') +FILES+=$(shell find . -iname 'moc_cgo*.go') + +FILES+=$(shell find ./qml -iname '*.qmlc') + +clean: + rm -f ${FILES} + diff --git a/internal/frontend/clientconfig/config.go b/internal/frontend/clientconfig/config.go new file mode 100644 index 00000000..5495c3a9 --- /dev/null +++ b/internal/frontend/clientconfig/config.go @@ -0,0 +1,76 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// Package clientconfig provides automatic config of IMAP and SMTP. +// For now only for Apple Mail. +package clientconfig + +import ( + "errors" + + "github.com/ProtonMail/proton-bridge/internal/config/settings" + "github.com/ProtonMail/proton-bridge/internal/config/useragent" + "github.com/ProtonMail/proton-bridge/internal/frontend/types" + "github.com/sirupsen/logrus" +) + +type AutoConfig interface { + Name() string + Configure(imapPort int, smtpPort int, imapSSl, smtpSSL bool, user types.User, address string) error +} + +var ( + available = map[string]AutoConfig{} //nolint[gochecknoglobals] + ErrNotAvailable = errors.New("configuration not available") +) + +const AppleMailClient = "Apple Mail" + +func ConfigureAppleMail(user types.User, address string, s *settings.Settings) (needRestart bool, err error) { + return configure(AppleMailClient, user, address, s) +} + +func configure(configName string, user types.User, address string, s *settings.Settings) (needRestart bool, err error) { + log := logrus.WithField("pkg", "client_config").WithField("client", configName) + + config, ok := available[configName] + if !ok { + return false, ErrNotAvailable + } + + imapPort := s.GetInt(settings.IMAPPortKey) + imapSSL := false + smtpPort := s.GetInt(settings.SMTPPortKey) + smtpSSL := s.GetBool(settings.SMTPSSLKey) + + if address == "" { + address = user.GetPrimaryAddress() + } + + if configName == AppleMailClient { + // If configuring apple mail for Catalina or newer, users should use SSL. + needRestart = false + if !smtpSSL && useragent.IsCatalinaOrNewer() { + smtpSSL = true + s.SetBool(settings.SMTPSSLKey, true) + log.Warn("Detected Catalina or newer with bad SMTP SSL settings, now using SSL, bridge needs to restart") + needRestart = true + } + } + + return needRestart, config.Configure(imapPort, smtpPort, imapSSL, smtpSSL, user, address) +} diff --git a/internal/frontend/clientconfig/config_applemail.go b/internal/frontend/clientconfig/config_applemail.go new file mode 100644 index 00000000..561817b0 --- /dev/null +++ b/internal/frontend/clientconfig/config_applemail.go @@ -0,0 +1,121 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// +build darwin + +package clientconfig + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/ProtonMail/proton-bridge/internal/bridge" + "github.com/ProtonMail/proton-bridge/internal/config/useragent" + "github.com/ProtonMail/proton-bridge/internal/frontend/types" + "github.com/ProtonMail/proton-bridge/pkg/mobileconfig" +) + +const ( + bigSurPreferncesPane = "/System/Library/PreferencePanes/Profiles.prefPane" +) + +func init() { //nolint[gochecknoinit] + available[AppleMailClient] = &appleMail{} +} + +type appleMail struct{} + +func (c *appleMail) Name() string { return AppleMailClient } + +func (c *appleMail) Configure(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.User, address string) error { + mc := prepareMobileConfig(imapPort, smtpPort, imapSSL, smtpSSL, user, address) + + confPath, err := saveConfigTemporarily(mc) + if err != nil { + return err + } + + if useragent.IsBigSurOrNewer() { + return exec.Command("open", bigSurPreferncesPane, confPath).Run() //nolint[gosec] G204: open command is safe, mobileconfig is generated by us + } + + return exec.Command("open", confPath).Run() //nolint[gosec] G204: open command is safe, mobileconfig is generated by us +} + +func prepareMobileConfig(imapPort, smtpPort int, imapSSL, smtpSSL bool, user types.User, address string) *mobileconfig.Config { + displayName := address + addresses := address + + if user.IsCombinedAddressMode() { + displayName = user.GetPrimaryAddress() + addresses = strings.Join(user.GetAddresses(), ",") + } + + timestamp := strconv.FormatInt(time.Now().Unix(), 10) + + return &mobileconfig.Config{ + EmailAddress: addresses, + DisplayName: displayName, + Identifier: "protonmail " + displayName + timestamp, + IMAP: &mobileconfig.IMAP{ + Hostname: bridge.Host, + Port: imapPort, + TLS: imapSSL, + Username: displayName, + Password: user.GetBridgePassword(), + }, + SMTP: &mobileconfig.SMTP{ + Hostname: bridge.Host, + Port: smtpPort, + TLS: smtpSSL, + Username: displayName, + }, + } +} + +func saveConfigTemporarily(mc *mobileconfig.Config) (fname string, err error) { + dir, err := ioutil.TempDir("", "protonmail-autoconfig") + if err != nil { + return + } + + // Make sure the temporary file is deleted. + go (func() { + <-time.After(10 * time.Minute) + _ = os.RemoveAll(dir) + })() + + // Make sure the file is only readable for the current user. + fname = filepath.Clean(filepath.Join(dir, "protonmail.mobileconfig")) + f, err := os.OpenFile(fname, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + return + } + + if err = mc.WriteOut(f); err != nil { + _ = f.Close() + return + } + _ = f.Close() + + return +} diff --git a/internal/frontend/frontend.go b/internal/frontend/frontend.go index 4f7ad05a..cf15eedf 100644 --- a/internal/frontend/frontend.go +++ b/internal/frontend/frontend.go @@ -24,6 +24,7 @@ import ( "github.com/ProtonMail/proton-bridge/internal/config/settings" "github.com/ProtonMail/proton-bridge/internal/config/useragent" "github.com/ProtonMail/proton-bridge/internal/frontend/cli" + "github.com/ProtonMail/proton-bridge/internal/frontend/qt" "github.com/ProtonMail/proton-bridge/internal/frontend/types" "github.com/ProtonMail/proton-bridge/internal/locations" "github.com/ProtonMail/proton-bridge/internal/updater" @@ -59,6 +60,23 @@ func New( ) Frontend { bridgeWrap := types.NewBridgeWrap(bridge) switch frontendType { + case "qt": + return qt.New( + version, + buildVersion, + programName, + showWindowOnStart, + panicHandler, + locations, + settings, + eventListener, + updater, + userAgent, + bridgeWrap, + noEncConfirmator, + autostart, + restarter, + ) case "cli": return cli.New( panicHandler, diff --git a/internal/frontend/qml/AccountDelegate.qml b/internal/frontend/qml/AccountDelegate.qml index 4782e2ef..011565b7 100644 --- a/internal/frontend/qml/AccountDelegate.qml +++ b/internal/frontend/qml/AccountDelegate.qml @@ -27,12 +27,61 @@ Item { property ColorScheme colorScheme property var user - implicitHeight: children[0].implicitHeight - implicitWidth: children[0].implicitWidth + property var _spacing: 12 + property var _leftRightMargins: { + switch(root.type) { + case AccountDelegate.SmallView: return 12 + case AccountDelegate.LargeView: return 0 + } + } + property var _topBottomMargins: { + switch(root.type) { + case AccountDelegate.SmallView: return 10 + case AccountDelegate.LargeView: return 0 + } + } + + property color usedSpaceColor : { + if (!root.enabled) return root.colorScheme.text_weak + if (root.type == AccountDelegate.SmallView) return root.colorScheme.text_weak + if (root.usedFraction < .50) return root.colorScheme.signal_success + if (root.usedFraction < .75) return root.colorScheme.signal_warning + return root.colorScheme.signal_danger + } + property real usedFraction: root.user.totalBytes ? root.user.usedBytes / root.user.totalBytes : 0 + property string totalSpace: root.spaceWithUnits(root.user.totalBytes) + property string usedSpace: root.spaceWithUnits(root.user.usedBytes) + + function spaceWithUnits(bytes){ + if (bytes*1 !== bytes ) return "0 kB" + var units = ['B',"kB", "MB", "TB"]; + var i = parseInt(Math.floor(Math.log(bytes)/Math.log(1024))); + + return Math.round(bytes*10 / Math.pow(1024, i))/10 + " " + units[i] + } + + signal clicked() + + // width expected to be set by parent object + implicitHeight : children[0].implicitHeight + 2*root._topBottomMargins + + enum ViewType{ + SmallView, LargeView + } + property var type : AccountDelegate.SmallView RowLayout { - anchors.fill: parent - spacing: 12 + spacing: root._spacing + + anchors { + top: root.top + left: root.left + right: root.rigth + leftMargin : root._leftRightMargins + rightMargin : root._leftRightMargins + topMargin : root._topBottomMargins + bottomMargin : root._topBottomMargins + } Rectangle { id: avatar @@ -48,8 +97,19 @@ Item { colorScheme: root.colorScheme anchors.fill: parent text: root.user.avatarText.toUpperCase() - type: Label.LabelType.Body - color: root.colorScheme.text_invert + type: { + switch (root.type) { + case AccountDelegate.SmallView: return Label.Body + case AccountDelegate.LargeView: return Label.Title + } + } + font.weight: Font.Normal + color: { + switch(root.type) { + case AccountDelegate.SmallView: return root.colorScheme.text_norm + case AccountDelegate.LargeView: return root.colorScheme.text_invert + } + } horizontalAlignment: Qt.AlignHCenter verticalAlignment: Qt.AlignVCenter } @@ -63,16 +123,78 @@ Item { spacing: 0 Label { + Layout.maximumWidth: root.width - ( + root._spacing + avatar.width + 2*root._leftRightMargins + ) + colorScheme: root.colorScheme text: user.username - type: Label.LabelType.Body + type: { + switch (root.type) { + case AccountDelegate.SmallView: return Label.Body + case AccountDelegate.LargeView: return Label.Title + } + } + elide: Text.ElideMiddle } - Label { - colorScheme: root.colorScheme - text: user.captionText - type: Label.LabelType.Caption + Item { implicitHeight: root.type == AccountDelegate.LargeView ? 6 : 0 } + + RowLayout { + Label { + colorScheme: root.colorScheme + text: root.usedSpace + color: root.usedSpaceColor + type: { + switch (root.type) { + case AccountDelegate.SmallView: return Label.Caption + case AccountDelegate.LargeView: return Label.Body + } + } + } + + Label { + colorScheme: root.colorScheme + text: " / " + root.totalSpace + color: root.colorScheme.text_weak + type: { + switch (root.type) { + case AccountDelegate.SmallView: return Label.Caption + case AccountDelegate.LargeView: return Label.Body + } + } + } + } + + + Rectangle { + visible: root.type == AccountDelegate.LargeView + + width: 140 + height: 4 + radius: 3 + color: root.colorScheme.border_weak + + Rectangle { + radius: 3 + color: root.usedSpaceColor + anchors { + top : parent.top + bottom : parent.bottom + left : parent.left + } + width: Math.min(1,Math.max(0.02,root.usedFraction)) * parent.width + } } } + + Item { + Layout.fillWidth: true + } + } + + MouseArea { + anchors.fill: root + onClicked: root.clicked() } } diff --git a/internal/frontend/qml/AccountView.qml b/internal/frontend/qml/AccountView.qml index 66f87424..d1e11044 100644 --- a/internal/frontend/qml/AccountView.qml +++ b/internal/frontend/qml/AccountView.qml @@ -21,34 +21,245 @@ import QtQuick.Controls 2.12 import Proton 4.0 -Item { +ScrollView { id: root property ColorScheme colorScheme + property var backend + property var notifications + property var user - implicitHeight: children[0].implicitHeight - implicitWidth: children[0].implicitWidth + clip: true + contentWidth: pane.width + contentHeight: pane.height + + property int _leftRightMargins: 64 + property int _topBottomMargins: 68 + property int _spacing: 22 + + Rectangle { + anchors { + bottom: pane.bottom + } + color: root.colorScheme.background_weak + width: root.width + height: configuration.height + root._topBottomMargins + } + + signal showSignIn() + signal showSetupGuide(var user, string address) ColumnLayout { - anchors.fill: parent - spacing: 0 + id: pane - Rectangle { - Layout.fillWidth: true - Layout.minimumHeight: 277 - Layout.maximumHeight: 277 + width: root.width - color: root.colorScheme.background_norm + ColumnLayout { + spacing: root._spacing + Layout.topMargin: root._topBottomMargins + Layout.leftMargin: root._leftRightMargins + Layout.rightMargin: root._leftRightMargins + Layout.maximumWidth: root.width - 2*root._leftRightMargins - ColumnLayout { + RowLayout { // account delegate with action buttons + Layout.fillWidth: true + AccountDelegate { + Layout.fillWidth: true + colorScheme: root.colorScheme + user: root.user + type: AccountDelegate.LargeView + enabled: root.user.loggedIn + } + + Button { + Layout.alignment: Qt.AlignTop + colorScheme: root.colorScheme + text: qsTr("Sign out") + secondary: true + visible: root.user.loggedIn + onClicked: root.user.logout() + } + + Button { + Layout.alignment: Qt.AlignTop + colorScheme: root.colorScheme + icon.source: "icons/ic-trash.svg" + secondary: true + visible: root.user.loggedIn + onClicked: root.user.remove() + } + + Button { + Layout.alignment: Qt.AlignTop + colorScheme: root.colorScheme + text: qsTr("Sign in") + secondary: true + visible: !root.user.loggedIn + onClicked: root.parent.rightContent.showSignIn() + } } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: root.colorScheme.border_weak + } + + SettingsItem { + colorScheme: root.colorScheme + text: qsTr("Email clients") + actionText: qsTr("Configure") + description: "MISSING WIREFRAME" // TODO + type: SettingsItem.Button + enabled: root.user.loggedIn + visible: !root.user.splitMode + onClicked: root.showSetupGuide(root.user,user.addresses[0]) + } + + SettingsItem { + id: splitMode + colorScheme: root.colorScheme + text: qsTr("Split addresses") + description: qsTr("Split addresses allows you to configure multiple email addresses individually. Changing its mode will require you to delete your accounts(s) from your email client and begin the setup process from scratch.") + type: SettingsItem.Toggle + checked: root.user.splitMode + visible: root.user.addresses.length > 1 + enabled: root.user.loggedIn + onClicked: { + if (!splitMode.checked){ + root.notifications.askEnableSplitMode(user) + } else { + root.user.toggleSplitMode(!splitMode.checked) + } + } + } + + RowLayout { + Layout.fillWidth: true + enabled: root.user.loggedIn + + visible: root.user.splitMode + + ComboBox { + id: addressSelector + Layout.fillWidth: true + model: root.user.addresses + + property var _topBottomMargins : 8 + property var _leftRightMargins : 16 + + background: RoundedRectangle { + radiusTopLeft : 6 + radiusTopRight : 6 + radiusBottomLeft : addressSelector.down ? 0 : 6 + radiusBottomRight : addressSelector.down ? 0 : 6 + + height: addressSelector.contentItem.height + //width: addressSelector.contentItem.width + + fillColor : root.colorScheme.background_norm + strokeColor : root.colorScheme.border_norm + strokeWidth : 1 + } + + delegate: Rectangle { + id: listItem + width: root.width + height: children[0].height + 4 + 2*addressSelector._topBottomMargins + + Label { + anchors { + top : parent.top + left : parent.left + topMargin : addressSelector._topBottomMargins + 4 + leftMargin : addressSelector._leftRightMargins + } + + colorScheme: root.colorScheme + text: modelData + elide: Text.ElideMiddle + } + + property bool isOver: false + color: { + if (listItem.isOver) return root.colorScheme.interaction_weak_hover + if (addressSelector.highlightedIndex === index) return root.colorScheme.interaction_weak + return root.colorScheme.background_norm + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: listItem.isOver = true + onExited: listItem.isOver = false + onClicked : { + addressSelector.currentIndex = index + addressSelector.popup.close() + } + } + } + + contentItem: Label { + topPadding : addressSelector._topBottomMargins+4 + bottomPadding : addressSelector._topBottomMargins + leftPadding : addressSelector._leftRightMargins + rightPadding : addressSelector._leftRightMargins + + colorScheme: root.colorScheme + text: addressSelector.displayText + elide: Text.ElideMiddle + } + } + + Button { + colorScheme: root.colorScheme + text: qsTr("Configure") + secondary: true + onClicked: root.showSetupGuide(root.user, addressSelector.displayText) + } + } + + Item {implicitHeight: 1} } - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true + ColumnLayout { + id: configuration + Layout.bottomMargin: root._topBottomMargins + Layout.leftMargin: root._leftRightMargins + Layout.rightMargin: root._leftRightMargins + Layout.maximumWidth: root.width - 2*root._leftRightMargins + spacing: root._spacing + visible: root.user.loggedIn - color: root.colorScheme.background_weak + property string currentAddress: addressSelector.displayText + + Item {height: 1} + + Label { + colorScheme: root.colorScheme + text: qsTr("Mailbox details") + type: Label.Body_semibold + } + + Configuration { + colorScheme: root.colorScheme + title: qsTr("IMAP") + hostname: root.backend.hostname + port: root.backend.portIMAP.toString() + username: configuration.currentAddress + password: root.user.password + security: "STARTTLS" + } + + Configuration { + colorScheme: root.colorScheme + title: qsTr("SMTP") + hostname : root.backend.hostname + port : root.backend.portSMTP.toString() + username : configuration.currentAddress + password : root.user.password + security : root.backend.useSSLforSMTP ? "SSL" : "STARTTLS" + } } } } diff --git a/internal/frontend/qml/Banner.qml b/internal/frontend/qml/Banner.qml index 4a6aaea8..7cb26c55 100644 --- a/internal/frontend/qml/Banner.qml +++ b/internal/frontend/qml/Banner.qml @@ -28,9 +28,13 @@ Popup { property ColorScheme colorScheme property Notification notification + property var mainWindow + + topMargin: 37 + leftMargin: (mainWindow.width - root.implicitWidth)/2 implicitHeight: contentLayout.implicitHeight + contentLayout.anchors.topMargin + contentLayout.anchors.bottomMargin - implicitWidth: contentLayout.implicitWidth + contentLayout.anchors.leftMargin + contentLayout.anchors.rightMargin + implicitWidth: 600 // contentLayout.implicitWidth + contentLayout.anchors.leftMargin + contentLayout.anchors.rightMargin popupType: ApplicationWindow.PopupType.Banner @@ -74,13 +78,13 @@ Popup { } switch (root.notification.type) { - case Notification.NotificationType.Info: + case Notification.NotificationType.Info: return root.colorScheme.signal_info - case Notification.NotificationType.Success: + case Notification.NotificationType.Success: return root.colorScheme.signal_success - case Notification.NotificationType.Warning: + case Notification.NotificationType.Warning: return root.colorScheme.signal_warning - case Notification.NotificationType.Danger: + case Notification.NotificationType.Danger: return root.colorScheme.signal_danger } } @@ -109,13 +113,13 @@ Popup { } switch (root.notification.type) { - case Notification.NotificationType.Info: + case Notification.NotificationType.Info: return "./icons/ic-info-circle-filled.svg" - case Notification.NotificationType.Success: + case Notification.NotificationType.Success: return "./icons/ic-info-circle-filled.svg" - case Notification.NotificationType.Warning: + case Notification.NotificationType.Warning: return "./icons/ic-exclamation-circle-filled.svg" - case Notification.NotificationType.Danger: + case Notification.NotificationType.Danger: return "./icons/ic-exclamation-circle-filled.svg" } } @@ -145,13 +149,13 @@ Popup { } switch (root.notification.type) { - case Notification.NotificationType.Info: + case Notification.NotificationType.Info: return root.colorScheme.signal_info_active - case Notification.NotificationType.Success: + case Notification.NotificationType.Success: return root.colorScheme.signal_success_active - case Notification.NotificationType.Warning: + case Notification.NotificationType.Warning: return root.colorScheme.signal_warning_active - case Notification.NotificationType.Danger: + case Notification.NotificationType.Danger: return root.colorScheme.signal_danger_active } } @@ -183,22 +187,22 @@ Popup { var active switch (root.notification.type) { - case Notification.NotificationType.Info: + case Notification.NotificationType.Info: norm = root.colorScheme.signal_info hover = root.colorScheme.signal_info_hover active = root.colorScheme.signal_info_active break; - case Notification.NotificationType.Success: + case Notification.NotificationType.Success: norm = root.colorScheme.signal_success hover = root.colorScheme.signal_success_hover active = root.colorScheme.signal_success_active break; - case Notification.NotificationType.Warning: + case Notification.NotificationType.Warning: norm = root.colorScheme.signal_warning hover = root.colorScheme.signal_warning_hover active = root.colorScheme.signal_warning_active break; - case Notification.NotificationType.Danger: + case Notification.NotificationType.Danger: norm = root.colorScheme.signal_danger hover = root.colorScheme.signal_danger_hover active = root.colorScheme.signal_danger_active diff --git a/internal/frontend/qml/Bridge.qml b/internal/frontend/qml/Bridge.qml index 7fba4a98..11c1151e 100644 --- a/internal/frontend/qml/Bridge.qml +++ b/internal/frontend/qml/Bridge.qml @@ -25,12 +25,7 @@ import Notifications 1.0 QtObject { id: root - property var backend - - signal login(string username, string password) - signal login2FA(string username, string code) - signal login2Password(string username, string password) - signal loginAbort(string username) + property var backend: go property Notifications _notifications: Notifications { id: notifications @@ -45,19 +40,23 @@ QtObject { visible: false backend: root.backend - notifications: notifications + notifications: root._notifications onLogin: { - root.login(username, password) + backend.login(username, password) } onLogin2FA: { - root.login2FA(username, code) + backend.login2FA(username, code) } onLogin2Password: { - root.login2Password(username, password) + backend.login2Password(username, password) } onLoginAbort: { - root.loginAbort(username) + backend.loginAbort(username) + } + + onVisibleChanged: { + backend.dockIconVisible = visible } } @@ -66,20 +65,45 @@ QtObject { visible: false backend: root.backend - notifications: notifications + notifications: root._notifications + + property var x_center: 10 + property var x_min: 0 + property var x_max: 100 + property var y_center: 1000 + property var y_min: 0 + property var y_max: 10000 + + x: bound(x_center,x_min, x_max-statusWindow.width) + y: bound(y_center,y_min, y_max-statusWindow.height) + onShowMainWindow: { mainWindow.visible = true } + onShowHelp: { - + mainWindow.showHelp() + mainWindow.visible = true } + onShowSettings: { - + mainWindow.showSettings() + mainWindow.visible = true } + + onShowSignIn: { + mainWindow.showSignIn(username) + mainWindow.visible = true + } + onQuit: { backend.quit() } + + function bound(num, lower_limit, upper_limit) { + return Math.max(lower_limit, Math.min(upper_limit, num)) + } } property SystemTrayIcon _trayIcon: SystemTrayIcon { @@ -88,103 +112,59 @@ QtObject { iconSource: "./icons/ic-systray.svg" onActivated: { function calcStatusWindowPosition(statusWidth, statusHeight) { - function bound(num, lower_limit, upper_limit) { - return Math.max(lower_limit, Math.min(upper_limit, num)) + function isInInterval(num, lower_limit, upper_limit) { + return lower_limit <= num && num <= upper_limit } - // checks if rect1 fits within rect2 - function isRectFit(rect1, rect2) { - //if (rect2.) - if ((rect2.left > rect1.left) || - (rect2.right < rect1.right) || - (rect2.top > rect1.top) || - (rect2.bottom < rect1.bottom)) { - return false - } - return true - } // First we get icon center position. // On some platforms (X11 / Wayland) Qt does not provide icon geometry info. // In this case we rely on cursor position + var iconWidth = geometry.width *1.2 + var iconHeight = geometry.height *1.2 var iconCenter = Qt.point(geometry.x + (geometry.width / 2), geometry.y + (geometry.height / 2)) - if (geometry.width == 0 && geometry.height == 0) { iconCenter = backend.getCursorPos() + // fallback: simple guess, no data to estimate + iconWidth = 25 + iconHeight = 25 } - // Now bound this position to virtual screen available rect - // TODO: here we should detect which screen mouse is on and use that screen available geometry to bound - iconCenter.x = bound(iconCenter.x, 0, Qt.application.screens[0].desktopAvailableWidth) - iconCenter.y = bound(iconCenter.y, 0, Qt.application.screens[0].desktopAvailableHeight) + // Find screen + var screen = Qt.application.screens[0] - var x = 0 - var y = 0 - - // Check if window may fit above - x = iconCenter.x - statusWidth / 2 - y = iconCenter.y - statusHeight - if (isRectFit( - Qt.rect(x, y, statusWidth, statusHeight), - // TODO: we should detect which screen mouse is on and use that screen available geometry to bound - Qt.rect(0, 0, Qt.application.screens[0].desktopAvailableWidth, Qt.application.screens[0].desktopAvailableHeight) - )) { - return Qt.point(x, y) + for (var i in Qt.application.screens) { + screen = Qt.application.screens[i] + if ( + isInInterval(iconCenter.x, screen.virtualX, screen.virtualX+screen.width) && + isInInterval(iconCenter.y, screen.virtualY, screen.virtualY+screen.heigh) + ) { + return + } } - // Check if window may fit below - x = iconCenter.x - statusWidth / 2 - y = iconCenter.y - if (isRectFit( - Qt.rect(x, y, statusWidth, statusHeight), - // TODO: we should detect which screen mouse is on and use that screen available geometry to bound - Qt.rect(0, 0, Qt.application.screens[0].desktopAvailableWidth, Qt.application.screens[0].desktopAvailableHeight) - )) { - return Qt.point(x, y) - } - - // Check if window may fit to the left - x = iconCenter.x - statusWidth - y = iconCenter.y - statusHeight / 2 - if (isRectFit( - Qt.rect(x, y, statusWidth, statusHeight), - // TODO: we should detect which screen mouse is on and use that screen available geometry to bound - Qt.rect(0, 0, Qt.application.screens[0].desktopAvailableWidth, Qt.application.screens[0].desktopAvailableHeight) - )) { - return Qt.point(x, y) - } - - // Check if window may fit to the right - x = iconCenter.x - y = iconCenter.y - statusHeight / 2 - if (isRectFit( - Qt.rect(x, y, statusWidth, statusHeight), - // TODO: we should detect which screen mouse is on and use that screen available geometry to bound - Qt.rect(0, 0, Qt.application.screens[0].desktopAvailableWidth, Qt.application.screens[0].desktopAvailableHeight) - )) { - return Qt.point(x, y) - } - - // TODO: add fallback + // Calculate allowed square where status window top left corner can be positioned + statusWindow.x_center = iconCenter.x + statusWindow.y_center = iconCenter.y + statusWindow.x_min = screen.virtualX + iconWidth + statusWindow.x_max = screen.virtualX + screen.width - iconWidth + statusWindow.y_min = screen.virtualY + iconHeight + statusWindow.y_max = screen.virtualY + screen.height - iconHeight } switch (reason) { - case SystemTrayIcon.Unknown: + case SystemTrayIcon.Unknown: break; - case SystemTrayIcon.Context: - case SystemTrayIcon.Trigger:!statusWindow.visible - if (!statusWindow.visible) { - var point = calcStatusWindowPosition(statusWindow.width, statusWindow.height) - statusWindow.x = point.x - statusWindow.y = point.y - } + case SystemTrayIcon.Context: + case SystemTrayIcon.Trigger: + calcStatusWindowPosition() statusWindow.visible = !statusWindow.visible break - case SystemTrayIcon.DoubleClick: - case SystemTrayIcon.MiddleClick: + case SystemTrayIcon.DoubleClick: + case SystemTrayIcon.MiddleClick: mainWindow.visible = !mainWindow.visible break; - default: + default: break; } } diff --git a/internal/frontend/qml/BridgeTest/UserControl.qml b/internal/frontend/qml/BridgeTest/UserControl.qml index 881f8695..911abc11 100644 --- a/internal/frontend/qml/BridgeTest/UserControl.qml +++ b/internal/frontend/qml/BridgeTest/UserControl.qml @@ -236,6 +236,53 @@ ColumnLayout { } } + Button { + colorScheme: root.colorScheme + text: "Login Finished" + + onClicked: { + root.backend.loginFinished() + user.resetLoginRequests() + } + } + + RowLayout { + TextField { + colorScheme: root.colorScheme + label: "used:" + text: user && user.usedBytes ? user.usedBytes : 0 + validator: DoubleValidator {bottom: 1; top: 1024*1024*1024*1024*1024} + onEditingFinished: { + user.usedBytes = parseFloat(text) + } + implicitWidth: 200 + } + TextField { + colorScheme: root.colorScheme + label: "total:" + text: user && user.totalBytes ? user.totalBytes : 0 + validator: DoubleValidator {bottom: 1; top: 1024*1024*1024*1024*1024} + onEditingFinished: { + user.totalBytes = parseFloat(text) + } + implicitWidth: 200 + } + } + + RowLayout { + Label {colorScheme: root.colorScheme; text: "Split mode"} + Toggle { colorScheme: root.colorScheme; checked: user ? user.splitMode : false; onClicked: {user.splitMode = !user.splitMode}} + Button { colorScheme: root.colorScheme; text: "Toggle Finished"; onClicked: {user.toggleSplitModeFinished()}} + } + + TextArea { + colorScheme: root.colorScheme + text: user && user.addresses ? user.addresses.join("\n") : "user@protonmail.com" + Layout.fillWidth: true + onEditingFinished: { + user.addresses = text.split("\n") + } + } Item { Layout.fillHeight: true diff --git a/internal/frontend/qml/Bridge_test.qml b/internal/frontend/qml/Bridge_test.qml index d81e0b1a..03a8f1bd 100644 --- a/internal/frontend/qml/Bridge_test.qml +++ b/internal/frontend/qml/Bridge_test.qml @@ -33,8 +33,10 @@ import Notifications 1.0 Window { id: root - width: 640 - height: 480 + x: 10 + y: 10 + width: 800 + height: 600 property ColorScheme colorScheme: ProtonStyle.darkStyle @@ -103,12 +105,21 @@ Window { QtObject { property string username: "" property bool loggedIn: false + property bool splitMode: false property bool setupGuideSeen: true - property string captionText: "50.3 MB / 20 GB" + property var usedBytes: 5350*1024*1024 + property var totalBytes: 20*1024*1024*1024 property string avatarText: "jd" + property string password: "SMj975NnEYYsqu55GGmlpv" + property var addresses: [ + "janedoe@protonmail.com", + "jane@pm.me", + "jdoe@pm.me" + ] + signal loginUsernamePasswordError() signal loginFreeUserError() signal loginConnectionError() @@ -130,6 +141,30 @@ Window { root.log("<- User (" + username + "): " + msg) } + function toggleSplitMode(makeActive) { + userSignal("toggle split mode "+makeActive) + } + signal toggleSplitModeFinished() + + function configureAppleMail(address){ + userSignal("confugure apple mail "+address) + } + + function logout(){ + userSignal("logout") + loggedIn = false + } + function remove(){ + console.log("remove this", users.count) + for (var i=0; i disk cache", enableDiskCache, diskCachePath) + } + signal changeLocalCacheFinished() + + + // Settings + property bool isAutomaticUpdateOn : true + function toggleAutomaticUpdate(makeItActive) { + console.debug("-> silent updates", makeItActive, root.isAutomaticUpdateOn) + root.isAutomaticUpdateOn = makeItActive + } + + property bool isAutostartOn : true // Example of settings with loading state + function toggleAutostart(makeItActive) { + console.debug("-> autostart", makeItActive, root.isAutomaticUpdateOn) + } + signal toggleAutostartFinished() + + property bool isBetaEnabled : false + function toggleBeta(makeItActive){ + console.debug("-> beta", makeItActive, root.isBetaEnabled) + root.isBetaEnabled = makeItActive + } + + property bool isDoHEnabled : true + function toggleDoH(makeItActive){ + console.debug("-> DoH", makeItActive, root.isDoHEnabled) + root.isDoHEnabled = makeItActive + } + + property bool useSSLforSMTP: false + function toggleUseSSLforSMTP(makeItActive){ + console.debug("-> SMTP SSL", makeItActive, root.useSSLforSMTP) + } + signal toggleUseSSLFinished() + + property string hostname: "127.0.0.1" + property int portIMAP: 1143 + property int portSMTP: 1025 + function changePorts(imapPort, smtpPort){ + console.debug("-> ports", imapPort, smtpPort) + } + function isPortFree(port){ + if (port == portIMAP) return false + if (port == portSMTP) return false + if (port == 12345) return false + return true + } + signal changePortFinished() + signal portIssueIMAP() + signal portIssueSMTP() + + function triggerReset() { + console.debug("-> trigger reset") + } + signal resetFinished() + + property string logsPath: "/home/cuto" // StandardPaths.locate(StandardPaths.DesktopLocation) + property string version: "v2.0.X" + property string licensePath: "/home/cuto" // StandardPaths.locate(StandardPaths.DesktopLocation) + property string releaseNotesLink: "https://protonmail.com/download/bridge/early_releases.html" + + property string currentEmailClient: "" // "Apple Mail 14.0" + function updateCurrentMailClient(){ + currentEmailClient = "Apple Mail 14.0" + } + + function reportBug(description,address,emailClient,includeLogs){ + console.log("report bug") + console.log(" description",description) + console.log(" address",address) + console.log(" emailClient",emailClient) + console.log(" includeLogs",includeLogs) + } + signal reportBugFinished() signal bugReportSendSuccess() signal bugReportSendError() - signal cacheAnavailable() - signal cacheCantMove() + property var availableKeychain: ["gnome-keyring", "pass"] + property string selectedKeychain + function selectKeychain(wantedKeychain){ + selectedKeychain = wantedKeychain + } + signal hasNoKeychain() + + signal noActiveKeyForRecipient(string email) + signal showMainWindow() + + signal addressChanged(string address) + signal addressChangedLogout(string address) + signal userDisconnected(string username) + signal apiCertIssue() + + + + function login(username, password) { + root.log("-> login(" + username + ", " + password + ")") + + loginUser.username = username + loginUser.isLoginRequested = true + } + + function login2FA(username, code) { + root.log("-> login2FA(" + username + ", " + code + ")") + + loginUser.isLogin2FAProvided = true + } + + function login2Password(username, password) { + root.log("-> login2FA(" + username + ", " + password + ")") + + loginUser.isLogin2PasswordProvided = true + } + + function loginAbort(username) { + root.log("-> loginAbort(" + username + ")") + + loginUser.resetLoginRequests() + } - signal diskFull() onLoginUsernamePasswordError: { console.debug("<- loginUsernamePasswordError") @@ -557,6 +812,9 @@ Window { onLogin2PasswordErrorAbort: { console.debug("<- login2PasswordErrorAbort") } + onLoginFinished: { + console.debug("<- loginFinished") + } onInternetOff: { console.debug("<- internetOff") @@ -571,30 +829,6 @@ Window { Bridge { backend: root - onLogin: { - root.log("-> login(" + username + ", " + password + ")") - - loginUser.username = username - loginUser.isLoginRequested = true - } - - onLogin2FA: { - root.log("-> login2FA(" + username + ", " + code + ")") - - loginUser.isLogin2FAProvided = true - } - - onLogin2Password: { - root.log("-> login2FA(" + username + ", " + password + ")") - - loginUser.isLogin2PasswordProvided = true - } - - onLoginAbort: { - root.log("-> loginAbort(" + username + ")") - - loginUser.resetLoginRequests() - } } } diff --git a/internal/frontend/qml/BugReportView.qml b/internal/frontend/qml/BugReportView.qml new file mode 100644 index 00000000..30868de0 --- /dev/null +++ b/internal/frontend/qml/BugReportView.qml @@ -0,0 +1,167 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 + +import Proton 4.0 + +SettingsView { + id: root + + property var selectedAddress + + Label { + text: qsTr("Report a problem") + colorScheme: root.colorScheme + type: Label.Heading + } + + + TextArea { + id: description + property int _minChars: 150 + property bool _inputOK: description.text.length>=description._minChars + + label: qsTr("Description") + colorScheme: root.colorScheme + Layout.fillWidth: true + Layout.minimumHeight: 100 + hint: description.text.length + "/800" + placeholderText: qsTr("Tell us what went wrong or isn't working (min. 150 characters).") + onEditingFinished: { + if (!description._inputOK) { + description.error = true + description.assistiveText = qsTr("Enter a problem description (min. 150 characters)") + } else { + description.error = false + description.assistiveText = "" + } + } + } + + + TextField { + id: address + property bool _inputOK: root.isValidEmail(address.text) + + label: qsTr("Your contact email") + colorScheme: root.colorScheme + Layout.fillWidth: true + placeholderText: qsTr("e.g. jane.doe@protonmail.com") + + onEditingFinished: { + if (!address._inputOK) { + address.error = true + address.assistiveText = qsTr("Enter valid email address") + } else { + address.assistiveText = "" + address.error = false + } + } + } + + TextField { + id: emailClient + property bool _inputOK: emailClient.text.length > 0 + + label: qsTr("Your email client (including version)") + colorScheme: root.colorScheme + Layout.fillWidth: true + placeholderText: qsTr("e.g. Apple Mail 14.0") + onEditingFinished: { + if (!emailClient._inputOK) { + emailClient.assistiveText = qsTr("Enter an email client name and version") + emailClient.error = true + } else { + emailClient.assistiveText = "" + emailClient.error = false + } + } + } + + + RowLayout { + CheckBox { + id: includeLogs + text: qsTr("Include my recent logs") + colorScheme: root.colorScheme + checked: true + } + Button { + Layout.leftMargin: 12 + text: qsTr("View logs") + secondary: true + colorScheme: root.colorScheme + onClicked: Qt.openUrlExternally("file://"+root.backend.logsPath) + } + } + + Label { + text: { + var address = "bridge@protonmail.com" + var mailTo = `${address}` + return qsTr("These reports are not end-to-end encrypted. In case of sensitive information, contact us at %1.").arg(mailTo) + } + colorScheme: root.colorScheme + Layout.fillWidth: true + wrapMode: Text.WordWrap + type: Label.Caption + color: root.colorScheme.text_weak + } + + Button { + id: sendButton + text: qsTr("Send") + colorScheme: root.colorScheme + onClicked: root.submit() + enabled: description._inputOK && address._inputOK && emailClient._inputOK + + Connections {target: root.backend; onReportBugFinished: sendButton.loading = false } + } + + function setDefaultValue() { + description.text = "" + address.text = root.selectedAddress + emailClient.text = root.backend.currentEmailClient + includeLogs.checked = true + } + + function isValidEmail(text){ + var reEmail = /\w+@\w+\.\w+/ + return reEmail.test(text) + } + + function submit() { + sendButton.loading = true + root.backend.reportBug( + description.text, + address.text, + emailClient.text, + includeLogs.checked + ) + } + + Component.onCompleted: root.setDefaultValue() + + + onBack: { + root.setDefaultValue() + root.parent.showHelpView() + } +} diff --git a/internal/frontend/qml/Configuration.qml b/internal/frontend/qml/Configuration.qml new file mode 100644 index 00000000..42521ed2 --- /dev/null +++ b/internal/frontend/qml/Configuration.qml @@ -0,0 +1,73 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 +import QtQuick.Controls.impl 2.12 + +import Proton 4.0 + +Rectangle { + id: root + + property ColorScheme colorScheme + property string title + property string hostname + property string port + property string username + property string password + property string security + + implicitWidth: 304 + implicitHeight: content.height + 2*root._margin + + color: root.colorScheme.background_norm + radius: 9 + + property int _margin: 24 + + ColumnLayout { + id: content + width: root.width - 2*root._margin + anchors{ + top: root.top + left: root.left + leftMargin : root._margin + rightMargin : root._margin + topMargin : root._margin + bottomMargin : root._margin + } + + spacing: 12 + + Label { + colorScheme: root.colorScheme + text: root.title + type: Label.Body_semibold + } + + Item{} + + ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Hostname") ; value: root.hostname } + ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Port") ; value: root.port } + ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Username") ; value: root.username } + ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Password") ; value: root.password } + ConfigurationItem{ colorScheme: root.colorScheme; label: qsTr("Security") ; value: root.security } + } +} + diff --git a/internal/frontend/qml/ConfigurationItem.qml b/internal/frontend/qml/ConfigurationItem.qml new file mode 100644 index 00000000..ed37b4ab --- /dev/null +++ b/internal/frontend/qml/ConfigurationItem.qml @@ -0,0 +1,81 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls.impl 2.12 + +import Proton 4.0 + +ColumnLayout { + id: root + Layout.fillWidth: true + + property var colorScheme + property string label + property string value + + RowLayout { + Layout.fillWidth: true + + ColumnLayout { + Label { + colorScheme: root.colorScheme + text: root.label + type: Label.Body + } + TextEdit { + id: valueText + text: root.value + color: root.colorScheme.text_weak + readOnly: true + selectByMouse: true + selectByKeyboard: true + selectionColor: root.colorScheme.text_weak + } + } + + Item { + Layout.fillWidth: true + } + + ColorImage { + source: "icons/ic-copy.svg" + color: root.colorScheme.text_norm + height: root.colorScheme.body_font_size + + MouseArea { + anchors.fill: parent + onClicked : { + valueText.select(0, valueText.length) + valueText.copy() + valueText.deselect() + } + onPressed: parent.scale = 0.90 + onReleased: parent.scale = 1 + } + + } + } + + + Rectangle { + Layout.fillWidth: true + height: 1 + color: root.colorScheme.border_norm + } +} diff --git a/internal/frontend/qml/ContentWrapper.qml b/internal/frontend/qml/ContentWrapper.qml index 98db99d0..07287042 100644 --- a/internal/frontend/qml/ContentWrapper.qml +++ b/internal/frontend/qml/ContentWrapper.qml @@ -26,12 +26,26 @@ Item { property ColorScheme colorScheme property var backend + property var notifications signal login(string username, string password) signal login2FA(string username, string code) signal login2Password(string username, string password) signal loginAbort(string username) + signal showSetupGuide(var user, string address) + + property var noUser: QtObject { + property var avatarText: "" + property var username: "" + property var password: "" + property var usedBytes: 1 + property var totalBytes: 1 + property var loggedIn: false + property var splitMode: false + property var addresses: [] + } + RowLayout { anchors.fill: parent spacing: 0 @@ -91,6 +105,8 @@ Item { horizontalPadding: 0 icon.source: "./icons/ic-question-circle.svg" + + onClicked: rightContent.showHelpView() } Button { @@ -109,10 +125,14 @@ Item { horizontalPadding: 0 icon.source: "./icons/ic-cog-wheel.svg" + + onClicked: rightContent.showGeneralSettings() } } - // Separator + Item {implicitHeight:10} + + // Separator line Rectangle { Layout.fillWidth: true Layout.minimumHeight: 1 @@ -122,14 +142,20 @@ Item { ListView { id: accounts + + property var _topBottomMargins: 24 + property var _leftRightMargins: 16 + Layout.fillWidth: true Layout.fillHeight: true - Layout.leftMargin: 16 - Layout.rightMargin: 16 - Layout.topMargin: 24 - Layout.bottomMargin: 24 + Layout.leftMargin: accounts._leftRightMargins + Layout.rightMargin: accounts._leftRightMargins + Layout.topMargin: accounts._topBottomMargins + Layout.bottomMargin: accounts._topBottomMargins spacing: 12 + clip: true + boundsBehavior: Flickable.StopAtBounds header: Rectangle { height: headerLabel.height+16 @@ -142,11 +168,28 @@ Item { } } + highlight: Rectangle { + color: leftBar.colorScheme.interaction_default_active + radius: 4 + } + model: root.backend.users delegate: AccountDelegate{ + width: leftBar.width - 2*accounts._leftRightMargins + id: accountDelegate colorScheme: leftBar.colorScheme - user: modelData + user: root.backend.users.get(index) + onClicked: { + var user = root.backend.users.get(index) + accounts.currentIndex = index + if (user.loggedIn) { + rightContent.showAccount() + } else { + signIn.username = user.username + rightContent.showSignIn() + } + } } } @@ -181,15 +224,16 @@ Item { icon.source: "./icons/ic-plus.svg" - onClicked: root.showSignIn() + onClicked: { + signIn.username = "" + rightContent.showSignIn() + } } } } } - Rectangle { - id: rightPlane - + Rectangle { // right content background Layout.fillWidth: true Layout.fillHeight: true @@ -199,14 +243,44 @@ Item { id: rightContent anchors.fill: parent - AccountView { + AccountView { // 0 colorScheme: root.colorScheme + backend: root.backend + notifications: root.notifications + user: { + if (accounts.currentIndex < 0) return root.noUser + if (root.backend.users.count == 0) return root.noUser + return root.backend.users.get(accounts.currentIndex) + } + onShowSignIn: { + signIn.username = this.user.username + rightContent.showSignIn() + } + onShowSetupGuide: { + root.showSetupGuide(user,address) + } } - GridLayout { + GridLayout { // 1 + columns: 2 + + Button { + id: backButton + Layout.leftMargin: 18 + Layout.topMargin: 10 + Layout.alignment: Qt.AlignTop + + colorScheme: root.colorScheme + onClicked: rightContent.showAccount() + icon.source: "icons/ic-arrow-left.svg" + secondary: true + horizontalPadding: 8 + } + SignIn { + id: signIn Layout.topMargin: 68 - Layout.leftMargin: 80 + Layout.leftMargin: 80 - backButton.width - 18 Layout.rightMargin: 80 Layout.bottomMargin: 68 Layout.preferredWidth: 320 @@ -214,21 +288,70 @@ Item { Layout.fillHeight: true colorScheme: root.colorScheme - user: (root.backend.users.count === 1 && root.backend.users.get(0).loggedIn === false) ? root.backend.users.get(0) : undefined backend: root.backend - onLogin : { root.login ( username , password ) } - onLogin2FA : { root.login2FA ( username , code ) } - onLogin2Password : { root.login2Password ( username , password ) } - onLoginAbort : { root.loginAbort ( username ) } + onLogin : { root.backend.login ( username , password ) } + onLogin2FA : { root.backend.login2FA ( username , code ) } + onLogin2Password : { root.backend.login2Password ( username , password ) } + onLoginAbort : { root.backend.loginAbort ( username ) } } } + + GeneralSettings { // 2 + colorScheme: root.colorScheme + backend: root.backend + notifications: root.notifications + } + + PortSettings { // 3 + colorScheme: root.colorScheme + backend: root.backend + } + + SMTPSettings { // 4 + colorScheme: root.colorScheme + backend: root.backend + } + + LocalCacheSettings { // 5 + colorScheme: root.colorScheme + backend: root.backend + notifications: root.notifications + } + + HelpView { // 6 + colorScheme: root.colorScheme + backend: root.backend + } + + BugReportView { // 7 + colorScheme: root.colorScheme + backend: root.backend + selectedAddress: { + if (accounts.currentIndex < 0) return "" + if (root.backend.users.count == 0) return "" + return root.backend.users.get(accounts.currentIndex).addresses[0] + } + } + + function showAccount () { rightContent.currentIndex = 0 } + function showSignIn () { rightContent.currentIndex = 1 } + function showGeneralSettings () { rightContent.currentIndex = 2 } + function showPortSettings () { rightContent.currentIndex = 3 } + function showSMTPSettings () { rightContent.currentIndex = 4 } + function showLocalCacheSettings () { rightContent.currentIndex = 5 } + function showHelpView () { rightContent.currentIndex = 6 } + function showBugReport () { rightContent.currentIndex = 7 } } } } - - function showSignIn() { - rightContent.currentIndex = 1 + function showLocalCacheSettings(){rightContent.showLocalCacheSettings() } + function showSettings(){rightContent.showGeneralSettings() } + function showHelp(){rightContent.showHelpView() } + function showSignIn(username){ + signIn.username = username + rightContent.showSignIn() } + } diff --git a/internal/frontend/qml/GeneralSettings.qml b/internal/frontend/qml/GeneralSettings.qml new file mode 100644 index 00000000..ccedbd15 --- /dev/null +++ b/internal/frontend/qml/GeneralSettings.qml @@ -0,0 +1,168 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.13 +import QtQuick.Controls.impl 2.13 + +import Proton 4.0 + +SettingsView { + id: root + + property bool _isAdvancedShown: false + property var notifications + + Label { + colorScheme: root.colorScheme + text: qsTr("Settings") + type: Label.Heading + Layout.fillWidth: true + } + + SettingsItem { + id: autoUpdate + colorScheme: root.colorScheme + text: qsTr("Automatic updates") + description: qsTr("Bridge will automatically update in the background.") + type: SettingsItem.Toggle + checked: root.backend.isAutomaticUpdateOn + onClicked: root.backend.toggleAutomaticUpdate(!autoUpdate.checked) + } + + SettingsItem { + id: autostart + colorScheme: root.colorScheme + text: qsTr("Automatically start Bridge") + description: qsTr("The app will autostart everytime you reset your device.") + type: SettingsItem.Toggle + checked: root.backend.isAutostartOn + onClicked: { + autostart.loading = true + root.backend.toggleAutostart(!autoUpdate.checked) + } + Connections{ + target: root.backend + onToggleAutostartFinished: { + autostart.loading = false + } + } + } + + SettingsItem { + id: beta + colorScheme: root.colorScheme + text: qsTr("Enable Beta access") + description: qsTr("Be the first one to see new features.") + type: SettingsItem.Toggle + checked: root.backend.isBetaEnabled + onClicked: { + if (!beta.checked) { + root.notifications.askEnableBeta() + } else { + root.notifications.askDisableBeta() + } + } + } + + RowLayout { + ColorImage { + Layout.alignment: Qt.AlignTop + + source: root._isAdvancedShown ? "icons/ic-chevron-up.svg" : "icons/ic-chevron-down.svg" + color: root.colorScheme.interaction_norm + height: root.colorScheme.body_font_size + MouseArea { + anchors.fill: parent + onClicked: root._isAdvancedShown = !root._isAdvancedShown + } + } + + Label { + id: advSettLabel + colorScheme: root.colorScheme + text: qsTr("Advanced settings") + color: root.colorScheme.interaction_norm + type: Label.Body + + MouseArea { + anchors.fill: parent + onClicked: root._isAdvancedShown = !root._isAdvancedShown + } + } + } + + SettingsItem { + id: doh + visible: root._isAdvancedShown + colorScheme: root.colorScheme + text: qsTr("Alternative routing") + description: qsTr("If Proton’s servers are blocked in your location, alternative network routing will be used to reach Proton.") + type: SettingsItem.Toggle + checked: root.backend.isDoHEnabled + onClicked: root.backend.toggleDoH(!doh.checked) + } + + SettingsItem { + id: ports + visible: root._isAdvancedShown + colorScheme: root.colorScheme + text: qsTr("Default ports") + actionText: qsTr("Change") + description: qsTr("Choose which ports are used by default.") + type: SettingsItem.Button + onClicked: root.parent.showPortSettings() + } + + SettingsItem { + id: smtp + visible: root._isAdvancedShown + colorScheme: root.colorScheme + text: qsTr("SMTP connection mode") + actionText: qsTr("Change") + description: qsTr("Change the protocol Bridge and your client use to connect.") + type: SettingsItem.Button + onClicked: root.parent.showSMTPSettings() + } + + SettingsItem { + id: cache + visible: root._isAdvancedShown + colorScheme: root.colorScheme + text: qsTr("Local cache") + actionText: qsTr("Configure") + description: qsTr("Configure Bridge's local cache settings.") + type: SettingsItem.Button + onClicked: root.parent.showLocalCacheSettings() + } + + SettingsItem { + id: reset + visible: root._isAdvancedShown + colorScheme: root.colorScheme + text: qsTr("Reset Bridge") + actionText: qsTr("Reset") + description: qsTr("Remove all accounts, clear cached data, and restore the original settings.") + type: SettingsItem.Button + onClicked: { + root.notifications.askResetBridge() + } + } + + onBack: root.parent.showAccount() +} diff --git a/internal/frontend/qml/HelpView.qml b/internal/frontend/qml/HelpView.qml new file mode 100644 index 00000000..a14f1cb9 --- /dev/null +++ b/internal/frontend/qml/HelpView.qml @@ -0,0 +1,110 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 + +import Proton 4.0 + +SettingsView { + id: root + + Label { + colorScheme: root.colorScheme + text: qsTr("Help") + type: Label.Heading + Layout.fillWidth: true + } + + SettingsItem { + id: setupPage + colorScheme: root.colorScheme + text: qsTr("Installation and setup") + actionText: qsTr("Go to help topics") + actionIcon: "./icons/ic-external-link.svg" + description: qsTr("Get help setting up your client with our instructions and FAQs.") + type: SettingsItem.PrimaryButton + onClicked: {Qt.openUrlExternally("https://protonmail.com/bridge/install")} + } + + SettingsItem { + id: checkUpdates + colorScheme: root.colorScheme + text: qsTr("Updates") + actionText: qsTr("Check now") + description: qsTr("Check that you're using the latest version of Bridge. To stay up to date, enable auto-updates in settings.") + type: SettingsItem.Button + onClicked: { + checkUpdates.loading = true + root.backend.checkUpdates() + } + + Connections {target: root.backend; onCheckUpdatesFinished: checkUpdates.loading = false} + } + + SettingsItem { + id: logs + colorScheme: root.colorScheme + text: qsTr("Logs") + actionText: qsTr("View logs") + description: qsTr("Open and review logs to troubleshoot.") + type: SettingsItem.Button + onClicked: {Qt.openUrlExternally(root.backend.logsPath)} + } + + SettingsItem { + id: reportBug + colorScheme: root.colorScheme + text: qsTr("Report a problem") + actionText: qsTr("Report a problem") + description: qsTr("Something not working as expected? Let us know.") + type: SettingsItem.Button + onClicked: { + root.backend.updateCurrentMailClient() + root.parent.showBugReport() + } + } + + Label { + Layout.alignment: Qt.AlignHCenter + colorScheme: root.colorScheme + type: Label.Caption + color: root.colorScheme.text_weak + textFormat: Text.RichText + linkColor: root.colorScheme.interaction_norm_active + + text: { + var version = root.backend.version + var license = qsTr("License") + var licensePath = root.backend.licensePath + var release= qsTr("Release notes") + var releaseNotesLink = root.backend.releaseNotesLink + return `

Proton Mail Bridge v${version}
+ © 2021 Proton Technologies AG
+ ${license} + ${release} +

` + } + + onLinkActivated: Qt.openUrlExternally(link) + } + + onBack: { + root.parent.showAccount() + } +} diff --git a/internal/frontend/qml/LocalCacheSettings.qml b/internal/frontend/qml/LocalCacheSettings.qml new file mode 100644 index 00000000..8809ff47 --- /dev/null +++ b/internal/frontend/qml/LocalCacheSettings.qml @@ -0,0 +1,146 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.13 +import QtQuick.Controls.impl 2.13 +import QtQuick.Dialogs 1.1 + +import Proton 4.0 + +SettingsView { + id: root + + property var notifications + property bool _diskCacheEnabled: true + property string _diskCachePath: "/home" + + Label { + colorScheme: root.colorScheme + text: qsTr("Local cache") + type: Label.Heading + Layout.fillWidth: true + } + + Label { + colorScheme: root.colorScheme + text: qsTr("Bridge caches your encrypted messages localy to optimise the communication with the local client. Disabling this feature might have a nevative impact on performance.") + type: Label.Body + color: root.colorScheme.text_weak + Layout.fillWidth: true + Layout.maximumWidth: this.parent.Layout.maximumWidth + wrapMode: Text.WordWrap + } + + SettingsItem { + colorScheme: root.colorScheme + text: qsTr("Enable local cache") + description: "When enabled messages are stored on disk." // TODO: wrong text in wireframe + type: SettingsItem.Toggle + checked: root._diskCacheEnabled + onClicked: root._diskCacheEnabled = !root._diskCacheEnabled + } + + SettingsItem { + colorScheme: root.colorScheme + text: qsTr("Current cache location") + actionText: qsTr("Change location") + description: root._diskCachePath + type: SettingsItem.Button + enabled: root._diskCacheEnabled + onClicked: { + pathDialog.open() + } + + FileDialog { + id: pathDialog + title: qsTr("Select cache location") + folder: shortcuts.home + onAccepted: root.sanitizePath(pathDialog.fileUrl.toString()) + selectFolder: true + } + } + + RowLayout { + spacing: 12 + + Button { + id: submitButton + colorScheme: root.colorScheme + text: qsTr("Save and restart") + enabled: ( + root.backend.diskCachePath != root._diskCachePath || + root.backend.isDiskCacheEnabled != root._diskCacheEnabled + ) + onClicked: { + root.submit() + } + } + + Button { + colorScheme: root.colorScheme + text: qsTr("Cancel") + onClicked: root.back() + secondary: true + } + + Connections { + target: root.backend + + onChangeLocalCacheFinished: { + submitButton.loading = false + root.setDefaultValues() + } + } + } + + onBack: { + root.parent.showGeneralSettings() + root.setDefaultValues() + } + + function submit(){ + console.log("submit") + if (!root._diskCacheEnabled && root.backend.isDiskCacheEnabled) { + root.notifications.askDisableLocalCache() + return + } + + if (root._diskCacheEnabled && !root.backend.isDiskCacheEnabled) { + root.notifications.askEnableLocalCache(root._diskCachePath) + return + } + + // Not asking, only changing path + submitButton.loading = true + root.backend.changeLocalCache(root.backend.isDiskCacheEnabled, root._diskCachePath) + } + + function setDefaultValues(){ + root._diskCacheEnabled = root.backend.isDiskCacheEnabled + root._diskCachePath = root.backend.diskCachePath + } + + function sanitizePath(path) { + var pattern = "file://" + if (root.backend.goos=="windows") pattern+="/" + root._diskCachePath = path.replace(pattern, "") + } + + Component.onCompleted: root.setDefaultValues() +} diff --git a/internal/frontend/qml/MainWindow.qml b/internal/frontend/qml/MainWindow.qml index bcf7cf38..d57a5843 100644 --- a/internal/frontend/qml/MainWindow.qml +++ b/internal/frontend/qml/MainWindow.qml @@ -62,7 +62,7 @@ ApplicationWindow { return } - root.showSetup(user) + root.showSetup(user,user.addresses[0]) } onRowsAboutToBeRemoved: { @@ -78,15 +78,6 @@ ApplicationWindow { } } - function showSetup(user) { - setupGuide.user = user - if (setupGuide.user) { - contentLayout._showSetup = true - } else { - contentLayout._showSetup = false - } - } - StackLayout { id: contentLayout @@ -111,12 +102,18 @@ ApplicationWindow { } ContentWrapper { + id: contentWrapper colorScheme: root.colorScheme backend: root.backend + notifications: root.notifications Layout.fillHeight: true Layout.fillWidth: true + onShowSetupGuide: { + root.showSetup(user,address) + } + onLogin: { root.login(username, password) } @@ -161,7 +158,7 @@ ApplicationWindow { Layout.fillWidth: true onDismissed: { - root.showSetup(null) + root.showSetup(null,"") } } } @@ -169,5 +166,25 @@ ApplicationWindow { NotificationPopups { colorScheme: root.colorScheme notifications: root.notifications + mainWindow: root + } + + function showLocalCacheSettings() { contentWrapper.showLocalCacheSettings() } + function showSettings() { contentWrapper.showSettings() } + function showHelp() { contentWrapper.showHelp() } + + function showSignIn(username) { + if (contentLayout.currentIndex == 1) return + contentWrapper.showSignIn(username) + } + + function showSetup(user, address) { + setupGuide.user = user + setupGuide.address = address + if (setupGuide.user) { + contentLayout._showSetup = true + } else { + contentLayout._showSetup = false + } } } diff --git a/internal/frontend/qml/NotificationDialog.qml b/internal/frontend/qml/NotificationDialog.qml index 914ba8f4..79e98962 100644 --- a/internal/frontend/qml/NotificationDialog.qml +++ b/internal/frontend/qml/NotificationDialog.qml @@ -55,13 +55,12 @@ Dialog { } switch (root.notification.type) { - case Notification.NotificationType.Info: - // TODO: Add info icon? - return "" - case Notification.NotificationType.Success: + case Notification.NotificationType.Info: + return "./icons/ic-info.svg" + case Notification.NotificationType.Success: return "./icons/ic-success.svg" - case Notification.NotificationType.Warning: - case Notification.NotificationType.Danger: + case Notification.NotificationType.Warning: + case Notification.NotificationType.Danger: return "./icons/ic-alert.svg" } } @@ -110,6 +109,8 @@ Dialog { action: modelData secondary: index > 0 + + loading: notification.loading } } } diff --git a/internal/frontend/qml/NotificationPopups.qml b/internal/frontend/qml/NotificationPopups.qml index bb9d6433..a98a4095 100644 --- a/internal/frontend/qml/NotificationPopups.qml +++ b/internal/frontend/qml/NotificationPopups.qml @@ -28,6 +28,7 @@ Item { property ColorScheme colorScheme property var notifications + property var mainWindow property int notificationWhitelist: NotificationFilter.FilterConsts.All property int notificationBlacklist: NotificationFilter.FilterConsts.None @@ -42,6 +43,7 @@ Item { Banner { colorScheme: root.colorScheme notification: bannerNotificationFilter.topmost + mainWindow: root.mainWindow } NotificationDialog { @@ -66,17 +68,17 @@ Item { NotificationDialog { colorScheme: root.colorScheme - notification: root.notifications.bugReportSendSuccess + notification: root.notifications.disableBeta } NotificationDialog { colorScheme: root.colorScheme - notification: root.notifications.bugReportSendError + notification: root.notifications.enableBeta } NotificationDialog { colorScheme: root.colorScheme - notification: root.notifications.cacheAnavailable + notification: root.notifications.cacheUnavailable } NotificationDialog { @@ -88,4 +90,24 @@ Item { colorScheme: root.colorScheme notification: root.notifications.diskFull } + + NotificationDialog { + colorScheme: root.colorScheme + notification: root.notifications.enableSplitMode + } + + NotificationDialog { + colorScheme: root.colorScheme + notification: root.notifications.disableLocalCache + } + + NotificationDialog { + colorScheme: root.colorScheme + notification: root.notifications.enableLocalCache + } + + NotificationDialog { + colorScheme: root.colorScheme + notification: root.notifications.resetBridge + } } diff --git a/internal/frontend/qml/Notifications/Notification.qml b/internal/frontend/qml/Notifications/Notification.qml index 5c267b53..3c773021 100644 --- a/internal/frontend/qml/Notifications/Notification.qml +++ b/internal/frontend/qml/Notifications/Notification.qml @@ -39,6 +39,7 @@ QtObject { property bool dismissed: false property bool active: false + property bool loading: false readonly property var occurred: active ? new Date() : undefined property var data diff --git a/internal/frontend/qml/Notifications/Notifications.qml b/internal/frontend/qml/Notifications/Notifications.qml index f3f110bf..c4302dc3 100644 --- a/internal/frontend/qml/Notifications/Notifications.qml +++ b/internal/frontend/qml/Notifications/Notifications.qml @@ -29,6 +29,13 @@ QtObject { property StatusWindow frontendStatus property SystemTrayIcon frontendTray + signal askDisableBeta() + signal askEnableBeta() + signal askEnableSplitMode(var user) + signal askDisableLocalCache() + signal askEnableLocalCache(var path) + signal askResetBridge() + enum Group { Connection = 1, Update = 2, @@ -48,12 +55,20 @@ QtObject { root.updateForceError, root.updateSilentRestartNeeded, root.updateSilentError, + root.updateIsLatestVersion, + root.disableBeta, + root.enableBeta, root.bugReportSendSuccess, root.bugReportSendError, - root.cacheAnavailable, + root.cacheUnavailable, root.cacheCantMove, root.accountChanged, - root.diskFull + root.diskFull, + root.cacheLocationChangeSuccess, + root.enableSplitMode, + root.disableLocalCache, + root.enableLocalCache, + root.resetBridge ] // Connection @@ -93,10 +108,18 @@ QtObject { action: [ Action { - text: qsTr("Update") + text: qsTr("Install update") onTriggered: { - // TODO: call update from backend + root.backend.installUpdate() + root.updateManualReady.active = false + } + }, + Action { + text: qsTr("Update manually") + + onTriggered: { + Qt.openUrlExternally(root.backend.getLandingPage()) root.updateManualReady.active = false } }, @@ -104,7 +127,6 @@ QtObject { text: qsTr("Remind me later") onTriggered: { - // TODO: start timer here root.updateManualReady.active = false } } @@ -128,14 +150,14 @@ QtObject { text: qsTr("Restart Bridge") onTriggered: { - // TODO + root.backend.restart() root.updateManualRestartNeeded.active = false } } } property Notification updateManualError: Notification { - text: qsTr("Bridge couldn’t update") + text: qsTr("Bridge couldn’t update. Please update manually.") icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Warning group: Notifications.Group.Update @@ -147,19 +169,28 @@ QtObject { } } - action: Action { - text: qsTr("Update manually") + action: [ + Action { + text: qsTr("Update manually") - onTriggered: { - // TODO - root.updateManualError.active = false + onTriggered: { + Qt.openUrlExternally(root.backend.getLandingPage()) + root.updateManualError.active = false + } + }, + Action { + text: qsTr("Remind me later") + + onTriggered: { + root.updateManualReady.active = false + } } - } + ] } property Notification updateForce: Notification { text: qsTr("Update to ProtonMail Bridge") + " " + (data ? data.version : "") - description: qsTr("This version of Bridge is no longer supported, please update. Learn why. To update manually, go to: https:/protonmail.com/bridge/download") + description: qsTr("This version of Bridge is no longer supported, please update.") icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Danger group: Notifications.Group.Update | Notifications.Group.Dialogs @@ -175,18 +206,26 @@ QtObject { action: [ Action { - text: qsTr("Update") + text: qsTr("Install update") onTriggered: { - // TODO: trigger update here + root.backend.installUpdate() root.updateForce.active = false } }, Action { - text: qsTr("Quite Bridge") + text: qsTr("Update manually") onTriggered: { - // TODO: quit Bridge here + Qt.openUrlExternally(root.backend.getLandingPage()) + root.updateForce.active = false + } + }, + Action { + text: qsTr("Quit Bridge") + + onTriggered: { + root.backend.quit() root.updateForce.active = false } } @@ -195,7 +234,7 @@ QtObject { property Notification updateForceError: Notification { text: qsTr("Bridge coudn’t update") - description: qsTr("You must update manually. Go to: https:/protonmail.com/bridge/download") + description: qsTr("You must update manually.") icon: "./icons/ic-exclamation-circle-filled.svg" type: Notification.NotificationType.Danger group: Notifications.Group.Update | Notifications.Group.Dialogs @@ -213,15 +252,15 @@ QtObject { text: qsTr("Update manually") onTriggered: { - // TODO: trigger update here + Qt.openUrlExternally(root.backend.getLandingPage()) root.updateForceError.active = false } }, Action { - text: qsTr("Quite Bridge") + text: qsTr("Quit Bridge") onTriggered: { - // TODO: quit Bridge here + root.backend.quit() root.updateForce.active = false } } @@ -245,7 +284,7 @@ QtObject { text: qsTr("Restart Bridge") onTriggered: { - // TODO + root.backend.restart() root.updateSilentRestartNeeded.active = false } } @@ -268,18 +307,105 @@ QtObject { text: qsTr("Update manually") onTriggered: { - // TODO + Qt.openUrlExternally(root.backend.getLandingPage()) root.updateSilentError.active = false } } } + property Notification updateIsLatestVersion: Notification { + text: qsTr("Bridge is up to date") + icon: "./icons/ic-info-circle-filled.svg" + type: Notification.NotificationType.Info + group: Notifications.Group.Update + + Connections { + target: root.backend + onUpdateIsLatestVersion: { + root.updateIsLatestVersion.active = true + } + } + + action: Action { + text: qsTr("Ok") + + onTriggered: { + root.updateIsLatestVersion.active = false + } + } + } + + property Notification disableBeta: Notification { + text: qsTr("Disable beta access?") + description: qsTr("This resets Bridge to the current release and will restart the app. Your preferences, cached data, and email client configurations will be cleared. ") + icon: "./icons/ic-exclamation-circle-filled.svg" + type: Notification.NotificationType.Warning + group: Notifications.Group.Update | Notifications.Group.Dialogs + + Connections { + target: root + onAskDisableBeta: { + root.disableBeta.active = true + } + } + + action: [ + Action { + text: qsTr("Remind me later") + + onTriggered: { + root.disableBeta.active = false + } + }, + Action { + text: qsTr("Disable and restart") + onTriggered: { + root.backend.toggleBeta(false) + root.disableBeta.loading = true + } + } + ] + } + + property Notification enableBeta: Notification { + text: qsTr("Enable beta access?") + description: qsTr("Bridge will update to the latest beta version according to your update preferences. Disabling beta access later on will reset Bridge and require you to reconfigure your client.") + icon: "./icons/ic-info-circle-filled.svg" + type: Notification.NotificationType.Info + group: Notifications.Group.Update | Notifications.Group.Dialogs + + Connections { + target: root + onAskEnableBeta: { + root.enableBeta.active = true + } + } + + action: [ + Action { + text: qsTr("Enable") + onTriggered: { + root.backend.toggleBeta(true) + root.enableBeta.active = false + } + }, + Action { + text: qsTr("Cancel") + + onTriggered: { + root.enableBeta.active = false + } + } + ] + } + + // Bug reports property Notification bugReportSendSuccess: Notification { - text: qsTr("Bug report sent") - description: qsTr("We’ve received your report, thank you! Our team will get back to you as soon as we can.") + text: qsTr("Thank you for the report. We'll get back to you as soon as we can.") + icon: "./icons/ic-info-circle-filled.svg" type: Notification.NotificationType.Success - group: Notifications.Group.Configuration | Notifications.Group.Dialogs + group: Notifications.Group.Configuration Connections { target: root.backend @@ -302,10 +428,10 @@ QtObject { } property Notification bugReportSendError: Notification { - text: qsTr("There was a problem") - description: qsTr("There was a problem with sending your report. Please try again later or contact us directly at security@protonmail.com") - type: Notification.NotificationType.Warning - group: Notifications.Group.Configuration | Notifications.Group.Dialogs + text: qsTr("Report could not be sent. Try again or email us directly.") + icon: "./icons/ic-exclamation-circle-filled.svg" + type: Notification.NotificationType.Danger + group: Notifications.Group.Configuration Connections { target: root.backend @@ -323,7 +449,7 @@ QtObject { } // Cache - property Notification cacheAnavailable: Notification { + property Notification cacheUnavailable: Notification { text: qsTr("Cache location is unavailable") description: qsTr("Check the directory or change it in your settings.") type: Notification.NotificationType.Warning @@ -331,8 +457,8 @@ QtObject { Connections { target: root.backend - onCacheAnavailable: { - root.cacheAnavailable.active = true + onCacheUnavailable: { + root.cacheUnavailable.active = true } } @@ -340,13 +466,15 @@ QtObject { Action { text: qsTr("Quit Bridge") onTriggered: { - root.cacheAnavailable.active = false + root.backend.quit() + root.cacheUnavailable.active = false } }, Action { text: qsTr("Change location") onTriggered: { - root.cacheAnavailable.active = false + root.cacheUnavailable.active = false + root.frontendMain.showLocalCacheSettings() } } ] @@ -376,6 +504,31 @@ QtObject { text: qsTr("Change location") onTriggered: { root.cacheCantMove.active = false + root.frontendMain.showLocalCacheSettings() + } + } + ] + } + + property Notification cacheLocationChangeSuccess: Notification { + text: qsTr("Cache location successfully changed") + icon: "./icons/ic-info-circle-filled.svg" + type: Notification.NotificationType.Success + group: Notifications.Group.Configuration + + Connections { + target: root.backend + onCacheLocationChangeSuccess: { + console.log("notify location changed succesfully") + root.cacheLocationChangeSuccess.active = true + } + } + + action: [ + Action { + text: qsTr("Ok") + onTriggered: { + root.cacheLocationChangeSuccess.active = false } } ] @@ -414,6 +567,7 @@ QtObject { Action { text: qsTr("Quit Bridge") onTriggered: { + root.backend.quit() root.diskFull.active = false } }, @@ -421,6 +575,171 @@ QtObject { text: qsTr("Settings") onTriggered: { root.diskFull.active = false + root.frontendMain.showLocalCacheSettings() + } + } + ] + } + + property Notification enableSplitMode: Notification { + text: qsTr("Enable split mode?") + description: qsTr("Changing between split and combined address mode will require you to delete your accounts(s) from your email client and begin the setup process from scratch.") + type: Notification.NotificationType.Warning + group: Notifications.Group.Configuration | Notifications.Group.Dialogs + + property var user + + Connections { + target: root + onAskEnableSplitMode: { + root.enableSplitMode.user = user + root.enableSplitMode.active = true + } + } + + + Connections { + target: (root && root.enableSplitMode && root.enableSplitMode.user ) ? root.enableSplitMode.user : null + onToggleSplitModeFinished: { + root.enableSplitMode.active = false + root.enableSplitMode.loading = false + } + } + + action: [ + Action { + text: qsTr("Cancel") + onTriggered: { + root.enableSplitMode.active = false + } + }, + Action { + text: qsTr("Enable split mode") + onTriggered: { + root.enableSplitMode.loading = true + root.enableSplitMode.user.toggleSplitMode(true) + } + } + ] + } + + property Notification disableLocalCache: Notification { + text: qsTr("Disable local cache?") + description: qsTr("This action will clear your local cache, including locally stored messages. Bridge will restart.") + type: Notification.NotificationType.Warning + group: Notifications.Group.Configuration | Notifications.Group.Dialogs + + Connections { + target: root + onAskDisableLocalCache: { + root.disableLocalCache.active = true + } + } + + + Connections { + target: root.backend + onChangeLocalCacheFinished: { + root.disableLocalCache.active = false + root.disableLocalCache.loading = false + } + } + + action: [ + Action { + text: qsTr("Cancel") + onTriggered: { + root.disableLocalCache.active = false + } + }, + Action { + text: qsTr("Disable and restart") + onTriggered: { + root.disableLocalCache.loading = true + root.backend.changeLocalCache(false, root.backend.diskCachePath) + } + } + ] + } + + property Notification enableLocalCache: Notification { + text: qsTr("Enable local cache?") + description: qsTr("Bridge will restart.") + type: Notification.NotificationType.Warning + group: Notifications.Group.Configuration | Notifications.Group.Dialogs + + property var path + + Connections { + target: root + onAskEnableLocalCache: { + root.enableLocalCache.active = true + root.enableLocalCache.path = path + } + } + + + Connections { + target: root.backend + onChangeLocalCacheFinished: { + root.enableLocalCache.active = false + root.enableLocalCache.loading = false + } + } + + action: [ + Action { + text: qsTr("Enable and restart") + onTriggered: { + root.enableLocalCache.loading = true + root.backend.changeLocalCache(true, root.enableLocalCache.path) + } + }, + Action { + text: qsTr("Cancel") + onTriggered: { + root.enableLocalCache.active = false + } + } + ] + } + + property Notification resetBridge: Notification { + text: qsTr("Reset Bridge?") + description: qsTr("This will clear your accounts, preferences, and cached data. You will need to reconfigure your email client. Bridge will automatically restart") + type: Notification.NotificationType.Warning + group: Notifications.Group.Configuration | Notifications.Group.Dialogs + + property var user + + Connections { + target: root + onAskResetBridge: { + root.resetBridge.active = true + } + } + + + Connections { + target: root.backend + onResetFinished: { + root.resetBridge.active = false + root.resetBridge.loading = false + } + } + + action: [ + Action { + text: qsTr("Cancel") + onTriggered: { + root.resetBridge.active = false + } + }, + Action { + text: qsTr("Reset and restart") + onTriggered: { + root.resetBridge.loading = true + root.backend.triggerReset() } } ] diff --git a/internal/frontend/qml/PortSettings.qml b/internal/frontend/qml/PortSettings.qml new file mode 100644 index 00000000..cfde2e49 --- /dev/null +++ b/internal/frontend/qml/PortSettings.qml @@ -0,0 +1,154 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.13 +import QtQuick.Controls.impl 2.13 + +import Proton 4.0 + +SettingsView { + id: root + + property bool _valuesOK: !imapField.error && !smtpField.error + property bool _valuesChanged: ( + imapField.text*1 != root.backend.portIMAP || + smtpField.text*1 != root.backend.portSMTP + ) + + Label { + colorScheme: root.colorScheme + text: qsTr("Default ports") + type: Label.Heading + Layout.fillWidth: true + } + + Label { + colorScheme: root.colorScheme + text: qsTr("Changes require reconfiguration of your email client. Bridge will automatically restart.") + type: Label.Body + color: root.colorScheme.text_weak + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + RowLayout { + spacing: 16 + + TextField { + id: imapField + colorScheme: root.colorScheme + label: qsTr("IMAP port") + Layout.preferredWidth: 160 + onEditingFinished: root.validate(imapField) + } + TextField { + id: smtpField + colorScheme: root.colorScheme + label: qsTr("SMTP port") + Layout.preferredWidth: 160 + onEditingFinished: root.validate(smtpField) + } + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: root.colorScheme.border_weak + } + + RowLayout { + spacing: 12 + + Button { + id: submitButton + colorScheme: root.colorScheme + text: qsTr("Save and restart") + enabled: root._valuesOK && root._valuesChanged + onClicked: { + submitButton.loading = true + root.submit() + } + } + + Button { + colorScheme: root.colorScheme + text: qsTr("Cancel") + onClicked: root.back() + secondary: true + } + + Connections { + target: root.backend + + onChangePortFinished: submitButton.loading = false + } + } + + onBack: { + root.parent.showGeneralSettings() + root.setDefaultValues() + } + + function validate(field) { + var num = field.text*1 + if (! (num > 1 && num < 65536) ) { + field.error = true + field.assistiveText = qsTr("Invalid port number.") + return + } + + if (imapField.text == smtpField.text) { + field.error = true + field.assistiveText = qsTr("Port numbers must be different.") + return + } + + field.error = false + field.assistiveText = "" + } + + function isPortFree(field) { + field.error = false + field.assistiveText = "" + + var num = field.text*1 + if (num == root.backend.portIMAP) return true + if (num == root.backend.portSMTP) return true + if (!root.backend.isPortFree(num)) { + field.error = true + field.assistiveText = qsTr("Port occupied.") + submitButton.loading = false + return false + } + } + + function submit(){ + submitButton.loading = true + if (!isPortFree(imapField)) return + if (!isPortFree(smtpField)) return + root.backend.changePorts(imapField.text, smtpField.text) + } + + function setDefaultValues(){ + imapField.text = backend.portIMAP + smtpField.text = backend.portSMTP + } + + Component.onCompleted: root.setDefaultValues() +} diff --git a/internal/frontend/qml/Proton/Button.qml b/internal/frontend/qml/Proton/Button.qml index f1c77c55..4ccb9983 100644 --- a/internal/frontend/qml/Proton/Button.qml +++ b/internal/frontend/qml/Proton/Button.qml @@ -246,9 +246,25 @@ T.Button { } } - border.color: control.colorScheme.border_norm + border.color: { + return control.colorScheme.border_norm + } border.width: secondary && !borderless ? 1 : 0 opacity: control.enabled || control.loading ? 1.0 : 0.5 } + + + Component.onCompleted: { + if (!control.colorScheme) { + console.trace() + var next = root + for (var i = 0; i<1000; i++) { + console.log(i, next, "colorscheme", next.colorScheme) + next = next.parent + if (!next) break + } + console.error("ColorScheme not defined") + } + } } diff --git a/internal/frontend/qml/Proton/RoundedRectangle.qml b/internal/frontend/qml/Proton/RoundedRectangle.qml index a2edbd4c..8c94d5bd 100644 --- a/internal/frontend/qml/Proton/RoundedRectangle.qml +++ b/internal/frontend/qml/Proton/RoundedRectangle.qml @@ -21,7 +21,7 @@ import QtQuick 2.8 Rectangle { id: root - color: Style.transparent + color: "transparent" property color fillColor : Style.currentStyle.background_norm property color strokeColor : Style.currentStyle.background_strong diff --git a/internal/frontend/qml/Proton/TextArea.qml b/internal/frontend/qml/Proton/TextArea.qml index fb4b0a62..4ad302a3 100644 --- a/internal/frontend/qml/Proton/TextArea.qml +++ b/internal/frontend/qml/Proton/TextArea.qml @@ -20,6 +20,7 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Controls.impl 2.12 import QtQuick.Templates 2.12 as T +import "." Item { id: root @@ -86,9 +87,10 @@ Item { property alias wrapMode: control.wrapMode implicitWidth: background.width - implicitHeight: control.implicitHeight + - Math.max(label.implicitHeight + label.anchors.topMargin + label.anchors.bottomMargin, hint.implicitHeight + hint.anchors.topMargin + hint.anchors.bottomMargin) + - assistiveText.implicitHeight + implicitHeight: control.implicitHeight + Math.max( + label.implicitHeight + label.anchors.topMargin + label.anchors.bottomMargin, + hint.implicitHeight + hint.anchors.topMargin + hint.anchors.bottomMargin + ) + assistiveText.implicitHeight property alias label: label.text property alias hint: hint.text @@ -96,6 +98,8 @@ Item { property bool error: false + signal editingFinished() + // Backgroud is moved away from within control as it will be clipped with scrollview Rectangle { id: background @@ -200,12 +204,16 @@ Item { T.TextArea { id: control - implicitWidth: Math.max(contentWidth + leftPadding + rightPadding, - implicitBackgroundWidth + leftInset + rightInset, - placeholder.implicitWidth + leftPadding + rightPadding) - implicitHeight: Math.max(contentHeight + topPadding + bottomPadding, - implicitBackgroundHeight + topInset + bottomInset, - placeholder.implicitHeight + topPadding + bottomPadding) + implicitWidth: Math.max( + contentWidth + leftPadding + rightPadding, + implicitBackgroundWidth + leftInset + rightInset, + placeholder.implicitWidth + leftPadding + rightPadding + ) + implicitHeight: Math.max( + contentHeight + topPadding + bottomPadding, + implicitBackgroundHeight + topInset + bottomInset, + placeholder.implicitHeight + topPadding + bottomPadding + ) padding: 8 leftPadding: 12 @@ -216,6 +224,8 @@ Item { selectionColor: control.palette.highlight selectedTextColor: control.palette.highlightedText + onEditingFinished: root.editingFinished() + cursorDelegate: Rectangle { id: cursor width: 1 diff --git a/internal/frontend/qml/Proton/Toggle.qml b/internal/frontend/qml/Proton/Toggle.qml new file mode 100644 index 00000000..2064b3b3 --- /dev/null +++ b/internal/frontend/qml/Proton/Toggle.qml @@ -0,0 +1,107 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.13 +import QtQuick.Controls.impl 2.13 + +RowLayout{ + id: root + property var colorScheme + property bool checked + property bool disabled + property bool hovered + property bool loading + + signal clicked + + Rectangle { + id: indicator + implicitWidth: 40 + implicitHeight: 24 + + radius: 20 + color: { + if (root.loading) return "transparent" + if (root.disabled) return root.colorScheme.background_strong + return root.colorScheme.background_norm + } + border { + width: 1 + color: (root.disabled || root.loading) ? "transparent" : colorScheme.field_norm + } + + Rectangle { + anchors.verticalCenter: indicator.verticalCenter + anchors.left: indicator.left + anchors.leftMargin: root.checked ? 16 : 0 + width: 24 + height: 24 + radius: 12 + color: { + if (root.loading) return "transparent" + if (root.disabled) return root.colorScheme.field_disabled + + if (root.checked) { + if (root.hovered) return root.colorScheme.interaction_norm_hover + return root.colorScheme.interaction_norm + } else { + if (root.hovered) return root.colorScheme.field_hover + return root.colorScheme.field_norm + } + } + + ColorImage { + anchors.centerIn: parent + source: "../icons/ic-check.svg" + color: root.colorScheme.background_norm + height: root.colorScheme.body_font_size + visible: root.checked + } + } + + ColorImage { + id: loader + anchors.centerIn: parent + source: "../icons/Loader_16.svg" + color: root.colorScheme.text_norm + height: root.colorScheme.body_font_size + visible: root.loading + + RotationAnimation { + target: loader + loops: Animation.Infinite + duration: 1000 + from: 0 + to: 360 + direction: RotationAnimation.Clockwise + running: root.loading + } + } + + MouseArea { + anchors.fill: indicator + hoverEnabled: true + onEntered: {root.hovered = true } + onExited: {root.hovered = false } + onClicked: { root.clicked();} + onPressed: {root.hovered = true } + onReleased: { root.hovered = containsMouse } + } + } +} diff --git a/internal/frontend/qml/Proton/qmldir b/internal/frontend/qml/Proton/qmldir index 4a0b011e..a2942c98 100644 --- a/internal/frontend/qml/Proton/qmldir +++ b/internal/frontend/qml/Proton/qmldir @@ -34,3 +34,4 @@ RoundedRectangle 4.0 RoundedRectangle.qml Switch 4.0 Switch.qml TextArea 4.0 TextArea.qml TextField 4.0 TextField.qml +Toggle 4.0 Toggle.qml diff --git a/internal/frontend/qml/SMTPSettings.qml b/internal/frontend/qml/SMTPSettings.qml new file mode 100644 index 00000000..d03642e9 --- /dev/null +++ b/internal/frontend/qml/SMTPSettings.qml @@ -0,0 +1,120 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.13 +import QtQuick.Controls.impl 2.13 + +import Proton 4.0 + +SettingsView { + id: root + + Label { + colorScheme: root.colorScheme + text: qsTr("SMTP connection mode") + type: Label.Heading + Layout.fillWidth: true + } + + Label { + colorScheme: root.colorScheme + text: qsTr("Changes require reconfiguration of email client. Bridge will automatically restart.") + type: Label.Body + color: root.colorScheme.text_weak + Layout.fillWidth: true + Layout.maximumWidth: this.parent.Layout.maximumWidth + wrapMode: Text.WordWrap + } + + ColumnLayout { + spacing: 16 + + ButtonGroup{ id: protocolSelection } + + Label { + colorScheme: root.colorScheme + text: qsTr("SMTP connection security") + } + + RadioButton { + id: sslButton + colorScheme: root.colorScheme + ButtonGroup.group: protocolSelection + text: qsTr("SSL") + } + + RadioButton { + id: starttlsButton + colorScheme: root.colorScheme + ButtonGroup.group: protocolSelection + text: qsTr("STARTLS") + } + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: root.colorScheme.border_weak + } + + RowLayout { + spacing: 12 + + Button { + id: submitButton + colorScheme: root.colorScheme + text: qsTr("Save and restart") + onClicked: { + submitButton.loading = true + root.submit() + } + } + + Button { + colorScheme: root.colorScheme + text: qsTr("Cancel") + onClicked: root.back() + secondary: true + } + + Connections { + target: root.backend + + onToggleUseSSLFinished: submitButton.loading = false + } + } + + onBack: { + root.parent.showGeneralSettings() + root.setDefaultValues() + } + + function submit(){ + submitButton.loading = true + root.backend.toggleUseSSLforSMTP(sslButton.checked) + } + + function setDefaultValues(){ + sslButton.checked = root.backend.useSSLforSMTP + starttlsButton.checked = !root.backend.useSSLforSMTP + } + + + Component.onCompleted: root.setDefaultValues() +} diff --git a/internal/frontend/qml/SettingsItem.qml b/internal/frontend/qml/SettingsItem.qml new file mode 100644 index 00000000..320f3387 --- /dev/null +++ b/internal/frontend/qml/SettingsItem.qml @@ -0,0 +1,105 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.12 + +import Proton 4.0 + +ColumnLayout { + id: root + property var colorScheme + + property string text: "Text" + property string actionText: "Action" + property string actionIcon: "" + property string description: "Lorem ipsum dolor sit amet" + property var type: SettingsItem.ActionType.Toggle + + property bool checked: true + property bool disabled: false + property bool loading: false + + signal clicked + + spacing: 20 + + Layout.fillWidth: true + Layout.maximumWidth: root.parent.Layout.maximumWidth + + enum ActionType { + Toggle = 1, Button = 2, PrimaryButton = 3 + } + + RowLayout { + Layout.fillWidth: true + + ColumnLayout { + Label { + id:mainLabel + colorScheme: root.colorScheme + text: root.text + type: Label.Body_semibold + } + + Label { + Layout.minimumWidth: mainLabel.width + Layout.maximumWidth: root.Layout.maximumWidth - root.spacing - ( + toggle.visible ? toggle.width : button.width + ) + + wrapMode: Text.WordWrap + colorScheme: root.colorScheme + text: root.description + color: root.colorScheme.text_weak + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } + + Toggle { + id: toggle + colorScheme: root.colorScheme + visible: root.type == SettingsItem.ActionType.Toggle + + checked: root.checked + loading: root.loading + onClicked: { if (!root.loading) root.clicked() } + } + + Button { + id: button + colorScheme: root.colorScheme + visible: root.type == SettingsItem.Button || root.type == SettingsItem.PrimaryButton + text: root.actionText + (root.actionIcon != "" ? " " : "") + loading: root.loading + icon.source: root.actionIcon + onClicked: { if (!root.loading) root.clicked() } + secondary: root.type != SettingsItem.PrimaryButton + } + } + + Rectangle { + Layout.fillWidth: true + color: colorScheme.border_weak + height: 1 + } +} diff --git a/internal/frontend/qml/SettingsView.qml b/internal/frontend/qml/SettingsView.qml new file mode 100644 index 00000000..28f31e90 --- /dev/null +++ b/internal/frontend/qml/SettingsView.qml @@ -0,0 +1,71 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +import QtQuick 2.13 +import QtQuick.Layouts 1.12 +import QtQuick.Controls 2.13 +import QtQuick.Controls.impl 2.13 + +import Proton 4.0 + +ScrollView { + id: root + + property var colorScheme + property var backend + default property alias items: content.children + + signal back() + + property int _leftRightMargins: 64 + property int _topBottomMargins: 68 + property int _spacing: 22 + + clip: true + contentWidth: pane.width + contentHeight: pane.height + + RowLayout{ + id: pane + width: root.width + + ColumnLayout { + id: content + spacing: root._spacing + Layout.maximumWidth: root.width - 2*root._leftRightMargins + Layout.fillWidth: true + Layout.topMargin: root._topBottomMargins + Layout.bottomMargin: root._topBottomMargins + Layout.leftMargin: root._leftRightMargins + Layout.rightMargin: root._leftRightMargins + } + } + + Button { + anchors { + top: parent.top + left: parent.left + topMargin: 10 + leftMargin: 18 + } + colorScheme: root.colorScheme + onClicked: root.back() + icon.source: "icons/ic-arrow-left.svg" + secondary: true + horizontalPadding: 8 + } +} diff --git a/internal/frontend/qml/SetupGuide.qml b/internal/frontend/qml/SetupGuide.qml index ae3b902e..5799d868 100644 --- a/internal/frontend/qml/SetupGuide.qml +++ b/internal/frontend/qml/SetupGuide.qml @@ -30,12 +30,14 @@ Item { property var backend property var user + property string address signal dismissed() implicitHeight: children[0].implicitHeight implicitWidth: children[0].implicitWidth + RowLayout { anchors.fill: parent spacing: 0 @@ -56,7 +58,7 @@ Item { Label { colorScheme: root.colorScheme - text: user ? user.username : "" + text: address color: root.colorScheme.text_weak type: Label.LabelType.Lead } @@ -80,30 +82,50 @@ Item { Repeater { model: clients - ColumnLayout { - RowLayout { - Layout.topMargin: 12 - Layout.bottomMargin: 12 - Layout.leftMargin: 16 - Layout.rightMargin: 16 + Rectangle { + implicitWidth: clientRow.width + implicitHeight: clientRow.height - ColorImage { - source: model.iconSource - height: 36 + ColumnLayout { + id: clientRow + + RowLayout { + Layout.topMargin: 12 + Layout.bottomMargin: 12 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + ColorImage { + source: model.iconSource + height: 36 + } + + Label { + colorScheme: root.colorScheme + Layout.leftMargin: 12 + text: model.name + type: Label.LabelType.Body + } } - Label { - colorScheme: root.colorScheme - Layout.leftMargin: 12 - text: model.name - type: Label.LabelType.Body + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: root.colorScheme.border_weak } } - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 1 - color: root.colorScheme.border_weak + + MouseArea { + anchors.fill: parent + onClicked: { + if (model.name != "Apple Mail") { + console.log(" TODO configure ", model.name) + return + } + root.user.configureAppleMail(root.address) + root.dismissed() + } } } } diff --git a/internal/frontend/qml/SignIn.qml b/internal/frontend/qml/SignIn.qml index 8698753b..59db1305 100644 --- a/internal/frontend/qml/SignIn.qml +++ b/internal/frontend/qml/SignIn.qml @@ -42,18 +42,9 @@ Item { property var backend property var window - // in case of adding new account this property should be undefined - property var user + property alias username: usernameTextField.text state: "Page 1" - onUserChanged: { - stackLayout.currentIndex = 0 - loginNormalLayout.reset() - passwordTextField.text = "" - login2FALayout.reset() - login2PasswordLayout.reset() - } - onLoginAbort: { stackLayout.currentIndex = 0 loginNormalLayout.reset() @@ -78,15 +69,15 @@ Item { } Connections { - target: user !== undefined ? user : root.backend + target: root.backend onLoginUsernamePasswordError: { console.assert(stackLayout.currentIndex == 0, "Unexpected loginUsernamePasswordError") console.assert(signInButton.loading == true, "Unexpected loginUsernamePasswordError") stackLayout.loginFailed() - errorLabel.text = qsTr("Your email and/or password are incorrect") - + if (errorMsg!="") errorLabel.text = errorMsg + else errorLabel.text = qsTr("Your email and/or password are incorrect") } onLoginFreeUserError: { @@ -152,6 +143,14 @@ Item { errorLabel.text = qsTr("Incorrect login credentials. Please try again.") passwordTextField.text = "" } + + onLoginFinished: { + stackLayout.currentIndex = 0 + loginNormalLayout.reset() + passwordTextField.text = "" + login2FALayout.reset() + login2PasswordLayout.reset() + } } ColumnLayout { @@ -218,8 +217,6 @@ Item { id: usernameTextField label: qsTr("Username or email") - text: user !== undefined ? user.username : "" - Layout.fillWidth: true Layout.topMargin: 24 @@ -304,12 +301,7 @@ Item { enabled = false loading = true - if (root.user !== undefined) { - root.user.login(usernameTextField.text, passwordTextField.text) - return - } - - root.login(usernameTextField.text, passwordTextField.text) + root.login(usernameTextField.text, Qt.btoa(passwordTextField.text)) } } @@ -394,12 +386,7 @@ Item { enabled = false loading = true - if (root.user !== undefined) { - root.user.login2FA(usernameTextField.text, twoFactorPasswordTextField.text) - return - } - - root.login2FA(usernameTextField.text, twoFactorPasswordTextField.text) + root.login2FA(usernameTextField.text, Qt.btoa(twoFactorPasswordTextField.text)) } } } @@ -471,12 +458,7 @@ Item { enabled = false loading = true - if (root.user !== undefined) { - root.user.login2Password(usernameTextField.text, secondPasswordTextField.text) - return - } - - root.login2Password(usernameTextField.text, secondPasswordTextField.text) + root.login2Password(usernameTextField.text, Qt.btoa(secondPasswordTextField.text)) } } } diff --git a/internal/frontend/qml/StatusWindow.qml b/internal/frontend/qml/StatusWindow.qml index d74ca461..a2897422 100644 --- a/internal/frontend/qml/StatusWindow.qml +++ b/internal/frontend/qml/StatusWindow.qml @@ -22,20 +22,16 @@ import QtQuick.Layouts 1.12 import QtQuick.Controls 2.13 import Proton 4.0 -import ProtonBackend 1.0 import Notifications 1.0 -// Because of https://bugreports.qt.io/browse/QTBUG-69777 and other bugs alike it is impossible -// to use Window with flags: Qt.Popup here since it won't close by it's own on click outside. -PopupWindow { +Window { id: root title: "ProtonMail Bridge" height: contentLayout.implicitHeight width: contentLayout.implicitWidth - minimumHeight: 201 - minimumWidth: 448 + flags: Qt.FramelessWindowHint property ColorScheme colorScheme: ProtonStyle.currentStyle @@ -47,15 +43,19 @@ PopupWindow { signal showMainWindow() signal showHelp() signal showSettings() + signal showSignIn(string username) signal quit() ColumnLayout { id: contentLayout + Layout.minimumHeight: 201 + anchors.fill: parent spacing: 0 ColumnLayout { + Layout.minimumWidth: 448 Layout.fillWidth: true spacing: 0 @@ -76,13 +76,13 @@ PopupWindow { } switch (statusItem.activeNotification.type) { - case Notification.NotificationType.Danger: + case Notification.NotificationType.Danger: return root.colorScheme.signal_danger - case Notification.NotificationType.Warning: + case Notification.NotificationType.Warning: return root.colorScheme.signal_warning - case Notification.NotificationType.Success: + case Notification.NotificationType.Success: return root.colorScheme.signal_success - case Notification.NotificationType.Info: + case Notification.NotificationType.Info: return root.colorScheme.signal_info } } @@ -149,8 +149,8 @@ PopupWindow { Layout.fillHeight: true Layout.maximumHeight: accountListView.count ? - accountListView.contentHeight / accountListView.count * 3 + accountListView.anchors.topMargin + accountListView.anchors.bottomMargin : - Number.POSITIVE_INFINITY + accountListView.contentHeight / accountListView.count * 3 + accountListView.anchors.topMargin + accountListView.anchors.bottomMargin : + Number.POSITIVE_INFINITY color: root.colorScheme.background_norm clip: true @@ -171,13 +171,17 @@ PopupWindow { interactive: contentHeight > parent.height snapMode: ListView.SnapToItem + boundsBehavior: Flickable.StopAtBounds delegate: Item { + id: viewItem width: ListView.view.width implicitHeight: children[0].implicitHeight implicitWidth: children[0].implicitWidth + property var user: root.backend.users.get(index) + RowLayout { spacing: 0 anchors.fill: parent @@ -187,15 +191,19 @@ PopupWindow { Layout.margins: 12 - user: modelData + user: viewItem.user colorScheme: root.colorScheme - } + Button { Layout.margins: 12 colorScheme: root.colorScheme - visible: true - text: "test" + visible: !viewItem.user.loggedIn + text: qsTr("Sign in") + onClicked: { + root.showSignIn(viewItem.username) + root.visible = false + } } } } @@ -297,4 +305,8 @@ PopupWindow { } } } + + onActiveChanged: { + if (!active) root.close() + } } diff --git a/internal/frontend/qml/WelcomeGuide.qml b/internal/frontend/qml/WelcomeGuide.qml index 26f56ff9..489092a2 100644 --- a/internal/frontend/qml/WelcomeGuide.qml +++ b/internal/frontend/qml/WelcomeGuide.qml @@ -239,7 +239,7 @@ Item { root.loginAbort(username) } - user: (backend.users.count === 1 && backend.users.get(0).loggedIn === false) ? backend.users.get(0) : undefined + username: (backend.users.count === 1 && backend.users.get(0).loggedIn === false) ? backend.users.get(0).username : "" backend: root.backend window: root.window } diff --git a/internal/frontend/qml/icons/ic-chevron-down.svg b/internal/frontend/qml/icons/ic-chevron-down.svg new file mode 100644 index 00000000..eda8be41 --- /dev/null +++ b/internal/frontend/qml/icons/ic-chevron-down.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/internal/frontend/qml/icons/ic-chevron-up.svg b/internal/frontend/qml/icons/ic-chevron-up.svg new file mode 100644 index 00000000..a190565d --- /dev/null +++ b/internal/frontend/qml/icons/ic-chevron-up.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/internal/frontend/qml/icons/ic-copy.svg b/internal/frontend/qml/icons/ic-copy.svg new file mode 100644 index 00000000..f27164ab --- /dev/null +++ b/internal/frontend/qml/icons/ic-copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/internal/frontend/qml/icons/ic-external-link.svg b/internal/frontend/qml/icons/ic-external-link.svg new file mode 100644 index 00000000..e2abee05 --- /dev/null +++ b/internal/frontend/qml/icons/ic-external-link.svg @@ -0,0 +1,3 @@ + + + diff --git a/internal/frontend/qml/icons/ic-info.svg b/internal/frontend/qml/icons/ic-info.svg new file mode 100644 index 00000000..f9eacb9a --- /dev/null +++ b/internal/frontend/qml/icons/ic-info.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/internal/frontend/qml/icons/ic-trash.svg b/internal/frontend/qml/icons/ic-trash.svg new file mode 100644 index 00000000..516f894f --- /dev/null +++ b/internal/frontend/qml/icons/ic-trash.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/internal/frontend/qt/dockicon/DockIcon.h b/internal/frontend/qt/dockicon/DockIcon.h new file mode 100644 index 00000000..e4310044 --- /dev/null +++ b/internal/frontend/qt/dockicon/DockIcon.h @@ -0,0 +1,24 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// +build darwin +// +build build_qt + +#include + +void SetDockIconVisibleState(bool visible); +bool GetDockIconVisibleState(); diff --git a/internal/frontend/qt/dockicon/DockIcon.m b/internal/frontend/qt/dockicon/DockIcon.m new file mode 100644 index 00000000..82fdc667 --- /dev/null +++ b/internal/frontend/qt/dockicon/DockIcon.m @@ -0,0 +1,42 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// +build darwin +// +build build_qt + +#include "DockIcon.h" +#include + +void SetDockIconVisibleState(bool visible) { + if (visible) { + [NSApp setActivationPolicy: NSApplicationActivationPolicyRegular]; + return; + } else { + [NSApp setActivationPolicy: NSApplicationActivationPolicyAccessory]; + return; + } +} + +bool GetDockIconVisibleState() { + switch ([NSApp activationPolicy]) { + case NSApplicationActivationPolicyAccessory: + case NSApplicationActivationPolicyProhibited: + return false; + case NSApplicationActivationPolicyRegular: + return true; + } +} diff --git a/internal/frontend/qt/dockicon/dockicon_darwin.go b/internal/frontend/qt/dockicon/dockicon_darwin.go new file mode 100644 index 00000000..a6352e55 --- /dev/null +++ b/internal/frontend/qt/dockicon/dockicon_darwin.go @@ -0,0 +1,33 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// +build darwin +// +build build_qt + +package dockicon + +// #cgo CFLAGS: -x objective-c +// #cgo LDFLAGS: -framework Cocoa +// #include "DockIcon.h" +import "C" + +func SetDockIconVisibleState(visible bool) { + C.SetDockIconVisibleState(C.bool(visible)) +} +func GetDockIconVisibleState() bool { + return bool(C.GetDockIconVisibleState()) +} diff --git a/internal/frontend/qt/dockicon/dockicon_default.go b/internal/frontend/qt/dockicon/dockicon_default.go new file mode 100644 index 00000000..d2927587 --- /dev/null +++ b/internal/frontend/qt/dockicon/dockicon_default.go @@ -0,0 +1,26 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// +build !darwin +// +build build_qt + +package dockicon + +func SetDockIconVisibleState(visible bool) {} +func GetDockIconVisibleState() bool { + return true +} diff --git a/internal/frontend/qt/frontend.go b/internal/frontend/qt/frontend.go new file mode 100644 index 00000000..3cba590a --- /dev/null +++ b/internal/frontend/qt/frontend.go @@ -0,0 +1,146 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// +build build_qt + +// Package qt provides communication between Qt/QML frontend and Go backend +package qt + +import ( + "fmt" + "sync" + + "github.com/ProtonMail/go-autostart" + "github.com/ProtonMail/proton-bridge/internal/config/settings" + "github.com/ProtonMail/proton-bridge/internal/config/useragent" + "github.com/ProtonMail/proton-bridge/internal/frontend/types" + "github.com/ProtonMail/proton-bridge/internal/locations" + "github.com/ProtonMail/proton-bridge/internal/updater" + "github.com/ProtonMail/proton-bridge/pkg/listener" + "github.com/ProtonMail/proton-bridge/pkg/pmapi" + "github.com/sirupsen/logrus" + "github.com/therecipe/qt/qml" + "github.com/therecipe/qt/widgets" +) + +type FrontendQt struct { + programName, programVersion string + + panicHandler types.PanicHandler + locations *locations.Locations + settings *settings.Settings + eventListener listener.Listener + updater types.Updater + userAgent *useragent.UserAgent + bridge types.Bridger + noEncConfirmator types.NoEncConfirmator + autostart *autostart.App + restarter types.Restarter + + authClient pmapi.Client + auth *pmapi.Auth + password []byte + + newVersionInfo updater.VersionInfo + + log *logrus.Entry + usersMtx sync.Mutex + + app *widgets.QApplication + engine *qml.QQmlApplicationEngine + qml *QMLBackend +} + +func New( + version, + buildVersion, + programName string, + showWindowOnStart bool, + panicHandler types.PanicHandler, + locations *locations.Locations, + settings *settings.Settings, + eventListener listener.Listener, + updater types.Updater, + userAgent *useragent.UserAgent, + bridge types.Bridger, + _ types.NoEncConfirmator, + autostart *autostart.App, + restarter types.Restarter, +) *FrontendQt { + return &FrontendQt{ + programName: "Proton Mail Bridge", + programVersion: version, + log: logrus.WithField("pkg", "frontend/qt"), + + panicHandler: panicHandler, + locations: locations, + settings: settings, + eventListener: eventListener, + updater: updater, + userAgent: userAgent, + bridge: bridge, + autostart: autostart, + restarter: restarter, + } +} + +func (f *FrontendQt) Loop() error { + err := f.initiateQtApplication() + if err != nil { + return err + } + + go func() { + defer f.panicHandler.HandlePanic() + f.watchEvents() + }() + + if ret := f.app.Exec(); ret != 0 { + err := fmt.Errorf("Event loop ended with return value: %v", ret) + f.log.Warn("App exec", err) + return err + } + + return nil +} + +func (f *FrontendQt) NotifyManualUpdate(version updater.VersionInfo, canInstall bool) { + if canInstall { + f.qml.UpdateManualReady(version.Version.String()) + } else { + f.qml.UpdateManualError() + } +} + +func (f *FrontendQt) SetVersion(version updater.VersionInfo) { + f.newVersionInfo = version + f.qml.SetReleaseNotesLink(version.ReleaseNotesPage) + f.qml.SetLandingPageLink(version.LandingPage) +} + +func (f *FrontendQt) NotifySilentUpdateInstalled() { + f.qml.UpdateSilentRestartNeeded() +} + +func (f *FrontendQt) NotifySilentUpdateError(err error) { + f.log.WithError(err).Warn("Update failed, asking for manual.") + f.qml.UpdateManualError() +} + +func (f *FrontendQt) WaitUntilFrontendIsReady() { + // TODO: Implement +} diff --git a/internal/frontend/qt/frontend_events.go b/internal/frontend/qt/frontend_events.go new file mode 100644 index 00000000..13631ec4 --- /dev/null +++ b/internal/frontend/qt/frontend_events.go @@ -0,0 +1,85 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// +build build_qt + +// Package qt provides communication between Qt/QML frontend and Go backend +package qt + +import ( + "strings" + + "github.com/ProtonMail/proton-bridge/internal/events" +) + +func (f *FrontendQt) watchEvents() { + f.WaitUntilFrontendIsReady() + + errorCh := f.eventListener.ProvideChannel(events.ErrorEvent) + credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent) + noActiveKeyForRecipientCh := f.eventListener.ProvideChannel(events.NoActiveKeyForRecipientEvent) + internetOffCh := f.eventListener.ProvideChannel(events.InternetOffEvent) + internetOnCh := f.eventListener.ProvideChannel(events.InternetOnEvent) + secondInstanceCh := f.eventListener.ProvideChannel(events.SecondInstanceEvent) + restartBridgeCh := f.eventListener.ProvideChannel(events.RestartBridgeEvent) + addressChangedCh := f.eventListener.ProvideChannel(events.AddressChangedEvent) + addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent) + logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent) + updateApplicationCh := f.eventListener.ProvideChannel(events.UpgradeApplicationEvent) + userChangedCh := f.eventListener.ProvideChannel(events.UserRefreshEvent) + certIssue := f.eventListener.ProvideChannel(events.TLSCertIssue) + + for { + select { + case errorDetails := <-errorCh: + if strings.Contains(errorDetails, "IMAP failed") { + f.qml.PortIssueIMAP() + } + if strings.Contains(errorDetails, "SMTP failed") { + f.qml.PortIssueSMTP() + } + case <-credentialsErrorCh: + f.qml.NotifyHasNoKeychain() + case email := <-noActiveKeyForRecipientCh: + f.qml.NoActiveKeyForRecipient(email) + case <-internetOffCh: + f.qml.InternetOff() + case <-internetOnCh: + f.qml.InternetOn() + case <-secondInstanceCh: + f.qml.ShowMainWindow() + case <-restartBridgeCh: + f.restart() + case address := <-addressChangedCh: + f.qml.AddressChanged(address) + case address := <-addressChangedLogoutCh: + f.qml.AddressChangedLogout(address) + case userID := <-logoutCh: + user, err := f.bridge.GetUser(userID) + if err != nil { + return + } + f.qml.UserDisconnected(user.Username()) + case <-updateApplicationCh: + f.updateForce() + case userID := <-userChangedCh: + f.userChanged(userID) + case <-certIssue: + f.qml.ApiCertIssue() + } + } +} diff --git a/internal/frontend/qt/frontend_help.go b/internal/frontend/qt/frontend_help.go new file mode 100644 index 00000000..e105e0f4 --- /dev/null +++ b/internal/frontend/qt/frontend_help.go @@ -0,0 +1,45 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// +build build_qt + +package qt + +func (f *FrontendQt) setVersion() { + f.qml.SetVersion(f.programVersion) +} + +func (f *FrontendQt) setLogsPath() { + path, err := f.locations.ProvideLogsPath() + if err != nil { + f.log.WithError(err).Error("Cannot update path folder") + return + } + f.qml.SetLogsPath(path) +} + +func (f *FrontendQt) setLicensePath() { + f.qml.SetLicensePath(f.locations.GetLicenseFilePath()) +} + +func (f *FrontendQt) setCurrentEmailClient() { + f.qml.SetCurrentEmailClient(f.userAgent.String()) +} + +func (f *FrontendQt) reportBug(description, address, emailClient string, includeLogs bool) { + //TODO +} diff --git a/internal/frontend/qt/frontend_init.go b/internal/frontend/qt/frontend_init.go new file mode 100644 index 00000000..1d213fb8 --- /dev/null +++ b/internal/frontend/qt/frontend_init.go @@ -0,0 +1,71 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// +build build_qt + +package qt + +import ( + "errors" + qmlLog "github.com/ProtonMail/proton-bridge/internal/frontend/qt/log" + "github.com/therecipe/qt/core" + "github.com/therecipe/qt/qml" + "github.com/therecipe/qt/quickcontrols2" + "github.com/therecipe/qt/widgets" + "os" +) + +func (f *FrontendQt) initiateQtApplication() error { + qmlLog.InstallMessageHandler() + + f.app = widgets.NewQApplication(len(os.Args), os.Args) + + core.QCoreApplication_SetApplicationName(f.programName) + core.QCoreApplication_SetApplicationVersion(f.programVersion) + + // High DPI scaling for windows. + core.QCoreApplication_SetAttribute(core.Qt__AA_EnableHighDpiScaling, false) + // Software OpenGL: to avoid dedicated GPU. + core.QCoreApplication_SetAttribute(core.Qt__AA_UseSoftwareOpenGL, true) + + // Bridge runs background, no window is needed to be opened. + f.app.SetQuitOnLastWindowClosed(false) + + // QML Engine and path + f.engine = qml.NewQQmlApplicationEngine(f.app) + + f.qml = NewQMLBackend(nil) + f.qml.setup(f) + f.engine.RootContext().SetContextProperty("go", f.qml) + + f.engine.AddImportPath("qrc:/qml/") + f.engine.AddPluginPath("qrc:/qml/") + + // Add style: if colorScheme / style is forgotten we should fallback to + // default style and should be Proton + quickcontrols2.QQuickStyle_AddStylePath("qrc:/qml/") + quickcontrols2.QQuickStyle_SetStyle("Proton") + + f.engine.Load(core.NewQUrl3("qrc:/qml/Bridge.qml", 0)) + + // Check QML is loaded properly. + if len(f.engine.RootObjects()) == 0 { + return errors.New("QML not loaded properly") + } + + return nil +} diff --git a/internal/frontend/qt/frontend_nogui.go b/internal/frontend/qt/frontend_nogui.go new file mode 100644 index 00000000..dfe96f03 --- /dev/null +++ b/internal/frontend/qt/frontend_nogui.go @@ -0,0 +1,83 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// +build !build_qt + +package qt + +import ( + "fmt" + "net/http" + + "github.com/ProtonMail/go-autostart" + "github.com/ProtonMail/proton-bridge/internal/config/settings" + "github.com/ProtonMail/proton-bridge/internal/config/useragent" + "github.com/ProtonMail/proton-bridge/internal/frontend/types" + "github.com/ProtonMail/proton-bridge/internal/locations" + "github.com/ProtonMail/proton-bridge/internal/updater" + "github.com/ProtonMail/proton-bridge/pkg/listener" + "github.com/sirupsen/logrus" +) + +var log = logrus.WithField("pkg", "frontend-nogui") //nolint[gochecknoglobals] + +type FrontendHeadless struct{} + +func New( + version, + buildVersion, + programName string, + showWindowOnStart bool, + panicHandler types.PanicHandler, + locations *locations.Locations, + settings *settings.Settings, + eventListener listener.Listener, + updater types.Updater, + userAgent *useragent.UserAgent, + bridge types.Bridger, + noEncConfirmator types.NoEncConfirmator, + autostart *autostart.App, + restarter types.Restarter, +) *FrontendHeadless { + return &FrontendHeadless{} +} + +func (s *FrontendHeadless) Loop() error { + log.Info("Check status on localhost:8081") + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Bridge is running") + }) + return http.ListenAndServe(":8081", nil) +} + +func (s *FrontendHeadless) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) { + // NOTE: Save the update somewhere so that it can be installed when user chooses "install now". +} + +func (s *FrontendHeadless) WaitUntilFrontendIsReady() { +} + +func (s *FrontendHeadless) SetVersion(update updater.VersionInfo) { +} + +func (s *FrontendHeadless) NotifySilentUpdateInstalled() { +} + +func (s *FrontendHeadless) NotifySilentUpdateError(err error) { +} + +func (s *FrontendHeadless) InstanceExistAlert() {} diff --git a/internal/frontend/qt/frontend_settings.go b/internal/frontend/qt/frontend_settings.go new file mode 100644 index 00000000..1b335659 --- /dev/null +++ b/internal/frontend/qt/frontend_settings.go @@ -0,0 +1,151 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// +build build_qt + +package qt + +import ( + "time" + + "github.com/ProtonMail/proton-bridge/internal/config/settings" + "github.com/ProtonMail/proton-bridge/internal/frontend/clientconfig" + "github.com/ProtonMail/proton-bridge/pkg/keychain" + "github.com/ProtonMail/proton-bridge/pkg/ports" +) + +func (f *FrontendQt) setIsDiskCacheEnabled() { + //TODO +} + +func (f *FrontendQt) setDiskCachePath() { + //TODO +} + +func (f *FrontendQt) changeLocalCache(enableDiskCache bool, diskCachePath string) { + //TODO +} + +func (f *FrontendQt) setIsAutostartOn() { + f.qml.SetIsAutostartOn(f.autostart.IsEnabled()) +} + +func (f *FrontendQt) toggleAutostart(makeItEnabled bool) { + defer f.qml.ToggleAutostartFinished() + if makeItEnabled == f.autostart.IsEnabled() { + f.setIsAutostartOn() + return + } + + var err error + if makeItEnabled { + err = f.autostart.Enable() + } else { + err = f.autostart.Disable() + } + f.setIsAutostartOn() + + if err != nil { + f.log. + WithField("makeItEnabled", makeItEnabled). + WithField("isEnabled", f.qml.IsAutostartOn()). + WithError(err). + Error("Autostart change failed") + } +} + +func (f *FrontendQt) toggleDoH(makeItEnabled bool) { + if f.settings.GetBool(settings.AllowProxyKey) == makeItEnabled { + f.qml.SetIsDoHEnabled(makeItEnabled) + return + } + f.settings.SetBool(settings.AllowProxyKey, makeItEnabled) + f.restart() +} + +func (f *FrontendQt) toggleUseSSLforSMTP(makeItEnabled bool) { + if f.settings.GetBool(settings.SMTPSSLKey) == makeItEnabled { + f.qml.SetUseSSLforSMTP(makeItEnabled) + return + } + f.settings.SetBool(settings.SMTPPortKey, makeItEnabled) + f.restart() +} + +func (f *FrontendQt) changePorts(imapPort, smtpPort int) { + f.settings.SetInt(settings.IMAPPortKey, imapPort) + f.settings.SetInt(settings.SMTPPortKey, smtpPort) + f.restart() +} + +func (f *FrontendQt) isPortFree(port int) bool { + return ports.IsPortFree(port) +} + +func (f *FrontendQt) configureAppleMail(userID, address string) { + user, err := f.bridge.GetUser(userID) + if err != nil { + f.log.WithField("userID", userID).Error("Cannot configure AppleMail for user") + return + } + + needRestart, err := clientconfig.ConfigureAppleMail(user, address, f.settings) + if err != nil { + f.log.WithError(err).Error("Apple Mail config failed") + } + + if needRestart { + // There is delay needed for external window to open + time.Sleep(2 * time.Second) + f.restart() + } +} + +func (f *FrontendQt) triggerReset() { + defer f.qml.ResetFinished() + //TODO + f.restart() +} + +func (f *FrontendQt) setKeychain() { + availableKeychain := []string{} + for chain := range keychain.Helpers { + availableKeychain = append(availableKeychain, chain) + } + f.qml.SetAvailableKeychain(availableKeychain) + f.qml.SetSelectedKeychain(f.bridge.GetKeychainApp()) +} + +func (f *FrontendQt) selectKeychain(wantKeychain string) { + if f.bridge.GetKeychainApp() == wantKeychain { + return + } + + f.bridge.SetKeychainApp(wantKeychain) + f.restart() +} + +func (f *FrontendQt) restart() { + f.log.Info("Restarting bridge") + f.restarter.SetToRestart() + f.app.Exit(0) +} + +func (f *FrontendQt) quit() { + f.log.Warn("Your wish is my command.. I quit!") + f.app.Exit(0) +} diff --git a/internal/frontend/qt/frontend_updates.go b/internal/frontend/qt/frontend_updates.go new file mode 100644 index 00000000..9f02e448 --- /dev/null +++ b/internal/frontend/qt/frontend_updates.go @@ -0,0 +1,130 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// +build build_qt + +package qt + +import ( + "sync" + + "github.com/ProtonMail/proton-bridge/internal/config/settings" + "github.com/ProtonMail/proton-bridge/internal/updater" +) + +var checkingUpdates = sync.Mutex{} + +func (f *FrontendQt) checkUpdates() error { + version, err := f.updater.Check() + if err != nil { + return err + } + + f.SetVersion(version) + return nil +} + +func (f *FrontendQt) checkUpdatesAndNotify(isRequestFromUser bool) { + checkingUpdates.Lock() + defer checkingUpdates.Lock() + defer f.qml.CheckUpdatesFinished() + + if err := f.checkUpdates(); err != nil { + f.log.WithError(err).Error("An error occurred while checking updates") + if isRequestFromUser { + f.qml.UpdateManualError() + } + return + } + + if !f.updater.IsUpdateApplicable(f.newVersionInfo) { + f.log.Debug("No need to update") + if isRequestFromUser { + f.qml.UpdateIsLatestVersion() + } + return + } + + if !f.updater.CanInstall(f.newVersionInfo) { + f.log.Debug("A manual update is required") + f.qml.UpdateManualReady(f.newVersionInfo.Version.String()) + return + } +} + +func (f *FrontendQt) updateForce() { + checkingUpdates.Lock() + defer checkingUpdates.Lock() + + version := "" + if err := f.checkUpdates(); err == nil { + version = f.newVersionInfo.Version.String() + } + + f.qml.UpdateForce(version) +} + +func (f *FrontendQt) setIsAutomaticUpdateOn() { + f.qml.SetIsAutomaticUpdateOn(f.settings.GetBool(settings.AutoUpdateKey)) +} + +func (f *FrontendQt) toggleAutomaticUpdate(makeItEnabled bool) { + f.qml.SetIsAutomaticUpdateOn(makeItEnabled) + isEnabled := f.settings.GetBool(settings.AutoUpdateKey) + if makeItEnabled == isEnabled { + return + } + + f.settings.SetBool(settings.AutoUpdateKey, makeItEnabled) + + f.checkUpdatesAndNotify(false) +} + +func (f *FrontendQt) setIsBetaEnabled() { + channel := f.bridge.GetUpdateChannel() + f.qml.SetIsBetaEnabled(channel == updater.EarlyChannel) +} + +func (f *FrontendQt) toggleBeta(makeItEnabled bool) { + channel := f.bridge.GetUpdateChannel() + + if makeItEnabled == (channel == updater.EarlyChannel) { + f.qml.SetIsBetaEnabled(makeItEnabled) + return + } + + channel = updater.StableChannel + if makeItEnabled { + channel = updater.EarlyChannel + } + + needRestart, err := f.bridge.SetUpdateChannel(channel) + f.setIsBetaEnabled() + + if err != nil { + f.log.WithError(err).Warn("Switching udpate channel failed.") + f.qml.UpdateManualError() + return + } + + if needRestart { + f.restart() + return + } + + f.checkUpdatesAndNotify(false) +} diff --git a/internal/frontend/qt/frontend_users.go b/internal/frontend/qt/frontend_users.go new file mode 100644 index 00000000..f26bd298 --- /dev/null +++ b/internal/frontend/qt/frontend_users.go @@ -0,0 +1,224 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// +build build_qt + +package qt + +import ( + "context" + "encoding/base64" + "github.com/ProtonMail/proton-bridge/internal/frontend/types" + "github.com/ProtonMail/proton-bridge/pkg/pmapi" +) + +func (f *FrontendQt) loadUsers() { + f.usersMtx.Lock() + defer f.usersMtx.Unlock() + + f.qml.Users().clear() + + for _, user := range f.bridge.GetUsers() { + f.qml.Users().addUser(newQMLUserFromBacked(f, user)) + } + + // If there are no active accounts. + if f.qml.Users().Count() == 0 { + f.log.Info("No active accounts") + } +} + +func (f *FrontendQt) userChanged(userID string) { + f.usersMtx.Lock() + defer f.usersMtx.Unlock() + + fUsers := f.qml.Users() + + index := fUsers.indexByID(userID) + user, err := f.bridge.GetUser(userID) + + if user == nil || err != nil { + if index >= 0 { // delete existing user + fUsers.removeUser(index) + } + return + } + + if index < 0 { // add non-existing user + fUsers.addUser(newQMLUserFromBacked(f, user)) + return + } + + // update exiting user + fUsers.users[index].update(user) +} + +func newQMLUserFromBacked(f *FrontendQt, user types.User) *QMLUser { + qu := NewQMLUser(nil) + qu.ID = user.ID() + + qu.update(user) + + qu.ConnectToggleSplitMode(func(activateSplitMode bool) { + go func() { + defer qu.ToggleSplitModeFinished() + if activateSplitMode == user.IsCombinedAddressMode() { + user.SwitchAddressMode() + } + qu.SetSplitMode(!user.IsCombinedAddressMode()) + }() + }) + + qu.ConnectLogout(func() { + qu.SetLoggedIn(false) + go user.Logout() + }) + + qu.ConnectConfigureAppleMail(func(address string) { + go f.configureAppleMail(qu.ID, address) + }) + + return qu +} + +func (f *FrontendQt) login(username, password string) { + var err error + f.password, err = base64.StdEncoding.DecodeString(password) + if err != nil { + f.log.WithError(err).Error("Cannot decode password") + f.qml.LoginUsernamePasswordError("Cannot decode password") + f.loginClean() + return + } + + f.authClient, f.auth, err = f.bridge.Login(username, f.password) + if err != nil { + f.qml.LoginUsernamePasswordError(err.Error()) + f.loginClean() + return + } + + if f.auth.HasTwoFactor() { + f.qml.Login2FARequested() + return + } + if f.auth.HasMailboxPassword() { + f.qml.Login2PasswordRequested() + return + } + + f.finishLogin() +} + +func (f *FrontendQt) login2FA(username, code string) { + if f.auth == nil || f.authClient == nil { + f.log.Errorf("Login 2FA: authethication incomplete %p %p", f.auth, f.authClient) + f.qml.Login2FAErrorAbort("Missing authentication, try again.") + f.loginClean() + return + } + + twoFA, err := base64.StdEncoding.DecodeString(code) + if err != nil { + f.log.WithError(err).Error("Cannot decode 2fa code") + f.qml.LoginUsernamePasswordError("Cannot decode 2fa code") + f.loginClean() + return + } + + err = f.authClient.Auth2FA(context.Background(), string(twoFA)) + if err == pmapi.ErrBad2FACodeTryAgain { + f.log.Warn("Login 2FA: retry 2fa") + f.qml.Login2FAError("") + return + } + + if err == pmapi.ErrBad2FACode { + f.log.Warn("Login 2FA: abort 2fa") + f.qml.Login2FAErrorAbort("") + f.loginClean() + return + } + + if err != nil { + f.log.WithError(err).Warn("Login 2FA: failed.") + f.qml.Login2FAErrorAbort(err.Error()) + f.loginClean() + return + } + + if f.auth.HasMailboxPassword() { + f.qml.Login2PasswordRequested() + return + } + + f.finishLogin() +} + +func (f *FrontendQt) login2Password(username, mboxPassword string) { + var err error + f.password, err = base64.StdEncoding.DecodeString(mboxPassword) + if err != nil { + f.log.WithError(err).Error("Cannot decode mbox password") + f.qml.LoginUsernamePasswordError("Cannot decode mbox password") + f.loginClean() + return + } + + f.finishLogin() +} + +func (f *FrontendQt) finishLogin() { + defer f.loginClean() + + if f.auth == nil || f.authClient == nil { + f.log.Errorf("Finish login: Authethication incomplete %p %p", f.auth, f.authClient) + f.qml.Login2PasswordErrorAbort("Missing authentication, try again.") + return + } + + user, err := f.bridge.FinishLogin(f.authClient, f.auth, f.password) + if err != nil { + f.log.Errorf("Authethication incomplete %p %p", f.auth, f.authClient) + f.qml.Login2PasswordErrorAbort("Missing authentication, try again.") + return + } + + index := f.qml.Users().indexByID(user.ID()) + if index < 0 { + qu := newQMLUserFromBacked(f, user) + qu.SetSetupGuideSeen(false) + f.qml.Users().addUser(qu) + return + } + + f.qml.Users().users[index].update(user) + f.qml.LoginFinished() +} + +func (f *FrontendQt) loginAbort(username string) { + f.loginClean() +} + +func (f *FrontendQt) loginClean() { + f.auth = nil + f.authClient = nil + for i := range f.password { + f.password[i] = '\x00' + } + f.password = f.password[0:0] +} diff --git a/internal/frontend/qt/helpers.go b/internal/frontend/qt/helpers.go new file mode 100644 index 00000000..50d83ce7 --- /dev/null +++ b/internal/frontend/qt/helpers.go @@ -0,0 +1,70 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// +build build_qt + +package qt + +import ( + "regexp" + "strings" + + "github.com/therecipe/qt/core" + "github.com/therecipe/qt/gui" +) + +// getCursorPos returns current mouse position to be able to use in QML +func getCursorPos() *core.QPoint { + return gui.QCursor_Pos() +} + +// newQByteArrayFromString is a wrapper for new QByteArray from string. +func newQByteArrayFromString(name string) *core.QByteArray { + return core.NewQByteArray2(name, len(name)) +} + +var ( + reMultiSpaces = regexp.MustCompile(`\s{2,}`) + reStartWithSymbol = regexp.MustCompile(`^[.,/#!$@%^&*;:{}=\-_` + "`" + `~()]`) +) + +// getInitials based on webapp implementation: +// https://github.com/ProtonMail/WebClients/blob/55d96a8b4afaaa4372fc5f1ef34953f2070fd7ec/packages/shared/lib/helpers/string.ts#L145 +func getInitials(fullName string) string { + words := strings.Split( + reMultiSpaces.ReplaceAllString(fullName, " "), + " ", + ) + + n := 0 + for _, word := range words { + if !reStartWithSymbol.MatchString(word) { + words[n] = word + n++ + } + } + + if n == 0 { + return "?" + } + + initials := words[0][0:1] + if n != 1 { + initials += words[n-1][0:1] + } + return strings.ToUpper(initials) +} diff --git a/internal/frontend/qt/log/log.cpp b/internal/frontend/qt/log/log.cpp new file mode 100644 index 00000000..1950b060 --- /dev/null +++ b/internal/frontend/qt/log/log.cpp @@ -0,0 +1,44 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + + +// +build build_qt + +#include "log.h" +#include "_cgo_export.h" + +#include +#include +#include +#include +#include + +void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + Q_UNUSED( type ) + Q_UNUSED( context ) + QByteArray localMsg = msg.toUtf8().prepend("WHITESPACE"); + logMsgPacked( + const_cast( (localMsg.constData()) +10 ), + localMsg.size()-10 + ); +} + +void InstallMessageHandler() { + qInstallMessageHandler(messageHandler); +} + diff --git a/internal/frontend/qt/log/log.go b/internal/frontend/qt/log/log.go new file mode 100644 index 00000000..5af48931 --- /dev/null +++ b/internal/frontend/qt/log/log.go @@ -0,0 +1,46 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// +build build_qt + +// Package log redirects QML logs to logrus +package log + +//#include "log.h" +import "C" + +import ( + "github.com/sirupsen/logrus" + "github.com/therecipe/qt/core" +) + +var logQML = logrus.WithField("pkg", "frontent/qml") + +// InstallMessageHandler is registering logQML as logger for QML calls. +func InstallMessageHandler() { + C.InstallMessageHandler() +} + +//export logMsgPacked +func logMsgPacked(data *C.char, len C.int) { + logQML.Warn(C.GoStringN(data, len)) +} + +// logDummy is here to trigger qtmoc to create cgo instructions +type logDummy struct { + core.QObject +} diff --git a/internal/frontend/qt/log/log.h b/internal/frontend/qt/log/log.h new file mode 100644 index 00000000..777e97af --- /dev/null +++ b/internal/frontend/qt/log/log.h @@ -0,0 +1,36 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +#pragma once + +#ifndef LOGRUS_QML_LOG_H +#define LOGRUS_QML_LOG_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif // C++ + + void InstallMessageHandler(); + ; + +#ifdef __cplusplus +} +#endif // C++ + +#endif // LOGRUS_QML_LOG_H diff --git a/internal/frontend/qt/qml_backend.go b/internal/frontend/qt/qml_backend.go new file mode 100644 index 00000000..01f9ce32 --- /dev/null +++ b/internal/frontend/qt/qml_backend.go @@ -0,0 +1,203 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// +build build_qt + +package qt + +import ( + "runtime" + + "github.com/ProtonMail/proton-bridge/internal/bridge" + "github.com/ProtonMail/proton-bridge/internal/config/settings" + dockIcon "github.com/ProtonMail/proton-bridge/internal/frontend/qt/dockicon" + "github.com/therecipe/qt/core" +) + +// QMLBackend connects QML frontend with Go backend. +type QMLBackend struct { + core.QObject + + _ func() *core.QPoint `slot:"getCursorPos"` + _ func() `slot:"quit"` + _ func() `slot:"restart"` + + _ bool `property:dockIconVisible` + + _ QMLUserModel `property:"users"` + + // TODO copy stuff from Bridge_test.qml backend object + _ string `property:"goos"` + + _ func(username, password string) `slot:"login"` + _ func(username, code string) `slot:"login2FA"` + _ func(username, password string) `slot:"login2Password"` + _ func(username string) `slot:"loginAbort"` + _ func(errorMsg string) `signal:"loginUsernamePasswordError"` + _ func(errorMsg string) `signal:"loginFreeUserError"` + _ func(errorMsg string) `signal:"loginConnectionError"` + _ func() `signal:"login2FARequested"` + _ func(errorMsg string) `signal:"login2FAError"` + _ func(errorMsg string) `signal:"login2FAErrorAbort"` + _ func() `signal:"login2PasswordRequested"` + _ func(errorMsg string) `signal:"login2PasswordError"` + _ func(errorMsg string) `signal:"login2PasswordErrorAbort"` + _ func() `signal:"loginFinished"` + + _ func() `signal:"internetOff"` + _ func() `signal:"internetOn"` + + _ func(version string) `signal:"updateManualReady"` + _ func() `signal:"updateManualRestartNeeded"` + _ func() `signal:"updateManualError"` + _ func(version string) `signal:"updateForce"` + _ func() `signal:"updateForceError"` + _ func() `signal:"updateSilentRestartNeeded"` + _ func() `signal:"updateSilentError"` + _ func() `signal:"updateIsLatestVersion"` + _ func() `slot:"checkUpdates"` + _ func() `signal:"checkUpdatesFinished"` + + _ bool `property:"isDiskCacheEnabled"` + _ string `property:"diskCachePath"` + _ func() `signal:"cacheUnavailable"` + _ func() `signal:"cacheCantMove"` + _ func() `signal:"cacheLocationChangeSuccess"` + _ func() `signal:"diskFull"` + _ func(enableDiskCache bool, diskCachePath string) `slot:"changeLocalCache"` + _ func() `signal:"changeLocalCacheFinished"` + + _ bool `property:"isAutomaticUpdateOn"` + _ func(makeItActive bool) `slot:"toggleAutomaticUpdate"` + + _ bool `property:"isAutostartOn"` + _ func(makeItActive bool) `slot:"toggleAutostart"` + _ func() `signal:"toggleAutostartFinished"` + + _ bool `property:"isBetaEnabled"` + _ func(makeItActive bool) `slot:"toggleBeta"` + + _ bool `property:"isDoHEnabled"` + _ func(makeItActive bool) `slot:"toggleDoH"` + + _ bool `property:"useSSLforSMTP"` + _ func(makeItActive bool) `slot:"toggleUseSSLforSMTP"` + _ func() `signal:"toggleUseSSLFinished"` + + _ string `property:"hostname"` + _ int `property:"portIMAP"` + _ int `property:"portSMTP"` + _ func(imapPort, smtpPort int) `slot:"changePorts"` + _ func(port int) bool `slot:"isPortFree"` + _ func() `signal:"changePortFinished"` + _ func() `signal:"portIssueIMAP"` + _ func() `signal:"portIssueSMTP"` + + _ func() `slot:"triggerReset"` + _ func() `signal:"resetFinished"` + + _ string `property:"version"` + _ string `property:"logsPath"` + _ string `property:"licensePath"` + _ string `property:"releaseNotesLink"` + _ string `property:"landingPageLink"` + + _ string `property:"currentEmailClient"` + _ func() `slot:"updateCurrentMailClient"` + _ func(description, address, emailClient string, includeLogs bool) `slot:"reportBug"` + _ func() `signal:"reportBugFinished"` + _ func() `signal:"bugReportSendSuccess"` + _ func() `signal:"bugReportSendError"` + + _ []string `property:"availableKeychain"` + _ string `property:"selectedKeychain"` + _ func(keychain string) `slot:"selectKeychain"` + _ func() `signal:"notifyHasNoKeychain"` + + _ func(email string) `signal:noActiveKeyForRecipient` + _ func() `signal:showMainWindow` + + _ func(address string) `signal:addressChanged` + _ func(address string) `signal:addressChangedLogout` + _ func(username string) `signal:userDisconnected` + _ func() `signal:apiCertIssue` +} + +func (q *QMLBackend) setup(f *FrontendQt) { + q.ConnectGetCursorPos(getCursorPos) + q.ConnectQuit(f.quit) + q.ConnectRestart(f.restart) + + q.ConnectIsDockIconVisible(func() bool { + return dockIcon.GetDockIconVisibleState() + }) + q.ConnectSetDockIconVisible(func(visible bool) { + dockIcon.SetDockIconVisibleState(visible) + }) + + q.SetUsers(NewQMLUserModel(nil)) + f.loadUsers() + + q.SetGoos(runtime.GOOS) + + q.ConnectLogin(func(u, p string) { go f.login(u, p) }) + q.ConnectLogin2FA(func(u, p string) { go f.login2FA(u, p) }) + q.ConnectLogin2Password(func(u, p string) { go f.login2Password(u, p) }) + q.ConnectLoginAbort(func(u string) { go f.loginAbort(u) }) + + go f.checkUpdatesAndNotify(false) + q.ConnectCheckUpdates(func() { go f.checkUpdatesAndNotify(true) }) + + f.setIsDiskCacheEnabled() + f.setDiskCachePath() + q.ConnectChangeLocalCache(f.changeLocalCache) + + f.setIsAutomaticUpdateOn() + q.ConnectToggleAutomaticUpdate(func(m bool) { go f.toggleAutomaticUpdate(m) }) + + f.setIsAutostartOn() + q.ConnectToggleAutostart(f.toggleAutostart) + + f.setIsBetaEnabled() + q.ConnectToggleBeta(func(m bool) { go f.toggleBeta(m) }) + + q.SetIsDoHEnabled(f.settings.GetBool(settings.AllowProxyKey)) + q.ConnectToggleDoH(f.toggleDoH) + + q.SetUseSSLforSMTP(f.settings.GetBool(settings.SMTPSSLKey)) + q.ConnectToggleUseSSLforSMTP(f.toggleUseSSLforSMTP) + + q.SetHostname(bridge.Host) + q.SetPortIMAP(f.settings.GetInt(settings.IMAPPortKey)) + q.SetPortSMTP(f.settings.GetInt(settings.SMTPPortKey)) + q.ConnectChangePorts(f.changePorts) + q.ConnectIsPortFree(f.isPortFree) + + q.ConnectTriggerReset(func() { go f.triggerReset() }) + + f.setVersion() + f.setLogsPath() + // release notes link is set by update + f.setLicensePath() + + f.setCurrentEmailClient() + q.ConnectUpdateCurrentMailClient(func() { go f.setCurrentEmailClient() }) + q.ConnectReportBug(func(d, a, e string, i bool) { go f.reportBug(d, a, e, i) }) + + f.setKeychain() + q.ConnectSelectKeychain(func(k string) { go f.selectKeychain(k) }) +} diff --git a/internal/frontend/qt/qml_users.go b/internal/frontend/qt/qml_users.go new file mode 100644 index 00000000..ae15aea8 --- /dev/null +++ b/internal/frontend/qt/qml_users.go @@ -0,0 +1,135 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +// +build build_qt + +package qt + +import ( + "github.com/ProtonMail/proton-bridge/internal/frontend/types" + "github.com/therecipe/qt/core" +) + +// QMLUserModel stores list of of users +type QMLUserModel struct { + core.QAbstractListModel + + _ map[int]*core.QByteArray `property:"roles"` + _ int `property:"count"` + _ func() `constructor:"init"` + _ func(row int) *core.QVariant `slot:"get"` + + users []*QMLUser +} + +func (um *QMLUserModel) init() { + um.SetRoles(map[int]*core.QByteArray{ + int(core.Qt__UserRole + 1): newQByteArrayFromString("object"), + }) + um.ConnectRowCount(um.rowCount) + um.ConnectData(um.data) + um.ConnectGet(um.get) + um.users = []*QMLUser{} + um.setCount() +} + +func (um *QMLUserModel) data(index *core.QModelIndex, property int) *core.QVariant { + if !index.IsValid() { + return core.NewQVariant() + } + return um.get(index.Row()) +} + +func (um *QMLUserModel) get(index int) *core.QVariant { + if index < 0 || index >= um.rowCount(nil) { + return core.NewQVariant() + } + return um.users[index].ToVariant() +} + +func (um *QMLUserModel) rowCount(*core.QModelIndex) int { + return len(um.users) +} + +func (um *QMLUserModel) setCount() { + um.SetCount(len(um.users)) +} + +func (um *QMLUserModel) addUser(user *QMLUser) { + um.BeginInsertRows(core.NewQModelIndex(), um.rowCount(nil), um.rowCount(nil)) + um.users = append(um.users, user) + um.setCount() + um.EndInsertRows() +} + +func (um *QMLUserModel) removeUser(row int) { + um.BeginRemoveRows(core.NewQModelIndex(), row, row) + um.users = append(um.users[:row], um.users[row+1:]...) + um.setCount() + um.EndRemoveRows() +} + +func (um *QMLUserModel) clear() { + um.BeginRemoveRows(core.NewQModelIndex(), 0, um.rowCount(nil)) + um.users = []*QMLUser{} + um.setCount() + um.EndRemoveRows() +} + +func (um *QMLUserModel) indexByID(id string) int { + for i, qu := range um.users { + if id == qu.ID { + return i + } + } + return -1 +} + +// QMLUser holds data, slots and signals and for user. +type QMLUser struct { + core.QObject + + _ string `property:"username"` + _ string `property:"avatarText"` + _ bool `property:"loggedIn"` + _ bool `property:"splitMode"` + _ bool `property:"setupGuideSeen"` + _ float32 `property:"usedBytes"` + _ float32 `property:"totalBytes"` + _ string `property:"password"` + _ []string `property:"addresses"` + + _ func(makeItActive bool) `slot:"toggleSplitMode"` + _ func() `signal:"toggleSplitModeFinished"` + _ func() `slot:"logout"` + _ func(address string) `slot:"configureAppleMail"` + + ID string +} + +func (qu *QMLUser) update(user types.User) { + username := user.Username() + qu.SetAvatarText(getInitials(username)) + qu.SetUsername(username) + qu.SetLoggedIn(user.IsConnected()) + qu.SetSplitMode(!user.IsCombinedAddressMode()) + qu.SetSetupGuideSeen(true) + qu.SetUsedBytes(1.0) // TODO + qu.SetTotalBytes(10000.0) // TODO + qu.SetPassword(user.GetBridgePassword()) + qu.SetAddresses(user.GetAddresses()) +} diff --git a/internal/frontend/share/Bridge.icns b/internal/frontend/share/Bridge.icns new file mode 100644 index 00000000..3520a0eb Binary files /dev/null and b/internal/frontend/share/Bridge.icns differ diff --git a/internal/frontend/share/logo.ico b/internal/frontend/share/logo.ico new file mode 100644 index 00000000..2b0e2f0f Binary files /dev/null and b/internal/frontend/share/logo.ico differ diff --git a/internal/frontend/share/logo.svg b/internal/frontend/share/logo.svg new file mode 100644 index 00000000..dc807142 --- /dev/null +++ b/internal/frontend/share/logo.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + diff --git a/internal/users/user.go b/internal/users/user.go index b3f60932..5f9ef246 100644 --- a/internal/users/user.go +++ b/internal/users/user.go @@ -349,6 +349,7 @@ func (u *User) CheckBridgeLogin(password string) error { func (u *User) UpdateUser(ctx context.Context) error { u.lock.Lock() defer u.lock.Unlock() + defer u.listener.Emit(events.UserRefreshEvent, u.userID) _, err := u.client.UpdateUser(ctx) if err != nil { @@ -376,6 +377,7 @@ func (u *User) SwitchAddressMode() error { u.lock.Lock() defer u.lock.Unlock() + defer u.listener.Emit(events.UserRefreshEvent, u.userID) u.CloseAllConnections() @@ -414,7 +416,6 @@ func (u *User) logout() error { if wasConnected { u.listener.Emit(events.LogoutEvent, u.userID) - u.listener.Emit(events.UserRefreshEvent, u.userID) } return err @@ -425,6 +426,7 @@ func (u *User) logout() error { func (u *User) Logout() error { u.lock.Lock() defer u.lock.Unlock() + defer u.listener.Emit(events.UserRefreshEvent, u.userID) u.log.Debug("Logging out user") diff --git a/internal/users/user_credentials_test.go b/internal/users/user_credentials_test.go index df994fed..6c4cb030 100644 --- a/internal/users/user_credentials_test.go +++ b/internal/users/user_credentials_test.go @@ -41,6 +41,7 @@ func TestUpdateUser(t *testing.T) { m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), m.credentialsStore.EXPECT().UpdateEmails("user", []string{testPMAPIAddress.Email}).Return(testCredentials, nil), + m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"), ) r.NoError(t, user.UpdateUser(context.Background())) @@ -68,6 +69,7 @@ func TestUserSwitchAddressMode(t *testing.T) { m.pmapiClient.EXPECT().CountMessages(gomock.Any(), "").Return([]*pmapi.MessagesCount{}, nil), m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), m.credentialsStore.EXPECT().SwitchAddressMode("user").Return(testCredentialsSplit, nil), + m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"), ) // Check switch to split mode. @@ -85,6 +87,7 @@ func TestUserSwitchAddressMode(t *testing.T) { m.pmapiClient.EXPECT().CountMessages(gomock.Any(), "").Return([]*pmapi.MessagesCount{}, nil), m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), m.credentialsStore.EXPECT().SwitchAddressMode("user").Return(testCredentials, nil), + m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"), ) // Check switch to combined mode. @@ -105,6 +108,7 @@ func TestLogoutUser(t *testing.T) { m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil), m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil), m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"), + m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"), ) err := user.Logout() @@ -123,6 +127,7 @@ func TestLogoutUserFailsLogout(t *testing.T) { m.credentialsStore.EXPECT().Logout("user").Return(nil, errors.New("logout failed")), m.credentialsStore.EXPECT().Delete("user").Return(nil), m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"), + m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"), ) err := user.Logout() diff --git a/internal/users/user_new_test.go b/internal/users/user_new_test.go index b3b4045b..215856f8 100644 --- a/internal/users/user_new_test.go +++ b/internal/users/user_new_test.go @@ -52,8 +52,8 @@ func TestNewUserUnlockFails(t *testing.T) { m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil), m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil), m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me"), - m.eventListener.EXPECT().Emit(events.LogoutEvent, "user"), m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user"), + m.eventListener.EXPECT().Emit(events.LogoutEvent, "user"), ) checkNewUserHasCredentials(m, "failed to unlock user: bad password", testCredentialsDisconnected) diff --git a/internal/users/users.go b/internal/users/users.go index 01056515..b6b21c63 100644 --- a/internal/users/users.go +++ b/internal/users/users.go @@ -351,6 +351,7 @@ func (u *Users) ClearData() error { func (u *Users) DeleteUser(userID string, clearStore bool) error { u.lock.Lock() defer u.lock.Unlock() + defer u.events.Emit(events.UserRefreshEvent, userID) log := log.WithField("user", userID) diff --git a/internal/users/users_clear_test.go b/internal/users/users_clear_test.go index 91800e85..af54fb7d 100644 --- a/internal/users/users_clear_test.go +++ b/internal/users/users_clear_test.go @@ -32,6 +32,9 @@ func TestClearData(t *testing.T) { users := testNewUsersWithUsers(t, m) defer cleanUpUsersData(users) + m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user") + m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "users") + m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "users@pm.me") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "anotheruser@pm.me") diff --git a/internal/users/users_delete_test.go b/internal/users/users_delete_test.go index 0aee1052..de9b32cd 100644 --- a/internal/users/users_delete_test.go +++ b/internal/users/users_delete_test.go @@ -38,7 +38,9 @@ func TestDeleteUser(t *testing.T) { m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil), m.credentialsStore.EXPECT().Delete("user").Return(nil), ) + m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") + m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user") err := users.DeleteUser("user", true) r.NoError(t, err) @@ -61,7 +63,9 @@ func TestDeleteUserWithFailingLogout(t *testing.T) { m.credentialsStore.EXPECT().Delete("user").Return(nil), ) + m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me") + m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user") err := users.DeleteUser("user", true) r.NoError(t, err) diff --git a/internal/users/users_new_test.go b/internal/users/users_new_test.go index c00c42b1..1d286bef 100644 --- a/internal/users/users_new_test.go +++ b/internal/users/users_new_test.go @@ -91,6 +91,7 @@ func TestNewUsersWithConnectedUserWithBadToken(t *testing.T) { m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil) m.credentialsStore.EXPECT().Logout("user").Return(testCredentialsDisconnected, nil) + m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user") m.eventListener.EXPECT().Emit(events.LogoutEvent, "user") m.eventListener.EXPECT().Emit(events.UserRefreshEvent, "user") m.eventListener.EXPECT().Emit(events.CloseConnectionEvent, "user@pm.me")