Composer: smarter suggestions, links over selections, big images attached
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 3m5s
ci / announce (pull_request) Skipped

Recipient suggestions:
- the words typed match in any order, each one the start of a word in the
  name, a nickname, the organization or the address: "jane smi" finds
  "Smith, Jane", and "globex" finds the people at Globex;
- someone written to lately ranks a little above an equal match;
- an address already in To, Cc or Bcc is no longer offered in the other
  two fields.

Pasting:
- a single web or mailto address pasted over selected words makes those
  words the link, instead of replacing them with the address;
- a pasted or dropped image over 10 MB goes in as an attachment rather
  than inline, where it would swell every reply.

No new strings.
This commit is contained in:
jcoffey-dev committed 2026-10-05 11:52:54 -07:00
1 parent 3434a5ed39
commit b11f3997d4
7 files changed
+275 -15

No files matched your search

+28 -3
View File
@@ -25,6 +25,15 @@ interface Props {
imageUpload?: (file: File) => Promise<string>;
}
/** Pasted or dropped images larger than this go in as attachments, not inline. */
export const INLINE_IMAGE_MAX = 10 * 1024 * 1024;
/** A single http(s) or mailto address, nothing else: what may become a link over a selection. */
export function isLinkToPaste(text: string): boolean {
if (!text || /\s/.test(text)) return false;
return /^(https?:\/\/[^\s]+|mailto:[^\s@]+@[^\s@]+)$/i.test(text);
}
const EMOJI = "😀 😃 😄 😁 😆 😅 😂 🤣 🙂 😉 😊 😇 🥰 😍 😘 😋 😜 🤪 🤗 🤔 🤫 🤐 😐 😑 😶 😏 😒 🙄 😬 😌 😔 😪 😴 😷 🤒 🤕 🤢 🤮 🥵 🥶 🥴 😵 🤯 🤠 🥳 😎 🤓 🧐 😕 😟 🙁 😮 😯 😲 😳 🥺 😦 😧 😨 😰 😥 😢 😭 😱 😖 😣 😞 😓 😩 😫 🥱 😤 😡 😠 🤬 👍 👎 👌 ✌️ 🤞 🤟 🤘 🤙 👈 👉 👆 👇 ☝️ 👋 🤚 🖐️ ✋ 🖖 👏 🙌 👐 🤲 🤝 🙏 💪 ❤️ 🧡 💛 💚 💙 💜 🖤 🤍 💔 ❣️ 💕 💯 💥 🔥 ✨ 🎉 🎊 🎈 🎁 🏆 ⭐ 🌟 ☀️ 🌙 ⚡ ☕ 🍕 🍺 🚀 ✈️ 🏠 💼 📅 📎 📌 ✅ ❌ ⚠️ ❓ ❗ 💡 🔔 📧 🙈 🙉 🙊 🐱 🐶 🦊 🐼".split(" ");
const COLORS = ["#000000", "#434343", "#666666", "#999999", "#b7b7b7", "#cccccc", "#d9d9d9", "#ffffff", "#980000", "#ff0000", "#ff9900", "#ffff00", "#00ff00", "#00ffff", "#4a86e8", "#0000ff", "#9900ff", "#ff00ff", "#e6b8af", "#f4cccc", "#fce5cd", "#fff2cc", "#d9ead3", "#d0e0e3", "#c9daf8", "#cfe2f3", "#d9d2e9", "#ead1dc", "#cc4125", "#e06666", "#f6b26b", "#ffd966", "#93c47d", "#76a5af", "#6d9eeb", "#6fa8dc", "#8e7cc3", "#c27ba0", "#a61c00", "#cc0000", "#e69138", "#f1c232", "#6aa84f", "#45818e", "#3c78d8", "#3d85c6", "#674ea7", "#a64d79"];
@@ -133,10 +142,25 @@ export const RichEditor = forwardRef<RichEditorHandle, Props>(function RichEdito
const f = imgItem.getAsFile();
if (f) {
e.preventDefault();
insertImageFile(f);
// A large image is a file to send, not a picture in the text: it goes
// in as an attachment, where an inline copy would swell every reply.
if (f.size > INLINE_IMAGE_MAX && onFiles) onFiles([f]);
else insertImageFile(f);
return;
}
}
/*
* A link pasted over selected words makes those words the link, the way
* most editors do, instead of replacing them with the address.
*/
const pastedText = e.clipboardData.getData("text/plain").trim();
const sel = window.getSelection();
if (isLinkToPaste(pastedText) && sel && !sel.isCollapsed && elRef.current?.contains(sel.anchorNode)) {
e.preventDefault();
document.execCommand("createLink", false, pastedText);
emit();
return;
}
const htmlData = e.clipboardData.getData("text/html");
if (htmlData) {
e.preventDefault();
@@ -175,8 +199,9 @@ export const RichEditor = forwardRef<RichEditorHandle, Props>(function RichEdito
const files = Array.from(e.dataTransfer.files);
if (!files.length) return;
e.preventDefault();
const images = files.filter((f) => f.type.startsWith("image/"));
const others = files.filter((f) => !f.type.startsWith("image/"));
const inline = (f: File) => f.type.startsWith("image/") && !(f.size > INLINE_IMAGE_MAX && onFiles);
const images = files.filter(inline);
const others = files.filter((f) => !inline(f));
images.forEach(insertImageFile);
if (others.length) onFiles?.(others);
};