@@ -1067,6 +1067,22 @@ function App() {
10671067 . filter ( ( user ) => user . handle . toLowerCase ( ) . includes ( composerMentionState . query . toLowerCase ( ) ) || user . name . toLowerCase ( ) . includes ( composerMentionState . query . toLowerCase ( ) ) )
10681068 . slice ( 0 , 5 ) ;
10691069 } , [ data , activeConversation , composerMentionState ] ) ;
1070+ const mentionableUsersByHandle = useMemo ( ( ) => {
1071+ const result = new Map ( ) ;
1072+ if ( ! data ) {
1073+ return result ;
1074+ }
1075+ const allowed = new Set ( activeConversation ?. members ?? [ ] ) ;
1076+ data . users
1077+ . filter ( ( user ) => allowed . has ( user . id ) )
1078+ . forEach ( ( user ) => {
1079+ const handle = String ( user . handle ?? "" ) . trim ( ) . toLowerCase ( ) ;
1080+ if ( handle && ! result . has ( handle ) ) {
1081+ result . set ( handle , user ) ;
1082+ }
1083+ } ) ;
1084+ return result ;
1085+ } , [ data , activeConversation ] ) ;
10701086
10711087 const draftSegments = useMemo (
10721088 ( ) => draftsByConversationId [ activeConversationId ] ?? [ ] ,
@@ -2856,7 +2872,13 @@ function App() {
28562872 onKeyUp=${ syncComposerFromEditor }
28572873 onPaste=${ ( event ) => {
28582874 event . preventDefault ( ) ;
2859- insertPlainTextAtSelection ( event . clipboardData ?. getData ( "text/plain" ) ?? "" ) ;
2875+ const pasted = event . clipboardData ?. getData ( "text/plain" ) ?? "" ;
2876+ const segments = normalizeTextMentions ( [ { type : "text" , text : pasted } ] , mentionableUsersByHandle ) ;
2877+ if ( segments . some ( ( segment ) => segment . type === "mention" ) ) {
2878+ insertComposerSegmentsAtSelection ( segments ) ;
2879+ } else {
2880+ insertPlainTextAtSelection ( pasted ) ;
2881+ }
28602882 syncComposerFromEditor ( ) ;
28612883 } }
28622884 />
@@ -4555,17 +4577,16 @@ function createMentionTokenElement(user) {
45554577 return token ;
45564578}
45574579
4558- function renderComposerSegments ( root , segments ) {
4559- if ( ! root ) {
4580+ function appendComposerSegments ( parent , segments ) {
4581+ if ( ! parent ) {
45604582 return ;
45614583 }
4562- root . replaceChildren ( ) ;
45634584 for ( const segment of segments ?? [ ] ) {
45644585 if ( ! segment ) {
45654586 continue ;
45664587 }
45674588 if ( segment . type === "mention" ) {
4568- root . append ( createMentionTokenElement ( {
4589+ parent . append ( createMentionTokenElement ( {
45694590 id : segment . userId ,
45704591 name : segment . userName ,
45714592 handle : segment . userName ,
@@ -4575,15 +4596,23 @@ function renderComposerSegments(root, segments) {
45754596 const parts = String ( segment . text ?? "" ) . split ( "\n" ) ;
45764597 parts . forEach ( ( part , index ) => {
45774598 if ( part ) {
4578- root . append ( document . createTextNode ( part ) ) ;
4599+ parent . append ( document . createTextNode ( part ) ) ;
45794600 }
45804601 if ( index < parts . length - 1 ) {
4581- root . append ( document . createElement ( "br" ) ) ;
4602+ parent . append ( document . createElement ( "br" ) ) ;
45824603 }
45834604 } ) ;
45844605 }
45854606}
45864607
4608+ function renderComposerSegments ( root , segments ) {
4609+ if ( ! root ) {
4610+ return ;
4611+ }
4612+ root . replaceChildren ( ) ;
4613+ appendComposerSegments ( root , segments ) ;
4614+ }
4615+
45874616function parseComposerSegments ( root ) {
45884617 if ( ! root ) {
45894618 return [ ] ;
@@ -4707,6 +4736,54 @@ function serializeComposerSegments(segments) {
47074736 } ) . join ( "" ) ;
47084737}
47094738
4739+ function splitTextSegmentByMentions ( text , mentionableUsersByHandle ) {
4740+ const content = String ( text ?? "" ) ;
4741+ if ( ! content || ! mentionableUsersByHandle || mentionableUsersByHandle . size === 0 ) {
4742+ return content ? [ { type : "text" , text : content } ] : [ ] ;
4743+ }
4744+ const mentionPattern = / ( ^ | [ ^ \w ] ) @ ( [ a - z A - Z 0 - 9 . _ - ] + ) / g;
4745+ const segments = [ ] ;
4746+ let lastIndex = 0 ;
4747+ let match ;
4748+ while ( ( match = mentionPattern . exec ( content ) ) !== null ) {
4749+ const prefix = match [ 1 ] ?? "" ;
4750+ const handle = match [ 2 ] ?? "" ;
4751+ const user = mentionableUsersByHandle . get ( handle . toLowerCase ( ) ) ;
4752+ if ( ! user ) {
4753+ continue ;
4754+ }
4755+ const mentionStart = match . index + prefix . length ;
4756+ if ( mentionStart > lastIndex ) {
4757+ segments . push ( { type : "text" , text : content . slice ( lastIndex , mentionStart ) } ) ;
4758+ }
4759+ segments . push ( {
4760+ type : "mention" ,
4761+ userId : user . id ,
4762+ userName : user . name || user . handle || user . id ,
4763+ } ) ;
4764+ lastIndex = mentionStart + handle . length + 1 ;
4765+ }
4766+ if ( lastIndex < content . length ) {
4767+ segments . push ( { type : "text" , text : content . slice ( lastIndex ) } ) ;
4768+ }
4769+ return segments ;
4770+ }
4771+
4772+ function normalizeTextMentions ( segments , mentionableUsersByHandle ) {
4773+ const normalized = [ ] ;
4774+ for ( const segment of segments ?? [ ] ) {
4775+ if ( ! segment ) {
4776+ continue ;
4777+ }
4778+ if ( segment . type === "mention" ) {
4779+ normalized . push ( segment ) ;
4780+ continue ;
4781+ }
4782+ normalized . push ( ...splitTextSegmentByMentions ( segment . text ?? "" , mentionableUsersByHandle ) ) ;
4783+ }
4784+ return normalizeComposerSegments ( normalized ) ;
4785+ }
4786+
47104787function getComposerMentionState ( root ) {
47114788 if ( ! root ) {
47124789 return null ;
@@ -4822,6 +4899,25 @@ function insertPlainTextAtSelection(text) {
48224899 selection . addRange ( nextRange ) ;
48234900}
48244901
4902+ function insertComposerSegmentsAtSelection ( segments ) {
4903+ const selection = window . getSelection ( ) ;
4904+ if ( ! selection || selection . rangeCount === 0 ) {
4905+ return ;
4906+ }
4907+ const range = selection . getRangeAt ( 0 ) ;
4908+ range . deleteContents ( ) ;
4909+ const marker = document . createTextNode ( "" ) ;
4910+ const fragment = document . createDocumentFragment ( ) ;
4911+ appendComposerSegments ( fragment , segments ) ;
4912+ fragment . append ( marker ) ;
4913+ range . insertNode ( fragment ) ;
4914+ const nextRange = document . createRange ( ) ;
4915+ nextRange . setStart ( marker , 0 ) ;
4916+ nextRange . collapse ( true ) ;
4917+ selection . removeAllRanges ( ) ;
4918+ selection . addRange ( nextRange ) ;
4919+ }
4920+
48254921function removeAdjacentMentionToken ( root , direction ) {
48264922 if ( ! root ) {
48274923 return false ;
0 commit comments