Merge branch 'main' into feat/selectable-text

# Conflicts:
#	src/view/com/util/Link.tsx
This commit is contained in:
Hailey
2024-01-12 23:41:10 -08:00
58 changed files with 19550 additions and 4517 deletions
+2
View File
@@ -49,6 +49,7 @@
--text: white;
--background: black;
--backgroundLight: #26272D;
color-scheme: dark;
}
@media (prefers-color-scheme: light) {
html.colorMode--system {
@@ -62,6 +63,7 @@
--text: white;
--background: black;
--backgroundLight: #26272D;
color-scheme: dark;
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
/** @type {import('@lingui/conf').LinguiConfig} */
module.exports = {
locales: ['en', 'hi', 'ja', 'fr', 'de', 'es', 'ko', 'es', 'pt-BR', 'uk', 'id'],
locales: ['en', 'de', 'es', 'fr', 'hi', 'id', 'ja', 'ko', 'pt-BR', 'uk'],
catalogs: [
{
path: '<rootDir>/src/locale/locales/{locale}/messages',
+21 -21
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.63.0",
"version": "1.64.0",
"private": true,
"engines": {
"node": ">=18"
@@ -12,7 +12,7 @@
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
"build-web": "expo export:web && node ./scripts/post-web-build.js && cp --verbose ./web-build/static/js/*.* ./bskyweb/static/js/",
"build-web": "expo export:web && node ./scripts/post-web-build.js && cp -v ./web-build/static/js/*.* ./bskyweb/static/js/",
"build-all": "yarn intl:build && eas build --platform all",
"start": "expo start --dev-client",
"start:prod": "expo start --dev-client --no-dev --minify",
@@ -60,7 +60,7 @@
"@react-native-community/datetimepicker": "7.6.1",
"@react-native-masked-view/masked-view": "0.3.0",
"@react-native-menu/menu": "^0.8.0",
"@react-native-picker/picker": "2.5.1",
"@react-native-picker/picker": "2.6.1",
"@react-navigation/bottom-tabs": "^6.5.7",
"@react-navigation/drawer": "^6.6.2",
"@react-navigation/native": "^6.1.6",
@@ -94,25 +94,26 @@
"email-validator": "^2.0.4",
"emoji-mart": "^5.5.2",
"eventemitter3": "^5.0.1",
"expo": "^50.0.0-preview.7",
"expo-application": "~5.8.1",
"expo": "^50.0.0-preview.10",
"expo-application": "~5.8.2",
"expo-build-properties": "^0.11.0",
"expo-camera": "~14.0.1",
"expo-constants": "~15.4.2",
"expo-dev-client": "~3.3.4",
"expo-device": "~5.9.1",
"expo-image": "~1.10.1",
"expo-constants": "~15.4.3",
"expo-dev-client": "~3.3.5",
"expo-device": "~5.9.2",
"expo-image": "~1.10.3",
"expo-image-manipulator": "^11.8.0",
"expo-image-picker": "~14.7.1",
"expo-localization": "~14.8.1",
"expo-localization": "~14.8.2",
"expo-media-library": "~15.9.1",
"expo-notifications": "~0.27.2",
"expo-notifications": "~0.27.3",
"expo-sharing": "^11.10.0",
"expo-splash-screen": "~0.26.1",
"expo-splash-screen": "~0.26.2",
"expo-status-bar": "~1.11.1",
"expo-system-ui": "~2.9.2",
"expo-system-ui": "~2.9.3",
"expo-task-manager": "~11.7.0",
"expo-updates": "~0.24.5",
"expo-updates": "~0.24.7",
"expo-web-browser": "~12.8.1",
"fast-text-encoding": "^1.0.6",
"history": "^5.3.0",
"js-sha256": "^0.9.0",
@@ -140,7 +141,7 @@
"react-avatar-editor": "^13.0.0",
"react-circular-progressbar": "^2.1.0",
"react-dom": "^18.2.0",
"react-native": "0.73.1",
"react-native": "0.73.2",
"react-native-appstate-hook": "^1.0.6",
"react-native-drawer-layout": "^4.0.0-alpha.3",
"react-native-fs": "^2.20.0",
@@ -148,24 +149,23 @@
"react-native-get-random-values": "~1.8.0",
"react-native-haptic-feedback": "^1.14.0",
"react-native-image-crop-picker": "^0.38.1",
"react-native-inappbrowser-reborn": "^3.6.3",
"react-native-ios-context-menu": "^1.15.3",
"react-native-linear-gradient": "^2.6.2",
"react-native-pager-view": "6.2.2",
"react-native-pager-view": "6.2.3",
"react-native-picker-select": "^8.1.0",
"react-native-progress": "bluesky-social/react-native-progress",
"react-native-reanimated": "^3.6.0",
"react-native-root-siblings": "^4.1.1",
"react-native-safe-area-context": "4.7.4",
"react-native-screens": "~3.27.0",
"react-native-svg": "14.0.0",
"react-native-safe-area-context": "4.8.2",
"react-native-screens": "~3.29.0",
"react-native-svg": "14.1.0",
"react-native-url-polyfill": "^1.3.0",
"react-native-uuid": "^2.0.1",
"react-native-version-number": "^0.3.6",
"react-native-web": "~0.19.6",
"react-native-web-linear-gradient": "^1.1.2",
"react-native-web-webview": "^1.0.2",
"react-native-webview": "^13.6.3",
"react-native-webview": "13.6.4",
"react-responsive": "^9.0.2",
"rn-fetch-blob": "^0.12.0",
"sentry-expo": "~7.0.1",
-12
View File
@@ -1,12 +0,0 @@
diff --git a/node_modules/babel-preset-fbjs/plugins/inline-requires.js b/node_modules/babel-preset-fbjs/plugins/inline-requires.js
index b11fc83..e18661a 100644
--- a/node_modules/babel-preset-fbjs/plugins/inline-requires.js
+++ b/node_modules/babel-preset-fbjs/plugins/inline-requires.js
@@ -256,6 +256,7 @@ function getInlineableModule(path, state) {
return moduleName == null ||
state.ignoredRequires.has(moduleName) ||
+ moduleName.startsWith('@babel/runtime/') ||
isRequireInScope
? null
: { moduleName, requireFnName: fnName };
@@ -1,5 +1,5 @@
diff --git a/node_modules/metro-transform-worker/src/index.js b/node_modules/metro-transform-worker/src/index.js
index cae11e7..42f251b 100644
index 9f2e3d2..5222c8e 100644
--- a/node_modules/metro-transform-worker/src/index.js
+++ b/node_modules/metro-transform-worker/src/index.js
@@ -189,6 +189,10 @@ async function transformJS(file, { config, options, projectRoot }) {
-69
View File
@@ -1,69 +0,0 @@
/**
* This is a temporary off-spec search endpoint
* TODO removeme when we land this in proto!
*/
import {AppBskyFeedPost} from '@atproto/api'
const PROFILES_ENDPOINT = 'https://search.bsky.social/search/profiles'
const POSTS_ENDPOINT = 'https://search.bsky.social/search/posts'
export interface ProfileSearchItem {
$type: string
avatar: {
cid: string
mimeType: string
}
banner: {
cid: string
mimeType: string
}
description: string | undefined
displayName: string | undefined
did: string
}
export interface PostSearchItem {
tid: string
cid: string
user: {
did: string
handle: string
}
post: AppBskyFeedPost.Record
}
export async function searchProfiles(
query: string,
): Promise<ProfileSearchItem[]> {
return await doFetch<ProfileSearchItem[]>(PROFILES_ENDPOINT, query)
}
export async function searchPosts(query: string): Promise<PostSearchItem[]> {
return await doFetch<PostSearchItem[]>(POSTS_ENDPOINT, query)
}
async function doFetch<T>(endpoint: string, query: string): Promise<T> {
const controller = new AbortController()
const to = setTimeout(() => controller.abort(), 15e3)
const uri = new URL(endpoint)
uri.searchParams.set('q', query)
const res = await fetch(String(uri), {
method: 'get',
headers: {
accept: 'application/json',
},
signal: controller.signal,
})
const resHeaders: Record<string, string> = {}
res.headers.forEach((value: string, key: string) => {
resHeaders[key] = value
})
let resBody = await res.json()
clearTimeout(to)
return resBody as unknown as T
}
+30 -2
View File
@@ -1,11 +1,39 @@
import {QueryClient} from '@tanstack/react-query'
import {AppState, AppStateStatus} from 'react-native'
import {QueryClient, focusManager} from '@tanstack/react-query'
import {isNative} from '#/platform/detection'
focusManager.setEventListener(onFocus => {
if (isNative) {
const subscription = AppState.addEventListener(
'change',
(status: AppStateStatus) => {
focusManager.setFocused(status === 'active')
},
)
return () => subscription.remove()
} else if (typeof window !== 'undefined' && window.addEventListener) {
// these handlers are a bit redundant but focus catches when the browser window
// is blurred/focused while visibilitychange seems to only handle when the
// window minimizes (both of them catch tab changes)
// there's no harm to redundant fires because refetchOnWindowFocus is only
// used with queries that employ stale data times
const handler = () => onFocus()
window.addEventListener('focus', handler, false)
window.addEventListener('visibilitychange', handler, false)
return () => {
window.removeEventListener('visibilitychange', handler)
window.removeEventListener('focus', handler)
}
}
})
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
// NOTE
// refetchOnWindowFocus breaks some UIs (like feeds)
// so we NEVER want to enable this
// so we only selectively want to enable this
// -prf
refetchOnWindowFocus: false,
// Structural sharing between responses makes it impossible to rely on
+29
View File
@@ -0,0 +1,29 @@
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
import {linkRequiresWarning} from './url-helpers'
export function richTextToString(rt: RichText): string {
const {text, facets} = rt
if (!facets?.length) {
return text
}
let result = ''
for (const segment of rt.segments()) {
const link = segment.link
if (link && AppBskyRichtextFacet.validateLink(link).success) {
const href = link.uri
const text = segment.text
const requiresWarning = linkRequiresWarning(href, text)
result += !requiresWarning ? href : `[${text}](${href})`
} else {
result += segment.text
}
}
return result
}
+10 -2
View File
@@ -22,6 +22,14 @@ export function code3ToCode2(lang: string): string {
return lang
}
export function code3ToCode2Strict(lang: string): string | undefined {
if (lang.length === 3) {
return LANGUAGES_MAP_CODE3[lang]?.code2
}
return undefined
}
export function codeToLanguageName(lang: string): string {
const lang2 = code3ToCode2(lang)
return LANGUAGES_MAP_CODE2[lang2]?.name || lang
@@ -113,14 +121,14 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
// DISABLED until this translation is fixed -prf
// case 'de':
// return AppLanguage.de
case 'id':
return AppLanguage.id
case 'es':
return AppLanguage.es
case 'fr':
return AppLanguage.fr
case 'hi':
return AppLanguage.hi
case 'id':
return AppLanguage.id
case 'ja':
return AppLanguage.ja
case 'ko':
+4 -4
View File
@@ -27,10 +27,6 @@ export async function dynamicActivate(locale: AppLanguage) {
// i18n.loadAndActivate({locale, messages: messagesDe})
// break
// }
case AppLanguage.id: {
i18n.loadAndActivate({locale, messages: messagesId})
break
}
case AppLanguage.es: {
i18n.loadAndActivate({locale, messages: messagesEs})
break
@@ -43,6 +39,10 @@ export async function dynamicActivate(locale: AppLanguage) {
i18n.loadAndActivate({locale, messages: messagesHi})
break
}
case AppLanguage.id: {
i18n.loadAndActivate({locale, messages: messagesId})
break
}
case AppLanguage.ja: {
i18n.loadAndActivate({locale, messages: messagesJa})
break
+8 -5
View File
@@ -17,10 +17,6 @@ export async function dynamicActivate(locale: AppLanguage) {
// mod = await import(`./locales/de/messages`)
// break
// }
case AppLanguage.id: {
mod = await import(`./locales/id/messages`)
break
}
case AppLanguage.es: {
mod = await import(`./locales/es/messages`)
break
@@ -33,6 +29,10 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/hi/messages`)
break
}
case AppLanguage.id: {
mod = await import(`./locales/id/messages`)
break
}
case AppLanguage.ja: {
mod = await import(`./locales/ja/messages`)
break
@@ -62,6 +62,9 @@ export async function dynamicActivate(locale: AppLanguage) {
export async function useLocaleLanguage() {
const {appLanguage} = useLanguagePrefs()
useEffect(() => {
dynamicActivate(sanitizeAppLanguageSetting(appLanguage))
const sanitizedLanguage = sanitizeAppLanguageSetting(appLanguage)
document.documentElement.lang = sanitizedLanguage
dynamicActivate(sanitizedLanguage)
}, [appLanguage])
}
+2 -2
View File
@@ -8,10 +8,10 @@ export enum AppLanguage {
en = 'en',
// DISABLED until this translation is fixed -prf
// de = 'de',
id = 'id',
es = 'es',
fr = 'fr',
hi = 'hi',
id = 'id',
ja = 'ja',
ko = 'ko',
pt_BR = 'pt-BR',
@@ -27,10 +27,10 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
{code2: AppLanguage.en, name: 'English'},
// DISABLED until this translation is fixed -prf
// {code2: AppLanguage.de, name: 'Deutsch'},
{code2: AppLanguage.id, name: 'Indonesian'},
{code2: AppLanguage.es, name: 'Español'},
{code2: AppLanguage.fr, name: 'Français'},
{code2: AppLanguage.hi, name: 'हिंदी'},
{code2: AppLanguage.id, name: 'Bahasa Indonesia'},
{code2: AppLanguage.ja, name: '日本語'},
{code2: AppLanguage.ko, name: '한국어'},
{code2: AppLanguage.pt_BR, name: 'Português (BR)'},
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
+6
View File
@@ -187,6 +187,11 @@ export interface EmbedConsentModal {
onAccept: () => void
}
export interface InAppBrowserConsentModal {
name: 'in-app-browser-consent'
href: string
}
export type Modal =
// Account
| AddAppPasswordModal
@@ -231,6 +236,7 @@ export type Modal =
| ConfirmModal
| LinkWarningModal
| EmbedConsentModal
| InAppBrowserConsentModal
const ModalContext = React.createContext<{
isModalActive: boolean
+2
View File
@@ -53,6 +53,7 @@ export const schema = z.object({
step: z.string(),
}),
hiddenPosts: z.array(z.string()).optional(), // should move to server
useInAppBrowser: z.boolean().optional(),
})
export type Schema = z.infer<typeof schema>
@@ -84,4 +85,5 @@ export const defaults: Schema = {
step: 'Home',
},
hiddenPosts: [],
useInAppBrowser: undefined,
}
+79
View File
@@ -0,0 +1,79 @@
import React from 'react'
import * as persisted from '#/state/persisted'
import {Linking} from 'react-native'
import * as WebBrowser from 'expo-web-browser'
import {isNative} from '#/platform/detection'
import {useModalControls} from '../modals'
type StateContext = persisted.Schema['useInAppBrowser']
type SetContext = (v: persisted.Schema['useInAppBrowser']) => void
const stateContext = React.createContext<StateContext>(
persisted.defaults.useInAppBrowser,
)
const setContext = React.createContext<SetContext>(
(_: persisted.Schema['useInAppBrowser']) => {},
)
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = React.useState(persisted.get('useInAppBrowser'))
const setStateWrapped = React.useCallback(
(inAppBrowser: persisted.Schema['useInAppBrowser']) => {
setState(inAppBrowser)
persisted.write('useInAppBrowser', inAppBrowser)
},
[setState],
)
React.useEffect(() => {
return persisted.onUpdate(() => {
setState(persisted.get('useInAppBrowser'))
})
}, [setStateWrapped])
return (
<stateContext.Provider value={state}>
<setContext.Provider value={setStateWrapped}>
{children}
</setContext.Provider>
</stateContext.Provider>
)
}
export function useInAppBrowser() {
return React.useContext(stateContext)
}
export function useSetInAppBrowser() {
return React.useContext(setContext)
}
export function useOpenLink() {
const {openModal} = useModalControls()
const enabled = useInAppBrowser()
const openLink = React.useCallback(
(url: string, override?: boolean) => {
if (isNative && !url.startsWith('mailto:')) {
if (override === undefined && enabled === undefined) {
openModal({
name: 'in-app-browser-consent',
href: url,
})
return
} else if (override ?? enabled) {
WebBrowser.openBrowserAsync(url, {
presentationStyle:
WebBrowser.WebBrowserPresentationStyle.FULL_SCREEN,
})
return
}
}
Linking.openURL(url)
},
[enabled, openModal],
)
return openLink
}
+4 -1
View File
@@ -3,6 +3,7 @@ import {Provider as LanguagesProvider} from './languages'
import {Provider as AltTextRequiredProvider} from '../preferences/alt-text-required'
import {Provider as HiddenPostsProvider} from '../preferences/hidden-posts'
import {Provider as ExternalEmbedsProvider} from './external-embeds-prefs'
import {Provider as InAppBrowserProvider} from './in-app-browser'
export {useLanguagePrefs, useLanguagePrefsApi} from './languages'
export {
@@ -20,7 +21,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
<LanguagesProvider>
<AltTextRequiredProvider>
<ExternalEmbedsProvider>
<HiddenPostsProvider>{children}</HiddenPostsProvider>
<HiddenPostsProvider>
<InAppBrowserProvider>{children}</InAppBrowserProvider>
</HiddenPostsProvider>
</ExternalEmbedsProvider>
</AltTextRequiredProvider>
</LanguagesProvider>
+1
View File
@@ -35,4 +35,5 @@ export interface CachedFeedPage {
usableInFeed: boolean
syncedAt: Date
data: FeedPage | undefined
unreadCount: number
}
+28 -3
View File
@@ -25,7 +25,10 @@ type StateContext = string
interface ApiContext {
markAllRead: () => Promise<void>
checkUnread: (opts?: {invalidate?: boolean}) => Promise<void>
checkUnread: (opts?: {
invalidate?: boolean
isPoll?: boolean
}) => Promise<void>
getCachedUnreadPage: () => FeedPage | undefined
}
@@ -50,6 +53,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
usableInFeed: false,
syncedAt: new Date(),
data: undefined,
unreadCount: 0,
})
// periodic sync
@@ -58,7 +62,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
return
}
checkUnreadRef.current() // fire on init
const interval = setInterval(checkUnreadRef.current, UPDATE_INTERVAL)
const interval = setInterval(
() => checkUnreadRef.current?.({isPoll: true}),
UPDATE_INTERVAL,
)
return () => clearInterval(interval)
}, [hasSession])
@@ -69,6 +76,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
usableInFeed: false,
syncedAt: new Date(),
data: undefined,
unreadCount:
data.event === '30+'
? 30
: data.event === ''
? 0
: parseInt(data.event, 10) || 1,
}
setNumUnread(data.event)
}
@@ -95,13 +108,24 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}
},
async checkUnread({invalidate}: {invalidate?: boolean} = {}) {
async checkUnread({
invalidate,
isPoll,
}: {invalidate?: boolean; isPoll?: boolean} = {}) {
try {
if (!getAgent().session) return
if (AppState.currentState !== 'active') {
return
}
// reduce polling if unread count is set
if (isPoll && cacheRef.current?.unreadCount !== 0) {
// if hit 30+ then don't poll, otherwise reduce polling by 50%
if (cacheRef.current?.unreadCount >= 30 || Math.random() >= 0.5) {
return
}
}
// count
const page = await fetchPage({
cursor: undefined,
@@ -133,6 +157,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
usableInFeed: !!invalidate, // will be used immediately
data: page,
syncedAt: !lastIndexed || now > lastIndexed ? now : lastIndexed,
unreadCount,
}
// update & broadcast
+1 -1
View File
@@ -31,7 +31,7 @@ export function usePreferencesQuery() {
return useQuery({
staleTime: STALE.SECONDS.FIFTEEN,
structuralSharing: true,
refetchInterval: STALE.SECONDS.FIFTEEN,
refetchOnWindowFocus: true,
queryKey: preferencesQueryKey,
queryFn: async () => {
const agent = getAgent()
+1 -1
View File
@@ -35,7 +35,7 @@ export function useProfileQuery({did}: {did: string | undefined}) {
// if you remove it, the UI infinite-loops
// -prf
staleTime: isCurrentAccount ? STALE.SECONDS.THIRTY : STALE.MINUTES.FIVE,
refetchInterval: STALE.MINUTES.FIVE,
refetchOnWindowFocus: true,
queryKey: RQKEY(did || ''),
queryFn: async () => {
const res = await getAgent().getProfile({actor: did || ''})
+45 -23
View File
@@ -136,9 +136,11 @@ function createPersistSessionHandler(
event,
did: refreshedAccount.did,
handle: refreshedAccount.handle,
service: refreshedAccount.service,
})
if (expired) {
logger.warn(`session: expired`)
emitSessionDropped()
}
@@ -191,11 +193,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
)
const clearCurrentAccount = React.useCallback(() => {
logger.debug(
`session: clear current account`,
{},
logger.DebugContext.session,
)
logger.warn(`session: clear current account`)
__globalAgent = PUBLIC_BSKY_AGENT
queryClient.clear()
setStateAndPersist(s => ({
@@ -225,6 +223,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
throw new Error(`session: createAccount failed to establish a session`)
}
/*dont await*/ agent.upsertProfile(_existing => {
return {
displayName: handle,
}
})
const account: SessionAccount = {
service: agent.service.toString(),
did: agent.session.did,
@@ -320,8 +324,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
)
const logout = React.useCallback<ApiContext['logout']>(async () => {
logger.info(`session: logout`)
clearCurrentAccount()
logger.debug(`session: logout`, {}, logger.DebugContext.session)
setStateAndPersist(s => {
return {
...s,
@@ -549,30 +553,36 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
return persisted.onUpdate(() => {
const session = persisted.get('session')
logger.debug(`session: onUpdate`, {}, logger.DebugContext.session)
logger.info(`session: persisted onUpdate`, {})
if (session.currentAccount) {
if (session.currentAccount && session.currentAccount.refreshJwt) {
if (session.currentAccount?.did !== state.currentAccount?.did) {
logger.debug(
`session: switching account`,
{
from: {
did: state.currentAccount?.did,
handle: state.currentAccount?.handle,
},
to: {
did: session.currentAccount.did,
handle: session.currentAccount.handle,
},
logger.info(`session: persisted onUpdate, switching accounts`, {
from: {
did: state.currentAccount?.did,
handle: state.currentAccount?.handle,
},
logger.DebugContext.session,
)
to: {
did: session.currentAccount.did,
handle: session.currentAccount.handle,
},
})
initSession(session.currentAccount)
} else {
logger.info(`session: persisted onUpdate, updating session`, {})
/*
* Use updated session in this tab's agent. Do not call
* upsertAccount, since that will only persist the session that's
* already persisted, and we'll get a loop between tabs.
*/
// @ts-ignore we checked for `refreshJwt` above
__globalAgent.session = session.currentAccount
}
} else if (!session.currentAccount && state.currentAccount) {
logger.debug(
`session: logging out`,
`session: persisted onUpdate, logging out`,
{
did: state.currentAccount?.did,
handle: state.currentAccount?.handle,
@@ -580,10 +590,22 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
logger.DebugContext.session,
)
/*
* No need to do a hard logout here. If we reach this, tokens for this
* account have already been cleared either by an `expired` event
* handled by `persistSession` (which nukes this accounts tokens only),
* or by a `logout` call which nukes all accounts tokens)
*/
clearCurrentAccount()
}
setState(s => ({
...s,
accounts: session.accounts,
currentAccount: session.currentAccount,
}))
})
}, [state, clearCurrentAccount, initSession])
}, [state, setState, clearCurrentAccount, initSession])
const stateContext = React.useMemo(
() => ({
+2
View File
@@ -45,6 +45,7 @@ import {Gallery} from './photos/Gallery'
import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
import {LabelsBtn} from './labels/LabelsBtn'
import {SelectLangBtn} from './select-language/SelectLangBtn'
import {SuggestedLanguage} from './select-language/SuggestedLanguage'
import {insertMentionAt} from 'lib/strings/mention-manip'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -454,6 +455,7 @@ export const ComposePost = observer(function ComposePost({
))}
</View>
) : null}
<SuggestedLanguage text={richtext.text} />
<View style={[pal.border, styles.bottomBar]}>
{canSelectImages ? (
<>
@@ -0,0 +1,101 @@
import React, {useEffect, useState} from 'react'
import {StyleSheet, View} from 'react-native'
import lande from 'lande'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Text} from '../../util/text/Text'
import {Button} from '../../util/forms/Button'
import {code3ToCode2Strict, codeToLanguageName} from '#/locale/helpers'
import {
toPostLanguages,
useLanguagePrefs,
useLanguagePrefsApi,
} from '#/state/preferences/languages'
import {usePalette} from '#/lib/hooks/usePalette'
import {s} from '#/lib/styles'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
// fallbacks for safari
const onIdle = globalThis.requestIdleCallback || (cb => setTimeout(cb, 1))
const cancelIdle = globalThis.cancelIdleCallback || clearTimeout
export function SuggestedLanguage({text}: {text: string}) {
const [suggestedLanguage, setSuggestedLanguage] = useState<string>()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
const pal = usePalette('default')
const {_} = useLingui()
useEffect(() => {
const textTrimmed = text.trim()
// Don't run the language model on small posts, the results are likely
// to be inaccurate anyway.
if (textTrimmed.length < 40) {
setSuggestedLanguage(undefined)
return
}
const idle = onIdle(() => {
// Only select languages that have a high confidence and convert to code2
const result = lande(textTrimmed).filter(
([lang, value]) => value >= 0.97 && code3ToCode2Strict(lang),
)
setSuggestedLanguage(
result.length > 0 ? code3ToCode2Strict(result[0][0]) : undefined,
)
})
return () => cancelIdle(idle)
}, [text])
return suggestedLanguage &&
!toPostLanguages(langPrefs.postLanguage).includes(suggestedLanguage) ? (
<View style={[pal.border, styles.infoBar]}>
<FontAwesomeIcon
icon="language"
style={pal.text as FontAwesomeIconStyle}
size={24}
/>
<Text style={[pal.text, s.flex1]}>
<Trans>
Are you writing in{' '}
<Text type="sm-bold" style={pal.text}>
{codeToLanguageName(suggestedLanguage)}
</Text>
?
</Trans>
</Text>
<Button
type="default"
onPress={() => setLangPrefs.setPostLanguage(suggestedLanguage)}
accessibilityLabel={_(
msg`Change post language to ${codeToLanguageName(suggestedLanguage)}`,
)}
accessibilityHint="">
<Text type="button" style={[pal.link, s.fw600]}>
<Trans>Yes</Trans>
</Text>
</Button>
</View>
) : null
}
const styles = StyleSheet.create({
infoBar: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
borderWidth: 1,
borderRadius: 6,
paddingHorizontal: 16,
paddingVertical: 12,
marginHorizontal: 10,
marginBottom: 10,
},
})
+1
View File
@@ -174,6 +174,7 @@ export function FeedPage({
feed={feed}
feedParams={feedParams}
pollInterval={POLL_FREQ}
disablePoll={hasNew}
scrollElRef={scrollElRef}
onScrolledDownChange={setIsScrolledDown}
onHasNew={setHasNew}
+1 -1
View File
@@ -38,7 +38,7 @@ export function Component(props: ReportComponentProps) {
? 'com.atproto.repo.strongRef'
: 'com.atproto.admin.defs#repoRef'
await getAgent().createModerationReport({
reasonType: ComAtprotoModerationDefs.REASONOTHER,
reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
subject: {
$type,
...props,
+102
View File
@@ -0,0 +1,102 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {s} from 'lib/styles'
import {Text} from '../util/text/Text'
import {Button} from '../util/forms/Button'
import {ScrollView} from './util'
import {usePalette} from 'lib/hooks/usePalette'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {
useOpenLink,
useSetInAppBrowser,
} from '#/state/preferences/in-app-browser'
export const snapPoints = [350]
export function Component({href}: {href: string}) {
const pal = usePalette('default')
const {closeModal} = useModalControls()
const {_} = useLingui()
const setInAppBrowser = useSetInAppBrowser()
const openLink = useOpenLink()
const onUseIAB = React.useCallback(() => {
setInAppBrowser(true)
closeModal()
openLink(href, true)
}, [closeModal, setInAppBrowser, href, openLink])
const onUseLinking = React.useCallback(() => {
setInAppBrowser(false)
closeModal()
openLink(href, false)
}, [closeModal, setInAppBrowser, href, openLink])
return (
<ScrollView
testID="inAppBrowserConsentModal"
style={[s.flex1, pal.view, {paddingHorizontal: 20, paddingTop: 10}]}>
<Text style={[pal.text, styles.title]}>
<Trans>How should we open this link?</Trans>
</Text>
<Text style={pal.text}>
<Trans>
Your choice will be saved, but can be changed later in settings.
</Trans>
</Text>
<View style={[styles.btnContainer]}>
<Button
testID="confirmBtn"
type="inverted"
onPress={onUseIAB}
accessibilityLabel={_(msg`Use in-app browser`)}
accessibilityHint=""
label={_(msg`Use in-app browser`)}
labelContainerStyle={{justifyContent: 'center', padding: 8}}
labelStyle={[s.f18]}
/>
<Button
testID="confirmBtn"
type="inverted"
onPress={onUseLinking}
accessibilityLabel={_(msg`Use my default browser`)}
accessibilityHint=""
label={_(msg`Use my default browser`)}
labelContainerStyle={{justifyContent: 'center', padding: 8}}
labelStyle={[s.f18]}
/>
<Button
testID="cancelBtn"
type="default"
onPress={() => {
closeModal()
}}
accessibilityLabel={_(msg`Cancel`)}
accessibilityHint=""
label="Cancel"
labelContainerStyle={{justifyContent: 'center', padding: 8}}
labelStyle={[s.f18]}
/>
</View>
</ScrollView>
)
}
const styles = StyleSheet.create({
title: {
textAlign: 'center',
fontWeight: 'bold',
fontSize: 24,
marginBottom: 12,
},
btnContainer: {
marginTop: 20,
flexDirection: 'column',
justifyContent: 'center',
rowGap: 10,
},
})
+4 -2
View File
@@ -1,5 +1,5 @@
import React from 'react'
import {Linking, SafeAreaView, StyleSheet, View} from 'react-native'
import {SafeAreaView, StyleSheet, View} from 'react-native'
import {ScrollView} from './util'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from '../util/text/Text'
@@ -12,6 +12,7 @@ import {isPossiblyAUrl, splitApexDomain} from 'lib/strings/url-helpers'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useOpenLink} from '#/state/preferences/in-app-browser'
export const snapPoints = ['50%']
@@ -21,10 +22,11 @@ export function Component({text, href}: {text: string; href: string}) {
const {isMobile} = useWebMediaQueries()
const {_} = useLingui()
const potentiallyMisleading = isPossiblyAUrl(text)
const openLink = useOpenLink()
const onPressVisit = () => {
closeModal()
Linking.openURL(href)
openLink(href)
}
return (
+4
View File
@@ -39,6 +39,7 @@ import * as ChangeEmailModal from './ChangeEmail'
import * as SwitchAccountModal from './SwitchAccount'
import * as LinkWarningModal from './LinkWarning'
import * as EmbedConsentModal from './EmbedConsent'
import * as InAppBrowserConsentModal from './InAppBrowserConsent'
const DEFAULT_SNAPPOINTS = ['90%']
const HANDLE_HEIGHT = 24
@@ -180,6 +181,9 @@ export function ModalsContainer() {
} else if (activeModal?.name === 'embed-consent') {
snapPoints = EmbedConsentModal.snapPoints
element = <EmbedConsentModal.Component {...activeModal} />
} else if (activeModal?.name === 'in-app-browser-consent') {
snapPoints = InAppBrowserConsentModal.snapPoints
element = <InAppBrowserConsentModal.Component {...activeModal} />
} else {
return null
}
@@ -336,6 +336,7 @@ let PostThreadItemLoaded = ({
postCid={post.cid}
postUri={post.uri}
record={record}
richText={richText}
showAppealLabelItem={
post.author.did === currentAccount?.did && isModeratedPost
}
@@ -440,6 +441,7 @@ let PostThreadItemLoaded = ({
big
post={post}
record={record}
richText={richText}
onPressReply={onPressReply}
/>
</View>
@@ -588,6 +590,7 @@ let PostThreadItemLoaded = ({
<PostCtrls
post={post}
record={record}
richText={richText}
onPressReply={onPressReply}
/>
</View>
+6 -1
View File
@@ -213,7 +213,12 @@ function PostInner({
</ContentHider>
) : null}
</ContentHider>
<PostCtrls post={post} record={record} onPressReply={onPressReply} />
<PostCtrls
post={post}
record={record}
richText={richText}
onPressReply={onPressReply}
/>
</View>
</View>
</Link>
+9 -5
View File
@@ -36,7 +36,8 @@ const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const ERROR_ITEM = {_reactKey: '__error__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
const REFRESH_AFTER = STALE.HOURS.ONE
// DISABLED need to check if this is causing random feed refreshes -prf
// const REFRESH_AFTER = STALE.HOURS.ONE
const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY
let Feed = ({
@@ -46,6 +47,7 @@ let Feed = ({
style,
enabled,
pollInterval,
disablePoll,
scrollElRef,
onScrolledDownChange,
onHasNew,
@@ -63,6 +65,7 @@ let Feed = ({
style?: StyleProp<ViewStyle>
enabled?: boolean
pollInterval?: number
disablePoll?: boolean
scrollElRef?: ListRef
onHasNew?: (v: boolean) => void
onScrolledDownChange?: (isScrolledDown: boolean) => void
@@ -107,7 +110,7 @@ let Feed = ({
)
const checkForNew = React.useCallback(async () => {
if (!data?.pages[0] || isFetching || !onHasNew || !enabled) {
if (!data?.pages[0] || isFetching || !onHasNew || !enabled || disablePoll) {
return
}
try {
@@ -117,7 +120,7 @@ let Feed = ({
} catch (e) {
logger.error('Poll latest failed', {feed, error: String(e)})
}
}, [feed, data, isFetching, onHasNew, enabled])
}, [feed, data, isFetching, onHasNew, enabled, disablePoll])
const myDid = currentAccount?.did || ''
const onPostCreated = React.useCallback(() => {
@@ -146,11 +149,12 @@ let Feed = ({
React.useEffect(() => {
if (enabled) {
const timeSinceFirstLoad = Date.now() - lastFetchRef.current
if (timeSinceFirstLoad > REFRESH_AFTER) {
// DISABLED need to check if this is causing random feed refreshes -prf
/*if (timeSinceFirstLoad > REFRESH_AFTER) {
// do a full refresh
scrollElRef?.current?.scrollToOffset({offset: 0, animated: false})
queryClient.resetQueries({queryKey: RQKEY(feed)})
} else if (
} else*/ if (
timeSinceFirstLoad > CHECK_LATEST_AFTER &&
checkForNewRef.current
) {
+1
View File
@@ -304,6 +304,7 @@ let FeedItemInner = ({
<PostCtrls
post={post}
record={record}
richText={richText}
onPressReply={onPressReply}
showAppealLabelItem={
post.author.did === currentAccount?.did && isModeratedPost
+10 -3
View File
@@ -1,6 +1,5 @@
import React, {ComponentProps, memo, useMemo} from 'react'
import {
Linking,
GestureResponderEvent,
Platform,
StyleProp,
@@ -31,6 +30,7 @@ import {sanitizeUrl} from '@braintree/sanitize-url'
import {PressableWithHover} from './PressableWithHover'
import FixedTouchableHighlight from '../pager/FixedTouchableHighlight'
import {useModalControls} from '#/state/modals'
import {useOpenLink} from '#/state/preferences/in-app-browser'
type Event =
| React.MouseEvent<HTMLAnchorElement, MouseEvent>
@@ -65,6 +65,7 @@ export const Link = memo(function Link({
const {closeModal} = useModalControls()
const navigation = useNavigation<NavigationProp>()
const anchorHref = asAnchor ? sanitizeUrl(href) : undefined
const openLink = useOpenLink()
const onPress = React.useCallback(
(e?: Event) => {
@@ -74,11 +75,12 @@ export const Link = memo(function Link({
navigation,
href: sanitizeUrl(href),
navigationAction,
openLink,
e,
})
}
},
[closeModal, navigation, navigationAction, href],
[closeModal, navigation, navigationAction, href, openLink],
)
if (noFeedback) {
@@ -172,6 +174,7 @@ export const TextLink = memo(function TextLink({
const {...props} = useLinkProps({to: sanitizeUrl(href)})
const navigation = useNavigation<NavigationProp>()
const {openModal, closeModal} = useModalControls()
const openLink = useOpenLink()
if (warnOnMismatchingLabel && typeof text !== 'string') {
console.error('Unable to detect mismatching label')
@@ -189,6 +192,7 @@ export const TextLink = memo(function TextLink({
text,
navigationAction,
warnOnMismatchingLabel,
openLink,
})
},
[
@@ -200,6 +204,7 @@ export const TextLink = memo(function TextLink({
text,
navigationAction,
warnOnMismatchingLabel,
openLink,
],
)
const hrefAttrs = useMemo(() => {
@@ -313,6 +318,7 @@ export const onTextLinkPress = ({
text,
navigationAction = 'push',
warnOnMismatchingLabel,
openLink,
}: {
onPress?: (e: GestureResponderEvent) => void
e?: Event
@@ -323,6 +329,7 @@ export const onTextLinkPress = ({
text?: any
navigationAction?: 'push' | 'replace' | 'navigate'
warnOnMismatchingLabel?: boolean
openLink: (href: string) => void
}) => {
const requiresWarning =
warnOnMismatchingLabel &&
@@ -368,7 +375,7 @@ export const onTextLinkPress = ({
if (shouldHandle) {
href = convertBskyAppUrlIfNeeded(href)
if (newTab || href.startsWith('http') || href.startsWith('mailto')) {
Linking.openURL(href)
openLink(href)
} else {
closeModal() // close any active modals
+47 -42
View File
@@ -3,9 +3,11 @@ import {ScrollProvider} from '#/lib/ScrollContext'
import {NativeScrollEvent} from 'react-native'
import {useSetMinimalShellMode, useMinimalShellMode} from '#/state/shell'
import {useShellLayout} from '#/state/shell/shell-layout'
import {isWeb} from 'platform/detection'
import {isNative} from 'platform/detection'
import {useSharedValue, interpolate} from 'react-native-reanimated'
const WEB_HIDE_SHELL_THRESHOLD = 200
function clamp(num: number, min: number, max: number) {
'worklet'
return Math.min(Math.max(num, min), max)
@@ -21,8 +23,10 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
const onBeginDrag = useCallback(
(e: NativeScrollEvent) => {
'worklet'
startDragOffset.value = e.contentOffset.y
startMode.value = mode.value
if (isNative) {
startDragOffset.value = e.contentOffset.y
startMode.value = mode.value
}
},
[mode, startDragOffset, startMode],
)
@@ -30,14 +34,16 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
const onEndDrag = useCallback(
(e: NativeScrollEvent) => {
'worklet'
startDragOffset.value = null
startMode.value = null
if (e.contentOffset.y < headerHeight.value / 2) {
// If we're close to the top, show the shell.
setMode(false)
} else {
// Snap to whichever state is the closest.
setMode(Math.round(mode.value) === 1)
if (isNative) {
startDragOffset.value = null
startMode.value = null
if (e.contentOffset.y < headerHeight.value / 2) {
// If we're close to the top, show the shell.
setMode(false)
} else {
// Snap to whichever state is the closest.
setMode(Math.round(mode.value) === 1)
}
}
},
[startDragOffset, startMode, setMode, mode, headerHeight],
@@ -46,41 +52,40 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
const onScroll = useCallback(
(e: NativeScrollEvent) => {
'worklet'
if (startDragOffset.value === null || startMode.value === null) {
if (mode.value !== 0 && e.contentOffset.y < headerHeight.value) {
// If we're close enough to the top, always show the shell.
// Even if we're not dragging.
setMode(false)
if (isNative) {
if (startDragOffset.value === null || startMode.value === null) {
if (mode.value !== 0 && e.contentOffset.y < headerHeight.value) {
// If we're close enough to the top, always show the shell.
// Even if we're not dragging.
setMode(false)
}
return
}
if (isWeb) {
// On the web, there is no concept of "starting" the drag.
// When we get the first scroll event, we consider that the start.
startDragOffset.value = e.contentOffset.y
startMode.value = mode.value
}
return
}
// The "mode" value is always between 0 and 1.
// Figure out how much to move it based on the current dragged distance.
const dy = e.contentOffset.y - startDragOffset.value
const dProgress = interpolate(
dy,
[-headerHeight.value, headerHeight.value],
[-1, 1],
)
const newValue = clamp(startMode.value + dProgress, 0, 1)
if (newValue !== mode.value) {
// Manually adjust the value. This won't be (and shouldn't be) animated.
mode.value = newValue
}
if (isWeb) {
// On the web, there is no concept of "starting" the drag,
// so we don't have any specific anchor point to calculate the distance.
// Instead, update it continuosly along the way and diff with the last event.
// The "mode" value is always between 0 and 1.
// Figure out how much to move it based on the current dragged distance.
const dy = e.contentOffset.y - startDragOffset.value
const dProgress = interpolate(
dy,
[-headerHeight.value, headerHeight.value],
[-1, 1],
)
const newValue = clamp(startMode.value + dProgress, 0, 1)
if (newValue !== mode.value) {
// Manually adjust the value. This won't be (and shouldn't be) animated.
mode.value = newValue
}
} else {
// On the web, we don't try to follow the drag because we don't know when it ends.
// Instead, show/hide immediately based on whether we're scrolling up or down.
const dy = e.contentOffset.y - (startDragOffset.value ?? 0)
startDragOffset.value = e.contentOffset.y
startMode.value = mode.value
if (dy < 0 || e.contentOffset.y < WEB_HIDE_SHELL_THRESHOLD) {
setMode(false)
} else if (dy > 0) {
setMode(true)
}
}
},
[headerHeight, mode, setMode, startDragOffset, startMode],
+13 -3
View File
@@ -2,7 +2,12 @@ import React, {memo} from 'react'
import {Linking, StyleProp, View, ViewStyle} from 'react-native'
import Clipboard from '@react-native-clipboard/clipboard'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {AppBskyActorDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
import {
AppBskyActorDefs,
AppBskyFeedPost,
AtUri,
RichText as RichTextAPI,
} from '@atproto/api'
import {toShareUrl} from 'lib/strings/url-helpers'
import {useTheme} from 'lib/ThemeContext'
import {shareUrl} from 'lib/sharing'
@@ -24,6 +29,7 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSession} from '#/state/session'
import {isWeb} from '#/platform/detection'
import {richTextToString} from '#/lib/strings/rich-text-helpers'
let PostDropdownBtn = ({
testID,
@@ -31,6 +37,7 @@ let PostDropdownBtn = ({
postCid,
postUri,
record,
richText,
style,
showAppealLabelItem,
}: {
@@ -39,6 +46,7 @@ let PostDropdownBtn = ({
postCid: string
postUri: string
record: AppBskyFeedPost.Record
richText: RichTextAPI
style?: StyleProp<ViewStyle>
showAppealLabelItem?: boolean
}): React.ReactNode => {
@@ -96,9 +104,11 @@ let PostDropdownBtn = ({
}, [rootUri, toggleThreadMute, _])
const onCopyPostText = React.useCallback(() => {
Clipboard.setString(record?.text || '')
const str = richTextToString(richText)
Clipboard.setString(str)
Toast.show(_(msg`Copied to clipboard`))
}, [record, _])
}, [_, richText])
const onOpenTranslate = React.useCallback(() => {
Linking.openURL(translatorUrl)
+8 -1
View File
@@ -6,7 +6,11 @@ import {
View,
ViewStyle,
} from 'react-native'
import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
import {
AppBskyFeedDefs,
AppBskyFeedPost,
RichText as RichTextAPI,
} from '@atproto/api'
import {Text} from '../text/Text'
import {PostDropdownBtn} from '../forms/PostDropdownBtn'
import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons'
@@ -33,6 +37,7 @@ let PostCtrls = ({
big,
post,
record,
richText,
showAppealLabelItem,
style,
onPressReply,
@@ -40,6 +45,7 @@ let PostCtrls = ({
big?: boolean
post: Shadow<AppBskyFeedDefs.PostView>
record: AppBskyFeedPost.Record
richText: RichTextAPI
showAppealLabelItem?: boolean
style?: StyleProp<ViewStyle>
onPressReply: () => void
@@ -212,6 +218,7 @@ let PostCtrls = ({
postCid={post.cid}
postUri={post.uri}
record={record}
richText={richText}
showAppealLabelItem={showAppealLabelItem}
style={styles.ctrlPad}
/>
+16 -16
View File
@@ -6,6 +6,7 @@ import {
AppBskyEmbedImages,
AppBskyEmbedRecordWithMedia,
ModerationUI,
AppBskyEmbedExternal,
} from '@atproto/api'
import {AtUri} from '@atproto/api'
import {PostMeta} from '../PostMeta'
@@ -87,15 +88,19 @@ export function QuoteEmbed({
() => quote.text.trim().length === 0,
[quote.text],
)
const imagesEmbed = React.useMemo(
() =>
quote.embeds?.find(
embed =>
AppBskyEmbedImages.isView(embed) ||
AppBskyEmbedRecordWithMedia.isView(embed),
),
[quote.embeds],
)
const embed = React.useMemo(() => {
const e = quote.embeds?.[0]
if (AppBskyEmbedImages.isView(e) || AppBskyEmbedExternal.isView(e)) {
return e
} else if (
AppBskyEmbedRecordWithMedia.isView(e) &&
(AppBskyEmbedImages.isView(e.media) ||
AppBskyEmbedExternal.isView(e.media))
) {
return e.media
}
}, [quote.embeds])
return (
<Link
style={[styles.container, pal.borderDark, style]}
@@ -113,16 +118,11 @@ export function QuoteEmbed({
<PostAlerts moderation={moderation} style={styles.alert} />
) : null}
{!isEmpty ? (
<Text type="post-text" style={pal.text} numberOfLines={6}>
<Text type="post-text" style={pal.text} numberOfLines={20}>
{quote.text}
</Text>
) : null}
{AppBskyEmbedImages.isView(imagesEmbed) && (
<PostEmbeds embed={imagesEmbed} moderation={{}} />
)}
{AppBskyEmbedRecordWithMedia.isView(imagesEmbed) && (
<PostEmbeds embed={imagesEmbed.media} moderation={{}} />
)}
{embed && <PostEmbeds embed={embed} moderation={{}} />}
</Link>
)
}
+6 -2
View File
@@ -97,6 +97,7 @@ export function FeedsScreen(_props: Props) {
data: preferences,
isLoading: isPreferencesLoading,
error: preferencesError,
refetch: refetchPreferences,
} = usePreferencesQuery()
const {
data: popularFeeds,
@@ -151,9 +152,12 @@ export function FeedsScreen(_props: Props) {
}, [query, debouncedSearch])
const onPullToRefresh = React.useCallback(async () => {
setIsPTR(true)
await refetchPopularFeeds()
await Promise.all([
refetchPreferences().catch(_e => undefined),
refetchPopularFeeds().catch(_e => undefined),
])
setIsPTR(false)
}, [setIsPTR, refetchPopularFeeds])
}, [setIsPTR, refetchPreferences, refetchPopularFeeds])
const onEndReached = React.useCallback(() => {
if (
isPopularFeedsFetching ||
+1
View File
@@ -490,6 +490,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
enabled={isFocused}
feed={feed}
pollInterval={60e3}
disablePoll={hasNew}
scrollElRef={scrollElRef}
onHasNew={setHasNew}
onScrolledDownChange={setIsScrolledDown}
+1
View File
@@ -646,6 +646,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
enabled={isFocused}
feed={feed}
pollInterval={60e3}
disablePoll={hasNew}
scrollElRef={scrollElRef}
onHasNew={setHasNew}
onScrolledDownChange={setIsScrolledDown}
+1
View File
@@ -572,6 +572,7 @@ export function SearchScreen(
accessibilityLabel={_(msg`Search`)}
accessibilityHint=""
autoCorrect={false}
autoComplete="off"
autoCapitalize="none"
/>
{query ? (
+26 -2
View File
@@ -70,6 +70,11 @@ import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
import {
useInAppBrowser,
useSetInAppBrowser,
} from '#/state/preferences/in-app-browser'
import {isNative} from '#/platform/detection'
function SettingsAccountCard({account}: {account: SessionAccount}) {
const pal = usePalette('default')
@@ -146,6 +151,8 @@ export function SettingsScreen({}: Props) {
const setMinimalShellMode = useSetMinimalShellMode()
const requireAltTextEnabled = useRequireAltTextEnabled()
const setRequireAltTextEnabled = useSetRequireAltTextEnabled()
const inAppBrowserPref = useInAppBrowser()
const setUseInAppBrowser = useSetInAppBrowser()
const onboardingDispatch = useOnboardingDispatch()
const navigation = useNavigation<NavigationProp>()
const {isMobile} = useWebMediaQueries()
@@ -313,8 +320,14 @@ export function SettingsScreen({}: Props) {
/>
</>
)}
<Text type="lg" style={pal.text}>
{currentAccount.email || '(no email)'}{' '}
<Text
type="lg"
numberOfLines={1}
style={[
pal.text,
{overflow: 'hidden', marginRight: 4, flex: 1},
]}>
{currentAccount.email || '(no email)'}
</Text>
<Link onPress={() => openModal({name: 'change-email'})}>
<Text type="lg" style={pal.link}>
@@ -658,6 +671,17 @@ export function SettingsScreen({}: Props) {
<Trans>Change handle</Trans>
</Text>
</TouchableOpacity>
{isNative && (
<View style={[pal.view, styles.toggleCard]}>
<ToggleButton
type="default-light"
label={_(msg`Open links with in-app browser`)}
labelType="lg"
isSelected={inAppBrowserPref ?? false}
onPress={() => setUseInAppBrowser(!inAppBrowserPref)}
/>
</View>
)}
<View style={styles.spacer20} />
<Text type="xl-bold" style={[pal.text, styles.heading]}>
<Trans>Danger Zone</Trans>
+3
View File
@@ -169,6 +169,9 @@ export function DesktopSearch() {
accessibilityRole="search"
accessibilityLabel={_(msg`Search`)}
accessibilityHint=""
autoCorrect={false}
autoComplete="off"
autoCapitalize="none"
/>
{query ? (
<View style={styles.cancelBtn}>
+2
View File
@@ -53,6 +53,7 @@
--text: white;
--background: black;
--backgroundLight: #26272D;
color-scheme: dark;
}
@media (prefers-color-scheme: light) {
html.colorMode--system {
@@ -66,6 +67,7 @@
--text: white;
--background: black;
--backgroundLight: #26272D;
color-scheme: dark;
}
}
+392 -292
View File
File diff suppressed because it is too large Load Diff