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 import {logger} from '#/logger' 14 import {STALE} from '#/state/queries' 15 import {useMyListsQuery} from '#/state/queries/my-lists' 16 import { 17 createPostgateQueryKey, 18 getPostgateRecord, ··· 25 } from '#/state/queries/postgate/util' 26 import { 27 createThreadgateViewQueryKey, 28 - getThreadgateView, 29 type ThreadgateAllowUISetting, 30 threadgateViewToAllowUISetting, 31 useSetThreadgateAllowMutation, 32 useThreadgateViewQuery, 33 } from '#/state/queries/threadgate' 34 import {useAgent, useSession} from '#/state/session' 35 import * as Toast from '#/view/com/util/Toast' 36 import {atoms as a, useTheme} from '#/alf' ··· 133 export function PostInteractionSettingsDialog( 134 props: PostInteractionSettingsDialogProps, 135 ) { 136 return ( 137 <Dialog.Outer control={props.control}> 138 <Dialog.Handle /> 139 - <PostInteractionSettingsDialogControlledInner {...props} /> 140 </Dialog.Outer> 141 ) 142 } ··· 558 }) { 559 const queryClient = useQueryClient() 560 const agent = useAgent() 561 562 return React.useCallback(async () => { 563 try { ··· 570 }), 571 queryClient.prefetchQuery({ 572 queryKey: createThreadgateViewQueryKey(rootPostUri), 573 - queryFn: () => getThreadgateView({agent, postUri: rootPostUri}), 574 staleTime: STALE.SECONDS.THIRTY, 575 }), 576 ]) ··· 579 safeMessage: e.message, 580 }) 581 } 582 - }, [queryClient, agent, postUri, rootPostUri]) 583 }
··· 13 import {logger} from '#/logger' 14 import {STALE} from '#/state/queries' 15 import {useMyListsQuery} from '#/state/queries/my-lists' 16 + import {useGetPost} from '#/state/queries/post' 17 import { 18 createPostgateQueryKey, 19 getPostgateRecord, ··· 26 } from '#/state/queries/postgate/util' 27 import { 28 createThreadgateViewQueryKey, 29 type ThreadgateAllowUISetting, 30 threadgateViewToAllowUISetting, 31 useSetThreadgateAllowMutation, 32 useThreadgateViewQuery, 33 } from '#/state/queries/threadgate' 34 + import { 35 + PostThreadContextProvider, 36 + usePostThreadContext, 37 + } from '#/state/queries/usePostThread' 38 import {useAgent, useSession} from '#/state/session' 39 import * as Toast from '#/view/com/util/Toast' 40 import {atoms as a, useTheme} from '#/alf' ··· 137 export function PostInteractionSettingsDialog( 138 props: PostInteractionSettingsDialogProps, 139 ) { 140 + const postThreadContext = usePostThreadContext() 141 return ( 142 <Dialog.Outer control={props.control}> 143 <Dialog.Handle /> 144 + <PostThreadContextProvider context={postThreadContext}> 145 + <PostInteractionSettingsDialogControlledInner {...props} /> 146 + </PostThreadContextProvider> 147 </Dialog.Outer> 148 ) 149 } ··· 565 }) { 566 const queryClient = useQueryClient() 567 const agent = useAgent() 568 + const getPost = useGetPost() 569 570 return React.useCallback(async () => { 571 try { ··· 578 }), 579 queryClient.prefetchQuery({ 580 queryKey: createThreadgateViewQueryKey(rootPostUri), 581 + queryFn: async () => { 582 + const post = await getPost({uri: rootPostUri}) 583 + return post.threadgate ?? null 584 + }, 585 staleTime: STALE.SECONDS.THIRTY, 586 }), 587 ]) ··· 590 safeMessage: e.message, 591 }) 592 } 593 + }, [queryClient, agent, postUri, rootPostUri, getPost]) 594 }
+78 -74
src/locale/locales/en/messages.po
··· 712 msgid "Add another account" 713 msgstr "" 714 715 - #: src/view/com/composer/Composer.tsx:853 716 msgid "Add another post" 717 msgstr "" 718 719 - #: src/view/com/composer/Composer.tsx:1490 720 msgid "Add another post to thread" 721 msgstr "" 722 ··· 911 msgid "Allow others to be notified of your posts" 912 msgstr "" 913 914 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:348 915 msgid "Allow quote posts" 916 msgstr "" 917 918 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:393 919 msgid "Allow replies from:" 920 msgstr "" 921 ··· 1225 msgid "Are you sure you want to remove this from your feeds?" 1226 msgstr "" 1227 1228 - #: src/view/com/composer/Composer.tsx:802 1229 msgid "Are you sure you'd like to discard this draft?" 1230 msgstr "" 1231 1232 - #: src/view/com/composer/Composer.tsx:992 1233 msgid "Are you sure you'd like to discard this post?" 1234 msgstr "" 1235 ··· 1630 #: src/screens/Settings/Settings.tsx:289 1631 #: src/screens/Takendown.tsx:108 1632 #: src/screens/Takendown.tsx:111 1633 - #: src/view/com/composer/Composer.tsx:1047 1634 - #: src/view/com/composer/Composer.tsx:1058 1635 #: src/view/com/composer/photos/EditImageDialog.web.tsx:43 1636 #: src/view/com/composer/photos/EditImageDialog.web.tsx:52 1637 #: src/view/shell/desktop/LeftNav.tsx:213 ··· 1884 msgid "Click here to update your email" 1885 msgstr "" 1886 1887 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:341 1888 msgid "Click to disable quote posts of this post." 1889 msgstr "" 1890 1891 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:342 1892 msgid "Click to enable quote posts of this post." 1893 msgstr "" 1894 ··· 2007 msgid "Closes password update alert" 2008 msgstr "" 2009 2010 - #: src/view/com/composer/Composer.tsx:1055 2011 msgid "Closes post composer and discards post draft" 2012 msgstr "" 2013 ··· 2066 msgid "Compose new post" 2067 msgstr "" 2068 2069 - #: src/view/com/composer/Composer.tsx:956 2070 msgid "Compose posts up to {0, plural, other {# characters}} in length" 2071 msgstr "" 2072 ··· 2074 msgid "Compose reply" 2075 msgstr "" 2076 2077 - #: src/view/com/composer/Composer.tsx:1883 2078 msgid "Compressing video..." 2079 msgstr "" 2080 ··· 2512 msgid "Customization options" 2513 msgstr "" 2514 2515 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:107 2516 msgid "Customize who can interact with this post." 2517 msgstr "" 2518 ··· 2637 2638 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:710 2639 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:712 2640 - #: src/view/com/composer/Composer.tsx:966 2641 msgid "Delete post" 2642 msgstr "" 2643 ··· 2750 2751 #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:92 2752 #: src/screens/Profile/Header/EditProfileDialog.tsx:82 2753 - #: src/view/com/composer/Composer.tsx:804 2754 - #: src/view/com/composer/Composer.tsx:999 2755 msgid "Discard" 2756 msgstr "" 2757 ··· 2760 msgid "Discard changes?" 2761 msgstr "" 2762 2763 - #: src/view/com/composer/Composer.tsx:801 2764 msgid "Discard draft?" 2765 msgstr "" 2766 2767 - #: src/view/com/composer/Composer.tsx:991 2768 msgid "Discard post?" 2769 msgstr "" 2770 ··· 2787 msgid "Dismiss" 2788 msgstr "" 2789 2790 - #: src/view/com/composer/Composer.tsx:1807 2791 msgid "Dismiss error" 2792 msgstr "" 2793 ··· 3009 msgid "Edit People" 3010 msgstr "" 3011 3012 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:73 3013 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:243 3014 msgid "Edit post interaction settings" 3015 msgstr "" 3016 ··· 3229 msgid "Entertainment" 3230 msgstr "" 3231 3232 - #: src/view/com/composer/Composer.tsx:1892 3233 #: src/view/com/util/error/ErrorScreen.tsx:42 3234 msgid "Error" 3235 msgstr "" ··· 3262 msgid "Error: {error}" 3263 msgstr "" 3264 3265 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:398 3266 msgid "Everybody" 3267 msgstr "" 3268 ··· 3964 msgid "From @{sanitizedAuthor}" 3965 msgstr "" 3966 3967 - #: src/view/com/posts/PostFeedItem.tsx:345 3968 msgctxt "from-feed" 3969 msgid "From <0/>" 3970 msgstr "" ··· 4136 msgid "Go to conversation with {0}" 4137 msgstr "" 4138 4139 #: src/screens/Login/ForgotPasswordForm.tsx:165 4140 msgid "Go to next" 4141 msgstr "" ··· 4609 msgid "It's just you right now! Add more people to your starter pack by searching above." 4610 msgstr "" 4611 4612 - #: src/view/com/composer/Composer.tsx:1826 4613 msgid "Job ID: {0}" 4614 msgstr "" 4615 ··· 5138 msgid "mentioned users" 5139 msgstr "" 5140 5141 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:427 5142 msgid "Mentioned users" 5143 msgstr "" 5144 ··· 5726 msgid "No thanks" 5727 msgstr "" 5728 5729 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:409 5730 msgid "Nobody" 5731 msgstr "" 5732 ··· 5890 msgid "Onboarding reset" 5891 msgstr "" 5892 5893 - #: src/view/com/composer/Composer.tsx:398 5894 msgid "One or more GIFs is missing alt text." 5895 msgstr "" 5896 5897 - #: src/view/com/composer/Composer.tsx:395 5898 msgid "One or more images is missing alt text." 5899 msgstr "" 5900 ··· 5906 msgid "One or more of your selected files are too large. Maximum size is 100 MB." 5907 msgstr "" 5908 5909 - #: src/view/com/composer/Composer.tsx:405 5910 msgid "One or more videos is missing alt text." 5911 msgstr "" 5912 ··· 5963 msgstr "" 5964 5965 #: src/screens/Messages/components/MessageInput.web.tsx:181 5966 - #: src/view/com/composer/Composer.tsx:1475 5967 msgid "Open emoji picker" 5968 msgstr "" 5969 ··· 6063 msgid "Opens device gallery to select up to {MAX_IMAGES, plural, other {# images}}, or a single video or GIF." 6064 msgstr "" 6065 6066 - #: src/view/com/composer/Composer.tsx:1476 6067 msgid "Opens emoji picker" 6068 msgstr "" 6069 ··· 6115 msgid "Options:" 6116 msgstr "" 6117 6118 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:422 6119 msgid "Or combine these options:" 6120 msgstr "" 6121 ··· 6273 msgid "Pin to your profile" 6274 msgstr "" 6275 6276 - #: src/view/com/posts/PostFeedItem.tsx:426 6277 msgid "Pinned" 6278 msgstr "" 6279 ··· 6475 msgid "Porn" 6476 msgstr "" 6477 6478 - #: src/screens/PostThread/index.tsx:500 6479 msgctxt "description" 6480 msgid "Post" 6481 msgstr "" 6482 6483 - #: src/view/com/composer/Composer.tsx:1118 6484 msgctxt "action" 6485 msgid "Post" 6486 msgstr "" 6487 6488 - #: src/view/com/composer/Composer.tsx:1116 6489 msgctxt "action" 6490 msgid "Post All" 6491 msgstr "" ··· 6527 msgid "Post Hidden by You" 6528 msgstr "" 6529 6530 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:104 6531 msgid "Post interaction settings" 6532 msgstr "" 6533 ··· 6650 msgid "Privacy Policy" 6651 msgstr "" 6652 6653 - #: src/view/com/composer/Composer.tsx:1889 6654 msgid "Processing video..." 6655 msgstr "" 6656 ··· 6689 msgstr "" 6690 6691 #. Accessibility label for button to publish a single post 6692 - #: src/view/com/composer/Composer.tsx:1098 6693 msgid "Publish post" 6694 msgstr "" 6695 6696 #. Accessibility label for button to publish multiple posts in a thread 6697 - #: src/view/com/composer/Composer.tsx:1091 6698 msgid "Publish posts" 6699 msgstr "" 6700 6701 #. Accessibility label for button to publish multiple replies in a thread 6702 - #: src/view/com/composer/Composer.tsx:1076 6703 msgid "Publish replies" 6704 msgstr "" 6705 6706 #. Accessibility label for button to publish a single reply 6707 - #: src/view/com/composer/Composer.tsx:1083 6708 msgid "Publish reply" 6709 msgstr "" 6710 ··· 6762 msgid "Quote posts disabled" 6763 msgstr "" 6764 6765 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:333 6766 msgid "Quote settings" 6767 msgstr "" 6768 ··· 7094 msgid "Replies to this post are disabled." 7095 msgstr "" 7096 7097 - #: src/view/com/composer/Composer.tsx:1114 7098 msgctxt "action" 7099 msgid "Reply" 7100 msgstr "" ··· 7118 msgid "Reply notifications" 7119 msgstr "" 7120 7121 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:389 7122 msgid "Reply settings" 7123 msgstr "" 7124 7125 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:374 7126 msgid "Reply settings are chosen by the author of the thread" 7127 msgstr "" 7128 ··· 7256 msgid "Reposted By" 7257 msgstr "" 7258 7259 - #: src/view/com/posts/PostFeedItem.tsx:366 7260 - msgid "Reposted by {0}" 7261 msgstr "" 7262 7263 - #: src/view/com/posts/PostFeedItem.tsx:385 7264 - msgid "Reposted by <0><1/></0>" 7265 msgstr "" 7266 7267 - #: src/view/com/posts/PostFeedItem.tsx:364 7268 - #: src/view/com/posts/PostFeedItem.tsx:383 7269 msgid "Reposted by you" 7270 msgstr "" 7271 ··· 7410 #: src/components/dialogs/BirthDateSettings.tsx:156 7411 #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:292 7412 #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:307 7413 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:483 7414 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:489 7415 #: src/components/live/EditLiveDialog.tsx:216 7416 #: src/components/live/EditLiveDialog.tsx:223 7417 #: src/components/StarterPack/QrCodeDialog.tsx:204 ··· 8880 msgid "There was an issue! {0}" 8881 msgstr "" 8882 8883 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:221 8884 #: src/screens/List/ListHiddenScreen.tsx:63 8885 #: src/screens/List/ListHiddenScreen.tsx:77 8886 #: src/screens/List/ListHiddenScreen.tsx:99 ··· 9075 msgid "This post will be hidden from feeds and threads. This cannot be undone." 9076 msgstr "" 9077 9078 - #: src/view/com/composer/Composer.tsx:514 9079 msgid "This post's author has disabled quote posts." 9080 msgstr "" 9081 ··· 9503 msgid "Unsubscribed from list" 9504 msgstr "" 9505 9506 - #: src/view/com/composer/Composer.tsx:894 9507 msgid "Unsupported video type: {mimeType}" 9508 msgstr "" 9509 ··· 9589 msgid "Uploading link thumbnail..." 9590 msgstr "" 9591 9592 - #: src/view/com/composer/Composer.tsx:1886 9593 msgid "Uploading video..." 9594 msgstr "" 9595 ··· 9705 msgid "Users I follow" 9706 msgstr "" 9707 9708 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:460 9709 msgid "Users in \"{0}\"" 9710 msgstr "" 9711 9712 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:437 9713 msgid "Users you follow" 9714 msgstr "" 9715 ··· 9847 msgid "Video settings" 9848 msgstr "" 9849 9850 - #: src/view/com/composer/Composer.tsx:1896 9851 msgid "Video uploaded" 9852 msgstr "" 9853 ··· 9863 msgid "Videos must be less than 3 minutes long." 9864 msgstr "" 9865 9866 - #: src/view/com/composer/Composer.tsx:585 9867 msgctxt "Action to view the post the user just created" 9868 msgid "View" 9869 msgstr "" ··· 9925 msgid "View more trending videos" 9926 msgstr "" 9927 9928 - #: src/view/com/composer/Composer.tsx:580 9929 msgid "View post" 9930 msgstr "" 9931 ··· 10153 msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." 10154 msgstr "" 10155 10156 - #: src/view/com/composer/Composer.tsx:511 10157 msgid "We're sorry! The post you are replying to has been deleted." 10158 msgstr "" 10159 ··· 10204 10205 #: src/view/com/auth/SplashScreen.tsx:51 10206 #: src/view/com/auth/SplashScreen.web.tsx:103 10207 - #: src/view/com/composer/Composer.tsx:854 10208 msgid "What's up?" 10209 msgstr "" 10210 ··· 10282 msgid "Write a message" 10283 msgstr "" 10284 10285 - #: src/view/com/composer/Composer.tsx:954 10286 msgid "Write post" 10287 msgstr "" 10288 10289 #: src/screens/PostThread/components/ThreadComposePrompt.tsx:90 10290 - #: src/view/com/composer/Composer.tsx:852 10291 msgid "Write your reply" 10292 msgstr "" 10293 ··· 10443 msgid "You can select up to {MAX_IMAGES, plural, other {# images}} in total." 10444 msgstr "" 10445 10446 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:85 10447 msgid "You can set default interaction settings in <0>Settings → Moderation → Interaction settings</0>." 10448 msgstr "" 10449 ··· 10825 msgid "Your first like!" 10826 msgstr "" 10827 10828 - #: src/components/dialogs/PostInteractionSettingsDialog.tsx:447 10829 msgid "Your followers" 10830 msgstr "" 10831 ··· 10870 msgid "Your password must be at least 8 characters long." 10871 msgstr "" 10872 10873 - #: src/view/com/composer/Composer.tsx:576 10874 msgid "Your post was sent" 10875 msgstr "" 10876 10877 - #: src/view/com/composer/Composer.tsx:573 10878 msgid "Your posts were sent" 10879 msgstr "" 10880 ··· 10899 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 msgstr "" 10901 10902 - #: src/view/com/composer/Composer.tsx:575 10903 msgid "Your reply was sent" 10904 msgstr "" 10905
··· 712 msgid "Add another account" 713 msgstr "" 714 715 + #: src/view/com/composer/Composer.tsx:852 716 msgid "Add another post" 717 msgstr "" 718 719 + #: src/view/com/composer/Composer.tsx:1489 720 msgid "Add another post to thread" 721 msgstr "" 722 ··· 911 msgid "Allow others to be notified of your posts" 912 msgstr "" 913 914 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:355 915 msgid "Allow quote posts" 916 msgstr "" 917 918 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:400 919 msgid "Allow replies from:" 920 msgstr "" 921 ··· 1225 msgid "Are you sure you want to remove this from your feeds?" 1226 msgstr "" 1227 1228 + #: src/view/com/composer/Composer.tsx:801 1229 msgid "Are you sure you'd like to discard this draft?" 1230 msgstr "" 1231 1232 + #: src/view/com/composer/Composer.tsx:991 1233 msgid "Are you sure you'd like to discard this post?" 1234 msgstr "" 1235 ··· 1630 #: src/screens/Settings/Settings.tsx:289 1631 #: src/screens/Takendown.tsx:108 1632 #: src/screens/Takendown.tsx:111 1633 + #: src/view/com/composer/Composer.tsx:1046 1634 + #: src/view/com/composer/Composer.tsx:1057 1635 #: src/view/com/composer/photos/EditImageDialog.web.tsx:43 1636 #: src/view/com/composer/photos/EditImageDialog.web.tsx:52 1637 #: src/view/shell/desktop/LeftNav.tsx:213 ··· 1884 msgid "Click here to update your email" 1885 msgstr "" 1886 1887 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:348 1888 msgid "Click to disable quote posts of this post." 1889 msgstr "" 1890 1891 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:349 1892 msgid "Click to enable quote posts of this post." 1893 msgstr "" 1894 ··· 2007 msgid "Closes password update alert" 2008 msgstr "" 2009 2010 + #: src/view/com/composer/Composer.tsx:1054 2011 msgid "Closes post composer and discards post draft" 2012 msgstr "" 2013 ··· 2066 msgid "Compose new post" 2067 msgstr "" 2068 2069 + #: src/view/com/composer/Composer.tsx:955 2070 msgid "Compose posts up to {0, plural, other {# characters}} in length" 2071 msgstr "" 2072 ··· 2074 msgid "Compose reply" 2075 msgstr "" 2076 2077 + #: src/view/com/composer/Composer.tsx:1884 2078 msgid "Compressing video..." 2079 msgstr "" 2080 ··· 2512 msgid "Customization options" 2513 msgstr "" 2514 2515 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:111 2516 msgid "Customize who can interact with this post." 2517 msgstr "" 2518 ··· 2637 2638 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:710 2639 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:712 2640 + #: src/view/com/composer/Composer.tsx:965 2641 msgid "Delete post" 2642 msgstr "" 2643 ··· 2750 2751 #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:92 2752 #: src/screens/Profile/Header/EditProfileDialog.tsx:82 2753 + #: src/view/com/composer/Composer.tsx:803 2754 + #: src/view/com/composer/Composer.tsx:998 2755 msgid "Discard" 2756 msgstr "" 2757 ··· 2760 msgid "Discard changes?" 2761 msgstr "" 2762 2763 + #: src/view/com/composer/Composer.tsx:800 2764 msgid "Discard draft?" 2765 msgstr "" 2766 2767 + #: src/view/com/composer/Composer.tsx:990 2768 msgid "Discard post?" 2769 msgstr "" 2770 ··· 2787 msgid "Dismiss" 2788 msgstr "" 2789 2790 + #: src/view/com/composer/Composer.tsx:1808 2791 msgid "Dismiss error" 2792 msgstr "" 2793 ··· 3009 msgid "Edit People" 3010 msgstr "" 3011 3012 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:77 3013 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:250 3014 msgid "Edit post interaction settings" 3015 msgstr "" 3016 ··· 3229 msgid "Entertainment" 3230 msgstr "" 3231 3232 + #: src/view/com/composer/Composer.tsx:1893 3233 #: src/view/com/util/error/ErrorScreen.tsx:42 3234 msgid "Error" 3235 msgstr "" ··· 3262 msgid "Error: {error}" 3263 msgstr "" 3264 3265 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:405 3266 msgid "Everybody" 3267 msgstr "" 3268 ··· 3964 msgid "From @{sanitizedAuthor}" 3965 msgstr "" 3966 3967 + #: src/view/com/posts/PostFeedReason.tsx:47 3968 msgctxt "from-feed" 3969 msgid "From <0/>" 3970 msgstr "" ··· 4136 msgid "Go to conversation with {0}" 4137 msgstr "" 4138 4139 + #: src/view/com/posts/PostFeedReason.tsx:38 4140 + msgid "Go to feed" 4141 + msgstr "" 4142 + 4143 #: src/screens/Login/ForgotPasswordForm.tsx:165 4144 msgid "Go to next" 4145 msgstr "" ··· 4613 msgid "It's just you right now! Add more people to your starter pack by searching above." 4614 msgstr "" 4615 4616 + #: src/view/com/composer/Composer.tsx:1827 4617 msgid "Job ID: {0}" 4618 msgstr "" 4619 ··· 5142 msgid "mentioned users" 5143 msgstr "" 5144 5145 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:434 5146 msgid "Mentioned users" 5147 msgstr "" 5148 ··· 5730 msgid "No thanks" 5731 msgstr "" 5732 5733 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:416 5734 msgid "Nobody" 5735 msgstr "" 5736 ··· 5894 msgid "Onboarding reset" 5895 msgstr "" 5896 5897 + #: src/view/com/composer/Composer.tsx:397 5898 msgid "One or more GIFs is missing alt text." 5899 msgstr "" 5900 5901 + #: src/view/com/composer/Composer.tsx:394 5902 msgid "One or more images is missing alt text." 5903 msgstr "" 5904 ··· 5910 msgid "One or more of your selected files are too large. Maximum size is 100 MB." 5911 msgstr "" 5912 5913 + #: src/view/com/composer/Composer.tsx:404 5914 msgid "One or more videos is missing alt text." 5915 msgstr "" 5916 ··· 5967 msgstr "" 5968 5969 #: src/screens/Messages/components/MessageInput.web.tsx:181 5970 + #: src/view/com/composer/Composer.tsx:1474 5971 msgid "Open emoji picker" 5972 msgstr "" 5973 ··· 6067 msgid "Opens device gallery to select up to {MAX_IMAGES, plural, other {# images}}, or a single video or GIF." 6068 msgstr "" 6069 6070 + #: src/view/com/composer/Composer.tsx:1475 6071 msgid "Opens emoji picker" 6072 msgstr "" 6073 ··· 6119 msgid "Options:" 6120 msgstr "" 6121 6122 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:429 6123 msgid "Or combine these options:" 6124 msgstr "" 6125 ··· 6277 msgid "Pin to your profile" 6278 msgstr "" 6279 6280 + #: src/view/com/posts/PostFeedReason.tsx:125 6281 msgid "Pinned" 6282 msgstr "" 6283 ··· 6479 msgid "Porn" 6480 msgstr "" 6481 6482 + #: src/screens/PostThread/index.tsx:504 6483 msgctxt "description" 6484 msgid "Post" 6485 msgstr "" 6486 6487 + #: src/view/com/composer/Composer.tsx:1117 6488 msgctxt "action" 6489 msgid "Post" 6490 msgstr "" 6491 6492 + #: src/view/com/composer/Composer.tsx:1115 6493 msgctxt "action" 6494 msgid "Post All" 6495 msgstr "" ··· 6531 msgid "Post Hidden by You" 6532 msgstr "" 6533 6534 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:108 6535 msgid "Post interaction settings" 6536 msgstr "" 6537 ··· 6654 msgid "Privacy Policy" 6655 msgstr "" 6656 6657 + #: src/view/com/composer/Composer.tsx:1890 6658 msgid "Processing video..." 6659 msgstr "" 6660 ··· 6693 msgstr "" 6694 6695 #. Accessibility label for button to publish a single post 6696 + #: src/view/com/composer/Composer.tsx:1097 6697 msgid "Publish post" 6698 msgstr "" 6699 6700 #. Accessibility label for button to publish multiple posts in a thread 6701 + #: src/view/com/composer/Composer.tsx:1090 6702 msgid "Publish posts" 6703 msgstr "" 6704 6705 #. Accessibility label for button to publish multiple replies in a thread 6706 + #: src/view/com/composer/Composer.tsx:1075 6707 msgid "Publish replies" 6708 msgstr "" 6709 6710 #. Accessibility label for button to publish a single reply 6711 + #: src/view/com/composer/Composer.tsx:1082 6712 msgid "Publish reply" 6713 msgstr "" 6714 ··· 6766 msgid "Quote posts disabled" 6767 msgstr "" 6768 6769 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:340 6770 msgid "Quote settings" 6771 msgstr "" 6772 ··· 7098 msgid "Replies to this post are disabled." 7099 msgstr "" 7100 7101 + #: src/view/com/composer/Composer.tsx:1113 7102 msgctxt "action" 7103 msgid "Reply" 7104 msgstr "" ··· 7122 msgid "Reply notifications" 7123 msgstr "" 7124 7125 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:396 7126 msgid "Reply settings" 7127 msgstr "" 7128 7129 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:381 7130 msgid "Reply settings are chosen by the author of the thread" 7131 msgstr "" 7132 ··· 7260 msgid "Reposted By" 7261 msgstr "" 7262 7263 + #: src/view/com/posts/PostFeedReason.tsx:77 7264 + msgid "Reposted by {reposter}" 7265 msgstr "" 7266 7267 + #: src/view/com/posts/PostFeedReason.tsx:91 7268 + msgid "Reposted by <0><1>{reposter}</1></0>" 7269 msgstr "" 7270 7271 + #: src/view/com/posts/PostFeedReason.tsx:77 7272 + #: src/view/com/posts/PostFeedReason.tsx:89 7273 msgid "Reposted by you" 7274 msgstr "" 7275 ··· 7414 #: src/components/dialogs/BirthDateSettings.tsx:156 7415 #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:292 7416 #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:307 7417 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:490 7418 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:496 7419 #: src/components/live/EditLiveDialog.tsx:216 7420 #: src/components/live/EditLiveDialog.tsx:223 7421 #: src/components/StarterPack/QrCodeDialog.tsx:204 ··· 8884 msgid "There was an issue! {0}" 8885 msgstr "" 8886 8887 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:228 8888 #: src/screens/List/ListHiddenScreen.tsx:63 8889 #: src/screens/List/ListHiddenScreen.tsx:77 8890 #: src/screens/List/ListHiddenScreen.tsx:99 ··· 9079 msgid "This post will be hidden from feeds and threads. This cannot be undone." 9080 msgstr "" 9081 9082 + #: src/view/com/composer/Composer.tsx:513 9083 msgid "This post's author has disabled quote posts." 9084 msgstr "" 9085 ··· 9507 msgid "Unsubscribed from list" 9508 msgstr "" 9509 9510 + #: src/view/com/composer/Composer.tsx:893 9511 msgid "Unsupported video type: {mimeType}" 9512 msgstr "" 9513 ··· 9593 msgid "Uploading link thumbnail..." 9594 msgstr "" 9595 9596 + #: src/view/com/composer/Composer.tsx:1887 9597 msgid "Uploading video..." 9598 msgstr "" 9599 ··· 9709 msgid "Users I follow" 9710 msgstr "" 9711 9712 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:467 9713 msgid "Users in \"{0}\"" 9714 msgstr "" 9715 9716 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:444 9717 msgid "Users you follow" 9718 msgstr "" 9719 ··· 9851 msgid "Video settings" 9852 msgstr "" 9853 9854 + #: src/view/com/composer/Composer.tsx:1897 9855 msgid "Video uploaded" 9856 msgstr "" 9857 ··· 9867 msgid "Videos must be less than 3 minutes long." 9868 msgstr "" 9869 9870 + #: src/view/com/composer/Composer.tsx:584 9871 msgctxt "Action to view the post the user just created" 9872 msgid "View" 9873 msgstr "" ··· 9929 msgid "View more trending videos" 9930 msgstr "" 9931 9932 + #: src/view/com/composer/Composer.tsx:579 9933 msgid "View post" 9934 msgstr "" 9935 ··· 10157 msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." 10158 msgstr "" 10159 10160 + #: src/view/com/composer/Composer.tsx:510 10161 msgid "We're sorry! The post you are replying to has been deleted." 10162 msgstr "" 10163 ··· 10208 10209 #: src/view/com/auth/SplashScreen.tsx:51 10210 #: src/view/com/auth/SplashScreen.web.tsx:103 10211 + #: src/view/com/composer/Composer.tsx:853 10212 msgid "What's up?" 10213 msgstr "" 10214 ··· 10286 msgid "Write a message" 10287 msgstr "" 10288 10289 + #: src/view/com/composer/Composer.tsx:953 10290 msgid "Write post" 10291 msgstr "" 10292 10293 #: src/screens/PostThread/components/ThreadComposePrompt.tsx:90 10294 + #: src/view/com/composer/Composer.tsx:851 10295 msgid "Write your reply" 10296 msgstr "" 10297 ··· 10447 msgid "You can select up to {MAX_IMAGES, plural, other {# images}} in total." 10448 msgstr "" 10449 10450 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:89 10451 msgid "You can set default interaction settings in <0>Settings → Moderation → Interaction settings</0>." 10452 msgstr "" 10453 ··· 10829 msgid "Your first like!" 10830 msgstr "" 10831 10832 + #: src/components/dialogs/PostInteractionSettingsDialog.tsx:454 10833 msgid "Your followers" 10834 msgstr "" 10835 ··· 10874 msgid "Your password must be at least 8 characters long." 10875 msgstr "" 10876 10877 + #: src/view/com/composer/Composer.tsx:575 10878 msgid "Your post was sent" 10879 msgstr "" 10880 10881 + #: src/view/com/composer/Composer.tsx:572 10882 msgid "Your posts were sent" 10883 msgstr "" 10884 ··· 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." 10904 msgstr "" 10905 10906 + #: src/view/com/composer/Composer.tsx:574 10907 msgid "Your reply was sent" 10908 msgstr "" 10909
+4 -4
src/screens/Post/PostLikedBy.tsx
··· 7 type NativeStackScreenProps, 8 } from '#/lib/routes/types' 9 import {makeRecordUri} from '#/lib/strings/url-helpers' 10 - import {usePostThreadQuery} from '#/state/queries/post-thread' 11 import {useSetMinimalShellMode} from '#/state/shell' 12 import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy' 13 import * as Layout from '#/components/Layout' ··· 17 const setMinimalShellMode = useSetMinimalShellMode() 18 const {name, rkey} = route.params 19 const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 20 - const {data: post} = usePostThreadQuery(uri) 21 22 let likeCount 23 - if (post?.thread.type === 'post') { 24 - likeCount = post.thread.post.likeCount 25 } 26 27 useFocusEffect(
··· 7 type NativeStackScreenProps, 8 } from '#/lib/routes/types' 9 import {makeRecordUri} from '#/lib/strings/url-helpers' 10 + import {usePostQuery} from '#/state/queries/post' 11 import {useSetMinimalShellMode} from '#/state/shell' 12 import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy' 13 import * as Layout from '#/components/Layout' ··· 17 const setMinimalShellMode = useSetMinimalShellMode() 18 const {name, rkey} = route.params 19 const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 20 + const {data: post} = usePostQuery(uri) 21 22 let likeCount 23 + if (post) { 24 + likeCount = post.likeCount 25 } 26 27 useFocusEffect(
+4 -4
src/screens/Post/PostQuotes.tsx
··· 7 type NativeStackScreenProps, 8 } from '#/lib/routes/types' 9 import {makeRecordUri} from '#/lib/strings/url-helpers' 10 - import {usePostThreadQuery} from '#/state/queries/post-thread' 11 import {useSetMinimalShellMode} from '#/state/shell' 12 import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuotes' 13 import * as Layout from '#/components/Layout' ··· 17 const setMinimalShellMode = useSetMinimalShellMode() 18 const {name, rkey} = route.params 19 const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 20 - const {data: post} = usePostThreadQuery(uri) 21 22 let quoteCount 23 - if (post?.thread.type === 'post') { 24 - quoteCount = post.thread.post.quoteCount 25 } 26 27 useFocusEffect(
··· 7 type NativeStackScreenProps, 8 } from '#/lib/routes/types' 9 import {makeRecordUri} from '#/lib/strings/url-helpers' 10 + import {usePostQuery} from '#/state/queries/post' 11 import {useSetMinimalShellMode} from '#/state/shell' 12 import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuotes' 13 import * as Layout from '#/components/Layout' ··· 17 const setMinimalShellMode = useSetMinimalShellMode() 18 const {name, rkey} = route.params 19 const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 20 + const {data: post} = usePostQuery(uri) 21 22 let quoteCount 23 + if (post) { 24 + quoteCount = post.quoteCount 25 } 26 27 useFocusEffect(
+4 -4
src/screens/Post/PostRepostedBy.tsx
··· 7 type NativeStackScreenProps, 8 } from '#/lib/routes/types' 9 import {makeRecordUri} from '#/lib/strings/url-helpers' 10 - import {usePostThreadQuery} from '#/state/queries/post-thread' 11 import {useSetMinimalShellMode} from '#/state/shell' 12 import {PostRepostedBy as PostRepostedByComponent} from '#/view/com/post-thread/PostRepostedBy' 13 import * as Layout from '#/components/Layout' ··· 17 const {name, rkey} = route.params 18 const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 19 const setMinimalShellMode = useSetMinimalShellMode() 20 - const {data: post} = usePostThreadQuery(uri) 21 22 let quoteCount 23 - if (post?.thread.type === 'post') { 24 - quoteCount = post.thread.post.repostCount 25 } 26 27 useFocusEffect(
··· 7 type NativeStackScreenProps, 8 } from '#/lib/routes/types' 9 import {makeRecordUri} from '#/lib/strings/url-helpers' 10 + import {usePostQuery} from '#/state/queries/post' 11 import {useSetMinimalShellMode} from '#/state/shell' 12 import {PostRepostedBy as PostRepostedByComponent} from '#/view/com/post-thread/PostRepostedBy' 13 import * as Layout from '#/components/Layout' ··· 17 const {name, rkey} = route.params 18 const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 19 const setMinimalShellMode = useSetMinimalShellMode() 20 + const {data: post} = usePostQuery(uri) 21 22 let quoteCount 23 + if (post) { 24 + quoteCount = post.repostCount 25 } 26 27 useFocusEffect(
+7 -3
src/screens/PostThread/index.tsx
··· 7 import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 8 import {useFeedFeedback} from '#/state/feed-feedback' 9 import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences' 10 - import {type ThreadItem, usePostThread} from '#/state/queries/usePostThread' 11 import {useSession} from '#/state/session' 12 import {type OnPostSuccessData} from '#/state/shell/composer' 13 import {useShellLayout} from '#/state/shell/shell-layout' ··· 492 const defaultListFooterHeight = hasParents ? windowHeight - 200 : undefined 493 494 return ( 495 - <> 496 <Layout.Header.Outer headerRef={headerRef}> 497 <Layout.Header.BackButton /> 498 <Layout.Header.Content> ··· 575 {!gtMobile && canReply && hasSession && ( 576 <MobileComposePrompt onPressReply={onReplyToAnchor} /> 577 )} 578 - </> 579 ) 580 } 581
··· 7 import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 8 import {useFeedFeedback} from '#/state/feed-feedback' 9 import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences' 10 + import { 11 + PostThreadContextProvider, 12 + type ThreadItem, 13 + usePostThread, 14 + } from '#/state/queries/usePostThread' 15 import {useSession} from '#/state/session' 16 import {type OnPostSuccessData} from '#/state/shell/composer' 17 import {useShellLayout} from '#/state/shell/shell-layout' ··· 496 const defaultListFooterHeight = hasParents ? windowHeight - 200 : undefined 497 498 return ( 499 + <PostThreadContextProvider context={thread.context}> 500 <Layout.Header.Outer headerRef={headerRef}> 501 <Layout.Header.BackButton /> 502 <Layout.Header.Content> ··· 579 {!gtMobile && canReply && hasSession && ( 580 <MobileComposePrompt onPressReply={onReplyToAnchor} /> 581 )} 582 + </PostThreadContextProvider> 583 ) 584 } 585
-6
src/state/cache/post-shadow.ts
··· 12 import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed' 13 import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed' 14 import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' 15 - import {findAllPostsInQueryData as findAllPostsInThreadQueryData} from '#/state/queries/post-thread' 16 import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts' 17 import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/state/queries/usePostThread/queryCache' 18 import {castAsShadow, type Shadow} from './types' ··· 175 } 176 for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) { 177 yield post 178 - } 179 - for (let node of findAllPostsInThreadQueryData(queryClient, uri)) { 180 - if (node.type === 'post') { 181 - yield node.post 182 - } 183 } 184 for (let post of findAllPostsInThreadV2QueryData(queryClient, uri)) { 185 yield post
··· 12 import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed' 13 import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed' 14 import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' 15 import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts' 16 import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/state/queries/usePostThread/queryCache' 17 import {castAsShadow, type Shadow} from './types' ··· 174 } 175 for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) { 176 yield post 177 } 178 for (let post of findAllPostsInThreadV2QueryData(queryClient, uri)) { 179 yield post
-2
src/state/cache/profile-shadow.ts
··· 16 import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '#/state/queries/post-liked-by' 17 import {findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData} from '#/state/queries/post-quotes' 18 import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '#/state/queries/post-reposted-by' 19 - import {findAllProfilesInQueryData as findAllProfilesInPostThreadQueryData} from '#/state/queries/post-thread' 20 import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '#/state/queries/profile' 21 import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers' 22 import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows' ··· 175 yield* findAllProfilesInActorSearchQueryData(queryClient, did) 176 yield* findAllProfilesInListConvosQueryData(queryClient, did) 177 yield* findAllProfilesInFeedsQueryData(queryClient, did) 178 - yield* findAllProfilesInPostThreadQueryData(queryClient, did) 179 yield* findAllProfilesInPostThreadV2QueryData(queryClient, did) 180 yield* findAllProfilesInKnownFollowersQueryData(queryClient, did) 181 yield* findAllProfilesInExploreFeedPreviewsQueryData(queryClient, did)
··· 16 import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '#/state/queries/post-liked-by' 17 import {findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData} from '#/state/queries/post-quotes' 18 import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '#/state/queries/post-reposted-by' 19 import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '#/state/queries/profile' 20 import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers' 21 import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows' ··· 174 yield* findAllProfilesInActorSearchQueryData(queryClient, did) 175 yield* findAllProfilesInListConvosQueryData(queryClient, did) 176 yield* findAllProfilesInFeedsQueryData(queryClient, did) 177 yield* findAllProfilesInPostThreadV2QueryData(queryClient, did) 178 yield* findAllProfilesInKnownFollowersQueryData(queryClient, did) 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 import { 2 - AppBskyFeedDefs, 3 - type AppBskyFeedGetPostThread, 4 AppBskyFeedThreadgate, 5 AtUri, 6 type BskyAgent, ··· 8 import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' 9 10 import {networkRetry, retry} from '#/lib/async/retry' 11 - import {until} from '#/lib/async/until' 12 import {STALE} from '#/state/queries' 13 - import {RQKEY_ROOT as postThreadQueryKeyRoot} from '#/state/queries/post-thread' 14 import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate/types' 15 import { 16 createThreadgateRecord, ··· 18 threadgateAllowUISettingToAllowRecordValue, 19 threadgateViewToAllowUISetting, 20 } from '#/state/queries/threadgate/util' 21 import {useAgent} from '#/state/session' 22 import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies' 23 import * as bsky from '#/types/bsky' ··· 71 postUri?: string 72 initialData?: AppBskyFeedDefs.ThreadgateView 73 } = {}) { 74 - const agent = useAgent() 75 76 return useQuery({ 77 enabled: !!postUri, ··· 79 placeholderData: initialData, 80 staleTime: STALE.MINUTES.ONE, 81 async queryFn() { 82 - return getThreadgateView({ 83 - agent, 84 - postUri: postUri!, 85 - }) 86 }, 87 }) 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 } 108 109 export async function getThreadgateRecord({ ··· 248 export function useSetThreadgateAllowMutation() { 249 const agent = useAgent() 250 const queryClient = useQueryClient() 251 252 return useMutation({ 253 mutationFn: async ({ ··· 272 }) 273 }, 274 async onSuccess(_, {postUri, allow}) { 275 - await until( 276 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, 283 ) 284 - return JSON.stringify(fetchedSettings) === JSON.stringify(allow) 285 } 286 - return false 287 - }, 288 - () => { 289 - return agent.app.bsky.feed.getPostThread({ 290 - uri: postUri, 291 - depth: 0, 292 - }) 293 }, 294 - ) 295 296 - queryClient.invalidateQueries({ 297 - queryKey: [postThreadQueryKeyRoot], 298 - }) 299 queryClient.invalidateQueries({ 300 queryKey: [threadgateRecordQueryKeyRoot], 301 })
··· 1 import { 2 + type AppBskyFeedDefs, 3 AppBskyFeedThreadgate, 4 AtUri, 5 type BskyAgent, ··· 7 import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' 8 9 import {networkRetry, retry} from '#/lib/async/retry' 10 import {STALE} from '#/state/queries' 11 + import {useGetPost} from '#/state/queries/post' 12 import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate/types' 13 import { 14 createThreadgateRecord, ··· 16 threadgateAllowUISettingToAllowRecordValue, 17 threadgateViewToAllowUISetting, 18 } from '#/state/queries/threadgate/util' 19 + import {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread' 20 import {useAgent} from '#/state/session' 21 import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies' 22 import * as bsky from '#/types/bsky' ··· 70 postUri?: string 71 initialData?: AppBskyFeedDefs.ThreadgateView 72 } = {}) { 73 + const getPost = useGetPost() 74 75 return useQuery({ 76 enabled: !!postUri, ··· 78 placeholderData: initialData, 79 staleTime: STALE.MINUTES.ONE, 80 async queryFn() { 81 + const post = await getPost({uri: postUri!}) 82 + return post.threadgate ?? null 83 }, 84 }) 85 } 86 87 export async function getThreadgateRecord({ ··· 226 export function useSetThreadgateAllowMutation() { 227 const agent = useAgent() 228 const queryClient = useQueryClient() 229 + const getPost = useGetPost() 230 + const updatePostThreadThreadgate = useUpdatePostThreadThreadgateQueryCache() 231 232 return useMutation({ 233 mutationFn: async ({ ··· 252 }) 253 }, 254 async onSuccess(_, {postUri, allow}) { 255 + const data = await retry<AppBskyFeedDefs.ThreadgateView | undefined>( 256 5, // 5 tries 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`, 264 ) 265 } 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 275 }, 276 + 1e3, // 1s delay between tries 277 + ).catch(() => {}) 278 + 279 + if (data) updatePostThreadThreadgate(data) 280 281 queryClient.invalidateQueries({ 282 queryKey: [threadgateRecordQueryKeyRoot], 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 TREE_VIEW_BELOW_DESKTOP, 12 TREE_VIEW_BF, 13 } from '#/state/queries/usePostThread/const' 14 import { 15 createCacheMutator, 16 getThreadPlaceholder, ··· 31 import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 32 import {useBreakpoints} from '#/alf' 33 34 export * from '#/state/queries/usePostThread/types' 35 36 export function usePostThread({anchor}: {anchor?: string}) { ··· 277 setOtherItemsVisible, 278 ]) 279 280 - return useMemo( 281 - () => ({ 282 state: { 283 /* 284 * Copy in any query state that is useful ··· 309 setSort, 310 setView, 311 }, 312 - }), 313 - [ 314 - query, 315 - mutator.insertReplies, 316 - otherItemsVisible, 317 - sort, 318 - view, 319 - setSort, 320 - setView, 321 - threadgate, 322 - items, 323 - ], 324 - ) 325 }
··· 11 TREE_VIEW_BELOW_DESKTOP, 12 TREE_VIEW_BF, 13 } from '#/state/queries/usePostThread/const' 14 + import {type PostThreadContextType} from '#/state/queries/usePostThread/context' 15 import { 16 createCacheMutator, 17 getThreadPlaceholder, ··· 32 import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 33 import {useBreakpoints} from '#/alf' 34 35 + export * from '#/state/queries/usePostThread/context' 36 + export {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread/queryCache' 37 export * from '#/state/queries/usePostThread/types' 38 39 export function usePostThread({anchor}: {anchor?: string}) { ··· 280 setOtherItemsVisible, 281 ]) 282 283 + return useMemo(() => { 284 + const context: PostThreadContextType = { 285 + postThreadQueryKey, 286 + postThreadOtherQueryKey, 287 + } 288 + return { 289 + context, 290 state: { 291 /* 292 * Copy in any query state that is useful ··· 317 setSort, 318 setView, 319 }, 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 + ]) 334 }
+51 -1
src/state/queries/usePostThread/queryCache.ts
··· 1 import { 2 type $Typed, 3 type AppBskyActorDefs, ··· 7 type AppBskyUnspeccedGetPostThreadV2, 8 AtUri, 9 } from '@atproto/api' 10 - import {type QueryClient} from '@tanstack/react-query' 11 12 import { 13 dangerousGetPostShadow, ··· 18 import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed' 19 import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' 20 import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts' 21 import {getBranch} from '#/state/queries/usePostThread/traversal' 22 import { 23 type ApiThreadItem, ··· 322 } 323 } 324 }
··· 1 + import {useCallback} from 'react' 2 import { 3 type $Typed, 4 type AppBskyActorDefs, ··· 8 type AppBskyUnspeccedGetPostThreadV2, 9 AtUri, 10 } from '@atproto/api' 11 + import {type QueryClient, useQueryClient} from '@tanstack/react-query' 12 13 import { 14 dangerousGetPostShadow, ··· 19 import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed' 20 import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' 21 import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts' 22 + import {usePostThreadContext} from '#/state/queries/usePostThread' 23 import {getBranch} from '#/state/queries/usePostThread/traversal' 24 import { 25 type ApiThreadItem, ··· 324 } 325 } 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 import {useSafeAreaInsets} from 'react-native-safe-area-context' 45 import {type ImagePickerAsset} from 'expo-image-picker' 46 import { 47 - AppBskyFeedDefs, 48 - type AppBskyFeedGetPostThread, 49 AppBskyUnspeccedDefs, 50 AtUri, 51 type BskyAgent, 52 type RichText, ··· 549 if (initQuote) { 550 // We want to wait for the quote count to update before we call `onPost`, which will refetch data 551 whenAppViewReady(agent, initQuote.uri, res => { 552 - const quotedThread = res.data.thread 553 if ( 554 - AppBskyFeedDefs.isThreadViewPost(quotedThread) && 555 - quotedThread.post.quoteCount !== initQuote.quoteCount 556 ) { 557 onPost?.(postUri) 558 onPostSuccess?.(postSuccessData) ··· 1661 async function whenAppViewReady( 1662 agent: BskyAgent, 1663 uri: string, 1664 - fn: (res: AppBskyFeedGetPostThread.Response) => boolean, 1665 ) { 1666 await until( 1667 5, // 5 tries 1668 1e3, // 1s delay between tries 1669 fn, 1670 () => 1671 - agent.app.bsky.feed.getPostThread({ 1672 - uri, 1673 - depth: 0, 1674 }), 1675 ) 1676 }
··· 44 import {useSafeAreaInsets} from 'react-native-safe-area-context' 45 import {type ImagePickerAsset} from 'expo-image-picker' 46 import { 47 AppBskyUnspeccedDefs, 48 + type AppBskyUnspeccedGetPostThreadV2, 49 AtUri, 50 type BskyAgent, 51 type RichText, ··· 548 if (initQuote) { 549 // We want to wait for the quote count to update before we call `onPost`, which will refetch data 550 whenAppViewReady(agent, initQuote.uri, res => { 551 + const anchor = res.data.thread.at(0) 552 if ( 553 + AppBskyUnspeccedDefs.isThreadItemPost(anchor?.value) && 554 + anchor.value.post.quoteCount !== initQuote.quoteCount 555 ) { 556 onPost?.(postUri) 557 onPostSuccess?.(postSuccessData) ··· 1660 async function whenAppViewReady( 1661 agent: BskyAgent, 1662 uri: string, 1663 + fn: (res: AppBskyUnspeccedGetPostThreadV2.Response) => boolean, 1664 ) { 1665 await until( 1666 5, // 5 tries 1667 1e3, // 1s delay between tries 1668 fn, 1669 () => 1670 + agent.app.bsky.unspecced.getPostThreadV2({ 1671 + anchor: uri, 1672 + above: false, 1673 + below: 0, 1674 + branchingFactor: 0, 1675 }), 1676 ) 1677 }