Minor fixes

This commit is contained in:
diamondburned 2021-05-04 20:30:50 -07:00
parent 4a615e02bb
commit d8f68cb852
17 changed files with 138 additions and 52 deletions

2
go.mod
View File

@ -6,7 +6,7 @@ require (
github.com/Xuanwo/go-locale v1.0.0 github.com/Xuanwo/go-locale v1.0.0
github.com/alecthomas/chroma v0.7.3 github.com/alecthomas/chroma v0.7.3
github.com/diamondburned/cchat v0.6.4 github.com/diamondburned/cchat v0.6.4
github.com/diamondburned/cchat-discord v0.0.0-20210501072434-cc2b2ee4c799 github.com/diamondburned/cchat-discord v0.0.0-20210501221918-71c3069fa46f
github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828 github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828
github.com/diamondburned/handy v0.0.0-20210329054445-387ad28eb2c2 github.com/diamondburned/handy v0.0.0-20210329054445-387ad28eb2c2
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972 github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972

2
go.sum
View File

@ -147,6 +147,8 @@ github.com/diamondburned/cchat-discord v0.0.0-20210326063953-deb4ccb32bff h1:p5X
github.com/diamondburned/cchat-discord v0.0.0-20210326063953-deb4ccb32bff/go.mod h1:zbm+BpkQOMD6s87x4FrP3lTt9ddJLWTTPXyMROT+LZs= github.com/diamondburned/cchat-discord v0.0.0-20210326063953-deb4ccb32bff/go.mod h1:zbm+BpkQOMD6s87x4FrP3lTt9ddJLWTTPXyMROT+LZs=
github.com/diamondburned/cchat-discord v0.0.0-20210501072434-cc2b2ee4c799 h1:xxqeuAx0T9SsS8DYKe4jxzL2saEpLyQeAttD0sX/g1E= github.com/diamondburned/cchat-discord v0.0.0-20210501072434-cc2b2ee4c799 h1:xxqeuAx0T9SsS8DYKe4jxzL2saEpLyQeAttD0sX/g1E=
github.com/diamondburned/cchat-discord v0.0.0-20210501072434-cc2b2ee4c799/go.mod h1:zbm+BpkQOMD6s87x4FrP3lTt9ddJLWTTPXyMROT+LZs= github.com/diamondburned/cchat-discord v0.0.0-20210501072434-cc2b2ee4c799/go.mod h1:zbm+BpkQOMD6s87x4FrP3lTt9ddJLWTTPXyMROT+LZs=
github.com/diamondburned/cchat-discord v0.0.0-20210501221918-71c3069fa46f h1:IDC3qToEm5owHf5FlJY9q9Kjbsv45+nly4I2YMv76lE=
github.com/diamondburned/cchat-discord v0.0.0-20210501221918-71c3069fa46f/go.mod h1:zbm+BpkQOMD6s87x4FrP3lTt9ddJLWTTPXyMROT+LZs=
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db h1:VQI2PdbsdsRJ7d669kp35GbCUO44KZ0Xfqdu4o/oqVg= github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db h1:VQI2PdbsdsRJ7d669kp35GbCUO44KZ0Xfqdu4o/oqVg=
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db/go.mod h1:M87kjNzWVPlkZycFNzpGPKQXzkHNnZphuwMf3E9ckgc= github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db/go.mod h1:M87kjNzWVPlkZycFNzpGPKQXzkHNnZphuwMf3E9ckgc=
github.com/diamondburned/gotk3 v0.0.0-20201209182406-e7291341a091 h1:lQpSWzbi3rQf66aMSip/rIypasIFwqCqF0Wfn5og6gw= github.com/diamondburned/gotk3 v0.0.0-20201209182406-e7291341a091 h1:lQpSWzbi3rQf66aMSip/rIypasIFwqCqF0Wfn5og6gw=

View File

@ -1,6 +1,7 @@
package gts package gts
import ( import (
"context"
"io" "io"
"os" "os"
"time" "time"
@ -154,6 +155,7 @@ func Main(wfn func() MainApplication) {
// Async runs fn asynchronously, then runs the function it returns in the Gtk // Async runs fn asynchronously, then runs the function it returns in the Gtk
// main thread. // main thread.
// TODO: deprecate Async.
func Async(fn func() (func(), error)) { func Async(fn func() (func(), error)) {
go func() { go func() {
f, err := fn() f, err := fn()
@ -168,27 +170,66 @@ func Async(fn func() (func(), error)) {
}() }()
} }
// AsyncCancel is similar to AsyncCtx, but the context is created internally.
func AsyncCancel(fn func(ctx context.Context) (func(), error)) context.CancelFunc {
ctx, cancel := context.WithCancel(context.Background())
go func() {
// fn() is assumed to use the same given ctx.
f, err := fn(ctx)
if err != nil {
log.Error(err)
}
// Attempt to run the callback if it's there.
if f != nil {
ExecAsyncCtx(ctx, f)
}
}()
return cancel
}
// AsyncCtx does what Async does, except the returned callback will not be
// executed if the given context has expired or the returned callback is called.
func AsyncCtx(ctx context.Context, fn func() (func(), error)) {
go func() {
// fn() is assumed to use the same given ctx.
f, err := fn()
if err != nil {
log.Error(err)
}
// Attempt to run the callback if it's there.
if f != nil {
ExecAsyncCtx(ctx, f)
}
}()
}
// ExecLater executes the function asynchronously with a low priority. // ExecLater executes the function asynchronously with a low priority.
func ExecLater(fn func()) { func ExecLater(fn func()) {
glib.IdleAddPriority(glib.PRIORITY_DEFAULT_IDLE, fn) glib.IdleAddPriority(glib.PRIORITY_DEFAULT_IDLE, fn)
} }
// ExecAsync executes function asynchronously in the Gtk main thread. // ExecAsync executes function asynchronously in the Gtk main thread.
// TODO: deprecate Async.
func ExecAsync(fn func()) { func ExecAsync(fn func()) {
glib.IdleAddPriority(glib.PRIORITY_HIGH, fn) glib.IdleAddPriority(glib.PRIORITY_HIGH, fn)
} }
// ExecSync executes the function asynchronously, but returns a channel that // ExecAsyncCtx executes the function asynchronously in the Gtk main thread only
// indicates when the job is done. // if the context has not expired. This API has absolutely no race conditions if
func ExecSync(fn func()) <-chan struct{} { // the context is only canceled in the main thread.
var ch = make(chan struct{}) func ExecAsyncCtx(ctx context.Context, fn func()) {
ExecAsync(func() {
select {
case <-ctx.Done():
glib.IdleAddPriority(glib.PRIORITY_HIGH, func() { default:
fn() fn()
close(ch) }
}) })
return ch
} }
// DoAfter calls f after the given duration in the Gtk main loop. // DoAfter calls f after the given duration in the Gtk main loop.

View File

@ -21,8 +21,8 @@ type truncator struct {
var shortTruncators = []truncator{ var shortTruncators = []truncator{
{d: Day, s: "15:04"}, {d: Day, s: "15:04"},
{d: Week, s: "Mon 15:04"}, {d: Week, s: "Mon 15:04"},
{d: Year, s: "15:04 02/01"}, {d: Year, s: "02/01 15:04"},
{d: -1, s: "15:04 02/01/2006"}, {d: -1, s: "02/01/2006 15:04"},
} }
func TimeAgo(t time.Time) string { func TimeAgo(t time.Time) string {

View File

@ -110,3 +110,16 @@ var toRestore = map[string]interface{}{}
func RegisterConfig(filename string, jsonValue interface{}) { func RegisterConfig(filename string, jsonValue interface{}) {
toRestore[filename] = jsonValue toRestore[filename] = jsonValue
} }
// Updaters contains a list of callbacks to be called when something is updated.
type Updaters []func()
func (us *Updaters) Add(f func()) {
*us = append(*us, f)
}
func (us *Updaters) Updated() {
for _, f := range *us {
f()
}
}

View File

@ -6,10 +6,29 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container" "github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message" "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"
"github.com/gotk3/gotk3/gtk"
) )
type Container struct { type Container struct {
*container.ListContainer *container.ListContainer
sg SizeGroups
}
type SizeGroups struct {
Timestamp *gtk.SizeGroup
Username *gtk.SizeGroup
}
func NewSizeGroups() SizeGroups {
sg1, _ := gtk.SizeGroupNew(gtk.SIZE_GROUP_HORIZONTAL)
sg2, _ := gtk.SizeGroupNew(gtk.SIZE_GROUP_HORIZONTAL)
return SizeGroups{sg1, sg2}
}
func (sgs *SizeGroups) Add(msg Message) {
sgs.Timestamp.AddWidget(msg.Timestamp)
sgs.Username.AddWidget(msg.Username)
} }
var _ container.Container = (*Container)(nil) var _ container.Container = (*Container)(nil)
@ -17,11 +36,12 @@ var _ container.Container = (*Container)(nil)
func NewContainer(ctrl container.Controller) *Container { func NewContainer(ctrl container.Controller) *Container {
c := container.NewListContainer(ctrl) c := container.NewListContainer(ctrl)
primitives.AddClass(c, "compact-container") primitives.AddClass(c, "compact-container")
return &Container{c} return &Container{c, NewSizeGroups()}
} }
func (c *Container) NewPresendMessage(state *message.PresendState) container.PresendMessageRow { func (c *Container) NewPresendMessage(state *message.PresendState) container.PresendMessageRow {
msg := WrapPresendMessage(state) msg := WrapPresendMessage(state)
c.sg.Add(msg.Message)
c.addMessage(msg) c.addMessage(msg)
return msg return msg
} }
@ -29,6 +49,7 @@ func (c *Container) NewPresendMessage(state *message.PresendState) container.Pre
func (c *Container) CreateMessage(msg cchat.MessageCreate) { func (c *Container) CreateMessage(msg cchat.MessageCreate) {
gts.ExecAsync(func() { gts.ExecAsync(func() {
msg := WrapMessage(message.NewState(msg)) msg := WrapMessage(message.NewState(msg))
c.sg.Add(msg)
c.addMessage(msg) c.addMessage(msg)
c.CleanMessages() c.CleanMessages()
}) })

View File

@ -16,8 +16,7 @@ import (
var messageTimeCSS = primitives.PrepareClassCSS("", ` var messageTimeCSS = primitives.PrepareClassCSS("", `
.message-time { .message-time {
margin-left: 1em; margin: 0 8px;
margin-right: 1em;
} }
`) `)
@ -52,13 +51,18 @@ var _ container.MessageRow = (*Message)(nil)
func WrapMessage(ct *message.State) Message { func WrapMessage(ct *message.State) Message {
ts := message.NewTimestamp() ts := message.NewTimestamp()
ts.SetVAlign(gtk.ALIGN_START) ts.SetVAlign(gtk.ALIGN_START)
ts.SetHAlign(gtk.ALIGN_END)
ts.SetXAlign(1.00)
ts.SetText(humanize.TimeAgo(ct.Time)) ts.SetText(humanize.TimeAgo(ct.Time))
ts.SetTooltipText(ct.Time.Format(time.Stamp)) ts.SetTooltipText(ct.Time.Format(time.Stamp))
ts.Show() ts.Show()
messageTimeCSS(ts) messageTimeCSS(ts)
user := message.NewUsername() user := message.NewUsername()
user.SetMaxWidthChars(25) user.SetMaxWidthChars(22)
user.SetHAlign(gtk.ALIGN_END)
user.SetXAlign(1.0)
user.SetJustify(gtk.JUSTIFY_RIGHT)
user.SetEllipsize(pango.ELLIPSIZE_NONE) user.SetEllipsize(pango.ELLIPSIZE_NONE)
user.SetLineWrap(true) user.SetLineWrap(true)
user.SetLineWrapMode(pango.WRAP_WORD_CHAR) user.SetLineWrapMode(pango.WRAP_WORD_CHAR)

View File

@ -10,10 +10,7 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
) )
const ( const AvatarSize = message.AvatarSize
AvatarSize = 40
AvatarMargin = 10
)
// NewMessage creates a new message. // NewMessage creates a new message.
func NewMessage( func NewMessage(
@ -47,7 +44,7 @@ func NewContainer(ctrl container.Controller) *Container {
return &Container{ListContainer: c} return &Container{ListContainer: c}
} }
const splitDuration = 10 * time.Minute const splitDuration = 3 * time.Minute
// isCollapsible returns true if the given lastMsg has matching conditions with // isCollapsible returns true if the given lastMsg has matching conditions with
// the given msg. // the given msg.

View File

@ -3,7 +3,6 @@ package cozy
import ( import (
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container" "github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/message" "github.com/diamondburned/cchat-gtk/internal/ui/messages/message"
"github.com/gotk3/gotk3/gtk"
) )
// Collapsed is a message that follows after FullMessage. It does not show // Collapsed is a message that follows after FullMessage. It does not show
@ -11,36 +10,26 @@ import (
type CollapsedMessage struct { type CollapsedMessage struct {
// Author is still updated normally. // Author is still updated normally.
*message.State *message.State
Timestamp *gtk.Label
} }
// WrapCollapsedMessage wraps the given message state to be a collapsed message. // WrapCollapsedMessage wraps the given message state to be a collapsed message.
func WrapCollapsedMessage(gc *message.State) *CollapsedMessage { func WrapCollapsedMessage(gc *message.State) *CollapsedMessage {
// Set Timestamp's padding accordingly to Avatar's.
ts := message.NewTimestamp()
ts.SetSizeRequest(AvatarSize, -1)
ts.SetVAlign(gtk.ALIGN_START)
ts.SetXAlign(0.5) // middle align
ts.SetMarginEnd(container.ColumnSpacing)
ts.SetMarginStart(container.ColumnSpacing * 2)
// Set Content's padding accordingly to FullMessage's main box. // Set Content's padding accordingly to FullMessage's main box.
gc.Content.SetMarginEnd(container.ColumnSpacing * 2) gc.Content.SetMarginStart(container.ColumnSpacing*2 + AvatarSize)
gc.Content.SetMarginEnd(container.ColumnSpacing)
gc.PackStart(ts, false, false, 0)
gc.PackStart(gc.Content, true, true, 0) gc.PackStart(gc.Content, true, true, 0)
gc.SetClass("cozy-collapsed") gc.SetClass("cozy-collapsed")
return &CollapsedMessage{ return &CollapsedMessage{
State: gc, State: gc,
Timestamp: ts,
} }
} }
func (c *CollapsedMessage) Revert() *message.State { func (c *CollapsedMessage) Revert() *message.State {
c.ClearBox() c.ClearBox()
c.Content.SetMarginStart(0)
c.Content.SetMarginEnd(0) c.Content.SetMarginEnd(0)
c.Timestamp.Destroy()
return c.Unwrap() return c.Unwrap()
} }

View File

@ -61,7 +61,7 @@ func WrapFullMessage(gc *message.State) *FullMessage {
header.Show() header.Show()
avatar := NewAvatar(gc.Row) avatar := NewAvatar(gc.Row)
avatar.SetMarginStart(container.ColumnSpacing * 2) avatar.SetMarginStart(container.ColumnSpacing)
avatar.Connect("clicked", func(w gtk.IWidget) { avatar.Connect("clicked", func(w gtk.IWidget) {
if output := header.Output(); len(output.Mentions) > 0 { if output := header.Output(); len(output.Mentions) > 0 {
labeluri.PopoverMentioner(w, output.Input, output.Mentions[0]) labeluri.PopoverMentioner(w, output.Input, output.Mentions[0])
@ -78,12 +78,10 @@ func WrapFullMessage(gc *message.State) *FullMessage {
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
main.PackStart(header, false, false, 0) main.PackStart(header, false, false, 0)
main.PackStart(gc.Content, false, false, 0) main.PackStart(gc.Content, false, false, 0)
main.SetMarginEnd(container.ColumnSpacing * 2) main.SetMarginEnd(container.ColumnSpacing)
main.SetMarginStart(container.ColumnSpacing) main.SetMarginStart(container.ColumnSpacing)
main.Show() main.Show()
mainCSS(main)
// Also attach a class for the main box shown on the right.
primitives.AddClass(main, "cozy-main")
gc.PackStart(avatar, false, false, 0) gc.PackStart(avatar, false, false, 0)
gc.PackStart(main, true, true, 0) gc.PackStart(main, true, true, 0)

View File

@ -479,6 +479,7 @@ func (c *ListStore) Highlight(msg MessageRow) {
} }
func destroyMsg(row *messageRow) { func destroyMsg(row *messageRow) {
row.Revert()
row.state.Author.Name.Stop() row.state.Author.Name.Stop()
row.state.Row.Destroy() row.state.Row.Destroy()
} }

View File

@ -14,14 +14,16 @@ import (
const AvatarSize = 24 const AvatarSize = 24
var showUser = true var (
var currentRevealer = func(bool) {} // noop by default showUser = true
updaters config.Updaters
)
func init() { func init() {
// Bind this revealer in settings. // Bind this revealer in settings.
config.AppearanceAdd("Show Username in Input", config.Switch( config.AppearanceAdd("Show Username in Input", config.Switch(
&showUser, &showUser,
func(b bool) { currentRevealer(b) }, func(b bool) { updaters.Updated() },
)) ))
} }
@ -55,7 +57,7 @@ func NewContainer() *Container {
// Bind the current global revealer to this revealer for settings. This // Bind the current global revealer to this revealer for settings. This
// operation should be thread-safe, as everything is being done in the main // operation should be thread-safe, as everything is being done in the main
// thread. // thread.
currentRevealer = rev.SetRevealChild updaters.Add(func() { rev.SetRevealChild(showUser) })
author := message.NewCustomAuthor("", text.Plain("self")) author := message.NewCustomAuthor("", text.Plain("self"))
@ -68,6 +70,7 @@ func NewContainer() *Container {
u.avatar = roundimage.NewImage(0) u.avatar = roundimage.NewImage(0)
u.avatar.SetSize(AvatarSize) u.avatar.SetSize(AvatarSize)
u.avatar.SetHAlign(gtk.ALIGN_CENTER)
u.avatar.SetPlaceholderIcon("user-available-symbolic", AvatarSize) u.avatar.SetPlaceholderIcon("user-available-symbolic", AvatarSize)
u.avatar.Show() u.avatar.Show()

View File

@ -15,6 +15,8 @@ import (
"github.com/gotk3/gotk3/pango" "github.com/gotk3/gotk3/pango"
) )
const AvatarSize = 40
// Container describes a message container that wraps a state. These methods are // Container describes a message container that wraps a state. These methods are
// made for containers to override; methods not meant to be override are not // made for containers to override; methods not meant to be override are not
// exposed and will be done directly on the State. // exposed and will be done directly on the State.
@ -70,6 +72,7 @@ func NewState(msg cchat.MessageCreate) *State {
// immediately afterwards; it is invalid once the state is used. // immediately afterwards; it is invalid once the state is used.
func NewEmptyState() *State { func NewEmptyState() *State {
ctbody := labeluri.NewLabel(text.Rich{}) ctbody := labeluri.NewLabel(text.Rich{})
ctbody.Tooltip = false
ctbody.SetHAlign(gtk.ALIGN_FILL) ctbody.SetHAlign(gtk.ALIGN_FILL)
ctbody.SetEllipsize(pango.ELLIPSIZE_NONE) ctbody.SetEllipsize(pango.ELLIPSIZE_NONE)
ctbody.SetLineWrap(true) ctbody.SetLineWrap(true)

View File

@ -24,6 +24,8 @@ func RenderSkipImages(rich text.Rich) markup.RenderOutput {
// need to manually // need to manually
type Label struct { type Label struct {
gtk.Label gtk.Label
Tooltip bool
label text.Rich label text.Rich
output markup.RenderOutput output markup.RenderOutput
render LabelRenderer render LabelRenderer
@ -41,7 +43,7 @@ func NewStaticLabel(rich text.Rich) *Label {
label.SetMarkup(markup.Render(rich)) label.SetMarkup(markup.Render(rich))
} }
return &Label{Label: *label} return &Label{Label: *label, Tooltip: true}
} }
// NewLabel creates a self-updating label. // NewLabel creates a self-updating label.
@ -83,7 +85,10 @@ func (l *Label) SetLabel(content text.Rich) {
l.output = out l.output = out
l.SetMarkup(out.Markup) l.SetMarkup(out.Markup)
l.SetTooltipMarkup(out.Markup)
if l.Tooltip {
l.SetTooltipMarkup(out.Markup)
}
} }
// SetRenderer sets a custom renderer. If the given renderer is nil, then the // SetRenderer sets a custom renderer. If the given renderer is nil, then the

View File

@ -59,6 +59,7 @@ type ServerRow struct {
mentioned bool mentioned bool
showLabel bool showLabel bool
UnreadIndicator cchat.UnreadIndicator
// callback to cancel unread indicator // callback to cancel unread indicator
cancelUnread func() cancelUnread func()
} }
@ -96,9 +97,10 @@ func NewHollowServer(p traverse.Breadcrumber, sv cchat.Server, ctrl ParentContro
serverRow.children.SetUnreadHandler(serverRow.SetUnreadUnsafe) serverRow.children.SetUnreadHandler(serverRow.SetUnreadUnsafe)
case messenger != nil: case messenger != nil:
if unreader := messenger.AsUnreadIndicator(); unreader != nil { serverRow.UnreadIndicator = messenger.AsUnreadIndicator()
if serverRow.UnreadIndicator != nil {
gts.Async(func() (func(), error) { gts.Async(func() (func(), error) {
c, err := unreader.UnreadIndicate(&serverRow) c, err := serverRow.UnreadIndicator.UnreadIndicate(&serverRow)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "Failed to use unread indicator") return nil, errors.Wrap(err, "Failed to use unread indicator")
} }

View File

@ -181,8 +181,10 @@ func (s *Servers) setDone() {
s.SetVisibleChild(s.Main) s.SetVisibleChild(s.Main)
// stop the spinner. // stop the spinner.
s.spinner.Destroy() if s.spinner != nil {
s.spinner = nil s.spinner.Destroy()
s.spinner = nil
}
} }
// setLoading shows a loading spinner. Use this after the session row is // setLoading shows a loading spinner. Use this after the session row is

View File

@ -20,7 +20,12 @@ undershoot { background-size: 0 }
*/ */
.top-level .server-list.expanded { .top-level .server-list.expanded {
background-color: @borders; background-color: @theme_bg_color;
}
.top-level .server-list.expanded > .server-button,
.top-level .server-list.expanded > revealer > .server-children {
background-color: mix(alpha(@theme_selected_bg_color, 0.5), @borders, 0.25);
} }
.top-level .server-button { .top-level .server-button {