Merge branch 'main' into ja-translation-14

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

After

Width:  |  Height:  |  Size: 239 B

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

After

Width:  |  Height:  |  Size: 374 B

+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"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",
+20
View File
@@ -0,0 +1,20 @@
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
@@ -11,6 +11,7 @@ internal fun PlayerView.applyRequiresLinearPlayback(requireLinearPlayback: Boole
setShowPreviousButton(!requireLinearPlayback)
setShowNextButton(!requireLinearPlayback)
setTimeBarInteractive(requireLinearPlayback)
+ setShowSubtitleButton(true)
}
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
@@ -27,7 +28,8 @@ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) {
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) {
- val fullscreenButton = findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
+ val fullscreenButton =
+ findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
fullscreenButton?.visibility = if (visible) {
android.view.View.VISIBLE
} else {
+41 -38
View File
@@ -23,10 +23,12 @@ import {
} from '#/lib/statsig/statsig'
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
View File
@@ -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
View File
@@ -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,
},
/*
+3
View File
@@ -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>
+7 -3
View File
@@ -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>
)
+9 -3
View File
@@ -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} />
+4 -9
View File
@@ -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},
+3 -3
View File
@@ -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>
)
+12 -4
View File
@@ -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}>
+3 -1
View File
@@ -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]}>
+9
View File
@@ -0,0 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Play_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M5 5.086C5 2.736 7.578 1.3 9.576 2.534L20.77 9.448c1.899 1.172 1.899 3.932 0 5.104L9.576 21.466C7.578 22.701 5 21.263 5 18.914V5.086Zm3.525-.85A1 1 0 0 0 7 5.085v13.828a1 1 0 0 0 1.525.85l11.194-6.913a1 1 0 0 0 0-1.702L8.525 4.235Z',
})
export const Play_Filled_Corner2_Rounded = createSinglePathSVG({
path: 'M9.576 2.534C7.578 1.299 5 2.737 5 5.086v13.828c0 2.35 2.578 3.787 4.576 2.552l11.194-6.914c1.899-1.172 1.899-3.932 0-5.104L9.576 2.534Z',
})
@@ -174,7 +174,7 @@ export function LabelerLabelPreference({
disabled?: boolean
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>
.
+10 -4
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
}
}
+16 -4
View File
@@ -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>
+8 -2
View File
@@ -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>,
)
+1
View File
@@ -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>
+25 -15
View File
@@ -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),
})),
],
}
+11 -11
View File
@@ -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
}
+10 -3
View File
@@ -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>
+4 -5
View File
@@ -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,
},
})
+1 -2
View File
@@ -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,
+3 -4
View File
@@ -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',
+1 -1
View File
@@ -73,7 +73,7 @@ export function HomeHeaderLayoutMobile({
]}>
{IS_DEV && (
<>
<Link to="/sys/debug">
<Link label="View storybook" to="/sys/debug">
<ColorPalette size="md" />
</Link>
</>
+1 -2
View File
@@ -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: {
+3 -4
View File
@@ -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,
+1 -2
View File
@@ -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>
+36 -27
View File
@@ -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>
))}
+1 -2
View File
@@ -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,
},
})
+5 -6
View File
@@ -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,
},
+1 -2
View File
@@ -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}>
+58 -33
View File
@@ -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>
)
+1
View File
@@ -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
+2 -3
View File
@@ -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,
+1 -2
View File
@@ -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,
+2 -8
View File
@@ -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,
+3 -4
View File
@@ -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',
+4 -5
View File
@@ -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],
),
}
}
+2 -3
View File
@@ -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')
}
+3 -4
View File
@@ -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,
+5 -6
View File
@@ -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,
+5 -3
View File
@@ -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'}]}>
+2 -3
View File
@@ -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]}>
+6 -5
View File
@@ -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,
+9 -8
View File
@@ -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
+2 -3
View File
@@ -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}}>
+4 -2
View File
@@ -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,
},
+3 -4
View File
@@ -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,
},
+9 -4
View File
@@ -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"