Merge branch 'main' into ja-translation-17
This commit is contained in:
@@ -40,4 +40,4 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
||||
@@ -23,7 +23,7 @@ class ShareViewController: UIViewController {
|
||||
await self.handleUrl(item: firstAttachment)
|
||||
} else if firstAttachment.hasItemConformingToTypeIdentifier("public.image") {
|
||||
await self.handleImages(items: attachments)
|
||||
} else if firstAttachment.hasItemConformingToTypeIdentifier("public.video") {
|
||||
} else if firstAttachment.hasItemConformingToTypeIdentifier("public.movie") {
|
||||
await self.handleVideos(items: attachments)
|
||||
} else {
|
||||
self.completeRequest()
|
||||
@@ -101,13 +101,13 @@ class ShareViewController: UIViewController {
|
||||
private func handleVideos(items: [NSItemProvider]) async {
|
||||
let firstItem = items.first
|
||||
|
||||
if let dataUri = try? await firstItem?.loadItem(forTypeIdentifier: "public.video") as? URL {
|
||||
if let dataUri = try? await firstItem?.loadItem(forTypeIdentifier: "public.movie") as? URL {
|
||||
let ext = String(dataUri.lastPathComponent.split(separator: ".").last ?? "mp4")
|
||||
if let tempUrl = getTempUrl(ext: ext) {
|
||||
let data = try? Data(contentsOf: dataUri)
|
||||
try? data?.write(to: tempUrl)
|
||||
|
||||
if let encoded = dataUri.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
|
||||
if let encoded = tempUrl.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
|
||||
let url = URL(string: "\(self.appScheme)://intent/compose?videoUri=\(encoded)") {
|
||||
_ = self.openURL(url)
|
||||
}
|
||||
@@ -150,7 +150,8 @@ class ShareViewController: UIViewController {
|
||||
var responder: UIResponder? = self
|
||||
while responder != nil {
|
||||
if let application = responder as? UIApplication {
|
||||
return application.perform(#selector(openURL(_:)), with: url) != nil
|
||||
application.open(url)
|
||||
return true
|
||||
}
|
||||
responder = responder?.next
|
||||
}
|
||||
|
||||
+6
-3
@@ -45,15 +45,18 @@ export function applyFonts(
|
||||
// '100': 'Inter-Thin',
|
||||
// '200': 'Inter-ExtraLight',
|
||||
// '300': 'Inter-Light',
|
||||
// '500': 'Inter-Medium',
|
||||
// '700': 'Inter-Bold',
|
||||
// '900': 'Inter-Black',
|
||||
'100': 'Inter-Regular',
|
||||
'200': 'Inter-Regular',
|
||||
'300': 'Inter-Regular',
|
||||
'400': 'Inter-Regular',
|
||||
'500': 'Inter-Medium',
|
||||
'500': 'Inter-SemiBold',
|
||||
'600': 'Inter-SemiBold',
|
||||
'700': 'Inter-Bold',
|
||||
'700': 'Inter-SemiBold',
|
||||
'800': 'Inter-ExtraBold',
|
||||
'900': 'Inter-Black',
|
||||
'900': 'Inter-ExtraBold',
|
||||
}[style.fontWeight as string] || 'Inter-Regular'
|
||||
|
||||
if (style.fontStyle === 'italic') {
|
||||
|
||||
@@ -60,6 +60,15 @@ export const fontWeight = {
|
||||
} as const
|
||||
|
||||
export const gradients = {
|
||||
primary: {
|
||||
values: [
|
||||
[0, '#054CFF'],
|
||||
[0.4, '#1085FE'],
|
||||
[0.6, '#1085FE'],
|
||||
[1, '#59B9FF'],
|
||||
],
|
||||
hover_value: '#1085FE',
|
||||
},
|
||||
sky: {
|
||||
values: [
|
||||
[0, '#0A7AFF'],
|
||||
|
||||
@@ -126,10 +126,12 @@ function AccountItem({
|
||||
<UserAvatar avatar={profile?.avatar} size={24} />
|
||||
</View>
|
||||
<Text style={[a.align_baseline, a.flex_1, a.flex_row, a.py_sm]}>
|
||||
<Text style={[a.font_bold]}>
|
||||
<Text emoji style={[a.font_bold]}>
|
||||
{profile?.displayName || account.handle}{' '}
|
||||
</Text>
|
||||
<Text style={[t.atoms.text_contrast_medium]}>{account.handle}</Text>
|
||||
<Text emoji style={[t.atoms.text_contrast_medium]}>
|
||||
{account.handle}
|
||||
</Text>
|
||||
</Text>
|
||||
{isCurrentAccount ? (
|
||||
<Check
|
||||
|
||||
@@ -24,6 +24,7 @@ export type ButtonColor =
|
||||
| 'secondary'
|
||||
| 'secondary_inverted'
|
||||
| 'negative'
|
||||
| 'gradient_primary'
|
||||
| 'gradient_sky'
|
||||
| 'gradient_midnight'
|
||||
| 'gradient_sunrise'
|
||||
@@ -412,6 +413,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
secondary: tokens.gradients.sky,
|
||||
secondary_inverted: tokens.gradients.sky,
|
||||
negative: tokens.gradients.sky,
|
||||
gradient_primary: tokens.gradients.primary,
|
||||
gradient_sky: tokens.gradients.sky,
|
||||
gradient_midnight: tokens.gradients.midnight,
|
||||
gradient_sunrise: tokens.gradients.sunrise,
|
||||
@@ -444,7 +446,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
[state, variant, color, size, disabled],
|
||||
)
|
||||
|
||||
const flattenedBaseStyles = flatten(baseStyles)
|
||||
const flattenedBaseStyles = flatten([baseStyles, style])
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
@@ -464,7 +466,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
flattenedBaseStyles,
|
||||
flatten(style),
|
||||
...(state.hovered || state.pressed
|
||||
? [hoverStyles, flatten(hoverStyleProp)]
|
||||
: []),
|
||||
@@ -626,9 +627,9 @@ export function useSharedButtonTextStyles() {
|
||||
}
|
||||
|
||||
if (size === 'large') {
|
||||
baseStyles.push(a.text_md, a.leading_tight, web({paddingTop: 1}))
|
||||
baseStyles.push(a.text_md, a.leading_tight, web({top: -0.4}))
|
||||
} else if (size === 'small') {
|
||||
baseStyles.push(a.text_sm, a.leading_tight, web({paddingTop: 1}))
|
||||
baseStyles.push(a.text_sm, a.leading_tight, web({top: -0.4}))
|
||||
} else if (size === 'tiny') {
|
||||
baseStyles.push(a.text_xs, a.leading_tight)
|
||||
}
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import Svg, {Circle, Path} from 'react-native-svg'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {Nux, useUpsertNuxMutation} from '#/state/queries/nuxs'
|
||||
import {atoms as a, ViewStyleProp} from '#/alf'
|
||||
import {Button, ButtonProps} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {TenMillion} from './'
|
||||
|
||||
export function Trigger({children}: {children: ButtonProps['children']}) {
|
||||
const {_} = useLingui()
|
||||
const {mutate: upsertNux} = useUpsertNuxMutation()
|
||||
const [show, setShow] = React.useState(false)
|
||||
const [fallback, setFallback] = React.useState(false)
|
||||
const control = Prompt.usePromptControl()
|
||||
|
||||
const handleOnPress = () => {
|
||||
if (!fallback) {
|
||||
setShow(true)
|
||||
upsertNux({
|
||||
id: Nux.TenMillionDialog,
|
||||
completed: true,
|
||||
data: undefined,
|
||||
})
|
||||
} else {
|
||||
control.open()
|
||||
}
|
||||
}
|
||||
|
||||
const onHandleFallback = () => {
|
||||
setFallback(true)
|
||||
control.open()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
label={_(msg`Bluesky is celebrating 10 million users!`)}
|
||||
onPress={handleOnPress}>
|
||||
{children}
|
||||
</Button>
|
||||
|
||||
{show && !fallback && (
|
||||
<TenMillion
|
||||
showTimeout={0}
|
||||
onClose={() => setShow(false)}
|
||||
onFallback={onHandleFallback}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Prompt.Outer control={control}>
|
||||
<View style={{maxWidth: 300}}>
|
||||
<Prompt.TitleText>
|
||||
<Trans>Bluesky is celebrating 10 million users!</Trans>
|
||||
</Prompt.TitleText>
|
||||
</View>
|
||||
<Prompt.DescriptionText>
|
||||
<Trans>
|
||||
Together, we're rebuilding the social internet. We're glad you're
|
||||
here.
|
||||
</Trans>
|
||||
</Prompt.DescriptionText>
|
||||
<Prompt.DescriptionText>
|
||||
<Trans>
|
||||
To learn more,{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`View our post`)}
|
||||
to="/profile/bsky.app/post/3l47prg3wgy23"
|
||||
onPress={() => {
|
||||
control.close()
|
||||
}}
|
||||
style={[a.text_md, a.leading_snug]}>
|
||||
<Trans>check out our post.</Trans>
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
</Prompt.DescriptionText>
|
||||
<Dialog.Close />
|
||||
</Prompt.Outer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function Icon({width, style}: {width: number} & ViewStyleProp) {
|
||||
return (
|
||||
<Svg width={width} height={width} viewBox="0 0 36 36" style={style}>
|
||||
<Path
|
||||
fill="#dd2e44"
|
||||
d="M11.626 7.488a1.4 1.4 0 0 0-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937c.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269c1.562-1.562-.971-6.627-5.656-11.313c-4.687-4.686-9.752-7.218-11.315-5.656"
|
||||
/>
|
||||
<Path
|
||||
fill="#ea596e"
|
||||
d="M13 12L.416 32.506l-.282.635l.011.011c-.208.403.14 1.223.853 1.937c.232.232.473.408.709.557L17 17z"
|
||||
/>
|
||||
<Path
|
||||
fill="#a0041e"
|
||||
d="M23.012 13.066c4.67 4.672 7.263 9.652 5.789 11.124c-1.473 1.474-6.453-1.118-11.126-5.788c-4.671-4.672-7.263-9.654-5.79-11.127c1.474-1.473 6.454 1.119 11.127 5.791"
|
||||
/>
|
||||
<Path
|
||||
fill="#aa8dd8"
|
||||
d="M18.59 13.609a1 1 0 0 1-.734.215c-.868-.094-1.598-.396-2.109-.873c-.541-.505-.808-1.183-.735-1.862c.128-1.192 1.324-2.286 3.363-2.066c.793.085 1.147-.17 1.159-.292c.014-.121-.277-.446-1.07-.532c-.868-.094-1.598-.396-2.11-.873c-.541-.505-.809-1.183-.735-1.862c.13-1.192 1.325-2.286 3.362-2.065c.578.062.883-.057 1.012-.134c.103-.063.144-.123.148-.158c.012-.121-.275-.446-1.07-.532a1 1 0 0 1-.886-1.102a.997.997 0 0 1 1.101-.886c2.037.219 2.973 1.542 2.844 2.735c-.13 1.194-1.325 2.286-3.364 2.067c-.578-.063-.88.057-1.01.134c-.103.062-.145.123-.149.157c-.013.122.276.446 1.071.532c2.037.22 2.973 1.542 2.844 2.735s-1.324 2.286-3.362 2.065c-.578-.062-.882.058-1.012.134c-.104.064-.144.124-.148.158c-.013.121.276.446 1.07.532a1 1 0 0 1 .52 1.773"
|
||||
/>
|
||||
<Path
|
||||
fill="#77b255"
|
||||
d="M30.661 22.857c1.973-.557 3.334.323 3.658 1.478c.324 1.154-.378 2.615-2.35 3.17c-.77.216-1.001.584-.97.701c.034.118.425.312 1.193.095c1.972-.555 3.333.325 3.657 1.479c.326 1.155-.378 2.614-2.351 3.17c-.769.216-1.001.585-.967.702s.423.311 1.192.095a1 1 0 1 1 .54 1.925c-1.971.555-3.333-.323-3.659-1.479c-.324-1.154.379-2.613 2.353-3.169c.77-.217 1.001-.584.967-.702c-.032-.117-.422-.312-1.19-.096c-1.974.556-3.334-.322-3.659-1.479c-.325-1.154.378-2.613 2.351-3.17c.768-.215.999-.585.967-.701c-.034-.118-.423-.312-1.192-.096a1 1 0 1 1-.54-1.923"
|
||||
/>
|
||||
<Path
|
||||
fill="#aa8dd8"
|
||||
d="M23.001 20.16a1.001 1.001 0 0 1-.626-1.781c.218-.175 5.418-4.259 12.767-3.208a1 1 0 1 1-.283 1.979c-6.493-.922-11.187 2.754-11.233 2.791a1 1 0 0 1-.625.219"
|
||||
/>
|
||||
<Path
|
||||
fill="#77b255"
|
||||
d="M5.754 16a1 1 0 0 1-.958-1.287c1.133-3.773 2.16-9.794.898-11.364c-.141-.178-.354-.353-.842-.316c-.938.072-.849 2.051-.848 2.071a1 1 0 1 1-1.994.149c-.103-1.379.326-4.035 2.692-4.214c1.056-.08 1.933.287 2.552 1.057c2.371 2.951-.036 11.506-.542 13.192a1 1 0 0 1-.958.712"
|
||||
/>
|
||||
<Circle cx="25.5" cy="9.5" r="1.5" fill="#5c913b" />
|
||||
<Circle cx="2" cy="18" r="2" fill="#9266cc" />
|
||||
<Circle cx="32.5" cy="19.5" r="1.5" fill="#5c913b" />
|
||||
<Circle cx="23.5" cy="31.5" r="1.5" fill="#5c913b" />
|
||||
<Circle cx="28" cy="4" r="2" fill="#ffcc4d" />
|
||||
<Circle cx="32.5" cy="8.5" r="1.5" fill="#ffcc4d" />
|
||||
<Circle cx="29.5" cy="12.5" r="1.5" fill="#ffcc4d" />
|
||||
<Circle cx="7.5" cy="23.5" r="1.5" fill="#ffcc4d" />
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,712 +0,0 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import Animated, {FadeIn} from 'react-native-reanimated'
|
||||
import ViewShot from 'react-native-view-shot'
|
||||
import {Image} from 'expo-image'
|
||||
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
|
||||
import * as MediaLibrary from 'expo-media-library'
|
||||
import {moderateProfile} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {networkRetry} from '#/lib/async/retry'
|
||||
import {getCanvas} from '#/lib/canvas'
|
||||
import {shareUrl} from '#/lib/sharing'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {isIOS, isNative} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {useComposerControls} from '#/state/shell'
|
||||
import {formatCount} from '#/view/com/util/numeric/format'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {Logomark} from '#/view/icons/Logomark'
|
||||
import {
|
||||
atoms as a,
|
||||
ThemeProvider,
|
||||
tokens,
|
||||
useBreakpoints,
|
||||
useTheme,
|
||||
} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
|
||||
import {OnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/OnePercent'
|
||||
import {PointOnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/PointOnePercent'
|
||||
import {TenPercent} from '#/components/dialogs/nuxs/TenMillion/icons/TenPercent'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {GradientFill} from '#/components/GradientFill'
|
||||
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
||||
import {Download_Stroke2_Corner0_Rounded as Download} from '#/components/icons/Download'
|
||||
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const DEBUG = false
|
||||
const RATIO = 8 / 10
|
||||
const WIDTH = 2000
|
||||
const HEIGHT = WIDTH * RATIO
|
||||
|
||||
function getFontSize(count: number) {
|
||||
const length = count.toString().length
|
||||
if (length < 7) {
|
||||
return 80
|
||||
} else if (length < 5) {
|
||||
return 100
|
||||
} else {
|
||||
return 70
|
||||
}
|
||||
}
|
||||
|
||||
function getPercentBadge(percent: number) {
|
||||
if (percent <= 0.001) {
|
||||
return PointOnePercent
|
||||
} else if (percent <= 0.01) {
|
||||
return OnePercent
|
||||
} else if (percent <= 0.1) {
|
||||
return TenPercent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function Frame({children}: {children: React.ReactNode}) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.w_full,
|
||||
a.overflow_hidden,
|
||||
{
|
||||
paddingTop: '80%',
|
||||
},
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function TenMillion({
|
||||
showTimeout,
|
||||
onClose,
|
||||
onFallback,
|
||||
}: {
|
||||
showTimeout?: number
|
||||
onClose?: () => void
|
||||
onFallback?: () => void
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
const nuxDialogs = useNuxDialogContext()
|
||||
const [userNumber, setUserNumber] = React.useState<number>(0)
|
||||
const fetching = React.useRef(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
async function fetchUserNumber() {
|
||||
const isBlueskyHosted = agent.sessionManager.pdsUrl
|
||||
?.toString()
|
||||
.includes('bsky.network')
|
||||
|
||||
if (isBlueskyHosted && agent.session?.accessJwt) {
|
||||
const res = await fetch(
|
||||
`https://bsky.social/xrpc/com.atproto.temp.getSignupNumber`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${agent.session.accessJwt}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Network request failed')
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
if (data.number && data.number <= 10_000_000) {
|
||||
setUserNumber(data.number)
|
||||
} else {
|
||||
// should be rare
|
||||
nuxDialogs.dismissActiveNux()
|
||||
onFallback?.()
|
||||
}
|
||||
} else {
|
||||
nuxDialogs.dismissActiveNux()
|
||||
onFallback?.()
|
||||
}
|
||||
}
|
||||
|
||||
if (!fetching.current) {
|
||||
fetching.current = true
|
||||
networkRetry(3, fetchUserNumber).catch(() => {
|
||||
nuxDialogs.dismissActiveNux()
|
||||
onFallback?.()
|
||||
})
|
||||
}
|
||||
}, [
|
||||
agent.sessionManager.pdsUrl,
|
||||
agent.session?.accessJwt,
|
||||
setUserNumber,
|
||||
nuxDialogs.dismissActiveNux,
|
||||
nuxDialogs,
|
||||
onFallback,
|
||||
])
|
||||
|
||||
return userNumber ? (
|
||||
<TenMillionInner
|
||||
userNumber={userNumber}
|
||||
showTimeout={showTimeout ?? 3e3}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
export function TenMillionInner({
|
||||
userNumber,
|
||||
showTimeout,
|
||||
onClose: onCloseOuter,
|
||||
}: {
|
||||
userNumber: number
|
||||
showTimeout: number
|
||||
onClose?: () => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const lightTheme = useTheme('light')
|
||||
const {_, i18n} = useLingui()
|
||||
const control = Dialog.useDialogControl()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {openComposer} = useComposerControls()
|
||||
const {currentAccount} = useSession()
|
||||
const {
|
||||
isLoading: isProfileLoading,
|
||||
data: profile,
|
||||
error: profileError,
|
||||
} = useProfileQuery({
|
||||
did: currentAccount!.did,
|
||||
})
|
||||
const moderationOpts = useModerationOpts()
|
||||
const nuxDialogs = useNuxDialogContext()
|
||||
const moderation = React.useMemo(() => {
|
||||
return profile && moderationOpts
|
||||
? moderateProfile(profile, moderationOpts)
|
||||
: undefined
|
||||
}, [profile, moderationOpts])
|
||||
const [uri, setUri] = React.useState<string | null>(null)
|
||||
const percent = userNumber / 10_000_000
|
||||
const Badge = getPercentBadge(percent)
|
||||
const isLoadingData = isProfileLoading || !moderation || !profile
|
||||
const isLoadingImage = !uri
|
||||
|
||||
const displayName = React.useMemo(() => {
|
||||
if (!profile || !moderation) return ''
|
||||
return sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
)
|
||||
}, [profile, moderation])
|
||||
const handle = React.useMemo(() => {
|
||||
if (!profile) return ''
|
||||
return sanitizeHandle(profile.handle, '@')
|
||||
}, [profile])
|
||||
const joinedDate = React.useMemo(() => {
|
||||
if (!profile || !profile.createdAt) return ''
|
||||
const date = i18n.date(profile.createdAt, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
return date
|
||||
}, [i18n, profile])
|
||||
|
||||
const error: string = React.useMemo(() => {
|
||||
if (profileError) {
|
||||
return _(
|
||||
msg`Oh no! We weren't able to generate an image for you to share. Rest assured, we're glad you're here 🦋`,
|
||||
)
|
||||
}
|
||||
return ''
|
||||
}, [_, profileError])
|
||||
|
||||
/*
|
||||
* Opening and closing
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
control.open()
|
||||
}, showTimeout)
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [control, showTimeout])
|
||||
const onClose = React.useCallback(() => {
|
||||
nuxDialogs.dismissActiveNux()
|
||||
onCloseOuter?.()
|
||||
}, [nuxDialogs, onCloseOuter])
|
||||
|
||||
/*
|
||||
* Actions
|
||||
*/
|
||||
const sharePost = React.useCallback(() => {
|
||||
if (uri) {
|
||||
control.close(() => {
|
||||
setTimeout(() => {
|
||||
logEvent('tmd:post', {})
|
||||
openComposer({
|
||||
text: _(
|
||||
msg`Bluesky now has over 10 million users, and I was #${i18n.number(
|
||||
userNumber,
|
||||
)}!`,
|
||||
),
|
||||
imageUris: [
|
||||
{
|
||||
uri,
|
||||
width: WIDTH,
|
||||
height: HEIGHT,
|
||||
altText: _(
|
||||
msg`A virtual certificate with text "Celebrating 10M users on Bluesky, #${i18n.number(
|
||||
userNumber,
|
||||
)}, ${displayName} ${handle}, joined on ${joinedDate}"`,
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
}, 1e3)
|
||||
})
|
||||
}
|
||||
}, [
|
||||
_,
|
||||
i18n,
|
||||
control,
|
||||
openComposer,
|
||||
uri,
|
||||
userNumber,
|
||||
displayName,
|
||||
handle,
|
||||
joinedDate,
|
||||
])
|
||||
const onNativeShare = React.useCallback(() => {
|
||||
if (uri) {
|
||||
control.close(() => {
|
||||
logEvent('tmd:share', {})
|
||||
shareUrl(uri)
|
||||
})
|
||||
}
|
||||
}, [uri, control])
|
||||
const onNativeDownload = React.useCallback(async () => {
|
||||
if (uri) {
|
||||
const res = await requestMediaLibraryPermissionsAsync()
|
||||
|
||||
if (!res) {
|
||||
Toast.show(
|
||||
_(
|
||||
msg`You must grant access to your photo library to save the image.`,
|
||||
),
|
||||
'xmark',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await MediaLibrary.createAssetAsync(uri)
|
||||
logEvent('tmd:download', {})
|
||||
Toast.show(_(msg`Image saved to your camera roll!`))
|
||||
} catch (e: unknown) {
|
||||
console.log(e)
|
||||
Toast.show(_(msg`An error occurred while saving the image!`), 'xmark')
|
||||
return
|
||||
}
|
||||
}
|
||||
}, [_, uri])
|
||||
const onWebDownload = React.useCallback(async () => {
|
||||
if (uri) {
|
||||
const canvas = await getCanvas(uri)
|
||||
const imgHref = canvas
|
||||
.toDataURL('image/png')
|
||||
.replace('image/png', 'image/octet-stream')
|
||||
const link = document.createElement('a')
|
||||
link.setAttribute('download', `Bluesky 10M Users.png`)
|
||||
link.setAttribute('href', imgHref)
|
||||
link.click()
|
||||
logEvent('tmd:download', {})
|
||||
}
|
||||
}, [uri])
|
||||
|
||||
/*
|
||||
* Canvas stuff
|
||||
*/
|
||||
const imageRef = React.useRef<ViewShot>(null)
|
||||
const captureInProgress = React.useRef(false)
|
||||
const onCanvasReady = React.useCallback(async () => {
|
||||
if (
|
||||
imageRef.current &&
|
||||
imageRef.current.capture &&
|
||||
!captureInProgress.current
|
||||
) {
|
||||
captureInProgress.current = true
|
||||
const uri = await imageRef.current.capture()
|
||||
setUri(uri)
|
||||
}
|
||||
}, [setUri])
|
||||
const canvas = isLoadingData ? null : (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.overflow_hidden,
|
||||
DEBUG
|
||||
? {
|
||||
width: 600,
|
||||
height: 600 * RATIO,
|
||||
}
|
||||
: {
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
]}>
|
||||
<View style={{width: 600}}>
|
||||
<ThemeProvider theme="light">
|
||||
<Frame>
|
||||
<ViewShot
|
||||
ref={imageRef}
|
||||
options={{width: WIDTH, height: HEIGHT}}
|
||||
style={[a.absolute, a.inset_0]}>
|
||||
<View
|
||||
onLayout={onCanvasReady}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
{
|
||||
top: -1,
|
||||
bottom: -1,
|
||||
left: -1,
|
||||
right: -1,
|
||||
paddingVertical: 48,
|
||||
paddingHorizontal: 48,
|
||||
},
|
||||
]}>
|
||||
<GradientFill gradient={tokens.gradients.bonfire} />
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.w_full,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.rounded_md,
|
||||
{
|
||||
backgroundColor: 'white',
|
||||
shadowRadius: 32,
|
||||
shadowOpacity: 0.1,
|
||||
elevation: 24,
|
||||
shadowColor: tokens.gradients.bonfire.values[0][1],
|
||||
},
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.px_xl,
|
||||
a.py_xl,
|
||||
{
|
||||
top: 0,
|
||||
left: 0,
|
||||
},
|
||||
]}>
|
||||
<Logomark fill={t.palette.primary_500} width={36} />
|
||||
</View>
|
||||
|
||||
{/* Centered content */}
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
paddingBottom: isNative ? 0 : 24,
|
||||
},
|
||||
]}>
|
||||
<Text
|
||||
allowFontScaling={false}
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
a.text_center,
|
||||
a.pb_sm,
|
||||
lightTheme.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>
|
||||
Celebrating {formatCount(i18n, 10000000)} users
|
||||
</Trans>{' '}
|
||||
🎉
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.align_start]}>
|
||||
<Text
|
||||
allowFontScaling={false}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.font_heavy,
|
||||
{
|
||||
color: t.palette.primary_500,
|
||||
fontSize: 32,
|
||||
width: 32,
|
||||
top: isNative ? -10 : 0,
|
||||
left: 0,
|
||||
transform: [
|
||||
{
|
||||
translateX: -16,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}>
|
||||
#
|
||||
</Text>
|
||||
<Text
|
||||
allowFontScaling={false}
|
||||
style={[
|
||||
a.relative,
|
||||
a.text_center,
|
||||
a.font_heavy,
|
||||
{
|
||||
fontStyle: 'italic',
|
||||
fontSize: getFontSize(userNumber),
|
||||
lineHeight: getFontSize(userNumber),
|
||||
letterSpacing: -2,
|
||||
},
|
||||
]}>
|
||||
{i18n.number(userNumber)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{Badge && (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
{
|
||||
width: 64,
|
||||
height: 64,
|
||||
top: isNative ? 75 : 85,
|
||||
right: '5%',
|
||||
transform: [
|
||||
{
|
||||
rotate: '8deg',
|
||||
},
|
||||
],
|
||||
},
|
||||
]}>
|
||||
<Badge fill={t.palette.primary_500} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{/* End centered content */}
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.px_xl,
|
||||
a.py_xl,
|
||||
{
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
]}>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||
{/*
|
||||
<UserAvatar
|
||||
size={36}
|
||||
avatar={profile.avatar}
|
||||
moderation={moderation.ui('avatar')}
|
||||
onLoad={onCanvasReady}
|
||||
/>
|
||||
*/}
|
||||
<View style={[a.gap_2xs, a.flex_1]}>
|
||||
<Text
|
||||
allowFontScaling={false}
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
a.leading_tight,
|
||||
{maxWidth: '60%'},
|
||||
]}>
|
||||
{displayName}
|
||||
</Text>
|
||||
<View
|
||||
style={[a.flex_row, a.justify_between, a.gap_4xl]}>
|
||||
<Text
|
||||
allowFontScaling={false}
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
a.leading_snug,
|
||||
lightTheme.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{handle}
|
||||
</Text>
|
||||
|
||||
{profile.createdAt && (
|
||||
<Text
|
||||
allowFontScaling={false}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="head"
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
a.leading_snug,
|
||||
a.text_right,
|
||||
lightTheme.atoms.text_contrast_low,
|
||||
]}>
|
||||
<Trans>Joined on {joinedDate}</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ViewShot>
|
||||
</Frame>
|
||||
</ThemeProvider>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control} onClose={onClose}>
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Ten Million`)}
|
||||
style={[
|
||||
{
|
||||
padding: 0,
|
||||
paddingTop: 0,
|
||||
},
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
isNative && {
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
},
|
||||
]}>
|
||||
<Frame>
|
||||
<View
|
||||
style={[a.absolute, a.inset_0, a.align_center, a.justify_center]}>
|
||||
<GradientFill gradient={tokens.gradients.bonfire} />
|
||||
{error ? (
|
||||
<View>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.leading_snug,
|
||||
a.text_center,
|
||||
a.pb_md,
|
||||
{
|
||||
maxWidth: 300,
|
||||
},
|
||||
]}>
|
||||
(╯°□°)╯︵ ┻━┻
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_xl,
|
||||
a.font_bold,
|
||||
a.leading_snug,
|
||||
a.text_center,
|
||||
{
|
||||
maxWidth: 300,
|
||||
},
|
||||
]}>
|
||||
{error}
|
||||
</Text>
|
||||
</View>
|
||||
) : isLoadingData || isLoadingImage ? (
|
||||
<Loader size="xl" fill="white" />
|
||||
) : (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(150)}
|
||||
style={[a.w_full, a.h_full]}>
|
||||
<Image
|
||||
accessibilityIgnoresInvertColors
|
||||
source={{uri}}
|
||||
style={[a.w_full, a.h_full]}
|
||||
/>
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
</Frame>
|
||||
|
||||
{canvas}
|
||||
|
||||
<View style={[gtMobile ? a.p_2xl : a.p_xl]}>
|
||||
<Text
|
||||
allowFontScaling={false}
|
||||
style={[a.text_5xl, a.leading_tight, a.pb_lg, a.font_heavy]}>
|
||||
<Trans>Thanks for being one of our first 10 million users.</Trans>
|
||||
</Text>
|
||||
|
||||
<Text style={[a.leading_snug, a.text_lg, a.pb_xl]}>
|
||||
<Trans>
|
||||
Together, we're rebuilding the social internet. We're glad
|
||||
you're here.
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<Divider />
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_end,
|
||||
a.gap_md,
|
||||
a.pt_xl,
|
||||
]}>
|
||||
{gtMobile && (
|
||||
<Text
|
||||
style={[a.text_md, a.italic, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Brag a little!</Trans>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Button
|
||||
disabled={isLoadingImage}
|
||||
label={
|
||||
isNative && isIOS
|
||||
? _(msg`Share image externally`)
|
||||
: _(msg`Download image`)
|
||||
}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
shape="square"
|
||||
onPress={
|
||||
isNative
|
||||
? isIOS
|
||||
? onNativeShare
|
||||
: onNativeDownload
|
||||
: onWebDownload
|
||||
}>
|
||||
<ButtonIcon icon={isNative && isIOS ? Share : Download} />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isLoadingImage}
|
||||
label={_(msg`Share image in post`)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onPress={sharePost}>
|
||||
<ButtonText>{_(msg`Share`)}</ButtonText>
|
||||
<ButtonIcon position="right" icon={ImageIcon} />
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
@@ -16,10 +16,11 @@ import {
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {SessionAccount, useSession} from '#/state/session'
|
||||
import {useOnboardingState} from '#/state/shell'
|
||||
/*
|
||||
* NUXs
|
||||
*/
|
||||
import {NeueTypography} from '#/components/dialogs/nuxs/NeueTypography'
|
||||
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
|
||||
// NUXs
|
||||
import {TenMillion} from '#/components/dialogs/nuxs/TenMillion'
|
||||
import {IS_DEV} from '#/env'
|
||||
|
||||
type Context = {
|
||||
@@ -36,14 +37,11 @@ const queuedNuxs: {
|
||||
preferences: UsePreferencesQueryResponse
|
||||
}) => boolean
|
||||
}[] = [
|
||||
{
|
||||
id: Nux.TenMillionDialog,
|
||||
},
|
||||
{
|
||||
id: Nux.NeueTypography,
|
||||
enabled(props) {
|
||||
if (props.currentProfile.createdAt) {
|
||||
if (new Date(props.currentProfile.createdAt) < new Date('2024-09-25')) {
|
||||
if (new Date(props.currentProfile.createdAt) < new Date('2024-10-01')) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -176,7 +174,6 @@ function Inner({
|
||||
|
||||
return (
|
||||
<Context.Provider value={ctx}>
|
||||
{activeNux === Nux.TenMillionDialog && <TenMillion />}
|
||||
{activeNux === Nux.NeueTypography && <NeueTypography />}
|
||||
</Context.Provider>
|
||||
)
|
||||
|
||||
@@ -9,8 +9,8 @@ export function useHeaderOffset() {
|
||||
return 0
|
||||
}
|
||||
const navBarHeight = 42
|
||||
const tabBarPad = 10 + 10 + 3 // padding + border
|
||||
const normalLineHeight = 1.2
|
||||
const tabBarText = 16 * normalLineHeight * fontScale
|
||||
const tabBarPad = 10 + 10 + 6 // padding + arbitrary
|
||||
const normalLineHeight = 20 // matches tab bar
|
||||
const tabBarText = normalLineHeight * fontScale
|
||||
return navBarHeight + tabBarPad + tabBarText
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react'
|
||||
import * as Linking from 'expo-linking'
|
||||
|
||||
import {logEvent} from 'lib/statsig/statsig'
|
||||
import {isNative} from 'platform/detection'
|
||||
import {useSession} from 'state/session'
|
||||
import {useComposerControls} from 'state/shell'
|
||||
import {useCloseAllActiveElements} from 'state/util'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useComposerControls} from '#/state/shell'
|
||||
import {useCloseAllActiveElements} from '#/state/util'
|
||||
import {useIntentDialogs} from '#/components/intents/IntentDialogs'
|
||||
import {Referrer} from '../../../modules/expo-bluesky-swiss-army'
|
||||
|
||||
@@ -52,6 +52,7 @@ export function useIntentHandler() {
|
||||
composeIntent({
|
||||
text: params.get('text'),
|
||||
imageUrisStr: params.get('imageUris'),
|
||||
videoUri: params.get('videoUri'),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -80,14 +81,25 @@ export function useComposeIntent() {
|
||||
({
|
||||
text,
|
||||
imageUrisStr,
|
||||
videoUri,
|
||||
}: {
|
||||
text: string | null
|
||||
imageUrisStr: string | null // unused for right now, will be used later with intents
|
||||
imageUrisStr: string | null
|
||||
videoUri: string | null
|
||||
}) => {
|
||||
if (!hasSession) return
|
||||
|
||||
closeAllActiveElements()
|
||||
|
||||
// Whenever a video URI is present, we don't support adding images right now.
|
||||
if (videoUri) {
|
||||
openComposer({
|
||||
text: text ?? undefined,
|
||||
videoUri,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const imageUris = imageUrisStr
|
||||
?.split(',')
|
||||
.filter(part => {
|
||||
|
||||
+1
-1
@@ -1,10 +1,10 @@
|
||||
import format from 'date-fns/format'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {DebugContext} from '#/logger/debugContext'
|
||||
import {add} from '#/logger/logDump'
|
||||
import {Sentry} from '#/logger/sentry'
|
||||
import {isNetworkError} from 'lib/strings/errors'
|
||||
import * as env from '#/env'
|
||||
|
||||
export enum LogLevel {
|
||||
|
||||
@@ -3,23 +3,16 @@ import zod from 'zod'
|
||||
import {BaseNux} from '#/state/queries/nuxs/types'
|
||||
|
||||
export enum Nux {
|
||||
TenMillionDialog = 'TenMillionDialog',
|
||||
NeueTypography = 'NeueTypography',
|
||||
}
|
||||
|
||||
export const nuxNames = new Set(Object.values(Nux))
|
||||
|
||||
export type AppNux =
|
||||
| BaseNux<{
|
||||
id: Nux.TenMillionDialog
|
||||
data: undefined
|
||||
}>
|
||||
| BaseNux<{
|
||||
id: Nux.NeueTypography
|
||||
data: undefined
|
||||
}>
|
||||
export type AppNux = BaseNux<{
|
||||
id: Nux.NeueTypography
|
||||
data: undefined
|
||||
}>
|
||||
|
||||
export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
|
||||
[Nux.TenMillionDialog]: undefined,
|
||||
[Nux.NeueTypography]: undefined,
|
||||
}
|
||||
|
||||
@@ -7,17 +7,17 @@ import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {AbortError} from '#/lib/async/cancelable'
|
||||
import {SUPPORTED_MIME_TYPES, SupportedMimeTypes} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {
|
||||
ServerError,
|
||||
UploadLimitError,
|
||||
VideoTooLargeError,
|
||||
} from 'lib/media/video/errors'
|
||||
import {CompressedVideo} from 'lib/media/video/types'
|
||||
import {useCompressVideoMutation} from 'state/queries/video/compress-video'
|
||||
import {useVideoAgent} from 'state/queries/video/util'
|
||||
import {useUploadVideoMutation} from 'state/queries/video/video-upload'
|
||||
} from '#/lib/media/video/errors'
|
||||
import {CompressedVideo} from '#/lib/media/video/types'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useCompressVideoMutation} from '#/state/queries/video/compress-video'
|
||||
import {useVideoAgent} from '#/state/queries/video/util'
|
||||
import {useUploadVideoMutation} from '#/state/queries/video/video-upload'
|
||||
|
||||
type Status = 'idle' | 'compressing' | 'processing' | 'uploading' | 'done'
|
||||
|
||||
@@ -101,9 +101,11 @@ function reducer(queryClient: QueryClient) {
|
||||
|
||||
export function useUploadVideo({
|
||||
setStatus,
|
||||
initialVideoUri,
|
||||
}: {
|
||||
setStatus: (status: string) => void
|
||||
onSuccess: () => void
|
||||
initialVideoUri?: string
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -237,21 +239,24 @@ export function useUploadVideo({
|
||||
signal: state.abortController.signal,
|
||||
})
|
||||
|
||||
const selectVideo = (asset: ImagePickerAsset) => {
|
||||
// compression step on native converts to mp4, so no need to check there
|
||||
if (isWeb) {
|
||||
const mimeType = getMimeType(asset)
|
||||
if (!SUPPORTED_MIME_TYPES.includes(mimeType as SupportedMimeTypes)) {
|
||||
throw new Error(_(msg`Unsupported video type: ${mimeType}`))
|
||||
const selectVideo = React.useCallback(
|
||||
(asset: ImagePickerAsset) => {
|
||||
// compression step on native converts to mp4, so no need to check there
|
||||
if (isWeb) {
|
||||
const mimeType = getMimeType(asset)
|
||||
if (!SUPPORTED_MIME_TYPES.includes(mimeType as SupportedMimeTypes)) {
|
||||
throw new Error(_(msg`Unsupported video type: ${mimeType}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'SetAsset',
|
||||
asset,
|
||||
})
|
||||
onSelectVideo(asset)
|
||||
}
|
||||
dispatch({
|
||||
type: 'SetAsset',
|
||||
asset,
|
||||
})
|
||||
onSelectVideo(asset)
|
||||
},
|
||||
[_, onSelectVideo],
|
||||
)
|
||||
|
||||
const clearVideo = () => {
|
||||
dispatch({type: 'Reset'})
|
||||
@@ -265,6 +270,13 @@ export function useUploadVideo({
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Whenever we receive an initial video uri, we should immediately run compression if necessary
|
||||
useEffect(() => {
|
||||
if (initialVideoUri) {
|
||||
selectVideo({uri: initialVideoUri} as ImagePickerAsset)
|
||||
}
|
||||
}, [initialVideoUri, selectVideo])
|
||||
|
||||
return {
|
||||
state,
|
||||
dispatch,
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface ComposerOpts {
|
||||
openEmojiPicker?: (pos: DOMRect | undefined) => void
|
||||
text?: string
|
||||
imageUris?: {uri: string; width: number; height: number; altText?: string}[]
|
||||
videoUri?: string
|
||||
}
|
||||
|
||||
type StateContext = ComposerOpts | undefined
|
||||
|
||||
+12
-12
@@ -9,7 +9,7 @@
|
||||
@font-face {
|
||||
font-family: 'Inter-Regular';
|
||||
src: local('Inter-Regular'),
|
||||
url(/assets/fonts/inter/Inter-Regular.otf) format('font/otf');
|
||||
url(/assets/fonts/inter/Inter-Regular.otf) format('opentype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -17,7 +17,7 @@
|
||||
@font-face {
|
||||
font-family: 'Inter-Italic';
|
||||
src: local('Inter-Italic'),
|
||||
url(/assets/fonts/inter/Inter-Italic.otf) format('font/otf');
|
||||
url(/assets/fonts/inter/Inter-Italic.otf) format('opentype');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
@@ -25,14 +25,14 @@
|
||||
/*
|
||||
@font-face {
|
||||
font-family: "Inter-Medium";
|
||||
src: local("Inter-Medium"), url(/assets/fonts/inter/Inter-Medium.otf) format("font/otf");
|
||||
src: local("Inter-Medium"), url(/assets/fonts/inter/Inter-Medium.otf) format("opentype");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Inter-MediumItalic";
|
||||
src: local("Inter-MediumItalic"), url(/assets/fonts/inter/Inter-MediumItalic.otf) format("font/otf");
|
||||
src: local("Inter-MediumItalic"), url(/assets/fonts/inter/Inter-MediumItalic.otf) format("opentype");
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
@@ -41,7 +41,7 @@
|
||||
@font-face {
|
||||
font-family: 'Inter-SemiBold';
|
||||
src: local('Inter-SemiBold'),
|
||||
url(/assets/fonts/inter/Inter-SemiBold.otf) format('font/otf');
|
||||
url(/assets/fonts/inter/Inter-SemiBold.otf) format('opentype');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -49,7 +49,7 @@
|
||||
@font-face {
|
||||
font-family: 'Inter-SemiBoldItalic';
|
||||
src: local('Inter-SemiBoldItalic'),
|
||||
url(/assets/fonts/inter/Inter-SemiBoldItalic.otf) format('font/otf');
|
||||
url(/assets/fonts/inter/Inter-SemiBoldItalic.otf) format('opentype');
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
@@ -57,14 +57,14 @@
|
||||
/*
|
||||
@font-face {
|
||||
font-family: "Inter-Bold";
|
||||
src: local("Inter-Bold"), url(/assets/fonts/inter/Inter-Bold.otf) format("font/otf");
|
||||
src: local("Inter-Bold"), url(/assets/fonts/inter/Inter-Bold.otf) format("opentype");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Inter-BoldItalic";
|
||||
src: local("Inter-BoldItalic"), url(/assets/fonts/inter/Inter-BoldItalic.otf) format("font/otf");
|
||||
src: local("Inter-BoldItalic"), url(/assets/fonts/inter/Inter-BoldItalic.otf) format("opentype");
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
@@ -73,7 +73,7 @@
|
||||
@font-face {
|
||||
font-family: 'Inter-ExtraBold';
|
||||
src: local('Inter-ExtraBold'),
|
||||
url(/assets/fonts/inter/Inter-ExtraBold.otf) format('font/otf');
|
||||
url(/assets/fonts/inter/Inter-ExtraBold.otf) format('opentype');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -81,7 +81,7 @@
|
||||
@font-face {
|
||||
font-family: 'Inter-ExtraBoldItalic';
|
||||
src: local('Inter-ExtraBoldItalic'),
|
||||
url(/assets/fonts/inter/Inter-ExtraBoldItalic.otf) format('font/otf');
|
||||
url(/assets/fonts/inter/Inter-ExtraBoldItalic.otf) format('opentype');
|
||||
font-weight: 800;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
@@ -89,14 +89,14 @@
|
||||
/*
|
||||
@font-face {
|
||||
font-family: "Inter-Black";
|
||||
src: local("Inter-Black"), url(/assets/fonts/inter/Inter-Black.otf) format("font/otf");
|
||||
src: local("Inter-Black"), url(/assets/fonts/inter/Inter-Black.otf) format("opentype");
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Inter-BlackItalic";
|
||||
src: local("Inter-BlackItalic"), url(/assets/fonts/inter/Inter-BlackItalic.otf) format("font/otf");
|
||||
src: local("Inter-BlackItalic"), url(/assets/fonts/inter/Inter-BlackItalic.otf) format("opentype");
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
|
||||
@@ -39,21 +39,21 @@ export const SplashScreen = ({
|
||||
<Trans>What's up?</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<View testID="signinOrCreateAccount">
|
||||
<View
|
||||
testID="signinOrCreateAccount"
|
||||
style={[a.px_xl, a.gap_md, a.pb_2xl]}>
|
||||
<Button
|
||||
testID="createAccountButton"
|
||||
onPress={onPressCreateAccount}
|
||||
accessibilityRole="button"
|
||||
label={_(msg`Create new account`)}
|
||||
accessibilityHint={_(
|
||||
msg`Opens flow to create a new Bluesky account`,
|
||||
)}
|
||||
style={[a.mx_xl, a.mb_xl]}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="primary">
|
||||
<ButtonText>
|
||||
<Trans>Create a new account</Trans>
|
||||
<Trans>Create account</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
@@ -63,7 +63,6 @@ export const SplashScreen = ({
|
||||
accessibilityHint={_(
|
||||
msg`Opens flow to sign into your existing Bluesky account`,
|
||||
)}
|
||||
style={[a.mx_xl, a.mb_xl]}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="secondary">
|
||||
|
||||
@@ -85,21 +85,19 @@ export const SplashScreen = ({
|
||||
|
||||
<View
|
||||
testID="signinOrCreateAccount"
|
||||
style={[a.w_full, {maxWidth: 320}]}>
|
||||
style={[a.w_full, a.px_xl, a.gap_md, a.pb_2xl, {maxWidth: 320}]}>
|
||||
<Button
|
||||
testID="createAccountButton"
|
||||
onPress={onPressCreateAccount}
|
||||
accessibilityRole="button"
|
||||
label={_(msg`Create new account`)}
|
||||
accessibilityHint={_(
|
||||
msg`Opens flow to create a new Bluesky account`,
|
||||
)}
|
||||
style={[a.mx_xl, a.mb_xl]}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="primary">
|
||||
<ButtonText>
|
||||
<Trans>Create a new account</Trans>
|
||||
<Trans>Create account</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
@@ -109,7 +107,6 @@ export const SplashScreen = ({
|
||||
accessibilityHint={_(
|
||||
msg`Opens flow to sign into your existing Bluesky account`,
|
||||
)}
|
||||
style={[a.mx_xl, a.mb_xl]}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="secondary">
|
||||
|
||||
@@ -137,6 +137,7 @@ export const ComposePost = ({
|
||||
openEmojiPicker,
|
||||
text: initText,
|
||||
imageUris: initImageUris,
|
||||
videoUri: initVideoUri,
|
||||
cancelRef,
|
||||
}: Props & {
|
||||
cancelRef?: React.RefObject<CancelRef>
|
||||
@@ -199,6 +200,7 @@ export const ComposePost = ({
|
||||
onPressPublish(true)
|
||||
}
|
||||
},
|
||||
initialVideoUri: initVideoUri,
|
||||
})
|
||||
const hasVideo = Boolean(videoUploadState.asset || videoUploadState.video)
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ import {
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {ComposerOptsPostRef} from 'state/shell/composer'
|
||||
import {QuoteEmbed} from 'view/com/util/post-embeds/QuoteEmbed'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {ComposerOptsPostRef} from '#/state/shell/composer'
|
||||
import {QuoteEmbed} from '#/view/com/util/post-embeds/QuoteEmbed'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
|
||||
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
|
||||
@@ -91,7 +91,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
|
||||
type={replyTo.author.associated?.labeler ? 'labeler' : 'user'}
|
||||
/>
|
||||
<View style={styles.replyToPost}>
|
||||
<Text type="xl-medium" style={t.atoms.text} numberOfLines={1}>
|
||||
<Text type="xl-medium" style={t.atoms.text} numberOfLines={1} emoji>
|
||||
{sanitizeDisplayName(
|
||||
replyTo.author.displayName || sanitizeHandle(replyTo.author.handle),
|
||||
)}
|
||||
@@ -101,7 +101,8 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
|
||||
<Text
|
||||
type="post-text"
|
||||
style={t.atoms.text}
|
||||
numberOfLines={!showFull ? 6 : undefined}>
|
||||
numberOfLines={!showFull ? 6 : undefined}
|
||||
emoji>
|
||||
{replyTo.text}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useReducedMotion,
|
||||
useSharedValue,
|
||||
withDelay,
|
||||
withRepeat,
|
||||
withSequence,
|
||||
withSpring,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {useKawaiiMode} from '#/state/preferences/kawaii'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||
import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
// import {Logo} from '#/view/icons/Logo'
|
||||
import {Logo} from '#/view/icons/Logo'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Icon, Trigger} from '#/components/dialogs/nuxs/TenMillion/Trigger'
|
||||
import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
|
||||
import {Link} from '#/components/Link'
|
||||
import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
|
||||
@@ -48,43 +39,7 @@ function HomeHeaderLayoutDesktopAndTablet({
|
||||
const {headerHeight} = useShellLayout()
|
||||
const {hasSession} = useSession()
|
||||
const {_} = useLingui()
|
||||
|
||||
// TEMPORARY - REMOVE AFTER MILLY
|
||||
// This will just cause the icon to shake a bit when the user first opens the app, drawing attention to the celebration
|
||||
// 🎉
|
||||
const rotate = useSharedValue(0)
|
||||
const reducedMotion = useReducedMotion()
|
||||
|
||||
// Run this a single time on app mount.
|
||||
React.useEffect(() => {
|
||||
if (reducedMotion) return
|
||||
|
||||
// Waits 1500ms, then rotates 10 degrees with a spring animation. Repeats once.
|
||||
rotate.value = withDelay(
|
||||
1000,
|
||||
withRepeat(
|
||||
withSequence(
|
||||
withTiming(10, {duration: 100}),
|
||||
withSpring(0, {
|
||||
mass: 1,
|
||||
damping: 1,
|
||||
stiffness: 200,
|
||||
overshootClamping: false,
|
||||
}),
|
||||
),
|
||||
2,
|
||||
false,
|
||||
),
|
||||
)
|
||||
}, [rotate, reducedMotion])
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{
|
||||
rotateZ: `${rotate.value}deg`,
|
||||
},
|
||||
],
|
||||
}))
|
||||
const kawaii = useKawaiiMode()
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -101,30 +56,21 @@ function HomeHeaderLayoutDesktopAndTablet({
|
||||
t.atoms.bg,
|
||||
t.atoms.border_contrast_low,
|
||||
styles.bar,
|
||||
kawaii && {paddingTop: 22, paddingBottom: 16},
|
||||
]}>
|
||||
<Animated.View
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.pt_lg,
|
||||
a.m_auto,
|
||||
kawaii && {paddingTop: 4, paddingBottom: 0},
|
||||
{
|
||||
width: 28,
|
||||
width: kawaii ? 84 : 28,
|
||||
},
|
||||
animatedStyle,
|
||||
]}>
|
||||
<Trigger>
|
||||
{ctx => (
|
||||
<Icon
|
||||
width={28}
|
||||
style={{
|
||||
opacity: ctx.hovered || ctx.pressed ? 0.8 : 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Trigger>
|
||||
{/* <Logo width={28} /> */}
|
||||
</Animated.View>
|
||||
<Logo width={kawaii ? 60 : 28} />
|
||||
</View>
|
||||
|
||||
<Link
|
||||
to="/feeds"
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useReducedMotion,
|
||||
useSharedValue,
|
||||
withDelay,
|
||||
withRepeat,
|
||||
withSequence,
|
||||
withSpring,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useSetDrawerOpen} from '#/state/shell/drawer-open'
|
||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isWeb} from 'platform/detection'
|
||||
// import {Logo} from '#/view/icons/Logo'
|
||||
import {Logo} from '#/view/icons/Logo'
|
||||
import {atoms} from '#/alf'
|
||||
import {useTheme} from '#/alf'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Icon, Trigger} from '#/components/dialogs/nuxs/TenMillion/Trigger'
|
||||
import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette'
|
||||
import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
|
||||
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
||||
@@ -49,43 +39,6 @@ export function HomeHeaderLayoutMobile({
|
||||
setDrawerOpen(true)
|
||||
}, [setDrawerOpen])
|
||||
|
||||
// TEMPORARY - REMOVE AFTER MILLY
|
||||
// This will just cause the icon to shake a bit when the user first opens the app, drawing attention to the celebration
|
||||
// 🎉
|
||||
const rotate = useSharedValue(0)
|
||||
const reducedMotion = useReducedMotion()
|
||||
|
||||
// Run this a single time on app mount.
|
||||
React.useEffect(() => {
|
||||
if (reducedMotion) return
|
||||
|
||||
// Waits 1500ms, then rotates 10 degrees with a spring animation. Repeats once.
|
||||
rotate.value = withDelay(
|
||||
1000,
|
||||
withRepeat(
|
||||
withSequence(
|
||||
withTiming(10, {duration: 100}),
|
||||
withSpring(0, {
|
||||
mass: 1,
|
||||
damping: 1,
|
||||
stiffness: 200,
|
||||
overshootClamping: false,
|
||||
}),
|
||||
),
|
||||
2,
|
||||
false,
|
||||
),
|
||||
)
|
||||
}, [rotate, reducedMotion])
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{
|
||||
rotateZ: `${rotate.value}deg`,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]}
|
||||
@@ -106,19 +59,9 @@ export function HomeHeaderLayoutMobile({
|
||||
<Menu size="lg" fill={t.atoms.text_contrast_medium.color} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Animated.View style={animatedStyle}>
|
||||
<Trigger>
|
||||
{ctx => (
|
||||
<Icon
|
||||
width={28}
|
||||
style={{
|
||||
opacity: ctx.pressed ? 0.8 : 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Trigger>
|
||||
{/* <Logo width={30} /> */}
|
||||
</Animated.View>
|
||||
<View>
|
||||
<Logo width={30} />
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
atoms.flex_row,
|
||||
|
||||
@@ -27,11 +27,11 @@ import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useProfileUpdateMutation} from '#/state/queries/profile'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {EditableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {UserBanner} from '#/view/com/util/UserBanner'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {Text} from '../util/text/Text'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {EditableUserAvatar} from '../util/UserAvatar'
|
||||
import {UserBanner} from '../util/UserBanner'
|
||||
|
||||
const AnimatedTouchableOpacity =
|
||||
Animated.createAnimatedComponent(TouchableOpacity)
|
||||
|
||||
@@ -11,6 +11,7 @@ import {NON_BREAKING_SPACE} from '#/lib/strings/constants'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {niceDate} from '#/lib/strings/time'
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {precacheProfile} from '#/state/queries/profile'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {WebOnlyInlineLinkText} from '#/components/Link'
|
||||
@@ -70,14 +71,14 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
||||
</View>
|
||||
)}
|
||||
<ProfileHoverCard inline did={opts.author.did}>
|
||||
<Text numberOfLines={1} style={[a.flex_shrink]}>
|
||||
<Text numberOfLines={1} style={[isAndroid ? a.flex_1 : a.flex_shrink]}>
|
||||
<WebOnlyInlineLinkText
|
||||
to={profileLink}
|
||||
label={_(msg`View profile`)}
|
||||
disableMismatchWarning
|
||||
onPress={onBeforePressAuthor}
|
||||
style={[t.atoms.text]}>
|
||||
<Text emoji style={[a.text_md, a.font_bold, a.leading_tight]}>
|
||||
<Text emoji style={[a.text_md, a.font_bold, a.leading_snug]}>
|
||||
{forceLTR(
|
||||
sanitizeDisplayName(
|
||||
displayName,
|
||||
@@ -92,25 +93,23 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
||||
disableMismatchWarning
|
||||
disableUnderline
|
||||
onPress={onBeforePressAuthor}
|
||||
style={[a.text_md, t.atoms.text_contrast_medium, a.leading_tight]}>
|
||||
style={[a.text_md, t.atoms.text_contrast_medium, a.leading_snug]}>
|
||||
<Text
|
||||
emoji
|
||||
style={[
|
||||
a.text_md,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.leading_tight,
|
||||
]}>
|
||||
style={[a.text_md, t.atoms.text_contrast_medium, a.leading_snug]}>
|
||||
{NON_BREAKING_SPACE + sanitizeHandle(handle, '@')}
|
||||
</Text>
|
||||
</WebOnlyInlineLinkText>
|
||||
</Text>
|
||||
</ProfileHoverCard>
|
||||
|
||||
<Text
|
||||
style={[a.text_md, t.atoms.text_contrast_medium]}
|
||||
accessible={false}>
|
||||
·
|
||||
</Text>
|
||||
{!isAndroid && (
|
||||
<Text
|
||||
style={[a.text_md, t.atoms.text_contrast_medium]}
|
||||
accessible={false}>
|
||||
·
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<TimeElapsed timestamp={opts.timestamp}>
|
||||
{({timeElapsed}) => (
|
||||
@@ -124,7 +123,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
||||
style={[
|
||||
a.text_md,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.leading_tight,
|
||||
a.leading_snug,
|
||||
web({
|
||||
whiteSpace: 'nowrap',
|
||||
}),
|
||||
|
||||
@@ -202,7 +202,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
bannerImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
height: 150,
|
||||
},
|
||||
defaultBanner: {
|
||||
backgroundColor: '#0070ff',
|
||||
|
||||
@@ -985,7 +985,7 @@ let SearchInputBox = ({
|
||||
const t = useThemeNew()
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1, a.relative]}>
|
||||
<View style={[a.flex_1, a.mb_sm]}>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={MagnifyingGlass} />
|
||||
<TextField.Input
|
||||
|
||||
@@ -34,6 +34,7 @@ export function Composer({}: {winHeight: number}) {
|
||||
mention={state?.mention}
|
||||
text={state?.text}
|
||||
imageUris={state?.imageUris}
|
||||
videoUri={state?.videoUri}
|
||||
/>
|
||||
</Providers>
|
||||
</View>
|
||||
|
||||
@@ -54,6 +54,7 @@ export function Composer({winHeight}: {winHeight: number}) {
|
||||
mention={state.mention}
|
||||
text={state.text}
|
||||
imageUris={state.imageUris}
|
||||
videoUri={state.videoUri}
|
||||
/>
|
||||
</Animated.View>
|
||||
)
|
||||
|
||||
@@ -355,8 +355,7 @@ function Btn({
|
||||
<PressableScale
|
||||
testID={testID}
|
||||
style={styles.ctrl}
|
||||
onPress={onLongPress ? onPress : undefined}
|
||||
onPressIn={onLongPress ? undefined : onPress}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
accessible={accessible}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
|
||||
@@ -18,7 +18,6 @@ import {getCurrentRoute, isStateAtTabRoot, isTab} from '#/lib/routes/helpers'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
||||
import {isInvalidHandle} from '#/lib/strings/handles'
|
||||
import {colors, s} from '#/lib/styles'
|
||||
import {emitSoftReset} from '#/state/events'
|
||||
import {useFetchHandle} from '#/state/queries/handle'
|
||||
import {useUnreadMessageCount} from '#/state/queries/messages/list-converations'
|
||||
@@ -29,9 +28,10 @@ import {useComposerControls} from '#/state/shell/composer'
|
||||
import {Link} from '#/view/com/util/Link'
|
||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {PressableWithHover} from '#/view/com/util/PressableWithHover'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {NavSignupCard} from '#/view/shell/NavSignupCard'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {
|
||||
Bell_Filled_Corner0_Rounded as BellFilled,
|
||||
Bell_Stroke2_Corner0_Rounded as Bell,
|
||||
@@ -63,6 +63,7 @@ import {
|
||||
UserCircle_Filled_Corner0_Rounded as UserCircleFilled,
|
||||
UserCircle_Stroke2_Corner0_Rounded as UserCircle,
|
||||
} from '#/components/icons/UserCircle'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {router} from '../../../routes'
|
||||
|
||||
const NAV_ICON_WIDTH = 28
|
||||
@@ -149,9 +150,10 @@ interface NavItemProps {
|
||||
label: string
|
||||
}
|
||||
function NavItem({count, href, icon, iconFilled, label}: NavItemProps) {
|
||||
const pal = usePalette('default')
|
||||
const t = useTheme()
|
||||
const {currentAccount} = useSession()
|
||||
const {isDesktop, isTablet} = useWebMediaQueries()
|
||||
const {gtMobile, gtTablet} = useBreakpoints()
|
||||
const isTablet = gtMobile && !gtTablet
|
||||
const [pathName] = React.useMemo(() => router.matchPath(href), [href])
|
||||
const currentRouteInfo = useNavigationState(state => {
|
||||
if (!state) {
|
||||
@@ -183,8 +185,8 @@ function NavItem({count, href, icon, iconFilled, label}: NavItemProps) {
|
||||
|
||||
return (
|
||||
<PressableWithHover
|
||||
style={styles.navItemWrapper}
|
||||
hoverStyle={pal.viewLight}
|
||||
style={[a.flex_row, a.align_center, a.p_md, a.rounded_sm, a.gap_sm]}
|
||||
hoverStyle={t.atoms.bg_contrast_25}
|
||||
// @ts-ignore the function signature differs on web -prf
|
||||
onPress={onPressWrapped}
|
||||
// @ts-ignore web only -prf
|
||||
@@ -195,23 +197,50 @@ function NavItem({count, href, icon, iconFilled, label}: NavItemProps) {
|
||||
accessibilityHint="">
|
||||
<View
|
||||
style={[
|
||||
styles.navItemIconWrapper,
|
||||
isTablet && styles.navItemIconWrapperTablet,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.z_10,
|
||||
{
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
isTablet && {
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
]}>
|
||||
{isCurrent ? iconFilled : icon}
|
||||
{typeof count === 'string' && count ? (
|
||||
<Text
|
||||
type="button"
|
||||
style={[
|
||||
styles.navItemCount,
|
||||
isTablet && styles.navItemCountTablet,
|
||||
a.absolute,
|
||||
a.text_xs,
|
||||
a.font_bold,
|
||||
a.rounded_full,
|
||||
a.text_center,
|
||||
{
|
||||
top: '-10%',
|
||||
left: count.length === 1 ? '50%' : '40%',
|
||||
backgroundColor: t.palette.primary_500,
|
||||
color: t.palette.white,
|
||||
lineHeight: a.text_sm.fontSize,
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 1,
|
||||
minWidth: 16,
|
||||
},
|
||||
isTablet && [
|
||||
{
|
||||
top: '10%',
|
||||
left: count.length === 1 ? '50%' : '40%',
|
||||
},
|
||||
],
|
||||
]}>
|
||||
{count}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
{isDesktop && (
|
||||
<Text type="title" style={[isCurrent ? s.bold : s.normal, pal.text]}>
|
||||
{gtTablet && (
|
||||
<Text style={[a.text_xl, isCurrent ? a.font_heavy : a.font_normal]}>
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
@@ -268,21 +297,20 @@ function ComposeBtn() {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<View style={styles.newPostBtnContainer}>
|
||||
<TouchableOpacity
|
||||
<View style={[a.flex_row, a.pl_md, a.pt_xl]}>
|
||||
<Button
|
||||
disabled={isFetchingHandle}
|
||||
style={styles.newPostBtn}
|
||||
label={_(msg`New post`)}
|
||||
onPress={onPressCompose}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint="">
|
||||
<View style={styles.newPostBtnIconWrapper}>
|
||||
<EditBig width={19} style={styles.newPostBtnLabel} />
|
||||
</View>
|
||||
<Text type="button" style={styles.newPostBtnLabel}>
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
style={[a.rounded_full]}>
|
||||
<ButtonIcon icon={EditBig} position="left" />
|
||||
<ButtonText>
|
||||
<Trans context="action">New Post</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -440,67 +468,4 @@ const styles = StyleSheet.create({
|
||||
width: 30,
|
||||
height: 30,
|
||||
},
|
||||
|
||||
navItemWrapper: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 12,
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
gap: 10,
|
||||
},
|
||||
navItemIconWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 28,
|
||||
height: 24,
|
||||
marginTop: 2,
|
||||
zIndex: 1,
|
||||
},
|
||||
navItemIconWrapperTablet: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
navItemCount: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 15,
|
||||
backgroundColor: colors.blue3,
|
||||
color: colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
paddingHorizontal: 4,
|
||||
borderRadius: 6,
|
||||
},
|
||||
navItemCountTablet: {
|
||||
left: 18,
|
||||
fontSize: 14,
|
||||
},
|
||||
|
||||
newPostBtnContainer: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
newPostBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 24,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 12, // visually aligns the text vertically inside the button
|
||||
paddingLeft: 16,
|
||||
paddingRight: 18, // looks nicer like this
|
||||
backgroundColor: colors.blue3,
|
||||
marginLeft: 12,
|
||||
marginTop: 20,
|
||||
marginBottom: 10,
|
||||
gap: 8,
|
||||
},
|
||||
newPostBtnIconWrapper: {
|
||||
marginTop: 2, // aligns the icon visually with the text
|
||||
},
|
||||
newPostBtnLabel: {
|
||||
color: colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user