Works with cchat v0.3

This commit is contained in:
diamondburned 2020-10-14 23:32:11 -07:00
parent ba4728c3d6
commit a10230a8cd
36 changed files with 642 additions and 506 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
cchat-gtk
.direnv
.envrc

8
go.mod
View File

@ -7,9 +7,9 @@ replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200816
require (
github.com/Xuanwo/go-locale v0.2.0
github.com/alecthomas/chroma v0.7.3
github.com/diamondburned/cchat v0.0.49
github.com/diamondburned/cchat-discord v0.0.0-20200821041521-647c854d7b5e
github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b
github.com/diamondburned/cchat v0.3.7
github.com/diamondburned/cchat-discord v0.0.0-20201015062850-090259a6b4ca
github.com/diamondburned/cchat-mock v0.0.0-20201014202453-b9838fab0ab0
github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894
github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972
@ -23,6 +23,6 @@ require (
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/twmb/murmur3 v1.1.3
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
gopkg.in/yaml.v2 v2.2.7 // indirect
)

36
go.sum
View File

@ -64,6 +64,25 @@ github.com/diamondburned/cchat v0.0.48 h1:MAzGzKY20JBh/LnirOZVPwbMq07xfqu4Lb4XsV
github.com/diamondburned/cchat v0.0.48/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.49 h1:zP6QvjdRU3UqDZt3rEqjkR/5M68XRVms7htHfE9tLOc=
github.com/diamondburned/cchat v0.0.49/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.2.11 h1:w4c/6t02htGtVj6yIjznecOGMlkcj0TmmLy+K48gHeM=
github.com/diamondburned/cchat v0.2.11/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.2.12 h1:R4wrBdhELMfhv2Kn3xL/H3ci8UcLXzFRPq1IrY4+js4=
github.com/diamondburned/cchat v0.2.12/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.2.13 h1:12xmJ1DpLZTG9icGZSXruCPT2BylOdhgXfKKwbqUXx4=
github.com/diamondburned/cchat v0.2.13/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.2.14 h1:mxcwre0LMjimrN5UAZVmerBqg9p2OxgjF27fJ1ASMjw=
github.com/diamondburned/cchat v0.2.14/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.2.15 h1:GYKD4VTrWCf1zIsFmyDVsUYaLjvVhgEh7qrg4KtaM0k=
github.com/diamondburned/cchat v0.2.15/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.3.1 h1:7NbVjT50dmLxcHPm+eDFF5jcaZw3t/9IdSEkZ/md1Rg=
github.com/diamondburned/cchat v0.3.1/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.3.2 h1:KcaWAN5qztKsSVsVGAWE4Mr779fOFLwLkxqlGh2bLo8=
github.com/diamondburned/cchat v0.3.2/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.3.3 h1:FFdcahDUGGP/h9BvVjoYOKgNXSRTQS+6Blb8keQmxVw=
github.com/diamondburned/cchat v0.3.3/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.3.5/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.3.7 h1:0t3FkbzC/pBRAR3w0uYznJ+7dYqcR1M48a9wgz4JkIg=
github.com/diamondburned/cchat v0.3.7/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat-discord v0.0.0-20200719175346-af912db55401 h1:llmx/8UiJoTcHUw+GE5/rESVVmmnLh1HEPx3wRj+oQY=
github.com/diamondburned/cchat-discord v0.0.0-20200719175346-af912db55401/go.mod h1:+hSrIVYj5tIPLAorDsHj2Tbt2fWlZtOanzfEUHX53HM=
github.com/diamondburned/cchat-discord v0.0.0-20200730000036-2c93cdc1974e h1:EA5Vg0x57qLURJP80XhABBW+X0sbQSh2gw5qvPbZTs4=
@ -84,8 +103,24 @@ github.com/diamondburned/cchat-discord v0.0.0-20200820222718-68cfafc4c318 h1:mRG
github.com/diamondburned/cchat-discord v0.0.0-20200820222718-68cfafc4c318/go.mod h1:rhUseXyWXTVw0Da8edbQMHU9I4LRQ2zcRB3zRqg/oe4=
github.com/diamondburned/cchat-discord v0.0.0-20200821041521-647c854d7b5e h1:higtJiL7t6owP2dVAwJxItnpsD1MUypWDVant2uYv6g=
github.com/diamondburned/cchat-discord v0.0.0-20200821041521-647c854d7b5e/go.mod h1:rhUseXyWXTVw0Da8edbQMHU9I4LRQ2zcRB3zRqg/oe4=
github.com/diamondburned/cchat-discord v0.0.0-20201007015315-da520786d74b h1:IJYC5vKdT9zTX/vLRXKIpv9xC6FNVq13O3X5ndSsN6g=
github.com/diamondburned/cchat-discord v0.0.0-20201007015315-da520786d74b/go.mod h1:Bp7CERMjWVJf/Rv8pO8pdcg/ZLuvJ24TenDAzfW+Nl8=
github.com/diamondburned/cchat-discord v0.0.0-20201009070751-b6694ea24a39 h1:P1KcSdD1a8fszRH3exaT9B63OtKx1MKTR6YllbiXRXQ=
github.com/diamondburned/cchat-discord v0.0.0-20201009070751-b6694ea24a39/go.mod h1:ijG0kx3DLVygYUlhVPvvBAlLW8cNtUuXdFtAUtigvOw=
github.com/diamondburned/cchat-discord v0.0.0-20201009173316-1907986ceb08 h1:iytskZ4dvc6KLlMDCpFcTIB7nIZkbprjDnaL2bCkduE=
github.com/diamondburned/cchat-discord v0.0.0-20201009173316-1907986ceb08/go.mod h1:BF8CJaW6rdYDGjFd2qXODS5nSu9vvW7OehgkXIB8B0M=
github.com/diamondburned/cchat-discord v0.0.0-20201015062850-090259a6b4ca h1:36MnUdiunaz4hsqDO0313Nc03y59PzIPZtmEF8gUeCg=
github.com/diamondburned/cchat-discord v0.0.0-20201015062850-090259a6b4ca/go.mod h1:S0PDR6aj2qE871JSy94YvwtprQJCWwkIJWzRu7S1Asc=
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/cchat-mock v0.0.0-20201004204741-b841407af381 h1:8JWNJMgoa3fL2py3gXSeC3NiAC+39EZp+JmvaoDBTUU=
github.com/diamondburned/cchat-mock v0.0.0-20201004204741-b841407af381/go.mod h1:dObDshcI3LXSicnuBBoRiCV6j0H5FZwp6wq4yANMdyQ=
github.com/diamondburned/cchat-mock v0.0.0-20201009070609-ab7eccf48e52 h1:0XES1llczz7181MtWsmMBvSibBZg9CAlGx+eDpyvzdM=
github.com/diamondburned/cchat-mock v0.0.0-20201009070609-ab7eccf48e52/go.mod h1:qDKTtPsdMeAmOV1QWwIII1hBzjcCZOXsbxDYoyuw2eo=
github.com/diamondburned/cchat-mock v0.0.0-20201009173002-83501e8aad33 h1:1rw4gQwAPAR47OLB06st0RP77jCrH+29EH/Xgu0otKI=
github.com/diamondburned/cchat-mock v0.0.0-20201009173002-83501e8aad33/go.mod h1:tshTM0VduaKpzqYnVYdAozXVk/dBkEkoyM3KXua+cIk=
github.com/diamondburned/cchat-mock v0.0.0-20201014202453-b9838fab0ab0 h1:GwceonhYEE6QZzqMTCc/OsBJ+CZ9omWzN/4if+e62uA=
github.com/diamondburned/cchat-mock v0.0.0-20201014202453-b9838fab0ab0/go.mod h1:hYNki0Ic/d7zFVXTJIjp/td1W4OpxDNcVY8layxgTyc=
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/gotk3 v0.0.0-20200816224505-3cd69b83a48a h1:wEldljb421/Jp84RNb0zBfqmiWt/TTQzUE6R1ap6UuQ=
@ -138,6 +173,7 @@ github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8=
github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=

View File

@ -27,14 +27,8 @@ type Session struct {
func ConvertSession(ses cchat.Session) *Session {
var name = ses.Name().Content
saver, ok := ses.(cchat.SessionSaver)
if !ok {
return nil
}
s, err := saver.Save()
if err != nil {
log.Error(errors.Wrapf(err, "Failed to save session ID %s (%s)", ses.ID(), name))
saver := ses.AsSessionSaver()
if saver == nil {
return nil
}
@ -47,7 +41,7 @@ func ConvertSession(ses cchat.Session) *Session {
return &Session{
ID: ses.ID(),
Name: name,
Data: s,
Data: saver.SaveSession(),
}
}

View File

@ -41,7 +41,6 @@ type Container interface {
// Thread-safe methods.
cchat.MessagesContainer
cchat.MessagePrepender
// Thread-unsafe methods.
CreateMessageUnsafe(cchat.MessageCreate)
@ -73,7 +72,7 @@ type Controller interface {
Bottomed() bool
// AuthorEvent is called on message create/update. This is used to update
// the typer state.
AuthorEvent(a cchat.MessageAuthor)
AuthorEvent(a cchat.Author)
}
// Constructor is an interface for making custom message implementations which

View File

@ -70,10 +70,10 @@ func (c *Container) NewMessage(msg cchat.MessageCreate) container.GridMessage {
author := msg.Author()
// Try and reuse an existing avatar if the author has one.
if avatarURL, ok := author.(cchat.MessageAuthorAvatar); ok {
if avatarURL := author.Avatar(); avatarURL != "" {
// Try reusing the avatar, but fetch it from the interndet if we can't
// reuse. The reuse function does this for us.
c.reuseAvatar(author.ID(), avatarURL.Avatar(), full)
c.reuseAvatar(author.ID(), author.Avatar(), full)
}
return full

View File

@ -139,14 +139,10 @@ func (m *FullMessage) UpdateTimestamp(t time.Time) {
m.Timestamp.SetText(humanize.TimeAgoLong(t))
}
func (m *FullMessage) UpdateAuthor(author cchat.MessageAuthor) {
func (m *FullMessage) UpdateAuthor(author cchat.Author) {
// 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())
}
m.Avatar.SetURL(author.Avatar())
}
// CopyAvatarPixbuf sets the pixbuf into the given container. This shares the

View File

@ -192,41 +192,39 @@ func (c *GridStore) LastMessage() GridMessage {
// Message finds the message state in the container. It is not thread-safe. This
// exists for backwards compatibility.
func (c *GridStore) Message(msg cchat.MessageHeader) GridMessage {
if m := c.message(msg); m != nil {
func (c *GridStore) Message(msgID cchat.ID, nonce string) GridMessage {
if m := c.message(msgID, nonce); m != nil {
return m.GridMessage
}
return nil
}
func (c *GridStore) message(msg cchat.MessageHeader) *gridMessage {
func (c *GridStore) message(msgID cchat.ID, nonce string) *gridMessage {
// Search using the ID first.
m, ok := c.messages[msg.ID()]
m, ok := c.messages[msgID]
if ok {
return m
}
// Is this an existing message?
if noncer, ok := msg.(cchat.MessageNonce); ok {
var nonce = noncer.Nonce()
if nonce != "" {
// Things in this map are guaranteed to have presend != nil.
m, ok := c.messages[nonce]
if ok {
// Replace the nonce key with ID.
delete(c.messages, nonce)
c.messages[msg.ID()] = m
c.messages[msgID] = m
// Set the right ID.
m.presend.SetDone(msg.ID())
m.presend.SetDone(msgID)
// Destroy the presend struct.
m.presend = nil
// Replace the nonce inside the ID slice with the actual ID.
if ix := c.findIndex(nonce); ix > -1 {
c.messageIDs[ix] = msg.ID()
c.messageIDs[ix] = msgID
} else {
log.Error(fmt.Errorf("Missed ID %s in slice index %d", msg.ID(), ix))
log.Error(fmt.Errorf("Missed ID %s in slice index %d", msgID, ix))
}
return m
@ -280,7 +278,7 @@ func (c *GridStore) CreateMessageUnsafe(msg cchat.MessageCreate) {
defer c.Controller.AuthorEvent(msg.Author())
// Attempt to update before insertion (aka upsert).
if msgc := c.Message(msg); 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())
@ -305,11 +303,11 @@ func (c *GridStore) UpdateMessageUnsafe(msg cchat.MessageUpdate) {
// Call the event handler last.
defer c.Controller.AuthorEvent(msg.Author())
if msgc := c.Message(msg); msgc != nil {
if msgc := c.Message(msg.ID(), ""); msgc != nil {
if author := msg.Author(); author != nil {
msgc.UpdateAuthor(author)
}
if content := msg.Content(); !content.Empty() {
if content := msg.Content(); !content.IsEmpty() {
msgc.UpdateContent(content, true)
}
}

View File

@ -1,114 +0,0 @@
package completion
import (
"fmt"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gtk"
)
const (
ImageSmall = 25
ImageLarge = 40
ImagePadding = 6
)
var ppIcon = []imgutil.Processor{imgutil.Round(true)}
type View struct {
*completion.Completer
entries []cchat.CompletionEntry
completer cchat.ServerMessageSendCompleter
}
func New(text *gtk.TextView) *View {
v := &View{}
c := completion.NewCompleter(text, v)
v.Completer = c
return v
}
func (v *View) Reset() {
v.SetCompleter(nil)
}
func (v *View) SetCompleter(completer cchat.ServerMessageSendCompleter) {
v.Clear()
v.Hide()
v.completer = completer
}
func (v *View) Update(words []string, i int) []gtk.IWidget {
// If we don't have a completer, then don't run.
if v.completer == nil {
return nil
}
v.entries = v.completer.CompleteMessage(words, i)
var widgets = make([]gtk.IWidget, len(v.entries))
for i, entry := range v.entries {
// Container that holds the label.
lbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
lbox.SetVAlign(gtk.ALIGN_CENTER)
lbox.Show()
// Label for the primary text.
l := rich.NewLabel(entry.Text)
l.Show()
lbox.PackStart(l, false, false, 0)
// Get the iamge size so we can change and use if needed. The default
var size = ImageSmall
if !entry.Secondary.Empty() {
size = ImageLarge
s := rich.NewLabel(text.Rich{})
s.SetMarkup(fmt.Sprintf(
`<span alpha="50%%" size="small">%s</span>`,
markup.Render(entry.Secondary),
))
s.Show()
lbox.PackStart(s, false, false, 0)
}
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
b.PackEnd(lbox, true, true, ImagePadding)
b.Show()
// Do we have an icon?
if entry.IconURL != "" {
img, _ := gtk.ImageNew()
img.SetMarginStart(ImagePadding)
img.SetSizeRequest(size, size)
img.Show()
// Prepend the image into the box.
b.PackEnd(img, false, false, 0)
var pps []imgutil.Processor
if !entry.Image {
pps = ppIcon
}
httputil.AsyncImageSized(img, entry.IconURL, size, size, pps...)
}
widgets[i] = b
}
return widgets
}
func (v *View) Word(i int) string {
return v.entries[i].Raw
}

View File

@ -7,9 +7,9 @@ import (
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/attachment"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/completion"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/username"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
"github.com/diamondburned/gspell"
"github.com/gotk3/gotk3/gtk"
@ -24,7 +24,7 @@ type Controller interface {
type InputView struct {
*Field
Completer *completion.View
Completer *completion.Completer
}
var textCSS = primitives.PrepareCSS(`
@ -69,7 +69,7 @@ func NewView(ctrl Controller) *InputView {
primitives.AttachCSS(text, textCSS)
// Bind the text event handler to text first.
c := completion.New(text)
c := completion.NewCompleter(text)
// Bind the input callback later.
f := NewField(text, ctrl)
@ -78,12 +78,18 @@ func NewView(ctrl Controller) *InputView {
return &InputView{f, c}
}
func (v *InputView) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
v.Field.SetSender(session, sender)
func (v *InputView) SetMessenger(session cchat.Session, messenger cchat.Messenger) {
v.Field.SetMessenger(session, messenger)
if messenger == nil {
return
}
// Ignore ok; completer can be nil.
completer, _ := sender.(cchat.ServerMessageSendCompleter)
v.Completer.SetCompleter(completer)
// TODO: this is possibly racy vs the above SetMessenger.
if sender := messenger.AsSender(); sender != nil {
v.Completer.SetCompleter(sender.AsCompleter())
}
}
type Field struct {
@ -111,11 +117,12 @@ type Field struct {
}
type fieldState struct {
UserID string
Sender cchat.ServerMessageSender
upload bool // true if server supports files
editor cchat.ServerMessageEditor
typer cchat.ServerMessageTypingIndicator
UserID string
Messenger cchat.Messenger
Sender cchat.Sender
upload bool // true if server supports files
editor cchat.Editor
typing cchat.TypingIndicator
editingID string // never empty
lastTyped time.Time
@ -215,30 +222,30 @@ func (f *Field) Reset() {
f.clearText()
}
// SetSender changes the sender of the input field. If nil, the input will be
// disabled. Reset() should be called first.
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
// SetMessenger changes the messenger of the input field. If nil, the input
// will be disabled. Reset() should be called first.
func (f *Field) SetMessenger(session cchat.Session, messenger cchat.Messenger) {
// Update the left username container in the input.
f.Username.Update(session, sender)
f.Username.Update(session, messenger)
f.UserID = session.ID()
// Set the sender.
if sender != nil {
f.Sender = sender
if messenger != nil {
f.Messenger = messenger
f.Sender = messenger.AsSender()
f.text.SetSensitive(true)
// Allow editor to be nil.
f.editor, _ = sender.(cchat.ServerMessageEditor)
f.editor = f.Messenger.AsEditor()
// Allow typer to be nil.
f.typer, _ = sender.(cchat.ServerMessageTypingIndicator)
f.typing = f.Messenger.AsTypingIndicator()
// See if we can upload files.
_, allowUpload := sender.(cchat.ServerMessageAttachmentSender)
f.SetAllowUpload(allowUpload)
f.SetAllowUpload(f.Sender.CanAttach())
// Populate the duration state if typer is not nil.
if f.typer != nil {
f.typerDura = f.typer.TypingTimeout()
if f.typing != nil {
f.typerDura = f.typing.TypingTimeout()
}
}
}
@ -262,7 +269,7 @@ func (f *Field) SetAllowUpload(allow bool) {
// Editable returns whether or not the input field can be edited.
func (f *Field) Editable(msgID string) bool {
return f.editor != nil && f.editor.MessageEditable(msgID)
return f.editor != nil && f.editor.IsEditable(msgID)
}
func (f *Field) StartEditing(msgID string) bool {
@ -272,7 +279,7 @@ func (f *Field) StartEditing(msgID string) bool {
}
// Try and request the old message content for editing.
content, err := f.editor.RawMessageContent(msgID)
content, err := f.editor.RawContent(msgID)
if err != nil {
// TODO: show error
log.Error(errors.Wrap(err, "Failed to get message content"))

View File

@ -105,7 +105,7 @@ 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 {
if f.typing != 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) {
@ -113,7 +113,7 @@ func (f *Field) keyDown(tv *gtk.TextView, ev *gdk.Event) bool {
f.lastTyped = now
// Send asynchronously.
go func() {
if err := f.typer.Typing(); err != nil {
if err := f.typing.Typing(); err != nil {
log.Error(errors.Wrap(err, "Failed to announce typing"))
}
}()

View File

@ -46,7 +46,7 @@ func (f *Field) sendInput() {
// Are we editing anything?
if id := f.editingID; f.Editable(id) && id != "" {
go func() {
if err := f.editor.EditMessage(id, text); err != nil {
if err := f.editor.Edit(id, text); err != nil {
log.Error(errors.Wrap(err, "Failed to edit message"))
}
}()
@ -87,7 +87,7 @@ func (f *Field) SendMessage(data PresendMessage) {
// Copy the sender to prevent race conditions.
var sender = f.Sender
gts.Async(func() (func(), error) {
if err := sender.SendMessage(data); err != nil {
if err := sender.Send(data); err != nil {
return func() { onErr(err) }, errors.Wrap(err, "Failed to send message")
}
return nil, nil
@ -104,11 +104,13 @@ type SendMessageData struct {
files []attachment.File
}
var _ cchat.SendableMessage = (*SendMessageData)(nil)
type PresendMessage interface {
cchat.MessageHeader // returns nonce and time
cchat.SendableMessage
cchat.MessageNonce
cchat.SendableMessageAttachments
cchat.Noncer
cchat.Attachments
// These methods are reserved for internal use.
@ -145,6 +147,10 @@ func (s SendMessageData) AuthorAvatarURL() string {
return s.authorURL
}
func (s SendMessageData) AsNoncer() cchat.Noncer {
return s
}
func (s SendMessageData) Nonce() string {
return s.nonce
}
@ -153,6 +159,10 @@ func (s SendMessageData) Files() []attachment.File {
return s.files
}
func (s SendMessageData) AsAttachments() cchat.Attachments {
return s
}
func (s SendMessageData) Attachments() []cchat.MessageAttachment {
var attachments = make([]cchat.MessageAttachment, len(s.files))
for i, file := range s.files {

View File

@ -82,7 +82,7 @@ func (u *Container) SetRevealChild(reveal bool) {
// shouldReveal returns whether or not the container should reveal.
func (u *Container) shouldReveal() bool {
return (!u.label.GetLabel().Empty() || u.avatar.URL() != "") && showUser
return (!u.label.GetLabel().IsEmpty() || u.avatar.URL() != "") && showUser
}
func (u *Container) Reset() {
@ -92,19 +92,19 @@ func (u *Container) Reset() {
}
// Update is not thread-safe.
func (u *Container) Update(session cchat.Session, sender cchat.ServerMessageSender) {
func (u *Container) Update(session cchat.Session, messenger cchat.Messenger) {
// Set the fallback username.
u.label.SetLabelUnsafe(session.Name())
// Reveal the name if it's not empty.
u.SetRevealChild(true)
// Does sender (aka Server) implement ServerNickname? If yes, use it.
if nicknamer, ok := sender.(cchat.ServerNickname); ok {
// Does messenger implement Nicknamer? If yes, use it.
if nicknamer := messenger.AsNicknamer(); nicknamer != nil {
u.label.AsyncSetLabel(nicknamer.Nickname, "Error fetching server nickname")
}
// Does session implement an icon? Update if yes.
if iconer, ok := session.(cchat.Icon); ok {
if iconer := session.AsIconer(); iconer != nil {
u.avatar.AsyncSetIconer(iconer, "Error fetching session icon URL")
}
}

View File

@ -93,9 +93,9 @@ func (c *Container) Reset() {
// TryAsyncList tries to set the member list from the given server. It does type
// assertions and handles asynchronicity. Reset must be called before this.
func (c *Container) TryAsyncList(server cchat.ServerMessage) {
ls, ok := server.(cchat.ServerMessageMemberLister)
if !ok {
func (c *Container) TryAsyncList(server cchat.Messenger) {
ls := server.AsMemberLister()
if ls == nil {
return
}
@ -109,7 +109,7 @@ func (c *Container) TryAsyncList(server cchat.ServerMessage) {
})
}
func (c *Container) SetSections(sections []cchat.MemberListSection) {
func (c *Container) SetSections(sections []cchat.MemberSection) {
gts.ExecAsync(func() { c.SetSectionsUnsafe(sections) })
}
@ -121,7 +121,7 @@ func (c *Container) RemoveMember(sectionID string, id string) {
gts.ExecAsync(func() { c.RemoveMemberUnsafe(sectionID, id) })
}
func (c *Container) SetSectionsUnsafe(sections []cchat.MemberListSection) {
func (c *Container) SetSectionsUnsafe(sections []cchat.MemberSection) {
var newSections = make([]*Section, len(sections))
for i, section := range sections {
@ -191,7 +191,7 @@ var sectionBodyCSS = primitives.PrepareClassCSS("section-body", `
}
`)
func NewSection(sect cchat.MemberListSection) *Section {
func NewSection(sect cchat.MemberSection) *Section {
header := rich.NewLabel(text.Rich{})
header.Show()
sectionHeaderCSS(header)
@ -359,7 +359,7 @@ func NewMember(member cchat.ListMember) *Member {
func (m *Member) Update(member cchat.ListMember) {
m.ListBoxRow.SetName(member.Name().Content)
if iconer, ok := member.(cchat.Icon); ok {
if iconer := member.AsIconer(); iconer != nil {
m.Avatar.AsyncSetIconer(iconer, "Failed to get member list icon")
}
@ -370,7 +370,7 @@ func (m *Member) Update(member cchat.ListMember) {
statusColors(member.Status()), m.output.Markup,
))
if bot := member.Secondary(); !bot.Empty() {
if bot := member.Secondary(); !bot.IsEmpty() {
txt.WriteByte('\n')
txt.WriteString(fmt.Sprintf(
`<span alpha="85%%"><sup>%s</sup></span>`,
@ -392,15 +392,15 @@ func (m *Member) Popup() {
}
}
func statusColors(status cchat.UserStatus) uint32 {
func statusColors(status cchat.Status) uint32 {
switch status {
case cchat.OnlineStatus:
case cchat.StatusOnline:
return 0x43B581
case cchat.BusyStatus:
case cchat.StatusBusy:
return 0xF04747
case cchat.IdleStatus:
case cchat.StatusIdle:
return 0xFAA61A
case cchat.OfflineStatus:
case cchat.StatusOffline:
fallthrough
default:
return 0x747F8D

View File

@ -21,7 +21,7 @@ type Container interface {
AvatarURL() string // avatar
Nonce() string
UpdateAuthor(cchat.MessageAuthor)
UpdateAuthor(cchat.Author)
UpdateAuthorName(text.Rich)
UpdateContent(c text.Rich, edited bool)
UpdateTimestamp(time.Time)
@ -79,12 +79,9 @@ func NewContainer(msg cchat.MessageCreate) *GenericContainer {
c := NewEmptyContainer()
c.id = msg.ID()
c.time = msg.Time()
c.nonce = msg.Nonce()
c.authorID = msg.Author().ID()
if noncer, ok := msg.(cchat.MessageNonce); ok {
c.nonce = noncer.Nonce()
}
return c
}
@ -180,14 +177,10 @@ func (m *GenericContainer) UpdateTimestamp(t time.Time) {
m.Timestamp.SetTooltipText(t.Format(time.Stamp))
}
func (m *GenericContainer) UpdateAuthor(author cchat.MessageAuthor) {
func (m *GenericContainer) UpdateAuthor(author cchat.Author) {
m.authorID = author.ID()
m.avatarURL = author.Avatar()
m.UpdateAuthorName(author.Name())
// Set the avatar URL for future access on-demand.
if avatarer, ok := author.(cchat.MessageAuthorAvatar); ok {
m.avatarURL = avatarer.Avatar()
}
}
func (m *GenericContainer) UpdateAuthorName(name text.Rich) {

View File

@ -21,7 +21,7 @@ type State struct {
stopper func() // stops the event loop, not used atm
}
var _ cchat.TypingIndicator = (*State)(nil)
var _ cchat.TypingContainer = (*State)(nil)
func NewState(changed func(s *State, empty bool)) *State {
s := &State{changed: changed}
@ -41,7 +41,7 @@ func (s *State) reset() {
}
// Subscribe is thread-safe.
func (s *State) Subscribe(indicator cchat.ServerMessageTypingIndicator) {
func (s *State) Subscribe(indicator cchat.TypingIndicator) {
gts.Async(func() (func(), error) {
c, err := indicator.TypingSubscribe(s)
if err != nil {

View File

@ -71,17 +71,17 @@ func (c *Container) Reset() {
c.SetRevealChild(false)
}
func (c *Container) RemoveAuthor(author cchat.MessageAuthor) {
func (c *Container) RemoveAuthor(author cchat.Author) {
c.state.removeTyper(author.ID())
}
func (c *Container) TrySubscribe(svmsg cchat.ServerMessage) bool {
ti, ok := svmsg.(cchat.ServerMessageTypingIndicator)
if !ok {
func (c *Container) TrySubscribe(svmsg cchat.Messenger) bool {
var tindicator = svmsg.AsTypingIndicator()
if tindicator == nil {
return false
}
c.state.Subscribe(ti)
c.state.Subscribe(tindicator)
return true
}

View File

@ -260,7 +260,7 @@ func (v *View) MemberListUpdated(c *memberlist.Container) {
}
// JoinServer is not thread-safe, but it calls backend functions asynchronously.
func (v *View) JoinServer(session cchat.Session, server ServerMessage, bc traverse.Breadcrumber) {
func (v *View) JoinServer(session cchat.Session, server cchat.Server, bc traverse.Breadcrumber) {
// Reset before setting.
v.Reset()
@ -268,21 +268,25 @@ func (v *View) JoinServer(session cchat.Session, server ServerMessage, bc traver
v.FaceView.SetLoading()
v.ctrl.OnMessageBusy()
// Bind the state.
v.state.bind(session, server)
// Get the messenger once.
var messenger = server.AsMessenger()
// Exit if this server is not a messenger.
if messenger == nil {
return
}
// Bind the state.
v.state.bind(session, server, messenger)
// Skipping ok check because sender can be nil. Without the empty
// check, Go will panic.
sender, _ := server.(cchat.ServerMessageSender)
// We're setting this variable before actually calling JoinServer. This is
// because new messages created by JoinServer will use this state for things
// such as determinining if it's deletable or not.
v.InputView.SetSender(session, sender)
v.InputView.SetMessenger(session, messenger)
gts.Async(func() (func(), error) {
// We can use a background context here, as the user can't go anywhere
// that would require cancellation anyway. This is done in ui.go.
s, err := server.JoinServer(context.Background(), v.Container)
s, err := messenger.JoinServer(context.Background(), v.Container)
if err != nil {
err = errors.Wrap(err, "Failed to join server")
// Even if we're erroring out, we're running the done() callback
@ -304,10 +308,10 @@ func (v *View) JoinServer(session cchat.Session, server ServerMessage, bc traver
v.Header.SetBreadcrumber(bc)
// Try setting the typing indicator if available.
v.Typing.TrySubscribe(server)
v.Typing.TrySubscribe(messenger)
// Try and use the list.
v.MemberList.TryAsyncList(server)
v.MemberList.TryAsyncList(messenger)
}, nil
})
}
@ -338,7 +342,7 @@ func (v *View) FetchBacklog() {
ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second)
defer cancel()
err := backlogger.MessagesBefore(ctx, firstMsg.ID(), v.Container)
err := backlogger.Backlog(ctx, firstMsg.ID(), v.Container)
return done, errors.Wrap(err, "Failed to get messages before ID")
})
}
@ -361,7 +365,7 @@ func (v *View) AddPresendMessage(msg input.PresendMessage) func(error) {
}
// AuthorEvent should be called on message create/update/delete.
func (v *View) AuthorEvent(author cchat.MessageAuthor) {
func (v *View) AuthorEvent(author cchat.Author) {
// Remove the author from the typing list if it's not nil.
if author != nil {
v.Typing.RemoveAuthor(author)
@ -381,7 +385,7 @@ func (v *View) retryMessage(msg input.PresendMessage, presend container.PresendG
}
go func() {
if err := sender.SendMessage(msg); err != nil {
if err := sender.Send(msg); err != nil {
// Set the message's state to errored again, but we don't need to
// rebind the menu.
gts.ExecAsync(func() { presend.SetSentError(err) })
@ -404,7 +408,7 @@ func (v *View) BindMenu(msg container.GridMessage) {
// Do we have any custom actions? If yes, append it.
if v.hasActions() {
var actions = v.actioner.MessageActions(msg.ID())
var actions = v.actioner.Actions(msg.ID())
var items = make([]menu.Item, len(actions))
for i, action := range actions {
@ -424,7 +428,7 @@ func (v *View) makeActionItem(action, msgID string) menu.Item {
go func() {
// Run, get the error, and try to log it. The logger will ignore nil
// errors.
err := v.state.actioner.DoMessageAction(action, msgID)
err := v.state.actioner.Do(action, msgID)
log.Error(errors.Wrap(err, "Failed to do action "+action))
}()
})
@ -433,15 +437,15 @@ func (v *View) makeActionItem(action, msgID string) menu.Item {
// ServerMessage combines Server and ServerMessage from cchat.
type ServerMessage interface {
cchat.Server
cchat.ServerMessage
cchat.Messenger
}
type state struct {
session cchat.Session
server cchat.Server
actioner cchat.ServerMessageActioner
backlogger cchat.ServerMessageBacklogger
actioner cchat.Actioner
backlogger cchat.Backlogger
current func() // stop callback
author string
@ -483,7 +487,7 @@ const backloggingFreq = time.Second * 3
// Backlogger returns the backlogger instance if it's allowed to fetch more
// backlogs.
func (s *state) Backlogger() cchat.ServerMessageBacklogger {
func (s *state) Backlogger() cchat.Backlogger {
if s.backlogger == nil || s.current == nil {
return nil
}
@ -498,11 +502,11 @@ func (s *state) Backlogger() cchat.ServerMessageBacklogger {
return s.backlogger
}
func (s *state) bind(session cchat.Session, server ServerMessage) {
func (s *state) bind(session cchat.Session, server cchat.Server, msgr cchat.Messenger) {
s.session = session
s.server = server
s.actioner, _ = server.(cchat.ServerMessageActioner)
s.backlogger, _ = server.(cchat.ServerMessageBacklogger)
s.actioner = msgr.AsActioner()
s.backlogger = msgr.AsBacklogger()
}
func (s *state) setcurrent(fn func()) {

View File

@ -1,34 +1,51 @@
package completion
import (
"fmt"
"log"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/cchat/utils/split"
"github.com/diamondburned/imgutil"
"github.com/gotk3/gotk3/gtk"
)
type Completeable interface {
Update([]string, int) []gtk.IWidget
Word(i int) string
}
const (
ImageSmall = 25
ImageLarge = 40
ImagePadding = 6
)
// post-processor icon
var ppIcon = []imgutil.Processor{imgutil.Round(true)}
type Completer struct {
ctrl Completeable
Input *gtk.TextView
Buffer *gtk.TextBuffer
List *gtk.ListBox
Popover *gtk.Popover
popdown bool
Words []string
Index int
Cursor int
Splitter split.SplitFunc
words []string
index int64
cursor int64
entries []cchat.CompletionEntry
completer cchat.Completer
}
func WrapCompleter(input *gtk.TextView, ctrl Completeable) {
NewCompleter(input, ctrl)
func WrapCompleter(input *gtk.TextView) {
NewCompleter(input)
}
func NewCompleter(input *gtk.TextView, ctrl Completeable) *Completer {
func NewCompleter(input *gtk.TextView) *Completer {
l, _ := gtk.ListBoxNew()
l.Show()
@ -43,11 +60,11 @@ func NewCompleter(input *gtk.TextView, ctrl Completeable) *Completer {
ibuf, _ := input.GetBuffer()
c := &Completer{
Input: input,
Buffer: ibuf,
List: l,
Popover: p,
ctrl: ctrl,
Input: input,
Buffer: ibuf,
List: l,
Popover: p,
Splitter: split.SpaceIndexed,
}
// This one is for buffer modification.
@ -56,17 +73,40 @@ func NewCompleter(input *gtk.TextView, ctrl Completeable) *Completer {
input.Connect("move-cursor", c.onChange)
l.Connect("row-activated", func(l *gtk.ListBox, r *gtk.ListBoxRow) {
SwapWord(ibuf, ctrl.Word(r.GetIndex()), c.Cursor)
SwapWord(ibuf, c.entries[r.GetIndex()].Raw, c.cursor)
c.onChange() // signal change
c.Clear()
c.Hide()
c.Popdown()
input.GrabFocus()
})
return c
}
func (c *Completer) Hide() {
c.Popover.Popdown()
// SetCompleter sets the current completer. If completer is nil, then the
// completer is disabled.
func (c *Completer) SetCompleter(completer cchat.Completer) {
c.Clear()
c.Popdown()
c.completer = completer
}
func (c *Completer) Reset() {
c.SetCompleter(nil)
}
func (c *Completer) Popup() {
if c.popdown {
c.Popover.Popup()
c.popdown = false
}
}
func (c *Completer) Popdown() {
if !c.popdown {
c.Popover.Popdown()
c.popdown = true
}
}
func (c *Completer) Clear() {
@ -82,19 +122,36 @@ func (c *Completer) Clear() {
})
}
// Words returns the buffer content split into words.
func (c *Completer) Content() []string {
// This method not to be confused with c.words, which contains the state of
// completer words.
text, _ := c.Buffer.GetText(c.Buffer.GetStartIter(), c.Buffer.GetEndIter(), true)
if text == "" {
return nil
}
words, _ := c.Splitter(text, 0)
return words
}
func (c *Completer) onChange() {
t, v, blank := State(c.Buffer)
c.Cursor = v
c.cursor = v
// If the curssor is on a blank character, then we should not
log.Println("STATE:", t, v, blank)
// If the cursor is on a blank character, then we should not
// autocomplete anything, so we set the states to nil.
if blank {
c.Words = nil
c.Index = -1
} else {
c.Words, c.Index = split.SpaceIndexed(t, v)
c.words = nil
c.index = -1
log.Println("RESET INDEX TO -1")
return
}
c.words, c.index = c.Splitter(t, v)
log.Println("INDEX:", c.index)
c.complete()
}
@ -102,15 +159,15 @@ func (c *Completer) complete() {
c.Clear()
var widgets []gtk.IWidget
if len(c.Words) > 0 {
widgets = c.ctrl.Update(c.Words, c.Index)
if len(c.words) > 0 {
widgets = c.update()
}
if len(widgets) > 0 {
c.Popover.SetPointingTo(CursorRect(c.Input))
c.Popover.Popup()
c.Popup()
} else {
c.Hide()
c.Popdown()
return
}
@ -126,3 +183,67 @@ func (c *Completer) complete() {
}
}
}
func (c *Completer) update() []gtk.IWidget {
// If we don't have a completer, then don't run.
if c.completer == nil {
return nil
}
c.entries = c.completer.Complete(c.words, c.index)
var widgets = make([]gtk.IWidget, len(c.entries))
for i, entry := range c.entries {
// Container that holds the label.
lbox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
lbox.SetVAlign(gtk.ALIGN_CENTER)
lbox.Show()
// Label for the primary text.
l := rich.NewLabel(entry.Text)
l.Show()
lbox.PackStart(l, false, false, 0)
// Get the iamge size so we can change and use if needed. The default
var size = ImageSmall
if !entry.Secondary.IsEmpty() {
size = ImageLarge
s := rich.NewLabel(text.Rich{})
s.SetMarkup(fmt.Sprintf(
`<span alpha="50%%" size="small">%s</span>`,
markup.Render(entry.Secondary),
))
s.Show()
lbox.PackStart(s, false, false, 0)
}
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
b.PackEnd(lbox, true, true, ImagePadding)
b.Show()
// Do we have an icon?
if entry.IconURL != "" {
img, _ := gtk.ImageNew()
img.SetMarginStart(ImagePadding)
img.SetSizeRequest(size, size)
img.Show()
// Prepend the image into the box.
b.PackEnd(img, false, false, 0)
var pps []imgutil.Processor
if !entry.Image {
pps = ppIcon
}
httputil.AsyncImageSized(img, entry.IconURL, size, size, pps...)
}
widgets[i] = b
}
return widgets
}

View File

@ -78,7 +78,7 @@ func KeyDownHandler(l *gtk.ListBox, focus func()) KeyDownHandlerFn {
}
}
func SwapWord(b *gtk.TextBuffer, word string, offset int) {
func SwapWord(b *gtk.TextBuffer, word string, offset int64) {
// Get iter for word replacing.
start, end := GetWordIters(b, offset)
b.Delete(start, end)
@ -93,7 +93,7 @@ func CursorRect(i *gtk.TextView) gdk.Rectangle {
return *r
}
func State(buf *gtk.TextBuffer) (text string, offset int, blank bool) {
func State(buf *gtk.TextBuffer) (text string, offset int64, blank bool) {
// obtain current state
mark := buf.GetInsert()
iter := buf.GetIterAtMark(mark)
@ -102,7 +102,7 @@ func State(buf *gtk.TextBuffer) (text string, offset int, blank bool) {
start, end := buf.GetBounds()
text, _ = buf.GetText(start, end, true)
offset = iter.GetOffset()
offset = int64(iter.GetOffset())
// We need the rune before the cursor.
iter.BackwardChar()
@ -118,8 +118,8 @@ const searchFlags = 0 |
gtk.TEXT_SEARCH_TEXT_ONLY |
gtk.TEXT_SEARCH_VISIBLE_ONLY
func GetWordIters(buf *gtk.TextBuffer, offset int) (start, end *gtk.TextIter) {
iter := buf.GetIterAtOffset(offset)
func GetWordIters(buf *gtk.TextBuffer, offset int64) (start, end *gtk.TextIter) {
iter := buf.GetIterAtOffset(int(offset))
var ok bool

View File

@ -134,7 +134,7 @@ func (i *Icon) SetIcon(url string) {
gts.ExecAsync(func() { i.SetIconUnsafe(url) })
}
func (i *Icon) AsyncSetIconer(iconer cchat.Icon, errwrap string) {
func (i *Icon) AsyncSetIconer(iconer cchat.Iconer, errwrap string) {
// Reveal to show the placeholder.
i.SetRevealChild(true)

View File

@ -90,8 +90,8 @@ func BindRichLabel(label Labeler) {
bind(label, func(uri string, ptr gdk.Rectangle) bool {
var output = label.Output()
if mention := output.IsMention(uri); mention != nil {
if p := NewPopoverMentioner(label, output.Input, mention); p != nil {
if segment := output.IsMention(uri); segment != nil {
if p := NewPopoverMentioner(label, output.Input, segment); p != nil {
p.SetPointingTo(ptr)
p.Popup()
}
@ -103,19 +103,24 @@ func BindRichLabel(label Labeler) {
})
}
func PopoverMentioner(rel gtk.IWidget, input string, mention text.Mentioner) {
func PopoverMentioner(rel gtk.IWidget, input string, mention text.Segment) {
if p := NewPopoverMentioner(rel, input, mention); p != nil {
p.Popup()
}
}
func NewPopoverMentioner(rel gtk.IWidget, input string, mention text.Mentioner) *gtk.Popover {
var info = mention.MentionInfo()
if info.Empty() {
func NewPopoverMentioner(rel gtk.IWidget, input string, segment text.Segment) *gtk.Popover {
var mention = segment.AsMentioner()
if mention == nil {
return nil
}
start, end := mention.Bounds()
var info = mention.MentionInfo()
if info.IsEmpty() {
return nil
}
start, end := segment.Bounds()
h := input[start:end]
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
@ -125,12 +130,11 @@ func NewPopoverMentioner(rel gtk.IWidget, input string, mention text.Mentioner)
var url string
var round bool
switch v := mention.(type) {
case text.MentionerImage:
url = v.Image()
case text.MentionerAvatar: