Skip to content

Commit

Permalink
add in-app and system notifications (#540)
Browse files Browse the repository at this point in the history
- add toast notification implementation to notification page
- move toast layout code to toast.go
- add toast to load and common structs
- remove previous toast implementation

* implement system notification

* stack toast layout on modals layout

* add delay speeds to toast notification

- add NotifyError method
- add comments to Notify methods
- call NotifyError for error messages in layout code and Notify for success
  messages.
  • Loading branch information
oshorefueled committed Aug 19, 2021
1 parent 269ab3f commit 8652943
Show file tree
Hide file tree
Showing 16 changed files with 208 additions and 69 deletions.
31 changes: 6 additions & 25 deletions ui/load/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,29 @@
// the code be isolated in the package you're calling it from? Is it really needed by other packages in the ui package?
// or you're just planning for a use case that might never used.

// todo: fix toast notifications
// todo: clean up NewLoad method

package load

import (
"image"
"time"

"golang.org/x/exp/shiny/materialdesign/icons"
"golang.org/x/text/language"
"golang.org/x/text/message"

"gioui.org/io/key"
"gioui.org/op/paint"
"gioui.org/widget"

"github.com/planetdecred/dcrlibwallet"
"github.com/planetdecred/godcr/ui/decredmaterial"
"github.com/planetdecred/godcr/ui/notification"
"github.com/planetdecred/godcr/wallet"
"golang.org/x/exp/shiny/materialdesign/icons"
"golang.org/x/text/message"
)

type DCRUSDTBittrex struct {
LastTradeRate string
}

type Toast struct {
text string
success bool
Timer *time.Timer
}

type Receiver struct {
InternalLog chan string
NotificationsUpdate chan interface{}
Expand Down Expand Up @@ -77,7 +69,7 @@ type Load struct {

Icons Icons

Toast *Toast
Toast *notification.Toast

SelectedWallet *int
SelectedAccount *int
Expand Down Expand Up @@ -195,7 +187,7 @@ func NewLoad(th *decredmaterial.Theme, decredIcons map[string]image.Image) *Load
Icons: ic,
WL: wl,
Receiver: r,
Toast: &Toast{},
Toast: notification.NewToast(th),

Printer: message.NewPrinter(language.English),
}
Expand All @@ -209,14 +201,3 @@ func (l *Load) RefreshTheme() {
l.Theme.SwitchDarkMode(isDarkModeOn)
}
}

func (l *Load) CreateToast(text string, success bool) {
l.Toast = &Toast{
text: text,
success: success,
}
}

func (l *Load) DestroyToast() {
l.Toast = nil
}
54 changes: 54 additions & 0 deletions ui/notification/system.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package notification

import (
"fmt"
"os"
"path"
"path/filepath"

"github.com/gen2brain/beeep"
)

const (
icon = "ui/assets/decredicons/qrcodeSymbol.png"
title = "Godcr"
)

type SystemNotification struct {
iconPath string
message string
}

func NewSystemNotification() (*SystemNotification, error) {
absolutePath, err := getAbsolutePath()
if err != nil {
return nil, err
}

return &SystemNotification{
iconPath: filepath.Join(absolutePath, icon),
}, nil
}

func (s *SystemNotification) Notify(message string) error {
err := beeep.Notify(title, message, s.iconPath)
if err != nil {
return err
}

return nil
}

func getAbsolutePath() (string, error) {
ex, err := os.Executable()
if err != nil {
return "", fmt.Errorf("error getting executable path: %s", err.Error())
}

exSym, err := filepath.EvalSymlinks(ex)
if err != nil {
return "", fmt.Errorf("error getting filepath after evaluating sym links")
}

return path.Dir(exSym), nil
}
114 changes: 114 additions & 0 deletions ui/notification/toast.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package notification

import (
"sync"
"time"

"gioui.org/layout"
"gioui.org/op"

"github.com/planetdecred/godcr/ui/decredmaterial"
"github.com/planetdecred/godcr/ui/values"
)

type (
C = layout.Context
D = layout.Dimensions
)

type Toast struct {
sync.Mutex
theme *decredmaterial.Theme
success bool
message string
timer *time.Timer
}

type duration int32

const (
Short duration = iota
Long
)

func NewToast(th *decredmaterial.Theme) *Toast {
return &Toast{
theme: th,
}
}

func getDurationFromDelay(d duration) time.Duration {
if d == Long {
return 5 * time.Second
}
return 2 * time.Second
}

// Notify is called to display a message indicating a successful action. It updates the toast object with the toast message
// and duration. The duration parameter is optional.
func (t *Toast) Notify(message string, d ...duration) {
t.notify(message, true, d...)
}

// Notify is called to display a message indicating a failed action. It updates the toast object with the toast message
// and duration. The duration parameter is optional.
func (t *Toast) NotifyError(message string, d ...duration) {
t.notify(message, false, d...)
}

// notify updates notification parameters on the toast object. It takes the message, success and duration
// parameters.
func (t *Toast) notify(message string, success bool, d ...duration) {
var notificationDelay duration
if len(d) > 0 {
notificationDelay = d[0]
}

t.Lock()
t.message = message
t.success = success
t.timer = time.NewTimer(getDurationFromDelay(notificationDelay))
t.Unlock()
}

func (t *Toast) Layout(gtx layout.Context) layout.Dimensions {
t.handleToastDisplay(gtx)
if t.timer == nil {
return layout.Dimensions{}
}

color := t.theme.Color.Success
if !t.success {
color = t.theme.Color.Danger
}

card := t.theme.Card()
card.Color = color
return layout.Center.Layout(gtx, func(gtx C) D {
return layout.Inset{Top: values.MarginPadding65}.Layout(gtx, func(gtx C) D {
return card.Layout(gtx, func(gtx C) D {
return layout.Inset{
Top: values.MarginPadding7, Bottom: values.MarginPadding7,
Left: values.MarginPadding15, Right: values.MarginPadding15,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
msg := t.theme.Body1(t.message)
msg.Color = t.theme.Color.Surface
return msg.Layout(gtx)
})
})
})
})
}

