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(``, alt, src, title)
+ } else if title != "" {
+ return fmt.Sprintf(``, alt, src, title)
+ } else {
+ return fmt.Sprintf(``, 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 `
`
+ const escapedTitle = title.replaceAll(`\\`, `\\\\`).replaceAll(`"`, `\\"`)
+ return ``
+ //return `
`
}
return emoji.u
}