Browse Source

fix profile mentions

master
Silberengel 1 month ago
parent
commit
21f18ef1e1
  1. 24
      src/app.css
  2. 15
      src/lib/components/content/MarkdownRenderer.svelte
  3. 72
      src/lib/components/content/MentionsAutocomplete.svelte
  4. 26
      src/lib/components/layout/ProfileBadge.svelte
  5. 6
      src/lib/services/profile-search.ts

24
src/app.css

@ -472,7 +472,8 @@ img.emoji-inline { @@ -472,7 +472,8 @@ img.emoji-inline {
}
/* Responsive images and media - max 600px, scale down on smaller screens */
img:not(.profile-picture):not([alt*="profile" i]):not([alt*="avatar" i]):not([src*="avatar" i]):not([src*="profile" i]):not(.relay-icon):not(.relay-favorite-pic),
/* Exclude profile pictures, avatars, and emoji images from responsive sizing */
img:not(.profile-picture):not([alt*="profile" i]):not([alt*="avatar" i]):not([src*="avatar" i]):not([src*="profile" i]):not(.relay-icon):not(.relay-favorite-pic):not(.emoji-inline):not(.avatar),
video,
audio {
max-width: 600px !important;
@ -482,10 +483,11 @@ audio { @@ -482,10 +483,11 @@ audio {
}
/* Ensure media in markdown content is responsive */
.markdown-content img,
/* Exclude profile pictures, avatars, and emoji images from responsive sizing - they should maintain their fixed size */
.markdown-content img:not(.profile-picture):not([class*="profile"]):not(.emoji-inline):not(.avatar),
.markdown-content video,
.markdown-content audio,
.post-content img,
.post-content img:not(.profile-picture):not([class*="profile"]):not(.emoji-inline):not(.avatar),
.post-content video,
.post-content audio {
max-width: 600px;
@ -494,6 +496,22 @@ audio { @@ -494,6 +496,22 @@ audio {
display: block;
}
/* Ensure avatars in mentions autocomplete are not affected by responsive image styles */
.mentions-autocomplete img.avatar,
.mentions-autocomplete .avatar {
width: 1.25rem !important;
height: 1.25rem !important;
min-width: 1.25rem !important;
min-height: 1.25rem !important;
max-width: 1.25rem !important;
max-height: 1.25rem !important;
aspect-ratio: 1 / 1 !important;
border-radius: 50% !important;
object-fit: cover !important;
flex-shrink: 0 !important;
display: block !important;
}
/* Media gallery items should also be responsive */
.media-gallery img,
.media-gallery video {

15
src/lib/components/content/MarkdownRenderer.svelte

@ -1037,6 +1037,21 @@ @@ -1037,6 +1037,21 @@
align-items: center;
}
/* Ensure profile pictures in markdown content maintain circular shape and don't stretch */
:global(.markdown-content .profile-badge .profile-picture),
:global(.markdown-content .profile-badge .profile-placeholder) {
width: 1.5rem !important;
height: 1.5rem !important;
max-width: 1.5rem !important;
max-height: 1.5rem !important;
min-width: 1.5rem !important;
min-height: 1.5rem !important;
aspect-ratio: 1 / 1 !important;
border-radius: 50% !important;
object-fit: cover !important;
flex-shrink: 0 !important;
}
/* Embedded events should be block-level */
:global(.markdown-content [data-nostr-event]) {
display: block;

72
src/lib/components/content/MentionsAutocomplete.svelte

@ -26,29 +26,36 @@ @@ -26,29 +26,36 @@
const cursorPos = target.selectionStart || 0;
// 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 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])) {
if (text[start] === '@') {
foundAt = start;
}
start--;
}
if (start >= 0 && text[start] === '@') {
// Found @ mention
mentionStart = start;
const mentionText = text.substring(start + 1, cursorPos);
// If we found an @ and there's valid mention text after it, show suggestions
if (foundAt >= 0) {
mentionStart = foundAt;
const mentionText = text.substring(foundAt + 1, cursorPos);
// 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)
if (mentionText.length > 0 && /^[\w.-]+(@[\w.-]+)?$/.test(mentionText)) {
query = mentionText;
updateSuggestions(mentionText);
updatePosition(target, start);
updatePosition(target, foundAt);
showSuggestions = true;
} else if (mentionText.length === 0) {
// Just @, show all suggestions
query = '';
updateSuggestions('');
updatePosition(target, start);
updatePosition(target, foundAt);
showSuggestions = true;
} else {
showSuggestions = false;
@ -132,9 +139,21 @@ @@ -132,9 +139,21 @@
const suggestion = suggestions[index];
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 cursorPos = textarea.selectionStart || 0;
const textBefore = text.substring(0, mentionStart);
@ -186,6 +205,7 @@ @@ -186,6 +205,7 @@
src={suggestion.picture}
alt=""
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) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
@ -197,7 +217,9 @@ @@ -197,7 +217,9 @@
<div class="suggestion-name">
{suggestion.name || suggestion.handle || suggestion.pubkey.slice(0, 8)}
</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>
{/if}
</div>
@ -253,13 +275,29 @@ @@ -253,13 +275,29 @@
background: var(--fog-dark-highlight, #374151);
}
.avatar,
.avatar-placeholder {
width: 2rem;
height: 2rem;
border-radius: 50%;
flex-shrink: 0;
object-fit: cover;
.suggestion-item .avatar,
.suggestion-item .avatar-placeholder {
width: 1.25rem !important;
height: 1.25rem !important;
min-width: 1.25rem !important;
min-height: 1.25rem !important;
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 {

26
src/lib/components/layout/ProfileBadge.svelte

@ -143,7 +143,7 @@ @@ -143,7 +143,7 @@
<img
src={profile.picture}
alt={profile.name || pubkey}
class="profile-picture w-6 h-6 rounded flex-shrink-0"
class="profile-picture w-6 h-6 rounded-full flex-shrink-0"
class:nav-picture={pictureOnly}
loading="lazy"
onerror={() => {
@ -152,7 +152,7 @@ @@ -152,7 +152,7 @@
/>
{:else}
<div
class="profile-placeholder w-6 h-6 rounded flex-shrink-0 flex items-center justify-center text-xs font-semibold"
class="profile-placeholder w-6 h-6 rounded-full flex-shrink-0 flex items-center justify-center text-xs font-semibold"
class:nav-picture={pictureOnly}
style="background: {avatarColor}; color: white;"
title={profile?.name || pubkey}
@ -202,18 +202,36 @@ @@ -202,18 +202,36 @@
}
.nav-picture {
width: 2rem;
height: 2rem;
width: 2rem !important;
height: 2rem !important;
max-width: 2rem !important;
max-height: 2rem !important;
min-width: 2rem !important;
min-height: 2rem !important;
}
.profile-picture {
object-fit: cover;
display: block;
aspect-ratio: 1 / 1;
width: 1.5rem;
height: 1.5rem;
max-width: 1.5rem;
max-height: 1.5rem;
min-width: 1.5rem;
min-height: 1.5rem;
}
.profile-placeholder {
user-select: none;
line-height: 1;
aspect-ratio: 1 / 1;
width: 1.5rem;
height: 1.5rem;
max-width: 1.5rem;
max-height: 1.5rem;
min-width: 1.5rem;
min-height: 1.5rem;
}
.nip05-text {

6
src/lib/services/profile-search.ts

@ -16,7 +16,8 @@ import type { NostrEvent } from '../types/nostr.js'; @@ -16,7 +16,8 @@ import type { NostrEvent } from '../types/nostr.js';
export interface ProfileSearchResult {
pubkey: string;
name?: string;
handle?: string; // nip05 handle
handle?: string; // nip05 handle (part before @)
nip05?: string; // full NIP-05 address
picture?: string;
}
@ -78,6 +79,7 @@ export async function searchProfiles( @@ -78,6 +79,7 @@ export async function searchProfiles(
pubkey: cursor.key as string,
name: profile.name,
handle: handle || undefined,
nip05: firstNip05 || undefined,
picture: profile.picture
});
}
@ -141,6 +143,7 @@ export async function getProfileByPubkey( @@ -141,6 +143,7 @@ export async function getProfileByPubkey(
pubkey,
name: profile.name,
handle: handle || undefined,
nip05: nip05 || undefined,
picture: profile.picture
};
}
@ -168,6 +171,7 @@ export async function getProfileByPubkey( @@ -168,6 +171,7 @@ export async function getProfileByPubkey(
pubkey,
name: profile.name,
handle: handle || undefined,
nip05: nip05 || undefined,
picture: profile.picture
};
}

Loading…
Cancel
Save