Merge remote-tracking branch 'origin/main' into samuel/alf-login

zio/stable
Samuel Newman 2024-03-20 15:37:14 +00:00
commit d24ffba01d
62 changed files with 30007 additions and 10775 deletions

View File

@ -64,7 +64,7 @@ describe('Curate lists', () => {
await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Library')).tap()
await element(by.text('Upload from Library')).tap()
await sleep(3e3)
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
@ -81,7 +81,7 @@ describe('Curate lists', () => {
await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Remove')).tap()
await element(by.text('Remove Avatar')).tap()
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
await expect(element(by.id('userAvatarFallback'))).toExist()

View File

@ -17,7 +17,7 @@ describe('Home screen', () => {
it('Can go to feeds page using feeds button in tab bar', async () => {
await element(by.id('homeScreenFeedTabs-Feeds ✨')).tap()
await expect(element(by.text('Discover new feeds'))).toBeVisible()
await expect(element(by.text('Discover New Feeds'))).toBeVisible()
})
it('Feeds button disappears after pinning a feed', async () => {

View File

@ -70,10 +70,10 @@ describe('Profile screen', () => {
await element(by.id('profileHeaderEditProfileButton')).tap()
await expect(element(by.id('editProfileModal'))).toBeVisible()
await element(by.id('changeBannerBtn')).tap()
await element(by.text('Library')).tap()
await element(by.text('Upload from Library')).tap()
await sleep(3e3)
await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Library')).tap()
await element(by.text('Upload from Library')).tap()
await sleep(3e3)
await element(by.id('editProfileSaveBtn')).tap()
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
@ -87,9 +87,9 @@ describe('Profile screen', () => {
await element(by.id('profileHeaderEditProfileButton')).tap()
await expect(element(by.id('editProfileModal'))).toBeVisible()
await element(by.id('changeBannerBtn')).tap()
await element(by.text('Remove')).tap()
await element(by.text('Remove Banner')).tap()
await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Remove')).tap()
await element(by.text('Remove Avatar')).tap()
await element(by.id('editProfileSaveBtn')).tap()
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
await expect(element(by.id('userBannerFallback'))).toExist()

View File

@ -12,6 +12,7 @@ describe('Create account', () => {
})
it('I can create a new account with text verification', async () => {
console.log('SERVICE IS', service)
await element(by.id('e2eOpenLoggedOutView')).tap()
await element(by.id('createAccountButton')).tap()
@ -28,16 +29,17 @@ describe('Create account', () => {
await device.takeScreenshot('4- entered account details')
await element(by.id('nextBtn')).tap()
await element(by.id('phoneInput')).typeText('8042221111')
await element(by.id('requestCodeBtn')).tap()
await device.takeScreenshot('5- requested code')
await element(by.id('codeInput')).typeText('000000')
await device.takeScreenshot('6- entered code')
await element(by.id('handleInput')).typeText('text-verification-test')
await device.takeScreenshot('5- entered handle')
await element(by.id('nextBtn')).tap()
await element(by.id('handleInput')).typeText('text-verification-test')
await device.takeScreenshot('7- entered handle')
await element(by.id('phoneInput')).typeText('8042221111')
await element(by.id('requestCodeBtn')).tap()
await device.takeScreenshot('6- requested code')
await element(by.id('codeInput')).typeText('000000')
await device.takeScreenshot('7- entered code')
await element(by.id('nextBtn')).tap()
await element(by.id('nextBtn')).tap()

View File

@ -44,7 +44,7 @@
scrollbar-gutter: stable both-edges;
}
html, body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif;
}
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
@ -141,7 +141,7 @@
/* ProseMirror */
.ProseMirror {
font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif;
min-height: 140px;
}
.ProseMirror-dark {

View File

@ -72,6 +72,7 @@ export async function createServer(
const phoneParams = phoneRequired
? {
phoneVerificationRequired: true,
phoneVerificationProvider: 'twilio',
twilioAccountSid: 'ACXXXXXXX',
twilioAuthToken: 'AUTH',
twilioServiceSid: 'VAXXXXXXXX',
@ -95,6 +96,35 @@ export async function createServer(
})
mockTwilio(testNet.pds)
// add the test mod authority
if (!phoneRequired) {
const agent = new BskyAgent({service: pdsUrl})
const res = await agent.api.com.atproto.server.createAccount({
email: 'mod-authority@test.com',
handle: 'mod-authority.test',
password: 'hunter2',
})
agent.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`)
await agent.api.app.bsky.actor.profile.create(
{repo: res.data.did},
{
displayName: 'Dev-env Moderation',
description: `The pretend version of mod.bsky.app`,
},
)
await agent.api.app.bsky.labeler.service.create(
{repo: res.data.did, rkey: 'self'},
{
policies: {
labelValues: ['!hide', '!warn'],
labelValueDefinitions: [],
},
createdAt: new Date().toISOString(),
},
)
}
const pic = fs.readFileSync(
path.join(__dirname, '..', 'assets', 'default-avatar.png'),
)
@ -455,13 +485,13 @@ async function getPort(start = 3000) {
}
export const mockTwilio = (pds: TestPds) => {
if (!pds.ctx.twilio) return
if (!pds.ctx.phoneVerifier) return
pds.ctx.twilio.sendCode = async (_number: string) => {
pds.ctx.phoneVerifier.sendCode = async (_number: string) => {
// do nothing
}
pds.ctx.twilio.verifyCode = async (_number: string, code: string) => {
pds.ctx.phoneVerifier.verifyCode = async (_number: string, code: string) => {
return code === '000000'
}
}

View File

@ -4,6 +4,7 @@ module.exports = {
'en',
'de',
'es',
'fi',
'fr',
'hi',
'id',

View File

@ -46,7 +46,7 @@
"make-deploy-bundle": "bash scripts/bundleUpdate.sh"
},
"dependencies": {
"@atproto/api": "^0.12.1",
"@atproto/api": "^0.12.2",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@emoji-mart/react": "^1.1.1",

View File

@ -565,7 +565,11 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
}
function getCurrentRouteName() {
return navigationRef.getCurrentRoute()?.name
if (navigationRef.isReady()) {
return navigationRef.getCurrentRoute()?.name
} else {
return undefined
}
}
/**

View File

@ -206,12 +206,16 @@ export function Inner({children, style}: DialogInnerProps) {
)
}
export function ScrollableInner({children, style}: DialogInnerProps) {
export function ScrollableInner({
children,
keyboardDismissMode,
style,
}: DialogInnerProps) {
const insets = useSafeAreaInsets()
return (
<BottomSheetScrollView
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
keyboardDismissMode={keyboardDismissMode || 'on-drag'}
style={[
a.flex_1, // main diff is this
a.p_xl,

View File

@ -1,5 +1,9 @@
import React from 'react'
import type {AccessibilityProps, GestureResponderEvent} from 'react-native'
import type {
AccessibilityProps,
GestureResponderEvent,
ScrollViewProps,
} from 'react-native'
import {BottomSheetProps} from '@gorhom/bottom-sheet'
import {ViewStyleProp} from '#/alf'
@ -61,9 +65,11 @@ export type DialogInnerProps =
label?: undefined
accessibilityLabelledBy: A11yProps['aria-labelledby']
accessibilityDescribedBy: string
keyboardDismissMode?: ScrollViewProps['keyboardDismissMode']
}>
| DialogInnerPropsBase<{
label: string
accessibilityLabelledBy?: undefined
accessibilityDescribedBy?: undefined
keyboardDismissMode?: ScrollViewProps['keyboardDismissMode']
}>

View File

@ -37,7 +37,9 @@ function ReportDialogInner(props: ReportDialogProps) {
const isLoading = useDelayedLoading(500, isLabelerLoading)
return (
<Dialog.ScrollableInner label="Report Dialog">
<Dialog.ScrollableInner
label="Report Dialog"
keyboardDismissMode="interactive">
{isLoading ? (
<View style={[a.align_center, {height: 100}]}>
<Loader size="xl" />

View File

@ -87,7 +87,7 @@ export function TagMenu({
author: authorHandle,
})
},
testID: 'tagMenuSeachByUser',
testID: 'tagMenuSearchByUser',
icon: {
ios: {
name: 'magnifyingglass',

View File

@ -56,7 +56,8 @@ export function ScreenHider({
const isNoPwi = !!modui.blurs.find(
cause =>
cause.type === 'label' && cause.labelDef.id === '!no-unauthenticated',
cause.type === 'label' &&
cause.labelDef.identifier === '!no-unauthenticated',
)
return (
<CenteredView

View File

@ -6,6 +6,7 @@ import {useSessionApi, SessionAccount} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {useCloseAllActiveElements} from '#/state/util'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {LogEvents} from '../statsig/statsig'
export function useAccountSwitcher() {
const {track} = useAnalytics()
@ -14,7 +15,10 @@ export function useAccountSwitcher() {
const {requestSwitchToAccount} = useLoggedOutViewControls()
const onPressSwitchAccount = useCallback(
async (account: SessionAccount) => {
async (
account: SessionAccount,
logContext: LogEvents['account:loggedIn']['logContext'],
) => {
track('Settings:SwitchAccountButtonClicked')
try {
@ -28,7 +32,7 @@ export function useAccountSwitcher() {
// So we change the URL ourselves. The navigator will pick it up on remount.
history.pushState(null, '', '/')
}
await selectAccount(account)
await selectAccount(account, logContext)
setTimeout(() => {
Toast.show(`Signed in as @${account.handle}`)
}, 100)

View File

@ -6,6 +6,7 @@ let refCount = 0
function incrementRefCount() {
if (refCount === 0) {
document.body.style.overflow = 'hidden'
document.documentElement.style.scrollbarGutter = 'auto'
}
refCount++
}
@ -14,6 +15,7 @@ function decrementRefCount() {
refCount--
if (refCount === 0) {
document.body.style.overflow = ''
document.documentElement.style.scrollbarGutter = ''
}
}

View File

@ -3,7 +3,6 @@ import RNFS from 'react-native-fs'
import {CropperOptions} from './types'
import {compressIfNeeded} from './manip'
let _imageCounter = 0
async function getFile() {
let files = await RNFS.readDir(
RNFS.LibraryDirectoryPath.split('/')
@ -12,7 +11,7 @@ async function getFile() {
.join('/'),
)
files = files.filter(file => file.path.endsWith('.JPG'))
const file = files[_imageCounter++ % files.length]
const file = files[0]
return await compressIfNeeded({
path: file.path,
mime: 'image/jpeg',

View File

@ -118,11 +118,15 @@ export function useModerationCauseDescription(
(labeler?.creator.handle ? '@' + labeler?.creator.handle : undefined)
if (!source) {
if (cause.label.src === BSKY_LABELER_DID) {
source = 'Bluesky Moderation'
source = 'Bluesky Moderation Service'
} else {
source = cause.label.src
}
}
if (def.identifier === 'porn' || def.identifier === 'sexual') {
strings.name = 'Adult Content'
}
return {
icon:
def.identifier === '!no-unauthenticated'

View File

@ -7,6 +7,7 @@ import {logger} from '#/logger'
import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
import {truncateAndInvalidate} from '#/state/queries/util'
import {SessionAccount, getAgent} from '#/state/session'
import {logEvent} from '../statsig/statsig'
const SERVICE_DID = (serviceUrl?: string) =>
serviceUrl?.includes('staging')
@ -123,6 +124,7 @@ export function init(queryClient: QueryClient) {
logger.DebugContext.notifications,
)
track('Notificatons:OpenApp')
logEvent('notifications:openApp', {})
truncateAndInvalidate(queryClient, RQKEY_NOTIFS())
resetToTab('NotificationsTab') // open notifications tab
}

View File

@ -2,10 +2,26 @@ export type LogEvents = {
init: {
initMs: number
}
'account:loggedIn': {
logContext: 'LoginForm' | 'SwitchAccount' | 'ChooseAccountForm' | 'Settings'
withPassword: boolean
}
'account:loggedOut': {
logContext: 'SwitchAccount' | 'Settings' | 'Deactivated'
}
'notifications:openApp': {}
'state:background': {
secondsActive: number
}
'state:foreground': {}
'feed:endReached': {
feedType: string
itemCount: number
}
'feed:refresh': {
feedType: string
reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest'
}
'post:create': {
imageCount: number
isReply: boolean

View File

@ -1,9 +1,11 @@
import React from 'react'
import {Platform} from 'react-native'
import {
Statsig,
StatsigProvider,
useGate as useStatsigGate,
} from 'statsig-react-native-expo'
import {AppState, AppStateStatus} from 'react-native'
import {useSession} from '../../state/session'
import {sha256} from 'js-sha256'
import {LogEvents} from './events'
@ -58,9 +60,34 @@ function toStatsigUser(did: string | undefined) {
if (did) {
userID = sha256(did)
}
return {userID}
return {
userID,
platform: Platform.OS,
}
}
let lastState: AppStateStatus = AppState.currentState
let lastActive = lastState === 'active' ? performance.now() : null
AppState.addEventListener('change', (state: AppStateStatus) => {
if (state === lastState) {
return
}
lastState = state
if (state === 'active') {
lastActive = performance.now()
logEvent('state:foreground', {})
} else {
let secondsActive = 0
if (lastActive != null) {
secondsActive = Math.round((performance.now() - lastActive) / 1e3)
}
lastActive = null
logEvent('state:background', {
secondsActive,
})
}
})
export function Provider({children}: {children: React.ReactNode}) {
const {currentAccount} = useSession()
const currentStatsigUser = React.useMemo(

View File

@ -122,6 +122,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
return AppLanguage.de
case 'es':
return AppLanguage.es
case 'fi':
return AppLanguage.fi
case 'fr':
return AppLanguage.fr
case 'hi':

View File

@ -6,6 +6,7 @@ import {messages as messagesEn} from '#/locale/locales/en/messages'
import {messages as messagesDe} from '#/locale/locales/de/messages'
import {messages as messagesId} from '#/locale/locales/id/messages'
import {messages as messagesEs} from '#/locale/locales/es/messages'
import {messages as messagesFi} from '#/locale/locales/fi/messages'
import {messages as messagesFr} from '#/locale/locales/fr/messages'
import {messages as messagesHi} from '#/locale/locales/hi/messages'
import {messages as messagesJa} from '#/locale/locales/ja/messages'
@ -32,6 +33,10 @@ export async function dynamicActivate(locale: AppLanguage) {
i18n.loadAndActivate({locale, messages: messagesEs})
break
}
case AppLanguage.fi: {
i18n.loadAndActivate({locale, messages: messagesFi})
break
}
case AppLanguage.fr: {
i18n.loadAndActivate({locale, messages: messagesFr})
break

View File

@ -20,6 +20,10 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/es/messages`)
break
}
case AppLanguage.fi: {
mod = await import(`./locales/fi/messages`)
break
}
case AppLanguage.fr: {
mod = await import(`./locales/fr/messages`)
break

View File

@ -8,6 +8,7 @@ export enum AppLanguage {
en = 'en',
de = 'de',
es = 'es',
fi = 'fi',
fr = 'fr',
hi = 'hi',
id = 'id',
@ -29,6 +30,7 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
{code2: AppLanguage.en, name: 'English'},
{code2: AppLanguage.de, name: 'Deutsch German'},
{code2: AppLanguage.es, name: 'Español Spanish'},
{code2: AppLanguage.fi, name: 'Suomi Finnish'},
{code2: AppLanguage.fr, name: 'Français French'},
{code2: AppLanguage.hi, name: 'हिंदी Hindi'},
{code2: AppLanguage.id, name: 'Bahasa Indonesia Indonesian'},

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -147,7 +147,7 @@ export function Deactivated() {
variant="ghost"
size="large"
label={_(msg`Log out`)}
onPress={logout}>
onPress={() => logout('Deactivated')}>
<ButtonText style={[{color: t.palette.primary_500}]}>
<Trans>Log out</Trans>
</ButtonText>
@ -176,7 +176,7 @@ export function Deactivated() {
variant="ghost"
size="large"
label={_(msg`Log out`)}
onPress={logout}>
onPress={() => logout('Deactivated')}>
<ButtonText style={[{color: t.palette.primary_500}]}>
<Trans>Log out</Trans>
</ButtonText>

View File

@ -95,11 +95,14 @@ export const LoginForm = ({
}
// TODO remove double login
await login({
service: serviceUrl,
identifier: fullIdent,
password,
})
await login(
{
service: serviceUrl,
identifier: fullIdent,
password,
},
'LoginForm',
)
} catch (e: any) {
const errMsg = e.toString()
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)

View File

@ -20,6 +20,7 @@ import {useCloseAllActiveElements} from '#/state/util'
import {track} from '#/lib/analytics/analytics'
import {hasProp} from '#/lib/type-guards'
import {readLabelers} from './agent-config'
import {logEvent, LogEvents} from '#/lib/statsig/statsig'
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
@ -54,17 +55,22 @@ export type ApiContext = {
verificationPhone?: string
verificationCode?: string
}) => Promise<void>
login: (props: {
service: string
identifier: string
password: string
}) => Promise<void>
login: (
props: {
service: string
identifier: string
password: string
},
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
/**
* A full logout. Clears the `currentAccount` from session, AND removes
* access tokens from all accounts, so that returning as any user will
* require a full login.
*/
logout: () => Promise<void>
logout: (
logContext: LogEvents['account:loggedOut']['logContext'],
) => Promise<void>
/**
* A partial logout. Clears the `currentAccount` from session, but DOES NOT
* clear access tokens from accounts, allowing the user to return to their
@ -76,7 +82,10 @@ export type ApiContext = {
initSession: (account: SessionAccount) => Promise<void>
resumeSession: (account?: SessionAccount) => Promise<void>
removeAccount: (account: SessionAccount) => void
selectAccount: (account: SessionAccount) => Promise<void>
selectAccount: (
account: SessionAccount,
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
updateCurrentAccount: (
account: Partial<
Pick<SessionAccount, 'handle' | 'email' | 'emailConfirmed'>
@ -286,7 +295,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
)
const login = React.useCallback<ApiContext['login']>(
async ({service, identifier, password}) => {
async ({service, identifier, password}, logContext) => {
logger.debug(`session: login`, {}, logger.DebugContext.session)
const agent = new BskyAgent({service})
@ -329,24 +338,29 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
logger.debug(`session: logged in`, {}, logger.DebugContext.session)
track('Sign In', {resumedSession: false})
logEvent('account:loggedIn', {logContext, withPassword: true})
},
[upsertAccount, queryClient, clearCurrentAccount],
)
const logout = React.useCallback<ApiContext['logout']>(async () => {
logger.debug(`session: logout`)
clearCurrentAccount()
setStateAndPersist(s => {
return {
...s,
accounts: s.accounts.map(a => ({
...a,
refreshJwt: undefined,
accessJwt: undefined,
})),
}
})
}, [clearCurrentAccount, setStateAndPersist])
const logout = React.useCallback<ApiContext['logout']>(
async logContext => {
logger.debug(`session: logout`)
clearCurrentAccount()
setStateAndPersist(s => {
return {
...s,
accounts: s.accounts.map(a => ({
...a,
refreshJwt: undefined,
accessJwt: undefined,
})),
}
})
logEvent('account:loggedOut', {logContext})
},
[clearCurrentAccount, setStateAndPersist],
)
const initSession = React.useCallback<ApiContext['initSession']>(
async account => {
@ -540,11 +554,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
)
const selectAccount = React.useCallback<ApiContext['selectAccount']>(
async account => {
async (account, logContext) => {
setState(s => ({...s, isSwitchingAccounts: true}))
try {
await initSession(account)
setState(s => ({...s, isSwitchingAccounts: false}))
logEvent('account:loggedIn', {logContext, withPassword: false})
} catch (e) {
// reset this in case of error
setState(s => ({...s, isSwitchingAccounts: false}))

View File

@ -447,7 +447,7 @@ export const ComposePost = observer(function ComposePost({
/>
)}
{quote ? (
<View style={[s.mt5, isWeb && s.mb10]}>
<View style={[s.mt5, isWeb && s.mb10, {pointerEvents: 'none'}]}>
<QuoteEmbed quote={quote} />
</View>
) : undefined}

View File

@ -0,0 +1,45 @@
import {useState, useEffect} from 'react'
import * as apilib from 'lib/api/index'
import {getLinkMeta} from 'lib/link-meta/link-meta'
import {ComposerOpts} from 'state/shell/composer'
import {getAgent} from '#/state/session'
export function useExternalLinkFetch({}: {
setQuote: (opts: ComposerOpts['quote']) => void
}) {
const [extLink, setExtLink] = useState<apilib.ExternalEmbedDraft | undefined>(
undefined,
)
useEffect(() => {
let aborted = false
const cleanup = () => {
aborted = true
}
if (!extLink) {
return cleanup
}
if (!extLink.meta) {
getLinkMeta(getAgent(), extLink.uri).then(meta => {
if (aborted) {
return
}
setExtLink({
uri: extLink.uri,
isLoading: !!meta.image,
meta,
})
})
return cleanup
}
if (extLink.isLoading) {
setExtLink({
...extLink,
isLoading: false, // done
})
}
return cleanup
}, [extLink])
return {extLink, setExtLink}
}

View File

@ -22,6 +22,7 @@ import {listenSoftReset} from '#/state/events'
import {truncateAndInvalidate} from '#/state/queries/util'
import {TabState, getTabState, getRootNavigation} from '#/lib/routes/helpers'
import {isNative} from '#/platform/detection'
import {logEvent} from '#/lib/statsig/statsig'
const POLL_FREQ = 60e3 // 60sec
@ -68,6 +69,10 @@ export function FeedPage({
scrollToTop()
truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
setHasNew(false)
logEvent('feed:refresh', {
feedType: feed.split('|')[0],
reason: 'soft-reset',
})
}
}, [navigation, isPageFocused, scrollToTop, queryClient, feed, setHasNew])
@ -89,6 +94,10 @@ export function FeedPage({
scrollToTop()
truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
setHasNew(false)
logEvent('feed:refresh', {
feedType: feed.split('|')[0],
reason: 'load-latest',
})
}, [scrollToTop, feed, queryClient, setHasNew])
return (

View File

@ -6,7 +6,13 @@
*
*/
import React from 'react'
import {SafeAreaView, Text, TouchableOpacity, StyleSheet} from 'react-native'
import {
SafeAreaView,
TouchableOpacity,
StyleSheet,
ViewStyle,
} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@ -23,14 +29,14 @@ const ImageDefaultHeader = ({onRequestClose}: Props) => {
return (
<SafeAreaView style={styles.root}>
<TouchableOpacity
style={styles.closeButton}
style={[styles.closeButton, styles.blurredBackground]}
onPress={onRequestClose}
hitSlop={HIT_SLOP}
accessibilityRole="button"
accessibilityLabel={_(msg`Close image`)}
accessibilityHint={_(msg`Closes viewer for header image`)}
onAccessibilityEscape={onRequestClose}>
<Text style={styles.closeText}></Text>
<FontAwesomeIcon icon="close" color={'#fff'} size={22} />
</TouchableOpacity>
</SafeAreaView>
)
@ -42,8 +48,8 @@ const styles = StyleSheet.create({
pointerEvents: 'box-none',
},
closeButton: {
marginRight: 8,
marginTop: 8,
marginRight: 10,
marginTop: 10,
width: 44,
height: 44,
alignItems: 'center',
@ -51,13 +57,10 @@ const styles = StyleSheet.create({
borderRadius: 22,
backgroundColor: '#00000077',
},
closeText: {
lineHeight: 22,
fontSize: 19,
textAlign: 'center',
color: '#FFF',
includeFontPadding: false,
},
blurredBackground: {
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
} as ViewStyle,
})
export default ImageDefaultHeader

View File

@ -7,6 +7,7 @@ import {
StyleSheet,
View,
Pressable,
ViewStyle,
} from 'react-native'
import {
FontAwesomeIcon,
@ -24,6 +25,7 @@ import {
ProfileImageLightbox,
} from '#/state/lightbox'
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
interface Img {
uri: string
@ -111,6 +113,14 @@ function LightboxInner({
return () => window.removeEventListener('keydown', onKeyDown)
}, [onKeyDown])
const {isTabletOrDesktop} = useWebMediaQueries()
const btnStyle = React.useMemo(() => {
return isTabletOrDesktop ? styles.btnTablet : styles.btnMobile
}, [isTabletOrDesktop])
const iconSize = React.useMemo(() => {
return isTabletOrDesktop ? 32 : 24
}, [isTabletOrDesktop])
return (
<View style={styles.mask}>
<TouchableWithoutFeedback
@ -130,28 +140,38 @@ function LightboxInner({
{canGoLeft && (
<TouchableOpacity
onPress={onPressLeft}
style={[styles.btn, styles.leftBtn]}
style={[
styles.btn,
btnStyle,
styles.leftBtn,
styles.blurredBackground,
]}
accessibilityRole="button"
accessibilityLabel={_(msg`Previous image`)}
accessibilityHint="">
<FontAwesomeIcon
icon="angle-left"
style={styles.icon as FontAwesomeIconStyle}
size={40}
size={iconSize}
/>
</TouchableOpacity>
)}
{canGoRight && (
<TouchableOpacity
onPress={onPressRight}
style={[styles.btn, styles.rightBtn]}
style={[
styles.btn,
btnStyle,
styles.rightBtn,
styles.blurredBackground,
]}
accessibilityRole="button"
accessibilityLabel={_(msg`Next image`)}
accessibilityHint="">
<FontAwesomeIcon
icon="angle-right"
style={styles.icon as FontAwesomeIconStyle}
size={40}
size={iconSize}
/>
</TouchableOpacity>
)}
@ -213,20 +233,30 @@ const styles = StyleSheet.create({
},
btn: {
position: 'absolute',
backgroundColor: '#000',
width: 50,
height: 50,
backgroundColor: '#00000077',
justifyContent: 'center',
alignItems: 'center',
},
btnTablet: {
width: 50,
height: 50,
borderRadius: 25,
left: 30,
right: 30,
},
btnMobile: {
width: 44,
height: 44,
borderRadius: 22,
left: 20,
right: 20,
},
leftBtn: {
left: 30,
right: 'auto',
top: '50%',
},
rightBtn: {
position: 'absolute',
right: 30,
left: 'auto',
top: '50%',
},
footer: {
@ -234,4 +264,8 @@ const styles = StyleSheet.create({
paddingVertical: 24,
backgroundColor: colors.black,
},
blurredBackground: {
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
} as ViewStyle,
})

View File

@ -39,7 +39,7 @@ function SwitchAccountCard({account}: {account: SessionAccount}) {
track('Settings:SignOutButtonClicked')
closeAllActiveElements()
// needs to be in timeout or the modal re-opens
setTimeout(() => logout(), 0)
setTimeout(() => logout('SwitchAccount'), 0)
}, [track, logout, closeAllActiveElements])
const contents = (
@ -95,7 +95,9 @@ function SwitchAccountCard({account}: {account: SessionAccount}) {
key={account.did}
style={[isSwitchingAccounts && styles.dimmed]}
onPress={
isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account)
isSwitchingAccounts
? undefined
: () => onPressSwitchAccount(account, 'SwitchAccount')
}
accessibilityRole="button"
accessibilityLabel={_(msg`Switch to ${account.handle}`)}

View File

@ -108,7 +108,8 @@ export function PostThread({
?.ui('contentList')
.blurs.find(
cause =>
cause.type === 'label' && cause.labelDef.id === '!no-unauthenticated',
cause.type === 'label' &&
cause.labelDef.identifier === '!no-unauthenticated',
)
}, [rootPost, moderationOpts])

View File

@ -90,6 +90,7 @@ let Feed = ({
const [isPTRing, setIsPTRing] = React.useState(false)
const checkForNewRef = React.useRef<(() => void) | null>(null)
const lastFetchRef = React.useRef<number>(Date.now())
const feedType = feed.split('|')[0]
const opts = React.useMemo(
() => ({enabled, ignoreFilterFor}),
@ -214,6 +215,10 @@ let Feed = ({
const onRefresh = React.useCallback(async () => {
track('Feed:onRefresh')
logEvent('feed:refresh', {
feedType: feedType,
reason: 'pull-to-refresh',
})
setIsPTRing(true)
try {
await refetch()
@ -222,9 +227,8 @@ let Feed = ({
logger.error('Failed to refresh posts feed', {message: err})
}
setIsPTRing(false)
}, [refetch, track, setIsPTRing, onHasNew])
}, [refetch, track, setIsPTRing, onHasNew, feedType])
const feedType = feed.split('|')[0]
const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return

View File

@ -46,7 +46,7 @@ export function FeedErrorMessage({
if (
typeof knownError !== 'undefined' &&
knownError !== KnownError.Unknown &&
feedDesc.startsWith('feedgen')
(feedDesc.startsWith('feedgen') || knownError === KnownError.FeedNSFPublic)
) {
return (
<FeedgenErrorMessage
@ -240,6 +240,9 @@ function detectKnownError(
if (typeof error !== 'string') {
error = error.toString()
}
if (error.includes(KnownError.FeedNSFPublic)) {
return KnownError.FeedNSFPublic
}
if (!feedDesc.startsWith('feedgen')) {
return KnownError.Unknown
}
@ -263,8 +266,5 @@ function detectKnownError(
if (error.includes('feed provided an invalid response')) {
return KnownError.FeedgenBadResponse
}
if (error.includes(KnownError.FeedNSFPublic)) {
return KnownError.FeedNSFPublic
}
return KnownError.FeedgenUnknown
}

View File

@ -22,18 +22,24 @@ export function TestCtrls() {
const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation()
const {setShowLoggedOut} = useLoggedOutViewControls()
const onPressSignInAlice = async () => {
await login({
service: 'http://localhost:3000',
identifier: 'alice.test',
password: 'hunter2',
})
await login(
{
service: 'http://localhost:3000',
identifier: 'alice.test',
password: 'hunter2',
},
'LoginForm',
)
}
const onPressSignInBob = async () => {
await login({
service: 'http://localhost:3000',
identifier: 'bob.test',
password: 'hunter2',
})
await login(
{
service: 'http://localhost:3000',
identifier: 'bob.test',
password: 'hunter2',
},
'LoginForm',
)
}
return (
<View style={{position: 'absolute', top: 100, right: 0, zIndex: 100}}>
@ -51,7 +57,7 @@ export function TestCtrls() {
/>
<Pressable
testID="e2eSignOut"
onPress={() => logout()}
onPress={() => logout('Settings')}
accessibilityRole="button"
style={BTN}
/>

View File

@ -298,7 +298,10 @@ let EditableUserAvatar = ({
<Menu.Root>
<Menu.Trigger label={_(msg`Edit avatar`)}>
{({props}) => (
<TouchableOpacity {...props} activeOpacity={0.8}>
<TouchableOpacity
{...props}
activeOpacity={0.8}
testID="changeAvatarBtn">
{avatar ? (
<HighPriorityImage
testID="userAvatarImage"

View File

@ -84,7 +84,10 @@ export function UserBanner({
<Menu.Root>
<Menu.Trigger label={_(msg`Edit avatar`)}>
{({props}) => (
<TouchableOpacity {...props} activeOpacity={0.8}>
<TouchableOpacity
{...props}
activeOpacity={0.8}
testID="changeBannerBtn">
{banner ? (
<Image
testID="userBannerImage"

View File

@ -237,7 +237,7 @@ const styles = StyleSheet.create({
paddingRight: 12,
borderRadius: 8,
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif',
outline: 0,
border: 0,
},

View File

@ -2,7 +2,7 @@ import React from 'react'
import {Text as RNText, TextProps} from 'react-native'
import {s, lh} from 'lib/styles'
import {useTheme, TypographyVariant} from 'lib/ThemeContext'
import {isIOS} from 'platform/detection'
import {isIOS, isWeb} from 'platform/detection'
import {UITextView} from 'react-native-ui-text-view'
export type CustomTextProps = TextProps & {
@ -13,6 +13,11 @@ export type CustomTextProps = TextProps & {
selectable?: boolean
}
const fontFamilyStyle = {
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif',
}
export function Text({
type = 'md',
children,
@ -39,7 +44,13 @@ export function Text({
return (
<RNText
style={[s.black, typography, lineHeightStyle, style]}
style={[
s.black,
typography,
isWeb && fontFamilyStyle,
lineHeightStyle,
style,
]}
// @ts-ignore web only -esb
dataSet={Object.assign({tooltip: title}, dataSet || {})}
selectable={selectable}

View File

@ -131,7 +131,7 @@ export function ModerationBlockedAccounts({}: Props) {
<Text type="lg" style={[pal.text, styles.emptyText]}>
<Trans>
You have not blocked any accounts yet. To block an account, go
to their profile and selected "Block account" from the menu on
to their profile and select "Block account" from the menu on
their account.
</Trans>
</Text>

View File

@ -130,8 +130,8 @@ export function ModerationMutedAccounts({}: Props) {
<Text type="lg" style={[pal.text, styles.emptyText]}>
<Trans>
You have not muted any accounts yet. To mute an account, go to
their profile and selected "Mute account" from the menu on
their account.
their profile and select "Mute account" from the menu on their
account.
</Trans>
</Text>
</View>

View File

@ -100,7 +100,9 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
{isCurrentAccount ? (
<TouchableOpacity
testID="signOutBtn"
onPress={logout}
onPress={() => {
logout('Settings')
}}
accessibilityRole="button"
accessibilityLabel={_(msg`Sign out`)}
accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}>
@ -129,7 +131,9 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
testID={`switchToAccountBtn-${account.handle}`}
key={account.did}
onPress={
isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account)
isSwitchingAccounts
? undefined
: () => onPressSwitchAccount(account, 'Settings')
}
accessibilityRole="button"
accessibilityLabel={_(msg`Switch to ${account.handle}`)}
@ -711,7 +715,7 @@ export function SettingsScreen({}: Props) {
accessibilityRole="button"
accessibilityLabel={_(msg`Change handle`)}
accessibilityHint={_(
msg`Opens modal for choosing or creating a new Bluesky username`,
msg`Opens modal for choosing a new Bluesky handle`,
)}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
@ -772,7 +776,7 @@ export function SettingsScreen({}: Props) {
accessibilityRole="button"
accessibilityLabel={_(msg`Export my data`)}
accessibilityHint={_(
msg`Opens modal for downloading Bluesky account data (repository)`,
msg`Opens modal for downloading your Bluesky account data (repository)`,
)}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon

View File

@ -48,7 +48,7 @@
scrollbar-gutter: stable both-edges;
}
html, body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif;
}
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
@ -145,7 +145,7 @@
/* ProseMirror */
.ProseMirror {
font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif;
min-height: 140px;
}
.ProseMirror-dark {

View File

@ -34,10 +34,10 @@
jsonpointer "^5.0.0"
leven "^3.1.0"
"@atproto/api@^0.12.1":
version "0.12.1"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.1.tgz#3340cbbd6a51a8c2f3248dae55a01415ab71084e"
integrity sha512-Grigs9neuQxytXr2yHq/IfNlgXQVptWDO9KTQr5FDmgMY4Zly2X7Sa99u9c1CW9auwUTbcd+yRFBNEtbA3n3qg==
"@atproto/api@^0.12.2":
version "0.12.2"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.2.tgz#5df6d4f60dea0395c84fdebd9e81a7e853edf130"
integrity sha512-UVzCiDZH2j0wrr/O8nb1edD5cYLVqB5iujueXUCbHS3rAwIxgmyLtA3Hzm2QYsGPo/+xsIg1fNvpq9rNT6KWUA==
dependencies:
"@atproto/common-web" "^0.3.0"
"@atproto/lexicon" "^0.4.0"