1
0
Fork 0
forked from Mirrors/gomuks

all: use markdown for custom emojis, improve editing

Edits will now use a different HTML -> markdown converter than what is
used to generate the body. This allows the plaintext body to have a
plain shortcode for custom emojis, while still having the raw data for
edits.

Additionally, for sent events, the raw input is saved locally, which
allows preserving commands and other such things. A future extension
may store the raw input in a custom field in the Matrix event to allow
lossless edits of messages sent from other clients.
This commit is contained in:
Tulir Asokan 2024-11-02 13:52:49 +02:00
parent 2ff3f9120c
commit 5832a935cf
8 changed files with 75 additions and 10 deletions

2
go.mod
View file

@ -24,7 +24,7 @@ require (
golang.org/x/text v0.19.0
gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mauflag v1.0.0
maunium.net/go/mautrix v0.21.2-0.20241102103750-013afd06d341
maunium.net/go/mautrix v0.21.2-0.20241102114451-83e60efa1558
mvdan.cc/xurls/v2 v2.5.0
)

4
go.sum
View file

@ -89,7 +89,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.21.2-0.20241102103750-013afd06d341 h1:5oQl6MlXHTlN+MVxmnJbhaQM1+1Lt/o3w4g8CfovQjM=
maunium.net/go/mautrix v0.21.2-0.20241102103750-013afd06d341/go.mod h1:sjCZR1R/3NET/WjkcXPL6WpAHlWKku9HjRsdOkbM8Qw=
maunium.net/go/mautrix v0.21.2-0.20241102114451-83e60efa1558 h1:GWrBpixB+7hm2vk5Z6JvMoUgAea6ma8Mg4QaO53csYs=
maunium.net/go/mautrix v0.21.2-0.20241102114451-83e60efa1558/go.mod h1:b91JuKxF/JkOf0ra/YmhV44Sa/EtOjxkLJANzBJ1frg=
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=

View file

@ -293,6 +293,7 @@ type LocalContent struct {
WasPlaintext bool `json:"was_plaintext,omitempty"`
BigEmoji bool `json:"big_emoji,omitempty"`
HasMath bool `json:"has_math,omitempty"`
EditSource string `json:"edit_source,omitempty"`
}
type Event struct {

View file

@ -30,10 +30,38 @@ import (
)
var (
rainbowWithHTML = goldmark.New(format.Extensions, goldmark.WithExtensions(mdext.Math), format.HTMLOptions, goldmark.WithExtensions(rainbow.Extension))
defaultWithHTML = goldmark.New(format.Extensions, goldmark.WithExtensions(mdext.Math), format.HTMLOptions)
rainbowWithHTML = goldmark.New(format.Extensions, goldmark.WithExtensions(mdext.Math, mdext.CustomEmoji), format.HTMLOptions, goldmark.WithExtensions(rainbow.Extension))
defaultWithHTML = goldmark.New(format.Extensions, goldmark.WithExtensions(mdext.Math, mdext.CustomEmoji), format.HTMLOptions)
)
var htmlToMarkdownForInput = ptr.Clone(format.MarkdownHTMLParser)
func init() {
htmlToMarkdownForInput.PillConverter = func(displayname, mxid, eventID string, ctx format.Context) string {
switch {
case len(mxid) == 0, mxid[0] == '@':
return fmt.Sprintf("[%s](%s)", displayname, id.UserID(mxid).URI().MatrixToURL())
case len(eventID) > 0:
return fmt.Sprintf("[%s](%s)", displayname, id.RoomID(mxid).EventURI(id.EventID(eventID)).MatrixToURL())
case mxid[0] == '!' && displayname == mxid:
return fmt.Sprintf("[%s](%s)", displayname, id.RoomID(mxid).URI().MatrixToURL())
case mxid[0] == '#':
return fmt.Sprintf("[%s](%s)", displayname, id.RoomAlias(mxid).URI().MatrixToURL())
default:
return htmlToMarkdownForInput.LinkConverter(displayname, "https://matrix.to/#/"+mxid, ctx)
}
}
htmlToMarkdownForInput.ImageConverter = func(src, alt, title, width, height string, isEmoji bool) string {
if isEmoji {
return fmt.Sprintf(`![%s](%s "Emoji: %q")`, alt, src, title)
} else if title != "" {
return fmt.Sprintf(`![%s](%s "%s")`, alt, src, title)
} else {
return fmt.Sprintf(`![%s](%s)`, alt, src)
}
}
}
func (h *HiClient) SendMessage(
ctx context.Context,
roomID id.RoomID,
@ -44,6 +72,7 @@ func (h *HiClient) SendMessage(
) (*database.Event, error) {
var content event.MessageEventContent
msgType := event.MsgText
origText := text
if strings.HasPrefix(text, "/me ") {
msgType = event.MsgEmote
text = strings.TrimPrefix(text, "/me ")
@ -102,7 +131,7 @@ func (h *HiClient) SendMessage(
content.RelatesTo = relatesTo
}
}
return h.Send(ctx, roomID, event.EventMessage, &content)
return h.send(ctx, roomID, event.EventMessage, &content, origText)
}
func (h *HiClient) MarkRead(ctx context.Context, roomID id.RoomID, eventID id.EventID, receiptType event.ReceiptType) error {
@ -153,6 +182,16 @@ func (h *HiClient) Send(
roomID id.RoomID,
evtType event.Type,
content any,
) (*database.Event, error) {
return h.send(ctx, roomID, evtType, content, "")
}
func (h *HiClient) send(
ctx context.Context,
roomID id.RoomID,
evtType event.Type,
content any,
overrideEditSource string,
) (*database.Event, error) {
room, err := h.DB.Room.Get(ctx, roomID)
if err != nil {
@ -193,6 +232,9 @@ func (h *HiClient) Send(
var inlineImages []id.ContentURI
mautrixEvt := dbEvt.AsRawMautrix()
dbEvt.LocalContent, inlineImages = h.calculateLocalContent(ctx, dbEvt, mautrixEvt)
if overrideEditSource != "" {
dbEvt.LocalContent.EditSource = overrideEditSource
}
_, err = h.DB.Event.Insert(ctx, dbEvt)
if err != nil {
return nil, fmt.Errorf("failed to insert event into database: %w", err)

View file

@ -25,6 +25,7 @@ import (
"maunium.net/go/mautrix/crypto"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/pushrules"
@ -366,7 +367,7 @@ func (h *HiClient) calculateLocalContent(ctx context.Context, dbEvt *database.Ev
content = content.NewContent
}
if content != nil {
var sanitizedHTML string
var sanitizedHTML, editSource string
var wasPlaintext, hasMath, bigEmoji bool
var inlineImages []id.ContentURI
if content.Format == event.FormatHTML && content.FormattedBody != "" {
@ -384,6 +385,16 @@ func (h *HiClient) calculateLocalContent(ctx context.Context, dbEvt *database.Ev
}
inlineImages = nil
}
if dbEvt.LocalContent != nil && dbEvt.LocalContent.EditSource != "" {
editSource = dbEvt.LocalContent.EditSource
} else if evt.Sender == h.Account.UserID {
editSource, _ = format.HTMLToMarkdownFull(htmlToMarkdownForInput, content.FormattedBody)
if content.MsgType == event.MsgEmote {
editSource = "/me " + editSource
} else if content.MsgType == event.MsgNotice {
editSource = "/notice " + editSource
}
}
} else {
hasSpecialCharacters := false
for _, char := range content.Body {
@ -400,6 +411,11 @@ func (h *HiClient) calculateLocalContent(ctx context.Context, dbEvt *database.Ev
} else if len(content.Body) < 100 && emojirunes.IsOnlyEmojis(content.Body) {
bigEmoji = true
}
if content.MsgType == event.MsgEmote {
editSource = "/me " + content.Body
} else if content.MsgType == event.MsgNotice {
editSource = "/notice " + content.Body
}
wasPlaintext = true
}
return &database.LocalContent{
@ -408,6 +424,7 @@ func (h *HiClient) calculateLocalContent(ctx context.Context, dbEvt *database.Ev
WasPlaintext: wasPlaintext,
BigEmoji: bigEmoji,
HasMath: hasMath,
EditSource: editSource,
}, inlineImages
}
return nil, nil
@ -597,7 +614,7 @@ func (h *HiClient) processStateAndTimeline(
}
processNewEvent := func(evt *event.Event, isTimeline, isUnread bool) (database.EventRowID, error) {
evt.RoomID = room.ID
dbEvt, err := h.processEvent(ctx, evt, summary, decryptionQueue, false)
dbEvt, err := h.processEvent(ctx, evt, summary, decryptionQueue, evt.Unsigned.TransactionID != "")
if err != nil {
return -1, err
}

View file

@ -81,6 +81,7 @@ export enum UnreadType {
export interface LocalContent {
sanitized_html?: TrustedHTML
edit_source?: string
html_version?: number
was_plaintext?: boolean
big_emoji?: boolean

View file

@ -107,7 +107,9 @@ const MessageComposer = () => {
rawSetEditing(evt)
setState({
media: isMedia ? evtContent as MediaMessageEventContent : null,
text: (!evt.content.filename || evt.content.filename !== evt.content.body) ? (evtContent.body ?? "") : "",
text: (!evt.content.filename || evt.content.filename !== evt.content.body)
? (evt.local_content?.edit_source ?? evtContent.body ?? "")
: "",
replyTo: null,
})
textInput.current?.focus()

View file

@ -105,7 +105,9 @@ function filterAndSort(
export function emojiToMarkdown(emoji: PartialEmoji): string {
if (emoji.u.startsWith("mxc://")) {
const title = emoji.t && emoji.t !== emoji.n ? emoji.t : `:${emoji.n}:`
return `<img data-mx-emoticon height="32" src="${emoji.u}" alt=":${emoji.n}:" title="${title}"/>`
const escapedTitle = title.replaceAll(`\\`, `\\\\`).replaceAll(`"`, `\\"`)
return `![:${emoji.n}:](${emoji.u} "Emoji: ${escapedTitle}")`
//return `<img data-mx-emoticon height="32" src="${emoji.u}" alt=":${emoji.n}:" title="${title}"/>`
}
return emoji.u
}