Merge branch 'main' into ja-translation-14

This commit is contained in:
Takayuki KUSANO
2024-07-26 10:04:00 +09:00
67 changed files with 845 additions and 376 deletions
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M9.576 2.534C7.578 1.299 5 2.737 5 5.086v13.828c0 2.35 2.578 3.787 4.576 2.552l11.194-6.914c1.899-1.172 1.899-3.932 0-5.104L9.576 2.534Z"/></svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M5 5.086C5 2.736 7.578 1.3 9.576 2.534L20.77 9.448c1.899 1.172 1.899 3.932 0 5.104L9.576 21.466C7.578 22.701 5 21.263 5 18.914V5.086Zm3.525-.85A1 1 0 0 0 7 5.085v13.828a1 1 0 0 0 1.525.85l11.194-6.913a1 1 0 0 0 0-1.702L8.525 4.235Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 374 B

+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "bsky.app", "name": "bsky.app",
"version": "1.89.0", "version": "1.90.0",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@@ -143,6 +143,7 @@
"expo-web-browser": "~13.0.3", "expo-web-browser": "~13.0.3",
"fast-text-encoding": "^1.0.6", "fast-text-encoding": "^1.0.6",
"history": "^5.3.0", "history": "^5.3.0",
"hls.js": "^1.5.11",
"js-sha256": "^0.9.0", "js-sha256": "^0.9.0",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lande": "^1.0.10", "lande": "^1.0.10",
@@ -269,7 +270,7 @@
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"react-test-renderer": "18.2.0", "react-test-renderer": "18.2.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.3.3", "typescript": "^5.5.4",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"webpack": "^5.75.0", "webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.10.1", "webpack-bundle-analyzer": "^4.10.1",
+20
View File
@@ -0,0 +1,20 @@
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
@@ -11,6 +11,7 @@ internal fun PlayerView.applyRequiresLinearPlayback(requireLinearPlayback: Boole
setShowPreviousButton(!requireLinearPlayback)
setShowNextButton(!requireLinearPlayback)
setTimeBarInteractive(requireLinearPlayback)
+ setShowSubtitleButton(true)
}
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
@@ -27,7 +28,8 @@ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) {
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) {
- val fullscreenButton = findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
+ val fullscreenButton =
+ findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
fullscreenButton?.visibility = if (visible) {
android.view.View.VISIBLE
} else {
+41 -38
View File
@@ -23,10 +23,12 @@ import {
} from '#/lib/statsig/statsig' } from '#/lib/statsig/statsig'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
import {ThemeProvider} from '#/lib/ThemeContext' import {ThemeProvider} from '#/lib/ThemeContext'
import I18nProvider from '#/locale/i18nProvider'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Provider as A11yProvider} from '#/state/a11y' import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as DialogStateProvider} from '#/state/dialogs'
import {listenSessionDropped} from '#/state/events'
import {Provider as InvitesStateProvider} from '#/state/invites' import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox' import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages' import {MessagesProvider} from '#/state/messages'
@@ -49,6 +51,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {TestCtrls} from '#/view/com/testing/TestCtrls' import {TestCtrls} from '#/view/com/testing/TestCtrls'
import {ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoContext'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {Shell} from '#/view/shell' import {Shell} from '#/view/shell'
import {ThemeProvider as Alf} from '#/alf' import {ThemeProvider as Alf} from '#/alf'
@@ -58,8 +61,6 @@ import {Provider as PortalProvider} from '#/components/Portal'
import {Splash} from '#/Splash' import {Splash} from '#/Splash'
import {Provider as TourProvider} from '#/tours' import {Provider as TourProvider} from '#/tours'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import I18nProvider from './locale/i18nProvider'
import {listenSessionDropped} from './state/events'
SplashScreen.preventAutoHideAsync() SplashScreen.preventAutoHideAsync()
@@ -107,42 +108,44 @@ function InnerApp() {
<Alf theme={theme}> <Alf theme={theme}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<Splash isReady={isReady && hasCheckedReferrer}> <Splash isReady={isReady && hasCheckedReferrer}>
<RootSiblingParent> <ActiveVideoProvider>
<React.Fragment <RootSiblingParent>
// Resets the entire tree below when it changes: <React.Fragment
key={currentAccount?.did}> // Resets the entire tree below when it changes:
<QueryProvider currentDid={currentAccount?.did}> key={currentAccount?.did}>
<StatsigProvider> <QueryProvider currentDid={currentAccount?.did}>
<MessagesProvider> <StatsigProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */} <MessagesProvider>
<LabelDefsProvider> {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<ModerationOptsProvider> <LabelDefsProvider>
<LoggedOutViewProvider> <ModerationOptsProvider>
<SelectedFeedProvider> <LoggedOutViewProvider>
<UnreadNotifsProvider> <SelectedFeedProvider>
<BackgroundNotificationPreferencesProvider> <UnreadNotifsProvider>
<MutedThreadsProvider> <BackgroundNotificationPreferencesProvider>
<TourProvider> <MutedThreadsProvider>
<ProgressGuideProvider> <TourProvider>
<GestureHandlerRootView <ProgressGuideProvider>
style={s.h100pct}> <GestureHandlerRootView
<TestCtrls /> style={s.h100pct}>
<Shell /> <TestCtrls />
</GestureHandlerRootView> <Shell />
</ProgressGuideProvider> </GestureHandlerRootView>
</TourProvider> </ProgressGuideProvider>
</MutedThreadsProvider> </TourProvider>
</BackgroundNotificationPreferencesProvider> </MutedThreadsProvider>
</UnreadNotifsProvider> </BackgroundNotificationPreferencesProvider>
</SelectedFeedProvider> </UnreadNotifsProvider>
</LoggedOutViewProvider> </SelectedFeedProvider>
</ModerationOptsProvider> </LoggedOutViewProvider>
</LabelDefsProvider> </ModerationOptsProvider>
</MessagesProvider> </LabelDefsProvider>
</StatsigProvider> </MessagesProvider>
</QueryProvider> </StatsigProvider>
</React.Fragment> </QueryProvider>
</RootSiblingParent> </React.Fragment>
</RootSiblingParent>
</ActiveVideoProvider>
</Splash> </Splash>
</ThemeProvider> </ThemeProvider>
</Alf> </Alf>
+38 -35
View File
@@ -12,10 +12,12 @@ import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
import {QueryProvider} from '#/lib/react-query' import {QueryProvider} from '#/lib/react-query'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig' import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {ThemeProvider} from '#/lib/ThemeContext' import {ThemeProvider} from '#/lib/ThemeContext'
import I18nProvider from '#/locale/i18nProvider'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Provider as A11yProvider} from '#/state/a11y' import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as DialogStateProvider} from '#/state/dialogs'
import {listenSessionDropped} from '#/state/events'
import {Provider as InvitesStateProvider} from '#/state/invites' import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox' import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages' import {MessagesProvider} from '#/state/messages'
@@ -37,6 +39,7 @@ import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoContext'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {ToastContainer} from '#/view/com/util/Toast.web' import {ToastContainer} from '#/view/com/util/Toast.web'
import {Shell} from '#/view/shell/index' import {Shell} from '#/view/shell/index'
@@ -46,8 +49,6 @@ import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as PortalProvider} from '#/components/Portal' import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as TourProvider} from '#/tours' import {Provider as TourProvider} from '#/tours'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import I18nProvider from './locale/i18nProvider'
import {listenSessionDropped} from './state/events'
function InnerApp() { function InnerApp() {
const [isReady, setIsReady] = React.useState(false) const [isReady, setIsReady] = React.useState(false)
@@ -92,39 +93,41 @@ function InnerApp() {
<Alf theme={theme}> <Alf theme={theme}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<RootSiblingParent> <RootSiblingParent>
<React.Fragment <ActiveVideoProvider>
// Resets the entire tree below when it changes: <React.Fragment
key={currentAccount?.did}> // Resets the entire tree below when it changes:
<QueryProvider currentDid={currentAccount?.did}> key={currentAccount?.did}>
<StatsigProvider> <QueryProvider currentDid={currentAccount?.did}>
<MessagesProvider> <StatsigProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */} <MessagesProvider>
<LabelDefsProvider> {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<ModerationOptsProvider> <LabelDefsProvider>
<LoggedOutViewProvider> <ModerationOptsProvider>
<SelectedFeedProvider> <LoggedOutViewProvider>
<UnreadNotifsProvider> <SelectedFeedProvider>
<BackgroundNotificationPreferencesProvider> <UnreadNotifsProvider>
<MutedThreadsProvider> <BackgroundNotificationPreferencesProvider>
<SafeAreaProvider> <MutedThreadsProvider>
<TourProvider> <SafeAreaProvider>
<ProgressGuideProvider> <TourProvider>
<Shell /> <ProgressGuideProvider>
</ProgressGuideProvider> <Shell />
</TourProvider> </ProgressGuideProvider>
</SafeAreaProvider> </TourProvider>
</MutedThreadsProvider> </SafeAreaProvider>
</BackgroundNotificationPreferencesProvider> </MutedThreadsProvider>
</UnreadNotifsProvider> </BackgroundNotificationPreferencesProvider>
</SelectedFeedProvider> </UnreadNotifsProvider>
</LoggedOutViewProvider> </SelectedFeedProvider>
</ModerationOptsProvider> </LoggedOutViewProvider>
</LabelDefsProvider> </ModerationOptsProvider>
</MessagesProvider> </LabelDefsProvider>
</StatsigProvider> </MessagesProvider>
</QueryProvider> </StatsigProvider>
</React.Fragment> </QueryProvider>
<ToastContainer /> </React.Fragment>
<ToastContainer />
</ActiveVideoProvider>
</RootSiblingParent> </RootSiblingParent>
</ThemeProvider> </ThemeProvider>
</Alf> </Alf>
+5 -6
View File
@@ -2,7 +2,6 @@ import {Platform, StyleSheet} from 'react-native'
import * as tokens from '#/alf/tokens' import * as tokens from '#/alf/tokens'
import {native, web} from '#/alf/util/platform' import {native, web} from '#/alf/util/platform'
import hairlineWidth = StyleSheet.hairlineWidth
export const atoms = { export const atoms = {
/* /*
@@ -284,19 +283,19 @@ export const atoms = {
borderWidth: 0, borderWidth: 0,
}, },
border: { border: {
borderWidth: hairlineWidth, borderWidth: StyleSheet.hairlineWidth,
}, },
border_t: { border_t: {
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
}, },
border_b: { border_b: {
borderBottomWidth: hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
}, },
border_l: { border_l: {
borderLeftWidth: hairlineWidth, borderLeftWidth: StyleSheet.hairlineWidth,
}, },
border_r: { border_r: {
borderRightWidth: hairlineWidth, borderRightWidth: StyleSheet.hairlineWidth,
}, },
/* /*
+3
View File
@@ -70,6 +70,9 @@ export type ButtonProps = Pick<
AccessibilityProps & AccessibilityProps &
VariantProps & { VariantProps & {
testID?: string testID?: string
/**
* For a11y, try to make this descriptive and clear
*/
label: string label: string
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
hoverStyle?: StyleProp<ViewStyle> hoverStyle?: StyleProp<ViewStyle>
+7 -3
View File
@@ -40,7 +40,7 @@ type Props = {
export function Default(props: Props) { export function Default(props: Props) {
const {view} = props const {view} = props
return ( return (
<Link label={view.displayName} {...props}> <Link {...props}>
<Outer> <Outer>
<Header> <Header>
<Avatar src={view.avatar} /> <Avatar src={view.avatar} />
@@ -58,7 +58,7 @@ export function Link({
view, view,
children, children,
...props ...props
}: Props & Omit<LinkProps, 'to'>) { }: Props & Omit<LinkProps, 'to' | 'label'>) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const href = React.useMemo(() => { const href = React.useMemo(() => {
@@ -70,7 +70,11 @@ export function Link({
}, [view, queryClient]) }, [view, queryClient])
return ( return (
<InternalLink to={href} style={[a.flex_col]} {...props}> <InternalLink
label={view.displayName}
to={href}
style={[a.flex_col]}
{...props}>
{children} {children}
</InternalLink> </InternalLink>
) )
+9 -3
View File
@@ -190,7 +190,7 @@ export function SuggestedFollows() {
{profiles.slice(0, maxLength).map(profile => ( {profiles.slice(0, maxLength).map(profile => (
<ProfileCard.Link <ProfileCard.Link
key={profile.did} key={profile.did}
did={profile.handle} profile={profile}
onPress={() => { onPress={() => {
logEvent('feed:interstitial:profileCard:press', {}) logEvent('feed:interstitial:profileCard:press', {})
}} }}
@@ -266,7 +266,10 @@ export function SuggestedFollows() {
a.pt_xs, a.pt_xs,
a.gap_md, a.gap_md,
]}> ]}>
<InlineLinkText to="/search" style={[t.atoms.text_contrast_medium]}> <InlineLinkText
label={_(msg`Browse more suggestions`)}
to="/search"
style={[t.atoms.text_contrast_medium]}>
<Trans>Browse more suggestions</Trans> <Trans>Browse more suggestions</Trans>
</InlineLinkText> </InlineLinkText>
<Arrow size="sm" fill={t.atoms.text_contrast_medium.color} /> <Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
@@ -396,7 +399,10 @@ export function SuggestedFeeds() {
a.pt_xs, a.pt_xs,
a.gap_md, a.gap_md,
]}> ]}>
<InlineLinkText to="/search" style={[t.atoms.text_contrast_medium]}> <InlineLinkText
label={_(msg`Browse more suggestions`)}
to="/search"
style={[t.atoms.text_contrast_medium]}>
<Trans>Browse more suggestions</Trans> <Trans>Browse more suggestions</Trans>
</InlineLinkText> </InlineLinkText>
<Arrow size="sm" fill={t.atoms.text_contrast_medium.color} /> <Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
+4 -9
View File
@@ -40,11 +40,6 @@ type BaseLinkProps = Pick<
> & { > & {
testID?: string testID?: string
/**
* Label for a11y. Defaults to the href.
*/
label?: string
/** /**
* The React Navigation `StackAction` to perform when the link is pressed. * The React Navigation `StackAction` to perform when the link is pressed.
*/ */
@@ -197,7 +192,7 @@ export function useLink({
} }
export type LinkProps = Omit<BaseLinkProps, 'disableMismatchWarning'> & export type LinkProps = Omit<BaseLinkProps, 'disableMismatchWarning'> &
Omit<ButtonProps, 'onPress' | 'disabled' | 'label'> Omit<ButtonProps, 'onPress' | 'disabled'>
/** /**
* A interactive element that renders as a `<a>` tag on the web. On mobile it * A interactive element that renders as a `<a>` tag on the web. On mobile it
@@ -224,7 +219,6 @@ export function Link({
return ( return (
<Button <Button
label={href}
{...rest} {...rest}
style={[a.justify_start, flatten(rest.style)]} style={[a.justify_start, flatten(rest.style)]}
role="link" role="link"
@@ -249,7 +243,8 @@ export function Link({
export type InlineLinkProps = React.PropsWithChildren< export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'> BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
> > &
Pick<ButtonProps, 'label'>
export function InlineLinkText({ export function InlineLinkText({
children, children,
@@ -291,7 +286,7 @@ export function InlineLinkText({
<Text <Text
selectable={selectable} selectable={selectable}
accessibilityHint="" accessibilityHint=""
accessibilityLabel={label || href} accessibilityLabel={label}
{...rest} {...rest}
style={[ style={[
{color: t.palette.primary_500}, {color: t.palette.primary_500},
+3 -3
View File
@@ -44,7 +44,7 @@ type Props = {
export function Default(props: Props) { export function Default(props: Props) {
const {view, showPinButton} = props const {view, showPinButton} = props
return ( return (
<Link label={view.name} {...props}> <Link {...props}>
<Outer> <Outer>
<Header> <Header>
<Avatar src={view.avatar} /> <Avatar src={view.avatar} />
@@ -67,7 +67,7 @@ export function Link({
view, view,
children, children,
...props ...props
}: Props & Omit<LinkProps, 'to'>) { }: Props & Omit<LinkProps, 'to' | 'label'>) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const href = React.useMemo(() => { const href = React.useMemo(() => {
@@ -79,7 +79,7 @@ export function Link({
}, [view, queryClient]) }, [view, queryClient])
return ( return (
<InternalLink to={href} {...props}> <InternalLink label={view.name} to={href} {...props}>
{children} {children}
</InternalLink> </InternalLink>
) )
+12 -4
View File
@@ -36,7 +36,7 @@ export function Default({
logContext?: 'ProfileCard' | 'StarterPackProfilesList' logContext?: 'ProfileCard' | 'StarterPackProfilesList'
}) { }) {
return ( return (
<Link did={profile.did}> <Link profile={profile}>
<Card <Card
profile={profile} profile={profile}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
@@ -96,16 +96,24 @@ export function Header({
} }
export function Link({ export function Link({
did, profile,
children, children,
style, style,
...rest ...rest
}: {did: string} & Omit<LinkProps, 'to'>) { }: {
profile: AppBskyActorDefs.ProfileViewDetailed
} & Omit<LinkProps, 'to' | 'label'>) {
const {_} = useLingui()
return ( return (
<InternalLink <InternalLink
label={_(
msg`View ${
profile.displayName || sanitizeHandle(profile.handle)
}'s profile`,
)}
to={{ to={{
screen: 'Profile', screen: 'Profile',
params: {name: did}, params: {name: profile.did},
}} }}
style={[a.flex_col, style]} style={[a.flex_col, style]}
{...rest}> {...rest}>
+3 -1
View File
@@ -74,7 +74,9 @@ function DialogContent({
const onPressAudience = (setting: ThreadgateSetting) => { const onPressAudience = (setting: ThreadgateSetting) => {
// remove nobody // remove nobody
let newSelected = draft.filter(v => v.type !== 'nobody') let newSelected: ThreadgateSetting[] = draft.filter(
v => v.type !== 'nobody',
)
// toggle // toggle
const i = newSelected.findIndex(v => isEqual(v, setting)) const i = newSelected.findIndex(v => isEqual(v, setting))
if (i === -1) { if (i === -1) {
@@ -42,6 +42,7 @@ export function BlockedByListDialog({
<React.Fragment key={block.source.list.uri}> <React.Fragment key={block.source.list.uri}>
{i === 0 ? null : ', '} {i === 0 ? null : ', '}
<InlineLinkText <InlineLinkText
label={block.source.list.name}
to={listUriToHref(block.source.list.uri)} to={listUriToHref(block.source.list.uri)}
style={[a.text_md, a.leading_snug]}> style={[a.text_md, a.leading_snug]}>
{block.source.list.name} {block.source.list.name}
@@ -140,6 +140,7 @@ function HeaderReady({
userBlock?: ModerationCause userBlock?: ModerationCause
} }
}) { }) {
const {_} = useLingui()
const t = useTheme() const t = useTheme()
const convoState = useConvo() const convoState = useConvo()
const profile = useProfileShadow(profileUnshadowed) const profile = useProfileShadow(profileUnshadowed)
@@ -156,6 +157,7 @@ function HeaderReady({
<View style={[a.flex_1]}> <View style={[a.flex_1]}>
<View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}> <View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}>
<Link <Link
label={_(msg`View ${displayName}'s profile`)}
style={[a.flex_row, a.align_start, a.gap_md, a.flex_1, a.pr_md]} style={[a.flex_row, a.align_start, a.gap_md, a.flex_1, a.pr_md]}
to={makeProfileLink(profile)}> to={makeProfileLink(profile)}>
<View style={[a.pt_2xs]}> <View style={[a.pt_2xs]}>
+9
View File
@@ -0,0 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Play_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M5 5.086C5 2.736 7.578 1.3 9.576 2.534L20.77 9.448c1.899 1.172 1.899 3.932 0 5.104L9.576 21.466C7.578 22.701 5 21.263 5 18.914V5.086Zm3.525-.85A1 1 0 0 0 7 5.085v13.828a1 1 0 0 0 1.525.85l11.194-6.913a1 1 0 0 0 0-1.702L8.525 4.235Z',
})
export const Play_Filled_Corner2_Rounded = createSinglePathSVG({
path: 'M9.576 2.534C7.578 1.299 5 2.737 5 5.086v13.828c0 2.35 2.578 3.787 4.576 2.552l11.194-6.914c1.899-1.172 1.899-3.932 0-5.104L9.576 2.534Z',
})
@@ -174,7 +174,7 @@ export function LabelerLabelPreference({
disabled?: boolean disabled?: boolean
labelerDid?: string labelerDid?: string
}) { }) {
const {i18n} = useLingui() const {_, i18n} = useLingui()
const t = useTheme() const t = useTheme()
const {gtPhone} = useBreakpoints() const {gtPhone} = useBreakpoints()
@@ -243,7 +243,10 @@ export function LabelerLabelPreference({
) : isGlobalLabel ? ( ) : isGlobalLabel ? (
<Trans> <Trans>
Configured in{' '} Configured in{' '}
<InlineLinkText to="/moderation" style={a.text_sm}> <InlineLinkText
label={_(msg`moderation settings`)}
to="/moderation"
style={a.text_sm}>
moderation settings moderation settings
</InlineLinkText> </InlineLinkText>
. .
+10 -4
View File
@@ -128,6 +128,9 @@ function Label({
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const {labeler, strings} = useLabelInfo(label) const {labeler, strings} = useLabelInfo(label)
const sourceName = labeler
? sanitizeHandle(labeler.creator.handle, '@')
: label.src
return ( return (
<View <View
style={[ style={[
@@ -169,13 +172,12 @@ function Label({
<Trans> <Trans>
Source:{' '} Source:{' '}
<InlineLinkText <InlineLinkText
label={sourceName}
to={makeProfileLink( to={makeProfileLink(
labeler ? labeler.creator : {did: label.src, handle: ''}, labeler ? labeler.creator : {did: label.src, handle: ''},
)} )}
onPress={() => control.close()}> onPress={() => control.close()}>
{labeler {sourceName}
? sanitizeHandle(labeler.creator.handle, '@')
: label.src}
</InlineLinkText> </InlineLinkText>
</Trans> </Trans>
)} )}
@@ -203,6 +205,9 @@ function AppealForm({
const isAccountReport = 'did' in subject const isAccountReport = 'did' in subject
const agent = useAgent() const agent = useAgent()
const gate = useGate() const gate = useGate()
const sourceName = labeler
? sanitizeHandle(labeler.creator.handle, '@')
: label.src
const {mutate, isPending} = useMutation({ const {mutate, isPending} = useMutation({
mutationFn: async () => { mutationFn: async () => {
@@ -260,12 +265,13 @@ function AppealForm({
<Trans> <Trans>
This appeal will be sent to{' '} This appeal will be sent to{' '}
<InlineLinkText <InlineLinkText
label={sourceName}
to={makeProfileLink( to={makeProfileLink(
labeler ? labeler.creator : {did: label.src, handle: ''}, labeler ? labeler.creator : {did: label.src, handle: ''},
)} )}
onPress={() => control.close()} onPress={() => control.close()}
style={[a.text_md, a.leading_snug]}> style={[a.text_md, a.leading_snug]}>
{labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src} {sourceName}
</InlineLinkText> </InlineLinkText>
. .
</Trans> </Trans>
@@ -54,7 +54,10 @@ function ModerationDetailsDialogInner({
description = ( description = (
<Trans> <Trans>
This user is included in the{' '} This user is included in the{' '}
<InlineLinkText to={listUriToHref(list.uri)} style={[a.text_sm]}> <InlineLinkText
label={list.name}
to={listUriToHref(list.uri)}
style={[a.text_sm]}>
{list.name} {list.name}
</InlineLinkText>{' '} </InlineLinkText>{' '}
list which you have blocked. list which you have blocked.
@@ -83,7 +86,10 @@ function ModerationDetailsDialogInner({
description = ( description = (
<Trans> <Trans>
This user is included in the{' '} This user is included in the{' '}
<InlineLinkText to={listUriToHref(list.uri)} style={[a.text_sm]}> <InlineLinkText
label={list.name}
to={listUriToHref(list.uri)}
style={[a.text_sm]}>
{list.name} {list.name}
</InlineLinkText>{' '} </InlineLinkText>{' '}
list which you have muted. list which you have muted.
@@ -127,10 +133,11 @@ function ModerationDetailsDialogInner({
<Trans> <Trans>
This label was applied by{' '} This label was applied by{' '}
<InlineLinkText <InlineLinkText
label={desc.source || _(msg`an unknown labeler`)}
to={makeProfileLink({did: modcause.label.src, handle: ''})} to={makeProfileLink({did: modcause.label.src, handle: ''})}
onPress={() => control.close()} onPress={() => control.close()}
style={a.text_md}> style={a.text_md}>
{desc.source} {desc.source || _(msg`an unknown labeler`)}
</InlineLinkText> </InlineLinkText>
. .
</Trans> </Trans>
+70 -61
View File
@@ -14,29 +14,33 @@ export type FeedTunerFn = (
slices: FeedViewPostsSlice[], slices: FeedViewPostsSlice[],
) => FeedViewPostsSlice[] ) => FeedViewPostsSlice[]
type FeedSliceItem = {
post: AppBskyFeedDefs.PostView
reply?: AppBskyFeedDefs.ReplyRef
}
function toSliceItem(feedViewPost: FeedViewPost): FeedSliceItem {
return {
post: feedViewPost.post,
reply: feedViewPost.reply,
}
}
export class FeedViewPostsSlice { export class FeedViewPostsSlice {
_reactKey: string _reactKey: string
isFlattenedReply = false _feedPost: FeedViewPost
items: FeedSliceItem[]
constructor(public items: FeedViewPost[]) { constructor(feedPost: FeedViewPost) {
const item = items[0] this._feedPost = feedPost
this._reactKey = `slice-${item.post.uri}-${ this._reactKey = `slice-${feedPost.post.uri}-${
item.reason?.indexedAt || item.post.indexedAt feedPost.reason?.indexedAt || feedPost.post.indexedAt
}` }`
this.items = [toSliceItem(feedPost)]
} }
get uri() { get uri() {
if (this.isFlattenedReply) { return this._feedPost.post.uri
return this.items[1].post.uri
}
return this.items[0].post.uri
}
get ts() {
if (this.items[0].reason?.indexedAt) {
return this.items[0].reason.indexedAt as string
}
return this.items[0].post.indexedAt
} }
get isThread() { get isThread() {
@@ -48,31 +52,42 @@ export class FeedViewPostsSlice {
) )
} }
get isFullThread() { get isQuotePost() {
return this.isThread && !this.items[0].reply const embed = this._feedPost.post.embed
} return (
AppBskyEmbedRecord.isView(embed) ||
get rootItem() { AppBskyEmbedRecordWithMedia.isView(embed)
if (this.isFlattenedReply) { )
return this.items[1]
}
return this.items[0]
} }
get isReply() { get isReply() {
return ( return (
AppBskyFeedPost.isRecord(this.rootItem.post.record) && AppBskyFeedPost.isRecord(this._feedPost.post.record) &&
!!this.rootItem.post.record.reply !!this._feedPost.post.record.reply
) )
} }
get source(): ReasonFeedSource | undefined { get reason() {
return this.items.find(item => '__source' in item && !!item.__source) return '__source' in this._feedPost
?.__source as ReasonFeedSource ? (this._feedPost.__source as ReasonFeedSource)
: this._feedPost.reason
} }
get feedContext() { get feedContext() {
return this.items.find(item => item.feedContext)?.feedContext return this._feedPost.feedContext
}
get isRepost() {
const reason = this._feedPost.reason
return AppBskyFeedDefs.isReasonRepost(reason)
}
get includesThreadRoot() {
return !this.items[0].reply
}
get likeCount() {
return this._feedPost.post.likeCount ?? 0
} }
containsUri(uri: string) { containsUri(uri: string) {
@@ -97,24 +112,24 @@ export class FeedViewPostsSlice {
if (this.items[0].reply) { if (this.items[0].reply) {
const reply = this.items[0].reply const reply = this.items[0].reply
if (AppBskyFeedDefs.isPostView(reply.parent)) { if (AppBskyFeedDefs.isPostView(reply.parent)) {
this.isFlattenedReply = true
this.items.splice(0, 0, {post: reply.parent}) this.items.splice(0, 0, {post: reply.parent})
} }
} }
} }
isFollowingAllAuthors(userDid: string) { isFollowingAllAuthors(userDid: string) {
const item = this.rootItem const feedPost = this._feedPost
if (item.post.author.did === userDid) { if (feedPost.post.author.did === userDid) {
return true return true
} }
if (AppBskyFeedDefs.isPostView(item.reply?.parent)) { if (AppBskyFeedDefs.isPostView(feedPost.reply?.parent)) {
const parent = item.reply?.parent const parent = feedPost.reply?.parent
if (parent?.author.did === userDid) { if (parent?.author.did === userDid) {
return true return true
} }
return ( return (
parent?.author.viewer?.following && item.post.author.viewer?.following parent?.author.viewer?.following &&
feedPost.post.author.viewer?.following
) )
} }
return false return false
@@ -127,7 +142,7 @@ export class NoopFeedTuner {
feed: FeedViewPost[], feed: FeedViewPost[],
_opts?: {dryRun: boolean; maintainOrder: boolean}, _opts?: {dryRun: boolean; maintainOrder: boolean},
): FeedViewPostsSlice[] { ): FeedViewPostsSlice[] {
return feed.map(item => new FeedViewPostsSlice([item])) return feed.map(item => new FeedViewPostsSlice(item))
} }
} }
@@ -165,7 +180,7 @@ export class FeedTuner {
}) })
if (maintainOrder) { if (maintainOrder) {
slices = feed.map(item => new FeedViewPostsSlice([item])) slices = feed.map(item => new FeedViewPostsSlice(item))
} else { } else {
// arrange the posts into thread slices // arrange the posts into thread slices
for (let i = feed.length - 1; i >= 0; i--) { for (let i = feed.length - 1; i >= 0; i--) {
@@ -192,7 +207,7 @@ export class FeedTuner {
} }
} }
slices.unshift(new FeedViewPostsSlice([item])) slices.unshift(new FeedViewPostsSlice(item))
} }
} }
@@ -215,7 +230,7 @@ export class FeedTuner {
// turn non-threads with reply parents into threads // turn non-threads with reply parents into threads
for (const slice of slices) { for (const slice of slices) {
if (!slice.isThread && !slice.items[0].reason && slice.items[0].reply) { if (!slice.isThread && !slice.reason && slice.items[0].reply) {
const reply = slice.items[0].reply const reply = slice.items[0].reply
if ( if (
AppBskyFeedDefs.isPostView(reply.parent) && AppBskyFeedDefs.isPostView(reply.parent) &&
@@ -256,8 +271,7 @@ export class FeedTuner {
static removeReposts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) { static removeReposts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) {
for (let i = slices.length - 1; i >= 0; i--) { for (let i = slices.length - 1; i >= 0; i--) {
const reason = slices[i].rootItem.reason if (slices[i].isRepost) {
if (AppBskyFeedDefs.isReasonRepost(reason)) {
slices.splice(i, 1) slices.splice(i, 1)
} }
} }
@@ -266,11 +280,7 @@ export class FeedTuner {
static removeQuotePosts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) { static removeQuotePosts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) {
for (let i = slices.length - 1; i >= 0; i--) { for (let i = slices.length - 1; i >= 0; i--) {
const embed = slices[i].rootItem.post.embed if (slices[i].isQuotePost) {
if (
AppBskyEmbedRecord.isView(embed) ||
AppBskyEmbedRecordWithMedia.isView(embed)
) {
slices.splice(i, 1) slices.splice(i, 1)
} }
} }
@@ -315,19 +325,18 @@ export class FeedTuner {
// remove any replies without at least minLikes likes // remove any replies without at least minLikes likes
for (let i = slices.length - 1; i >= 0; i--) { for (let i = slices.length - 1; i >= 0; i--) {
const slice = slices[i] const slice = slices[i]
if (slice.isFullThread || !slice.isReply) { if (slice.isReply) {
continue if (slice.isThread && slice.includesThreadRoot) {
} continue
}
const item = slice.rootItem if (slice.isRepost) {
const isRepost = Boolean(item.reason) continue
if (isRepost) { }
continue if (slice.likeCount < minLikes) {
} slices.splice(i, 1)
if ((item.post.likeCount || 0) < minLikes) { } else if (followedOnly && !slice.isFollowingAllAuthors(userDid)) {
slices.splice(i, 1) slices.splice(i, 1)
} else if (followedOnly && !slice.isFollowingAllAuthors(userDid)) { }
slices.splice(i, 1)
} }
} }
return slices return slices
+1 -1
View File
@@ -251,7 +251,7 @@ class MergeFeedSource_Following extends MergeFeedSource {
dryRun: false, dryRun: false,
maintainOrder: true, maintainOrder: true,
}) })
res.data.feed = slices.map(slice => slice.rootItem) res.data.feed = slices.map(slice => slice._feedPost)
return res return res
} }
} }
+16 -4
View File
@@ -240,7 +240,10 @@ export function ModerationScreenInner({
)} )}
</Button> </Button>
<Divider /> <Divider />
<Link testID="moderationlistsBtn" to="/moderation/modlists"> <Link
label={_(msg`View your moderation lists`)}
testID="moderationlistsBtn"
to="/moderation/modlists">
{state => ( {state => (
<SubItem <SubItem
title={_(msg`Moderation lists`)} title={_(msg`Moderation lists`)}
@@ -252,7 +255,10 @@ export function ModerationScreenInner({
)} )}
</Link> </Link>
<Divider /> <Divider />
<Link testID="mutedAccountsBtn" to="/moderation/muted-accounts"> <Link
label={_(msg`View your muted accounts`)}
testID="mutedAccountsBtn"
to="/moderation/muted-accounts">
{state => ( {state => (
<SubItem <SubItem
title={_(msg`Muted accounts`)} title={_(msg`Muted accounts`)}
@@ -264,7 +270,10 @@ export function ModerationScreenInner({
)} )}
</Link> </Link>
<Divider /> <Divider />
<Link testID="blockedAccountsBtn" to="/moderation/blocked-accounts"> <Link
label={_(msg`View your blocked accounts`)}
testID="blockedAccountsBtn"
to="/moderation/blocked-accounts">
{state => ( {state => (
<SubItem <SubItem
title={_(msg`Blocked accounts`)} title={_(msg`Blocked accounts`)}
@@ -356,6 +365,7 @@ export function ModerationScreenInner({
<Trans> <Trans>
Adult content can only be enabled via the Web at{' '} Adult content can only be enabled via the Web at{' '}
<InlineLinkText <InlineLinkText
label={_(msg`The Bluesky web application`)}
to="" to=""
onPress={evt => { onPress={evt => {
evt.preventDefault() evt.preventDefault()
@@ -569,7 +579,9 @@ function PwiOptOut() {
</Trans> </Trans>
</Text> </Text>
<InlineLinkText to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy"> <InlineLinkText
label={_(msg`Learn more about what is public on Bluesky.`)}
to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy">
<Trans>Learn more about what is public on Bluesky.</Trans> <Trans>Learn more about what is public on Bluesky.</Trans>
</InlineLinkText> </InlineLinkText>
</View> </View>
+8 -2
View File
@@ -45,14 +45,20 @@ export const Policies = ({
const els = [] const els = []
if (tos) { if (tos) {
els.push( els.push(
<InlineLinkText key="tos" to={tos}> <InlineLinkText
label={_(msg`Read the Bluesky Terms of Service`)}
key="tos"
to={tos}>
{_(msg`Terms of Service`)} {_(msg`Terms of Service`)}
</InlineLinkText>, </InlineLinkText>,
) )
} }
if (pp) { if (pp) {
els.push( els.push(
<InlineLinkText key="pp" to={pp}> <InlineLinkText
label={_(msg`Read the Bluesky Privacy Policy`)}
key="pp"
to={pp}>
{_(msg`Privacy Policy`)} {_(msg`Privacy Policy`)}
</InlineLinkText>, </InlineLinkText>,
) )
+1
View File
@@ -166,6 +166,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
<Text style={[t.atoms.text, !gtMobile && a.text_md]}> <Text style={[t.atoms.text, !gtMobile && a.text_md]}>
<Trans>Having trouble?</Trans>{' '} <Trans>Having trouble?</Trans>{' '}
<InlineLinkText <InlineLinkText
label={_(msg`Contact support`)}
to={FEEDBACK_FORM_URL({email: state.email})} to={FEEDBACK_FORM_URL({email: state.email})}
style={[!gtMobile && a.text_md]}> style={[!gtMobile && a.text_md]}>
<Trans>Contact support</Trans> <Trans>Contact support</Trans>
+25 -15
View File
@@ -78,7 +78,10 @@ export interface FeedPostSliceItem {
uri: string uri: string
post: AppBskyFeedDefs.PostView post: AppBskyFeedDefs.PostView
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource reason?:
| AppBskyFeedDefs.ReasonRepost
| ReasonFeedSource
| {[k: string]: unknown; $type: string}
feedContext: string | undefined feedContext: string | undefined
moderation: ModerationDecision moderation: ModerationDecision
parentAuthor?: AppBskyActorDefs.ProfileViewBasic parentAuthor?: AppBskyActorDefs.ProfileViewBasic
@@ -311,7 +314,7 @@ export function usePostFeedQuery(
if (isDiscover) { if (isDiscover) {
userActionHistory.seen( userActionHistory.seen(
slice.items.map(item => ({ slice.items.map(item => ({
feedContext: item.feedContext, feedContext: slice.feedContext,
likeCount: item.post.likeCount ?? 0, likeCount: item.post.likeCount ?? 0,
repostCount: item.post.repostCount ?? 0, repostCount: item.post.repostCount ?? 0,
replyCount: item.post.replyCount ?? 0, replyCount: item.post.replyCount ?? 0,
@@ -323,10 +326,10 @@ export function usePostFeedQuery(
) )
} }
return { const feedPostSlice: FeedPostSlice = {
_reactKey: slice._reactKey, _reactKey: slice._reactKey,
_isFeedPostSlice: true, _isFeedPostSlice: true,
rootUri: slice.rootItem.post.uri, rootUri: slice.uri,
isThread: isThread:
slice.items.length > 1 && slice.items.length > 1 &&
slice.items.every( slice.items.every(
@@ -341,35 +344,42 @@ export function usePostFeedQuery(
AppBskyFeedPost.validateRecord(item.post.record) AppBskyFeedPost.validateRecord(item.post.record)
.success .success
) { ) {
const parentAuthor = const parent = item.reply?.parent
item.reply?.parent?.author ?? let parentAuthor:
slice.items[i + 1]?.reply?.grandparentAuthor | AppBskyActorDefs.ProfileViewBasic
| undefined
if (AppBskyFeedDefs.isPostView(parent)) {
parentAuthor = parent.author
}
if (!parentAuthor) {
parentAuthor =
slice.items[i + 1]?.reply?.grandparentAuthor
}
const replyRef = item.reply const replyRef = item.reply
const isParentBlocked = AppBskyFeedDefs.isBlockedPost( const isParentBlocked = AppBskyFeedDefs.isBlockedPost(
replyRef?.parent, replyRef?.parent,
) )
return { const feedPostSliceItem: FeedPostSliceItem = {
_reactKey: `${slice._reactKey}-${i}-${item.post.uri}`, _reactKey: `${slice._reactKey}-${i}-${item.post.uri}`,
uri: item.post.uri, uri: item.post.uri,
post: item.post, post: item.post,
record: item.post.record, record: item.post.record,
reason: reason: slice.reason,
i === 0 && slice.source feedContext: slice.feedContext,
? slice.source
: item.reason,
feedContext: item.feedContext || slice.feedContext,
moderation: moderations[i], moderation: moderations[i],
parentAuthor, parentAuthor,
isParentBlocked, isParentBlocked,
} }
return feedPostSliceItem
} }
return undefined return undefined
}) })
.filter(Boolean) as FeedPostSliceItem[], .filter(n => !!n),
} }
return feedPostSlice
}) })
.filter(Boolean) as FeedPostSlice[], .filter(n => !!n),
})), })),
], ],
} }
+11 -11
View File
@@ -4,7 +4,7 @@ export type ThreadgateSetting =
| {type: 'nobody'} | {type: 'nobody'}
| {type: 'mention'} | {type: 'mention'}
| {type: 'following'} | {type: 'following'}
| {type: 'list'; list: string} | {type: 'list'; list: unknown}
export function threadgateViewToSettings( export function threadgateViewToSettings(
threadgate: AppBskyFeedDefs.ThreadgateView | undefined, threadgate: AppBskyFeedDefs.ThreadgateView | undefined,
@@ -21,18 +21,18 @@ export function threadgateViewToSettings(
if (!record.allow?.length) { if (!record.allow?.length) {
return [{type: 'nobody'}] return [{type: 'nobody'}]
} }
return record.allow const settings: ThreadgateSetting[] = record.allow
.map(allow => { .map(allow => {
let setting: ThreadgateSetting | undefined
if (allow.$type === 'app.bsky.feed.threadgate#mentionRule') { if (allow.$type === 'app.bsky.feed.threadgate#mentionRule') {
return {type: 'mention'} setting = {type: 'mention'}
} else if (allow.$type === 'app.bsky.feed.threadgate#followingRule') {
setting = {type: 'following'}
} else if (allow.$type === 'app.bsky.feed.threadgate#listRule') {
setting = {type: 'list', list: allow.list}
} }
if (allow.$type === 'app.bsky.feed.threadgate#followingRule') { return setting
return {type: 'following'}
}
if (allow.$type === 'app.bsky.feed.threadgate#listRule') {
return {type: 'list', list: allow.list}
}
return undefined
}) })
.filter(Boolean) as ThreadgateSetting[] .filter(n => !!n)
return settings
} }
+10 -3
View File
@@ -132,6 +132,7 @@ export const SplashScreen = ({
function Footer() { function Footer() {
const t = useTheme() const t = useTheme()
const {_} = useLingui()
return ( return (
<View <View
@@ -147,13 +148,19 @@ function Footer() {
a.flex_1, a.flex_1,
t.atoms.border_contrast_medium, t.atoms.border_contrast_medium,
]}> ]}>
<InlineLinkText to="https://bsky.social"> <InlineLinkText
label={_(msg`Learn more about Bluesky`)}
to="https://bsky.social">
<Trans>Business</Trans> <Trans>Business</Trans>
</InlineLinkText> </InlineLinkText>
<InlineLinkText to="https://bsky.social/about/blog"> <InlineLinkText
label={_(msg`Read the Bluesky blog`)}
to="https://bsky.social/about/blog">
<Trans>Blog</Trans> <Trans>Blog</Trans>
</InlineLinkText> </InlineLinkText>
<InlineLinkText to="https://bsky.social/about/join"> <InlineLinkText
label={_(msg`See jobs at Bluesky`)}
to="https://bsky.social/about/join">
<Trans>Jobs</Trans> <Trans>Jobs</Trans>
</InlineLinkText> </InlineLinkText>
+4 -5
View File
@@ -99,7 +99,6 @@ import {SelectVideoBtn} from './videos/SelectVideoBtn'
import {useVideoState} from './videos/state' import {useVideoState} from './videos/state'
import {VideoPreview} from './videos/VideoPreview' import {VideoPreview} from './videos/VideoPreview'
import {VideoTranscodeProgress} from './videos/VideoTranscodeProgress' import {VideoTranscodeProgress} from './videos/VideoTranscodeProgress'
import hairlineWidth = StyleSheet.hairlineWidth
type CancelRef = { type CancelRef = {
onPressCancel: () => void onPressCancel: () => void
@@ -763,7 +762,7 @@ function useAnimatedBorders() {
const topBarAnimatedStyle = useAnimatedStyle(() => { const topBarAnimatedStyle = useAnimatedStyle(() => {
return { return {
borderBottomWidth: hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: interpolateColor( borderColor: interpolateColor(
hasScrolledTop.value, hasScrolledTop.value,
[0, 1], [0, 1],
@@ -773,7 +772,7 @@ function useAnimatedBorders() {
}) })
const bottomBarAnimatedStyle = useAnimatedStyle(() => { const bottomBarAnimatedStyle = useAnimatedStyle(() => {
return { return {
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
borderColor: interpolateColor( borderColor: interpolateColor(
hasScrolledBottom.value, hasScrolledBottom.value,
[0, 1], [0, 1],
@@ -855,7 +854,7 @@ const styles = StyleSheet.create({
marginBottom: 8, marginBottom: 8,
}, },
errorIcon: { errorIcon: {
borderWidth: hairlineWidth, borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.red4, borderColor: colors.red4,
color: colors.red4, color: colors.red4,
borderRadius: 30, borderRadius: 30,
@@ -891,6 +890,6 @@ const styles = StyleSheet.create({
paddingLeft: 7, paddingLeft: 7,
paddingRight: 16, paddingRight: 16,
alignItems: 'center', alignItems: 'center',
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
}, },
}) })
+1 -2
View File
@@ -9,7 +9,6 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import hairlineWidth = StyleSheet.hairlineWidth
export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) { export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
const {currentAccount} = useSession() const {currentAccount} = useSession()
@@ -49,7 +48,7 @@ const styles = StyleSheet.create({
paddingBottom: 10, paddingBottom: 10,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
}, },
labelMobile: { labelMobile: {
paddingLeft: 12, paddingLeft: 12,
+3 -4
View File
@@ -13,6 +13,7 @@ import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {logger} from '#/logger' import {logger} from '#/logger'
import {shouldClickOpenNewTab} from '#/platform/urls'
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed' import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
import { import {
useAddSavedFeedsMutation, useAddSavedFeedsMutation,
@@ -32,8 +33,6 @@ import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import hairlineWidth = StyleSheet.hairlineWidth
import {shouldClickOpenNewTab} from '#/platform/urls'
export function FeedSourceCard({ export function FeedSourceCard({
feedUri, feedUri,
@@ -209,7 +208,7 @@ export function FeedSourceCardLoaded({
styles.container, styles.container,
pal.border, pal.border,
style, style,
{borderTopWidth: hideTopBorder ? 0 : hairlineWidth}, {borderTopWidth: hideTopBorder ? 0 : StyleSheet.hairlineWidth},
]} ]}
onPress={e => { onPress={e => {
const shouldOpenInNewTab = shouldClickOpenNewTab(e) const shouldOpenInNewTab = shouldClickOpenNewTab(e)
@@ -330,7 +329,7 @@ const styles = StyleSheet.create({
gap: 14, gap: 14,
}, },
border: { border: {
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
}, },
headerContainer: { headerContainer: {
flexDirection: 'row', flexDirection: 'row',
+1 -1
View File
@@ -73,7 +73,7 @@ export function HomeHeaderLayoutMobile({
]}> ]}>
{IS_DEV && ( {IS_DEV && (
<> <>
<Link to="/sys/debug"> <Link label="View storybook" to="/sys/debug">
<ColorPalette size="md" /> <ColorPalette size="md" />
</Link> </Link>
</> </>
+1 -2
View File
@@ -14,7 +14,6 @@ import {RichText as RichTextCom} from '#/components/RichText'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import hairlineWidth = StyleSheet.hairlineWidth
export const ListCard = ({ export const ListCard = ({
testID, testID,
@@ -134,7 +133,7 @@ export const ListCard = ({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
outer: { outer: {
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
paddingHorizontal: 6, paddingHorizontal: 6,
}, },
outerNoBorder: { outerNoBorder: {
+3 -4
View File
@@ -29,7 +29,6 @@ import {Button} from '../util/forms/Button'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast' import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import hairlineWidth = StyleSheet.hairlineWidth
export const snapPoints = ['fullscreen'] export const snapPoints = ['fullscreen']
@@ -63,7 +62,7 @@ export function Component({
return [pal.border, {height: screenHeight / 1.5}] return [pal.border, {height: screenHeight / 1.5}]
} }
return [pal.border, {flex: 1, borderTopWidth: hairlineWidth}] return [pal.border, {flex: 1, borderTopWidth: StyleSheet.hairlineWidth}]
}, [pal.border, screenHeight]) }, [pal.border, screenHeight])
return ( return (
@@ -188,7 +187,7 @@ function ListItem({
style={[ style={[
styles.listItem, styles.listItem,
pal.border, pal.border,
index !== 0 && {borderTopWidth: hairlineWidth}, index !== 0 && {borderTopWidth: StyleSheet.hairlineWidth},
]}> ]}>
<View style={styles.listItemAvi}> <View style={styles.listItemAvi}>
<UserAvatar size={40} avatar={list.avatar} type="list" /> <UserAvatar size={40} avatar={list.avatar} type="list" />
@@ -248,7 +247,7 @@ const styles = StyleSheet.create({
gap: 10, gap: 10,
paddingTop: 10, paddingTop: 10,
paddingBottom: isAndroid ? 10 : 0, paddingBottom: isAndroid ? 10 : 0,
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
}, },
footerBtn: { footerBtn: {
paddingHorizontal: 24, paddingHorizontal: 24,
+1 -2
View File
@@ -24,7 +24,6 @@ import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceho
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
import {CenteredView} from '#/view/com/util/Views' import {CenteredView} from '#/view/com/util/Views'
import {FeedItem} from './FeedItem' import {FeedItem} from './FeedItem'
import hairlineWidth = StyleSheet.hairlineWidth
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
@@ -137,7 +136,7 @@ export function Feed({
<View <View
style={[ style={[
pal.border, pal.border,
!isTabletOrMobile && {borderTopWidth: hairlineWidth}, !isTabletOrMobile && {borderTopWidth: StyleSheet.hairlineWidth},
]}> ]}>
<NotificationFeedLoadingPlaceholder /> <NotificationFeedLoadingPlaceholder />
</View> </View>
+36 -27
View File
@@ -20,20 +20,28 @@ import {
import {AtUri} from '@atproto/api' import {AtUri} from '@atproto/api'
import {msg, plural, Trans} from '@lingui/macro' import {msg, plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {useGate} from '#/lib/statsig/statsig' import {useGate} from '#/lib/statsig/statsig'
import {parseTenorGif} from '#/lib/strings/embed-player'
import {logger} from '#/logger'
import {FeedNotification} from '#/state/queries/notifications/feed' import {FeedNotification} from '#/state/queries/notifications/feed'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {NavigationProp} from 'lib/routes/types'
import {forceLTR} from 'lib/strings/bidi'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {niceDate} from 'lib/strings/time' import {niceDate} from 'lib/strings/time'
import {colors, s} from 'lib/styles' import {colors, s} from 'lib/styles'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {DM_SERVICE_HEADERS} from 'state/queries/messages/const'
import {precacheProfile} from 'state/queries/profile' import {precacheProfile} from 'state/queries/profile'
import {useAgent} from 'state/session'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import { import {
ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon, ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon, ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon,
@@ -41,8 +49,10 @@ import {
import {Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled} from '#/components/icons/Heart2' import {Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled} from '#/components/icons/Heart2'
import {PersonPlus_Filled_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person' import {PersonPlus_Filled_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person'
import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost' import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost'
import {StarterPack} from '#/components/icons/StarterPack'
import {Link as NewLink} from '#/components/Link' import {Link as NewLink} from '#/components/Link'
import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
import {FeedSourceCard} from '../feeds/FeedSourceCard' import {FeedSourceCard} from '../feeds/FeedSourceCard'
import {Post} from '../post/Post' import {Post} from '../post/Post'
import {ImageHorzList} from '../util/images/ImageHorzList' import {ImageHorzList} from '../util/images/ImageHorzList'
@@ -52,19 +62,6 @@ import {Text} from '../util/text/Text'
import {TimeElapsed} from '../util/TimeElapsed' import {TimeElapsed} from '../util/TimeElapsed'
import {PreviewableUserAvatar, UserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar, UserAvatar} from '../util/UserAvatar'
import hairlineWidth = StyleSheet.hairlineWidth
import {useNavigation} from '@react-navigation/native'
import {parseTenorGif} from '#/lib/strings/embed-player'
import {logger} from '#/logger'
import {NavigationProp} from 'lib/routes/types'
import {forceLTR} from 'lib/strings/bidi'
import {DM_SERVICE_HEADERS} from 'state/queries/messages/const'
import {useAgent} from 'state/session'
import {Button, ButtonText} from '#/components/Button'
import {StarterPack} from '#/components/icons/StarterPack'
import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
const MAX_AUTHORS = 5 const MAX_AUTHORS = 5
const EXPANDED_AUTHOR_EL_HEIGHT = 35 const EXPANDED_AUTHOR_EL_HEIGHT = 35
@@ -171,7 +168,6 @@ let FeedItem = ({
) )
} }
let isFollowBack = false
let action = '' let action = ''
let icon = ( let icon = (
<HeartIconFilled <HeartIconFilled
@@ -192,7 +188,6 @@ let FeedItem = ({
item.notification.author.viewer?.following && item.notification.author.viewer?.following &&
gate('ungroup_follow_backs') gate('ungroup_follow_backs')
) { ) {
isFollowBack = true
action = _(msg`followed you back`) action = _(msg`followed you back`)
} else { } else {
action = _(msg`followed you`) action = _(msg`followed you`)
@@ -211,7 +206,22 @@ let FeedItem = ({
return null return null
} }
let formattedCount = authors.length > 1 ? formatCount(authors.length - 1) : '' const formattedCount =
authors.length > 1 ? formatCount(authors.length - 1) : ''
const firstAuthorName = sanitizeDisplayName(
authors[0].profile.displayName || authors[0].profile.handle,
)
const niceTimestamp = niceDate(item.notification.indexedAt)
const a11yLabelUsers =
authors.length > 1
? _(msg` and `) +
plural(authors.length - 1, {
one: `${formattedCount} other`,
other: `${formattedCount} others`,
})
: ''
const a11yLabel = `${firstAuthorName}${a11yLabelUsers} ${action} ${niceTimestamp}`
return ( return (
<Link <Link
testID={`feedItem-by-${item.notification.author.handle}`} testID={`feedItem-by-${item.notification.author.handle}`}
@@ -224,10 +234,12 @@ let FeedItem = ({
backgroundColor: pal.colors.unreadNotifBg, backgroundColor: pal.colors.unreadNotifBg,
borderColor: pal.colors.unreadNotifBorder, borderColor: pal.colors.unreadNotifBorder,
}, },
{borderTopWidth: hideTopBorder ? 0 : hairlineWidth}, {borderTopWidth: hideTopBorder ? 0 : StyleSheet.hairlineWidth},
]} ]}
href={itemHref} href={itemHref}
noFeedback noFeedback
accessibilityHint=""
accessibilityLabel={a11yLabel}
accessible={!isAuthorsExpanded} accessible={!isAuthorsExpanded}
accessibilityActions={ accessibilityActions={
authors.length > 1 authors.length > 1
@@ -272,19 +284,18 @@ let FeedItem = ({
visible={!isAuthorsExpanded} visible={!isAuthorsExpanded}
authors={authors} authors={authors}
onToggleAuthorsExpanded={onToggleAuthorsExpanded} onToggleAuthorsExpanded={onToggleAuthorsExpanded}
showDmButton={item.type === 'starterpack-joined' || isFollowBack} showDmButton={item.type === 'starterpack-joined'}
/> />
<ExpandedAuthorsList visible={isAuthorsExpanded} authors={authors} /> <ExpandedAuthorsList visible={isAuthorsExpanded} authors={authors} />
<Text style={[styles.meta, a.self_start]}> <Text
style={[styles.meta, a.self_start]}
accessibilityHint=""
accessibilityLabel={a11yLabel}>
<TextLink <TextLink
key={authors[0].href} key={authors[0].href}
style={[pal.text, s.bold]} style={[pal.text, s.bold]}
href={authors[0].href} href={authors[0].href}
text={forceLTR( text={forceLTR(firstAuthorName)}
sanitizeDisplayName(
authors[0].profile.displayName || authors[0].profile.handle,
),
)}
disableMismatchWarning disableMismatchWarning
/> />
{authors.length > 1 ? ( {authors.length > 1 ? (
@@ -306,7 +317,7 @@ let FeedItem = ({
{({timeElapsed}) => ( {({timeElapsed}) => (
<Text <Text
style={[pal.textLight, styles.pointer]} style={[pal.textLight, styles.pointer]}
title={niceDate(item.notification.indexedAt)}> title={niceTimestamp}>
{' ' + timeElapsed} {' ' + timeElapsed}
</Text> </Text>
)} )}
@@ -458,7 +469,6 @@ function CondensedAuthorsList({
profile={authors[0].profile} profile={authors[0].profile}
moderation={authors[0].moderation.ui('avatar')} moderation={authors[0].moderation.ui('avatar')}
type={authors[0].profile.associated?.labeler ? 'labeler' : 'user'} type={authors[0].profile.associated?.labeler ? 'labeler' : 'user'}
accessible={false}
/> />
{showDmButton ? <SayHelloBtn profile={authors[0].profile} /> : null} {showDmButton ? <SayHelloBtn profile={authors[0].profile} /> : null}
</View> </View>
@@ -476,7 +486,6 @@ function CondensedAuthorsList({
profile={author.profile} profile={author.profile}
moderation={author.moderation.ui('avatar')} moderation={author.moderation.ui('avatar')}
type={author.profile.associated?.labeler ? 'labeler' : 'user'} type={author.profile.associated?.labeler ? 'labeler' : 'user'}
accessible={false}
/> />
</View> </View>
))} ))}
+1 -2
View File
@@ -7,7 +7,6 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {PressableWithHover} from '../util/PressableWithHover' import {PressableWithHover} from '../util/PressableWithHover'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {DraggableScrollView} from './DraggableScrollView' import {DraggableScrollView} from './DraggableScrollView'
import hairlineWidth = StyleSheet.hairlineWidth
export interface TabBarProps { export interface TabBarProps {
testID?: string testID?: string
@@ -208,6 +207,6 @@ const mobileStyles = StyleSheet.create({
left: 0, left: 0,
right: 0, right: 0,
top: '100%', top: '100%',
borderBottomWidth: hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
}, },
}) })
+5 -6
View File
@@ -44,7 +44,6 @@ import {PostEmbeds} from '../util/post-embeds'
import {PostMeta} from '../util/PostMeta' import {PostMeta} from '../util/PostMeta'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
import hairlineWidth = StyleSheet.hairlineWidth
export function PostThreadItem({ export function PostThreadItem({
post, post,
@@ -604,7 +603,7 @@ function PostOuterWrapper({
{ {
flexDirection: 'row', flexDirection: 'row',
paddingHorizontal: isMobile ? 10 : 6, paddingHorizontal: isMobile ? 10 : 6,
borderTopWidth: depth === 1 ? hairlineWidth : 0, borderTopWidth: depth === 1 ? StyleSheet.hairlineWidth : 0,
}, },
]}> ]}>
{Array.from(Array(depth - 1)).map((_, n: number) => ( {Array.from(Array(depth - 1)).map((_, n: number) => (
@@ -699,7 +698,7 @@ function getThreadAuthor(
const styles = StyleSheet.create({ const styles = StyleSheet.create({
outer: { outer: {
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
paddingLeft: 8, paddingLeft: 8,
}, },
outerHighlighted: { outerHighlighted: {
@@ -709,7 +708,7 @@ const styles = StyleSheet.create({
paddingRight: 8, paddingRight: 8,
}, },
outerHighlightedRoot: { outerHighlightedRoot: {
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
paddingTop: 16, paddingTop: 16,
}, },
noTopBorder: { noTopBorder: {
@@ -761,8 +760,8 @@ const styles = StyleSheet.create({
expandedInfo: { expandedInfo: {
flexDirection: 'row', flexDirection: 'row',
padding: 10, padding: 10,
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
borderBottomWidth: hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
marginTop: 5, marginTop: 5,
marginBottom: 10, marginBottom: 10,
}, },
+1 -2
View File
@@ -37,7 +37,6 @@ import {PostMeta} from '../util/PostMeta'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
import {UserInfoText} from '../util/UserInfoText' import {UserInfoText} from '../util/UserInfoText'
import hairlineWidth = StyleSheet.hairlineWidth
export function Post({ export function Post({
post, post,
@@ -155,7 +154,7 @@ function PostInner({
style={[ style={[
styles.outer, styles.outer,
pal.border, pal.border,
!hideTopBorder && {borderTopWidth: hairlineWidth}, !hideTopBorder && {borderTopWidth: StyleSheet.hairlineWidth},
style, style,
]} ]}
onBeforePress={onBeforePress}> onBeforePress={onBeforePress}>
+58 -33
View File
@@ -16,8 +16,10 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {useGate} from '#/lib/statsig/statsig'
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
import {useFeedFeedbackContext} from '#/state/feed-feedback' import {useFeedFeedbackContext} from '#/state/feed-feedback'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer' import {useComposerControls} from '#/state/shell/composer'
import {isReasonFeedSource, ReasonFeedSource} from 'lib/api/feed/types' import {isReasonFeedSource, ReasonFeedSource} from 'lib/api/feed/types'
import {MAX_POST_LINES} from 'lib/constants' import {MAX_POST_LINES} from 'lib/constants'
@@ -29,6 +31,7 @@ import {countLines} from 'lib/strings/helpers'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {precacheProfile} from 'state/queries/profile' import {precacheProfile} from 'state/queries/profile'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
import {ContentHider} from '#/components/moderation/ContentHider' import {ContentHider} from '#/components/moderation/ContentHider'
import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
@@ -38,17 +41,19 @@ import {FeedNameText} from '../util/FeedInfoText'
import {Link, TextLink, TextLinkOnWebOnly} from '../util/Link' import {Link, TextLink, TextLinkOnWebOnly} from '../util/Link'
import {PostCtrls} from '../util/post-ctrls/PostCtrls' import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {PostEmbeds} from '../util/post-embeds' import {PostEmbeds} from '../util/post-embeds'
import {VideoEmbed} from '../util/post-embeds/VideoEmbed'
import {PostMeta} from '../util/PostMeta' import {PostMeta} from '../util/PostMeta'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
import {AviFollowButton} from './AviFollowButton' import {AviFollowButton} from './AviFollowButton'
import hairlineWidth = StyleSheet.hairlineWidth
import {useSession} from '#/state/session'
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
interface FeedItemProps { interface FeedItemProps {
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined reason:
| AppBskyFeedDefs.ReasonRepost
| ReasonFeedSource
| {[k: string]: unknown; $type: string}
| undefined
moderation: ModerationDecision moderation: ModerationDecision
parentAuthor: AppBskyActorDefs.ProfileViewBasic | undefined parentAuthor: AppBskyActorDefs.ProfileViewBasic | undefined
showReplyTo: boolean showReplyTo: boolean
@@ -132,6 +137,8 @@ let FeedItemInner = ({
const {openComposer} = useComposerControls() const {openComposer} = useComposerControls()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const gate = useGate()
const href = useMemo(() => { const href = useMemo(() => {
const urip = new AtUri(post.uri) const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey) return makeProfileLink(post.author, 'post', urip.rkey)
@@ -197,7 +204,8 @@ let FeedItemInner = ({
isThreadLastChild || (!isThreadChild && !isThreadParent) isThreadLastChild || (!isThreadChild && !isThreadParent)
? 8 ? 8
: undefined, : undefined,
borderTopWidth: hideTopBorder || isThreadChild ? 0 : hairlineWidth, borderTopWidth:
hideTopBorder || isThreadChild ? 0 : StyleSheet.hairlineWidth,
}, },
] ]
@@ -337,9 +345,11 @@ let FeedItemInner = ({
postHref={href} postHref={href}
onOpenAuthor={onOpenAuthor} onOpenAuthor={onOpenAuthor}
/> />
{!isThreadChild && showReplyTo && parentAuthor && ( {!isThreadChild &&
<ReplyToLabel blocked={isParentBlocked} profile={parentAuthor} /> showReplyTo &&
)} (parentAuthor || isParentBlocked) && (
<ReplyToLabel blocked={isParentBlocked} profile={parentAuthor} />
)}
<LabelsOnMyPost post={post} /> <LabelsOnMyPost post={post} />
<PostContent <PostContent
moderation={moderation} moderation={moderation}
@@ -348,6 +358,9 @@ let FeedItemInner = ({
postAuthor={post.author} postAuthor={post.author}
onOpenEmbed={onOpenEmbed} onOpenEmbed={onOpenEmbed}
/> />
{__DEV__ && gate('videos') && (
<VideoEmbed source="https://lumi.jazco.dev/watch/did:plc:q6gjnaw2blty4crticxkmujt/Qmc8w93UpTa2adJHg4ZhnDPrBs1EsbzrekzPcqF5SwusuZ/playlist.m3u8" />
)}
<PostCtrls <PostCtrls
post={post} post={post}
record={record} record={record}
@@ -431,12 +444,46 @@ function ReplyToLabel({
profile, profile,
blocked, blocked,
}: { }: {
profile: AppBskyActorDefs.ProfileViewBasic profile: AppBskyActorDefs.ProfileViewBasic | undefined
blocked?: boolean blocked?: boolean
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {currentAccount} = useSession() const {currentAccount} = useSession()
const isMe = profile.did === currentAccount?.did
let label
if (blocked) {
label = <Trans context="description">Reply to a blocked post</Trans>
} else if (profile != null) {
const isMe = profile.did === currentAccount?.did
if (isMe) {
label = <Trans context="description">Reply to you</Trans>
} else {
label = (
<Trans context="description">
Reply to{' '}
<ProfileHoverCard inline did={profile.did}>
<TextLinkOnWebOnly
type="md"
style={pal.textLight}
lineHeight={1.2}
numberOfLines={1}
href={makeProfileLink(profile)}
text={
profile.displayName
? sanitizeDisplayName(profile.displayName)
: sanitizeHandle(profile.handle)
}
/>
</ProfileHoverCard>
</Trans>
)
}
}
if (!label) {
// Should not happen.
return null
}
return ( return (
<View style={[s.flexRow, s.mb2, s.alignCenter]}> <View style={[s.flexRow, s.mb2, s.alignCenter]}>
@@ -450,29 +497,7 @@ function ReplyToLabel({
style={[pal.textLight, s.mr2]} style={[pal.textLight, s.mr2]}
lineHeight={1.2} lineHeight={1.2}
numberOfLines={1}> numberOfLines={1}>
{isMe ? ( {label}
<Trans context="description">Reply to you</Trans>
) : blocked ? (
<Trans context="description">Reply to a blocked post</Trans>
) : (
<Trans context="description">
Reply to{' '}
<ProfileHoverCard inline did={profile.did}>
<TextLinkOnWebOnly
type="md"
style={pal.textLight}
lineHeight={1.2}
numberOfLines={1}
href={makeProfileLink(profile)}
text={
profile.displayName
? sanitizeDisplayName(profile.displayName)
: sanitizeHandle(profile.handle)
}
/>
</ProfileHoverCard>
</Trans>
)}
</Text> </Text>
</View> </View>
) )
+1
View File
@@ -99,6 +99,7 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
<Trans> <Trans>
This feed is no longer online. We are showing{' '} This feed is no longer online. We are showing{' '}
<InlineLinkText <InlineLinkText
label={_(msg`The Discover feed`)}
to="/profile/bsky.app/feed/whats-hot" to="/profile/bsky.app/feed/whats-hot"
style={[a.text_md]}> style={[a.text_md]}>
Discover Discover
+2 -3
View File
@@ -23,12 +23,11 @@ import {
KnownFollowers, KnownFollowers,
shouldShowKnownFollowers, shouldShowKnownFollowers,
} from '#/components/KnownFollowers' } from '#/components/KnownFollowers'
import * as Pills from '#/components/Pills'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar' import {PreviewableUserAvatar} from '../util/UserAvatar'
import {FollowButton} from './FollowButton' import {FollowButton} from './FollowButton'
import hairlineWidth = StyleSheet.hairlineWidth
import * as Pills from '#/components/Pills'
export function ProfileCard({ export function ProfileCard({
testID, testID,
@@ -217,7 +216,7 @@ export function ProfileCardWithFollowBtn({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
outer: { outer: {
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
paddingHorizontal: 6, paddingHorizontal: 6,
paddingVertical: 4, paddingVertical: 4,
}, },
@@ -15,15 +15,13 @@ import {makeProfileLink} from 'lib/routes/links'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {isNative} from 'platform/detection' import {isNative} from 'platform/detection'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {StarterPack} from '#/components/icons/StarterPack'
import {TextLink} from '../util/Link' import {TextLink} from '../util/Link'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder' import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {UserAvatar, UserAvatarType} from '../util/UserAvatar' import {UserAvatar, UserAvatarType} from '../util/UserAvatar'
import {CenteredView} from '../util/Views' import {CenteredView} from '../util/Views'
import hairlineWidth = StyleSheet.hairlineWidth
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {StarterPack} from '#/components/icons/StarterPack'
export function ProfileSubpageHeader({ export function ProfileSubpageHeader({
isLoading, isLoading,
@@ -84,7 +82,7 @@ export function ProfileSubpageHeader({
{ {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
borderBottomWidth: hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
paddingTop: isNative ? 0 : 8, paddingTop: isNative ? 0 : 8,
paddingBottom: 8, paddingBottom: 8,
paddingHorizontal: isMobile ? 12 : 14, paddingHorizontal: isMobile ? 12 : 14,
+1 -2
View File
@@ -17,7 +17,6 @@ import {
Heart2_Stroke2_Corner0_Rounded as HeartIconOutline, Heart2_Stroke2_Corner0_Rounded as HeartIconOutline,
} from '#/components/icons/Heart2' } from '#/components/icons/Heart2'
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost' import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
import hairlineWidth = StyleSheet.hairlineWidth
export function LoadingPlaceholder({ export function LoadingPlaceholder({
width, width,
@@ -236,7 +235,7 @@ export function FeedLoadingPlaceholder({
{ {
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 18, paddingVertical: 18,
borderTopWidth: showTopBorder ? hairlineWidth : 0, borderTopWidth: showTopBorder ? StyleSheet.hairlineWidth : 0,
}, },
pal.border, pal.border,
style, style,
+2 -8
View File
@@ -55,7 +55,6 @@ interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
profile: AppBskyActorDefs.ProfileViewBasic profile: AppBskyActorDefs.ProfileViewBasic
disableHoverCard?: boolean disableHoverCard?: boolean
onBeforePress?: () => void onBeforePress?: () => void
accessible?: boolean
} }
const BLUR_AMOUNT = isWeb ? 5 : 100 const BLUR_AMOUNT = isWeb ? 5 : 100
@@ -412,7 +411,6 @@ let PreviewableUserAvatar = ({
profile, profile,
disableHoverCard, disableHoverCard,
onBeforePress, onBeforePress,
accessible = true,
...rest ...rest
}: PreviewableUserAvatarProps): React.ReactNode => { }: PreviewableUserAvatarProps): React.ReactNode => {
const {_} = useLingui() const {_} = useLingui()
@@ -426,12 +424,8 @@ let PreviewableUserAvatar = ({
return ( return (
<ProfileHoverCard did={profile.did} disable={disableHoverCard}> <ProfileHoverCard did={profile.did} disable={disableHoverCard}>
<Link <Link
label={ label={_(msg`${profile.displayName || profile.handle}'s avatar`)}
accessible accessibilityHint={_(msg`Opens this profile`)}
? _(msg`${profile.displayName || profile.handle}'s avatar`)
: undefined
}
accessibilityHint={accessible ? _(msg`Opens this profile`) : undefined}
to={makeProfileLink({ to={makeProfileLink({
did: profile.did, did: profile.did,
handle: profile.handle, handle: profile.handle,
+3 -4
View File
@@ -13,10 +13,9 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {useTheme} from '#/alf' import {useTheme} from '#/alf'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {Text} from './text/Text' import {Text} from './text/Text'
import {CenteredView} from './Views' import {CenteredView} from './Views'
import hairlineWidth = StyleSheet.hairlineWidth
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
@@ -154,7 +153,7 @@ function DesktopWebHeader({
styles.desktopHeader, styles.desktopHeader,
pal.border, pal.border,
{ {
borderBottomWidth: showBorder ? hairlineWidth : 0, borderBottomWidth: showBorder ? StyleSheet.hairlineWidth : 0,
}, },
{display: 'flex', flexDirection: 'column'}, {display: 'flex', flexDirection: 'column'},
]}> ]}>
@@ -243,7 +242,7 @@ const styles = StyleSheet.create({
marginRight: 'auto', marginRight: 'auto',
}, },
border: { border: {
borderBottomWidth: hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
}, },
titleContainer: { titleContainer: {
marginLeft: 'auto', marginLeft: 'auto',
+4 -5
View File
@@ -26,7 +26,6 @@ import Animated from 'react-native-reanimated'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {addStyle} from 'lib/styles' import {addStyle} from 'lib/styles'
import hairlineWidth = StyleSheet.hairlineWidth
interface AddedProps { interface AddedProps {
desktopFixedHeight?: boolean | number desktopFixedHeight?: boolean | number
@@ -50,8 +49,8 @@ export const CenteredView = React.forwardRef(function CenteredView(
} }
if (sideBorders) { if (sideBorders) {
style = addStyle(style, { style = addStyle(style, {
borderLeftWidth: hairlineWidth, borderLeftWidth: StyleSheet.hairlineWidth,
borderRightWidth: hairlineWidth, borderRightWidth: StyleSheet.hairlineWidth,
}) })
style = addStyle(style, pal.border) style = addStyle(style, pal.border)
} }
@@ -163,8 +162,8 @@ export const ScrollView = React.forwardRef(function ScrollViewImpl(
const styles = StyleSheet.create({ const styles = StyleSheet.create({
contentContainer: { contentContainer: {
borderLeftWidth: hairlineWidth, borderLeftWidth: StyleSheet.hairlineWidth,
borderRightWidth: hairlineWidth, borderRightWidth: StyleSheet.hairlineWidth,
// @ts-ignore web only // @ts-ignore web only
minHeight: '100vh', minHeight: '100vh',
}, },
@@ -13,7 +13,6 @@ import {clamp} from '#/lib/numbers'
import {colors} from '#/lib/styles' import {colors} from '#/lib/styles'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import hairlineWidth = StyleSheet.hairlineWidth
const AnimatedTouchableOpacity = const AnimatedTouchableOpacity =
Animated.createAnimatedComponent(TouchableOpacity) Animated.createAnimatedComponent(TouchableOpacity)
@@ -74,7 +73,7 @@ const styles = StyleSheet.create({
// @ts-ignore 'fixed' is web only -prf // @ts-ignore 'fixed' is web only -prf
position: isWeb ? 'fixed' : 'absolute', position: isWeb ? 'fixed' : 'absolute',
left: 18, left: 18,
borderWidth: hairlineWidth, borderWidth: StyleSheet.hairlineWidth,
width: 52, width: 52,
height: 52, height: 52,
borderRadius: 26, borderRadius: 26,
@@ -0,0 +1,48 @@
import React, {useCallback, useId, useMemo, useState} from 'react'
import {VideoPlayerProvider} from './VideoPlayerContext'
const ActiveVideoContext = React.createContext<{
activeViewId: string | null
setActiveView: (viewId: string, src: string) => void
} | null>(null)
export function ActiveVideoProvider({children}: {children: React.ReactNode}) {
const [activeViewId, setActiveViewId] = useState<string | null>(null)
const [source, setSource] = useState<string | null>(null)
const value = useMemo(
() => ({
activeViewId,
setActiveView: (viewId: string, src: string) => {
setActiveViewId(viewId)
setSource(src)
},
}),
[activeViewId],
)
return (
<ActiveVideoContext.Provider value={value}>
<VideoPlayerProvider source={source ?? ''} viewId={activeViewId}>
{children}
</VideoPlayerProvider>
</ActiveVideoContext.Provider>
)
}
export function useActiveVideoView() {
const context = React.useContext(ActiveVideoContext)
if (!context) {
throw new Error('useActiveVideo must be used within a ActiveVideoProvider')
}
const id = useId()
return {
active: context.activeViewId === id,
setActive: useCallback(
(source: string) => context.setActiveView(id, source),
[context, id],
),
}
}
+2 -3
View File
@@ -39,7 +39,6 @@ import {Link} from '../Link'
import {PostMeta} from '../PostMeta' import {PostMeta} from '../PostMeta'
import {Text} from '../text/Text' import {Text} from '../text/Text'
import {PostEmbeds} from '.' import {PostEmbeds} from '.'
import hairlineWidth = StyleSheet.hairlineWidth
export function MaybeQuoteEmbed({ export function MaybeQuoteEmbed({
embed, embed,
@@ -262,7 +261,7 @@ const styles = StyleSheet.create({
marginTop: 8, marginTop: 8,
paddingVertical: 12, paddingVertical: 12,
paddingHorizontal: 12, paddingHorizontal: 12,
borderWidth: hairlineWidth, borderWidth: StyleSheet.hairlineWidth,
}, },
errorContainer: { errorContainer: {
flexDirection: 'row', flexDirection: 'row',
@@ -272,7 +271,7 @@ const styles = StyleSheet.create({
marginTop: 8, marginTop: 8,
paddingVertical: 14, paddingVertical: 14,
paddingHorizontal: 14, paddingHorizontal: 14,
borderWidth: hairlineWidth, borderWidth: StyleSheet.hairlineWidth,
}, },
alert: { alert: {
marginBottom: 6, marginBottom: 6,
@@ -0,0 +1,44 @@
import React, {useCallback} from 'react'
import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {Play_Filled_Corner2_Rounded as PlayIcon} from '#/components/icons/Play'
import {useActiveVideoView} from './ActiveVideoContext'
import {VideoEmbedInner} from './VideoEmbedInner'
export function VideoEmbed({source}: {source: string}) {
const t = useTheme()
const {active, setActive} = useActiveVideoView()
const {_} = useLingui()
const onPress = useCallback(() => setActive(source), [setActive, source])
return (
<View
style={[
a.w_full,
a.rounded_sm,
{aspectRatio: 16 / 9},
a.overflow_hidden,
t.atoms.bg_contrast_25,
a.my_xs,
]}>
{active ? (
<VideoEmbedInner source={source} />
) : (
<Button
style={[a.flex_1, t.atoms.bg_contrast_25]}
onPress={onPress}
label={_(msg`Play video`)}
variant="ghost"
color="secondary"
size="large">
<ButtonIcon icon={PlayIcon} />
</Button>
)}
</View>
)
}
@@ -0,0 +1,138 @@
import React, {useCallback, useEffect, useRef, useState} from 'react'
import {Pressable, StyleSheet, useWindowDimensions, View} from 'react-native'
import Animated, {
measure,
runOnJS,
useAnimatedRef,
useFrameCallback,
useSharedValue,
} from 'react-native-reanimated'
import {VideoPlayer, VideoView} from 'expo-video'
import {atoms as a} from '#/alf'
import {Text} from '#/components/Typography'
import {useVideoPlayer} from './VideoPlayerContext'
export const VideoEmbedInner = ({}: {source: string}) => {
const player = useVideoPlayer()
const aref = useAnimatedRef<Animated.View>()
const {height: windowHeight} = useWindowDimensions()
const hasLeftView = useSharedValue(false)
const ref = useRef<VideoView>(null)
const onEnterView = useCallback(() => {
if (player.status === 'readyToPlay') {
player.play()
}
}, [player])
const onLeaveView = useCallback(() => {
player.pause()
}, [player])
const enterFullscreen = useCallback(() => {
if (ref.current) {
ref.current.enterFullscreen()
}
}, [])
useFrameCallback(() => {
const measurement = measure(aref)
if (measurement) {
if (hasLeftView.value) {
// Check if the video is in view
if (
measurement.pageY >= 0 &&
measurement.pageY + measurement.height <= windowHeight
) {
runOnJS(onEnterView)()
hasLeftView.value = false
}
} else {
// Check if the video is out of view
if (
measurement.pageY + measurement.height < 0 ||
measurement.pageY > windowHeight
) {
runOnJS(onLeaveView)()
hasLeftView.value = true
}
}
}
})
return (
<Animated.View
style={[a.flex_1, a.relative]}
ref={aref}
collapsable={false}>
<VideoView
ref={ref}
player={player}
style={a.flex_1}
nativeControls={true}
/>
<VideoControls player={player} enterFullscreen={enterFullscreen} />
</Animated.View>
)
}
function VideoControls({
player,
enterFullscreen,
}: {
player: VideoPlayer
enterFullscreen: () => void
}) {
const [currentTime, setCurrentTime] = useState(Math.floor(player.currentTime))
useEffect(() => {
const interval = setInterval(() => {
setCurrentTime(Math.floor(player.duration - player.currentTime))
// how often should we update the time?
// 1000 gets out of sync with the video time
}, 250)
return () => {
clearInterval(interval)
}
}, [player])
const minutes = Math.floor(currentTime / 60)
const seconds = String(currentTime % 60).padStart(2, '0')
return (
<View style={[a.absolute, a.inset_0]}>
<View style={styles.timeContainer} pointerEvents="none">
<Text style={styles.timeElapsed}>
{minutes}:{seconds}
</Text>
</View>
<Pressable
onPress={enterFullscreen}
style={a.flex_1}
accessibilityLabel="Video"
accessibilityHint="Tap to enter full screen"
accessibilityRole="button"
/>
</View>
)
}
const styles = StyleSheet.create({
timeContainer: {
backgroundColor: 'rgba(0, 0, 0, 0.75)',
borderRadius: 6,
paddingHorizontal: 6,
paddingVertical: 3,
position: 'absolute',
left: 5,
bottom: 5,
},
timeElapsed: {
color: 'white',
fontSize: 12,
fontWeight: 'bold',
},
})
@@ -0,0 +1,52 @@
import React, {useEffect, useRef} from 'react'
import Hls from 'hls.js'
import {atoms as a} from '#/alf'
export const VideoEmbedInner = ({source}: {source: string}) => {
const ref = useRef<HTMLVideoElement>(null)
// Use HLS.js to play HLS video
useEffect(() => {
if (ref.current) {
if (ref.current.canPlayType('application/vnd.apple.mpegurl')) {
ref.current.src = source
} else if (Hls.isSupported()) {
var hls = new Hls()
hls.loadSource(source)
hls.attachMedia(ref.current)
} else {
// TODO: fallback
}
}
}, [source])
useEffect(() => {
if (ref.current) {
const observer = new IntersectionObserver(
([entry]) => {
if (ref.current) {
if (entry.isIntersecting) {
if (ref.current.paused) {
ref.current.play()
}
} else {
if (!ref.current.paused) {
ref.current.pause()
}
}
}
},
{threshold: 0},
)
observer.observe(ref.current)
return () => {
observer.disconnect()
}
}
}, [])
return <video ref={ref} style={a.flex_1} controls playsInline autoPlay loop />
}
@@ -0,0 +1,41 @@
import React, {useContext, useEffect} from 'react'
import type {VideoPlayer} from 'expo-video'
import {useVideoPlayer as useExpoVideoPlayer} from 'expo-video'
const VideoPlayerContext = React.createContext<VideoPlayer | null>(null)
export function VideoPlayerProvider({
viewId,
source,
children,
}: {
viewId: string | null
source: string
children: React.ReactNode
}) {
// eslint-disable-next-line @typescript-eslint/no-shadow
const player = useExpoVideoPlayer(source, player => {
player.loop = true
player.play()
})
// make sure we're playing every time the viewId changes
// this means the video is different
useEffect(() => {
player.play()
}, [viewId, player])
return (
<VideoPlayerContext.Provider value={player}>
{children}
</VideoPlayerContext.Provider>
)
}
export function useVideoPlayer() {
const context = useContext(VideoPlayerContext)
if (!context) {
throw new Error('useVideoPlayer must be used within a VideoPlayerProvider')
}
return context
}
@@ -0,0 +1,9 @@
import React from 'react'
export function VideoPlayerProvider({children}: {children: React.ReactNode}) {
return children
}
export function useVideoPlayer() {
throw new Error('useVideoPlayer must not be used on web')
}
+3 -4
View File
@@ -19,18 +19,17 @@ import {
} from '@atproto/api' } from '@atproto/api'
import {ImagesLightbox, useLightboxControls} from '#/state/lightbox' import {ImagesLightbox, useLightboxControls} from '#/state/lightbox'
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
import {ContentHider} from '../../../../components/moderation/ContentHider' import {ContentHider} from '../../../../components/moderation/ContentHider'
import {AutoSizedImage} from '../images/AutoSizedImage' import {AutoSizedImage} from '../images/AutoSizedImage'
import {ImageLayoutGrid} from '../images/ImageLayoutGrid' import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
import {ExternalLinkEmbed} from './ExternalLinkEmbed' import {ExternalLinkEmbed} from './ExternalLinkEmbed'
import {ListEmbed} from './ListEmbed' import {ListEmbed} from './ListEmbed'
import {MaybeQuoteEmbed} from './QuoteEmbed' import {MaybeQuoteEmbed} from './QuoteEmbed'
import hairlineWidth = StyleSheet.hairlineWidth
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
type Embed = type Embed =
| AppBskyEmbedRecord.View | AppBskyEmbedRecord.View
@@ -205,7 +204,7 @@ const styles = StyleSheet.create({
fontWeight: 'bold', fontWeight: 'bold',
}, },
customFeedOuter: { customFeedOuter: {
borderWidth: hairlineWidth, borderWidth: StyleSheet.hairlineWidth,
borderRadius: 8, borderRadius: 8,
marginTop: 4, marginTop: 4,
paddingHorizontal: 12, paddingHorizontal: 12,
+5 -6
View File
@@ -33,14 +33,13 @@ import {ViewHeader} from 'view/com/util/ViewHeader'
import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed' import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType' import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Divider} from '#/components/Divider'
import * as FeedCard from '#/components/FeedCard'
import {IconCircle} from '#/components/IconCircle' import {IconCircle} from '#/components/IconCircle'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass' import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass'
import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle' import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle'
import hairlineWidth = StyleSheet.hairlineWidth
import {Divider} from '#/components/Divider'
import * as FeedCard from '#/components/FeedCard'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import * as ListCard from '#/components/ListCard' import * as ListCard from '#/components/ListCard'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Feeds'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'Feeds'>
@@ -793,13 +792,13 @@ const styles = StyleSheet.create({
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 14, paddingVertical: 14,
gap: 12, gap: 12,
borderBottomWidth: hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
}, },
savedFeedMobile: { savedFeedMobile: {
paddingVertical: 10, paddingVertical: 10,
}, },
offlineSlug: { offlineSlug: {
borderWidth: hairlineWidth, borderWidth: StyleSheet.hairlineWidth,
borderRadius: 4, borderRadius: 4,
paddingHorizontal: 4, paddingHorizontal: 4,
paddingVertical: 2, paddingVertical: 2,
+5 -3
View File
@@ -16,7 +16,6 @@ import {MyLists} from '#/view/com/lists/MyLists'
import {Button} from 'view/com/util/forms/Button' import {Button} from 'view/com/util/forms/Button'
import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader' import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import hairlineWidth = StyleSheet.hairlineWidth
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'>
export function ListsScreen({}: Props) { export function ListsScreen({}: Props) {
@@ -55,8 +54,11 @@ export function ListsScreen({}: Props) {
style={[ style={[
pal.border, pal.border,
isMobile isMobile
? {borderBottomWidth: hairlineWidth} ? {borderBottomWidth: StyleSheet.hairlineWidth}
: {borderLeftWidth: hairlineWidth, borderRightWidth: hairlineWidth}, : {
borderLeftWidth: StyleSheet.hairlineWidth,
borderRightWidth: StyleSheet.hairlineWidth,
},
]}> ]}>
<View style={{flex: 1}}> <View style={{flex: 1}}>
<Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}> <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
+2 -3
View File
@@ -65,7 +65,6 @@ import {useDialogControl} from '#/components/Dialog'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import hairlineWidth = StyleSheet.hairlineWidth
const SECTION_TITLES_CURATE = ['Posts', 'About'] const SECTION_TITLES_CURATE = ['Posts', 'About']
const SECTION_TITLES_MOD = ['About'] const SECTION_TITLES_MOD = ['About']
@@ -803,7 +802,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
<View <View
style={[ style={[
{ {
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
padding: isMobile ? 14 : 20, padding: isMobile ? 14 : 20,
gap: 12, gap: 12,
}, },
@@ -954,7 +953,7 @@ function ErrorScreen({error}: {error: string}) {
marginTop: 10, marginTop: 10,
paddingHorizontal: 18, paddingHorizontal: 18,
paddingVertical: 14, paddingVertical: 14,
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
}, },
]}> ]}>
<Text type="title-lg" style={[pal.text, s.mb10]}> <Text type="title-lg" style={[pal.text, s.mb10]}>
+6 -5
View File
@@ -32,7 +32,6 @@ import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType' import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
import hairlineWidth = StyleSheet.hairlineWidth
const HITSLOP_TOP = { const HITSLOP_TOP = {
top: 20, top: 20,
@@ -93,7 +92,8 @@ export function SavedFeeds({}: Props) {
<ViewHeader title={_(msg`Edit My Feeds`)} showOnDesktop showBorder /> <ViewHeader title={_(msg`Edit My Feeds`)} showOnDesktop showBorder />
<ScrollView style={s.flex1} contentContainerStyle={[styles.noBorder]}> <ScrollView style={s.flex1} contentContainerStyle={[styles.noBorder]}>
{noSavedFeedsOfAnyType && ( {noSavedFeedsOfAnyType && (
<View style={[pal.border, {borderBottomWidth: hairlineWidth}]}> <View
style={[pal.border, {borderBottomWidth: StyleSheet.hairlineWidth}]}>
<NoSavedFeedsOfAnyType /> <NoSavedFeedsOfAnyType />
</View> </View>
)} )}
@@ -135,7 +135,8 @@ export function SavedFeeds({}: Props) {
)} )}
{noFollowingFeed && ( {noFollowingFeed && (
<View style={[pal.border, {borderBottomWidth: hairlineWidth}]}> <View
style={[pal.border, {borderBottomWidth: StyleSheet.hairlineWidth}]}>
<NoFollowingFeed /> <NoFollowingFeed />
</View> </View>
)} )}
@@ -437,12 +438,12 @@ const styles = StyleSheet.create({
paddingHorizontal: 14, paddingHorizontal: 14,
paddingTop: 20, paddingTop: 20,
paddingBottom: 10, paddingBottom: 10,
borderBottomWidth: hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
}, },
itemContainer: { itemContainer: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
borderBottomWidth: hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
}, },
footerText: { footerText: {
paddingHorizontal: 26, paddingHorizontal: 26,
+9 -8
View File
@@ -75,17 +75,17 @@ function SuggestedItemsHeader({
) )
} }
type LoadMoreItems = type LoadMoreItem =
| { | {
type: 'profile' type: 'profile'
key: string key: string
avatar: string avatar: string | undefined
moderation: ModerationDecision moderation: ModerationDecision
} }
| { | {
type: 'feed' type: 'feed'
key: string key: string
avatar: string avatar: string | undefined
moderation: undefined moderation: undefined
} }
@@ -98,27 +98,28 @@ function LoadMore({
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const items = React.useMemo(() => { const items: LoadMoreItem[] = React.useMemo(() => {
return item.items return item.items
.map(_item => { .map(_item => {
let loadMoreItem: LoadMoreItem | undefined
if (_item.type === 'profile') { if (_item.type === 'profile') {
return { loadMoreItem = {
type: 'profile', type: 'profile',
key: _item.profile.did, key: _item.profile.did,
avatar: _item.profile.avatar, avatar: _item.profile.avatar,
moderation: moderateProfile(_item.profile, moderationOpts!), moderation: moderateProfile(_item.profile, moderationOpts!),
} }
} else if (_item.type === 'feed') { } else if (_item.type === 'feed') {
return { loadMoreItem = {
type: 'feed', type: 'feed',
key: _item.feed.uri, key: _item.feed.uri,
avatar: _item.feed.avatar, avatar: _item.feed.avatar,
moderation: undefined, moderation: undefined,
} }
} }
return undefined return loadMoreItem
}) })
.filter(Boolean) as LoadMoreItems[] .filter(n => !!n)
}, [item.items, moderationOpts]) }, [item.items, moderationOpts])
if (items.length === 0) return null if (items.length === 0) return null
@@ -94,6 +94,7 @@ export function ExportCarDialog({
This feature is in beta. You can read more about repository This feature is in beta. You can read more about repository
exports in{' '} exports in{' '}
<InlineLinkText <InlineLinkText
label={_(msg`View blogpost for more details`)}
to="https://docs.bsky.app/blog/repo-export" to="https://docs.bsky.app/blog/repo-export"
style={[a.text_sm]}> style={[a.text_sm]}>
this blogpost this blogpost
+2 -3
View File
@@ -62,13 +62,12 @@ import {UserAvatar} from 'view/com/util/UserAvatar'
import {ScrollView} from 'view/com/util/Views' import {ScrollView} from 'view/com/util/Views'
import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog' import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog'
import {useTheme} from '#/alf' import {useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {useDialogControl} from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog'
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
import {navigate, resetToTab} from '#/Navigation' import {navigate, resetToTab} from '#/Navigation'
import {Email2FAToggle} from './Email2FAToggle' import {Email2FAToggle} from './Email2FAToggle'
import {ExportCarDialog} from './ExportCarDialog' import {ExportCarDialog} from './ExportCarDialog'
import hairlineWidth = StyleSheet.hairlineWidth
import {atoms as a} from '#/alf'
function SettingsAccountCard({ function SettingsAccountCard({
account, account,
@@ -326,7 +325,7 @@ export function SettingsScreen({}: Props) {
showBackButton={isMobile} showBackButton={isMobile}
style={[ style={[
pal.border, pal.border,
{borderBottomWidth: hairlineWidth}, {borderBottomWidth: StyleSheet.hairlineWidth},
!isMobile && {borderLeftWidth: 1, borderRightWidth: 1}, !isMobile && {borderLeftWidth: 1, borderRightWidth: 1},
]}> ]}>
<View style={{flex: 1}}> <View style={{flex: 1}}>
+4 -2
View File
@@ -13,18 +13,20 @@ export function Links() {
<H1>Links</H1> <H1>Links</H1>
<View style={[a.gap_md, a.align_start]}> <View style={[a.gap_md, a.align_start]}>
<InlineLinkText to="https://google.com" style={[a.text_lg]}> <InlineLinkText label="foo" to="https://google.com" style={[a.text_lg]}>
https://google.com https://google.com
</InlineLinkText> </InlineLinkText>
<InlineLinkText to="https://google.com" style={[a.text_lg]}> <InlineLinkText label="foo" to="https://google.com" style={[a.text_lg]}>
External with custom children (google.com) External with custom children (google.com)
</InlineLinkText> </InlineLinkText>
<InlineLinkText <InlineLinkText
label="foo"
to="https://bsky.social" to="https://bsky.social"
style={[a.text_md, t.atoms.text_contrast_low]}> style={[a.text_md, t.atoms.text_contrast_low]}>
Internal (bsky.social) Internal (bsky.social)
</InlineLinkText> </InlineLinkText>
<InlineLinkText <InlineLinkText
label="foo"
to="https://bsky.app/profile/bsky.app" to="https://bsky.app/profile/bsky.app"
style={[a.text_md]}> style={[a.text_md]}>
Internal (bsky.app) Internal (bsky.app)
@@ -1,7 +1,6 @@
import {StyleSheet} from 'react-native' import {StyleSheet} from 'react-native'
import {colors} from 'lib/styles' import {colors} from 'lib/styles'
import hairlineWidth = StyleSheet.hairlineWidth
export const styles = StyleSheet.create({ export const styles = StyleSheet.create({
bottomBar: { bottomBar: {
@@ -10,7 +9,7 @@ export const styles = StyleSheet.create({
left: 0, left: 0,
right: 0, right: 0,
flexDirection: 'row', flexDirection: 'row',
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
paddingLeft: 5, paddingLeft: 5,
paddingRight: 10, paddingRight: 10,
}, },
+3 -4
View File
@@ -11,10 +11,9 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {TextLink} from 'view/com/util/Link' import {TextLink} from 'view/com/util/Link'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {ProgressGuideList} from '#/components/ProgressGuide/List'
import {DesktopFeeds} from './Feeds' import {DesktopFeeds} from './Feeds'
import {DesktopSearch} from './Search' import {DesktopSearch} from './Search'
import hairlineWidth = StyleSheet.hairlineWidth
import {ProgressGuideList} from '#/components/ProgressGuide/List'
export function DesktopRightNav({routeName}: {routeName: string}) { export function DesktopRightNav({routeName}: {routeName: string}) {
const pal = usePalette('default') const pal = usePalette('default')
@@ -135,8 +134,8 @@ const styles = StyleSheet.create({
marginBottom: 10, marginBottom: 10,
}, },
desktopFeedsContainer: { desktopFeedsContainer: {
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
borderBottomWidth: hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
marginTop: 18, marginTop: 18,
marginBottom: 18, marginBottom: 18,
}, },
+9 -4
View File
@@ -13345,6 +13345,11 @@ history@^5.3.0:
dependencies: dependencies:
"@babel/runtime" "^7.7.6" "@babel/runtime" "^7.7.6"
hls.js@^1.5.11:
version "1.5.11"
resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.5.11.tgz#3941347df454983859ae8c75fe19e8818719a826"
integrity sha512-q3We1izi2+qkOO+TvZdHv+dx6aFzdtk3xc1/Qesrvto4thLTT/x/1FK85c5h1qZE4MmMBNgKg+MIW8nxQfxwBw==
hmac-drbg@^1.0.1: hmac-drbg@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -21373,10 +21378,10 @@ typedarray-to-buffer@^3.1.5:
dependencies: dependencies:
is-typedarray "^1.0.0" is-typedarray "^1.0.0"
typescript@^5.3.3: typescript@^5.5.4:
version "5.3.3" version "5.5.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
ua-parser-js@^0.7.33: ua-parser-js@^0.7.33:
version "0.7.35" version "0.7.35"