Merge remote-tracking branch 'origin/main' into eric/alf

* origin/main: (44 commits)
  Patch `@lingui/core` to fix `unraw` import resolution error (#2548)
  1.65
  Fix the fallback to discover behavior on the home feed (#2546)
  Bump android version code
  Bump ios build number
  Add a new home feed-api wrapper and give a header indicating the fallback behavior (#2534)
  Add accept-language header (#2457)
  rss: filter out replies server-side (#2518)
  feat: show muted/blocked status on list card (#2523)
  1.64 (#2521)
  Bump react-native@0.73.2 (#2519)
  Create a profile record on new user (#2520)
  fix: truncate long email address (#2493)
  fix: set html lang according to app language (#2496)
  Ensure sessions and tokens are synced between tabs (#2498)
  package.json: cp --verbose doesn't exist on macos (#2501)
  Suggest post language correction (#2486)
  (optional) In app browser  (#2490)
  Toggle minimal shell on any scroll for web (#2499)
   New report type, appeal (#2455)
  ...
This commit is contained in:
Eric Bailey
2024-01-16 17:43:20 -06:00
162 changed files with 23660 additions and 4724 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ module.exports = function () {
/**
* Android build number. Must be incremented for each release.
*/
const ANDROID_VERSION_CODE = 55
const ANDROID_VERSION_CODE = 57
/**
* Uses built-in Expo env vars
+1 -1
View File
@@ -80,7 +80,7 @@ func (srv *Server) WebProfileRSS(c echo.Context) error {
}
}
af, err := appbsky.FeedGetAuthorFeed(ctx, srv.xrpcc, did.String(), "", "", 30)
af, err := appbsky.FeedGetAuthorFeed(ctx, srv.xrpcc, did.String(), "", "posts_no_replies", 30)
if err != nil {
log.Warn("failed to fetch author feed", "did", did, "err", err)
return err
+2
View File
@@ -68,6 +68,7 @@
--text: white;
--background: black;
--backgroundLight: #26272D;
color-scheme: dark;
}
@media (prefers-color-scheme: light) {
html.colorMode--system {
@@ -81,6 +82,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'],
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.65.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",
@@ -95,25 +95,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",
@@ -141,7 +142,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",
@@ -149,24 +150,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",
+10
View File
@@ -0,0 +1,10 @@
diff --git a/node_modules/@lingui/core/dist/index.mjs b/node_modules/@lingui/core/dist/index.mjs
index 9759736..881f67b 100644
--- a/node_modules/@lingui/core/dist/index.mjs
+++ b/node_modules/@lingui/core/dist/index.mjs
@@ -1,4 +1,4 @@
-import unraw from 'unraw';
+import { unraw } from 'unraw';
import { compileMessage } from '@lingui/message-utils/compileMessage';
const isString = (s) => typeof s === "string";
-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 }) {
+5 -2
View File
@@ -42,6 +42,8 @@ import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unre
import * as persisted from '#/state/persisted'
import {Splash} from '#/Splash'
import {Provider as PortalProvider} from '#/components/Portal'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
SplashScreen.preventAutoHideAsync()
@@ -50,17 +52,18 @@ function InnerApp() {
const {isInitialLoad, currentAccount} = useSession()
const {resumeSession} = useSessionApi()
const theme = useColorModeTheme(colorMode)
const {_} = useLingui()
// init
useEffect(() => {
notifications.init(queryClient)
listenSessionDropped(() => {
Toast.show('Sorry! Your session expired. Please log in again.')
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
})
const account = persisted.get('session').currentAccount
resumeSession(account)
}, [resumeSession])
}, [resumeSession, _])
return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
+44 -35
View File
@@ -76,6 +76,8 @@ import {PreferencesHomeFeed} from 'view/screens/PreferencesHomeFeed'
import {PreferencesThreads} from 'view/screens/PreferencesThreads'
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStackNavigatorWithAuth'
import {msg} from '@lingui/macro'
import {i18n, MessageDescriptor} from '@lingui/core'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
@@ -93,55 +95,56 @@ const Tab = createBottomTabNavigator<BottomTabNavigatorParams>()
* These "common screens" are reused across stacks.
*/
function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
const title = (page: string) => bskyTitle(page, unreadCountLabel)
const title = (page: MessageDescriptor) =>
bskyTitle(i18n._(page), unreadCountLabel)
return (
<>
<Stack.Screen
name="NotFound"
getComponent={() => NotFoundScreen}
options={{title: title('Not Found')}}
options={{title: title(msg`Not Found`)}}
/>
<Stack.Screen
name="Lists"
component={ListsScreen}
options={{title: title('Lists'), requireAuth: true}}
options={{title: title(msg`Lists`), requireAuth: true}}
/>
<Stack.Screen
name="Moderation"
getComponent={() => ModerationScreen}
options={{title: title('Moderation'), requireAuth: true}}
options={{title: title(msg`Moderation`), requireAuth: true}}
/>
<Stack.Screen
name="ModerationModlists"
getComponent={() => ModerationModlistsScreen}
options={{title: title('Moderation Lists'), requireAuth: true}}
options={{title: title(msg`Moderation Lists`), requireAuth: true}}
/>
<Stack.Screen
name="ModerationMutedAccounts"
getComponent={() => ModerationMutedAccounts}
options={{title: title('Muted Accounts'), requireAuth: true}}
options={{title: title(msg`Muted Accounts`), requireAuth: true}}
/>
<Stack.Screen
name="ModerationBlockedAccounts"
getComponent={() => ModerationBlockedAccounts}
options={{title: title('Blocked Accounts'), requireAuth: true}}
options={{title: title(msg`Blocked Accounts`), requireAuth: true}}
/>
<Stack.Screen
name="Settings"
getComponent={() => SettingsScreen}
options={{title: title('Settings'), requireAuth: true}}
options={{title: title(msg`Settings`), requireAuth: true}}
/>
<Stack.Screen
name="LanguageSettings"
getComponent={() => LanguageSettingsScreen}
options={{title: title('Language Settings'), requireAuth: true}}
options={{title: title(msg`Language Settings`), requireAuth: true}}
/>
<Stack.Screen
name="Profile"
getComponent={() => ProfileScreen}
options={({route}) => ({
title: title(`@${route.params.name}`),
title: title(msg`@${route.params.name}`),
animation: 'none',
})}
/>
@@ -149,106 +152,112 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
name="ProfileFollowers"
getComponent={() => ProfileFollowersScreen}
options={({route}) => ({
title: title(`People following @${route.params.name}`),
title: title(msg`People following @${route.params.name}`),
})}
/>
<Stack.Screen
name="ProfileFollows"
getComponent={() => ProfileFollowsScreen}
options={({route}) => ({
title: title(`People followed by @${route.params.name}`),
title: title(msg`People followed by @${route.params.name}`),
})}
/>
<Stack.Screen
name="ProfileList"
getComponent={() => ProfileListScreen}
options={{title: title('List'), requireAuth: true}}
options={{title: title(msg`List`), requireAuth: true}}
/>
<Stack.Screen
name="PostThread"
getComponent={() => PostThreadScreen}
options={({route}) => ({title: title(`Post by @${route.params.name}`)})}
options={({route}) => ({
title: title(msg`Post by @${route.params.name}`),
})}
/>
<Stack.Screen
name="PostLikedBy"
getComponent={() => PostLikedByScreen}
options={({route}) => ({title: title(`Post by @${route.params.name}`)})}
options={({route}) => ({
title: title(msg`Post by @${route.params.name}`),
})}
/>
<Stack.Screen
name="PostRepostedBy"
getComponent={() => PostRepostedByScreen}
options={({route}) => ({title: title(`Post by @${route.params.name}`)})}
options={({route}) => ({
title: title(msg`Post by @${route.params.name}`),
})}
/>
<Stack.Screen
name="ProfileFeed"
getComponent={() => ProfileFeedScreen}
options={{title: title('Feed'), requireAuth: true}}
options={{title: title(msg`Feed`), requireAuth: true}}
/>
<Stack.Screen
name="ProfileFeedLikedBy"
getComponent={() => ProfileFeedLikedByScreen}
options={{title: title('Liked by')}}
options={{title: title(msg`Liked by`)}}
/>
<Stack.Screen
name="Debug"
getComponent={() => Storybook}
options={{title: title('Debug'), requireAuth: true}}
options={{title: title(msg`Storybook`), requireAuth: true}}
/>
<Stack.Screen
name="Log"
getComponent={() => LogScreen}
options={{title: title('Log'), requireAuth: true}}
options={{title: title(msg`Log`), requireAuth: true}}
/>
<Stack.Screen
name="Support"
getComponent={() => SupportScreen}
options={{title: title('Support')}}
options={{title: title(msg`Support`)}}
/>
<Stack.Screen
name="PrivacyPolicy"
getComponent={() => PrivacyPolicyScreen}
options={{title: title('Privacy Policy')}}
options={{title: title(msg`Privacy Policy`)}}
/>
<Stack.Screen
name="TermsOfService"
getComponent={() => TermsOfServiceScreen}
options={{title: title('Terms of Service')}}
options={{title: title(msg`Terms of Service`)}}
/>
<Stack.Screen
name="CommunityGuidelines"
getComponent={() => CommunityGuidelinesScreen}
options={{title: title('Community Guidelines')}}
options={{title: title(msg`Community Guidelines`)}}
/>
<Stack.Screen
name="CopyrightPolicy"
getComponent={() => CopyrightPolicyScreen}
options={{title: title('Copyright Policy')}}
options={{title: title(msg`Copyright Policy`)}}
/>
<Stack.Screen
name="AppPasswords"
getComponent={() => AppPasswords}
options={{title: title('App Passwords'), requireAuth: true}}
options={{title: title(msg`App Passwords`), requireAuth: true}}
/>
<Stack.Screen
name="SavedFeeds"
getComponent={() => SavedFeeds}
options={{title: title('Edit My Feeds'), requireAuth: true}}
options={{title: title(msg`Edit My Feeds`), requireAuth: true}}
/>
<Stack.Screen
name="PreferencesHomeFeed"
getComponent={() => PreferencesHomeFeed}
options={{title: title('Home Feed Preferences'), requireAuth: true}}
options={{title: title(msg`Home Feed Preferences`), requireAuth: true}}
/>
<Stack.Screen
name="PreferencesThreads"
getComponent={() => PreferencesThreads}
options={{title: title('Threads Preferences'), requireAuth: true}}
options={{title: title(msg`Threads Preferences`), requireAuth: true}}
/>
<Stack.Screen
name="PreferencesExternalEmbeds"
getComponent={() => PreferencesExternalEmbeds}
options={{
title: title('External Media Preferences'),
title: title(msg`External Media Preferences`),
requireAuth: true,
}}
/>
@@ -407,7 +416,7 @@ const FlatNavigator = () => {
const pal = usePalette('default')
const numUnread = useUnreadNotifications()
const title = (page: string) => bskyTitle(page, numUnread)
const title = (page: MessageDescriptor) => bskyTitle(i18n._(page), numUnread)
return (
<Flat.Navigator
screenOptions={{
@@ -420,22 +429,22 @@ const FlatNavigator = () => {
<Flat.Screen
name="Home"
getComponent={() => HomeScreen}
options={{title: title('Home'), requireAuth: true}}
options={{title: title(msg`Home`), requireAuth: true}}
/>
<Flat.Screen
name="Search"
getComponent={() => SearchScreen}
options={{title: title('Search')}}
options={{title: title(msg`Search`)}}
/>
<Flat.Screen
name="Feeds"
getComponent={() => FeedsScreen}
options={{title: title('Feeds'), requireAuth: true}}
options={{title: title(msg`Feeds`), requireAuth: true}}
/>
<Flat.Screen
name="Notifications"
getComponent={() => NotificationsScreen}
options={{title: title('Notifications'), requireAuth: true}}
options={{title: title(msg`Notifications`), requireAuth: true}}
/>
{commonScreens(Flat as typeof HomeTab, numUnread)}
</Flat.Navigator>
+18 -9
View File
@@ -4,15 +4,20 @@ import {
} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
import {getContentLanguages} from '#/state/preferences/languages'
export class CustomFeedAPI implements FeedAPI {
constructor(public params: GetCustomFeed.QueryParams) {}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await getAgent().app.bsky.feed.getFeed({
...this.params,
limit: 1,
})
const contentLangs = getContentLanguages().join(',')
const res = await getAgent().app.bsky.feed.getFeed(
{
...this.params,
limit: 1,
},
{headers: {'Accept-Language': contentLangs}},
)
return res.data.feed[0]
}
@@ -23,11 +28,15 @@ export class CustomFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await getAgent().app.bsky.feed.getFeed({
...this.params,
cursor,
limit,
})
const contentLangs = getContentLanguages().join(',')
const res = await getAgent().app.bsky.feed.getFeed(
{
...this.params,
cursor,
limit,
},
{headers: {'Accept-Language': contentLangs}},
)
if (res.success) {
// NOTE
// some custom feeds fail to enforce the pagination limit
+89
View File
@@ -0,0 +1,89 @@
import {AppBskyFeedDefs} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {FollowingFeedAPI} from './following'
import {CustomFeedAPI} from './custom'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
// HACK
// the feed API does not include any facilities for passing down
// non-post elements. adding that is a bit of a heavy lift, and we
// have just one temporary usecase for it: flagging when the home feed
// falls back to discover.
// we use this fallback marker post to drive this instead. see Feed.tsx
// for the usage.
// -prf
export const FALLBACK_MARKER_POST: AppBskyFeedDefs.FeedViewPost = {
post: {
uri: 'fallback-marker-post',
cid: 'fake',
record: {},
author: {
did: 'did:fake',
handle: 'fake.com',
},
indexedAt: new Date().toISOString(),
},
}
export class HomeFeedAPI implements FeedAPI {
following: FollowingFeedAPI
discover: CustomFeedAPI
usingDiscover = false
itemCursor = 0
constructor() {
this.following = new FollowingFeedAPI()
this.discover = new CustomFeedAPI({feed: PROD_DEFAULT_FEED('whats-hot')})
}
reset() {
this.following = new FollowingFeedAPI()
this.discover = new CustomFeedAPI({feed: PROD_DEFAULT_FEED('whats-hot')})
this.usingDiscover = false
this.itemCursor = 0
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
if (this.usingDiscover) {
return this.discover.peekLatest()
}
return this.following.peekLatest()
}
async fetch({
cursor,
limit,
}: {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
if (!cursor) {
this.reset()
}
let returnCursor
let posts: AppBskyFeedDefs.FeedViewPost[] = []
if (!this.usingDiscover) {
const res = await this.following.fetch({cursor, limit})
returnCursor = res.cursor
posts = posts.concat(res.feed)
if (!returnCursor) {
cursor = ''
posts.push(FALLBACK_MARKER_POST)
this.usingDiscover = true
}
}
if (this.usingDiscover) {
const res = await this.discover.fetch({cursor, limit})
returnCursor = res.cursor
posts = posts.concat(res.feed)
}
return {
cursor: returnCursor,
feed: posts,
}
}
}
+11 -6
View File
@@ -8,6 +8,7 @@ import {FeedAPI, FeedAPIResponse, ReasonFeedSource} from './types'
import {FeedParams} from '#/state/queries/post-feed'
import {FeedTunerFn} from '../feed-manip'
import {getAgent} from '#/state/session'
import {getContentLanguages} from '#/state/preferences/languages'
const REQUEST_WAIT_MS = 500 // 500ms
const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours
@@ -25,7 +26,7 @@ export class MergeFeedAPI implements FeedAPI {
reset() {
this.following = new MergeFeedSource_Following(this.feedTuners)
this.customFeeds = [] // just empty the array, they will be captured in _fetchNext()
this.customFeeds = []
this.feedCursor = 0
this.itemCursor = 0
this.sampleCursor = 0
@@ -231,11 +232,15 @@ class MergeFeedSource_Custom extends MergeFeedSource {
limit: number,
): Promise<AppBskyFeedGetTimeline.Response> {
try {
const res = await getAgent().app.bsky.feed.getFeed({
cursor,
limit,
feed: this.feedUri,
})
const contentLangs = getContentLanguages().join(',')
const res = await getAgent().app.bsky.feed.getFeed(
{
cursor,
limit,
feed: this.feedUri,
},
{headers: {'Accept-Language': contentLangs}},
)
// NOTE
// some custom feeds fail to enforce the pagination limit
// so we manually truncate here
-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
}
+1
View File
@@ -7,6 +7,7 @@ test('sanitizeAppLanguageSetting', () => {
expect(sanitizeAppLanguageSetting('en')).toBe(AppLanguage.en)
expect(sanitizeAppLanguageSetting('pt-BR')).toBe(AppLanguage.pt_BR)
expect(sanitizeAppLanguageSetting('hi')).toBe(AppLanguage.hi)
expect(sanitizeAppLanguageSetting('id')).toBe(AppLanguage.id)
expect(sanitizeAppLanguageSetting('foo')).toBe(AppLanguage.en)
expect(sanitizeAppLanguageSetting('en,foo')).toBe(AppLanguage.en)
expect(sanitizeAppLanguageSetting('foo,en')).toBe(AppLanguage.en)
+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,13 +121,14 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
// DISABLED until this translation is fixed -prf
// case 'de':
// return AppLanguage.de
// DISABLED until this translation is more thoroughly reviewed -prf
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':
@@ -132,6 +141,5 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
continue
}
}
return AppLanguage.en
}
+5
View File
@@ -5,6 +5,7 @@ import {useLanguagePrefs} from '#/state/preferences'
import {messages as messagesEn} from '#/locale/locales/en/messages'
// DISABLED until this translation is fixed -prf
// 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 messagesFr} from '#/locale/locales/fr/messages'
import {messages as messagesHi} from '#/locale/locales/hi/messages'
@@ -38,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 -1
View File
@@ -29,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
@@ -58,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
View File
@@ -11,6 +11,7 @@ export enum AppLanguage {
es = 'es',
fr = 'fr',
hi = 'hi',
id = 'id',
ja = 'ja',
ko = 'ko',
pt_BR = 'pt-BR',
@@ -29,6 +30,7 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
{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
+6 -1
View File
@@ -18,6 +18,7 @@ import {LikesFeedAPI} from 'lib/api/feed/likes'
import {CustomFeedAPI} from 'lib/api/feed/custom'
import {ListFeedAPI} from 'lib/api/feed/list'
import {MergeFeedAPI} from 'lib/api/feed/merge'
import {HomeFeedAPI} from '#/lib/api/feed/home'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {precacheFeedPosts as precacheResolvedUris} from './resolve-uri'
@@ -338,7 +339,11 @@ function createApi(
feedTuners: FeedTunerFn[],
) {
if (feedDesc === 'home') {
return new MergeFeedAPI(params, feedTuners)
if (params.mergeFeedEnabled) {
return new MergeFeedAPI(params, feedTuners)
} else {
return new HomeFeedAPI()
}
} else if (feedDesc === 'following') {
return new FollowingFeedAPI()
} else if (feedDesc.startsWith('author')) {
+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(
() => ({
+1
View File
@@ -11,6 +11,7 @@ export interface ComposerOptsPostRef {
displayName?: string
avatar?: string
}
embed?: AppBskyEmbedRecord.ViewRecord['embed']
}
export interface ComposerOptsQuote {
uri: string
+2 -2
View File
@@ -2,7 +2,7 @@ import React from 'react'
import {View, Pressable} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {Trans, msg} from '@lingui/macro'
import {useNavigation} from '@react-navigation/native'
import {isIOS, isNative} from 'platform/detection'
@@ -119,7 +119,7 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
}}
onPress={onPressSearch}>
<Text type="lg-bold" style={[pal.text]}>
Search{' '}
<Trans>Search</Trans>{' '}
</Text>
<FontAwesomeIcon
icon="search"
+1 -1
View File
@@ -74,7 +74,7 @@ export const SplashScreen = ({
// TODO: web accessibility
accessibilityRole="button">
<Text style={[s.white, styles.btnLabel]}>
Create a new account
<Trans>Create a new account</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
+3 -2
View File
@@ -77,7 +77,7 @@ export function Step1({
value={uiState.serviceUrl}
editable
onChange={onChangeServiceUrl}
accessibilityHint="Input hosting provider address"
accessibilityHint={_(msg`Input hosting provider address`)}
accessibilityLabel={_(msg`Hosting provider address`)}
accessibilityLabelledBy="addressProvider"
/>
@@ -125,6 +125,7 @@ function Option({
}>) {
const theme = useTheme()
const pal = usePalette('default')
const {_} = useLingui()
const circleFillStyle = React.useMemo(
() => ({
backgroundColor: theme.palette.primary.background,
@@ -139,7 +140,7 @@ function Option({
testID={testID}
accessibilityRole="button"
accessibilityLabel={label}
accessibilityHint={`Sets hosting provider to ${label}`}>
accessibilityHint={_(msg`Sets hosting provider to ${label}`)}>
<View style={styles.optionHeading}>
<View style={[styles.circle, pal.border]}>
{isSelected ? (
+6 -6
View File
@@ -60,7 +60,7 @@ export function Step2({
{uiState.isInviteCodeRequired && (
<View style={s.pb20}>
<Text type="md-medium" style={[pal.text, s.mb2]}>
Invite code
<Trans>Invite code</Trans>
</Text>
<TextInput
testID="inviteCodeInput"
@@ -70,7 +70,7 @@ export function Step2({
editable
onChange={value => uiDispatch({type: 'set-invite-code', value})}
accessibilityLabel={_(msg`Invite code`)}
accessibilityHint="Input invite code to proceed"
accessibilityHint={_(msg`Input invite code to proceed`)}
autoCapitalize="none"
autoComplete="off"
autoCorrect={false}
@@ -80,7 +80,7 @@ export function Step2({
{!uiState.inviteCode && uiState.isInviteCodeRequired ? (
<Text style={[s.alignBaseline, pal.text]}>
Don't have an invite code?{' '}
<Trans>Don't have an invite code?</Trans>{' '}
<TouchableWithoutFeedback
onPress={onPressWaitlist}
accessibilityLabel={_(msg`Join the waitlist.`)}
@@ -106,7 +106,7 @@ export function Step2({
editable
onChange={value => uiDispatch({type: 'set-email', value})}
accessibilityLabel={_(msg`Email`)}
accessibilityHint="Input email for Bluesky waitlist"
accessibilityHint={_(msg`Input email for Bluesky waitlist`)}
accessibilityLabelledBy="email"
autoCapitalize="none"
autoComplete="off"
@@ -130,7 +130,7 @@ export function Step2({
secureTextEntry
onChange={value => uiDispatch({type: 'set-password', value})}
accessibilityLabel={_(msg`Password`)}
accessibilityHint="Set password"
accessibilityHint={_(msg`Set password`)}
accessibilityLabelledBy="password"
autoCapitalize="none"
autoComplete="off"
@@ -154,7 +154,7 @@ export function Step2({
buttonStyle={[pal.border, styles.dateInputButton]}
buttonLabelType="lg"
accessibilityLabel={_(msg`Birthday`)}
accessibilityHint="Enter your birth date"
accessibilityHint={_(msg`Enter your birth date`)}
accessibilityLabelledBy="birthDate"
/>
</View>
+1 -1
View File
@@ -36,7 +36,7 @@ export function Step3({
onChange={value => uiDispatch({type: 'set-handle', value})}
// TODO: Add explicit text label
accessibilityLabel={_(msg`User handle`)}
accessibilityHint="Input your user handle"
accessibilityHint={_(msg`Input your user handle`)}
/>
<Text type="lg" style={[pal.text, s.pl5, s.pt10]}>
<Trans>Your full handle will be</Trans>{' '}
+6 -1
View File
@@ -2,13 +2,18 @@ import React from 'react'
import {StyleSheet, View} from 'react-native'
import {Text} from 'view/com/util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {Trans} from '@lingui/macro'
export function StepHeader({step, title}: {step: string; title: string}) {
const pal = usePalette('default')
return (
<View style={styles.container}>
<Text type="lg" style={[pal.textLight]}>
{step === '3' ? 'Last step!' : <>Step {step} of 3</>}
{step === '3' ? (
<Trans>Last step!</Trans>
) : (
<Trans>Step {step} of 3</Trans>
)}
</Text>
<Text style={[pal.text]} type="title-xl">
{title}
@@ -42,7 +42,7 @@ function AccountItem({
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={_(msg`Sign in as ${account.handle}`)}
accessibilityHint="Double tap to sign in">
accessibilityHint={_(msg`Double tap to sign in`)}>
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
<View style={s.p10}>
<UserAvatar avatar={profile?.avatar} size={30} />
@@ -95,19 +95,19 @@ export const ChooseAccountForm = ({
if (account.accessJwt) {
if (account.did === currentAccount?.did) {
setShowLoggedOut(false)
Toast.show(`Already signed in as @${account.handle}`)
Toast.show(_(msg`Already signed in as @${account.handle}`))
} else {
await initSession(account)
track('Sign In', {resumedSession: true})
setTimeout(() => {
Toast.show(`Signed in as @${account.handle}`)
Toast.show(_(msg`Signed in as @${account.handle}`))
}, 100)
}
} else {
onSelectAccount(account)
}
},
[currentAccount, track, initSession, onSelectAccount, setShowLoggedOut],
[currentAccount, track, initSession, onSelectAccount, setShowLoggedOut, _],
)
return (
+10 -6
View File
@@ -67,7 +67,7 @@ export const ForgotPasswordForm = ({
const onPressNext = async () => {
if (!EmailValidator.validate(email)) {
return setError('Your email appears to be invalid.')
return setError(_(msg`Your email appears to be invalid.`))
}
setError('')
@@ -83,7 +83,9 @@ export const ForgotPasswordForm = ({
setIsProcessing(false)
if (isNetworkError(e)) {
setError(
'Unable to contact your service. Please check your Internet connection.',
_(
msg`Unable to contact your service. Please check your Internet connection.`,
),
)
} else {
setError(cleanError(errMsg))
@@ -112,7 +114,9 @@ export const ForgotPasswordForm = ({
onPress={onPressSelectService}
accessibilityRole="button"
accessibilityLabel={_(msg`Hosting provider`)}
accessibilityHint="Sets hosting provider for password reset">
accessibilityHint={_(
msg`Sets hosting provider for password reset`,
)}>
<FontAwesomeIcon
icon="globe"
style={[pal.textLight, styles.groupContentIcon]}
@@ -136,7 +140,7 @@ export const ForgotPasswordForm = ({
<TextInput
testID="forgotPasswordEmail"
style={[pal.text, styles.textInput]}
placeholder="Email address"
placeholder={_(msg`Email address`)}
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
autoFocus
@@ -146,7 +150,7 @@ export const ForgotPasswordForm = ({
onChangeText={setEmail}
editable={!isProcessing}
accessibilityLabel={_(msg`Email`)}
accessibilityHint="Sets email for password reset"
accessibilityHint={_(msg`Sets email for password reset`)}
/>
</View>
</View>
@@ -179,7 +183,7 @@ export const ForgotPasswordForm = ({
onPress={onPressNext}
accessibilityRole="button"
accessibilityLabel={_(msg`Go to next`)}
accessibilityHint="Navigates to the next screen">
accessibilityHint={_(msg`Navigates to the next screen`)}>
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Next</Trans>
</Text>
+9 -7
View File
@@ -145,7 +145,7 @@ export const LoginForm = ({
onPress={onPressSelectService}
accessibilityRole="button"
accessibilityLabel={_(msg`Select service`)}
accessibilityHint="Sets server for the Bluesky client">
accessibilityHint={_(msg`Sets server for the Bluesky client`)}>
<Text type="xl" style={[pal.text, styles.textBtnLabel]}>
{toNiceDomain(serviceUrl)}
</Text>
@@ -190,7 +190,9 @@ export const LoginForm = ({
}
editable={!isProcessing}
accessibilityLabel={_(msg`Username or email address`)}
accessibilityHint="Input the username or email address you used at signup"
accessibilityHint={_(
msg`Input the username or email address you used at signup`,
)}
/>
</View>
<View style={[pal.borderDark, styles.groupContent]}>
@@ -221,8 +223,8 @@ export const LoginForm = ({
accessibilityLabel={_(msg`Password`)}
accessibilityHint={
identifier === ''
? 'Input your password'
: `Input the password tied to ${identifier}`
? _(msg`Input your password`)
: _(msg`Input the password tied to ${identifier}`)
}
/>
<TouchableOpacity
@@ -231,7 +233,7 @@ export const LoginForm = ({
onPress={onPressForgotPassword}
accessibilityRole="button"
accessibilityLabel={_(msg`Forgot password`)}
accessibilityHint="Opens password reset form">
accessibilityHint={_(msg`Opens password reset form`)}>
<Text style={pal.link}>
<Trans>Forgot</Trans>
</Text>
@@ -261,7 +263,7 @@ export const LoginForm = ({
onPress={onPressRetryConnect}
accessibilityRole="button"
accessibilityLabel={_(msg`Retry`)}
accessibilityHint="Retries login">
accessibilityHint={_(msg`Retries login`)}>
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Retry</Trans>
</Text>
@@ -281,7 +283,7 @@ export const LoginForm = ({
onPress={onPressNext}
accessibilityRole="button"
accessibilityLabel={_(msg`Go to next`)}
accessibilityHint="Navigates to the next screen">
accessibilityHint={_(msg`Navigates to the next screen`)}>
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Next</Trans>
</Text>
@@ -36,7 +36,7 @@ export const PasswordUpdatedForm = ({
onPress={onPressNext}
accessibilityRole="button"
accessibilityLabel={_(msg`Close alert`)}
accessibilityHint="Closes password update alert">
accessibilityHint={_(msg`Closes password update alert`)}>
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Okay</Trans>
</Text>
@@ -95,7 +95,7 @@ export const SetNewPasswordForm = ({
<TextInput
testID="resetCodeInput"
style={[pal.text, styles.textInput]}
placeholder="Reset code"
placeholder={_(msg`Reset code`)}
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
autoCorrect={false}
@@ -106,7 +106,9 @@ export const SetNewPasswordForm = ({
editable={!isProcessing}
accessible={true}
accessibilityLabel={_(msg`Reset code`)}
accessibilityHint="Input code sent to your email for password reset"
accessibilityHint={_(
msg`Input code sent to your email for password reset`,
)}
/>
</View>
<View style={[pal.borderDark, styles.groupContent]}>
@@ -117,7 +119,7 @@ export const SetNewPasswordForm = ({
<TextInput
testID="newPasswordInput"
style={[pal.text, styles.textInput]}
placeholder="New password"
placeholder={_(msg`New password`)}
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
autoCorrect={false}
@@ -128,7 +130,7 @@ export const SetNewPasswordForm = ({
editable={!isProcessing}
accessible={true}
accessibilityLabel={_(msg`Password`)}
accessibilityHint="Input new password"
accessibilityHint={_(msg`Input new password`)}
/>
</View>
</View>
@@ -161,7 +163,7 @@ export const SetNewPasswordForm = ({
onPress={onPressNext}
accessibilityRole="button"
accessibilityLabel={_(msg`Go to next`)}
accessibilityHint="Navigates to the next screen">
accessibilityHint={_(msg`Navigates to the next screen`)}>
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Next</Trans>
</Text>
@@ -18,6 +18,8 @@ import {
} from '#/state/queries/preferences'
import {logger} from '#/logger'
import {useAnalytics} from '#/lib/analytics/analytics'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export function RecommendedFeedsItem({
item,
@@ -26,6 +28,7 @@ export function RecommendedFeedsItem({
}) {
const {isMobile} = useWebMediaQueries()
const pal = usePalette('default')
const {_} = useLingui()
const {data: preferences} = usePreferencesQuery()
const {
mutateAsync: pinFeed,
@@ -51,7 +54,7 @@ export function RecommendedFeedsItem({
await removeFeed({uri: item.uri})
resetRemoveFeed()
} catch (e) {
Toast.show('There was an issue contacting your server')
Toast.show(_(msg`There was an issue contacting your server`))
logger.error('Failed to unsave feed', {error: e})
}
} else {
@@ -60,7 +63,7 @@ export function RecommendedFeedsItem({
resetPinFeed()
track('Onboarding:CustomFeedAdded')
} catch (e) {
Toast.show('There was an issue contacting your server')
Toast.show(_(msg`There was an issue contacting your server`))
logger.error('Failed to pin feed', {error: e})
}
}
@@ -94,7 +97,7 @@ export function RecommendedFeedsItem({
</Text>
<Text style={[pal.textLight, {marginBottom: 8}]} numberOfLines={1}>
by {sanitizeHandle(item.creator.handle, '@')}
<Trans>by {sanitizeHandle(item.creator.handle, '@')}</Trans>
</Text>
{item.description ? (
@@ -133,7 +136,7 @@ export function RecommendedFeedsItem({
color={pal.colors.textInverted}
/>
<Text type="lg-medium" style={pal.textInverted}>
Added
<Trans>Added</Trans>
</Text>
</>
) : (
@@ -144,7 +147,7 @@ export function RecommendedFeedsItem({
color={pal.colors.textInverted}
/>
<Text type="lg-medium" style={pal.textInverted}>
Add
<Trans>Add</Trans>
</Text>
</>
)}
@@ -83,7 +83,7 @@ export function RecommendedFollows({next}: Props) {
<Text
type="2xl-medium"
style={{color: '#fff', position: 'relative', top: -1}}>
<Trans>Done</Trans>
<Trans context="action">Done</Trans>
</Text>
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
</View>
@@ -7,6 +7,7 @@ import {usePalette} from 'lib/hooks/usePalette'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout'
import {Button} from 'view/com/util/forms/Button'
import {Trans} from '@lingui/macro'
type Props = {
next: () => void
@@ -17,7 +18,7 @@ export function WelcomeDesktop({next}: Props) {
const pal = usePalette('default')
const horizontal = useMediaQuery({minWidth: 1300})
const title = (
<>
<Trans>
<Text
style={[
pal.textLight,
@@ -40,7 +41,7 @@ export function WelcomeDesktop({next}: Props) {
]}>
Bluesky
</Text>
</>
</Trans>
)
return (
<TitleColumnLayout
@@ -52,10 +53,12 @@ export function WelcomeDesktop({next}: Props) {
<FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}>
<Text type="xl-bold" style={[pal.text]}>
Bluesky is public.
<Trans>Bluesky is public.</Trans>
</Text>
<Text type="xl" style={[pal.text, s.pt2]}>
Your posts, likes, and blocks are public. Mutes are private.
<Trans>
Your posts, likes, and blocks are public. Mutes are private.
</Trans>
</Text>
</View>
</View>
@@ -63,10 +66,10 @@ export function WelcomeDesktop({next}: Props) {
<FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}>
<Text type="xl-bold" style={[pal.text]}>
Bluesky is open.
<Trans>Bluesky is open.</Trans>
</Text>
<Text type="xl" style={[pal.text, s.pt2]}>
Never lose access to your followers and data.
<Trans>Never lose access to your followers and data.</Trans>
</Text>
</View>
</View>
@@ -74,10 +77,13 @@ export function WelcomeDesktop({next}: Props) {
<FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}>
<Text type="xl-bold" style={[pal.text]}>
Bluesky is flexible.
<Trans>Bluesky is flexible.</Trans>
</Text>
<Text type="xl" style={[pal.text, s.pt2]}>
Choose the algorithms that power your experience with custom feeds.
<Trans>
Choose the algorithms that power your experience with custom
feeds.
</Trans>
</Text>
</View>
</View>
@@ -94,7 +100,7 @@ export function WelcomeDesktop({next}: Props) {
<Text
type="2xl-medium"
style={{color: '#fff', position: 'relative', top: -1}}>
Next
<Trans context="action">Next</Trans>
</Text>
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
</View>
+28 -37
View File
@@ -29,8 +29,6 @@ import {UserAvatar} from '../util/UserAvatar'
import * as apilib from 'lib/api/index'
import {ComposerOpts} from 'state/shell/composer'
import {s, colors, gradients} from 'lib/styles'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {cleanError} from 'lib/strings/errors'
import {shortenLinks} from 'lib/strings/rich-text-manip'
import {toShortUrl} from 'lib/strings/url-helpers'
@@ -47,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'
@@ -63,6 +62,7 @@ import {useComposerControls} from '#/state/shell/composer'
import {emitPostCreated} from '#/state/events'
import {ThreadgateSetting} from '#/state/queries/threadgate'
import {logger} from '#/logger'
import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo'
type Props = ComposerOpts
export const ComposePost = observer(function ComposePost({
@@ -261,7 +261,11 @@ export const ComposePost = observer(function ComposePost({
setLangPrefs.savePostLanguageToHistory()
onPost?.()
onClose()
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)
Toast.show(
replyTo
? _(msg`Your reply has been published`)
: _(msg`Your post has been published`),
)
}
const canPost = useMemo(
@@ -270,7 +274,9 @@ export const ComposePost = observer(function ComposePost({
(!requireAltTextEnabled || !gallery.needsAltText),
[graphemeLength, requireAltTextEnabled, gallery.needsAltText],
)
const selectTextInputPlaceholder = replyTo ? 'Write your reply' : `What's up?`
const selectTextInputPlaceholder = replyTo
? _(msg`Write your reply`)
: _(msg`What's up?`)
const canSelectImages = useMemo(() => gallery.size < 4, [gallery.size])
const hasMedia = gallery.size > 0 || Boolean(extLink)
@@ -292,7 +298,9 @@ export const ComposePost = observer(function ComposePost({
onAccessibilityEscape={onPressCancel}
accessibilityRole="button"
accessibilityLabel={_(msg`Cancel`)}
accessibilityHint="Closes post composer and discards post draft">
accessibilityHint={_(
msg`Closes post composer and discards post draft`,
)}>
<Text style={[pal.link, s.f18]}>
<Trans>Cancel</Trans>
</Text>
@@ -324,7 +332,7 @@ export const ComposePost = observer(function ComposePost({
onPress={onPressPublish}
accessibilityRole="button"
accessibilityLabel={
replyTo ? 'Publish reply' : 'Publish post'
replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
}
accessibilityHint="">
<LinearGradient
@@ -336,14 +344,18 @@ export const ComposePost = observer(function ComposePost({
end={{x: 1, y: 1}}
style={styles.postBtn}>
<Text style={[s.white, s.f16, s.bold]}>
{replyTo ? 'Reply' : 'Post'}
{replyTo ? (
<Trans context="action">Reply</Trans>
) : (
<Trans context="action">Post</Trans>
)}
</Text>
</LinearGradient>
</TouchableOpacity>
) : (
<View style={[styles.postBtn, pal.btn]}>
<Text style={[pal.textLight, s.f16, s.bold]}>
<Trans>Post</Trans>
<Trans context="action">Post</Trans>
</Text>
</View>
)}
@@ -379,22 +391,7 @@ export const ComposePost = observer(function ComposePost({
<ScrollView
style={styles.scrollView}
keyboardShouldPersistTaps="always">
{replyTo ? (
<View style={[pal.border, styles.replyToLayout]}>
<UserAvatar avatar={replyTo.author.avatar} size={50} />
<View style={styles.replyToPost}>
<Text type="xl-medium" style={[pal.text]}>
{sanitizeDisplayName(
replyTo.author.displayName ||
sanitizeHandle(replyTo.author.handle),
)}
</Text>
<Text type="post-text" style={pal.text} numberOfLines={6}>
{replyTo.text}
</Text>
</View>
</View>
) : undefined}
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
<View
style={[
@@ -416,7 +413,9 @@ export const ComposePost = observer(function ComposePost({
onError={setError}
accessible={true}
accessibilityLabel={_(msg`Write post`)}
accessibilityHint={`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`}
accessibilityHint={_(
msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`,
)}
/>
</View>
@@ -445,7 +444,9 @@ export const ComposePost = observer(function ComposePost({
onPress={() => onPressAddLinkCard(url)}
accessibilityRole="button"
accessibilityLabel={_(msg`Add link card`)}
accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}>
accessibilityHint={_(
msg`Creates a card with a thumbnail. The card links to ${url}`,
)}>
<Text style={pal.text}>
<Trans>Add link card:</Trans>{' '}
<Text style={[pal.link, s.ml5]}>{toShortUrl(url)}</Text>
@@ -454,6 +455,7 @@ export const ComposePost = observer(function ComposePost({
))}
</View>
) : null}
<SuggestedLanguage text={richtext.text} />
<View style={[pal.border, styles.bottomBar]}>
{canSelectImages ? (
<>
@@ -549,17 +551,6 @@ const styles = StyleSheet.create({
textInputLayoutMobile: {
flex: 1,
},
replyToLayout: {
flexDirection: 'row',
borderTopWidth: 1,
paddingTop: 16,
paddingBottom: 16,
},
replyToPost: {
flex: 1,
paddingLeft: 13,
paddingRight: 8,
},
addExtLinkBtn: {
borderWidth: 1,
borderRadius: 24,
+254
View File
@@ -0,0 +1,254 @@
import React from 'react'
import {LayoutAnimation, Pressable, StyleSheet, View} from 'react-native'
import {Image} from 'expo-image'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {
AppBskyEmbedImages,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedPost,
} from '@atproto/api'
import {ComposerOptsPostRef} from 'state/shell/composer'
import {usePalette} from 'lib/hooks/usePalette'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {Text} from 'view/com/util/text/Text'
import QuoteEmbed from 'view/com/util/post-embeds/QuoteEmbed'
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
const pal = usePalette('default')
const {_} = useLingui()
const {embed} = replyTo
const [showFull, setShowFull] = React.useState(false)
const onPress = React.useCallback(() => {
setShowFull(prev => !prev)
LayoutAnimation.configureNext({
duration: 350,
update: {type: 'spring', springDamping: 0.7},
})
}, [])
const quote = React.useMemo(() => {
if (
AppBskyEmbedRecord.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record) &&
AppBskyFeedPost.isRecord(embed.record.value)
) {
// Not going to include the images right now
return {
author: embed.record.author,
cid: embed.record.cid,
uri: embed.record.uri,
indexedAt: embed.record.indexedAt,
text: embed.record.value.text,
}
} else if (
AppBskyEmbedRecordWithMedia.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
AppBskyFeedPost.isRecord(embed.record.record.value)
) {
return {
author: embed.record.record.author,
cid: embed.record.record.cid,
uri: embed.record.record.uri,
indexedAt: embed.record.record.indexedAt,
text: embed.record.record.value.text,
}
}
}, [embed])
const images = React.useMemo(() => {
if (AppBskyEmbedImages.isView(embed)) {
return embed.images
} else if (
AppBskyEmbedRecordWithMedia.isView(embed) &&
AppBskyEmbedImages.isView(embed.media)
) {
return embed.media.images
}
}, [embed])
return (
<Pressable
style={[pal.border, styles.replyToLayout]}
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={_(
msg`Expand or collapse the full post you are replying to`,
)}
accessibilityHint={_(
msg`Expand or collapse the full post you are replying to`,
)}>
<UserAvatar avatar={replyTo.author.avatar} size={50} />
<View style={styles.replyToPost}>
<Text type="xl-medium" style={[pal.text]}>
{sanitizeDisplayName(
replyTo.author.displayName || sanitizeHandle(replyTo.author.handle),
)}
</Text>
<View style={styles.replyToBody}>
<View style={styles.replyToText}>
<Text
type="post-text"
style={pal.text}
numberOfLines={!showFull ? 6 : undefined}>
{replyTo.text}
</Text>
</View>
{images && (
<ComposerReplyToImages images={images} showFull={showFull} />
)}
</View>
{showFull && quote && <QuoteEmbed quote={quote} />}
</View>
</Pressable>
)
}
function ComposerReplyToImages({
images,
}: {
images: AppBskyEmbedImages.ViewImage[]
showFull: boolean
}) {
return (
<View
style={{
width: 65,
flexDirection: 'column',
alignItems: 'center',
}}>
<View style={styles.imagesContainer}>
{(images.length === 1 && (
<Image
source={{uri: images[0].thumb}}
style={styles.singleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
)) ||
(images.length === 2 && (
<View style={[styles.imagesInner, styles.imagesRow]}>
<Image
source={{uri: images[0].thumb}}
style={styles.doubleImageTall}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[1].thumb}}
style={styles.doubleImageTall}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
)) ||
(images.length === 3 && (
<View style={[styles.imagesInner, styles.imagesRow]}>
<Image
source={{uri: images[0].thumb}}
style={styles.doubleImageTall}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<View style={styles.imagesInner}>
<Image
source={{uri: images[1].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[2].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
</View>
)) ||
(images.length === 4 && (
<View style={styles.imagesInner}>
<View style={[styles.imagesInner, styles.imagesRow]}>
<Image
source={{uri: images[0].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[1].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
<View style={[styles.imagesInner, styles.imagesRow]}>
<Image
source={{uri: images[2].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
<Image
source={{uri: images[3].thumb}}
style={styles.doubleImage}
cachePolicy="memory-disk"
accessibilityIgnoresInvertColors
/>
</View>
</View>
))}
</View>
</View>
)
}
const styles = StyleSheet.create({
replyToLayout: {
flexDirection: 'row',
borderTopWidth: 1,
paddingTop: 16,
paddingBottom: 16,
},
replyToPost: {
flex: 1,
paddingLeft: 13,
paddingRight: 8,
},
replyToBody: {
flexDirection: 'row',
gap: 10,
},
replyToText: {
flex: 1,
flexGrow: 1,
},
imagesContainer: {
borderRadius: 6,
overflow: 'hidden',
marginTop: 2,
},
imagesInner: {
gap: 2,
},
imagesRow: {
flexDirection: 'row',
},
singleImage: {
width: 65,
height: 65,
},
doubleImageTall: {
width: 32.5,
height: 65,
},
doubleImage: {
width: 32.5,
height: 32.5,
},
})
+1 -1
View File
@@ -68,7 +68,7 @@ export const ExternalEmbed = ({
onPress={onRemove}
accessibilityRole="button"
accessibilityLabel={_(msg`Remove image preview`)}
accessibilityHint={`Removes default thumbnail from ${link.uri}`}
accessibilityHint={_(msg`Removes default thumbnail from ${link.uri}`)}
onAccessibilityEscape={onRemove}>
<FontAwesomeIcon size={18} icon="xmark" style={s.white} />
</TouchableOpacity>
+1 -1
View File
@@ -22,7 +22,7 @@ export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
onPress={() => onPressCompose()}
accessibilityRole="button"
accessibilityLabel={_(msg`Compose reply`)}
accessibilityHint="Opens composer">
accessibilityHint={_(msg`Opens composer`)}>
<UserAvatar avatar={profile?.avatar} size={38} />
<Text
type="xl"
@@ -58,7 +58,7 @@ export function OpenCameraBtn({gallery}: Props) {
hitSlop={HITSLOP_10}
accessibilityRole="button"
accessibilityLabel={_(msg`Camera`)}
accessibilityHint="Opens camera on device">
accessibilityHint={_(msg`Opens camera on device`)}>
<FontAwesomeIcon
icon="camera"
style={pal.link as FontAwesomeIconStyle}
@@ -41,7 +41,7 @@ export function SelectPhotoBtn({gallery}: Props) {
hitSlop={HITSLOP_10}
accessibilityRole="button"
accessibilityLabel={_(msg`Gallery`)}
accessibilityHint="Opens device photo gallery">
accessibilityHint={_(msg`Opens device photo gallery`)}>
<FontAwesomeIcon
icon={['far', 'image']}
style={pal.link as FontAwesomeIconStyle}
@@ -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,
},
})
@@ -17,6 +17,7 @@ import {usePalette} from 'lib/hooks/usePalette'
import {Text} from 'view/com/util/text/Text'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {useGrapheme} from '../hooks/useGrapheme'
import {Trans} from '@lingui/macro'
interface MentionListRef {
onKeyDown: (props: SuggestionKeyDownProps) => boolean
@@ -187,7 +188,7 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
})
) : (
<Text type="sm" style={[pal.text, styles.noResult]}>
No result
<Trans>No result</Trans>
</Text>
)}
</View>
+2 -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}
@@ -197,7 +198,7 @@ export function FeedPage({
onPress={onPressCompose}
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
accessibilityRole="button"
accessibilityLabel={_(msg`New post`)}
accessibilityLabel={_(msg({message: `New post`, context: 'action'}))}
accessibilityHint=""
/>
)}
+20 -13
View File
@@ -14,7 +14,7 @@ import * as Toast from 'view/com/util/Toast'
import {sanitizeHandle} from 'lib/strings/handles'
import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {msg} from '@lingui/macro'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
usePinFeedMutation,
@@ -108,9 +108,9 @@ export function FeedSourceCardLoaded({
try {
await removeFeed({uri: feed.uri})
// await item.unsave()
Toast.show('Removed from my feeds')
Toast.show(_(msg`Removed from my feeds`))
} catch (e) {
Toast.show('There was an issue contacting your server')
Toast.show(_(msg`There was an issue contacting your server`))
logger.error('Failed to unsave feed', {error: e})
}
},
@@ -122,9 +122,9 @@ export function FeedSourceCardLoaded({
} else {
await saveFeed({uri: feed.uri})
}
Toast.show('Added to my feeds')
Toast.show(_(msg`Added to my feeds`))
} catch (e) {
Toast.show('There was an issue contacting your server')
Toast.show(_(msg`There was an issue contacting your server`))
logger.error('Failed to save feed', {error: e})
}
}
@@ -164,7 +164,7 @@ export function FeedSourceCardLoaded({
testID={`feed-${feedUri}-toggleSave`}
disabled={isRemovePending}
accessibilityRole="button"
accessibilityLabel={'Remove from my feeds'}
accessibilityLabel={_(msg`Remove from my feeds`)}
accessibilityHint=""
onPress={() => {
openModal({
@@ -175,9 +175,11 @@ export function FeedSourceCardLoaded({
try {
await removeFeed({uri: feedUri})
// await item.unsave()
Toast.show('Removed from my feeds')
Toast.show(_(msg`Removed from my feeds`))
} catch (e) {
Toast.show('There was an issue contacting your server')
Toast.show(
_(msg`There was an issue contacting your server`),
)
logger.error('Failed to unsave feed', {error: e})
}
},
@@ -223,8 +225,11 @@ export function FeedSourceCardLoaded({
{feed.displayName}
</Text>
<Text style={[pal.textLight]} numberOfLines={3}>
{feed.type === 'feed' ? 'Feed' : 'List'} by{' '}
{sanitizeHandle(feed.creatorHandle, '@')}
{feed.type === 'feed' ? (
<Trans>Feed by {sanitizeHandle(feed.creatorHandle, '@')}</Trans>
) : (
<Trans>List by {sanitizeHandle(feed.creatorHandle, '@')}</Trans>
)}
</Text>
</View>
@@ -235,7 +240,7 @@ export function FeedSourceCardLoaded({
disabled={isSavePending || isPinPending || isRemovePending}
accessibilityRole="button"
accessibilityLabel={
isSaved ? 'Remove from my feeds' : 'Add to my feeds'
isSaved ? _(msg`Remove from my feeds`) : _(msg`Add to my feeds`)
}
accessibilityHint=""
onPress={onToggleSaved}
@@ -269,8 +274,10 @@ export function FeedSourceCardLoaded({
{showLikes && feed.type === 'feed' ? (
<Text type="sm-medium" style={[pal.text, pal.textLight]}>
Liked by {feed.likeCount || 0}{' '}
{pluralize(feed.likeCount || 0, 'user')}
<Trans>
Liked by {feed.likeCount || 0}{' '}
{pluralize(feed.likeCount || 0, 'user')}
</Trans>
</Text>
) : null}
</Pressable>
+7 -3
View File
@@ -9,13 +9,14 @@ import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {useProfileFeedgensQuery, RQKEY} from '#/state/queries/profile-feedgens'
import {logger} from '#/logger'
import {Trans} from '@lingui/macro'
import {Trans, msg} from '@lingui/macro'
import {cleanError} from '#/lib/strings/errors'
import {useTheme} from '#/lib/ThemeContext'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {hydrateFeedGenerator} from '#/state/queries/feed'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {isNative} from '#/platform/detection'
import {useLingui} from '@lingui/react'
const LOADING = {_reactKey: '__loading__'}
const EMPTY = {_reactKey: '__empty__'}
@@ -43,6 +44,7 @@ export const ProfileFeedgens = React.forwardRef<
ref,
) {
const pal = usePalette('default')
const {_} = useLingui()
const theme = useTheme()
const [isPTRing, setIsPTRing] = React.useState(false)
const opts = React.useMemo(() => ({enabled}), [enabled])
@@ -142,7 +144,9 @@ export const ProfileFeedgens = React.forwardRef<
} else if (item === LOAD_MORE_ERROR_ITEM) {
return (
<LoadMoreRetryBtn
label="There was an issue fetching your lists. Tap here to try again."
label={_(
msg`There was an issue fetching your lists. Tap here to try again.`,
)}
onPress={onPressRetryLoadMore}
/>
)
@@ -162,7 +166,7 @@ export const ProfileFeedgens = React.forwardRef<
}
return null
},
[error, refetch, onPressRetryLoadMore, pal, preferences],
[error, refetch, onPressRetryLoadMore, pal, preferences, _],
)
return (
@@ -24,7 +24,7 @@ const ImageDefaultHeader = ({onRequestClose}: Props) => (
hitSlop={HIT_SLOP}
accessibilityRole="button"
accessibilityLabel={t`Close image`}
accessibilityHint="Closes viewer for header image"
accessibilityHint={t`Closes viewer for header image`}
onAccessibilityEscape={onRequestClose}>
<Text style={styles.closeText}></Text>
</TouchableOpacity>
+10 -5
View File
@@ -15,6 +15,8 @@ import {
ProfileImageLightbox,
ImagesLightbox,
} from '#/state/lightbox'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export function Lightbox() {
const {activeLightbox} = useLightbox()
@@ -53,6 +55,7 @@ export function Lightbox() {
}
function LightboxFooter({imageIndex}: {imageIndex: number}) {
const {_} = useLingui()
const {activeLightbox} = useLightbox()
const [isAltExpanded, setAltExpanded] = React.useState(false)
const [permissionResponse, requestPermission] = MediaLibrary.usePermissions()
@@ -60,12 +63,14 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
const saveImageToAlbumWithToasts = React.useCallback(
async (uri: string) => {
if (!permissionResponse || permissionResponse.granted === false) {
Toast.show('Permission to access camera roll is required.')
Toast.show(_(msg`Permission to access camera roll is required.`))
if (permissionResponse?.canAskAgain) {
requestPermission()
} else {
Toast.show(
'Permission to access camera roll was denied. Please enable it in your system settings.',
_(
msg`Permission to access camera roll was denied. Please enable it in your system settings.`,
),
)
}
return
@@ -78,7 +83,7 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
Toast.show(`Failed to save image: ${String(e)}`)
}
},
[permissionResponse, requestPermission],
[permissionResponse, requestPermission, _],
)
const lightbox = activeLightbox
@@ -117,7 +122,7 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
onPress={() => saveImageToAlbumWithToasts(uri)}>
<FontAwesomeIcon icon={['far', 'floppy-disk']} style={s.white} />
<Text type="xl" style={s.white}>
Save
<Trans context="action">Save</Trans>
</Text>
</Button>
<Button
@@ -126,7 +131,7 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
onPress={() => shareImageModal({uri})}>
<FontAwesomeIcon icon="arrow-up-from-bracket" style={s.white} />
<Text type="xl" style={s.white}>
Share
<Trans context="action">Share</Trans>
</Text>
</Button>
</View>
+4 -2
View File
@@ -110,7 +110,7 @@ function LightboxInner({
onPress={onClose}
accessibilityRole="button"
accessibilityLabel={_(msg`Close image viewer`)}
accessibilityHint="Exits image view"
accessibilityHint={_(msg`Exits image view`)}
onAccessibilityEscape={onClose}>
<View style={styles.imageCenterer}>
<Image
@@ -154,7 +154,9 @@ function LightboxInner({
<View style={styles.footer}>
<Pressable
accessibilityLabel={_(msg`Expand alt text`)}
accessibilityHint="If alt text is long, toggles alt text expanded state"
accessibilityHint={_(
msg`If alt text is long, toggles alt text expanded state`,
)}
onPress={() => {
setAltExpanded(!isAltExpanded)
}}>
+29 -11
View File
@@ -11,6 +11,7 @@ import {useSession} from '#/state/session'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links'
import {Trans} from '@lingui/macro'
export const ListCard = ({
testID,
@@ -76,23 +77,40 @@ export const ListCard = ({
{sanitizeDisplayName(list.name)}
</Text>
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
{list.purpose === 'app.bsky.graph.defs#curatelist' && 'User list '}
{list.purpose === 'app.bsky.graph.defs#curatelist' &&
(list.creator.did === currentAccount?.did ? (
<Trans>User list by you</Trans>
) : (
<Trans>
User list by {sanitizeHandle(list.creator.handle, '@')}
</Trans>
))}
{list.purpose === 'app.bsky.graph.defs#modlist' &&
'Moderation list '}
by{' '}
{list.creator.did === currentAccount?.did
? 'you'
: sanitizeHandle(list.creator.handle, '@')}
(list.creator.did === currentAccount?.did ? (
<Trans>Moderation list by you</Trans>
) : (
<Trans>
Moderation list by {sanitizeHandle(list.creator.handle, '@')}
</Trans>
))}
</Text>
{!!list.viewer?.muted && (
<View style={s.flexRow}>
<View style={s.flexRow}>
{list.viewer?.muted ? (
<View style={[s.mt5, pal.btn, styles.pill]}>
<Text type="xs" style={pal.text}>
Subscribed
<Trans>Muted</Trans>
</Text>
</View>
</View>
)}
) : null}
{list.viewer?.blocked ? (
<View style={[s.mt5, pal.btn, styles.pill]}>
<Text type="xs" style={pal.text}>
<Trans>Blocked</Trans>
</Text>
</View>
) : null}
</View>
</View>
{renderButton ? (
<View style={styles.layoutButton}>{renderButton()}</View>
+9 -3
View File
@@ -20,6 +20,8 @@ import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {useSession} from '#/state/session'
import {cleanError} from '#/lib/strings/errors'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
const LOADING_ITEM = {_reactKey: '__loading__'}
const EMPTY_ITEM = {_reactKey: '__empty__'}
@@ -50,6 +52,7 @@ export function ListMembers({
desktopFixedHeightOffset?: number
}) {
const {track} = useAnalytics()
const {_} = useLingui()
const [isRefreshing, setIsRefreshing] = React.useState(false)
const {isMobile} = useWebMediaQueries()
const {openModal} = useModalControls()
@@ -143,12 +146,12 @@ export function ListMembers({
<Button
testID={`user-${profile.handle}-editBtn`}
type="default"
label="Edit"
label={_(msg({message: 'Edit', context: 'action'}))}
onPress={() => onPressEditMembership(profile)}
/>
)
},
[isOwner, onPressEditMembership],
[isOwner, onPressEditMembership, _],
)
const renderItem = React.useCallback(
@@ -165,7 +168,9 @@ export function ListMembers({
} else if (item === LOAD_MORE_ERROR_ITEM) {
return (
<LoadMoreRetryBtn
label="There was an issue fetching the list. Tap here to try again."
label={_(
msg`There was an issue fetching the list. Tap here to try again.`,
)}
onPress={onPressRetryLoadMore}
/>
)
@@ -191,6 +196,7 @@ export function ListMembers({
onPressTryAgain,
onPressRetryLoadMore,
isMobile,
_,
],
)
+7 -3
View File
@@ -10,11 +10,12 @@ import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useProfileListsQuery, RQKEY} from '#/state/queries/profile-lists'
import {logger} from '#/logger'
import {Trans} from '@lingui/macro'
import {Trans, msg} from '@lingui/macro'
import {cleanError} from '#/lib/strings/errors'
import {useTheme} from '#/lib/ThemeContext'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {isNative} from '#/platform/detection'
import {useLingui} from '@lingui/react'
const LOADING = {_reactKey: '__loading__'}
const EMPTY = {_reactKey: '__empty__'}
@@ -42,6 +43,7 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
const pal = usePalette('default')
const theme = useTheme()
const {track} = useAnalytics()
const {_} = useLingui()
const [isPTRing, setIsPTRing] = React.useState(false)
const opts = React.useMemo(() => ({enabled}), [enabled])
const {
@@ -149,7 +151,9 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
} else if (item === LOAD_MORE_ERROR_ITEM) {
return (
<LoadMoreRetryBtn
label="There was an issue fetching your lists. Tap here to try again."
label={_(
msg`There was an issue fetching your lists. Tap here to try again.`,
)}
onPress={onPressRetryLoadMore}
/>
)
@@ -164,7 +168,7 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
/>
)
},
[error, refetch, onPressRetryLoadMore, pal],
[error, refetch, onPressRetryLoadMore, pal, _],
)
return (
+16 -12
View File
@@ -72,10 +72,10 @@ export function Component({}: {}) {
const onCopy = React.useCallback(() => {
if (appPassword) {
Clipboard.setString(appPassword)
Toast.show('Copied to clipboard')
Toast.show(_(msg`Copied to clipboard`))
setWasCopied(true)
}
}, [appPassword])
}, [appPassword, _])
const onDone = React.useCallback(() => {
closeModal()
@@ -85,7 +85,9 @@ export function Component({}: {}) {
// if name is all whitespace, we don't allow it
if (!name || !name.trim()) {
Toast.show(
'Please enter a name for your app password. All spaces is not allowed.',
_(
msg`Please enter a name for your app password. All spaces is not allowed.`,
),
'times',
)
return
@@ -93,14 +95,14 @@ export function Component({}: {}) {
// if name is too short (under 4 chars), we don't allow it
if (name.length < 4) {
Toast.show(
'App Password names must be at least 4 characters long.',
_(msg`App Password names must be at least 4 characters long.`),
'times',
)
return
}
if (passwords?.find(p => p.name === name)) {
Toast.show('This name is already in use', 'times')
Toast.show(_(msg`This name is already in use`), 'times')
return
}
@@ -109,11 +111,11 @@ export function Component({}: {}) {
if (newPassword) {
setAppPassword(newPassword.password)
} else {
Toast.show('Failed to create app password.', 'times')
Toast.show(_(msg`Failed to create app password.`), 'times')
// TODO: better error handling (?)
}
} catch (e) {
Toast.show('Failed to create app password.', 'times')
Toast.show(_(msg`Failed to create app password.`), 'times')
logger.error('Failed to create app password', {error: e})
}
}
@@ -127,7 +129,9 @@ export function Component({}: {}) {
setName(text)
} else {
Toast.show(
'App Password names can only contain letters, numbers, spaces, dashes, and underscores.',
_(
msg`App Password names can only contain letters, numbers, spaces, dashes, and underscores.`,
),
)
}
}
@@ -158,7 +162,7 @@ export function Component({}: {}) {
style={[styles.input, pal.text]}
onChangeText={_onChangeText}
value={name}
placeholder="Enter a name for this App Password"
placeholder={_(msg`Enter a name for this App Password`)}
placeholderTextColor={pal.colors.textLight}
autoCorrect={false}
autoComplete="off"
@@ -175,7 +179,7 @@ export function Component({}: {}) {
onEndEditing={createAppPassword}
accessible={true}
accessibilityLabel={_(msg`Name`)}
accessibilityHint="Input name for app password"
accessibilityHint={_(msg`Input name for app password`)}
/>
</View>
) : (
@@ -184,7 +188,7 @@ export function Component({}: {}) {
onPress={onCopy}
accessibilityRole="button"
accessibilityLabel={_(msg`Copy`)}
accessibilityHint="Copies app password">
accessibilityHint={_(msg`Copies app password`)}>
<Text type="2xl-bold" style={[pal.text]}>
{appPassword}
</Text>
@@ -221,7 +225,7 @@ export function Component({}: {}) {
<View style={styles.btnContainer}>
<Button
type="primary"
label={!appPassword ? 'Create App Password' : 'Done'}
label={!appPassword ? _(msg`Create App Password`) : _(msg`Done`)}
style={styles.btn}
labelStyle={styles.btnLabel}
onPress={!appPassword ? createAppPassword : onDone}
+2 -2
View File
@@ -38,14 +38,14 @@ 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,
},
reason: details,
})
Toast.show("We'll look into your appeal promptly.")
Toast.show(_(msg`We'll look into your appeal promptly.`))
} finally {
closeModal()
}
+1 -1
View File
@@ -71,7 +71,7 @@ function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
buttonStyle={[pal.border, styles.dateInputButton]}
buttonLabelType="lg"
accessibilityLabel={_(msg`Birthday`)}
accessibilityHint="Enter your birth date"
accessibilityHint={_(msg`Enter your birth date`)}
accessibilityLabelledBy="birthDate"
/>
</View>
+3 -3
View File
@@ -38,7 +38,7 @@ export function Component() {
const onRequestChange = async () => {
if (email === currentAccount?.email) {
setError('Enter your new email above')
setError(_(msg`Enter your new email above`))
return
}
setError('')
@@ -53,7 +53,7 @@ export function Component() {
email: email.trim(),
emailConfirmed: false,
})
Toast.show('Email updated')
Toast.show(_(msg`Email updated`))
setStage(Stages.Done)
}
} catch (e) {
@@ -85,7 +85,7 @@ export function Component() {
email: email.trim(),
emailConfirmed: false,
})
Toast.show('Email updated')
Toast.show(_(msg`Email updated`))
setStage(Stages.Done)
} catch (e) {
setError(cleanError(String(e)))
+9 -7
View File
@@ -147,7 +147,7 @@ export function Inner({
onPress={onPressCancel}
accessibilityRole="button"
accessibilityLabel={_(msg`Cancel change handle`)}
accessibilityHint="Exits handle change process"
accessibilityHint={_(msg`Exits handle change process`)}
onAccessibilityEscape={onPressCancel}>
<Text type="lg" style={pal.textLight}>
Cancel
@@ -168,7 +168,7 @@ export function Inner({
onPress={onPressSave}
accessibilityRole="button"
accessibilityLabel={_(msg`Save handle change`)}
accessibilityHint={`Saves handle change to ${handle}`}>
accessibilityHint={_(msg`Saves handle change to ${handle}`)}>
<Text type="2xl-medium" style={pal.link}>
<Trans>Save</Trans>
</Text>
@@ -263,14 +263,16 @@ function ProvidedHandleForm({
editable={!isProcessing}
accessible={true}
accessibilityLabel={_(msg`Handle`)}
accessibilityHint="Sets Bluesky username"
accessibilityHint={_(msg`Sets Bluesky username`)}
/>
</View>
<Text type="md" style={[pal.textLight, s.pl10, s.pt10]}>
<Trans>Your full handle will be</Trans>{' '}
<Text type="md-bold" style={pal.textLight}>
@{createFullHandle(handle, userDomain)}
</Text>
<Trans>
Your full handle will be{' '}
<Text type="md-bold" style={pal.textLight}>
@{createFullHandle(handle, userDomain)}
</Text>
</Trans>
</Text>
<TouchableOpacity
onPress={onToggleCustom}
+5 -5
View File
@@ -12,7 +12,7 @@ import {cleanError} from 'lib/strings/errors'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {Trans, msg} from '@lingui/macro'
import type {ConfirmModal} from '#/state/modals'
import {useModalControls} from '#/state/modals'
@@ -72,10 +72,10 @@ export function Component({
onPress={onPress}
style={[styles.btn, confirmBtnStyle]}
accessibilityRole="button"
accessibilityLabel={_(msg`Confirm`)}
accessibilityLabel={_(msg({message: 'Confirm', context: 'action'}))}
accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}>
{confirmBtnText ?? 'Confirm'}
{confirmBtnText ?? <Trans context="action">Confirm</Trans>}
</Text>
</TouchableOpacity>
)}
@@ -85,10 +85,10 @@ export function Component({
onPress={onPressCancel}
style={[styles.btnCancel, s.mt10]}
accessibilityRole="button"
accessibilityLabel={_(msg`Cancel`)}
accessibilityLabel={_(msg({message: 'Cancel', context: 'action'}))}
accessibilityHint="">
<Text type="button-lg" style={pal.textLight}>
{cancelBtnText ?? 'Cancel'}
{cancelBtnText ?? <Trans context="action">Cancel</Trans>}
</Text>
</TouchableOpacity>
)}
@@ -104,6 +104,7 @@ export function Component({}: {}) {
function AdultContentEnabledPref() {
const pal = usePalette('default')
const {_} = useLingui()
const {data: preferences} = usePreferencesQuery()
const {mutate, variables} = usePreferencesSetAdultContentMutation()
const {openModal} = useModalControls()
@@ -121,36 +122,44 @@ function AdultContentEnabledPref() {
enabled: !(variables?.enabled ?? preferences?.adultContentEnabled),
})
} catch (e) {
Toast.show('There was an issue syncing your preferences with the server')
Toast.show(
_(msg`There was an issue syncing your preferences with the server`),
)
logger.error('Failed to update preferences with server', {error: e})
}
}, [variables, preferences, mutate])
}, [variables, preferences, mutate, _])
return (
<View style={s.mb10}>
{isIOS ? (
preferences?.adultContentEnabled ? null : (
<Text type="md" style={pal.textLight}>
Adult content can only be enabled via the Web at{' '}
<TextLink
style={pal.link}
href="https://bsky.app"
text="bsky.app"
/>
.
<Trans>
Adult content can only be enabled via the Web at{' '}
<TextLink
style={pal.link}
href="https://bsky.app"
text="bsky.app"
/>
.
</Trans>
</Text>
)
) : typeof preferences?.birthDate === 'undefined' ? (
<View style={[pal.viewLight, styles.agePrompt]}>
<Text type="md" style={[pal.text, {flex: 1}]}>
Confirm your age to enable adult content.
<Trans>Confirm your age to enable adult content.</Trans>
</Text>
<Button type="primary" label="Set Age" onPress={onSetAge} />
<Button
type="primary"
label={_(msg({message: 'Set Age', context: 'action'}))}
onPress={onSetAge}
/>
</View>
) : (preferences.userAge || 0) >= 18 ? (
<ToggleButton
type="default-light"
label="Enable Adult Content"
label={_(msg`Enable Adult Content`)}
isSelected={variables?.enabled ?? preferences?.adultContentEnabled}
onPress={onToggleAdultContent}
style={styles.toggleBtn}
@@ -158,9 +167,13 @@ function AdultContentEnabledPref() {
) : (
<View style={[pal.viewLight, styles.agePrompt]}>
<Text type="md" style={[pal.text, {flex: 1}]}>
You must be 18 or older to enable adult content.
<Trans>You must be 18 or older to enable adult content.</Trans>
</Text>
<Button type="primary" label="Set Age" onPress={onSetAge} />
<Button
type="primary"
label={_(msg({message: 'Set Age', context: 'action'}))}
onPress={onSetAge}
/>
</View>
)}
</View>
@@ -203,7 +216,7 @@ function ContentLabelPref({
{disabled || !visibility ? (
<Text type="sm-bold" style={pal.textLight}>
Hide
<Trans context="action">Hide</Trans>
</Text>
) : (
<SelectGroup
@@ -223,12 +236,14 @@ interface SelectGroupProps {
}
function SelectGroup({current, onChange, labelGroup}: SelectGroupProps) {
const {_} = useLingui()
return (
<View style={styles.selectableBtns}>
<SelectableBtn
current={current}
value="hide"
label="Hide"
label={_(msg`Hide`)}
left
onChange={onChange}
labelGroup={labelGroup}
@@ -236,14 +251,14 @@ function SelectGroup({current, onChange, labelGroup}: SelectGroupProps) {
<SelectableBtn
current={current}
value="warn"
label="Warn"
label={_(msg`Warn`)}
onChange={onChange}
labelGroup={labelGroup}
/>
<SelectableBtn
current={current}
value="ignore"
label="Show"
label={_(msg`Show`)}
right
onChange={onChange}
labelGroup={labelGroup}
@@ -273,6 +288,8 @@ function SelectableBtn({
}: SelectableBtnProps) {
const pal = usePalette('default')
const palPrimary = usePalette('inverted')
const {_} = useLingui()
return (
<Pressable
style={[
@@ -285,7 +302,9 @@ function SelectableBtn({
onPress={() => onChange(value)}
accessibilityRole="button"
accessibilityLabel={value}
accessibilityHint={`Set ${value} for ${labelGroup} content moderation policy`}>
accessibilityHint={_(
msg`Set ${value} for ${labelGroup} content moderation policy`,
)}>
<Text style={current === value ? palPrimary.text : pal.text}>
{label}
</Text>
+33 -12
View File
@@ -65,7 +65,6 @@ export function Component({
return 'app.bsky.graph.defs#curatelist'
}, [list, purpose])
const isCurateList = activePurpose === 'app.bsky.graph.defs#curatelist'
const purposeLabel = isCurateList ? 'User' : 'Moderation'
const [isProcessing, setProcessing] = useState<boolean>(false)
const [name, setName] = useState<string>(list?.name || '')
@@ -106,7 +105,7 @@ export function Component({
}
const nameTrimmed = name.trim()
if (!nameTrimmed) {
setError('Name is required')
setError(_(msg`Name is required`))
return
}
setProcessing(true)
@@ -121,7 +120,11 @@ export function Component({
description: description.trim(),
avatar: newAvatar,
})
Toast.show(`${purposeLabel} list updated`)
Toast.show(
isCurateList
? _(msg`User list updated`)
: _(msg`Moderation list updated`),
)
onSave?.(list.uri)
} else {
const res = await listCreateMutation.mutateAsync({
@@ -130,14 +133,20 @@ export function Component({
description,
avatar: newAvatar,
})
Toast.show(`${purposeLabel} list created`)
Toast.show(
isCurateList
? _(msg`User list created`)
: _(msg`Moderation list created`),
)
onSave?.(res.uri)
}
closeModal()
} catch (e: any) {
if (isNetworkError(e)) {
setError(
'Failed to create the list. Check your internet connection and try again.',
_(
msg`Failed to create the list. Check your internet connection and try again.`,
),
)
} else {
setError(cleanError(e))
@@ -153,13 +162,13 @@ export function Component({
closeModal,
activePurpose,
isCurateList,
purposeLabel,
name,
description,
newAvatar,
list,
listMetadataMutation,
listCreateMutation,
_,
])
return (
@@ -174,7 +183,17 @@ export function Component({
testID="createOrEditListModal">
<Text style={[styles.title, pal.text]}>
<Trans>
{list ? 'Edit' : 'New'} {purposeLabel} List
{isCurateList ? (
list ? (
<Trans>Edit User List</Trans>
) : (
<Trans>New User List</Trans>
)
) : list ? (
<Trans>Edit Moderation List</Trans>
) : (
<Trans>New Moderation List</Trans>
)}
</Trans>
</Text>
{error !== '' && (
@@ -202,7 +221,9 @@ export function Component({
testID="editNameInput"
style={[styles.textInput, pal.border, pal.text]}
placeholder={
isCurateList ? 'e.g. Great Posters' : 'e.g. Spammers'
isCurateList
? _(msg`e.g. Great Posters`)
: _(msg`e.g. Spammers`)
}
placeholderTextColor={colors.gray4}
value={name}
@@ -222,8 +243,8 @@ export function Component({
style={[styles.textArea, pal.border, pal.text]}
placeholder={
isCurateList
? 'e.g. The posters who never miss.'
: 'e.g. Users that repeatedly reply with ads.'
? _(msg`e.g. The posters who never miss.`)
: _(msg`e.g. Users that repeatedly reply with ads.`)
}
placeholderTextColor={colors.gray4}
keyboardAppearance={theme.colorScheme}
@@ -254,7 +275,7 @@ export function Component({
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text style={[s.white, s.bold]}>
<Trans>Save</Trans>
<Trans context="action">Save</Trans>
</Text>
</LinearGradient>
</TouchableOpacity>
@@ -269,7 +290,7 @@ export function Component({
onAccessibilityEscape={onPressCancel}>
<View style={[styles.btn]}>
<Text style={[s.black, s.bold, pal.text]}>
<Trans>Cancel</Trans>
<Trans context="action">Cancel</Trans>
</Text>
</View>
</TouchableOpacity>
+16 -9
View File
@@ -62,7 +62,7 @@ export function Component({}: {}) {
password,
token,
})
Toast.show('Your account has been deleted')
Toast.show(_(msg`Your account has been deleted`))
resetToTab('HomeTab')
removeAccount(currentAccount)
clearCurrentAccount()
@@ -125,7 +125,9 @@ export function Component({}: {}) {
onPress={onPressSendEmail}
accessibilityRole="button"
accessibilityLabel={_(msg`Send email`)}
accessibilityHint="Sends email with confirmation code for account deletion">
accessibilityHint={_(
msg`Sends email with confirmation code for account deletion`,
)}>
<LinearGradient
colors={[
gradients.blueLight.start,
@@ -135,7 +137,7 @@ export function Component({}: {}) {
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text type="button-lg" style={[s.white, s.bold]}>
<Trans>Send Email</Trans>
<Trans context="action">Send Email</Trans>
</Text>
</LinearGradient>
</TouchableOpacity>
@@ -147,7 +149,7 @@ export function Component({}: {}) {
accessibilityHint=""
onAccessibilityEscape={onCancel}>
<Text type="button-lg" style={pal.textLight}>
<Trans>Cancel</Trans>
<Trans context="action">Cancel</Trans>
</Text>
</TouchableOpacity>
</>
@@ -158,7 +160,7 @@ export function Component({}: {}) {
{/* TODO: Update this label to be more concise */}
<Text
type="lg"
style={styles.description}
style={[pal.text, styles.description]}
nativeID="confirmationCode">
<Trans>
Check your inbox for an email with the confirmation code to
@@ -174,9 +176,14 @@ export function Component({}: {}) {
onChangeText={setConfirmCode}
accessibilityLabelledBy="confirmationCode"
accessibilityLabel={_(msg`Confirmation code`)}
accessibilityHint="Input confirmation code for account deletion"
accessibilityHint={_(
msg`Input confirmation code for account deletion`,
)}
/>
<Text type="lg" style={styles.description} nativeID="password">
<Text
type="lg"
style={[pal.text, styles.description]}
nativeID="password">
<Trans>Please enter your password as well:</Trans>
</Text>
<TextInput
@@ -189,7 +196,7 @@ export function Component({}: {}) {
onChangeText={setPassword}
accessibilityLabelledBy="password"
accessibilityLabel={_(msg`Password`)}
accessibilityHint="Input password for account deletion"
accessibilityHint={_(msg`Input password for account deletion`)}
/>
{error ? (
<View style={styles.mt20}>
@@ -220,7 +227,7 @@ export function Component({}: {}) {
accessibilityHint="Exits account deletion process"
onAccessibilityEscape={onCancel}>
<Text type="button-lg" style={pal.textLight}>
<Trans>Cancel</Trans>
<Trans context="action">Cancel</Trans>
</Text>
</TouchableOpacity>
</>
+5 -5
View File
@@ -112,16 +112,16 @@ export const Component = observer(function EditImageImpl({
// },
{
name: 'flip' as const,
label: 'Flip horizontal',
label: _(msg`Flip horizontal`),
onPress: onFlipHorizontal,
},
{
name: 'flip' as const,
label: 'Flip vertically',
label: _(msg`Flip vertically`),
onPress: onFlipVertical,
},
],
[onFlipHorizontal, onFlipVertical],
[onFlipHorizontal, onFlipVertical, _],
)
useEffect(() => {
@@ -284,7 +284,7 @@ export const Component = observer(function EditImageImpl({
size={label?.startsWith('Flip') ? 22 : 24}
style={[
pal.text,
label === 'Flip vertically'
label === _(msg`Flip vertically`)
? styles.flipVertical
: undefined,
]}
@@ -330,7 +330,7 @@ export const Component = observer(function EditImageImpl({
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text type="xl-medium" style={s.white}>
<Trans>Done</Trans>
<Trans context="action">Done</Trans>
</Text>
</LinearGradient>
</Pressable>
+7 -6
View File
@@ -125,7 +125,7 @@ export function Component({
newUserAvatar,
newUserBanner,
})
Toast.show('Profile updated')
Toast.show(_(msg`Profile updated`))
onUpdate?.()
closeModal()
} catch (e: any) {
@@ -142,6 +142,7 @@ export function Component({
newUserAvatar,
newUserBanner,
setImageError,
_,
])
return (
@@ -181,7 +182,7 @@ export function Component({
<TextInput
testID="editProfileDisplayNameInput"
style={[styles.textInput, pal.border, pal.text]}
placeholder="e.g. Alice Roberts"
placeholder={_(msg`e.g. Alice Roberts`)}
placeholderTextColor={colors.gray4}
value={displayName}
onChangeText={v =>
@@ -189,7 +190,7 @@ export function Component({
}
accessible={true}
accessibilityLabel={_(msg`Display name`)}
accessibilityHint="Edit your display name"
accessibilityHint={_(msg`Edit your display name`)}
/>
</View>
<View style={s.pb10}>
@@ -199,7 +200,7 @@ export function Component({
<TextInput
testID="editProfileDescriptionInput"
style={[styles.textArea, pal.border, pal.text]}
placeholder="e.g. Artist, dog-lover, and avid reader."
placeholder={_(msg`e.g. Artist, dog-lover, and avid reader.`)}
placeholderTextColor={colors.gray4}
keyboardAppearance={theme.colorScheme}
multiline
@@ -207,7 +208,7 @@ export function Component({
onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
accessible={true}
accessibilityLabel={_(msg`Description`)}
accessibilityHint="Edit your profile description"
accessibilityHint={_(msg`Edit your profile description`)}
/>
</View>
{updateMutation.isPending ? (
@@ -221,7 +222,7 @@ export function Component({
onPress={onPressSave}
accessibilityRole="button"
accessibilityLabel={_(msg`Save`)}
accessibilityHint="Saves any changes to your profile">
accessibilityHint={_(msg`Saves any changes to your profile`)}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
+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,
},
})
+11 -8
View File
@@ -18,7 +18,7 @@ import {ScrollView} from './util'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Trans} from '@lingui/macro'
import {Trans, msg} from '@lingui/macro'
import {cleanError} from 'lib/strings/errors'
import {useModalControls} from '#/state/modals'
import {useInvitesState, useInvitesAPI} from '#/state/invites'
@@ -30,6 +30,7 @@ import {
useInviteCodesQuery,
InviteCodesQueryResponse,
} from '#/state/queries/invites'
import {useLingui} from '@lingui/react'
export const snapPoints = ['70%']
@@ -49,6 +50,7 @@ export function Component() {
export function Inner({invites}: {invites: InviteCodesQueryResponse}) {
const pal = usePalette('default')
const {_} = useLingui()
const {closeModal} = useModalControls()
const {isTabletOrDesktop} = useWebMediaQueries()
@@ -75,7 +77,7 @@ export function Inner({invites}: {invites: InviteCodesQueryResponse}) {
]}>
<Button
type="primary"
label="Done"
label={_(msg`Done`)}
style={styles.btn}
labelStyle={styles.btnLabel}
onPress={onClose}
@@ -118,7 +120,7 @@ export function Inner({invites}: {invites: InviteCodesQueryResponse}) {
<Button
testID="closeBtn"
type="primary"
label="Done"
label={_(msg`Done`)}
style={styles.btn}
labelStyle={styles.btnLabel}
onPress={onClose}
@@ -140,15 +142,16 @@ function InviteCode({
invites: InviteCodesQueryResponse
}) {
const pal = usePalette('default')
const {_} = useLingui()
const invitesState = useInvitesState()
const {setInviteCopied} = useInvitesAPI()
const uses = invite.uses
const onPress = React.useCallback(() => {
Clipboard.setString(invite.code)
Toast.show('Copied to clipboard')
Toast.show(_(msg`Copied to clipboard`))
setInviteCopied(invite.code)
}, [setInviteCopied, invite])
}, [setInviteCopied, invite, _])
return (
<View
@@ -163,10 +166,10 @@ function InviteCode({
accessibilityRole="button"
accessibilityLabel={
invites.available.length === 1
? 'Invite codes: 1 available'
: `Invite codes: ${invites.available.length} available`
? _(msg`Invite codes: 1 available`)
: _(msg`Invite codes: ${invites.available.length} available`)
}
accessibilityHint="Opens list of invite codes">
accessibilityHint={_(msg`Opens list of invite codes`)}>
<Text
testID={`${testID}-code`}
type={used ? 'md' : 'md-bold'}
+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 (
+3 -3
View File
@@ -67,7 +67,7 @@ export function Component({
<TextInput
testID="searchInput"
style={[styles.searchInput, pal.border, pal.text]}
placeholder="Search for users"
placeholder={_(msg`Search for users`)}
placeholderTextColor={pal.colors.textLight}
value={query}
onChangeText={setQuery}
@@ -85,7 +85,7 @@ export function Component({
onPress={onPressCancelSearch}
accessibilityRole="button"
accessibilityLabel={_(msg`Cancel search`)}
accessibilityHint="Exits inputting search query"
accessibilityHint={_(msg`Exits inputting search query`)}
onAccessibilityEscape={onPressCancelSearch}
hitSlop={HITSLOP_20}>
<FontAwesomeIcon
@@ -141,7 +141,7 @@ export function Component({
}}
accessibilityLabel={_(msg`Done`)}
accessibilityHint=""
label="Done"
label={_(msg({message: 'Done', context: 'action'}))}
labelContainerStyle={{justifyContent: 'center', padding: 4}}
labelStyle={[s.f18]}
/>
+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
}
+27 -18
View File
@@ -10,6 +10,8 @@ import {isWeb} from 'platform/detection'
import {listUriToHref} from 'lib/strings/url-helpers'
import {Button} from '../util/forms/Button'
import {useModalControls} from '#/state/modals'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
export const snapPoints = [300]
@@ -23,19 +25,21 @@ export function Component({
const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries()
const pal = usePalette('default')
const {_} = useLingui()
let name
let description
if (!moderation.cause) {
name = 'Content Warning'
description =
'Moderator has chosen to set a general warning on the content.'
name = _(msg`Content Warning`)
description = _(
msg`Moderator has chosen to set a general warning on the content.`,
)
} else if (moderation.cause.type === 'blocking') {
if (moderation.cause.source.type === 'list') {
const list = moderation.cause.source.list
name = 'User Blocked by List'
name = _(msg`User Blocked by List`)
description = (
<>
<Trans>
This user is included in the{' '}
<TextLink
type="2xl"
@@ -44,25 +48,30 @@ export function Component({
style={pal.link}
/>{' '}
list which you have blocked.
</>
</Trans>
)
} else {
name = 'User Blocked'
description = 'You have blocked this user. You cannot view their content.'
name = _(msg`User Blocked`)
description = _(
msg`You have blocked this user. You cannot view their content.`,
)
}
} else if (moderation.cause.type === 'blocked-by') {
name = 'User Blocks You'
description = 'This user has blocked you. You cannot view their content.'
name = _(msg`User Blocks You`)
description = _(
msg`This user has blocked you. You cannot view their content.`,
)
} else if (moderation.cause.type === 'block-other') {
name = 'Content Not Available'
description =
'This content is not available because one of the users involved has blocked the other.'
name = _(msg`Content Not Available`)
description = _(
msg`This content is not available because one of the users involved has blocked the other.`,
)
} else if (moderation.cause.type === 'muted') {
if (moderation.cause.source.type === 'list') {
const list = moderation.cause.source.list
name = <>Account Muted by List</>
name = _(msg`Account Muted by List`)
description = (
<>
<Trans>
This user is included the{' '}
<TextLink
type="2xl"
@@ -71,11 +80,11 @@ export function Component({
style={pal.link}
/>{' '}
list which you have muted.
</>
</Trans>
)
} else {
name = 'Account Muted'
description = 'You have muted this user.'
name = _(msg`Account Muted`)
description = _(msg`You have muted this user.`)
}
} else {
name = moderation.cause.labelDef.strings[context].en.name
+7 -4
View File
@@ -14,11 +14,14 @@ import {ErrorScreen} from '../util/error/ErrorScreen'
import {CenteredView} from '../util/Views'
import {cleanError} from '#/lib/strings/errors'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export const snapPoints = [520, '100%']
export function Component({did}: {did: string}) {
const pal = usePalette('default')
const {_} = useLingui()
const moderationOpts = useModerationOpts()
const {
data: profile,
@@ -43,7 +46,7 @@ export function Component({did}: {did: string}) {
if (profileError) {
return (
<ErrorScreen
title="Oops!"
title={_(msg`Oops!`)}
message={cleanError(profileError)}
onPressTryAgain={refetchProfile}
/>
@@ -55,8 +58,8 @@ export function Component({did}: {did: string}) {
// should never happen
return (
<ErrorScreen
title="Oops!"
message="Something went wrong and we're not sure what."
title={_(msg`Oops!`)}
message={_(msg`Something went wrong and we're not sure what.`)}
onPressTryAgain={refetchProfile}
/>
)
@@ -104,7 +107,7 @@ function ComponentLoaded({
<>
<InfoCircleIcon size={21} style={pal.textLight} />
<ThemedText type="xl" fg="light">
Swipe up to see more
<Trans>Swipe up to see more</Trans>
</ThemedText>
</>
)}
+19 -5
View File
@@ -37,11 +37,23 @@ export function Component({
style={[styles.actionBtn]}
onPress={onRepost}
accessibilityRole="button"
accessibilityLabel={isReposted ? 'Undo repost' : 'Repost'}
accessibilityHint={isReposted ? 'Remove repost' : 'Repost '}>
accessibilityLabel={
isReposted
? _(msg`Undo repost`)
: _(msg({message: `Repost`, context: 'action'}))
}
accessibilityHint={
isReposted
? _(msg`Remove repost`)
: _(msg({message: `Repost`, context: 'action'}))
}>
<RepostIcon strokeWidth={2} size={24} style={s.blue3} />
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
<Trans>{!isReposted ? 'Repost' : 'Undo repost'}</Trans>
{!isReposted ? (
<Trans context="action">Repost</Trans>
) : (
<Trans>Undo repost</Trans>
)}
</Text>
</TouchableOpacity>
<TouchableOpacity
@@ -49,11 +61,13 @@ export function Component({
style={[styles.actionBtn]}
onPress={onQuote}
accessibilityRole="button"
accessibilityLabel={_(msg`Quote post`)}
accessibilityLabel={_(
msg({message: `Quote post`, context: 'action'}),
)}
accessibilityHint="">
<FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} />
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
<Trans>Quote Post</Trans>
<Trans context="action">Quote Post</Trans>
</Text>
</TouchableOpacity>
</View>
+4 -4
View File
@@ -92,7 +92,7 @@ export function Component({
testID="sexualLabelBtn"
selected={selected.includes('sexual')}
left
label="Suggestive"
label={_(msg`Suggestive`)}
onSelect={() => toggleAdultLabel('sexual')}
accessibilityHint=""
style={s.flex1}
@@ -100,7 +100,7 @@ export function Component({
<SelectableBtn
testID="nudityLabelBtn"
selected={selected.includes('nudity')}
label="Nudity"
label={_(msg`Nudity`)}
onSelect={() => toggleAdultLabel('nudity')}
accessibilityHint=""
style={s.flex1}
@@ -108,7 +108,7 @@ export function Component({
<SelectableBtn
testID="pornLabelBtn"
selected={selected.includes('porn')}
label="Porn"
label={_(msg`Porn`)}
right
onSelect={() => toggleAdultLabel('porn')}
accessibilityHint=""
@@ -154,7 +154,7 @@ export function Component({
accessibilityLabel={_(msg`Confirm`)}
accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}>
<Trans>Done</Trans>
<Trans context="action">Done</Trans>
</Text>
</TouchableOpacity>
</View>
+4 -2
View File
@@ -101,7 +101,9 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
onChangeText={setCustomUrl}
accessibilityLabel={_(msg`Custom domain`)}
// TODO: Simplify this wording further to be understandable by everyone
accessibilityHint="Use your domain as your Bluesky client service provider"
accessibilityHint={_(
msg`Use your domain as your Bluesky client service provider`,
)}
/>
<TouchableOpacity
testID="customServerSelectBtn"
@@ -110,7 +112,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
accessibilityRole="button"
accessibilityLabel={`Confirm service. ${
customUrl === ''
? 'Button disabled. Input custom domain to proceed.'
? _(msg`Button disabled. Input custom domain to proceed.`)
: ''
}`}
accessibilityHint=""
+5 -3
View File
@@ -62,7 +62,9 @@ function SwitchAccountCard({account}: {account: SessionAccount}) {
onPress={isSwitchingAccounts ? undefined : onPressSignout}
accessibilityRole="button"
accessibilityLabel={_(msg`Sign out`)}
accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}>
accessibilityHint={_(
msg`Signs ${profile?.displayName} out of Bluesky`,
)}>
<Text type="lg" style={pal.link}>
<Trans>Sign out</Trans>
</Text>
@@ -92,8 +94,8 @@ function SwitchAccountCard({account}: {account: SessionAccount}) {
isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account)
}
accessibilityRole="button"
accessibilityLabel={`Switch to ${account.handle}`}
accessibilityHint="Switches the account you are logged in to">
accessibilityLabel={_(msg`Switch to ${account.handle}`)}
accessibilityHint={_(msg`Switches the account you are logged in to`)}>
{contents}
</TouchableOpacity>
)
+2 -2
View File
@@ -126,10 +126,10 @@ export function Component({
}}
style={styles.btn}
accessibilityRole="button"
accessibilityLabel={_(msg`Done`)}
accessibilityLabel={_(msg({message: `Done`, context: 'action'}))}
accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}>
<Trans>Done</Trans>
<Trans context="action">Done</Trans>
</Text>
</TouchableOpacity>
</View>
+18 -8
View File
@@ -76,10 +76,10 @@ export function Component({
type="default"
onPress={onPressDone}
style={styles.footerBtn}
accessibilityLabel={_(msg`Done`)}
accessibilityLabel={_(msg({message: `Done`, context: 'action'}))}
accessibilityHint=""
onAccessibilityEscape={onPressDone}
label="Done"
label={_(msg({message: `Done`, context: 'action'}))}
/>
</View>
</View>
@@ -175,12 +175,22 @@ function ListItem({
{sanitizeDisplayName(list.name)}
</Text>
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
{list.purpose === 'app.bsky.graph.defs#curatelist' && 'User list '}
{list.purpose === 'app.bsky.graph.defs#modlist' && 'Moderation list '}
by{' '}
{list.creator.did === currentAccount?.did
? 'you'
: sanitizeHandle(list.creator.handle, '@')}
{list.purpose === 'app.bsky.graph.defs#curatelist' &&
(list.creator.did === currentAccount?.did ? (
<Trans>User list by you</Trans>
) : (
<Trans>
User list by {sanitizeHandle(list.creator.handle, '@')}
</Trans>
))}
{list.purpose === 'app.bsky.graph.defs#modlist' &&
(list.creator.did === currentAccount?.did ? (
<Trans>Moderation list by you</Trans>
) : (
<Trans>
Moderation list by {sanitizeHandle(list.creator.handle, '@')}
</Trans>
))}
</Text>
</View>
<View>

Some files were not shown because too many files have changed in this diff Show More