|
|
|
@ -26,29 +26,36 @@ |
|
|
|
const cursorPos = target.selectionStart || 0; |
|
|
|
const cursorPos = target.selectionStart || 0; |
|
|
|
|
|
|
|
|
|
|
|
// Find @ mention starting from cursor backwards |
|
|
|
// Find @ mention starting from cursor backwards |
|
|
|
// Allow word chars, @, dots, and hyphens to support NIP-05 format (user@domain.com) |
|
|
|
// We need to find the leftmost @ that could start a mention |
|
|
|
let start = cursorPos - 1; |
|
|
|
let start = cursorPos - 1; |
|
|
|
|
|
|
|
let foundAt = -1; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First, find the leftmost @ going backwards while characters are valid mention chars |
|
|
|
|
|
|
|
// This handles cases like @user@domain.com - we want the first @ |
|
|
|
while (start >= 0 && /[\w@.-]/.test(text[start])) { |
|
|
|
while (start >= 0 && /[\w@.-]/.test(text[start])) { |
|
|
|
|
|
|
|
if (text[start] === '@') { |
|
|
|
|
|
|
|
foundAt = start; |
|
|
|
|
|
|
|
} |
|
|
|
start--; |
|
|
|
start--; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (start >= 0 && text[start] === '@') { |
|
|
|
// If we found an @ and there's valid mention text after it, show suggestions |
|
|
|
// Found @ mention |
|
|
|
if (foundAt >= 0) { |
|
|
|
mentionStart = start; |
|
|
|
mentionStart = foundAt; |
|
|
|
const mentionText = text.substring(start + 1, cursorPos); |
|
|
|
const mentionText = text.substring(foundAt + 1, cursorPos); |
|
|
|
|
|
|
|
|
|
|
|
// Check if we're still in a valid mention (word chars, @, dots, hyphens) |
|
|
|
// Check if we're still in a valid mention (word chars, @, dots, hyphens) |
|
|
|
// Support both simple handles (@user) and NIP-05 format (@user@domain.com) |
|
|
|
// Support both simple handles (@user) and NIP-05 format (@user@domain.com) |
|
|
|
if (mentionText.length > 0 && /^[\w.-]+(@[\w.-]+)?$/.test(mentionText)) { |
|
|
|
if (mentionText.length > 0 && /^[\w.-]+(@[\w.-]+)?$/.test(mentionText)) { |
|
|
|
query = mentionText; |
|
|
|
query = mentionText; |
|
|
|
updateSuggestions(mentionText); |
|
|
|
updateSuggestions(mentionText); |
|
|
|
updatePosition(target, start); |
|
|
|
updatePosition(target, foundAt); |
|
|
|
showSuggestions = true; |
|
|
|
showSuggestions = true; |
|
|
|
} else if (mentionText.length === 0) { |
|
|
|
} else if (mentionText.length === 0) { |
|
|
|
// Just @, show all suggestions |
|
|
|
// Just @, show all suggestions |
|
|
|
query = ''; |
|
|
|
query = ''; |
|
|
|
updateSuggestions(''); |
|
|
|
updateSuggestions(''); |
|
|
|
updatePosition(target, start); |
|
|
|
updatePosition(target, foundAt); |
|
|
|
showSuggestions = true; |
|
|
|
showSuggestions = true; |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
showSuggestions = false; |
|
|
|
showSuggestions = false; |
|
|
|
@ -132,9 +139,21 @@ |
|
|
|
|
|
|
|
|
|
|
|
const suggestion = suggestions[index]; |
|
|
|
const suggestion = suggestions[index]; |
|
|
|
const handle = suggestion.handle || suggestion.name || suggestion.pubkey.slice(0, 8); |
|
|
|
const handle = suggestion.handle || suggestion.name || suggestion.pubkey.slice(0, 8); |
|
|
|
const mentionText = `@${handle} `; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Replace @mention with selected handle |
|
|
|
// Convert pubkey to npub format (NIP-19) |
|
|
|
|
|
|
|
let npub: string; |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
npub = nip19.npubEncode(suggestion.pubkey); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
|
|
console.error('Error encoding pubkey to npub:', error); |
|
|
|
|
|
|
|
// Fallback to @handle if encoding fails |
|
|
|
|
|
|
|
npub = suggestion.pubkey; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Insert as nostr:npub... format instead of @handle |
|
|
|
|
|
|
|
const mentionText = `nostr:${npub} `; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Replace @mention with nostr:npub format |
|
|
|
const text = textarea.value; |
|
|
|
const text = textarea.value; |
|
|
|
const cursorPos = textarea.selectionStart || 0; |
|
|
|
const cursorPos = textarea.selectionStart || 0; |
|
|
|
const textBefore = text.substring(0, mentionStart); |
|
|
|
const textBefore = text.substring(0, mentionStart); |
|
|
|
@ -186,6 +205,7 @@ |
|
|
|
src={suggestion.picture} |
|
|
|
src={suggestion.picture} |
|
|
|
alt="" |
|
|
|
alt="" |
|
|
|
class="avatar" |
|
|
|
class="avatar" |
|
|
|
|
|
|
|
style="width: 1.25rem; height: 1.25rem; min-width: 1.25rem; min-height: 1.25rem; max-width: 1.25rem; max-height: 1.25rem; aspect-ratio: 1 / 1; border-radius: 50%; object-fit: cover; flex-shrink: 0; display: block;" |
|
|
|
onerror={(e) => { |
|
|
|
onerror={(e) => { |
|
|
|
(e.target as HTMLImageElement).style.display = 'none'; |
|
|
|
(e.target as HTMLImageElement).style.display = 'none'; |
|
|
|
}} |
|
|
|
}} |
|
|
|
@ -197,7 +217,9 @@ |
|
|
|
<div class="suggestion-name"> |
|
|
|
<div class="suggestion-name"> |
|
|
|
{suggestion.name || suggestion.handle || suggestion.pubkey.slice(0, 8)} |
|
|
|
{suggestion.name || suggestion.handle || suggestion.pubkey.slice(0, 8)} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
{#if suggestion.handle && suggestion.name} |
|
|
|
{#if suggestion.nip05} |
|
|
|
|
|
|
|
<div class="suggestion-handle">{suggestion.nip05}</div> |
|
|
|
|
|
|
|
{:else if suggestion.handle} |
|
|
|
<div class="suggestion-handle">@{suggestion.handle}</div> |
|
|
|
<div class="suggestion-handle">@{suggestion.handle}</div> |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -253,13 +275,29 @@ |
|
|
|
background: var(--fog-dark-highlight, #374151); |
|
|
|
background: var(--fog-dark-highlight, #374151); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.avatar, |
|
|
|
.suggestion-item .avatar, |
|
|
|
.avatar-placeholder { |
|
|
|
.suggestion-item .avatar-placeholder { |
|
|
|
width: 2rem; |
|
|
|
width: 1.25rem !important; |
|
|
|
height: 2rem; |
|
|
|
height: 1.25rem !important; |
|
|
|
border-radius: 50%; |
|
|
|
min-width: 1.25rem !important; |
|
|
|
flex-shrink: 0; |
|
|
|
min-height: 1.25rem !important; |
|
|
|
object-fit: cover; |
|
|
|
max-width: 1.25rem !important; |
|
|
|
|
|
|
|
max-height: 1.25rem !important; |
|
|
|
|
|
|
|
aspect-ratio: 1 / 1 !important; |
|
|
|
|
|
|
|
border-radius: 50% !important; |
|
|
|
|
|
|
|
flex-shrink: 0 !important; |
|
|
|
|
|
|
|
flex-grow: 0 !important; |
|
|
|
|
|
|
|
display: block !important; |
|
|
|
|
|
|
|
box-sizing: border-box !important; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.suggestion-item .avatar { |
|
|
|
|
|
|
|
object-fit: cover !important; |
|
|
|
|
|
|
|
image-rendering: auto; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.suggestion-item .avatar-placeholder { |
|
|
|
|
|
|
|
background: var(--fog-border, #e5e7eb); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.avatar-placeholder { |
|
|
|
.avatar-placeholder { |
|
|
|
|