this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Context-aware compose button

+496 -464
+10 -3
src/components/compose-button.jsx
··· 63 63 return; 64 64 } 65 65 66 + const composeDataElements = document.querySelectorAll('data.compose-data'); 67 + // If there's a lot of them, ignore 68 + const opts = 69 + composeDataElements.length === 1 70 + ? JSON.parse(composeDataElements[0].value) 71 + : undefined; 72 + 66 73 if (e.shiftKey) { 67 - const newWin = openCompose(); 74 + const newWin = openCompose(opts); 68 75 69 76 if (!newWin) { 70 - states.showCompose = true; 77 + states.showCompose = opts || true; 71 78 } 72 79 } else { 73 80 openOSK(); 74 - states.showCompose = true; 81 + states.showCompose = opts || true; 75 82 } 76 83 } 77 84
+2 -2
src/components/compose.jsx
··· 287 287 const focusTextarea = () => { 288 288 setTimeout(() => { 289 289 if (!textareaRef.current) return; 290 - // status starts with newline, focus on first position 291 - if (draftStatus?.status?.startsWith?.('\n')) { 290 + // status starts with newline or space, focus on first position 291 + if (/^\n|\s/.test(draftStatus?.status)) { 292 292 textareaRef.current.selectionStart = 0; 293 293 textareaRef.current.selectionEnd = 0; 294 294 }
+38 -38
src/locales/en.po
··· 100 100 101 101 #: src/components/account-info.jsx:454 102 102 #: src/components/account-info.jsx:856 103 - #: src/pages/account-statuses.jsx:482 103 + #: src/pages/account-statuses.jsx:483 104 104 #: src/pages/search.jsx:344 105 105 #: src/pages/search.jsx:491 106 106 msgid "Posts" ··· 116 116 #: src/components/status.jsx:1978 117 117 #: src/components/status.jsx:2599 118 118 #: src/components/status.jsx:2602 119 - #: src/pages/account-statuses.jsx:526 119 + #: src/pages/account-statuses.jsx:527 120 120 #: src/pages/accounts.jsx:118 121 - #: src/pages/hashtag.jsx:202 121 + #: src/pages/hashtag.jsx:203 122 122 #: src/pages/list.jsx:171 123 123 #: src/pages/public.jsx:116 124 124 #: src/pages/scheduled-posts.jsx:89 ··· 314 314 msgstr "Don't feature on profile" 315 315 316 316 #: src/components/account-info.jsx:1406 317 - #: src/pages/hashtag.jsx:331 317 + #: src/pages/hashtag.jsx:333 318 318 msgid "Feature on profile" 319 319 msgstr "" 320 320 ··· 440 440 441 441 #: src/components/account-info.jsx:1885 442 442 #: src/components/account-info.jsx:1889 443 - #: src/pages/hashtag.jsx:264 443 + #: src/pages/hashtag.jsx:265 444 444 msgid "Follow" 445 445 msgstr "" 446 446 ··· 604 604 msgid "Home" 605 605 msgstr "" 606 606 607 - #: src/components/compose-button.jsx:141 607 + #: src/components/compose-button.jsx:148 608 608 #: src/compose.jsx:38 609 609 msgid "Compose" 610 610 msgstr "" 611 611 612 - #: src/components/compose-button.jsx:168 612 + #: src/components/compose-button.jsx:175 613 613 #: src/components/nav-menu.jsx:265 614 614 #: src/pages/scheduled-posts.jsx:31 615 615 #: src/pages/scheduled-posts.jsx:78 616 616 msgid "Scheduled Posts" 617 617 msgstr "Scheduled Posts" 618 618 619 - #: src/components/compose-button.jsx:181 619 + #: src/components/compose-button.jsx:188 620 620 msgid "Add to thread" 621 621 msgstr "Add to thread" 622 622 ··· 1465 1465 #: src/pages/account-statuses.jsx:329 1466 1466 #: src/pages/filters.jsx:55 1467 1467 #: src/pages/filters.jsx:94 1468 - #: src/pages/hashtag.jsx:342 1468 + #: src/pages/hashtag.jsx:344 1469 1469 msgid "Filters" 1470 1470 msgstr "" 1471 1471 ··· 1949 1949 msgstr "" 1950 1950 1951 1951 #: src/components/shortcuts-settings.jsx:117 1952 - #: src/pages/hashtag.jsx:358 1952 + #: src/pages/hashtag.jsx:360 1953 1953 msgid "Media only" 1954 1954 msgstr "" 1955 1955 ··· 2662 2662 msgid "Showing posts in {0}" 2663 2663 msgstr "Showing posts in {0}" 2664 2664 2665 - #: src/pages/account-statuses.jsx:503 2665 + #: src/pages/account-statuses.jsx:504 2666 2666 msgid "Nothing to see here yet." 2667 2667 msgstr "" 2668 2668 2669 - #: src/pages/account-statuses.jsx:504 2669 + #: src/pages/account-statuses.jsx:505 2670 2670 #: src/pages/public.jsx:99 2671 2671 #: src/pages/trending.jsx:452 2672 2672 msgid "Unable to load posts" 2673 2673 msgstr "" 2674 2674 2675 - #: src/pages/account-statuses.jsx:545 2676 - #: src/pages/account-statuses.jsx:575 2675 + #: src/pages/account-statuses.jsx:546 2676 + #: src/pages/account-statuses.jsx:576 2677 2677 msgid "Unable to fetch account info" 2678 2678 msgstr "" 2679 2679 2680 2680 #. placeholder {0}: accountInstance ? ( <> {' '} (<b>{punycode.toUnicode(accountInstance)}</b>) </> ) : null 2681 - #: src/pages/account-statuses.jsx:552 2681 + #: src/pages/account-statuses.jsx:553 2682 2682 msgid "Switch to account's instance {0}" 2683 2683 msgstr "" 2684 2684 2685 - #: src/pages/account-statuses.jsx:582 2685 + #: src/pages/account-statuses.jsx:583 2686 2686 msgid "Switch to my instance (<0>{currentInstance}</0>)" 2687 2687 msgstr "" 2688 2688 2689 - #: src/pages/account-statuses.jsx:644 2689 + #: src/pages/account-statuses.jsx:656 2690 2690 msgid "Month" 2691 2691 msgstr "" 2692 2692 ··· 3173 3173 msgid "{hashtagTitle}" 3174 3174 msgstr "" 3175 3175 3176 - #: src/pages/hashtag.jsx:184 3176 + #: src/pages/hashtag.jsx:185 3177 3177 msgid "No one has posted anything with this tag yet." 3178 3178 msgstr "" 3179 3179 3180 - #: src/pages/hashtag.jsx:185 3180 + #: src/pages/hashtag.jsx:186 3181 3181 msgid "Unable to load posts with this tag" 3182 3182 msgstr "" 3183 3183 3184 - #: src/pages/hashtag.jsx:211 3184 + #: src/pages/hashtag.jsx:212 3185 3185 msgid "Unfollow #{hashtag}?" 3186 3186 msgstr "Unfollow #{hashtag}?" 3187 3187 3188 - #: src/pages/hashtag.jsx:226 3188 + #: src/pages/hashtag.jsx:227 3189 3189 msgid "Unfollowed #{hashtag}" 3190 3190 msgstr "" 3191 3191 3192 - #: src/pages/hashtag.jsx:241 3192 + #: src/pages/hashtag.jsx:242 3193 3193 msgid "Followed #{hashtag}" 3194 3194 msgstr "" 3195 3195 3196 - #: src/pages/hashtag.jsx:257 3196 + #: src/pages/hashtag.jsx:258 3197 3197 msgid "Following…" 3198 3198 msgstr "" 3199 3199 3200 - #: src/pages/hashtag.jsx:285 3200 + #: src/pages/hashtag.jsx:287 3201 3201 msgid "Unfeatured on profile" 3202 3202 msgstr "" 3203 3203 3204 - #: src/pages/hashtag.jsx:299 3204 + #: src/pages/hashtag.jsx:301 3205 3205 msgid "Unable to unfeature on profile" 3206 3206 msgstr "" 3207 3207 3208 - #: src/pages/hashtag.jsx:308 3209 - #: src/pages/hashtag.jsx:324 3208 + #: src/pages/hashtag.jsx:310 3209 + #: src/pages/hashtag.jsx:326 3210 3210 msgid "Featured on profile" 3211 3211 msgstr "" 3212 3212 3213 - #: src/pages/hashtag.jsx:396 3213 + #: src/pages/hashtag.jsx:398 3214 3214 msgid "{TOTAL_TAGS_LIMIT, plural, other {Max # tags}}" 3215 3215 msgstr "" 3216 3216 3217 - #: src/pages/hashtag.jsx:399 3217 + #: src/pages/hashtag.jsx:401 3218 3218 msgid "Add hashtag" 3219 3219 msgstr "" 3220 3220 3221 - #: src/pages/hashtag.jsx:431 3221 + #: src/pages/hashtag.jsx:433 3222 3222 msgid "Remove hashtag" 3223 3223 msgstr "" 3224 3224 3225 - #: src/pages/hashtag.jsx:445 3225 + #: src/pages/hashtag.jsx:447 3226 3226 msgid "{SHORTCUTS_LIMIT, plural, one {Max # shortcut reached. Unable to add shortcut.} other {Max # shortcuts reached. Unable to add shortcut.}}" 3227 3227 msgstr "" 3228 3228 3229 - #: src/pages/hashtag.jsx:474 3229 + #: src/pages/hashtag.jsx:476 3230 3230 msgid "This shortcut already exists" 3231 3231 msgstr "" 3232 3232 3233 - #: src/pages/hashtag.jsx:477 3233 + #: src/pages/hashtag.jsx:479 3234 3234 msgid "Hashtag shortcut added" 3235 3235 msgstr "" 3236 3236 3237 - #: src/pages/hashtag.jsx:483 3237 + #: src/pages/hashtag.jsx:485 3238 3238 msgid "Add to Shortcuts" 3239 3239 msgstr "" 3240 3240 3241 - #: src/pages/hashtag.jsx:489 3241 + #: src/pages/hashtag.jsx:491 3242 3242 #: src/pages/public.jsx:141 3243 3243 #: src/pages/trending.jsx:481 3244 3244 msgid "Enter a new instance e.g. \"mastodon.social\"" 3245 3245 msgstr "" 3246 3246 3247 - #: src/pages/hashtag.jsx:492 3247 + #: src/pages/hashtag.jsx:494 3248 3248 #: src/pages/public.jsx:144 3249 3249 #: src/pages/trending.jsx:484 3250 3250 msgid "Invalid instance" 3251 3251 msgstr "" 3252 3252 3253 - #: src/pages/hashtag.jsx:506 3253 + #: src/pages/hashtag.jsx:508 3254 3254 #: src/pages/public.jsx:158 3255 3255 #: src/pages/trending.jsx:496 3256 3256 msgid "Go to another instance…" 3257 3257 msgstr "" 3258 3258 3259 - #: src/pages/hashtag.jsx:519 3259 + #: src/pages/hashtag.jsx:521 3260 3260 #: src/pages/public.jsx:171 3261 3261 #: src/pages/trending.jsx:507 3262 3262 msgid "Go to my instance (<0>{currentInstance}</0>)"
+103 -91
src/pages/account-statuses.jsx
··· 477 477 const allowSwitch = !!account && !sameInstance; 478 478 479 479 return ( 480 - <Timeline 481 - key={id} 482 - title={`${account?.acct ? '@' + account.acct : t`Posts`}`} 483 - titleComponent={ 484 - <h1 485 - class="header-double-lines header-account" 486 - // onClick={() => { 487 - // states.showAccount = { 488 - // account, 489 - // instance, 490 - // }; 491 - // }} 492 - > 493 - <b> 494 - <EmojiText text={displayName} emojis={emojis} /> 495 - </b> 496 - <div> 497 - <span class="bidi-isolate">@{acct}</span> 498 - </div> 499 - </h1> 500 - } 501 - id="account-statuses" 502 - instance={instance} 503 - emptyText={t`Nothing to see here yet.`} 504 - errorText={t`Unable to load posts`} 505 - fetchItems={fetchAccountStatuses} 506 - useItemID 507 - view={media || mediaFirst ? 'media' : undefined} 508 - boostsCarousel={snapStates.settings.boostsCarousel} 509 - timelineStart={TimelineStart} 510 - refresh={[ 511 - excludeReplies, 512 - excludeBoosts, 513 - tagged, 514 - media, 515 - month + account?.acct, 516 - ].toString()} 517 - headerEnd={ 518 - <Menu2 519 - portal 520 - // setDownOverflow 521 - overflow="auto" 522 - viewScroll="close" 523 - position="anchor" 524 - menuButton={ 525 - <button type="button" class="plain"> 526 - <Icon icon="more" size="l" alt={t`More`} /> 527 - </button> 528 - } 529 - > 530 - <MenuItem 531 - disabled={!allowSwitch} 532 - onClick={() => { 533 - (async () => { 534 - try { 535 - const { masto } = api({ 536 - instance: accountInstance, 537 - }); 538 - const acc = await masto.v1.accounts.lookup({ 539 - acct: account.acct, 540 - }); 541 - const { id } = acc; 542 - location.hash = `/${accountInstance}/a/${id}`; 543 - } catch (e) { 544 - console.error(e); 545 - alert(t`Unable to fetch account info`); 546 - } 547 - })(); 548 - }} 480 + <> 481 + <Timeline 482 + key={id} 483 + title={`${account?.acct ? '@' + account.acct : t`Posts`}`} 484 + titleComponent={ 485 + <h1 486 + class="header-double-lines header-account" 487 + // onClick={() => { 488 + // states.showAccount = { 489 + // account, 490 + // instance, 491 + // }; 492 + // }} 549 493 > 550 - <Icon icon="transfer" />{' '} 551 - <small class="menu-double-lines"> 552 - <Trans> 553 - Switch to account's instance{' '} 554 - {accountInstance ? ( 555 - <> 556 - {' '} 557 - (<b>{punycode.toUnicode(accountInstance)}</b>) 558 - </> 559 - ) : null} 560 - </Trans> 561 - </small> 562 - </MenuItem> 563 - {!sameCurrentInstance && ( 494 + <b> 495 + <EmojiText text={displayName} emojis={emojis} /> 496 + </b> 497 + <div> 498 + <span class="bidi-isolate">@{acct}</span> 499 + </div> 500 + </h1> 501 + } 502 + id="account-statuses" 503 + instance={instance} 504 + emptyText={t`Nothing to see here yet.`} 505 + errorText={t`Unable to load posts`} 506 + fetchItems={fetchAccountStatuses} 507 + useItemID 508 + view={media || mediaFirst ? 'media' : undefined} 509 + boostsCarousel={snapStates.settings.boostsCarousel} 510 + timelineStart={TimelineStart} 511 + refresh={[ 512 + excludeReplies, 513 + excludeBoosts, 514 + tagged, 515 + media, 516 + month + account?.acct, 517 + ].toString()} 518 + headerEnd={ 519 + <Menu2 520 + portal 521 + // setDownOverflow 522 + overflow="auto" 523 + viewScroll="close" 524 + position="anchor" 525 + menuButton={ 526 + <button type="button" class="plain"> 527 + <Icon icon="more" size="l" alt={t`More`} /> 528 + </button> 529 + } 530 + > 564 531 <MenuItem 532 + disabled={!allowSwitch} 565 533 onClick={() => { 566 534 (async () => { 567 535 try { 568 - const acc = await currentMasto.v1.accounts.lookup({ 569 - acct: account.acct + '@' + instance, 536 + const { masto } = api({ 537 + instance: accountInstance, 538 + }); 539 + const acc = await masto.v1.accounts.lookup({ 540 + acct: account.acct, 570 541 }); 571 542 const { id } = acc; 572 - location.hash = `/${currentInstance}/a/${id}`; 543 + location.hash = `/${accountInstance}/a/${id}`; 573 544 } catch (e) { 574 545 console.error(e); 575 546 alert(t`Unable to fetch account info`); ··· 580 551 <Icon icon="transfer" />{' '} 581 552 <small class="menu-double-lines"> 582 553 <Trans> 583 - Switch to my instance (<b>{currentInstance}</b>) 554 + Switch to account's instance{' '} 555 + {accountInstance ? ( 556 + <> 557 + {' '} 558 + (<b>{punycode.toUnicode(accountInstance)}</b>) 559 + </> 560 + ) : null} 584 561 </Trans> 585 562 </small> 586 563 </MenuItem> 587 - )} 588 - </Menu2> 589 - } 590 - /> 564 + {!sameCurrentInstance && ( 565 + <MenuItem 566 + onClick={() => { 567 + (async () => { 568 + try { 569 + const acc = await currentMasto.v1.accounts.lookup({ 570 + acct: account.acct + '@' + instance, 571 + }); 572 + const { id } = acc; 573 + location.hash = `/${currentInstance}/a/${id}`; 574 + } catch (e) { 575 + console.error(e); 576 + alert(t`Unable to fetch account info`); 577 + } 578 + })(); 579 + }} 580 + > 581 + <Icon icon="transfer" />{' '} 582 + <small class="menu-double-lines"> 583 + <Trans> 584 + Switch to my instance (<b>{currentInstance}</b>) 585 + </Trans> 586 + </small> 587 + </MenuItem> 588 + )} 589 + </Menu2> 590 + } 591 + /> 592 + {acct && ( 593 + <data 594 + class="compose-data" 595 + value={JSON.stringify({ 596 + draftStatus: { 597 + status: `@${acct} `, 598 + }, 599 + })} 600 + /> 601 + )} 602 + </> 591 603 ); 592 604 } 593 605
+343 -330
src/pages/hashtag.jsx
··· 168 168 }, []); 169 169 170 170 return ( 171 - <Timeline 172 - key={instance + hashtagTitle} 173 - title={title} 174 - titleComponent={ 175 - !!instance && ( 176 - <h1 class="header-double-lines"> 177 - <b dir="auto">{hashtagTitle}</b> 178 - <div>{instance}</div> 179 - </h1> 180 - ) 181 - } 182 - id="hashtag" 183 - instance={instance} 184 - emptyText={t`No one has posted anything with this tag yet.`} 185 - errorText={t`Unable to load posts with this tag`} 186 - fetchItems={fetchHashtags} 187 - checkForUpdates={checkForUpdates} 188 - useItemID 189 - view={media || mediaFirst ? 'media' : undefined} 190 - refresh={media} 191 - // allowFilters 192 - filterContext="public" 193 - headerEnd={ 194 - <Menu2 195 - portal 196 - setDownOverflow 197 - overflow="auto" 198 - // viewScroll="close" 199 - position="anchor" 200 - menuButton={ 201 - <button type="button" class="plain"> 202 - <Icon icon="more" size="l" alt={t`More`} /> 203 - </button> 204 - } 205 - > 206 - {!!info && hashtags.length === 1 && ( 207 - <> 208 - <MenuConfirm 209 - subMenu 210 - confirm={info.following} 211 - confirmLabel={t`Unfollow #${hashtag}?`} 212 - disabled={followUIState === 'loading' || !authenticated} 213 - onClick={() => { 214 - setFollowUIState('loading'); 215 - if (info.following) { 216 - // const yes = confirm(`Unfollow #${hashtag}?`); 217 - // if (!yes) { 218 - // setFollowUIState('default'); 219 - // return; 220 - // } 221 - masto.v1.tags 222 - .$select(hashtag) 223 - .unfollow() 224 - .then(() => { 225 - setInfo({ ...info, following: false }); 226 - showToast(t`Unfollowed #${hashtag}`); 227 - }) 228 - .catch((e) => { 229 - alert(e); 230 - console.error(e); 231 - }) 232 - .finally(() => { 233 - setFollowUIState('default'); 234 - }); 235 - } else { 236 - masto.v1.tags 237 - .$select(hashtag) 238 - .follow() 239 - .then(() => { 240 - setInfo({ ...info, following: true }); 241 - showToast(t`Followed #${hashtag}`); 242 - }) 243 - .catch((e) => { 244 - alert(e); 245 - console.error(e); 246 - }) 247 - .finally(() => { 248 - setFollowUIState('default'); 249 - }); 250 - } 251 - }} 252 - > 253 - {info.following ? ( 254 - <> 255 - <Icon icon="check-circle" />{' '} 256 - <span> 257 - <Trans>Following…</Trans> 258 - </span> 259 - </> 260 - ) : ( 261 - <> 262 - <Icon icon="plus" />{' '} 263 - <span> 264 - <Trans>Follow</Trans> 265 - </span> 266 - </> 267 - )} 268 - </MenuConfirm> 269 - <MenuItem 270 - type="checkbox" 271 - checked={isFeaturedTag} 272 - disabled={featuredUIState === 'loading' || !authenticated} 273 - onClick={() => { 274 - setFeaturedUIState('loading'); 275 - if (isFeaturedTag) { 276 - const featuredTagID = featuredTags.find( 277 - (tag) => tag.name.toLowerCase() === hashtag.toLowerCase(), 278 - ).id; 279 - if (featuredTagID) { 171 + <> 172 + <Timeline 173 + key={instance + hashtagTitle} 174 + title={title} 175 + titleComponent={ 176 + !!instance && ( 177 + <h1 class="header-double-lines"> 178 + <b dir="auto">{hashtagTitle}</b> 179 + <div>{instance}</div> 180 + </h1> 181 + ) 182 + } 183 + id="hashtag" 184 + instance={instance} 185 + emptyText={t`No one has posted anything with this tag yet.`} 186 + errorText={t`Unable to load posts with this tag`} 187 + fetchItems={fetchHashtags} 188 + checkForUpdates={checkForUpdates} 189 + useItemID 190 + view={media || mediaFirst ? 'media' : undefined} 191 + refresh={media} 192 + // allowFilters 193 + filterContext="public" 194 + headerEnd={ 195 + <Menu2 196 + portal 197 + setDownOverflow 198 + overflow="auto" 199 + // viewScroll="close" 200 + position="anchor" 201 + menuButton={ 202 + <button type="button" class="plain"> 203 + <Icon icon="more" size="l" alt={t`More`} /> 204 + </button> 205 + } 206 + > 207 + {!!info && hashtags.length === 1 && ( 208 + <> 209 + <MenuConfirm 210 + subMenu 211 + confirm={info.following} 212 + confirmLabel={t`Unfollow #${hashtag}?`} 213 + disabled={followUIState === 'loading' || !authenticated} 214 + onClick={() => { 215 + setFollowUIState('loading'); 216 + if (info.following) { 217 + // const yes = confirm(`Unfollow #${hashtag}?`); 218 + // if (!yes) { 219 + // setFollowUIState('default'); 220 + // return; 221 + // } 222 + masto.v1.tags 223 + .$select(hashtag) 224 + .unfollow() 225 + .then(() => { 226 + setInfo({ ...info, following: false }); 227 + showToast(t`Unfollowed #${hashtag}`); 228 + }) 229 + .catch((e) => { 230 + alert(e); 231 + console.error(e); 232 + }) 233 + .finally(() => { 234 + setFollowUIState('default'); 235 + }); 236 + } else { 237 + masto.v1.tags 238 + .$select(hashtag) 239 + .follow() 240 + .then(() => { 241 + setInfo({ ...info, following: true }); 242 + showToast(t`Followed #${hashtag}`); 243 + }) 244 + .catch((e) => { 245 + alert(e); 246 + console.error(e); 247 + }) 248 + .finally(() => { 249 + setFollowUIState('default'); 250 + }); 251 + } 252 + }} 253 + > 254 + {info.following ? ( 255 + <> 256 + <Icon icon="check-circle" />{' '} 257 + <span> 258 + <Trans>Following…</Trans> 259 + </span> 260 + </> 261 + ) : ( 262 + <> 263 + <Icon icon="plus" />{' '} 264 + <span> 265 + <Trans>Follow</Trans> 266 + </span> 267 + </> 268 + )} 269 + </MenuConfirm> 270 + <MenuItem 271 + type="checkbox" 272 + checked={isFeaturedTag} 273 + disabled={featuredUIState === 'loading' || !authenticated} 274 + onClick={() => { 275 + setFeaturedUIState('loading'); 276 + if (isFeaturedTag) { 277 + const featuredTagID = featuredTags.find( 278 + (tag) => 279 + tag.name.toLowerCase() === hashtag.toLowerCase(), 280 + ).id; 281 + if (featuredTagID) { 282 + masto.v1.featuredTags 283 + .$select(featuredTagID) 284 + .remove() 285 + .then(() => { 286 + setIsFeaturedTag(false); 287 + showToast(t`Unfeatured on profile`); 288 + setFeaturedTags( 289 + featuredTags.filter( 290 + (tag) => tag.id !== featuredTagID, 291 + ), 292 + ); 293 + }) 294 + .catch((e) => { 295 + console.error(e); 296 + }) 297 + .finally(() => { 298 + setFeaturedUIState('default'); 299 + }); 300 + } else { 301 + showToast(t`Unable to unfeature on profile`); 302 + } 303 + } else { 280 304 masto.v1.featuredTags 281 - .$select(featuredTagID) 282 - .remove() 283 - .then(() => { 284 - setIsFeaturedTag(false); 285 - showToast(t`Unfeatured on profile`); 286 - setFeaturedTags( 287 - featuredTags.filter( 288 - (tag) => tag.id !== featuredTagID, 289 - ), 290 - ); 305 + .create({ 306 + name: hashtag, 307 + }) 308 + .then((value) => { 309 + setIsFeaturedTag(true); 310 + showToast(t`Featured on profile`); 311 + setFeaturedTags(featuredTags.concat(value)); 291 312 }) 292 313 .catch((e) => { 293 314 console.error(e); ··· 295 316 .finally(() => { 296 317 setFeaturedUIState('default'); 297 318 }); 319 + } 320 + }} 321 + > 322 + {isFeaturedTag ? ( 323 + <> 324 + <Icon icon="check-circle" /> 325 + <span> 326 + <Trans>Featured on profile</Trans> 327 + </span> 328 + </> 329 + ) : ( 330 + <> 331 + <Icon icon="check-circle" /> 332 + <span> 333 + <Trans>Feature on profile</Trans> 334 + </span> 335 + </> 336 + )} 337 + </MenuItem> 338 + <MenuDivider /> 339 + </> 340 + )} 341 + {!mediaFirst && ( 342 + <> 343 + <MenuHeader className="plain"> 344 + <Trans>Filters</Trans> 345 + </MenuHeader> 346 + <MenuItem 347 + type="checkbox" 348 + checked={!!media} 349 + onClick={() => { 350 + if (media) { 351 + searchParams.delete('media'); 298 352 } else { 299 - showToast(t`Unable to unfeature on profile`); 353 + searchParams.set('media', '1'); 354 + } 355 + setSearchParams(searchParams); 356 + }} 357 + > 358 + <Icon icon="check-circle" alt="☑️" />{' '} 359 + <span class="menu-grow"> 360 + <Trans>Media only</Trans> 361 + </span> 362 + </MenuItem> 363 + <MenuDivider /> 364 + </> 365 + )} 366 + <FocusableItem className="menu-field" disabled={reachLimit}> 367 + {({ ref }) => ( 368 + <form 369 + onSubmit={(e) => { 370 + e.preventDefault(); 371 + const newHashtag = e.target[0].value?.trim?.(); 372 + // Use includes but need to be case insensitive 373 + if ( 374 + newHashtag && 375 + !hashtags.some( 376 + (t) => t.toLowerCase() === newHashtag.toLowerCase(), 377 + ) 378 + ) { 379 + hashtags.push(newHashtag); 380 + hashtags.sort(); 381 + // navigate( 382 + // instance 383 + // ? `/${instance}/t/${hashtags.join('+')}` 384 + // : `/t/${hashtags.join('+')}`, 385 + // ); 386 + location.hash = instance 387 + ? `/${instance}/t/${hashtags.join('+')}${linkParams}` 388 + : `/t/${hashtags.join('+')}${linkParams}`; 389 + } 390 + }} 391 + > 392 + <Icon icon="hashtag" /> 393 + <input 394 + ref={ref} 395 + type="text" 396 + placeholder={ 397 + reachLimit 398 + ? plural(TOTAL_TAGS_LIMIT, { 399 + other: 'Max # tags', 400 + }) 401 + : t`Add hashtag` 300 402 } 301 - } else { 302 - masto.v1.featuredTags 303 - .create({ 304 - name: hashtag, 305 - }) 306 - .then((value) => { 307 - setIsFeaturedTag(true); 308 - showToast(t`Featured on profile`); 309 - setFeaturedTags(featuredTags.concat(value)); 310 - }) 311 - .catch((e) => { 312 - console.error(e); 313 - }) 314 - .finally(() => { 315 - setFeaturedUIState('default'); 316 - }); 317 - } 318 - }} 319 - > 320 - {isFeaturedTag ? ( 321 - <> 322 - <Icon icon="check-circle" /> 323 - <span> 324 - <Trans>Featured on profile</Trans> 325 - </span> 326 - </> 327 - ) : ( 328 - <> 329 - <Icon icon="check-circle" /> 330 - <span> 331 - <Trans>Feature on profile</Trans> 332 - </span> 333 - </> 334 - )} 335 - </MenuItem> 336 - <MenuDivider /> 337 - </> 338 - )} 339 - {!mediaFirst && ( 340 - <> 341 - <MenuHeader className="plain"> 342 - <Trans>Filters</Trans> 343 - </MenuHeader> 344 - <MenuItem 345 - type="checkbox" 346 - checked={!!media} 347 - onClick={() => { 348 - if (media) { 349 - searchParams.delete('media'); 350 - } else { 351 - searchParams.set('media', '1'); 352 - } 353 - setSearchParams(searchParams); 354 - }} 355 - > 356 - <Icon icon="check-circle" alt="☑️" />{' '} 357 - <span class="menu-grow"> 358 - <Trans>Media only</Trans> 359 - </span> 360 - </MenuItem> 361 - <MenuDivider /> 362 - </> 363 - )} 364 - <FocusableItem className="menu-field" disabled={reachLimit}> 365 - {({ ref }) => ( 366 - <form 367 - onSubmit={(e) => { 368 - e.preventDefault(); 369 - const newHashtag = e.target[0].value?.trim?.(); 370 - // Use includes but need to be case insensitive 371 - if ( 372 - newHashtag && 373 - !hashtags.some( 374 - (t) => t.toLowerCase() === newHashtag.toLowerCase(), 375 - ) 376 - ) { 377 - hashtags.push(newHashtag); 403 + required 404 + autocorrect="off" 405 + autocapitalize="off" 406 + spellCheck={false} 407 + // no spaces, no hashtags 408 + pattern="[^#][^\s#]+[^#]" 409 + disabled={reachLimit} 410 + dir="auto" 411 + /> 412 + </form> 413 + )} 414 + </FocusableItem> 415 + <MenuGroup takeOverflow> 416 + {hashtags.map((tag, i) => ( 417 + <MenuItem 418 + key={tag} 419 + disabled={hashtags.length === 1} 420 + onClick={(e) => { 421 + hashtags.splice(i, 1); 378 422 hashtags.sort(); 379 423 // navigate( 380 424 // instance ··· 384 428 location.hash = instance 385 429 ? `/${instance}/t/${hashtags.join('+')}${linkParams}` 386 430 : `/t/${hashtags.join('+')}${linkParams}`; 387 - } 388 - }} 389 - > 390 - <Icon icon="hashtag" /> 391 - <input 392 - ref={ref} 393 - type="text" 394 - placeholder={ 395 - reachLimit 396 - ? plural(TOTAL_TAGS_LIMIT, { 397 - other: 'Max # tags', 398 - }) 399 - : t`Add hashtag` 400 - } 401 - required 402 - autocorrect="off" 403 - autocapitalize="off" 404 - spellCheck={false} 405 - // no spaces, no hashtags 406 - pattern="[^#][^\s#]+[^#]" 407 - disabled={reachLimit} 408 - dir="auto" 409 - /> 410 - </form> 411 - )} 412 - </FocusableItem> 413 - <MenuGroup takeOverflow> 414 - {hashtags.map((tag, i) => ( 415 - <MenuItem 416 - key={tag} 417 - disabled={hashtags.length === 1} 418 - onClick={(e) => { 419 - hashtags.splice(i, 1); 420 - hashtags.sort(); 421 - // navigate( 422 - // instance 423 - // ? `/${instance}/t/${hashtags.join('+')}` 424 - // : `/t/${hashtags.join('+')}`, 425 - // ); 426 - location.hash = instance 427 - ? `/${instance}/t/${hashtags.join('+')}${linkParams}` 428 - : `/t/${hashtags.join('+')}${linkParams}`; 429 - }} 430 - > 431 - <Icon icon="x" alt={t`Remove hashtag`} class="danger-icon" /> 432 - <span class="bidi-isolate"> 433 - <span class="more-insignificant">#</span> 434 - {tag} 435 - </span> 436 - </MenuItem> 437 - ))} 438 - </MenuGroup> 439 - <MenuDivider /> 440 - <MenuItem 441 - disabled={!currentAuthenticated} 442 - onClick={() => { 443 - if (states.shortcuts.length >= SHORTCUTS_LIMIT) { 444 - alert( 445 - plural(SHORTCUTS_LIMIT, { 446 - one: 'Max # shortcut reached. Unable to add shortcut.', 447 - other: 'Max # shortcuts reached. Unable to add shortcut.', 448 - }), 449 - ); 450 - return; 451 - } 452 - const shortcut = { 453 - type: 'hashtag', 454 - hashtag: hashtags.join(' '), 455 - instance, 456 - media: media ? 'on' : undefined, 457 - }; 458 - // Check if already exists 459 - const exists = states.shortcuts.some( 460 - (s) => 461 - s.type === shortcut.type && 462 - s.hashtag 463 - .split(/[\s+]+/) 464 - .sort() 465 - .join(' ') === 466 - shortcut.hashtag 431 + }} 432 + > 433 + <Icon icon="x" alt={t`Remove hashtag`} class="danger-icon" /> 434 + <span class="bidi-isolate"> 435 + <span class="more-insignificant">#</span> 436 + {tag} 437 + </span> 438 + </MenuItem> 439 + ))} 440 + </MenuGroup> 441 + <MenuDivider /> 442 + <MenuItem 443 + disabled={!currentAuthenticated} 444 + onClick={() => { 445 + if (states.shortcuts.length >= SHORTCUTS_LIMIT) { 446 + alert( 447 + plural(SHORTCUTS_LIMIT, { 448 + one: 'Max # shortcut reached. Unable to add shortcut.', 449 + other: 'Max # shortcuts reached. Unable to add shortcut.', 450 + }), 451 + ); 452 + return; 453 + } 454 + const shortcut = { 455 + type: 'hashtag', 456 + hashtag: hashtags.join(' '), 457 + instance, 458 + media: media ? 'on' : undefined, 459 + }; 460 + // Check if already exists 461 + const exists = states.shortcuts.some( 462 + (s) => 463 + s.type === shortcut.type && 464 + s.hashtag 467 465 .split(/[\s+]+/) 468 466 .sort() 469 - .join(' ') && 470 - (s.instance ? s.instance === shortcut.instance : true) && 471 - (s.media ? !!s.media === !!shortcut.media : true), 472 - ); 473 - if (exists) { 474 - alert(t`This shortcut already exists`); 475 - } else { 476 - states.shortcuts.push(shortcut); 477 - showToast(t`Hashtag shortcut added`); 478 - } 479 - }} 480 - > 481 - <Icon icon="shortcut" />{' '} 482 - <span> 483 - <Trans>Add to Shortcuts</Trans> 484 - </span> 485 - </MenuItem> 486 - <MenuItem 487 - onClick={() => { 488 - let newInstance = prompt( 489 - t`Enter a new instance e.g. "mastodon.social"`, 490 - ); 491 - if (!/\./.test(newInstance)) { 492 - if (newInstance) alert(t`Invalid instance`); 493 - return; 494 - } 495 - if (newInstance) { 496 - newInstance = newInstance.toLowerCase().trim(); 497 - // navigate(`/${newInstance}/t/${hashtags.join('+')}`); 498 - location.hash = `/${newInstance}/t/${hashtags.join( 499 - '+', 500 - )}${linkParams}`; 501 - } 502 - }} 503 - > 504 - <Icon icon="bus" />{' '} 505 - <span> 506 - <Trans>Go to another instance…</Trans> 507 - </span> 508 - </MenuItem> 509 - {currentInstance !== instance && ( 467 + .join(' ') === 468 + shortcut.hashtag 469 + .split(/[\s+]+/) 470 + .sort() 471 + .join(' ') && 472 + (s.instance ? s.instance === shortcut.instance : true) && 473 + (s.media ? !!s.media === !!shortcut.media : true), 474 + ); 475 + if (exists) { 476 + alert(t`This shortcut already exists`); 477 + } else { 478 + states.shortcuts.push(shortcut); 479 + showToast(t`Hashtag shortcut added`); 480 + } 481 + }} 482 + > 483 + <Icon icon="shortcut" />{' '} 484 + <span> 485 + <Trans>Add to Shortcuts</Trans> 486 + </span> 487 + </MenuItem> 510 488 <MenuItem 511 489 onClick={() => { 512 - location.hash = `/${currentInstance}/t/${hashtags.join( 513 - '+', 514 - )}${linkParams}`; 490 + let newInstance = prompt( 491 + t`Enter a new instance e.g. "mastodon.social"`, 492 + ); 493 + if (!/\./.test(newInstance)) { 494 + if (newInstance) alert(t`Invalid instance`); 495 + return; 496 + } 497 + if (newInstance) { 498 + newInstance = newInstance.toLowerCase().trim(); 499 + // navigate(`/${newInstance}/t/${hashtags.join('+')}`); 500 + location.hash = `/${newInstance}/t/${hashtags.join( 501 + '+', 502 + )}${linkParams}`; 503 + } 515 504 }} 516 505 > 517 506 <Icon icon="bus" />{' '} 518 - <small class="menu-double-lines"> 519 - <Trans> 520 - Go to my instance (<b>{currentInstance}</b>) 521 - </Trans> 522 - </small> 507 + <span> 508 + <Trans>Go to another instance…</Trans> 509 + </span> 523 510 </MenuItem> 524 - )} 525 - </Menu2> 526 - } 527 - /> 511 + {currentInstance !== instance && ( 512 + <MenuItem 513 + onClick={() => { 514 + location.hash = `/${currentInstance}/t/${hashtags.join( 515 + '+', 516 + )}${linkParams}`; 517 + }} 518 + > 519 + <Icon icon="bus" />{' '} 520 + <small class="menu-double-lines"> 521 + <Trans> 522 + Go to my instance (<b>{currentInstance}</b>) 523 + </Trans> 524 + </small> 525 + </MenuItem> 526 + )} 527 + </Menu2> 528 + } 529 + /> 530 + {!!hashtags?.length && ( 531 + <data 532 + class="compose-data" 533 + value={JSON.stringify({ 534 + draftStatus: { 535 + status: `${hashtags.length > 1 ? '\n\n' : ' '}${hashtagTitle}`, 536 + }, 537 + })} 538 + /> 539 + )} 540 + </> 528 541 ); 529 542 } 530 543