Merge branch 'main' into ja-translation-14
This commit is contained in:
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.89.0",
|
||||
"version": "1.90.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -143,6 +143,7 @@
|
||||
"expo-web-browser": "~13.0.3",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
"hls.js": "^1.5.11",
|
||||
"js-sha256": "^0.9.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lande": "^1.0.10",
|
||||
@@ -269,7 +270,7 @@
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript": "^5.5.4",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.10.1",
|
||||
|
||||
@@ -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
@@ -23,10 +23,12 @@ import {
|
||||
} from '#/lib/statsig/statsig'
|
||||
import {s} from '#/lib/styles'
|
||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import I18nProvider from '#/locale/i18nProvider'
|
||||
import {logger} from '#/logger'
|
||||
import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||
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 StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
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 {Shell} from '#/view/shell'
|
||||
import {ThemeProvider as Alf} from '#/alf'
|
||||
@@ -58,8 +61,6 @@ import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Splash} from '#/Splash'
|
||||
import {Provider as TourProvider} from '#/tours'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
import I18nProvider from './locale/i18nProvider'
|
||||
import {listenSessionDropped} from './state/events'
|
||||
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
|
||||
@@ -107,42 +108,44 @@ function InnerApp() {
|
||||
<Alf theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Splash isReady={isReady && hasCheckedReferrer}>
|
||||
<RootSiblingParent>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<TourProvider>
|
||||
<ProgressGuideProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</ProgressGuideProvider>
|
||||
</TourProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
</RootSiblingParent>
|
||||
<ActiveVideoProvider>
|
||||
<RootSiblingParent>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<TourProvider>
|
||||
<ProgressGuideProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</ProgressGuideProvider>
|
||||
</TourProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
</RootSiblingParent>
|
||||
</ActiveVideoProvider>
|
||||
</Splash>
|
||||
</ThemeProvider>
|
||||
</Alf>
|
||||
|
||||
+38
-35
@@ -12,10 +12,12 @@ import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
|
||||
import {QueryProvider} from '#/lib/react-query'
|
||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import I18nProvider from '#/locale/i18nProvider'
|
||||
import {logger} from '#/logger'
|
||||
import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||
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 SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
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 {ToastContainer} from '#/view/com/util/Toast.web'
|
||||
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 TourProvider} from '#/tours'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
import I18nProvider from './locale/i18nProvider'
|
||||
import {listenSessionDropped} from './state/events'
|
||||
|
||||
function InnerApp() {
|
||||
const [isReady, setIsReady] = React.useState(false)
|
||||
@@ -92,39 +93,41 @@ function InnerApp() {
|
||||
<Alf theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<RootSiblingParent>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<TourProvider>
|
||||
<ProgressGuideProvider>
|
||||
<Shell />
|
||||
</ProgressGuideProvider>
|
||||
</TourProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
<ToastContainer />
|
||||
<ActiveVideoProvider>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<TourProvider>
|
||||
<ProgressGuideProvider>
|
||||
<Shell />
|
||||
</ProgressGuideProvider>
|
||||
</TourProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
<ToastContainer />
|
||||
</ActiveVideoProvider>
|
||||
</RootSiblingParent>
|
||||
</ThemeProvider>
|
||||
</Alf>
|
||||
|
||||
+5
-6
@@ -2,7 +2,6 @@ import {Platform, StyleSheet} from 'react-native'
|
||||
|
||||
import * as tokens from '#/alf/tokens'
|
||||
import {native, web} from '#/alf/util/platform'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
export const atoms = {
|
||||
/*
|
||||
@@ -284,19 +283,19 @@ export const atoms = {
|
||||
borderWidth: 0,
|
||||
},
|
||||
border: {
|
||||
borderWidth: hairlineWidth,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
border_t: {
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
border_b: {
|
||||
borderBottomWidth: hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
border_l: {
|
||||
borderLeftWidth: hairlineWidth,
|
||||
borderLeftWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
border_r: {
|
||||
borderRightWidth: hairlineWidth,
|
||||
borderRightWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
|
||||
/*
|
||||
|
||||
@@ -70,6 +70,9 @@ export type ButtonProps = Pick<
|
||||
AccessibilityProps &
|
||||
VariantProps & {
|
||||
testID?: string
|
||||
/**
|
||||
* For a11y, try to make this descriptive and clear
|
||||
*/
|
||||
label: string
|
||||
style?: StyleProp<ViewStyle>
|
||||
hoverStyle?: StyleProp<ViewStyle>
|
||||
|
||||
@@ -40,7 +40,7 @@ type Props = {
|
||||
export function Default(props: Props) {
|
||||
const {view} = props
|
||||
return (
|
||||
<Link label={view.displayName} {...props}>
|
||||
<Link {...props}>
|
||||
<Outer>
|
||||
<Header>
|
||||
<Avatar src={view.avatar} />
|
||||
@@ -58,7 +58,7 @@ export function Link({
|
||||
view,
|
||||
children,
|
||||
...props
|
||||
}: Props & Omit<LinkProps, 'to'>) {
|
||||
}: Props & Omit<LinkProps, 'to' | 'label'>) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const href = React.useMemo(() => {
|
||||
@@ -70,7 +70,11 @@ export function Link({
|
||||
}, [view, queryClient])
|
||||
|
||||
return (
|
||||
<InternalLink to={href} style={[a.flex_col]} {...props}>
|
||||
<InternalLink
|
||||
label={view.displayName}
|
||||
to={href}
|
||||
style={[a.flex_col]}
|
||||
{...props}>
|
||||
{children}
|
||||
</InternalLink>
|
||||
)
|
||||
|
||||
@@ -190,7 +190,7 @@ export function SuggestedFollows() {
|
||||
{profiles.slice(0, maxLength).map(profile => (
|
||||
<ProfileCard.Link
|
||||
key={profile.did}
|
||||
did={profile.handle}
|
||||
profile={profile}
|
||||
onPress={() => {
|
||||
logEvent('feed:interstitial:profileCard:press', {})
|
||||
}}
|
||||
@@ -266,7 +266,10 @@ export function SuggestedFollows() {
|
||||
a.pt_xs,
|
||||
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>
|
||||
</InlineLinkText>
|
||||
<Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
|
||||
@@ -396,7 +399,10 @@ export function SuggestedFeeds() {
|
||||
a.pt_xs,
|
||||
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>
|
||||
</InlineLinkText>
|
||||
<Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
|
||||
|
||||
@@ -40,11 +40,6 @@ type BaseLinkProps = Pick<
|
||||
> & {
|
||||
testID?: string
|
||||
|
||||
/**
|
||||
* Label for a11y. Defaults to the href.
|
||||
*/
|
||||
label?: string
|
||||
|
||||
/**
|
||||
* The React Navigation `StackAction` to perform when the link is pressed.
|
||||
*/
|
||||
@@ -197,7 +192,7 @@ export function useLink({
|
||||
}
|
||||
|
||||
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
|
||||
@@ -224,7 +219,6 @@ export function Link({
|
||||
|
||||
return (
|
||||
<Button
|
||||
label={href}
|
||||
{...rest}
|
||||
style={[a.justify_start, flatten(rest.style)]}
|
||||
role="link"
|
||||
@@ -249,7 +243,8 @@ export function Link({
|
||||
|
||||
export type InlineLinkProps = React.PropsWithChildren<
|
||||
BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
|
||||
>
|
||||
> &
|
||||
Pick<ButtonProps, 'label'>
|
||||
|
||||
export function InlineLinkText({
|
||||
children,
|
||||
@@ -291,7 +286,7 @@ export function InlineLinkText({
|
||||
<Text
|
||||
selectable={selectable}
|
||||
accessibilityHint=""
|
||||
accessibilityLabel={label || href}
|
||||
accessibilityLabel={label}
|
||||
{...rest}
|
||||
style={[
|
||||
{color: t.palette.primary_500},
|
||||
|
||||
@@ -44,7 +44,7 @@ type Props = {
|
||||
export function Default(props: Props) {
|
||||
const {view, showPinButton} = props
|
||||
return (
|
||||
<Link label={view.name} {...props}>
|
||||
<Link {...props}>
|
||||
<Outer>
|
||||
<Header>
|
||||
<Avatar src={view.avatar} />
|
||||
@@ -67,7 +67,7 @@ export function Link({
|
||||
view,
|
||||
children,
|
||||
...props
|
||||
}: Props & Omit<LinkProps, 'to'>) {
|
||||
}: Props & Omit<LinkProps, 'to' | 'label'>) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const href = React.useMemo(() => {
|
||||
@@ -79,7 +79,7 @@ export function Link({
|
||||
}, [view, queryClient])
|
||||
|
||||
return (
|
||||
<InternalLink to={href} {...props}>
|
||||
<InternalLink label={view.name} to={href} {...props}>
|
||||
{children}
|
||||
</InternalLink>
|
||||
)
|
||||
|
||||
@@ -36,7 +36,7 @@ export function Default({
|
||||
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
||||
}) {
|
||||
return (
|
||||
<Link did={profile.did}>
|
||||
<Link profile={profile}>
|
||||
<Card
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
@@ -96,16 +96,24 @@ export function Header({
|
||||
}
|
||||
|
||||
export function Link({
|
||||
did,
|
||||
profile,
|
||||
children,
|
||||
style,
|
||||
...rest
|
||||
}: {did: string} & Omit<LinkProps, 'to'>) {
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
} & Omit<LinkProps, 'to' | 'label'>) {
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<InternalLink
|
||||
label={_(
|
||||
msg`View ${
|
||||
profile.displayName || sanitizeHandle(profile.handle)
|
||||
}'s profile`,
|
||||
)}
|
||||
to={{
|
||||
screen: 'Profile',
|
||||
params: {name: did},
|
||||
params: {name: profile.did},
|
||||
}}
|
||||
style={[a.flex_col, style]}
|
||||
{...rest}>
|
||||
|
||||
@@ -74,7 +74,9 @@ function DialogContent({
|
||||
|
||||
const onPressAudience = (setting: ThreadgateSetting) => {
|
||||
// remove nobody
|
||||
let newSelected = draft.filter(v => v.type !== 'nobody')
|
||||
let newSelected: ThreadgateSetting[] = draft.filter(
|
||||
v => v.type !== 'nobody',
|
||||
)
|
||||
// toggle
|
||||
const i = newSelected.findIndex(v => isEqual(v, setting))
|
||||
if (i === -1) {
|
||||
|
||||
@@ -42,6 +42,7 @@ export function BlockedByListDialog({
|
||||
<React.Fragment key={block.source.list.uri}>
|
||||
{i === 0 ? null : ', '}
|
||||
<InlineLinkText
|
||||
label={block.source.list.name}
|
||||
to={listUriToHref(block.source.list.uri)}
|
||||
style={[a.text_md, a.leading_snug]}>
|
||||
{block.source.list.name}
|
||||
|
||||
@@ -140,6 +140,7 @@ function HeaderReady({
|
||||
userBlock?: ModerationCause
|
||||
}
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const convoState = useConvo()
|
||||
const profile = useProfileShadow(profileUnshadowed)
|
||||
@@ -156,6 +157,7 @@ function HeaderReady({
|
||||
<View style={[a.flex_1]}>
|
||||
<View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}>
|
||||
<Link
|
||||
label={_(msg`View ${displayName}'s profile`)}
|
||||
style={[a.flex_row, a.align_start, a.gap_md, a.flex_1, a.pr_md]}
|
||||
to={makeProfileLink(profile)}>
|
||||
<View style={[a.pt_2xs]}>
|
||||
|
||||
@@ -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
|
||||
labelerDid?: string
|
||||
}) {
|
||||
const {i18n} = useLingui()
|
||||
const {_, i18n} = useLingui()
|
||||
const t = useTheme()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
|
||||
@@ -243,7 +243,10 @@ export function LabelerLabelPreference({
|
||||
) : isGlobalLabel ? (
|
||||
<Trans>
|
||||
Configured in{' '}
|
||||
<InlineLinkText to="/moderation" style={a.text_sm}>
|
||||
<InlineLinkText
|
||||
label={_(msg`moderation settings`)}
|
||||
to="/moderation"
|
||||
style={a.text_sm}>
|
||||
moderation settings
|
||||
</InlineLinkText>
|
||||
.
|
||||
|
||||
@@ -128,6 +128,9 @@ function Label({
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {labeler, strings} = useLabelInfo(label)
|
||||
const sourceName = labeler
|
||||
? sanitizeHandle(labeler.creator.handle, '@')
|
||||
: label.src
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -169,13 +172,12 @@ function Label({
|
||||
<Trans>
|
||||
Source:{' '}
|
||||
<InlineLinkText
|
||||
label={sourceName}
|
||||
to={makeProfileLink(
|
||||
labeler ? labeler.creator : {did: label.src, handle: ''},
|
||||
)}
|
||||
onPress={() => control.close()}>
|
||||
{labeler
|
||||
? sanitizeHandle(labeler.creator.handle, '@')
|
||||
: label.src}
|
||||
{sourceName}
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
)}
|
||||
@@ -203,6 +205,9 @@ function AppealForm({
|
||||
const isAccountReport = 'did' in subject
|
||||
const agent = useAgent()
|
||||
const gate = useGate()
|
||||
const sourceName = labeler
|
||||
? sanitizeHandle(labeler.creator.handle, '@')
|
||||
: label.src
|
||||
|
||||
const {mutate, isPending} = useMutation({
|
||||
mutationFn: async () => {
|
||||
@@ -260,12 +265,13 @@ function AppealForm({
|
||||
<Trans>
|
||||
This appeal will be sent to{' '}
|
||||
<InlineLinkText
|
||||
label={sourceName}
|
||||
to={makeProfileLink(
|
||||
labeler ? labeler.creator : {did: label.src, handle: ''},
|
||||
)}
|
||||
onPress={() => control.close()}
|
||||
style={[a.text_md, a.leading_snug]}>
|
||||
{labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src}
|
||||
{sourceName}
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Trans>
|
||||
|
||||
@@ -54,7 +54,10 @@ function ModerationDetailsDialogInner({
|
||||
description = (
|
||||
<Trans>
|
||||
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}
|
||||
</InlineLinkText>{' '}
|
||||
list which you have blocked.
|
||||
@@ -83,7 +86,10 @@ function ModerationDetailsDialogInner({
|
||||
description = (
|
||||
<Trans>
|
||||
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}
|
||||
</InlineLinkText>{' '}
|
||||
list which you have muted.
|
||||
@@ -127,10 +133,11 @@ function ModerationDetailsDialogInner({
|
||||
<Trans>
|
||||
This label was applied by{' '}
|
||||
<InlineLinkText
|
||||
label={desc.source || _(msg`an unknown labeler`)}
|
||||
to={makeProfileLink({did: modcause.label.src, handle: ''})}
|
||||
onPress={() => control.close()}
|
||||
style={a.text_md}>
|
||||
{desc.source}
|
||||
{desc.source || _(msg`an unknown labeler`)}
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Trans>
|
||||
|
||||
+70
-61
@@ -14,29 +14,33 @@ export type FeedTunerFn = (
|
||||
slices: 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 {
|
||||
_reactKey: string
|
||||
isFlattenedReply = false
|
||||
_feedPost: FeedViewPost
|
||||
items: FeedSliceItem[]
|
||||
|
||||
constructor(public items: FeedViewPost[]) {
|
||||
const item = items[0]
|
||||
this._reactKey = `slice-${item.post.uri}-${
|
||||
item.reason?.indexedAt || item.post.indexedAt
|
||||
constructor(feedPost: FeedViewPost) {
|
||||
this._feedPost = feedPost
|
||||
this._reactKey = `slice-${feedPost.post.uri}-${
|
||||
feedPost.reason?.indexedAt || feedPost.post.indexedAt
|
||||
}`
|
||||
this.items = [toSliceItem(feedPost)]
|
||||
}
|
||||
|
||||
get uri() {
|
||||
if (this.isFlattenedReply) {
|
||||
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
|
||||
return this._feedPost.post.uri
|
||||
}
|
||||
|
||||
get isThread() {
|
||||
@@ -48,31 +52,42 @@ export class FeedViewPostsSlice {
|
||||
)
|
||||
}
|
||||
|
||||
get isFullThread() {
|
||||
return this.isThread && !this.items[0].reply
|
||||
}
|
||||
|
||||
get rootItem() {
|
||||
if (this.isFlattenedReply) {
|
||||
return this.items[1]
|
||||
}
|
||||
return this.items[0]
|
||||
get isQuotePost() {
|
||||
const embed = this._feedPost.post.embed
|
||||
return (
|
||||
AppBskyEmbedRecord.isView(embed) ||
|
||||
AppBskyEmbedRecordWithMedia.isView(embed)
|
||||
)
|
||||
}
|
||||
|
||||
get isReply() {
|
||||
return (
|
||||
AppBskyFeedPost.isRecord(this.rootItem.post.record) &&
|
||||
!!this.rootItem.post.record.reply
|
||||
AppBskyFeedPost.isRecord(this._feedPost.post.record) &&
|
||||
!!this._feedPost.post.record.reply
|
||||
)
|
||||
}
|
||||
|
||||
get source(): ReasonFeedSource | undefined {
|
||||
return this.items.find(item => '__source' in item && !!item.__source)
|
||||
?.__source as ReasonFeedSource
|
||||
get reason() {
|
||||
return '__source' in this._feedPost
|
||||
? (this._feedPost.__source as ReasonFeedSource)
|
||||
: this._feedPost.reason
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -97,24 +112,24 @@ export class FeedViewPostsSlice {
|
||||
if (this.items[0].reply) {
|
||||
const reply = this.items[0].reply
|
||||
if (AppBskyFeedDefs.isPostView(reply.parent)) {
|
||||
this.isFlattenedReply = true
|
||||
this.items.splice(0, 0, {post: reply.parent})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isFollowingAllAuthors(userDid: string) {
|
||||
const item = this.rootItem
|
||||
if (item.post.author.did === userDid) {
|
||||
const feedPost = this._feedPost
|
||||
if (feedPost.post.author.did === userDid) {
|
||||
return true
|
||||
}
|
||||
if (AppBskyFeedDefs.isPostView(item.reply?.parent)) {
|
||||
const parent = item.reply?.parent
|
||||
if (AppBskyFeedDefs.isPostView(feedPost.reply?.parent)) {
|
||||
const parent = feedPost.reply?.parent
|
||||
if (parent?.author.did === userDid) {
|
||||
return true
|
||||
}
|
||||
return (
|
||||
parent?.author.viewer?.following && item.post.author.viewer?.following
|
||||
parent?.author.viewer?.following &&
|
||||
feedPost.post.author.viewer?.following
|
||||
)
|
||||
}
|
||||
return false
|
||||
@@ -127,7 +142,7 @@ export class NoopFeedTuner {
|
||||
feed: FeedViewPost[],
|
||||
_opts?: {dryRun: boolean; maintainOrder: boolean},
|
||||
): FeedViewPostsSlice[] {
|
||||
return feed.map(item => new FeedViewPostsSlice([item]))
|
||||
return feed.map(item => new FeedViewPostsSlice(item))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +180,7 @@ export class FeedTuner {
|
||||
})
|
||||
|
||||
if (maintainOrder) {
|
||||
slices = feed.map(item => new FeedViewPostsSlice([item]))
|
||||
slices = feed.map(item => new FeedViewPostsSlice(item))
|
||||
} else {
|
||||
// arrange the posts into thread slices
|
||||
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
|
||||
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
|
||||
if (
|
||||
AppBskyFeedDefs.isPostView(reply.parent) &&
|
||||
@@ -256,8 +271,7 @@ export class FeedTuner {
|
||||
|
||||
static removeReposts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) {
|
||||
for (let i = slices.length - 1; i >= 0; i--) {
|
||||
const reason = slices[i].rootItem.reason
|
||||
if (AppBskyFeedDefs.isReasonRepost(reason)) {
|
||||
if (slices[i].isRepost) {
|
||||
slices.splice(i, 1)
|
||||
}
|
||||
}
|
||||
@@ -266,11 +280,7 @@ export class FeedTuner {
|
||||
|
||||
static removeQuotePosts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) {
|
||||
for (let i = slices.length - 1; i >= 0; i--) {
|
||||
const embed = slices[i].rootItem.post.embed
|
||||
if (
|
||||
AppBskyEmbedRecord.isView(embed) ||
|
||||
AppBskyEmbedRecordWithMedia.isView(embed)
|
||||
) {
|
||||
if (slices[i].isQuotePost) {
|
||||
slices.splice(i, 1)
|
||||
}
|
||||
}
|
||||
@@ -315,19 +325,18 @@ export class FeedTuner {
|
||||
// remove any replies without at least minLikes likes
|
||||
for (let i = slices.length - 1; i >= 0; i--) {
|
||||
const slice = slices[i]
|
||||
if (slice.isFullThread || !slice.isReply) {
|
||||
continue
|
||||
}
|
||||
|
||||
const item = slice.rootItem
|
||||
const isRepost = Boolean(item.reason)
|
||||
if (isRepost) {
|
||||
continue
|
||||
}
|
||||
if ((item.post.likeCount || 0) < minLikes) {
|
||||
slices.splice(i, 1)
|
||||
} else if (followedOnly && !slice.isFollowingAllAuthors(userDid)) {
|
||||
slices.splice(i, 1)
|
||||
if (slice.isReply) {
|
||||
if (slice.isThread && slice.includesThreadRoot) {
|
||||
continue
|
||||
}
|
||||
if (slice.isRepost) {
|
||||
continue
|
||||
}
|
||||
if (slice.likeCount < minLikes) {
|
||||
slices.splice(i, 1)
|
||||
} else if (followedOnly && !slice.isFollowingAllAuthors(userDid)) {
|
||||
slices.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return slices
|
||||
|
||||
@@ -251,7 +251,7 @@ class MergeFeedSource_Following extends MergeFeedSource {
|
||||
dryRun: false,
|
||||
maintainOrder: true,
|
||||
})
|
||||
res.data.feed = slices.map(slice => slice.rootItem)
|
||||
res.data.feed = slices.map(slice => slice._feedPost)
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,7 +240,10 @@ export function ModerationScreenInner({
|
||||
)}
|
||||
</Button>
|
||||
<Divider />
|
||||
<Link testID="moderationlistsBtn" to="/moderation/modlists">
|
||||
<Link
|
||||
label={_(msg`View your moderation lists`)}
|
||||
testID="moderationlistsBtn"
|
||||
to="/moderation/modlists">
|
||||
{state => (
|
||||
<SubItem
|
||||
title={_(msg`Moderation lists`)}
|
||||
@@ -252,7 +255,10 @@ export function ModerationScreenInner({
|
||||
)}
|
||||
</Link>
|
||||
<Divider />
|
||||
<Link testID="mutedAccountsBtn" to="/moderation/muted-accounts">
|
||||
<Link
|
||||
label={_(msg`View your muted accounts`)}
|
||||
testID="mutedAccountsBtn"
|
||||
to="/moderation/muted-accounts">
|
||||
{state => (
|
||||
<SubItem
|
||||
title={_(msg`Muted accounts`)}
|
||||
@@ -264,7 +270,10 @@ export function ModerationScreenInner({
|
||||
)}
|
||||
</Link>
|
||||
<Divider />
|
||||
<Link testID="blockedAccountsBtn" to="/moderation/blocked-accounts">
|
||||
<Link
|
||||
label={_(msg`View your blocked accounts`)}
|
||||
testID="blockedAccountsBtn"
|
||||
to="/moderation/blocked-accounts">
|
||||
{state => (
|
||||
<SubItem
|
||||
title={_(msg`Blocked accounts`)}
|
||||
@@ -356,6 +365,7 @@ export function ModerationScreenInner({
|
||||
<Trans>
|
||||
Adult content can only be enabled via the Web at{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`The Bluesky web application`)}
|
||||
to=""
|
||||
onPress={evt => {
|
||||
evt.preventDefault()
|
||||
@@ -569,7 +579,9 @@ function PwiOptOut() {
|
||||
</Trans>
|
||||
</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>
|
||||
</InlineLinkText>
|
||||
</View>
|
||||
|
||||
@@ -45,14 +45,20 @@ export const Policies = ({
|
||||
const els = []
|
||||
if (tos) {
|
||||
els.push(
|
||||
<InlineLinkText key="tos" to={tos}>
|
||||
<InlineLinkText
|
||||
label={_(msg`Read the Bluesky Terms of Service`)}
|
||||
key="tos"
|
||||
to={tos}>
|
||||
{_(msg`Terms of Service`)}
|
||||
</InlineLinkText>,
|
||||
)
|
||||
}
|
||||
if (pp) {
|
||||
els.push(
|
||||
<InlineLinkText key="pp" to={pp}>
|
||||
<InlineLinkText
|
||||
label={_(msg`Read the Bluesky Privacy Policy`)}
|
||||
key="pp"
|
||||
to={pp}>
|
||||
{_(msg`Privacy Policy`)}
|
||||
</InlineLinkText>,
|
||||
)
|
||||
|
||||
@@ -166,6 +166,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
|
||||
<Text style={[t.atoms.text, !gtMobile && a.text_md]}>
|
||||
<Trans>Having trouble?</Trans>{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`Contact support`)}
|
||||
to={FEEDBACK_FORM_URL({email: state.email})}
|
||||
style={[!gtMobile && a.text_md]}>
|
||||
<Trans>Contact support</Trans>
|
||||
|
||||
@@ -78,7 +78,10 @@ export interface FeedPostSliceItem {
|
||||
uri: string
|
||||
post: AppBskyFeedDefs.PostView
|
||||
record: AppBskyFeedPost.Record
|
||||
reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource
|
||||
reason?:
|
||||
| AppBskyFeedDefs.ReasonRepost
|
||||
| ReasonFeedSource
|
||||
| {[k: string]: unknown; $type: string}
|
||||
feedContext: string | undefined
|
||||
moderation: ModerationDecision
|
||||
parentAuthor?: AppBskyActorDefs.ProfileViewBasic
|
||||
@@ -311,7 +314,7 @@ export function usePostFeedQuery(
|
||||
if (isDiscover) {
|
||||
userActionHistory.seen(
|
||||
slice.items.map(item => ({
|
||||
feedContext: item.feedContext,
|
||||
feedContext: slice.feedContext,
|
||||
likeCount: item.post.likeCount ?? 0,
|
||||
repostCount: item.post.repostCount ?? 0,
|
||||
replyCount: item.post.replyCount ?? 0,
|
||||
@@ -323,10 +326,10 @@ export function usePostFeedQuery(
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
const feedPostSlice: FeedPostSlice = {
|
||||
_reactKey: slice._reactKey,
|
||||
_isFeedPostSlice: true,
|
||||
rootUri: slice.rootItem.post.uri,
|
||||
rootUri: slice.uri,
|
||||
isThread:
|
||||
slice.items.length > 1 &&
|
||||
slice.items.every(
|
||||
@@ -341,35 +344,42 @@ export function usePostFeedQuery(
|
||||
AppBskyFeedPost.validateRecord(item.post.record)
|
||||
.success
|
||||
) {
|
||||
const parentAuthor =
|
||||
item.reply?.parent?.author ??
|
||||
slice.items[i + 1]?.reply?.grandparentAuthor
|
||||
const parent = item.reply?.parent
|
||||
let parentAuthor:
|
||||
| AppBskyActorDefs.ProfileViewBasic
|
||||
| undefined
|
||||
if (AppBskyFeedDefs.isPostView(parent)) {
|
||||
parentAuthor = parent.author
|
||||
}
|
||||
if (!parentAuthor) {
|
||||
parentAuthor =
|
||||
slice.items[i + 1]?.reply?.grandparentAuthor
|
||||
}
|
||||
const replyRef = item.reply
|
||||
const isParentBlocked = AppBskyFeedDefs.isBlockedPost(
|
||||
replyRef?.parent,
|
||||
)
|
||||
|
||||
return {
|
||||
const feedPostSliceItem: FeedPostSliceItem = {
|
||||
_reactKey: `${slice._reactKey}-${i}-${item.post.uri}`,
|
||||
uri: item.post.uri,
|
||||
post: item.post,
|
||||
record: item.post.record,
|
||||
reason:
|
||||
i === 0 && slice.source
|
||||
? slice.source
|
||||
: item.reason,
|
||||
feedContext: item.feedContext || slice.feedContext,
|
||||
reason: slice.reason,
|
||||
feedContext: slice.feedContext,
|
||||
moderation: moderations[i],
|
||||
parentAuthor,
|
||||
isParentBlocked,
|
||||
}
|
||||
return feedPostSliceItem
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
.filter(Boolean) as FeedPostSliceItem[],
|
||||
.filter(n => !!n),
|
||||
}
|
||||
return feedPostSlice
|
||||
})
|
||||
.filter(Boolean) as FeedPostSlice[],
|
||||
.filter(n => !!n),
|
||||
})),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export type ThreadgateSetting =
|
||||
| {type: 'nobody'}
|
||||
| {type: 'mention'}
|
||||
| {type: 'following'}
|
||||
| {type: 'list'; list: string}
|
||||
| {type: 'list'; list: unknown}
|
||||
|
||||
export function threadgateViewToSettings(
|
||||
threadgate: AppBskyFeedDefs.ThreadgateView | undefined,
|
||||
@@ -21,18 +21,18 @@ export function threadgateViewToSettings(
|
||||
if (!record.allow?.length) {
|
||||
return [{type: 'nobody'}]
|
||||
}
|
||||
return record.allow
|
||||
const settings: ThreadgateSetting[] = record.allow
|
||||
.map(allow => {
|
||||
let setting: ThreadgateSetting | undefined
|
||||
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 {type: 'following'}
|
||||
}
|
||||
if (allow.$type === 'app.bsky.feed.threadgate#listRule') {
|
||||
return {type: 'list', list: allow.list}
|
||||
}
|
||||
return undefined
|
||||
return setting
|
||||
})
|
||||
.filter(Boolean) as ThreadgateSetting[]
|
||||
.filter(n => !!n)
|
||||
return settings
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@ export const SplashScreen = ({
|
||||
|
||||
function Footer() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -147,13 +148,19 @@ function Footer() {
|
||||
a.flex_1,
|
||||
t.atoms.border_contrast_medium,
|
||||
]}>
|
||||
<InlineLinkText to="https://bsky.social">
|
||||
<InlineLinkText
|
||||
label={_(msg`Learn more about Bluesky`)}
|
||||
to="https://bsky.social">
|
||||
<Trans>Business</Trans>
|
||||
</InlineLinkText>
|
||||
<InlineLinkText to="https://bsky.social/about/blog">
|
||||
<InlineLinkText
|
||||
label={_(msg`Read the Bluesky blog`)}
|
||||
to="https://bsky.social/about/blog">
|
||||
<Trans>Blog</Trans>
|
||||
</InlineLinkText>
|
||||
<InlineLinkText to="https://bsky.social/about/join">
|
||||
<InlineLinkText
|
||||
label={_(msg`See jobs at Bluesky`)}
|
||||
to="https://bsky.social/about/join">
|
||||
<Trans>Jobs</Trans>
|
||||
</InlineLinkText>
|
||||
|
||||
|
||||
@@ -99,7 +99,6 @@ import {SelectVideoBtn} from './videos/SelectVideoBtn'
|
||||
import {useVideoState} from './videos/state'
|
||||
import {VideoPreview} from './videos/VideoPreview'
|
||||
import {VideoTranscodeProgress} from './videos/VideoTranscodeProgress'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
type CancelRef = {
|
||||
onPressCancel: () => void
|
||||
@@ -763,7 +762,7 @@ function useAnimatedBorders() {
|
||||
|
||||
const topBarAnimatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
borderBottomWidth: hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: interpolateColor(
|
||||
hasScrolledTop.value,
|
||||
[0, 1],
|
||||
@@ -773,7 +772,7 @@ function useAnimatedBorders() {
|
||||
})
|
||||
const bottomBarAnimatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: interpolateColor(
|
||||
hasScrolledBottom.value,
|
||||
[0, 1],
|
||||
@@ -855,7 +854,7 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 8,
|
||||
},
|
||||
errorIcon: {
|
||||
borderWidth: hairlineWidth,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.red4,
|
||||
color: colors.red4,
|
||||
borderRadius: 30,
|
||||
@@ -891,6 +890,6 @@ const styles = StyleSheet.create({
|
||||
paddingLeft: 7,
|
||||
paddingRight: 16,
|
||||
alignItems: 'center',
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -9,7 +9,6 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
|
||||
const {currentAccount} = useSession()
|
||||
@@ -49,7 +48,7 @@ const styles = StyleSheet.create({
|
||||
paddingBottom: 10,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
labelMobile: {
|
||||
paddingLeft: 12,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {shouldClickOpenNewTab} from '#/platform/urls'
|
||||
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
||||
import {
|
||||
useAddSavedFeedsMutation,
|
||||
@@ -32,8 +33,6 @@ import * as Prompt from '#/components/Prompt'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
import {shouldClickOpenNewTab} from '#/platform/urls'
|
||||
|
||||
export function FeedSourceCard({
|
||||
feedUri,
|
||||
@@ -209,7 +208,7 @@ export function FeedSourceCardLoaded({
|
||||
styles.container,
|
||||
pal.border,
|
||||
style,
|
||||
{borderTopWidth: hideTopBorder ? 0 : hairlineWidth},
|
||||
{borderTopWidth: hideTopBorder ? 0 : StyleSheet.hairlineWidth},
|
||||
]}
|
||||
onPress={e => {
|
||||
const shouldOpenInNewTab = shouldClickOpenNewTab(e)
|
||||
@@ -330,7 +329,7 @@ const styles = StyleSheet.create({
|
||||
gap: 14,
|
||||
},
|
||||
border: {
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
headerContainer: {
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -73,7 +73,7 @@ export function HomeHeaderLayoutMobile({
|
||||
]}>
|
||||
{IS_DEV && (
|
||||
<>
|
||||
<Link to="/sys/debug">
|
||||
<Link label="View storybook" to="/sys/debug">
|
||||
<ColorPalette size="md" />
|
||||
</Link>
|
||||
</>
|
||||
|
||||
@@ -14,7 +14,6 @@ import {RichText as RichTextCom} from '#/components/RichText'
|
||||
import {Link} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
export const ListCard = ({
|
||||
testID,
|
||||
@@ -134,7 +133,7 @@ export const ListCard = ({
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outer: {
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
paddingHorizontal: 6,
|
||||
},
|
||||
outerNoBorder: {
|
||||
|
||||
@@ -29,7 +29,6 @@ import {Button} from '../util/forms/Button'
|
||||
import {Text} from '../util/text/Text'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
export const snapPoints = ['fullscreen']
|
||||
|
||||
@@ -63,7 +62,7 @@ export function Component({
|
||||
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])
|
||||
|
||||
return (
|
||||
@@ -188,7 +187,7 @@ function ListItem({
|
||||
style={[
|
||||
styles.listItem,
|
||||
pal.border,
|
||||
index !== 0 && {borderTopWidth: hairlineWidth},
|
||||
index !== 0 && {borderTopWidth: StyleSheet.hairlineWidth},
|
||||
]}>
|
||||
<View style={styles.listItemAvi}>
|
||||
<UserAvatar size={40} avatar={list.avatar} type="list" />
|
||||
@@ -248,7 +247,7 @@ const styles = StyleSheet.create({
|
||||
gap: 10,
|
||||
paddingTop: 10,
|
||||
paddingBottom: isAndroid ? 10 : 0,
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
footerBtn: {
|
||||
paddingHorizontal: 24,
|
||||
|
||||
@@ -24,7 +24,6 @@ import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceho
|
||||
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import {FeedItem} from './FeedItem'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
||||
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
||||
@@ -137,7 +136,7 @@ export function Feed({
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
!isTabletOrMobile && {borderTopWidth: hairlineWidth},
|
||||
!isTabletOrMobile && {borderTopWidth: StyleSheet.hairlineWidth},
|
||||
]}>
|
||||
<NotificationFeedLoadingPlaceholder />
|
||||
</View>
|
||||
|
||||
@@ -20,20 +20,28 @@ import {
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {msg, plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
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 {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
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 {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {niceDate} from 'lib/strings/time'
|
||||
import {colors, s} from 'lib/styles'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {DM_SERVICE_HEADERS} from 'state/queries/messages/const'
|
||||
import {precacheProfile} from 'state/queries/profile'
|
||||
import {useAgent} from 'state/session'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {
|
||||
ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
|
||||
ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon,
|
||||
@@ -41,8 +49,10 @@ import {
|
||||
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 {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost'
|
||||
import {StarterPack} from '#/components/icons/StarterPack'
|
||||
import {Link as NewLink} from '#/components/Link'
|
||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||
import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
||||
import {FeedSourceCard} from '../feeds/FeedSourceCard'
|
||||
import {Post} from '../post/Post'
|
||||
import {ImageHorzList} from '../util/images/ImageHorzList'
|
||||
@@ -52,19 +62,6 @@ import {Text} from '../util/text/Text'
|
||||
import {TimeElapsed} from '../util/TimeElapsed'
|
||||
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 EXPANDED_AUTHOR_EL_HEIGHT = 35
|
||||
@@ -171,7 +168,6 @@ let FeedItem = ({
|
||||
)
|
||||
}
|
||||
|
||||
let isFollowBack = false
|
||||
let action = ''
|
||||
let icon = (
|
||||
<HeartIconFilled
|
||||
@@ -192,7 +188,6 @@ let FeedItem = ({
|
||||
item.notification.author.viewer?.following &&
|
||||
gate('ungroup_follow_backs')
|
||||
) {
|
||||
isFollowBack = true
|
||||
action = _(msg`followed you back`)
|
||||
} else {
|
||||
action = _(msg`followed you`)
|
||||
@@ -211,7 +206,22 @@ let FeedItem = ({
|
||||
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 (
|
||||
<Link
|
||||
testID={`feedItem-by-${item.notification.author.handle}`}
|
||||
@@ -224,10 +234,12 @@ let FeedItem = ({
|
||||
backgroundColor: pal.colors.unreadNotifBg,
|
||||
borderColor: pal.colors.unreadNotifBorder,
|
||||
},
|
||||
{borderTopWidth: hideTopBorder ? 0 : hairlineWidth},
|
||||
{borderTopWidth: hideTopBorder ? 0 : StyleSheet.hairlineWidth},
|
||||
]}
|
||||
href={itemHref}
|
||||
noFeedback
|
||||
accessibilityHint=""
|
||||
accessibilityLabel={a11yLabel}
|
||||
accessible={!isAuthorsExpanded}
|
||||
accessibilityActions={
|
||||
authors.length > 1
|
||||
@@ -272,19 +284,18 @@ let FeedItem = ({
|
||||
visible={!isAuthorsExpanded}
|
||||
authors={authors}
|
||||
onToggleAuthorsExpanded={onToggleAuthorsExpanded}
|
||||
showDmButton={item.type === 'starterpack-joined' || isFollowBack}
|
||||
showDmButton={item.type === 'starterpack-joined'}
|
||||
/>
|
||||
<ExpandedAuthorsList visible={isAuthorsExpanded} authors={authors} />
|
||||
<Text style={[styles.meta, a.self_start]}>
|
||||
<Text
|
||||
style={[styles.meta, a.self_start]}
|
||||
accessibilityHint=""
|
||||
accessibilityLabel={a11yLabel}>
|
||||
<TextLink
|
||||
key={authors[0].href}
|
||||
style={[pal.text, s.bold]}
|
||||
href={authors[0].href}
|
||||
text={forceLTR(
|
||||
sanitizeDisplayName(
|
||||
authors[0].profile.displayName || authors[0].profile.handle,
|
||||
),
|
||||
)}
|
||||
text={forceLTR(firstAuthorName)}
|
||||
disableMismatchWarning
|
||||
/>
|
||||
{authors.length > 1 ? (
|
||||
@@ -306,7 +317,7 @@ let FeedItem = ({
|
||||
{({timeElapsed}) => (
|
||||
<Text
|
||||
style={[pal.textLight, styles.pointer]}
|
||||
title={niceDate(item.notification.indexedAt)}>
|
||||
title={niceTimestamp}>
|
||||
{' ' + timeElapsed}
|
||||
</Text>
|
||||
)}
|
||||
@@ -458,7 +469,6 @@ function CondensedAuthorsList({
|
||||
profile={authors[0].profile}
|
||||
moderation={authors[0].moderation.ui('avatar')}
|
||||
type={authors[0].profile.associated?.labeler ? 'labeler' : 'user'}
|
||||
accessible={false}
|
||||
/>
|
||||
{showDmButton ? <SayHelloBtn profile={authors[0].profile} /> : null}
|
||||
</View>
|
||||
@@ -476,7 +486,6 @@ function CondensedAuthorsList({
|
||||
profile={author.profile}
|
||||
moderation={author.moderation.ui('avatar')}
|
||||
type={author.profile.associated?.labeler ? 'labeler' : 'user'}
|
||||
accessible={false}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {PressableWithHover} from '../util/PressableWithHover'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {DraggableScrollView} from './DraggableScrollView'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
export interface TabBarProps {
|
||||
testID?: string
|
||||
@@ -208,6 +207,6 @@ const mobileStyles = StyleSheet.create({
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: '100%',
|
||||
borderBottomWidth: hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -44,7 +44,6 @@ import {PostEmbeds} from '../util/post-embeds'
|
||||
import {PostMeta} from '../util/PostMeta'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {PreviewableUserAvatar} from '../util/UserAvatar'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
export function PostThreadItem({
|
||||
post,
|
||||
@@ -604,7 +603,7 @@ function PostOuterWrapper({
|
||||
{
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: isMobile ? 10 : 6,
|
||||
borderTopWidth: depth === 1 ? hairlineWidth : 0,
|
||||
borderTopWidth: depth === 1 ? StyleSheet.hairlineWidth : 0,
|
||||
},
|
||||
]}>
|
||||
{Array.from(Array(depth - 1)).map((_, n: number) => (
|
||||
@@ -699,7 +698,7 @@ function getThreadAuthor(
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outer: {
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
paddingLeft: 8,
|
||||
},
|
||||
outerHighlighted: {
|
||||
@@ -709,7 +708,7 @@ const styles = StyleSheet.create({
|
||||
paddingRight: 8,
|
||||
},
|
||||
outerHighlightedRoot: {
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
paddingTop: 16,
|
||||
},
|
||||
noTopBorder: {
|
||||
@@ -761,8 +760,8 @@ const styles = StyleSheet.create({
|
||||
expandedInfo: {
|
||||
flexDirection: 'row',
|
||||
padding: 10,
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderBottomWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
marginTop: 5,
|
||||
marginBottom: 10,
|
||||
},
|
||||
|
||||
@@ -37,7 +37,6 @@ import {PostMeta} from '../util/PostMeta'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {PreviewableUserAvatar} from '../util/UserAvatar'
|
||||
import {UserInfoText} from '../util/UserInfoText'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
export function Post({
|
||||
post,
|
||||
@@ -155,7 +154,7 @@ function PostInner({
|
||||
style={[
|
||||
styles.outer,
|
||||
pal.border,
|
||||
!hideTopBorder && {borderTopWidth: hairlineWidth},
|
||||
!hideTopBorder && {borderTopWidth: StyleSheet.hairlineWidth},
|
||||
style,
|
||||
]}
|
||||
onBeforePress={onBeforePress}>
|
||||
|
||||
@@ -16,8 +16,10 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
|
||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {isReasonFeedSource, ReasonFeedSource} from 'lib/api/feed/types'
|
||||
import {MAX_POST_LINES} from 'lib/constants'
|
||||
@@ -29,6 +31,7 @@ import {countLines} from 'lib/strings/helpers'
|
||||
import {s} from 'lib/styles'
|
||||
import {precacheProfile} from 'state/queries/profile'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
|
||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||
import {RichText} from '#/components/RichText'
|
||||
@@ -38,17 +41,19 @@ import {FeedNameText} from '../util/FeedInfoText'
|
||||
import {Link, TextLink, TextLinkOnWebOnly} from '../util/Link'
|
||||
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
|
||||
import {PostEmbeds} from '../util/post-embeds'
|
||||
import {VideoEmbed} from '../util/post-embeds/VideoEmbed'
|
||||
import {PostMeta} from '../util/PostMeta'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {PreviewableUserAvatar} from '../util/UserAvatar'
|
||||
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 {
|
||||
record: AppBskyFeedPost.Record
|
||||
reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
|
||||
reason:
|
||||
| AppBskyFeedDefs.ReasonRepost
|
||||
| ReasonFeedSource
|
||||
| {[k: string]: unknown; $type: string}
|
||||
| undefined
|
||||
moderation: ModerationDecision
|
||||
parentAuthor: AppBskyActorDefs.ProfileViewBasic | undefined
|
||||
showReplyTo: boolean
|
||||
@@ -132,6 +137,8 @@ let FeedItemInner = ({
|
||||
const {openComposer} = useComposerControls()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const gate = useGate()
|
||||
|
||||
const href = useMemo(() => {
|
||||
const urip = new AtUri(post.uri)
|
||||
return makeProfileLink(post.author, 'post', urip.rkey)
|
||||
@@ -197,7 +204,8 @@ let FeedItemInner = ({
|
||||
isThreadLastChild || (!isThreadChild && !isThreadParent)
|
||||
? 8
|
||||
: undefined,
|
||||
borderTopWidth: hideTopBorder || isThreadChild ? 0 : hairlineWidth,
|
||||
borderTopWidth:
|
||||
hideTopBorder || isThreadChild ? 0 : StyleSheet.hairlineWidth,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -337,9 +345,11 @@ let FeedItemInner = ({
|
||||
postHref={href}
|
||||
onOpenAuthor={onOpenAuthor}
|
||||
/>
|
||||
{!isThreadChild && showReplyTo && parentAuthor && (
|
||||
<ReplyToLabel blocked={isParentBlocked} profile={parentAuthor} />
|
||||
)}
|
||||
{!isThreadChild &&
|
||||
showReplyTo &&
|
||||
(parentAuthor || isParentBlocked) && (
|
||||
<ReplyToLabel blocked={isParentBlocked} profile={parentAuthor} />
|
||||
)}
|
||||
<LabelsOnMyPost post={post} />
|
||||
<PostContent
|
||||
moderation={moderation}
|
||||
@@ -348,6 +358,9 @@ let FeedItemInner = ({
|
||||
postAuthor={post.author}
|
||||
onOpenEmbed={onOpenEmbed}
|
||||
/>
|
||||
{__DEV__ && gate('videos') && (
|
||||
<VideoEmbed source="https://lumi.jazco.dev/watch/did:plc:q6gjnaw2blty4crticxkmujt/Qmc8w93UpTa2adJHg4ZhnDPrBs1EsbzrekzPcqF5SwusuZ/playlist.m3u8" />
|
||||
)}
|
||||
<PostCtrls
|
||||
post={post}
|
||||
record={record}
|
||||
@@ -431,12 +444,46 @@ function ReplyToLabel({
|
||||
profile,
|
||||
blocked,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
profile: AppBskyActorDefs.ProfileViewBasic | undefined
|
||||
blocked?: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
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 (
|
||||
<View style={[s.flexRow, s.mb2, s.alignCenter]}>
|
||||
@@ -450,29 +497,7 @@ function ReplyToLabel({
|
||||
style={[pal.textLight, s.mr2]}
|
||||
lineHeight={1.2}
|
||||
numberOfLines={1}>
|
||||
{isMe ? (
|
||||
<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>
|
||||
)}
|
||||
{label}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -99,6 +99,7 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
|
||||
<Trans>
|
||||
This feed is no longer online. We are showing{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`The Discover feed`)}
|
||||
to="/profile/bsky.app/feed/whats-hot"
|
||||
style={[a.text_md]}>
|
||||
Discover
|
||||
|
||||
@@ -23,12 +23,11 @@ import {
|
||||
KnownFollowers,
|
||||
shouldShowKnownFollowers,
|
||||
} from '#/components/KnownFollowers'
|
||||
import * as Pills from '#/components/Pills'
|
||||
import {Link} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {PreviewableUserAvatar} from '../util/UserAvatar'
|
||||
import {FollowButton} from './FollowButton'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
import * as Pills from '#/components/Pills'
|
||||
|
||||
export function ProfileCard({
|
||||
testID,
|
||||
@@ -217,7 +216,7 @@ export function ProfileCardWithFollowBtn({
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outer: {
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
|
||||
@@ -15,15 +15,13 @@ import {makeProfileLink} from 'lib/routes/links'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
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 {LoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {UserAvatar, UserAvatarType} from '../util/UserAvatar'
|
||||
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({
|
||||
isLoading,
|
||||
@@ -84,7 +82,7 @@ export function ProfileSubpageHeader({
|
||||
{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderBottomWidth: hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
paddingTop: isNative ? 0 : 8,
|
||||
paddingBottom: 8,
|
||||
paddingHorizontal: isMobile ? 12 : 14,
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
Heart2_Stroke2_Corner0_Rounded as HeartIconOutline,
|
||||
} from '#/components/icons/Heart2'
|
||||
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
export function LoadingPlaceholder({
|
||||
width,
|
||||
@@ -236,7 +235,7 @@ export function FeedLoadingPlaceholder({
|
||||
{
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 18,
|
||||
borderTopWidth: showTopBorder ? hairlineWidth : 0,
|
||||
borderTopWidth: showTopBorder ? StyleSheet.hairlineWidth : 0,
|
||||
},
|
||||
pal.border,
|
||||
style,
|
||||
|
||||
@@ -55,7 +55,6 @@ interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
disableHoverCard?: boolean
|
||||
onBeforePress?: () => void
|
||||
accessible?: boolean
|
||||
}
|
||||
|
||||
const BLUR_AMOUNT = isWeb ? 5 : 100
|
||||
@@ -412,7 +411,6 @@ let PreviewableUserAvatar = ({
|
||||
profile,
|
||||
disableHoverCard,
|
||||
onBeforePress,
|
||||
accessible = true,
|
||||
...rest
|
||||
}: PreviewableUserAvatarProps): React.ReactNode => {
|
||||
const {_} = useLingui()
|
||||
@@ -426,12 +424,8 @@ let PreviewableUserAvatar = ({
|
||||
return (
|
||||
<ProfileHoverCard did={profile.did} disable={disableHoverCard}>
|
||||
<Link
|
||||
label={
|
||||
accessible
|
||||
? _(msg`${profile.displayName || profile.handle}'s avatar`)
|
||||
: undefined
|
||||
}
|
||||
accessibilityHint={accessible ? _(msg`Opens this profile`) : undefined}
|
||||
label={_(msg`${profile.displayName || profile.handle}'s avatar`)}
|
||||
accessibilityHint={_(msg`Opens this profile`)}
|
||||
to={makeProfileLink({
|
||||
did: profile.did,
|
||||
handle: profile.handle,
|
||||
|
||||
@@ -13,10 +13,9 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {useTheme} from '#/alf'
|
||||
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
||||
import {Text} from './text/Text'
|
||||
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}
|
||||
|
||||
@@ -154,7 +153,7 @@ function DesktopWebHeader({
|
||||
styles.desktopHeader,
|
||||
pal.border,
|
||||
{
|
||||
borderBottomWidth: showBorder ? hairlineWidth : 0,
|
||||
borderBottomWidth: showBorder ? StyleSheet.hairlineWidth : 0,
|
||||
},
|
||||
{display: 'flex', flexDirection: 'column'},
|
||||
]}>
|
||||
@@ -243,7 +242,7 @@ const styles = StyleSheet.create({
|
||||
marginRight: 'auto',
|
||||
},
|
||||
border: {
|
||||
borderBottomWidth: hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
titleContainer: {
|
||||
marginLeft: 'auto',
|
||||
|
||||
@@ -26,7 +26,6 @@ import Animated from 'react-native-reanimated'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {addStyle} from 'lib/styles'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
interface AddedProps {
|
||||
desktopFixedHeight?: boolean | number
|
||||
@@ -50,8 +49,8 @@ export const CenteredView = React.forwardRef(function CenteredView(
|
||||
}
|
||||
if (sideBorders) {
|
||||
style = addStyle(style, {
|
||||
borderLeftWidth: hairlineWidth,
|
||||
borderRightWidth: hairlineWidth,
|
||||
borderLeftWidth: StyleSheet.hairlineWidth,
|
||||
borderRightWidth: StyleSheet.hairlineWidth,
|
||||
})
|
||||
style = addStyle(style, pal.border)
|
||||
}
|
||||
@@ -163,8 +162,8 @@ export const ScrollView = React.forwardRef(function ScrollViewImpl(
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
contentContainer: {
|
||||
borderLeftWidth: hairlineWidth,
|
||||
borderRightWidth: hairlineWidth,
|
||||
borderLeftWidth: StyleSheet.hairlineWidth,
|
||||
borderRightWidth: StyleSheet.hairlineWidth,
|
||||
// @ts-ignore web only
|
||||
minHeight: '100vh',
|
||||
},
|
||||
|
||||
@@ -13,7 +13,6 @@ import {clamp} from '#/lib/numbers'
|
||||
import {colors} from '#/lib/styles'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useSession} from '#/state/session'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
const AnimatedTouchableOpacity =
|
||||
Animated.createAnimatedComponent(TouchableOpacity)
|
||||
@@ -74,7 +73,7 @@ const styles = StyleSheet.create({
|
||||
// @ts-ignore 'fixed' is web only -prf
|
||||
position: isWeb ? 'fixed' : 'absolute',
|
||||
left: 18,
|
||||
borderWidth: hairlineWidth,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
width: 52,
|
||||
height: 52,
|
||||
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],
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,6 @@ import {Link} from '../Link'
|
||||
import {PostMeta} from '../PostMeta'
|
||||
import {Text} from '../text/Text'
|
||||
import {PostEmbeds} from '.'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
export function MaybeQuoteEmbed({
|
||||
embed,
|
||||
@@ -262,7 +261,7 @@ const styles = StyleSheet.create({
|
||||
marginTop: 8,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
borderWidth: hairlineWidth,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
errorContainer: {
|
||||
flexDirection: 'row',
|
||||
@@ -272,7 +271,7 @@ const styles = StyleSheet.create({
|
||||
marginTop: 8,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 14,
|
||||
borderWidth: hairlineWidth,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
alert: {
|
||||
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')
|
||||
}
|
||||
@@ -19,18 +19,17 @@ import {
|
||||
} from '@atproto/api'
|
||||
|
||||
import {ImagesLightbox, useLightboxControls} from '#/state/lightbox'
|
||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
||||
import {ContentHider} from '../../../../components/moderation/ContentHider'
|
||||
import {AutoSizedImage} from '../images/AutoSizedImage'
|
||||
import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
|
||||
import {ExternalLinkEmbed} from './ExternalLinkEmbed'
|
||||
import {ListEmbed} from './ListEmbed'
|
||||
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 =
|
||||
| AppBskyEmbedRecord.View
|
||||
@@ -205,7 +204,7 @@ const styles = StyleSheet.create({
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
customFeedOuter: {
|
||||
borderWidth: hairlineWidth,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderRadius: 8,
|
||||
marginTop: 4,
|
||||
paddingHorizontal: 12,
|
||||
|
||||
@@ -33,14 +33,13 @@ import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||
import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
|
||||
import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import * as FeedCard from '#/components/FeedCard'
|
||||
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 {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass'
|
||||
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'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Feeds'>
|
||||
@@ -793,13 +792,13 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
gap: 12,
|
||||
borderBottomWidth: hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
savedFeedMobile: {
|
||||
paddingVertical: 10,
|
||||
},
|
||||
offlineSlug: {
|
||||
borderWidth: hairlineWidth,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 2,
|
||||
|
||||
@@ -16,7 +16,6 @@ import {MyLists} from '#/view/com/lists/MyLists'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'>
|
||||
export function ListsScreen({}: Props) {
|
||||
@@ -55,8 +54,11 @@ export function ListsScreen({}: Props) {
|
||||
style={[
|
||||
pal.border,
|
||||
isMobile
|
||||
? {borderBottomWidth: hairlineWidth}
|
||||
: {borderLeftWidth: hairlineWidth, borderRightWidth: hairlineWidth},
|
||||
? {borderBottomWidth: StyleSheet.hairlineWidth}
|
||||
: {
|
||||
borderLeftWidth: StyleSheet.hairlineWidth,
|
||||
borderRightWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
]}>
|
||||
<View style={{flex: 1}}>
|
||||
<Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
|
||||
|
||||
@@ -65,7 +65,6 @@ import {useDialogControl} from '#/components/Dialog'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
const SECTION_TITLES_CURATE = ['Posts', 'About']
|
||||
const SECTION_TITLES_MOD = ['About']
|
||||
@@ -803,7 +802,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
padding: isMobile ? 14 : 20,
|
||||
gap: 12,
|
||||
},
|
||||
@@ -954,7 +953,7 @@ function ErrorScreen({error}: {error: string}) {
|
||||
marginTop: 10,
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 14,
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
]}>
|
||||
<Text type="title-lg" style={[pal.text, s.mb10]}>
|
||||
|
||||
@@ -32,7 +32,6 @@ import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
|
||||
import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
const HITSLOP_TOP = {
|
||||
top: 20,
|
||||
@@ -93,7 +92,8 @@ export function SavedFeeds({}: Props) {
|
||||
<ViewHeader title={_(msg`Edit My Feeds`)} showOnDesktop showBorder />
|
||||
<ScrollView style={s.flex1} contentContainerStyle={[styles.noBorder]}>
|
||||
{noSavedFeedsOfAnyType && (
|
||||
<View style={[pal.border, {borderBottomWidth: hairlineWidth}]}>
|
||||
<View
|
||||
style={[pal.border, {borderBottomWidth: StyleSheet.hairlineWidth}]}>
|
||||
<NoSavedFeedsOfAnyType />
|
||||
</View>
|
||||
)}
|
||||
@@ -135,7 +135,8 @@ export function SavedFeeds({}: Props) {
|
||||
)}
|
||||
|
||||
{noFollowingFeed && (
|
||||
<View style={[pal.border, {borderBottomWidth: hairlineWidth}]}>
|
||||
<View
|
||||
style={[pal.border, {borderBottomWidth: StyleSheet.hairlineWidth}]}>
|
||||
<NoFollowingFeed />
|
||||
</View>
|
||||
)}
|
||||
@@ -437,12 +438,12 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 14,
|
||||
paddingTop: 20,
|
||||
paddingBottom: 10,
|
||||
borderBottomWidth: hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
itemContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderBottomWidth: hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
footerText: {
|
||||
paddingHorizontal: 26,
|
||||
|
||||
@@ -75,17 +75,17 @@ function SuggestedItemsHeader({
|
||||
)
|
||||
}
|
||||
|
||||
type LoadMoreItems =
|
||||
type LoadMoreItem =
|
||||
| {
|
||||
type: 'profile'
|
||||
key: string
|
||||
avatar: string
|
||||
avatar: string | undefined
|
||||
moderation: ModerationDecision
|
||||
}
|
||||
| {
|
||||
type: 'feed'
|
||||
key: string
|
||||
avatar: string
|
||||
avatar: string | undefined
|
||||
moderation: undefined
|
||||
}
|
||||
|
||||
@@ -98,27 +98,28 @@ function LoadMore({
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const items = React.useMemo(() => {
|
||||
const items: LoadMoreItem[] = React.useMemo(() => {
|
||||
return item.items
|
||||
.map(_item => {
|
||||
let loadMoreItem: LoadMoreItem | undefined
|
||||
if (_item.type === 'profile') {
|
||||
return {
|
||||
loadMoreItem = {
|
||||
type: 'profile',
|
||||
key: _item.profile.did,
|
||||
avatar: _item.profile.avatar,
|
||||
moderation: moderateProfile(_item.profile, moderationOpts!),
|
||||
}
|
||||
} else if (_item.type === 'feed') {
|
||||
return {
|
||||
loadMoreItem = {
|
||||
type: 'feed',
|
||||
key: _item.feed.uri,
|
||||
avatar: _item.feed.avatar,
|
||||
moderation: undefined,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
return loadMoreItem
|
||||
})
|
||||
.filter(Boolean) as LoadMoreItems[]
|
||||
.filter(n => !!n)
|
||||
}, [item.items, moderationOpts])
|
||||
|
||||
if (items.length === 0) return null
|
||||
|
||||
@@ -94,6 +94,7 @@ export function ExportCarDialog({
|
||||
This feature is in beta. You can read more about repository
|
||||
exports in{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`View blogpost for more details`)}
|
||||
to="https://docs.bsky.app/blog/repo-export"
|
||||
style={[a.text_sm]}>
|
||||
this blogpost
|
||||
|
||||
@@ -62,13 +62,12 @@ import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||
import {ScrollView} from 'view/com/util/Views'
|
||||
import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog'
|
||||
import {useTheme} from '#/alf'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
|
||||
import {navigate, resetToTab} from '#/Navigation'
|
||||
import {Email2FAToggle} from './Email2FAToggle'
|
||||
import {ExportCarDialog} from './ExportCarDialog'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
function SettingsAccountCard({
|
||||
account,
|
||||
@@ -326,7 +325,7 @@ export function SettingsScreen({}: Props) {
|
||||
showBackButton={isMobile}
|
||||
style={[
|
||||
pal.border,
|
||||
{borderBottomWidth: hairlineWidth},
|
||||
{borderBottomWidth: StyleSheet.hairlineWidth},
|
||||
!isMobile && {borderLeftWidth: 1, borderRightWidth: 1},
|
||||
]}>
|
||||
<View style={{flex: 1}}>
|
||||
|
||||
@@ -13,18 +13,20 @@ export function Links() {
|
||||
<H1>Links</H1>
|
||||
|
||||
<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
|
||||
</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)
|
||||
</InlineLinkText>
|
||||
<InlineLinkText
|
||||
label="foo"
|
||||
to="https://bsky.social"
|
||||
style={[a.text_md, t.atoms.text_contrast_low]}>
|
||||
Internal (bsky.social)
|
||||
</InlineLinkText>
|
||||
<InlineLinkText
|
||||
label="foo"
|
||||
to="https://bsky.app/profile/bsky.app"
|
||||
style={[a.text_md]}>
|
||||
Internal (bsky.app)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {StyleSheet} from 'react-native'
|
||||
|
||||
import {colors} from 'lib/styles'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
bottomBar: {
|
||||
@@ -10,7 +9,7 @@ export const styles = StyleSheet.create({
|
||||
left: 0,
|
||||
right: 0,
|
||||
flexDirection: 'row',
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 10,
|
||||
},
|
||||
|
||||
@@ -11,10 +11,9 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {s} from 'lib/styles'
|
||||
import {TextLink} from 'view/com/util/Link'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {ProgressGuideList} from '#/components/ProgressGuide/List'
|
||||
import {DesktopFeeds} from './Feeds'
|
||||
import {DesktopSearch} from './Search'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
import {ProgressGuideList} from '#/components/ProgressGuide/List'
|
||||
|
||||
export function DesktopRightNav({routeName}: {routeName: string}) {
|
||||
const pal = usePalette('default')
|
||||
@@ -135,8 +134,8 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 10,
|
||||
},
|
||||
desktopFeedsContainer: {
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderBottomWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
marginTop: 18,
|
||||
marginBottom: 18,
|
||||
},
|
||||
|
||||
@@ -13345,6 +13345,11 @@ history@^5.3.0:
|
||||
dependencies:
|
||||
"@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:
|
||||
version "1.0.1"
|
||||
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:
|
||||
is-typedarray "^1.0.0"
|
||||
|
||||
typescript@^5.3.3:
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
|
||||
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
|
||||
typescript@^5.5.4:
|
||||
version "5.5.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
|
||||
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
|
||||
|
||||
ua-parser-js@^0.7.33:
|
||||
version "0.7.35"
|
||||
|
||||
Reference in New Issue
Block a user