diff --git a/assets/icons/newskie.svg b/assets/icons/newskie.svg
new file mode 100644
index 0000000000..e3a9d83c80
--- /dev/null
+++ b/assets/icons/newskie.svg
@@ -0,0 +1 @@
+
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
index 1ccb0460c4..1dc2dfa7b0 100644
--- a/src/alf/atoms.ts
+++ b/src/alf/atoms.ts
@@ -267,6 +267,9 @@ export const atoms = {
font_bold: {
fontWeight: tokens.fontWeight.bold,
},
+ font_heavy: {
+ fontWeight: tokens.fontWeight.heavy,
+ },
italic: {
fontStyle: 'italic',
},
diff --git a/src/alf/tokens.ts b/src/alf/tokens.ts
index 1bddd95d43..675844e296 100644
--- a/src/alf/tokens.ts
+++ b/src/alf/tokens.ts
@@ -118,6 +118,7 @@ export const fontWeight = {
normal: '400',
semibold: '500',
bold: '600',
+ heavy: '700',
} as const
export const gradients = {
diff --git a/src/components/NewskieDialog.tsx b/src/components/NewskieDialog.tsx
new file mode 100644
index 0000000000..fcdae0daa5
--- /dev/null
+++ b/src/components/NewskieDialog.tsx
@@ -0,0 +1,81 @@
+import React from 'react'
+import {View} from 'react-native'
+import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {differenceInSeconds} from 'date-fns'
+
+import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {HITSLOP_10} from 'lib/constants'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {atoms as a} from '#/alf'
+import {Button} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {useDialogControl} from '#/components/Dialog'
+import {Newskie} from '#/components/icons/Newskie'
+import {Text} from '#/components/Typography'
+
+export function NewskieDialog({
+ profile,
+}: {
+ profile: AppBskyActorDefs.ProfileViewDetailed
+}) {
+ const {_} = useLingui()
+ const moderationOpts = useModerationOpts()
+ const control = useDialogControl()
+ const profileName = React.useMemo(() => {
+ const name = profile.displayName || profile.handle
+ if (!moderationOpts) return name
+ const moderation = moderateProfile(profile, moderationOpts)
+ return sanitizeDisplayName(name, moderation.ui('displayName'))
+ }, [moderationOpts, profile])
+ const timeAgo = useGetTimeAgo()
+ const createdAt = profile.createdAt as string | undefined
+ const daysOld = React.useMemo(() => {
+ if (!createdAt) return Infinity
+ return differenceInSeconds(new Date(), new Date(createdAt)) / 86400
+ }, [createdAt])
+
+ if (!createdAt || daysOld > 7) return null
+
+ return (
+
+
+
+
+
+
+
+
+ Say hello!
+
+
+
+ {profileName} joined Bluesky{' '}
+ {timeAgo(createdAt, {format: 'long'})} ago
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx
index e231ac5baf..c916f4efce 100644
--- a/src/components/forms/DateField/index.tsx
+++ b/src/components/forms/DateField/index.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import {View} from 'react-native'
+import {Keyboard, View} from 'react-native'
import DatePicker from 'react-native-date-picker'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -49,7 +49,10 @@ export function DateField({
{
+ Keyboard.dismiss()
+ control.open()
+ }}
isInvalid={isInvalid}
accessibilityHint={accessibilityHint}
/>
diff --git a/src/components/icons/Newskie.tsx b/src/components/icons/Newskie.tsx
new file mode 100644
index 0000000000..ddbb33201e
--- /dev/null
+++ b/src/components/icons/Newskie.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const Newskie = createSinglePathSVG({
+ path: 'M11.183 8.561c0 .544.348.984.892.984.545 0 .893-.44.893-.985V6.985c0-.544-.348-.985-.893-.985-.543 0-.892.44-.892.985v1.576Zm5.94 7.481c0 .539-.438.942-.976.942H8.004c-.538 0-.975-.411-.975-.95 0-2.782 2.264-5.021 5.046-5.021 2.783 0 5.047 2.247 5.047 5.03Zm-.43-4.584a.983.983 0 0 1 0-1.393l1.114-1.114a.985.985 0 0 1 1.393 1.393l-1.114 1.114a.985.985 0 0 1-1.393 0Zm2.897 3.741h1.575c.544 0 .985.349.985.892 0 .544-.44.892-.985.892h-1.67a.872.872 0 0 1-.89-.887c0-.543.44-.897.985-.897Zm-14.045.893c0-.544-.44-.892-.985-.892H2.985c-.544 0-.985.349-.985.892 0 .544.44.892.985.892H4.56c.545 0 .985-.349.985-.892Zm1.913-6.027a.985.985 0 0 1-1.393 1.393L4.95 10.344A.985.985 0 0 1 6.344 8.95l1.114 1.114Z',
+})
diff --git a/src/screens/Profile/Header/Handle.tsx b/src/screens/Profile/Header/Handle.tsx
index 9ab24fbbed..4f438a2868 100644
--- a/src/screens/Profile/Header/Handle.tsx
+++ b/src/screens/Profile/Header/Handle.tsx
@@ -5,7 +5,9 @@ import {Trans} from '@lingui/macro'
import {Shadow} from '#/state/cache/types'
import {isInvalidHandle} from 'lib/strings/handles'
+import {isAndroid} from 'platform/detection'
import {atoms as a, useTheme, web} from '#/alf'
+import {NewskieDialog} from '#/components/NewskieDialog'
import {Text} from '#/components/Typography'
export function ProfileHeaderHandle({
@@ -17,7 +19,10 @@ export function ProfileHeaderHandle({
const invalidHandle = isInvalidHandle(profile.handle)
const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy
return (
-
+
+
{profile.viewer?.followedBy && !blockHide ? (
diff --git a/src/view/com/util/layouts/LoggedOutLayout.tsx b/src/view/com/util/layouts/LoggedOutLayout.tsx
index 0272a44c6b..c2c080c171 100644
--- a/src/view/com/util/layouts/LoggedOutLayout.tsx
+++ b/src/view/com/util/layouts/LoggedOutLayout.tsx
@@ -3,6 +3,7 @@ import {ScrollView, StyleSheet, View} from 'react-native'
import {isWeb} from '#/platform/detection'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
+import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {atoms as a} from '#/alf'
@@ -29,13 +30,18 @@ export const LoggedOutLayout = ({
borderLeftWidth: 1,
})
+ const [isKeyboardVisible] = useIsKeyboardVisible()
+
if (isMobile) {
if (scrollable) {
return (
+ keyboardDismissMode="none"
+ contentContainerStyle={[
+ {paddingBottom: isKeyboardVisible ? 300 : 0},
+ ]}>
{children}
)
diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx
index c7f5f939fe..dd93bf8130 100644
--- a/src/view/screens/Search/Explore.tsx
+++ b/src/view/screens/Search/Explore.tsx
@@ -64,7 +64,7 @@ function SuggestedItemsHeader({
fill={t.palette.primary_500}
style={{marginLeft: -2}}
/>
- {title}
+ {title}
{description}
@@ -119,6 +119,9 @@ function LoadMore({
})
.filter(Boolean) as LoadMoreItems[]
}, [item.items, moderationOpts])
+
+ if (items.length === 0) return null
+
const type = items[0].type
return (
@@ -142,20 +145,20 @@ function LoadMore({
a.relative,
{
height: 32,
- width: 32 + 15 * 3,
+ width: 32 + 15 * items.length,
},
]}>
item.type === 'profile').slice(-3),
- })
+ if (hasNextProfilesPage) {
+ i.push({
+ type: 'loadMore',
+ key: 'loadMoreProfiles',
+ isLoadingMore: isLoadingMoreProfiles,
+ onLoadMore: onLoadMoreProfiles,
+ items: i.filter(item => item.type === 'profile').slice(-3),
+ })
+ }
} else {
if (profilesError) {
i.push({
@@ -412,7 +417,7 @@ export function Explore() {
message: _(msg`Failed to load feeds preferences`),
error: cleanError(preferencesError),
})
- } else {
+ } else if (hasNextFeedsPage) {
i.push({
type: 'loadMore',
key: 'loadMoreFeeds',
@@ -454,6 +459,8 @@ export function Explore() {
profilesError,
feedsError,
preferencesError,
+ hasNextProfilesPage,
+ hasNextFeedsPage,
])
const renderItem = React.useCallback(