mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-03-23 02:19:20 +00:00
Added limiting backlog and more general improvements
This commit is contained in:
parent
ce5c7a3016
commit
adeffc7717
|
@ -16,7 +16,7 @@ type header struct {
|
|||
|
||||
func newHeader() *header {
|
||||
left, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
left.SetSizeRequest(LeftWidth, -1)
|
||||
left.SetSizeRequest(leftMinWidth, -1)
|
||||
left.Show()
|
||||
|
||||
right := newHeaderRight()
|
||||
|
|
|
@ -9,6 +9,10 @@ import (
|
|||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
// BacklogLimit is the maximum number of messages to store in the container at
|
||||
// once.
|
||||
const BacklogLimit = 35
|
||||
|
||||
type GridMessage interface {
|
||||
message.Container
|
||||
// Attach should only be called once.
|
||||
|
@ -92,6 +96,32 @@ func NewGridContainer(constr Constructor, ctrl Controller) *GridContainer {
|
|||
}
|
||||
}
|
||||
|
||||
// CreateMessageUnsafe inserts a message as well as cleaning up the backlog if
|
||||
// the user is scrolled to the bottom.
|
||||
func (c *GridContainer) CreateMessageUnsafe(msg cchat.MessageCreate) {
|
||||
// Insert the message first.
|
||||
c.GridStore.CreateMessageUnsafe(msg)
|
||||
|
||||
// Determine if the user is scrolled to the bottom for cleaning up.
|
||||
if !c.ScrolledWindow.Bottomed {
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up the backlog.
|
||||
if clean := len(c.messages) - BacklogLimit; clean > 0 {
|
||||
// Remove them from the map and the container.
|
||||
for _, id := range c.messageIDs[:clean] {
|
||||
delete(c.messages, id)
|
||||
// We can gradually pop the first item off here, as we're removing
|
||||
// from 0th, and items are being shifted backwards.
|
||||
c.Grid.RemoveRow(0)
|
||||
}
|
||||
|
||||
// Cut the message IDs away by shifting the slice.
|
||||
c.messageIDs = append(c.messageIDs[:0], c.messageIDs[clean:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GridContainer) CreateMessage(msg cchat.MessageCreate) {
|
||||
gts.ExecAsync(func() { c.CreateMessageUnsafe(msg) })
|
||||
}
|
||||
|
|
|
@ -23,6 +23,20 @@ var (
|
|||
_ Unwrapper = (*FullSendingMessage)(nil)
|
||||
)
|
||||
|
||||
// Collapsible is an interface for cozy messages to return whether or not
|
||||
// they're full or collapsed.
|
||||
type Collapsible interface {
|
||||
// Compact returns true if the message is a compact one and not full.
|
||||
Collapsed() bool
|
||||
}
|
||||
|
||||
var (
|
||||
_ Collapsible = (*CollapsedMessage)(nil)
|
||||
_ Collapsible = (*CollapsedSendingMessage)(nil)
|
||||
_ Collapsible = (*FullMessage)(nil)
|
||||
_ Collapsible = (*FullSendingMessage)(nil)
|
||||
)
|
||||
|
||||
const (
|
||||
AvatarSize = 40
|
||||
AvatarMargin = 10
|
||||
|
@ -106,6 +120,25 @@ func (c *Container) reuseAvatar(authorID, avatarURL string, full *FullMessage) {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *Container) CreateMessage(msg cchat.MessageCreate) {
|
||||
gts.ExecAsync(func() {
|
||||
// Create the message in the parent's handler. This handler will also
|
||||
// wipe old messages.
|
||||
c.GridContainer.CreateMessageUnsafe(msg)
|
||||
|
||||
// Did the handler wipe old messages? It will only do so if the user is
|
||||
// scrolled to the bottom.
|
||||
if !c.ScrolledWindow.Bottomed {
|
||||
// If we're not at the bottom, then we exit.
|
||||
return
|
||||
}
|
||||
|
||||
// We need to uncollapse the first (top) message. No length check is
|
||||
// needed here, as we just inserted a message.
|
||||
c.uncompact(c.FirstMessage())
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
||||
gts.ExecAsync(func() {
|
||||
// Get the previous and next message before deleting. We'll need them to
|
||||
|
@ -138,21 +171,31 @@ func (c *Container) DeleteMessage(msg cchat.MessageDelete) {
|
|||
return
|
||||
}
|
||||
|
||||
// Get the unwrapper method, which allows us to get the
|
||||
// *message.GenericContainer.
|
||||
uw, ok := next.(Unwrapper)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Start the "lengthy" uncollapse process.
|
||||
full := WrapFullMessage(uw.Unwrap(c.Grid))
|
||||
// Update the container to reformat everything including the timestamps.
|
||||
message.RefreshContainer(full, full.GenericContainer)
|
||||
// Update the avatar if needed be, since we're now showing it.
|
||||
c.reuseAvatar(next.AuthorID(), next.AvatarURL(), full)
|
||||
|
||||
// Swap the old next message out for a new one.
|
||||
c.GridStore.SwapMessage(full)
|
||||
// Uncompact or turn the message to a full one.
|
||||
c.uncompact(next)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Container) uncompact(msg container.GridMessage) {
|
||||
// We should only uncompact the message if it's compacted in the first
|
||||
// place.
|
||||
if collapse, ok := msg.(Collapsible); !ok || !collapse.Collapsed() {
|
||||
return
|
||||
}
|
||||
|
||||
// We can't unwrap if the message doesn't implement Unwrapper.
|
||||
uw, ok := msg.(Unwrapper)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Start the "lengthy" uncollapse process.
|
||||
full := WrapFullMessage(uw.Unwrap(c.Grid))
|
||||
// Update the container to reformat everything including the timestamps.
|
||||
message.RefreshContainer(full, full.GenericContainer)
|
||||
// Update the avatar if needed be, since we're now showing it.
|
||||
c.reuseAvatar(msg.AuthorID(), msg.AvatarURL(), full)
|
||||
|
||||
// Swap the old next message out for a new one.
|
||||
c.GridStore.SwapMessage(full)
|
||||
}
|
||||
|
|
|
@ -36,6 +36,8 @@ func WrapCollapsedMessage(gc *message.GenericContainer) *CollapsedMessage {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *CollapsedMessage) Collapsed() bool { return true }
|
||||
|
||||
func (c *CollapsedMessage) Unwrap(grid *gtk.Grid) *message.GenericContainer {
|
||||
// Remove GenericContainer's widgets from the containers.
|
||||
grid.Remove(c.Timestamp)
|
|
@ -86,6 +86,8 @@ func WrapFullMessage(gc *message.GenericContainer) *FullMessage {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *FullMessage) Collapsed() bool { return false }
|
||||
|
||||
func (m *FullMessage) Unwrap(grid *gtk.Grid) *message.GenericContainer {
|
||||
// Remove GenericContainer's widgets from the containers.
|
||||
m.HeaderBox.Remove(m.Username)
|
||||
|
|
|
@ -126,10 +126,18 @@ func (c *GridStore) FindMessage(isMessage func(msg GridMessage) bool) GridMessag
|
|||
return nil
|
||||
}
|
||||
|
||||
// FirstMessage returns the first message.
|
||||
func (c *GridStore) FirstMessage() GridMessage {
|
||||
if len(c.messageIDs) > 0 {
|
||||
return c.messages[c.messageIDs[0]].GridMessage
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LastMessage returns the latest message.
|
||||
func (c *GridStore) LastMessage() GridMessage {
|
||||
if l := len(c.messageIDs); l > 0 {
|
||||
return c.messages[c.messageIDs[l-1]]
|
||||
return c.messages[c.messageIDs[l-1]].GridMessage
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -94,8 +94,6 @@ func NewView() *View {
|
|||
logo.Show()
|
||||
|
||||
view.FaceView = sadface.New(view.Box, logo)
|
||||
view.FaceView.Show()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/gotk3/gotk3/pango"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
@ -44,7 +45,8 @@ var (
|
|||
func NewLabel(content text.Rich) *Label {
|
||||
label, _ := gtk.LabelNew("")
|
||||
label.SetMarkup(parser.RenderMarkup(content))
|
||||
label.SetHAlign(gtk.ALIGN_START)
|
||||
label.SetXAlign(0) // left align
|
||||
label.SetEllipsize(pango.ELLIPSIZE_END)
|
||||
return &Label{*label, content}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@ func NewView() *View {
|
|||
sw, _ := gtk.ScrolledWindowNew(nil, nil)
|
||||
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||
sw.Add(box)
|
||||
sw.Show()
|
||||
|
||||
return &View{
|
||||
sw,
|
||||
|
|
|
@ -17,7 +17,23 @@ func init() {
|
|||
gts.LoadCSS(pkger.Include("/internal/ui/style.css"))
|
||||
}
|
||||
|
||||
const LeftWidth = 220
|
||||
// constraints for the left panel
|
||||
const (
|
||||
leftMinWidth = 200
|
||||
leftCurrentWidth = 250
|
||||
leftMaxWidth = 400
|
||||
)
|
||||
|
||||
func clamp(n, min, max int) int {
|
||||
switch {
|
||||
case n > max:
|
||||
return max
|
||||
case n < min:
|
||||
return min
|
||||
default:
|
||||
return n
|
||||
}
|
||||
}
|
||||
|
||||
type App struct {
|
||||
window *window
|
||||
|
@ -39,6 +55,14 @@ func NewApplication() *App {
|
|||
header: newHeader(),
|
||||
}
|
||||
|
||||
// Resize the left-side header w/ the left-side pane.
|
||||
app.window.Services.Connect("size-allocate", func(wv gtk.IWidget) {
|
||||
// Get the current width of the left sidebar.
|
||||
var width = app.window.GetPosition()
|
||||
// Set the left-side header's size.
|
||||
app.header.left.SetSizeRequest(width, -1)
|
||||
})
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
|
|
|
@ -7,24 +7,24 @@ import (
|
|||
)
|
||||
|
||||
type window struct {
|
||||
*gtk.Box
|
||||
*gtk.Paned
|
||||
Services *service.View
|
||||
MessageView *messages.View
|
||||
}
|
||||
|
||||
func newWindow() *window {
|
||||
services := service.NewView()
|
||||
services.SetSizeRequest(LeftWidth, -1)
|
||||
services.SetSizeRequest(leftMinWidth, -1)
|
||||
services.Show()
|
||||
|
||||
mesgview := messages.NewView()
|
||||
mesgview.Show()
|
||||
|
||||
separator, _ := gtk.SeparatorNew(gtk.ORIENTATION_VERTICAL)
|
||||
separator.Show()
|
||||
pane, _ := gtk.PanedNew(gtk.ORIENTATION_HORIZONTAL)
|
||||
pane.Pack1(services, false, false)
|
||||
pane.Pack2(mesgview, true, false)
|
||||
pane.SetPosition(leftCurrentWidth)
|
||||
pane.Show()
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
box.PackStart(services, false, false, 0)
|
||||
box.PackStart(separator, false, false, 0)
|
||||
box.PackStart(mesgview, true, true, 0)
|
||||
box.Show()
|
||||
|
||||
return &window{box, services, mesgview}
|
||||
return &window{pane, services, mesgview}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue