|
13 | 13 |
|
14 | 14 | const introductionPrefix = '#nostr #introductions\n\n'; |
15 | 15 | const mentionSearchDelayMs = 220; |
| 16 | + const croppableImageTypes = new Set(['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/avif']); |
16 | 17 | const emojiOptions = [ |
17 | 18 | '😀', '😃', '😄', '😁', '😆', '😂', '🤣', '😊', '🙂', '🙃', '😉', '😍', |
18 | | - '😘', '😎', '🤩', '🥳', '😇', '🤔', '🫡', '🤝', '🙏', '👏', '🙌', '💪', |
19 | | - '👍', '👎', '👊', '✌️', '🤘', '👌', '👀', '🧡', '❤️', '💙', '💜', '💚', |
20 | | - '💛', '🖤', '🤍', '💔', '🔥', '⚡', '✨', '⭐', '🌟', '💥', '💯', '🎉', |
21 | | - '🚀', '🌈', '☀️', '🌙', '🍕', '☕', '🍺', '🍻', '🎵', '🎸', '🎧', '📸', |
22 | | - '💡', '📌', '📣', '🧠', '🛠️', '🔒', '🔑', '🛡️', '₿', '💸', '⚽', '🏆' |
| 19 | + '😘', '😗', '😙', '😚', '😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', |
| 20 | + '🫢', '🫣', '🤫', '🤔', '🫡', '🤐', '🤨', '😐', '😑', '😶', '🫥', '😏', |
| 21 | + '😒', '🙄', '😬', '😮💨', '🤥', '😌', '😔', '😪', '🤤', '😴', '😷', '🤒', |
| 22 | + '🤕', '🤢', '🤮', '🤧', '🥵', '🥶', '🥴', '😵', '🤯', '🤠', '🥳', '🥸', |
| 23 | + '😎', '🤓', '🧐', '😕', '🫤', '😟', '🙁', '☹️', '😮', '😯', '😲', '😳', |
| 24 | + '🥺', '🥹', '😦', '😧', '😨', '😰', '😥', '😢', '😭', '😱', '😖', '😣', |
| 25 | + '😞', '😓', '😩', '😫', '😤', '😡', '😠', '🤬', '😈', '👿', '💀', '☠️', |
| 26 | + '💩', '🤡', '👻', '👽', '🤖', '😺', '😸', '😹', '😻', '😼', '😽', '🙀', |
| 27 | + '😿', '😾', '🙈', '🙉', '🙊', '💋', '💌', '💘', '💝', '💖', '💗', '💓', |
| 28 | + '💞', '💕', '💟', '❣️', '💔', '❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', |
| 29 | + '🤍', '🤎', '💯', '💢', '💥', '💫', '💦', '💨', '🕳️', '💣', '💬', '👁️🗨️', |
| 30 | + '🗨️', '🗯️', '💭', '💤', '👋', '🤚', '🖐️', '✋', '🖖', '👌', '🤌', '🤏', |
| 31 | + '✌️', '🤞', '🫰', '🤟', '🤘', '🤙', '👈', '👉', '👆', '🖕', '👇', '☝️', |
| 32 | + '🫵', '👍', '👎', '✊', '👊', '🤛', '🤜', '👏', '🙌', '🫶', '👐', '🤲', |
| 33 | + '🤝', '🙏', '✍️', '💅', '🤳', '💪', '🦾', '🧠', '🫀', '🫁', '🦷', '🦴', |
| 34 | + '👀', '👁️', '👅', '👄', '🧑', '👨', '👩', '🧔', '👴', '👵', '🙍', '🙎', |
| 35 | + '🙅', '🙆', '💁', '🙋', '🧏', '🙇', '🤦', '🤷', '👨💻', '👩💻', '🧙', '🧚', |
| 36 | + '🧛', '🧜', '🧝', '🧞', '🧟', '🏃', '🚶', '🧘', '🛌', '🌍', '🌎', '🌏', |
| 37 | + '🌐', '🗺️', '🧭', '🏔️', '⛰️', '🌋', '🏕️', '🏖️', '🏜️', '🏝️', '🏞️', '🏟️', |
| 38 | + '🏛️', '🏗️', '🏘️', '🏠', '🏡', '🏢', '🏬', '🏥', '🏦', '🏨', '🏫', '🏭', |
| 39 | + '🌁', '🌃', '🌆', '🌇', '🌉', '🌌', '🎠', '🎡', '🎢', '🚂', '🚀', '🛸', |
| 40 | + '☀️', '🌤️', '⛅', '🌥️', '☁️', '🌦️', '🌧️', '⛈️', '🌩️', '🌨️', '❄️', '☃️', |
| 41 | + '⛄', '🌬️', '💨', '🌪️', '🌈', '🌂', '☂️', '⚡', '🔥', '💧', '🌊', '🎄', |
| 42 | + '✨', '🎋', '🎍', '🌱', '🌿', '☘️', '🍀', '🎍', '🌵', '🌲', '🌳', '🌴', |
| 43 | + '🪵', '🌾', '🌺', '🌻', '🌹', '🥀', '🌷', '🌼', '🌸', '🍄', '🌰', '🪨', |
| 44 | + '🍇', '🍈', '🍉', '🍊', '🍋', '🍌', '🍍', '🥭', '🍎', '🍏', '🍐', '🍑', |
| 45 | + '🍒', '🍓', '🫐', '🥝', '🍅', '🫒', '🥥', '🥑', '🍆', '🥔', '🥕', '🌽', |
| 46 | + '🌶️', '🫑', '🥒', '🥬', '🥦', '🧄', '🧅', '🥜', '🫘', '🌰', '🍞', '🥐', |
| 47 | + '🥖', '🥨', '🥯', '🥞', '🧇', '🧀', '🍖', '🍗', '🥩', '🥓', '🍔', '🍟', |
| 48 | + '🍕', '🌭', '🥪', '🌮', '🌯', '🫔', '🥙', '🧆', '🥚', '🍳', '🥘', '🍲', |
| 49 | + '🫕', '🥣', '🥗', '🍿', '🧈', '🧂', '🥫', '🍱', '🍘', '🍙', '🍚', '🍛', |
| 50 | + '🍜', '🍝', '🍠', '🍢', '🍣', '🍤', '🍥', '🥮', '🍡', '🥟', '🥠', '🥡', |
| 51 | + '🦪', '🍦', '🍧', '🍨', '🍩', '🍪', '🎂', '🍰', '🧁', '🥧', '🍫', '🍬', |
| 52 | + '🍭', '🍮', '🍯', '🍼', '🥛', '☕', '🫖', '🍵', '🍶', '🍾', '🍷', '🍸', |
| 53 | + '🍹', '🍺', '🍻', '🥂', '🥃', '🧃', '🧉', '🧊', '🥢', '🍽️', '⚽', '🏀', |
| 54 | + '🏈', '⚾', '🥎', '🎾', '🏐', '🏉', '🥏', '🎱', '🪀', '🏓', '🏸', '🏒', |
| 55 | + '🏑', '🥍', '🏏', '🪃', '🥅', '⛳', '🪁', '🏹', '🎣', '🤿', '🥊', '🥋', |
| 56 | + '🎽', '🛹', '🛼', '🛷', '⛸️', '🥌', '🎿', '⛷️', '🏂', '🪂', '🏋️', '🤼', |
| 57 | + '🤸', '⛹️', '🤺', '🤾', '🏌️', '🏇', '🧗', '🏊', '🚣', '🏄', '🎪', '🎭', |
| 58 | + '🎨', '🎬', '🎤', '🎧', '🎼', '🎹', '🥁', '🪘', '🎷', '🎺', '🪗', '🎸', |
| 59 | + '🪕', '🎻', '🎲', '♟️', '🎯', '🎳', '🎮', '🎰', '🧩', '⌚', '📱', '💻', |
| 60 | + '⌨️', '🖥️', '🖨️', '🖱️', '💽', '💾', '💿', '📀', '📷', '📸', '🎥', '📺', |
| 61 | + '📻', '🎙️', '⏰', '⌛', '⏳', '📡', '🔋', '🪫', '🔌', '💡', '🔦', '🕯️', |
| 62 | + '🧯', '🛢️', '💸', '💵', '💴', '💶', '💷', '🪙', '💰', '💳', '🧾', '💎', |
| 63 | + '⚖️', '🧰', '🔧', '🔨', '⚒️', '🛠️', '⛏️', '🪚', '🔩', '⚙️', '🪤', '🧱', |
| 64 | + '⛓️', '🧲', '🔫', '💣', '🧨', '🪓', '🔪', '🗡️', '🛡️', '🚬', '⚰️', '🪦', |
| 65 | + '⚱️', '🏺', '🔮', '📿', '🧿', '💈', '⚗️', '🔭', '🔬', '🕳️', '🩹', '🩺', |
| 66 | + '💊', '💉', '🩸', '🧬', '🦠', '🧫', '🧪', '🌡️', '🧹', '🪠', '🧽', '🧼', |
| 67 | + '🪥', '🪒', '🧴', '🛎️', '🔑', '🗝️', '🚪', '🪑', '🛋️', '🛏️', '🧸', '🪆', |
| 68 | + '🖼️', '🛍️', '🛒', '🎁', '🎈', '🎏', '🎀', '🎊', '🎉', '🪩', '🪅', '📩', |
| 69 | + '📨', '📧', '💌', '📥', '📤', '📦', '🏷️', '🪧', '📪', '📫', '📬', '📭', |
| 70 | + '📮', '📯', '📜', '📃', '📄', '📑', '🧾', '📊', '📈', '📉', '🗒️', '🗓️', |
| 71 | + '📆', '📅', '🗑️', '📌', '📍', '✂️', '🖊️', '🖋️', '✒️', '🖌️', '🖍️', '📝', |
| 72 | + '🔍', '🔎', '🔏', '🔐', '🔒', '🔓', '❤️🔥', '❤️🩹', '🏆', '🥇', '🥈', '🥉', |
| 73 | + '₿', '🧡', '⚡', '🤙' |
23 | 74 | ]; |
24 | 75 | let content = ''; |
25 | 76 | let busy = false; |
26 | 77 | let uploading = false; |
27 | 78 | let error = ''; |
28 | 79 | let emojiPickerOpen = false; |
| 80 | + let emojiButtonElement: HTMLElement; |
29 | 81 | let emojiPickerElement: HTMLElement; |
30 | 82 | let mediaInput: HTMLInputElement; |
31 | 83 | let textarea: HTMLTextAreaElement; |
|
77 | 129 |
|
78 | 130 | onDestroy(() => { |
79 | 131 | clearTimeout(mentionSearchTimer); |
80 | | - if (browser) document.removeEventListener('pointerdown', closeEmojiPickerFromOutside); |
| 132 | + if (browser) document.removeEventListener('pointerdown', closeComposerPopupsFromOutside); |
81 | 133 | }); |
82 | 134 |
|
83 | | - $: if (browser && $composerOpen) syncEmojiPickerListener(emojiPickerOpen); |
| 135 | + $: if (browser && $composerOpen) syncComposerPopupListener(emojiPickerOpen); |
84 | 136 |
|
85 | 137 | async function submit() { |
86 | 138 | if (busy || uploading || !$session || !content.trim()) return; |
|
124 | 176 | return; |
125 | 177 | } |
126 | 178 |
|
127 | | - openCropper(file); |
| 179 | + if (shouldCropImage(file)) { |
| 180 | + openCropper(file); |
| 181 | + return; |
| 182 | + } |
| 183 | +
|
| 184 | + await uploadCroppedMedia(file); |
128 | 185 | } |
129 | 186 |
|
130 | 187 | async function uploadCroppedMedia(file: File) { |
|
148 | 205 | error = ''; |
149 | 206 | } |
150 | 207 |
|
| 208 | + function shouldCropImage(file: File) { |
| 209 | + if (croppableImageTypes.has(file.type.toLowerCase())) return true; |
| 210 | + return /\.(jpe?g|png|webp|avif)$/i.test(file.name); |
| 211 | + } |
| 212 | +
|
151 | 213 | function closeCropper() { |
152 | 214 | cropFile = undefined; |
153 | 215 | if (mediaInput) mediaInput.value = ''; |
|
217 | 279 | textarea?.setSelectionRange(caret, caret); |
218 | 280 | } |
219 | 281 |
|
220 | | - function syncEmojiPickerListener(open: boolean) { |
| 282 | + function syncComposerPopupListener(open: boolean) { |
221 | 283 | if (!browser) return; |
222 | | - if (open) document.addEventListener('pointerdown', closeEmojiPickerFromOutside); |
223 | | - else document.removeEventListener('pointerdown', closeEmojiPickerFromOutside); |
224 | | - } |
225 | | -
|
226 | | - function closeEmojiPickerFromOutside(event: PointerEvent) { |
227 | | - if (!emojiPickerOpen || !emojiPickerElement || !(event.target instanceof Node) || emojiPickerElement.contains(event.target)) return; |
228 | | - emojiPickerOpen = false; |
| 284 | + if (open) document.addEventListener('pointerdown', closeComposerPopupsFromOutside); |
| 285 | + else document.removeEventListener('pointerdown', closeComposerPopupsFromOutside); |
| 286 | + } |
| 287 | +
|
| 288 | + function closeComposerPopupsFromOutside(event: PointerEvent) { |
| 289 | + if (!(event.target instanceof Node)) return; |
| 290 | + if ( |
| 291 | + emojiPickerOpen && |
| 292 | + emojiPickerElement && |
| 293 | + emojiButtonElement && |
| 294 | + !emojiPickerElement.contains(event.target) && |
| 295 | + !emojiButtonElement.contains(event.target) |
| 296 | + ) { |
| 297 | + emojiPickerOpen = false; |
| 298 | + } |
229 | 299 | } |
230 | 300 |
|
231 | 301 | async function focusComposerStart() { |
|
394 | 464 | <MentionSuggestions profiles={mentionSuggestions} searching={searchingMentions} on:select={(event) => selectMention(event.detail)} /> |
395 | 465 | {/if} |
396 | 466 | </div> |
| 467 | + {#if emojiPickerOpen} |
| 468 | + <div class="composer-emoji-picker" bind:this={emojiPickerElement} aria-label="Emoji picker"> |
| 469 | + {#each emojiOptions as emoji} |
| 470 | + <button type="button" aria-label={`Insert ${emoji}`} on:click={() => insertEmoji(emoji)}>{emoji}</button> |
| 471 | + {/each} |
| 472 | + </div> |
| 473 | + {/if} |
397 | 474 | <div class="composer-actions"> |
398 | 475 | <input class="visually-hidden" type="file" accept="image/*" bind:this={mediaInput} on:change={(event) => uploadMedia(event.currentTarget.files?.[0])} /> |
399 | 476 | <button class="icon-button" disabled={uploading || !$session} aria-label="Add media" on:click={() => mediaInput.click()}> |
400 | 477 | {#if uploading}<Loader2 size={20} class="spin" />{:else}<ImagePlus size={20} />{/if} |
401 | 478 | </button> |
402 | | - <span class="composer-emoji-wrap" bind:this={emojiPickerElement}> |
403 | | - <button class="icon-button" type="button" disabled={!$session} aria-label="Add emoji" aria-expanded={emojiPickerOpen} on:click={() => (emojiPickerOpen = !emojiPickerOpen)}> |
404 | | - <Smile size={20} /> |
405 | | - </button> |
406 | | - {#if emojiPickerOpen} |
407 | | - <span class="composer-emoji-picker" aria-label="Emoji picker"> |
408 | | - {#each emojiOptions as emoji} |
409 | | - <button type="button" aria-label={`Insert ${emoji}`} on:click={() => insertEmoji(emoji)}>{emoji}</button> |
410 | | - {/each} |
411 | | - </span> |
412 | | - {/if} |
413 | | - </span> |
| 479 | + <button bind:this={emojiButtonElement} class="icon-button" type="button" disabled={!$session} aria-label="Add emoji" aria-expanded={emojiPickerOpen} on:click={() => (emojiPickerOpen = !emojiPickerOpen)}> |
| 480 | + <Smile size={20} /> |
| 481 | + </button> |
414 | 482 | <span class="composer-count">{content.length}/2000</span> |
415 | 483 | <button class="primary" disabled={busy || uploading || !$session || !content.trim()} on:click={submit}><Send size={18} /> {$editTarget ? 'Save edit' : 'Post'}</button> |
416 | 484 | </div> |
|
0 commit comments