about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Dockerfile2
-rw-r--r--app.config.js3
-rw-r--r--bskyweb/cmd/bskyweb/mailmodo.go70
-rw-r--r--bskyweb/cmd/bskyweb/main.go12
-rw-r--r--bskyweb/cmd/bskyweb/server.go55
-rw-r--r--package.json6
-rw-r--r--plugins/withAndroidManifestFCMIconPlugin.js37
-rw-r--r--src/components/Lists.tsx13
-rw-r--r--src/components/forms/DateField/index.android.tsx39
-rw-r--r--src/components/forms/DateField/index.tsx20
-rw-r--r--src/lib/hooks/useInitialNumToRender.ts11
-rw-r--r--src/lib/strings/url-helpers.ts12
-rw-r--r--src/screens/Hashtag.tsx11
-rw-r--r--src/state/queries/preferences/const.ts2
-rw-r--r--src/state/queries/preferences/index.ts2
-rw-r--r--src/view/com/posts/Feed.tsx4
-rw-r--r--src/view/com/util/Views.d.ts4
-rw-r--r--src/view/com/util/Views.web.tsx11
-rw-r--r--src/view/com/util/forms/DateInput.tsx32
-rw-r--r--src/view/shell/desktop/LeftNav.tsx2
-rw-r--r--yarn.lock20
21 files changed, 156 insertions, 212 deletions
diff --git a/Dockerfile b/Dockerfile
index fcd2413cd..3ad05b6ec 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -31,7 +31,7 @@ RUN \. "$NVM_DIR/nvm.sh" && \
   nvm use $NODE_VERSION && \
   npm install --global yarn && \
   yarn && \
-  yarn intl:compile && \
+  yarn intl:build && \
   yarn build-web
 
 # DEBUG
diff --git a/app.config.js b/app.config.js
index 530e07b9b..2552d3489 100644
--- a/app.config.js
+++ b/app.config.js
@@ -153,10 +153,11 @@ module.exports = function (config) {
           'expo-notifications',
           {
             icon: './assets/icon-android-notification.png',
-            color: '#ffffff',
+            color: '#1185fe',
           },
         ],
         './plugins/withAndroidManifestPlugin.js',
+        './plugins/withAndroidManifestFCMIconPlugin.js',
         './plugins/withAndroidStylesWindowBackgroundPlugin.js',
         './plugins/shareExtension/withShareExtensions.js',
       ].filter(Boolean),
