diff --git a/go.mod b/go.mod index 4f5fb92..51aba19 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 69ab61e..f259946 100644 --- a/go.sum +++ b/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= 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= diff --git a/pkg/hicli/database/event.go b/pkg/hicli/database/event.go index f0935d7..8a5b09b 100644 --- a/pkg/hicli/database/event.go +++ b/pkg/hicli/database/event.go @@ -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 { diff --git a/pkg/hicli/send.go b/pkg/hicli/send.go index 28fc252..daedbde 100644 --- a/pkg/hicli/send.go +++ b/pkg/hicli/send.go @@ -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) diff --git a/pkg/hicli/sync.go b/pkg/hicli/sync.go index 5bc593f..e02071f 100644 --- a/pkg/hicli/sync.go +++ b/pkg/hicli/sync.go @@ -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 } diff --git a/web/src/api/types/hitypes.ts b/web/src/api/types/hitypes.ts index fab9136..1bce825 100644 --- a/web/src/api/types/hitypes.ts +++ b/web/src/api/types/hitypes.ts @@ -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 diff --git a/web/src/ui/composer/MessageComposer.tsx b/web/src/ui/composer/MessageComposer.tsx index c408b64..0a27b3e 100644 --- a/web/src/ui/composer/MessageComposer.tsx +++ b/web/src/ui/composer/MessageComposer.tsx @@ -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() diff --git a/web/src/util/emoji/index.ts b/web/src/util/emoji/index.ts index 048e924..94abedf 100644 --- a/web/src/util/emoji/index.ts +++ b/web/src/util/emoji/index.ts @@ -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 `:${emoji.n}:` + const escapedTitle = title.replaceAll(`\\`, `\\\\`).replaceAll(`"`, `\\"`) + return `![:${emoji.n}:](${emoji.u} "Emoji: ${escapedTitle}")` + //return `:${emoji.n}:` } return emoji.u }