From 150329e5dd90dbc542695cb20924af6747d28ca4 Mon Sep 17 00:00:00 2001 From: diamondburned Date: Sat, 2 Jan 2021 20:52:30 -0800 Subject: [PATCH] added message selection, minor bug fixes, etc --- go.mod | 2 +- go.sum | 2 + internal/gts/gts.go | 5 + internal/ui/messages/container/container.go | 34 ++++++- internal/ui/messages/container/cozy/cozy.go | 2 - internal/ui/messages/container/list.go | 33 +++++- internal/ui/messages/header.go | 14 ++- internal/ui/messages/input/input.go | 32 ++++-- internal/ui/messages/message/message.go | 14 ++- internal/ui/messages/msgctrl.go | 107 ++++++++++++++++++++ internal/ui/messages/state.go | 86 ++++++++++++++++ internal/ui/messages/typing/typing.go | 17 +++- internal/ui/messages/view.go | 97 +++--------------- internal/ui/primitives/menu/menu.go | 11 ++ internal/ui/rich/labeluri/labeluri.go | 4 +- internal/ui/rich/parser/attrmap/attrmap.go | 2 +- internal/ui/service/config/config.go | 4 +- internal/ui/service/session/servers.go | 6 +- internal/ui/ui.go | 2 +- 19 files changed, 365 insertions(+), 109 deletions(-) create mode 100644 internal/ui/messages/msgctrl.go create mode 100644 internal/ui/messages/state.go diff --git a/go.mod b/go.mod index 860e6f0..f1e9406 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/Xuanwo/go-locale v1.0.0 github.com/alecthomas/chroma v0.7.3 github.com/diamondburned/cchat v0.3.17 - github.com/diamondburned/cchat-discord v0.0.0-20210102085253-a691813b9041 + github.com/diamondburned/cchat-discord v0.0.0-20210103044430-14970d0e05eb github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828 github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374 diff --git a/go.sum b/go.sum index 6d71c98..125b116 100644 --- a/go.sum +++ b/go.sum @@ -80,6 +80,8 @@ github.com/diamondburned/cchat-discord v0.0.0-20210102040711-73b0d3f39c41 h1:cUV github.com/diamondburned/cchat-discord v0.0.0-20210102040711-73b0d3f39c41/go.mod h1:KL3i+ER58BrJ8JBkpy6WQ0mDZdlkgz7KWm3Ex7i6Mk0= github.com/diamondburned/cchat-discord v0.0.0-20210102085253-a691813b9041 h1:ZTovoKIyXiK5VFRTVrY6YWHIFR5x98u9Q+k9rMjZzvg= github.com/diamondburned/cchat-discord v0.0.0-20210102085253-a691813b9041/go.mod h1:KL3i+ER58BrJ8JBkpy6WQ0mDZdlkgz7KWm3Ex7i6Mk0= +github.com/diamondburned/cchat-discord v0.0.0-20210103044430-14970d0e05eb h1:IAitorXxVndnBGEW5uLcjjRLlpNBRPsUlwg9bUfyZLo= +github.com/diamondburned/cchat-discord v0.0.0-20210103044430-14970d0e05eb/go.mod h1:KL3i+ER58BrJ8JBkpy6WQ0mDZdlkgz7KWm3Ex7i6Mk0= github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db h1:VQI2PdbsdsRJ7d669kp35GbCUO44KZ0Xfqdu4o/oqVg= github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db/go.mod h1:M87kjNzWVPlkZycFNzpGPKQXzkHNnZphuwMf3E9ckgc= github.com/diamondburned/gotk3 v0.0.0-20201209182406-e7291341a091 h1:lQpSWzbi3rQf66aMSip/rIypasIFwqCqF0Wfn5og6gw= diff --git a/internal/gts/gts.go b/internal/gts/gts.go index 2272165..f8e4daf 100644 --- a/internal/gts/gts.go +++ b/internal/gts/gts.go @@ -222,6 +222,11 @@ func EventIsRightClick(ev *gdk.Event) bool { return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_SECONDARY } +func EventIsLeftClick(ev *gdk.Event) bool { + keyev := gdk.EventButtonNewFromEvent(ev) + return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_PRIMARY +} + func SpawnUploader(dirpath string, callback func(absolutePaths []string)) { dialog, _ := gtk.FileChooserNativeDialogNew( "Upload File", App.Window, diff --git a/internal/ui/messages/container/container.go b/internal/ui/messages/container/container.go index 3c51a0c..b7eba52 100644 --- a/internal/ui/messages/container/container.go +++ b/internal/ui/messages/container/container.go @@ -4,8 +4,10 @@ import ( "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-gtk/internal/ui/messages/input" "github.com/diamondburned/cchat-gtk/internal/ui/messages/message" + "github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu" "github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri" + "github.com/diamondburned/handy" "github.com/gotk3/gotk3/gtk" ) @@ -19,6 +21,8 @@ type MessageRow interface { Row() *gtk.ListBoxRow // AttachMenu should override the stored constructor. AttachMenu(items []menu.Item) // save memory + // MenuItems returns the list of attached menu items. + MenuItems() []menu.Item // SetReferenceHighlighter sets the reference highlighter into the message. SetReferenceHighlighter(refer labeluri.ReferenceHighlighter) } @@ -63,6 +67,8 @@ type Container interface { // Controller is for menu actions. type Controller interface { + // Connector is used for button press events to unselect messages. + primitives.Connector // BindMenu expects the controller to add actioner into the message. BindMenu(MessageRow) // Bottomed returns whether or not the message scroller is at the bottom. @@ -70,6 +76,10 @@ type Controller interface { // AuthorEvent is called on message create/update. This is used to update // the typer state. AuthorEvent(a cchat.Author) + // SelectMessage is called when a message is selected. + SelectMessage(list *ListStore, msg MessageRow) + // UnselectMessage is called when the message selection is cleared. + UnselectMessage() } // Constructor is an interface for making custom message implementations which @@ -84,7 +94,10 @@ const ColumnSpacing = 8 // ListContainer is an implementation of Container, which allows flexible // message grids. type ListContainer struct { + *handy.Clamp + *ListStore + Controller } @@ -97,8 +110,20 @@ type messageRow struct { var _ Container = (*ListContainer)(nil) func NewListContainer(constr Constructor, ctrl Controller) *ListContainer { + listStore := NewListStore(constr, ctrl) + listStore.ListBox.Show() + + clamp := handy.ClampNew() + clamp.SetMaximumSize(800) + clamp.SetTighteningThreshold(600) + clamp.SetHExpand(true) + clamp.SetVExpand(true) + clamp.Add(listStore.ListBox) + clamp.Show() + return &ListContainer{ - ListStore: NewListStore(constr, ctrl), + Clamp: clamp, + ListStore: listStore, Controller: ctrl, } } @@ -123,3 +148,10 @@ func (c *ListContainer) CleanMessages() bool { return false } + +func (c *ListContainer) SetFocusHAdjustment(adj *gtk.Adjustment) { + c.ListBox.SetFocusHAdjustment(adj) +} +func (c *ListContainer) SetFocusVAdjustment(adj *gtk.Adjustment) { + c.ListBox.SetFocusVAdjustment(adj) +} diff --git a/internal/ui/messages/container/cozy/cozy.go b/internal/ui/messages/container/cozy/cozy.go index bc1f1b0..e4aa6d0 100644 --- a/internal/ui/messages/container/cozy/cozy.go +++ b/internal/ui/messages/container/cozy/cozy.go @@ -48,8 +48,6 @@ type Container struct { func NewContainer(ctrl container.Controller) *Container { c := &Container{} c.ListContainer = container.NewListContainer(c, ctrl) - // A not-so-generous row padding, as we will rely on margins per widget. - // c.ListContainer.Grid.SetRowSpacing(4) primitives.AddClass(c, "cozy-container") return c diff --git a/internal/ui/messages/container/list.go b/internal/ui/messages/container/list.go index 8fb66c0..d16d0f1 100644 --- a/internal/ui/messages/container/list.go +++ b/internal/ui/messages/container/list.go @@ -26,7 +26,7 @@ var messageListCSS = primitives.PrepareClassCSS("message-list", ` `) type ListStore struct { - *gtk.ListBox + ListBox *gtk.ListBox Construct Constructor Controller Controller @@ -39,17 +39,42 @@ type ListStore struct { func NewListStore(constr Constructor, ctrl Controller) *ListStore { listBox, _ := gtk.ListBoxNew() - listBox.SetSelectionMode(gtk.SELECTION_NONE) + listBox.SetSelectionMode(gtk.SELECTION_SINGLE) listBox.Show() messageListCSS(listBox) - return &ListStore{ + listStore := ListStore{ ListBox: listBox, Construct: constr, Controller: ctrl, messages: make(map[messageKey]*messageRow, BacklogLimit+1), messageList: list.New(), } + + var selected bool + + listBox.Connect("row-selected", func(listBox *gtk.ListBox, r *gtk.ListBoxRow) { + if r == nil || selected { + if selected { + listBox.UnselectAll() + selected = false + } + ctrl.UnselectMessage() + return + } + + id, _ := r.GetName() + + msg := listStore.Message(id, "") + if msg == nil { + return + } + + selected = true + ctrl.SelectMessage(&listStore, msg) + }) + + return &listStore } func (c *ListStore) Reset() { @@ -267,6 +292,8 @@ func (c *ListStore) AddPresendMessage(msg input.PresendMessage) PresendMessageRo } func (c *ListStore) bindMessage(msgc *messageRow) { + // Bind the message ID to the row so we can easily do a lookup. + msgc.Row().SetName(msgc.ID()) msgc.SetReferenceHighlighter(c) c.Controller.BindMenu(msgc.MessageRow) } diff --git a/internal/ui/messages/header.go b/internal/ui/messages/header.go index 29a30b7..99461a5 100644 --- a/internal/ui/messages/header.go +++ b/internal/ui/messages/header.go @@ -14,12 +14,15 @@ import ( // const BreadcrumbSlash = `` const BreadcrumbSlash = " 〉" +const iconSize = gtk.ICON_SIZE_BUTTON + type Header struct { handy.HeaderBar ShowBackBtn *gtk.Revealer BackButton *gtk.Button Breadcrumb *gtk.Label + MessageCtrl *MessageControl ShowMembers *gtk.ToggleButton breadcrumbs []string @@ -39,7 +42,7 @@ var rightBreadcrumbCSS = primitives.PrepareClassCSS("right-breadcrumb", ` `) func NewHeader() *Header { - bk, _ := gtk.ButtonNewFromIconName("go-previous-symbolic", gtk.ICON_SIZE_BUTTON) + bk, _ := gtk.ButtonNewFromIconName("go-previous-symbolic", iconSize) bk.SetVAlign(gtk.ALIGN_CENTER) bk.Show() backButtonCSS(bk) @@ -61,7 +64,11 @@ func NewHeader() *Header { bc.Show() rightBreadcrumbCSS(bc) - memberIcon, _ := gtk.ImageNewFromIconName("system-users-symbolic", gtk.ICON_SIZE_BUTTON) + msgctrl := NewMessageControl() + msgctrl.Disable() + msgctrl.Show() + + memberIcon, _ := gtk.ImageNewFromIconName("system-users-symbolic", iconSize) memberIcon.Show() mb, _ := gtk.ToggleButtonNew() @@ -75,6 +82,7 @@ func NewHeader() *Header { header.PackStart(rbk) header.PackStart(bc) header.PackEnd(mb) + header.PackEnd(msgctrl) header.Show() // Hack to hide the title. @@ -86,12 +94,14 @@ func NewHeader() *Header { ShowBackBtn: rbk, BackButton: bk, Breadcrumb: bc, + MessageCtrl: msgctrl, ShowMembers: mb, } } func (h *Header) Reset() { h.SetBreadcrumber(nil) + h.MessageCtrl.Disable() } func (h *Header) OnBackPressed(fn func()) { diff --git a/internal/ui/messages/input/input.go b/internal/ui/messages/input/input.go index 068f9f1..3753e40 100644 --- a/internal/ui/messages/input/input.go +++ b/internal/ui/messages/input/input.go @@ -11,6 +11,7 @@ import ( "github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput" + "github.com/diamondburned/handy" "github.com/gotk3/gotk3/gtk" "github.com/pkg/errors" ) @@ -54,7 +55,7 @@ var textCSS = primitives.PrepareCSS(` } `) -var inputBoxCSS = primitives.PrepareClassCSS("input-box", ` +var inputMainBoxCSS = primitives.PrepareClassCSS("input-box", ` .input-box { background-color: @theme_bg_color; } @@ -111,14 +112,20 @@ const ( editButtonIcon = "document-edit-symbolic" replyButtonIcon = "mail-reply-sender-symbolic" sendButtonSize = gtk.ICON_SIZE_BUTTON + + ClampMaxSize = 1000 + ClampThreshold = ClampMaxSize ) type Field struct { - // Box contains the field box and the attachment container. *gtk.Box + Clamp *handy.Clamp + + // MainBox contains the field box and the attachment container. + MainBox *gtk.Box Attachments *attachment.Container - // FieldBox contains the username container and the input field. It spans + // FieldMainBox contains the username container and the input field. It spans // horizontally. FieldBox *gtk.Box Username *username.Container @@ -212,11 +219,22 @@ func NewField(text *gtk.TextView, ctrl Controller, labeler LabelBorrower) *Field field.Attachments = attachment.New() field.Attachments.Show() - field.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2) - field.Box.PackStart(field.Attachments, false, false, 0) - field.Box.PackStart(field.FieldBox, false, false, 0) + field.MainBox, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2) + field.MainBox.PackStart(field.Attachments, false, false, 0) + field.MainBox.PackStart(field.FieldBox, false, false, 0) + field.MainBox.Show() + + field.Clamp = handy.ClampNew() + field.Clamp.SetMaximumSize(ClampMaxSize) + field.Clamp.SetTighteningThreshold(ClampThreshold) + field.Clamp.SetHExpand(true) + field.Clamp.Add(field.MainBox) + field.Clamp.Show() + + field.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) + field.Box.Add(field.Clamp) field.Box.Show() - inputBoxCSS(field.Box) + inputMainBoxCSS(field.Clamp) text.SetFocusHAdjustment(field.TextScroll.GetHAdjustment()) text.SetFocusVAdjustment(field.TextScroll.GetVAdjustment()) diff --git a/internal/ui/messages/message/message.go b/internal/ui/messages/message/message.go index 21d2ef1..1b2ce8d 100644 --- a/internal/ui/messages/message/message.go +++ b/internal/ui/messages/message/message.go @@ -67,7 +67,7 @@ type GenericContainer struct { contentBox *gtk.Box // basically what is in Content ContentBody *labeluri.Label - MenuItems []menu.Item + menuItems []menu.Item } var _ Container = (*GenericContainer)(nil) @@ -114,7 +114,7 @@ func NewEmptyContainer() *GenericContainer { ctbody := labeluri.NewLabel(text.Rich{}) ctbody.SetVExpand(true) - ctbody.SetHExpand(true) + ctbody.SetHAlign(gtk.ALIGN_START) ctbody.SetEllipsize(pango.ELLIPSIZE_NONE) ctbody.SetLineWrap(true) ctbody.SetLineWrapMode(pango.WRAP_WORD_CHAR) @@ -125,6 +125,7 @@ func NewEmptyContainer() *GenericContainer { // Wrap the content label inside a content box. ctbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) + ctbox.SetHExpand(true) ctbox.PackStart(ctbody, false, false, 0) ctbox.Show() @@ -161,7 +162,7 @@ func NewEmptyContainer() *GenericContainer { // Bind the custom popup menu to the content label. gc.ContentBody.Connect("populate-popup", func(l *gtk.Label, m *gtk.Menu) { menu.MenuSeparator(m) - menu.MenuItems(m, gc.MenuItems) + menu.MenuItems(m, gc.menuItems) }) return gc @@ -248,7 +249,12 @@ func (m *GenericContainer) UpdateContent(content text.Rich, edited bool) { // AttachMenu connects signal handlers to handle a list of menu items from // the container. func (m *GenericContainer) AttachMenu(newItems []menu.Item) { - m.MenuItems = newItems + m.menuItems = newItems +} + +// MenuItems returns the list of menu items for this message. +func (m *GenericContainer) MenuItems() []menu.Item { + return m.menuItems } func (m *GenericContainer) Focusable() gtk.IWidget { diff --git a/internal/ui/messages/msgctrl.go b/internal/ui/messages/msgctrl.go new file mode 100644 index 0000000..574eff3 --- /dev/null +++ b/internal/ui/messages/msgctrl.go @@ -0,0 +1,107 @@ +package messages + +import ( + "github.com/diamondburned/cchat-gtk/internal/ui/messages/container" + "github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu" + "github.com/gotk3/gotk3/glib" + "github.com/gotk3/gotk3/gtk" +) + +type bindableButton struct { + gtk.Button + h glib.SignalHandle +} + +func newBindableButton(iconName string) *bindableButton { + btn, _ := gtk.ButtonNewFromIconName(iconName, iconSize) + return &bindableButton{ + Button: *btn, + } +} + +func (btn *bindableButton) bind(fn func()) { + btn.unbind() + if fn != nil { + btn.h = btn.Connect("clicked", func(*gtk.Button) { fn() }) + btn.SetSensitive(true) + btn.Show() + } +} + +func (btn *bindableButton) unbind() { + if btn.h > 0 { + btn.HandlerDisconnect(btn.h) + btn.h = 0 + btn.SetSensitive(false) + btn.Hide() + } +} + +// MessageItemNames contains names that MessageControl will use for its menu +// action callbacks. +type MessageItemNames struct { + Reply, Edit, Delete string +} + +// MessageControl controls buttons that control a selected message. +type MessageControl struct { + gtk.Revealer + Box *gtk.Box + + hide bool + + Reply *bindableButton + Edit *bindableButton + Delete *bindableButton // Actions "Delete" +} + +func NewMessageControl() *MessageControl { + mc := MessageControl{} + + mc.Reply = newBindableButton("mail-reply-sender-symbolic") + mc.Edit = newBindableButton("document-edit-symbolic") + mc.Delete = newBindableButton("edit-delete-symbolic") + + mc.Box, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 2) + mc.Box.Add(mc.Reply) + mc.Box.Add(mc.Edit) + mc.Box.Add(mc.Delete) + mc.Box.Show() + + r, _ := gtk.RevealerNew() + mc.Revealer = *r + mc.Revealer.SetTransitionDuration(75) + mc.Revealer.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_CROSSFADE) + mc.Revealer.Add(mc.Box) + + mc.Disable() + + return &mc +} + +// Enable enables the MessageControl with the given message. +func (mc *MessageControl) Enable(msg container.MessageRow, names MessageItemNames) { + mc.SetSensitive(true) + mc.SetRevealChild(true && !mc.hide) + + items := msg.MenuItems() + + mc.Reply.bind(menu.FindItemFunc(items, names.Reply)) + mc.Edit.bind(menu.FindItemFunc(items, names.Edit)) + mc.Delete.bind(menu.FindItemFunc(items, names.Delete)) +} + +// SetHidden sets whether or not the control should be hidden. +func (mc *MessageControl) SetHidden(hidden bool) { + mc.hide = hidden +} + +// Disable disables the MessageControl and hides it. +func (mc *MessageControl) Disable() { + mc.SetSensitive(false) + mc.SetRevealChild(false) + + mc.Reply.unbind() + mc.Edit.unbind() + mc.Delete.unbind() +} diff --git a/internal/ui/messages/state.go b/internal/ui/messages/state.go new file mode 100644 index 0000000..e232f78 --- /dev/null +++ b/internal/ui/messages/state.go @@ -0,0 +1,86 @@ +package messages + +import ( + "time" + + "github.com/diamondburned/cchat" +) + +// ServerMessage combines Server and ServerMessage from cchat. +type ServerMessage interface { + cchat.Server + cchat.Messenger +} + +type state struct { + session cchat.Session + server cchat.Server + + actioner cchat.Actioner + backlogger cchat.Backlogger + + current func() // stop callback + author string + + lastBacklogged time.Time +} + +func (s *state) Reset() { + // If we still have the last server to leave, then leave it. + if s.current != nil { + s.current() + } + + // Lazy way to reset the state. + *s = state{} +} + +func (s *state) hasActions() bool { + return s.actioner != nil +} + +// SessionID returns the session ID, or an empty string if there's no session. +func (s *state) SessionID() string { + if s.session != nil { + return s.session.ID() + } + return "" +} + +// ServerID returns the server ID, or an empty string if there's no server. +func (s *state) ServerID() string { + if s.server != nil { + return s.server.ID() + } + return "" +} + +const backloggingFreq = time.Second * 3 + +// Backlogger returns the backlogger instance if it's allowed to fetch more +// backlogs. +func (s *state) Backlogger() cchat.Backlogger { + if s.backlogger == nil || s.current == nil { + return nil + } + + var now = time.Now() + + if s.lastBacklogged.Add(backloggingFreq).After(now) { + return nil + } + + s.lastBacklogged = now + return s.backlogger +} + +func (s *state) bind(session cchat.Session, server cchat.Server, msgr cchat.Messenger) { + s.session = session + s.server = server + s.actioner = msgr.AsActioner() + s.backlogger = msgr.AsBacklogger() +} + +func (s *state) setcurrent(fn func()) { + s.current = fn +} diff --git a/internal/ui/messages/typing/typing.go b/internal/ui/messages/typing/typing.go index 2395547..5dbeda0 100644 --- a/internal/ui/messages/typing/typing.go +++ b/internal/ui/messages/typing/typing.go @@ -6,6 +6,7 @@ import ( "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup" + "github.com/diamondburned/handy" "github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/pango" ) @@ -33,10 +34,17 @@ var smallfonts = primitives.PrepareCSS(` * { font-size: 0.9em; } `) +const ( + // Keep the same as input. + ClampMaxSize = 1000 - 6*2 // account for margin + ClampThreshold = ClampMaxSize +) + type Container struct { *gtk.Revealer state *State + clamp *handy.Clamp dots *gtk.Box label *gtk.Label @@ -62,11 +70,18 @@ func New() *Container { b.PackStart(l, true, true, 0) b.Show() + c := handy.ClampNew() + c.SetMaximumSize(ClampMaxSize) + c.SetTighteningThreshold(ClampThreshold) + c.SetHExpand(true) + c.Add(b) + c.Show() + r, _ := gtk.RevealerNew() r.SetTransitionDuration(100) r.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_CROSSFADE) r.SetRevealChild(false) - r.Add(b) + r.Add(c) typingIndicatorCSS(b) diff --git a/internal/ui/messages/view.go b/internal/ui/messages/view.go index a011205..dbe6bc6 100644 --- a/internal/ui/messages/view.go +++ b/internal/ui/messages/view.go @@ -55,8 +55,8 @@ type Controller interface { type MessagesContainer interface { gtk.IWidget - container.Container cchat.MessagesContainer + container.Container } type View struct { @@ -250,11 +250,8 @@ func (v *View) reset() { func (v *View) SetFolded(folded bool) { v.parentFolded = folded - - // Change to a mini breadcrumb if we're collapsed. v.Header.SetMiniBreadcrumb(folded) - - // Hide the username in the input bar if we're collapsed. + v.Header.MessageCtrl.SetHidden(folded) v.InputView.Username.SetRevealChild(!folded) // Hide the member list automatically on folded. @@ -266,7 +263,7 @@ func (v *View) SetFolded(folded bool) { // MemberListUpdated is called everytime the member list is updated. func (v *View) MemberListUpdated(c *memberlist.Container) { // We can show the members list if it's not empty. - var empty = c.IsEmpty() + empty := c.IsEmpty() v.Header.SetCanShowMembers(!empty) // If the member list is now empty, then hide the entire thing. @@ -430,6 +427,12 @@ func (v *View) retryMessage(msg input.PresendMessage, presend container.PresendM }() } +var messageItemNames = MessageItemNames{ + Reply: "Reply", + Edit: "Edit", + Delete: "Delete", +} + // BindMenu attaches the menu constructor into the message with the needed // states and callbacks. func (v *View) BindMenu(msg container.MessageRow) { @@ -475,81 +478,13 @@ func (v *View) makeActionItem(action, msgID string) menu.Item { }) } -// ServerMessage combines Server and ServerMessage from cchat. -type ServerMessage interface { - cchat.Server - cchat.Messenger +// SelectMessage is called when a message is selected. +func (v *View) SelectMessage(_ *container.ListStore, msg container.MessageRow) { + // Hijack the message's action list to search for what we have above. + v.Header.MessageCtrl.Enable(msg, messageItemNames) } -type state struct { - session cchat.Session - server cchat.Server - - actioner cchat.Actioner - backlogger cchat.Backlogger - - current func() // stop callback - author string - - lastBacklogged time.Time -} - -func (s *state) Reset() { - // If we still have the last server to leave, then leave it. - if s.current != nil { - s.current() - } - - // Lazy way to reset the state. - *s = state{} -} - -func (s *state) hasActions() bool { - return s.actioner != nil -} - -// SessionID returns the session ID, or an empty string if there's no session. -func (s *state) SessionID() string { - if s.session != nil { - return s.session.ID() - } - return "" -} - -// ServerID returns the server ID, or an empty string if there's no server. -func (s *state) ServerID() string { - if s.server != nil { - return s.server.ID() - } - return "" -} - -const backloggingFreq = time.Second * 3 - -// Backlogger returns the backlogger instance if it's allowed to fetch more -// backlogs. -func (s *state) Backlogger() cchat.Backlogger { - if s.backlogger == nil || s.current == nil { - return nil - } - - var now = time.Now() - - if s.lastBacklogged.Add(backloggingFreq).After(now) { - return nil - } - - s.lastBacklogged = now - return s.backlogger -} - -func (s *state) bind(session cchat.Session, server cchat.Server, msgr cchat.Messenger) { - s.session = session - s.server = server - s.actioner = msgr.AsActioner() - s.backlogger = msgr.AsBacklogger() -} - -func (s *state) setcurrent(fn func()) { - s.current = fn +// UnselectMessage is called when the message selection is cleared. +func (v *View) UnselectMessage() { + v.Header.MessageCtrl.Disable() } diff --git a/internal/ui/primitives/menu/menu.go b/internal/ui/primitives/menu/menu.go index 8cb5709..aca632c 100644 --- a/internal/ui/primitives/menu/menu.go +++ b/internal/ui/primitives/menu/menu.go @@ -72,6 +72,17 @@ func MenuItems(menu MenuAppender, items []Item) { } } +// FindItemFunc iterates over the list of items and returns the first item with +// the matching name. +func FindItemFunc(items []Item, name string) func() { + for _, item := range items { + if item.Name == name { + return item.Func + } + } + return nil +} + type ToolbarInserter interface { Insert(gtk.IToolItem, int) } diff --git a/internal/ui/rich/labeluri/labeluri.go b/internal/ui/rich/labeluri/labeluri.go index 10519e5..0cd9130 100644 --- a/internal/ui/rich/labeluri/labeluri.go +++ b/internal/ui/rich/labeluri/labeluri.go @@ -27,8 +27,8 @@ import ( const ( AvatarSize = 96 PopoverWidth = 250 - MaxWidth = 350 - MaxHeight = 350 + MaxWidth = 500 + MaxHeight = 500 ) type WidgetConnector interface { diff --git a/internal/ui/rich/parser/attrmap/attrmap.go b/internal/ui/rich/parser/attrmap/attrmap.go index d7e205c..7879160 100644 --- a/internal/ui/rich/parser/attrmap/attrmap.go +++ b/internal/ui/rich/parser/attrmap/attrmap.go @@ -117,7 +117,7 @@ func (a AppendMap) Get(ind int) (tags string) { // Borrowing appended's backing array to add prepended is probably fine, as // the length of the actual appended slice is going to stay the same. - return string(append(appended, prepended...)) + return string(append(prepended, appended...)) } func (a *AppendMap) Finalize(strlen int) []int { diff --git a/internal/ui/service/config/config.go b/internal/ui/service/config/config.go index b7b162f..16ec120 100644 --- a/internal/ui/service/config/config.go +++ b/internal/ui/service/config/config.go @@ -36,7 +36,7 @@ func Restore(conf Configurator) { file := serviceFile(conf) - if err := config.UnmarshalFromFile(file, c); err != nil { + if err := config.UnmarshalFromFile(file, &c); err != nil { return nil, errors.Wrapf(err, "failed to unmarshal %s config", conf.Name()) } @@ -57,7 +57,7 @@ func Spawn(conf Configurator) error { file := serviceFile(conf) - err = config.UnmarshalFromFile(file, c) + err = config.UnmarshalFromFile(file, &c) err = errors.Wrapf(err, "failed to unmarshal %s config", conf.Name()) return func() { diff --git a/internal/ui/service/session/servers.go b/internal/ui/service/session/servers.go index 41bd4f9..2040e5e 100644 --- a/internal/ui/service/session/servers.go +++ b/internal/ui/service/session/servers.go @@ -114,9 +114,13 @@ func (s *Servers) SetServers(servers []cchat.Server) { gts.ExecAsync(func() { s.Children.SetServersUnsafe(servers) - if servers == nil { + if len(servers) == 0 { s.ctrl.ClearMessenger() + return } + + // Reload all top-level nodes. + s.Children.LoadAll() }) } diff --git a/internal/ui/ui.go b/internal/ui/ui.go index f6517a0..8092cd6 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -160,7 +160,7 @@ func (app *App) SessionSelected(svc *service.Service, ses *session.Row) { func (app *App) ClearMessenger(ses *session.Row) { if app.MessageView.SessionID() == ses.Session.ID() { - return + app.MessageView.Reset() } }