mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Implement fromchat.ru/@username and fromchat.ru/?u=userId links, mentions
This commit is contained in:
@@ -10,10 +10,11 @@ import { ecdhSharedSecret, deriveWrappingKey } from "@/utils/crypto/asymmetric";
|
||||
import { importAesGcmKey, aesGcmDecrypt } from "@/utils/crypto/symmetric";
|
||||
import { getAuthHeaders } from "@/core/api/authApi";
|
||||
import { useAppState } from "@/pages/chat/state";
|
||||
import { fetchUserProfileById } from "@/core/api/profileApi";
|
||||
import { fetchUserProfileById, fetchUserProfile } from "@/core/api/profileApi";
|
||||
import { ub64 } from "@/utils/utils";
|
||||
import { useImmer } from "use-immer";
|
||||
import { createPortal } from "react-dom";
|
||||
import { parseProfileLink } from "@/core/profileLinks";
|
||||
|
||||
interface MessageReactionsProps {
|
||||
reactions?: Reaction[];
|
||||
@@ -147,7 +148,6 @@ interface Rect {
|
||||
}
|
||||
|
||||
export function Message({ message, isAuthor, onContextMenu, onReactionClick, isDm = false, dmRecipientPublicKey }: MessageProps) {
|
||||
const [formattedMessage, setFormattedMessage] = useState({ __html: "" });
|
||||
const [decryptedFiles, updateDecryptedFiles] = useImmer<Map<string, string>>(new Map());
|
||||
const [loadedImages, updateLoadedImages] = useImmer<Set<string>>(new Set());
|
||||
const [downloadingPaths, updateDownloadingPaths] = useImmer<Set<string>>(new Set());
|
||||
@@ -164,15 +164,29 @@ export function Message({ message, isAuthor, onContextMenu, onReactionClick, isD
|
||||
const imageRefs = useRef<Map<string, HTMLImageElement>>(new Map());
|
||||
const dmEnvelope = message.runtimeData?.dmEnvelope;
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setFormattedMessage({
|
||||
__html: DOMPurify.sanitize(
|
||||
await parse(message.content)
|
||||
).trim()
|
||||
});
|
||||
})();
|
||||
}, [message]);
|
||||
const formattedMessage = useMemo(() => {
|
||||
// First, temporarily replace existing fromchat.ru links to avoid conflicts
|
||||
const linkPlaceholders: string[] = [];
|
||||
let content = message.content.replace(/https?:\/\/fromchat\.ru\/@[a-zA-Z0-9_.-]+/g, (match) => {
|
||||
const placeholder = `__LINK_PLACEHOLDER_${linkPlaceholders.length}__`;
|
||||
linkPlaceholders.push(match);
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// Now process @mentions that aren't in existing links
|
||||
content = content.replace(/@([a-zA-Z0-9_.-]+)/g, (match, username) => {
|
||||
return `<a href="https://fromchat.ru/@${username}" class="mention-link">${match}</a>`;
|
||||
});
|
||||
|
||||
// Restore the original links
|
||||
linkPlaceholders.forEach((link, index) => {
|
||||
content = content.replace(`__LINK_PLACEHOLDER_${index}__`, link);
|
||||
});
|
||||
|
||||
return {
|
||||
__html: DOMPurify.sanitize(parse(content, { async: false })).trim()
|
||||
};
|
||||
}, [message.content]);
|
||||
|
||||
// Auto-decrypt images in DMs
|
||||
useEffect(() => {
|
||||
@@ -406,6 +420,44 @@ export function Message({ message, isAuthor, onContextMenu, onReactionClick, isD
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLinkClick(e: React.MouseEvent<HTMLDivElement>) {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
if (target.tagName === 'A') {
|
||||
const profileLink = parseProfileLink((target as HTMLAnchorElement).href);
|
||||
|
||||
if (profileLink) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!user.authToken) return;
|
||||
|
||||
try {
|
||||
let userProfile;
|
||||
|
||||
if (profileLink.userId) {
|
||||
userProfile = await fetchUserProfileById(user.authToken, profileLink.userId);
|
||||
} else if (profileLink.username) {
|
||||
userProfile = await fetchUserProfile(user.authToken, profileLink.username);
|
||||
}
|
||||
|
||||
if (userProfile) {
|
||||
setProfileDialog({
|
||||
...userProfile,
|
||||
userId: userProfile.id,
|
||||
memberSince: userProfile.created_at,
|
||||
isOwnProfile: userProfile.id === user.currentUser?.id
|
||||
});
|
||||
} else {
|
||||
throw new Error("Invalid link: " + (target as HTMLAnchorElement).href);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user profile from link:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleContextMenu(e: React.MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -460,7 +512,11 @@ export function Message({ message, isAuthor, onContextMenu, onReactionClick, isD
|
||||
</Quote>
|
||||
)}
|
||||
|
||||
<div className={`message-content ${isEmojiMessage ? "emoji-content" : ""} ${isSingleEmojiMessage ? "single-emoji-content" : ""}`} dangerouslySetInnerHTML={formattedMessage} />
|
||||
<div
|
||||
className={`message-content ${isEmojiMessage ? "emoji-content" : ""} ${isSingleEmojiMessage ? "single-emoji-content" : ""}`}
|
||||
dangerouslySetInnerHTML={formattedMessage}
|
||||
onClick={handleLinkClick}
|
||||
/>
|
||||
|
||||
{message.files && message.files.length > 0 && (
|
||||
<mdui-list className="message-attachments">
|
||||
|
||||
Reference in New Issue
Block a user