diff --git a/go.mod b/go.mod index 963edac..e0eb03a 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200630 require ( github.com/Xuanwo/go-locale v0.2.0 github.com/alecthomas/chroma v0.7.3 - github.com/diamondburned/cchat v0.0.43 - github.com/diamondburned/cchat-discord v0.0.0-20200717002543-508f355b9657 + github.com/diamondburned/cchat v0.0.45 + github.com/diamondburned/cchat-discord v0.0.0-20200717063909-2f4cb5f246c4 github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972 github.com/disintegration/imaging v1.6.2 diff --git a/go.sum b/go.sum index ecaa939..e8a4fbe 100644 --- a/go.sum +++ b/go.sum @@ -44,20 +44,22 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb h1:Ja/niwykeFoSkYxdRRzM8QUAuCswfLmaiBTd2UIU+54= github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb/go.mod h1:q1MbMBfZrv7xqV8n7LgMwhHs3oBbNwWJes8exs2AmDs= -github.com/diamondburned/arikawa v0.10.5 h1:o5lBopooA+8cXlKZdct5qF0xztuZZ35phvQrwGS5vYM= -github.com/diamondburned/arikawa v0.10.5/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660= +github.com/diamondburned/arikawa v0.12.4 h1:lhWJqcGkIIMiOYWdsoEuGlri2UbMkzMeh+VfuJPkXt4= +github.com/diamondburned/arikawa v0.12.4/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660= github.com/diamondburned/cchat v0.0.43 h1:HetAujSaUSdnQgAUZgprNLARjf/MSWXpCfWdvX2wOCU= github.com/diamondburned/cchat v0.0.43/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= -github.com/diamondburned/cchat-discord v0.0.0-20200717002543-508f355b9657 h1:/ZwVENnNKBioK34qA4sr/2B0pcJbDpPyw6DpiI3Cjr0= -github.com/diamondburned/cchat-discord v0.0.0-20200717002543-508f355b9657/go.mod h1:cX6rGfvIv2rfNPrhfcRx88bfNxyL7eFmiYZLCWGfchw= +github.com/diamondburned/cchat v0.0.45 h1:HMVSKx1h6lh2OenWaBTvMSK531hWaXAW7I0tKZepYug= +github.com/diamondburned/cchat v0.0.45/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= +github.com/diamondburned/cchat-discord v0.0.0-20200717063909-2f4cb5f246c4 h1:otasqokx6kIGo9KnJt1F22MdCyUIvZmxZkLv+kcdKiI= +github.com/diamondburned/cchat-discord v0.0.0-20200717063909-2f4cb5f246c4/go.mod h1:Z0uWBUaheEtozKj4NMgsSK4X5a3Du5tYakDb5plEluY= github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b h1:sq0MXjJc3yAOZvuolRxOpKQNvpMLyTmsECxQqdYgF5E= github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b/go.mod h1:+bAf0m2o5qH54DmYJ/lR1HeITV53ol0JaoKyFFx3m3E= github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d h1:Ha/I6PMKi+B4hpWclwlXj0tUMehR7Q0TNxPczzBwzPI= github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972 h1:OWxllHbUptXzDias6YI4MM0R3o50q8MfhkkwVIlfiNo= github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ= -github.com/diamondburned/ningen v0.1.1-0.20200715040340-2395a0dbd0fa h1:ntHcz6GNzxn3TovtYZVwOBvL3xn7Iq1luaV/KEIEXrk= -github.com/diamondburned/ningen v0.1.1-0.20200715040340-2395a0dbd0fa/go.mod h1:SKPY3387RHCbMrnefex9D+zlrA2yB+LCtaaQAgatAuc= +github.com/diamondburned/ningen v0.1.1-0.20200717013108-297a3bdf84dc h1:YZ84Kdlv91tdcyLfGfQ+LG9kWZN8dTKIic0KlEtGV0U= +github.com/diamondburned/ningen v0.1.1-0.20200717013108-297a3bdf84dc/go.mod h1:Sunqp1b9Tc0+DtWKslhf83Zepgj/TELB6h8J9HZCPqQ= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= diff --git a/internal/ui/messages/input/input.go b/internal/ui/messages/input/input.go index 13ca7b7..3752199 100644 --- a/internal/ui/messages/input/input.go +++ b/internal/ui/messages/input/input.go @@ -1,6 +1,8 @@ package input import ( + "time" + "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-gtk/internal/gts" "github.com/diamondburned/cchat-gtk/internal/log" @@ -96,12 +98,14 @@ type Field struct { UserID string Sender cchat.ServerMessageSender editor cchat.ServerMessageEditor + typer cchat.ServerMessageTypingIndicator ctrl Controller // states editingID string // never empty - sendings []PresendMessage + lastTyped time.Time + typerDura time.Duration } var inputFieldCSS = primitives.PrepareCSS(` @@ -178,6 +182,10 @@ func (f *Field) Reset() { f.UserID = "" f.Sender = nil f.editor = nil + f.typer = nil + f.lastTyped = time.Time{} + f.typerDura = 0 + f.Username.Reset() // reset the input @@ -197,11 +205,14 @@ func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSende f.text.SetSensitive(true) // Allow editor to be nil. - ed, ok := sender.(cchat.ServerMessageEditor) - if !ok { - log.Printlnf("Editor is not implemented for %T", sender) + f.editor, _ = sender.(cchat.ServerMessageEditor) + // Allow typer to be nil. + f.typer, _ = sender.(cchat.ServerMessageTypingIndicator) + + // Populate the duration state if typer is not nil. + if f.typer != nil { + f.typerDura = f.typer.TypingTimeout() } - f.editor = ed } } diff --git a/internal/ui/messages/input/keydown.go b/internal/ui/messages/input/keydown.go index a0314a2..b1a49aa 100644 --- a/internal/ui/messages/input/keydown.go +++ b/internal/ui/messages/input/keydown.go @@ -1,6 +1,8 @@ package input import ( + "time" + "github.com/diamondburned/cchat-gtk/internal/gts" "github.com/diamondburned/cchat-gtk/internal/log" "github.com/gotk3/gotk3/gdk" @@ -95,6 +97,23 @@ func (f *Field) keyDown(tv *gtk.TextView, ev *gdk.Event) bool { } } + // If the server supports typing indication, then announce that we are + // typing with a proper rate limit. + if f.typer != nil { + // Get the current time; if the next timestamp is before now, then that + // means it's time for us to update it and send a typing indication. + if now := time.Now(); f.lastTyped.Add(f.typerDura).Before(now) { + // Update. + f.lastTyped = now + // Send asynchronously. + go func() { + if err := f.typer.Typing(); err != nil { + log.Error(errors.Wrap(err, "Failed to announce typing")) + } + }() + } + } + // Passthrough. return false } diff --git a/internal/ui/primitives/primitives.go b/internal/ui/primitives/primitives.go index 91af3d2..a543a29 100644 --- a/internal/ui/primitives/primitives.go +++ b/internal/ui/primitives/primitives.go @@ -64,6 +64,19 @@ func RemoveClass(styleCtx StyleContexter, classes ...string) { } } +type ClassEnum struct{ class string } + +func (c *ClassEnum) SetClass(ctx StyleContexter, class string) { + var style, _ = ctx.GetStyleContext() + if c.class != "" { + style.RemoveClass(c.class) + } + + if c.class = class; class != "" { + style.AddClass(class) + } +} + type StyleContextFocuser interface { StyleContexter GrabFocus() diff --git a/internal/ui/rich/parser/attrmap/attrmap.go b/internal/ui/rich/parser/attrmap/attrmap.go index 3157269..ae08642 100644 --- a/internal/ui/rich/parser/attrmap/attrmap.go +++ b/internal/ui/rich/parser/attrmap/attrmap.go @@ -39,7 +39,7 @@ func (a *AppendMap) Anchor(start, end int, href string) { a.Close(end, "") } -// AnchorNU makes a new tag without underlines. +// AnchorNU makes a new tag without underlines and colors. func (a *AppendMap) AnchorNU(start, end int, href string) { a.Anchor(start, end, href) a.Span(start, end, `underline="none"`) diff --git a/internal/ui/service/button/button.go b/internal/ui/service/session/server/button/button.go similarity index 79% rename from internal/ui/service/button/button.go rename to internal/ui/service/session/server/button/button.go index 84b1015..754b9d2 100644 --- a/internal/ui/service/button/button.go +++ b/internal/ui/service/session/server/button/button.go @@ -3,8 +3,9 @@ package button import ( "github.com/diamondburned/cchat" "github.com/diamondburned/cchat-gtk/internal/gts" - "github.com/diamondburned/cchat-gtk/internal/ui/rich" + "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" "github.com/diamondburned/cchat/text" ) @@ -15,6 +16,7 @@ type ToggleButtonImage struct { menu *menu.LazyMenu clicked func(bool) + readcss primitives.ClassEnum err error icon string // whether or not the button has an icon @@ -23,6 +25,12 @@ type ToggleButtonImage struct { var _ cchat.IconContainer = (*ToggleButtonImage)(nil) +var serverButtonCSS = primitives.PrepareCSS(` + .read { color: alpha(@theme_fg_color, 0.5) } + .unread { color: @theme_fg_color } + .mentioned { color: red } +`) + func NewToggleButtonImage(content text.Rich) *ToggleButtonImage { b := rich.NewToggleButtonImage(content) b.Show() @@ -33,10 +41,8 @@ func NewToggleButtonImage(content text.Rich) *ToggleButtonImage { clicked: func(bool) {}, menu: menu.NewLazyMenu(b.ToggleButton), } - - tb.Connect("clicked", func() { - tb.clicked(tb.GetActive()) - }) + tb.Connect("clicked", func() { tb.clicked(tb.GetActive()) }) + primitives.AttachCSS(tb, serverButtonCSS) return tb } @@ -92,6 +98,17 @@ func (b *ToggleButtonImage) SetFailed(err error, retry func()) { } } +func (b *ToggleButtonImage) SetUnreadUnsafe(unread, mentioned bool) { + switch { + case unread: + b.readcss.SetClass(b, "unread") + case mentioned: + b.readcss.SetClass(b, "mentioned") + default: + b.readcss.SetClass(b, "read") + } +} + func (b *ToggleButtonImage) SetPlaceholderIcon(iconName string, iconSzPx int) { b.icon = iconName b.Image.SetPlaceholderIcon(iconName, iconSzPx) diff --git a/internal/ui/service/session/server/server.go b/internal/ui/service/session/server/server.go index 355733c..a72b162 100644 --- a/internal/ui/service/session/server/server.go +++ b/internal/ui/service/session/server/server.go @@ -7,7 +7,7 @@ import ( "github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu" "github.com/diamondburned/cchat-gtk/internal/ui/rich" "github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb" - "github.com/diamondburned/cchat-gtk/internal/ui/service/button" + "github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/button" "github.com/diamondburned/cchat/text" "github.com/gotk3/gotk3/gtk" "github.com/pkg/errors" @@ -45,6 +45,21 @@ func NewServerRow(p breadcrumb.Breadcrumber, server cchat.Server, ctrl Controlle case cchat.ServerMessage: row.Button.SetClickedIfTrue(func() { ctrl.RowSelected(serverRow, server) }) primitives.AddClass(row, "server-message") + + // Check if the server is capable of indicating unread state. + if unreader, ok := server.(cchat.ServerMessageUnreadIndicator); ok { + // Set as read by default. + row.Button.SetUnreadUnsafe(false, false) + + gts.Async(func() (func(), error) { + c, err := unreader.UnreadIndicate(row) + if err != nil { + return nil, errors.Wrap(err, "Failed to use unread indicator") + } + + return func() { row.Connect("destroy", c) }, nil + }) + } } return serverRow @@ -208,6 +223,15 @@ func (r *Row) SetRevealChild(reveal bool) { } } +// GetRevealChild returns whether or not the server list is expanded, or always +// false if there is no server list. +func (r *Row) GetRevealChild() bool { + if r.childrev != nil { + return r.childrev.GetRevealChild() + } + return false +} + // Load loads the row without uncollapsing it. func (r *Row) Load() { // Safeguard. @@ -239,11 +263,11 @@ func (r *Row) Load() { }() } -// GetRevealChild returns whether or not the server list is expanded, or always -// false if there is no server list. -func (r *Row) GetRevealChild() bool { - if r.childrev != nil { - return r.childrev.GetRevealChild() - } - return false +// SetUnread is thread-safe. +func (r *Row) SetUnread(unread, mentioned bool) { + gts.ExecAsync(func() { r.SetUnreadUnsafe(unread, mentioned) }) +} + +func (r *Row) SetUnreadUnsafe(unread, mentioned bool) { + r.Button.SetUnreadUnsafe(unread, mentioned) } diff --git a/internal/ui/service/session/session.go b/internal/ui/service/session/session.go index d4fd0d5..78d05a5 100644 --- a/internal/ui/service/session/session.go +++ b/internal/ui/service/session/session.go @@ -62,12 +62,8 @@ type Row struct { ActionsMenu *actions.Menu // session.* - // TODO: enum class? having the button be red on fail would be good - // put commander in either a hover menu or a right click menu. maybe in the // headerbar as well. - // TODO headerbar how? custom interface to get menu items and callbacks in - // controller? cmder *commander.Buffer }