Fixed nonce bug; improved message content
This commit is contained in:
parent
cedc39d8d0
commit
1a9cc2626e
6
go.mod
6
go.mod
|
@ -2,12 +2,12 @@ module github.com/diamondburned/cchat-gtk
|
||||||
|
|
||||||
go 1.14
|
go 1.14
|
||||||
|
|
||||||
replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200612012846-9df87fea4f6d
|
replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200619213419-0533bcce0dd6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Xuanwo/go-locale v0.2.0
|
github.com/Xuanwo/go-locale v0.2.0
|
||||||
github.com/diamondburned/cchat v0.0.31
|
github.com/diamondburned/cchat v0.0.31
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20200619080941-dac771656ebf
|
github.com/diamondburned/cchat-discord v0.0.0-20200619222738-e5babcbb42e3
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20200615015702-8cac8b16378d
|
github.com/diamondburned/cchat-mock v0.0.0-20200615015702-8cac8b16378d
|
||||||
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64
|
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64
|
||||||
github.com/goodsign/monday v1.0.0
|
github.com/goodsign/monday v1.0.0
|
||||||
|
@ -18,5 +18,7 @@ require (
|
||||||
github.com/markbates/pkger v0.17.0
|
github.com/markbates/pkger v0.17.0
|
||||||
github.com/peterbourgon/diskv v2.0.1+incompatible
|
github.com/peterbourgon/diskv v2.0.1+incompatible
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
|
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
|
github.com/zalando/go-keyring v0.0.0-20200121091418-667557018717
|
||||||
)
|
)
|
||||||
|
|
10
go.sum
10
go.sum
|
@ -13,12 +13,12 @@ github.com/diamondburned/cchat v0.0.28 h1:+1VnltW0rl8/NZTUP+x89jVhi3YTTR+e6iLprZ
|
||||||
github.com/diamondburned/cchat v0.0.28/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
github.com/diamondburned/cchat v0.0.28/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||||
github.com/diamondburned/cchat v0.0.31 h1:yUgrh5xbGX0R55glyxYtVewIDL2eXLJ+okIEfVaVoFk=
|
github.com/diamondburned/cchat v0.0.31 h1:yUgrh5xbGX0R55glyxYtVewIDL2eXLJ+okIEfVaVoFk=
|
||||||
github.com/diamondburned/cchat v0.0.31/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
github.com/diamondburned/cchat v0.0.31/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20200619080941-dac771656ebf h1:61qusonLp8kVZGxZc6joldPnp90dfLQvywWr5mh6i3U=
|
github.com/diamondburned/cchat-discord v0.0.0-20200619222738-e5babcbb42e3 h1:8RCcaY3gtA+8NG2mwkcC/PIFK+eS8XnGyeVaUbCXbF0=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20200619080941-dac771656ebf/go.mod h1:hTzJBvDRH984m9cOjH+pfQtcZOOVZLKsDxGwlg5HRtw=
|
github.com/diamondburned/cchat-discord v0.0.0-20200619222738-e5babcbb42e3/go.mod h1:4q0jHEl1gJEzkS92oacwcSf9+3fFcNPukOpURDJpV/A=
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20200615015702-8cac8b16378d h1:LkzARyvdGRvAsaKEPTV3XcqMHENH6J+KRAI+3sq41Qs=
|
github.com/diamondburned/cchat-mock v0.0.0-20200615015702-8cac8b16378d h1:LkzARyvdGRvAsaKEPTV3XcqMHENH6J+KRAI+3sq41Qs=
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20200615015702-8cac8b16378d/go.mod h1:SVTt5je4G+re8aSVJAFk/x8vvbRzXdpKgSKmVGoM1tg=
|
github.com/diamondburned/cchat-mock v0.0.0-20200615015702-8cac8b16378d/go.mod h1:SVTt5je4G+re8aSVJAFk/x8vvbRzXdpKgSKmVGoM1tg=
|
||||||
github.com/diamondburned/gotk3 v0.0.0-20200612012846-9df87fea4f6d h1:NFTuwBU+CNZDB1iaGC3gDuBRf9FTd1h2WnIh6NF7elg=
|
github.com/diamondburned/gotk3 v0.0.0-20200619213419-0533bcce0dd6 h1:ZzLrfQqszhzWI7zqwltzQIWtppfcL7m2aIEpB4kuqx0=
|
||||||
github.com/diamondburned/gotk3 v0.0.0-20200612012846-9df87fea4f6d/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
github.com/diamondburned/gotk3 v0.0.0-20200619213419-0533bcce0dd6/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||||
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64 h1:/ykUYHuYyj+NN/aaqe6lfaCZQc3EMZs93wAGVJTh5j0=
|
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64 h1:/ykUYHuYyj+NN/aaqe6lfaCZQc3EMZs93wAGVJTh5j0=
|
||||||
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
|
github.com/diamondburned/imgutil v0.0.0-20200611215339-650ac7cfaf64/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
|
||||||
github.com/diamondburned/ningen v0.1.0 h1:cTnRNrN0g2Wr/kgjLLpa3pqlbEd6JPNa1yGDer8uV4U=
|
github.com/diamondburned/ningen v0.1.0 h1:cTnRNrN0g2Wr/kgjLLpa3pqlbEd6JPNa1yGDer8uV4U=
|
||||||
|
@ -62,6 +62,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||||
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
|
|
|
@ -50,7 +50,7 @@ func NewContainer(ctrl container.Controller) *Container {
|
||||||
c := &Container{}
|
c := &Container{}
|
||||||
c.GridContainer = container.NewGridContainer(c, ctrl)
|
c.GridContainer = container.NewGridContainer(c, ctrl)
|
||||||
// A not-so-generous row padding, as we will rely on margins per widget.
|
// A not-so-generous row padding, as we will rely on margins per widget.
|
||||||
c.GridContainer.Grid.SetRowSpacing(2)
|
c.GridContainer.Grid.SetRowSpacing(0)
|
||||||
|
|
||||||
primitives.AddClass(c, "cozy-container")
|
primitives.AddClass(c, "cozy-container")
|
||||||
return c
|
return c
|
||||||
|
|
|
@ -17,7 +17,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// TopFullMargin is the margin on top of every full message.
|
// TopFullMargin is the margin on top of every full message.
|
||||||
const TopFullMargin = 12
|
const TopFullMargin = 8
|
||||||
|
|
||||||
type FullMessage struct {
|
type FullMessage struct {
|
||||||
*message.GenericContainer
|
*message.GenericContainer
|
||||||
|
@ -87,7 +87,7 @@ func WrapFullMessage(gc *message.GenericContainer) *FullMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *FullMessage) Collapsed() bool { return false }
|
func (m *FullMessage) Collapsed() bool { return false }
|
||||||
|
|
||||||
func (m *FullMessage) Unwrap(grid *gtk.Grid) *message.GenericContainer {
|
func (m *FullMessage) Unwrap(grid *gtk.Grid) *message.GenericContainer {
|
||||||
// Remove GenericContainer's widgets from the containers.
|
// Remove GenericContainer's widgets from the containers.
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package input
|
package input
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
@ -10,16 +12,25 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/twmb/murmur3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var globalID uint64
|
var globalID uint64
|
||||||
|
|
||||||
// generateNonce creates a nonce that should prevent collision
|
// generateNonce creates a nonce that should prevent collision. This function
|
||||||
|
// will always return a 24-byte long string.
|
||||||
func (f *Field) generateNonce() string {
|
func (f *Field) generateNonce() string {
|
||||||
return fmt.Sprintf(
|
raw := fmt.Sprintf(
|
||||||
"cchat-gtk/%s/%X/%X",
|
"cchat-gtk/%s/%X/%X",
|
||||||
f.UserID, time.Now().UnixNano(), atomic.AddUint64(&globalID, 1),
|
f.UserID, time.Now().UnixNano(), atomic.AddUint64(&globalID, 1),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
h1, h2 := murmur3.StringSum128(raw)
|
||||||
|
nonce := make([]byte, 8*2)
|
||||||
|
binary.LittleEndian.PutUint64(nonce[0:8], h1)
|
||||||
|
binary.LittleEndian.PutUint64(nonce[8:16], h2)
|
||||||
|
|
||||||
|
return base64.RawURLEncoding.EncodeToString(nonce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Field) sendInput() {
|
func (f *Field) sendInput() {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
)
|
)
|
||||||
|
|
||||||
const AvatarSize = 20
|
const AvatarSize = 24
|
||||||
|
|
||||||
type usernameContainer struct {
|
type usernameContainer struct {
|
||||||
*gtk.Revealer
|
*gtk.Revealer
|
||||||
|
|
|
@ -53,7 +53,8 @@ type GenericContainer struct {
|
||||||
|
|
||||||
Timestamp *gtk.Label
|
Timestamp *gtk.Label
|
||||||
Username *gtk.Label
|
Username *gtk.Label
|
||||||
Content *gtk.Label
|
Content *gtk.TextView
|
||||||
|
CBuffer *gtk.TextBuffer
|
||||||
|
|
||||||
MenuItems []menu.Item
|
MenuItems []menu.Item
|
||||||
}
|
}
|
||||||
|
@ -92,15 +93,15 @@ func NewEmptyContainer() *GenericContainer {
|
||||||
user.SetSelectable(true)
|
user.SetSelectable(true)
|
||||||
user.Show()
|
user.Show()
|
||||||
|
|
||||||
content, _ := gtk.LabelNew("")
|
content, _ := gtk.TextViewNew()
|
||||||
content.SetHExpand(true)
|
content.SetHExpand(true)
|
||||||
content.SetXAlign(0) // left-align with size filled
|
content.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
||||||
content.SetVAlign(gtk.ALIGN_START)
|
content.SetCursorVisible(false)
|
||||||
content.SetLineWrap(true)
|
content.SetEditable(false)
|
||||||
content.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
|
||||||
content.SetSelectable(true)
|
|
||||||
content.Show()
|
content.Show()
|
||||||
|
|
||||||
|
cbuffer, _ := content.GetBuffer()
|
||||||
|
|
||||||
// Add CSS classes.
|
// Add CSS classes.
|
||||||
primitives.AddClass(ts, "message-time")
|
primitives.AddClass(ts, "message-time")
|
||||||
primitives.AddClass(user, "message-author")
|
primitives.AddClass(user, "message-author")
|
||||||
|
@ -110,6 +111,7 @@ func NewEmptyContainer() *GenericContainer {
|
||||||
Timestamp: ts,
|
Timestamp: ts,
|
||||||
Username: user,
|
Username: user,
|
||||||
Content: content,
|
Content: content,
|
||||||
|
CBuffer: cbuffer,
|
||||||
}
|
}
|
||||||
|
|
||||||
gc.Content.Connect("populate-popup", func(l *gtk.Label, m *gtk.Menu) {
|
gc.Content.Connect("populate-popup", func(l *gtk.Label, m *gtk.Menu) {
|
||||||
|
@ -166,12 +168,12 @@ func (m *GenericContainer) UpdateAuthorName(name text.Rich) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *GenericContainer) UpdateContent(content text.Rich, edited bool) {
|
func (m *GenericContainer) UpdateContent(content text.Rich, edited bool) {
|
||||||
var markup = parser.RenderMarkup(content)
|
// Render the content.
|
||||||
if edited {
|
parser.RenderTextBuffer(m.CBuffer, content)
|
||||||
markup += " " + rich.Small("(edited)")
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Content.SetMarkup(markup)
|
if edited {
|
||||||
|
parser.AppendEditBadge(m.CBuffer, m.Time())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AttachMenu connects signal handlers to handle a list of menu items from
|
// AttachMenu connects signal handlers to handle a list of menu items from
|
||||||
|
|
|
@ -48,16 +48,17 @@ func (m *GenericPresendContainer) SetDone(id string) {
|
||||||
m.id = id
|
m.id = id
|
||||||
m.SetSensitive(true)
|
m.SetSensitive(true)
|
||||||
m.sendString = ""
|
m.sendString = ""
|
||||||
|
m.Content.SetTooltipText("")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *GenericPresendContainer) SetLoading() {
|
func (m *GenericPresendContainer) SetLoading() {
|
||||||
m.SetSensitive(false)
|
m.SetSensitive(false)
|
||||||
m.Content.SetText(m.sendString)
|
m.CBuffer.SetText(m.sendString)
|
||||||
m.Content.SetTooltipText("")
|
m.Content.SetTooltipText("")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *GenericPresendContainer) SetSentError(err error) {
|
func (m *GenericPresendContainer) SetSentError(err error) {
|
||||||
m.SetSensitive(true) // allow events incl right clicks
|
m.SetSensitive(true) // allow events incl right clicks
|
||||||
m.Content.SetMarkup(`<span color="red">` + html.EscapeString(m.sendString) + `</span>`)
|
m.CBuffer.SetText(`<span color="red">` + html.EscapeString(m.sendString) + `</span>`)
|
||||||
m.Content.SetTooltipText(err.Error())
|
m.Content.SetTooltipText(err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,161 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
type attrAppendMap struct {
|
||||||
|
appended map[int]string
|
||||||
|
indices []int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAttrAppendedMap() attrAppendMap {
|
||||||
|
return attrAppendMap{
|
||||||
|
appended: make(map[int]string),
|
||||||
|
indices: []int{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *attrAppendMap) span(start, end int, attr string) {
|
||||||
|
a.add(start, `<span `+attr+`>`)
|
||||||
|
a.add(end, "</span>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *attrAppendMap) pair(start, end int, open, close string) {
|
||||||
|
a.add(start, open)
|
||||||
|
a.add(end, close)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *attrAppendMap) addf(ind int, f string, argv ...interface{}) {
|
||||||
|
a.add(ind, fmt.Sprintf(f, argv...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *attrAppendMap) pad(ind int) {
|
||||||
|
a.add(ind, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *attrAppendMap) add(ind int, attr string) {
|
||||||
|
if _, ok := a.appended[ind]; ok {
|
||||||
|
a.appended[ind] += attr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.appended[ind] = attr
|
||||||
|
a.indices = append(a.indices, ind)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a attrAppendMap) get(ind int) string {
|
||||||
|
return a.appended[ind]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *attrAppendMap) finalize(strlen int) []int {
|
||||||
|
// make sure there's always a closing tag at the end so the entire string
|
||||||
|
// gets flushed.
|
||||||
|
a.add(strlen, "")
|
||||||
|
sort.Ints(a.indices)
|
||||||
|
return a.indices
|
||||||
|
}
|
||||||
|
|
||||||
|
func markupAttr(attr text.Attribute) string {
|
||||||
|
// meme fast path
|
||||||
|
if attr == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var attrs = make([]string, 0, 1)
|
||||||
|
if attr.Has(text.AttrBold) {
|
||||||
|
attrs = append(attrs, `weight="bold"`)
|
||||||
|
}
|
||||||
|
if attr.Has(text.AttrItalics) {
|
||||||
|
attrs = append(attrs, `style="italic"`)
|
||||||
|
}
|
||||||
|
if attr.Has(text.AttrUnderline) {
|
||||||
|
attrs = append(attrs, `underline="single"`)
|
||||||
|
}
|
||||||
|
if attr.Has(text.AttrStrikethrough) {
|
||||||
|
attrs = append(attrs, `strikethrough="true"`)
|
||||||
|
}
|
||||||
|
if attr.Has(text.AttrSpoiler) {
|
||||||
|
attrs = append(attrs, `foreground="#808080"`) // no fancy click here
|
||||||
|
}
|
||||||
|
if attr.Has(text.AttrMonospace) {
|
||||||
|
attrs = append(attrs, `font_family="monospace"`)
|
||||||
|
}
|
||||||
|
return strings.Join(attrs, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderMarkup(content text.Rich) string {
|
||||||
|
// Fast path.
|
||||||
|
if len(content.Segments) == 0 {
|
||||||
|
return html.EscapeString(content.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
buf.Grow(len(content.Content))
|
||||||
|
|
||||||
|
// // Sort so that all starting points are sorted incrementally.
|
||||||
|
// sort.Slice(content.Segments, func(i, j int) bool {
|
||||||
|
// i, _ = content.Segments[i].Bounds()
|
||||||
|
// j, _ = content.Segments[j].Bounds()
|
||||||
|
// return i < j
|
||||||
|
// })
|
||||||
|
|
||||||
|
// map to append strings to indices
|
||||||
|
var appended = newAttrAppendedMap()
|
||||||
|
|
||||||
|
// Parse all segments.
|
||||||
|
for _, segment := range content.Segments {
|
||||||
|
start, end := segment.Bounds()
|
||||||
|
|
||||||
|
switch segment := segment.(type) {
|
||||||
|
case text.Linker:
|
||||||
|
appended.addf(start, `<a href="%s">`, html.EscapeString(segment.Link()))
|
||||||
|
appended.add(end, "</a>")
|
||||||
|
|
||||||
|
case text.Imager:
|
||||||
|
// Ends don't matter with images.
|
||||||
|
appended.addf(start,
|
||||||
|
`<a href="%s">%s</a>`,
|
||||||
|
html.EscapeString(segment.Image()), html.EscapeString(segment.ImageText()),
|
||||||
|
)
|
||||||
|
|
||||||
|
case text.Colorer:
|
||||||
|
appended.span(start, end, fmt.Sprintf(`color="#%06X"`, segment.Color()))
|
||||||
|
|
||||||
|
case text.Attributor:
|
||||||
|
appended.span(start, end, markupAttr(segment.Attribute()))
|
||||||
|
|
||||||
|
case text.Codeblocker:
|
||||||
|
// Treat codeblocks the same as a monospace tag.
|
||||||
|
// TODO: add highlighting
|
||||||
|
appended.span(start, end, `font_family="monospace"`)
|
||||||
|
|
||||||
|
case text.Quoteblocker:
|
||||||
|
// TODO: pls.
|
||||||
|
appended.span(start, end, `color="#789922"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastIndex = 0
|
||||||
|
|
||||||
|
for _, index := range appended.finalize(len(content.Content)) {
|
||||||
|
// Write the content.
|
||||||
|
buf.WriteString(html.EscapeString(content.Content[lastIndex:index]))
|
||||||
|
// Write the tags.
|
||||||
|
buf.WriteString(appended.get(index))
|
||||||
|
// Set the last index.
|
||||||
|
lastIndex = index
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func span(key, value string) string {
|
||||||
|
return "<span key=\"" + value + "\">"
|
||||||
|
}
|
|
@ -1,161 +1,170 @@
|
||||||
package parser
|
package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"time"
|
||||||
|
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
"github.com/gotk3/gotk3/pango"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/skratchdot/open-golang/open"
|
||||||
)
|
)
|
||||||
|
|
||||||
type attrAppendMap struct {
|
func AppendEditBadge(b *gtk.TextBuffer, editedAt time.Time) {
|
||||||
appended map[int]string
|
r := newRenderCtx(b)
|
||||||
indices []int
|
|
||||||
|
t := r.createTag(map[string]interface{}{
|
||||||
|
"scale": 0.84,
|
||||||
|
"scale-set": true,
|
||||||
|
"foreground": "#808080", // blue-ish URL color
|
||||||
|
})
|
||||||
|
|
||||||
|
bindClicker(t, func(_ *gtk.TextView, ev *gdk.Event) {
|
||||||
|
switch ev := gdk.EventMotionNewFromEvent(ev); ev.Type() {
|
||||||
|
case gdk.EVENT_PROXIMITY_IN:
|
||||||
|
log.Println("Proximity in")
|
||||||
|
case gdk.EVENT_PROXIMITY_OUT:
|
||||||
|
log.Println("Proximity out")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.InsertWithTag(b.GetEndIter(), " (edited)", t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAttrAppendedMap() attrAppendMap {
|
func RenderTextBuffer(b *gtk.TextBuffer, content text.Rich) {
|
||||||
return attrAppendMap{
|
r := newRenderCtx(b)
|
||||||
appended: make(map[int]string),
|
b.SetText(content.Content)
|
||||||
indices: []int{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *attrAppendMap) span(start, end int, attr string) {
|
// Sort so that all starting points are sorted incrementally.
|
||||||
a.add(start, `<span `+attr+`>`)
|
sort.Slice(content.Segments, func(i, j int) bool {
|
||||||
a.add(end, "</span>")
|
i, _ = content.Segments[i].Bounds()
|
||||||
}
|
j, _ = content.Segments[j].Bounds()
|
||||||
|
return i < j
|
||||||
|
})
|
||||||
|
|
||||||
func (a *attrAppendMap) pair(start, end int, open, close string) {
|
|
||||||
a.add(start, open)
|
|
||||||
a.add(end, close)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *attrAppendMap) addf(ind int, f string, argv ...interface{}) {
|
|
||||||
a.add(ind, fmt.Sprintf(f, argv...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *attrAppendMap) pad(ind int) {
|
|
||||||
a.add(ind, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *attrAppendMap) add(ind int, attr string) {
|
|
||||||
if _, ok := a.appended[ind]; ok {
|
|
||||||
a.appended[ind] += attr
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appended[ind] = attr
|
|
||||||
a.indices = append(a.indices, ind)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a attrAppendMap) get(ind int) string {
|
|
||||||
return a.appended[ind]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *attrAppendMap) finalize(strlen int) []int {
|
|
||||||
// make sure there's always a closing tag at the end so the entire string
|
|
||||||
// gets flushed.
|
|
||||||
a.add(strlen, "")
|
|
||||||
sort.Ints(a.indices)
|
|
||||||
return a.indices
|
|
||||||
}
|
|
||||||
|
|
||||||
func markupAttr(attr text.Attribute) string {
|
|
||||||
// meme fast path
|
|
||||||
if attr == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var attrs = make([]string, 0, 1)
|
|
||||||
if attr.Has(text.AttrBold) {
|
|
||||||
attrs = append(attrs, `weight="bold"`)
|
|
||||||
}
|
|
||||||
if attr.Has(text.AttrItalics) {
|
|
||||||
attrs = append(attrs, `style="italic"`)
|
|
||||||
}
|
|
||||||
if attr.Has(text.AttrUnderline) {
|
|
||||||
attrs = append(attrs, `underline="single"`)
|
|
||||||
}
|
|
||||||
if attr.Has(text.AttrStrikethrough) {
|
|
||||||
attrs = append(attrs, `strikethrough="true"`)
|
|
||||||
}
|
|
||||||
if attr.Has(text.AttrSpoiler) {
|
|
||||||
attrs = append(attrs, `foreground="#808080"`) // no fancy click here
|
|
||||||
}
|
|
||||||
if attr.Has(text.AttrMonospace) {
|
|
||||||
attrs = append(attrs, `font_family="monospace"`)
|
|
||||||
}
|
|
||||||
return strings.Join(attrs, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func RenderMarkup(content text.Rich) string {
|
|
||||||
// Fast path.
|
|
||||||
if len(content.Segments) == 0 {
|
|
||||||
return html.EscapeString(content.Content)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
|
||||||
buf.Grow(len(content.Content))
|
|
||||||
|
|
||||||
// // Sort so that all starting points are sorted incrementally.
|
|
||||||
// sort.Slice(content.Segments, func(i, j int) bool {
|
|
||||||
// i, _ = content.Segments[i].Bounds()
|
|
||||||
// j, _ = content.Segments[j].Bounds()
|
|
||||||
// return i < j
|
|
||||||
// })
|
|
||||||
|
|
||||||
// map to append strings to indices
|
|
||||||
var appended = newAttrAppendedMap()
|
|
||||||
|
|
||||||
// Parse all segments.
|
|
||||||
for _, segment := range content.Segments {
|
for _, segment := range content.Segments {
|
||||||
start, end := segment.Bounds()
|
start, end := segment.Bounds()
|
||||||
|
|
||||||
switch segment := segment.(type) {
|
switch segment := segment.(type) {
|
||||||
case text.Linker:
|
case text.Attributor:
|
||||||
appended.addf(start, `<a href="%s">`, html.EscapeString(segment.Link()))
|
r.tagAttr(start, end, segment.Attribute())
|
||||||
appended.add(end, "</a>")
|
|
||||||
|
|
||||||
case text.Imager:
|
|
||||||
// Ends don't matter with images.
|
|
||||||
appended.addf(start,
|
|
||||||
`<a href="%s">%s</a>`,
|
|
||||||
html.EscapeString(segment.Image()), html.EscapeString(segment.ImageText()),
|
|
||||||
)
|
|
||||||
|
|
||||||
case text.Colorer:
|
case text.Colorer:
|
||||||
appended.span(start, end, fmt.Sprintf(`color="#%06X"`, segment.Color()))
|
color := fmt.Sprintf("#%06X", segment.Color())
|
||||||
|
r.applyProps(start, end, map[string]interface{}{
|
||||||
case text.Attributor:
|
"foreground": color,
|
||||||
appended.span(start, end, markupAttr(segment.Attribute()))
|
})
|
||||||
|
|
||||||
case text.Codeblocker:
|
case text.Codeblocker:
|
||||||
// Treat codeblocks the same as a monospace tag.
|
r.applyProps(start, end, map[string]interface{}{
|
||||||
// TODO: add highlighting
|
"family": "Monospace",
|
||||||
appended.span(start, end, `font_family="monospace"`)
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case text.Quoteblocker:
|
type renderCtx struct {
|
||||||
// TODO: pls.
|
b *gtk.TextBuffer
|
||||||
appended.span(start, end, `color="#789922"`)
|
t *gtk.TextTagTable
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRenderCtx(b *gtk.TextBuffer) *renderCtx {
|
||||||
|
t, _ := b.GetTagTable()
|
||||||
|
return &renderCtx{b, t}
|
||||||
|
}
|
||||||
|
|
||||||
|
type OnClicker func(tv *gtk.TextView, ev *gdk.Event)
|
||||||
|
|
||||||
|
func bindClicker(v primitives.Connector, fn OnClicker) {
|
||||||
|
v.Connect("event", func(_ *gtk.TextTag, tv *gtk.TextView, ev *gdk.Event) {
|
||||||
|
evButton := gdk.EventButtonNewFromEvent(ev)
|
||||||
|
if evButton.Type() != gdk.EVENT_BUTTON_RELEASE || evButton.Button() != gdk.BUTTON_PRIMARY {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fn(tv, ev)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *renderCtx) applyHyperlink(start, end int, url string) {
|
||||||
|
t := r.createTag(map[string]interface{}{
|
||||||
|
"underline": pango.UNDERLINE_SINGLE,
|
||||||
|
"foreground": "#3F7CE0", // blue-ish URL color
|
||||||
|
})
|
||||||
|
|
||||||
|
bindClicker(t, func(*gtk.TextView, *gdk.Event) {
|
||||||
|
if err := open.Start(url); err != nil {
|
||||||
|
log.Error(errors.Wrap(err, "Failed to open image URL"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *renderCtx) applyProps(start, end int, props map[string]interface{}) {
|
||||||
|
tag := r.createTag(props)
|
||||||
|
r.applyTag(start, end, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *renderCtx) applyTag(start, end int, tag *gtk.TextTag) {
|
||||||
|
istart, iend := r.iters(start, end)
|
||||||
|
r.b.ApplyTag(tag, istart, iend)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *renderCtx) createTag(props map[string]interface{}) *gtk.TextTag {
|
||||||
|
t, _ := gtk.TextTagNew("")
|
||||||
|
r.t.Add(t)
|
||||||
|
|
||||||
|
if props != nil {
|
||||||
|
for k, v := range props {
|
||||||
|
t.SetProperty(k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastIndex = 0
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
for _, index := range appended.finalize(len(content.Content)) {
|
func (r *renderCtx) iters(start, end int) (is, ie *gtk.TextIter) {
|
||||||
// Write the content.
|
return r.b.GetIterAtOffset(start), r.b.GetIterAtOffset(end)
|
||||||
buf.WriteString(html.EscapeString(content.Content[lastIndex:index]))
|
}
|
||||||
// Write the tags.
|
|
||||||
buf.WriteString(appended.get(index))
|
func (r *renderCtx) tagAttr(start, end int, attr text.Attribute) {
|
||||||
// Set the last index.
|
var props = tagAttrMap(attr)
|
||||||
lastIndex = index
|
if props == nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return buf.String()
|
r.applyTag(start, end, r.createTag(props))
|
||||||
}
|
}
|
||||||
|
|
||||||
func span(key, value string) string {
|
func tagAttrMap(attr text.Attribute) map[string]interface{} {
|
||||||
return "<span key=\"" + value + "\">"
|
if attr == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var props = make(map[string]interface{}, 1)
|
||||||
|
|
||||||
|
if attr.Has(text.AttrBold) {
|
||||||
|
props["weight"] = pango.WEIGHT_BOLD
|
||||||
|
}
|
||||||
|
if attr.Has(text.AttrItalics) {
|
||||||
|
props["style"] = pango.STYLE_ITALIC
|
||||||
|
}
|
||||||
|
if attr.Has(text.AttrUnderline) {
|
||||||
|
props["underline"] = pango.UNDERLINE_SINGLE
|
||||||
|
}
|
||||||
|
if attr.Has(text.AttrStrikethrough) {
|
||||||
|
props["strikethrough"] = true
|
||||||
|
}
|
||||||
|
if attr.Has(text.AttrSpoiler) {
|
||||||
|
props["foreground"] = "#808080"
|
||||||
|
}
|
||||||
|
if attr.Has(text.AttrMonospace) {
|
||||||
|
props["family"] = "Monospace"
|
||||||
|
}
|
||||||
|
|
||||||
|
return props
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const ChildrenMargin = 24
|
const ChildrenMargin = 24
|
||||||
const IconSize = 18
|
const IconSize = 20
|
||||||
|
|
||||||
type Controller interface {
|
type Controller interface {
|
||||||
RowSelected(*ServerRow, cchat.ServerMessage)
|
RowSelected(*ServerRow, cchat.ServerMessage)
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
headerbar { padding: 0; }
|
headerbar { padding: 0; }
|
||||||
|
|
||||||
.services button { border-radius: 0; }
|
.services button { border-radius: 0; }
|
||||||
|
|
||||||
|
.message-content, .message-content text {
|
||||||
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue