package cozy import ( "time" "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-gtk/internal/gts/httputil" "github.com/diamondburned/cchat-gtk/internal/humanize" "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/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/primitives/roundimage" "github.com/diamondburned/cchat-gtk/internal/ui/rich/labeluri" "github.com/gotk3/gotk3/gtk" ) // TopFullMargin is the margin on top of every full message. const TopFullMargin = 4 type FullMessage struct { *message.GenericContainer // Grid widgets. Avatar *Avatar MainBox *gtk.Box // wraps header and content // Header wraps author and timestamp. HeaderBox *gtk.Box } type AvatarPixbufCopier interface { CopyAvatarPixbuf(img httputil.ImageContainer) } var ( _ AvatarPixbufCopier = (*FullMessage)(nil) _ message.Container = (*FullMessage)(nil) _ container.GridMessage = (*FullMessage)(nil) ) var boldCSS = primitives.PrepareCSS(` * { font-weight: 600; } `) var avatarCSS = primitives.PrepareClassCSS("cozy-avatar", ` /* Slightly dip down on click */ .cozy-avatar:active { margin-top: 1px; } `) func NewFullMessage(msg cchat.MessageCreate) *FullMessage { msgc := WrapFullMessage(message.NewContainer(msg)) // Don't update the avatar. NewMessage in controller will try and reuse the // pixbuf if possible. msgc.UpdateAuthorName(msg.Author().Name()) msgc.UpdateTimestamp(msg.Time()) msgc.UpdateContent(msg.Content(), false) return msgc } func WrapFullMessage(gc *message.GenericContainer) *FullMessage { avatar := NewAvatar() avatar.SetMarginTop(TopFullMargin) avatar.SetMarginStart(container.ColumnSpacing * 2) avatar.Connect("clicked", func() { if output := gc.Username.Output(); len(output.Mentions) > 0 { labeluri.PopoverMentioner(avatar, output.Input, output.Mentions[0]) } }) // We don't call avatar.Show(). That's called in Attach. // Style the timestamp accordingly. gc.Timestamp.SetXAlign(0.0) // left-align gc.Timestamp.SetVAlign(gtk.ALIGN_END) // bottom-align gc.Timestamp.SetMarginStart(0) // clear margins // Attach the class and CSS for the left avatar. avatarCSS(avatar) // Attach the username style provider. primitives.AttachCSS(gc.Username, boldCSS) header, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0) header.PackStart(gc.Username, false, false, 0) header.PackStart(gc.Timestamp, false, false, 7) // padding header.Show() main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) main.PackStart(header, false, false, 0) main.PackStart(gc.Content, false, false, 0) main.SetMarginTop(TopFullMargin) main.SetMarginEnd(container.ColumnSpacing * 2) main.Show() // Also attach a class for the main box shown on the right. primitives.AddClass(main, "cozy-main") return &FullMessage{ GenericContainer: gc, Avatar: avatar, MainBox: main, HeaderBox: header, } } func (m *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) m.HeaderBox.Remove(m.Timestamp) m.MainBox.Remove(m.Content) // Return after removing. return m.GenericContainer } func (m *FullMessage) UpdateTimestamp(t time.Time) { m.GenericContainer.UpdateTimestamp(t) m.Timestamp.SetText(humanize.TimeAgoLong(t)) } func (m *FullMessage) UpdateAuthor(author cchat.MessageAuthor) { // Call the parent's method to update the labels. m.GenericContainer.UpdateAuthor(author) // If the author has an avatar: if avatarer, ok := author.(cchat.MessageAuthorAvatar); ok { m.Avatar.SetURL(avatarer.Avatar()) } } // CopyAvatarPixbuf sets the pixbuf into the given container. This shares the // same pixbuf, but gtk.Image should take its own reference from the pixbuf. func (m *FullMessage) CopyAvatarPixbuf(dst httputil.ImageContainer) { switch img := m.Avatar.Image; img.GetStorageType() { case gtk.IMAGE_PIXBUF: dst.SetFromPixbuf(img.GetPixbuf()) case gtk.IMAGE_ANIMATION: dst.SetFromAnimation(img.GetAnimation()) } } func (m *FullMessage) Attach(grid *gtk.Grid, row int) { m.Avatar.Show() container.AttachRow(grid, row, m.Avatar, m.MainBox) } func (m *FullMessage) AttachMenu(items []menu.Item) { // Bind to parent's container as well. m.GenericContainer.AttachMenu(items) // Bind to the box. // TODO lol } type FullSendingMessage struct { message.PresendContainer FullMessage } var ( // _ AvatarPixbufCopier = (*FullSendingMessage)(nil) _ message.Container = (*FullSendingMessage)(nil) _ container.GridMessage = (*FullSendingMessage)(nil) ) func NewFullSendingMessage(msg input.PresendMessage) *FullSendingMessage { var msgc = message.NewPresendContainer(msg) return &FullSendingMessage{ PresendContainer: msgc, FullMessage: *WrapFullMessage(msgc.GenericContainer), } } type Avatar struct { roundimage.Button url string } func NewAvatar() *Avatar { avatar, _ := roundimage.NewButton() avatar.SetSizeRequest(AvatarSize, AvatarSize) avatar.SetVAlign(gtk.ALIGN_START) // Default icon. primitives.SetImageIcon( avatar.Image.Image, "user-available-symbolic", AvatarSize, ) return &Avatar{*avatar, ""} } // SetURL updates the Avatar to be that URL. It does nothing if URL is empty or // matches the existing one. func (a *Avatar) SetURL(url string) { // Check if the URL is the same. This will save us quite a few requests, as // some methods rely on the side-effects of other methods, and they may call // UpdateAuthor multiple times. if a.url == url || url == "" { return } a.url = url httputil.AsyncImageSized(a.Image, url, AvatarSize, AvatarSize) } // ManuallySetURL sets the URL without downloading the image. It assumes the // pixbuf is borrowed elsewhere. func (a *Avatar) ManuallySetURL(url string) { a.url = url }