mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-03-23 02:19:20 +00:00
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
|
||||
|
||||
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 (
|
||||
github.com/Xuanwo/go-locale v0.2.0
|
||||
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/imgutil v0.0.0-20200611215339-650ac7cfaf64
|
||||
github.com/goodsign/monday v1.0.0
|
||||
|
@ -18,5 +18,7 @@ require (
|
|||
github.com/markbates/pkger v0.17.0
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible
|
||||
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
|
||||
)
|
||||
|
|
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.31 h1:yUgrh5xbGX0R55glyxYtVewIDL2eXLJ+okIEfVaVoFk=
|
||||
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-20200619080941-dac771656ebf/go.mod h1:hTzJBvDRH984m9cOjH+pfQtcZOOVZLKsDxGwlg5HRtw=
|
||||
github.com/diamondburned/cchat-discord v0.0.0-20200619222738-e5babcbb42e3 h1:8RCcaY3gtA+8NG2mwkcC/PIFK+eS8XnGyeVaUbCXbF0=
|
||||
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/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-20200612012846-9df87fea4f6d/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||
github.com/diamondburned/gotk3 v0.0.0-20200619213419-0533bcce0dd6 h1:ZzLrfQqszhzWI7zqwltzQIWtppfcL7m2aIEpB4kuqx0=
|
||||
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/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
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/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
|
|
|
@ -50,7 +50,7 @@ func NewContainer(ctrl container.Controller) *Container {
|
|||
c := &Container{}
|
||||
c.GridContainer = container.NewGridContainer(c, ctrl)
|
||||
// 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")
|
||||
return c
|
||||
|
|
|
@ -17,7 +17,7 @@ import (
|
|||
)
|
||||
|
||||
// TopFullMargin is the margin on top of every full message.
|
||||
const TopFullMargin = 12
|
||||
const TopFullMargin = 8
|
||||
|
||||
type FullMessage struct {
|
||||
*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 {
|
||||
// Remove GenericContainer's widgets from the containers.
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package input
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
@ -10,16 +12,25 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/twmb/murmur3"
|
||||
)
|
||||
|
||||
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 {
|
||||
return fmt.Sprintf(
|
||||
raw := fmt.Sprintf(
|
||||
"cchat-gtk/%s/%X/%X",
|
||||
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() {
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
const AvatarSize = 20
|
||||
const AvatarSize = 24
|
||||
|
||||
type usernameContainer struct {
|
||||
*gtk.Revealer
|
||||
|
|
|
@ -53,7 +53,8 @@ type GenericContainer struct {
|
|||
|
||||
Timestamp *gtk.Label
|
||||
Username *gtk.Label
|
||||
Content *gtk.Label
|
||||
Content *gtk.TextView
|
||||
CBuffer *gtk.TextBuffer
|
||||
|
||||
MenuItems []menu.Item
|
||||
}
|
||||
|
@ -92,15 +93,15 @@ func NewEmptyContainer() *GenericContainer {
|
|||
user.SetSelectable(true)
|
||||
user.Show()
|
||||
|
||||
content, _ := gtk.LabelNew("")
|
||||
content, _ := gtk.TextViewNew()
|
||||
content.SetHExpand(true)
|
||||
content.SetXAlign(0) // left-align with size filled
|
||||
content.SetVAlign(gtk.ALIGN_START)
|
||||
content.SetLineWrap(true)
|
||||
content.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
||||
content.SetSelectable(true)
|
||||
content.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
||||
content.SetCursorVisible(false)
|
||||
content.SetEditable(false)
|
||||
content.Show()
|
||||
|
||||
cbuffer, _ := content.GetBuffer()
|
||||
|
||||
// Add CSS classes.
|
||||
primitives.AddClass(ts, "message-time")
|
||||
primitives.AddClass(user, "message-author")
|
||||
|
@ -110,6 +111,7 @@ func NewEmptyContainer() *GenericContainer {
|
|||
Timestamp: ts,
|
||||
Username: user,
|
||||
Content: content,
|
||||
CBuffer: cbuffer,
|
||||
}
|
||||
|
||||
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) {
|
||||
var markup = parser.RenderMarkup(content)
|
||||
if edited {
|
||||
markup += " " + rich.Small("(edited)")
|
||||
}
|
||||
// Render the content.
|
||||
parser.RenderTextBuffer(m.CBuffer, content)
|
||||
|
||||
m.Content.SetMarkup(markup)
|
||||
if edited {
|
||||
parser.AppendEditBadge(m.CBuffer, m.Time())
|
||||
}
|
||||
}
|
||||
|
||||
// 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.SetSensitive(true)
|
||||
m.sendString = ""
|
||||
m.Content.SetTooltipText("")
|
||||
}
|
||||
|
||||
func (m *GenericPresendContainer) SetLoading() {
|
||||
m.SetSensitive(false)
|
||||
m.Content.SetText(m.sendString)
|
||||
m.CBuffer.SetText(m.sendString)
|
||||
m.Content.SetTooltipText("")
|
||||
}
|
||||
|
||||
func (m *GenericPresendContainer) SetSentError(err error) {
|
||||
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())
|
||||
}
|
||||
|
|
161
internal/ui/rich/parser/markup.go
Normal file
161
internal/ui/rich/parser/markup.go
Normal file
|
@ -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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"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/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 {
|
||||
appended map[int]string
|
||||
indices []int
|
||||
func AppendEditBadge(b *gtk.TextBuffer, editedAt time.Time) {
|
||||
r := newRenderCtx(b)
|
||||
|
||||
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 {
|
||||
return attrAppendMap{
|
||||
appended: make(map[int]string),
|
||||
indices: []int{},
|
||||
}
|
||||
}
|
||||
func RenderTextBuffer(b *gtk.TextBuffer, content text.Rich) {
|
||||
r := newRenderCtx(b)
|
||||
b.SetText(content.Content)
|
||||
|
||||
func (a *attrAppendMap) span(start, end int, attr string) {
|
||||
a.add(start, `<span `+attr+`>`)
|
||||
a.add(end, "</span>")
|
||||
}
|
||||
// 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
|
||||
})
|
||||
|
||||
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.Attributor:
|
||||
r.tagAttr(start, end, segment.Attribute())
|
||||
|
||||
case text.Colorer:
|
||||
appended.span(start, end, fmt.Sprintf(`color="#%06X"`, segment.Color()))
|
||||
|
||||
case text.Attributor:
|
||||
appended.span(start, end, markupAttr(segment.Attribute()))
|
||||
color := fmt.Sprintf("#%06X", segment.Color())
|
||||
r.applyProps(start, end, map[string]interface{}{
|
||||
"foreground": color,
|
||||
})
|
||||
|
||||
case text.Codeblocker:
|
||||
// Treat codeblocks the same as a monospace tag.
|
||||
// TODO: add highlighting
|
||||
appended.span(start, end, `font_family="monospace"`)
|
||||
r.applyProps(start, end, map[string]interface{}{
|
||||
"family": "Monospace",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case text.Quoteblocker:
|
||||
// TODO: pls.
|
||||
appended.span(start, end, `color="#789922"`)
|
||||
type renderCtx struct {
|
||||
b *gtk.TextBuffer
|
||||
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)) {
|
||||
// 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
|
||||
func (r *renderCtx) iters(start, end int) (is, ie *gtk.TextIter) {
|
||||
return r.b.GetIterAtOffset(start), r.b.GetIterAtOffset(end)
|
||||
}
|
||||
|
||||
func (r *renderCtx) tagAttr(start, end int, attr text.Attribute) {
|
||||
var props = tagAttrMap(attr)
|
||||
if props == nil {
|
||||
return
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
r.applyTag(start, end, r.createTag(props))
|
||||
}
|
||||
|
||||
func span(key, value string) string {
|
||||
return "<span key=\"" + value + "\">"
|
||||
func tagAttrMap(attr text.Attribute) map[string]interface{} {
|
||||
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 IconSize = 18
|
||||
const IconSize = 20
|
||||
|
||||
type Controller interface {
|
||||
RowSelected(*ServerRow, cchat.ServerMessage)
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
headerbar { padding: 0; }
|
||||
|
||||
.services button { border-radius: 0; }
|
||||
|
||||
.message-content, .message-content text {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue