mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
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:
parent
2ff3f9120c
commit
5832a935cf
8 changed files with 75 additions and 10 deletions
2
go.mod
2
go.mod
|
@ -24,7 +24,7 @@ require (
|
||||||
golang.org/x/text v0.19.0
|
golang.org/x/text v0.19.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
maunium.net/go/mauflag v1.0.0
|
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
|
mvdan.cc/xurls/v2 v2.5.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -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=
|
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 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
||||||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
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.20241102114451-83e60efa1558 h1:GWrBpixB+7hm2vk5Z6JvMoUgAea6ma8Mg4QaO53csYs=
|
||||||
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/go.mod h1:b91JuKxF/JkOf0ra/YmhV44Sa/EtOjxkLJANzBJ1frg=
|
||||||
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
||||||
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
||||||
|
|
|
@ -293,6 +293,7 @@ type LocalContent struct {
|
||||||
WasPlaintext bool `json:"was_plaintext,omitempty"`
|
WasPlaintext bool `json:"was_plaintext,omitempty"`
|
||||||
BigEmoji bool `json:"big_emoji,omitempty"`
|
BigEmoji bool `json:"big_emoji,omitempty"`
|
||||||
HasMath bool `json:"has_math,omitempty"`
|
HasMath bool `json:"has_math,omitempty"`
|
||||||
|
EditSource string `json:"edit_source,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Event struct {
|
type Event struct {
|
||||||
|
|
|
@ -30,10 +30,38 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
rainbowWithHTML = goldmark.New(format.Extensions, goldmark.WithExtensions(mdext.Math), format.HTMLOptions, goldmark.WithExtensions(rainbow.Extension))
|
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), format.HTMLOptions)
|
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(``, alt, src, title)
|
||||||
|
} else if title != "" {
|
||||||
|
return fmt.Sprintf(``, alt, src, title)
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf(``, alt, src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *HiClient) SendMessage(
|
func (h *HiClient) SendMessage(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
roomID id.RoomID,
|
roomID id.RoomID,
|
||||||
|
@ -44,6 +72,7 @@ func (h *HiClient) SendMessage(
|
||||||
) (*database.Event, error) {
|
) (*database.Event, error) {
|
||||||
var content event.MessageEventContent
|
var content event.MessageEventContent
|
||||||
msgType := event.MsgText
|
msgType := event.MsgText
|
||||||
|
origText := text
|
||||||
if strings.HasPrefix(text, "/me ") {
|
if strings.HasPrefix(text, "/me ") {
|
||||||
msgType = event.MsgEmote
|
msgType = event.MsgEmote
|
||||||
text = strings.TrimPrefix(text, "/me ")
|
text = strings.TrimPrefix(text, "/me ")
|
||||||
|
@ -102,7 +131,7 @@ func (h *HiClient) SendMessage(
|
||||||
content.RelatesTo = relatesTo
|
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 {
|
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,
|
roomID id.RoomID,
|
||||||
evtType event.Type,
|
evtType event.Type,
|
||||||
content any,
|
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) {
|
) (*database.Event, error) {
|
||||||
room, err := h.DB.Room.Get(ctx, roomID)
|
room, err := h.DB.Room.Get(ctx, roomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -193,6 +232,9 @@ func (h *HiClient) Send(
|
||||||
var inlineImages []id.ContentURI
|
var inlineImages []id.ContentURI
|
||||||
mautrixEvt := dbEvt.AsRawMautrix()
|
mautrixEvt := dbEvt.AsRawMautrix()
|
||||||
dbEvt.LocalContent, inlineImages = h.calculateLocalContent(ctx, dbEvt, mautrixEvt)
|
dbEvt.LocalContent, inlineImages = h.calculateLocalContent(ctx, dbEvt, mautrixEvt)
|
||||||
|
if overrideEditSource != "" {
|
||||||
|
dbEvt.LocalContent.EditSource = overrideEditSource
|
||||||
|
}
|
||||||
_, err = h.DB.Event.Insert(ctx, dbEvt)
|
_, err = h.DB.Event.Insert(ctx, dbEvt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to insert event into database: %w", err)
|
return nil, fmt.Errorf("failed to insert event into database: %w", err)
|
||||||
|
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"maunium.net/go/mautrix/crypto"
|
"maunium.net/go/mautrix/crypto"
|
||||||
"maunium.net/go/mautrix/crypto/olm"
|
"maunium.net/go/mautrix/crypto/olm"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
|
"maunium.net/go/mautrix/format"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
"maunium.net/go/mautrix/pushrules"
|
"maunium.net/go/mautrix/pushrules"
|
||||||
|
|
||||||
|
@ -366,7 +367,7 @@ func (h *HiClient) calculateLocalContent(ctx context.Context, dbEvt *database.Ev
|
||||||
content = content.NewContent
|
content = content.NewContent
|
||||||
}
|
}
|
||||||
if content != nil {
|
if content != nil {
|
||||||
var sanitizedHTML string
|
var sanitizedHTML, editSource string
|
||||||
var wasPlaintext, hasMath, bigEmoji bool
|
var wasPlaintext, hasMath, bigEmoji bool
|
||||||
var inlineImages []id.ContentURI
|
var inlineImages []id.ContentURI
|
||||||
if content.Format == event.FormatHTML && content.FormattedBody != "" {
|
if content.Format == event.FormatHTML && content.FormattedBody != "" {
|
||||||
|
@ -384,6 +385,16 @@ func (h *HiClient) calculateLocalContent(ctx context.Context, dbEvt *database.Ev
|
||||||
}
|
}
|
||||||
inlineImages = nil
|
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 {
|
} else {
|
||||||
hasSpecialCharacters := false
|
hasSpecialCharacters := false
|
||||||
for _, char := range content.Body {
|
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) {
|
} else if len(content.Body) < 100 && emojirunes.IsOnlyEmojis(content.Body) {
|
||||||
bigEmoji = true
|
bigEmoji = true
|
||||||
}
|
}
|
||||||
|
if content.MsgType == event.MsgEmote {
|
||||||
|
editSource = "/me " + content.Body
|
||||||
|
} else if content.MsgType == event.MsgNotice {
|
||||||
|
editSource = "/notice " + content.Body
|
||||||
|
}
|
||||||
wasPlaintext = true
|
wasPlaintext = true
|
||||||
}
|
}
|
||||||
return &database.LocalContent{
|
return &database.LocalContent{
|
||||||
|
@ -408,6 +424,7 @@ func (h *HiClient) calculateLocalContent(ctx context.Context, dbEvt *database.Ev
|
||||||
WasPlaintext: wasPlaintext,
|
WasPlaintext: wasPlaintext,
|
||||||
BigEmoji: bigEmoji,
|
BigEmoji: bigEmoji,
|
||||||
HasMath: hasMath,
|
HasMath: hasMath,
|
||||||
|
EditSource: editSource,
|
||||||
}, inlineImages
|
}, inlineImages
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -597,7 +614,7 @@ func (h *HiClient) processStateAndTimeline(
|
||||||
}
|
}
|
||||||
processNewEvent := func(evt *event.Event, isTimeline, isUnread bool) (database.EventRowID, error) {
|
processNewEvent := func(evt *event.Event, isTimeline, isUnread bool) (database.EventRowID, error) {
|
||||||
evt.RoomID = room.ID
|
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 {
|
if err != nil {
|
||||||
return -1, err
|
return -1, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,7 @@ export enum UnreadType {
|
||||||
|
|
||||||
export interface LocalContent {
|
export interface LocalContent {
|
||||||
sanitized_html?: TrustedHTML
|
sanitized_html?: TrustedHTML
|
||||||
|
edit_source?: string
|
||||||
html_version?: number
|
html_version?: number
|
||||||
was_plaintext?: boolean
|
was_plaintext?: boolean
|
||||||
big_emoji?: boolean
|
big_emoji?: boolean
|
||||||
|
|
|
@ -107,7 +107,9 @@ const MessageComposer = () => {
|
||||||
rawSetEditing(evt)
|
rawSetEditing(evt)
|
||||||
setState({
|
setState({
|
||||||
media: isMedia ? evtContent as MediaMessageEventContent : null,
|
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,
|
replyTo: null,
|
||||||
})
|
})
|
||||||
textInput.current?.focus()
|
textInput.current?.focus()
|
||||||
|
|
|
@ -105,7 +105,9 @@ function filterAndSort(
|
||||||
export function emojiToMarkdown(emoji: PartialEmoji): string {
|
export function emojiToMarkdown(emoji: PartialEmoji): string {
|
||||||
if (emoji.u.startsWith("mxc://")) {
|
if (emoji.u.startsWith("mxc://")) {
|
||||||
const title = emoji.t && emoji.t !== emoji.n ? emoji.t : `:${emoji.n}:`
|
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 ``
|
||||||
|
//return `<img data-mx-emoticon height="32" src="${emoji.u}" alt=":${emoji.n}:" title="${title}"/>`
|
||||||
}
|
}
|
||||||
return emoji.u
|
return emoji.u
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue