diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx
index 63f61ce856..7b861dc660 100644
--- a/src/components/KnownFollowers.tsx
+++ b/src/components/KnownFollowers.tsx
@@ -100,7 +100,15 @@ function KnownFollowersInner({
moderation,
}
})
- const count = cachedKnownFollowers.count
+
+ // Does not have blocks applied. Always >= slices.length
+ const serverCount = cachedKnownFollowers.count
+
+ /*
+ * We check above too, but here for clarity and a reminder to _check for
+ * valid indices_
+ */
+ if (slice.length === 0) return null
return (
- {count > 2 ? (
-
- Followed by{' '}
-
- {slice[0].profile.displayName}
-
- ,{' '}
-
- {slice[1].profile.displayName}
-
- , and{' '}
-
-
- ) : count === 2 ? (
+ {slice.length >= 2 ? (
+ // 2-n followers, including blocks
+ serverCount > 2 ? (
+
+ Followed by{' '}
+
+ {slice[0].profile.displayName}
+
+ ,{' '}
+
+ {slice[1].profile.displayName}
+
+ , and{' '}
+
+
+ ) : (
+ // only 2
+
+ Followed by{' '}
+
+ {slice[0].profile.displayName}
+ {' '}
+ and{' '}
+
+ {slice[1].profile.displayName}
+
+
+ )
+ ) : serverCount > 1 ? (
+ // 1-n followers, including blocks
Followed by{' '}
{slice[0].profile.displayName}
{' '}
and{' '}
-
- {slice[1].profile.displayName}
-
+
) : (
+ // only 1
Followed by{' '}
diff --git a/src/components/moderation/PostAlerts.tsx b/src/components/moderation/PostAlerts.tsx
index 0b48b51d1d..ec7529a4ff 100644
--- a/src/components/moderation/PostAlerts.tsx
+++ b/src/components/moderation/PostAlerts.tsx
@@ -92,6 +92,8 @@ function PostLabel({
) : (
diff --git a/src/components/moderation/PostHider.tsx b/src/components/moderation/PostHider.tsx
index 8a64742978..b6fb174528 100644
--- a/src/components/moderation/PostHider.tsx
+++ b/src/components/moderation/PostHider.tsx
@@ -1,6 +1,6 @@
import React, {ComponentProps} from 'react'
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
-import {AppBskyActorDefs, ModerationUI} from '@atproto/api'
+import {AppBskyActorDefs, ModerationCause, ModerationUI} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
@@ -45,7 +45,8 @@ export function PostHider({
const [override, setOverride] = React.useState(false)
const control = useModerationDetailsDialogControl()
const blur =
- modui.blurs[0] || (interpretFilterAsBlur ? modui.filters[0] : undefined)
+ modui.blurs[0] ||
+ (interpretFilterAsBlur ? getBlurrableFilter(modui) : undefined)
const desc = useModerationCauseDescription(blur)
const onBeforePress = React.useCallback(() => {
@@ -134,6 +135,13 @@ export function PostHider({
)
}
+function getBlurrableFilter(modui: ModerationUI): ModerationCause | undefined {
+ // moderation causes get "downgraded" when they originate from embedded content
+ // a downgraded cause should *only* drive filtering in feeds, so we want to look
+ // for filters that arent downgraded
+ return modui.filters.find(filter => !filter.downgraded)
+}
+
const styles = StyleSheet.create({
child: {
borderWidth: 0,
diff --git a/src/lib/strings/embed-player.ts b/src/lib/strings/embed-player.ts
index 30ced14921..44e42fae1c 100644
--- a/src/lib/strings/embed-player.ts
+++ b/src/lib/strings/embed-player.ts
@@ -1,7 +1,8 @@
-import {Dimensions, Platform} from 'react-native'
+import {Dimensions} from 'react-native'
import {isSafari} from 'lib/browser'
import {isWeb} from 'platform/detection'
+
const {height: SCREEN_HEIGHT} = Dimensions.get('window')
const IFRAME_HOST = isWeb
@@ -342,40 +343,17 @@ export function parseEmbedPlayerFromUrl(
}
}
- if (urlp.hostname === 'media.tenor.com') {
- let [_, id, filename] = urlp.pathname.split('/')
+ const tenorGif = parseTenorGif(urlp)
+ if (tenorGif.success) {
+ const {playerUri, dimensions} = tenorGif
- const h = urlp.searchParams.get('hh')
- const w = urlp.searchParams.get('ww')
- let dimensions
- if (h && w) {
- dimensions = {
- height: Number(h),
- width: Number(w),
- }
- }
-
- if (id && filename && dimensions && id.includes('AAAAC')) {
- if (Platform.OS === 'web') {
- if (isSafari) {
- id = id.replace('AAAAC', 'AAAP1')
- filename = filename.replace('.gif', '.mp4')
- } else {
- id = id.replace('AAAAC', 'AAAP3')
- filename = filename.replace('.gif', '.webm')
- }
- } else {
- id = id.replace('AAAAC', 'AAAAM')
- }
-
- return {
- type: 'tenor_gif',
- source: 'tenor',
- isGif: true,
- hideDetails: true,
- playerUri: `https://t.gifs.bsky.app/${id}/${filename}`,
- dimensions,
- }
+ return {
+ type: 'tenor_gif',
+ source: 'tenor',
+ isGif: true,
+ hideDetails: true,
+ playerUri,
+ dimensions,
}
}
@@ -516,3 +494,55 @@ export function getGiphyMetaUri(url: URL) {
}
}
}
+
+export function parseTenorGif(urlp: URL):
+ | {success: false}
+ | {
+ success: true
+ playerUri: string
+ dimensions: {height: number; width: number}
+ } {
+ if (urlp.hostname !== 'media.tenor.com') {
+ return {success: false}
+ }
+
+ let [_, id, filename] = urlp.pathname.split('/')
+
+ if (!id || !filename) {
+ return {success: false}
+ }
+
+ if (!id.includes('AAAAC')) {
+ return {success: false}
+ }
+
+ const h = urlp.searchParams.get('hh')
+ const w = urlp.searchParams.get('ww')
+
+ if (!h || !w) {
+ return {success: false}
+ }
+
+ const dimensions = {
+ height: Number(h),
+ width: Number(w),
+ }
+
+ if (isWeb) {
+ if (isSafari) {
+ id = id.replace('AAAAC', 'AAAP1')
+ filename = filename.replace('.gif', '.mp4')
+ } else {
+ id = id.replace('AAAAC', 'AAAP3')
+ filename = filename.replace('.gif', '.webm')
+ }
+ } else {
+ id = id.replace('AAAAC', 'AAAAM')
+ }
+
+ return {
+ success: true,
+ playerUri: `https://t.gifs.bsky.app/${id}/${filename}`,
+ dimensions,
+ }
+}
diff --git a/src/screens/Signup/state.ts b/src/screens/Signup/state.ts
index facc680bd7..87700cb88e 100644
--- a/src/screens/Signup/state.ts
+++ b/src/screens/Signup/state.ts
@@ -252,7 +252,6 @@ export function useSubmitSignup({
dispatch({type: 'setIsLoading', value: true})
try {
- onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view
await createAccount({
service: state.serviceUrl,
email: state.email,
@@ -262,8 +261,12 @@ export function useSubmitSignup({
inviteCode: state.inviteCode.trim(),
verificationCode: verificationCode,
})
+ /*
+ * Must happen last so that if the user has multiple tabs open and
+ * createAccount fails, one tab is not stuck in onboarding — Eric
+ */
+ onboardingDispatch({type: 'start'})
} catch (e: any) {
- onboardingDispatch({type: 'skip'}) // undo starting the onboard
let errMsg = e.toString()
if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
dispatch({
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index 2fb80de37d..4e44c1c695 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -78,6 +78,7 @@ export interface FeedPostSliceItem {
feedContext: string | undefined
moderation: ModerationDecision
parentAuthor?: AppBskyActorDefs.ProfileViewBasic
+ isParentBlocked?: boolean
}
export interface FeedPostSlice {
@@ -311,6 +312,10 @@ export function usePostFeedQuery(
const parentAuthor =
item.reply?.parent?.author ??
slice.items[i + 1]?.reply?.grandparentAuthor
+ const replyRef = item.reply
+ const isParentBlocked = AppBskyFeedDefs.isBlockedPost(
+ replyRef?.parent,
+ )
return {
_reactKey: `${slice._reactKey}-${i}-${item.post.uri}`,
@@ -324,6 +329,7 @@ export function usePostFeedQuery(
feedContext: item.feedContext || slice.feedContext,
moderation: moderations[i],
parentAuthor,
+ isParentBlocked,
}
}
return undefined
diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts
index 6afb3af88c..5a58937faa 100644
--- a/src/state/session/agent.ts
+++ b/src/state/session/agent.ts
@@ -38,21 +38,7 @@ export async function createAgentAndResume(
}
const gates = tryFetchGates(storedAccount.did, 'prefer-low-latency')
const moderation = configureModerationForAccount(agent, storedAccount)
- const prevSession: AtpSessionData = {
- // Sorted in the same property order as when returned by BskyAgent (alphabetical).
- accessJwt: storedAccount.accessJwt ?? '',
- did: storedAccount.did,
- email: storedAccount.email,
- emailAuthFactor: storedAccount.emailAuthFactor,
- emailConfirmed: storedAccount.emailConfirmed,
- handle: storedAccount.handle,
- refreshJwt: storedAccount.refreshJwt ?? '',
- /**
- * @see https://github.com/bluesky-social/atproto/blob/c5d36d5ba2a2c2a5c4f366a5621c06a5608e361e/packages/api/src/agent.ts#L188
- */
- active: storedAccount.active ?? true,
- status: storedAccount.status,
- }
+ const prevSession: AtpSessionData = sessionAccountToSession(storedAccount)
if (isSessionExpired(storedAccount)) {
await networkRetry(1, () => agent.resumeSession(prevSession))
} else {
@@ -253,3 +239,23 @@ export function agentToSessionAccount(
pdsUrl: agent.pdsUrl?.toString(),
}
}
+
+export function sessionAccountToSession(
+ account: SessionAccount,
+): AtpSessionData {
+ return {
+ // Sorted in the same property order as when returned by BskyAgent (alphabetical).
+ accessJwt: account.accessJwt ?? '',
+ did: account.did,
+ email: account.email,
+ emailAuthFactor: account.emailAuthFactor,
+ emailConfirmed: account.emailConfirmed,
+ handle: account.handle,
+ refreshJwt: account.refreshJwt ?? '',
+ /**
+ * @see https://github.com/bluesky-social/atproto/blob/c5d36d5ba2a2c2a5c4f366a5621c06a5608e361e/packages/api/src/agent.ts#L188
+ */
+ active: account.active ?? true,
+ status: account.status,
+ }
+}
diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx
index 371bd459ad..314945bcf9 100644
--- a/src/state/session/index.tsx
+++ b/src/state/session/index.tsx
@@ -14,6 +14,7 @@ import {
createAgentAndCreateAccount,
createAgentAndLogin,
createAgentAndResume,
+ sessionAccountToSession,
} from './agent'
import {getInitialState, reducer} from './reducer'
@@ -175,8 +176,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
if (syncedAccount.did !== state.currentAgentState.did) {
resumeSession(syncedAccount)
} else {
- // @ts-ignore we checked for `refreshJwt` above
- state.currentAgentState.agent.session = syncedAccount
+ const agent = state.currentAgentState.agent as BskyAgent
+ agent.session = sessionAccountToSession(syncedAccount)
}
}
})
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 624c6e5837..fbbfcd53be 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -8,6 +8,7 @@ import {
} from 'react-native'
import {
AppBskyActorDefs,
+ AppBskyEmbedExternal,
AppBskyEmbedImages,
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
@@ -51,6 +52,7 @@ import {TimeElapsed} from '../util/TimeElapsed'
import {PreviewableUserAvatar, UserAvatar} from '../util/UserAvatar'
import hairlineWidth = StyleSheet.hairlineWidth
+import {parseTenorGif} from '#/lib/strings/embed-player'
import {StarterPackIcon} from '#/components/icons/StarterPackIcon'
const MAX_AUTHORS = 5
@@ -487,17 +489,48 @@ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) {
const pal = usePalette('default')
if (post && AppBskyFeedPost.isRecord(post?.record)) {
const text = post.record.text
- const images = AppBskyEmbedImages.isView(post.embed)
- ? post.embed.images
- : AppBskyEmbedRecordWithMedia.isView(post.embed) &&
- AppBskyEmbedImages.isView(post.embed.media)
- ? post.embed.media.images
- : undefined
+ let images
+ let isGif = false
+
+ if (AppBskyEmbedImages.isView(post.embed)) {
+ images = post.embed.images
+ } else if (
+ AppBskyEmbedRecordWithMedia.isView(post.embed) &&
+ AppBskyEmbedImages.isView(post.embed.media)
+ ) {
+ images = post.embed.media.images
+ } else if (
+ AppBskyEmbedExternal.isView(post.embed) &&
+ post.embed.external.thumb
+ ) {
+ let url: URL | undefined
+ try {
+ url = new URL(post.embed.external.uri)
+ } catch {}
+ if (url) {
+ const {success} = parseTenorGif(url)
+ if (success) {
+ isGif = true
+ images = [
+ {
+ thumb: post.embed.external.thumb,
+ alt: post.embed.external.title,
+ fullsize: post.embed.external.thumb,
+ },
+ ]
+ }
+ }
+ }
+
return (
<>
{text?.length > 0 && {text}}
{images && images.length > 0 && (
-
+
)}
>
)
diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx
index e940e8d1a9..1c83ecd6e2 100644
--- a/src/view/com/pager/TabBar.tsx
+++ b/src/view/com/pager/TabBar.tsx
@@ -180,7 +180,7 @@ const desktopStyles = StyleSheet.create({
position: 'absolute',
left: 0,
right: 0,
- bottom: -1,
+ top: '100%',
borderBottomWidth: 1,
},
})
@@ -207,7 +207,7 @@ const mobileStyles = StyleSheet.create({
position: 'absolute',
left: 0,
right: 0,
- bottom: -1,
+ top: '100%',
borderBottomWidth: hairlineWidth,
},
})
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index 8061eb11c9..a6c1a46487 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -331,7 +331,11 @@ export function PostThread({
- setHiddenRepliesState(HiddenRepliesState.ShowAndOverridePostHider)
+ setHiddenRepliesState(
+ item === SHOW_HIDDEN_REPLIES
+ ? HiddenRepliesState.Show
+ : HiddenRepliesState.ShowAndOverridePostHider,
+ )
}
hideTopBorder={index === 0}
/>
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 675f23a88c..cc767a4a3d 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -56,6 +56,7 @@ interface FeedItemProps {
isThreadParent?: boolean
feedContext: string | undefined
hideTopBorder?: boolean
+ isParentBlocked?: boolean
}
export function FeedItem({
@@ -70,6 +71,7 @@ export function FeedItem({
isThreadLastChild,
isThreadParent,
hideTopBorder,
+ isParentBlocked,
}: FeedItemProps & {post: AppBskyFeedDefs.PostView}): React.ReactNode {
const postShadowed = usePostShadow(post)
const richText = useMemo(
@@ -100,6 +102,7 @@ export function FeedItem({
isThreadLastChild={isThreadLastChild}
isThreadParent={isThreadParent}
hideTopBorder={hideTopBorder}
+ isParentBlocked={isParentBlocked}
/>
)
}
@@ -119,6 +122,7 @@ let FeedItemInner = ({
isThreadLastChild,
isThreadParent,
hideTopBorder,
+ isParentBlocked,
}: FeedItemProps & {
richText: RichTextAPI
post: Shadow
@@ -320,7 +324,7 @@ let FeedItemInner = ({
onOpenAuthor={onOpenAuthor}
/>
{!isThreadChild && showReplyTo && parentAuthor && (
-
+
)}
-
- Reply to{' '}
-
-
-
-
+ {blocked ? (
+ Reply to a blocked post
+ ) : (
+
+ Reply to{' '}
+
+
+
+
+ )}
)
diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx
index aeb24e8bbf..3e08f253cf 100644
--- a/src/view/com/posts/FeedSlice.tsx
+++ b/src/view/com/posts/FeedSlice.tsx
@@ -34,6 +34,7 @@ let FeedSlice = ({
isThreadParent={isThreadParentAt(slice.items, 0)}
isThreadChild={isThreadChildAt(slice.items, 0)}
hideTopBorder={hideTopBorder}
+ isParentBlocked={slice.items[0].isParentBlocked}
/>
>
@@ -82,6 +85,7 @@ let FeedSlice = ({
isThreadLastChild={
isThreadChildAt(slice.items, i) && slice.items.length === i + 1
}
+ isParentBlocked={slice.items[i].isParentBlocked}
hideTopBorder={hideTopBorder && i === 0}
/>
))}
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index 587b466a3c..c212ea4c02 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -35,6 +35,7 @@ export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler'
interface BaseUserAvatarProps {
type?: UserAvatarType
+ shape?: 'circle' | 'square'
size: number
avatar?: string | null
}
@@ -60,12 +61,16 @@ const BLUR_AMOUNT = isWeb ? 5 : 100
let DefaultAvatar = ({
type,
+ shape: overrideShape,
size,
}: {
type: UserAvatarType
+ shape?: 'square' | 'circle'
size: number
}): React.ReactNode => {
+ const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square')
if (type === 'algo') {
+ // TODO: shape=circle
// Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc.
return (