func (t *Toast) handleToastDisplay(gtx layout.Context) {
if t.timer == nil {
return
}

select {
case <-t.timer.C:
t.timer = nil
op.InvalidateOp{}.Add(gtx.Ops)
default:
}
}
2 changes: 1 addition & 1 deletion ui/page/privacy_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ func (pg *PrivacyPage) showModalPasswordStartAccountMixer() {
return
}
pm.Dismiss()
// common.notify("Start Successfully", true)
pg.Toast.Notify("Start Successfully")
}()

return false
Expand Down
2 changes: 1 addition & 1 deletion ui/page/proposal/proposal_vote_modal.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func (vm *voteModal) sendVotes() {
return
}
pm.Dismiss()
vm.CreateToast("Vote sent successfully, refreshing proposals!", true)
vm.Toast.Notify("Vote sent successfully, refreshing proposals!")
go vm.WL.MultiWallet.Politeia.Sync()
vm.Dismiss()
}()
Expand Down
4 changes: 2 additions & 2 deletions ui/page/seed_backup_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -532,13 +532,13 @@ func (pg *BackupPage) Handle() {
errMessage := "Failed to verify. Please go through every word and try again."
s := strings.Join(pg.selectedSeeds, " ")
if !dcrlibwallet.VerifySeed(s) {
pg.CreateToast(errMessage, false)
pg.Toast.NotifyError(errMessage)
return
}

err := pg.wal.VerifyWalletSeedPhrase(pg.info.Wallets[*pg.selectedWallet].ID, s, pg.privpass)
if err != nil {
pg.CreateToast(errMessage, false)
pg.Toast.NotifyError(errMessage)
return
}
pg.info.Wallets[*pg.selectedWallet].Seed = nil
Expand Down
2 changes: 1 addition & 1 deletion ui/page/send/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ func (pg *Page) feeEstimationError(err string) {
pg.amount.setError(invalidAmountErr)
} else {
pg.amount.setError(err)
pg.CreateToast("Error estimating transaction: "+err, false)
pg.Toast.NotifyError("Error estimating transaction: " + err)
}

pg.clearEstimates()
Expand Down
4 changes: 2 additions & 2 deletions ui/page/send/send_confirm_modal.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ func (scm *sendConfirmModal) broadcastTransaction() {
_, err := scm.authoredTxData.txAuthor.Broadcast([]byte(password))
scm.isSending = false
if err != nil {
scm.CreateToast(err.Error(), false)
scm.Toast.NotifyError(err.Error())
return
}
scm.CreateToast("Transaction sent!", true)
scm.Toast.Notify("Transaction sent!")

scm.txSent()
scm.Dismiss()
Expand Down
4 changes: 2 additions & 2 deletions ui/page/settings_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,10 +507,10 @@ func (pg *SettingsPage) Handle() {
case err := <-pg.errorReceiver:
if err.Error() == dcrlibwallet.ErrInvalidPassphrase {
e := "Password is incorrect"
pg.CreateToast(e, false)
pg.Toast.NotifyError(e)
return
}
pg.CreateToast(err.Error(), false)
pg.Toast.NotifyError(err.Error())
default:
}
}
Expand Down
6 changes: 3 additions & 3 deletions ui/page/start_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ package page
import (
"os"

"github.com/planetdecred/godcr/ui/load"
"github.com/planetdecred/godcr/ui/modal"

"gioui.org/layout"
"gioui.org/text"
"gioui.org/widget"

"github.com/planetdecred/dcrlibwallet"
"github.com/planetdecred/godcr/ui/decredmaterial"
"github.com/planetdecred/godcr/ui/load"
"github.com/planetdecred/godcr/ui/modal"
"github.com/planetdecred/godcr/ui/values"
)

Expand Down
10 changes: 5 additions & 5 deletions ui/page/tickets/purchase_modal.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func (tp *ticketPurchaseModal) OnResume() {
tp.initializeAccountSelector()
err := tp.accountSelector.SelectFirstWalletValidAccount()
if err != nil {
tp.CreateToast(err.Error(), false)
tp.Toast.NotifyError(err.Error())
}

tp.vspSelector = newVSPSelector(tp.Load).title("Select a vsp")
Expand Down Expand Up @@ -226,23 +226,23 @@ func (tp *ticketPurchaseModal) createNewVSPD() {
selectedVSP := tp.vspSelector.SelectedVSP()
vspd, err := tp.WL.NewVSPD(selectedVSP.Host, selectedAccount.WalletID, selectedAccount.Number)
if err != nil {
tp.CreateToast(err.Error(), false)
tp.Toast.NotifyError(err.Error())
}
tp.vsp = vspd
}

func (tp *ticketPurchaseModal) purchaseTickets(password []byte) {
tp.Dismiss()
tp.CreateToast(fmt.Sprintf("attempting to purchase %v ticket(s)", tp.ticketCount()), true)
tp.Toast.Notify(fmt.Sprintf("attempting to purchase %v ticket(s)", tp.ticketCount()))

go func() {
account := tp.accountSelector.SelectedAccount()
err := tp.WL.PurchaseTicket(account.WalletID, uint32(tp.ticketCount()), password, tp.vsp)
if err != nil {
tp.CreateToast(err.Error(), false)
tp.Toast.NotifyError(err.Error())
return
}
tp.CreateToast(fmt.Sprintf("%v ticket(s) purchased successfully", tp.ticketCount()), true)
tp.Toast.Notify(fmt.Sprintf("%v ticket(s) purchased successfully", tp.ticketCount()))
}()
}

Expand Down
2 changes: 1 addition & 1 deletion ui/page/tickets/vsp_selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ func (v *vspSelectorModal) Handle() {
go func() {
err := v.WL.AddVSP(v.inputVSP.Editor.Text())
if err != nil {
v.CreateToast(err.Error(), false)
v.Toast.NotifyError(err.Error())
} else {
v.inputVSP.Editor.SetText("")
}
Expand Down

0 comments on commit 8652943

Please sign in to comment.