diff --git a/ui/modal/controller.go b/ui/modal/controller.go index c7e6deb6..b22bcd66 100644 --- a/ui/modal/controller.go +++ b/ui/modal/controller.go @@ -101,23 +101,22 @@ func (m *ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) { diff, isDifferent, count := participation.HasChanged(*m.transactionModal.Participation, acct.Participation) // The account is valid and we registered - if isValid && !isDifferent && m.Type == app.TransactionModal && !m.transactionModal.Active { + if isValid && !isDifferent && m.Type == app.TransactionModal && !m.transactionModal.OfflineControls { m.SetActive(true) m.infoModal.Prefix = "Successfully registered online!\n" m.HasPrefix = true m.SetType(app.InfoModal) // For the love of all that is good, please lets refactor this. Preferably with a daemon - } else if isValid && isDifferent && count != 6 && (m.Type == app.InfoModal || (m.Type == app.TransactionModal && !m.transactionModal.Active)) { + } else if isValid && isDifferent && count != 6 && (m.Type == app.InfoModal || (m.Type == app.TransactionModal && !m.transactionModal.OfflineControls)) { // It is online, has a participation key but not the one we are looking at AND all the keys are not different // (AND it's the info modal (this case we are checking on enter) OR we are waiting to register a key, and we made a mistake - // You know it's getting bad when the plugin recommendation is Grazie - // TODO: refactor this beast to have isolated state from the modal controller - - // Ahh yes, classic "Set Active to the inverse then only navigate when there is no prefix" + // Ahh yes, classic "Set Active to the inverse then only navigate when there is no prefix and it's not the dilution changing" + // Dilution is likely to match some active keys so we just ignore it. First and last rounds must be unique pairs // This is the closest thing we have to state, between this and the transaction modal state it works + // Set active ensures the offline modal is changed when a corruption happens m.SetActive(false) - if m.infoModal.Prefix == "" { + if m.infoModal.Prefix == "" && diff.VoteKeyDilution { m.infoModal.Prefix = "***WARNING***\nRegistered online but keys do not fully match\nCheck your registered keys carefully against the node keys\n\n" if diff.VoteFirstValid { m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Vote First Valid\n" @@ -141,7 +140,7 @@ func (m *ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) { m.SetType(app.InfoModal) } - } else if !isOnline && m.Type == app.TransactionModal && m.transactionModal.Active && m.transactionModal.ATxn.VotePK == nil { + } else if !isOnline && m.Type == app.TransactionModal && m.transactionModal.OfflineControls && m.transactionModal.ATxn.VotePK == nil { m.SetActive(false) m.infoModal.Prefix = "Successfully registered offline!\n" m.HasPrefix = true @@ -160,13 +159,12 @@ func (m *ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) { } if msg.Type == app.InfoModal { - m.infoModal.Prefix = msg.Prefix m.generateModal.SetStep(generate.AddressStep) } // On closing events if msg.Type == app.CloseModal { m.Open = false - m.generateModal.Input.Focus() + m.generateModal.AddressInput.Focus() } else { m.Open = true } @@ -179,7 +177,7 @@ func (m *ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) { m.Open = false m.SetType(app.InfoModal) m.generateModal.SetStep(generate.AddressStep) - m.generateModal.Input.Focus() + m.generateModal.AddressInput.Focus() case app.TransactionModal: m.SetType(app.InfoModal) case app.ExceptionModal: diff --git a/ui/modal/interfaces.go b/ui/modal/interfaces.go new file mode 100644 index 00000000..ef1bae69 --- /dev/null +++ b/ui/modal/interfaces.go @@ -0,0 +1,8 @@ +package modal + +type Modal interface { + Title() string + BorderColor() string + Controls() string + Body() string +} diff --git a/ui/modal/modal_test.go b/ui/modal/modal_test.go index 2f5c953f..b34078af 100644 --- a/ui/modal/modal_test.go +++ b/ui/modal/modal_test.go @@ -3,75 +3,18 @@ package modal import ( "bytes" "errors" - "github.com/algorandfoundation/nodekit/internal/algod/participation" "github.com/algorandfoundation/nodekit/internal/test/mock" "github.com/algorandfoundation/nodekit/ui/app" "github.com/algorandfoundation/nodekit/ui/internal/test" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/x/ansi" - "github.com/charmbracelet/x/exp/golden" "github.com/charmbracelet/x/exp/teatest" "testing" "time" ) func Test_Snapshot(t *testing.T) { - t.Run("NoKey", func(t *testing.T) { - model := New(lipgloss.NewStyle().Width(80).Height(80).Render(""), true, test.GetState(nil)) - - model, _ = model.HandleMessage(tea.WindowSizeMsg{Width: 80, Height: 40}) - got := ansi.Strip(model.View()) - golden.RequireEqual(t, []byte(got)) - }) - t.Run("InfoModal", func(t *testing.T) { - model := New(lipgloss.NewStyle().Width(80).Height(80).Render(""), true, test.GetState(nil)) - model.SetKey(&mock.Keys[0]) - model.SetType(app.InfoModal) - model, _ = model.HandleMessage(tea.WindowSizeMsg{Width: 80, Height: 40}) - got := ansi.Strip(model.View()) - golden.RequireEqual(t, []byte(got)) - }) - t.Run("ConfirmModal", func(t *testing.T) { - model := New(lipgloss.NewStyle().Width(80).Height(80).Render(""), true, test.GetState(nil)) - model.SetKey(&mock.Keys[0]) - model.SetType(app.ConfirmModal) - model, _ = model.HandleMessage(tea.WindowSizeMsg{Width: 80, Height: 40}) - got := ansi.Strip(model.View()) - golden.RequireEqual(t, []byte(got)) - }) - t.Run("ExceptionModal", func(t *testing.T) { - model := New(lipgloss.NewStyle().Width(80).Height(80).Render(""), true, test.GetState(nil)) - model.SetKey(&mock.Keys[0]) - model.SetType(app.ExceptionModal) - model, _ = model.HandleMessage(errors.New("test error")) - got := ansi.Strip(model.View()) - golden.RequireEqual(t, []byte(got)) - }) - t.Run("GenerateModal", func(t *testing.T) { - model := New(lipgloss.NewStyle().Width(80).Height(80).Render(""), true, test.GetState(nil)) - model.SetKey(&mock.Keys[0]) - model.SetAddress("ABC") - model.SetType(app.GenerateModal) - model, _ = model.HandleMessage(tea.WindowSizeMsg{Width: 80, Height: 40}) - got := ansi.Strip(model.View()) - golden.RequireEqual(t, []byte(got)) - }) - - t.Run("TransactionModal", func(t *testing.T) { - t.Skip("qa is not a priority for this project") - model := New(lipgloss.NewStyle().Width(80).Height(80).Render(""), true, test.GetState(nil)) - model.State.Status.Network = "testnet-v1.0" - model.SetShortLink(participation.ShortLinkResponse{ - Id: "1234", - }) - model.SetKey(&mock.Keys[0]) - model.SetActive(true) - model.SetType(app.TransactionModal) - model, _ = model.HandleMessage(tea.WindowSizeMsg{Width: 80, Height: 40}) - got := ansi.Strip(model.View()) - golden.RequireEqual(t, []byte(got)) - }) + t.Skip("TODO:") } func Test_Messages(t *testing.T) { diff --git a/ui/modal/model.go b/ui/modal/model.go index a5e271ea..809820d8 100644 --- a/ui/modal/model.go +++ b/ui/modal/model.go @@ -35,10 +35,10 @@ type ViewModel struct { Link *participation.ShortLinkResponse // Views - infoModal *info.ViewModel + infoModal info.ViewModel transactionModal *transaction.ViewModel - confirmModal *confirm.ViewModel - generateModal *generate.ViewModel + confirmModal confirm.ViewModel + generateModal generate.ViewModel exceptionModal *exception.ViewModel // Current Component Data @@ -51,28 +51,26 @@ type ViewModel struct { // SetAddress updates the ViewModel's Address property and synchronizes it with the associated generateModal. func (m *ViewModel) SetAddress(address string) { m.Address = address - m.generateModal.SetAddress(address) + //m.generateModal.SetAddress(address) } // SetKey updates the participation key across infoModal, confirmModal, and transactionModal in the ViewModel. func (m *ViewModel) SetKey(key *api.ParticipationKey) { m.infoModal.Participation = key - m.confirmModal.ActiveKey = key + m.confirmModal.Participation = key m.transactionModal.Participation = key } // SetActive sets the active state for both infoModal and transactionModal, and updates their respective states. func (m *ViewModel) SetActive(active bool) { - m.infoModal.Active = active - m.infoModal.UpdateState() - m.transactionModal.Active = active + m.infoModal.OfflineControls = active + m.transactionModal.OfflineControls = active m.transactionModal.UpdateState() } // SetSuspended sets the suspended state func (m *ViewModel) SetSuspended(sus bool) { m.infoModal.Suspended = sus - m.infoModal.UpdateState() m.transactionModal.Suspended = sus m.transactionModal.UpdateState() } @@ -86,22 +84,6 @@ func (m *ViewModel) SetShortLink(res participation.ShortLinkResponse) { func (m *ViewModel) SetType(modal app.ModalType) { m.Type = modal switch modal { - case app.InfoModal: - m.title = m.infoModal.Title - m.controls = m.infoModal.Controls - m.borderColor = m.infoModal.BorderColor - case app.ConfirmModal: - m.title = m.confirmModal.Title - m.controls = m.confirmModal.Controls - m.borderColor = m.confirmModal.BorderColor - case app.GenerateModal: - m.title = m.generateModal.Title - m.controls = m.generateModal.Controls - m.borderColor = m.generateModal.BorderColor - case app.TransactionModal: - m.title = m.transactionModal.Title - m.controls = m.transactionModal.Controls - m.borderColor = m.transactionModal.BorderColor case app.ExceptionModal: m.title = m.exceptionModal.Title m.controls = m.exceptionModal.Controls @@ -124,7 +106,7 @@ func New(parent string, open bool, state *algod.StateModel) *ViewModel { infoModal: info.New(state), transactionModal: transaction.New(state), - confirmModal: confirm.New(state), + confirmModal: confirm.New(state, nil), generateModal: generate.New("", state), exceptionModal: exception.New(""), diff --git a/ui/modal/testdata/Test_Snapshot/ConfirmModal.golden b/ui/modal/testdata/Test_Snapshot/ConfirmModal.golden deleted file mode 100644 index e78113bb..00000000 --- a/ui/modal/testdata/Test_Snapshot/ConfirmModal.golden +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ╭──Delete Key────────────────────────────────────────────────╮ - │ │ - │ Are you sure you want to delete this key from your node? │ - │ │ - │ Account Address: │ - │ ABC │ - │ │ - │ Participation Key: │ - │ 123 │ - │ │ - ╰────────────────────────────────────────( (y)es | (n)o )────╯ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ui/modal/testdata/Test_Snapshot/ExceptionModal.golden b/ui/modal/testdata/Test_Snapshot/ExceptionModal.golden deleted file mode 100644 index 5068e929..00000000 --- a/ui/modal/testdata/Test_Snapshot/ExceptionModal.golden +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ╭──Error─────╮ - │ test error │ - ╰─( esc )────╯ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ui/modal/testdata/Test_Snapshot/GenerateModal.golden b/ui/modal/testdata/Test_Snapshot/GenerateModal.golden deleted file mode 100644 index 11c74911..00000000 --- a/ui/modal/testdata/Test_Snapshot/GenerateModal.golden +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ╭──Generate Consensus Participation Keys─────────────────────────────────╮ - │ │ - │ Create keys required to participate in Algorand consensus. │ - │ │ - │ Account address: │ - │ > ABC │ - │ │ - ╰───────────────────────────────────────────────────( esc to cancel )────╯ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ui/modal/testdata/Test_Snapshot/InfoModal.golden b/ui/modal/testdata/Test_Snapshot/InfoModal.golden deleted file mode 100644 index 7e5459fc..00000000 --- a/ui/modal/testdata/Test_Snapshot/InfoModal.golden +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ╭──Key Information──────────────╮ - │ │ - │ Account: ABC │ - │ Participation ID: 123 │ - │ │ - │ Vote Key: VEVTVEtFWQ== │ - │ Selection Key: VEVTVEtFWQ== │ - │ State Proof Key: VEVTVEtFWQ== │ - │ │ - │ Vote First Valid: 0 │ - │ Vote Last Valid: 30000 │ - │ Vote Key Dilution: 100 │ - │ │ - ╰────( (d)elete | (o)nline )────╯ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ui/modal/testdata/Test_Snapshot/NoKey.golden b/ui/modal/testdata/Test_Snapshot/NoKey.golden deleted file mode 100644 index 6fc9abbd..00000000 --- a/ui/modal/testdata/Test_Snapshot/NoKey.golden +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ╭─────────────────╮ - │ No key selected │ - ╰─────────────────╯ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ui/modal/testdata/Test_Snapshot/TransactionModal.golden b/ui/modal/testdata/Test_Snapshot/TransactionModal.golden deleted file mode 100644 index f134ccd7..00000000 --- a/ui/modal/testdata/Test_Snapshot/TransactionModal.golden +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - ╭──Register Offline──────────────────────────────────────────╮ - │ Sign this transaction to register your account as offline │ - │ │ - │ Scan the QR code with Pera or Defly │ - │ (make sure you use the testnet-v1.0 network) │ - │ │ - │ █████████████████████████████ │ - │ ██ ▄▄▄▄▄ █▀█▄▀█▀█ ▀█ ▄▄▄▄▄ ██ │ - │ ██ █ █ ██▀▄▀ █ █ █ █ █ ██ │ - │ ██ █▄▄▄█ █▄▀▀▀█▀ ██ █▄▄▄█ ██ │ - │ ██▄▄▄▄▄▄▄█▄█▄█▄▀ ▀ █▄▄▄▄▄▄▄██ │ - │ ██▄▀ ▀ █▄▀▀ ▄█▄ █▀▀▀▀▄██ ▀██ │ - │ ███▀ ▄▀▄▄▀▀█▄█▄▀▀█▀▄▀▀▀▄█ ▄██ │ - │ ███▀█ ▀▄ █ ▄▀▄▄█▄█▀ ██▀ ▀██ │ - │ ██▄▀█ ▄▄ █▄ █▀ ██▄▀█▄▄█▄▄██ │ - │ ██▄███▄█▄▄▀▄▀█ █▀ ▄▄▄ █▀ ██ │ - │ ██ ▄▄▄▄▄ ██▄ ▀▀▀▀█ █▄█ ██ ██ │ - │ ██ █ █ █▄ ▀█ ▄▀▀ ▄▄ ▀█████ │ - │ ██ █▄▄▄█ █▄▀ █▄▀ ▀▄▀▄ ▄▄▀▄██ │ - │ ██▄▄▄▄▄▄▄█▄█▄██▄█▄▄▄▄▄███▄▄██ │ - │ │ - │ -or- │ - │ │ - │ Open this URL in your browser: │ - │ │ - │ https://b.nodekit.run/1234 │ - │ │ - │ Note: this will take effect after 320 rounds (~15 min.) │ - │ Please keep your node running during this cooldown period. │ - ╰─────────────────────────────────────────────────( esc )────╯ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ui/modal/view.go b/ui/modal/view.go index ee2b0303..63221edd 100644 --- a/ui/modal/view.go +++ b/ui/modal/view.go @@ -3,7 +3,6 @@ package modal import ( "github.com/algorandfoundation/nodekit/ui/app" "github.com/algorandfoundation/nodekit/ui/style" - "github.com/charmbracelet/lipgloss" ) // View renders the current modal's UI based on its type and state, or returns the parent content if the modal is closed. @@ -24,17 +23,6 @@ func (m ViewModel) View() string { case app.ExceptionModal: render = m.exceptionModal.View() } - width := lipgloss.Width(render) + 2 - height := lipgloss.Height(render) - return style.WithOverlay(style.WithNavigation( - m.controls, - style.WithTitle( - m.title, - style.ApplyBorder(width, height, m.borderColor). - PaddingRight(1). - PaddingLeft(1). - Render(render), - ), - ), m.Parent) + return style.WithOverlay(render, m.Parent) } diff --git a/ui/modals/confirm/confirm.go b/ui/modals/confirm/confirm.go index 8080724a..db6f3614 100644 --- a/ui/modals/confirm/confirm.go +++ b/ui/modals/confirm/confirm.go @@ -9,73 +9,106 @@ import ( "github.com/charmbracelet/lipgloss" ) -type ViewModel struct { +// Init initializes the ViewModel and returns a tea.Cmd to start the program or execute initial commands. +func (m ViewModel) Init() tea.Cmd { + return nil +} +// ViewModel represents the main structure containing state, dimensions, and participation data for view rendering. +type ViewModel struct { // Width defines the horizontal dimension of the ViewModel, typically measured in units such as characters or pixels. Width int - // Height defines the vertical dimension of the ViewModel, commonly measured in units such as characters or pixels. - Height int - Title string - Controls string - BorderColor string - ActiveKey *api.ParticipationKey - Data *algod.StateModel + Height int + // Participation is a pointer to an api.ParticipationKey representing a participation key used by the node. + Participation *api.ParticipationKey + // State is a pointer to an algod.StateModel, representing the state of the application including its configurations. + State *algod.StateModel } -func New(state *algod.StateModel) *ViewModel { - return &ViewModel{ - Width: 0, - Height: 0, - Title: "Delete Key", - BorderColor: "9", - Controls: "( " + style.Green.Render("(y)es") + " | " + style.Red.Render("(n)o") + " )", - Data: state, +// New initializes and returns a new ViewModel with specified state and participation key, setting dimensions to default values. +func New(state *algod.StateModel, participation *api.ParticipationKey) ViewModel { + return ViewModel{ + Width: 0, + Height: 0, + Participation: participation, + State: state, } } -func (m ViewModel) Init() tea.Cmd { - return nil -} - +// Update processes a message, updates the ViewModel state, and returns the updated model along with a possible command. func (m ViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.HandleMessage(msg) } -func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) { + +// HandleMessage processes incoming messages, updates ViewModel state, and returns the updated model alongside a command. +func (m ViewModel) HandleMessage(msg tea.Msg) (ViewModel, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "esc", "n": - return &m, app.EmitModalEvent(app.ModalEvent{ + return m, app.EmitModalEvent(app.ModalEvent{ Type: app.CancelModal, }) case "y": var ( cmds []tea.Cmd ) - cmds = append(cmds, app.EmitDeleteKey(m.Data.Context, m.Data.Client, m.ActiveKey.Id)) - return &m, tea.Batch(cmds...) + cmds = append(cmds, app.EmitDeleteKey(m.State.Context, m.State.Client, m.Participation.Id)) + return m, tea.Batch(cmds...) } case tea.WindowSizeMsg: m.Width = msg.Width m.Height = msg.Height } - return &m, nil + return m, nil +} + +// Title returns the static title string "Delete Key" for the ViewModel. +func (m ViewModel) Title() string { + return "Delete Key" } -func (m ViewModel) View() string { - if m.ActiveKey == nil { - return "No key selected" - } - return renderDeleteConfirmationModal(m.ActiveKey) +// BorderColor returns the border color as a string, typically used for rendering styled components in the ViewModel. +func (m ViewModel) BorderColor() string { + return "9" } -func renderDeleteConfirmationModal(partKey *api.ParticipationKey) string { +// Controls returns a formatted string displaying the available control options (yes or no) with styled color representations. +func (m ViewModel) Controls() string { + return "( " + style.Green.Render("(y)es") + " | " + style.Red.Render("(n)o") + " )" +} + +// Body returns the formatted body content of the ViewModel, including participation key details or a default message. +func (m ViewModel) Body() string { + if m.Participation == nil { + return "No key selected" + } return lipgloss.NewStyle().Padding(1).Render(lipgloss.JoinVertical(lipgloss.Center, "Are you sure you want to delete this key from your node?\n", style.Cyan.Render("Account Address:"), - partKey.Address+"\n", + m.Participation.Address+"\n", style.Cyan.Render("Participation Key:"), - partKey.Id, + m.Participation.Id, )) + +} + +// View renders the ViewModel as a styled string, incorporating title, controls, and body content with dynamic borders. +func (m ViewModel) View() string { + body := m.Body() + width := lipgloss.Width(body) + height := lipgloss.Height(body) + return style.WithNavigation( + m.Controls(), + style.WithTitle( + m.Title(), + // Apply the Borders with the Padding + style.ApplyBorder(width+2, height-4, m.BorderColor()). + PaddingRight(1). + PaddingLeft(1). + Render(m.Body()), + ), + ) + } diff --git a/ui/modals/confirm/confirm_test.go b/ui/modals/confirm/confirm_test.go index 6f6799de..7a496c0f 100644 --- a/ui/modals/confirm/confirm_test.go +++ b/ui/modals/confirm/confirm_test.go @@ -13,11 +13,11 @@ import ( ) func Test_New(t *testing.T) { - m := New(test.GetState(nil)) - if m.ActiveKey != nil { + m := New(test.GetState(nil), nil) + if m.Participation != nil { t.Errorf("expected ActiveKey to be nil") } - m.ActiveKey = &mock.Keys[0] + m.Participation = &mock.Keys[0] // Handle Delete m, cmd := m.HandleMessage(tea.KeyMsg{ Type: tea.KeyRunes, @@ -30,13 +30,12 @@ func Test_New(t *testing.T) { } func Test_Snapshot(t *testing.T) { t.Run("NoKey", func(t *testing.T) { - model := New(test.GetState(nil)) + model := New(test.GetState(nil), nil) got := ansi.Strip(model.View()) golden.RequireEqual(t, []byte(got)) }) t.Run("Visible", func(t *testing.T) { - model := New(test.GetState(nil)) - model.ActiveKey = &mock.Keys[0] + model := New(test.GetState(nil), &mock.Keys[0]) model, _ = model.HandleMessage(tea.WindowSizeMsg{Width: 80, Height: 40}) got := ansi.Strip(model.View()) golden.RequireEqual(t, []byte(got)) @@ -45,8 +44,7 @@ func Test_Snapshot(t *testing.T) { func Test_Messages(t *testing.T) { // Create the Model - m := New(test.GetState(nil)) - m.ActiveKey = &mock.Keys[0] + m := New(test.GetState(nil), &mock.Keys[0]) tm := teatest.NewTestModel( t, m, teatest.WithInitialTermSize(80, 40), diff --git a/ui/modals/confirm/testdata/Test_Snapshot/NoKey.golden b/ui/modals/confirm/testdata/Test_Snapshot/NoKey.golden index eacdfa5a..3575044b 100644 --- a/ui/modals/confirm/testdata/Test_Snapshot/NoKey.golden +++ b/ui/modals/confirm/testdata/Test_Snapshot/NoKey.golden @@ -1 +1,3 @@ -No key selected \ No newline at end of file +╭──Delete Key─────╮ +│ No key selected │ +╰─────────────────╯ \ No newline at end of file diff --git a/ui/modals/confirm/testdata/Test_Snapshot/Visible.golden b/ui/modals/confirm/testdata/Test_Snapshot/Visible.golden index 290e2e59..aad56627 100644 --- a/ui/modals/confirm/testdata/Test_Snapshot/Visible.golden +++ b/ui/modals/confirm/testdata/Test_Snapshot/Visible.golden @@ -1,9 +1,11 @@ - - Are you sure you want to delete this key from your node? - - Account Address: - ABC - - Participation Key: - 123 - \ No newline at end of file +╭──Delete Key────────────────────────────────────────────────╮ +│ │ +│ Are you sure you want to delete this key from your node? │ +│ │ +│ Account Address: │ +│ ABC │ +│ │ +│ Participation Key: │ +│ 123 │ +│ │ +╰────────────────────────────────────────( (y)es | (n)o )────╯ \ No newline at end of file diff --git a/ui/modals/generate/controller.go b/ui/modals/generate/controller.go index cd5e0f66..19ad987d 100644 --- a/ui/modals/generate/controller.go +++ b/ui/modals/generate/controller.go @@ -13,44 +13,33 @@ import ( tea "github.com/charmbracelet/bubbletea" ) +// Init initializes the ViewModel by batching commands for text input blinking and spinner ticking. func (m ViewModel) Init() tea.Cmd { return tea.Batch(textinput.Blink, spinner.Tick) } +// Update processes incoming messages, updating the ViewModel state and returning a new model and command. func (m ViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.HandleMessage(msg) } -func (m *ViewModel) SetStep(step Step) { - m.Step = step - switch m.Step { - case AddressStep: - m.Controls = "( esc to cancel )" - m.Title = DefaultTitle - m.InputError = "" - m.BorderColor = DefaultBorderColor - case DurationStep: - m.Controls = "( (s)witch range )" - m.Title = "Validity Range" - m.InputTwo.SetValue("") - m.InputTwo.Focus() - m.InputTwo.PromptStyle = focusedStyle - m.InputTwo.TextStyle = focusedStyle - m.InputTwoError = "" - m.Input.Blur() - case WaitingStep: - m.Controls = "" - m.Title = "Generating Keys" - m.BorderColor = "9" - } -} - -func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) { +// HandleMessage processes incoming messages, updates the ViewModel state, and returns an updated model and command. +func (m ViewModel) HandleMessage(msg tea.Msg) (ViewModel, tea.Cmd) { var ( cmd tea.Cmd cmds []tea.Cmd ) switch msg := msg.(type) { + // Account selection from list + case app.AccountSelected: + if msg.Address != m.Address { + m.Reset(msg.Address) + } + // Event triggered the Generate Modal + case app.ModalEvent: + if msg.Type == app.GenerateModal { + m.Reset(msg.Address) + } case tea.WindowSizeMsg: m.Width = msg.Width m.Height = msg.Height @@ -58,7 +47,7 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) { switch msg.String() { case "esc": if m.Step != WaitingStep { - return &m, app.EmitModalEvent(app.ModalEvent{ + return m, app.EmitModalEvent(app.ModalEvent{ Type: app.CancelModal, }) } @@ -72,26 +61,26 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) { case Round: m.Range = Day } - return &m, nil + return m, nil } case "enter": switch m.Step { case AddressStep: - addr := m.Input.Value() + addr := m.AddressInput.Value() if !algod.ValidateAddress(addr) { - m.InputError = "Error: invalid address" - return &m, nil + m.AddressInputError = "Error: invalid address" + return m, nil } - m.InputError = "" + m.AddressInputError = "" m.SetStep(DurationStep) - return &m, app.EmitShowModal(app.GenerateModal) + return m, app.EmitShowModal(app.GenerateModal) case DurationStep: - val, err := strconv.Atoi(m.InputTwo.Value()) + val, err := strconv.Atoi(m.DurationInput.Value()) if err != nil || val <= 0 { - m.InputTwoError = "Error: duration must be a positive number" - return &m, nil + m.DurationInputError = "Error: duration must be a positive number" + return m, nil } - m.InputTwoError = "" + m.DurationInputError = "" m.SetStep(WaitingStep) var rangeType participation.RangeType var dur int @@ -106,7 +95,7 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) { dur = val rangeType = participation.RoundRange } - return &m, tea.Sequence(app.EmitShowModal(app.GenerateModal), app.GenerateCmd(m.Input.Value(), rangeType, dur, m.State)) + return m, tea.Sequence(app.EmitShowModal(app.GenerateModal), app.GenerateCmd(m.AddressInput.Value(), rangeType, dur, m.State)) } @@ -116,17 +105,12 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) { switch m.Step { case AddressStep: - // Handle character input and blinking - var val textinput.Model - val, cmd = m.Input.Update(msg) - m.Input = &val + m.AddressInput, cmd = m.AddressInput.Update(msg) cmds = append(cmds, cmd) case DurationStep: - var val textinput.Model - val, cmd = m.InputTwo.Update(msg) - m.InputTwo = &val + m.DurationInput, cmd = m.DurationInput.Update(msg) cmds = append(cmds, cmd) } - return &m, tea.Batch(cmds...) + return m, tea.Batch(cmds...) } diff --git a/ui/modals/generate/generate_test.go b/ui/modals/generate/generate_test.go index f8c62b1f..d3355f7a 100644 --- a/ui/modals/generate/generate_test.go +++ b/ui/modals/generate/generate_test.go @@ -12,9 +12,7 @@ import ( ) func Test_New(t *testing.T) { - m := New("ABC", test.GetState(nil)) - - m.SetAddress("TUIDKH2C7MUHZDD77MAMUREJRKNK25SYXB7OAFA6JFBB24PEL5UX4S4GUU") + m := New("TUIDKH2C7MUHZDD77MAMUREJRKNK25SYXB7OAFA6JFBB24PEL5UX4S4GUU", test.GetState(nil)) if m.Address != "TUIDKH2C7MUHZDD77MAMUREJRKNK25SYXB7OAFA6JFBB24PEL5UX4S4GUU" { t.Error("Did not set address") @@ -24,12 +22,12 @@ func Test_New(t *testing.T) { if m.Step != AddressStep { t.Error("Did not advance to address step") } - if m.Controls != "( esc to cancel )" { + if m.Controls() != "( esc to cancel )" { t.Error("Did not set controls") } m.SetStep(DurationStep) - m.InputTwo.SetValue("1") + m.DurationInput.SetValue("1") m, cmd := m.HandleMessage(tea.KeyMsg{ Type: tea.KeyRunes, @@ -44,7 +42,7 @@ func Test_New(t *testing.T) { m.SetStep(DurationStep) m.Range = Month - m.InputTwo.SetValue("1") + m.DurationInput.SetValue("1") m, cmd = m.HandleMessage(tea.KeyMsg{ Type: tea.KeyRunes, Runes: []rune("enter"), @@ -55,7 +53,7 @@ func Test_New(t *testing.T) { m.SetStep(DurationStep) m.Range = Round - m.InputTwo.SetValue("1") + m.DurationInput.SetValue("1") m, cmd = m.HandleMessage(tea.KeyMsg{ Type: tea.KeyRunes, Runes: []rune("enter"), diff --git a/ui/modals/generate/model.go b/ui/modals/generate/model.go index 0afd0a5e..80c4e29c 100644 --- a/ui/modals/generate/model.go +++ b/ui/modals/generate/model.go @@ -1,9 +1,9 @@ package generate import ( + "github.com/algorandfoundation/nodekit/api" "github.com/algorandfoundation/nodekit/internal/algod" "github.com/charmbracelet/bubbles/cursor" - "github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/textinput" ) @@ -27,61 +27,75 @@ type ViewModel struct { Width int Height int - Address string - Input *textinput.Model - InputError string - InputTwo *textinput.Model - InputTwoError string - Spinner *spinner.Model - Step Step - Range Range - - Title string - Controls string - BorderColor string - - State *algod.StateModel - cursorMode cursor.Mode + Address string + + AddressInput textinput.Model + AddressInputError string + + DurationInput textinput.Model + DurationInputError string + + Step Step + Range Range + + Participation *api.ParticipationKey + State *algod.StateModel + cursorMode cursor.Mode } -func (m *ViewModel) SetAddress(address string) { +func (m *ViewModel) Reset(address string) { m.Address = address - m.Input.SetValue(address) + m.AddressInput.SetValue(address) + m.AddressInputError = "" + m.AddressInput.Focus() + m.SetStep(AddressStep) + m.DurationInput.SetValue("") + m.DurationInputError = "" +} +func (m *ViewModel) SetStep(step Step) { + m.Step = step + switch m.Step { + case AddressStep: + m.AddressInputError = "" + case DurationStep: + m.DurationInput.SetValue("") + m.DurationInput.Focus() + m.DurationInput.PromptStyle = focusedStyle + m.DurationInput.TextStyle = focusedStyle + m.DurationInputError = "" + m.AddressInput.Blur() + } } -var DefaultControls = "( esc to cancel )" -var DefaultTitle = "Generate Consensus Participation Keys" -var DefaultBorderColor = "2" +//func (m ViewModel) SetAddress(address string) { +// m.Address = address +// m.AddressInput.SetValue(address) +//} -func New(address string, state *algod.StateModel) *ViewModel { - input := textinput.New() - input2 := textinput.New() +func New(address string, state *algod.StateModel) ViewModel { m := ViewModel{ - Address: address, - State: state, - Input: &input, - InputError: "", - InputTwo: &input2, - InputTwoError: "", - Step: AddressStep, - Range: Day, - Title: DefaultTitle, - Controls: DefaultControls, - BorderColor: DefaultBorderColor, + Address: address, + State: state, + AddressInput: textinput.New(), + AddressInputError: "", + DurationInput: textinput.New(), + DurationInputError: "", + Step: AddressStep, + Range: Day, } - input.Cursor.Style = cursorStyle - input.CharLimit = 58 - input.Placeholder = "Wallet Address" - input.Focus() - input.PromptStyle = focusedStyle - input.TextStyle = focusedStyle - - input2.Cursor.Style = cursorStyle - input2.CharLimit = 58 - input2.Placeholder = "Length of time" - - input2.PromptStyle = noStyle - input2.TextStyle = noStyle - return &m + m.AddressInput.Cursor.Style = cursorStyle + m.AddressInput.CharLimit = 58 + m.AddressInput.Placeholder = "Wallet Address" + m.AddressInput.Focus() + m.AddressInput.PromptStyle = focusedStyle + m.AddressInput.TextStyle = focusedStyle + + m.DurationInput.Cursor.Style = cursorStyle + m.DurationInput.CharLimit = 58 + m.DurationInput.Placeholder = "Length of time" + + m.DurationInput.PromptStyle = noStyle + m.DurationInput.TextStyle = noStyle + return m } diff --git a/ui/modals/generate/testdata/Test_Snapshot/Duration.golden b/ui/modals/generate/testdata/Test_Snapshot/Duration.golden index cc1ad542..6b67e813 100644 --- a/ui/modals/generate/testdata/Test_Snapshot/Duration.golden +++ b/ui/modals/generate/testdata/Test_Snapshot/Duration.golden @@ -1,6 +1,8 @@ - -How long should the keys be valid for? - -Duration in days: -> Length of time - \ No newline at end of file +╭──Validity Range────────────────────────────────────────────────────────╮ +│ │ +│ How long should the keys be valid for? │ +│ │ +│ Duration in days: │ +│ > Length of time │ +│ │ +╰──────────────────────────────────────────────────( (s)witch range )────╯ \ No newline at end of file diff --git a/ui/modals/generate/testdata/Test_Snapshot/Visible.golden b/ui/modals/generate/testdata/Test_Snapshot/Visible.golden index a6014f05..0f7b640f 100644 --- a/ui/modals/generate/testdata/Test_Snapshot/Visible.golden +++ b/ui/modals/generate/testdata/Test_Snapshot/Visible.golden @@ -1,6 +1,8 @@ - -Create keys required to participate in Algorand consensus. - -Account address: -> Wallet Address - \ No newline at end of file +╭──Generate Consensus Participation Keys─────────────────────────────────╮ +│ │ +│ Create keys required to participate in Algorand consensus. │ +│ │ +│ Account address: │ +│ > Wallet Address │ +│ │ +╰───────────────────────────────────────────────────( esc to cancel )────╯ \ No newline at end of file diff --git a/ui/modals/generate/testdata/Test_Snapshot/Waiting.golden b/ui/modals/generate/testdata/Test_Snapshot/Waiting.golden index 2ea08da5..68dacfef 100644 --- a/ui/modals/generate/testdata/Test_Snapshot/Waiting.golden +++ b/ui/modals/generate/testdata/Test_Snapshot/Waiting.golden @@ -1,5 +1,7 @@ - -Generating Participation Keys... - -Please wait. This operation can take a few minutes. - \ No newline at end of file +╭──Generating Keys───────────────────────────────────────────────────────╮ +│ │ +│ Generating Participation Keys... │ +│ │ +│ Please wait. This operation can take a few minutes. │ +│ │ +╰────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/ui/modals/generate/view.go b/ui/modals/generate/view.go index 79a377a1..ca793938 100644 --- a/ui/modals/generate/view.go +++ b/ui/modals/generate/view.go @@ -7,7 +7,42 @@ import ( "github.com/charmbracelet/lipgloss" ) -func (m ViewModel) View() string { +// Title returns a string representing the title based on the current step in the ViewModel. +func (m ViewModel) Title() string { + switch m.Step { + case DurationStep: + return "Validity Range" + case WaitingStep: + return "Generating Keys" + default: + return "Generate Consensus Participation Keys" + } +} + +// BorderColor returns a string representing the border color based on the current step in the ViewModel. +func (m ViewModel) BorderColor() string { + switch m.Step { + case WaitingStep: + return "9" + default: + return "2" + } +} + +// Controls returns a string representing control instructions based on the current step in the ViewModel. +func (m ViewModel) Controls() string { + switch m.Step { + case AddressStep: + return "( esc to cancel )" + case DurationStep: + return "( (s)witch range )" + default: + return "" + } +} + +// Body returns a styled string representation of content based on the current step in the ViewModel. +func (m ViewModel) Body() string { render := "" switch m.Step { case AddressStep: @@ -16,13 +51,13 @@ func (m ViewModel) View() string { "Create keys required to participate in Algorand consensus.", "", "Account address:", - m.Input.View(), + m.AddressInput.View(), "", ) - if m.InputError != "" { + if m.AddressInputError != "" { render = lipgloss.JoinVertical(lipgloss.Left, render, - style.Red.Render(m.InputError), + style.Red.Render(m.AddressInputError), ) } case DurationStep: @@ -31,13 +66,13 @@ func (m ViewModel) View() string { "How long should the keys be valid for?", "", fmt.Sprintf("Duration in %ss:", m.Range), - m.InputTwo.View(), + m.DurationInput.View(), "", ) - if m.InputTwoError != "" { + if m.DurationInputError != "" { render = lipgloss.JoinVertical(lipgloss.Left, render, - style.Red.Render(m.InputTwoError), + style.Red.Render(m.DurationInputError), ) } case WaitingStep: @@ -51,3 +86,22 @@ func (m ViewModel) View() string { return lipgloss.NewStyle().Width(70).Render(render) } + +// View renders the ViewModel as a styled string, incorporating title, controls, and body content with dynamic borders. +func (m ViewModel) View() string { + body := m.Body() + width := lipgloss.Width(body) + height := lipgloss.Height(body) + return style.WithNavigation( + m.Controls(), + style.WithTitle( + m.Title(), + // Apply the Borders with the Padding + style.ApplyBorder(width+2, height-4, m.BorderColor()). + PaddingRight(1). + PaddingLeft(1). + Render(m.Body()), + ), + ) + +} diff --git a/ui/modals/info/info.go b/ui/modals/info/info.go index 3a683ca6..4d6c04d9 100644 --- a/ui/modals/info/info.go +++ b/ui/modals/info/info.go @@ -12,26 +12,20 @@ import ( ) type ViewModel struct { - Width int - Height int - Title string - Controls string - BorderColor string - Active bool - Suspended bool - Prefix string - Participation *api.ParticipationKey - State *algod.StateModel + Width int + Height int + OfflineControls bool + Suspended bool + Prefix string + Participation *api.ParticipationKey + State *algod.StateModel } -func New(state *algod.StateModel) *ViewModel { - return &ViewModel{ - Width: 0, - Height: 0, - Title: "Key Information", - BorderColor: "3", - Controls: "( " + style.Red.Render("(d)elete") + " | " + style.Green.Render("(o)nline") + " )", - State: state, +func New(state *algod.StateModel) ViewModel { + return ViewModel{ + Width: 0, + Height: 0, + State: state, } } @@ -39,54 +33,73 @@ func (m ViewModel) Init() tea.Cmd { return nil } +// Update processes a message and returns the updated model and command based on the received input. func (m ViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.HandleMessage(msg) } -func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) { - +func (m ViewModel) HandleMessage(msg tea.Msg) (ViewModel, tea.Cmd) { switch msg := msg.(type) { + case app.ModalEvent: + if msg.Type == app.InfoModal { + m.Prefix = msg.Prefix + m.Participation = msg.Key + m.OfflineControls = msg.Active + } case tea.KeyMsg: switch msg.String() { case "esc": - return &m, app.EmitModalEvent(app.ModalEvent{ + return m, app.EmitModalEvent(app.ModalEvent{ Type: app.CancelModal, }) case "d": - if !m.Active { - return &m, app.EmitShowModal(app.ConfirmModal) + if !m.OfflineControls { + return m, app.EmitShowModal(app.ConfirmModal) } case "r": - if !m.Active { - return &m, app.EmitCreateShortLink(m.Active, m.Participation, m.State) + if !m.OfflineControls { + return m, app.EmitCreateShortLink(false, m.Participation, m.State) } case "o": - if m.Active { - return &m, app.EmitCreateShortLink(m.Active, m.Participation, m.State) + if m.OfflineControls { + return m, app.EmitCreateShortLink(true, m.Participation, m.State) } } case tea.WindowSizeMsg: m.Width = msg.Width m.Height = msg.Height } - m.UpdateState() - return &m, nil + return m, nil +} + +// Title returns the fixed title string "Key Information" for the ViewModel. +func (m ViewModel) Title() string { + return "Key Information" } -func (m *ViewModel) UpdateState() { + +// Controls generates a string representation of control options based on the state of Participation and Active fields. +func (m ViewModel) Controls() string { if m.Participation == nil { - return + return "" } - - if m.Active && !m.Suspended { - m.BorderColor = "1" - m.Controls = "( " + style.Red.Render(style.Red.Render("take (o)ffline")) + " )" + if m.OfflineControls { + return "( " + style.Red.Render(style.Red.Render("take (o)ffline")) + " )" } - if !m.Active { - m.BorderColor = "3" - m.Controls = "( " + style.Red.Render("(d)elete") + " | " + style.Green.Render("(r)egister online") + " )" + return "( " + style.Red.Render("(d)elete") + " | " + style.Green.Render("(r)egister online") + " )" + +} + +// BorderColor determines border color based on the state of participation and activity. +// Returns "3" if Participation is nil, "4" if Active is true, and "5" otherwise. +func (m ViewModel) BorderColor() string { + if m.OfflineControls { + return "1" } + return "3" } -func (m ViewModel) View() string { + +// Body generates the formatted content of the ViewModel, displaying key details or indicating no key is selected. +func (m ViewModel) Body() string { if m.Participation == nil { return "No key selected" } @@ -106,7 +119,6 @@ func (m ViewModel) View() string { if m.Prefix != "" { prefix = "\n" + m.Prefix } - return ansi.Hardwrap(lipgloss.JoinVertical(lipgloss.Left, prefix, account, @@ -121,5 +133,23 @@ func (m ViewModel) View() string { voteKeyDilution, "", ), m.Width, true) +} + +// View renders the ViewModel as a styled string, incorporating title, controls, and body content with dynamic borders. +func (m ViewModel) View() string { + body := m.Body() + width := lipgloss.Width(body) + height := lipgloss.Height(body) + return style.WithNavigation( + m.Controls(), + style.WithTitle( + m.Title(), + // Apply the Borders with the Padding + style.ApplyBorder(width+2, height-4, m.BorderColor()). + PaddingRight(1). + PaddingLeft(1). + Render(m.Body()), + ), + ) } diff --git a/ui/modals/info/info_test.go b/ui/modals/info/info_test.go index f3211e9f..da9d5748 100644 --- a/ui/modals/info/info_test.go +++ b/ui/modals/info/info_test.go @@ -14,23 +14,20 @@ import ( func Test_New(t *testing.T) { m := New(test.GetState(nil)) - if m == nil { - t.Fatal("New returned nil") - } m.Participation = &mock.Keys[0] account := m.State.Accounts[mock.Keys[0].Address] account.Status = "Online" m.State.Accounts[mock.Keys[0].Address] = account - m.Active = true - m.UpdateState() - if m.BorderColor != "1" { + m.OfflineControls = true + if m.BorderColor() != "1" { t.Error("State is not correct, border should be 1") } - if m.Controls != "( take (o)ffline )" { + if m.Controls() != "( take (o)ffline )" { t.Error("Controls are not correct") } } func Test_Snapshot(t *testing.T) { + // TODO: Suspended, and Corrupt Key t.Run("Visible", func(t *testing.T) { model := New(test.GetState(nil)) model.Participation = &mock.Keys[0] diff --git a/ui/modals/info/testdata/Test_Snapshot/NoKey.golden b/ui/modals/info/testdata/Test_Snapshot/NoKey.golden index eacdfa5a..770529e2 100644 --- a/ui/modals/info/testdata/Test_Snapshot/NoKey.golden +++ b/ui/modals/info/testdata/Test_Snapshot/NoKey.golden @@ -1 +1,3 @@ -No key selected \ No newline at end of file +╭──Key Information╮ +│ No key selected │ +╰─────────────────╯ \ No newline at end of file diff --git a/ui/modals/info/testdata/Test_Snapshot/Visible.golden b/ui/modals/info/testdata/Test_Snapshot/Visible.golden index 9ccb5058..c93fd1cb 100644 --- a/ui/modals/info/testdata/Test_Snapshot/Visible.golden +++ b/ui/modals/info/testdata/Test_Snapshot/Visible.golden @@ -1,12 +1,14 @@ - -Account: ABC -Participation ID: 123 - -Vote Key: VEVTVEtFWQ== -Selection Key: VEVTVEtFWQ== -State Proof Key: VEVTVEtFWQ== - -Vote First Valid: 0 -Vote Last Valid: 30000 -Vote Key Dilution: 100 - \ No newline at end of file +╭──Key Information──────────────╮ +│ │ +│ Account: ABC │ +│ Participation ID: 123 │ +│ │ +│ Vote Key: VEVTVEtFWQ== │ +│ Selection Key: VEVTVEtFWQ== │ +│ State Proof Key: VEVTVEtFWQ== │ +│ │ +│ Vote First Valid: 0 │ +│ Vote Last Valid: 30000 │ +│ Vote Key Dilution: 100 │ +│ │ +╰───────────────────────────────╯ \ No newline at end of file diff --git a/ui/modals/transaction/controller.go b/ui/modals/transaction/controller.go index 7067200f..54c3f599 100644 --- a/ui/modals/transaction/controller.go +++ b/ui/modals/transaction/controller.go @@ -7,17 +7,9 @@ import ( "github.com/algorandfoundation/algourl/encoder" "github.com/algorandfoundation/nodekit/internal/algod" "github.com/algorandfoundation/nodekit/ui/app" - "github.com/algorandfoundation/nodekit/ui/style" tea "github.com/charmbracelet/bubbletea" ) -type Title string - -const ( - OnlineTitle Title = "Register Online" - OfflineTitle Title = "Register Offline" -) - func (m ViewModel) Init() tea.Cmd { return nil } @@ -76,27 +68,10 @@ func (m *ViewModel) ShouldAddIncentivesFee() bool { // 2) online keyreg // 3) protocol supports incentives // 4) account is not already incentives eligible - return m.State != nil && !m.State.IncentivesDisabled && !m.Active && m.IsIncentiveProtocol() && m.Account() != nil && !m.Account().IncentiveEligible -} - -func (m *ViewModel) GetControlText() string { - escLegend := style.Red.Render("(esc) go back") - if m.IsQREnabled() { - otherView := "link" - if m.ShowLink { - otherView = "QR" - } - return "( " + style.Yellow.Render("(s)how "+otherView) + " | " + escLegend + " )" - } - return "( " + escLegend + " )" + return m.State != nil && !m.State.IncentivesDisabled && !m.OfflineControls && m.IsIncentiveProtocol() && m.Account() != nil && !m.Account().IncentiveEligible } func (m *ViewModel) UpdateState() { - controlText := m.GetControlText() - if m.Controls != m.GetControlText() { - // TODO BUG: the controls do not re-render when changed - m.Controls = controlText - } if m.Participation == nil { return @@ -116,9 +91,9 @@ func (m *ViewModel) UpdateState() { m.ATxn.AUrlTxnKeyCommon.Type = string(types.KeyRegistrationTx) m.ATxn.AUrlTxnKeyCommon.Fee = fee - if !m.Active { - m.Title = string(OnlineTitle) - m.BorderColor = "2" + if !m.OfflineControls { + //m.Title = string(OnlineTitle) + //m.BorderColor = "2" votePartKey := base64.RawURLEncoding.EncodeToString(m.Participation.Key.VoteParticipationKey) selPartKey := base64.RawURLEncoding.EncodeToString(m.Participation.Key.SelectionParticipationKey) spKey := base64.RawURLEncoding.EncodeToString(*m.Participation.Key.StateProofKey) @@ -133,8 +108,8 @@ func (m *ViewModel) UpdateState() { m.ATxn.AUrlTxnKeyreg.VoteLast = &lastValid m.ATxn.AUrlTxnKeyreg.VoteKeyDilution = &vkDilution } else { - m.Title = string(OfflineTitle) - m.BorderColor = "9" + //m.Title = string(OfflineTitle) + //m.BorderColor = "9" m.ATxn.AUrlTxnKeyreg.VotePK = nil m.ATxn.AUrlTxnKeyreg.SelectionPK = nil m.ATxn.AUrlTxnKeyreg.StateProofPK = nil diff --git a/ui/modals/transaction/model.go b/ui/modals/transaction/model.go index fcd42384..fb4ecf06 100644 --- a/ui/modals/transaction/model.go +++ b/ui/modals/transaction/model.go @@ -6,7 +6,6 @@ import ( "github.com/algorandfoundation/nodekit/api" "github.com/algorandfoundation/nodekit/internal/algod" "github.com/algorandfoundation/nodekit/internal/algod/participation" - "github.com/algorandfoundation/nodekit/ui/style" ) type ViewModel struct { @@ -15,23 +14,16 @@ type ViewModel struct { // Height is the last known vertical lines Height int - Title string + //Title string // Active Participation Key - Participation *api.ParticipationKey - Active bool - Suspended bool - Link *participation.ShortLinkResponse + Participation *api.ParticipationKey + OfflineControls bool + Suspended bool + Link *participation.ShortLinkResponse // Pointer to the State State *algod.StateModel - IsOnline bool - - // Components - BorderColor string - Controls string - navigation string - ShowLink bool // QR Code @@ -49,13 +41,8 @@ func (m ViewModel) IsQREnabled() bool { // New creates and instance of the ViewModel with a default controls.Model func New(state *algod.StateModel) *ViewModel { return &ViewModel{ - State: state, - Title: "Offline Transaction", - ShowLink: true, - IsOnline: false, - BorderColor: "9", - navigation: "| accounts | keys | " + style.Green.Render("txn") + " |", - Controls: "", - ATxn: nil, + State: state, + ShowLink: true, + ATxn: nil, } } diff --git a/ui/modals/transaction/transaction_test.go b/ui/modals/transaction/transaction_test.go index 9392cdb7..916adf6a 100644 --- a/ui/modals/transaction/transaction_test.go +++ b/ui/modals/transaction/transaction_test.go @@ -46,7 +46,7 @@ func Test_Snapshot(t *testing.T) { Height: 40, Width: 80, }) - model.Active = true + model.OfflineControls = true model.UpdateState() got := ansi.Strip(model.View()) golden.RequireEqual(t, []byte(got)) diff --git a/ui/modals/transaction/view.go b/ui/modals/transaction/view.go index 2f771892..052e19eb 100644 --- a/ui/modals/transaction/view.go +++ b/ui/modals/transaction/view.go @@ -9,7 +9,34 @@ import ( "github.com/charmbracelet/x/ansi" ) -func (m ViewModel) View() string { +const ( + OnlineTitle = "Register Online" + OfflineTitle = "Register Offline" +) + +func (m ViewModel) Title() string { + if m.OfflineControls { + return OfflineTitle + } else { + return OnlineTitle + } +} +func (m ViewModel) BorderColor() string { + return "9" +} +func (m ViewModel) Controls() string { + escLegend := style.Red.Render("(esc) go back") + if m.IsQREnabled() { + otherView := "link" + if m.ShowLink { + otherView = "QR" + } + return "( " + style.Yellow.Render("(s)how "+otherView) + " | " + escLegend + " )" + } + return "( " + escLegend + " )" +} + +func (m ViewModel) Body() string { if m.Participation == nil { return "No key selected" } @@ -64,7 +91,6 @@ func (m ViewModel) View() string { "Open this URL in your browser:", "", style.WithHyperlink(link, link), - "", ) } else { // TODO: Refactor ATxn to Interface @@ -96,3 +122,21 @@ func (m ViewModel) View() string { return render } + +// View renders the ViewModel as a styled string, incorporating title, controls, and body content with dynamic borders. +func (m ViewModel) View() string { + body := m.Body() + width := lipgloss.Width(body) + height := lipgloss.Height(body) + return style.WithNavigation( + m.Controls(), + style.WithTitle( + m.Title(), + // Apply the Borders with the Padding + style.ApplyBorder(width+2, height+2, m.BorderColor()). + Padding(1). + Render(m.Body()), + ), + ) + +}