Merge branch 'main' into notification-follow-buttons
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
type AppBskyAgeassuranceGetConfig,
|
||||
type AppBskyAgeassuranceGetState,
|
||||
AtpAgent,
|
||||
type ChatBskyActorDeclaration,
|
||||
getAgeAssuranceRegionConfig,
|
||||
} from '@atproto/api'
|
||||
import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister'
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
hasSnoozedBirthdateUpdateForDid,
|
||||
snoozeBirthdateUpdateAllowedForDid,
|
||||
} from '#/state/birthdate'
|
||||
import {fetchActorDeclarationRecord} from '#/state/queries/messages/actor-declaration'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import * as debug from '#/ageAssurance/debug'
|
||||
import {logger} from '#/ageAssurance/logger'
|
||||
@@ -53,7 +55,7 @@ const [, cacheHydrationPromise] = persistQueryClient({
|
||||
persister,
|
||||
})
|
||||
|
||||
function getDidFromAgentSession(agent: AtpAgent) {
|
||||
export function getDidFromAgentSession(agent: AtpAgent) {
|
||||
const sessionManager = agent.sessionManager
|
||||
if (!sessionManager || !sessionManager.did) return
|
||||
return sessionManager.did
|
||||
@@ -329,19 +331,25 @@ export function useServerStateQuery() {
|
||||
|
||||
export type OtherRequiredData = {
|
||||
birthdate: string | undefined
|
||||
actorDeclaration?: ChatBskyActorDeclaration.Main
|
||||
}
|
||||
export function createOtherRequiredDataQueryKey({did}: {did: string}) {
|
||||
return ['otherRequiredData', did]
|
||||
}
|
||||
export async function getOtherRequiredData({
|
||||
async function getOtherRequiredData({
|
||||
agent,
|
||||
}: {
|
||||
agent: AtpAgent
|
||||
}): Promise<OtherRequiredData> {
|
||||
if (debug.enabled) return debug.resolve(debug.otherRequiredData)
|
||||
const [prefs] = await Promise.all([agent.getPreferences()])
|
||||
const did = getDidFromAgentSession(agent)
|
||||
const [prefs, actorDeclaration] = await Promise.all([
|
||||
agent.getPreferences(),
|
||||
fetchActorDeclarationRecord({did, agent}),
|
||||
])
|
||||
const data: OtherRequiredData = {
|
||||
birthdate: prefs.birthDate ? prefs.birthDate.toISOString() : undefined,
|
||||
actorDeclaration,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -359,7 +367,6 @@ export async function getOtherRequiredData({
|
||||
}
|
||||
}
|
||||
|
||||
const did = getDidFromAgentSession(agent)
|
||||
if (data && did && birthdateCache.has(did)) {
|
||||
/*
|
||||
* If birthdate was just set, use the local cache value. On subsequent
|
||||
@@ -394,6 +401,26 @@ export function getOtherRequiredDataFromCache({
|
||||
createOtherRequiredDataQueryKey({did}),
|
||||
)
|
||||
}
|
||||
export function setOtherRequiredDataActorDeclarationCache({
|
||||
did,
|
||||
actorDeclaration,
|
||||
}: {
|
||||
did: string
|
||||
actorDeclaration: ChatBskyActorDeclaration.Main
|
||||
}) {
|
||||
const prev = getOtherRequiredDataFromCache({did})
|
||||
const next: OtherRequiredData = {
|
||||
birthdate: prev?.birthdate,
|
||||
actorDeclaration: {
|
||||
...(prev?.actorDeclaration || {}),
|
||||
...actorDeclaration,
|
||||
},
|
||||
}
|
||||
qc.setQueryData<OtherRequiredData>(
|
||||
createOtherRequiredDataQueryKey({did}),
|
||||
next,
|
||||
)
|
||||
}
|
||||
export async function prefetchOtherRequiredData({agent}: {agent: AtpAgent}) {
|
||||
const did = getDidFromAgentSession(agent)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {createContext, useCallback, useContext, useEffect, useMemo} from 'react'
|
||||
|
||||
import {useGetAndRegisterPushToken} from '#/lib/notifications/notifications'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {Provider as RedirectOverlayProvider} from '#/ageAssurance/components/RedirectOverlay'
|
||||
import {
|
||||
AgeAssuranceDataProvider,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
} from '#/ageAssurance/types'
|
||||
import {
|
||||
isUnderAge,
|
||||
maybeRestrictChatSettings,
|
||||
MIN_ACCESS_AGE,
|
||||
useAgeAssuranceRegionConfigWithFallback,
|
||||
} from '#/ageAssurance/util'
|
||||
@@ -78,6 +80,7 @@ export function Provider({children}: {children: React.ReactNode}) {
|
||||
}
|
||||
|
||||
function InnerProvider({children}: {children: React.ReactNode}) {
|
||||
const agent = useAgent()
|
||||
const state = useAgeAssuranceState()
|
||||
const {data} = useAgeAssuranceDataContext()
|
||||
const config = useAgeAssuranceRegionConfigWithFallback()
|
||||
@@ -85,11 +88,13 @@ function InnerProvider({children}: {children: React.ReactNode}) {
|
||||
|
||||
const handleAccessUpdate = useCallback(
|
||||
(s: AgeAssuranceState) => {
|
||||
void getAndRegisterPushToken({
|
||||
isAgeRestricted: s.access !== AgeAssuranceAccess.Full,
|
||||
})
|
||||
const isAgeRestricted = s.access !== AgeAssuranceAccess.Full
|
||||
if (isAgeRestricted) {
|
||||
void getAndRegisterPushToken({isAgeRestricted})
|
||||
maybeRestrictChatSettings({agent})
|
||||
}
|
||||
},
|
||||
[getAndRegisterPushToken],
|
||||
[agent, getAndRegisterPushToken],
|
||||
)
|
||||
useOnAgeAssuranceAccessUpdate(handleAccessUpdate)
|
||||
|
||||
|
||||
+140
-71
@@ -1,8 +1,15 @@
|
||||
import {useEffect, useMemo, useState} from 'react'
|
||||
import {computeAgeAssuranceRegionAccess} from '@atproto/api'
|
||||
|
||||
import {getAge} from '#/lib/strings/time'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useAgeAssuranceDataContext} from '#/ageAssurance/data'
|
||||
import {
|
||||
type AgeAssuranceData,
|
||||
getConfigFromCache,
|
||||
getOtherRequiredDataFromCache,
|
||||
getServerStateFromCache,
|
||||
useAgeAssuranceDataContext,
|
||||
} from '#/ageAssurance/data'
|
||||
import {logger} from '#/ageAssurance/logger'
|
||||
import {
|
||||
AgeAssuranceAccess,
|
||||
@@ -12,82 +19,144 @@ import {
|
||||
parseStatusFromString,
|
||||
} from '#/ageAssurance/types'
|
||||
import {getAgeAssuranceRegionConfigWithFallback} from '#/ageAssurance/util'
|
||||
import {useGeolocation} from '#/geolocation'
|
||||
import {type Geolocation, useGeolocation} from '#/geolocation'
|
||||
import {device} from '#/storage'
|
||||
|
||||
/**
|
||||
* Get final evaluated age assurance state. Handles fallbacks and defers to
|
||||
* server state before computing access based on AA config from the server +
|
||||
* geolocation and other data.
|
||||
*/
|
||||
export function computeAgeAssuranceState({
|
||||
hasSession,
|
||||
config,
|
||||
geolocation,
|
||||
state,
|
||||
data,
|
||||
}: {
|
||||
hasSession: boolean
|
||||
config: AgeAssuranceData['config']
|
||||
geolocation: Geolocation
|
||||
state: AgeAssuranceData['state']
|
||||
data: AgeAssuranceData['data']
|
||||
}) {
|
||||
/**
|
||||
* This is where we control logged-out moderation prefs. It's all
|
||||
* downstream of AA now.
|
||||
*/
|
||||
if (!hasSession)
|
||||
return {
|
||||
status: AgeAssuranceStatus.Unknown,
|
||||
access: AgeAssuranceAccess.Safe,
|
||||
}
|
||||
|
||||
/**
|
||||
* This can happen if the prefetch fails (such as due to network issues).
|
||||
* The query handler will try it again, but if it continues to fail, of
|
||||
* course we won't have config.
|
||||
*
|
||||
* In this case, fail open to avoid blocking users.
|
||||
*/
|
||||
if (!config) {
|
||||
logger.warn('useAgeAssuranceState: missing config')
|
||||
return {
|
||||
status: AgeAssuranceStatus.Unknown,
|
||||
access: AgeAssuranceAccess.Safe,
|
||||
error: 'config' as const,
|
||||
}
|
||||
}
|
||||
|
||||
const region = getAgeAssuranceRegionConfigWithFallback(config, geolocation)
|
||||
const isAARequired = region.countryCode !== '*'
|
||||
const isTerminalState =
|
||||
state?.status === 'assured' || state?.status === 'blocked'
|
||||
|
||||
/*
|
||||
* If we are in a terminal state and AA is required for this region,
|
||||
* we can trust the server state completely and avoid recomputing.
|
||||
*/
|
||||
if (isTerminalState && isAARequired) {
|
||||
return {
|
||||
lastInitiatedAt: state.lastInitiatedAt,
|
||||
status: parseStatusFromString(state.status),
|
||||
access: parseAccessFromString(state.access),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Otherwise, we need to compute the access based on the latest data. For
|
||||
* accounts with an accurate birthdate, our default fallback rules should
|
||||
* ensure correct access.
|
||||
*/
|
||||
const result = computeAgeAssuranceRegionAccess(region, data)
|
||||
const computed = {
|
||||
lastInitiatedAt: state?.lastInitiatedAt,
|
||||
// prefer server state
|
||||
status: state?.status
|
||||
? parseStatusFromString(state?.status)
|
||||
: AgeAssuranceStatus.Unknown,
|
||||
// prefer server state
|
||||
access: result
|
||||
? parseAccessFromString(result.access)
|
||||
: AgeAssuranceAccess.Full,
|
||||
}
|
||||
logger.debug('debug useAgeAssuranceState', {
|
||||
region,
|
||||
state,
|
||||
data,
|
||||
computed,
|
||||
})
|
||||
return computed
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a last-ditch helper for out-of-band reads of the AA state, such as
|
||||
* during account creation. Don't use it for anything else.
|
||||
*/
|
||||
export function getAndComputeAgeAssuranceState({did}: {did: string}) {
|
||||
const config = getConfigFromCache()
|
||||
const state = getServerStateFromCache({did})
|
||||
const data = getOtherRequiredDataFromCache({did})
|
||||
const geolocation = device.get(['mergedGeolocation'])
|
||||
|
||||
if (!geolocation || !config || !state || !data) {
|
||||
return {
|
||||
status: AgeAssuranceStatus.Unknown,
|
||||
access: AgeAssuranceAccess.Safe,
|
||||
}
|
||||
}
|
||||
|
||||
return computeAgeAssuranceState({
|
||||
hasSession: true,
|
||||
config,
|
||||
geolocation,
|
||||
state: state.state,
|
||||
data: {
|
||||
accountCreatedAt: state.metadata?.accountCreatedAt,
|
||||
declaredAge: data?.birthdate
|
||||
? getAge(new Date(data.birthdate))
|
||||
: undefined,
|
||||
birthdate: data?.birthdate,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useAgeAssuranceState(): AgeAssuranceState {
|
||||
const {hasSession} = useSession()
|
||||
const geolocation = useGeolocation()
|
||||
const {config, state, data} = useAgeAssuranceDataContext()
|
||||
|
||||
return useMemo(() => {
|
||||
/**
|
||||
* This is where we control logged-out moderation prefs. It's all
|
||||
* downstream of AA now.
|
||||
*/
|
||||
if (!hasSession)
|
||||
return {
|
||||
status: AgeAssuranceStatus.Unknown,
|
||||
access: AgeAssuranceAccess.Safe,
|
||||
}
|
||||
|
||||
/**
|
||||
* This can happen if the prefetch fails (such as due to network issues).
|
||||
* The query handler will try it again, but if it continues to fail, of
|
||||
* course we won't have config.
|
||||
*
|
||||
* In this case, fail open to avoid blocking users.
|
||||
*/
|
||||
if (!config) {
|
||||
logger.warn('useAgeAssuranceState: missing config')
|
||||
return {
|
||||
status: AgeAssuranceStatus.Unknown,
|
||||
access: AgeAssuranceAccess.Safe,
|
||||
error: 'config',
|
||||
}
|
||||
}
|
||||
|
||||
const region = getAgeAssuranceRegionConfigWithFallback(config, geolocation)
|
||||
const isAARequired = region.countryCode !== '*'
|
||||
const isTerminalState =
|
||||
state?.status === 'assured' || state?.status === 'blocked'
|
||||
|
||||
/*
|
||||
* If we are in a terminal state and AA is required for this region,
|
||||
* we can trust the server state completely and avoid recomputing.
|
||||
*/
|
||||
if (isTerminalState && isAARequired) {
|
||||
return {
|
||||
lastInitiatedAt: state.lastInitiatedAt,
|
||||
status: parseStatusFromString(state.status),
|
||||
access: parseAccessFromString(state.access),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Otherwise, we need to compute the access based on the latest data. For
|
||||
* accounts with an accurate birthdate, our default fallback rules should
|
||||
* ensure correct access.
|
||||
*/
|
||||
const result = computeAgeAssuranceRegionAccess(region, data)
|
||||
const computed = {
|
||||
lastInitiatedAt: state?.lastInitiatedAt,
|
||||
// prefer server state
|
||||
status: state?.status
|
||||
? parseStatusFromString(state?.status)
|
||||
: AgeAssuranceStatus.Unknown,
|
||||
// prefer server state
|
||||
access: result
|
||||
? parseAccessFromString(result.access)
|
||||
: AgeAssuranceAccess.Full,
|
||||
}
|
||||
logger.debug('debug useAgeAssuranceState', {
|
||||
region,
|
||||
state,
|
||||
data,
|
||||
computed,
|
||||
})
|
||||
return computed
|
||||
}, [hasSession, geolocation, config, state, data])
|
||||
return useMemo(
|
||||
() =>
|
||||
computeAgeAssuranceState({
|
||||
hasSession,
|
||||
config,
|
||||
geolocation,
|
||||
state,
|
||||
data,
|
||||
}),
|
||||
[hasSession, geolocation, config, state, data],
|
||||
)
|
||||
}
|
||||
|
||||
export function useOnAgeAssuranceAccessUpdate(
|
||||
|
||||
@@ -2,13 +2,19 @@ import {useMemo} from 'react'
|
||||
import {
|
||||
ageAssuranceRuleIDs as ids,
|
||||
type AppBskyAgeassuranceDefs,
|
||||
type AtpAgent,
|
||||
getAgeAssuranceRegionConfig,
|
||||
type ModerationPrefs,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {getAge} from '#/lib/strings/time'
|
||||
import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings'
|
||||
import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
|
||||
import {useAgeAssuranceDataContext} from '#/ageAssurance/data'
|
||||
import {
|
||||
getDidFromAgentSession,
|
||||
getOtherRequiredDataFromCache,
|
||||
useAgeAssuranceDataContext,
|
||||
} from '#/ageAssurance/data'
|
||||
import {AgeAssuranceAccess} from '#/ageAssurance/types'
|
||||
import {type Geolocation, useGeolocation} from '#/geolocation'
|
||||
|
||||
@@ -109,3 +115,16 @@ export const makeAgeRestrictedModerationPrefs = (
|
||||
adultContentEnabled: false,
|
||||
labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES,
|
||||
})
|
||||
|
||||
/**
|
||||
* Checks our cache of the actor's chat declaration record, and if it's not
|
||||
* already restricted, restricts it.
|
||||
*/
|
||||
export function maybeRestrictChatSettings({agent}: {agent: AtpAgent}) {
|
||||
const did = getDidFromAgentSession(agent)
|
||||
if (!did) return
|
||||
const data = getOtherRequiredDataFromCache({did})
|
||||
// ...update the chat setting record if allowIncoming is not already 'none'.
|
||||
if (data?.actorDeclaration?.allowIncoming === 'none') return
|
||||
restrictChatSettings({agent, did})
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ export enum Features {
|
||||
ImageUploadsHighResolution = 'image_uploads:high_resolution',
|
||||
ImageUploadsBlobSize2mbEnabled = 'image_uploads:blob_size_2mb:enabled',
|
||||
GroupChatsEnable = 'group_chats:enable',
|
||||
GroupChatsHasBeenReleased = 'group_chats:has_been_released',
|
||||
DmsNewMessageComposerEnable = 'dms:new_message_composer:enable',
|
||||
ComposerLanguageDetectionEnable = 'composer:language_detection:enable',
|
||||
KlipyGifProviderEnable = 'klipy_gif_provider:enable',
|
||||
PostGalleryEmbedEnable = 'post_gallery_embed:enable',
|
||||
NotificationsExpandedProfileCardEnable = 'notifications:expanded_profile_card:enable',
|
||||
|
||||
@@ -487,6 +487,7 @@ export type Events = {
|
||||
| 'SeeMoreSuggestedUsers'
|
||||
| 'ProgressGuide'
|
||||
location: 'Card' | 'Profile' | 'FollowAll'
|
||||
recSource?: 'Search'
|
||||
recId?: number | string
|
||||
position: number
|
||||
suggestedDid: string
|
||||
@@ -514,6 +515,7 @@ export type Events = {
|
||||
| 'Onboarding'
|
||||
| 'SeeMoreSuggestedUsers'
|
||||
| 'ProgressGuide'
|
||||
recSource?: 'Search'
|
||||
recId?: number | string
|
||||
position: number
|
||||
suggestedDid: string
|
||||
@@ -798,6 +800,100 @@ export type Events = {
|
||||
*/
|
||||
resultSourceLanguage: string
|
||||
}
|
||||
'composer:language:suggestLanguage': {
|
||||
os: Platform['OS']
|
||||
/**
|
||||
* The language we detected and suggested to the user as an override for the
|
||||
* expected target language.
|
||||
*/
|
||||
suggestedLanguage: string | undefined
|
||||
/**
|
||||
* This is the user's current composer languages, which are always defined.
|
||||
*/
|
||||
currentTargetLanguages: string[]
|
||||
/**
|
||||
* The length of the text being translated. We assume shorter texts are
|
||||
* more likely to have inaccurate translations.
|
||||
*/
|
||||
textLength: number
|
||||
}
|
||||
'composer:language:acceptSuggestion': {
|
||||
os: Platform['OS']
|
||||
/**
|
||||
* The language we detected and suggested to the user as an override for the
|
||||
* expected target language.
|
||||
*/
|
||||
suggestedLanguage: string | undefined
|
||||
/**
|
||||
* This is the user's current composer languages, which are always defined.
|
||||
*/
|
||||
currentTargetLanguages: string[]
|
||||
/**
|
||||
* The length of the text being translated. We assume shorter texts are
|
||||
* more likely to have inaccurate translations.
|
||||
*/
|
||||
textLength: number
|
||||
}
|
||||
'composer:language:declineSuggestion': {
|
||||
os: Platform['OS']
|
||||
/**
|
||||
* The language we detected and suggested to the user as an override for the
|
||||
* expected target language.
|
||||
*/
|
||||
suggestedLanguage: string | undefined
|
||||
/**
|
||||
* This is the user's current composer languages, which are always defined.
|
||||
*/
|
||||
currentTargetLanguages: string[]
|
||||
/**
|
||||
* The length of the text being translated. We assume shorter texts are
|
||||
* more likely to have inaccurate translations.
|
||||
*/
|
||||
textLength: number
|
||||
}
|
||||
'composer:language:replyNudgeAccept': {
|
||||
/**
|
||||
* The language of the post the user is replying to.
|
||||
*/
|
||||
replyToLanguage: string
|
||||
/**
|
||||
* This is the user's current composer languages, which are always defined.
|
||||
*/
|
||||
currentTargetLanguages: string[]
|
||||
}
|
||||
'composer:language:replyNudgeDecline': {
|
||||
/**
|
||||
* The language of the post the user is replying to.
|
||||
*/
|
||||
replyToLanguage: string
|
||||
/**
|
||||
* This is the user's current composer languages, which are always defined.
|
||||
*/
|
||||
currentTargetLanguages: string[]
|
||||
}
|
||||
'composer:language:nudgeUser': {
|
||||
os: Platform['OS']
|
||||
/**
|
||||
* The language we detected and suggested to the user as an override for the
|
||||
* expected target language.
|
||||
*/
|
||||
suggestedLanguage: string | undefined
|
||||
/**
|
||||
* This is the user's current composer languages, which are always defined.
|
||||
*/
|
||||
currentTargetLanguages: string[]
|
||||
/**
|
||||
* The length of the text being translated. We assume shorter texts are
|
||||
* more likely to have inaccurate translations.
|
||||
*/
|
||||
textLength: number
|
||||
}
|
||||
'composer:language:langSelectorPressed': {
|
||||
/**
|
||||
* If the user was nudged by our language detection to update their language
|
||||
*/
|
||||
wasNudged: boolean
|
||||
}
|
||||
|
||||
'postMenu:openMuteWordsDialog': {
|
||||
uri: string
|
||||
|
||||
@@ -28,7 +28,10 @@ export function AvatarBubbles({
|
||||
size = 'large',
|
||||
}: Props) {
|
||||
const {currentAccount} = useSession()
|
||||
const profiles = allProfiles.filter(p => p.did !== currentAccount?.did)
|
||||
const profiles =
|
||||
allProfiles.length > 2
|
||||
? allProfiles.filter(p => p.did !== currentAccount?.did)
|
||||
: allProfiles
|
||||
const containerSize =
|
||||
typeof size === 'number'
|
||||
? size
|
||||
@@ -45,7 +48,12 @@ export function AvatarBubbles({
|
||||
: size === 'medium'
|
||||
? 56 / 120
|
||||
: 1
|
||||
const marginOffset = size === 'small' || size === 'medium' ? -2 : 0
|
||||
const marginOffset =
|
||||
(typeof size === 'number' && size < 120) ||
|
||||
size === 'small' ||
|
||||
size === 'medium'
|
||||
? -2
|
||||
: 0
|
||||
|
||||
const initialValue = animate ? 0 : 1
|
||||
const p0 = useSharedValue(initialValue)
|
||||
@@ -81,7 +89,7 @@ export function AvatarBubbles({
|
||||
let avatars = (
|
||||
<>
|
||||
<AvatarBubble
|
||||
profile={profiles[0] ?? allProfiles[0]}
|
||||
profile={profiles[0]}
|
||||
scale={p0}
|
||||
size={76}
|
||||
x={-2}
|
||||
|
||||
@@ -54,7 +54,7 @@ import {HITSLOP_10} from '#/lib/constants'
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {logger} from '#/logger'
|
||||
import {atoms as a, platform, tokens, useTheme} from '#/alf'
|
||||
import {atoms as a, flatten, platform, tokens, useTheme} from '#/alf'
|
||||
import {
|
||||
Context,
|
||||
ItemContext,
|
||||
@@ -527,7 +527,8 @@ export function AuxiliaryView({
|
||||
}
|
||||
})
|
||||
|
||||
const menuContext = useMemo(() => ({align}), [align])
|
||||
const xOffset = (flatten(style)?.marginLeft as number) ?? 0
|
||||
const menuContext = useMemo(() => ({align, xOffset}), [align, xOffset])
|
||||
|
||||
const onLayout = useCallback(() => {
|
||||
if (!measurement) return
|
||||
@@ -655,7 +656,8 @@ export function Outer({
|
||||
[context.measurement, frame.height, insets, translationSV],
|
||||
)
|
||||
|
||||
const menuContext = useMemo(() => ({align}), [align])
|
||||
const xOffset = (flatten(style)?.marginLeft as number) ?? 0
|
||||
const menuContext = useMemo(() => ({align, xOffset}), [align, xOffset])
|
||||
|
||||
if (!context.isOpen || !context.measurement) return null
|
||||
|
||||
@@ -763,7 +765,7 @@ export function Item({
|
||||
onOut: onPressOut,
|
||||
} = useInteractionState()
|
||||
const id = useId()
|
||||
const {align} = useContextMenuMenuContext()
|
||||
const {align, xOffset: menuXOffset} = useContextMenuMenuContext()
|
||||
|
||||
const {close, measurement, registerHoverable} = context
|
||||
|
||||
@@ -779,8 +781,8 @@ export function Item({
|
||||
const xOffset = position
|
||||
? position.x
|
||||
: align === 'left'
|
||||
? measurement.x
|
||||
: measurement.x + measurement.width - layout.width
|
||||
? measurement.x + menuXOffset
|
||||
: measurement.x + measurement.width - layout.width - menuXOffset
|
||||
|
||||
registerHoverable(
|
||||
id,
|
||||
@@ -796,7 +798,16 @@ export function Item({
|
||||
},
|
||||
)
|
||||
},
|
||||
[id, measurement, registerHoverable, close, onPress, align, position],
|
||||
[
|
||||
id,
|
||||
measurement,
|
||||
registerHoverable,
|
||||
close,
|
||||
onPress,
|
||||
align,
|
||||
menuXOffset,
|
||||
position,
|
||||
],
|
||||
)
|
||||
|
||||
const itemContext = useMemo(
|
||||
|
||||
@@ -65,6 +65,7 @@ export type ContextType = {
|
||||
|
||||
export type MenuContextType = {
|
||||
align: 'left' | 'right'
|
||||
xOffset: number
|
||||
}
|
||||
|
||||
export type ItemContextType = {
|
||||
|
||||
@@ -167,6 +167,7 @@ export function SuggestedFollowsHome() {
|
||||
|
||||
return (
|
||||
<ProfileGrid
|
||||
recId={data?.recId}
|
||||
isSuggestionsLoading={isLoading}
|
||||
profiles={filteredProfiles}
|
||||
totalProfileCount={allProfiles.length}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import {forwardRef, memo, useContext, useMemo} from 'react'
|
||||
import {StyleSheet, View, type ViewProps, type ViewStyle} from 'react-native'
|
||||
import {type StyleProp} from 'react-native'
|
||||
import {
|
||||
type StyleProp,
|
||||
StyleSheet,
|
||||
View,
|
||||
type ViewProps,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
KeyboardAwareScrollView,
|
||||
type KeyboardAwareScrollViewProps,
|
||||
@@ -11,6 +16,7 @@ import Animated, {
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
|
||||
import {useEnableMinimalShellModeForScreen} from '#/state/shell'
|
||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||
import {
|
||||
atoms as a,
|
||||
@@ -30,6 +36,7 @@ export * as Header from '#/components/Layout/Header'
|
||||
export type ScreenProps = React.ComponentProps<typeof View> & {
|
||||
style?: StyleProp<ViewStyle>
|
||||
noInsetTop?: boolean
|
||||
minimalShell?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,9 +45,13 @@ export type ScreenProps = React.ComponentProps<typeof View> & {
|
||||
export const Screen = memo(function Screen({
|
||||
style,
|
||||
noInsetTop,
|
||||
minimalShell = false,
|
||||
...props
|
||||
}: ScreenProps) {
|
||||
const {top} = useSafeAreaInsets()
|
||||
|
||||
useEnableMinimalShellModeForScreen({enabled: minimalShell})
|
||||
|
||||
return (
|
||||
<>
|
||||
{IS_WEB && <WebCenterBorders />}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {useImperativeHandle, useRef, useState} from 'react'
|
||||
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {BlueskyVideoView} from '@haileyok/bluesky-video'
|
||||
import {BlueskyVideoView} from '@bsky.app/video'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
|
||||
@@ -249,6 +249,7 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
moderationOpts={moderationOpts!}
|
||||
noBorder={index === 0}
|
||||
position={index}
|
||||
recSource={hasSearchText ? 'Search' : undefined}
|
||||
recId={recIdForLogging}
|
||||
isGuide={isGuide}
|
||||
/>
|
||||
@@ -264,7 +265,7 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
return null
|
||||
}
|
||||
},
|
||||
[moderationOpts, recIdForLogging, isGuide],
|
||||
[moderationOpts, hasSearchText, recIdForLogging, isGuide],
|
||||
)
|
||||
|
||||
// Track seen profiles
|
||||
@@ -286,6 +287,7 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
|
||||
)
|
||||
ax.metric('suggestedUser:seen', {
|
||||
logContext: isGuide ? 'ProgressGuide' : 'SeeMoreSuggestedUsers',
|
||||
recSource: hasSearchText ? 'Search' : undefined,
|
||||
recId: recIdForLogging,
|
||||
position: position !== -1 ? position : 0,
|
||||
suggestedDid: item.profile.did,
|
||||
@@ -548,6 +550,7 @@ let FollowProfileCard = ({
|
||||
moderationOpts,
|
||||
noBorder,
|
||||
position,
|
||||
recSource,
|
||||
recId,
|
||||
isGuide,
|
||||
}: {
|
||||
@@ -555,6 +558,7 @@ let FollowProfileCard = ({
|
||||
moderationOpts: ModerationOpts
|
||||
noBorder?: boolean
|
||||
position: number
|
||||
recSource?: 'Search'
|
||||
recId?: string
|
||||
isGuide: boolean
|
||||
}): React.ReactNode => {
|
||||
@@ -564,6 +568,7 @@ let FollowProfileCard = ({
|
||||
moderationOpts={moderationOpts}
|
||||
noBorder={noBorder}
|
||||
position={position}
|
||||
recSource={recSource}
|
||||
recId={recId}
|
||||
isGuide={isGuide}
|
||||
/>
|
||||
@@ -577,6 +582,7 @@ function FollowProfileCardInner({
|
||||
onFollow,
|
||||
noBorder,
|
||||
position,
|
||||
recSource,
|
||||
recId,
|
||||
isGuide,
|
||||
}: {
|
||||
@@ -585,6 +591,7 @@ function FollowProfileCardInner({
|
||||
onFollow?: () => void
|
||||
noBorder?: boolean
|
||||
position: number
|
||||
recSource?: 'Search'
|
||||
recId?: string
|
||||
isGuide: boolean
|
||||
}) {
|
||||
@@ -625,6 +632,7 @@ function FollowProfileCardInner({
|
||||
? 'ProgressGuide'
|
||||
: 'SeeMoreSuggestedUsers',
|
||||
location: 'Card',
|
||||
recSource,
|
||||
recId,
|
||||
position,
|
||||
suggestedDid: profile.did,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {useCallback, useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {useCleanError} from '#/lib/hooks/useCleanError'
|
||||
import {isAppPassword} from '#/lib/jwt'
|
||||
@@ -34,7 +32,7 @@ export function BirthDateSettingsDialog({
|
||||
control: Dialog.DialogControlProps
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const {isLoading, error, data: preferences} = usePreferencesQuery()
|
||||
const isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed()
|
||||
const {currentAccount} = useSession()
|
||||
@@ -45,11 +43,11 @@ export function BirthDateSettingsDialog({
|
||||
<Dialog.Handle />
|
||||
{isBirthdateUpdateAllowed ? (
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`My Birthdate`)}
|
||||
label={l`My birthdate`}
|
||||
style={web({maxWidth: 400})}>
|
||||
<View style={[a.gap_md]}>
|
||||
<Text style={[a.text_xl, a.font_semi_bold]}>
|
||||
<Trans>My Birthdate</Trans>
|
||||
<Trans>My birthdate</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
@@ -64,9 +62,7 @@ export function BirthDateSettingsDialog({
|
||||
<ErrorMessage
|
||||
message={
|
||||
error?.toString() ||
|
||||
_(
|
||||
msg`We were unable to load your birthdate preferences. Please try again.`,
|
||||
)
|
||||
l`We were unable to load your birthdate preferences. Please try again.`
|
||||
}
|
||||
style={[a.rounded_sm]}
|
||||
/>
|
||||
@@ -88,7 +84,7 @@ export function BirthDateSettingsDialog({
|
||||
</Dialog.ScrollableInner>
|
||||
) : (
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`You recently changed your birthdate`)}
|
||||
label={l`You recently changed your birthdate`}
|
||||
style={web({maxWidth: 400})}>
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text
|
||||
@@ -123,15 +119,16 @@ function BirthdayInner({
|
||||
control: Dialog.DialogControlProps
|
||||
preferences: UsePreferencesQueryResponse
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const cleanError = useCleanError()
|
||||
const [date, setDate] = useState(preferences.birthDate || getDateAgo(18))
|
||||
const {isPending, error, mutateAsync: setBirthDate} = useBirthdateMutation()
|
||||
const hasChanged = date !== preferences.birthDate
|
||||
const errorMessage = useMemo(() => {
|
||||
if (error) {
|
||||
const {raw, clean} = cleanError(error)
|
||||
return clean || raw || error.toString()
|
||||
const e = error as Error
|
||||
const {raw, clean} = cleanError(e)
|
||||
return clean || raw || e.toString()
|
||||
}
|
||||
}, [error, cleanError])
|
||||
|
||||
@@ -146,7 +143,8 @@ function BirthdayInner({
|
||||
await setBirthDate({birthDate: date})
|
||||
}
|
||||
control.close()
|
||||
} catch (e: any) {
|
||||
} catch (error) {
|
||||
const e = error as Error
|
||||
logger.error(`setBirthDate failed`, {message: e.message})
|
||||
}
|
||||
}, [date, setBirthDate, control, hasChanged])
|
||||
@@ -158,11 +156,10 @@ function BirthdayInner({
|
||||
testID="birthdayInput"
|
||||
value={date}
|
||||
onChangeDate={newDate => setDate(new Date(newDate))}
|
||||
label={_(msg`Birthdate`)}
|
||||
accessibilityHint={_(msg`Enter your birthdate`)}
|
||||
label={l`Birthdate`}
|
||||
accessibilityHint={l`Enter your birthdate`}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{isUnder18 && hasChanged && (
|
||||
<Admonition type="info">
|
||||
<Trans>
|
||||
@@ -171,30 +168,27 @@ function BirthdayInner({
|
||||
</Trans>
|
||||
</Admonition>
|
||||
)}
|
||||
|
||||
{isUnder13 && (
|
||||
<Admonition type="error">
|
||||
<Trans>
|
||||
You must be at least 13 years old to use Bluesky. Read our{' '}
|
||||
<SimpleInlineLinkText
|
||||
to="https://bsky.social/about/support/tos"
|
||||
label={_(msg`Terms of Service`)}>
|
||||
label={l`Terms of Service`}>
|
||||
Terms of Service
|
||||
</SimpleInlineLinkText>{' '}
|
||||
for more information.
|
||||
</Trans>
|
||||
</Admonition>
|
||||
)}
|
||||
|
||||
{errorMessage ? (
|
||||
<ErrorMessage message={errorMessage} style={[a.rounded_sm]} />
|
||||
) : undefined}
|
||||
|
||||
<View style={IS_WEB && [a.flex_row, a.justify_end]}>
|
||||
<Button
|
||||
label={hasChanged ? _(msg`Save birthdate`) : _(msg`Done`)}
|
||||
label={hasChanged ? l`Save birthdate` : l`Done`}
|
||||
size="large"
|
||||
onPress={onSave}
|
||||
onPress={() => void onSave()}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
disabled={isUnder13}>
|
||||
|
||||
@@ -56,12 +56,11 @@ let DateDivider = ({date: dateStr}: {date: string}): React.ReactNode => {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.w_full, a.my_sm]}>
|
||||
<View style={[a.w_full, a.mt_md]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_xs,
|
||||
a.text_center,
|
||||
t.atoms.bg,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.px_md,
|
||||
]}>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function EmojiReactionPicker({
|
||||
const t = useTheme()
|
||||
const isFromSelf = message.sender?.did === currentAccount?.did
|
||||
const {measurement, close} = useContextMenuContext()
|
||||
const {align} = useContextMenuMenuContext()
|
||||
const {align, xOffset} = useContextMenuMenuContext()
|
||||
const [layout, setLayout] = useState({width: 0, height: 0})
|
||||
const {width: screenWidth} = useWindowDimensions()
|
||||
|
||||
@@ -44,12 +44,15 @@ export function EmojiReactionPicker({
|
||||
|
||||
const position = useMemo(() => {
|
||||
return {
|
||||
x: align === 'left' ? 12 : screenWidth - layout.width - 12,
|
||||
x:
|
||||
align === 'left'
|
||||
? (measurement?.x ?? 0) + xOffset
|
||||
: (measurement?.x ?? 0) + (measurement?.width ?? 0) - layout.width,
|
||||
y: (measurement?.y ?? 0) - tokens.space.xs - layout.height,
|
||||
height: layout.height,
|
||||
width: layout.width,
|
||||
}
|
||||
}, [measurement, align, screenWidth, layout])
|
||||
}, [measurement, align, xOffset, screenWidth, layout])
|
||||
|
||||
const limitReacted = hasReachedReactionLimit(message, currentAccount?.did)
|
||||
|
||||
|
||||
@@ -168,12 +168,18 @@ let MessageItem = ({
|
||||
isInCluster && !effectiveFirstInCluster && !effectiveLastInCluster
|
||||
|
||||
const hasReactions = message.reactions && message.reactions.length > 0
|
||||
const prevHasReactions =
|
||||
prevIsMessage &&
|
||||
prevMessage.reactions != null &&
|
||||
prevMessage.reactions.length > 0
|
||||
const squaredBottomCorner =
|
||||
!hasReactions &&
|
||||
isInCluster &&
|
||||
(isInMiddleOfCluster || effectiveFirstInCluster)
|
||||
const squaredTopCorner =
|
||||
isInCluster && (isInMiddleOfCluster || effectiveLastInCluster)
|
||||
!prevHasReactions &&
|
||||
isInCluster &&
|
||||
(isInMiddleOfCluster || effectiveLastInCluster)
|
||||
|
||||
const pendingColor = t.palette.primary_300
|
||||
|
||||
@@ -398,8 +404,7 @@ let MessageItem = ({
|
||||
<DateDivider date={message.sentAt} />
|
||||
</Animated.View>
|
||||
)}
|
||||
<View
|
||||
style={[messageInset, isFirstInCluster && !showDateDivider && a.mt_sm]}>
|
||||
<View style={[messageInset, effectiveFirstInCluster && a.mt_md]}>
|
||||
<View style={[a.relative]}>
|
||||
{showAvatar ? (
|
||||
<View
|
||||
@@ -474,7 +479,23 @@ let MessageItem = ({
|
||||
]}>
|
||||
<RichText
|
||||
value={rt}
|
||||
style={[a.text_md, isFromSelf && {color: t.palette.white}]}
|
||||
style={[
|
||||
a.text_md,
|
||||
isFromSelf && {color: t.palette.white},
|
||||
// Emoji-only: add top leading to avoid clipping the
|
||||
// glyph, then pull the bottom up by the same amount so
|
||||
// the glyph bottom-aligns with the avatar instead of
|
||||
// sitting above its line-box baseline.
|
||||
isOnlyEmoji(message.text) && [
|
||||
a.leading_tight,
|
||||
// Visually align bottom of the emoji with the avatar
|
||||
!isFromSelf &&
|
||||
platform({
|
||||
android: {marginTop: a.mt_2xs.marginTop},
|
||||
default: {marginBottom: -a.mb_sm.marginBottom},
|
||||
}),
|
||||
],
|
||||
]}
|
||||
interactiveStyle={a.underline}
|
||||
enableTags
|
||||
emojiMultiplier={3}
|
||||
|
||||
@@ -29,7 +29,6 @@ let MessageItemEmbed = ({
|
||||
<View
|
||||
style={[
|
||||
!isFromSelf && a.ml_sm,
|
||||
t.atoms.bg,
|
||||
native({
|
||||
flexBasis: 0,
|
||||
width: Math.min(screen.width, 600) / 1.4,
|
||||
|
||||
@@ -357,7 +357,7 @@ function ReactionTab({
|
||||
accessibilityRole="button"
|
||||
accessibilityHint={
|
||||
reaction.key === 'all'
|
||||
? l`Tap to show all reactions `
|
||||
? l`Tap to show all reactions`
|
||||
: l`Tap to show ${reaction.value} reactions`
|
||||
}
|
||||
hitSlop={HITSLOP_10}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import {View} from 'react-native'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {type ConvoItem} from '#/state/messages/convo/types'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {getSystemMessageInfo} from '#/components/dms/getSystemMessageInfo'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function SystemMessageItem({
|
||||
item,
|
||||
}: {
|
||||
item: ConvoItem & {type: 'system-message'}
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {i18n} = useLingui()
|
||||
|
||||
const info = getSystemMessageInfo(item.message.data, item.relatedProfiles)
|
||||
if (!info) return null
|
||||
|
||||
const {Icon, message} = info
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.px_md,
|
||||
a.mt_md,
|
||||
]}>
|
||||
<Icon size="xs" style={[a.mr_2xs, t.atoms.text_contrast_medium]} />
|
||||
<Text
|
||||
style={[
|
||||
a.text_xs,
|
||||
a.text_center,
|
||||
t.atoms.text_contrast_medium,
|
||||
{includeFontPadding: false, textAlignVertical: 'center'},
|
||||
]}>
|
||||
{i18n._(message)}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import {AppBskyEmbedRecord, ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {type I18n} from '@lingui/core'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
|
||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
import {
|
||||
postUriToRelativePath,
|
||||
toBskyAppUrl,
|
||||
toShortUrl,
|
||||
} from '#/lib/strings/url-helpers'
|
||||
|
||||
export type UserMessageInfo = {
|
||||
message: string | null
|
||||
sentAt: string
|
||||
reportableMessage?: ChatBskyConvoDefs.MessageView
|
||||
}
|
||||
|
||||
export function getMessageInfo({
|
||||
convo,
|
||||
currentAccountDid,
|
||||
i18n,
|
||||
}: {
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
currentAccountDid: string | undefined
|
||||
i18n: I18n
|
||||
}): UserMessageInfo | null {
|
||||
if (!ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const lastMessage = convo.lastMessage
|
||||
const isFromMe = lastMessage.sender?.did === currentAccountDid
|
||||
const senderDid = lastMessage.sender?.did
|
||||
const sender = convo.members.find(m => m.did === senderDid)
|
||||
const name = sender ? createSanitizedDisplayName(sender) : null
|
||||
const isGroup = ChatBskyConvoDefs.isGroupConvo(convo.kind)
|
||||
|
||||
const reportableMessage = isFromMe ? undefined : lastMessage
|
||||
|
||||
const prefix = (message: string) => {
|
||||
if (isFromMe) {
|
||||
return i18n._(
|
||||
msg({
|
||||
message: `You: ${message}`,
|
||||
comment: 'When the last message in a chat was made by you.',
|
||||
}),
|
||||
)
|
||||
} else if (isGroup && name) {
|
||||
return i18n._(
|
||||
msg({
|
||||
message: `${name}: ${message}`,
|
||||
comment:
|
||||
'When the last message in a group chat came from someone other than you.',
|
||||
}),
|
||||
)
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
let message: string | null = null
|
||||
|
||||
if (lastMessage.text) {
|
||||
message = prefix(lastMessage.text)
|
||||
} else if (lastMessage.embed) {
|
||||
const defaultEmbeddedContentMessage = i18n._(
|
||||
msg`(contains embedded content)`,
|
||||
)
|
||||
|
||||
if (AppBskyEmbedRecord.isView(lastMessage.embed)) {
|
||||
const embed = lastMessage.embed
|
||||
|
||||
if (AppBskyEmbedRecord.isViewRecord(embed.record)) {
|
||||
const record = embed.record
|
||||
const path = postUriToRelativePath(record.uri, {
|
||||
handle: record.author.handle,
|
||||
})
|
||||
const href = path ? toBskyAppUrl(path) : undefined
|
||||
const short = href ? toShortUrl(href) : defaultEmbeddedContentMessage
|
||||
message = prefix(short)
|
||||
} else {
|
||||
message = prefix(defaultEmbeddedContentMessage)
|
||||
}
|
||||
} else {
|
||||
message = prefix(defaultEmbeddedContentMessage)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message,
|
||||
sentAt: lastMessage.sentAt,
|
||||
reportableMessage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import {ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {type I18n} from '@lingui/core'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
|
||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
|
||||
export type UserReactionInfo = {
|
||||
message: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export function getReactionInfo({
|
||||
convo,
|
||||
currentAccountDid,
|
||||
i18n,
|
||||
}: {
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
currentAccountDid: string | undefined
|
||||
i18n: I18n
|
||||
}): UserReactionInfo | null {
|
||||
if (!ChatBskyConvoDefs.isMessageAndReactionView(convo.lastReaction)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const {reaction, message: reactedTo} = convo.lastReaction
|
||||
const isFromMe = reaction.sender.did === currentAccountDid
|
||||
const senderDid = reaction.sender.did
|
||||
const sender = convo.members.find(m => m.did === senderDid)
|
||||
const name = sender ? createSanitizedDisplayName(sender) : null
|
||||
|
||||
const lastMessageText = reactedTo.text
|
||||
const fallbackMessage = i18n._(
|
||||
msg({
|
||||
message: 'a message',
|
||||
comment:
|
||||
'If last message does not contain text, fall back to "{user} reacted to {a message}"',
|
||||
}),
|
||||
)
|
||||
const target = lastMessageText ? `"${lastMessageText}"` : fallbackMessage
|
||||
|
||||
let message: string
|
||||
if (isFromMe) {
|
||||
message = i18n._(msg`You reacted ${reaction.value} to ${target}`)
|
||||
} else if (name) {
|
||||
message = i18n._(msg`${name} reacted ${reaction.value} to ${target}`)
|
||||
} else {
|
||||
message = i18n._(msg`Someone reacted ${reaction.value} to ${target}`)
|
||||
}
|
||||
|
||||
return {
|
||||
message,
|
||||
createdAt: reaction.createdAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import {type ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {type MessageDescriptor} from '@lingui/core'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
|
||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft'
|
||||
import {ArrowBoxRight_Stroke2_Corner3_Rounded as JoinIcon} from '#/components/icons/ArrowBoxRight'
|
||||
import {
|
||||
ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon,
|
||||
ChainLinkBroken_Stroke2_Corner0_Rounded as ChainLinkBrokenIcon,
|
||||
} from '#/components/icons/ChainLink'
|
||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {
|
||||
Lock_Stroke2_Corner0_Rounded as LockIcon,
|
||||
Unlock_Stroke2_Corner2_Rounded as UnlockIcon,
|
||||
} from '#/components/icons/Lock'
|
||||
import {PencilLine_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil'
|
||||
|
||||
export type SystemMessageInfo = {
|
||||
message: MessageDescriptor
|
||||
Icon: React.ComponentType<SVGIconProps>
|
||||
}
|
||||
|
||||
function getReferredDisplayName(
|
||||
user: ChatBskyConvoDefs.SystemMessageReferredUser,
|
||||
relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[],
|
||||
): string | null {
|
||||
const profile = relatedProfiles.find(p => p.did === user.did)
|
||||
return profile ? createSanitizedDisplayName(profile) : null
|
||||
}
|
||||
|
||||
export function getSystemMessageInfo(
|
||||
data: ChatBskyConvoDefs.SystemMessageView['data'],
|
||||
relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[],
|
||||
): SystemMessageInfo | null {
|
||||
if (ChatBskyConvoDefs.isSystemMessageDataAddMember(data)) {
|
||||
const name = getReferredDisplayName(data.member, relatedProfiles)
|
||||
return {
|
||||
Icon: JoinIcon,
|
||||
message: name
|
||||
? msg`${name} was added to the group`
|
||||
: msg`Someone was added to the group`,
|
||||
}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataRemoveMember(data)) {
|
||||
const name = getReferredDisplayName(data.member, relatedProfiles)
|
||||
return {
|
||||
Icon: LeaveIcon,
|
||||
message: name
|
||||
? msg`${name} was removed from the group`
|
||||
: msg`Someone was removed from the group`,
|
||||
}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataMemberJoin(data)) {
|
||||
const name = getReferredDisplayName(data.member, relatedProfiles)
|
||||
return {
|
||||
Icon: JoinIcon,
|
||||
message: name
|
||||
? msg`${name} joined the group`
|
||||
: msg`Someone joined the group`,
|
||||
}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataMemberLeave(data)) {
|
||||
const name = getReferredDisplayName(data.member, relatedProfiles)
|
||||
return {
|
||||
Icon: LeaveIcon,
|
||||
message: name ? msg`${name} left the group` : msg`Someone left the group`,
|
||||
}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataLockConvo(data)) {
|
||||
return {Icon: LockIcon, message: msg`Chat locked`}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataUnlockConvo(data)) {
|
||||
return {Icon: UnlockIcon, message: msg`Chat unlocked`}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataLockConvoPermanently(data)) {
|
||||
return {Icon: LockIcon, message: msg`Chat locked permanently`}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataEditGroup(data)) {
|
||||
return {
|
||||
Icon: PencilIcon,
|
||||
message: data.newName
|
||||
? msg`Chat title changed to ${data.newName}`
|
||||
: msg`Chat title changed`,
|
||||
}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataCreateJoinLink(data)) {
|
||||
return {Icon: ChainLinkIcon, message: msg`Invite link created`}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataEditJoinLink(data)) {
|
||||
return {Icon: ChainLinkIcon, message: msg`Invite link edited`}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataEnableJoinLink(data)) {
|
||||
return {Icon: ChainLinkIcon, message: msg`Invite link enabled`}
|
||||
} else if (ChatBskyConvoDefs.isSystemMessageDataDisableJoinLink(data)) {
|
||||
return {Icon: ChainLinkBrokenIcon, message: msg`Invite link disabled`}
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -110,14 +110,16 @@ export function parseConvoView(
|
||||
owner = member as GroupConvoMember
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
logger.warn(
|
||||
'Expected a GroupConvoMember, got an unknown kind of member',
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
if (!owner) {
|
||||
throw new Error('No owner found in group convo')
|
||||
logger.warn('No owner found in group convo')
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -136,7 +138,8 @@ export function parseConvoView(
|
||||
const otherUser = convoView.members.find(m => m.did !== ownDid)
|
||||
|
||||
if (!otherUser) {
|
||||
throw new Error('No other user found in direct convo')
|
||||
logger.warn('No other user found in direct convo')
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const ArrowBoxRight_Stroke2_Corner3_Rounded = createSinglePathSVG({
|
||||
path: 'M17 3a4 4 0 0 1 4 4v10a4 4 0 0 1-4 4h-2a1 1 0 1 1 0-2h2a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2a1 1 0 1 1 0-2h2Zm-6.707 4.793a1 1 0 0 1 1.414 0l3.5 3.5a1 1 0 0 1 0 1.414l-3.5 3.5a1 1 0 1 1-1.414-1.414L12.086 13H4a1 1 0 1 1 0-2h8.086l-1.793-1.793a1 1 0 0 1 0-1.414Z',
|
||||
})
|
||||
@@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
|
||||
export const ChainLink_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M18.535 5.465a5.003 5.003 0 0 0-7.076 0l-.005.005-.752.742a1 1 0 1 1-1.404-1.424l.749-.74a7.003 7.003 0 0 1 9.904 9.905l-.002.003-.737.746a1 1 0 1 1-1.424-1.404l.747-.757a5.003 5.003 0 0 0 0-7.076ZM6.202 9.288a1 1 0 0 1 .01 1.414l-.747.757a5.003 5.003 0 1 0 7.076 7.076l.005-.005.752-.742a1 1 0 1 1 1.404 1.424l-.746.737-.003.002a7.003 7.003 0 0 1-9.904-9.904l.74-.75a1 1 0 0 1 1.413-.009Zm8.505.005a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414-1.414l4-4a1 1 0 0 1 1.414 0Z',
|
||||
})
|
||||
|
||||
export const ChainLinkBroken_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M14.3 23v-1.1a1 1 0 0 1 2 0V23a1 1 0 1 1-2 0Zm5.243-3.457a1 1 0 0 1 1.414 0l1.1 1.1a1 1 0 1 1-1.414 1.414l-1.1-1.1a1 1 0 0 1 0-1.414ZM4.788 9.298a1 1 0 0 1 1.424 1.404l-.742.752-.004.005a5.003 5.003 0 1 0 7.075 7.075l.005-.004.752-.742a1 1 0 0 1 1.404 1.424l-.747.736a7.003 7.003 0 1 1-9.904-9.904l.737-.746ZM23 14.3a1 1 0 0 1 0 2h-1.1a1 1 0 1 1 0-2H23ZM10.044 4.05a7.005 7.005 0 0 1 9.905 9.906h0l-.737.746a1 1 0 0 1-1.424-1.404l.742-.752.004-.005a5.003 5.003 0 1 0-7.075-7.075l-.005.004-.752.742a1 1 0 0 1-1.404-1.424l.746-.737ZM2.1 7.7a1 1 0 1 1 0 2H1a1 1 0 0 1 0-2h1.1Zm-.157-5.757a1 1 0 0 1 1.414 0l1.1 1.1a1 1 0 1 1-1.414 1.414l-1.1-1.1a1 1 0 0 1 0-1.414ZM7.7 2.1V1a1 1 0 1 1 2 0v1.1a1 1 0 0 1-2 0Z',
|
||||
})
|
||||
|
||||
@@ -7,3 +7,7 @@ export const Lock_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
export const Lock_Stroke2_Corner2_Rounded = createSinglePathSVG({
|
||||
path: 'M7 7a5 5 0 0 1 10 0v2a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-7a3 3 0 0 1 3-3V7Zm0 4a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1H7Zm8-2H9V7a3 3 0 1 1 6 0v2Zm-3 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z',
|
||||
})
|
||||
|
||||
export const Unlock_Stroke2_Corner2_Rounded = createSinglePathSVG({
|
||||
path: 'M12 13a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z"/><path fill="#000" fill-rule="evenodd" d="M12 2a5 5 0 0 1 4.843 3.751 1 1 0 0 1-1.938.498A3.002 3.002 0 0 0 9 7v2h8a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-7a3 3 0 0 1 3-3V7a5 5 0 0 1 5-5Zm-5 9a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1H7Z',
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ import {Trans} from '@lingui/react/macro'
|
||||
|
||||
import {type Dimensions} from '#/lib/media/types'
|
||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -210,12 +210,17 @@ export function AutoSizedImage({
|
||||
color: utils.alpha(t.atoms.bg.backgroundColor, 0.2),
|
||||
foreground: true,
|
||||
}}
|
||||
style={[
|
||||
style={({pressed}) => [
|
||||
a.w_full,
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
t.atoms.bg_contrast_25,
|
||||
{aspectRatio: max ?? 1},
|
||||
web([
|
||||
a.transition_transform,
|
||||
{transitionDuration: '200ms'},
|
||||
pressed && {transform: [{scale: 0.99}]},
|
||||
]),
|
||||
]}>
|
||||
{contents}
|
||||
</Pressable>
|
||||
@@ -237,7 +242,16 @@ export function AutoSizedImage({
|
||||
color: utils.alpha(t.atoms.bg.backgroundColor, 0.2),
|
||||
foreground: true,
|
||||
}}
|
||||
style={[a.h_full]}>
|
||||
style={({pressed}) => [
|
||||
a.h_full,
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
web([
|
||||
a.transition_transform,
|
||||
{transitionDuration: '200ms'},
|
||||
pressed && {transform: [{scale: 0.99}]},
|
||||
]),
|
||||
]}>
|
||||
{contents}
|
||||
</Pressable>
|
||||
</ConstrainedImage>
|
||||
|
||||
@@ -257,7 +257,6 @@ export function Gallery({
|
||||
horizontal
|
||||
pagingEnabled={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
decelerationRate={0.993}
|
||||
directionalLockEnabled
|
||||
nestedScrollEnabled
|
||||
alwaysBounceVertical={false}
|
||||
@@ -431,15 +430,20 @@ function GalleryImage({
|
||||
color: utils.alpha(t.atoms.bg.backgroundColor, 0.2),
|
||||
foreground: true,
|
||||
}}
|
||||
style={[
|
||||
style={({pressed}) => [
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
t.atoms.bg_contrast_25,
|
||||
web({
|
||||
cursor: 'inherit',
|
||||
outline: 0,
|
||||
border: 0,
|
||||
}),
|
||||
web([
|
||||
{
|
||||
cursor: 'inherit',
|
||||
outline: 0,
|
||||
border: 0,
|
||||
},
|
||||
a.transition_transform,
|
||||
{transitionDuration: '200ms'},
|
||||
pressed && {transform: [{scale: 0.99}]},
|
||||
]),
|
||||
]}>
|
||||
<Image
|
||||
source={{uri: image.thumb}}
|
||||
|
||||
@@ -21,8 +21,6 @@ export type PaletteColor = {
|
||||
textInverted: string
|
||||
link: string
|
||||
border: string
|
||||
borderDark: string
|
||||
icon: string
|
||||
[k: string]: string
|
||||
}
|
||||
export type Palette = Record<PaletteColorName, PaletteColor>
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import {useCallback, useInsertionEffect, useRef} from 'react'
|
||||
|
||||
// This should be used sparingly. It erases reactivity, i.e. when the inputs
|
||||
// change, the function itself will remain the same. This means that if you
|
||||
// use this at a higher level of your tree, and then some state you read in it
|
||||
// changes, there is no mechanism for anything below in the tree to "react"
|
||||
// to this change (e.g. by knowing to call your function again).
|
||||
//
|
||||
// Also, you should avoid calling the returned function during rendering
|
||||
// since the values captured by it are going to lag behind.
|
||||
export function useNonReactiveCallback<T extends Function>(fn: T): T {
|
||||
const ref = useRef(fn)
|
||||
const noop = () => {}
|
||||
|
||||
/**
|
||||
* This should be used sparingly. It erases reactivity, i.e. when the inputs
|
||||
* change, the function itself will remain the same. This means that if you use
|
||||
* this at a higher level of your tree, and then some state you read in it
|
||||
* changes, there is no mechanism for anything below in the tree to "react" to
|
||||
* this change (e.g. by knowing to call your function again).
|
||||
*
|
||||
* Also, you should avoid calling the returned function during rendering since
|
||||
* the values captured by it are going to lag behind.
|
||||
*
|
||||
* For objects, see `useNonReactiveObject` instead.
|
||||
*/
|
||||
export function useNonReactiveCallback<T extends Function = () => void>(
|
||||
fn?: T,
|
||||
): T {
|
||||
const ref = useRef<T>((fn ?? noop) as T)
|
||||
useInsertionEffect(() => {
|
||||
ref.current = fn
|
||||
ref.current = (fn ?? noop) as T
|
||||
}, [fn])
|
||||
return useCallback(
|
||||
(...args: any) => {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import {useInsertionEffect, useRef} from 'react'
|
||||
|
||||
/**
|
||||
* This should be used sparingly. It erases reactivity, i.e. when the inputs
|
||||
* change, the returned object itself will remain the same. This means that if
|
||||
* you use this at a higher level of your tree, and then some state you read in
|
||||
* it changes, there is no mechanism for anything below in the tree to "react"
|
||||
* to this change (e.g. by knowing to call your function again).
|
||||
*
|
||||
* For callbacks, see `useNonReactiveCallback` instead.
|
||||
*/
|
||||
export function useNonReactiveObject<T extends Record<string, unknown>>(
|
||||
o: T,
|
||||
): React.RefObject<T> {
|
||||
const ref = useRef(o)
|
||||
useInsertionEffect(() => {
|
||||
ref.current = o
|
||||
}, [o])
|
||||
return ref
|
||||
}
|
||||
@@ -13,12 +13,10 @@ export interface UsePaletteValue {
|
||||
viewLight: ViewStyle
|
||||
btn: ViewStyle
|
||||
border: ViewStyle
|
||||
borderDark: ViewStyle
|
||||
text: TextStyle
|
||||
textLight: TextStyle
|
||||
textInverted: TextStyle
|
||||
link: TextStyle
|
||||
icon: TextStyle
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,9 +40,6 @@ export function usePalette(color: PaletteColorName): UsePaletteValue {
|
||||
border: {
|
||||
borderColor: palette.border,
|
||||
},
|
||||
borderDark: {
|
||||
borderColor: palette.borderDark,
|
||||
},
|
||||
text: {
|
||||
color: palette.text,
|
||||
},
|
||||
@@ -57,9 +52,6 @@ export function usePalette(color: PaletteColorName): UsePaletteValue {
|
||||
link: {
|
||||
color: palette.link,
|
||||
},
|
||||
icon: {
|
||||
color: palette.icon,
|
||||
},
|
||||
}
|
||||
}, [theme, color])
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import {useMemo} from 'react'
|
||||
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
return children
|
||||
}
|
||||
|
||||
const noop = () => {}
|
||||
|
||||
export function useHotkeysContext() {
|
||||
return {
|
||||
enableScope: () => {},
|
||||
disableScope: () => {},
|
||||
}
|
||||
return useMemo(
|
||||
() => ({
|
||||
enableScope: noop,
|
||||
disableScope: noop,
|
||||
}),
|
||||
[],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,8 +54,6 @@ export const colors = {
|
||||
green3: '#20bc07',
|
||||
green4: '#148203',
|
||||
green5: '#082b03',
|
||||
|
||||
unreadNotifBg: '#ebf6ff',
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,19 +17,6 @@ export const defaultTheme: Theme = {
|
||||
textInverted: lightPalette.white,
|
||||
link: lightPalette.primary_500,
|
||||
border: lightPalette.contrast_100,
|
||||
borderDark: lightPalette.contrast_200,
|
||||
icon: lightPalette.contrast_500,
|
||||
|
||||
// non-standard
|
||||
textVeryLight: lightPalette.contrast_400,
|
||||
replyLine: lightPalette.contrast_100,
|
||||
replyLineDot: lightPalette.contrast_200,
|
||||
unreadNotifBg: lightPalette.primary_25,
|
||||
unreadNotifBorder: lightPalette.primary_100,
|
||||
postCtrl: lightPalette.contrast_500,
|
||||
brandText: lightPalette.primary_500,
|
||||
emptyStateIcon: lightPalette.contrast_300,
|
||||
borderLinkHover: lightPalette.contrast_300,
|
||||
},
|
||||
primary: {
|
||||
background: colors.blue3,
|
||||
@@ -39,8 +26,6 @@ export const defaultTheme: Theme = {
|
||||
textInverted: colors.blue3,
|
||||
link: colors.blue0,
|
||||
border: colors.blue4,
|
||||
borderDark: colors.blue5,
|
||||
icon: colors.blue4,
|
||||
},
|
||||
secondary: {
|
||||
background: colors.green3,
|
||||
@@ -50,8 +35,6 @@ export const defaultTheme: Theme = {
|
||||
textInverted: colors.green4,
|
||||
link: colors.green1,
|
||||
border: colors.green4,
|
||||
borderDark: colors.green5,
|
||||
icon: colors.green4,
|
||||
},
|
||||
inverted: {
|
||||
background: darkPalette.black,
|
||||
@@ -61,8 +44,6 @@ export const defaultTheme: Theme = {
|
||||
textInverted: darkPalette.black,
|
||||
link: darkPalette.primary_500,
|
||||
border: darkPalette.contrast_100,
|
||||
borderDark: darkPalette.contrast_200,
|
||||
icon: darkPalette.contrast_500,
|
||||
},
|
||||
error: {
|
||||
background: colors.red3,
|
||||
@@ -72,8 +53,6 @@ export const defaultTheme: Theme = {
|
||||
textInverted: colors.red3,
|
||||
link: colors.red1,
|
||||
border: colors.red4,
|
||||
borderDark: colors.red5,
|
||||
icon: colors.red4,
|
||||
},
|
||||
},
|
||||
shapes: {
|
||||
@@ -303,19 +282,6 @@ export const darkTheme: Theme = {
|
||||
textInverted: darkPalette.black,
|
||||
link: darkPalette.primary_500,
|
||||
border: darkPalette.contrast_100,
|
||||
borderDark: darkPalette.contrast_200,
|
||||
icon: darkPalette.contrast_500,
|
||||
|
||||
// non-standard
|
||||
textVeryLight: darkPalette.contrast_400,
|
||||
replyLine: darkPalette.contrast_200,
|
||||
replyLineDot: darkPalette.contrast_200,
|
||||
unreadNotifBg: darkPalette.primary_25,
|
||||
unreadNotifBorder: darkPalette.primary_100,
|
||||
postCtrl: darkPalette.contrast_500,
|
||||
brandText: darkPalette.primary_500,
|
||||
emptyStateIcon: darkPalette.contrast_300,
|
||||
borderLinkHover: darkPalette.contrast_300,
|
||||
},
|
||||
primary: {
|
||||
...defaultTheme.palette.primary,
|
||||
@@ -333,8 +299,6 @@ export const darkTheme: Theme = {
|
||||
textInverted: darkPalette.white,
|
||||
link: lightPalette.primary_500,
|
||||
border: lightPalette.contrast_100,
|
||||
borderDark: lightPalette.contrast_200,
|
||||
icon: lightPalette.contrast_500,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -352,19 +316,6 @@ export const dimTheme: Theme = {
|
||||
textInverted: dimPalette.black,
|
||||
link: dimPalette.primary_500,
|
||||
border: dimPalette.contrast_100,
|
||||
borderDark: dimPalette.contrast_200,
|
||||
icon: dimPalette.contrast_500,
|
||||
|
||||
// non-standard
|
||||
textVeryLight: dimPalette.contrast_400,
|
||||
replyLine: dimPalette.contrast_200,
|
||||
replyLineDot: dimPalette.contrast_200,
|
||||
unreadNotifBg: dimPalette.primary_25,
|
||||
unreadNotifBorder: dimPalette.primary_100,
|
||||
postCtrl: dimPalette.contrast_500,
|
||||
brandText: dimPalette.primary_500,
|
||||
emptyStateIcon: dimPalette.contrast_300,
|
||||
borderLinkHover: dimPalette.contrast_300,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+1452
-969
File diff suppressed because it is too large
Load Diff
+1428
-945
File diff suppressed because it is too large
Load Diff
+1451
-968
File diff suppressed because it is too large
Load Diff
+1428
-945
File diff suppressed because it is too large
Load Diff
+1428
-945
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1454
-971
File diff suppressed because it is too large
Load Diff
+1454
-971
File diff suppressed because it is too large
Load Diff
+1454
-971
File diff suppressed because it is too large
Load Diff
+1454
-971
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
+1455
-972
File diff suppressed because it is too large
Load Diff
+645
-556
File diff suppressed because it is too large
Load Diff
+1458
-975
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
+1451
-968
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
+1458
-975
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
+1454
-971
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
+1451
-968
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
+1450
-967
File diff suppressed because it is too large
Load Diff
+1454
-971
File diff suppressed because it is too large
Load Diff
+1428
-945
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
+1459
-976
File diff suppressed because it is too large
Load Diff
+1428
-945
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
+1454
-971
File diff suppressed because it is too large
Load Diff
+1453
-970
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
+1453
-970
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
+1458
-975
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
+1452
-969
File diff suppressed because it is too large
Load Diff
+1456
-973
File diff suppressed because it is too large
Load Diff
+1464
-981
File diff suppressed because it is too large
Load Diff
+1458
-975
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,6 @@ import {
|
||||
} from '#/lib/routes/types'
|
||||
import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation'
|
||||
import {useBookmarksQuery} from '#/state/queries/bookmarks/useBookmarksQuery'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {Post} from '#/view/com/post/Post'
|
||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||
import {List} from '#/view/com/util/List'
|
||||
@@ -43,14 +42,12 @@ import {IS_IOS} from '#/env'
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Bookmarks'>
|
||||
|
||||
export function BookmarksScreen({}: Props) {
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const ax = useAnalytics()
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
ax.metric('bookmarks:view', {})
|
||||
}, [setMinimalShellMode, ax]),
|
||||
}, [ax]),
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useCallback, useLayoutEffect, useState} from 'react'
|
||||
import {useState} from 'react'
|
||||
import {LayoutAnimationConfig} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
type AllNavigatorParams,
|
||||
type NativeStackScreenProps,
|
||||
} from '#/lib/routes/types'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useEnableMinimalShellMode} from '#/state/shell'
|
||||
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||
import {FindContactsFlow} from '#/components/contacts/FindContactsFlow'
|
||||
import {useFindContactsFlowState} from '#/components/contacts/state'
|
||||
@@ -36,12 +36,7 @@ export function FindContactsFlowScreen({navigation}: Props) {
|
||||
})
|
||||
})
|
||||
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const effect = useCallback(() => {
|
||||
setMinimalShellMode(true)
|
||||
return () => setMinimalShellMode(false)
|
||||
}, [setMinimalShellMode])
|
||||
useLayoutEffect(effect)
|
||||
useEnableMinimalShellMode()
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
|
||||
+3
-16
@@ -4,7 +4,6 @@ import {type AppBskyFeedDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
@@ -17,7 +16,6 @@ import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {enforceLen} from '#/lib/strings/helpers'
|
||||
import {useSearchPostsQuery} from '#/state/queries/search-posts'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||
import {useCloseAllActiveElements} from '#/state/util'
|
||||
import {Pager} from '#/view/com/pager/Pager'
|
||||
@@ -79,21 +77,10 @@ export default function HashtagScreen({
|
||||
}, [tag, author])
|
||||
|
||||
const [activeTab, setActiveTab] = useState(0)
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
}, [setMinimalShellMode]),
|
||||
)
|
||||
|
||||
const onPageSelected = useCallback(
|
||||
(index: number) => {
|
||||
setMinimalShellMode(false)
|
||||
setActiveTab(index)
|
||||
},
|
||||
[setMinimalShellMode],
|
||||
)
|
||||
const onPageSelected = (index: number) => {
|
||||
setActiveTab(index)
|
||||
}
|
||||
|
||||
const sections = useMemo(() => {
|
||||
return [
|
||||
|
||||
+2
-10
@@ -1,9 +1,8 @@
|
||||
import {useCallback, useState} from 'react'
|
||||
import {useState} from 'react'
|
||||
import {LayoutAnimation, Pressable, View} from 'react-native'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
||||
import {
|
||||
@@ -11,7 +10,7 @@ import {
|
||||
type NativeStackScreenProps,
|
||||
} from '#/lib/routes/types'
|
||||
import {getEntries} from '#/logger/logDump'
|
||||
import {useSetMinimalShellMode, useTickEveryMinute} from '#/state/shell'
|
||||
import {useTickEveryMinute} from '#/state/shell'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {
|
||||
ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottomIcon,
|
||||
@@ -28,17 +27,10 @@ export function LogScreen({}: NativeStackScreenProps<
|
||||
>) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const [expanded, setExpanded] = useState<string[]>([])
|
||||
const timeAgo = useGetTimeAgo()
|
||||
const tick = useTickEveryMinute()
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
}, [setMinimalShellMode]),
|
||||
)
|
||||
|
||||
const toggler = (id: string) => () => {
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
||||
if (expanded.includes(id)) {
|
||||
|
||||
@@ -38,6 +38,7 @@ import * as Layout from '#/components/Layout'
|
||||
import {Link} from '#/components/Link'
|
||||
import {ListFooter} from '#/components/Lists'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAgeAssurance} from '#/ageAssurance'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
import {ChatListItem} from './components/ChatListItem'
|
||||
import {InboxPreview} from './components/InboxPreview'
|
||||
@@ -71,21 +72,24 @@ type Props = NativeStackScreenProps<MessagesTabNavigatorParams, 'Messages'>
|
||||
export function MessagesScreen(props: Props) {
|
||||
const {_} = useLingui()
|
||||
const aaCopy = useAgeAssuranceCopy()
|
||||
const aa = useAgeAssurance()
|
||||
|
||||
return (
|
||||
<AgeRestrictedScreen
|
||||
screenTitle={_(msg`Chats`)}
|
||||
infoText={aaCopy.chatsInfoText}
|
||||
rightHeaderSlot={
|
||||
<Link
|
||||
to="/messages/settings"
|
||||
label={_(msg`Chat settings`)}
|
||||
size="small"
|
||||
color="secondary">
|
||||
<ButtonText>
|
||||
<Trans>Chat settings</Trans>
|
||||
</ButtonText>
|
||||
</Link>
|
||||
aa.flags.chatDisabled ? null : (
|
||||
<Link
|
||||
to="/messages/settings"
|
||||
label={_(msg`Chat settings`)}
|
||||
size="small"
|
||||
color="secondary">
|
||||
<ButtonText>
|
||||
<Trans>Chat settings</Trans>
|
||||
</ButtonText>
|
||||
</Link>
|
||||
)
|
||||
}>
|
||||
<MessagesScreenInner {...props} />
|
||||
</AgeRestrictedScreen>
|
||||
|
||||
@@ -6,9 +6,7 @@ import {
|
||||
ScrollEdgeEffect,
|
||||
ScrollEdgeEffectProvider,
|
||||
} from '@bsky.app/expo-scroll-edge-effect'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {
|
||||
type RouteProp,
|
||||
useFocusEffect,
|
||||
@@ -32,11 +30,11 @@ import {useCurrentConvoId} from '#/state/messages/current-convo-id'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useConvoQuery} from '#/state/queries/messages/conversation'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {MessagesList} from '#/screens/Messages/components/MessagesList'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
|
||||
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {
|
||||
EmailDialogScreenID,
|
||||
useEmailDialogControl,
|
||||
@@ -47,6 +45,9 @@ import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util'
|
||||
import {Error} from '#/components/Error'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_LIQUID_GLASS, IS_WEB} from '#/env'
|
||||
import {ChatDisabled} from './components/ChatDisabled'
|
||||
|
||||
@@ -56,11 +57,11 @@ type Props = NativeStackScreenProps<
|
||||
>
|
||||
|
||||
export function MessagesConversationScreen(props: Props) {
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const aaCopy = useAgeAssuranceCopy()
|
||||
return (
|
||||
<AgeRestrictedScreen
|
||||
screenTitle={_(msg`Conversation`)}
|
||||
screenTitle={l`Conversation`}
|
||||
infoText={aaCopy.chatsInfoText}>
|
||||
<MessagesConversationScreenInner {...props} />
|
||||
</AgeRestrictedScreen>
|
||||
@@ -68,25 +69,22 @@ export function MessagesConversationScreen(props: Props) {
|
||||
}
|
||||
|
||||
export function MessagesConversationScreenInner({route}: Props) {
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
|
||||
const convoId = route.params.conversation
|
||||
const {setCurrentConvoId} = useCurrentConvoId()
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setCurrentConvoId(convoId)
|
||||
setMinimalShellMode(true)
|
||||
|
||||
return () => {
|
||||
setCurrentConvoId(undefined)
|
||||
setMinimalShellMode(false)
|
||||
}
|
||||
}, [convoId, setCurrentConvoId, setMinimalShellMode]),
|
||||
}, [convoId, setCurrentConvoId]),
|
||||
)
|
||||
|
||||
return (
|
||||
<Layout.Screen
|
||||
minimalShell
|
||||
testID="convoScreen"
|
||||
noInsetTop={IS_LIQUID_GLASS}
|
||||
style={web([{minHeight: 0}, a.flex_1])}>
|
||||
@@ -102,7 +100,7 @@ export function MessagesConversationScreenInner({route}: Props) {
|
||||
function Inner({convoId}: {convoId: string}) {
|
||||
const t = useTheme()
|
||||
const convoState = useConvo()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const isFocused = useIsFocused()
|
||||
const {top: topInset} = useSafeAreaInsets()
|
||||
@@ -140,8 +138,8 @@ function Inner({convoId}: {convoId: string}) {
|
||||
<MessagesListHeader convo={convo} />
|
||||
</Layout.Center>
|
||||
<Error
|
||||
title={_(msg`Something went wrong`)}
|
||||
message={_(msg`We couldn't load this conversation`)}
|
||||
title={l`Something went wrong`}
|
||||
message={l`We couldn't load this conversation`}
|
||||
onRetry={() => convoState.error.retry()}
|
||||
sideBorders={false}
|
||||
/>
|
||||
@@ -293,6 +291,74 @@ function InnerReady({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{convo?.kind === 'group' && <GroupChatGate />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function GroupChatGate() {
|
||||
const {t: l} = useLingui()
|
||||
const ax = useAnalytics()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const groupChatGateDialogControl = Dialog.useDialogControl()
|
||||
|
||||
const isGatedGroupChat = !ax.features.enabled(ax.features.GroupChatsEnable)
|
||||
|
||||
useEffect(() => {
|
||||
if (isGatedGroupChat) {
|
||||
setTimeout(() => groupChatGateDialogControl.open())
|
||||
}
|
||||
}, [isGatedGroupChat, groupChatGateDialogControl])
|
||||
|
||||
const hasBeenReleased = ax.features.enabled(
|
||||
ax.features.GroupChatsHasBeenReleased,
|
||||
)
|
||||
|
||||
const onGoBack = () => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.replace('Messages', {animation: 'pop'})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Prompt.Outer
|
||||
control={groupChatGateDialogControl}
|
||||
nativeOptions={{preventDismiss: true, preventExpansion: true}}
|
||||
testID="groupChatGateDialog">
|
||||
<Prompt.Content>
|
||||
<View style={[a.w_full, a.align_center, a.py_2xl]}>
|
||||
<Text style={{fontSize: 48}} emoji>
|
||||
🐴
|
||||
</Text>
|
||||
</View>
|
||||
<Prompt.TitleText>
|
||||
{hasBeenReleased ? (
|
||||
<Trans>Group chats are now available</Trans>
|
||||
) : (
|
||||
<Trans>Group chats are not yet available</Trans>
|
||||
)}
|
||||
</Prompt.TitleText>
|
||||
<Prompt.DescriptionText>
|
||||
{hasBeenReleased ? (
|
||||
<Trans>Update your app to the latest version to join in!</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
This feature isn't available to you yet. Please check back later.
|
||||
</Trans>
|
||||
)}
|
||||
</Prompt.DescriptionText>
|
||||
</Prompt.Content>
|
||||
<Prompt.Actions>
|
||||
<Prompt.Action
|
||||
cta={l`Go Back`}
|
||||
onPress={onGoBack}
|
||||
color="primary_subtle"
|
||||
/>
|
||||
</Prompt.Actions>
|
||||
</Prompt.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {useEditGroupName} from '#/state/queries/messages/edit-group-name'
|
||||
import {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability'
|
||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
|
||||
import {useListJoinRequestsQuery} from '#/state/queries/messages/list-join-requests'
|
||||
import {useMuteConvo} from '#/state/queries/messages/mute-conversation'
|
||||
import {useProfileBlockMutationQueue} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
@@ -109,7 +110,7 @@ export function MessagesConversationSettingsScreen({route}: Props) {
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
<ConvoProvider key={convoId} convoId={convoId}>
|
||||
<SettingsInner />
|
||||
<SettingsInner convoId={convoId} />
|
||||
</ConvoProvider>
|
||||
</Layout.Screen>
|
||||
)
|
||||
@@ -119,7 +120,7 @@ function keyExtractor(item: Item) {
|
||||
return item.type === 'CHAT_MEMBER' ? item.profile.did : item.type
|
||||
}
|
||||
|
||||
function SettingsInner() {
|
||||
function SettingsInner({convoId}: {convoId: string}) {
|
||||
const {t: l} = useLingui()
|
||||
|
||||
const initialNumToRender = useInitialNumToRender({minItemHeight: 68})
|
||||
@@ -137,6 +138,17 @@ function SettingsInner() {
|
||||
const data: bsky.profile.AnyProfileView[] = convo?.members ?? []
|
||||
const invites: string[] = []
|
||||
|
||||
const {data: joinRequestsData, hasNextPage: hasMoreRequests} =
|
||||
useListJoinRequestsQuery({
|
||||
convoId,
|
||||
enabled: isOwner,
|
||||
})
|
||||
const requestCount =
|
||||
joinRequestsData?.pages.reduce(
|
||||
(sum, page) => sum + page.requests.length,
|
||||
0,
|
||||
) ?? 0
|
||||
|
||||
const items = [
|
||||
{
|
||||
type: 'MEMBERS_AND_REQUESTS',
|
||||
@@ -172,7 +184,8 @@ function SettingsInner() {
|
||||
return (
|
||||
<MembersAndRequests
|
||||
memberCount={data.length}
|
||||
requestCount={5}
|
||||
requestCount={requestCount}
|
||||
hasMoreRequests={!!hasMoreRequests}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
)
|
||||
@@ -229,10 +242,12 @@ function SettingsInner() {
|
||||
function MembersAndRequests({
|
||||
memberCount,
|
||||
requestCount,
|
||||
hasMoreRequests,
|
||||
isOwner,
|
||||
}: {
|
||||
memberCount: number
|
||||
requestCount: number
|
||||
hasMoreRequests: boolean
|
||||
isOwner: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
@@ -245,21 +260,32 @@ function MembersAndRequests({
|
||||
<Trans>Members</Trans>{' '}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_xs,
|
||||
a.font_medium,
|
||||
{color: t.palette.contrast_500},
|
||||
]}>{l`${memberCount}/${MEMBER_LIMIT}`}</Text>
|
||||
style={[a.text_xs, a.font_medium, {color: t.palette.contrast_500}]}>
|
||||
{l({
|
||||
message: `${memberCount}/${MEMBER_LIMIT}`,
|
||||
comment:
|
||||
'The number of group chat members out of the total number of permitted users.',
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
{isOwner && requestCount > 0 ? (
|
||||
<InlineLinkText
|
||||
label={l`View incoming group chat requests`}
|
||||
style={[a.text_sm, a.text_right, a.font_semi_bold]}
|
||||
to="#">
|
||||
{l`${plural(requestCount, {
|
||||
one: '# request',
|
||||
other: '# requests',
|
||||
})}`}
|
||||
{hasMoreRequests
|
||||
? l({
|
||||
message: `${requestCount}+ requests`,
|
||||
comment:
|
||||
'Displayed when there are more than 50 requests to join a group chat',
|
||||
})
|
||||
: l({
|
||||
message: `${plural(requestCount, {
|
||||
one: '# request',
|
||||
other: '# requests',
|
||||
})}`,
|
||||
comment: 'The number of requests to join a group chat.',
|
||||
})}
|
||||
</InlineLinkText>
|
||||
) : null}
|
||||
</View>
|
||||
@@ -655,7 +681,7 @@ function MemberMenu({
|
||||
label={l`Message ${displayName}`}
|
||||
onPress={handleMessageMember}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Message</Trans>
|
||||
<Trans context="action">Message</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={MessageIcon} />
|
||||
</Menu.Item>
|
||||
@@ -919,7 +945,6 @@ function SettingsHeader({
|
||||
|
||||
function SettingsHeaderPlaceholder() {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
|
||||
return (
|
||||
<View style={[a.px_xl, a.py_4xl, a.border_b, t.atoms.border_contrast_low]}>
|
||||
@@ -928,7 +953,7 @@ function SettingsHeaderPlaceholder() {
|
||||
</View>
|
||||
<Text
|
||||
style={[a.text_2xl, a.font_bold, a.text_center, a.pt_lg, t.atoms.text]}>
|
||||
{l`…`}
|
||||
…
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
@@ -1015,7 +1040,7 @@ function SettingsButtonPlaceholder() {
|
||||
a.pt_xs,
|
||||
t.atoms.text,
|
||||
]}>
|
||||
{l`…`}
|
||||
…
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
@@ -1084,7 +1109,7 @@ function InviteLinkPrompt({
|
||||
<Prompt.Basic
|
||||
control={control}
|
||||
title={l`Invite link`}
|
||||
description={l`An invite link lets people join this group chat without being added directly. You control who can use the link and whether they need your approval. You can disable the link at any time. Your name, avatar, and the name of the group chat will be visible to everyone`}
|
||||
description={l`An invite link lets people join this group chat without being added directly. You control who can use the link and whether they need your approval. You can disable the link at any time. Your name, avatar, and the name of the group chat will be visible to everyone.`}
|
||||
confirmButtonCta={l`Get started`}
|
||||
cancelButtonCta={l`Cancel`}
|
||||
onConfirm={onConfirm}
|
||||
|
||||
@@ -11,6 +11,8 @@ import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
|
||||
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import * as Layout from '#/components/Layout'
|
||||
@@ -24,7 +26,16 @@ type AllowIncoming = 'all' | 'none' | 'following'
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesSettings'>
|
||||
|
||||
export function MessagesSettingsScreen(props: Props) {
|
||||
return <MessagesSettingsScreenInner {...props} />
|
||||
const {_} = useLingui()
|
||||
const aaCopy = useAgeAssuranceCopy()
|
||||
|
||||
return (
|
||||
<AgeRestrictedScreen
|
||||
screenTitle={_(msg`Chat settings`)}
|
||||
infoText={aaCopy.chatsInfoText}>
|
||||
<MessagesSettingsScreenInner {...props} />
|
||||
</AgeRestrictedScreen>
|
||||
)
|
||||
}
|
||||
|
||||
export function MessagesSettingsScreenInner({}: Props) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {useCallback, useMemo, useState} from 'react'
|
||||
import {type GestureResponderEvent, View} from 'react-native'
|
||||
import {
|
||||
AppBskyEmbedRecord,
|
||||
ChatBskyConvoDefs,
|
||||
moderateProfile,
|
||||
type ModerationDecision,
|
||||
@@ -14,13 +13,7 @@ import {GestureActionView} from '#/lib/custom-animations/GestureActionView'
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
import {decrementBadgeCount} from '#/lib/notifications/notifications'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {
|
||||
postUriToRelativePath,
|
||||
toBskyAppUrl,
|
||||
toShortUrl,
|
||||
} from '#/lib/strings/url-helpers'
|
||||
import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {
|
||||
@@ -36,6 +29,9 @@ import * as tokens from '#/alf/tokens'
|
||||
import {AvatarBubbles} from '#/components/AvatarBubbles'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
||||
import {getMessageInfo} from '#/components/dms/getMessageInfo'
|
||||
import {getReactionInfo} from '#/components/dms/getReactionInfo'
|
||||
import {getSystemMessageInfo} from '#/components/dms/getSystemMessageInfo'
|
||||
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
|
||||
import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util'
|
||||
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
|
||||
@@ -53,12 +49,6 @@ import type * as bsky from '#/types/bsky'
|
||||
|
||||
export const ChatListItemPortal = createPortalGroup()
|
||||
|
||||
/**
|
||||
* IMPORTANT NOTE: THIS IS CURRENTLY JANKY AF AND PROBABLY BROKEN, JUST WANTED TO ADD GROUPCHAT SUPPPORT
|
||||
*
|
||||
* TAKE A SECOND PASS PLEASE -sfn
|
||||
*/
|
||||
|
||||
export function ChatListItem({
|
||||
convo: convoView,
|
||||
showMenu = true,
|
||||
@@ -93,8 +83,9 @@ export function ChatListItem({
|
||||
<GroupChatItem
|
||||
convo={convo}
|
||||
moderationOpts={moderationOpts}
|
||||
showMenu={showMenu}
|
||||
/>
|
||||
showMenu={showMenu}>
|
||||
{children}
|
||||
</GroupChatItem>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
@@ -188,7 +179,7 @@ function GroupChatItem({
|
||||
return (
|
||||
<BaseChatItem
|
||||
convo={convo.view}
|
||||
avatar={<AvatarBubbles profiles={convo.members} size="medium" />}
|
||||
avatar={<AvatarBubbles profiles={convo.members} size={52} />}
|
||||
title={chatName}
|
||||
accessibilityHint={l`Go to the group chat named "${chatName}"`}
|
||||
primaryProfile={groupOwner}
|
||||
@@ -233,7 +224,7 @@ function BaseChatItem({
|
||||
}) {
|
||||
const ax = useAnalytics()
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const {t: l, i18n} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const menuControl = useMenuControl()
|
||||
const leaveConvoControl = useDialogControl()
|
||||
@@ -242,7 +233,7 @@ function BaseChatItem({
|
||||
|
||||
const playHaptic = useHaptics()
|
||||
const queryClient = useQueryClient()
|
||||
const isUnread = convo.unreadCount > 0
|
||||
const hasUnread = convo.unreadCount > 0 && !isDeletedAccount
|
||||
|
||||
const blockInfo = useMemo(() => {
|
||||
const modui = primaryProfileModeration.ui('profileView')
|
||||
@@ -265,51 +256,7 @@ function BaseChatItem({
|
||||
|
||||
let latestReportableMessage: ChatBskyConvoDefs.MessageView | undefined
|
||||
|
||||
if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
|
||||
const isFromMe = convo.lastMessage.sender?.did === currentAccount?.did
|
||||
|
||||
if (!isFromMe) {
|
||||
latestReportableMessage = convo.lastMessage
|
||||
}
|
||||
|
||||
if (convo.lastMessage.text) {
|
||||
if (isFromMe) {
|
||||
lastMessage = l`You: ${convo.lastMessage.text}`
|
||||
} else {
|
||||
lastMessage = convo.lastMessage.text
|
||||
}
|
||||
} else if (convo.lastMessage.embed) {
|
||||
const defaultEmbeddedContentMessage = l`(contains embedded content)`
|
||||
|
||||
if (AppBskyEmbedRecord.isView(convo.lastMessage.embed)) {
|
||||
const embed = convo.lastMessage.embed
|
||||
|
||||
if (AppBskyEmbedRecord.isViewRecord(embed.record)) {
|
||||
const record = embed.record
|
||||
const path = postUriToRelativePath(record.uri, {
|
||||
handle: record.author.handle,
|
||||
})
|
||||
const href = path ? toBskyAppUrl(path) : undefined
|
||||
const short = href
|
||||
? toShortUrl(href)
|
||||
: defaultEmbeddedContentMessage
|
||||
if (isFromMe) {
|
||||
lastMessage = l`You: ${short}`
|
||||
} else {
|
||||
lastMessage = short
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isFromMe) {
|
||||
lastMessage = l`You: ${defaultEmbeddedContentMessage}`
|
||||
} else {
|
||||
lastMessage = defaultEmbeddedContentMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastMessageSentAt = convo.lastMessage.sentAt
|
||||
}
|
||||
// Deleted message
|
||||
if (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage)) {
|
||||
lastMessageSentAt = convo.lastMessage.sentAt
|
||||
|
||||
@@ -318,47 +265,43 @@ function BaseChatItem({
|
||||
: l`Message deleted`
|
||||
}
|
||||
|
||||
if (ChatBskyConvoDefs.isMessageAndReactionView(convo.lastReaction)) {
|
||||
if (
|
||||
!lastMessageSentAt ||
|
||||
new Date(lastMessageSentAt) <
|
||||
new Date(convo.lastReaction.reaction.createdAt)
|
||||
) {
|
||||
const isFromMe =
|
||||
convo.lastReaction.reaction.sender.did === currentAccount?.did
|
||||
const lastMessageText = convo.lastReaction.message.text
|
||||
const fallbackMessage = l({
|
||||
message: 'a message',
|
||||
comment: `If last message does not contain text, fall back to "{user} reacted to {a message}"`,
|
||||
})
|
||||
// Message
|
||||
if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
|
||||
const info = getMessageInfo({
|
||||
convo,
|
||||
currentAccountDid: currentAccount?.did,
|
||||
i18n,
|
||||
})
|
||||
if (info) {
|
||||
lastMessage = info.message ?? lastMessage
|
||||
lastMessageSentAt = info.sentAt
|
||||
latestReportableMessage = info.reportableMessage
|
||||
}
|
||||
}
|
||||
|
||||
if (isFromMe) {
|
||||
lastMessage = l`You reacted ${convo.lastReaction.reaction.value} to ${
|
||||
lastMessageText
|
||||
? `"${convo.lastReaction.message.text}"`
|
||||
: fallbackMessage
|
||||
}`
|
||||
} else {
|
||||
const senderDid = convo.lastReaction.reaction.sender.did
|
||||
const sender = convo.members.find(
|
||||
member => member.did === senderDid,
|
||||
)
|
||||
if (sender) {
|
||||
lastMessage = l`${sanitizeDisplayName(
|
||||
sender.displayName || sender.handle,
|
||||
)} reacted ${convo.lastReaction.reaction.value} to ${
|
||||
lastMessageText
|
||||
? `"${convo.lastReaction.message.text}"`
|
||||
: fallbackMessage
|
||||
}`
|
||||
} else {
|
||||
lastMessage = l`Someone reacted ${convo.lastReaction.reaction.value} to ${
|
||||
lastMessageText
|
||||
? `"${convo.lastReaction.message.text}"`
|
||||
: fallbackMessage
|
||||
}`
|
||||
}
|
||||
}
|
||||
// Reaction
|
||||
if (ChatBskyConvoDefs.isMessageAndReactionView(convo.lastReaction)) {
|
||||
const info = getReactionInfo({
|
||||
convo,
|
||||
currentAccountDid: currentAccount?.did,
|
||||
i18n,
|
||||
})
|
||||
if (
|
||||
info &&
|
||||
(!lastMessageSentAt ||
|
||||
new Date(lastMessageSentAt) < new Date(info.createdAt))
|
||||
) {
|
||||
lastMessage = info.message
|
||||
lastMessageSentAt = info.createdAt
|
||||
}
|
||||
}
|
||||
|
||||
// System message
|
||||
if (ChatBskyConvoDefs.isSystemMessageView(convo.lastMessage)) {
|
||||
const info = getSystemMessageInfo(convo.lastMessage.data, convo.members)
|
||||
if (info) {
|
||||
lastMessage = i18n._(info.message)
|
||||
lastMessageSentAt = convo.lastMessage.sentAt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,14 +310,7 @@ function BaseChatItem({
|
||||
lastMessageSentAt,
|
||||
latestReportableMessage,
|
||||
}
|
||||
}, [
|
||||
l,
|
||||
convo.lastMessage,
|
||||
convo.lastReaction,
|
||||
currentAccount?.did,
|
||||
isDeletedAccount,
|
||||
convo.members,
|
||||
])
|
||||
}, [l, convo, currentAccount?.did, isDeletedAccount, i18n])
|
||||
|
||||
const [showActions, setShowActions] = useState(false)
|
||||
|
||||
@@ -434,7 +370,7 @@ function BaseChatItem({
|
||||
},
|
||||
}
|
||||
|
||||
const actions = isUnread
|
||||
const actions = hasUnread
|
||||
? {
|
||||
leftFirst: markReadAction,
|
||||
leftSecond: deleteAction,
|
||||
@@ -443,8 +379,6 @@ function BaseChatItem({
|
||||
leftFirst: deleteAction,
|
||||
}
|
||||
|
||||
const hasUnread = convo.unreadCount > 0 && !isDeletedAccount
|
||||
|
||||
return (
|
||||
<ChatListItemPortal.Provider>
|
||||
<GestureActionView actions={actions}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useState} from 'react'
|
||||
import {useRef, useState} from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {
|
||||
useKeyboardHandler,
|
||||
@@ -19,6 +19,7 @@ import {countGraphemes} from 'unicode-segmenter/grapheme'
|
||||
|
||||
import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {isBskyPostUrl} from '#/lib/strings/url-helpers'
|
||||
import {useEmail} from '#/state/email-verification'
|
||||
import {
|
||||
@@ -32,7 +33,7 @@ import {GlassView} from '#/components/GlassView'
|
||||
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji'
|
||||
import {PaperPlaneVertical_Filled_Stroke2_Corner1_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {IS_ANDROID, IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env'
|
||||
import {IS_ANDROID, IS_IOS, IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env'
|
||||
|
||||
const MIN_HEIGHT = 40
|
||||
|
||||
@@ -61,9 +62,9 @@ export function MessageComposer({
|
||||
useSaveMessageDraft(text)
|
||||
|
||||
// Android interactive dismiss sometimes doesn't blur the input
|
||||
const blur = () => {
|
||||
const blur = useNonReactiveCallback(() => {
|
||||
composerInternalApiRef.current?.input?.blur()
|
||||
}
|
||||
})
|
||||
|
||||
useKeyboardHandler({
|
||||
onEnd: evt => {
|
||||
@@ -76,10 +77,10 @@ export function MessageComposer({
|
||||
|
||||
const submitDisabled = !editable || (!hasEmbed && text.trim().length === 0)
|
||||
|
||||
const onSubmit = () => {
|
||||
const onSubmit = (message: string) => {
|
||||
if (!editable) return
|
||||
if (!hasEmbed && text.trim() === '') return
|
||||
const graphemeCount = countGraphemes(text)
|
||||
if (!hasEmbed && message.trim() === '') return
|
||||
const graphemeCount = countGraphemes(message)
|
||||
if (graphemeCount > MAX_DM_GRAPHEME_LENGTH) {
|
||||
Toast.show(
|
||||
l`Message is too long (${graphemeCount}/${MAX_DM_GRAPHEME_LENGTH})`,
|
||||
@@ -89,7 +90,7 @@ export function MessageComposer({
|
||||
}
|
||||
|
||||
clearDraft()
|
||||
onSendMessage(text)
|
||||
onSendMessage(message)
|
||||
playHaptic()
|
||||
setEmbed(undefined)
|
||||
composerInternalApiRef.current?.clear()
|
||||
@@ -99,6 +100,45 @@ export function MessageComposer({
|
||||
}
|
||||
}
|
||||
|
||||
const isFlushingAutocorrectSuggestion = useRef(false)
|
||||
const handleSubmit = () => {
|
||||
if (IS_IOS) {
|
||||
// HACKFIX: If there's a pending autocomplete suggestion, iOS will prioritize
|
||||
// accepting the suggestion over any imperative `.clear()` action on the textinput.
|
||||
// This means we'll send the message with the typo while the corrected text remains
|
||||
// in the composer's textinput.
|
||||
//
|
||||
// In MessageInput, the previous iteration, we simply sent it, and if another text change
|
||||
// event came in, we'd clear it again. However, it's nicer UX to actually accept the suggestion.
|
||||
//
|
||||
// Thus the solution:
|
||||
// 1. Set a ref indicating we're flushing the autocorrect suggestion
|
||||
// 2. Watch for incoming onChange events. If something comes in, it's almost certainly the corrected text,
|
||||
// so send that
|
||||
// 3. Meanwhile, race that against a simple timeout. If the timeout fires first, send the original text.
|
||||
//
|
||||
// Hopefully, it's delaying the send by no more than a couple frames -sfn
|
||||
isFlushingAutocorrectSuggestion.current = true
|
||||
setTimeout(() => {
|
||||
if (isFlushingAutocorrectSuggestion.current) {
|
||||
isFlushingAutocorrectSuggestion.current = false
|
||||
onSubmit(text)
|
||||
}
|
||||
}, 20)
|
||||
} else {
|
||||
onSubmit(text)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (nextText: string) => {
|
||||
if (IS_IOS && isFlushingAutocorrectSuggestion.current) {
|
||||
isFlushingAutocorrectSuggestion.current = false
|
||||
onSubmit(nextText)
|
||||
} else {
|
||||
setText(nextText)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ComposerContainer>
|
||||
{children}
|
||||
@@ -180,7 +220,7 @@ export function MessageComposer({
|
||||
paddingBottom: 10,
|
||||
paddingRight: 16 + platform({web: 20, default: 0}),
|
||||
}}
|
||||
onChange={setText}
|
||||
onChange={handleChange}
|
||||
onFacetCommitted={facet => {
|
||||
if (facet.type === 'url' && isBskyPostUrl(facet.value)) {
|
||||
setEmbed(facet.value)
|
||||
@@ -189,11 +229,11 @@ export function MessageComposer({
|
||||
onRequestSubmit={req => {
|
||||
if (req.platform === 'web' && req.shiftKey) return
|
||||
req.nativeEvent.preventDefault()
|
||||
onSubmit()
|
||||
handleSubmit()
|
||||
}}
|
||||
/>
|
||||
</GlassView>
|
||||
<SubmitButton onPress={onSubmit} disabled={submitDisabled} />
|
||||
<SubmitButton onPress={handleSubmit} disabled={submitDisabled} />
|
||||
</GlassContainer>
|
||||
</View>
|
||||
</ComposerContainer>
|
||||
|
||||
@@ -43,6 +43,7 @@ export function MessageListError({item}: {item: ConvoItem & {type: 'error'}}) {
|
||||
{description}
|
||||
{item.retry && (
|
||||
<>
|
||||
{' '}
|
||||
·{' '}
|
||||
<InlineLinkText
|
||||
label={help}
|
||||
|
||||
@@ -58,6 +58,7 @@ import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill'
|
||||
import {DateDividerToggleProvider} from '#/components/dms/DateDividerToggle'
|
||||
import {MessageItem} from '#/components/dms/MessageItem'
|
||||
import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
|
||||
import {SystemMessageItem} from '#/components/dms/SystemMessageItem'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
@@ -383,6 +384,8 @@ export function MessagesList({
|
||||
)
|
||||
} else if (item.type === 'deleted-message') {
|
||||
return <Text>Deleted message</Text>
|
||||
} else if (item.type === 'system-message') {
|
||||
return <SystemMessageItem item={item} />
|
||||
} else if (item.type === 'error') {
|
||||
return <MessageListError item={item} />
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import {LABELS} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {getLabelingServiceTitle, isAppLabeler} from '#/lib/moderation'
|
||||
import {
|
||||
@@ -21,7 +20,6 @@ import {
|
||||
usePreferencesSetAdultContentMutation,
|
||||
} from '#/state/queries/preferences'
|
||||
import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf'
|
||||
import * as Admonition from '#/components/Admonition'
|
||||
import {AgeAssuranceAdmonition} from '#/components/ageAssurance/AgeAssuranceAdmonition'
|
||||
@@ -161,7 +159,6 @@ export function ModerationScreenInner({
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
|
||||
const {
|
||||
@@ -197,12 +194,6 @@ export function ModerationScreenInner({
|
||||
}
|
||||
}
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
}, [setMinimalShellMode]),
|
||||
)
|
||||
|
||||
const {mutateAsync: setAdultContentPref, variables: optimisticAdultContent} =
|
||||
usePreferencesSetAdultContentMutation()
|
||||
let adultContentEnabled = !!(
|
||||
|
||||
@@ -105,6 +105,7 @@ export function StepSuggestedAccounts() {
|
||||
ax.metric('suggestedUser:follow', {
|
||||
logContext: 'Onboarding',
|
||||
location: 'FollowAll',
|
||||
recSource: !useFullExperience ? 'Search' : undefined,
|
||||
recId: suggestedUsers?.recId,
|
||||
position: i,
|
||||
suggestedDid: did,
|
||||
@@ -155,6 +156,7 @@ export function StepSuggestedAccounts() {
|
||||
seenProfilesRef.current.add(did)
|
||||
ax.metric('suggestedUser:seen', {
|
||||
logContext: 'Onboarding',
|
||||
recSource: !useFullExperience ? 'Search' : undefined,
|
||||
recId: suggestedUsers?.recId,
|
||||
position,
|
||||
suggestedDid: did,
|
||||
@@ -162,7 +164,7 @@ export function StepSuggestedAccounts() {
|
||||
})
|
||||
}
|
||||
},
|
||||
[ax, selectedInterest, suggestedUsers?.recId],
|
||||
[ax, selectedInterest, suggestedUsers?.recId, useFullExperience],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -248,6 +250,7 @@ export function StepSuggestedAccounts() {
|
||||
position={index}
|
||||
category={selectedInterest}
|
||||
onSeen={onProfileSeen}
|
||||
recSource={!useFullExperience ? 'Search' : undefined}
|
||||
recId={suggestedUsers.recId}
|
||||
/>
|
||||
))}
|
||||
@@ -358,6 +361,7 @@ function SuggestedProfileCard({
|
||||
position,
|
||||
category,
|
||||
onSeen,
|
||||
recSource,
|
||||
recId,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
@@ -365,6 +369,7 @@ function SuggestedProfileCard({
|
||||
position: number
|
||||
category: string | null
|
||||
onSeen: (did: string, position: number) => void
|
||||
recSource?: 'Search'
|
||||
recId?: number | string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
@@ -432,6 +437,7 @@ function SuggestedProfileCard({
|
||||
ax.metric('suggestedUser:follow', {
|
||||
logContext: 'Onboarding',
|
||||
location: 'Card',
|
||||
recSource,
|
||||
recId,
|
||||
position,
|
||||
suggestedDid: profile.did,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import {useCallback} from 'react'
|
||||
import {Plural, Trans} from '@lingui/react/macro'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {
|
||||
type CommonNavigatorParams,
|
||||
@@ -8,13 +6,11 @@ import {
|
||||
} from '#/lib/routes/types'
|
||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
||||
import {usePostQuery} from '#/state/queries/post'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'>
|
||||
export const PostLikedByScreen = ({route}: Props) => {
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {name, rkey} = route.params
|
||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||
const {data: post} = usePostQuery(uri)
|
||||
@@ -24,12 +20,6 @@ export const PostLikedByScreen = ({route}: Props) => {
|
||||
likeCount = post.likeCount
|
||||
}
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
}, [setMinimalShellMode]),
|
||||
)
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header.Outer>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import {useCallback} from 'react'
|
||||
import {Plural, Trans} from '@lingui/react/macro'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {
|
||||
type CommonNavigatorParams,
|
||||
@@ -8,13 +6,11 @@ import {
|
||||
} from '#/lib/routes/types'
|
||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
||||
import {usePostQuery} from '#/state/queries/post'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuotes'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostQuotes'>
|
||||
export const PostQuotesScreen = ({route}: Props) => {
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {name, rkey} = route.params
|
||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||
const {data: post} = usePostQuery(uri)
|
||||
@@ -24,12 +20,6 @@ export const PostQuotesScreen = ({route}: Props) => {
|
||||
quoteCount = post.quoteCount
|
||||
}
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
}, [setMinimalShellMode]),
|
||||
)
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header.Outer>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user