diff --git a/go.mod b/go.mod index 1cc2675..3426c87 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/diamondburned/cchat-gtk go 1.14 -replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200816224505-3cd69b83a48a +replace github.com/gotk3/gotk3 => github.com/gotk3/gotk3 v0.5.1-0.20201028052159-952547abf55a require ( github.com/Xuanwo/go-locale v1.0.0 diff --git a/go.sum b/go.sum index cc5b11e..f00c92e 100644 --- a/go.sum +++ b/go.sum @@ -234,6 +234,8 @@ github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gotk3/gotk3 v0.5.1-0.20201028052159-952547abf55a h1:9O8VeGmNRqh8UPYLfjYc+W3Gu7vSVTo2uEswq4FO9xI= +github.com/gotk3/gotk3 v0.5.1-0.20201028052159-952547abf55a/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= diff --git a/internal/gts/gts.go b/internal/gts/gts.go index 982a703..f927690 100644 --- a/internal/gts/gts.go +++ b/internal/gts/gts.go @@ -226,11 +226,10 @@ func RenderPixbuf(img image.Image) *gdk.Pixbuf { } func SpawnUploader(dirpath string, callback func(absolutePaths []string)) { - dialog, _ := gtk.FileChooserDialogNewWith2Buttons( + dialog, _ := gtk.FileChooserNativeDialogNew( "Upload File", App.Window, gtk.FILE_CHOOSER_ACTION_OPEN, - "Cancel", gtk.RESPONSE_CANCEL, - "Upload", gtk.RESPONSE_ACCEPT, + "Upload", "Cancel", ) App.Throttler.Connect(dialog) @@ -248,9 +247,7 @@ func SpawnUploader(dirpath string, callback func(absolutePaths []string)) { dialog.SetCurrentFolder(dirpath) dialog.SetSelectMultiple(true) - defer dialog.Close() - - if res := dialog.Run(); res != gtk.RESPONSE_ACCEPT { + if res := dialog.Run(); res != int(gtk.RESPONSE_ACCEPT) { return } @@ -259,12 +256,12 @@ func SpawnUploader(dirpath string, callback func(absolutePaths []string)) { } // BindPreviewer binds the file chooser dialog with a previewer. -func BindPreviewer(fc *gtk.FileChooserDialog) { +func BindPreviewer(fc *gtk.FileChooserNativeDialog) { img, _ := gtk.ImageNew() fc.SetPreviewWidget(img) fc.Connect("update-preview", - func(fc *gtk.FileChooserDialog, img *gtk.Image) { + func(_ interface{}, img *gtk.Image) { file := fc.GetPreviewFilename() b, err := gdk.PixbufNewFromFileAtScale(file, 256, 256, true) diff --git a/internal/gts/throttler/throttler.go b/internal/gts/throttler/throttler.go index 7d14f79..4ecdc50 100644 --- a/internal/gts/throttler/throttler.go +++ b/internal/gts/throttler/throttler.go @@ -16,7 +16,6 @@ type State struct { } type Connector interface { - gtk.IWidget Connect(string, interface{}, ...interface{}) (glib.SignalHandle, error) } diff --git a/internal/ui/messages/container/compact/compact.go b/internal/ui/messages/container/compact/compact.go index 31742d3..96d5675 100644 --- a/internal/ui/messages/container/compact/compact.go +++ b/internal/ui/messages/container/compact/compact.go @@ -2,6 +2,7 @@ package compact import ( "github.com/diamondburned/cchat" + "github.com/diamondburned/cchat-gtk/internal/gts" "github.com/diamondburned/cchat-gtk/internal/ui/messages/container" "github.com/diamondburned/cchat-gtk/internal/ui/messages/input" "github.com/diamondburned/cchat-gtk/internal/ui/primitives" @@ -17,6 +18,18 @@ func NewContainer(ctrl container.Controller) *Container { return &Container{c} } +func (c *Container) CreateMessage(msg cchat.MessageCreate) { + gts.ExecAsync(func() { c.GridContainer.CreateMessageUnsafe(msg) }) +} + +func (c *Container) UpdateMessage(msg cchat.MessageUpdate) { + gts.ExecAsync(func() { c.GridContainer.UpdateMessageUnsafe(msg) }) +} + +func (c *Container) DeleteMessage(msg cchat.MessageDelete) { + gts.ExecAsync(func() { c.GridContainer.DeleteMessageUnsafe(msg) }) +} + type constructor struct{} func (constructor) NewMessage(msg cchat.MessageCreate) container.GridMessage { diff --git a/internal/ui/messages/container/container.go b/internal/ui/messages/container/container.go index 5cdf597..36662f5 100644 --- a/internal/ui/messages/container/container.go +++ b/internal/ui/messages/container/container.go @@ -2,7 +2,6 @@ package container import ( "github.com/diamondburned/cchat" - "github.com/diamondburned/cchat-gtk/internal/gts" "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/menu" @@ -34,9 +33,12 @@ type Container interface { gtk.IWidget // Thread-safe methods. - cchat.MessagesContainer + // cchat.MessagesContainer // Thread-unsafe methods. + + // CreateMessageUnsafe creates a new message and returns the index that is + // the location the message is added to. CreateMessageUnsafe(cchat.MessageCreate) UpdateMessageUnsafe(cchat.MessageUpdate) DeleteMessageUnsafe(cchat.MessageDelete) @@ -111,15 +113,3 @@ func (c *GridContainer) CreateMessageUnsafe(msg cchat.MessageCreate) { c.DeleteEarliest(c.MessagesLen() - BacklogLimit) } } - -func (c *GridContainer) CreateMessage(msg cchat.MessageCreate) { - gts.ExecAsync(func() { c.CreateMessageUnsafe(msg) }) -} - -func (c *GridContainer) UpdateMessage(msg cchat.MessageUpdate) { - gts.ExecAsync(func() { c.UpdateMessageUnsafe(msg) }) -} - -func (c *GridContainer) DeleteMessage(msg cchat.MessageDelete) { - gts.ExecAsync(func() { c.DeleteMessageUnsafe(msg) }) -} diff --git a/internal/ui/messages/container/cozy/cozy.go b/internal/ui/messages/container/cozy/cozy.go index 6a759c8..1efb4eb 100644 --- a/internal/ui/messages/container/cozy/cozy.go +++ b/internal/ui/messages/container/cozy/cozy.go @@ -121,7 +121,8 @@ func (c *Container) reuseAvatar(authorID, avatarURL string, full *FullMessage) { } func (c *Container) lastMessageIsAuthor(id string, offset int) bool { - var last = c.GridStore.NthMessage(c.GridStore.MessagesLen() - (1 + offset)) + // Get the offfsetth message from last. + var last = c.GridStore.NthMessage((c.GridStore.MessagesLen() - 1) + offset) return last != nil && last.AuthorID() == id } @@ -131,33 +132,42 @@ func (c *Container) CreateMessage(msg cchat.MessageCreate) { // wipe old messages. c.GridContainer.CreateMessageUnsafe(msg) - // Should we collapse this message? Yes, if the current message's author - // is the same as the last author. - if c.lastMessageIsAuthor(msg.Author().ID(), 1) { - c.compact(c.GridContainer.LastMessage()) - } - - // See if we need to collapse the second message. - if sec := c.NthMessage(1); sec != nil { - // If the author isn't the same, then ignore. - if sec.AuthorID() != msg.Author().ID() { - return - } - - // The author is the same; collapse. - c.compact(sec) - } - // Did the handler wipe old messages? It will only do so if the user is // scrolled to the bottom. - if !c.Bottomed() { - // If we're not at the bottom, then we exit. - return + if c.Bottomed() { + // We need to uncollapse the first (top) message. No length check is + // needed here, as we just inserted a message. + c.uncompact(c.FirstMessage()) } - // We need to uncollapse the first (top) message. No length check is - // needed here, as we just inserted a message. - c.uncompact(c.FirstMessage()) + switch msg.ID() { + // Should we collapse this message? Yes, if the current message is + // inserted at the end and its author is the same as the last author. + case c.GridContainer.LastMessage().ID(): + if c.lastMessageIsAuthor(msg.Author().ID(), -1) { + c.compact(c.GridContainer.LastMessage()) + } + + // If we've prepended the message, then see if we need to collapse the + // second message. + case c.GridContainer.FirstMessage().ID(): + if sec := c.NthMessage(1); sec != nil { + // If the author isn't the same, then ignore. + if sec.AuthorID() != msg.Author().ID() { + return + } + + // The author is the same; collapse. + c.compact(sec) + } + } + + }) +} + +func (c *Container) UpdateMessage(msg cchat.MessageUpdate) { + gts.ExecAsync(func() { + c.UpdateMessageUnsafe(msg) }) } diff --git a/internal/ui/messages/container/grid.go b/internal/ui/messages/container/grid.go index c276a19..e1f0dff 100644 --- a/internal/ui/messages/container/grid.go +++ b/internal/ui/messages/container/grid.go @@ -208,12 +208,20 @@ func (c *GridStore) NthMessage(n int) GridMessage { // FirstMessage returns the first message. func (c *GridStore) FirstMessage() GridMessage { - return c.NthMessage(0) + if c.messageList.Len() == 0 { + return nil + } + // Long unwrap. + return c.messageList.Front().Value.(*gridMessage).GridMessage } // LastMessage returns the latest message. func (c *GridStore) LastMessage() GridMessage { - return c.NthMessage(c.MessagesLen() - 1) + if c.messageList.Len() == 0 { + return nil + } + // Long unwrap. + return c.messageList.Back().Value.(*gridMessage).GridMessage } // Message finds the message state in the container. It is not thread-safe. This @@ -273,17 +281,21 @@ func (c *GridStore) AddPresendMessage(msg input.PresendMessage) PresendGridMessa return presend } +// Many attempts were made to have CreateMessageUnsafe return an index. That is +// unreliable. The index might be off if the message buffer is cleaned up. Don't +// rely on it. + func (c *GridStore) CreateMessageUnsafe(msg cchat.MessageCreate) { // Call the event handler last. defer c.Controller.AuthorEvent(msg.Author()) // Attempt to update before insertion (aka upsert). - if msgc := c.Message(msg.ID(), msg.Nonce()); msgc != nil { + if msgc := c.message(msg.ID(), msg.Nonce()); msgc != nil { msgc.UpdateAuthor(msg.Author()) msgc.UpdateContent(msg.Content(), false) msgc.UpdateTimestamp(msg.Time()) - c.Controller.BindMenu(msgc) + c.Controller.BindMenu(msgc.GridMessage) return } diff --git a/internal/ui/messages/input/input.go b/internal/ui/messages/input/input.go index b5cf4f6..b027158 100644 --- a/internal/ui/messages/input/input.go +++ b/internal/ui/messages/input/input.go @@ -241,7 +241,7 @@ func (f *Field) SetMessenger(session cchat.Session, messenger cchat.Messenger) { f.typing = f.Messenger.AsTypingIndicator() // See if we can upload files. - f.SetAllowUpload(f.Sender.CanAttach()) + f.SetAllowUpload(f.Sender != nil && f.Sender.CanAttach()) // Populate the duration state if typer is not nil. if f.typing != nil { diff --git a/internal/ui/messages/view.go b/internal/ui/messages/view.go index 7f79cf5..50be090 100644 --- a/internal/ui/messages/view.go +++ b/internal/ui/messages/view.go @@ -52,6 +52,12 @@ type Controller interface { OnMessageDone() } +type MessagesContainer interface { + gtk.IWidget + container.Container + cchat.MessagesContainer +} + type View struct { *gtk.Box @@ -66,7 +72,7 @@ type View struct { MsgBox *gtk.Box Typing *typing.Container - Container container.Container + Container MessagesContainer contType int // msgIndex MemberList *memberlist.Container // right box diff --git a/internal/ui/primitives/primitives.go b/internal/ui/primitives/primitives.go index d62d19f..2432de7 100644 --- a/internal/ui/primitives/primitives.go +++ b/internal/ui/primitives/primitives.go @@ -2,7 +2,6 @@ package primitives import ( "runtime/debug" - "time" "github.com/diamondburned/cchat-gtk/internal/gts" "github.com/diamondburned/cchat-gtk/internal/log" @@ -279,24 +278,7 @@ func InlineCSS(ctx StyleContexter, css string) { // LeafletOnFold binds a callback to a leaflet that would be called when the // leaflet's folded state changes. func LeafletOnFold(leaflet *handy.Leaflet, foldedFn func(folded bool)) { - var lastFold = leaflet.GetFolded() - foldedFn(lastFold) - - // Give each callback a 500ms wait for animations to complete. - const dt = 500 * time.Millisecond - var last = time.Now() - - leaflet.ConnectAfter("size-allocate", func() { - // Ignore if this event is too recent. - if now := time.Now(); now.Add(-dt).Before(last) { - return - } else { - last = now - } - - if folded := leaflet.GetFolded(); folded != lastFold { - lastFold = folded - foldedFn(folded) - } + leaflet.ConnectAfter("notify::folded", func() { + foldedFn(leaflet.GetFolded()) }) } diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 86a4c2f..275d6f2 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -15,6 +15,7 @@ import ( "github.com/diamondburned/handy" "github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/glib" + "github.com/gotk3/gotk3/gtk" "github.com/pkg/errors" ) @@ -83,10 +84,19 @@ func NewApplication() *App { app.HeaderGroup.AddHeaderBar(&app.Services.Header.HeaderBar) app.HeaderGroup.AddHeaderBar(&app.MessageView.Header.HeaderBar) + separator, _ := gtk.SeparatorNew(gtk.ORIENTATION_VERTICAL) + separator.Show() + app.Leaflet = *handy.LeafletNew() - app.Leaflet.SetTransitionType(handy.LeafletTransitionTypeUnder) + app.Leaflet.SetChildTransitionDuration(75) + app.Leaflet.SetTransitionType(handy.LeafletTransitionTypeSlide) + app.Leaflet.SetCanSwipeBack(true) + app.Leaflet.Add(app.Services) + app.Leaflet.Add(separator) app.Leaflet.Add(app.MessageView) + + app.Leaflet.ChildSetProperty(separator, "navigatable", false) app.Leaflet.Show() // Bind the preferences action for our GAction button in the header popover. diff --git a/shell.nix b/shell.nix index 7059abc..56e87d8 100644 --- a/shell.nix +++ b/shell.nix @@ -1,12 +1,26 @@ { pkgs ? import {} }: -pkgs.stdenv.mkDerivation rec { +let libhandy = pkgs.libhandy.overrideAttrs(old: { + name = "libhandy-1.0.1"; + src = builtins.fetchGit { + url = "https://gitlab.gnome.org/GNOME/libhandy.git"; + rev = "5cee0927b8b39dea1b2a62ec6d19169f73ba06c6"; + }; + patches = []; + + buildInputs = old.buildInputs ++ (with pkgs; [ + gnome3.librsvg + gdk-pixbuf + ]); +}); + +in pkgs.stdenv.mkDerivation rec { name = "cchat-gtk"; version = "0.0.2"; - buildInputs = with pkgs; [ - libhandy gnome3.gspell gnome3.glib gnome3.gtk - ]; + buildInputs = [ libhandy ] ++ (with pkgs; [ + gnome3.gspell gnome3.glib gnome3.gtk + ]); nativeBuildInputs = with pkgs; [ pkgconfig go