From ab36f88c0e3acc227292bd5201824855d8675bd5 Mon Sep 17 00:00:00 2001 From: Kyren223 Date: Mon, 20 Jan 2025 16:13:08 +0200 Subject: Implemented member list --- internal/client/ui/core/chat/chat.go | 10 +- internal/client/ui/core/core.go | 31 ++- internal/client/ui/core/memberlist/memberlist.go | 306 +++++++++++++++++++++++ internal/client/ui/ui.go | 5 + 4 files changed, 340 insertions(+), 12 deletions(-) create mode 100644 internal/client/ui/core/memberlist/memberlist.go (limited to 'internal/client') 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 -- cgit v1.3.1