diff --git a/bskyweb/cmd/bskyweb/mailmodo.go b/bskyweb/cmd/bskyweb/mailmodo.go
deleted file mode 100644
index e892971f9..000000000
--- a/bskyweb/cmd/bskyweb/mailmodo.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package main
-
-import (
-	"bytes"
-	"context"
-	"crypto/sha256"
-	"encoding/json"
-	"fmt"
-	"net/http"
-	"time"
-)
-
-type Mailmodo struct {
-	httpClient *http.Client
-	APIKey     string
-	BaseURL    string
-	ListName   string
-}
-
-func NewMailmodo(apiKey, listName string) *Mailmodo {
-	return &Mailmodo{
-		APIKey:     apiKey,
-		BaseURL:    "https://api.mailmodo.com/api/v1",
-		httpClient: &http.Client{},
-		ListName:   listName,
-	}
-}
-
-func (m *Mailmodo) request(ctx context.Context, httpMethod string, apiMethod string, data any) error {
-	endpoint := fmt.Sprintf("%s/%s", m.BaseURL, apiMethod)
-	js, err := json.Marshal(data)
-	if err != nil {
-		return fmt.Errorf("Mailmodo JSON encoding failed: %w", err)
-	}
-	req, err := http.NewRequestWithContext(ctx, httpMethod, endpoint, bytes.NewBuffer(js))
-	if err != nil {
-		return fmt.Errorf("Mailmodo HTTP creating request %s %s failed: %w", httpMethod, apiMethod, err)
-	}
-	req.Header.Set("mmApiKey", m.APIKey)
-	req.Header.Set("Content-Type", "application/json")
-
-	res, err := m.httpClient.Do(req)
-	if err != nil {
-		return fmt.Errorf("Mailmodo HTTP making request %s %s failed: %w", httpMethod, apiMethod, err)
-	}
-	defer res.Body.Close()
-
-	status := struct {
-		Success bool   `json:"success"`
-		Message string `json:"message"`
-	}{}
-	if err := json.NewDecoder(res.Body).Decode(&status); err != nil {
-		return fmt.Errorf("Mailmodo HTTP parsing response %s %s failed: %w", httpMethod, apiMethod, err)
-	}
-	if !status.Success {
-		return fmt.Errorf("Mailmodo API response %s %s failed: %s", httpMethod, apiMethod, status.Message)
-	}
-	return nil
-}
-
-func (m *Mailmodo) AddToList(ctx context.Context, email string) error {
-	return m.request(ctx, "POST", "addToList", map[string]any{
-		"listName": m.ListName,
-		"email":    email,
-		"data": map[string]any{
-			"email_hashed": fmt.Sprintf("%x", sha256.Sum256([]byte(email))),
-		},
-		"created_at": time.Now().UTC().Format(time.RFC3339),
-	})
-}
diff --git a/bskyweb/cmd/bskyweb/main.go b/bskyweb/cmd/bskyweb/main.go
index a2952cae2..5185ff573 100644
--- a/bskyweb/cmd/bskyweb/main.go
+++ b/bskyweb/cmd/bskyweb/main.go
@@ -41,18 +41,6 @@ func run(args []string) {
 					EnvVars: []string{"ATP_APPVIEW_HOST", "ATP_PDS_HOST"},
 				},
 				&cli.StringFlag{
-					Name:     "mailmodo-api-key",
-					Usage:    "Mailmodo API key",
-					Required: false,
-					EnvVars:  []string{"MAILMODO_API_KEY"},
-				},
-				&cli.StringFlag{
-					Name:     "mailmodo-list-name",
-					Usage:    "Mailmodo contact list to add email addresses to",
-					Required: false,
-					EnvVars:  []string{"MAILMODO_LIST_NAME"},
-				},
-				&cli.StringFlag{
 					Name:     "http-address",
 					Usage:    "Specify the local IP/port to bind to",
 					Required: false,
diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go
index 6b76acc94..e159d780a 100644
--- a/bskyweb/cmd/bskyweb/server.go
+++ b/bskyweb/cmd/bskyweb/server.go
@@ -2,11 +2,9 @@ package main
 
 import (
 	"context"
-	"encoding/json"
 	"errors"
 	"fmt"
 	"io/fs"
-	"io/ioutil"
 	"net/http"
 	"os"
 	"os/signal"
@@ -29,25 +27,19 @@ import (
 )
 
 type Server struct {
-	echo     *echo.Echo
-	httpd    *http.Server
-	mailmodo *Mailmodo
-	xrpcc    *xrpc.Client
+	echo  *echo.Echo
+	httpd *http.Server
+	xrpcc *xrpc.Client
 }
 
 func serve(cctx *cli.Context) error {
 	debug := cctx.Bool("debug")
 	httpAddress := cctx.String("http-address")
 	appviewHost := cctx.String("appview-host")
-	mailmodoAPIKey := cctx.String("mailmodo-api-key")
-	mailmodoListName := cctx.String("mailmodo-list-name")
 
 	// Echo
 	e := echo.New()
 
-	// Mailmodo client.
-	mailmodo := NewMailmodo(mailmodoAPIKey, mailmodoListName)
-
 	// create a new session (no auth)
 	xrpcc := &xrpc.Client{
 		Client: cliutil.NewHttpClient(),
@@ -77,9 +69,8 @@ func serve(cctx *cli.Context) error {
 	// server
 	//
 	server := &Server{
-		echo:     e,
-		mailmodo: mailmodo,
-		xrpcc:    xrpcc,
+		echo:  e,
+		xrpcc: xrpcc,
 	}
 
 	// Create the HTTP server.
@@ -221,9 +212,6 @@ func serve(cctx *cli.Context) error {
 	e.GET("/profile/:handleOrDID/post/:rkey/liked-by", server.WebGeneric)
 	e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric)
 
-	// Mailmodo
-	e.POST("/api/waitlist", server.apiWaitlist)
-
 	// Start the server.
 	log.Infof("starting server address=%s", httpAddress)
 	go func() {
@@ -398,36 +386,3 @@ func (srv *Server) WebProfile(c echo.Context) error {
 	data["requestHost"] = req.Host
 	return c.Render(http.StatusOK, "profile.html", data)
 }
-
-func (srv *Server) apiWaitlist(c echo.Context) error {
-	type jsonError struct {
-		Error string `json:"error"`
-	}
-
-	// Read the API request.
-	type apiRequest struct {
-		Email string `json:"email"`
-	}
-
-	bodyReader := http.MaxBytesReader(c.Response(), c.Request().Body, 16*1024)
-	payload, err := ioutil.ReadAll(bodyReader)
-	if err != nil {
-		return err
-	}
-	var req apiRequest
-	if err := json.Unmarshal(payload, &req); err != nil {
-		return c.JSON(http.StatusBadRequest, jsonError{Error: "Invalid API request"})
-	}
-
-	if req.Email == "" {
-		return c.JSON(http.StatusBadRequest, jsonError{Error: "Please enter a valid email address."})
-	}
-
-	if err := srv.mailmodo.AddToList(c.Request().Context(), req.Email); err != nil {
-		log.Errorf("adding email to waitlist failed: %s", err)
-		return c.JSON(http.StatusBadRequest, jsonError{
-			Error: "Storing email in waitlist failed. Please enter a valid email address.",
-		})
-	}
-	return c.JSON(http.StatusOK, map[string]bool{"success": true})
-}
diff --git a/package.json b/package.json
index 59ee33193..2571fe772 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "bsky.app",
-  "version": "1.71.0",
+  "version": "1.72.0",
   "private": true,
   "engines": {
     "node": ">=18"
@@ -44,7 +44,7 @@
     "update-extensions": "scripts/updateExtensions.sh"
   },
   "dependencies": {
-    "@atproto/api": "^0.10.4",
+    "@atproto/api": "^0.10.5",
     "@bam.tech/react-native-image-resizer": "^3.0.4",
     "@braintree/sanitize-url": "^6.0.2",
     "@emoji-mart/react": "^1.1.1",
@@ -63,7 +63,6 @@
     "@react-native-camera-roll/camera-roll": "^5.2.2",
     "@react-native-clipboard/clipboard": "^1.10.0",
     "@react-native-community/blur": "^4.3.0",
-    "@react-native-community/datetimepicker": "7.6.1",
     "@react-native-masked-view/masked-view": "0.3.0",
     "@react-native-menu/menu": "^0.8.0",
     "@react-native-picker/picker": "2.6.1",
@@ -152,6 +151,7 @@
     "react-keyed-flatten-children": "^3.0.0",
     "react-native": "0.73.2",
     "react-native-appstate-hook": "^1.0.6",
+    "react-native-date-picker": "^4.4.0",
     "react-native-drawer-layout": "^4.0.0-alpha.3",
     "react-native-fs": "^2.20.0",
     "react-native-gesture-handler": "~2.14.0",
diff --git a/plugins/withAndroidManifestFCMIconPlugin.js b/plugins/withAndroidManifestFCMIconPlugin.js
new file mode 100644
index 000000000..066a975d8
--- /dev/null
+++ b/plugins/withAndroidManifestFCMIconPlugin.js
@@ -0,0 +1,37 @@
+const {withAndroidManifest} = require('expo/config-plugins')
+
+module.exports = function withAndroidManifestFCMIconPlugin(appConfig) {
+  return withAndroidManifest(appConfig, function (decoratedAppConfig) {
+    try {
+      function addOrModifyMetaData(metaData, name, resource) {
+        const elem = metaData.find(elem => elem.$['android:name'] === name)
+        if (elem === undefined) {
+          metaData.push({
+            $: {
+              'android:name': name,
+              'android:resource': resource,
+            },
+          })
+        } else {
+          elem.$['android:resource'] = resource
+        }
+      }
+      const androidManifest = decoratedAppConfig.modResults.manifest
+      const metaData = androidManifest.application[0]['meta-data']
+      addOrModifyMetaData(
+        metaData,
+        'com.google.firebase.messaging.default_notification_color',
+        '@color/notification_icon_color',
+      )
+      addOrModifyMetaData(
+        metaData,
+        'com.google.firebase.messaging.default_notification_icon',
+        '@drawable/notification_icon',
+      )
+      return decoratedAppConfig
+    } catch (e) {
+      console.error(`withAndroidManifestFCMIconPlugin failed`, e)
+    }
+    return decoratedAppConfig
+  })
+}
diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx
index 9778545d2..c879f9411 100644
--- a/src/components/Lists.tsx
+++ b/src/components/Lists.tsx
@@ -1,6 +1,7 @@
 import React from 'react'
 import {atoms as a, useBreakpoints, useTheme} from '#/alf'
 import {View} from 'react-native'
+import {CenteredView} from 'view/com/util/Views'
 import {Loader} from '#/components/Loader'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
@@ -145,7 +146,7 @@ export function ListMaybePlaceholder({
 }) {
   const navigation = useNavigation<NavigationProp>()
   const t = useTheme()
-  const {gtMobile} = useBreakpoints()
+  const {gtMobile, gtTablet} = useBreakpoints()
   const {_} = useLingui()
 
   const canGoBack = navigation.canGoBack()
@@ -168,14 +169,16 @@ export function ListMaybePlaceholder({
   if (!isEmpty) return null
 
   return (
-    <View
+    <CenteredView
       style={[
         a.flex_1,
         a.align_center,
-        !gtMobile ? [a.justify_between, a.border_t] : a.gap_5xl,
+        !gtMobile ? a.justify_between : a.gap_5xl,
         t.atoms.border_contrast_low,
         {paddingTop: 175, paddingBottom: 110},
-      ]}>
+      ]}
+      sideBorders={gtMobile}
+      topBorder={!gtTablet}>
       {isLoading ? (
         <View style={[a.w_full, a.align_center, {top: 100}]}>
           <Loader size="xl" />
@@ -244,6 +247,6 @@ export function ListMaybePlaceholder({
           </View>
         </>
       )}
-    </View>
+    </CenteredView>
   )
 }
diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx
index cddb643d6..451810a5e 100644
--- a/src/components/forms/DateField/index.android.tsx
+++ b/src/components/forms/DateField/index.android.tsx
@@ -1,8 +1,5 @@
 import React from 'react'
 import {View, Pressable} from 'react-native'
-import DateTimePicker, {
-  BaseProps as DateTimePickerProps,
-} from '@react-native-community/datetimepicker'
 
 import {useTheme, atoms} from '#/alf'
 import {Text} from '#/components/Typography'
@@ -15,6 +12,8 @@ import {
   localizeDate,
   toSimpleDateString,
 } from '#/components/forms/DateField/utils'
+import DatePicker from 'react-native-date-picker'
+import {isAndroid} from 'platform/detection'
 
 export * as utils from '#/components/forms/DateField/utils'
 export const Label = TextField.Label
@@ -38,20 +37,20 @@ export function DateField({
   const {chromeFocus, chromeError, chromeErrorHover} =
     TextField.useSharedInputStyles()
 
-  const onChangeInternal = React.useCallback<
-    Required<DateTimePickerProps>['onChange']
-  >(
-    (_event, date) => {
+  const onChangeInternal = React.useCallback(
+    (date: Date) => {
       setOpen(false)
 
-      if (date) {
-        const formatted = toSimpleDateString(date)
-        onChangeDate(formatted)
-      }
+      const formatted = toSimpleDateString(date)
+      onChangeDate(formatted)
     },
     [onChangeDate, setOpen],
   )
 
+  const onCancel = React.useCallback(() => {
+    setOpen(false)
+  }, [])
+
   return (
     <View style={[atoms.relative, atoms.w_full]}>
       <Pressable
@@ -89,18 +88,18 @@ export function DateField({
       </Pressable>
 
       {open && (
-        <DateTimePicker
+        <DatePicker
+          modal={isAndroid}
+          open={isAndroid}
+          theme={t.name === 'light' ? 'light' : 'dark'}
+          date={new Date(value)}
+          onConfirm={onChangeInternal}
+          onCancel={onCancel}
+          mode="date"
+          testID={`${testID}-datepicker`}
           aria-label={label}
           accessibilityLabel={label}
           accessibilityHint={undefined}
-          testID={`${testID}-datepicker`}
-          mode="date"
-          timeZoneName={'Etc/UTC'}
-          display="spinner"
-          // @ts-ignore applies in iOS only -prf
-          themeVariant={t.name === 'light' ? 'light' : 'dark'}
-          value={new Date(value)}
-          onChange={onChangeInternal}
         />
       )}
     </View>
diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx
index e65936e0e..49e47a01e 100644
--- a/src/components/forms/DateField/index.tsx
+++ b/src/components/forms/DateField/index.tsx
@@ -1,13 +1,11 @@
 import React from 'react'
 import {View} from 'react-native'
-import DateTimePicker, {
-  DateTimePickerEvent,
-} from '@react-native-community/datetimepicker'
 
 import {useTheme, atoms} from '#/alf'
 import * as TextField from '#/components/forms/TextField'
 import {toSimpleDateString} from '#/components/forms/DateField/utils'
 import {DateFieldProps} from '#/components/forms/DateField/types'
+import DatePicker from 'react-native-date-picker'
 
 export * as utils from '#/components/forms/DateField/utils'
 export const Label = TextField.Label
@@ -28,7 +26,7 @@ export function DateField({
   const t = useTheme()
 
   const onChangeInternal = React.useCallback(
-    (event: DateTimePickerEvent, date: Date | undefined) => {
+    (date: Date | undefined) => {
       if (date) {
         const formatted = toSimpleDateString(date)
         onChangeDate(formatted)
@@ -39,17 +37,15 @@ export function DateField({
 
   return (
     <View style={[atoms.relative, atoms.w_full]}>
-      <DateTimePicker
+      <DatePicker
+        theme={t.name === 'light' ? 'light' : 'dark'}
+        date={new Date(value)}
+        onDateChange={onChangeInternal}
+        mode="date"
+        testID={`${testID}-datepicker`}
         aria-label={label}
         accessibilityLabel={label}
         accessibilityHint={undefined}
-        testID={`${testID}-datepicker`}
-        mode="date"
-        timeZoneName={'Etc/UTC'}
-        display="spinner"
-        themeVariant={t.name === 'light' ? 'light' : 'dark'}
-        value={new Date(value)}
-        onChange={onChangeInternal}
       />
     </View>
   )
diff --git a/src/lib/hooks/useInitialNumToRender.ts b/src/lib/hooks/useInitialNumToRender.ts
new file mode 100644
index 000000000..942f0404a
--- /dev/null
+++ b/src/lib/hooks/useInitialNumToRender.ts
@@ -0,0 +1,11 @@
+import React from 'react'
+import {Dimensions} from 'react-native'
+
+const MIN_POST_HEIGHT = 100
+
+export function useInitialNumToRender(minItemHeight: number = MIN_POST_HEIGHT) {
+  return React.useMemo(() => {
+    const screenHeight = Dimensions.get('window').height
+    return Math.ceil(screenHeight / minItemHeight) + 1
+  }, [minItemHeight])
+}
diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts
index ba2cdb39b..7729e4a38 100644
--- a/src/lib/strings/url-helpers.ts
+++ b/src/lib/strings/url-helpers.ts
@@ -148,6 +148,11 @@ export function feedUriToHref(url: string): string {
 export function linkRequiresWarning(uri: string, label: string) {
   const labelDomain = labelToDomain(label)
 
+  // If the uri started with a / we know it is internal.
+  if (uri.startsWith('/')) {
+    return false
+  }
+
   let urip
   try {
     urip = new URL(uri)
@@ -156,9 +161,12 @@ export function linkRequiresWarning(uri: string, label: string) {
   }
 
   const host = urip.hostname.toLowerCase()
-
   // Hosts that end with bsky.app or bsky.social should be trusted by default.
-  if (host.endsWith('bsky.app') || host.endsWith('bsky.social')) {
+  if (
+    host.endsWith('bsky.app') ||
+    host.endsWith('bsky.social') ||
+    host.endsWith('blueskyweb.xyz')
+  ) {
     // if this is a link to internal content,
     // warn if it represents itself as a URL to another app
     return !!labelDomain && labelDomain !== host && isPossiblyAUrl(labelDomain)
diff --git a/src/screens/Hashtag.tsx b/src/screens/Hashtag.tsx
index 09a1f2824..f1b817370 100644
--- a/src/screens/Hashtag.tsx
+++ b/src/screens/Hashtag.tsx
@@ -1,6 +1,5 @@
 import React from 'react'
 import {ListRenderItemInfo, Pressable} from 'react-native'
-import {atoms as a, useBreakpoints} from '#/alf'
 import {useFocusEffect} from '@react-navigation/native'
 import {useSetMinimalShellMode} from 'state/shell'
 import {ViewHeader} from 'view/com/util/ViewHeader'
@@ -19,11 +18,11 @@ import {List} from 'view/com/util/List'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {sanitizeHandle} from 'lib/strings/handles'
-import {CenteredView} from 'view/com/util/Views'
 import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox'
 import {shareUrl} from 'lib/sharing'
 import {HITSLOP_10} from 'lib/constants'
 import {isNative} from 'platform/detection'
+import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
 
 const renderItem = ({item}: ListRenderItemInfo<PostView>) => {
   return <Post post={item} />
@@ -38,8 +37,8 @@ export default function HashtagScreen({
 }: NativeStackScreenProps<CommonNavigatorParams, 'Hashtag'>) {
   const {tag, author} = route.params
   const setMinimalShellMode = useSetMinimalShellMode()
-  const {gtMobile} = useBreakpoints()
   const {_} = useLingui()
+  const initialNumToRender = useInitialNumToRender()
   const [isPTR, setIsPTR] = React.useState(false)
 
   const fullTag = React.useMemo(() => {
@@ -103,7 +102,7 @@ export default function HashtagScreen({
   }, [isFetching, hasNextPage, error, fetchNextPage])
 
   return (
-    <CenteredView style={a.flex_1} sideBorders={gtMobile}>
+    <>
       <ViewHeader
         title={headerTitle}
         subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined}
@@ -157,8 +156,10 @@ export default function HashtagScreen({
               onRetry={fetchNextPage}
             />
           }
+          initialNumToRender={initialNumToRender}
+          windowSize={11}
         />
       )}
-    </CenteredView>
+    </>
   )
 }
diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts
index 25d284998..53c9e482a 100644
--- a/src/state/queries/preferences/const.ts
+++ b/src/state/queries/preferences/const.ts
@@ -7,7 +7,7 @@ import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/
 export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs'] =
   {
     hideReplies: false,
-    hideRepliesByUnfollowed: false,
+    hideRepliesByUnfollowed: true,
     hideRepliesByLikeCount: 0,
     hideReposts: false,
     hideQuotePosts: false,
diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts
index 07198de77..37ef10ae0 100644
--- a/src/state/queries/preferences/index.ts
+++ b/src/state/queries/preferences/index.ts
@@ -169,7 +169,7 @@ export function usePreferencesSetBirthDateMutation() {
 
   return useMutation<void, unknown, {birthDate: Date}>({
     mutationFn: async ({birthDate}: {birthDate: Date}) => {
-      await getAgent().setPersonalDetails({birthDate})
+      await getAgent().setPersonalDetails({birthDate: birthDate.toISOString()})
       // triggers a refetch
       await queryClient.invalidateQueries({
         queryKey: preferencesQueryKey,
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index 54d8aa224..cd3e98785 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -32,6 +32,7 @@ import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
 import {FALLBACK_MARKER_POST} from '#/lib/api/feed/home'
+import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
 
 const LOADING_ITEM = {_reactKey: '__loading__'}
 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@@ -84,6 +85,7 @@ let Feed = ({
   const {_} = useLingui()
   const queryClient = useQueryClient()
   const {currentAccount} = useSession()
+  const initialNumToRender = useInitialNumToRender()
   const [isPTRing, setIsPTRing] = React.useState(false)
   const checkForNewRef = React.useRef<(() => void) | null>(null)
   const lastFetchRef = React.useRef<number>(Date.now())
@@ -327,6 +329,8 @@ let Feed = ({
         desktopFixedHeight={
           desktopFixedHeightOffset ? desktopFixedHeightOffset : true
         }
+        initialNumToRender={initialNumToRender}
+        windowSize={11}
       />
     </View>
   )
diff --git a/src/view/com/util/Views.d.ts b/src/view/com/util/Views.d.ts
index 6a90cc229..16713921f 100644
--- a/src/view/com/util/Views.d.ts
+++ b/src/view/com/util/Views.d.ts
@@ -5,4 +5,6 @@ export function CenteredView({
   style,
   sideBorders,
   ...props
-}: React.PropsWithChildren<ViewProps & {sideBorders?: boolean}>)
+}: React.PropsWithChildren<
+  ViewProps & {sideBorders?: boolean; topBorder?: boolean}
+>)
diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx
index db3b9de0d..ae165077c 100644
--- a/src/view/com/util/Views.web.tsx
+++ b/src/view/com/util/Views.web.tsx
@@ -32,8 +32,11 @@ interface AddedProps {
 export function CenteredView({
   style,
   sideBorders,
+  topBorder,
   ...props
-}: React.PropsWithChildren<ViewProps & {sideBorders?: boolean}>) {
+}: React.PropsWithChildren<
+  ViewProps & {sideBorders?: boolean; topBorder?: boolean}
+>) {
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
   if (!isMobile) {
@@ -46,6 +49,12 @@ export function CenteredView({
     })
     style = addStyle(style, pal.border)
   }
+  if (topBorder) {
+    style = addStyle(style, {
+      borderTopWidth: 1,
+    })
+    style = addStyle(style, pal.border)
+  }
   return <View style={style} {...props} />
 }
 
diff --git a/src/view/com/util/forms/DateInput.tsx b/src/view/com/util/forms/DateInput.tsx
index c5f0afc8f..0104562aa 100644
--- a/src/view/com/util/forms/DateInput.tsx
+++ b/src/view/com/util/forms/DateInput.tsx
@@ -1,8 +1,5 @@
 import React, {useState, useCallback} from 'react'
 import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
-import DateTimePicker, {
-  DateTimePickerEvent,
-} from '@react-native-community/datetimepicker'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
@@ -14,6 +11,7 @@ import {TypographyVariant} from 'lib/ThemeContext'
 import {useTheme} from 'lib/ThemeContext'
 import {usePalette} from 'lib/hooks/usePalette'
 import {getLocales} from 'expo-localization'
+import DatePicker from 'react-native-date-picker'
 
 const LOCALE = getLocales()[0]
 
@@ -43,11 +41,9 @@ export function DateInput(props: Props) {
   }, [props.handleAsUTC])
 
   const onChangeInternal = useCallback(
-    (event: DateTimePickerEvent, date: Date | undefined) => {
+    (date: Date) => {
       setShow(false)
-      if (date) {
-        props.onChange(date)
-      }
+      props.onChange(date)
     },
     [setShow, props],
   )
@@ -56,6 +52,10 @@ export function DateInput(props: Props) {
     setShow(true)
   }, [setShow])
 
+  const onCancel = useCallback(() => {
+    setShow(false)
+  }, [])
+
   return (
     <View>
       {isAndroid && (
@@ -80,15 +80,17 @@ export function DateInput(props: Props) {
         </Button>
       )}
       {(isIOS || show) && (
-        <DateTimePicker
-          testID={props.testID ? `${props.testID}-datepicker` : undefined}
+        <DatePicker
+          timeZoneOffsetInMinutes={0}
+          modal={isAndroid}
+          open={isAndroid}
+          theme={theme.colorScheme}
+          date={props.value}
+          onDateChange={onChangeInternal}
+          onConfirm={onChangeInternal}
+          onCancel={onCancel}
           mode="date"
-          timeZoneName={props.handleAsUTC ? 'Etc/UTC' : undefined}
-          display="spinner"
-          // @ts-ignore applies in iOS only -prf
-          themeVariant={theme.colorScheme}
-          value={props.value}
-          onChange={onChangeInternal}
+          testID={props.testID ? `${props.testID}-datepicker` : undefined}
           accessibilityLabel={props.accessibilityLabel}
           accessibilityHint={props.accessibilityHint}
           accessibilityLabelledBy={props.accessibilityLabelledBy}
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index def0333c7..c56ba941e 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -391,7 +391,7 @@ export function DesktopLeftNav() {
               <FontAwesomeIcon
                 icon="hand"
                 style={pal.text as FontAwesomeIconStyle}
-                size={isDesktop ? 20 : 26}
+                size={isDesktop ? 23 : 26}
               />
             }
             label={_(msg`Moderation`)}
diff --git a/yarn.lock b/yarn.lock
index 3add1af64..386bc1a3b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -34,10 +34,10 @@
     jsonpointer "^5.0.0"
     leven "^3.1.0"
 
-"@atproto/api@^0.10.4":
-  version "0.10.4"
-  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.10.4.tgz#b73446f2344783c42c6040082756449443f15750"
-  integrity sha512-9gwZt4v4pngfD4mgsET9i9Ym0PpMSzftTzqBjCbFpObx15zMkFemYnLUnyT/NEww2u/aRxjAe2TeBnU0dIbbuQ==
+"@atproto/api@^0.10.5":
+  version "0.10.5"
+  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.10.5.tgz#e778e2843d08690df8df81f24028a7578e9b3cb4"
+  integrity sha512-GYdST5sPKU2JnPmm8x3KqjOSlDiYXrp4GkW7bpQTVLPabnUNq5NLN6HJEoJABjjOAsaLF12rBoV+JpRb1UjNsQ==
   dependencies:
     "@atproto/common-web" "^0.2.3"
     "@atproto/lexicon" "^0.3.2"
@@ -4986,13 +4986,6 @@
     prompts "^2.4.2"
     semver "^7.5.2"
 
-"@react-native-community/datetimepicker@7.6.1":
-  version "7.6.1"
-  resolved "https://registry.yarnpkg.com/@react-native-community/datetimepicker/-/datetimepicker-7.6.1.tgz#98bdee01e3df490526ee1125e438c2030becac1f"
-  integrity sha512-g66Q2Kd9Uw3eRL7kkrTsGhi+eXxNoPDRFYH6z78sZQuYjPkUQgJDDMUYgBmaBsQx/fKMtemPrCj1ulGmyi0OSw==
-  dependencies:
-    invariant "^2.2.4"
-
 "@react-native-community/eslint-config@^3.0.0":
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/@react-native-community/eslint-config/-/eslint-config-3.2.0.tgz#42f677d5fff385bccf1be1d3b8faa8c086cf998d"
@@ -18563,6 +18556,11 @@ react-native-appstate-hook@^1.0.6:
   resolved "https://registry.yarnpkg.com/react-native-appstate-hook/-/react-native-appstate-hook-1.0.6.tgz#cbc16e7b89cfaea034cabd999f00e99053cabd06"
   integrity sha512-0hPVyf5yLxCSVrrNEuGqN1ZnSSj3Ye2gZex0NtcK/AHYwMc0rXWFNZjBKOoZSouspqu3hXBbQ6NOUSTzrME1AQ==
 
+react-native-date-picker@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/react-native-date-picker/-/react-native-date-picker-4.4.0.tgz#fe5b6eb8d85a4a30b2991ada5169a30ce5023ead"
+  integrity sha512-Axx3byihwwhKRLRVjPAr/UaEysapkRcKmjjM8/05UaVm4Q0xDn2RFUcRdy1QAahhRcjLjlVYhepxvU5bdgy7ZQ==
+
 react-native-dotenv@^3.3.1:
   version "3.4.9"
   resolved "https://registry.yarnpkg.com/react-native-dotenv/-/react-native-dotenv-3.4.9.tgz#621c5b0c1d0c5c7f569bfe5a1d804bec7885c010"