Bluesky app fork with some witchin' additions 💫

Merge branch 'main' of https://github.com/bluesky-social/social-app

+269 -804
+15 -4
src/components/dialogs/PostInteractionSettingsDialog.tsx
··· 13 13 import {logger} from '#/logger' 14 14 import {STALE} from '#/state/queries' 15 15 import {useMyListsQuery} from '#/state/queries/my-lists' 16 + import {useGetPost} from '#/state/queries/post' 16 17 import { 17 18 createPostgateQueryKey, 18 19 getPostgateRecord, ··· 25 26 } from '#/state/queries/postgate/util' 26 27 import { 27 28 createThreadgateViewQueryKey, 28 - getThreadgateView, 29 29 type ThreadgateAllowUISetting, 30 30 threadgateViewToAllowUISetting, 31 31 useSetThreadgateAllowMutation, 32 32 useThreadgateViewQuery, 33 33 } from '#/state/queries/threadgate' 34 + import { 35 + PostThreadContextProvider, 36 + usePostThreadContext, 37 + } from '#/state/queries/usePostThread' 34 38 import {useAgent, useSession} from '#/state/session' 35 39 import * as Toast from '#/view/com/util/Toast' 36 40 import {atoms as a, useTheme} from '#/alf' ··· 133 137 export function PostInteractionSettingsDialog( 134 138 props: PostInteractionSettingsDialogProps, 135 139 ) { 140 + const postThreadContext = usePostThreadContext() 136 141 return ( 137 142 <Dialog.Outer control={props.control}> 138 143 <Dialog.Handle /> 139 - <PostInteractionSettingsDialogControlledInner {...props} /> 144 + <PostThreadContextProvider context={postThreadContext}> 145 + <PostInteractionSettingsDialogControlledInner {...props} /> 146 + </PostThreadContextProvider> 140 147 </Dialog.Outer> 141 148 ) 142 149 } ··· 558 565 }) { 559 566 const queryClient = useQueryClient() 560 567 const agent = useAgent() 568 + const getPost = useGetPost() 561 569 562 570 return React.useCallback(async () => { 563 571 try { ··· 570 578 }), 571 579 queryClient.prefetchQuery({ 572 580 queryKey: createThreadgateViewQueryKey(rootPostUri), 573 - queryFn: () => getThreadgateView({agent, postUri: rootPostUri}), 581 + queryFn: async () => { 582 + const post = await getPost({uri: rootPostUri}) 583 + return post.threadgate ?? null 584 + }, 574 585 staleTime: STALE.SECONDS.THIRTY, 575 586 }), 576 587 ]) ··· 579 590 safeMessage: e.message, 580 591 }) 581 592 } 582 - }, [queryClient, agent, postUri, rootPostUri]) 593 + }, [queryClient, agent, postUri, rootPostUri, getPost]) 583 594 }
+78 -74
src/locale/locales/en/messages.po
··· 712 712 msgid "Add another account" 713 713 msgstr "" 714 714 715 - #: src/view/com/composer/Composer.tsx:853 715 + #: src/view/com/composer/Composer.tsx:852 716 716 msgid "Add another post" 717 717 msgstr "" 718 718 719 - #: src/view/com/composer/Composer.tsx:1490 719 + #: src/view/com/composer/Composer.tsx:1489 720 720 msgid "Add another post to thread" 721 721 msgstr "" 722 722 ··· 911 911 msgid "Allow others to be notified of your posts" 912 912 msgstr "" 913 913 914 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:348 914 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:355 915 915 msgid "Allow quote posts" 916 916 msgstr "" 917 917 918 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:393 918 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:400 919 919 msgid "Allow replies from:" 920 920 msgstr "" 921 921 ··· 1225 1225 msgid "Are you sure you want to remove this from your feeds?" 1226 1226 msgstr "" 1227 1227 1228 - #: src/view/com/composer/Composer.tsx:802 1228 + #: src/view/com/composer/Composer.tsx:801 1229 1229 msgid "Are you sure you'd like to discard this draft?" 1230 1230 msgstr "" 1231 1231 1232 - #: src/view/com/composer/Composer.tsx:992 1232 + #: src/view/com/composer/Composer.tsx:991 1233 1233 msgid "Are you sure you'd like to discard this post?" 1234 1234 msgstr "" 1235 1235 ··· 1630 1630 #: src/screens/Settings/Settings.tsx:289 1631 1631 #: src/screens/Takendown.tsx:108 1632 1632 #: src/screens/Takendown.tsx:111 1633 - #: src/view/com/composer/Composer.tsx:1047 1634 - #: src/view/com/composer/Composer.tsx:1058 1633 + #: src/view/com/composer/Composer.tsx:1046 1634 + #: src/view/com/composer/Composer.tsx:1057 1635 1635 #: src/view/com/composer/photos/EditImageDialog.web.tsx:43 1636 1636 #: src/view/com/composer/photos/EditImageDialog.web.tsx:52 1637 1637 #: src/view/shell/desktop/LeftNav.tsx:213 ··· 1884 1884 msgid "Click here to update your email" 1885 1885 msgstr "" 1886 1886 1887 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:341 1887 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:348 1888 1888 msgid "Click to disable quote posts of this post." 1889 1889 msgstr "" 1890 1890 1891 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:342 1891 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:349 1892 1892 msgid "Click to enable quote posts of this post." 1893 1893 msgstr "" 1894 1894 ··· 2007 2007 msgid "Closes password update alert" 2008 2008 msgstr "" 2009 2009 2010 - #: src/view/com/composer/Composer.tsx:1055 2010 + #: src/view/com/composer/Composer.tsx:1054 2011 2011 msgid "Closes post composer and discards post draft" 2012 2012 msgstr "" 2013 2013 ··· 2066 2066 msgid "Compose new post" 2067 2067 msgstr "" 2068 2068 2069 - #: src/view/com/composer/Composer.tsx:956 2069 + #: src/view/com/composer/Composer.tsx:955 2070 2070 msgid "Compose posts up to {0, plural, other {# characters}} in length" 2071 2071 msgstr "" 2072 2072 ··· 2074 2074 msgid "Compose reply" 2075 2075 msgstr "" 2076 2076 2077 - #: src/view/com/composer/Composer.tsx:1883 2077 + #: src/view/com/composer/Composer.tsx:1884 2078 2078 msgid "Compressing video..." 2079 2079 msgstr "" 2080 2080 ··· 2512 2512 msgid "Customization options" 2513 2513 msgstr "" 2514 2514 2515 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:107 2515 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:111 2516 2516 msgid "Customize who can interact with this post." 2517 2517 msgstr "" 2518 2518 ··· 2637 2637 2638 2638 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:710 2639 2639 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:712 2640 - #: src/view/com/composer/Composer.tsx:966 2640 + #: src/view/com/composer/Composer.tsx:965 2641 2641 msgid "Delete post" 2642 2642 msgstr "" 2643 2643 ··· 2750 2750 2751 2751 #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:92 2752 2752 #: src/screens/Profile/Header/EditProfileDialog.tsx:82 2753 - #: src/view/com/composer/Composer.tsx:804 2754 - #: src/view/com/composer/Composer.tsx:999 2753 + #: src/view/com/composer/Composer.tsx:803 2754 + #: src/view/com/composer/Composer.tsx:998 2755 2755 msgid "Discard" 2756 2756 msgstr "" 2757 2757 ··· 2760 2760 msgid "Discard changes?" 2761 2761 msgstr "" 2762 2762 2763 - #: src/view/com/composer/Composer.tsx:801 2763 + #: src/view/com/composer/Composer.tsx:800 2764 2764 msgid "Discard draft?" 2765 2765 msgstr "" 2766 2766 2767 - #: src/view/com/composer/Composer.tsx:991 2767 + #: src/view/com/composer/Composer.tsx:990 2768 2768 msgid "Discard post?" 2769 2769 msgstr "" 2770 2770 ··· 2787 2787 msgid "Dismiss" 2788 2788 msgstr "" 2789 2789 2790 - #: src/view/com/composer/Composer.tsx:1807 2790 + #: src/view/com/composer/Composer.tsx:1808 2791 2791 msgid "Dismiss error" 2792 2792 msgstr "" 2793 2793 ··· 3009 3009 msgid "Edit People" 3010 3010 msgstr "" 3011 3011 3012 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:73 3013 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:243 3012 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:77 3013 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:250 3014 3014 msgid "Edit post interaction settings" 3015 3015 msgstr "" 3016 3016 ··· 3229 3229 msgid "Entertainment" 3230 3230 msgstr "" 3231 3231 3232 - #: src/view/com/composer/Composer.tsx:1892 3232 + #: src/view/com/composer/Composer.tsx:1893 3233 3233 #: src/view/com/util/error/ErrorScreen.tsx:42 3234 3234 msgid "Error" 3235 3235 msgstr "" ··· 3262 3262 msgid "Error: {error}" 3263 3263 msgstr "" 3264 3264 3265 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:398 3265 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:405 3266 3266 msgid "Everybody" 3267 3267 msgstr "" 3268 3268 ··· 3964 3964 msgid "From @{sanitizedAuthor}" 3965 3965 msgstr "" 3966 3966 3967 - #: src/view/com/posts/PostFeedItem.tsx:345 3967 + #: src/view/com/posts/PostFeedReason.tsx:47 3968 3968 msgctxt "from-feed" 3969 3969 msgid "From <0/>" 3970 3970 msgstr "" ··· 4136 4136 msgid "Go to conversation with {0}" 4137 4137 msgstr "" 4138 4138 4139 + #: src/view/com/posts/PostFeedReason.tsx:38 4140 + msgid "Go to feed" 4141 + msgstr "" 4142 + 4139 4143 #: src/screens/Login/ForgotPasswordForm.tsx:165 4140 4144 msgid "Go to next" 4141 4145 msgstr "" ··· 4609 4613 msgid "It's just you right now! Add more people to your starter pack by searching above." 4610 4614 msgstr "" 4611 4615 4612 - #: src/view/com/composer/Composer.tsx:1826 4616 + #: src/view/com/composer/Composer.tsx:1827 4613 4617 msgid "Job ID: {0}" 4614 4618 msgstr "" 4615 4619 ··· 5138 5142 msgid "mentioned users" 5139 5143 msgstr "" 5140 5144 5141 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:427 5145 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:434 5142 5146 msgid "Mentioned users" 5143 5147 msgstr "" 5144 5148 ··· 5726 5730 msgid "No thanks" 5727 5731 msgstr "" 5728 5732 5729 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:409 5733 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:416 5730 5734 msgid "Nobody" 5731 5735 msgstr "" 5732 5736 ··· 5890 5894 msgid "Onboarding reset" 5891 5895 msgstr "" 5892 5896 5893 - #: src/view/com/composer/Composer.tsx:398 5897 + #: src/view/com/composer/Composer.tsx:397 5894 5898 msgid "One or more GIFs is missing alt text." 5895 5899 msgstr "" 5896 5900 5897 - #: src/view/com/composer/Composer.tsx:395 5901 + #: src/view/com/composer/Composer.tsx:394 5898 5902 msgid "One or more images is missing alt text." 5899 5903 msgstr "" 5900 5904 ··· 5906 5910 msgid "One or more of your selected files are too large. Maximum size is 100 MB." 5907 5911 msgstr "" 5908 5912 5909 - #: src/view/com/composer/Composer.tsx:405 5913 + #: src/view/com/composer/Composer.tsx:404 5910 5914 msgid "One or more videos is missing alt text." 5911 5915 msgstr "" 5912 5916 ··· 5963 5967 msgstr "" 5964 5968 5965 5969 #: src/screens/Messages/components/MessageInput.web.tsx:181 5966 - #: src/view/com/composer/Composer.tsx:1475 5970 + #: src/view/com/composer/Composer.tsx:1474 5967 5971 msgid "Open emoji picker" 5968 5972 msgstr "" 5969 5973 ··· 6063 6067 msgid "Opens device gallery to select up to {MAX_IMAGES, plural, other {# images}}, or a single video or GIF." 6064 6068 msgstr "" 6065 6069 6066 - #: src/view/com/composer/Composer.tsx:1476 6070 + #: src/view/com/composer/Composer.tsx:1475 6067 6071 msgid "Opens emoji picker" 6068 6072 msgstr "" 6069 6073 ··· 6115 6119 msgid "Options:" 6116 6120 msgstr "" 6117 6121 6118 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:422 6122 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:429 6119 6123 msgid "Or combine these options:" 6120 6124 msgstr "" 6121 6125 ··· 6273 6277 msgid "Pin to your profile" 6274 6278 msgstr "" 6275 6279 6276 - #: src/view/com/posts/PostFeedItem.tsx:426 6280 + #: src/view/com/posts/PostFeedReason.tsx:125 6277 6281 msgid "Pinned" 6278 6282 msgstr "" 6279 6283 ··· 6475 6479 msgid "Porn" 6476 6480 msgstr "" 6477 6481 6478 - #: src/screens/PostThread/index.tsx:500 6482 + #: src/screens/PostThread/index.tsx:504 6479 6483 msgctxt "description" 6480 6484 msgid "Post" 6481 6485 msgstr "" 6482 6486 6483 - #: src/view/com/composer/Composer.tsx:1118 6487 + #: src/view/com/composer/Composer.tsx:1117 6484 6488 msgctxt "action" 6485 6489 msgid "Post" 6486 6490 msgstr "" 6487 6491 6488 - #: src/view/com/composer/Composer.tsx:1116 6492 + #: src/view/com/composer/Composer.tsx:1115 6489 6493 msgctxt "action" 6490 6494 msgid "Post All" 6491 6495 msgstr "" ··· 6527 6531 msgid "Post Hidden by You" 6528 6532 msgstr "" 6529 6533 6530 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:104 6534 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:108 6531 6535 msgid "Post interaction settings" 6532 6536 msgstr "" 6533 6537 ··· 6650 6654 msgid "Privacy Policy" 6651 6655 msgstr "" 6652 6656 6653 - #: src/view/com/composer/Composer.tsx:1889 6657 + #: src/view/com/composer/Composer.tsx:1890 6654 6658 msgid "Processing video..." 6655 6659 msgstr "" 6656 6660 ··· 6689 6693 msgstr "" 6690 6694 6691 6695 #. Accessibility label for button to publish a single post 6692 - #: src/view/com/composer/Composer.tsx:1098 6696 + #: src/view/com/composer/Composer.tsx:1097 6693 6697 msgid "Publish post" 6694 6698 msgstr "" 6695 6699 6696 6700 #. Accessibility label for button to publish multiple posts in a thread 6697 - #: src/view/com/composer/Composer.tsx:1091 6701 + #: src/view/com/composer/Composer.tsx:1090 6698 6702 msgid "Publish posts" 6699 6703 msgstr "" 6700 6704 6701 6705 #. Accessibility label for button to publish multiple replies in a thread 6702 - #: src/view/com/composer/Composer.tsx:1076 6706 + #: src/view/com/composer/Composer.tsx:1075 6703 6707 msgid "Publish replies" 6704 6708 msgstr "" 6705 6709 6706 6710 #. Accessibility label for button to publish a single reply 6707 - #: src/view/com/composer/Composer.tsx:1083 6711 + #: src/view/com/composer/Composer.tsx:1082 6708 6712 msgid "Publish reply" 6709 6713 msgstr "" 6710 6714 ··· 6762 6766 msgid "Quote posts disabled" 6763 6767 msgstr "" 6764 6768 6765 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:333 6769 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:340 6766 6770 msgid "Quote settings" 6767 6771 msgstr "" 6768 6772 ··· 7094 7098 msgid "Replies to this post are disabled." 7095 7099 msgstr "" 7096 7100 7097 - #: src/view/com/composer/Composer.tsx:1114 7101 + #: src/view/com/composer/Composer.tsx:1113 7098 7102 msgctxt "action" 7099 7103 msgid "Reply" 7100 7104 msgstr "" ··· 7118 7122 msgid "Reply notifications" 7119 7123 msgstr "" 7120 7124 7121 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:389 7125 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:396 7122 7126 msgid "Reply settings" 7123 7127 msgstr "" 7124 7128 7125 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:374 7129 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:381 7126 7130 msgid "Reply settings are chosen by the author of the thread" 7127 7131 msgstr "" 7128 7132 ··· 7256 7260 msgid "Reposted By" 7257 7261 msgstr "" 7258 7262 7259 - #: src/view/com/posts/PostFeedItem.tsx:366 7260 - msgid "Reposted by {0}" 7263 + #: src/view/com/posts/PostFeedReason.tsx:77 7264 + msgid "Reposted by {reposter}" 7261 7265 msgstr "" 7262 7266 7263 - #: src/view/com/posts/PostFeedItem.tsx:385 7264 - msgid "Reposted by <0><1/></0>" 7267 + #: src/view/com/posts/PostFeedReason.tsx:91 7268 + msgid "Reposted by <0><1>{reposter}</1></0>" 7265 7269 msgstr "" 7266 7270 7267 - #: src/view/com/posts/PostFeedItem.tsx:364 7268 - #: src/view/com/posts/PostFeedItem.tsx:383 7271 + #: src/view/com/posts/PostFeedReason.tsx:77 7272 + #: src/view/com/posts/PostFeedReason.tsx:89 7269 7273 msgid "Reposted by you" 7270 7274 msgstr "" 7271 7275 ··· 7410 7414 #: src/components/dialogs/BirthDateSettings.tsx:156 7411 7415 #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:292 7412 7416 #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:307 7413 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:483 7414 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:489 7417 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:490 7418 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:496 7415 7419 #: src/components/live/EditLiveDialog.tsx:216 7416 7420 #: src/components/live/EditLiveDialog.tsx:223 7417 7421 #: src/components/StarterPack/QrCodeDialog.tsx:204 ··· 8880 8884 msgid "There was an issue! {0}" 8881 8885 msgstr "" 8882 8886 8883 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:221 8887 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:228 8884 8888 #: src/screens/List/ListHiddenScreen.tsx:63 8885 8889 #: src/screens/List/ListHiddenScreen.tsx:77 8886 8890 #: src/screens/List/ListHiddenScreen.tsx:99 ··· 9075 9079 msgid "This post will be hidden from feeds and threads. This cannot be undone." 9076 9080 msgstr "" 9077 9081 9078 - #: src/view/com/composer/Composer.tsx:514 9082 + #: src/view/com/composer/Composer.tsx:513 9079 9083 msgid "This post's author has disabled quote posts." 9080 9084 msgstr "" 9081 9085 ··· 9503 9507 msgid "Unsubscribed from list" 9504 9508 msgstr "" 9505 9509 9506 - #: src/view/com/composer/Composer.tsx:894 9510 + #: src/view/com/composer/Composer.tsx:893 9507 9511 msgid "Unsupported video type: {mimeType}" 9508 9512 msgstr "" 9509 9513 ··· 9589 9593 msgid "Uploading link thumbnail..." 9590 9594 msgstr "" 9591 9595 9592 - #: src/view/com/composer/Composer.tsx:1886 9596 + #: src/view/com/composer/Composer.tsx:1887 9593 9597 msgid "Uploading video..." 9594 9598 msgstr "" 9595 9599 ··· 9705 9709 msgid "Users I follow" 9706 9710 msgstr "" 9707 9711 9708 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:460 9712 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:467 9709 9713 msgid "Users in \"{0}\"" 9710 9714 msgstr "" 9711 9715 9712 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:437 9716 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:444 9713 9717 msgid "Users you follow" 9714 9718 msgstr "" 9715 9719 ··· 9847 9851 msgid "Video settings" 9848 9852 msgstr "" 9849 9853 9850 - #: src/view/com/composer/Composer.tsx:1896 9854 + #: src/view/com/composer/Composer.tsx:1897 9851 9855 msgid "Video uploaded" 9852 9856 msgstr "" 9853 9857 ··· 9863 9867 msgid "Videos must be less than 3 minutes long." 9864 9868 msgstr "" 9865 9869 9866 - #: src/view/com/composer/Composer.tsx:585 9870 + #: src/view/com/composer/Composer.tsx:584 9867 9871 msgctxt "Action to view the post the user just created" 9868 9872 msgid "View" 9869 9873 msgstr "" ··· 9925 9929 msgid "View more trending videos" 9926 9930 msgstr "" 9927 9931 9928 - #: src/view/com/composer/Composer.tsx:580 9932 + #: src/view/com/composer/Composer.tsx:579 9929 9933 msgid "View post" 9930 9934 msgstr "" 9931 9935 ··· 10153 10157 msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." 10154 10158 msgstr "" 10155 10159 10156 - #: src/view/com/composer/Composer.tsx:511 10160 + #: src/view/com/composer/Composer.tsx:510 10157 10161 msgid "We're sorry! The post you are replying to has been deleted." 10158 10162 msgstr "" 10159 10163 ··· 10204 10208 10205 10209 #: src/view/com/auth/SplashScreen.tsx:51 10206 10210 #: src/view/com/auth/SplashScreen.web.tsx:103 10207 - #: src/view/com/composer/Composer.tsx:854 10211 + #: src/view/com/composer/Composer.tsx:853 10208 10212 msgid "What's up?" 10209 10213 msgstr "" 10210 10214 ··· 10282 10286 msgid "Write a message" 10283 10287 msgstr "" 10284 10288 10285 - #: src/view/com/composer/Composer.tsx:954 10289 + #: src/view/com/composer/Composer.tsx:953 10286 10290 msgid "Write post" 10287 10291 msgstr "" 10288 10292 10289 10293 #: src/screens/PostThread/components/ThreadComposePrompt.tsx:90 10290 - #: src/view/com/composer/Composer.tsx:852 10294 + #: src/view/com/composer/Composer.tsx:851 10291 10295 msgid "Write your reply" 10292 10296 msgstr "" 10293 10297 ··· 10443 10447 msgid "You can select up to {MAX_IMAGES, plural, other {# images}} in total." 10444 10448 msgstr "" 10445 10449 10446 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:85 10450 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:89 10447 10451 msgid "You can set default interaction settings in <0>Settings → Moderation → Interaction settings</0>." 10448 10452 msgstr "" 10449 10453 ··· 10825 10829 msgid "Your first like!" 10826 10830 msgstr "" 10827 10831 10828 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:447 10832 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:454 10829 10833 msgid "Your followers" 10830 10834 msgstr "" 10831 10835 ··· 10870 10874 msgid "Your password must be at least 8 characters long." 10871 10875 msgstr "" 10872 10876 10873 - #: src/view/com/composer/Composer.tsx:576 10877 + #: src/view/com/composer/Composer.tsx:575 10874 10878 msgid "Your post was sent" 10875 10879 msgstr "" 10876 10880 10877 - #: src/view/com/composer/Composer.tsx:573 10881 + #: src/view/com/composer/Composer.tsx:572 10878 10882 msgid "Your posts were sent" 10879 10883 msgstr "" 10880 10884 ··· 10899 10903 msgid "Your profile, posts, feeds, and lists will no longer be visible to other Bluesky users. You can reactivate your account at any time by logging in." 10900 10904 msgstr "" 10901 10905 10902 - #: src/view/com/composer/Composer.tsx:575 10906 + #: src/view/com/composer/Composer.tsx:574 10903 10907 msgid "Your reply was sent" 10904 10908 msgstr "" 10905 10909
+4 -4
src/screens/Post/PostLikedBy.tsx
··· 7 7 type NativeStackScreenProps, 8 8 } from '#/lib/routes/types' 9 9 import {makeRecordUri} from '#/lib/strings/url-helpers' 10 - import {usePostThreadQuery} from '#/state/queries/post-thread' 10 + import {usePostQuery} from '#/state/queries/post' 11 11 import {useSetMinimalShellMode} from '#/state/shell' 12 12 import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy' 13 13 import * as Layout from '#/components/Layout' ··· 17 17 const setMinimalShellMode = useSetMinimalShellMode() 18 18 const {name, rkey} = route.params 19 19 const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 20 - const {data: post} = usePostThreadQuery(uri) 20 + const {data: post} = usePostQuery(uri) 21 21 22 22 let likeCount 23 - if (post?.thread.type === 'post') { 24 - likeCount = post.thread.post.likeCount 23 + if (post) { 24 + likeCount = post.likeCount 25 25 } 26 26 27 27 useFocusEffect(
+4 -4
src/screens/Post/PostQuotes.tsx
··· 7 7 type NativeStackScreenProps, 8 8 } from '#/lib/routes/types' 9 9 import {makeRecordUri} from '#/lib/strings/url-helpers' 10 - import {usePostThreadQuery} from '#/state/queries/post-thread' 10 + import {usePostQuery} from '#/state/queries/post' 11 11 import {useSetMinimalShellMode} from '#/state/shell' 12 12 import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuotes' 13 13 import * as Layout from '#/components/Layout' ··· 17 17 const setMinimalShellMode = useSetMinimalShellMode() 18 18 const {name, rkey} = route.params 19 19 const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 20 - const {data: post} = usePostThreadQuery(uri) 20 + const {data: post} = usePostQuery(uri) 21 21 22 22 let quoteCount 23 - if (post?.thread.type === 'post') { 24 - quoteCount = post.thread.post.quoteCount 23 + if (post) { 24 + quoteCount = post.quoteCount 25 25 } 26 26 27 27 useFocusEffect(
+4 -4
src/screens/Post/PostRepostedBy.tsx
··· 7 7 type NativeStackScreenProps, 8 8 } from '#/lib/routes/types' 9 9 import {makeRecordUri} from '#/lib/strings/url-helpers' 10 - import {usePostThreadQuery} from '#/state/queries/post-thread' 10 + import {usePostQuery} from '#/state/queries/post' 11 11 import {useSetMinimalShellMode} from '#/state/shell' 12 12 import {PostRepostedBy as PostRepostedByComponent} from '#/view/com/post-thread/PostRepostedBy' 13 13 import * as Layout from '#/components/Layout' ··· 17 17 const {name, rkey} = route.params 18 18 const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 19 19 const setMinimalShellMode = useSetMinimalShellMode() 20 - const {data: post} = usePostThreadQuery(uri) 20 + const {data: post} = usePostQuery(uri) 21 21 22 22 let quoteCount 23 - if (post?.thread.type === 'post') { 24 - quoteCount = post.thread.post.repostCount 23 + if (post) { 24 + quoteCount = post.repostCount 25 25 } 26 26 27 27 useFocusEffect(
+7 -3
src/screens/PostThread/index.tsx
··· 7 7 import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 8 8 import {useFeedFeedback} from '#/state/feed-feedback' 9 9 import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences' 10 - import {type ThreadItem, usePostThread} from '#/state/queries/usePostThread' 10 + import { 11 + PostThreadContextProvider, 12 + type ThreadItem, 13 + usePostThread, 14 + } from '#/state/queries/usePostThread' 11 15 import {useSession} from '#/state/session' 12 16 import {type OnPostSuccessData} from '#/state/shell/composer' 13 17 import {useShellLayout} from '#/state/shell/shell-layout' ··· 492 496 const defaultListFooterHeight = hasParents ? windowHeight - 200 : undefined 493 497 494 498 return ( 495 - <> 499 + <PostThreadContextProvider context={thread.context}> 496 500 <Layout.Header.Outer headerRef={headerRef}> 497 501 <Layout.Header.BackButton /> 498 502 <Layout.Header.Content> ··· 575 579 {!gtMobile && canReply && hasSession && ( 576 580 <MobileComposePrompt onPressReply={onReplyToAnchor} /> 577 581 )} 578 - </> 582 + </PostThreadContextProvider> 579 583 ) 580 584 } 581 585
-6
src/state/cache/post-shadow.ts
··· 12 12 import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed' 13 13 import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed' 14 14 import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' 15 - import {findAllPostsInQueryData as findAllPostsInThreadQueryData} from '#/state/queries/post-thread' 16 15 import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts' 17 16 import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/state/queries/usePostThread/queryCache' 18 17 import {castAsShadow, type Shadow} from './types' ··· 175 174 } 176 175 for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) { 177 176 yield post 178 - } 179 - for (let node of findAllPostsInThreadQueryData(queryClient, uri)) { 180 - if (node.type === 'post') { 181 - yield node.post 182 - } 183 177 } 184 178 for (let post of findAllPostsInThreadV2QueryData(queryClient, uri)) { 185 179 yield post
-2
src/state/cache/profile-shadow.ts
··· 16 16 import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '#/state/queries/post-liked-by' 17 17 import {findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData} from '#/state/queries/post-quotes' 18 18 import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '#/state/queries/post-reposted-by' 19 - import {findAllProfilesInQueryData as findAllProfilesInPostThreadQueryData} from '#/state/queries/post-thread' 20 19 import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '#/state/queries/profile' 21 20 import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers' 22 21 import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows' ··· 175 174 yield* findAllProfilesInActorSearchQueryData(queryClient, did) 176 175 yield* findAllProfilesInListConvosQueryData(queryClient, did) 177 176 yield* findAllProfilesInFeedsQueryData(queryClient, did) 178 - yield* findAllProfilesInPostThreadQueryData(queryClient, did) 179 177 yield* findAllProfilesInPostThreadV2QueryData(queryClient, did) 180 178 yield* findAllProfilesInKnownFollowersQueryData(queryClient, did) 181 179 yield* findAllProfilesInExploreFeedPreviewsQueryData(queryClient, did)
-631
src/state/queries/post-thread.ts
··· 1 - import { 2 - type AppBskyActorDefs, 3 - type AppBskyEmbedRecord, 4 - AppBskyFeedDefs, 5 - type AppBskyFeedGetPostThread, 6 - AppBskyFeedPost, 7 - AtUri, 8 - moderatePost, 9 - type ModerationDecision, 10 - type ModerationOpts, 11 - } from '@atproto/api' 12 - import {type QueryClient, useQuery, useQueryClient} from '@tanstack/react-query' 13 - 14 - import { 15 - findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData, 16 - findAllProfilesInQueryData as findAllProfilesInExploreFeedPreviewsQueryData, 17 - } from '#/state/queries/explore-feed-previews' 18 - import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' 19 - import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types' 20 - import { 21 - findAllPostsInQueryData as findAllPostsInSearchQueryData, 22 - findAllProfilesInQueryData as findAllProfilesInSearchQueryData, 23 - } from '#/state/queries/search-posts' 24 - import {useAgent} from '#/state/session' 25 - import * as bsky from '#/types/bsky' 26 - import { 27 - findAllPostsInQueryData as findAllPostsInNotifsQueryData, 28 - findAllProfilesInQueryData as findAllProfilesInNotifsQueryData, 29 - } from './notifications/feed' 30 - import { 31 - findAllPostsInQueryData as findAllPostsInFeedQueryData, 32 - findAllProfilesInQueryData as findAllProfilesInFeedQueryData, 33 - } from './post-feed' 34 - import { 35 - didOrHandleUriMatches, 36 - embedViewRecordToPostView, 37 - getEmbeddedPost, 38 - } from './util' 39 - 40 - const REPLY_TREE_DEPTH = 10 41 - export const RQKEY_ROOT = 'post-thread' 42 - export const RQKEY = (uri: string) => [RQKEY_ROOT, uri] 43 - type ThreadViewNode = AppBskyFeedGetPostThread.OutputSchema['thread'] 44 - 45 - export interface ThreadCtx { 46 - depth: number 47 - isHighlightedPost?: boolean 48 - hasMore?: boolean 49 - isParentLoading?: boolean 50 - isChildLoading?: boolean 51 - isSelfThread?: boolean 52 - hasMoreSelfThread?: boolean 53 - } 54 - 55 - export type ThreadPost = { 56 - type: 'post' 57 - _reactKey: string 58 - uri: string 59 - post: AppBskyFeedDefs.PostView 60 - record: AppBskyFeedPost.Record 61 - parent: ThreadNode | undefined 62 - replies: ThreadNode[] | undefined 63 - hasOPLike: boolean | undefined 64 - ctx: ThreadCtx 65 - } 66 - 67 - export type ThreadNotFound = { 68 - type: 'not-found' 69 - _reactKey: string 70 - uri: string 71 - ctx: ThreadCtx 72 - } 73 - 74 - export type ThreadBlocked = { 75 - type: 'blocked' 76 - _reactKey: string 77 - uri: string 78 - ctx: ThreadCtx 79 - } 80 - 81 - export type ThreadUnknown = { 82 - type: 'unknown' 83 - uri: string 84 - } 85 - 86 - export type ThreadNode = 87 - | ThreadPost 88 - | ThreadNotFound 89 - | ThreadBlocked 90 - | ThreadUnknown 91 - 92 - export type ThreadModerationCache = WeakMap<ThreadNode, ModerationDecision> 93 - 94 - export type PostThreadQueryData = { 95 - thread: ThreadNode 96 - threadgate?: AppBskyFeedDefs.ThreadgateView 97 - } 98 - 99 - export function usePostThreadQuery(uri: string | undefined) { 100 - const queryClient = useQueryClient() 101 - const agent = useAgent() 102 - return useQuery<PostThreadQueryData, Error>({ 103 - gcTime: 0, 104 - queryKey: RQKEY(uri || ''), 105 - async queryFn() { 106 - const res = await agent.getPostThread({ 107 - uri: uri!, 108 - depth: REPLY_TREE_DEPTH, 109 - }) 110 - if (res.success) { 111 - const thread = responseToThreadNodes(res.data.thread) 112 - annotateSelfThread(thread) 113 - return { 114 - thread, 115 - threadgate: res.data.threadgate as 116 - | AppBskyFeedDefs.ThreadgateView 117 - | undefined, 118 - } 119 - } 120 - return {thread: {type: 'unknown', uri: uri!}} 121 - }, 122 - enabled: !!uri, 123 - placeholderData: () => { 124 - if (!uri) return 125 - const post = findPostInQueryData(queryClient, uri) 126 - if (post) { 127 - return {thread: post} 128 - } 129 - return undefined 130 - }, 131 - }) 132 - } 133 - 134 - export function fillThreadModerationCache( 135 - cache: ThreadModerationCache, 136 - node: ThreadNode, 137 - moderationOpts: ModerationOpts, 138 - ) { 139 - if (node.type === 'post') { 140 - cache.set(node, moderatePost(node.post, moderationOpts)) 141 - if (node.parent) { 142 - fillThreadModerationCache(cache, node.parent, moderationOpts) 143 - } 144 - if (node.replies) { 145 - for (const reply of node.replies) { 146 - fillThreadModerationCache(cache, reply, moderationOpts) 147 - } 148 - } 149 - } 150 - } 151 - 152 - export function sortThread( 153 - node: ThreadNode, 154 - opts: UsePreferencesQueryResponse['threadViewPrefs'], 155 - modCache: ThreadModerationCache, 156 - currentDid: string | undefined, 157 - justPostedUris: Set<string>, 158 - threadgateRecordHiddenReplies: Set<string>, 159 - fetchedAtCache: Map<string, number>, 160 - fetchedAt: number, 161 - randomCache: Map<string, number>, 162 - ): ThreadNode { 163 - if (node.type !== 'post') { 164 - return node 165 - } 166 - if (node.replies) { 167 - node.replies.sort((a: ThreadNode, b: ThreadNode) => { 168 - if (a.type !== 'post') { 169 - return 1 170 - } 171 - if (b.type !== 'post') { 172 - return -1 173 - } 174 - 175 - if (node.ctx.isHighlightedPost || opts.lab_treeViewEnabled) { 176 - const aIsJustPosted = 177 - a.post.author.did === currentDid && justPostedUris.has(a.post.uri) 178 - const bIsJustPosted = 179 - b.post.author.did === currentDid && justPostedUris.has(b.post.uri) 180 - if (aIsJustPosted && bIsJustPosted) { 181 - return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest 182 - } else if (aIsJustPosted) { 183 - return -1 // reply while onscreen 184 - } else if (bIsJustPosted) { 185 - return 1 // reply while onscreen 186 - } 187 - } 188 - 189 - const aIsByOp = a.post.author.did === node.post?.author.did 190 - const bIsByOp = b.post.author.did === node.post?.author.did 191 - if (aIsByOp && bIsByOp) { 192 - return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest 193 - } else if (aIsByOp) { 194 - return -1 // op's own reply 195 - } else if (bIsByOp) { 196 - return 1 // op's own reply 197 - } 198 - 199 - const aIsBySelf = a.post.author.did === currentDid 200 - const bIsBySelf = b.post.author.did === currentDid 201 - if (aIsBySelf && bIsBySelf) { 202 - return a.post.indexedAt.localeCompare(b.post.indexedAt) // oldest 203 - } else if (aIsBySelf) { 204 - return -1 // current account's reply 205 - } else if (bIsBySelf) { 206 - return 1 // current account's reply 207 - } 208 - 209 - const aHidden = threadgateRecordHiddenReplies.has(a.uri) 210 - const bHidden = threadgateRecordHiddenReplies.has(b.uri) 211 - if (aHidden && !aIsBySelf && !bHidden) { 212 - return 1 213 - } else if (bHidden && !bIsBySelf && !aHidden) { 214 - return -1 215 - } 216 - 217 - const aBlur = Boolean(modCache.get(a)?.ui('contentList').blur) 218 - const bBlur = Boolean(modCache.get(b)?.ui('contentList').blur) 219 - if (aBlur !== bBlur) { 220 - if (aBlur) { 221 - return 1 222 - } 223 - if (bBlur) { 224 - return -1 225 - } 226 - } 227 - 228 - const aPin = Boolean(a.record.text.trim() === '📌') 229 - const bPin = Boolean(b.record.text.trim() === '📌') 230 - if (aPin !== bPin) { 231 - if (aPin) { 232 - return 1 233 - } 234 - if (bPin) { 235 - return -1 236 - } 237 - } 238 - 239 - if (opts.prioritizeFollowedUsers) { 240 - const af = a.post.author.viewer?.following 241 - const bf = b.post.author.viewer?.following 242 - if (af && !bf) { 243 - return -1 244 - } else if (!af && bf) { 245 - return 1 246 - } 247 - } 248 - 249 - // Split items from different fetches into separate generations. 250 - let aFetchedAt = fetchedAtCache.get(a.uri) 251 - if (aFetchedAt === undefined) { 252 - fetchedAtCache.set(a.uri, fetchedAt) 253 - aFetchedAt = fetchedAt 254 - } 255 - let bFetchedAt = fetchedAtCache.get(b.uri) 256 - if (bFetchedAt === undefined) { 257 - fetchedAtCache.set(b.uri, fetchedAt) 258 - bFetchedAt = fetchedAt 259 - } 260 - 261 - if (aFetchedAt !== bFetchedAt) { 262 - return aFetchedAt - bFetchedAt // older fetches first 263 - } else if (opts.sort === 'hotness') { 264 - const aHotness = getHotness(a, aFetchedAt) 265 - const bHotness = getHotness(b, bFetchedAt /* same as aFetchedAt */) 266 - return bHotness - aHotness 267 - } else if (opts.sort === 'oldest') { 268 - return a.post.indexedAt.localeCompare(b.post.indexedAt) 269 - } else if (opts.sort === 'newest') { 270 - return b.post.indexedAt.localeCompare(a.post.indexedAt) 271 - } else if (opts.sort === 'most-likes') { 272 - if (a.post.likeCount === b.post.likeCount) { 273 - return b.post.indexedAt.localeCompare(a.post.indexedAt) // newest 274 - } else { 275 - return (b.post.likeCount || 0) - (a.post.likeCount || 0) // most likes 276 - } 277 - } else if (opts.sort === 'random') { 278 - let aRandomScore = randomCache.get(a.uri) 279 - if (aRandomScore === undefined) { 280 - aRandomScore = Math.random() 281 - randomCache.set(a.uri, aRandomScore) 282 - } 283 - let bRandomScore = randomCache.get(b.uri) 284 - if (bRandomScore === undefined) { 285 - bRandomScore = Math.random() 286 - randomCache.set(b.uri, bRandomScore) 287 - } 288 - // this is vaguely criminal but we can get away with it 289 - return aRandomScore - bRandomScore 290 - } else { 291 - return b.post.indexedAt.localeCompare(a.post.indexedAt) 292 - } 293 - }) 294 - node.replies.forEach(reply => 295 - sortThread( 296 - reply, 297 - opts, 298 - modCache, 299 - currentDid, 300 - justPostedUris, 301 - threadgateRecordHiddenReplies, 302 - fetchedAtCache, 303 - fetchedAt, 304 - randomCache, 305 - ), 306 - ) 307 - } 308 - return node 309 - } 310 - 311 - // internal methods 312 - // = 313 - 314 - // Inspired by https://join-lemmy.org/docs/contributors/07-ranking-algo.html 315 - // We want to give recent comments a real chance (and not bury them deep below the fold) 316 - // while also surfacing well-liked comments from the past. In the future, we can explore 317 - // something more sophisticated, but we don't have much data on the client right now. 318 - function getHotness(threadPost: ThreadPost, fetchedAt: number) { 319 - const {post, hasOPLike} = threadPost 320 - const hoursAgo = Math.max( 321 - 0, 322 - (new Date(fetchedAt).getTime() - new Date(post.indexedAt).getTime()) / 323 - (1000 * 60 * 60), 324 - ) 325 - const likeCount = post.likeCount ?? 0 326 - const likeOrder = Math.log(3 + likeCount) * (hasOPLike ? 1.45 : 1.0) 327 - const timePenaltyExponent = 1.5 + 1.5 / (1 + Math.log(1 + likeCount)) 328 - const opLikeBoost = hasOPLike ? 0.8 : 1.0 329 - const timePenalty = Math.pow(hoursAgo + 2, timePenaltyExponent * opLikeBoost) 330 - return likeOrder / timePenalty 331 - } 332 - 333 - function responseToThreadNodes( 334 - node: ThreadViewNode, 335 - depth = 0, 336 - direction: 'up' | 'down' | 'start' = 'start', 337 - ): ThreadNode { 338 - if ( 339 - AppBskyFeedDefs.isThreadViewPost(node) && 340 - bsky.dangerousIsType<AppBskyFeedPost.Record>( 341 - node.post.record, 342 - AppBskyFeedPost.isRecord, 343 - ) 344 - ) { 345 - const post = node.post 346 - // These should normally be present. They're missing only for 347 - // posts that were *just* created. Ideally, the backend would 348 - // know to return zeros. Fill them in manually to compensate. 349 - post.replyCount ??= 0 350 - post.likeCount ??= 0 351 - post.repostCount ??= 0 352 - return { 353 - type: 'post', 354 - _reactKey: node.post.uri, 355 - uri: node.post.uri, 356 - post: post, 357 - record: node.post.record, 358 - parent: 359 - node.parent && direction !== 'down' 360 - ? responseToThreadNodes(node.parent, depth - 1, 'up') 361 - : undefined, 362 - replies: 363 - node.replies?.length && direction !== 'up' 364 - ? node.replies 365 - .map(reply => responseToThreadNodes(reply, depth + 1, 'down')) 366 - // do not show blocked posts in replies 367 - .filter(node => node.type !== 'blocked') 368 - : undefined, 369 - hasOPLike: Boolean(node?.threadContext?.rootAuthorLike), 370 - ctx: { 371 - depth, 372 - isHighlightedPost: depth === 0, 373 - hasMore: 374 - direction === 'down' && !node.replies?.length && !!post.replyCount, 375 - isSelfThread: false, // populated `annotateSelfThread` 376 - hasMoreSelfThread: false, // populated in `annotateSelfThread` 377 - }, 378 - } 379 - } else if (AppBskyFeedDefs.isBlockedPost(node)) { 380 - return {type: 'blocked', _reactKey: node.uri, uri: node.uri, ctx: {depth}} 381 - } else if (AppBskyFeedDefs.isNotFoundPost(node)) { 382 - return {type: 'not-found', _reactKey: node.uri, uri: node.uri, ctx: {depth}} 383 - } else { 384 - return {type: 'unknown', uri: ''} 385 - } 386 - } 387 - 388 - function annotateSelfThread(thread: ThreadNode) { 389 - if (thread.type !== 'post') { 390 - return 391 - } 392 - const selfThreadNodes: ThreadPost[] = [thread] 393 - 394 - let parent: ThreadNode | undefined = thread.parent 395 - while (parent) { 396 - if ( 397 - parent.type !== 'post' || 398 - parent.post.author.did !== thread.post.author.did 399 - ) { 400 - // not a self-thread 401 - return 402 - } 403 - selfThreadNodes.unshift(parent) 404 - parent = parent.parent 405 - } 406 - 407 - let node = thread 408 - for (let i = 0; i < 10; i++) { 409 - const reply = node.replies?.find( 410 - r => r.type === 'post' && r.post.author.did === thread.post.author.did, 411 - ) 412 - if (reply?.type !== 'post') { 413 - break 414 - } 415 - selfThreadNodes.push(reply) 416 - node = reply 417 - } 418 - 419 - if (selfThreadNodes.length > 1) { 420 - for (const selfThreadNode of selfThreadNodes) { 421 - selfThreadNode.ctx.isSelfThread = true 422 - } 423 - const last = selfThreadNodes[selfThreadNodes.length - 1] 424 - if ( 425 - last && 426 - last.ctx.depth === REPLY_TREE_DEPTH && // at the edge of the tree depth 427 - last.post.replyCount && // has replies 428 - !last.replies?.length // replies were not hydrated 429 - ) { 430 - last.ctx.hasMoreSelfThread = true 431 - } 432 - } 433 - } 434 - 435 - function findPostInQueryData( 436 - queryClient: QueryClient, 437 - uri: string, 438 - ): ThreadNode | void { 439 - let partial 440 - for (let item of findAllPostsInQueryData(queryClient, uri)) { 441 - if (item.type === 'post') { 442 - // Currently, the backend doesn't send full post info in some cases 443 - // (for example, for quoted posts). We use missing `likeCount` 444 - // as a way to detect that. In the future, we should fix this on 445 - // the backend, which will let us always stop on the first result. 446 - const hasAllInfo = item.post.likeCount != null 447 - if (hasAllInfo) { 448 - return item 449 - } else { 450 - partial = item 451 - // Keep searching, we might still find a full post in the cache. 452 - } 453 - } 454 - } 455 - return partial 456 - } 457 - 458 - export function* findAllPostsInQueryData( 459 - queryClient: QueryClient, 460 - uri: string, 461 - ): Generator<ThreadNode, void> { 462 - const atUri = new AtUri(uri) 463 - 464 - const queryDatas = queryClient.getQueriesData<PostThreadQueryData>({ 465 - queryKey: [RQKEY_ROOT], 466 - }) 467 - for (const [_queryKey, queryData] of queryDatas) { 468 - if (!queryData) { 469 - continue 470 - } 471 - const {thread} = queryData 472 - for (const item of traverseThread(thread)) { 473 - if (item.type === 'post' && didOrHandleUriMatches(atUri, item.post)) { 474 - const placeholder = threadNodeToPlaceholderThread(item) 475 - if (placeholder) { 476 - yield placeholder 477 - } 478 - } 479 - const quotedPost = 480 - item.type === 'post' ? getEmbeddedPost(item.post.embed) : undefined 481 - if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) { 482 - yield embedViewRecordToPlaceholderThread(quotedPost) 483 - } 484 - } 485 - } 486 - for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) { 487 - // Check notifications first. If you have a post in notifications, 488 - // it's often due to a like or a repost, and we want to prioritize 489 - // a post object with >0 likes/reposts over a stale version with no 490 - // metrics in order to avoid a notification->post scroll jump. 491 - yield postViewToPlaceholderThread(post) 492 - } 493 - for (let post of findAllPostsInFeedQueryData(queryClient, uri)) { 494 - yield postViewToPlaceholderThread(post) 495 - } 496 - for (let post of findAllPostsInQuoteQueryData(queryClient, uri)) { 497 - yield postViewToPlaceholderThread(post) 498 - } 499 - for (let post of findAllPostsInSearchQueryData(queryClient, uri)) { 500 - yield postViewToPlaceholderThread(post) 501 - } 502 - for (let post of findAllPostsInExploreFeedPreviewsQueryData( 503 - queryClient, 504 - uri, 505 - )) { 506 - yield postViewToPlaceholderThread(post) 507 - } 508 - } 509 - 510 - export function* findAllProfilesInQueryData( 511 - queryClient: QueryClient, 512 - did: string, 513 - ): Generator<AppBskyActorDefs.ProfileViewBasic, void> { 514 - const queryDatas = queryClient.getQueriesData<PostThreadQueryData>({ 515 - queryKey: [RQKEY_ROOT], 516 - }) 517 - for (const [_queryKey, queryData] of queryDatas) { 518 - if (!queryData) { 519 - continue 520 - } 521 - const {thread} = queryData 522 - for (const item of traverseThread(thread)) { 523 - if (item.type === 'post' && item.post.author.did === did) { 524 - yield item.post.author 525 - } 526 - const quotedPost = 527 - item.type === 'post' ? getEmbeddedPost(item.post.embed) : undefined 528 - if (quotedPost?.author.did === did) { 529 - yield quotedPost?.author 530 - } 531 - } 532 - } 533 - for (let profile of findAllProfilesInFeedQueryData(queryClient, did)) { 534 - yield profile 535 - } 536 - for (let profile of findAllProfilesInNotifsQueryData(queryClient, did)) { 537 - yield profile 538 - } 539 - for (let profile of findAllProfilesInSearchQueryData(queryClient, did)) { 540 - yield profile 541 - } 542 - for (let profile of findAllProfilesInExploreFeedPreviewsQueryData( 543 - queryClient, 544 - did, 545 - )) { 546 - yield profile 547 - } 548 - } 549 - 550 - function* traverseThread(node: ThreadNode): Generator<ThreadNode, void> { 551 - if (node.type === 'post') { 552 - if (node.parent) { 553 - yield* traverseThread(node.parent) 554 - } 555 - yield node 556 - if (node.replies?.length) { 557 - for (const reply of node.replies) { 558 - yield* traverseThread(reply) 559 - } 560 - } 561 - } 562 - } 563 - 564 - function threadNodeToPlaceholderThread( 565 - node: ThreadNode, 566 - ): ThreadNode | undefined { 567 - if (node.type !== 'post') { 568 - return undefined 569 - } 570 - return { 571 - type: node.type, 572 - _reactKey: node._reactKey, 573 - uri: node.uri, 574 - post: node.post, 575 - record: node.record, 576 - parent: undefined, 577 - replies: undefined, 578 - hasOPLike: undefined, 579 - ctx: { 580 - depth: 0, 581 - isHighlightedPost: true, 582 - hasMore: false, 583 - isParentLoading: !!node.record.reply, 584 - isChildLoading: !!node.post.replyCount, 585 - }, 586 - } 587 - } 588 - 589 - function postViewToPlaceholderThread( 590 - post: AppBskyFeedDefs.PostView, 591 - ): ThreadNode { 592 - return { 593 - type: 'post', 594 - _reactKey: post.uri, 595 - uri: post.uri, 596 - post: post, 597 - record: post.record as AppBskyFeedPost.Record, // validated in notifs 598 - parent: undefined, 599 - replies: undefined, 600 - hasOPLike: undefined, 601 - ctx: { 602 - depth: 0, 603 - isHighlightedPost: true, 604 - hasMore: false, 605 - isParentLoading: !!(post.record as AppBskyFeedPost.Record).reply, 606 - isChildLoading: true, // assume yes (show the spinner) just in case 607 - }, 608 - } 609 - } 610 - 611 - function embedViewRecordToPlaceholderThread( 612 - record: AppBskyEmbedRecord.ViewRecord, 613 - ): ThreadNode { 614 - return { 615 - type: 'post', 616 - _reactKey: record.uri, 617 - uri: record.uri, 618 - post: embedViewRecordToPostView(record), 619 - record: record.value as AppBskyFeedPost.Record, // validated in getEmbeddedPost 620 - parent: undefined, 621 - replies: undefined, 622 - hasOPLike: undefined, 623 - ctx: { 624 - depth: 0, 625 - isHighlightedPost: true, 626 - hasMore: false, 627 - isParentLoading: !!(record.value as AppBskyFeedPost.Record).reply, 628 - isChildLoading: true, // not available, so assume yes (to show the spinner) 629 - }, 630 - } 631 - }
+29 -47
src/state/queries/threadgate/index.ts
··· 1 1 import { 2 - AppBskyFeedDefs, 3 - type AppBskyFeedGetPostThread, 2 + type AppBskyFeedDefs, 4 3 AppBskyFeedThreadgate, 5 4 AtUri, 6 5 type BskyAgent, ··· 8 7 import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' 9 8 10 9 import {networkRetry, retry} from '#/lib/async/retry' 11 - import {until} from '#/lib/async/until' 12 10 import {STALE} from '#/state/queries' 13 - import {RQKEY_ROOT as postThreadQueryKeyRoot} from '#/state/queries/post-thread' 11 + import {useGetPost} from '#/state/queries/post' 14 12 import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate/types' 15 13 import { 16 14 createThreadgateRecord, ··· 18 16 threadgateAllowUISettingToAllowRecordValue, 19 17 threadgateViewToAllowUISetting, 20 18 } from '#/state/queries/threadgate/util' 19 + import {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread' 21 20 import {useAgent} from '#/state/session' 22 21 import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies' 23 22 import * as bsky from '#/types/bsky' ··· 71 70 postUri?: string 72 71 initialData?: AppBskyFeedDefs.ThreadgateView 73 72 } = {}) { 74 - const agent = useAgent() 73 + const getPost = useGetPost() 75 74 76 75 return useQuery({ 77 76 enabled: !!postUri, ··· 79 78 placeholderData: initialData, 80 79 staleTime: STALE.MINUTES.ONE, 81 80 async queryFn() { 82 - return getThreadgateView({ 83 - agent, 84 - postUri: postUri!, 85 - }) 81 + const post = await getPost({uri: postUri!}) 82 + return post.threadgate ?? null 86 83 }, 87 84 }) 88 - } 89 - 90 - export async function getThreadgateView({ 91 - agent, 92 - postUri, 93 - }: { 94 - agent: BskyAgent 95 - postUri: string 96 - }) { 97 - const {data} = await agent.app.bsky.feed.getPostThread({ 98 - uri: postUri!, 99 - depth: 0, 100 - }) 101 - 102 - if (AppBskyFeedDefs.isThreadViewPost(data.thread)) { 103 - return data.thread.post.threadgate ?? null 104 - } 105 - 106 - return null 107 85 } 108 86 109 87 export async function getThreadgateRecord({ ··· 248 226 export function useSetThreadgateAllowMutation() { 249 227 const agent = useAgent() 250 228 const queryClient = useQueryClient() 229 + const getPost = useGetPost() 230 + const updatePostThreadThreadgate = useUpdatePostThreadThreadgateQueryCache() 251 231 252 232 return useMutation({ 253 233 mutationFn: async ({ ··· 272 252 }) 273 253 }, 274 254 async onSuccess(_, {postUri, allow}) { 275 - await until( 255 + const data = await retry<AppBskyFeedDefs.ThreadgateView | undefined>( 276 256 5, // 5 tries 277 - 1e3, // 1s delay between tries 278 - (res: AppBskyFeedGetPostThread.Response) => { 279 - const thread = res.data.thread 280 - if (AppBskyFeedDefs.isThreadViewPost(thread)) { 281 - const fetchedSettings = threadgateViewToAllowUISetting( 282 - thread.post.threadgate, 257 + _e => true, 258 + async () => { 259 + const post = await getPost({uri: postUri}) 260 + const threadgate = post.threadgate 261 + if (!threadgate) { 262 + throw new Error( 263 + `useSetThreadgateAllowMutation: could not fetch threadgate, appview may not be ready yet`, 283 264 ) 284 - return JSON.stringify(fetchedSettings) === JSON.stringify(allow) 285 265 } 286 - return false 287 - }, 288 - () => { 289 - return agent.app.bsky.feed.getPostThread({ 290 - uri: postUri, 291 - depth: 0, 292 - }) 266 + const fetchedSettings = threadgateViewToAllowUISetting(threadgate) 267 + const isReady = 268 + JSON.stringify(fetchedSettings) === JSON.stringify(allow) 269 + if (!isReady) { 270 + throw new Error( 271 + `useSetThreadgateAllowMutation: appview isn't ready yet`, 272 + ) // try again 273 + } 274 + return threadgate 293 275 }, 294 - ) 276 + 1e3, // 1s delay between tries 277 + ).catch(() => {}) 278 + 279 + if (data) updatePostThreadThreadgate(data) 295 280 296 - queryClient.invalidateQueries({ 297 - queryKey: [postThreadQueryKeyRoot], 298 - }) 299 281 queryClient.invalidateQueries({ 300 282 queryKey: [threadgateRecordQueryKeyRoot], 301 283 })
+43
src/state/queries/usePostThread/context.tsx
··· 1 + import {createContext, useContext} from 'react' 2 + 3 + import { 4 + type createPostThreadOtherQueryKey, 5 + type createPostThreadQueryKey, 6 + } from '#/state/queries/usePostThread/types' 7 + 8 + /** 9 + * Contains static metadata about the post thread query, suitable for 10 + * context e.g. query keys and other things that don't update frequently. 11 + * 12 + * Be careful adding things here, as it could cause unnecessary re-renders. 13 + */ 14 + export type PostThreadContextType = { 15 + postThreadQueryKey: ReturnType<typeof createPostThreadQueryKey> 16 + postThreadOtherQueryKey: ReturnType<typeof createPostThreadOtherQueryKey> 17 + } 18 + 19 + const PostThreadContext = createContext<PostThreadContextType | undefined>( 20 + undefined, 21 + ) 22 + 23 + /** 24 + * Use the current {@link PostThreadContext}, if one is available. If not, 25 + * returns `undefined`. 26 + */ 27 + export function usePostThreadContext() { 28 + return useContext(PostThreadContext) 29 + } 30 + 31 + export function PostThreadContextProvider({ 32 + children, 33 + context, 34 + }: { 35 + children: React.ReactNode 36 + context?: PostThreadContextType 37 + }) { 38 + return ( 39 + <PostThreadContext.Provider value={context}> 40 + {children} 41 + </PostThreadContext.Provider> 42 + ) 43 + }
+24 -15
src/state/queries/usePostThread/index.ts
··· 11 11 TREE_VIEW_BELOW_DESKTOP, 12 12 TREE_VIEW_BF, 13 13 } from '#/state/queries/usePostThread/const' 14 + import {type PostThreadContextType} from '#/state/queries/usePostThread/context' 14 15 import { 15 16 createCacheMutator, 16 17 getThreadPlaceholder, ··· 31 32 import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 32 33 import {useBreakpoints} from '#/alf' 33 34 35 + export * from '#/state/queries/usePostThread/context' 36 + export {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread/queryCache' 34 37 export * from '#/state/queries/usePostThread/types' 35 38 36 39 export function usePostThread({anchor}: {anchor?: string}) { ··· 277 280 setOtherItemsVisible, 278 281 ]) 279 282 280 - return useMemo( 281 - () => ({ 283 + return useMemo(() => { 284 + const context: PostThreadContextType = { 285 + postThreadQueryKey, 286 + postThreadOtherQueryKey, 287 + } 288 + return { 289 + context, 282 290 state: { 283 291 /* 284 292 * Copy in any query state that is useful ··· 309 317 setSort, 310 318 setView, 311 319 }, 312 - }), 313 - [ 314 - query, 315 - mutator.insertReplies, 316 - otherItemsVisible, 317 - sort, 318 - view, 319 - setSort, 320 - setView, 321 - threadgate, 322 - items, 323 - ], 324 - ) 320 + } 321 + }, [ 322 + query, 323 + mutator.insertReplies, 324 + otherItemsVisible, 325 + sort, 326 + view, 327 + setSort, 328 + setView, 329 + threadgate, 330 + items, 331 + postThreadQueryKey, 332 + postThreadOtherQueryKey, 333 + ]) 325 334 }
+51 -1
src/state/queries/usePostThread/queryCache.ts
··· 1 + import {useCallback} from 'react' 1 2 import { 2 3 type $Typed, 3 4 type AppBskyActorDefs, ··· 7 8 type AppBskyUnspeccedGetPostThreadV2, 8 9 AtUri, 9 10 } from '@atproto/api' 10 - import {type QueryClient} from '@tanstack/react-query' 11 + import {type QueryClient, useQueryClient} from '@tanstack/react-query' 11 12 12 13 import { 13 14 dangerousGetPostShadow, ··· 18 19 import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed' 19 20 import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' 20 21 import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts' 22 + import {usePostThreadContext} from '#/state/queries/usePostThread' 21 23 import {getBranch} from '#/state/queries/usePostThread/traversal' 22 24 import { 23 25 type ApiThreadItem, ··· 322 324 } 323 325 } 324 326 } 327 + 328 + export function useUpdatePostThreadThreadgateQueryCache() { 329 + const qc = useQueryClient() 330 + const context = usePostThreadContext() 331 + 332 + return useCallback( 333 + (threadgate: AppBskyFeedDefs.ThreadgateView) => { 334 + if (!context) return 335 + 336 + function mutator<T>(thread: ApiThreadItem[]): T[] { 337 + for (let i = 0; i < thread.length; i++) { 338 + const item = thread[i] 339 + 340 + if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) continue 341 + 342 + if (item.depth === 0) { 343 + thread.splice(i, 1, { 344 + ...item, 345 + value: { 346 + ...item.value, 347 + post: { 348 + ...item.value.post, 349 + threadgate, 350 + }, 351 + }, 352 + }) 353 + } 354 + } 355 + 356 + return thread as T[] 357 + } 358 + 359 + qc.setQueryData<AppBskyUnspeccedGetPostThreadV2.OutputSchema>( 360 + context.postThreadQueryKey, 361 + data => { 362 + if (!data) return 363 + return { 364 + ...data, 365 + thread: mutator<AppBskyUnspeccedGetPostThreadV2.ThreadItem>([ 366 + ...data.thread, 367 + ]), 368 + } 369 + }, 370 + ) 371 + }, 372 + [qc, context], 373 + ) 374 + }
+10 -9
src/view/com/composer/Composer.tsx
··· 44 44 import {useSafeAreaInsets} from 'react-native-safe-area-context' 45 45 import {type ImagePickerAsset} from 'expo-image-picker' 46 46 import { 47 - AppBskyFeedDefs, 48 - type AppBskyFeedGetPostThread, 49 47 AppBskyUnspeccedDefs, 48 + type AppBskyUnspeccedGetPostThreadV2, 50 49 AtUri, 51 50 type BskyAgent, 52 51 type RichText, ··· 549 548 if (initQuote) { 550 549 // We want to wait for the quote count to update before we call `onPost`, which will refetch data 551 550 whenAppViewReady(agent, initQuote.uri, res => { 552 - const quotedThread = res.data.thread 551 + const anchor = res.data.thread.at(0) 553 552 if ( 554 - AppBskyFeedDefs.isThreadViewPost(quotedThread) && 555 - quotedThread.post.quoteCount !== initQuote.quoteCount 553 + AppBskyUnspeccedDefs.isThreadItemPost(anchor?.value) && 554 + anchor.value.post.quoteCount !== initQuote.quoteCount 556 555 ) { 557 556 onPost?.(postUri) 558 557 onPostSuccess?.(postSuccessData) ··· 1661 1660 async function whenAppViewReady( 1662 1661 agent: BskyAgent, 1663 1662 uri: string, 1664 - fn: (res: AppBskyFeedGetPostThread.Response) => boolean, 1663 + fn: (res: AppBskyUnspeccedGetPostThreadV2.Response) => boolean, 1665 1664 ) { 1666 1665 await until( 1667 1666 5, // 5 tries 1668 1667 1e3, // 1s delay between tries 1669 1668 fn, 1670 1669 () => 1671 - agent.app.bsky.feed.getPostThread({ 1672 - uri, 1673 - depth: 0, 1670 + agent.app.bsky.unspecced.getPostThreadV2({ 1671 + anchor: uri, 1672 + above: false, 1673 + below: 0, 1674 + branchingFactor: 0, 1674 1675 }), 1675 1676 ) 1676 1677 }