Implemented updating frequency properties both client-side and server-side

This commit is contained in:
2025-01-14 17:33:15 +02:00
parent 016e8cfc3c
commit 43bd630ecb
7 changed files with 421 additions and 1 deletions

View File

@@ -16,6 +16,7 @@ import (
"github.com/kyren223/eko/internal/client/ui/core/chat"
"github.com/kyren223/eko/internal/client/ui/core/frequencycreation"
"github.com/kyren223/eko/internal/client/ui/core/frequencylist"
"github.com/kyren223/eko/internal/client/ui/core/frequencyupdate"
"github.com/kyren223/eko/internal/client/ui/core/networkcreation"
"github.com/kyren223/eko/internal/client/ui/core/networkjoin"
"github.com/kyren223/eko/internal/client/ui/core/networklist"
@@ -55,6 +56,7 @@ type Model struct {
networkUpdatePopup *networkupdate.Model
networkJoinPopup *networkjoin.Model
frequencyCreationPopup *frequencycreation.Model
frequencyUpdatePopup *frequencyupdate.Model
networkList networklist.Model
frequencyList frequencylist.Model
chat chat.Model
@@ -73,6 +75,7 @@ func New(privKey ed25519.PrivateKey, name string) Model {
networkUpdatePopup: nil,
networkJoinPopup: nil,
frequencyCreationPopup: nil,
frequencyUpdatePopup: nil,
networkList: networklist.New(),
frequencyList: frequencylist.New(),
chat: chat.New(70),
@@ -110,6 +113,8 @@ func (m Model) View() string {
popup = m.networkUpdatePopup.View()
} else if m.frequencyCreationPopup != nil {
popup = m.frequencyCreationPopup.View()
} else if m.frequencyUpdatePopup != nil {
popup = m.frequencyUpdatePopup.View()
} else if m.networkJoinPopup != nil {
popup = m.networkJoinPopup.View()
}
@@ -283,12 +288,20 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd {
case "u":
index := m.networkList.Index()
networkFocus := m.focus == FocusNetworkList
frequencyFocus := m.focus == FocusFrequencyList
if !m.HasPopup() && networkFocus && index != networklist.PeersIndex {
networkId := state.NetworkId(index)
if networkId != nil {
popup := networkupdate.New(*networkId)
m.networkUpdatePopup = &popup
}
} else if !m.HasPopup() && frequencyFocus {
networkId := state.NetworkId(index)
if networkId != nil {
index := m.frequencyList.Index()
popup := frequencyupdate.New(*networkId, index)
m.frequencyUpdatePopup = &popup
}
} else {
cmd := m.updatePopups(msg)
if cmd != nil {
@@ -301,6 +314,7 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd {
m.networkCreationPopup = nil
m.networkUpdatePopup = nil
m.frequencyCreationPopup = nil
m.frequencyUpdatePopup = nil
m.networkJoinPopup = nil
}
@@ -323,6 +337,12 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd {
m.frequencyCreationPopup = nil
}
return cmd
} else if m.frequencyUpdatePopup != nil {
cmd := m.frequencyUpdatePopup.Select()
if cmd != nil {
m.frequencyUpdatePopup = nil
}
return cmd
} else if m.networkJoinPopup != nil {
cmd := m.networkJoinPopup.Select()
if cmd != nil {
@@ -417,6 +437,10 @@ func (m *Model) updatePopups(msg tea.Msg) tea.Cmd {
popup, cmd := m.frequencyCreationPopup.Update(msg)
m.frequencyCreationPopup = &popup
return cmd
} else if m.frequencyUpdatePopup != nil {
popup, cmd := m.frequencyUpdatePopup.Update(msg)
m.frequencyUpdatePopup = &popup
return cmd
} else if m.networkJoinPopup != nil {
popup, cmd := m.networkJoinPopup.Update(msg)
m.networkJoinPopup = &popup
@@ -429,5 +453,6 @@ func (m *Model) HasPopup() bool {
return m.networkCreationPopup != nil ||
m.networkUpdatePopup != nil ||
m.frequencyCreationPopup != nil ||
m.frequencyUpdatePopup != nil ||
m.networkJoinPopup != nil
}

View File

@@ -0,0 +1,295 @@
package frequencyupdate
import (
"errors"
"strings"
"github.com/charmbracelet/bubbles/textinput"
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/packet"
"github.com/kyren223/eko/pkg/assert"
"github.com/kyren223/eko/pkg/snowflake"
)
var (
width = 48
style = lipgloss.NewStyle().
Border(lipgloss.ThickBorder()).
Padding(1, 4).
Align(lipgloss.Center, lipgloss.Center)
headerStyle = lipgloss.NewStyle().Foreground(colors.Turquoise)
focusStyle = lipgloss.NewStyle().Foreground(colors.Focus)
fieldBlurredStyle = lipgloss.NewStyle().
PaddingLeft(1).
Border(lipgloss.RoundedBorder()).
BorderForeground(colors.DarkCyan)
fieldFocusedStyle = fieldBlurredStyle.
BorderForeground(colors.Focus).
Border(lipgloss.ThickBorder())
underlineStyle = func(s string, width int, color lipgloss.Color) string {
underline := lipgloss.NewStyle().Foreground(color).
Render(strings.Repeat(lipgloss.ThickBorder().Bottom, width))
return lipgloss.JoinVertical(lipgloss.Left, s, underline)
}
colorHeader = headerStyle.Bold(true).Render(" Color # ")
blurredUpdate = lipgloss.NewStyle().
Background(colors.Gray).Padding(0, 1).Render("Update Frequency")
focusedUpdate = lipgloss.NewStyle().
Background(colors.Blue).Padding(0, 1).Render("Update Frequency")
permsHeader = lipgloss.NewStyle().
Foreground(colors.Turquoise).
Render("Permissions for non-admins:")
leftpad = 1
)
const (
MaxHexDigits = 6
)
const (
NameField = iota
ReadWriteField
ReadOnlyField
NoAccessField
ColorField
UpdateField
FieldCount
)
type Model struct {
name field.Model
precomputedStyle lipgloss.Style
lastColor lipgloss.Color
update string
color textinput.Model
perms int
nameWidth int
selected int
network snowflake.ID
frequency snowflake.ID
}
func New(network snowflake.ID, frequencyIndex int) Model {
frequency := state.State.Frequencies[network][frequencyIndex]
name := field.New(width)
name.Header = "Frequency Name"
name.HeaderStyle = headerStyle
name.FocusedStyle = fieldFocusedStyle
name.BlurredStyle = fieldBlurredStyle
name.ErrorStyle = lipgloss.NewStyle().Foreground(colors.Error)
name.Input.CharLimit = packet.MaxFrequencyName
name.Focus()
name.Input.Validate = func(s string) error {
if strings.TrimSpace(s) == "" {
return errors.New("cannot be empty")
}
return nil
}
name.Input.SetValue(frequency.Name)
nameWidth := lipgloss.Width(name.View())
color := textinput.New()
color.Prompt = ""
color.CharLimit = MaxHexDigits
color.Placeholder = "000000"
color.Validate = func(s string) error {
if len(s) != MaxHexDigits {
return errors.New("err")
}
return nil
}
color.SetValue(frequency.HexColor[1:])
return Model{
name: name,
color: color,
lastColor: lipgloss.Color("#" + color.Value()),
perms: int(frequency.Perms),
update: blurredUpdate,
network: network,
frequency: frequency.ID,
nameWidth: nameWidth,
precomputedStyle: lipgloss.NewStyle().Width(nameWidth / 3),
}
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) View() string {
name := m.name.View()
color := colors.Gray
if m.color.Err != nil {
color = colors.Error
} else if m.selected == ColorField {
color = colors.Focus
}
colorInput := underlineStyle(m.color.View(), MaxHexDigits, color)
colorInput = lipgloss.NewStyle().Width(MaxHexDigits + 1).Render(colorInput)
colorIndicator := lipgloss.NewStyle().Foreground(m.lastColor).Render("■")
colorText := lipgloss.JoinHorizontal(lipgloss.Top, colorHeader, colorInput, colorIndicator)
readWrite := "[ ] Read & Write"
if m.perms == packet.PermReadWrite {
readWrite = "[x] Read & Write"
}
if m.selected == ReadWriteField {
readWrite = focusStyle.Render(readWrite)
}
readOnly := "[ ] Read Only"
if m.perms == packet.PermRead {
readOnly = "[x] Read Only"
}
if m.selected == ReadOnlyField {
readOnly = focusStyle.Render(readOnly)
}
noAccess := "[ ] No Access"
if m.perms == packet.PermNoAccess {
noAccess = "[x] No Access"
}
if m.selected == NoAccessField {
noAccess = focusStyle.Render(noAccess)
}
width := lipgloss.Width(noAccess) + lipgloss.Width(readOnly) + lipgloss.Width(readWrite)
padding := (m.nameWidth - (leftpad * 2) - width) / 2
readWrite = lipgloss.NewStyle().PaddingRight(padding).Render(readWrite)
readOnly = lipgloss.NewStyle().PaddingRight(padding).Render(readOnly)
perms := lipgloss.JoinHorizontal(lipgloss.Top, readWrite, readOnly, noAccess)
perms = lipgloss.JoinVertical(lipgloss.Left, permsHeader, perms)
perms = lipgloss.NewStyle().PaddingLeft(leftpad).Render(perms)
update := lipgloss.NewStyle().Width(m.nameWidth).Align(lipgloss.Center).Render(m.update)
content := flex.NewVertical(name, perms, colorText, 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 ColorField:
oldValue := m.color.Value()
position := m.color.Position()
m.color, cmd = m.color.Update(msg)
newValue := m.color.Value()
hex := "0123456789abcdefABCDEF"
invalid := false
for _, c := range newValue {
if !strings.ContainsRune(hex, c) {
invalid = true
break
}
}
if invalid {
m.color.SetValue(oldValue)
m.color.SetCursor(position)
} else if len(m.color.Value()) == 6 {
m.lastColor = lipgloss.Color("#" + m.color.Value())
}
}
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.color.Blur()
m.update = blurredUpdate
switch m.selected {
case NameField:
return m.name.Focus()
case ColorField:
return m.color.Focus()
case NoAccessField, ReadOnlyField, ReadWriteField:
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 {
switch m.selected {
case NoAccessField:
m.perms = packet.PermNoAccess
return nil
case ReadOnlyField:
m.perms = packet.PermRead
return nil
case ReadWriteField:
m.perms = packet.PermReadWrite
return nil
}
if m.selected != UpdateField {
return nil
}
m.name.Input.Err = m.name.Input.Validate(m.name.Input.Value())
m.color.Err = m.color.Validate(m.color.Value())
if m.name.Input.Err != nil || m.color.Err != nil {
return nil
}
request := packet.UpdateFrequency{
Frequency: m.frequency,
Name: m.name.Input.Value(),
HexColor: "#" + m.color.Value(),
Perms: m.perms,
}
return gateway.Send(&request)
}

View File

@@ -134,3 +134,36 @@ func (q *Queries) SwapFrequencies(ctx context.Context, arg SwapFrequenciesParams
_, err := q.db.ExecContext(ctx, swapFrequencies, arg.Pos1, arg.Pos2, arg.NetworkID)
return err
}
const updateFrequency = `-- name: UpdateFrequency :one
UPDATE frequencies SET
name = ?, hex_color = ?, perms = ?
WHERE id = ?
RETURNING id, network_id, name, hex_color, perms, position
`
type UpdateFrequencyParams struct {
Name string
HexColor string
Perms int64
ID snowflake.ID
}
func (q *Queries) UpdateFrequency(ctx context.Context, arg UpdateFrequencyParams) (Frequency, error) {
row := q.db.QueryRowContext(ctx, updateFrequency,
arg.Name,
arg.HexColor,
arg.Perms,
arg.ID,
)
var i Frequency
err := row.Scan(
&i.ID,
&i.NetworkID,
&i.Name,
&i.HexColor,
&i.Perms,
&i.Position,
)
return i, err
}

View File

@@ -98,7 +98,7 @@ type UpdateFrequency struct {
Name string
HexColor string
Frequency snowflake.ID
Perms byte
Perms int
}
func (m *UpdateFrequency) Type() PacketType {

View File

@@ -761,3 +761,62 @@ func UpdateNetwork(ctx context.Context, sess *session.Session, request *packet.U
Partial: true,
})
}
func UpdateFrequency(ctx context.Context, sess *session.Session, request *packet.UpdateFrequency) packet.Payload {
queries := data.New(db)
frequency, err := queries.GetFrequencyById(ctx, request.Frequency)
if err == sql.ErrNoRows {
return &packet.Error{Error: "frequency doesn't exist"}
}
if err != nil {
log.Println("database error 0:", err)
return &ErrInternalError
}
isAdmin, err := IsNetworkAdmin(ctx, queries, sess.ID(), frequency.NetworkID)
if err == sql.ErrNoRows {
return &packet.Error{Error: "either network doesn't exist or user is not apart of this network"}
}
if err != nil {
log.Println("database error 1:", err)
return &ErrInternalError
}
if !isAdmin {
return &ErrPermissionDenied
}
if len(request.Name) > packet.MaxFrequencyName {
return &packet.Error{Error: fmt.Sprintf(
"exceeded allowed frequency name length, max %v bytes",
packet.MaxFrequencyName,
)}
}
if ok, err := isValidHexColor(request.HexColor); !ok {
return &packet.Error{Error: err}
}
if request.Perms < 0 || request.Perms >= packet.PermMax {
return &packet.Error{Error: fmt.Sprintf(
"exceeded allowed perms value: 0 <= perms < %v", packet.PermMax,
)}
}
frequency, err = queries.UpdateFrequency(ctx, data.UpdateFrequencyParams{
Name: request.Name,
HexColor: request.HexColor,
Perms: int64(request.Perms),
ID: frequency.ID,
})
if err != nil {
log.Println("database error 2:", err)
return &ErrInternalError
}
return NetworkPropagate(ctx, sess, frequency.NetworkID, &packet.FrequenciesInfo{
RemovedFrequencies: nil,
Frequencies: []data.Frequency{frequency},
Network: frequency.NetworkID,
})
}

View File

@@ -352,6 +352,8 @@ func processRequest(ctx context.Context, sess *session.Session, request packet.P
case *packet.CreateFrequency:
response = timeout(5*time.Millisecond, api.CreateFrequency, ctx, sess, request)
case *packet.UpdateFrequency:
response = timeout(5*time.Millisecond, api.UpdateFrequency, ctx, sess, request)
case *packet.DeleteFrequency:
response = timeout(200*time.Millisecond, api.DeleteFrequency, ctx, sess, request)
case *packet.SwapFrequencies:

View File

@@ -18,6 +18,12 @@ INSERT INTO frequencies (
)
RETURNING *;
-- name: UpdateFrequency :one
UPDATE frequencies SET
name = ?, hex_color = ?, perms = ?
WHERE id = ?
RETURNING *;
-- name: SwapFrequencies :exec
UPDATE frequencies SET
position = CASE