mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-02-01 22:47:01 +00:00
Added recursive unread states; improved lazy-loading
This commit is contained in:
parent
d965ed2118
commit
8cf1fcd8a1
2
go.mod
2
go.mod
|
@ -8,7 +8,7 @@ require (
|
|||
github.com/Xuanwo/go-locale v0.2.0
|
||||
github.com/alecthomas/chroma v0.7.3
|
||||
github.com/diamondburned/cchat v0.0.45
|
||||
github.com/diamondburned/cchat-discord v0.0.0-20200717063909-2f4cb5f246c4
|
||||
github.com/diamondburned/cchat-discord v0.0.0-20200718071554-360b34de2a71
|
||||
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
|
||||
|
|
8
go.sum
8
go.sum
|
@ -50,16 +50,16 @@ github.com/diamondburned/cchat v0.0.43 h1:HetAujSaUSdnQgAUZgprNLARjf/MSWXpCfWdvX
|
|||
github.com/diamondburned/cchat v0.0.43/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||
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-discord v0.0.0-20200718071554-360b34de2a71 h1:4alCL+MeOJuz//uhYKuzyn8RS6+yEQFwL9SGCluNRL4=
|
||||
github.com/diamondburned/cchat-discord v0.0.0-20200718071554-360b34de2a71/go.mod h1:tFB/vWkCPp2kQ7v/VkpCvM1aoB6+fvwuEU/acA/g5CY=
|
||||
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.20200717013108-297a3bdf84dc h1:YZ84Kdlv91tdcyLfGfQ+LG9kWZN8dTKIic0KlEtGV0U=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200717013108-297a3bdf84dc/go.mod h1:Sunqp1b9Tc0+DtWKslhf83Zepgj/TELB6h8J9HZCPqQ=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200717072304-e483f86c08e6 h1:YN0cj0aOCa+tKmx0aD5qsbSYaIJnyrA0/+eygMKP+/w=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200717072304-e483f86c08e6/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=
|
||||
|
|
|
@ -13,6 +13,8 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// TODO:
|
||||
|
||||
type ImageContainer interface {
|
||||
SetFromPixbuf(*gdk.Pixbuf)
|
||||
SetFromAnimation(*gdk.PixbufAnimation)
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
const TPS = 15 // tps
|
||||
const TPS = 24 // tps
|
||||
|
||||
type State struct {
|
||||
throttling bool
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/icons"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/actions"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/traverse"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
|
||||
"github.com/gotk3/gotk3/glib"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
|
|
|
@ -9,6 +9,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/rich/labeluri"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/gotk3/gotk3/pango"
|
||||
|
@ -189,8 +190,15 @@ func (m *GenericContainer) UpdateAuthor(author cchat.MessageAuthor) {
|
|||
}
|
||||
}
|
||||
|
||||
// authorRenderCfg is the config to render author names. It disables author
|
||||
// mention links, as there's no way to make normal names not appear blue.
|
||||
var authorRenderCfg = markup.RenderConfig{
|
||||
NoMentionLinks: true,
|
||||
}
|
||||
|
||||
func (m *GenericContainer) UpdateAuthorName(name text.Rich) {
|
||||
m.Username.SetLabelUnsafe(name)
|
||||
var out = markup.RenderCmplxWithConfig(name, authorRenderCfg)
|
||||
m.Username.SetOutput(out)
|
||||
}
|
||||
|
||||
func (m *GenericContainer) UpdateContent(content text.Rich, edited bool) {
|
||||
|
|
|
@ -111,9 +111,9 @@ func (m *GenericPresendContainer) SetSentError(err error) {
|
|||
m.contentBox.Add(m.ContentBody)
|
||||
|
||||
// Style the label appropriately by making it red.
|
||||
var content = html.EscapeString(m.presend.Content())
|
||||
if content == "" {
|
||||
content = EmptyContentPlaceholder
|
||||
var content = EmptyContentPlaceholder
|
||||
if m.presend != nil && m.presend.Content() != "" {
|
||||
content = m.presend.Content()
|
||||
}
|
||||
m.ContentBody.SetMarkup(fmt.Sprintf(`<span color="red">%s</span>`, content))
|
||||
|
||||
|
|
|
@ -77,6 +77,12 @@ func (l *Label) Output() markup.RenderOutput {
|
|||
return l.output
|
||||
}
|
||||
|
||||
// SetOutput sets the internal output and label.
|
||||
func (l *Label) SetOutput(o markup.RenderOutput) {
|
||||
l.output = o
|
||||
l.SetMarkup(o.Markup)
|
||||
}
|
||||
|
||||
func BindRichLabel(label Labeler) {
|
||||
bind(label, func(uri string, ptr gdk.Rectangle) bool {
|
||||
var output = label.Output()
|
||||
|
|
|
@ -35,13 +35,14 @@ func (a *AppendMap) appendIndex(ind int) {
|
|||
}
|
||||
|
||||
func (a *AppendMap) Anchor(start, end int, href string) {
|
||||
a.Openf(start, `<a href="%s">`, html.EscapeString(href))
|
||||
a.Close(end, "</a>")
|
||||
a.AnchorNU(start, end, href)
|
||||
}
|
||||
|
||||
// AnchorNU makes a new <a> tag without underlines and colors.
|
||||
func (a *AppendMap) AnchorNU(start, end int, href string) {
|
||||
a.Anchor(start, end, href)
|
||||
a.Openf(start, `<a href="%s">`, html.EscapeString(href))
|
||||
a.Close(end, "</a>")
|
||||
// a.Anchor(start, end, href)
|
||||
a.Span(start, end, `underline="none"`)
|
||||
}
|
||||
|
||||
|
|
|
@ -51,6 +51,16 @@ func Render(content text.Rich) string {
|
|||
|
||||
// RenderCmplx renders content into a complete output.
|
||||
func RenderCmplx(content text.Rich) RenderOutput {
|
||||
return RenderCmplxWithConfig(content, RenderConfig{})
|
||||
}
|
||||
|
||||
type RenderConfig struct {
|
||||
// NoMentionLinks prevents the renderer from wrapping mentions with a
|
||||
// hyperlink. This prevents invalid colors.
|
||||
NoMentionLinks bool
|
||||
}
|
||||
|
||||
func RenderCmplxWithConfig(content text.Rich, cfg RenderConfig) RenderOutput {
|
||||
// Fast path.
|
||||
if len(content.Segments) == 0 {
|
||||
return RenderOutput{
|
||||
|
@ -87,8 +97,7 @@ func RenderCmplx(content text.Rich) RenderOutput {
|
|||
start, end := segment.Bounds()
|
||||
|
||||
if segment, ok := segment.(text.Linker); ok {
|
||||
appended.Openf(start, `<a href="%s">`, html.EscapeString(segment.Link()))
|
||||
appended.Close(end, "</a>")
|
||||
appended.Anchor(start, end, segment.Link())
|
||||
}
|
||||
|
||||
if segment, ok := segment.(text.Imager); ok {
|
||||
|
@ -111,7 +120,11 @@ func RenderCmplx(content text.Rich) RenderOutput {
|
|||
if segment, ok := segment.(text.Mentioner); ok {
|
||||
// Render the mention into "cchat://mention:0" or such. Other
|
||||
// components will take care of showing the information.
|
||||
appended.AnchorNU(start, end, fmt.Sprintf(f_Mention, len(mentions)))
|
||||
if cfg.NoMentionLinks {
|
||||
appended.AnchorNU(start, end, fmt.Sprintf(f_Mention, len(mentions)))
|
||||
}
|
||||
|
||||
// Add the mention segment into the list regardless of hyperlinks.
|
||||
mentions = append(mentions, segment)
|
||||
|
||||
if segment, ok := segment.(text.Colorer); ok {
|
||||
|
@ -177,8 +190,11 @@ func color(c uint32, bg bool) []string {
|
|||
return attrs
|
||||
}
|
||||
|
||||
// string constant for formatting width and height in URL fragments
|
||||
const f_FragmentSize = "w=%d;h=%d"
|
||||
const (
|
||||
// string constant for formatting width and height in URL fragments
|
||||
f_FragmentSize = "w=%d;h=%d"
|
||||
f_AnchorNoUnderline = `<a href="%s"><span underline="none">%s</span></a>`
|
||||
)
|
||||
|
||||
func composeImageMarkup(imager text.Imager) string {
|
||||
u, err := url.Parse(imager.Image())
|
||||
|
@ -193,7 +209,7 @@ func composeImageMarkup(imager text.Imager) string {
|
|||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
`<a href="%s">%s</a>`,
|
||||
f_AnchorNoUnderline,
|
||||
html.EscapeString(u.String()), html.EscapeString(imager.ImageText()),
|
||||
)
|
||||
}
|
||||
|
@ -211,7 +227,7 @@ func composeAvatarMarkup(avatarer text.Avatarer) string {
|
|||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
`<a href="%s">%s</a>`,
|
||||
f_AnchorNoUnderline,
|
||||
html.EscapeString(u.String()), html.EscapeString(avatarer.AvatarText()),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/traverse"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
|
|
|
@ -26,9 +26,30 @@ 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 }
|
||||
.selected-server {
|
||||
border-left: 2px solid mix(@theme_base_color, @theme_fg_color, 0.1);
|
||||
background-color: mix(@theme_base_color, @theme_fg_color, 0.1);
|
||||
color: @theme_fg_color;
|
||||
}
|
||||
|
||||
.read {
|
||||
color: alpha(@theme_fg_color, 0.5);
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
|
||||
.unread {
|
||||
color: @theme_fg_color;
|
||||
border-left: 2px solid alpha(@theme_fg_color, 0.75);
|
||||
background-color: alpha(@theme_fg_color, 0.05);
|
||||
}
|
||||
|
||||
@define-color mentioned rgb(240, 71, 71);
|
||||
|
||||
.mentioned {
|
||||
color: @mentioned;
|
||||
border-left: 2px solid alpha(@mentioned, 0.75);
|
||||
background-color: alpha(@mentioned, 0.05);
|
||||
}
|
||||
`)
|
||||
|
||||
func NewToggleButtonImage(content text.Rich) *ToggleButtonImage {
|
||||
|
@ -47,6 +68,22 @@ func NewToggleButtonImage(content text.Rich) *ToggleButtonImage {
|
|||
return tb
|
||||
}
|
||||
|
||||
func (b *ToggleButtonImage) SetSelected(selected bool) {
|
||||
// Set the clickability the opposite as the boolean.
|
||||
b.SetSensitive(!selected)
|
||||
|
||||
if selected {
|
||||
primitives.AddClass(b, "selected-server")
|
||||
} else {
|
||||
primitives.RemoveClass(b, "selected-server")
|
||||
}
|
||||
|
||||
// Some special edge case that I forgot.
|
||||
if !selected {
|
||||
b.SetActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *ToggleButtonImage) SetClicked(clicked func(bool)) {
|
||||
b.clicked = clicked
|
||||
}
|
||||
|
@ -100,10 +137,11 @@ func (b *ToggleButtonImage) SetFailed(err error, retry func()) {
|
|||
|
||||
func (b *ToggleButtonImage) SetUnreadUnsafe(unread, mentioned bool) {
|
||||
switch {
|
||||
case unread:
|
||||
b.readcss.SetClass(b, "unread")
|
||||
// Prioritize mentions over unreads.
|
||||
case mentioned:
|
||||
b.readcss.SetClass(b, "mentioned")
|
||||
case unread:
|
||||
b.readcss.SetClass(b, "unread")
|
||||
default:
|
||||
b.readcss.SetClass(b, "read")
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/loading"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/traverse"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
|
@ -13,18 +13,25 @@ type Controller interface {
|
|||
RowSelected(*ServerRow, cchat.ServerMessage)
|
||||
}
|
||||
|
||||
// Children is a children server with a reference to the parent.
|
||||
// Children is a children server with a reference to the parent. By default, a
|
||||
// children will contain hollow rows. They are rows that do not yet have any
|
||||
// widgets. This changes as soon as Row's Load is called.
|
||||
type Children struct {
|
||||
*gtk.Box
|
||||
load *loading.Button // only not nil while loading
|
||||
|
||||
load *loading.Button // only not nil while loading
|
||||
loading bool
|
||||
|
||||
Rows []*ServerRow
|
||||
|
||||
Parent traverse.Breadcrumber
|
||||
rowctrl Controller
|
||||
|
||||
// Unreadable state for children rows to use. The parent row that has this
|
||||
// Children will bind a handler to this.
|
||||
traverse.Unreadable
|
||||
}
|
||||
|
||||
// reserved
|
||||
var childrenCSS = primitives.PrepareClassCSS("server-children", `
|
||||
.server-children {
|
||||
margin: 0;
|
||||
|
@ -33,20 +40,73 @@ var childrenCSS = primitives.PrepareClassCSS("server-children", `
|
|||
}
|
||||
`)
|
||||
|
||||
func NewChildren(p traverse.Breadcrumber, ctrl Controller) *Children {
|
||||
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
main.SetMarginStart(ChildrenMargin)
|
||||
childrenCSS(main)
|
||||
|
||||
// NewHollowChildren creates a hollow children, which is a children without any
|
||||
// widgets.
|
||||
func NewHollowChildren(p traverse.Breadcrumber, ctrl Controller) *Children {
|
||||
return &Children{
|
||||
Box: main,
|
||||
Parent: p,
|
||||
rowctrl: ctrl,
|
||||
}
|
||||
}
|
||||
|
||||
// setLoading shows the loading circle as a list child.
|
||||
// NewChildren creates a hollow children then immediately unhollows it.
|
||||
func NewChildren(p traverse.Breadcrumber, ctrl Controller) *Children {
|
||||
c := NewHollowChildren(p, ctrl)
|
||||
c.Init()
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Children) IsHollow() bool {
|
||||
return c.Box == nil
|
||||
}
|
||||
|
||||
// Init ensures that the children container is not hollow. It does nothing after
|
||||
// the first call. It does not actually populate the list with widgets. This is
|
||||
// done for lazy loading. To load everything, call LoadAll after this.
|
||||
//
|
||||
// Nothing but ServerRow should call this method.
|
||||
func (c *Children) Init() {
|
||||
if c.IsHollow() {
|
||||
c.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
c.Box.SetMarginStart(ChildrenMargin)
|
||||
childrenCSS(c.Box)
|
||||
|
||||
// Check if we're still loading. This is effectively restoring the
|
||||
// state that was set before we had widgets.
|
||||
if c.loading {
|
||||
c.setLoading()
|
||||
} else {
|
||||
c.setNotLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset ensures that the children container is no longer hollow, then reset all
|
||||
// states.
|
||||
func (c *Children) Reset() {
|
||||
// If the children container isn't hollow, then we have to remove the known
|
||||
// rows from the container box.
|
||||
if c.Box != nil {
|
||||
// Remove old servers from the list.
|
||||
for _, row := range c.Rows {
|
||||
c.Box.Remove(row)
|
||||
}
|
||||
}
|
||||
|
||||
// Wipe the list empty.
|
||||
c.Rows = nil
|
||||
}
|
||||
|
||||
// setLoading shows the loading circle as a list child. If hollow, this function
|
||||
// will only update the state.
|
||||
func (c *Children) setLoading() {
|
||||
c.loading = true
|
||||
|
||||
// Don't do the rest if we're still hollow.
|
||||
if c.IsHollow() {
|
||||
return
|
||||
}
|
||||
|
||||
// Exit if we're already loading.
|
||||
if c.load != nil {
|
||||
return
|
||||
|
@ -61,19 +121,16 @@ func (c *Children) setLoading() {
|
|||
c.Box.Add(c.load)
|
||||
}
|
||||
|
||||
func (c *Children) Reset() {
|
||||
// Remove old servers from the list.
|
||||
for _, row := range c.Rows {
|
||||
c.Box.Remove(row)
|
||||
}
|
||||
|
||||
// Wipe the list empty.
|
||||
c.Rows = nil
|
||||
}
|
||||
|
||||
// setNotLoading removes the loading circle, if any. This is not in Reset()
|
||||
// anymore, since the backend may not necessarily call SetServers.
|
||||
func (c *Children) setNotLoading() {
|
||||
c.loading = false
|
||||
|
||||
// Don't call the rest if we're still hollow.
|
||||
if c.IsHollow() {
|
||||
return
|
||||
}
|
||||
|
||||
// Do we have the spinning circle button? If yes, remove it.
|
||||
if c.load != nil {
|
||||
// Stop the loading mode. The reset function should do everything for us.
|
||||
|
@ -85,31 +142,54 @@ func (c *Children) setNotLoading() {
|
|||
// SetServers is reserved for cchat.ServersContainer.
|
||||
func (c *Children) SetServers(servers []cchat.Server) {
|
||||
gts.ExecAsync(func() {
|
||||
// Save the current state.
|
||||
var oldID string
|
||||
for _, row := range c.Rows {
|
||||
if row.GetActive() {
|
||||
oldID = row.Server.ID()
|
||||
break
|
||||
}
|
||||
// Save the current state (if any) if the children container is not
|
||||
// hollow.
|
||||
if !c.IsHollow() {
|
||||
restore := c.saveSelectedRow()
|
||||
defer restore()
|
||||
}
|
||||
|
||||
// Reset before inserting new servers.
|
||||
c.Reset()
|
||||
|
||||
// Insert hollow servers.
|
||||
c.Rows = make([]*ServerRow, len(servers))
|
||||
|
||||
for i, server := range servers {
|
||||
row := NewServerRow(c, server, c.rowctrl)
|
||||
row.Show()
|
||||
// row.SetFocusHAdjustment(c.GetFocusHAdjustment()) // inherit
|
||||
// row.SetFocusVAdjustment(c.GetFocusVAdjustment())
|
||||
|
||||
c.Rows[i] = row
|
||||
c.Box.Add(row)
|
||||
c.Rows[i] = NewHollowServer(c, server, c.rowctrl)
|
||||
}
|
||||
|
||||
// Update parent reference? Only if it's activated.
|
||||
// We should not unhollow everything here, but rather on uncollapse.
|
||||
// Since the root node is always unhollow, calls to this function will
|
||||
// pass the hollow test and unhollow its children nodes. That should not
|
||||
// happen.
|
||||
})
|
||||
}
|
||||
|
||||
// LoadAll forces all children rows to be unhollowed (initialized). It does
|
||||
// NOT check if the children container itself is hollow.
|
||||
func (c *Children) LoadAll() {
|
||||
AssertUnhollow(c)
|
||||
|
||||
for _, row := range c.Rows {
|
||||
row.Init() // this is the alloc-heavy method
|
||||
row.Show()
|
||||
c.Box.Add(row)
|
||||
}
|
||||
}
|
||||
|
||||
// saveSelectedRow saves the current selected row and returns a callback that
|
||||
// restores the selection.
|
||||
func (c *Children) saveSelectedRow() (restore func()) {
|
||||
// Save the current state.
|
||||
var oldID string
|
||||
for _, row := range c.Rows {
|
||||
if row.GetActive() {
|
||||
oldID = row.Server.ID()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return func() {
|
||||
if oldID != "" {
|
||||
for _, row := range c.Rows {
|
||||
if row.Server.ID() == oldID {
|
||||
|
@ -117,7 +197,9 @@ func (c *Children) SetServers(servers []cchat.Server) {
|
|||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// TODO Update parent reference? Only if it's activated.
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Children) Breadcrumb() traverse.Breadcrumb {
|
||||
|
|
|
@ -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/session/server/button"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/traverse"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -16,9 +16,23 @@ import (
|
|||
const ChildrenMargin = 24
|
||||
const IconSize = 32
|
||||
|
||||
func AssertUnhollow(hollower interface{ IsHollow() bool }) {
|
||||
if hollower.IsHollow() {
|
||||
panic("Server is hollow, but a normal method was called.")
|
||||
}
|
||||
}
|
||||
|
||||
type ServerRow struct {
|
||||
*Row
|
||||
ctrl Controller
|
||||
Server cchat.Server
|
||||
|
||||
// State that's updated even when stale. Initializations will use these.
|
||||
unread bool
|
||||
mentioned bool
|
||||
|
||||
// callback to cancel unread indicator
|
||||
cancelUnread func()
|
||||
}
|
||||
|
||||
var serverCSS = primitives.PrepareClassCSS("server", `
|
||||
|
@ -30,34 +44,30 @@ var serverCSS = primitives.PrepareClassCSS("server", `
|
|||
}
|
||||
`)
|
||||
|
||||
func NewServerRow(p traverse.Breadcrumber, server cchat.Server, ctrl Controller) *ServerRow {
|
||||
row := NewRow(p, server.Name())
|
||||
row.SetIconer(server)
|
||||
serverCSS(row)
|
||||
// NewHollowServer creates a new hollow ServerRow. It will automatically create
|
||||
// hollow children containers and rows for the given server.
|
||||
func NewHollowServer(p traverse.Breadcrumber, sv cchat.Server, ctrl Controller) *ServerRow {
|
||||
var serverRow = &ServerRow{
|
||||
Row: NewHollowRow(p),
|
||||
ctrl: ctrl,
|
||||
Server: sv,
|
||||
cancelUnread: func() {},
|
||||
}
|
||||
|
||||
var serverRow = &ServerRow{Row: row, Server: server}
|
||||
|
||||
switch server := server.(type) {
|
||||
switch sv := sv.(type) {
|
||||
case cchat.ServerList:
|
||||
row.SetServerList(server, ctrl)
|
||||
primitives.AddClass(row, "server-list")
|
||||
serverRow.SetHollowServerList(sv, ctrl)
|
||||
serverRow.children.SetUnreadHandler(serverRow.SetUnreadUnsafe)
|
||||
|
||||
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)
|
||||
|
||||
if unreader, ok := sv.(cchat.ServerMessageUnreadIndicator); ok {
|
||||
gts.Async(func() (func(), error) {
|
||||
c, err := unreader.UnreadIndicate(row)
|
||||
c, err := unreader.UnreadIndicate(serverRow)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to use unread indicator")
|
||||
}
|
||||
|
||||
return func() { row.Connect("destroy", c) }, nil
|
||||
return func() { serverRow.cancelUnread = c }, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -65,67 +75,153 @@ func NewServerRow(p traverse.Breadcrumber, server cchat.Server, ctrl Controller)
|
|||
return serverRow
|
||||
}
|
||||
|
||||
// Init brings the row out of the hollow state. It loads the children (if any),
|
||||
// but this process does not make more widgets.
|
||||
func (r *ServerRow) Init() {
|
||||
if !r.IsHollow() {
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize the row, which would fill up the button and others as well.
|
||||
r.Row.Init(r.Server.Name())
|
||||
r.Row.SetIconer(r.Server)
|
||||
serverCSS(r.Row)
|
||||
|
||||
// Connect the destroyer, if any.
|
||||
r.Row.Connect("destroy", r.cancelUnread)
|
||||
|
||||
// Restore the read state.
|
||||
r.Button.SetUnreadUnsafe(r.unread, r.mentioned) // update with state
|
||||
|
||||
switch server := r.Server.(type) {
|
||||
case cchat.ServerList:
|
||||
primitives.AddClass(r, "server-list")
|
||||
r.children.Init()
|
||||
r.children.Show()
|
||||
|
||||
r.childrev, _ = gtk.RevealerNew()
|
||||
r.childrev.SetRevealChild(false)
|
||||
r.childrev.Add(r.children)
|
||||
r.childrev.Show()
|
||||
|
||||
r.Box.PackStart(r.childrev, false, false, 0)
|
||||
r.Button.SetClicked(r.SetRevealChild)
|
||||
|
||||
case cchat.ServerMessage:
|
||||
primitives.AddClass(r, "server-message")
|
||||
r.Button.SetClickedIfTrue(func() { r.ctrl.RowSelected(r, server) })
|
||||
}
|
||||
}
|
||||
|
||||
// GetActiveServerMessage returns true if the row is currently selected AND it
|
||||
// is a message row.
|
||||
func (r *ServerRow) GetActiveServerMessage() bool {
|
||||
// If the button is nil, then that probably means we're still in a hollow
|
||||
// state. This obviously means nothing is being selected.
|
||||
if r.Button == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return r.children == nil && r.Button.GetActive()
|
||||
}
|
||||
|
||||
// SetUnread is thread-safe.
|
||||
func (r *ServerRow) SetUnread(unread, mentioned bool) {
|
||||
gts.ExecAsync(func() { r.SetUnreadUnsafe(unread, mentioned) })
|
||||
}
|
||||
|
||||
func (r *ServerRow) SetUnreadUnsafe(unread, mentioned bool) {
|
||||
// We're never unread if we're reading this current server.
|
||||
if r.GetActiveServerMessage() {
|
||||
unread, mentioned = false, false
|
||||
}
|
||||
|
||||
// Update the local state.
|
||||
r.unread = unread
|
||||
r.mentioned = mentioned
|
||||
|
||||
// Button is nil if we're still in a hollow state. A nil check should tell
|
||||
// us that.
|
||||
if r.Button != nil {
|
||||
r.Button.SetUnreadUnsafe(r.unread, r.mentioned)
|
||||
}
|
||||
|
||||
// Still update the parent's state even if we're hollow.
|
||||
traverse.TrySetUnread(r.parentcrumb, r.Server.ID(), r.unread, r.mentioned)
|
||||
}
|
||||
|
||||
type Row struct {
|
||||
*gtk.Box
|
||||
Button *button.ToggleButtonImage
|
||||
|
||||
parentcrumb traverse.Breadcrumber
|
||||
|
||||
// non-nil if server list and the function returns error
|
||||
childrenErr error
|
||||
|
||||
childrev *gtk.Revealer
|
||||
children *Children
|
||||
serverList cchat.ServerList
|
||||
loaded bool
|
||||
}
|
||||
|
||||
func NewRow(parent traverse.Breadcrumber, name text.Rich) *Row {
|
||||
button := button.NewToggleButtonImage(name)
|
||||
button.Box.SetHAlign(gtk.ALIGN_START)
|
||||
button.SetRelief(gtk.RELIEF_NONE)
|
||||
button.Show()
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
box.PackStart(button, false, false, 0)
|
||||
|
||||
row := &Row{
|
||||
Box: box,
|
||||
Button: button,
|
||||
func NewHollowRow(parent traverse.Breadcrumber) *Row {
|
||||
return &Row{
|
||||
parentcrumb: parent,
|
||||
}
|
||||
}
|
||||
|
||||
return row
|
||||
func (r *Row) IsHollow() bool {
|
||||
return r.Box == nil
|
||||
}
|
||||
|
||||
// Init initializes the row from its initial hollow state. It does nothing after
|
||||
// the first call.
|
||||
func (r *Row) Init(name text.Rich) {
|
||||
if !r.IsHollow() {
|
||||
return
|
||||
}
|
||||
|
||||
r.Button = button.NewToggleButtonImage(name)
|
||||
r.Button.Box.SetHAlign(gtk.ALIGN_START)
|
||||
r.Button.SetRelief(gtk.RELIEF_NONE)
|
||||
r.Button.Show()
|
||||
|
||||
r.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
r.Box.PackStart(r.Button, false, false, 0)
|
||||
|
||||
// Ensure errors are displayed.
|
||||
r.childrenSetErr(r.childrenErr)
|
||||
}
|
||||
|
||||
func (r *Row) Breadcrumb() traverse.Breadcrumb {
|
||||
return traverse.TryBreadcrumb(r.parentcrumb, r.Button.GetText())
|
||||
}
|
||||
|
||||
func (r *Row) SetLabelUnsafe(name text.Rich) {
|
||||
r.Button.SetLabelUnsafe(name)
|
||||
}
|
||||
|
||||
func (r *Row) SetIconer(v interface{}) {
|
||||
if iconer, ok := v.(cchat.Icon); ok {
|
||||
r.Button.Image.SetSize(IconSize)
|
||||
r.Button.Image.AsyncSetIconer(iconer, "Error getting server icon URL")
|
||||
}
|
||||
}
|
||||
|
||||
// SetServerList sets the row to a server list.
|
||||
func (r *Row) SetServerList(list cchat.ServerList, ctrl Controller) {
|
||||
r.Button.SetClicked(func(active bool) {
|
||||
r.SetRevealChild(active)
|
||||
})
|
||||
|
||||
r.children = NewChildren(r, ctrl)
|
||||
r.children.Show()
|
||||
|
||||
r.childrev, _ = gtk.RevealerNew()
|
||||
r.childrev.SetRevealChild(false)
|
||||
r.childrev.Add(r.children)
|
||||
r.childrev.Show()
|
||||
|
||||
r.Box.PackStart(r.childrev, false, false, 0)
|
||||
// SetHollowServerList sets the row to a hollow server list (children) and
|
||||
// recursively create
|
||||
func (r *Row) SetHollowServerList(list cchat.ServerList, ctrl Controller) {
|
||||
r.serverList = list
|
||||
|
||||
r.children = NewHollowChildren(r, ctrl)
|
||||
r.children.setLoading()
|
||||
|
||||
go func() {
|
||||
var err = list.Servers(r.children)
|
||||
|
||||
gts.ExecAsync(func() {
|
||||
// Announce that we're not loading anymore.
|
||||
r.children.setNotLoading()
|
||||
|
||||
if !r.IsHollow() {
|
||||
// Restore clickability.
|
||||
r.SetSensitive(true)
|
||||
}
|
||||
|
||||
// Use the childrenX method instead of SetX. We can wrap nil
|
||||
// errors.
|
||||
r.childrenSetErr(errors.Wrap(err, "Failed to get servers"))
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
// Reset clears off all children servers. It's a no-op if there are none.
|
||||
|
@ -139,13 +235,45 @@ func (r *Row) Reset() {
|
|||
}
|
||||
|
||||
// Reset the state.
|
||||
r.loaded = false
|
||||
r.serverList = nil
|
||||
r.children = nil
|
||||
}
|
||||
|
||||
func (r *Row) childrenSetErr(err error) {
|
||||
// Update the state and only use this state field.
|
||||
r.childrenErr = err
|
||||
|
||||
// Only call this if we're not hollow. If we are, then Init() will read the
|
||||
// state field above and render the failed button.
|
||||
if !r.IsHollow() {
|
||||
if err != nil {
|
||||
// If the user chooses to retry, the list will automatically expand.
|
||||
r.SetFailed(err, func() { r.SetRevealChild(true) })
|
||||
} else {
|
||||
r.SetDone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Row) SetLabelUnsafe(name text.Rich) {
|
||||
AssertUnhollow(r)
|
||||
|
||||
r.Button.SetLabelUnsafe(name)
|
||||
}
|
||||
|
||||
func (r *Row) SetIconer(v interface{}) {
|
||||
AssertUnhollow(r)
|
||||
|
||||
if iconer, ok := v.(cchat.Icon); ok {
|
||||
r.Button.Image.SetSize(IconSize)
|
||||
r.Button.Image.AsyncSetIconer(iconer, "Error getting server icon URL")
|
||||
}
|
||||
}
|
||||
|
||||
// SetLoading is called by the parent struct.
|
||||
func (r *Row) SetLoading() {
|
||||
AssertUnhollow(r)
|
||||
|
||||
r.SetSensitive(false)
|
||||
r.Button.SetLoading()
|
||||
}
|
||||
|
@ -154,6 +282,8 @@ func (r *Row) SetLoading() {
|
|||
// because both of those errors share the same appearance, just different
|
||||
// callbacks.
|
||||
func (r *Row) SetFailed(err error, retry func()) {
|
||||
AssertUnhollow(r)
|
||||
|
||||
r.SetSensitive(true)
|
||||
r.SetTooltipText(err.Error())
|
||||
r.Button.SetFailed(err, retry)
|
||||
|
@ -163,47 +293,39 @@ func (r *Row) SetFailed(err error, retry func()) {
|
|||
// SetDone is shared between the parent struct and the children list. This is
|
||||
// because both will use the same SetFailed.
|
||||
func (r *Row) SetDone() {
|
||||
AssertUnhollow(r)
|
||||
|
||||
r.Button.SetNormal()
|
||||
r.SetSensitive(true)
|
||||
r.SetTooltipText("")
|
||||
}
|
||||
|
||||
func (r *Row) SetNormalExtraMenu(items []menu.Item) {
|
||||
AssertUnhollow(r)
|
||||
|
||||
r.Button.SetNormalExtraMenu(items)
|
||||
r.SetSensitive(true)
|
||||
r.SetTooltipText("")
|
||||
}
|
||||
|
||||
func (r *Row) childrenFailed(err error) {
|
||||
// If the user chooses to retry, the list will automatically expand.
|
||||
r.SetFailed(err, func() { r.SetRevealChild(true) })
|
||||
}
|
||||
|
||||
func (r *Row) childrenDone() {
|
||||
r.loaded = true
|
||||
|
||||
// I don't think this is supposed to be called here...
|
||||
// r.SetDone()
|
||||
}
|
||||
|
||||
// SetSelected is used for highlighting the current message server.
|
||||
func (r *Row) SetSelected(selected bool) {
|
||||
// Set the clickability the opposite as the boolean.
|
||||
r.Button.SetSensitive(!selected)
|
||||
AssertUnhollow(r)
|
||||
|
||||
// Some special edge case that I forgot.
|
||||
if !selected {
|
||||
r.Button.SetActive(false)
|
||||
}
|
||||
r.Button.SetSelected(selected)
|
||||
}
|
||||
|
||||
func (r *Row) GetActive() bool {
|
||||
AssertUnhollow(r)
|
||||
|
||||
return r.Button.GetActive()
|
||||
}
|
||||
|
||||
// SetRevealChild reveals the list of servers. It does nothing if there are no
|
||||
// servers, meaning if Row does not represent a ServerList.
|
||||
func (r *Row) SetRevealChild(reveal bool) {
|
||||
AssertUnhollow(r)
|
||||
|
||||
// Do the above noop check.
|
||||
if r.children == nil {
|
||||
return
|
||||
|
@ -217,57 +339,19 @@ func (r *Row) SetRevealChild(reveal bool) {
|
|||
return
|
||||
}
|
||||
|
||||
// If we haven't loaded yet and we're still not loading, then load.
|
||||
if !r.loaded && r.children.load == nil {
|
||||
r.Load()
|
||||
}
|
||||
// Load the list of servers if we're still in loading mode. Before, we have
|
||||
// to call Servers on this. Now, we already know that there are hollow
|
||||
// servers in the children container.
|
||||
r.children.LoadAll()
|
||||
}
|
||||
|
||||
// GetRevealChild returns whether or not the server list is expanded, or always
|
||||
// false if there is no server list.
|
||||
func (r *Row) GetRevealChild() bool {
|
||||
AssertUnhollow(r)
|
||||
|
||||
if r.childrev != nil {
|
||||
return r.childrev.GetRevealChild()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Load loads the row without uncollapsing it.
|
||||
func (r *Row) Load() {
|
||||
// Safeguard.
|
||||
if r.children == nil || r.serverList == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Set that we're now loading.
|
||||
r.children.setLoading()
|
||||
r.SetLoading()
|
||||
r.SetSensitive(false)
|
||||
|
||||
// Load the list of servers if we're still in loading mode.
|
||||
go func() {
|
||||
err := r.serverList.Servers(r.children)
|
||||
gts.ExecAsync(func() {
|
||||
// We're not loading anymore, so remove the loading circle.
|
||||
r.children.setNotLoading()
|
||||
// Restore clickability.
|
||||
r.SetSensitive(true)
|
||||
|
||||
// Use the childrenX method instead of SetX.
|
||||
if err != nil {
|
||||
r.childrenFailed(errors.Wrap(err, "Failed to get servers"))
|
||||
} else {
|
||||
r.childrenDone()
|
||||
}
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
|
134
internal/ui/service/session/server/traverse/traverse.go
Normal file
134
internal/ui/service/session/server/traverse/traverse.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
// Package traverse implements an extensible interface that allows children
|
||||
// widgets to announce state changes to their parent container.
|
||||
//
|
||||
// The objective of this package is to allow for easier parent traversal without
|
||||
// cluttering its structure with too much state. It also allows for proper
|
||||
// encapsulation as well as a fallback mechanism without lengthy boolean checks.
|
||||
package traverse
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Breadcrumb []string
|
||||
|
||||
func (b Breadcrumb) String() string {
|
||||
return strings.Join([]string(b), "/")
|
||||
}
|
||||
|
||||
// Breadcrumber is the base interface that other interfaces extend on. A child
|
||||
// must at minimum implement this interface to use any other.
|
||||
type Breadcrumber interface {
|
||||
// Breadcrumb returns the parent's path before the children's breadcrumb.
|
||||
// This method recursively joins the parent's crumb with the children's,
|
||||
// then eventually make its way up to the root node.
|
||||
Breadcrumb() Breadcrumb
|
||||
}
|
||||
|
||||
// TryBreadcrumb accepts a nilable breadcrumber and handles it appropriately.
|
||||
func TryBreadcrumb(i Breadcrumber, appended ...string) []string {
|
||||
if i == nil {
|
||||
return appended
|
||||
}
|
||||
|
||||
return append(i.Breadcrumb(), appended...)
|
||||
}
|
||||
|
||||
// Unreadabler extends Breadcrumber to add unread states to the parent node.
|
||||
type Unreadabler interface {
|
||||
SetState(id string, unread, mentioned bool)
|
||||
}
|
||||
|
||||
// TrySetUnread tries to check if a breadcrumber parent node supports
|
||||
// Unreadabler. If it does, then this function will set the state appropriately.
|
||||
func TrySetUnread(parent Breadcrumber, selfID string, unread, mentioned bool) {
|
||||
if u, ok := parent.(Unreadabler); ok {
|
||||
u.SetState(selfID, unread, mentioned)
|
||||
}
|
||||
}
|
||||
|
||||
// UnreadSetter is an interface that a single row implements to set state. It
|
||||
// does not have to do with Breadcrumber.
|
||||
type UnreadSetter interface {
|
||||
SetUnreadUnsafe(unread, mentioned bool)
|
||||
}
|
||||
|
||||
// Unreadable is a struct that nodes could embed to implement unreadable
|
||||
// capability, that is, the unread and mentioned states. A zero-value Unreadable
|
||||
// is a valid Unreadable without an update handler.
|
||||
//
|
||||
// Typically, parent nodes would implement this as a way to count the number of
|
||||
// unread and mentioned children nodes.
|
||||
type Unreadable struct {
|
||||
UnreadableState
|
||||
unreadHandler func(unread, mentioned bool)
|
||||
}
|
||||
|
||||
func NewUnreadable(unreadHandler UnreadSetter) *Unreadable {
|
||||
u := &Unreadable{}
|
||||
u.SetUnreadHandler(unreadHandler.SetUnreadUnsafe)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetUpdateHandler sets the parent's update handler. This update handler must
|
||||
// refer to the parent's breadcrumb.
|
||||
func (u *Unreadable) SetUnreadHandler(updateHandler func(unread, mentioned bool)) {
|
||||
// Update with the current state.
|
||||
if u.unreadHandler = updateHandler; updateHandler != nil {
|
||||
updateHandler(u.State())
|
||||
}
|
||||
}
|
||||
|
||||
// SetState updates the node ID's state in this parent unreadable state
|
||||
// container.
|
||||
func (u *Unreadable) SetState(id string, unread, mentioned bool) {
|
||||
u.UnreadableState.SetState(id, unread, mentioned)
|
||||
|
||||
if u.unreadHandler != nil {
|
||||
u.unreadHandler(u.UnreadableState.State())
|
||||
}
|
||||
}
|
||||
|
||||
// UnreadableState implements a map of unread children for indication. A
|
||||
// zero-value UnreadableState is a valid value.
|
||||
type UnreadableState struct {
|
||||
// both maps represent sets of server IDs
|
||||
unreads map[string]struct{}
|
||||
mentions map[string]struct{}
|
||||
}
|
||||
|
||||
func NewUnreadableState() *UnreadableState {
|
||||
return &UnreadableState{}
|
||||
}
|
||||
|
||||
func (s *UnreadableState) Reset() {
|
||||
s.unreads = map[string]struct{}{}
|
||||
s.mentions = map[string]struct{}{}
|
||||
}
|
||||
|
||||
func (s *UnreadableState) State() (unread, mentioned bool) {
|
||||
unread = len(s.unreads) > 0
|
||||
mentioned = len(s.mentions) > 0
|
||||
|
||||
// Count mentioned as unread.
|
||||
return unread || mentioned, mentioned
|
||||
}
|
||||
|
||||
func (s *UnreadableState) SetState(id string, unread, mentioned bool) {
|
||||
if s.unreads == nil && s.mentions == nil {
|
||||
s.Reset()
|
||||
}
|
||||
|
||||
setIf(unread, id, s.unreads)
|
||||
setIf(mentioned, id, s.mentions)
|
||||
}
|
||||
|
||||
// setIf sets the ID into the given map if the cond boolean is true, or deletes
|
||||
// it if the boolean is false.
|
||||
func setIf(cond bool, id string, m map[string]struct{}) {
|
||||
if cond {
|
||||
m[id] = struct{}{}
|
||||
} else {
|
||||
delete(m, id)
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/humanize"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/spinner"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/traverse"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/gotk3/gotk3/pango"
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/commander"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/traverse"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
|
@ -176,6 +176,9 @@ func (r *Row) Activate() {
|
|||
if r.Session == nil {
|
||||
r.ReconnectSession()
|
||||
} else {
|
||||
// Load all servers in this root node, then call the parent controller's
|
||||
// method.
|
||||
r.Servers.Children.LoadAll()
|
||||
r.svcctrl.SessionSelected(r)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
package traverse
|
||||
|
||||
import "strings"
|
||||
|
||||
type Breadcrumb []string
|
||||
|
||||
func (b Breadcrumb) String() string {
|
||||
return strings.Join([]string(b), "/")
|
||||
}
|
||||
|
||||
type Breadcrumber interface {
|
||||
Breadcrumb() Breadcrumb
|
||||
}
|
||||
|
||||
// TryBreadcrumb accepts a nilable breadcrumber and handles it appropriately.
|
||||
func TryBreadcrumb(i Breadcrumber, appended ...string) []string {
|
||||
if i == nil {
|
||||
return appended
|
||||
}
|
||||
return append(i.Breadcrumb(), appended...)
|
||||
}
|
29
profile.go
29
profile.go
|
@ -4,14 +4,11 @@ package main
|
|||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
_ "net/http/pprof"
|
||||
|
||||
_ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -21,18 +18,28 @@ func init() {
|
|||
// }
|
||||
// }()
|
||||
|
||||
runtime.SetBlockProfileRate(1)
|
||||
// runtime.SetBlockProfileRate(1)
|
||||
|
||||
// f, _ := os.Create("/tmp/cchat.pprof")
|
||||
// p := pprof.Lookup("block")
|
||||
|
||||
// destructor = func() {
|
||||
// log.Println("==destructor==")
|
||||
|
||||
// if err := p.WriteTo(f, 2); err != nil {
|
||||
// log.Println("Profile writeTo error:", err)
|
||||
// }
|
||||
|
||||
// f.Close()
|
||||
// }
|
||||
|
||||
f, _ := os.Create("/tmp/cchat.pprof")
|
||||
p := pprof.Lookup("block")
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
destructor = func() {
|
||||
log.Println("==destructor==")
|
||||
|
||||
if err := p.WriteTo(f, 2); err != nil {
|
||||
log.Println("Profile writeTo error:", err)
|
||||
}
|
||||
|
||||
pprof.StopCPUProfile()
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue