main pico / pkg / tui / invite.go
Antonio Mika  ·  2026-10-10
  1package tui
  2
  3import (
  4	"time"
  5
  6	"github.com/picosh/pico/pkg/db"
  7	"go.rockorager.dev/vaxis"
  8	"go.rockorager.dev/vaxis/vxfw"
  9	"go.rockorager.dev/vaxis/vxfw/button"
 10	"go.rockorager.dev/vaxis/vxfw/list"
 11	"go.rockorager.dev/vaxis/vxfw/richtext"
 12	"go.rockorager.dev/vaxis/vxfw/text"
 13)
 14
 15type AddInvitePage struct {
 16	shared *SharedModel
 17	list   list.Dynamic
 18
 19	err     error
 20	focus   string
 21	input   *TextInput
 22	btn     *button.Button
 23	invites []*db.Invite
 24}
 25
 26func NewAddInvitePage(shrd *SharedModel) *AddInvitePage {
 27	btn := button.New("SEND", func() (vxfw.Command, error) { return nil, nil })
 28	btn.Style = button.StyleSet{
 29		Default: vaxis.Style{Background: grey},
 30		Focus:   vaxis.Style{Background: oj, Foreground: black},
 31	}
 32	m := &AddInvitePage{
 33		shared: shrd,
 34
 35		input: NewTextInput("pico user"),
 36		btn:   btn,
 37	}
 38	m.list = list.Dynamic{DrawCursor: true, Builder: m.getWidget, Gap: 1}
 39	return m
 40}
 41
 42func (m *AddInvitePage) Footer() []Shortcut {
 43	return []Shortcut{
 44		{Shortcut: "tab", Text: "focus"},
 45		{Shortcut: "enter", Text: "invite user"},
 46	}
 47}
 48
 49func (m *AddInvitePage) getWidget(i uint, cursor uint) vxfw.Widget {
 50	if int(i) >= len(m.invites) {
 51		return nil
 52	}
 53
 54	style := vaxis.Style{Foreground: grey}
 55	isSelected := i == cursor
 56	if isSelected {
 57		style = vaxis.Style{Foreground: fuschia}
 58	}
 59
 60	invite := m.invites[i]
 61
 62	txt := richtext.New([]vaxis.Segment{
 63		{Text: "User: ", Style: style},
 64		{Text: invite.ToUserName + "\n"},
 65
 66		{Text: "Invited: ", Style: style},
 67		{Text: invite.CreatedAt.Format(time.DateOnly)},
 68	})
 69
 70	return txt
 71}
 72
 73func (m *AddInvitePage) CaptureEvent(ev vaxis.Event) (vxfw.Command, error) {
 74	switch msg := ev.(type) {
 75	case vaxis.Key:
 76		if msg.Matches(vaxis.KeyEnter) {
 77			err := m.addInvite(m.input.GetValue())
 78			m.err = err
 79			if err == nil {
 80				m.input.Reset()
 81				m.shared.App.PostEvent(Navigate{To: HOME})
 82				return nil, nil
 83			}
 84			return vxfw.RedrawCmd{}, nil
 85		}
 86	}
 87	return nil, nil
 88}
 89
 90func (m *AddInvitePage) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
 91	switch msg := ev.(type) {
 92	case PageIn:
 93		m.err = m.fetchInvites()
 94		m.focus = "input"
 95		m.input.Reset()
 96		return m.input.FocusIn()
 97	case vaxis.Key:
 98		if msg.Matches(vaxis.KeyTab) {
 99			if m.focus == "input" {
100				m.focus = "button"
101				cmd, _ := m.input.FocusOut()
102				return vxfw.BatchCmd([]vxfw.Command{
103					vxfw.FocusWidgetCmd(m.btn),
104					cmd,
105				}), nil
106			}
107			m.focus = "input"
108			return m.input.FocusIn()
109		}
110	}
111
112	return nil, nil
113}
114
115func (m *AddInvitePage) fetchInvites() error {
116	invites, err := m.shared.Dbpool.FindInvitesByUser(m.shared.User.ID)
117	if err != nil {
118		return err
119
120	}
121	m.invites = invites
122	return nil
123}
124
125func (m *AddInvitePage) addInvite(username string) error {
126	db := m.shared.Dbpool
127	user, err := db.FindUserByName(username)
128	if err != nil {
129		return err
130	}
131
132	err = db.InviteUser(m.shared.User.ID, user.ID)
133	if err != nil {
134		return err
135	}
136
137	return m.fetchInvites()
138}
139
140func (m *AddInvitePage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
141	w := ctx.Max.Width
142	h := ctx.Max.Height
143	root := vxfw.NewSurface(w, h, m)
144	ah := 0
145
146	header := text.New("Invite a user to pico!  Invitee must already have a pico account.  This grants them `pgs`, `prose` and `pastes` access on their respective free tiers. Only pico+ members or users with an active invite can invite other users.")
147	headerSurf, _ := header.Draw(ctx)
148	root.AddChild(0, ah, headerSurf)
149	ah += int(headerSurf.Size.Height) + 1
150
151	inputSurf, _ := m.input.Draw(ctx)
152	root.AddChild(0, ah, inputSurf)
153	ah += int(headerSurf.Size.Height) + 1
154
155	btnSurf, _ := m.btn.Draw(vxfw.DrawContext{
156		Characters: ctx.Characters,
157		Max:        vxfw.Size{Width: 6, Height: 1},
158	})
159	root.AddChild(0, ah, btnSurf)
160	ah += int(btnSurf.Size.Height) + 1
161
162	if m.err != nil {
163		e := richtext.New([]vaxis.Segment{
164			{
165				Text:  m.err.Error(),
166				Style: vaxis.Style{Foreground: red},
167			},
168		})
169		errSurf, _ := e.Draw(createDrawCtx(ctx, 1))
170		root.AddChild(0, ah, errSurf)
171		ah += int(errSurf.Size.Height) + 1
172	}
173
174	listSurf, _ := m.list.Draw(createDrawCtx(ctx, ctx.Max.Height-uint16(ah)))
175	root.AddChild(0, ah, listSurf)
176
177	return root, nil
178}