summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKyren223 <Kyren223@proton.me>2025-01-20 17:32:31 +0200
committerKyren223 <Kyren223@proton.me>2025-01-20 17:32:31 +0200
commit5a5c09282416d131ec4a77bc9cf854fa0f29ae12 (patch)
treee327591b3c7c71227edc4befcf3675367e46fa49
parentab36f88c0e3acc227292bd5201824855d8675bd5 (diff)
Implemented user settings both client side and server side
-rw-r--r--internal/client/ui/core/core.go44
-rw-r--r--internal/client/ui/core/networklist/networklist.go5
-rw-r--r--internal/client/ui/core/state/state.go4
-rw-r--r--internal/client/ui/core/usersettings/usersettings.go206
-rw-r--r--internal/data/users.sql.go35
-rw-r--r--internal/packet/models.go2
-rw-r--r--internal/packet/types.go3
-rw-r--r--internal/server/api/api.go75
-rw-r--r--query/users.sql8
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 = ?;