Bluesky app fork with some witchin' additions 💫

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

+413 -274
+52 -1
bskyweb/cmd/bskyweb/server.go
··· 313 313 e.GET("/profile/:handleOrDID/known-followers", server.WebGeneric) 314 314 e.GET("/profile/:handleOrDID/search", server.WebGeneric) 315 315 e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric) 316 - e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric) 316 + e.GET("/profile/:handleOrDID/feed/:rkey", server.WebFeed) 317 317 e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric) 318 318 e.GET("/profile/:handleOrDID/labeler/liked-by", server.WebGeneric) 319 319 ··· 601 601 data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) 602 602 data["requestHost"] = req.Host 603 603 return c.Render(http.StatusOK, "profile.html", data) 604 + } 605 + 606 + func (srv *Server) WebFeed(c echo.Context) error { 607 + ctx := c.Request().Context() 608 + data := srv.NewTemplateContext() 609 + 610 + // sanity check arguments. don't 4xx, just let app handle if not expected format 611 + rkeyParam := c.Param("rkey") 612 + rkey, err := syntax.ParseRecordKey(rkeyParam) 613 + if err != nil { 614 + return c.Render(http.StatusOK, "feed.html", data) 615 + } 616 + handleOrDIDParam := c.Param("handleOrDID") 617 + handleOrDID, err := syntax.ParseAtIdentifier(handleOrDIDParam) 618 + if err != nil { 619 + return c.Render(http.StatusOK, "feed.html", data) 620 + } 621 + 622 + identifier := handleOrDID.Normalize().String() 623 + 624 + // requires two fetches: first fetch profile to get DID 625 + pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, identifier) 626 + if err != nil { 627 + log.Warnf("failed to fetch profile for: %s\t%v", identifier, err) 628 + return c.Render(http.StatusOK, "feed.html", data) 629 + } 630 + unauthedViewingOkay := true 631 + for _, label := range pv.Labels { 632 + if label.Src == pv.Did && label.Val == "!no-unauthenticated" { 633 + unauthedViewingOkay = false 634 + } 635 + } 636 + 637 + if !unauthedViewingOkay { 638 + return c.Render(http.StatusOK, "feed.html", data) 639 + } 640 + did := pv.Did 641 + data["did"] = did 642 + 643 + // then fetch the feed generator 644 + feedURI := fmt.Sprintf("at://%s/app.bsky.feed.generator/%s", did, rkey) 645 + fgv, err := appbsky.FeedGetFeedGenerator(ctx, srv.xrpcc, feedURI) 646 + if err != nil { 647 + log.Warnf("failed to fetch feed generator: %s\t%v", feedURI, err) 648 + return c.Render(http.StatusOK, "feed.html", data) 649 + } 650 + req := c.Request() 651 + data["feedView"] = fgv.View 652 + data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) 653 + 654 + return c.Render(http.StatusOK, "feed.html", data) 604 655 } 605 656 606 657 type IPCCRequest struct {
+54
bskyweb/templates/feed.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block head_title %} 4 + {%- if feedView -%} 5 + {{ feedView.DisplayName }} by @{{ feedView.Creator.Handle }} | Bluesky Feed 6 + {%- else -%} 7 + Bluesky 8 + {%- endif -%} 9 + {% endblock %} 10 + 11 + {% block html_head_extra -%} 12 + {%- if feedView -%} 13 + <meta property="og:site_name" content="Bluesky Social"> 14 + <meta property="og:type" content="website"> 15 + {%- if requestURI %} 16 + <meta property="og:url" content="{{ requestURI }}"> 17 + <link rel="canonical" href="{{ requestURI|canonicalize_url }}" /> 18 + {% endif -%} 19 + 20 + {%- if feedView.DisplayName %} 21 + <meta property="og:title" content="{{ feedView.DisplayName }} by @{{ feedView.Creator.Handle }}"> 22 + {% else %} 23 + <meta property="og:title" content="Feed by @{{ feedView.Creator.Handle }}"> 24 + {% endif -%} 25 + 26 + {%- if feedView.Description %} 27 + <meta name="description" content="{{ feedView.Description }}"> 28 + <meta property="og:description" content="{{ feedView.Description }}"> 29 + <meta property="twitter:description" content="{{ feedView.Description }}"> 30 + {% endif -%} 31 + 32 + {%- if feedView.Avatar %} 33 + <meta property="og:image" content="{{ feedView.Avatar }}"> 34 + <meta property="twitter:image" content="{{ feedView.Avatar }}"> 35 + <meta name="twitter:card" content="summary"> 36 + {% endif %} 37 + 38 + <meta name="twitter:label1" content="Created by"> 39 + <meta name="twitter:value1" content="@{{ feedView.Creator.Handle }}"> 40 + 41 + <link rel="alternate" href="{{ feedView.Uri }}" /> 42 + {% endif -%} 43 + {%- endblock %} 44 + 45 + {% block noscript_extra -%} 46 + {%- if feedView -%} 47 + <div id="bsky_feed_summary"> 48 + <h3>Feed</h3> 49 + <p id="bsky_feed_name">{{ feedView.DisplayName }}</p> 50 + <p id="bsky_feed_creator">{{ feedView.Creator.Handle }}</p> 51 + <p id="bsky_feed_description">{{ feedView.Description }}</p> 52 + </div> 53 + {% endif -%} 54 + {%- endblock %}
+7 -5
src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx
··· 3 3 import {useLingui} from '@lingui/react' 4 4 5 5 import {atoms as a, useTheme} from '#/alf' 6 - import {Button, ButtonText} from '#/components/Button' 6 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 7 + import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateIcon} from '#/components/icons/ArrowRotateCounterClockwise' 8 + import {MediaInsetBorder} from '#/components/MediaInsetBorder' 7 9 import {Text as TypoText} from '#/components/Typography' 8 10 9 11 export function Container({children}: {children: React.ReactNode}) { ··· 16 18 a.justify_center, 17 19 a.align_center, 18 20 a.px_lg, 19 - a.border, 20 - t.atoms.border_contrast_low, 21 - a.rounded_sm, 21 + a.rounded_md, 22 + a.overflow_hidden, 22 23 a.gap_lg, 23 24 ]}> 24 25 {children} 26 + <MediaInsetBorder /> 25 27 </View> 26 28 ) 27 29 } ··· 50 52 onPress={onPress} 51 53 size="small" 52 54 color="secondary_inverted" 53 - variant="solid" 54 55 label={_(msg`Retry`)}> 56 + <ButtonIcon icon={ArrowRotateIcon} /> 55 57 <ButtonText> 56 58 <Trans>Retry</Trans> 57 59 </ButtonText>
+7 -1
src/components/Post/Embed/VideoEmbed/index.web.tsx
··· 86 86 const contents = ( 87 87 <div 88 88 ref={ref} 89 - style={{display: 'flex', flex: 1, cursor: 'default'}} 89 + style={{ 90 + display: 'flex', 91 + flex: 1, 92 + cursor: 'default', 93 + backgroundImage: `url(${embed.thumbnail})`, 94 + backgroundSize: 'cover', 95 + }} 90 96 onClick={evt => evt.stopPropagation()}> 91 97 <ErrorBoundary renderError={renderError} key={key}> 92 98 <OnlyNearScreen>
+20 -24
src/components/StarterPack/ProfileStarterPacks.tsx
··· 1 - import React, { 2 - useCallback, 3 - useEffect, 4 - useImperativeHandle, 5 - useState, 6 - } from 'react' 1 + import {useCallback, useEffect, useImperativeHandle, useState} from 'react' 7 2 import { 8 3 findNodeHandle, 9 4 type ListRenderItemInfo, 10 5 type StyleProp, 6 + useWindowDimensions, 11 7 View, 12 8 type ViewStyle, 13 9 } from 'react-native' ··· 42 38 } 43 39 44 40 interface ProfileFeedgensProps { 41 + ref?: React.Ref<SectionRef> 45 42 scrollElRef: ListRef 46 43 did: string 47 44 headerOffset: number ··· 56 53 return item.uri 57 54 } 58 55 59 - export const ProfileStarterPacks = React.forwardRef< 60 - SectionRef, 61 - ProfileFeedgensProps 62 - >(function ProfileFeedgensImpl( 63 - { 64 - scrollElRef, 65 - did, 66 - headerOffset, 67 - enabled, 68 - style, 69 - testID, 70 - setScrollViewTag, 71 - isMe, 72 - }, 56 + export function ProfileStarterPacks({ 73 57 ref, 74 - ) { 58 + scrollElRef, 59 + did, 60 + headerOffset, 61 + enabled, 62 + style, 63 + testID, 64 + setScrollViewTag, 65 + isMe, 66 + }: ProfileFeedgensProps) { 75 67 const t = useTheme() 76 68 const bottomBarOffset = useBottomBarOffset(100) 69 + const {height} = useWindowDimensions() 77 70 const [isPTRing, setIsPTRing] = useState(false) 78 71 const { 79 72 data, ··· 101 94 setIsPTRing(false) 102 95 }, [refetch, setIsPTRing]) 103 96 104 - const onEndReached = React.useCallback(async () => { 97 + const onEndReached = useCallback(async () => { 105 98 if (isFetchingNextPage || !hasNextPage || isError) return 106 99 try { 107 100 await fetchNextPage() ··· 144 137 refreshing={isPTRing} 145 138 headerOffset={headerOffset} 146 139 progressViewOffset={ios(0)} 147 - contentContainerStyle={{paddingBottom: headerOffset + bottomBarOffset}} 140 + contentContainerStyle={{ 141 + minHeight: height + headerOffset, 142 + paddingBottom: bottomBarOffset, 143 + }} 148 144 removeClippedSubviews={true} 149 145 desktopFixedHeight 150 146 onEndReached={onEndReached} ··· 158 154 /> 159 155 </View> 160 156 ) 161 - }) 157 + } 162 158 163 159 function CreateAnother() { 164 160 const {_} = useLingui()
+36 -36
src/locale/locales/en/messages.po
··· 987 987 msgid "An error occurred while fetching the feed." 988 988 msgstr "" 989 989 990 - #: src/components/StarterPack/ProfileStarterPacks.tsx:343 990 + #: src/components/StarterPack/ProfileStarterPacks.tsx:339 991 991 msgid "An error occurred while generating your starter pack. Want to try again?" 992 992 msgstr "" 993 993 ··· 995 995 msgid "An error occurred while loading the video. Please try again later." 996 996 msgstr "" 997 997 998 - #: src/components/Post/Embed/VideoEmbed/index.web.tsx:226 998 + #: src/components/Post/Embed/VideoEmbed/index.web.tsx:232 999 999 msgid "An error occurred while loading the video. Please try again." 1000 1000 msgstr "" 1001 1001 ··· 1307 1307 msgstr "" 1308 1308 1309 1309 #: src/components/dialogs/StarterPackDialog.tsx:71 1310 - #: src/components/StarterPack/ProfileStarterPacks.tsx:235 1311 - #: src/components/StarterPack/ProfileStarterPacks.tsx:245 1310 + #: src/components/StarterPack/ProfileStarterPacks.tsx:231 1311 + #: src/components/StarterPack/ProfileStarterPacks.tsx:241 1312 1312 msgid "Before creating a starter pack, you must first verify your email." 1313 1313 msgstr "" 1314 1314 ··· 1422 1422 msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you. You will not see their content and they will be prevented from seeing yours." 1423 1423 msgstr "" 1424 1424 1425 - #: src/screens/Profile/Sections/Labels.tsx:203 1425 + #: src/screens/Profile/Sections/Labels.tsx:204 1426 1426 msgid "Blocking does not prevent this labeler from placing labels on your account." 1427 1427 msgstr "" 1428 1428 ··· 1469 1469 msgid "Bluesky Social Terms of Service" 1470 1470 msgstr "" 1471 1471 1472 - #: src/components/StarterPack/ProfileStarterPacks.tsx:310 1472 + #: src/components/StarterPack/ProfileStarterPacks.tsx:306 1473 1473 msgid "Bluesky will choose a set of recommended accounts from people in your network." 1474 1474 msgstr "" 1475 1475 ··· 1819 1819 msgid "Choose Feeds" 1820 1820 msgstr "" 1821 1821 1822 - #: src/components/StarterPack/ProfileStarterPacks.tsx:318 1822 + #: src/components/StarterPack/ProfileStarterPacks.tsx:314 1823 1823 msgid "Choose for me" 1824 1824 msgstr "" 1825 1825 ··· 2422 2422 #. Text on button to create a new starter pack 2423 2423 #: src/components/dialogs/StarterPackDialog.tsx:112 2424 2424 #: src/components/dialogs/StarterPackDialog.tsx:201 2425 - #: src/components/StarterPack/ProfileStarterPacks.tsx:300 2425 + #: src/components/StarterPack/ProfileStarterPacks.tsx:296 2426 2426 msgid "Create" 2427 2427 msgstr "" 2428 2428 ··· 2430 2430 msgid "Create a QR code for a starter pack" 2431 2431 msgstr "" 2432 2432 2433 - #: src/components/StarterPack/ProfileStarterPacks.tsx:178 2434 - #: src/components/StarterPack/ProfileStarterPacks.tsx:287 2433 + #: src/components/StarterPack/ProfileStarterPacks.tsx:174 2434 + #: src/components/StarterPack/ProfileStarterPacks.tsx:283 2435 2435 #: src/Navigation.tsx:589 2436 2436 msgid "Create a starter pack" 2437 2437 msgstr "" 2438 2438 2439 - #: src/components/StarterPack/ProfileStarterPacks.tsx:274 2439 + #: src/components/StarterPack/ProfileStarterPacks.tsx:270 2440 2440 msgid "Create a starter pack for me" 2441 2441 msgstr "" 2442 2442 ··· 2474 2474 msgid "Create an avatar instead" 2475 2475 msgstr "" 2476 2476 2477 - #: src/components/StarterPack/ProfileStarterPacks.tsx:185 2477 + #: src/components/StarterPack/ProfileStarterPacks.tsx:181 2478 2478 msgid "Create another" 2479 2479 msgstr "" 2480 2480 ··· 3173 3173 msgid "Enabled" 3174 3174 msgstr "" 3175 3175 3176 - #: src/screens/Profile/Sections/Feed.tsx:113 3176 + #: src/screens/Profile/Sections/Feed.tsx:109 3177 3177 msgid "End of feed" 3178 3178 msgstr "" 3179 3179 ··· 3985 3985 msgid "From <0/>" 3986 3986 msgstr "" 3987 3987 3988 - #: src/components/StarterPack/ProfileStarterPacks.tsx:307 3988 + #: src/components/StarterPack/ProfileStarterPacks.tsx:303 3989 3989 msgid "Generate a starter pack" 3990 3990 msgstr "" 3991 3991 ··· 4691 4691 msgid "Labels added" 4692 4692 msgstr "" 4693 4693 4694 - #: src/screens/Profile/Sections/Labels.tsx:194 4694 + #: src/screens/Profile/Sections/Labels.tsx:195 4695 4695 msgid "Labels are annotations on users and content. They can be used to hide, warn, and categorize the network." 4696 4696 msgstr "" 4697 4697 ··· 4825 4825 msgid "left to go." 4826 4826 msgstr "" 4827 4827 4828 - #: src/components/StarterPack/ProfileStarterPacks.tsx:323 4828 + #: src/components/StarterPack/ProfileStarterPacks.tsx:319 4829 4829 msgid "Let me choose" 4830 4830 msgstr "" 4831 4831 ··· 5036 5036 msgstr "" 5037 5037 5038 5038 #: src/screens/Profile/ProfileFeed/index.tsx:224 5039 - #: src/screens/Profile/Sections/Feed.tsx:98 5039 + #: src/screens/Profile/Sections/Feed.tsx:94 5040 5040 #: src/screens/ProfileList/FeedSection.tsx:105 5041 5041 #: src/view/com/feeds/FeedPage.tsx:169 5042 5042 msgid "Load new posts" ··· 5087 5087 msgid "Make adjustments to email settings for your account" 5088 5088 msgstr "" 5089 5089 5090 - #: src/components/StarterPack/ProfileStarterPacks.tsx:282 5090 + #: src/components/StarterPack/ProfileStarterPacks.tsx:278 5091 5091 msgid "Make one for me" 5092 5092 msgstr "" 5093 5093 ··· 5692 5692 msgid "No posts here" 5693 5693 msgstr "" 5694 5694 5695 - #: src/screens/Profile/Sections/Feed.tsx:66 5695 + #: src/screens/Profile/Sections/Feed.tsx:62 5696 5696 msgid "No posts yet." 5697 5697 msgstr "" 5698 5698 ··· 5956 5956 msgstr "" 5957 5957 5958 5958 #: src/components/Lists.tsx:173 5959 - #: src/components/StarterPack/ProfileStarterPacks.tsx:332 5960 - #: src/components/StarterPack/ProfileStarterPacks.tsx:341 5959 + #: src/components/StarterPack/ProfileStarterPacks.tsx:328 5960 + #: src/components/StarterPack/ProfileStarterPacks.tsx:337 5961 5961 #: src/screens/Settings/AppPasswords.tsx:59 5962 5962 #: src/screens/Settings/components/ChangeHandleDialog.tsx:106 5963 5963 #: src/view/screens/Profile.tsx:125 ··· 7381 7381 #: src/components/Error.tsx:65 7382 7382 #: src/components/Lists.tsx:110 7383 7383 #: src/components/moderation/ReportDialog/index.tsx:229 7384 - #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:54 7385 - #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:56 7386 - #: src/components/StarterPack/ProfileStarterPacks.tsx:346 7384 + #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:55 7385 + #: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoFallback.tsx:58 7386 + #: src/components/StarterPack/ProfileStarterPacks.tsx:342 7387 7387 #: src/screens/Login/LoginForm.tsx:323 7388 7388 #: src/screens/Login/LoginForm.tsx:330 7389 7389 #: src/screens/Messages/ChatList.tsx:291 ··· 8321 8321 8322 8322 #: src/components/ReportDialog/index.tsx:54 8323 8323 #: src/screens/Moderation/index.tsx:112 8324 - #: src/screens/Profile/Sections/Labels.tsx:184 8324 + #: src/screens/Profile/Sections/Labels.tsx:185 8325 8325 msgid "Something went wrong, please try again." 8326 8326 msgstr "" 8327 8327 ··· 8431 8431 msgid "Starter Packs" 8432 8432 msgstr "" 8433 8433 8434 - #: src/components/StarterPack/ProfileStarterPacks.tsx:266 8434 + #: src/components/StarterPack/ProfileStarterPacks.tsx:262 8435 8435 msgid "Starter packs let you easily share your favorite feeds and people with your friends." 8436 8436 msgstr "" 8437 8437 ··· 8480 8480 msgid "Subscribe" 8481 8481 msgstr "" 8482 8482 8483 - #: src/screens/Profile/Sections/Labels.tsx:231 8483 + #: src/screens/Profile/Sections/Labels.tsx:232 8484 8484 msgid "Subscribe to @{0} to use these labels:" 8485 8485 msgstr "" 8486 8486 ··· 8866 8866 msgid "There was an issue fetching your app passwords" 8867 8867 msgstr "" 8868 8868 8869 - #: src/view/com/feeds/ProfileFeedgens.tsx:151 8870 - #: src/view/com/lists/ProfileLists.tsx:150 8869 + #: src/view/com/feeds/ProfileFeedgens.tsx:163 8870 + #: src/view/com/lists/ProfileLists.tsx:161 8871 8871 msgid "There was an issue fetching your lists. Tap here to try again." 8872 8872 msgstr "" 8873 8873 ··· 9058 9058 msgid "This label was applied by you." 9059 9059 msgstr "" 9060 9060 9061 - #: src/screens/Profile/Sections/Labels.tsx:218 9061 + #: src/screens/Profile/Sections/Labels.tsx:219 9062 9062 msgid "This labeler hasn't declared what labels it publishes, and may not be active." 9063 9063 msgstr "" 9064 9064 ··· 9870 9870 msgid "Video is playing" 9871 9871 msgstr "" 9872 9872 9873 - #: src/components/Post/Embed/VideoEmbed/index.web.tsx:219 9873 + #: src/components/Post/Embed/VideoEmbed/index.web.tsx:225 9874 9874 msgid "Video not found." 9875 9875 msgstr "" 9876 9876 ··· 10548 10548 msgid "You have no conversations yet. Start one!" 10549 10549 msgstr "" 10550 10550 10551 - #: src/view/com/feeds/ProfileFeedgens.tsx:139 10551 + #: src/view/com/feeds/ProfileFeedgens.tsx:151 10552 10552 msgid "You have no feeds." 10553 10553 msgstr "" 10554 10554 10555 10555 #: src/view/com/lists/MyLists.tsx:81 10556 - #: src/view/com/lists/ProfileLists.tsx:135 10556 + #: src/view/com/lists/ProfileLists.tsx:149 10557 10557 msgid "You have no lists." 10558 10558 msgstr "" 10559 10559 ··· 10581 10581 msgid "You have temporarily reached the limit for video uploads. Please try again later." 10582 10582 msgstr "" 10583 10583 10584 - #: src/components/StarterPack/ProfileStarterPacks.tsx:263 10584 + #: src/components/StarterPack/ProfileStarterPacks.tsx:259 10585 10585 msgid "You haven't created a starter pack yet!" 10586 10586 msgstr "" 10587 10587 ··· 10622 10622 msgid "You must be at least 13 years old to use Bluesky. Read our <0>Terms of Service</0> for more information." 10623 10623 msgstr "" 10624 10624 10625 - #: src/components/StarterPack/ProfileStarterPacks.tsx:334 10625 + #: src/components/StarterPack/ProfileStarterPacks.tsx:330 10626 10626 msgid "You must be following at least seven other people to generate a starter pack." 10627 10627 msgstr "" 10628 10628 ··· 10810 10810 msgid "Your birth date" 10811 10811 msgstr "" 10812 10812 10813 - #: src/components/Post/Embed/VideoEmbed/index.web.tsx:223 10813 + #: src/components/Post/Embed/VideoEmbed/index.web.tsx:229 10814 10814 msgid "Your browser does not support the video format. Please try a different browser." 10815 10815 msgstr "" 10816 10816
+17 -21
src/screens/Profile/Sections/Feed.tsx
··· 1 - import React from 'react' 1 + import {useCallback, useEffect, useImperativeHandle, useState} from 'react' 2 2 import {findNodeHandle, View} from 'react-native' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' ··· 18 18 import {type SectionRef} from './types' 19 19 20 20 interface FeedSectionProps { 21 + ref?: React.Ref<SectionRef> 21 22 feed: FeedDescriptor 22 23 headerHeight: number 23 24 isFocused: boolean ··· 25 26 ignoreFilterFor?: string 26 27 setScrollViewTag: (tag: number | null) => void 27 28 } 28 - export const ProfileFeedSection = React.forwardRef< 29 - SectionRef, 30 - FeedSectionProps 31 - >(function FeedSectionImpl( 32 - { 33 - feed, 34 - headerHeight, 35 - isFocused, 36 - scrollElRef, 37 - ignoreFilterFor, 38 - setScrollViewTag, 39 - }, 29 + export function ProfileFeedSection({ 40 30 ref, 41 - ) { 31 + feed, 32 + headerHeight, 33 + isFocused, 34 + scrollElRef, 35 + ignoreFilterFor, 36 + setScrollViewTag, 37 + }: FeedSectionProps) { 42 38 const {_} = useLingui() 43 39 const queryClient = useQueryClient() 44 - const [hasNew, setHasNew] = React.useState(false) 45 - const [isScrolledDown, setIsScrolledDown] = React.useState(false) 40 + const [hasNew, setHasNew] = useState(false) 41 + const [isScrolledDown, setIsScrolledDown] = useState(false) 46 42 const shouldUseAdjustedNumToRender = feed.endsWith('posts_and_author_threads') 47 43 const isVideoFeed = isNative && feed.endsWith('posts_with_video') 48 44 const adjustedInitialNumToRender = useInitialNumToRender({ 49 45 screenHeightOffset: headerHeight, 50 46 }) 51 47 52 - const onScrollToTop = React.useCallback(() => { 48 + const onScrollToTop = useCallback(() => { 53 49 scrollElRef.current?.scrollToOffset({ 54 50 animated: isNative, 55 51 offset: -headerHeight, ··· 58 54 setHasNew(false) 59 55 }, [scrollElRef, headerHeight, queryClient, feed, setHasNew]) 60 56 61 - React.useImperativeHandle(ref, () => ({ 57 + useImperativeHandle(ref, () => ({ 62 58 scrollToTop: onScrollToTop, 63 59 })) 64 60 65 - const renderPostsEmpty = React.useCallback(() => { 61 + const renderPostsEmpty = useCallback(() => { 66 62 return <EmptyState icon="growth" message={_(msg`No posts yet.`)} /> 67 63 }, [_]) 68 64 69 - React.useEffect(() => { 65 + useEffect(() => { 70 66 if (isIOS && isFocused && scrollElRef.current) { 71 67 const nativeTag = findNodeHandle(scrollElRef.current) 72 68 setScrollViewTag(nativeTag) ··· 101 97 )} 102 98 </View> 103 99 ) 104 - }) 100 + } 105 101 106 102 function ProfileEndOfFeed() { 107 103 const t = useTheme()
+1
src/screens/Profile/Sections/Labels.tsx
··· 33 33 isFocused: boolean 34 34 setScrollViewTag: (tag: number | null) => void 35 35 } 36 + 36 37 export function ProfileLabelsSection({ 37 38 ref, 38 39 isLabelerLoading,
+18 -9
src/screens/Takendown.tsx
··· 9 9 import {useMutation} from '@tanstack/react-query' 10 10 import Graphemer from 'graphemer' 11 11 12 - import {MAX_REPORT_REASON_GRAPHEME_LENGTH} from '#/lib/constants' 12 + import { 13 + BLUESKY_MOD_SERVICE_HEADERS, 14 + MAX_REPORT_REASON_GRAPHEME_LENGTH, 15 + } from '#/lib/constants' 13 16 import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController' 14 17 import {cleanError} from '#/lib/strings/errors' 15 18 import {isIOS, isWeb} from '#/platform/detection' ··· 49 52 } = useMutation({ 50 53 mutationFn: async (appealText: string) => { 51 54 if (!currentAccount) throw new Error('No session') 52 - await agent.com.atproto.moderation.createReport({ 53 - reasonType: ComAtprotoModerationDefs.REASONAPPEAL, 54 - subject: { 55 - $type: 'com.atproto.admin.defs#repoRef', 56 - did: currentAccount.did, 57 - } satisfies ComAtprotoAdminDefs.RepoRef, 58 - reason: appealText, 59 - }) 55 + await agent.com.atproto.moderation.createReport( 56 + { 57 + reasonType: ComAtprotoModerationDefs.REASONAPPEAL, 58 + subject: { 59 + $type: 'com.atproto.admin.defs#repoRef', 60 + did: currentAccount.did, 61 + } satisfies ComAtprotoAdminDefs.RepoRef, 62 + reason: appealText, 63 + }, 64 + { 65 + encoding: 'application/json', 66 + headers: BLUESKY_MOD_SERVICE_HEADERS, 67 + }, 68 + ) 60 69 }, 61 70 onSuccess: () => setReason(''), 62 71 })
+32 -19
src/view/com/feeds/ProfileFeedgens.tsx
··· 1 - import React from 'react' 1 + import { 2 + useCallback, 3 + useEffect, 4 + useImperativeHandle, 5 + useMemo, 6 + useState, 7 + } from 'react' 2 8 import { 3 9 findNodeHandle, 4 10 type ListRenderItemInfo, 5 11 type StyleProp, 12 + useWindowDimensions, 6 13 View, 7 14 type ViewStyle, 8 15 } from 'react-native' ··· 34 41 } 35 42 36 43 interface ProfileFeedgensProps { 44 + ref?: React.Ref<SectionRef> 37 45 did: string 38 46 scrollElRef: ListRef 39 47 headerOffset: number ··· 43 51 setScrollViewTag: (tag: number | null) => void 44 52 } 45 53 46 - export const ProfileFeedgens = React.forwardRef< 47 - SectionRef, 48 - ProfileFeedgensProps 49 - >(function ProfileFeedgensImpl( 50 - {did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag}, 54 + export function ProfileFeedgens({ 51 55 ref, 52 - ) { 56 + did, 57 + scrollElRef, 58 + headerOffset, 59 + enabled, 60 + style, 61 + testID, 62 + setScrollViewTag, 63 + }: ProfileFeedgensProps) { 53 64 const {_} = useLingui() 54 65 const t = useTheme() 55 - const [isPTRing, setIsPTRing] = React.useState(false) 56 - const opts = React.useMemo(() => ({enabled}), [enabled]) 66 + const [isPTRing, setIsPTRing] = useState(false) 67 + const {height} = useWindowDimensions() 68 + const opts = useMemo(() => ({enabled}), [enabled]) 57 69 const { 58 70 data, 59 71 isPending, ··· 67 79 const isEmpty = !isPending && !data?.pages[0]?.feeds.length 68 80 const {data: preferences} = usePreferencesQuery() 69 81 70 - const items = React.useMemo(() => { 82 + const items = useMemo(() => { 71 83 let items: any[] = [] 72 84 if (isError && isEmpty) { 73 85 items = items.concat([ERROR_ITEM]) ··· 91 103 92 104 const queryClient = useQueryClient() 93 105 94 - const onScrollToTop = React.useCallback(() => { 106 + const onScrollToTop = useCallback(() => { 95 107 scrollElRef.current?.scrollToOffset({ 96 108 animated: isNative, 97 109 offset: -headerOffset, ··· 99 111 queryClient.invalidateQueries({queryKey: RQKEY(did)}) 100 112 }, [scrollElRef, queryClient, headerOffset, did]) 101 113 102 - React.useImperativeHandle(ref, () => ({ 114 + useImperativeHandle(ref, () => ({ 103 115 scrollToTop: onScrollToTop, 104 116 })) 105 117 106 - const onRefresh = React.useCallback(async () => { 118 + const onRefresh = useCallback(async () => { 107 119 setIsPTRing(true) 108 120 try { 109 121 await refetch() ··· 113 125 setIsPTRing(false) 114 126 }, [refetch, setIsPTRing]) 115 127 116 - const onEndReached = React.useCallback(async () => { 128 + const onEndReached = useCallback(async () => { 117 129 if (isFetchingNextPage || !hasNextPage || isError) return 118 130 119 131 try { ··· 123 135 } 124 136 }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) 125 137 126 - const onPressRetryLoadMore = React.useCallback(() => { 138 + const onPressRetryLoadMore = useCallback(() => { 127 139 fetchNextPage() 128 140 }, [fetchNextPage]) 129 141 130 142 // rendering 131 143 // = 132 144 133 - const renderItem = React.useCallback( 145 + const renderItem = useCallback( 134 146 ({item, index}: ListRenderItemInfo<any>) => { 135 147 if (item === EMPTY) { 136 148 return ( ··· 174 186 [_, t, error, refetch, onPressRetryLoadMore, preferences], 175 187 ) 176 188 177 - React.useEffect(() => { 189 + useEffect(() => { 178 190 if (isIOS && enabled && scrollElRef.current) { 179 191 const nativeTag = findNodeHandle(scrollElRef.current) 180 192 setScrollViewTag(nativeTag) 181 193 } 182 194 }, [enabled, scrollElRef, setScrollViewTag]) 183 195 184 - const ProfileFeedgensFooter = React.useCallback(() => { 196 + const ProfileFeedgensFooter = useCallback(() => { 185 197 if (isEmpty) return null 186 198 return ( 187 199 <ListFooter ··· 217 229 removeClippedSubviews={true} 218 230 desktopFixedHeight 219 231 onEndReached={onEndReached} 232 + contentContainerStyle={{minHeight: height + headerOffset}} 220 233 /> 221 234 </View> 222 235 ) 223 - }) 236 + } 224 237 225 238 function keyExtractor(item: any) { 226 239 return item._reactKey || item.uri
+169 -158
src/view/com/lists/ProfileLists.tsx
··· 1 - import React from 'react' 1 + import { 2 + useCallback, 3 + useEffect, 4 + useImperativeHandle, 5 + useMemo, 6 + useState, 7 + } from 'react' 2 8 import { 3 9 findNodeHandle, 4 10 type ListRenderItemInfo, 5 11 type StyleProp, 12 + useWindowDimensions, 6 13 View, 7 14 type ViewStyle, 8 15 } from 'react-native' ··· 33 40 } 34 41 35 42 interface ProfileListsProps { 43 + ref?: React.Ref<SectionRef> 36 44 did: string 37 45 scrollElRef: ListRef 38 46 headerOffset: number ··· 42 50 setScrollViewTag: (tag: number | null) => void 43 51 } 44 52 45 - export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>( 46 - function ProfileListsImpl( 47 - {did, scrollElRef, headerOffset, enabled, style, testID, setScrollViewTag}, 48 - ref, 49 - ) { 50 - const t = useTheme() 51 - const {_} = useLingui() 52 - const [isPTRing, setIsPTRing] = React.useState(false) 53 - const opts = React.useMemo(() => ({enabled}), [enabled]) 54 - const { 55 - data, 56 - isPending, 57 - hasNextPage, 58 - fetchNextPage, 59 - isFetchingNextPage, 60 - isError, 61 - error, 62 - refetch, 63 - } = useProfileListsQuery(did, opts) 64 - const isEmpty = !isPending && !data?.pages[0]?.lists.length 53 + export function ProfileLists({ 54 + ref, 55 + did, 56 + scrollElRef, 57 + headerOffset, 58 + enabled, 59 + style, 60 + testID, 61 + setScrollViewTag, 62 + }: ProfileListsProps) { 63 + const t = useTheme() 64 + const {_} = useLingui() 65 + const {height} = useWindowDimensions() 66 + const [isPTRing, setIsPTRing] = useState(false) 67 + const opts = useMemo(() => ({enabled}), [enabled]) 68 + const { 69 + data, 70 + isPending, 71 + hasNextPage, 72 + fetchNextPage, 73 + isFetchingNextPage, 74 + isError, 75 + error, 76 + refetch, 77 + } = useProfileListsQuery(did, opts) 78 + const isEmpty = !isPending && !data?.pages[0]?.lists.length 65 79 66 - const items = React.useMemo(() => { 67 - let items: any[] = [] 68 - if (isError && isEmpty) { 69 - items = items.concat([ERROR_ITEM]) 70 - } 71 - if (isPending) { 72 - items = items.concat([LOADING]) 73 - } else if (isEmpty) { 74 - items = items.concat([EMPTY]) 75 - } else if (data?.pages) { 76 - for (const page of data?.pages) { 77 - items = items.concat(page.lists) 78 - } 79 - } 80 - if (isError && !isEmpty) { 81 - items = items.concat([LOAD_MORE_ERROR_ITEM]) 80 + const items = useMemo(() => { 81 + let items: any[] = [] 82 + if (isError && isEmpty) { 83 + items = items.concat([ERROR_ITEM]) 84 + } 85 + if (isPending) { 86 + items = items.concat([LOADING]) 87 + } else if (isEmpty) { 88 + items = items.concat([EMPTY]) 89 + } else if (data?.pages) { 90 + for (const page of data?.pages) { 91 + items = items.concat(page.lists) 82 92 } 83 - return items 84 - }, [isError, isEmpty, isPending, data]) 93 + } 94 + if (isError && !isEmpty) { 95 + items = items.concat([LOAD_MORE_ERROR_ITEM]) 96 + } 97 + return items 98 + }, [isError, isEmpty, isPending, data]) 85 99 86 - // events 87 - // = 100 + // events 101 + // = 88 102 89 - const queryClient = useQueryClient() 103 + const queryClient = useQueryClient() 90 104 91 - const onScrollToTop = React.useCallback(() => { 92 - scrollElRef.current?.scrollToOffset({ 93 - animated: isNative, 94 - offset: -headerOffset, 95 - }) 96 - queryClient.invalidateQueries({queryKey: RQKEY(did)}) 97 - }, [scrollElRef, queryClient, headerOffset, did]) 105 + const onScrollToTop = useCallback(() => { 106 + scrollElRef.current?.scrollToOffset({ 107 + animated: isNative, 108 + offset: -headerOffset, 109 + }) 110 + queryClient.invalidateQueries({queryKey: RQKEY(did)}) 111 + }, [scrollElRef, queryClient, headerOffset, did]) 98 112 99 - React.useImperativeHandle(ref, () => ({ 100 - scrollToTop: onScrollToTop, 101 - })) 113 + useImperativeHandle(ref, () => ({ 114 + scrollToTop: onScrollToTop, 115 + })) 102 116 103 - const onRefresh = React.useCallback(async () => { 104 - setIsPTRing(true) 105 - try { 106 - await refetch() 107 - } catch (err) { 108 - logger.error('Failed to refresh lists', {message: err}) 109 - } 110 - setIsPTRing(false) 111 - }, [refetch, setIsPTRing]) 117 + const onRefresh = useCallback(async () => { 118 + setIsPTRing(true) 119 + try { 120 + await refetch() 121 + } catch (err) { 122 + logger.error('Failed to refresh lists', {message: err}) 123 + } 124 + setIsPTRing(false) 125 + }, [refetch, setIsPTRing]) 112 126 113 - const onEndReached = React.useCallback(async () => { 114 - if (isFetchingNextPage || !hasNextPage || isError) return 115 - try { 116 - await fetchNextPage() 117 - } catch (err) { 118 - logger.error('Failed to load more lists', {message: err}) 119 - } 120 - }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) 127 + const onEndReached = useCallback(async () => { 128 + if (isFetchingNextPage || !hasNextPage || isError) return 129 + try { 130 + await fetchNextPage() 131 + } catch (err) { 132 + logger.error('Failed to load more lists', {message: err}) 133 + } 134 + }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) 121 135 122 - const onPressRetryLoadMore = React.useCallback(() => { 123 - fetchNextPage() 124 - }, [fetchNextPage]) 136 + const onPressRetryLoadMore = useCallback(() => { 137 + fetchNextPage() 138 + }, [fetchNextPage]) 125 139 126 - // rendering 127 - // = 140 + // rendering 141 + // = 128 142 129 - const renderItemInner = React.useCallback( 130 - ({item, index}: ListRenderItemInfo<any>) => { 131 - if (item === EMPTY) { 132 - return ( 133 - <EmptyState 134 - icon="list-ul" 135 - message={_(msg`You have no lists.`)} 136 - testID="listsEmpty" 137 - /> 138 - ) 139 - } else if (item === ERROR_ITEM) { 140 - return ( 141 - <ErrorMessage 142 - message={cleanError(error)} 143 - onPressTryAgain={refetch} 144 - /> 145 - ) 146 - } else if (item === LOAD_MORE_ERROR_ITEM) { 147 - return ( 148 - <LoadMoreRetryBtn 149 - label={_( 150 - msg`There was an issue fetching your lists. Tap here to try again.`, 151 - )} 152 - onPress={onPressRetryLoadMore} 153 - /> 154 - ) 155 - } else if (item === LOADING) { 156 - return <FeedLoadingPlaceholder /> 157 - } 143 + const renderItemInner = useCallback( 144 + ({item, index}: ListRenderItemInfo<any>) => { 145 + if (item === EMPTY) { 158 146 return ( 159 - <View 160 - style={[ 161 - (index !== 0 || isWeb) && a.border_t, 162 - t.atoms.border_contrast_low, 163 - a.px_lg, 164 - a.py_lg, 165 - ]}> 166 - <ListCard.Default view={item} /> 167 - </View> 147 + <EmptyState 148 + icon="list-ul" 149 + message={_(msg`You have no lists.`)} 150 + testID="listsEmpty" 151 + /> 168 152 ) 169 - }, 170 - [error, refetch, onPressRetryLoadMore, _, t.atoms.border_contrast_low], 171 - ) 172 - 173 - React.useEffect(() => { 174 - if (isIOS && enabled && scrollElRef.current) { 175 - const nativeTag = findNodeHandle(scrollElRef.current) 176 - setScrollViewTag(nativeTag) 153 + } else if (item === ERROR_ITEM) { 154 + return ( 155 + <ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} /> 156 + ) 157 + } else if (item === LOAD_MORE_ERROR_ITEM) { 158 + return ( 159 + <LoadMoreRetryBtn 160 + label={_( 161 + msg`There was an issue fetching your lists. Tap here to try again.`, 162 + )} 163 + onPress={onPressRetryLoadMore} 164 + /> 165 + ) 166 + } else if (item === LOADING) { 167 + return <FeedLoadingPlaceholder /> 177 168 } 178 - }, [enabled, scrollElRef, setScrollViewTag]) 179 - 180 - const ProfileListsFooter = React.useCallback(() => { 181 - if (isEmpty) return null 182 169 return ( 183 - <ListFooter 184 - hasNextPage={hasNextPage} 185 - isFetchingNextPage={isFetchingNextPage} 186 - onRetry={fetchNextPage} 187 - error={cleanError(error)} 188 - height={180 + headerOffset} 189 - /> 170 + <View 171 + style={[ 172 + (index !== 0 || isWeb) && a.border_t, 173 + t.atoms.border_contrast_low, 174 + a.px_lg, 175 + a.py_lg, 176 + ]}> 177 + <ListCard.Default view={item} /> 178 + </View> 190 179 ) 191 - }, [ 192 - hasNextPage, 193 - error, 194 - isFetchingNextPage, 195 - headerOffset, 196 - fetchNextPage, 197 - isEmpty, 198 - ]) 180 + }, 181 + [error, refetch, onPressRetryLoadMore, _, t.atoms.border_contrast_low], 182 + ) 183 + 184 + useEffect(() => { 185 + if (isIOS && enabled && scrollElRef.current) { 186 + const nativeTag = findNodeHandle(scrollElRef.current) 187 + setScrollViewTag(nativeTag) 188 + } 189 + }, [enabled, scrollElRef, setScrollViewTag]) 199 190 191 + const ProfileListsFooter = useCallback(() => { 192 + if (isEmpty) return null 200 193 return ( 201 - <View testID={testID} style={style}> 202 - <List 203 - testID={testID ? `${testID}-flatlist` : undefined} 204 - ref={scrollElRef} 205 - data={items} 206 - keyExtractor={keyExtractor} 207 - renderItem={renderItemInner} 208 - ListFooterComponent={ProfileListsFooter} 209 - refreshing={isPTRing} 210 - onRefresh={onRefresh} 211 - headerOffset={headerOffset} 212 - progressViewOffset={ios(0)} 213 - removeClippedSubviews={true} 214 - desktopFixedHeight 215 - onEndReached={onEndReached} 216 - /> 217 - </View> 194 + <ListFooter 195 + hasNextPage={hasNextPage} 196 + isFetchingNextPage={isFetchingNextPage} 197 + onRetry={fetchNextPage} 198 + error={cleanError(error)} 199 + height={180 + headerOffset} 200 + /> 218 201 ) 219 - }, 220 - ) 202 + }, [ 203 + hasNextPage, 204 + error, 205 + isFetchingNextPage, 206 + headerOffset, 207 + fetchNextPage, 208 + isEmpty, 209 + ]) 210 + 211 + return ( 212 + <View testID={testID} style={style}> 213 + <List 214 + testID={testID ? `${testID}-flatlist` : undefined} 215 + ref={scrollElRef} 216 + data={items} 217 + keyExtractor={keyExtractor} 218 + renderItem={renderItemInner} 219 + ListFooterComponent={ProfileListsFooter} 220 + refreshing={isPTRing} 221 + onRefresh={onRefresh} 222 + headerOffset={headerOffset} 223 + progressViewOffset={ios(0)} 224 + removeClippedSubviews={true} 225 + desktopFixedHeight 226 + onEndReached={onEndReached} 227 + contentContainerStyle={{minHeight: height + headerOffset}} 228 + /> 229 + </View> 230 + ) 231 + } 221 232 222 233 function keyExtractor(item: any) { 223 234 return item._reactKey || item.uri