diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 7a78b40..4c64d60 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -5,13 +5,13 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build for linux run: make docker-build-linux - name: Upload binary artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: katzen path: ./katzen @@ -20,40 +20,40 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build for linux (alpine) run: make distro=alpine docker-build-linux - name: Upload binary artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: katzen.alpine path: ./katzen - - name: Build for Nix - run: make docker-build-nix + #- name: Build for Nix + # run: make docker-build-nix - - name: Save output artifact name - run: ls nix_build | head -1 > nixos.output + #- name: Save output artifact name + # run: ls nix_build | head -1 > nixos.output - - name: Upload binary artifact - uses: actions/upload-artifact@v3 - with: - name: nixos.output - path: nixos.output + #- name: Upload binary artifact + # uses: actions/upload-artifact@v4 + # with: + # name: nixos.output + # path: nixos.output build_windows: runs-on: ubuntu-22.04 steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build for windows run: make docker-build-windows - name: Upload binary artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: katzen.exe path: ./katzen.exe @@ -61,18 +61,18 @@ jobs: build_macos: strategy: matrix: - go-version: [1.19.x] + go-version: [1.22.x] os: [macos-12] runs-on: ${{ matrix.os }} steps: - name: Install Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build for MacOS (amd64) run: CGO_CFLAGS_ALLOW="-DPARAMS=sphincs-shake-256f" CGO_ENABLED=1 GOOS="darwin" GOARCH="amd64" go build -trimpath -ldflags=-buildid= -tags dynamic -o katzen-macos-amd64 @@ -81,13 +81,13 @@ jobs: run: CGO_CFLAGS_ALLOW="-DPARAMS=sphincs-shake-256f" CGO_ENABLED=1 GOOS="darwin" GOARCH="arm64" go build -trimpath -ldflags=-buildid= -tags dynamic -o katzen-macos-arm64 - name: Upload binary artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: katzen-macos-amd64 path: ./katzen-macos-amd64 - name: Upload binary artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: katzen-macos-arm64 path: ./katzen-macos-arm64 @@ -96,13 +96,13 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build for android run: make KEYSTORE=reproducible.keystore KEYPASS=reproducible docker-build-android - name: Upload binary artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: katzen.apk path: ./katzen.apk @@ -112,40 +112,40 @@ jobs: needs: [build_linux, build_other_linuxes, build_windows, build_macos, build_android] steps: - name: Download katzen linux - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: katzen - name: Download katzen windows - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: katzen.exe - name: Download katzen android - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: katzen.apk - name: Download katzen macos - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: katzen-macos-amd64 - name: Download katzen macos - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: katzen-macos-arm64 - - name: Download nixos output name - uses: actions/download-artifact@v3 - with: - name: nixos.output + #- name: Download nixos output name + # uses: actions/download-artifact@v4 + # with: + # name: nixos.output - name: Hash and commit run: | sha256sum katzen katzen.apk katzen-macos-arm64 katzen-macos-amd64 katzen.exe > katzen.sha256 - echo -n "# the nixos output was: " >> katzen.sha256 - cat nixos.output >> katzen.sha256 + # echo -n "# the nixos output was: " >> katzen.sha256 + # cat nixos.output >> katzen.sha256 - name: Create release id: create_release diff --git a/.gitignore b/.gitignore index cde53e6..13a9a30 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ sign.keystore # Nix build symlinks result + +# cache +cache/ diff --git a/Makefile b/Makefile index 0d17fa7..e1c6655 100644 --- a/Makefile +++ b/Makefile @@ -3,20 +3,27 @@ warped?=false ldflags=-buildid= -X github.com/katzenpost/katzenpost/core/epochtime.WarpedEpoch=${warped} -X github.com/katzenpost/katzenpost/server/internal/pki.WarpedEpoch=${warped} -X github.com/katzenpost/katzenpost/minclient/pki.WarpedEpoch=${warped} KEYSTORE := sign.keystore KEYPASS := password -go_package_cache_dir := /tmp/katzen_go_package_cache +# gogio requires a version string ("%d.%d.%d.%d", &sv.Major, &sv.Minor, &sv.Patch, &sv.VersionCode) +# this is katzen v1 with katzenpost v0.0.35 +VERSION := 1.35.0 +# this is the app store application version code that must incrememnt with each official release +VERSIONCODE := 1 +cache_dir=cache # you can say, eg, 'make go_package_cache_arg= docker-shell' to not use the package cache -go_package_cache_arg := -v $(go_package_cache_dir):/go/pkg +go_package_cache_arg := -v $(shell readlink -f .)/$(cache_dir)/go:/go/ -e GOCACHE=/go/cache docker_run_cmd=run --rm -v "$(shell readlink -f .)":/go/katzen/ $(go_package_cache_arg) --workdir /go/katzen -e CGO_CFLAGS_ALLOW="-DPARAMS=sphincs-shake-256f" distro=debian -$(go_package_cache_dir): - mkdir -p $(go_package_cache_dir) +$(cache_dir): $(cache_dir)/go + +$(cache_dir)/go: + mkdir -p $(cache_dir)/go docker-build-linux: docker-$(distro)-base @([ "$(distro)" = "debian" ] || [ "$(distro)" = "alpine" ]) || \ (echo "can only docker-build-linux for debian or alpine, not $(distro)" && false) - $(docker) $(docker_run_cmd) katzen/$(distro)_base bash -c 'cd /go/katzen/; go mod tidy; go build -trimpath -ldflags="${ldflags}"' + $(docker) $(docker_run_cmd) katzen/$(distro)_base go build -trimpath -ldflags="${ldflags}" docker-build-windows: docker-debian-base @if [ "$(distro)" != "debian" ]; then \ @@ -33,34 +40,41 @@ docker-android-base: $(KEYSTORE): $(docker) $(docker_run_cmd) katzen/android_sdk bash -c "keytool -genkey -keystore $(KEYSTORE) -storepass ${KEYPASS} -alias android -keyalg RSA -keysize 2048 -validity 10000 -noprompt -dname CN=android" -docker-build-android: $(go_package_cache_dir) docker-android-base $(KEYSTORE) +docker-build-android: $(cache_dir) docker-android-base $(KEYSTORE) @if [ "$(distro)" != "debian" ]; then \ echo "can only docker-build-android on debian"; \ false; \ fi - $(docker) $(docker_run_cmd) katzen/android_sdk bash -c "cd replace-gogio && go install gioui.org/cmd/gogio && cd .. && gogio -arch arm64,amd64 -x -target android -appid chat.katzen -version 1 -signkey $(KEYSTORE) -signpass ${KEYPASS} ." + $(docker) $(docker_run_cmd) katzen/android_sdk bash -c "cd replace-gogio && go install gioui.org/cmd/gogio && cd .. && gogio -arch arm64,amd64 -x -target android -appid chat.katzen -version $(VERSION).$(VERSIONCODE) -signkey $(KEYSTORE) -signpass ${KEYPASS} ." # this builds the debian base image, ready to have the golang deps installed -docker-debian-base: $(go_package_cache_dir) +docker-debian-base: $(cache_dir) if ! $(docker) images|grep katzen/debian_base; then \ - $(docker) run --replace --name katzen_debian_base docker.io/golang:bullseye bash -c "echo -e 'deb https://deb.debian.org/debian bullseye main\ndeb https://deb.debian.org/debian bullseye-updates main\ndeb https://deb.debian.org/debian-security bullseye-security main' > /etc/apt/sources.list && cat /etc/apt/sources.list && apt update && apt upgrade -y && apt install -y --no-install-recommends build-essential libgles2 libgles2-mesa-dev libglib2.0-dev libxkbcommon-dev libxkbcommon-x11-dev libglu1-mesa-dev libxcursor-dev libwayland-dev libx11-xcb-dev libvulkan-dev gcc-mingw-w64-x86-64" \ + $(docker) run --replace --name katzen_debian_base docker.io/golang:bookworm bash -c "echo -e 'deb https://deb.debian.org/debian bookworm main\ndeb https://deb.debian.org/debian bookworm-updates main\ndeb https://deb.debian.org/debian-security bookworm-security main' > /etc/apt/sources.list && cat /etc/apt/sources.list && apt update && apt upgrade -y && apt install -y --no-install-recommends build-essential libgles2 libgles2-mesa-dev libglib2.0-dev libxkbcommon-dev libxkbcommon-x11-dev libglu1-mesa-dev libxcursor-dev libwayland-dev libx11-xcb-dev libvulkan-dev gcc-mingw-w64-x86-64" \ && $(docker) commit katzen_debian_base katzen/debian_base \ && $(docker) rm katzen_debian_base; \ fi -docker-nix-base: $(go_package_cache_dir) - if ! $(docker) images|grep katzen/nix_base; then \ - $(docker) run --replace --name katzen_nix_base \ - -v "$(shell readlink -f .)":/katzen/ --workdir /katzen \ - nixos/nix:master nix \ - --extra-experimental-features flakes \ - --extra-experimental-features nix-command \ - develop --command true \ +docker-nix-base.stamp: $(cache_dir) + $(docker) run --replace --name katzen_nix_base \ + -v "$(shell readlink -f .)":/katzen/ --workdir /katzen \ + docker.io/nixos/nix:master nix \ + --extra-experimental-features flakes \ + --extra-experimental-features nix-command \ + develop --command true \ && $(docker) commit katzen_nix_base katzen/nix_base \ - && $(docker) rm katzen_nix_base; \ - fi - -docker-build-nix: docker-nix-base + && $(docker) rm katzen_nix_base + touch $@ + +docker-nix-flake-update: docker-nix-base.stamp + $(docker) pull docker.io/nixos/nix:master + $(docker) run --rm -v "$(shell readlink -f .)":/katzen/ --workdir /katzen \ + docker.io/nixos/nix:master nix \ + --extra-experimental-features flakes \ + --extra-experimental-features nix-command \ + flake update -L + +docker-build-nix: docker-nix-base.stamp # this is for testing and updating the vendorHash (manually, after running go mod...). # actual nix users should see README (FIXME put nix command in README) @mkdir -p nix_build @@ -71,7 +85,7 @@ docker-build-nix: docker-nix-base build . -L \ && cp -rp $$(readlink result) nix_build/' -docker-alpine-base: $(go_package_cache_dir) +docker-alpine-base: $(cache_dir) @if ! $(docker) images|grep katzen/alpine_base; then \ $(docker) run --replace --name katzen_alpine_base docker.io/golang:alpine \ sh -c 'apk add bash gcc musl-dev libxkbcommon-dev pkgconf wayland-dev \ @@ -101,11 +115,11 @@ docker-android-shell: docker-android-base $(docker) $(docker_run_cmd) --rm -it katzen/android_sdk bash docker-clean: - -chmod -R 755 $(go_package_cache_dir) ./go_package_cache -rm -vf result -rm -rvf nix_build - -rm -rvf $(go_package_cache_dir) + -rm -rvf $(cache_dir) -rm -rvf ./go_package_cache # for users of old versions of this makefile + -rm -fv *.stamp -$(docker) rm katzen_debian_base -$(docker) rm katzen_alpine_base -$(docker) rm katzen_nix_base diff --git a/avatar.go b/avatar.go index aa95893..eaaba95 100644 --- a/avatar.go +++ b/avatar.go @@ -198,17 +198,17 @@ func (p *AvatarPicker) Layout(gtx layout.Context) layout.Dimensions { } func (p *AvatarPicker) Event(gtx C) interface{} { - if p.up.Clicked() { + if p.up.Clicked(gtx) { if u, err := filepath.Abs(filepath.Join(p.path, "..")); err == nil { return ChooseAvatarPath{nickname: p.nickname, path: u} } } - if p.back.Clicked() { + if p.back.Clicked(gtx) { return BackEvent{} } - for _, e := range p.avatar.Events(gtx.Queue) { - if e.Type == gesture.TypeClick { + if e, ok := p.avatar.Update(gtx.Source); ok { + if e.Kind == gesture.KindClick { ct := Contactal{} ct.Reset() sz := image.Point{X: gtx.Dp(96), Y: gtx.Dp(96)} @@ -223,8 +223,8 @@ func (p *AvatarPicker) Event(gtx C) interface{} { } for filename, click := range p.clicks { - for _, e := range click.Events(gtx.Queue) { - if e.Type == gesture.TypeClick { + if e, ok := click.Update(gtx.Source); ok { + if e.Kind == gesture.KindClick { // if it is a directory path - change the path // if it is a file path, return the file selection event if u, err := filepath.Abs(filepath.Join(p.path, filename)); err == nil { diff --git a/cache/go.mod b/cache/go.mod new file mode 100644 index 0000000..e69de29 diff --git a/contact.go b/contact.go index bb99d23..7942ea2 100644 --- a/contact.go +++ b/contact.go @@ -7,6 +7,8 @@ import ( "fmt" "gioui.org/gesture" "gioui.org/io/clipboard" + "gioui.org/io/key" + "gioui.org/io/transfer" "gioui.org/layout" "gioui.org/op/clip" "gioui.org/op/paint" @@ -15,15 +17,16 @@ import ( "gioui.org/widget/material" "github.com/benc-uk/gofract/pkg/colors" "github.com/benc-uk/gofract/pkg/fractals" - "github.com/katzenpost/katzenpost/catshadow" "github.com/katzenpost/katzenpost/core/crypto/rand" qrcode "github.com/skip2/go-qrcode" "golang.org/x/exp/shiny/materialdesign/icons" "image" "image/png" + "io" mrand "math/rand" "runtime" - "sort" + "strings" + "sync" ) // AddContactComplete is emitted when catshadow.NewContact has been called @@ -131,6 +134,7 @@ type AddContactPage struct { secret *widget.Editor submit *widget.Clickable cancel *widget.Clickable + initOnce *sync.Once } // Layout returns a simple centered layout prompting user for contact nickname and secret @@ -140,6 +144,13 @@ func (p *AddContactPage) Layout(gtx layout.Context) layout.Dimensions { Inset: layout.Inset{}, } + // set the default window focus to nickname entry on first layout + p.initOnce.Do(func() { + if len(p.nickname.Text()) == 0 { + gtx.Execute(key.FocusCmd{Tag: p.nickname}) + } + }) + return bg.Layout(gtx, func(gtx C) D { return layout.Flex{Axis: layout.Vertical, Alignment: layout.End}.Layout(gtx, layout.Rigid(func(gtx C) D { @@ -203,68 +214,72 @@ func (p *AddContactPage) Layout(gtx layout.Context) layout.Dimensions { // Event catches the widget submit events and calls catshadow.NewContact func (p *AddContactPage) Event(gtx layout.Context) interface{} { - if p.back.Clicked() { + if p.back.Clicked(gtx) { return BackEvent{} } - for _, ev := range p.nickname.Events() { + if ev, ok := p.nickname.Update(gtx); ok { switch ev.(type) { case widget.SubmitEvent: - p.secret.Focus() + gtx.Execute(key.FocusCmd{Tag: p.secret}) } } - for _, e := range p.newQr.Events(gtx.Queue) { - if e.Type == gesture.TypeClick { + if ev, ok := p.newQr.Update(gtx.Source); ok { + if ev.Kind == gesture.KindClick { p.contactal.Reset() p.secret.SetText(p.contactal.SharedSecret) } } - if p.copy.Clicked() { - clipboard.WriteOp{Text: p.secret.Text()}.Add(gtx.Ops) - return nil + if p.copy.Clicked(gtx) { + gtx.Execute(clipboard.WriteCmd{ + Data: io.NopCloser(strings.NewReader(p.secret.Text())), + }) } - if p.paste.Clicked() { - clipboard.ReadOp{Tag: p}.Add(gtx.Ops) + if p.paste.Clicked(gtx) { + gtx.Execute(clipboard.ReadCmd{Tag: p}) } - for _, e := range gtx.Events(p) { - ce := e.(clipboard.Event) - p.secret.SetText(ce.Text) - p.contactal.SharedSecret = ce.Text - return RedrawEvent{} + if ev, ok := gtx.Event(transfer.TargetFilter{Target: p, Type: "application/text"}); ok { + switch e := ev.(type) { + case transfer.DataEvent: + f := e.Open() + defer f.Close() + if b, err := io.ReadAll(f); err == nil { + p.secret.SetText(string(b)) + p.contactal.SharedSecret = string(b) + } + } } - for _, e := range p.newAvatar.Events(gtx.Queue) { - if e.Type == gesture.TypeClick { + if ev, ok := p.newAvatar.Update(gtx.Source); ok { + if ev.Kind == gesture.KindClick { p.contactal = NewContactal() p.secret.SetText(p.contactal.SharedSecret) - return RedrawEvent{} } } - for _, ev := range p.secret.Events() { + if ev, ok := p.secret.Update(gtx); ok { switch ev.(type) { case widget.SubmitEvent: p.submit.Click() case widget.ChangeEvent: p.contactal.SharedSecret = p.secret.Text() - return RedrawEvent{} } } - if p.cancel.Clicked() { + if p.cancel.Clicked(gtx) { return BackEvent{} } - if p.submit.Clicked() { + if p.submit.Clicked(gtx) { if len(p.secret.Text()) < minPasswordLen { p.secret.SetText("") - p.secret.Focus() + gtx.Execute(key.FocusCmd{Tag: p.secret}) return nil } if len(p.nickname.Text()) == 0 { - p.nickname.Focus() + gtx.Execute(key.FocusCmd{Tag: p.nickname}) return nil } @@ -304,7 +319,8 @@ func newAddContactPage(a *App) *AddContactPage { // generate random avatar parameters p.contactal = NewContactal() p.secret.SetText(p.contactal.SharedSecret) - p.nickname.Focus() + + p.initOnce = new(sync.Once) return p } @@ -338,41 +354,6 @@ func (p *AddContactPage) layoutQr(gtx C) D { } -type sortedContacts []*catshadow.Contact - -func (s sortedContacts) Less(i, j int) bool { - // sorts contacts with messages most-recent-first, followed by contacts - // without messages alphabetically - if s[i].LastMessage == nil && s[j].LastMessage == nil { - return s[i].Nickname < s[j].Nickname - } else if s[i].LastMessage == nil { - return false - } else if s[j].LastMessage == nil { - return true - } else { - return s[i].LastMessage.Timestamp.After(s[j].LastMessage.Timestamp) - } -} -func (s sortedContacts) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} -func (s sortedContacts) Len() int { - return len(s) -} - -func getSortedContacts(a *App) (contacts sortedContacts) { - if a.c == nil { - return - } - - // returns map[string]*Contact - for _, contact := range a.c.GetContacts() { - contacts = append(contacts, contact) - } - sort.Sort(contacts) - return -} - func button(th *material.Theme, button *widget.Clickable, icon *widget.Icon) material.IconButtonStyle { return material.IconButtonStyle{ Background: th.Palette.Bg, diff --git a/conversation.go b/conversation.go index 471d72e..6f255b8 100644 --- a/conversation.go +++ b/conversation.go @@ -5,14 +5,17 @@ import ( "github.com/katzenpost/katzenpost/catshadow" "golang.org/x/exp/shiny/materialdesign/icons" "image" + "io" "runtime" "strings" "time" "gioui.org/gesture" "gioui.org/io/clipboard" + "gioui.org/io/event" "gioui.org/io/key" "gioui.org/io/pointer" + "gioui.org/io/transfer" "gioui.org/layout" "gioui.org/op/clip" "gioui.org/unit" @@ -60,69 +63,21 @@ type EditContact struct { } func (c *conversationPage) Event(gtx layout.Context) interface{} { - // receive keystroke to editor panel - for _, ev := range c.compose.Events() { - switch ev.(type) { + // check for editor SubmitEvents + if e, ok := c.compose.Update(gtx); ok { + switch e.(type) { case widget.SubmitEvent: c.send.Click() + case widget.ChangeEvent: } } - for _, ev := range c.msgpaste.Events(gtx.Queue) { - switch ev.Type { - case LongPressed: - clipboard.ReadOp{Tag: c}.Add(gtx.Ops) - return RedrawEvent{} - default: - // return focus to the editor - c.compose.Focus() - } - } - key.InputOp{Tag: c, Keys: shortcuts}.Add(gtx.Ops) - for _, e := range gtx.Events(c) { - switch e := e.(type) { - case key.Event: - if e.Name == key.NameEscape && e.State == key.Release { - return BackEvent{} - } - if e.Name == key.NameF5 && e.State == key.Release { - return EditContact{nickname: c.nickname} - } - if e.Name == key.NameUpArrow && e.State == key.Release { - messageList.ScrollToEnd = false - if messageList.Position.First > 0 { - messageList.Position.First = messageList.Position.First - 1 - } - } - if e.Name == key.NameDownArrow && e.State == key.Release { - messageList.ScrollToEnd = true - messageList.Position.First = messageList.Position.First + 1 - } - if e.Name == key.NamePageUp && e.State == key.Release { - messageList.ScrollToEnd = false - if messageList.Position.First-messageList.Position.Count > 0 { - messageList.Position.First = messageList.Position.First - messageList.Position.Count - } + // catch clicks on send button, update list view position to bottom + if c.send.Clicked(gtx) { + messageList.ScrollToEnd = true + // XXX: could do this in Layout where we know the # of messages + messageList.ScrollTo(0x1 << 32) - } - if e.Name == key.NamePageDown && e.State == key.Release { - messageList.ScrollToEnd = true - messageList.Position.First = messageList.Position.First + messageList.Position.Count - } - return RedrawEvent{} - - case clipboard.Event: - if c.compose.SelectionLen() > 0 { - c.compose.Delete(1) // deletes the selection as a single rune - } - start, _ := c.compose.Selection() - txt := c.compose.Text() - c.compose.SetText(txt[:start] + e.Text + txt[start:]) - c.compose.Focus() - } - } - - if c.send.Clicked() { msg := []byte(c.compose.Text()) c.compose.SetText("") if len(msg) == 0 { @@ -136,36 +91,93 @@ func (c *conversationPage) Event(gtx layout.Context) interface{} { msgId := c.a.c.SendMessage(c.nickname, msg) return MessageSent{nickname: c.nickname, msgId: msgId} } - for _, e := range c.edit.Events(gtx.Queue) { - if e.Type == gesture.TypeClick { + + // check for long press + if e, ok := c.msgpaste.Update(gtx); ok { + if e.Type == LongPressed { + gtx.Source.Execute(clipboard.ReadCmd{Tag: c.msgpaste}) + } else { // LongPressCancelled + gtx.Execute(key.FocusCmd{Tag: c.compose}) + } + } + + // catch clipboard transfer triggered by long press and update composition + if ev, ok := gtx.Event(transfer.TargetFilter{Target: c.msgpaste, Type: "application/text"}); ok { + switch e := ev.(type) { + case transfer.DataEvent: + f := e.Open() + defer f.Close() + if b, err := io.ReadAll(f); err == nil { + if c.compose.SelectionLen() > 0 { + c.compose.Delete(1) // deletes the selection as a single rune + } + start, _ := c.compose.Selection() + txt := c.compose.Text() + c.compose.SetText(txt[:start] + string(b) + txt[start:]) + gtx.Execute(key.FocusCmd{Tag: c.compose}) + } + } + } + + if e, ok := c.edit.Update(gtx.Source); ok { + if e.Kind == gesture.KindClick { return EditContact{nickname: c.nickname} } } - if c.back.Clicked() { + if c.back.Clicked(gtx) { return BackEvent{} } - if c.msgcopy.Clicked() { - clipboard.WriteOp{Text: string(c.messageClicked.Plaintext)}.Add(gtx.Ops) + if c.msgcopy.Clicked(gtx) { + gtx.Source.Execute(clipboard.WriteCmd{ + Data: io.NopCloser(strings.NewReader(string(c.messageClicked.Plaintext))), + }) c.messageClicked = nil - return nil } - if c.msgdetails.Clicked() { + if c.msgdetails.Clicked(gtx) { c.messageClicked = nil // not implemented } for msg, click := range c.messageClicks { - for _, e := range click.Events(gtx.Queue) { - if e.Type == gesture.TypeClick { - c.messageClicked = msg - } + if _, ok := click.Update(gtx.Source); ok { + c.messageClicked = msg } } - for _, e := range c.cancel.Events(gtx.Queue) { - if e.Type == gesture.TypeClick { - c.messageClicked = nil + if _, ok := c.cancel.Update(gtx.Source); ok { + c.messageClicked = nil + } + + /* XXX: doesn't seem to work yet + if e, ok := readMenuKeys(c, gtx); ok { + if e.Name == key.NameEscape && e.State == key.Release { + return BackEvent{} } + if e.Name == key.NameF5 && e.State == key.Release { + return EditContact{nickname: c.nickname} + } + if e.Name == key.NameUpArrow && e.State == key.Release { + messageList.ScrollToEnd = false + if messageList.Position.First > 0 { + messageList.Position.First = messageList.Position.First - 1 + } + } + if e.Name == key.NameDownArrow && e.State == key.Release { + messageList.ScrollToEnd = true + messageList.Position.First = messageList.Position.First + 1 + } + if e.Name == key.NamePageUp && e.State == key.Release { + messageList.ScrollToEnd = false + if messageList.Position.First-messageList.Position.Count > 0 { + messageList.Position.First = messageList.Position.First - messageList.Position.Count + } + } + if e.Name == key.NamePageDown && e.State == key.Release { + messageList.ScrollToEnd = true + messageList.Position.First = messageList.Position.First + messageList.Position.Count + } + return RedrawEvent{} } + */ return nil } @@ -227,12 +239,12 @@ func layoutMessage(gtx C, msg *catshadow.Message, isSelected bool, expires time. } func (c *conversationPage) Layout(gtx layout.Context) layout.Dimensions { + // set focus on composition + gtx.Execute(key.FocusCmd{Tag: c.compose}) contact := c.a.c.GetContacts()[c.nickname] if n, ok := notifications[c.nickname]; ok { - if c.a.focus { - n.Cancel() - delete(notifications, c.nickname) - } + n.Cancel() + delete(notifications, c.nickname) } messages := c.a.c.GetSortedConversation(c.nickname) expires, _ := c.a.c.GetExpiration(c.nickname) @@ -369,7 +381,7 @@ func (c *conversationPage) Layout(gtx layout.Context) layout.Dimensions { a := clip.Rect(image.Rectangle{Max: dims.Size}) x := a.Push(gtx.Ops) defer x.Pop() - c.msgpaste.Add(gtx.Ops) + event.Op(gtx.Ops, c.msgpaste) return dims }), layout.Rigid(func(gtx C) D { @@ -398,6 +410,5 @@ func newConversationPage(a *App, nickname string) *conversationPage { send: &widget.Clickable{}, edit: new(gesture.Click), } - p.compose.Focus() return p } diff --git a/editcontact.go b/editcontact.go index 83a601c..4b96cf5 100644 --- a/editcontact.go +++ b/editcontact.go @@ -30,8 +30,8 @@ type EditContactPage struct { } const ( - minExpiration = 0.0 // never delete messages - maxExpiration = 14.0 // 2 weeks + minExpiration = float32(0.0) // never delete messages + maxExpiration = float32(14.0) // 2 weeks ) // Layout returns the contact options menu @@ -77,30 +77,42 @@ type RenameContact struct { nickname string } +func valueToDuration(val float32) time.Duration { + // multiply by the maximum range, in days + duration := val * maxExpiration + // round to a multiple of days + duration = float32(math.Round(float64(duration))) + // update the slider to a rounded value + return time.Duration(int64(duration) * int64(time.Hour) * 24) +} + +func durationToValue(dur time.Duration) float32 { + // convert duration to days + fdur := float64(int64(dur) / (int64(time.Hour) * 24)) + // round to a multiple of days and return the scaled slider value + return float32(math.Round(fdur)) / maxExpiration +} + // Event catches the widget submit events and calls catshadow.NewContact func (p *EditContactPage) Event(gtx layout.Context) interface{} { - if p.back.Clicked() { + if p.back.Clicked(gtx) { return BackEvent{} } - for _, e := range p.avatar.Events(gtx.Queue) { - if e.Type == gesture.TypeClick { - return ChooseAvatar{nickname: p.nickname} - } + if _, ok := p.avatar.Update(gtx.Source); ok { + return ChooseAvatar{nickname: p.nickname} } - if p.clear.Clicked() { + if p.clear.Clicked(gtx) { // TODO: confirmation dialog p.a.c.WipeConversation(p.nickname) return EditContactComplete{nickname: p.nickname} } - if p.expiry.Changed() { - p.expiry.Value = float32(math.Round(float64(p.expiry.Value))) + if p.expiry.Update(gtx) { + p.duration = valueToDuration(p.expiry.Value) } - // update duration - p.duration = time.Duration(int64(p.expiry.Value)) * time.Minute * 60 * 24 - if p.rename.Clicked() { + if p.rename.Clicked(gtx) { return RenameContact{nickname: p.nickname} } - if p.remove.Clicked() { + if p.remove.Clicked(gtx) { // TODO: confirmation dialog p.a.c.RemoveContact(p.nickname) p.a.c.DeleteBlob("avatar://" + p.nickname) @@ -108,7 +120,7 @@ func (p *EditContactPage) Event(gtx layout.Context) interface{} { delete(avatars, p.nickname) return EditContactComplete{nickname: p.nickname} } - if p.apply.Clicked() { + if p.apply.Clicked(gtx) { p.a.c.ChangeExpiration(p.nickname, p.duration) return BackEvent{} } @@ -119,14 +131,14 @@ func (p *EditContactPage) Start(stop <-chan struct{}) { } func newEditContactPage(a *App, contact string) *EditContactPage { - expiry, _ := a.c.GetExpiration(contact) p := &EditContactPage{a: a, nickname: contact, back: &widget.Clickable{}, avatar: &gesture.Click{}, clear: &widget.Clickable{}, expiry: &widget.Float{}, rename: &widget.Clickable{}, remove: &widget.Clickable{}, apply: &widget.Clickable{}, settings: &layout.List{Axis: layout.Vertical}, } - p.expiry.Value = float32(math.Round(float64(expiry) / float64(time.Minute*60*24))) + p.duration, _ = a.c.GetExpiration(contact) + p.expiry.Value = durationToValue(p.duration) p.widgets = []layout.Widget{ func(gtx C) D { dims := layout.Center.Layout(gtx, func(gtx C) D { @@ -141,14 +153,14 @@ func newEditContactPage(a *App, contact string) *EditContactPage { layout.Spacer{Height: unit.Dp(8)}.Layout, func(gtx C) D { var label string - if p.expiry.Value < 1.0 { + if p.expiry.Value < 1.0/maxExpiration { label = "Delete after: never" } else { label = "Delete after: " + durafmt.Parse(p.duration).Format(units) } return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle, Spacing: layout.SpaceBetween}.Layout(gtx, layout.Rigid(material.Body2(th, "Message deletion").Layout), - layout.Rigid(material.Slider(th, p.expiry, minExpiration, maxExpiration).Layout), + layout.Rigid(material.Slider(th, p.expiry).Layout), layout.Rigid(material.Caption(th, label).Layout), ) }, diff --git a/events.go b/events.go new file mode 100644 index 0000000..f7d1a17 --- /dev/null +++ b/events.go @@ -0,0 +1,24 @@ +//go:build !android + +package main + +import ( + "gioui.org/app" + "gioui.org/io/key" + "errors" +) + +// handleGioEvents +func (a *App) handleGioEvents(e interface{}) error { + switch e := e.(type) { + case key.FocusEvent: + // XXX: figure out what this is useful for + case app.DestroyEvent: + return errors.New("system.DestroyEvent receieved") + case app.FrameEvent: + gtx := app.NewContext(a.ops, e) + a.Layout(gtx) + e.Frame(gtx.Ops) + } + return nil +} diff --git a/events_android.go b/events_android.go new file mode 100644 index 0000000..26e1daa --- /dev/null +++ b/events_android.go @@ -0,0 +1,29 @@ +//go:build android + +package main + +import ( + "errors" + "gioui.org/app" + "gioui.org/io/key" +) + +// handleGioEvents starts and stops the android foreground service when +// AndroidViewEvents indicate that the application has a view or not. +func (a *App) handleGioEvents(e interface{}) error { + switch e := e.(type) { + case key.FocusEvent: + // XXX: figure out what this is useful for + case app.DestroyEvent: + return errors.New("system.DestroyEvent receieved") + case app.FrameEvent: + gtx := app.NewContext(a.ops, e) + a.Layout(gtx) + e.Frame(gtx.Ops) + case app.AndroidViewEvent: + if e.View == 0 { + return errors.New("app.AndroidViewEvent nil received") + } + } + return nil +} diff --git a/flake.lock b/flake.lock index ccd9b6d..c4cd3cd 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1681215634, - "narHash": "sha256-dI0SsSWb7ksK3OtTCf61vdlBhok838aIpwQ2EUM+jhY=", + "lastModified": 1726583932, + "narHash": "sha256-zACxiQx8knB3F8+Ze+1BpiYrI+CbhxyWpcSID9kVhkQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5a156c2e89c1eca09b40bcdcee86760e0e4d79a9", + "rev": "658e7223191d2598641d50ee4e898126768fe847", "type": "github" }, "original": { diff --git a/gesture.go b/gesture.go index cc970ac..43f0c1c 100644 --- a/gesture.go +++ b/gesture.go @@ -3,9 +3,8 @@ package main import ( "time" - "gioui.org/io/event" "gioui.org/io/pointer" - "gioui.org/op" + "gioui.org/layout" ) // LongPressType represents a successful or cancelled LongPress action. @@ -37,39 +36,28 @@ type LongPress struct { callback func() } -// Add the handler to the operation list to receive click events. -func (l *LongPress) Add(ops *op.Ops) { - op := pointer.InputOp{ - Tag: l, - Types: pointer.Press | pointer.Release | pointer.Leave, - } - op.Add(ops) -} - -// Events returns the next click event, if any. -func (l *LongPress) Events(q event.Queue) []LongPressEvent { - var events []LongPressEvent +// Update returns the next click event, if any. +func (l *LongPress) Update(gtx layout.Context) (*LongPressEvent, bool) { // consume pointer events and start or stop a timer - for _, evt := range q.Events(l) { - e, ok := evt.(pointer.Event) - if !ok { - continue - } - switch e.Type { - case pointer.Press: - l.pressedAt = time.Now() - l.timer = time.NewTimer(l.detectAt) - time.AfterFunc(l.detectAt, l.callback) - l.pressed = true - case pointer.Cancel, pointer.Release, pointer.Leave: - if l.pressed { - l.pressed = false - l.pressedFor = time.Now().Sub(l.pressedAt) - if !l.timer.Stop() { - <-l.timer.C + filt := pointer.Filter{Target: l, Kinds: pointer.Press | pointer.Release | pointer.Leave} + if e, ok := gtx.Event(filt); ok { + if e, ok := e.(pointer.Event); ok { + switch e.Kind { + case pointer.Press: + l.pressedAt = time.Now() + l.timer = time.NewTimer(l.detectAt) + time.AfterFunc(l.detectAt, l.callback) + l.pressed = true + case pointer.Cancel, pointer.Release, pointer.Leave: + if l.pressed { + l.pressed = false + l.pressedFor = time.Now().Sub(l.pressedAt) + if !l.timer.Stop() { + <-l.timer.C + } + l.timer = nil + return &LongPressEvent{Type: LongPressCancelled}, true } - l.timer = nil - events = append(events, LongPressEvent{Type: LongPressCancelled}) } } } @@ -82,14 +70,13 @@ func (l *LongPress) Events(q event.Queue) []LongPressEvent { if l.pressed { l.pressed = false l.pressedFor = time.Now().Sub(l.pressedAt) - events = append(events, LongPressEvent{Type: LongPressed}) + return &LongPressEvent{Type: LongPressed}, true } default: } } - return events - + return &LongPressEvent{}, false } // NewLongPress returns a LongPress that triggers after the duration diff --git a/go.mod b/go.mod index f962e87..303b4c4 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,13 @@ module github.com/katzenpost/katzen -go 1.19 +go 1.21 -replace gioui.org => github.com/mixmasala/gio v0.0.0-20230917185150-1c97118905a5 +toolchain go1.22.6 + +replace gioui.org => github.com/mixmasala/gio v0.0.0-20240830054638-20227d2a5fc2 require ( - gioui.org v0.3.0 + gioui.org v0.7.0 gioui.org/x v0.3.0 github.com/benc-uk/gofract v0.0.0-20211012214247-47caccaf3aac github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b @@ -18,7 +20,7 @@ require ( require ( filippo.io/edwards25519 v1.0.0 // indirect gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect - gioui.org/shader v1.0.7 // indirect + gioui.org/shader v1.0.8 // indirect git.sr.ht/~jackmordaunt/go-toast v1.0.0 // indirect git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 // indirect github.com/BurntSushi/toml v1.2.1 // indirect @@ -29,7 +31,7 @@ require ( github.com/esiqveland/notify v0.11.0 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 // indirect + github.com/go-text/typesetting v0.1.1 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/henrydcase/nobs v0.0.0-20210422124615-3a8ac85da11b // indirect diff --git a/go.sum b/go.sum index 850ebbb..7c3e964 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,12 @@ eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY= +eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA= filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc= gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= -gioui.org/shader v1.0.7 h1:fDoor1Id/tRxoIpzBSAr5TBo6QfSkMTOmdbMEyWDgGE= -gioui.org/shader v1.0.7/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= +gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA= +gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= gioui.org/x v0.3.0 h1:XupHLCT2D1G8qvXxN/EoX+IMpDiG5CHokg6j33rSH9I= gioui.org/x v0.3.0/go.mod h1:Y/VG4cEJuj938VSzbN21oDg/ZgpKa/P9ipwtTSL1vbU= git.sr.ht/~jackmordaunt/go-toast v1.0.0 h1:bbRox6VkotdOj3QcWimZQ84APoszIsA/pSIj8ypDdV8= @@ -36,9 +37,10 @@ github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrt github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo= -github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= -github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI= +github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo= +github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI= +github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY= +github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -49,6 +51,7 @@ github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hajimehoshi/bitmapfont v1.2.0/go.mod h1:h9QrPk6Ktb2neObTlAbma6Ini1xgMjbJ3w7ysmD7IOU= github.com/hajimehoshi/ebiten v1.11.2/go.mod h1:aDEhx0K9gSpXw3Cxf2hCXDxPSoF8vgjNqKxrZa/B4Dg= github.com/hajimehoshi/go-mp3 v0.2.1/go.mod h1:Rr+2P46iH6PwTPVgSsEwBkon0CK5DxCAeX/Rp65DCTE= @@ -74,6 +77,7 @@ github.com/katzenpost/nyquist v0.0.0-20230504173433-e12f6b943410/go.mod h1:9EM6K github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -82,8 +86,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= -github.com/mixmasala/gio v0.0.0-20230917185150-1c97118905a5 h1:kvQeHCNOJSsx6lTbmyzeAV0gwUVLoVKOUmtPMQCWjHw= -github.com/mixmasala/gio v0.0.0-20230917185150-1c97118905a5/go.mod h1:qNrPZk3UNMLs9mQbpUw8WvjxEzo1AJiRXQPOUuYpeSk= +github.com/mixmasala/gio v0.0.0-20240830054638-20227d2a5fc2 h1:HYtwHpvR8maM1iu8EFIYwp9VLkNMlbBba2V+Fr/yFOU= +github.com/mixmasala/gio v0.0.0-20240830054638-20227d2a5fc2/go.mod h1:19wZxaNP+eHN4H2YdZwEfbkAAgoYB5rcIbDHo4BqUl4= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= @@ -102,6 +106,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/ugorji/go/codec v1.2.8 h1:sgBJS6COt0b/P40VouWKdseidkDgHxYGm0SAglUHfP0= github.com/ugorji/go/codec v1.2.8/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= diff --git a/home.go b/home.go index f859478..e47c3f1 100644 --- a/home.go +++ b/home.go @@ -3,12 +3,12 @@ package main import ( "bytes" "encoding/base64" + "gioui.org/font" "gioui.org/gesture" "gioui.org/io/key" "gioui.org/layout" "gioui.org/op/clip" "gioui.org/op/paint" - "gioui.org/font" "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" @@ -18,14 +18,15 @@ import ( "golang.org/x/exp/shiny/materialdesign/icons" "image" "image/png" + "sort" "strings" + "sync" "time" ) var ( contactList = &layout.List{Axis: layout.Vertical, ScrollToEnd: false} selectedIdx = 0 - kb = false connectIcon, _ = widget.NewIcon(icons.DeviceSignalWiFi4Bar) disconnectIcon, _ = widget.NewIcon(icons.DeviceSignalWiFiOff) settingsIcon, _ = widget.NewIcon(icons.ActionSettings) @@ -33,21 +34,13 @@ var ( logo = getLogo() units, _ = durafmt.UnitsCoder{PluralSep: ":", UnitsSep: ","}.Decode("y:y,w:w,d:d,h:h,m:m,s:s,ms:ms,us:us") avatars = make(map[string]layout.Widget) - shortcuts = key.Set(key.NameUpArrow + "|" + - key.NameDownArrow + "|" + - key.NamePageUp + "|" + - key.NamePageDown + "|" + - key.NameReturn + "|" + - key.NameEscape + "|" + - key.NameF1 + "|" + // show help page (not implemented) - key.NameF2 + "|" + // add contact - key.NameF3 + "|" + // show client settings - key.NameF4 + "|" + // toggle connection status - key.NameF5) // edit contact ) type HomePage struct { + l *sync.Mutex a *App + updateCh chan interface{} + contacts []*catshadow.Contact addContact *widget.Clickable connect *widget.Clickable showSettings *widget.Clickable @@ -59,7 +52,7 @@ type AddContactClick struct{} type ShowSettingsClick struct{} func (p *HomePage) Layout(gtx layout.Context) layout.Dimensions { - contacts := getSortedContacts(p.a) + contacts := p.contacts // xxx do not request this every frame... bg := Background{ Color: th.Bg, @@ -75,14 +68,12 @@ func (p *HomePage) Layout(gtx layout.Context) layout.Dimensions { } // re-center list view for keyboard contact selection - if kb { - if selectedIdx < contactList.Position.First || selectedIdx >= contactList.Position.First+contactList.Position.Count { - // list doesn't wrap around view to end, so do not give negative value for First - if selectedIdx < contactList.Position.Count-1 { - contactList.Position.First = 0 - } else { - contactList.Position.First = (selectedIdx - contactList.Position.Count + 1) - } + if selectedIdx < contactList.Position.First || selectedIdx >= contactList.Position.First+contactList.Position.Count { + // list doesn't wrap around view to end, so do not give negative value for First + if selectedIdx < contactList.Position.Count-1 { + contactList.Position.First = 0 + } else { + contactList.Position.First = (selectedIdx - contactList.Position.Count + 1) } } @@ -118,7 +109,7 @@ func (p *HomePage) Layout(gtx layout.Context) layout.Dimensions { // if the layout is selected, change background color bg := Background{Inset: in} - if kb && i == selectedIdx { + if i == selectedIdx { bg.Color = th.ContrastBg } else { bg.Color = th.Bg @@ -149,21 +140,22 @@ func (p *HomePage) Layout(gtx layout.Context) layout.Dimensions { }), layout.Rigid(func(gtx C) D { return layout.Flex{Axis: layout.Vertical, Alignment: layout.Start, Spacing: layout.SpaceEnd}.Layout(gtx, - layout.Rigid(func(gtx C) D { - if contacts[i].IsPending { - return pandaIcon.Layout(gtx, th.Palette.ContrastBg) - } - return fill{th.Bg}.Layout(gtx) - }), - layout.Rigid(func(gtx C) D { - // timestamp - if lastMsg != nil { - messageAge := strings.Replace(durafmt.ParseShort(time.Now().Round(0).Sub(lastMsg.Timestamp).Truncate(time.Minute)).Format(units), "0 s", "now", 1) - return material.Caption(th, messageAge).Layout(gtx) - } - return fill{th.Bg}.Layout(gtx) - }), - )}), + layout.Rigid(func(gtx C) D { + if contacts[i].IsPending { + return pandaIcon.Layout(gtx, th.Palette.ContrastBg) + } + return fill{th.Bg}.Layout(gtx) + }), + layout.Rigid(func(gtx C) D { + // timestamp + if lastMsg != nil { + messageAge := strings.Replace(durafmt.ParseShort(time.Now().Round(0).Sub(lastMsg.Timestamp).Truncate(time.Minute)).Format(units), "0 s", "now", 1) + return material.Caption(th, messageAge).Layout(gtx) + } + return fill{th.Bg}.Layout(gtx) + }), + ) + }), ) }), // last message @@ -262,61 +254,53 @@ type OfflineClick struct { // Event returns a ChooseContactClick event when a contact is chosen func (p *HomePage) Event(gtx layout.Context) interface{} { - if p.connect.Clicked() { + if p.connect.Clicked(gtx) { if !isConnected && !isConnecting { return OnlineClick{} } return OfflineClick{} } // listen for pointer right click events on the addContact widget - if p.addContact.Clicked() { + if p.addContact.Clicked(gtx) { return AddContactClick{} } - if p.showSettings.Clicked() { + if p.showSettings.Clicked(gtx) { return ShowSettingsClick{} } for nickname, click := range p.contactClicks { - for _, e := range click.Events(gtx.Queue) { - if e.Type == gesture.TypeClick { + if e, ok := click.Update(gtx.Source); ok { + if e.Kind == gesture.KindClick { return ChooseContactClick{nickname: nickname} } } } // check for keypress events - key.InputOp{Tag: p, Keys: shortcuts}.Add(gtx.Ops) - for _, e := range gtx.Events(p) { - switch e := e.(type) { - case key.Event: - if e.Name == key.NameF1 && e.State == key.Release { - } - if e.Name == key.NameF2 && e.State == key.Release { - return AddContactClick{} - } - if e.Name == key.NameF3 && e.State == key.Release { - return ShowSettingsClick{} - } - if e.Name == key.NameF4 && e.State == key.Release { - if !isConnected { - return OnlineClick{} - } - return OfflineClick{} - } - if e.Name == key.NameUpArrow && e.State == key.Release { - kb = true - selectedIdx = selectedIdx - 1 - } - if e.Name == key.NameDownArrow && e.State == key.Release { - kb = true - selectedIdx = selectedIdx + 1 - } - if e.Name == key.NameEscape && e.State == key.Release { - kb = false + if e, ok := shortcutEvents(gtx); ok { + if e.Name == key.NameF2 { + return AddContactClick{} + } + if e.Name == key.NameF3 { + return ShowSettingsClick{} + } + if e.Name == key.NameF4 { + if !isConnected { + return OnlineClick{} } - if e.Name == key.NameReturn && e.State == key.Release { - contacts := getSortedContacts(p.a) - kb = false - return ChooseContactClick{nickname: contacts[selectedIdx].Nickname} + return OfflineClick{} + } + if e.Name == key.NameUpArrow { + selectedIdx = selectedIdx - 1 + } + if e.Name == key.NameDownArrow { + selectedIdx = selectedIdx + 1 + } + if e.Name == key.NameReturn { + p.l.Lock() + defer p.l.Unlock() + if len(p.contacts) < selectedIdx+1 { + return nil } + return ChooseContactClick{nickname: p.contacts[selectedIdx].Nickname} } } @@ -324,11 +308,47 @@ func (p *HomePage) Event(gtx layout.Context) interface{} { } func (p *HomePage) Start(stop <-chan struct{}) { + // receive commands to update the contact list, e.g. from KeyExchangeCompleted events + go func() { + for { + select { + case <-stop: + return + case <-p.updateCh: + p.UpdateContacts() + } + } + }() +} + +func (h *HomePage) UpdateContacts() { + h.l.Lock() + defer h.l.Unlock() + if h.a == nil { + return + } + if h.a.c == nil { + return + } + contacts := make(sortedContacts, 0) + + // GetContacts() returns map[string]*Contact + for _, contact := range h.a.c.GetContacts() { + contacts = append(contacts, contact) + } + sort.Sort(contacts) + h.contacts = contacts + h.a.w.Invalidate() } func newHomePage(a *App) *HomePage { + updateCh := make(chan interface{}, 1) + updateCh <- struct{}{} // prod page worker to fetch contacts from catshadow return &HomePage{ a: a, + l: new(sync.Mutex), + updateCh: updateCh, + contacts: []*catshadow.Contact{}, addContact: &widget.Clickable{}, connect: &widget.Clickable{}, showSettings: &widget.Clickable{}, diff --git a/katzen.go b/katzen.go index 344c637..e0b856a 100644 --- a/katzen.go +++ b/katzen.go @@ -3,7 +3,6 @@ package main import ( "context" _ "embed" - "errors" "flag" "fmt" "os" @@ -17,11 +16,10 @@ import ( _ "gioui.org/app/permission/foreground" _ "gioui.org/app/permission/storage" "gioui.org/font/gofont" - "gioui.org/text" - "gioui.org/io/key" - "gioui.org/io/system" + "gioui.org/io/event" "gioui.org/layout" "gioui.org/op" + "gioui.org/text" "gioui.org/unit" "gioui.org/widget/material" "gioui.org/x/notify" @@ -38,7 +36,7 @@ var ( dataDirName = "catshadow" clientConfigFile = flag.String("f", "", "Path to the client config file.") stateFile = flag.String("s", "catshadow_statefile", "Path to the client state file.") - debug = flag.Bool("d", false, "Enable golang debug service.") + debug = flag.Int("d", 0, "Enable golang debug service.") th *material.Theme @@ -61,8 +59,6 @@ type App struct { ops *op.Ops c *catshadow.Client stack pageStack - focus bool - stage system.Stage } func newApp(w *app.Window) *App { @@ -79,6 +75,21 @@ func (a *App) Layout(gtx layout.Context) { } func (a *App) update(gtx layout.Context) { + // handle global shortcuts + if backEvent(gtx) { + // XXX: this means that after signin, the top level page is homescreen + // and therefore pressing back won't logout + if a.stack.Len() > 1 { + a.stack.Pop() + return + } + } + + if a.stack.Len() == 0 { + a.stack.Push(newSignInPage(a)) + return + } + page := a.stack.Current() if e := page.Event(gtx); e != nil { switch e := e.(type) { @@ -131,6 +142,11 @@ func (a *App) update(gtx layout.Context) { a.stack.Push(newAddContactPage(a)) case AddContactComplete: a.stack.Pop() + p := a.stack.Current() + // return to home page, and update the contacts page + if h, ok := p.(*HomePage); ok { + h.UpdateContacts() + } case ChooseContactClick: a.stack.Push(newConversationPage(a, e.nickname)) case ChooseAvatar: @@ -150,12 +166,19 @@ func (a *App) update(gtx layout.Context) { } func (a *App) run() error { + // on Android, this will start a foreground service, and does nothing on other platforms + cancelForeground, err := app.Start("Background Connection", "") + if err != nil { + return err + } + defer cancelForeground() + // only read window events until client is established for { if a.c != nil { break } - e := <-a.w.Events() + e := a.w.Event() if err := a.handleGioEvents(e); err != nil { return err } @@ -167,6 +190,19 @@ func (a *App) run() error { } }() + evCh := make(chan event.Event) + ackCh := make(chan struct{}) + go func() { + for { + ev := a.w.Event() + evCh <- ev + <-ackCh + if _, ok := ev.(app.DestroyEvent); ok { + return + } + } + }() + // select from all event sources for { select { @@ -174,10 +210,12 @@ func (a *App) run() error { if err := a.handleCatshadowEvent(e); err != nil { return err } - case e := <-a.w.Events(): + case e := <-evCh: if err := a.handleGioEvents(e); err != nil { + ackCh <- struct{}{} return err } + ackCh <- struct{}{} case <-time.After(1 * time.Minute): // redraw the screen to update the message timestamps once per minute a.w.Invalidate() @@ -189,9 +227,9 @@ func main() { flag.Parse() fmt.Println("Katzenpost is still pre-alpha. DO NOT DEPEND ON IT FOR STRONG SECURITY OR ANONYMITY.") - if *debug { + if *debug != 0 { go func() { - http.ListenAndServe("localhost:8080", nil) + http.ListenAndServe(fmt.Sprintf("localhost:%d", *debug), nil) }() runtime.SetMutexProfileFraction(1) runtime.SetBlockProfileRate(1) @@ -203,8 +241,8 @@ func main() { func uiMain() { go func() { - w := app.NewWindow( - app.Size(unit.Dp(400), unit.Dp(400)), + w := new(app.Window) + w.Option(app.Size(unit.Dp(400), unit.Dp(400)), app.Title("Katzen"), app.NavigationColor(rgb(0x0)), app.StatusColor(rgb(0x0)), @@ -284,7 +322,7 @@ func (a *App) handleCatshadowEvent(e interface{}) error { case *conversationPage: // XXX: on android, input focus is not lost when the application does not have foreground // but system.Stage is changed. On desktop linux, the stage does not change, but window focus is lost. - if p.nickname == event.Nickname && a.stage == system.StageRunning && a.focus { + if p.nickname == event.Nickname { a.w.Invalidate() return nil } @@ -307,48 +345,3 @@ func (a *App) handleCatshadowEvent(e interface{}) error { a.w.Invalidate() return nil } - -func (a *App) handleGioEvents(e interface{}) error { - switch e := e.(type) { - case key.FocusEvent: - a.focus = e.Focus - case system.DestroyEvent: - return errors.New("system.DestroyEvent receieved") - case system.FrameEvent: - gtx := layout.NewContext(a.ops, e) - key.InputOp{Tag: a.w, Keys: key.NameEscape + "|" + key.NameBack}.Add(a.ops) - for _, e := range gtx.Events(a.w) { - switch e := e.(type) { - case key.Event: - if (e.Name == key.NameEscape && e.State == key.Release) || e.Name == key.NameBack { - if a.stack.Len() > 1 { - a.stack.Pop() - a.w.Invalidate() - } - } - } - } - a.Layout(gtx) - e.Frame(gtx.Ops) - case system.StageEvent: - fmt.Printf("StageEvent %s received\n", e.Stage) - a.stage = e.Stage - if e.Stage >= system.StageRunning { - if a.stack.Len() == 0 { - a.stack.Push(newSignInPage(a)) - } - } - if e.Stage == system.StagePaused { - var err error - a.endBg, err = app.Start("Is running in the background", "") - if err != nil { - return err - } - } else { - if a.endBg != nil { - a.endBg() - } - } - } - return nil -} diff --git a/katzen.nix b/katzen.nix index 60393f1..7419002 100644 --- a/katzen.nix +++ b/katzen.nix @@ -7,7 +7,7 @@ buildGoModule rec { pname = "katzen"; inherit src version; - vendorHash = "sha256-VeQBSDAgCsla93by2NPYR+zmaxqlNJPq/YxMqvet7pE="; + vendorHash = "sha256-bUap2v9fXlrVWbzTe2BRQ1T7K3OhfoMKo6iC7+qliF0="; # This hash is may drift from the actual vendoring and break the build, # see https://nixos.org/manual/nixpkgs/unstable/#ssec-language-go. @@ -55,7 +55,7 @@ buildGoModule rec { meta = with lib; { description = "Traffic analysis resistant messaging"; homepage = "https://katzenpost.mixnetworks.org/"; - license = licenses.agpl3; + license = licenses.agpl3Only; maintainers = with maintainers; [ ehmry ]; }; } diff --git a/renamecontact.go b/renamecontact.go index b492376..8913b08 100644 --- a/renamecontact.go +++ b/renamecontact.go @@ -1,6 +1,7 @@ package main import ( + "gioui.org/io/key" "gioui.org/layout" "gioui.org/widget" "gioui.org/widget/material" @@ -22,6 +23,7 @@ func (p *RenameContactPage) Layout(gtx layout.Context) layout.Dimensions { Inset: layout.Inset{}, } + gtx.Execute(key.FocusCmd{Tag: p.newnickname}) return bg.Layout(gtx, func(gtx C) D { return layout.Flex{Axis: layout.Vertical, Alignment: layout.End}.Layout(gtx, layout.Rigid(func(gtx C) D { @@ -41,16 +43,16 @@ func (p *RenameContactPage) Layout(gtx layout.Context) layout.Dimensions { // Event catches the widget submit events and calls catshadow.NewContact func (p *RenameContactPage) Event(gtx layout.Context) interface{} { - if p.back.Clicked() { + if p.back.Clicked(gtx) { return BackEvent{} } - for _, ev := range p.newnickname.Events() { + if ev, ok := p.newnickname.Update(gtx); ok { switch ev.(type) { case widget.SubmitEvent: p.submit.Click() } } - if p.submit.Clicked() { + if p.submit.Clicked(gtx) { err := p.a.c.RenameContact(p.nickname, p.newnickname.Text()) if err == nil { return EditContactComplete{} @@ -66,7 +68,6 @@ func (p *RenameContactPage) Start(stop <-chan struct{}) { func newRenameContactPage(a *App, nickname string) *RenameContactPage { p := &RenameContactPage{a: a, nickname: nickname} p.newnickname = &widget.Editor{SingleLine: true, Submit: true} - p.newnickname.Focus() p.back = &widget.Clickable{} p.submit = &widget.Clickable{} return p diff --git a/replace-gogio/go.mod b/replace-gogio/go.mod index 49cbef3..746b48b 100644 --- a/replace-gogio/go.mod +++ b/replace-gogio/go.mod @@ -1,17 +1,18 @@ module github.com/katzenpost/katzen/replace-gogio -go 1.19 +go 1.21 -replace gioui.org/cmd => github.com/mixmasala/gio-cmd v0.0.0-20230926133305-3e18ff4fc2c6 +toolchain go1.22.6 + +replace gioui.org/cmd => github.com/mixmasala/gio-cmd v0.0.0-20240830064144-f7c2488b0736 require ( - gioui.org/cmd v0.0.0-20230822165948-7cb98d0557e7 // indirect + gioui.org/cmd v0.0.0-20240830064144-f7c2488b0736 github.com/akavel/rsrc v0.10.1 // indirect - golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect - golang.org/x/mod v0.4.2 // indirect - golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect - golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect - golang.org/x/text v0.3.7 // indirect - golang.org/x/tools v0.1.0 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + golang.org/x/image v0.5.0 // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/tools v0.6.0 // indirect ) diff --git a/replace-gogio/go.sum b/replace-gogio/go.sum index b7fd626..840eb91 100644 --- a/replace-gogio/go.sum +++ b/replace-gogio/go.sum @@ -2,36 +2,64 @@ github.com/akavel/rsrc v0.10.1 h1:hCCPImjmFKVNGpeLZyTDRHEFC283DzyTXTo0cO0Rq9o= github.com/akavel/rsrc v0.10.1/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/mixmasala/gio-cmd v0.0.0-20230926133305-3e18ff4fc2c6 h1:Hq8ONfRFbPni5lahjhDB/0QuPqzoohSWpThgsLtkKfs= github.com/mixmasala/gio-cmd v0.0.0-20230926133305-3e18ff4fc2c6/go.mod h1:Ka3v7oyXel7Kmtuqwtlp05nEQzm9t4wskHnIEWojWNU= +github.com/mixmasala/gio-cmd v0.0.0-20240830064144-f7c2488b0736 h1:qikc8lB8dls+Q1KlfGwE8SNPxDbiweo4DET5tPAvRkI= +github.com/mixmasala/gio-cmd v0.0.0-20240830064144-f7c2488b0736/go.mod h1:XUN+Yxnof8LH0Xo1/l/CyIE0infHdT8dSKuOsa9a0Hg= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs= golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= +golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= diff --git a/settings.go b/settings.go index 0745f05..5a8219b 100644 --- a/settings.go +++ b/settings.go @@ -76,10 +76,10 @@ type restartClient struct{} // Event catches the widget submit events and calls Settings func (p *SettingsPage) Event(gtx layout.Context) interface{} { - if p.back.Clicked() { + if p.back.Clicked(gtx) { return BackEvent{} } - if p.switchUseTor.Changed() { + if p.switchUseTor.Update(gtx) { if p.switchUseTor.Value && !hasTor() { p.switchUseTor.Value = false p.a.c.DeleteBlob("UseTor") @@ -92,14 +92,14 @@ func (p *SettingsPage) Event(gtx layout.Context) interface{} { p.a.c.DeleteBlob("UseTor") } } - if p.switchAutoConnect.Changed() { + if p.switchAutoConnect.Update(gtx) { if p.switchAutoConnect.Value { p.a.c.AddBlob("AutoConnect", []byte{1}) } else { p.a.c.DeleteBlob("AutoConnect") } } - if p.submit.Clicked() { + if p.submit.Clicked(gtx) { go func() { if n, err := notify.Push("Restarting", "Katzen is restarting"); err == nil { <-time.After(notificationTimeout) diff --git a/shortcuts.go b/shortcuts.go new file mode 100644 index 0000000..2f98b0b --- /dev/null +++ b/shortcuts.go @@ -0,0 +1,53 @@ +package main + +import ( + "runtime" + + "gioui.org/io/event" + "gioui.org/io/key" + "gioui.org/layout" +) + +// helper to capture back / escape presses from any page +func backEvent(gtx layout.Context) bool { + filters := []event.Filter{ + key.Filter{Name: key.NameEscape}, + key.Filter{Name: key.NameBack}, + } + if ke, ok := gtx.Event(filters...); ok { + switch ke := ke.(type) { + case key.Event: + if runtime.GOOS == "android" || ke.State == key.Release { + return true + } + } + } + return false +} + +// helper to capture hotkeys used in various pages +func shortcutEvents(gtx layout.Context) (key.Event, bool) { + // hotkeys + filters := []event.Filter{ + key.Filter{Name: key.NameF1}, + key.Filter{Name: key.NameF2}, + key.Filter{Name: key.NameF3}, + key.Filter{Name: key.NameF4}, + key.Filter{Name: key.NameF5}, + key.Filter{Name: key.NameUpArrow}, + key.Filter{Name: key.NameDownArrow}, + key.Filter{Name: key.NamePageUp}, + key.Filter{Name: key.NamePageDown}, + key.Filter{Name: key.NameReturn}, + } + if ke, ok := gtx.Event(filters...); ok { + switch ke := ke.(type) { + case key.Event: + // if on android key.Release isn't implemented + if runtime.GOOS == "android" || ke.State == key.Press { + return ke, true + } + } + } + return key.Event{}, false +} diff --git a/signin.go b/signin.go index 9603472..127291b 100644 --- a/signin.go +++ b/signin.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "gioui.org/io/key" "gioui.org/layout" "gioui.org/widget" "gioui.org/widget/material" @@ -21,7 +22,7 @@ func (p *signInPage) Start(stop <-chan struct{}) { } func (p *signInPage) Layout(gtx layout.Context) layout.Dimensions { - p.password.Focus() + gtx.Execute(key.FocusCmd{Tag: p.password}) bg := Background{ Color: th.Bg, Inset: layout.Inset{}, @@ -46,14 +47,14 @@ type signInStarted struct { } func (p *signInPage) Event(gtx layout.Context) interface{} { - for _, ev := range p.password.Events() { + if ev, ok := p.password.Update(gtx); ok { switch ev.(type) { case widget.SubmitEvent: p.submit.Click() } } - if p.submit.Clicked() { + if p.submit.Clicked(gtx) { p.connecting = true pw := p.password.Text() p.password.SetText("") diff --git a/sort.go b/sort.go new file mode 100644 index 0000000..980f00d --- /dev/null +++ b/sort.go @@ -0,0 +1,27 @@ +package main + +import ( + "github.com/katzenpost/katzenpost/catshadow" +) + +type sortedContacts []*catshadow.Contact + +func (s sortedContacts) Less(i, j int) bool { + // sorts contacts with messages most-recent-first, followed by contacts + // without messages alphabetically + if s[i].LastMessage == nil && s[j].LastMessage == nil { + return s[i].Nickname < s[j].Nickname + } else if s[i].LastMessage == nil { + return false + } else if s[j].LastMessage == nil { + return true + } else { + return s[i].LastMessage.Timestamp.After(s[j].LastMessage.Timestamp) + } +} +func (s sortedContacts) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} +func (s sortedContacts) Len() int { + return len(s) +} diff --git a/spool.go b/spool.go index 9481760..261aafa 100644 --- a/spool.go +++ b/spool.go @@ -1,30 +1,30 @@ package main import ( + "gioui.org/gesture" "gioui.org/layout" - "time" "gioui.org/op/clip" - "gioui.org/x/notify" - "gioui.org/widget" - "sync" - "image" - "gioui.org/gesture" "gioui.org/unit" + "gioui.org/widget" "gioui.org/widget/material" + "gioui.org/x/notify" "github.com/katzenpost/katzenpost/catshadow" + "image" + "sync" + "time" //"gioui.org/widget/material" ) type SpoolPage struct { - a *App - provider *layout.List + a *App + provider *layout.List providerClicks map[string]*gesture.Click - connect *widget.Clickable - settings *widget.Clickable - back *widget.Clickable - submit *widget.Clickable - once *sync.Once - errCh chan error + connect *widget.Clickable + settings *widget.Clickable + back *widget.Clickable + submit *widget.Clickable + once *sync.Once + errCh chan error } func (p *SpoolPage) Start(stop <-chan struct{}) { @@ -90,7 +90,7 @@ func (p *SpoolPage) Layout(gtx layout.Context) layout.Dimensions { // if the layout is selected, change background color bg := Background{Inset: in} - if kb && i == selectedIdx { + if i == selectedIdx { bg.Color = th.ContrastBg } else { bg.Color = th.Bg @@ -118,30 +118,28 @@ func (p *SpoolPage) Layout(gtx layout.Context) layout.Dimensions { } func (p *SpoolPage) Event(gtx layout.Context) interface{} { - if p.back.Clicked() { + if p.back.Clicked(gtx) { return BackEvent{} } - if p.connect.Clicked() { + if p.connect.Clicked(gtx) { if !isConnected && !isConnecting { return OnlineClick{} } return OfflineClick{} } - if p.settings.Clicked() { + if p.settings.Clicked(gtx) { return ShowSettingsClick{} } for provider, click := range p.providerClicks { - for _, e := range click.Events(gtx.Queue) { - if e.Type == gesture.TypeClick { - provider := provider // copy reference to provider - go p.once.Do(func() { - select{ - case p.errCh <- p.a.c.CreateRemoteSpoolOn(provider): - case <-p.a.c.HaltCh(): - return - } - }) - } + if _, ok := click.Update(gtx.Source); ok { + provider := provider // copy reference to provider + go p.once.Do(func() { + select { + case p.errCh <- p.a.c.CreateRemoteSpoolOn(provider): + case <-p.a.c.HaltCh(): + return + } + }) } } select { diff --git a/stack.go b/stack.go index fcfd9e3..7794cb5 100644 --- a/stack.go +++ b/stack.go @@ -126,6 +126,9 @@ func (s *pageStack) Pop() { s.pages[i] = nil s.pages = s.pages[:i] } + if len(s.pages) > 0 { + s.start() // start new top of stack + } } func (s *pageStack) start() {