summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/client/ui/core/chat/chat.go10
-rw-r--r--internal/client/ui/core/core.go31
-rw-r--r--internal/client/ui/core/memberlist/memberlist.go306
-rw-r--r--internal/client/ui/ui.go5
4 files changed, 340 insertions, 12 deletions
diff --git a/internal/client/ui/core/chat/chat.go b/internal/client/ui/core/chat/chat.go
index 960d563..184d82e 100644
--- a/internal/client/ui/core/chat/chat.go
+++ b/internal/client/ui/core/chat/chat.go
@@ -38,10 +38,6 @@ var (
VimModeStyle = lipgloss.NewStyle().Bold(true)
- NormalMemberStyle = lipgloss.NewStyle().Foreground(colors.Purple).SetString("󰀉")
- AdminMemberStyle = lipgloss.NewStyle().Foreground(colors.Red).Bold(true).SetString("󰓏")
- OwnerMemberStyle = AdminMemberStyle.Foreground(colors.Gold).SetString("󱟜")
-
DateTimeStyle = lipgloss.NewStyle().Foreground(colors.LightGray).SetString("")
PaddingCount = 1
@@ -771,11 +767,11 @@ func (m *Model) renderHeader(message data.Message, selected bool) []byte {
member := members[message.SenderID]
user := state.State.Users[message.SenderID]
- senderStyle := NormalMemberStyle
+ senderStyle := ui.NormalMemberStyle
if ownerId == member.UserID {
- senderStyle = OwnerMemberStyle
+ senderStyle = ui.OwnerMemberStyle
} else if member.IsAdmin {
- senderStyle = AdminMemberStyle
+ senderStyle = ui.AdminMemberStyle
}
sender := senderStyle.Render(user.Name)
diff --git a/internal/client/ui/core/core.go b/internal/client/ui/core/core.go
index dbc1907..c4a6627 100644
--- a/internal/client/ui/core/core.go
+++ b/internal/client/ui/core/core.go
@@ -18,6 +18,7 @@ import (
"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/memberlist"
"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"
@@ -45,8 +46,9 @@ const (
const (
FocusNetworkList = iota
- FocusFrequencyList
+ FocusLeftSidebar
FocusChat
+ FocusRightSidebar
FocusMax
)
@@ -66,6 +68,7 @@ type Model struct {
frequencyUpdatePopup *frequencyupdate.Model
networkList networklist.Model
frequencyList frequencylist.Model
+ memberList memberlist.Model
chat chat.Model
focus int
}
@@ -85,6 +88,7 @@ func New(privKey ed25519.PrivateKey, name string) Model {
frequencyUpdatePopup: nil,
networkList: networklist.New(),
frequencyList: frequencylist.New(),
+ memberList: memberlist.New(),
chat: chat.New(),
focus: FocusNetworkList,
}
@@ -105,7 +109,8 @@ func (m Model) View() string {
networkList := m.networkList.View()
frequencyList := m.frequencyList.View()
chat := m.chat.View()
- result := lipgloss.JoinHorizontal(lipgloss.Top, networkList, frequencyList, chat, frequencyList)
+ memberList := m.memberList.View()
+ result := lipgloss.JoinHorizontal(lipgloss.Top, networkList, frequencyList, chat, memberList)
result = lipgloss.Place(
ui.Width, ui.Height,
@@ -196,6 +201,7 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd {
log.Println("chatWidth:", chatWidth)
m.frequencyList.SetWidth(sidebarWidth)
+ m.memberList.SetWidth(sidebarWidth)
m.chat.SetWidth(chatWidth)
switch msg := msg.(type) {
@@ -223,6 +229,7 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd {
if networkId == nil && m.networkList.Index() != networklist.PeersIndex {
m.networkList.SetIndex(m.networkList.Index() - 1)
m.frequencyList.SetNetworkIndex(m.networkList.Index())
+ m.memberList.SetNetworkAndFrequency(m.networkList.Index(), m.frequencyList.Index())
m.chat.SetFrequency(m.networkList.Index(), m.frequencyList.Index())
}
@@ -239,6 +246,7 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd {
length := len(state.State.Frequencies[*networkId])
if index >= length {
m.frequencyList.SetIndex(index - 1)
+ m.memberList.SetNetworkAndFrequency(m.networkList.Index(), m.frequencyList.Index())
m.chat.SetFrequency(m.networkList.Index(), m.frequencyList.Index())
}
@@ -259,7 +267,7 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd {
return cmd
}
}
- case FocusFrequencyList:
+ case FocusLeftSidebar:
if !m.HasPopup() {
networkId := state.NetworkId(m.networkList.Index())
if networkId == nil {
@@ -308,7 +316,7 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd {
case "e":
index := m.networkList.Index()
networkFocus := m.focus == FocusNetworkList
- frequencyFocus := m.focus == FocusFrequencyList
+ frequencyFocus := m.focus == FocusLeftSidebar
if !m.HasPopup() && networkFocus && index != networklist.PeersIndex {
networkId := state.NetworkId(index)
if networkId == nil {
@@ -414,6 +422,10 @@ func (m *Model) updateConnected(msg tea.Msg) tea.Cmd {
m.frequencyList, cmd = m.frequencyList.Update(msg)
cmds = append(cmds, cmd)
+ m.memberList.SetNetworkAndFrequency(m.networkList.Index(), m.frequencyList.Index())
+ m.memberList, cmd = m.memberList.Update(msg)
+ cmds = append(cmds, cmd)
+
cmd = m.chat.SetFrequency(m.networkList.Index(), m.frequencyList.Index())
cmds = append(cmds, cmd)
m.chat, cmd = m.chat.Update(msg)
@@ -438,16 +450,25 @@ func (m *Model) move(direction int) {
switch m.focus {
case FocusNetworkList:
m.frequencyList.Blur()
+ m.memberList.Blur()
m.chat.Blur()
m.networkList.Focus()
- case FocusFrequencyList:
+ case FocusLeftSidebar:
m.networkList.Blur()
+ m.memberList.Blur()
m.chat.Blur()
m.frequencyList.Focus()
case FocusChat:
m.networkList.Blur()
+ m.memberList.Blur()
m.frequencyList.Blur()
m.chat.Focus()
+ case FocusRightSidebar:
+ m.networkList.Blur()
+ m.memberList.Blur()
+ m.frequencyList.Blur()
+ m.chat.Blur()
+ m.memberList.Focus()
default:
assert.Never("missing switch statement field in move", "focus", m.focus)
}
diff --git a/internal/client/ui/core/memberlist/memberlist.go b/internal/client/ui/core/memberlist/memberlist.go
new file mode 100644
index 0000000..b67550d
--- /dev/null
+++ b/internal/client/ui/core/memberlist/memberlist.go
@@ -0,0 +1,306 @@
+package memberlist
+
+import (
+ "slices"
+ "strings"
+
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/kyren223/eko/internal/client/ui"
+ "github.com/kyren223/eko/internal/client/ui/colors"
+ "github.com/kyren223/eko/internal/client/ui/core/state"
+ "github.com/kyren223/eko/internal/data"
+ "github.com/kyren223/eko/internal/packet"
+ "github.com/kyren223/eko/pkg/snowflake"
+)
+
+var (
+ sepStyle = lipgloss.NewStyle().Width(0).
+ Border(lipgloss.ThickBorder(), false, true, false, false)
+
+ headerStyle = lipgloss.NewStyle().
+ Margin(0, 0, 1).Padding(1).Align(lipgloss.Center).
+ Border(lipgloss.ThickBorder(), false, false, true)
+
+ margin = 2
+ padding = 1
+ memberStyle = lipgloss.NewStyle().
+ Margin(0, margin).Padding(0, padding).Align(lipgloss.Left)
+
+ symbolReadWrite = "󰖩 "
+ symbolReadOnly = "󱛂 "
+ symbolReadOnlyAdmin = "󰖩 "
+ symbolNoAccess = "󱚿 "
+ symbolNoAccessAdmin = "󱛀 "
+ symbolWidth = 2
+
+ widthWithoutMember = ((margin + padding) * 2) + symbolWidth
+
+ ellipsis = "…"
+)
+
+type Model struct {
+ networkIndex int
+ frequencyIndex int
+ base int
+ index int
+ focus bool
+ width int
+ height int
+}
+
+func New() Model {
+ return Model{
+ networkIndex: -1,
+ frequencyIndex: -1,
+ base: 0,
+ index: -1,
+ focus: false,
+ width: -1,
+ height: 1,
+ }
+}
+
+func (m Model) Init() tea.Cmd {
+ return nil
+}
+
+func (m Model) View() string {
+ networkId := state.NetworkId(m.networkIndex)
+ if networkId == nil || m.frequencyIndex == -1 {
+ return ""
+ }
+
+ memberStyle := memberStyle.Width(m.width - (margin * 2))
+ maxMemberWidth := m.width - widthWithoutMember
+ ownerId := state.State.Networks[*networkId].OwnerID
+
+ var builder strings.Builder
+
+ builder.WriteString(m.renderHeader())
+ builder.WriteString("\n")
+
+ membersMap := m.Members()
+ members := make([]data.Member, 0, len(membersMap))
+ for _, member := range membersMap {
+ members = append(members, member)
+ }
+ slices.SortFunc(members, func(a, b data.Member) int {
+ if a.UserID == ownerId {
+ return -1
+ } else if b.UserID == ownerId {
+ return 1
+ }
+
+ if a.IsAdmin && !b.IsAdmin {
+ return -1
+ } else if !a.IsAdmin && b.IsAdmin {
+ return 1
+ }
+
+ aName, bName := m.Users()[a.UserID].Name, m.Users()[b.UserID].Name
+ if aName == bName {
+ return int(a.UserID.Time()) - int(b.UserID.Time())
+ } else if aName < bName {
+ return -1
+ } else {
+ return 1
+ }
+ })
+
+ upper := min(m.base+m.height, len(members))
+ members = members[m.base:upper]
+
+ for i, member := range members {
+ memberStyle := memberStyle
+ if m.index == m.base+i {
+ memberStyle = memberStyle.Background(colors.BackgroundHighlight)
+ }
+
+ userStyle := ui.NormalMemberStyle
+ if ownerId == member.UserID {
+ userStyle = ui.OwnerMemberStyle
+ } else if member.IsAdmin {
+ userStyle = ui.AdminMemberStyle
+ }
+ memberName := m.Users()[member.UserID].Name
+ memberName = userStyle.Render(memberName)
+
+ if lipgloss.Width(memberName) <= maxMemberWidth {
+ memberName = lipgloss.NewStyle().
+ MaxWidth(maxMemberWidth).
+ Render(memberName)
+ } else {
+ memberName = lipgloss.NewStyle().
+ MaxWidth(maxMemberWidth-1).
+ Render(memberName) + ellipsis
+ }
+
+ builder.WriteString(memberStyle.Render(memberName))
+ builder.WriteString("\n")
+ }
+
+ sidebar := builder.String()
+ sep := sepStyle.Height(ui.Height)
+ if m.focus {
+ sep = sep.BorderForeground(colors.Focus)
+ }
+ result := lipgloss.JoinHorizontal(lipgloss.Top, sep.String(), sidebar)
+
+ return result
+}
+
+func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
+ if state.NetworkId(m.networkIndex) != nil {
+ // Calculate height for frequencies
+ m.height = ui.Height
+ m.height -= lipgloss.Height(m.renderHeader())
+ m.height -= 1
+ }
+
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ if !m.focus {
+ return m, nil
+ }
+
+ key := msg.String()
+ switch key {
+ case "k":
+ m.SetIndex(m.index - 1)
+ case "j":
+ m.SetIndex(m.index + 1)
+ case "g":
+ m.SetIndex(0)
+ case "G":
+ m.SetIndex(m.MembersLength() - 1)
+ case "ctrl+u":
+ m.SetIndex(m.index - m.height/2)
+ case "ctrl+d":
+ m.SetIndex(m.index + m.height/2)
+
+ case "p":
+ // TODO: profile
+
+ // Admin
+ case "x", "K":
+ // TODO: kick
+ case "b":
+ // TODO: ban
+
+ // Owner
+ case "D":
+ // TODO: demote (remove admin)
+ case "P":
+ // TODO: promote (add admin)
+ case "T":
+ // TODO: transfer ownership
+
+ }
+ }
+
+ return m, nil
+}
+
+func (m *Model) Focus() {
+ m.focus = true
+ m.SetIndex(0)
+}
+
+func (m *Model) Blur() {
+ m.focus = false
+ m.index = -1
+ m.base = 0
+}
+
+func (m *Model) SetNetworkAndFrequency(networkIndex, frequencyIndex int) {
+ if networkIndex == -1 || frequencyIndex == -1 {
+ m.networkIndex = -1
+ m.frequencyIndex = -1
+ m.index = -1
+ m.base = 0
+ return
+ }
+ m.networkIndex = networkIndex
+
+ if m.frequencyIndex != -1 {
+ fromFrequency := m.Frequencies()[m.frequencyIndex]
+ toFrequency := m.Frequencies()[frequencyIndex]
+
+ diffPerms := fromFrequency.Perms != toFrequency.Perms
+ fromIsNoAccess := fromFrequency.Perms == packet.PermNoAccess
+ toIsNoAccess := toFrequency.Perms == packet.PermNoAccess
+ if (fromIsNoAccess || toIsNoAccess) && diffPerms {
+ m.base = 0
+ m.index = -1
+ }
+ }
+
+ m.frequencyIndex = frequencyIndex
+}
+
+func (m Model) MembersLength() int {
+ networkId := state.NetworkId(m.networkIndex)
+ if networkId == nil {
+ return 0
+ }
+ return len(state.State.Members[*networkId])
+}
+
+func (m Model) Network() *data.Network {
+ networkId := state.NetworkId(m.networkIndex)
+ if networkId == nil {
+ return nil
+ }
+ network := state.State.Networks[*networkId]
+ return &network
+}
+
+func (m Model) Frequencies() []data.Frequency {
+ networkId := state.NetworkId(m.networkIndex)
+ if networkId == nil {
+ return nil
+ }
+ return state.State.Frequencies[*networkId]
+}
+
+func (m Model) Members() map[snowflake.ID]data.Member {
+ networkId := state.NetworkId(m.networkIndex)
+ if networkId == nil {
+ return nil
+ }
+ return state.State.Members[*networkId]
+}
+
+func (m Model) Users() map[snowflake.ID]data.User {
+ networkId := state.NetworkId(m.networkIndex)
+ if networkId == nil {
+ return nil
+ }
+ return state.State.Users
+}
+
+func (m *Model) Index() int {
+ return m.index
+}
+
+func (m *Model) SetIndex(index int) {
+ m.index = min(max(index, 0), m.MembersLength()-1)
+ if m.index < m.base && m.index != -1 {
+ m.base = m.index
+ } else if m.index >= m.base+m.height {
+ m.base = 1 + m.index - m.height
+ }
+}
+
+func (m Model) renderHeader() string {
+ headerStyle := headerStyle.Width(m.width)
+ if m.focus {
+ headerStyle = headerStyle.BorderForeground(colors.Focus)
+ }
+ return headerStyle.Render("Member List")
+}
+
+func (m *Model) SetWidth(width int) {
+ m.width = width
+}
diff --git a/internal/client/ui/ui.go b/internal/client/ui/ui.go
index 4ff8d27..26a39b1 100644
--- a/internal/client/ui/ui.go
+++ b/internal/client/ui/ui.go
@@ -12,6 +12,7 @@ import (
"github.com/muesli/ansi"
"github.com/muesli/reflow/truncate"
+ "github.com/kyren223/eko/internal/client/ui/colors"
"github.com/kyren223/eko/pkg/assert"
)
@@ -26,6 +27,10 @@ var (
Width int
Height int
Program *tea.Program
+
+ NormalMemberStyle = lipgloss.NewStyle().Foreground(colors.Purple).SetString("󰀉")
+ AdminMemberStyle = lipgloss.NewStyle().Foreground(colors.Red).Bold(true).SetString("󰓏")
+ OwnerMemberStyle = AdminMemberStyle.Foreground(colors.Gold).SetString("󱟜")
)
var NewAuth func() tea.Model