diff options
| -rw-r--r-- | internal/client/ui/core/core.go | 44 | ||||
| -rw-r--r-- | internal/client/ui/core/networklist/networklist.go | 5 | ||||
| -rw-r--r-- | internal/client/ui/core/state/state.go | 4 | ||||
| -rw-r--r-- | internal/client/ui/core/usersettings/usersettings.go | 206 | ||||
| -rw-r--r-- | internal/data/users.sql.go | 35 | ||||
| -rw-r--r-- | internal/packet/models.go | 2 | ||||
| -rw-r--r-- | internal/packet/types.go | 3 | ||||
| -rw-r--r-- | internal/server/api/api.go | 75 | ||||
| -rw-r--r-- | query/users.sql | 8 |
9 files changed, 359 insertions, 23 deletions
diff --git a/internal/client/ui/core/core.go b/internal/client/ui/core/core.go index c4a6627..59b4523 100644 --- a/internal/client/ui/core/core.go +++ b/internal/client/ui/core/core.go @@ -24,6 +24,7 @@ import ( "github.com/kyren223/eko/internal/client/ui/core/networklist" "github.com/kyren223/eko/internal/client/ui/core/networkupdate" "github.com/kyren223/eko/internal/client/ui/core/state" + "github.com/kyren223/eko/internal/client/ui/core/usersettings" "github.com/kyren223/eko/internal/client/ui/loadscreen" "github.com/kyren223/eko/internal/packet" "github.com/kyren223/eko/pkg/assert" @@ -61,6 +62,7 @@ type Model struct { timeout time.Duration connected bool + userSettingsPopup *usersettings.Model networkCreationPopup *networkcreation.Model networkUpdatePopup *networkupdate.Model networkJoinPopup *networkjoin.Model @@ -81,6 +83,7 @@ func New(privKey ed25519.PrivateKey, name string) Model { timer: newTimer(initialTimeout), timeout: initialTimeout, connected: false, + userSettingsPopup: nil, networkCreationPopup: nil, networkUpdatePopup: nil, networkJoinPopup: nil, @@ -119,7 +122,9 @@ func (m Model) View() string { ) var popup string - if m.networkCreationPopup != nil { + if m.userSettingsPopup != nil { + popup = m.userSettingsPopup.View() + } else if m.networkCreationPopup != nil { popup = m.networkCreationPopup.View() } else if m.networkUpdatePopup != nil { popup = m.networkUpdatePopup.View() @@ -221,7 +226,12 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd { } case *packet.SetUserData: - state.FromJsonUserData(msg.Data) + if msg.User != nil { + state.State.Users[msg.User.ID] = *msg.User + } + if msg.Data != nil { + state.FromJsonUserData(*msg.Data) + } case *packet.NetworksInfo: state.UpdateNetworks(msg) @@ -346,8 +356,21 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd { } } + // user/profile/options/settings + case "u", "p", "o", "s": + if !m.HasPopup() && m.focus == FocusNetworkList { + popup := usersettings.New() + m.userSettingsPopup = &popup + } else if m.HasPopup() { + cmd := m.updatePopups(msg) + if cmd != nil { + return cmd + } + } + case "esc": if m.HasPopup() { + m.userSettingsPopup = nil m.networkCreationPopup = nil m.networkUpdatePopup = nil m.frequencyCreationPopup = nil @@ -356,7 +379,13 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd { } case "enter": - if m.networkCreationPopup != nil { + if m.userSettingsPopup != nil { + cmd := m.userSettingsPopup.Select() + if cmd != nil { + m.userSettingsPopup = nil + } + return cmd + } else if m.networkCreationPopup != nil { cmd := m.networkCreationPopup.Select() if cmd != nil { m.networkCreationPopup = nil @@ -475,7 +504,11 @@ func (m *Model) move(direction int) { } func (m *Model) updatePopups(msg tea.Msg) tea.Cmd { - if m.networkCreationPopup != nil { + if m.userSettingsPopup != nil { + popup, cmd := m.userSettingsPopup.Update(msg) + m.userSettingsPopup = &popup + return cmd + } else if m.networkCreationPopup != nil { popup, cmd := m.networkCreationPopup.Update(msg) m.networkCreationPopup = &popup return cmd @@ -500,7 +533,8 @@ func (m *Model) updatePopups(msg tea.Msg) tea.Cmd { } func (m *Model) HasPopup() bool { - return m.networkCreationPopup != nil || + return m.userSettingsPopup != nil || + m.networkCreationPopup != nil || m.networkUpdatePopup != nil || m.frequencyCreationPopup != nil || m.frequencyUpdatePopup != nil || diff --git a/internal/client/ui/core/networklist/networklist.go b/internal/client/ui/core/networklist/networklist.go index eae1bd6..c308886 100644 --- a/internal/client/ui/core/networklist/networklist.go +++ b/internal/client/ui/core/networklist/networklist.go @@ -181,8 +181,11 @@ func (m *Model) Swap(dir int) (Model, tea.Cmd) { state.Data.Networks[m.index] = state.Data.Networks[m.index+dir] state.Data.Networks[m.index+dir] = tmp m.SetIndex(m.index + dir) + + data := state.JsonUserData() return *m, gateway.Send(&packet.SetUserData{ - Data: state.JsonUserData(), + Data: &data, + User: nil, }) } diff --git a/internal/client/ui/core/state/state.go b/internal/client/ui/core/state/state.go index 47ca6f6..182a957 100644 --- a/internal/client/ui/core/state/state.go +++ b/internal/client/ui/core/state/state.go @@ -91,8 +91,10 @@ func UpdateNetworks(info *packet.NetworksInfo) { } } + data := JsonUserData() gateway.SendAsync(&packet.SetUserData{ - Data: JsonUserData(), + Data: &data, + User: nil, }) } diff --git a/internal/client/ui/core/usersettings/usersettings.go b/internal/client/ui/core/usersettings/usersettings.go new file mode 100644 index 0000000..5307238 --- /dev/null +++ b/internal/client/ui/core/usersettings/usersettings.go @@ -0,0 +1,206 @@ +package usersettings + +import ( + "errors" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/kyren223/eko/internal/client/gateway" + "github.com/kyren223/eko/internal/client/ui/colors" + "github.com/kyren223/eko/internal/client/ui/core/state" + "github.com/kyren223/eko/internal/client/ui/field" + "github.com/kyren223/eko/internal/client/ui/layouts/flex" + "github.com/kyren223/eko/internal/data" + "github.com/kyren223/eko/internal/packet" + "github.com/kyren223/eko/pkg/assert" +) + +var ( + width = 48 + + style = lipgloss.NewStyle(). + Border(lipgloss.ThickBorder()). + Padding(1, 4). + Align(lipgloss.Center, lipgloss.Center) + + headerStyle = lipgloss.NewStyle().Foreground(colors.Turquoise) + + fieldBlurredStyle = lipgloss.NewStyle(). + PaddingLeft(1). + Border(lipgloss.RoundedBorder()). + BorderForeground(colors.DarkCyan) + fieldFocusedStyle = fieldBlurredStyle. + BorderForeground(colors.Focus). + Border(lipgloss.ThickBorder()) + + blurredUpdate = lipgloss.NewStyle(). + Background(colors.Gray).Padding(0, 1).Render("Update User Settings") + focusedUpdate = lipgloss.NewStyle(). + Background(colors.Blue).Padding(0, 1).Render("Update User Settings") +) + +const ( + NameField = iota + Description + PrivateField + UpdateField + FieldCount +) + +type Model struct { + name field.Model + description field.Model + privateDM bool + update string + + selected int + nameWidth int +} + +func New() Model { + user, ok := state.State.Users[*state.UserID] + assert.Assert(ok, "user should always exist when connected to server") + + name := field.New(width) + name.Header = "Username" + name.HeaderStyle = headerStyle + name.FocusedStyle = fieldFocusedStyle + name.BlurredStyle = fieldBlurredStyle + name.ErrorStyle = lipgloss.NewStyle().Foreground(colors.Error) + name.Input.CharLimit = width + name.Focus() + name.Input.Validate = func(s string) error { + if strings.TrimSpace(s) == "" { + return errors.New("cannot be empty") + } + return nil + } + nameWidth := lipgloss.Width(name.View()) + name.Input.SetValue(user.Name) + + description := field.New(width) + description.Header = "Description" + description.HeaderStyle = headerStyle + description.FocusedStyle = fieldFocusedStyle + description.BlurredStyle = fieldBlurredStyle + description.ErrorStyle = lipgloss.NewStyle().Foreground(colors.Error) + description.Input.CharLimit = width + description.Input.SetValue(user.Description) + + return Model{ + name: name, + description: description, + privateDM: !user.IsPublicDM, + update: blurredUpdate, + selected: 0, + nameWidth: nameWidth, + } +} + +func (m Model) Init() tea.Cmd { + return nil +} + +func (m Model) View() string { + name := m.name.View() + description := m.description.View() + + privateStyle := lipgloss.NewStyle().PaddingLeft(1) + if m.selected == PrivateField { + privateStyle = privateStyle.Foreground(colors.Focus) + } + private := "[ ] Private DMs" + if m.privateDM { + private = "[x] Private DMs" + } + private = privateStyle.Render(private) + + update := lipgloss.NewStyle().Width(m.nameWidth).Align(lipgloss.Center).Render(m.update) + + content := flex.NewVertical(name, description, private, update).WithGap(1).View() + return style.Render(content) +} + +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + key := msg.Type + switch key { + case tea.KeyTab: + return m, m.cycle(1) + case tea.KeyShiftTab: + return m, m.cycle(-1) + + default: + var cmd tea.Cmd + switch m.selected { + case NameField: + m.name, cmd = m.name.Update(msg) + case Description: + m.description, cmd = m.description.Update(msg) + } + return m, cmd + } + } + + return m, nil +} + +func (m *Model) cycle(step int) tea.Cmd { + m.selected += step + if m.selected < 0 { + m.selected = FieldCount - 1 + } else { + m.selected %= FieldCount + } + return m.updateFocus() +} + +func (m *Model) updateFocus() tea.Cmd { + m.name.Blur() + m.description.Blur() + m.update = blurredUpdate + switch m.selected { + case NameField: + return m.name.Focus() + case Description: + return m.description.Focus() + case PrivateField: + return nil + case UpdateField: + m.update = focusedUpdate + return nil + default: + assert.Never("missing switch statement field in update focus", "selected", m.selected) + return nil + } +} + +func (m *Model) Select() tea.Cmd { + if m.selected == PrivateField { + m.privateDM = !m.privateDM + return nil + } + + if m.selected != UpdateField { + return nil + } + + m.name.Input.Err = m.name.Input.Validate(m.name.Input.Value()) + if m.name.Input.Err != nil { + return nil + } + + return gateway.Send(&packet.SetUserData{ + Data: nil, + User: &data.User{ + ID: *state.UserID, + Name: m.name.Input.Value(), + Description: m.description.Input.Value(), + IsPublicDM: !m.privateDM, + IsDeleted: false, + PublicKey: nil, + }, + }) +} diff --git a/internal/data/users.sql.go b/internal/data/users.sql.go index db28ca7..2e20cec 100644 --- a/internal/data/users.sql.go +++ b/internal/data/users.sql.go @@ -73,7 +73,7 @@ func (q *Queries) GetUserById(ctx context.Context, id snowflake.ID) (User, error const getUserByPublicKey = `-- name: GetUserByPublicKey :one SELECT id, name, public_key, description, is_public_dm, is_deleted FROM users -WHERE public_key = ? AND is_deleted = false +WHERE public_key = ? ` func (q *Queries) GetUserByPublicKey(ctx context.Context, publicKey ed25519.PublicKey) (User, error) { @@ -126,3 +126,36 @@ func (q *Queries) SetUserData(ctx context.Context, arg SetUserDataParams) (UserD err := row.Scan(&i.UserID, &i.Data) return i, err } + +const updateUser = `-- name: UpdateUser :one +UPDATE users SET + name = ?, description = ?, is_public_dm = ? +WHERE id = ? +RETURNING id, name, public_key, description, is_public_dm, is_deleted +` + +type UpdateUserParams struct { + Name string + Description string + IsPublicDM bool + ID snowflake.ID +} + +func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) { + row := q.db.QueryRowContext(ctx, updateUser, + arg.Name, + arg.Description, + arg.IsPublicDM, + arg.ID, + ) + var i User + err := row.Scan( + &i.ID, + &i.Name, + &i.PublicKey, + &i.Description, + &i.IsPublicDM, + &i.IsDeleted, + ) + return i, err +} diff --git a/internal/packet/models.go b/internal/packet/models.go index 27b6300..aebd473 100644 --- a/internal/packet/models.go +++ b/internal/packet/models.go @@ -8,6 +8,8 @@ const ( MaxFrequencyName = 32 MaxUserDataBytes = 8192 MaxMessageBytes = 2000 + MaxUsernameBytes = 32 + MaxUserDescriptionBytes = 200 ) const ( diff --git a/internal/packet/types.go b/internal/packet/types.go index e03a419..f142a48 100644 --- a/internal/packet/types.go +++ b/internal/packet/types.go @@ -190,7 +190,8 @@ func (m *MembersInfo) Type() PacketType { } type SetUserData struct { - Data string + Data *string + User *data.User } func (m *SetUserData) Type() PacketType { diff --git a/internal/server/api/api.go b/internal/server/api/api.go index 24112cf..e52372b 100644 --- a/internal/server/api/api.go +++ b/internal/server/api/api.go @@ -4,6 +4,7 @@ import ( "context" "crypto/ed25519" "database/sql" + "errors" "fmt" "log" "strconv" @@ -165,6 +166,11 @@ func CreateOrGetUser(ctx context.Context, node *snowflake.Node, pubKey ed25519.P PublicKey: pubKey, }) } + + if user.IsDeleted { + err = errors.New("public key is already taken by a deleted user") + } + return user, err } @@ -670,38 +676,81 @@ func SetMember(ctx context.Context, sess *session.Session, request *packet.SetMe func SetUserData(ctx context.Context, sess *session.Session, request *packet.SetUserData) packet.Payload { queries := data.New(db) - if len(request.Data) > packet.MaxUserDataBytes { - return &packet.Error{ - Error: "data bytes may not exceed " + - strconv.FormatInt(packet.MaxUserDataBytes, 10) + " bytes", + if request.Data != nil { + if len(*request.Data) > packet.MaxUserDataBytes { + return &packet.Error{Error: fmt.Sprintf( + "data bytes may not exceed %v bytes", + packet.MaxUserDataBytes, + )} + } + + _, err := queries.SetUserData(ctx, data.SetUserDataParams{ + UserID: sess.ID(), + Data: *request.Data, + }) + if err != nil { + log.Println("database error 0:", err) + return &ErrInternalError } } - _, err := queries.SetUserData(ctx, data.SetUserDataParams{ - UserID: sess.ID(), - Data: request.Data, - }) - if err != nil { - log.Println("database error:", err) - return &ErrInternalError + var userPtr *data.User = nil + if request.User != nil { + + name := request.User.Name + if len(name) > packet.MaxUsernameBytes { + return &packet.Error{Error: fmt.Sprintf( + "username bytes may not exceed %v bytes", + packet.MaxUsernameBytes, + )} + } + + description := request.User.Description + if len(name) > packet.MaxUserDescriptionBytes { + return &packet.Error{Error: fmt.Sprintf( + "user description bytes may not exceed %v bytes", + packet.MaxUserDescriptionBytes, + )} + } + + user, err := queries.UpdateUser(ctx, data.UpdateUserParams{ + Name: name, + Description: description, + IsPublicDM: request.User.IsPublicDM, + ID: sess.ID(), + }) + if err != nil { + log.Println("database error 1:", err) + return &ErrInternalError + } + + userPtr = &user } return &packet.SetUserData{ Data: request.Data, + User: userPtr, } } func GetUserData(ctx context.Context, sess *session.Session, request *packet.GetUserData) packet.Payload { queries := data.New(db) + user, err := queries.GetUserById(ctx, sess.ID()) + if err != nil { + log.Println("database error 0:", err) + return &ErrInternalError + } + data, err := queries.GetUserData(ctx, sess.ID()) if err != nil { - log.Println("database error:", err) + log.Println("database error 1:", err) return &ErrInternalError } return &packet.SetUserData{ - Data: data, + Data: &data, + User: &user, } } diff --git a/query/users.sql b/query/users.sql index 0d58b2d..bd91cda 100644 --- a/query/users.sql +++ b/query/users.sql @@ -4,7 +4,7 @@ WHERE id = ? AND is_deleted = false; -- name: GetUserByPublicKey :one SELECT * FROM users -WHERE public_key = ? AND is_deleted = false; +WHERE public_key = ?; -- name: CreateUser :one INSERT INTO users ( @@ -31,6 +31,12 @@ UPDATE SET WHERE user_id = EXCLUDED.user_id RETURNING *; +-- name: UpdateUser :one +UPDATE users SET + name = ?, description = ?, is_public_dm = ? +WHERE id = ? +RETURNING *; + -- name: GetUserData :one SELECT data FROM user_data WHERE user_id = ?; |
