package core import ( "crypto/ed25519" "fmt" "log" "math" "os" "runtime" "slices" "strings" "time" "github.com/atotto/clipboard" "github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/timer" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/kyren223/eko/internal/client/config" "github.com/kyren223/eko/internal/client/gateway" "github.com/kyren223/eko/internal/client/ui" "github.com/kyren223/eko/internal/client/ui/colors" "github.com/kyren223/eko/internal/client/ui/core/banreason" "github.com/kyren223/eko/internal/client/ui/core/banview" "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/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" "github.com/kyren223/eko/internal/client/ui/core/networkupdate" "github.com/kyren223/eko/internal/client/ui/core/profile" "github.com/kyren223/eko/internal/client/ui/core/signaladd" "github.com/kyren223/eko/internal/client/ui/core/signallist" "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/client/ui/tosscreen" "github.com/kyren223/eko/internal/client/ui/viminput" "github.com/kyren223/eko/internal/data" "github.com/kyren223/eko/internal/packet" "github.com/kyren223/eko/pkg/assert" "github.com/kyren223/eko/pkg/snowflake" ) const ( ConnectingToServer = "Connecting to server.." ConnectionFailed = "Connection failed - retrying in %d sec..." ConnectionTimeout = 5 * time.Second InitialTimeout = 3750 * time.Millisecond TimerInterval = 50 * time.Millisecond ) const ( NetworkWidth = 9 SidebarPercentage = 0.20 MinSidebarWidth = 16 ) const ( FocusNetworkList = iota FocusLeftSidebar FocusChat FocusRightSidebar FocusMax ) type State int const ( Disconnected State = iota Connected ConnectedReceivedTos ConnectedAcceptedTos Authenticated ) const ( VerticalBorder = "┃" TopLeftCorner = "┏" BottomLeftCorner = "┗" TopRightCorner = "┓" BottomRightCorner = "┛" ) type Model struct { name string privKey ed25519.PrivateKey loading loadscreen.Model tos *tosscreen.Model tosHash string timer timer.Model timeout time.Duration state State helpPopup *HelpPopup userSettingsPopup *usersettings.Model networkCreationPopup *networkcreation.Model networkUpdatePopup *networkupdate.Model networkJoinPopup *networkjoin.Model frequencyCreationPopup *frequencycreation.Model frequencyUpdatePopup *frequencyupdate.Model banReasonPopup *banreason.Model banViewPopup *banview.Model signalAddPopup *signaladd.Model profilePopup *profile.Model networkList networklist.Model signalList signallist.Model frequencyList frequencylist.Model memberList memberlist.Model chat chat.Model focus int } func New(privKey ed25519.PrivateKey, name string) Model { m := Model{ name: name, privKey: privKey, loading: loadscreen.New(ConnectingToServer), tos: nil, tosHash: "", timer: newTimer(InitialTimeout), timeout: InitialTimeout, state: Disconnected, helpPopup: nil, userSettingsPopup: nil, networkCreationPopup: nil, networkUpdatePopup: nil, networkJoinPopup: nil, frequencyCreationPopup: nil, frequencyUpdatePopup: nil, banReasonPopup: nil, banViewPopup: nil, signalAddPopup: nil, profilePopup: nil, networkList: networklist.New(), signalList: signallist.New(), frequencyList: frequencylist.New(), memberList: memberlist.New(), chat: chat.New(), focus: FocusNetworkList, } m.move(0) // Update focus return m } func (m Model) Init() tea.Cmd { return tea.Batch(gateway.Connect(ConnectionTimeout), m.loading.Init()) } func (m Model) View() string { switch m.state { case Disconnected: return m.loading.View() case Connected: return m.loading.View() case ConnectedReceivedTos: assert.NotNil(m.tos, "TOS is expected to not be nil if received TOS already") return m.tos.View() case ConnectedAcceptedTos: // TODO: auth return m.loading.View() case Authenticated: // Continue default: panic(fmt.Sprintf("unexpected core.State: %#v", m.state)) } if m.HasPopup() { colors.Darken() } networkList := m.networkList.View() var leftSidebar string if m.networkList.Index() == networklist.SignalsIndex { leftSidebar = m.signalList.View() } else { leftSidebar = m.frequencyList.View() } chat := m.chat.View() var rightSidebar string if m.networkList.Index() == networklist.SignalsIndex { // TODO: decide what to put here width := lipgloss.Width(leftSidebar) height := ui.Height if config.ReadConfig().ScreenBorders { height -= 2 // To account for borders } focusColor := colors.White if m.focus == FocusRightSidebar { focusColor = colors.Focus } rightSidebar = lipgloss.NewStyle(). Width(width).Height(height). Border(lipgloss.ThickBorder(), config.ReadConfig().ScreenBorders, false). BorderBackground(colors.BackgroundDim).BorderForeground(focusColor). Background(colors.BackgroundDim).Foreground(colors.White). Align(lipgloss.Center, lipgloss.Center). Render("what should we put here?") } else { rightSidebar = m.memberList.View() } leftBorder := "" rightBorder := "" if config.ReadConfig().ScreenBorders { leftBorder = TopLeftCorner + strings.Repeat("\n"+VerticalBorder, ui.Height-2) + "\n" + BottomLeftCorner if m.focus == FocusNetworkList { leftBorder = lipgloss.NewStyle().Background(colors.BackgroundDimmer).Foreground(colors.Focus).Render(leftBorder) } else { leftBorder = lipgloss.NewStyle().Background(colors.BackgroundDimmer).Foreground(colors.White).Render(leftBorder) } rightBorder = TopRightCorner + strings.Repeat("\n"+VerticalBorder, ui.Height-2) + "\n" + BottomRightCorner if m.focus == FocusRightSidebar { rightBorder = lipgloss.NewStyle().Background(colors.BackgroundDimmer).Foreground(colors.Focus).Render(rightBorder) } else { rightBorder = lipgloss.NewStyle().Background(colors.BackgroundDimmer).Foreground(colors.White).Render(rightBorder) } } result := lipgloss.JoinHorizontal(lipgloss.Top, leftBorder, networkList, leftSidebar, chat, rightSidebar, rightBorder) // result = lipgloss.Place( // ui.Width, ui.Height, // lipgloss.Left, lipgloss.Top, // result, // ) if m.HasPopup() { colors.Restore() var popup string if m.helpPopup != nil { popup = m.helpPopup.View() } else 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() } 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() } else if m.banReasonPopup != nil { popup = m.banReasonPopup.View() } else if m.banViewPopup != nil { popup = m.banViewPopup.View() } else if m.signalAddPopup != nil { popup = m.signalAddPopup.View() } else if m.profilePopup != nil { popup = m.profilePopup.View() } else { assert.Never("missing handling of a popup!") } x := (ui.Width - lipgloss.Width(popup)) / 2 y := (ui.Height - lipgloss.Height(popup)) / 2 result = ui.PlaceOverlay(x, y, popup, result) } return result } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch m.state { case Disconnected: cmd := m.updateDisconnected(msg) return m, cmd case Connected: fallthrough case ConnectedReceivedTos: fallthrough case ConnectedAcceptedTos: cmd := m.updateConnected(msg) return m, cmd case Authenticated: cmd := m.updateAuthenticated(msg) return m, cmd default: panic(fmt.Sprintf("unexpected core.State: %#v", m.state)) } } func (m *Model) updateDisconnected(msg tea.Msg) tea.Cmd { switch msg := msg.(type) { case gateway.ConnectionEstablished: m.state = Connected m.timeout = InitialTimeout return m.timer.Stop() case gateway.ConnectionFailed: log.Println("failed to connect:", msg) m.timer = newTimer(m.timeout) m.updateLoadScreenContent() return m.timer.Start() case timer.TimeoutMsg: m.timeout = min(m.timeout*2, time.Minute) m.loading.SetContent(ConnectingToServer) return gateway.Connect(ConnectionTimeout) case timer.StartStopMsg: var cmd tea.Cmd m.timer, cmd = m.timer.Update(msg) return cmd case timer.TickMsg: m.updateLoadScreenContent() var cmd tea.Cmd m.timer, cmd = m.timer.Update(msg) return cmd case spinner.TickMsg: var loadscreenCmd tea.Cmd m.loading, loadscreenCmd = m.loading.Update(msg) return loadscreenCmd default: return nil } } func (m *Model) updateConnected(message tea.Msg) tea.Cmd { switch msg := message.(type) { case gateway.ConnectionLost: m.state = Disconnected m.timeout = InitialTimeout return tea.Batch(gateway.Connect(ConnectionTimeout), m.loading.Init()) case *packet.TosInfo: m.state = ConnectedReceivedTos server := config.ReadConfig().ServerName hash, ok := config.ReadCache().TosHashes[server] if ok && hash == msg.Hash { // AUTO ACCEPT IF IN CACHE m.state = ConnectedAcceptedTos return gateway.Send(&packet.AcceptTos{ IAgreeToTheTermsOfServiceAndPrivacyPolicy: true, }) } else { m.tosHash = msg.Hash combined := msg.Tos + "\n" + msg.PrivacyPolicy tos := tosscreen.New(combined) m.tos = &tos } case *packet.Error: if m.state == ConnectedAcceptedTos { if msg.Error == "success" { return tea.Batch(gateway.Send(&packet.GetNonce{}), SendAnalytics()) } else { log.Println("received error:", msg.Error) gateway.Disconnect() return ui.Transition(ui.NewAuth()) } } case *packet.NonceInfo: if m.state == ConnectedAcceptedTos { pubKey := ed25519.PublicKey(make([]byte, ed25519.PublicKeySize)) copy(pubKey, m.privKey[32:]) signature := ed25519.Sign(m.privKey, msg.Nonce) return gateway.Send(&packet.Authenticate{ PubKey: pubKey, Signature: signature, }) } case *packet.UsersInfo: if m.state == ConnectedAcceptedTos { assert.Assert(len(msg.Users) == 1, "as per the spec, server must send a UsersInfo with exactly a single user", "users", msg.Users) m.state = Authenticated state.UserID = &msg.Users[0].ID var setName tea.Cmd if m.name != "" { setName = gateway.Send(&packet.SetUserData{ Data: nil, User: &data.User{ Name: m.name, Description: "", IsPublicDM: true, }, }) m.name = "" } return setName } case tea.KeyMsg: switch msg.String() { case "enter": if m.state == ConnectedReceivedTos { m.state = ConnectedAcceptedTos assert.Assert(m.tosHash != "", "hash must have been set by the TosInfo") err := config.UseCache(func(cache *config.Cache) { server := config.ReadConfig().ServerName cache.TosHashes[server] = m.tosHash }) log.Println("unable to cache TOS acceptance: ", err) return gateway.Send(&packet.AcceptTos{ IAgreeToTheTermsOfServiceAndPrivacyPolicy: true, }) } } } if m.state == ConnectedReceivedTos { tos, cmd := m.tos.Update(message) m.tos = &tos return cmd } return nil } func (m *Model) updateAuthenticated(message tea.Msg) tea.Cmd { totalWidth := max(ui.Width, ui.MinWidth) totalWidth -= NetworkWidth if config.ReadConfig().ScreenBorders { totalWidth -= 2 // Left/right borders } sidebarWidth := int(math.Round(float64(totalWidth) * SidebarPercentage)) sidebarWidth = max(sidebarWidth, MinSidebarWidth) chatWidth := totalWidth - (2 * (sidebarWidth + 1)) // log.Println("Widths:", ui.Width) // log.Println("sidebarWidth:", sidebarWidth) // log.Println("chatWidth:", chatWidth) m.signalList.SetWidth(sidebarWidth) m.frequencyList.SetWidth(sidebarWidth) m.memberList.SetWidth(sidebarWidth) m.chat.SetWidth(chatWidth) switch msg := message.(type) { case ui.QuitMsg: state.SendFinalData() // blocks gateway.Disconnect() case gateway.ConnectionLost: state.UserID = nil m.state = Disconnected m.timeout = InitialTimeout return tea.Batch(gateway.Connect(ConnectionTimeout), m.loading.Init()) case *packet.Error: err := "new connection from another location, closing this one" if err == msg.Error { gateway.Disconnect() return ui.Transition(ui.NewAuth()) } case *packet.SetUserData: 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) networkId := state.NetworkId(m.networkList.Index()) if networkId == nil && m.networkList.Index() != networklist.SignalsIndex { 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()) } case *packet.MembersInfo: state.UpdateMembers(msg) networkId := state.NetworkId(m.networkList.Index()) if networkId != nil { members := state.State.Members[*networkId] index := m.memberList.Index() if index >= len(members) { m.memberList.SetIndex(m.memberList.Index()) } } case *packet.FrequenciesInfo: state.UpdateFrequencies(msg) networkId := state.NetworkId(m.networkList.Index()) if networkId == nil { return nil } index := m.frequencyList.Index() 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()) } case *packet.MessagesInfo: if len(msg.Messages) == 1 { m.chat.OnNewMessageReceived(msg) // MUST BE BEFORE STATE UPDATE } state.UpdateMessages(msg) case *packet.TrustInfo: state.UpdateTrustedUsers(msg) case *packet.BlockInfo: state.UpdateBlockedUsers(msg) case *packet.UsersInfo: state.UpdateUsersInfo(msg) case *packet.NotificationsInfo: signals := state.UpdateNotifications(msg) if m.networkList.Index() == networklist.SignalsIndex { m.chat.SetReceiver(-1) state.Data.Signals = slices.Insert(state.Data.Signals, 0, signals...) return m.chat.SetReceiver(m.signalList.Index()) } else { state.Data.Signals = slices.Insert(state.Data.Signals, 0, signals...) } case ui.BanReasonPopupMsg: popup := banreason.New(msg.User, msg.Network) m.banReasonPopup = &popup case ui.BanViewPopupMsg: popup := banview.New(msg.User, msg.Network) m.banViewPopup = &popup case ui.ProfilePopupMsg: popup := profile.New(msg.User) m.profilePopup = &popup case tea.KeyMsg: switch msg.String() { case "n": switch m.focus { case FocusNetworkList: if !m.HasPopup() { popup := networkcreation.New() m.networkCreationPopup = &popup message = ui.EmptyMsg{} } case FocusLeftSidebar: IsFrequenciesSidebar := m.networkList.Index() != networklist.SignalsIndex if !m.HasPopup() && IsFrequenciesSidebar { networkId := state.NetworkId(m.networkList.Index()) if networkId == nil { return nil } member := state.State.Members[*networkId][*state.UserID] if !member.IsAdmin { return nil } popup := frequencycreation.New(*networkId) m.frequencyCreationPopup = &popup message = ui.EmptyMsg{} } } case "a": if !m.HasPopup() { if m.focus == FocusNetworkList { popup := networkjoin.New() m.networkJoinPopup = &popup message = ui.EmptyMsg{} } IsSignalsSidebar := m.networkList.Index() == networklist.SignalsIndex if m.focus == FocusLeftSidebar && IsSignalsSidebar { popup := signaladd.New() m.signalAddPopup = &popup message = ui.EmptyMsg{} } } case "i": index := m.networkList.Index() isNetworkListFocused := m.focus == FocusNetworkList isFrequenciesSidebar := index != networklist.SignalsIndex if !m.HasPopup() && isNetworkListFocused && isFrequenciesSidebar { networkId := state.NetworkId(index) if networkId != nil { _ = clipboard.WriteAll(networkId.String()) } return nil } case "e": index := m.networkList.Index() networkFocus := m.focus == FocusNetworkList frequencyFocus := m.focus == FocusLeftSidebar if !m.HasPopup() && networkFocus && index != networklist.SignalsIndex { networkId := state.NetworkId(index) if networkId == nil { return nil } if state.State.Networks[*networkId].OwnerID != *state.UserID { return nil } popup := networkupdate.New(*networkId) m.networkUpdatePopup = &popup message = ui.EmptyMsg{} } else if !m.HasPopup() && frequencyFocus { networkId := state.NetworkId(index) if networkId == nil { return nil } member := state.State.Members[*networkId][*state.UserID] if !member.IsAdmin { return nil } index := m.frequencyList.Index() popup := frequencyupdate.New(*networkId, index) m.frequencyUpdatePopup = &popup message = ui.EmptyMsg{} } // user [s]ettings case "s": isChatLocked := m.focus == FocusChat && m.chat.Locked() if !m.HasPopup() && !isChatLocked { popup := usersettings.New() m.userSettingsPopup = &popup message = ui.EmptyMsg{} } case "?": normalMode := m.chat.Mode() == viminput.NormalMode if !m.HasPopup() && (m.focus != FocusChat || !m.chat.Locked() || normalMode) { switch m.focus { case FocusNetworkList: m.helpPopup = NewHelpPopup(HelpNetworkList) case FocusLeftSidebar: if m.networkList.Index() == networklist.SignalsIndex { m.helpPopup = NewHelpPopup(HelpSignalList) } else { m.helpPopup = NewHelpPopup(HelpFrequencyList) } case FocusChat: if m.chat.Locked() { m.helpPopup = NewHelpPopup(HelpVim) } else { m.helpPopup = NewHelpPopup(HelpChat) } case FocusRightSidebar: if m.memberList.IsBanList() { m.helpPopup = NewHelpPopup(HelpBanList) } else { m.helpPopup = NewHelpPopup(HelpMemberList) } } } case "esc": if m.HasPopup() { m.helpPopup = nil m.userSettingsPopup = nil m.networkCreationPopup = nil m.networkUpdatePopup = nil m.frequencyCreationPopup = nil m.frequencyUpdatePopup = nil m.networkJoinPopup = nil m.banReasonPopup = nil m.banViewPopup = nil m.signalAddPopup = nil m.profilePopup = nil } case "enter": if m.helpPopup != nil { m.helpPopup = nil } else 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 } return cmd } else if m.networkUpdatePopup != nil { cmd := m.networkUpdatePopup.Select() if cmd != nil { m.networkUpdatePopup = nil } return cmd } else if m.frequencyCreationPopup != nil { cmd := m.frequencyCreationPopup.Select() if cmd != nil { 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 { m.networkJoinPopup = nil } return cmd } else if m.banReasonPopup != nil { cmd := m.banReasonPopup.Select() if cmd != nil { m.banReasonPopup = nil } return cmd } else if m.banViewPopup != nil { m.banViewPopup = nil } else if m.signalAddPopup != nil { cmd, i := m.signalAddPopup.Select() if i != -1 { m.signalAddPopup = nil m.signalList.SetIndex(i) } return cmd } else if m.profilePopup != nil { m.profilePopup = nil } default: isChatLocked := m.focus == FocusChat && m.chat.Locked() if !m.HasPopup() && !isChatLocked { left := msg.String() == "H" right := msg.String() == "L" direction := 0 if left { direction = -1 } else if right { direction = 1 } m.move(direction) } } } var cmds []tea.Cmd var cmd tea.Cmd if m.HasPopup() { cmd := m.updatePopups(message) cmds = append(cmds, cmd) message = ui.EmptyMsg{} colors.Darken() } m.networkList, cmd = m.networkList.Update(message) cmds = append(cmds, cmd) if m.networkList.Index() == networklist.SignalsIndex { m.signalList, cmd = m.signalList.Update(message) cmds = append(cmds, cmd) cmd = m.chat.SetReceiver(m.signalList.Index()) cmds = append(cmds, cmd) } else { m.frequencyList.SetNetworkIndex(m.networkList.Index()) m.frequencyList, cmd = m.frequencyList.Update(message) cmds = append(cmds, cmd) cmd = m.chat.SetFrequency(m.networkList.Index(), m.frequencyList.Index()) cmds = append(cmds, cmd) } m.memberList.SetNetworkAndFrequency(m.networkList.Index(), m.frequencyList.Index()) m.memberList, cmd = m.memberList.Update(message) cmds = append(cmds, cmd) m.chat, cmd = m.chat.Update(message) cmds = append(cmds, cmd) calculateNotifications() if m.HasPopup() { colors.Restore() } return tea.Batch(cmds...) } func (m *Model) updateLoadScreenContent() { seconds := m.timer.Timeout.Round(time.Second) / time.Second m.loading.SetContent(fmt.Sprintf(ConnectionFailed, seconds)) } func newTimer(timeout time.Duration) timer.Model { return timer.NewWithInterval(timeout.Truncate(time.Second)+(time.Second/2), TimerInterval) } func (m *Model) move(direction int) { focus := m.focus + direction m.focus = max(0, min(FocusMax-1, focus)) switch m.focus { case FocusNetworkList: m.signalList.Blur() m.frequencyList.Blur() m.memberList.Blur() m.chat.Blur() m.networkList.Focus() case FocusLeftSidebar: m.networkList.Blur() m.memberList.Blur() m.chat.Blur() if m.networkList.Index() == networklist.SignalsIndex { m.frequencyList.Blur() m.signalList.Focus() } else { m.signalList.Blur() m.frequencyList.Focus() } case FocusChat: m.signalList.Blur() m.networkList.Blur() m.memberList.Blur() m.frequencyList.Blur() m.chat.Focus() case FocusRightSidebar: m.signalList.Blur() m.networkList.Blur() m.frequencyList.Blur() m.chat.Blur() m.memberList.Focus() default: assert.Never("missing switch statement field in move", "focus", m.focus) } } func (m *Model) updatePopups(msg tea.Msg) tea.Cmd { if m.helpPopup != nil { popup, cmd := m.helpPopup.Update(msg) m.helpPopup = &popup return cmd } else 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 } else if m.networkUpdatePopup != nil { popup, cmd := m.networkUpdatePopup.Update(msg) m.networkUpdatePopup = &popup return cmd } else if m.frequencyCreationPopup != nil { 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 return cmd } else if m.banReasonPopup != nil { popup, cmd := m.banReasonPopup.Update(msg) m.banReasonPopup = &popup return cmd } else if m.banViewPopup != nil { popup, cmd := m.banViewPopup.Update(msg) m.banViewPopup = &popup return cmd } else if m.signalAddPopup != nil { popup, cmd := m.signalAddPopup.Update(msg) m.signalAddPopup = &popup return cmd } else if m.profilePopup != nil { popup, cmd := m.profilePopup.Update(msg) m.profilePopup = &popup return cmd } return nil } func (m *Model) HasPopup() bool { return m.helpPopup != nil || m.userSettingsPopup != nil || m.networkCreationPopup != nil || m.networkUpdatePopup != nil || m.frequencyCreationPopup != nil || m.frequencyUpdatePopup != nil || m.networkJoinPopup != nil || m.banReasonPopup != nil || m.banViewPopup != nil || m.signalAddPopup != nil || m.profilePopup != nil } func calculateNotifications() { for _, signalId := range state.Data.Signals { if _, ok := state.State.Messages[signalId]; !ok { continue } pings := getSignalNotification(signalId) state.State.LocalNotifications[signalId] = pings } for networkId := range state.State.Networks { for _, frequency := range state.State.Frequencies[networkId] { if _, ok := state.State.Messages[frequency.ID]; !ok { continue } pings, hasNotif := getFrequencyNotification(networkId, frequency.ID) if hasNotif { state.State.LocalNotifications[frequency.ID] = pings } else { delete(state.State.LocalNotifications, frequency.ID) } } } } func getFrequencyNotification(networkId, frequencyId snowflake.ID) (_ int, _ bool) { lastReadMsg := state.State.LastReadMessages[frequencyId] if lastReadMsg == nil { return 0, false } btree := state.State.Messages[frequencyId] if btree == nil { return 0, false } pings := 0 hasNotif := false isAdmin := state.State.Members[networkId][*state.UserID].IsAdmin btree.AscendGreaterOrEqual(data.Message{ID: *lastReadMsg + 1}, func(item data.Message) bool { hasNotif = true if item.Ping == nil { return true } if *item.Ping == packet.PingEveryone { pings++ } else if *item.Ping == packet.PingAdmins && isAdmin { pings++ } else if *item.Ping == *state.UserID { pings++ } // No need to continue if we have 10 pings return pings < 10 }) return pings, hasNotif } func getSignalNotification(signal snowflake.ID) int { lastReadMsg := state.State.LastReadMessages[signal] if lastReadMsg == nil { return 0 } btree := state.State.Messages[signal] if btree == nil { return 0 } pings := 0 btree.AscendGreaterOrEqual(data.Message{ID: *lastReadMsg + 1}, func(item data.Message) bool { pings++ // No need to continue if we have 10 pings return pings < 10 }) return pings } func (m Model) ViewTos() string { return "TODO TOS" } func SendAnalytics() tea.Cmd { if !config.ReadConfig().AnonymousDeviceAnalytics { return nil // User disabled analytics } assert.Assert(config.ReadConfig().AnonymousDeviceAnalytics, "who is paranoid") osName := runtime.GOOS arch := runtime.GOARCH term := os.Getenv("TERM") colorterm := os.Getenv("COLORTERM") return gateway.Send(&packet.DeviceAnalytics{ DeviceID: config.ReadCache().DeviceID, OS: osName, Arch: arch, Term: term, Colorterm: colorterm, }) }