Fixed nonce bug; improved message content

This commit is contained in:
diamondburned (Forefront) 2020-06-19 15:40:06 -07:00
parent cedc39d8d0
commit 1a9cc2626e
13 changed files with 352 additions and 158 deletions

6
go.mod
View File

@ -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
View File

@ -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=

View File

@ -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

View File

@ -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.

View File

@ -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() {

View File

@ -9,7 +9,7 @@ import (
"github.com/gotk3/gotk3/gtk"
)
const AvatarSize = 20
const AvatarSize = 24
type usernameContainer struct {
*gtk.Revealer

View File

@ -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

View File

@ -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())
}

View 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 + "\">"
}

View File

@ -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
}

View File

@ -16,7 +16,7 @@ import (
)
const ChildrenMargin = 24
const IconSize = 18
const IconSize = 20
type Controller interface {
RowSelected(*ServerRow, cchat.ServerMessage)

View File

@ -1,3 +1,9 @@
headerbar { padding: 0; }
.services button { border-radius: 0; }
.message-content, .message-content text {
background: none;
box-shadow: none;
border: none;
}

File diff suppressed because one or more lines are too long