@@ -16,7 +16,7 @@ limitations under the License.
16
16
17
17
import React , { ClipboardEvent , createRef , KeyboardEvent } from "react" ;
18
18
import EMOJI_REGEX from "emojibase-regex" ;
19
- import { IContent , MatrixEvent , IEventRelation } from "matrix-js-sdk/src/models/event" ;
19
+ import { IContent , MatrixEvent , IEventRelation , IMentions } from "matrix-js-sdk/src/models/event" ;
20
20
import { DebouncedFunc , throttle } from "lodash" ;
21
21
import { EventType , RelationType } from "matrix-js-sdk/src/@types/event" ;
22
22
import { logger } from "matrix-js-sdk/src/logger" ;
@@ -36,7 +36,7 @@ import {
36
36
unescapeMessage ,
37
37
} from "../../../editor/serialize" ;
38
38
import BasicMessageComposer , { REGEX_EMOTICON } from "./BasicMessageComposer" ;
39
- import { CommandPartCreator , Part , PartCreator , SerializedPart } from "../../../editor/parts" ;
39
+ import { CommandPartCreator , Part , PartCreator , SerializedPart , Type } from "../../../editor/parts" ;
40
40
import { findEditableEvent } from "../../../utils/EventUtils" ;
41
41
import SendHistoryManager from "../../../SendHistoryManager" ;
42
42
import { CommandCategories } from "../../../SlashCommands" ;
@@ -60,6 +60,102 @@ import { PosthogAnalytics } from "../../../PosthogAnalytics";
60
60
import { addReplyToMessageContent } from "../../../utils/Reply" ;
61
61
import { doMaybeLocalRoomAction } from "../../../utils/local-room" ;
62
62
63
+ /**
64
+ * Build the mentions information based on the editor model (and any related events):
65
+ *
66
+ * 1. Search the model parts for room or user pills and fill in the mentions object.
67
+ * 2. If this is a reply to another event, include any user mentions from that
68
+ * (but do not include a room mention).
69
+ *
70
+ * @param sender - The Matrix ID of the user sending the event.
71
+ * @param content - The event content.
72
+ * @param model - The editor model to search for mentions, null if there is no editor.
73
+ * @param replyToEvent - The event being replied to or undefined if it is not a reply.
74
+ * @param editedContent - The content of the parent event being edited.
75
+ */
76
+ export function attachMentions (
77
+ sender : string ,
78
+ content : IContent ,
79
+ model : EditorModel | null ,
80
+ replyToEvent : MatrixEvent | undefined ,
81
+ editedContent : IContent | null = null ,
82
+ ) : void {
83
+ // If this feature is disabled, do nothing.
84
+ if ( ! SettingsStore . getValue ( "feature_intentional_mentions" ) ) {
85
+ return ;
86
+ }
87
+
88
+ // The mentions property *always* gets included to disable legacy push rules.
89
+ const mentions : IMentions = ( content [ "org.matrix.msc3952.mentions" ] = { } ) ;
90
+
91
+ const userMentions = new Set < string > ( ) ;
92
+ let roomMention = false ;
93
+
94
+ // If there's a reply, initialize the mentioned users as the sender of that
95
+ // event + any mentioned users in that event.
96
+ if ( replyToEvent ) {
97
+ userMentions . add ( replyToEvent . sender ! . userId ) ;
98
+ // TODO What do we do if the reply event *doeesn't* have this property?
99
+ // Try to fish out replies from the contents?
100
+ const userIds = replyToEvent . getContent ( ) [ "org.matrix.msc3952.mentions" ] ?. user_ids ;
101
+ if ( Array . isArray ( userIds ) ) {
102
+ userIds . forEach ( ( userId ) => userMentions . add ( userId ) ) ;
103
+ }
104
+ }
105
+
106
+ // If user provided content is available, check to see if any users are mentioned.
107
+ if ( model ) {
108
+ // Add any mentioned users in the current content.
109
+ for ( const part of model . parts ) {
110
+ if ( part . type === Type . UserPill ) {
111
+ userMentions . add ( part . resourceId ) ;
112
+ } else if ( part . type === Type . AtRoomPill ) {
113
+ roomMention = true ;
114
+ }
115
+ }
116
+ }
117
+
118
+ // Ensure the *current* user isn't listed in the mentioned users.
119
+ userMentions . delete ( sender ) ;
120
+
121
+ // Finally, if this event is editing a previous event, only include users who
122
+ // were not previously mentioned and a room mention if the previous event was
123
+ // not a room mention.
124
+ if ( editedContent ) {
125
+ // First, the new event content gets the *full* set of users.
126
+ const newContent = content [ "m.new_content" ] ;
127
+ const newMentions : IMentions = ( newContent [ "org.matrix.msc3952.mentions" ] = { } ) ;
128
+
129
+ // Only include the users/room if there is any content.
130
+ if ( userMentions . size ) {
131
+ newMentions . user_ids = [ ...userMentions ] ;
132
+ }
133
+ if ( roomMention ) {
134
+ newMentions . room = true ;
135
+ }
136
+
137
+ // Fetch the mentions from the original event and remove any previously
138
+ // mentioned users.
139
+ const prevMentions = editedContent [ "org.matrix.msc3952.mentions" ] ;
140
+ if ( Array . isArray ( prevMentions ?. user_ids ) ) {
141
+ prevMentions ! . user_ids . forEach ( ( userId ) => userMentions . delete ( userId ) ) ;
142
+ }
143
+
144
+ // If the original event mentioned the room, nothing to do here.
145
+ if ( prevMentions ?. room ) {
146
+ roomMention = false ;
147
+ }
148
+ }
149
+
150
+ // Only include the users/room if there is any content.
151
+ if ( userMentions . size ) {
152
+ mentions . user_ids = [ ...userMentions ] ;
153
+ }
154
+ if ( roomMention ) {
155
+ mentions . room = true ;
156
+ }
157
+ }
158
+
63
159
// Merges favouring the given relation
64
160
export function attachRelation ( content : IContent , relation ?: IEventRelation ) : void {
65
161
if ( relation ) {
@@ -72,6 +168,7 @@ export function attachRelation(content: IContent, relation?: IEventRelation): vo
72
168
73
169
// exported for tests
74
170
export function createMessageContent (
171
+ sender : string ,
75
172
model : EditorModel ,
76
173
replyToEvent : MatrixEvent | undefined ,
77
174
relation : IEventRelation | undefined ,
@@ -102,6 +199,9 @@ export function createMessageContent(
102
199
content . formatted_body = formattedBody ;
103
200
}
104
201
202
+ // Build the mentions property and add it to the event content.
203
+ attachMentions ( sender , content , model , replyToEvent ) ;
204
+
105
205
attachRelation ( content , relation ) ;
106
206
if ( replyToEvent ) {
107
207
addReplyToMessageContent ( content , replyToEvent , {
@@ -381,6 +481,8 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
381
481
}
382
482
383
483
if ( cmd . category === CommandCategories . messages || cmd . category === CommandCategories . effects ) {
484
+ // Attach any mentions which might be contained in the command content.
485
+ attachMentions ( this . props . mxClient . getSafeUserId ( ) , content , model , replyToEvent ) ;
384
486
attachRelation ( content , this . props . relation ) ;
385
487
if ( replyToEvent ) {
386
488
addReplyToMessageContent ( content , replyToEvent , {
@@ -413,6 +515,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
413
515
const { roomId } = this . props . room ;
414
516
if ( ! content ) {
415
517
content = createMessageContent (
518
+ this . props . mxClient . getSafeUserId ( ) ,
416
519
model ,
417
520
replyToEvent ,
418
521
this . props . relation ,
0 commit comments