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:
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
@@ -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
-1
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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)'},
|
||||
|
||||
+1871
-367
File diff suppressed because it is too large
Load Diff
+1858
-370
File diff suppressed because it is too large
Load Diff
+1871
-367
File diff suppressed because it is too large
Load Diff
+1871
-367
File diff suppressed because it is too large
Load Diff
+1866
-378
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1871
-367
File diff suppressed because it is too large
Load Diff
+1587
-560
File diff suppressed because it is too large
Load Diff
+1867
-379
File diff suppressed because it is too large
Load Diff
+1900
-412
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -35,4 +35,5 @@ export interface CachedFeedPage {
|
||||
usableInFeed: boolean
|
||||
syncedAt: Date
|
||||
data: FeedPage | undefined
|
||||
unreadCount: number
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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(
|
||||
() => ({
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface ComposerOptsPostRef {
|
||||
displayName?: string
|
||||
avatar?: string
|
||||
}
|
||||
embed?: AppBskyEmbedRecord.ViewRecord['embed']
|
||||
}
|
||||
export interface ComposerOptsQuote {
|
||||
uri: string
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>{' '}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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=""
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
_,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
@@ -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'}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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]}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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=""
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user