Merge remote-tracking branch 'upstream/main' into Improve-notification-localization

This commit is contained in:
Minseo Lee
2024-07-25 14:45:00 +09:00
95 changed files with 11824 additions and 10509 deletions
+3 -2
View File
@@ -132,7 +132,8 @@ appId: xyz.blueskyweb.app
id: "feedItem-by-bob.test" id: "feedItem-by-bob.test"
- tapOn: - tapOn:
id: "e2eGotoFeeds" id: "e2eGotoFeeds"
- tapOn: "Good Ppl" - tapOn:
id: "saved-feed-Good Ppl"
- assertVisible: - assertVisible:
id: "feedItem-by-bob.test" id: "feedItem-by-bob.test"
- tapOn: - tapOn:
@@ -168,7 +169,7 @@ appId: xyz.blueskyweb.app
id: "profilePager-selector" id: "profilePager-selector"
direction: LEFT direction: LEFT
- tapOn: - tapOn:
id: "profilePager-selector-5" id: "profilePager-selector-6"
- tapOn: "Good Ppl" - tapOn: "Good Ppl"
- tapOn: - tapOn:
+1
View File
@@ -197,6 +197,7 @@ func serve(cctx *cli.Context) error {
e.GET("/search", server.WebGeneric) e.GET("/search", server.WebGeneric)
e.GET("/feeds", server.WebGeneric) e.GET("/feeds", server.WebGeneric)
e.GET("/notifications", server.WebGeneric) e.GET("/notifications", server.WebGeneric)
e.GET("/notifications/settings", server.WebGeneric)
e.GET("/lists", server.WebGeneric) e.GET("/lists", server.WebGeneric)
e.GET("/moderation", server.WebGeneric) e.GET("/moderation", server.WebGeneric)
e.GET("/moderation/modlists", server.WebGeneric) e.GET("/moderation/modlists", server.WebGeneric)
+1 -1
View File
@@ -52,7 +52,7 @@
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web" "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.12.23", "@atproto/api": "0.12.25",
"@bam.tech/react-native-image-resizer": "^3.0.4", "@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
+4 -1
View File
@@ -95,7 +95,10 @@ function InnerApp() {
useEffect(() => { useEffect(() => {
return listenSessionDropped(() => { return listenSessionDropped(() => {
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`)) Toast.show(
_(msg`Sorry! Your session expired. Please log in again.`),
'info',
)
}) })
}, [_]) }, [_])
+4 -1
View File
@@ -77,7 +77,10 @@ function InnerApp() {
useEffect(() => { useEffect(() => {
return listenSessionDropped(() => { return listenSessionDropped(() => {
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`)) Toast.show(
_(msg`Sorry! Your session expired. Please log in again.`),
'info',
)
}) })
}, [_]) }, [_])
+6
View File
@@ -76,6 +76,7 @@ import {LogScreen} from './view/screens/Log'
import {ModerationModlistsScreen} from './view/screens/ModerationModlists' import {ModerationModlistsScreen} from './view/screens/ModerationModlists'
import {NotFoundScreen} from './view/screens/NotFound' import {NotFoundScreen} from './view/screens/NotFound'
import {NotificationsScreen} from './view/screens/Notifications' import {NotificationsScreen} from './view/screens/Notifications'
import {NotificationsSettingsScreen} from './view/screens/NotificationsSettings'
import {PostLikedByScreen} from './view/screens/PostLikedBy' import {PostLikedByScreen} from './view/screens/PostLikedBy'
import {PostRepostedByScreen} from './view/screens/PostRepostedBy' import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
import {PostThreadScreen} from './view/screens/PostThread' import {PostThreadScreen} from './view/screens/PostThread'
@@ -324,6 +325,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => MessagesSettingsScreen} getComponent={() => MessagesSettingsScreen}
options={{title: title(msg`Chat settings`), requireAuth: true}} options={{title: title(msg`Chat settings`), requireAuth: true}}
/> />
<Stack.Screen
name="NotificationsSettings"
getComponent={() => NotificationsSettingsScreen}
options={{title: title(msg`Notification settings`), requireAuth: true}}
/>
<Stack.Screen <Stack.Screen
name="Feeds" name="Feeds"
getComponent={() => FeedsScreen} getComponent={() => FeedsScreen}
+3
View File
@@ -145,6 +145,9 @@ export const atoms = {
flex_shrink: { flex_shrink: {
flexShrink: 1, flexShrink: 1,
}, },
flex_shrink_0: {
flexShrink: 0,
},
justify_start: { justify_start: {
justifyContent: 'flex-start', justifyContent: 'flex-start',
}, },
+3 -3
View File
@@ -264,9 +264,9 @@ function SaveButtonInner({
]) ])
} }
Toast.show(_(msg`Feeds updated!`)) Toast.show(_(msg`Feeds updated!`))
} catch (e: any) { } catch (err: any) {
logger.error(e, {context: `FeedCard: failed to update feeds`, pin}) logger.error(err, {context: `FeedCard: failed to update feeds`, pin})
Toast.show(_(msg`Failed to update feeds`)) Toast.show(_(msg`Failed to update feeds`), 'xmark')
} }
}, },
[_, pin, saveFeeds, removeFeed, uri, savedFeedConfig, type], [_, pin, saveFeeds, removeFeed, uri, savedFeedConfig, type],
+1 -1
View File
@@ -189,7 +189,7 @@ let ListMaybePlaceholder = ({
return ( return (
<Error <Error
title={errorTitle ?? _(msg`Oops!`)} title={errorTitle ?? _(msg`Oops!`)}
message={errorMessage ?? _(`Something went wrong!`)} message={errorMessage ?? _(msg`Something went wrong!`)}
onRetry={onRetry} onRetry={onRetry}
onGoBack={onGoBack} onGoBack={onGoBack}
sideBorders={sideBorders} sideBorders={sideBorders}
+6 -6
View File
@@ -306,9 +306,9 @@ export function FollowButtonInner({
)}`, )}`,
), ),
) )
} catch (e: any) { } catch (err: any) {
if (e?.name !== 'AbortError') { if (err?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`)) Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
} }
} }
} }
@@ -326,9 +326,9 @@ export function FollowButtonInner({
)}`, )}`,
), ),
) )
} catch (e: any) { } catch (err: any) {
if (e?.name !== 'AbortError') { if (err?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`)) Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
} }
} }
} }
+4 -1
View File
@@ -67,7 +67,10 @@ export function QrCodeDialog({
try { try {
await createAssetAsync(`file://${uri}`) await createAssetAsync(`file://${uri}`)
} catch (e: unknown) { } catch (e: unknown) {
Toast.show(_(msg`An error occurred while saving the QR code!`)) Toast.show(
_(msg`An error occurred while saving the QR code!`),
'xmark',
)
logger.error('Failed to save QR code', {error: e}) logger.error('Failed to save QR code', {error: e})
return return
} }
+2 -1
View File
@@ -66,6 +66,7 @@ function ShareDialogInner({
if (!res) { if (!res) {
Toast.show( Toast.show(
_(msg`You must grant access to your photo library to save the image.`), _(msg`You must grant access to your photo library to save the image.`),
'xmark',
) )
return return
} }
@@ -75,7 +76,7 @@ function ShareDialogInner({
Toast.show(_(msg`Image saved to your camera roll!`)) Toast.show(_(msg`Image saved to your camera roll!`))
control.close() control.close()
} catch (e: unknown) { } catch (e: unknown) {
Toast.show(_(msg`An error occurred while saving the QR code!`)) Toast.show(_(msg`An error occurred while saving the QR code!`), 'xmark')
logger.error('Failed to save QR code', {error: e}) logger.error('Failed to save QR code', {error: e})
return return
} }
+1
View File
@@ -115,6 +115,7 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
_( _(
msg`There was an issue. Please check your internet connection and try again.`, msg`There was an issue. Please check your internet connection and try again.`,
), ),
'xmark',
) )
logger.error('Failed to edit threadgate', {message: err}) logger.error('Failed to edit threadgate', {message: err})
} }
+1 -1
View File
@@ -85,7 +85,7 @@ let ConvoMenu = ({
} }
}, },
onError: () => { onError: () => {
Toast.show(_(msg`Could not mute chat`)) Toast.show(_(msg`Could not mute chat`), 'xmark')
}, },
}) })
+1 -1
View File
@@ -36,7 +36,7 @@ export function LeaveConvoPrompt({
} }
}, },
onError: () => { onError: () => {
Toast.show(_(msg`Could not leave chat`)) Toast.show(_(msg`Could not leave chat`), 'xmark')
}, },
}) })
+1 -1
View File
@@ -54,7 +54,7 @@ export let MessageMenu = ({
) )
Clipboard.setStringAsync(str) Clipboard.setStringAsync(str)
Toast.show(_(msg`Copied to clipboard`)) Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
}, [_, message.text, message.facets]) }, [_, message.text, message.facets])
const onPressTranslateMessage = React.useCallback(() => { const onPressTranslateMessage = React.useCallback(() => {
+1 -1
View File
@@ -57,7 +57,7 @@ function DialogInner({
const [initialized, setInitialzed] = React.useState(false) const [initialized, setInitialzed] = React.useState(false)
const {mutate: updateDeclaration} = useUpdateActorDeclaration({ const {mutate: updateDeclaration} = useUpdateActorDeclaration({
onError: () => { onError: () => {
Toast.show(_(msg`Failed to update settings`)) Toast.show(_(msg`Failed to update settings`), 'xmark')
}, },
}) })
+3 -1
View File
@@ -2,6 +2,7 @@ import React, {useCallback} from 'react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import {logEvent} from 'lib/statsig/statsig' import {logEvent} from 'lib/statsig/statsig'
import {FAB} from '#/view/com/util/fab/FAB' import {FAB} from '#/view/com/util/fab/FAB'
@@ -31,7 +32,8 @@ export function NewChat({
logEvent('chat:open', {logContext: 'NewChatDialog'}) logEvent('chat:open', {logContext: 'NewChatDialog'})
}, },
onError: error => { onError: error => {
Toast.show(error.message) logger.error('Failed to create chat', {safeMessage: error})
Toast.show(_(msg`An issue occurred starting the chat`), 'xmark')
}, },
}) })
@@ -2,6 +2,7 @@ import React, {useCallback} from 'react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import {logEvent} from 'lib/statsig/statsig' import {logEvent} from 'lib/statsig/statsig'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
@@ -43,7 +44,11 @@ function SendViaChatDialogInner({
logEvent('chat:open', {logContext: 'SendViaChatDialog'}) logEvent('chat:open', {logContext: 'SendViaChatDialog'})
}, },
onError: error => { onError: error => {
Toast.show(error.message) logger.error('Failed to share post to chat', {message: error})
Toast.show(
_(msg`An issue occurred while trying to open the chat`),
'xmark',
)
}, },
}) })
+20 -14
View File
@@ -1,6 +1,8 @@
import React from 'react' import React from 'react'
import {Pressable, View, ViewStyle} from 'react-native' import {Pressable, View, ViewStyle} from 'react-native'
import Animated, {LinearTransition} from 'react-native-reanimated'
import {isNative} from '#/platform/detection'
import {HITSLOP_10} from 'lib/constants' import {HITSLOP_10} from 'lib/constants'
import { import {
atoms as a, atoms as a,
@@ -244,6 +246,7 @@ export function LabelText({
<Text <Text
style={[ style={[
a.font_bold, a.font_bold,
a.leading_tight,
{ {
userSelect: 'none', userSelect: 'none',
color: disabled color: disabled
@@ -251,7 +254,7 @@ export function LabelText({
: t.atoms.text_contrast_high.color, : t.atoms.text_contrast_high.color,
}, },
native({ native({
paddingTop: 3, paddingTop: 2,
}), }),
flatten(style), flatten(style),
]}> ]}>
@@ -344,10 +347,10 @@ export function Checkbox() {
style={[ style={[
a.justify_center, a.justify_center,
a.align_center, a.align_center,
a.border,
a.rounded_xs, a.rounded_xs,
t.atoms.border_contrast_high, t.atoms.border_contrast_high,
{ {
borderWidth: 1,
height: 20, height: 20,
width: 20, width: 20,
}, },
@@ -375,34 +378,35 @@ export function Switch() {
<View <View
style={[ style={[
a.relative, a.relative,
a.border,
a.rounded_full, a.rounded_full,
t.atoms.bg, t.atoms.bg,
t.atoms.border_contrast_high, t.atoms.border_contrast_high,
{ {
borderWidth: 1,
height: 20, height: 20,
width: 30, width: 32,
padding: 2,
}, },
baseStyles, baseStyles,
hovered ? baseHoverStyles : {}, hovered ? baseHoverStyles : {},
]}> ]}>
<View <Animated.View
layout={LinearTransition.duration(100)}
style={[ style={[
a.absolute,
a.rounded_full, a.rounded_full,
{ {
height: 12, height: 14,
width: 12, width: 14,
top: 3,
left: 3,
backgroundColor: t.palette.contrast_400,
}, },
selected selected
? { ? {
backgroundColor: t.palette.primary_500, backgroundColor: t.palette.primary_500,
left: 13, alignSelf: 'flex-end',
} }
: {}, : {
backgroundColor: t.palette.contrast_400,
alignSelf: 'flex-start',
},
indicatorStyles, indicatorStyles,
]} ]}
/> />
@@ -428,10 +432,10 @@ export function Radio() {
style={[ style={[
a.justify_center, a.justify_center,
a.align_center, a.align_center,
a.border,
a.rounded_full, a.rounded_full,
t.atoms.border_contrast_high, t.atoms.border_contrast_high,
{ {
borderWidth: 1,
height: 20, height: 20,
width: 20, width: 20,
}, },
@@ -456,3 +460,5 @@ export function Radio() {
</View> </View>
) )
} }
export const Platform = isNative ? Switch : Checkbox
+2 -2
View File
@@ -32,7 +32,7 @@ export function useFollowMethods({
} catch (e: any) { } catch (e: any) {
logger.error(`useFollowMethods: failed to follow`, {message: String(e)}) logger.error(`useFollowMethods: failed to follow`, {message: String(e)})
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`)) Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
} }
} }
}) })
@@ -47,7 +47,7 @@ export function useFollowMethods({
message: String(e), message: String(e),
}) })
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`)) Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
} }
} }
}) })
-5
View File
@@ -1,5 +0,0 @@
import {createSinglePathSVG} from './TEMPLATE'
export const SettingsGear2_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M11.1 2a1 1 0 0 0-.832.445L8.851 4.57 6.6 4.05a1 1 0 0 0-.932.268l-1.35 1.35a1 1 0 0 0-.267.932l.52 2.251-2.126 1.417A1 1 0 0 0 2 11.1v1.8a1 1 0 0 0 .445.832l2.125 1.417-.52 2.251a1 1 0 0 0 .268.932l1.35 1.35a1 1 0 0 0 .932.267l2.251-.52 1.417 2.126A1 1 0 0 0 11.1 22h1.8a1 1 0 0 0 .832-.445l1.417-2.125 2.251.52a1 1 0 0 0 .932-.268l1.35-1.35a1 1 0 0 0 .267-.932l-.52-2.251 2.126-1.417A1 1 0 0 0 22 12.9v-1.8a1 1 0 0 0-.445-.832L19.43 8.851l.52-2.251a1 1 0 0 0-.268-.932l-1.35-1.35a1 1 0 0 0-.932-.267l-2.251.52-1.417-2.126A1 1 0 0 0 12.9 2h-1.8Zm-.968 4.255L11.635 4h.73l1.503 2.255a1 1 0 0 0 1.057.42l2.385-.551.566.566-.55 2.385a1 1 0 0 0 .42 1.057L20 11.635v.73l-2.255 1.503a1 1 0 0 0-.42 1.057l.551 2.385-.566.566-2.385-.55a1 1 0 0 0-1.057.42L12.365 20h-.73l-1.503-2.255a1 1 0 0 0-1.057-.42l-2.385.551-.566-.566.55-2.385a1 1 0 0 0-.42-1.057L4 12.365v-.73l2.255-1.503a1 1 0 0 0 .42-1.057L6.123 6.69l.566-.566 2.385.55a1 1 0 0 0 1.057-.42ZM8 12a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm4-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z',
})
@@ -241,7 +241,7 @@ function AppealForm({
}, },
onError: err => { onError: err => {
logger.error('Failed to submit label appeal', {message: err}) logger.error('Failed to submit label appeal', {message: err})
Toast.show(_(msg`Failed to submit appeal, please try again.`)) Toast.show(_(msg`Failed to submit appeal, please try again.`), 'xmark')
}, },
onSuccess: () => { onSuccess: () => {
control.close() control.close()
+6 -4
View File
@@ -1,10 +1,10 @@
import {timeout} from './timeout' import {timeout} from './timeout'
export async function until( export async function until<T>(
retries: number, retries: number,
delay: number, delay: number,
cond: (v: any, err: any) => boolean, cond: (v: T, err: any) => boolean,
fn: () => Promise<any>, fn: () => Promise<T>,
): Promise<boolean> { ): Promise<boolean> {
while (retries > 0) { while (retries > 0) {
try { try {
@@ -13,7 +13,9 @@ export async function until(
return true return true
} }
} catch (e: any) { } catch (e: any) {
if (cond(undefined, e)) { // TODO: change the type signature of cond to accept undefined
// however this breaks every existing usage of until -sfn
if (cond(undefined as unknown as T, e)) {
return true return true
} }
} }
+2 -2
View File
@@ -17,14 +17,14 @@ export async function openPicker(opts?: ImagePickerOptions) {
}) })
if (response.assets && response.assets.length > 4) { if (response.assets && response.assets.length > 4) {
Toast.show('You may only select up to 4 images') Toast.show('You may only select up to 4 images', 'exclamation-circle')
} }
return (response.assets ?? []) return (response.assets ?? [])
.slice(0, 4) .slice(0, 4)
.filter(asset => { .filter(asset => {
if (asset.mimeType?.startsWith('image/')) return true if (asset.mimeType?.startsWith('image/')) return true
Toast.show('Only image files are supported') Toast.show('Only image files are supported', 'exclamation-circle')
return false return false
}) })
.map(image => ({ .map(image => ({
+6 -9
View File
@@ -42,14 +42,13 @@ export type CommonNavigatorParams = {
Hashtag: {tag: string; author?: string} Hashtag: {tag: string; author?: string}
MessagesConversation: {conversation: string; embed?: string} MessagesConversation: {conversation: string; embed?: string}
MessagesSettings: undefined MessagesSettings: undefined
NotificationsSettings: undefined
Feeds: undefined Feeds: undefined
Start: {name: string; rkey: string} Start: {name: string; rkey: string}
StarterPack: {name: string; rkey: string; new?: boolean} StarterPack: {name: string; rkey: string; new?: boolean}
StarterPackShort: {code: string} StarterPackShort: {code: string}
StarterPackWizard: undefined StarterPackWizard: undefined
StarterPackEdit: { StarterPackEdit: {rkey?: string}
rkey?: string
}
} }
export type BottomTabNavigatorParams = CommonNavigatorParams & { export type BottomTabNavigatorParams = CommonNavigatorParams & {
@@ -69,7 +68,7 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & {
} }
export type NotificationsTabNavigatorParams = CommonNavigatorParams & { export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
Notifications: undefined Notifications: {show?: 'all'}
} }
export type MyProfileTabNavigatorParams = CommonNavigatorParams & { export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
@@ -84,7 +83,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & {
Home: undefined Home: undefined
Search: {q?: string} Search: {q?: string}
Feeds: undefined Feeds: undefined
Notifications: undefined Notifications: {show?: 'all'}
Hashtag: {tag: string; author?: string} Hashtag: {tag: string; author?: string}
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
} }
@@ -96,7 +95,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
Search: {q?: string} Search: {q?: string}
Feeds: undefined Feeds: undefined
NotificationsTab: undefined NotificationsTab: undefined
Notifications: undefined Notifications: {show?: 'all'}
MyProfileTab: undefined MyProfileTab: undefined
Hashtag: {tag: string; author?: string} Hashtag: {tag: string; author?: string}
MessagesTab: undefined MessagesTab: undefined
@@ -105,9 +104,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
StarterPack: {name: string; rkey: string; new?: boolean} StarterPack: {name: string; rkey: string; new?: boolean}
StarterPackShort: {code: string} StarterPackShort: {code: string}
StarterPackWizard: undefined StarterPackWizard: undefined
StarterPackEdit: { StarterPackEdit: {rkey?: string}
rkey?: string
}
} }
// NOTE // NOTE
+1 -1
View File
@@ -20,6 +20,6 @@ export async function shareUrl(url: string) {
// React Native Share is not supported by web. Web Share API // React Native Share is not supported by web. Web Share API
// has increasing but not full support, so default to clipboard // has increasing but not full support, so default to clipboard
setStringAsync(url) setStringAsync(url)
Toast.show('Copied to clipboard') Toast.show('Copied to clipboard', 'clipboard-check')
} }
} }
+1
View File
@@ -14,3 +14,4 @@ export type Gate =
| 'suggested_follows_interstitial' | 'suggested_follows_interstitial'
| 'ungroup_follow_backs' | 'ungroup_follow_backs'
| 'videos' | 'videos'
| 'small_avi_thumb'
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -5,6 +5,7 @@ export const router = new Router({
Search: '/search', Search: '/search',
Feeds: '/feeds', Feeds: '/feeds',
Notifications: '/notifications', Notifications: '/notifications',
NotificationsSettings: '/notifications/settings',
Settings: '/settings', Settings: '/settings',
LanguageSettings: '/settings/language', LanguageSettings: '/settings/language',
Lists: '/lists', Lists: '/lists',
@@ -84,7 +84,7 @@ function DialogInner() {
}, },
onError: err => { onError: err => {
logger.error('Failed to submit chat appeal', {message: err}) logger.error('Failed to submit chat appeal', {message: err})
Toast.show(_(msg`Failed to submit appeal, please try again.`)) Toast.show(_(msg`Failed to submit appeal, please try again.`), 'xmark')
}, },
onSuccess: () => { onSuccess: () => {
control.close() control.close()
@@ -67,7 +67,7 @@ export function MessageInput({
return return
} }
if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) { if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
Toast.show(_(msg`Message is too long`)) Toast.show(_(msg`Message is too long`), 'xmark')
return return
} }
clearDraft() clearDraft()
@@ -46,7 +46,7 @@ export function MessageInput({
return return
} }
if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) { if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
Toast.show(_(msg`Message is too long`)) Toast.show(_(msg`Message is too long`), 'xmark')
return return
} }
clearDraft() clearDraft()
@@ -106,6 +106,7 @@ function Inner() {
title={_(msg`Something went wrong`)} title={_(msg`Something went wrong`)}
message={_(msg`We couldn't load this conversation`)} message={_(msg`We couldn't load this conversation`)}
onRetry={() => convoState.error.retry()} onRetry={() => convoState.error.retry()}
sideBorders={false}
/> />
</CenteredView> </CenteredView>
) )
+1 -1
View File
@@ -309,7 +309,7 @@ function DesktopHeader({
a.gap_lg, a.gap_lg,
a.px_lg, a.px_lg,
a.pr_md, a.pr_md,
a.py_md, a.py_sm,
a.border_b, a.border_b,
t.atoms.border_contrast_low, t.atoms.border_contrast_low,
]}> ]}>
+5 -5
View File
@@ -11,7 +11,7 @@ import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {ViewHeader} from '#/view/com/util/ViewHeader' import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views' import {ScrollView} from '#/view/com/util/Views'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
@@ -32,7 +32,7 @@ export function MessagesSettingsScreen({}: Props) {
const {mutate: updateDeclaration} = useUpdateActorDeclaration({ const {mutate: updateDeclaration} = useUpdateActorDeclaration({
onError: () => { onError: () => {
Toast.show(_(msg`Failed to update settings`)) Toast.show(_(msg`Failed to update settings`), 'xmark')
}, },
}) })
@@ -55,7 +55,7 @@ export function MessagesSettingsScreen({}: Props) {
) )
return ( return (
<CenteredView sideBorders style={a.h_full_vh}> <ScrollView stickyHeaderIndices={[0]}>
<ViewHeader title={_(msg`Chat Settings`)} showOnDesktop showBorder /> <ViewHeader title={_(msg`Chat Settings`)} showOnDesktop showBorder />
<View style={[a.p_lg, a.gap_md]}> <View style={[a.p_lg, a.gap_md]}>
<Text style={[a.text_lg, a.font_bold]}> <Text style={[a.text_lg, a.font_bold]}>
@@ -107,7 +107,7 @@ export function MessagesSettingsScreen({}: Props) {
a.rounded_md, a.rounded_md,
t.atoms.bg_contrast_25, t.atoms.bg_contrast_25,
]}> ]}>
<Text style={[t.atoms.text_contrast_high]}> <Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
<Trans> <Trans>
You can continue ongoing conversations regardless of which setting You can continue ongoing conversations regardless of which setting
you choose. you choose.
@@ -149,6 +149,6 @@ export function MessagesSettingsScreen({}: Props) {
</> </>
)} )}
</View> </View>
</CenteredView> </ScrollView>
) )
} }
@@ -116,6 +116,7 @@ let ProfileHeaderLabeler = ({
_( _(
msg`There was an an issue contacting the server, please check your internet connection and try again.`, msg`There was an an issue contacting the server, please check your internet connection and try again.`,
), ),
'xmark',
) )
logger.error(`Failed to toggle labeler like`, {message: e.message}) logger.error(`Failed to toggle labeler like`, {message: e.message})
} }
@@ -104,7 +104,7 @@ let ProfileHeaderStandard = ({
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to follow', {message: String(e)}) logger.error('Failed to follow', {message: String(e)})
Toast.show(_(msg`There was an issue! ${e.toString()}`)) Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
} }
} }
}) })
@@ -126,7 +126,7 @@ let ProfileHeaderStandard = ({
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to unfollow', {message: String(e)}) logger.error('Failed to unfollow', {message: String(e)})
Toast.show(_(msg`There was an issue! ${e.toString()}`)) Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
} }
} }
}) })
@@ -140,7 +140,7 @@ let ProfileHeaderStandard = ({
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to unblock account', {message: e}) logger.error('Failed to unblock account', {message: e})
Toast.show(_(msg`There was an issue! ${e.toString()}`)) Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
} }
} }
}, [_, queueUnblock, track]) }, [_, queueUnblock, track])
@@ -359,7 +359,7 @@ function Header({
captureAction(ProgressGuideAction.Follow, dids.length) captureAction(ProgressGuideAction.Follow, dids.length)
Toast.show(_(msg`All accounts have been followed!`)) Toast.show(_(msg`All accounts have been followed!`))
} catch (e) { } catch (e) {
Toast.show(_(msg`An error occurred while trying to follow all`)) Toast.show(_(msg`An error occurred while trying to follow all`), 'xmark')
} finally { } finally {
setIsProcessing(false) setIsProcessing(false)
} }
@@ -672,7 +672,7 @@ function InvalidStarterPack({rkey}: {rkey: string}) {
onError: e => { onError: e => {
setIsProcessing(false) setIsProcessing(false)
logger.error('Failed to delete invalid starter pack', {safeMessage: e}) logger.error('Failed to delete invalid starter pack', {safeMessage: e})
Toast.show(_(msg`Failed to delete starter pack`)) Toast.show(_(msg`Failed to delete starter pack`), 'xmark')
}, },
}) })
+5 -2
View File
@@ -74,7 +74,10 @@ function reducer(state: State, action: Action): State {
break break
case 'AddProfile': case 'AddProfile':
if (state.profiles.length >= 51) { if (state.profiles.length >= 51) {
Toast.show(msg`You may only add up to 50 profiles`.message ?? '') Toast.show(
msg`You may only add up to 50 profiles`.message ?? '',
'info',
)
} else { } else {
updatedState = {...state, profiles: [...state.profiles, action.profile]} updatedState = {...state, profiles: [...state.profiles, action.profile]}
} }
@@ -89,7 +92,7 @@ function reducer(state: State, action: Action): State {
break break
case 'AddFeed': case 'AddFeed':
if (state.feeds.length >= 50) { if (state.feeds.length >= 50) {
Toast.show(msg`You may only add up to 50 feeds`.message ?? '') Toast.show(msg`You may only add up to 50 feeds`.message ?? '', 'info')
} else { } else {
updatedState = {...state, feeds: [...state.feeds, action.feed]} updatedState = {...state, feeds: [...state.feeds, action.feed]}
} }
+2 -2
View File
@@ -227,7 +227,7 @@ function WizardInner({
onError: e => { onError: e => {
logger.error('Failed to create starter pack', {safeMessage: e}) logger.error('Failed to create starter pack', {safeMessage: e})
dispatch({type: 'SetProcessing', processing: false}) dispatch({type: 'SetProcessing', processing: false})
Toast.show(_(msg`Failed to create starter pack`)) Toast.show(_(msg`Failed to create starter pack`), 'xmark')
}, },
}) })
const {mutate: editStarterPack} = useEditStarterPackMutation({ const {mutate: editStarterPack} = useEditStarterPackMutation({
@@ -235,7 +235,7 @@ function WizardInner({
onError: e => { onError: e => {
logger.error('Failed to edit starter pack', {safeMessage: e}) logger.error('Failed to edit starter pack', {safeMessage: e})
dispatch({type: 'SetProcessing', processing: false}) dispatch({type: 'SetProcessing', processing: false})
Toast.show(_(msg`Failed to create starter pack`)) Toast.show(_(msg`Failed to create starter pack`), 'xmark')
}, },
}) })
+22 -11
View File
@@ -45,9 +45,13 @@ export function useActorAutocompleteQuery(
}, },
select: React.useCallback( select: React.useCallback(
(data: AppBskyActorDefs.ProfileViewBasic[]) => { (data: AppBskyActorDefs.ProfileViewBasic[]) => {
return computeSuggestions(data, moderationOpts || DEFAULT_MOD_OPTS) return computeSuggestions({
q: prefix,
searched: data,
moderationOpts: moderationOpts || DEFAULT_MOD_OPTS,
})
}, },
[moderationOpts], [prefix, moderationOpts],
), ),
placeholderData: maintainData ? keepPreviousData : undefined, placeholderData: maintainData ? keepPreviousData : undefined,
}) })
@@ -81,19 +85,25 @@ export function useActorAutocompleteFn() {
} }
} }
return computeSuggestions( return computeSuggestions({
res?.data.actors, q: query,
moderationOpts || DEFAULT_MOD_OPTS, searched: res?.data.actors,
) moderationOpts: moderationOpts || DEFAULT_MOD_OPTS,
})
}, },
[queryClient, moderationOpts, agent], [queryClient, moderationOpts, agent],
) )
} }
function computeSuggestions( function computeSuggestions({
searched: AppBskyActorDefs.ProfileViewBasic[] = [], q,
moderationOpts: ModerationOpts, searched = [],
) { moderationOpts,
}: {
q?: string
searched?: AppBskyActorDefs.ProfileViewBasic[]
moderationOpts: ModerationOpts
}) {
let items: AppBskyActorDefs.ProfileViewBasic[] = [] let items: AppBskyActorDefs.ProfileViewBasic[] = []
for (const item of searched) { for (const item of searched) {
if (!items.find(item2 => item2.handle === item.handle)) { if (!items.find(item2 => item2.handle === item.handle)) {
@@ -102,6 +112,7 @@ function computeSuggestions(
} }
return items.filter(profile => { return items.filter(profile => {
const modui = moderateProfile(profile, moderationOpts).ui('profileList') const modui = moderateProfile(profile, moderationOpts).ui('profileList')
return !modui.filter || isJustAMute(modui) const isExactMatch = q && profile.handle.toLowerCase() === q
return isExactMatch || !modui.filter || isJustAMute(modui)
}) })
} }
+22 -15
View File
@@ -46,11 +46,14 @@ const PAGE_SIZE = 30
type RQPageParam = string | undefined type RQPageParam = string | undefined
const RQKEY_ROOT = 'notification-feed' const RQKEY_ROOT = 'notification-feed'
export function RQKEY() { export function RQKEY(priority?: false) {
return [RQKEY_ROOT] return [RQKEY_ROOT, priority]
} }
export function useNotificationFeedQuery(opts?: {enabled?: boolean}) { export function useNotificationFeedQuery(opts?: {
enabled?: boolean
overridePriorityNotifications?: boolean
}) {
const agent = useAgent() const agent = useAgent()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
@@ -59,6 +62,10 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
const lastPageCountRef = useRef(0) const lastPageCountRef = useRef(0)
const gate = useGate() const gate = useGate()
// false: force showing all notifications
// undefined: let the server decide
const priority = opts?.overridePriorityNotifications ? false : undefined
const query = useInfiniteQuery< const query = useInfiniteQuery<
FeedPage, FeedPage,
Error, Error,
@@ -67,7 +74,7 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
RQPageParam RQPageParam
>({ >({
staleTime: STALE.INFINITY, staleTime: STALE.INFINITY,
queryKey: RQKEY(), queryKey: RQKEY(priority),
async queryFn({pageParam}: {pageParam: RQPageParam}) { async queryFn({pageParam}: {pageParam: RQPageParam}) {
let page let page
if (!pageParam) { if (!pageParam) {
@@ -75,17 +82,17 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
page = unreads.getCachedUnreadPage() page = unreads.getCachedUnreadPage()
} }
if (!page) { if (!page) {
page = ( const {page: fetchedPage} = await fetchPage({
await fetchPage({ agent,
agent, limit: PAGE_SIZE,
limit: PAGE_SIZE, cursor: pageParam,
cursor: pageParam, queryClient,
queryClient, moderationOpts,
moderationOpts, fetchAdditionalData: true,
fetchAdditionalData: true, shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'),
shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'), priority,
}) })
).page page = fetchedPage
} }
// if the first page has an unread, mark all read // if the first page has an unread, mark all read
@@ -0,0 +1,69 @@
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {until} from '#/lib/async/until'
import {logger} from '#/logger'
import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread'
import {useAgent} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
export function useNotificationsSettingsMutation() {
const {_} = useLingui()
const agent = useAgent()
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (keys: string[]) => {
const enabled = keys[0] === 'enabled'
await agent.api.app.bsky.notification.putPreferences({
priority: enabled,
})
await until(
5, // 5 tries
1e3, // 1s delay between tries
res => res.data.priority === enabled,
() => agent.api.app.bsky.notification.listNotifications({limit: 1}),
)
eagerlySetCachedPriority(queryClient, enabled)
},
onError: err => {
logger.error('Failed to save notification preferences', {
safeMessage: err,
})
Toast.show(
_(msg`Failed to save notification preferences, please try again`),
'xmark',
)
},
onSuccess: () => {
Toast.show(_(msg`Preference saved`))
},
onSettled: () => {
invalidateCachedUnreadPage()
queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS()})
},
})
}
function eagerlySetCachedPriority(
queryClient: ReturnType<typeof useQueryClient>,
enabled: boolean,
) {
queryClient.setQueryData(RQKEY_NOTIFS(), (old: any) => {
if (!old) return old
return {
...old,
pages: old.pages.map((page: any) => {
return {
...page,
priority: enabled,
}
}),
}
})
}
+1
View File
@@ -22,6 +22,7 @@ export interface FeedPage {
cursor: string | undefined cursor: string | undefined
seenAt: Date seenAt: Date
items: FeedNotification[] items: FeedNotification[]
priority: boolean
} }
export interface CachedFeedPage { export interface CachedFeedPage {
+7 -1
View File
@@ -39,10 +39,15 @@ export async function fetchPage({
moderationOpts: ModerationOpts | undefined moderationOpts: ModerationOpts | undefined
fetchAdditionalData: boolean fetchAdditionalData: boolean
shouldUngroupFollowBacks?: () => boolean shouldUngroupFollowBacks?: () => boolean
}): Promise<{page: FeedPage; indexedAt: string | undefined}> { priority?: boolean
}): Promise<{
page: FeedPage
indexedAt: string | undefined
}> {
const res = await agent.listNotifications({ const res = await agent.listNotifications({
limit, limit,
cursor, cursor,
// priority,
}) })
const indexedAt = res.data.notifications[0]?.indexedAt const indexedAt = res.data.notifications[0]?.indexedAt
@@ -88,6 +93,7 @@ export async function fetchPage({
cursor: res.data.cursor, cursor: res.data.cursor,
seenAt, seenAt,
items: notifsGrouped, items: notifsGrouped,
priority: res.data.priority ?? false,
}, },
indexedAt, indexedAt,
} }
+1 -1
View File
@@ -24,7 +24,7 @@ export function useVideoState({setError}: {setError: (error: string) => void}) {
onError: (e: any) => { onError: (e: any) => {
// Don't log these errors in sentry, just let the user know // Don't log these errors in sentry, just let the user know
if (e instanceof VideoTooLargeError) { if (e instanceof VideoTooLargeError) {
Toast.show(_(msg`Videos cannot be larger than 100MB`)) Toast.show(_(msg`Videos cannot be larger than 100MB`), 'xmark')
return return
} }
logger.error('Failed to compress video', {safeError: e}) logger.error('Failed to compress video', {safeError: e})
+2 -2
View File
@@ -125,7 +125,7 @@ export function FeedSourceCardLoaded({
]) ])
Toast.show(_(msg`Added to my feeds`)) Toast.show(_(msg`Added to my feeds`))
} catch (e) { } catch (e) {
Toast.show(_(msg`There was an issue contacting your server`)) Toast.show(_(msg`There was an issue contacting your server`), 'xmark')
logger.error('Failed to save feed', {message: e}) logger.error('Failed to save feed', {message: e})
} }
}, [_, feed, pinOnSave, addSavedFeeds, isSaved]) }, [_, feed, pinOnSave, addSavedFeeds, isSaved])
@@ -138,7 +138,7 @@ export function FeedSourceCardLoaded({
// await item.unsave() // await item.unsave()
Toast.show(_(msg`Removed from my feeds`)) Toast.show(_(msg`Removed from my feeds`))
} catch (e) { } catch (e) {
Toast.show(_(msg`There was an issue contacting your server`)) Toast.show(_(msg`There was an issue contacting your server`), 'xmark')
logger.error('Failed to unsave feed', {message: e}) logger.error('Failed to unsave feed', {message: e})
} }
}, [_, removeFeed, savedFeedConfig]) }, [_, removeFeed, savedFeedConfig])
+6 -2
View File
@@ -66,7 +66,10 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
const saveImageToAlbumWithToasts = React.useCallback( const saveImageToAlbumWithToasts = React.useCallback(
async (uri: string) => { async (uri: string) => {
if (!permissionResponse || permissionResponse.granted === false) { if (!permissionResponse || permissionResponse.granted === false) {
Toast.show(_(msg`Permission to access camera roll is required.`)) Toast.show(
_(msg`Permission to access camera roll is required.`),
'info',
)
if (permissionResponse?.canAskAgain) { if (permissionResponse?.canAskAgain) {
requestPermission() requestPermission()
} else { } else {
@@ -74,6 +77,7 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
_( _(
msg`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.`,
), ),
'xmark',
) )
} }
return return
@@ -83,7 +87,7 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
await saveImageToMediaLibrary({uri}) await saveImageToMediaLibrary({uri})
Toast.show(_(msg`Saved to your camera roll`)) Toast.show(_(msg`Saved to your camera roll`))
} catch (e: any) { } catch (e: any) {
Toast.show(_(msg`Failed to save image: ${String(e)}`)) Toast.show(_(msg`Failed to save image: ${String(e)}`), 'xmark')
} }
}, },
[permissionResponse, requestPermission, _], [permissionResponse, requestPermission, _],
+7 -6
View File
@@ -77,7 +77,7 @@ export function Component({}: {}) {
const onCopy = React.useCallback(() => { const onCopy = React.useCallback(() => {
if (appPassword) { if (appPassword) {
setStringAsync(appPassword) setStringAsync(appPassword)
Toast.show(_(msg`Copied to clipboard`)) Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
setWasCopied(true) setWasCopied(true)
} }
}, [appPassword, _]) }, [appPassword, _])
@@ -93,7 +93,7 @@ export function Component({}: {}) {
_( _(
msg`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', 'xmark',
) )
return return
} }
@@ -101,13 +101,13 @@ export function Component({}: {}) {
if (name.length < 4) { if (name.length < 4) {
Toast.show( Toast.show(
_(msg`App Password names must be at least 4 characters long.`), _(msg`App Password names must be at least 4 characters long.`),
'times', 'xmark',
) )
return return
} }
if (passwords?.find(p => p.name === name)) { if (passwords?.find(p => p.name === name)) {
Toast.show(_(msg`This name is already in use`), 'times') Toast.show(_(msg`This name is already in use`), 'xmark')
return return
} }
@@ -116,11 +116,11 @@ export function Component({}: {}) {
if (newPassword) { if (newPassword) {
setAppPassword(newPassword.password) setAppPassword(newPassword.password)
} else { } else {
Toast.show(_(msg`Failed to create app password.`), 'times') Toast.show(_(msg`Failed to create app password.`), 'xmark')
// TODO: better error handling (?) // TODO: better error handling (?)
} }
} catch (e) { } catch (e) {
Toast.show(_(msg`Failed to create app password.`), 'times') Toast.show(_(msg`Failed to create app password.`), 'xmark')
logger.error('Failed to create app password', {message: e}) logger.error('Failed to create app password', {message: e})
} }
} }
@@ -137,6 +137,7 @@ export function Component({}: {}) {
_( _(
msg`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.`,
), ),
'xmark',
) )
} }
} }
+1 -1
View File
@@ -317,7 +317,7 @@ function CustomHandleForm({
// = // =
const onPressCopy = React.useCallback(() => { const onPressCopy = React.useCallback(() => {
setStringAsync(isDNSForm ? `did=${currentAccount.did}` : currentAccount.did) setStringAsync(isDNSForm ? `did=${currentAccount.did}` : currentAccount.did)
Toast.show(_(msg`Copied to clipboard`)) Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
}, [currentAccount, isDNSForm, _]) }, [currentAccount, isDNSForm, _])
const onChangeHandle = React.useCallback( const onChangeHandle = React.useCallback(
(v: string) => { (v: string) => {
+1 -1
View File
@@ -150,7 +150,7 @@ function InviteCode({
const onPress = React.useCallback(() => { const onPress = React.useCallback(() => {
setStringAsync(invite.code) setStringAsync(invite.code)
Toast.show(_(msg`Copied to clipboard`)) Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
setInviteCopied(invite.code) setInviteCopied(invite.code)
}, [setInviteCopied, invite, _]) }, [setInviteCopied, invite, _])
+19 -18
View File
@@ -7,32 +7,33 @@ import {
View, View,
} from 'react-native' } from 'react-native'
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
import {ScrollView, TextInput} from './util'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from '../util/text/Text' import {msg, Trans} from '@lingui/macro'
import {Button} from '../util/forms/Button'
import {UserAvatar} from '../util/UserAvatar'
import * as Toast from '../util/Toast'
import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
import {cleanError} from 'lib/strings/errors'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {HITSLOP_20} from '#/lib/constants'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {HITSLOP_20} from '#/lib/constants'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import { import {
useDangerousListMembershipsQuery,
getMembership, getMembership,
ListMembersip, ListMembersip,
useDangerousListMembershipsQuery,
useListMembershipAddMutation, useListMembershipAddMutation,
useListMembershipRemoveMutation, useListMembershipRemoveMutation,
} from '#/state/queries/list-memberships' } from '#/state/queries/list-memberships'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {cleanError} from 'lib/strings/errors'
import {sanitizeHandle} from 'lib/strings/handles'
import {colors, s} from 'lib/styles'
import {isWeb} from 'platform/detection'
import {Button} from '../util/forms/Button'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar'
import {ScrollView, TextInput} from './util'
export const snapPoints = ['90%'] export const snapPoints = ['90%']
@@ -200,7 +201,7 @@ function UserResult({
onChange?.('remove', profile) onChange?.('remove', profile)
} }
} catch (e) { } catch (e) {
Toast.show(cleanError(e)) Toast.show(cleanError(e), 'xmark')
} finally { } finally {
setIsProcessing(false) setIsProcessing(false)
} }
+1 -1
View File
@@ -166,7 +166,7 @@ function ListItem({
onRemove?.(list.uri) onRemove?.(list.uri)
} }
} catch (e) { } catch (e) {
Toast.show(cleanError(e)) Toast.show(cleanError(e), 'xmark')
} finally { } finally {
setIsProcessing(false) setIsProcessing(false)
} }
+6 -1
View File
@@ -35,11 +35,13 @@ export function Feed({
onPressTryAgain, onPressTryAgain,
onScrolledDownChange, onScrolledDownChange,
ListHeaderComponent, ListHeaderComponent,
overridePriorityNotifications,
}: { }: {
scrollElRef?: ListRef scrollElRef?: ListRef
onPressTryAgain?: () => void onPressTryAgain?: () => void
onScrolledDownChange: (isScrolledDown: boolean) => void onScrolledDownChange: (isScrolledDown: boolean) => void
ListHeaderComponent?: () => JSX.Element ListHeaderComponent?: () => JSX.Element
overridePriorityNotifications?: boolean
}) { }) {
const initialNumToRender = useInitialNumToRender() const initialNumToRender = useInitialNumToRender()
@@ -59,7 +61,10 @@ export function Feed({
hasNextPage, hasNextPage,
isFetchingNextPage, isFetchingNextPage,
fetchNextPage, fetchNextPage,
} = useNotificationFeedQuery({enabled: !!moderationOpts}) } = useNotificationFeedQuery({
enabled: !!moderationOpts,
overridePriorityNotifications,
})
const isEmpty = !isFetching && !data?.pages[0]?.items.length const isEmpty = !isFetching && !data?.pages[0]?.items.length
const items = React.useMemo(() => { const items = React.useMemo(() => {
@@ -96,7 +96,7 @@ function PostThreadFollowBtnLoaded({
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to follow', {message: String(e)}) logger.error('Failed to follow', {message: String(e)})
Toast.show(_(msg`There was an issue! ${e.toString()}`)) Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
} }
} }
}) })
@@ -108,7 +108,7 @@ function PostThreadFollowBtnLoaded({
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to unfollow', {message: String(e)}) logger.error('Failed to unfollow', {message: String(e)})
Toast.show(_(msg`There was an issue! ${e.toString()}`)) Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
} }
} }
}) })
+1
View File
@@ -144,6 +144,7 @@ function FeedgenErrorMessage({
_l( _l(
msgLingui`There was an an issue removing this feed. Please check your internet connection and try again.`, msgLingui`There was an an issue removing this feed. Please check your internet connection and try again.`,
), ),
'exclamation-circle',
) )
logger.error('Failed to remove feed', {message: err}) logger.error('Failed to remove feed', {message: err})
} }
+2
View File
@@ -51,6 +51,7 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
_( _(
msg`There was an an issue updating your feeds, please check your internet connection and try again.`, msg`There was an an issue updating your feeds, please check your internet connection and try again.`,
), ),
'exclamation-circle',
) )
logger.error('Failed up update feeds', {message: err}) logger.error('Failed up update feeds', {message: err})
} }
@@ -69,6 +70,7 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
_( _(
msg`There was an an issue updating your feeds, please check your internet connection and try again.`, msg`There was an an issue updating your feeds, please check your internet connection and try again.`,
), ),
'exclamation-circle',
) )
logger.error('Failed up update feeds', {message: err}) logger.error('Failed up update feeds', {message: err})
} }
+2 -2
View File
@@ -56,12 +56,12 @@ let FeedSlice = ({
record={slice.items[last].record} record={slice.items[last].record}
reason={slice.items[last].reason} reason={slice.items[last].reason}
feedContext={slice.items[last].feedContext} feedContext={slice.items[last].feedContext}
parentAuthor={slice.items[2].parentAuthor} parentAuthor={slice.items[last].parentAuthor}
showReplyTo={false} showReplyTo={false}
moderation={slice.items[last].moderation} moderation={slice.items[last].moderation}
isThreadParent={isThreadParentAt(slice.items, last)} isThreadParent={isThreadParentAt(slice.items, last)}
isThreadChild={isThreadChildAt(slice.items, last)} isThreadChild={isThreadChildAt(slice.items, last)}
isParentBlocked={slice.items[2].isParentBlocked} isParentBlocked={slice.items[last].isParentBlocked}
isThreadLastChild isThreadLastChild
/> />
</> </>
+2 -2
View File
@@ -33,7 +33,7 @@ export function FollowButton({
await queueFollow() await queueFollow()
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`)) Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
} }
} }
} }
@@ -43,7 +43,7 @@ export function FollowButton({
await queueUnfollow() await queueUnfollow()
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`)) Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
} }
} }
} }
@@ -185,7 +185,7 @@ function SuggestedFollow({
await queueFollow() await queueFollow()
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`)) Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
} }
} }
}, [queueFollow, track, _]) }, [queueFollow, track, _])
@@ -195,7 +195,7 @@ function SuggestedFollow({
await queueUnfollow() await queueUnfollow()
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`)) Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
} }
} }
}, [queueUnfollow, _]) }, [queueUnfollow, _])
+6 -6
View File
@@ -108,7 +108,7 @@ let ProfileMenu = ({
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to unmute account', {message: e}) logger.error('Failed to unmute account', {message: e})
Toast.show(_(msg`There was an issue! ${e.toString()}`)) Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
} }
} }
} else { } else {
@@ -119,7 +119,7 @@ let ProfileMenu = ({
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to mute account', {message: e}) logger.error('Failed to mute account', {message: e})
Toast.show(_(msg`There was an issue! ${e.toString()}`)) Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
} }
} }
} }
@@ -134,7 +134,7 @@ let ProfileMenu = ({
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to unblock account', {message: e}) logger.error('Failed to unblock account', {message: e})
Toast.show(_(msg`There was an issue! ${e.toString()}`)) Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
} }
} }
} else { } else {
@@ -145,7 +145,7 @@ let ProfileMenu = ({
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to block account', {message: e}) logger.error('Failed to block account', {message: e})
Toast.show(_(msg`There was an issue! ${e.toString()}`)) Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
} }
} }
} }
@@ -159,7 +159,7 @@ let ProfileMenu = ({
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to follow account', {message: e}) logger.error('Failed to follow account', {message: e})
Toast.show(_(msg`There was an issue! ${e.toString()}`)) Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
} }
} }
}, [_, queueFollow, track]) }, [_, queueFollow, track])
@@ -172,7 +172,7 @@ let ProfileMenu = ({
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to unfollow account', {message: e}) logger.error('Failed to unfollow account', {message: e})
Toast.show(_(msg`There was an issue! ${e.toString()}`)) Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
} }
} }
}, [_, queueUnfollow, track]) }, [_, queueUnfollow, track])
+75 -69
View File
@@ -1,87 +1,93 @@
import React, {useEffect, useState} from 'react'
import {View} from 'react-native'
import Animated, {FadeInUp, FadeOutUp} from 'react-native-reanimated'
import RootSiblings from 'react-native-root-siblings' import RootSiblings from 'react-native-root-siblings'
import React from 'react' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {Animated, StyleSheet, View} from 'react-native' import {
import {Props as FontAwesomeProps} from '@fortawesome/react-native-fontawesome' FontAwesomeIcon,
import {Text} from './text/Text' Props as FontAwesomeProps,
import {colors} from 'lib/styles' } from '@fortawesome/react-native-fontawesome'
import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette' import {atoms as a, useTheme} from '#/alf'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {Text} from '#/components/Typography'
import {IS_TEST} from '#/env' import {IS_TEST} from '#/env'
const TIMEOUT = 4e3 const TIMEOUT = 2e3
export function show( export function show(
message: string, message: string,
_icon: FontAwesomeProps['icon'] = 'check', icon: FontAwesomeProps['icon'] = 'check',
) { ) {
if (IS_TEST) return if (IS_TEST) return
const item = new RootSiblings(<Toast message={message} />) const item = new RootSiblings(<Toast message={message} icon={icon} />)
// timeout has some leeway to account for the animation
setTimeout(() => { setTimeout(() => {
item.destroy() item.destroy()
}, TIMEOUT) }, TIMEOUT + 1e3)
} }
function Toast({message}: {message: string}) { function Toast({
const theme = useTheme() message,
const pal = usePalette('default') icon,
const interp = useAnimatedValue(0) }: {
message: string
icon: FontAwesomeProps['icon']
}) {
const t = useTheme()
const {top} = useSafeAreaInsets()
React.useEffect(() => { // for the exit animation to work on iOS the animated component
Animated.sequence([ // must not be the root component
Animated.timing(interp, { // so we need to wrap it in a view and unmount the toast ahead of time
toValue: 1, const [alive, setAlive] = useState(true)
duration: 150,
useNativeDriver: true, useEffect(() => {
}), setTimeout(() => {
Animated.delay(3700), setAlive(false)
Animated.timing(interp, { }, TIMEOUT)
toValue: 0, }, [])
duration: 150,
useNativeDriver: true,
}),
]).start()
})
const opacityStyle = {opacity: interp}
return ( return (
<View style={styles.container} pointerEvents="none"> <View
<Animated.View style={[a.absolute, {top: top + 15, left: 16, right: 16}]}
style={[ pointerEvents="none">
pal.view, {alive && (
pal.border, <Animated.View
styles.toast, entering={FadeInUp}
theme.colorScheme === 'dark' && styles.toastDark, exiting={FadeOutUp}
opacityStyle, style={[
]}> a.flex_1,
<Text type="lg-medium" style={pal.text}> t.atoms.bg,
{message} a.shadow_lg,
</Text> a.rounded_sm,
</Animated.View> t.atoms.border_contrast_medium,
a.rounded_sm,
a.px_md,
a.py_lg,
a.border,
a.flex_row,
a.gap_md,
]}>
<View
style={[
a.flex_shrink_0,
a.rounded_full,
{width: 32, height: 32},
t.atoms.bg_contrast_25,
a.align_center,
a.justify_center,
]}>
<FontAwesomeIcon
icon={icon}
size={16}
style={t.atoms.text_contrast_low}
/>
</View>
<View style={[a.h_full, a.justify_center, a.flex_1]}>
<Text style={a.text_md}>{message}</Text>
</View>
</Animated.View>
)}
</View> </View>
) )
} }
const styles = StyleSheet.create({
container: {
position: 'absolute',
top: 60,
left: 0,
right: 0,
alignItems: 'center',
},
toast: {
paddingHorizontal: 18,
paddingVertical: 10,
borderRadius: 24,
borderWidth: 1,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowOffset: {width: 0, height: 4},
marginHorizontal: 6,
},
toastDark: {
backgroundColor: colors.gray6,
shadowOpacity: 0.5,
},
})
+3 -2
View File
@@ -2,7 +2,7 @@
* Note: the dataSet properties are used to leverage custom CSS in public/index.html * Note: the dataSet properties are used to leverage custom CSS in public/index.html
*/ */
import React, {useState, useEffect} from 'react' import React, {useEffect, useState} from 'react'
import {StyleSheet, Text, View} from 'react-native' import {StyleSheet, Text, View} from 'react-native'
import { import {
FontAwesomeIcon, FontAwesomeIcon,
@@ -39,7 +39,7 @@ export const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
<View style={styles.container}> <View style={styles.container}>
<FontAwesomeIcon <FontAwesomeIcon
icon={activeToast.icon} icon={activeToast.icon}
size={24} size={20}
style={styles.icon as FontAwesomeIconStyle} style={styles.icon as FontAwesomeIconStyle}
/> />
<Text style={styles.text}>{activeToast.text}</Text> <Text style={styles.text}>{activeToast.text}</Text>
@@ -79,6 +79,7 @@ const styles = StyleSheet.create({
}, },
icon: { icon: {
color: '#fff', color: '#fff',
flexShrink: 0,
}, },
text: { text: {
color: '#fff', color: '#fff',
+24 -2
View File
@@ -8,6 +8,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger' import {logger} from '#/logger'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import { import {
@@ -179,6 +180,7 @@ let UserAvatar = ({
const pal = usePalette('default') const pal = usePalette('default')
const backgroundColor = pal.colors.backgroundLight const backgroundColor = pal.colors.backgroundLight
const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square') const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square')
const gate = useGate()
const aviStyle = useMemo(() => { const aviStyle = useMemo(() => {
if (finalShape === 'square') { if (finalShape === 'square') {
@@ -221,7 +223,12 @@ let UserAvatar = ({
testID="userAvatarImage" testID="userAvatarImage"
style={aviStyle} style={aviStyle}
resizeMode="cover" resizeMode="cover"
source={{uri: avatar}} source={{
uri: hackModifyThumbnailPath(
avatar,
size < 90 && gate('small_avi_thumb'),
),
}}
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
/> />
) : ( ) : (
@@ -229,7 +236,12 @@ let UserAvatar = ({
testID="userAvatarImage" testID="userAvatarImage"
style={aviStyle} style={aviStyle}
contentFit="cover" contentFit="cover"
source={{uri: avatar}} source={{
uri: hackModifyThumbnailPath(
avatar,
size < 90 && gate('small_avi_thumb'),
),
}}
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
/> />
)} )}
@@ -433,6 +445,16 @@ let PreviewableUserAvatar = ({
PreviewableUserAvatar = memo(PreviewableUserAvatar) PreviewableUserAvatar = memo(PreviewableUserAvatar)
export {PreviewableUserAvatar} export {PreviewableUserAvatar}
// HACK
// We have started serving smaller avis but haven't updated lexicons to give the data properly
// manually string-replace to use the smaller ones
// -prf
function hackModifyThumbnailPath(uri: string, isEnabled: boolean): string {
return isEnabled
? uri.replace('/img/avatar/plain/', '/img/avatar_thumbnail/plain/')
: uri
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
editButtonContainer: { editButtonContainer: {
position: 'absolute', position: 'absolute',
+6 -3
View File
@@ -149,7 +149,7 @@ let PostDropdownBtn = ({
}, },
e => { e => {
logger.error('Failed to delete post', {message: e}) logger.error('Failed to delete post', {message: e})
Toast.show(_(msg`Failed to delete post, please try again`)) Toast.show(_(msg`Failed to delete post, please try again`), 'xmark')
}, },
) )
}, [ }, [
@@ -177,7 +177,10 @@ let PostDropdownBtn = ({
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to toggle thread mute', {message: e}) logger.error('Failed to toggle thread mute', {message: e})
Toast.show(_(msg`Failed to toggle thread mute, please try again`)) Toast.show(
_(msg`Failed to toggle thread mute, please try again`),
'xmark',
)
} }
} }
}, [isThreadMuted, unmuteThread, _, muteThread]) }, [isThreadMuted, unmuteThread, _, muteThread])
@@ -186,7 +189,7 @@ let PostDropdownBtn = ({
const str = richTextToString(richText, true) const str = richTextToString(richText, true)
Clipboard.setStringAsync(str) Clipboard.setStringAsync(str)
Toast.show(_(msg`Copied to clipboard`)) Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
}, [_, richText]) }, [_, richText])
const onPressTranslate = React.useCallback(() => { const onPressTranslate = React.useCallback(() => {
+1 -1
View File
@@ -354,7 +354,7 @@ let PostCtrls = ({
onPress={e => { onPress={e => {
e.stopPropagation() e.stopPropagation()
Clipboard.setStringAsync(feedContext) Clipboard.setStringAsync(feedContext)
Toast.show(_(msg`Copied to clipboard`)) Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
}}> }}>
<Text <Text
style={{ style={{
+2
View File
@@ -51,6 +51,7 @@ import {faChevronRight} from '@fortawesome/free-solid-svg-icons/faChevronRight'
import {faCircleCheck} from '@fortawesome/free-solid-svg-icons/faCircleCheck' import {faCircleCheck} from '@fortawesome/free-solid-svg-icons/faCircleCheck'
import {faCircleDot} from '@fortawesome/free-solid-svg-icons/faCircleDot' import {faCircleDot} from '@fortawesome/free-solid-svg-icons/faCircleDot'
import {faCircleExclamation} from '@fortawesome/free-solid-svg-icons/faCircleExclamation' import {faCircleExclamation} from '@fortawesome/free-solid-svg-icons/faCircleExclamation'
import {faClipboardCheck} from '@fortawesome/free-solid-svg-icons/faClipboardCheck'
import {faClone} from '@fortawesome/free-solid-svg-icons/faClone' import {faClone} from '@fortawesome/free-solid-svg-icons/faClone'
import {faCommentSlash} from '@fortawesome/free-solid-svg-icons/faCommentSlash' import {faCommentSlash} from '@fortawesome/free-solid-svg-icons/faCommentSlash'
import {faDownload} from '@fortawesome/free-solid-svg-icons/faDownload' import {faDownload} from '@fortawesome/free-solid-svg-icons/faDownload'
@@ -140,6 +141,7 @@ library.add(
faCircleExclamation, faCircleExclamation,
faCirclePlay, faCirclePlay,
faCircleUser, faCircleUser,
faClipboardCheck,
faClone, faClone,
farClone, farClone,
faComment, faComment,
+2 -2
View File
@@ -36,7 +36,7 @@ export function AccessibilitySettingsScreen({}: Props) {
const pal = usePalette('default') const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {screen} = useAnalytics() const {screen} = useAnalytics()
const {isMobile} = useWebMediaQueries() const {isMobile, isTabletOrMobile} = useWebMediaQueries()
const {_} = useLingui() const {_} = useLingui()
const requireAltTextEnabled = useRequireAltTextEnabled() const requireAltTextEnabled = useRequireAltTextEnabled()
@@ -58,7 +58,7 @@ export function AccessibilitySettingsScreen({}: Props) {
return ( return (
<View style={s.hContentRegion} testID="accessibilitySettingsScreen"> <View style={s.hContentRegion} testID="accessibilitySettingsScreen">
<SimpleViewHeader <SimpleViewHeader
showBackButton={isMobile} showBackButton={isTabletOrMobile}
style={[ style={[
pal.border, pal.border,
{borderBottomWidth: 1}, {borderBottomWidth: 1},
+76 -41
View File
@@ -1,11 +1,19 @@
import React from 'react' import React, {useCallback} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useFocusEffect, useIsFocused} from '@react-navigation/native' import {useFocusEffect, useIsFocused} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {useAnalytics} from '#/lib/analytics/analytics'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {ComposeIcon2} from '#/lib/icons'
import {
NativeStackScreenProps,
NotificationsTabNavigatorParams,
} from '#/lib/routes/types'
import {s} from '#/lib/styles'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {emitSoftReset, listenSoftReset} from '#/state/events' import {emitSoftReset, listenSoftReset} from '#/state/events'
@@ -17,37 +25,32 @@ import {
import {truncateAndInvalidate} from '#/state/queries/util' import {truncateAndInvalidate} from '#/state/queries/util'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer' import {useComposerControls} from '#/state/shell/composer'
import {useAnalytics} from 'lib/analytics/analytics' import {Feed} from '#/view/com/notifications/Feed'
import {usePalette} from 'lib/hooks/usePalette' import {FAB} from '#/view/com/util/fab/FAB'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {MainScrollProvider} from '#/view/com/util/MainScrollProvider'
import {ComposeIcon2} from 'lib/icons' import {ViewHeader} from '#/view/com/util/ViewHeader'
import {
NativeStackScreenProps,
NotificationsTabNavigatorParams,
} from 'lib/routes/types'
import {colors, s} from 'lib/styles'
import {TextLink} from 'view/com/util/Link'
import {ListMethods} from 'view/com/util/List' import {ListMethods} from 'view/com/util/List'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {CenteredView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2'
import {Link} from '#/components/Link'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Feed} from '../com/notifications/Feed' import {Text} from '#/components/Typography'
import {FAB} from '../com/util/fab/FAB'
import {MainScrollProvider} from '../com/util/MainScrollProvider'
import {ViewHeader} from '../com/util/ViewHeader'
type Props = NativeStackScreenProps< type Props = NativeStackScreenProps<
NotificationsTabNavigatorParams, NotificationsTabNavigatorParams,
'Notifications' 'Notifications'
> >
export function NotificationsScreen({}: Props) { export function NotificationsScreen({route: {params}}: Props) {
const {_} = useLingui() const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const [isScrolledDown, setIsScrolledDown] = React.useState(false) const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const [isLoadingLatest, setIsLoadingLatest] = React.useState(false) const [isLoadingLatest, setIsLoadingLatest] = React.useState(false)
const scrollElRef = React.useRef<ListMethods>(null) const scrollElRef = React.useRef<ListMethods>(null)
const {screen} = useAnalytics() const {screen} = useAnalytics()
const pal = usePalette('default') const t = useTheme()
const {isDesktop} = useWebMediaQueries() const {isDesktop} = useWebMediaQueries()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const unreadNotifs = useUnreadNotifications() const unreadNotifs = useUnreadNotifications()
@@ -109,56 +112,87 @@ export function NotificationsScreen({}: Props) {
return listenSoftReset(onPressLoadLatest) return listenSoftReset(onPressLoadLatest)
}, [onPressLoadLatest, isScreenFocused]) }, [onPressLoadLatest, isScreenFocused])
const renderButton = useCallback(() => {
return (
<Link
to="/notifications/settings"
label={_(msg`Notification settings`)}
size="small"
variant="ghost"
color="secondary"
shape="square"
style={[a.justify_center]}>
<SettingsIcon size="md" style={t.atoms.text_contrast_medium} />
</Link>
)
}, [_, t])
const ListHeaderComponent = React.useCallback(() => { const ListHeaderComponent = React.useCallback(() => {
if (isDesktop) { if (isDesktop) {
return ( return (
<View <View
style={[ style={[
pal.view, t.atoms.bg,
{ a.flex_row,
flexDirection: 'row', a.align_center,
alignItems: 'center', a.justify_between,
justifyContent: 'space-between', a.gap_lg,
paddingHorizontal: 18, a.px_lg,
paddingVertical: 12, a.pr_md,
}, a.py_sm,
]}> ]}>
<TextLink <Button
type="title-lg" label={_(msg`Notifications`)}
href="/notifications" accessibilityHint={_(msg`Refresh notifications`)}
style={[pal.text, {fontWeight: 'bold'}]} onPress={emitSoftReset}>
text={ {({hovered, pressed}) => (
<> <Text
<Trans>Notifications</Trans>{' '} style={[
a.text_2xl,
a.font_bold,
(hovered || pressed) && a.underline,
]}>
<Trans>Notifications</Trans>
{hasNew && ( {hasNew && (
<View <View
style={{ style={{
left: 4,
top: -8, top: -8,
backgroundColor: colors.blue3, backgroundColor: t.palette.primary_500,
width: 8, width: 8,
height: 8, height: 8,
borderRadius: 4, borderRadius: 4,
}} }}
/> />
)} )}
</> </Text>
} )}
onPress={emitSoftReset} </Button>
/> <View style={[a.flex_row, a.align_center, a.gap_sm]}>
{isLoadingLatest ? <Loader size="md" /> : <></>} {isLoadingLatest ? <Loader size="md" /> : <></>}
{renderButton()}
</View>
</View> </View>
) )
} }
return <></> return <></>
}, [isDesktop, pal, hasNew, isLoadingLatest]) }, [isDesktop, t, hasNew, renderButton, _, isLoadingLatest])
const renderHeaderSpinner = React.useCallback(() => { const renderHeaderSpinner = React.useCallback(() => {
return ( return (
<View style={{width: 30, height: 20, alignItems: 'flex-end'}}> <View
style={[
{width: 30, height: 20},
a.flex_row,
a.align_center,
a.justify_end,
a.gap_md,
]}>
{isLoadingLatest ? <Loader width={20} /> : <></>} {isLoadingLatest ? <Loader width={20} /> : <></>}
{renderButton()}
</View> </View>
) )
}, [isLoadingLatest]) }, [renderButton, isLoadingLatest])
return ( return (
<CenteredView <CenteredView
@@ -176,6 +210,7 @@ export function NotificationsScreen({}: Props) {
onScrolledDownChange={setIsScrolledDown} onScrolledDownChange={setIsScrolledDown}
scrollElRef={scrollElRef} scrollElRef={scrollElRef}
ListHeaderComponent={ListHeaderComponent} ListHeaderComponent={ListHeaderComponent}
overridePriorityNotifications={params?.show === 'all'}
/> />
</MainScrollProvider> </MainScrollProvider>
{(isScrolledDown || hasNew) && ( {(isScrolledDown || hasNew) && (
@@ -0,0 +1,94 @@
import React from 'react'
import {View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AllNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
import {useNotificationsSettingsMutation} from '#/state/queries/notifications/settings'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {ScrollView} from '#/view/com/util/Views'
import {atoms as a, useTheme} from '#/alf'
import {Error} from '#/components/Error'
import * as Toggle from '#/components/forms/Toggle'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<AllNavigatorParams, 'NotificationsSettings'>
export function NotificationsSettingsScreen({}: Props) {
const {_} = useLingui()
const t = useTheme()
const {data, isError: isQueryError, refetch} = useNotificationFeedQuery()
const serverPriority = data?.pages.at(0)?.priority
const {
mutate: onChangePriority,
isPending: isMutationPending,
variables,
} = useNotificationsSettingsMutation()
const priority = isMutationPending
? variables[0] === 'enabled'
: serverPriority
return (
<ScrollView stickyHeaderIndices={[0]}>
<ViewHeader
title={_(msg`Notification Settings`)}
showOnDesktop
showBorder
/>
{isQueryError ? (
<Error
title={_(msg`Oops!`)}
message={_(msg`Something went wrong!`)}
onRetry={refetch}
sideBorders={false}
/>
) : (
<View style={[a.p_lg, a.gap_md]}>
<Text style={[a.text_lg, a.font_bold]}>
<FontAwesomeIcon icon="flask" style={t.atoms.text} />{' '}
<Trans>Notification filters</Trans>
</Text>
<Toggle.Group
label={_(msg`Priority notifications`)}
type="checkbox"
values={priority ? ['enabled'] : []}
onChange={onChangePriority}
disabled={typeof priority !== 'boolean' || isMutationPending}>
<View>
<Toggle.Item
name="enabled"
label={_(msg`Enable priority notifications`)}
style={[a.justify_between, a.py_sm]}>
<Toggle.LabelText>
<Trans>Enable priority notifications</Trans>
</Toggle.LabelText>
{!data ? <Loader size="md" /> : <Toggle.Platform />}
</Toggle.Item>
</View>
</Toggle.Group>
<View
style={[
a.mt_sm,
a.px_xl,
a.py_lg,
a.rounded_md,
t.atoms.bg_contrast_25,
]}>
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
<Trans>
Experimental: When this preference is enabled, you'll only
receive reply and quote notifications from users you follow.
We'll continue to add more controls here over time.
</Trans>
</Text>
</View>
</View>
)}
</ScrollView>
)
}
+15 -18
View File
@@ -30,7 +30,7 @@ export function PreferencesExternalEmbeds({}: Props) {
const pal = usePalette('default') const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {screen} = useAnalytics() const {screen} = useAnalytics()
const {isMobile} = useWebMediaQueries() const {isTabletOrMobile} = useWebMediaQueries()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
@@ -41,26 +41,23 @@ export function PreferencesExternalEmbeds({}: Props) {
return ( return (
<View style={s.hContentRegion} testID="preferencesExternalEmbedsScreen"> <View style={s.hContentRegion} testID="preferencesExternalEmbedsScreen">
<SimpleViewHeader
showBackButton={isMobile}
style={[
pal.border,
{borderBottomWidth: 1},
!isMobile && {borderLeftWidth: 1, borderRightWidth: 1},
]}>
<View style={{flex: 1}}>
<Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
<Trans>External Media Preferences</Trans>
</Text>
<Text style={pal.textLight}>
<Trans>Customize media from external sites.</Trans>
</Text>
</View>
</SimpleViewHeader>
<ScrollView <ScrollView
// @ts-ignore web only -prf // @ts-ignore web only -prf
dataSet={{'stable-gutters': 1}} dataSet={{'stable-gutters': 1}}
contentContainerStyle={[pal.viewLight, {paddingBottom: 200}]}> contentContainerStyle={[pal.viewLight, {paddingBottom: 75}]}>
<SimpleViewHeader
showBackButton={isTabletOrMobile}
style={[pal.border, {borderBottomWidth: 1}]}>
<View style={{flex: 1}}>
<Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
<Trans>External Media Preferences</Trans>
</Text>
<Text style={pal.textLight}>
<Trans>Customize media from external sites.</Trans>
</Text>
</View>
</SimpleViewHeader>
<View style={[pal.view]}> <View style={[pal.view]}>
<View style={styles.infoCard}> <View style={styles.infoCard}>
<Text style={pal.text}> <Text style={pal.text}>
+34 -59
View File
@@ -1,24 +1,24 @@
import React, {useState} from 'react' import React, {useState} from 'react'
import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Plural, Trans} from '@lingui/macro' import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Slider} from '@miblanchard/react-native-slider' import {Slider} from '@miblanchard/react-native-slider'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {colors, s} from '#/lib/styles'
import {isWeb} from '#/platform/detection'
import { import {
usePreferencesQuery, usePreferencesQuery,
useSetFeedViewPreferencesMutation, useSetFeedViewPreferencesMutation,
} from '#/state/queries/preferences' } from '#/state/queries/preferences'
import {usePalette} from 'lib/hooks/usePalette' import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' import {Text} from '#/view/com/util/text/Text'
import {colors, s} from 'lib/styles' import {ScrollView} from '#/view/com/util/Views'
import {isWeb} from 'platform/detection'
import {ToggleButton} from 'view/com/util/forms/ToggleButton'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {CenteredView} from 'view/com/util/Views'
import {Text} from '../com/util/text/Text'
function RepliesThresholdInput({ function RepliesThresholdInput({
enabled, enabled,
@@ -79,10 +79,10 @@ type Props = NativeStackScreenProps<
CommonNavigatorParams, CommonNavigatorParams,
'PreferencesFollowingFeed' 'PreferencesFollowingFeed'
> >
export function PreferencesFollowingFeed({navigation}: Props) { export function PreferencesFollowingFeed({}: Props) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {isTabletOrDesktop} = useWebMediaQueries() const {isTabletOrMobile} = useWebMediaQueries()
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
const {mutate: setFeedViewPref, variables} = const {mutate: setFeedViewPref, variables} =
useSetFeedViewPreferencesMutation() useSetFeedViewPreferencesMutation()
@@ -92,26 +92,25 @@ export function PreferencesFollowingFeed({navigation}: Props) {
) )
return ( return (
<CenteredView <View testID="preferencesHomeFeedScreen" style={s.hContentRegion}>
testID="preferencesHomeFeedScreen" <ScrollView
style={[ // @ts-ignore web only -sfn
pal.view, dataSet={{'stable-gutters': 1}}
pal.border, contentContainerStyle={{paddingBottom: 75}}>
styles.container, <SimpleViewHeader
isTabletOrDesktop && styles.desktopContainer, showBackButton={isTabletOrMobile}
]}> style={[pal.border, {borderBottomWidth: 1}]}>
<ViewHeader title={_(msg`Following Feed Preferences`)} showOnDesktop /> <View style={{flex: 1}}>
<View <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
style={[ <Trans>Following Feed Preferences</Trans>
styles.titleSection, </Text>
isTabletOrDesktop && {paddingTop: 20, paddingBottom: 20}, <Text style={pal.textLight}>
]}> <Trans>
<Text type="xl" style={[pal.textLight, styles.description]}> Fine-tune the content you see on your Following feed.
<Trans>Fine-tune the content you see on your Following feed.</Trans> </Trans>
</Text> </Text>
</View> </View>
</SimpleViewHeader>
<ScrollView>
<View style={styles.cardsContainer}> <View style={styles.cardsContainer}>
<View style={[pal.viewLight, styles.card]}> <View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}> <Text type="title-sm" style={[pal.text, s.pb5]}>
@@ -253,7 +252,7 @@ export function PreferencesFollowingFeed({navigation}: Props) {
<View style={[pal.viewLight, styles.card]}> <View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}> <Text type="title-sm" style={[pal.text, s.pb5]}>
<FontAwesomeIcon icon="flask" color={pal.colors.text} /> <FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '}
<Trans>Show Posts from My Feeds</Trans> <Trans>Show Posts from My Feeds</Trans>
</Text> </Text>
<Text style={[pal.text, s.pb10]}> <Text style={[pal.text, s.pb10]}>
@@ -288,42 +287,17 @@ export function PreferencesFollowingFeed({navigation}: Props) {
</View> </View>
</View> </View>
</ScrollView> </ScrollView>
</View>
<View
style={[
styles.btnContainer,
!isTabletOrDesktop && {borderTopWidth: 1, paddingHorizontal: 20},
pal.border,
]}>
<TouchableOpacity
testID="confirmBtn"
onPress={() => {
navigation.canGoBack()
? navigation.goBack()
: navigation.navigate('Settings')
}}
style={[styles.btn, isTabletOrDesktop && styles.btnDesktop]}
accessibilityRole="button"
accessibilityLabel={_(msg`Confirm`)}
accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}>
<Trans>Done</Trans>
</Text>
</TouchableOpacity>
</View>
</CenteredView>
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
paddingBottom: 90,
}, },
desktopContainer: { desktopContainer: {
borderLeftWidth: 1, borderLeftWidth: 1,
borderRightWidth: 1, borderRightWidth: 1,
paddingBottom: 40,
}, },
titleSection: { titleSection: {
paddingBottom: 30, paddingBottom: 30,
@@ -338,6 +312,7 @@ const styles = StyleSheet.create({
}, },
cardsContainer: { cardsContainer: {
paddingHorizontal: 20, paddingHorizontal: 20,
paddingVertical: 16,
}, },
card: { card: {
padding: 16, padding: 16,
+39 -69
View File
@@ -1,33 +1,29 @@
import React from 'react' import React from 'react'
import { import {ActivityIndicator, StyleSheet, View} from 'react-native'
ActivityIndicator,
ScrollView,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from '../com/util/text/Text' import {msg, Trans} from '@lingui/macro'
import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {ToggleButton} from 'view/com/util/forms/ToggleButton'
import {RadioGroup} from 'view/com/util/forms/RadioGroup'
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {CenteredView} from 'view/com/util/Views'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {colors, s} from '#/lib/styles'
import { import {
usePreferencesQuery, usePreferencesQuery,
useSetThreadViewPreferencesMutation, useSetThreadViewPreferencesMutation,
} from '#/state/queries/preferences' } from '#/state/queries/preferences'
import {RadioGroup} from '#/view/com/util/forms/RadioGroup'
import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
import {Text} from '#/view/com/util/text/Text'
import {ScrollView} from '#/view/com/util/Views'
import {atoms as a} from '#/alf'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'>
export function PreferencesThreads({navigation}: Props) { export function PreferencesThreads({}: Props) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {isTabletOrDesktop} = useWebMediaQueries() const {isTabletOrMobile} = useWebMediaQueries()
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
const {mutate: setThreadViewPrefs, variables} = const {mutate: setThreadViewPrefs, variables} =
useSetThreadViewPreferencesMutation() useSetThreadViewPreferencesMutation()
@@ -42,27 +38,25 @@ export function PreferencesThreads({navigation}: Props) {
) )
return ( return (
<CenteredView <View testID="preferencesThreadsScreen" style={s.hContentRegion}>
testID="preferencesThreadsScreen" <ScrollView
style={[ // @ts-ignore web only -prf
pal.view, dataSet={{'stable-gutters': 1}}
pal.border, contentContainerStyle={{paddingBottom: 75}}>
styles.container, <SimpleViewHeader
isTabletOrDesktop && styles.desktopContainer, showBackButton={isTabletOrMobile}
]}> style={[pal.border, {borderBottomWidth: 1}]}>
<ViewHeader title={_(msg`Thread Preferences`)} showOnDesktop /> <View style={{flex: 1}}>
<View <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
style={[ <Trans>Thread Preferences</Trans>
styles.titleSection, </Text>
isTabletOrDesktop && {paddingTop: 20, paddingBottom: 20}, <Text style={pal.textLight}>
]}> <Trans>Fine-tune the discussion threads.</Trans>
<Text type="xl" style={[pal.textLight, styles.description]}> </Text>
<Trans>Fine-tune the discussion threads.</Trans> </View>
</Text> </SimpleViewHeader>
</View>
{preferences ? ( {preferences ? (
<ScrollView>
<View style={styles.cardsContainer}> <View style={styles.cardsContainer}>
<View style={[pal.viewLight, styles.card]}> <View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}> <Text type="title-sm" style={[pal.text, s.pb5]}>
@@ -136,46 +130,21 @@ export function PreferencesThreads({navigation}: Props) {
/> />
</View> </View>
</View> </View>
</ScrollView> ) : (
) : ( <ActivityIndicator style={a.flex_1} />
<ActivityIndicator /> )}
)} </ScrollView>
</View>
<View
style={[
styles.btnContainer,
!isTabletOrDesktop && {borderTopWidth: 1, paddingHorizontal: 20},
pal.border,
]}>
<TouchableOpacity
testID="confirmBtn"
onPress={() => {
navigation.canGoBack()
? navigation.goBack()
: navigation.navigate('Settings')
}}
style={[styles.btn, isTabletOrDesktop && styles.btnDesktop]}
accessibilityRole="button"
accessibilityLabel={_(msg`Confirm`)}
accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}>
<Trans context="action">Done</Trans>
</Text>
</TouchableOpacity>
</View>
</CenteredView>
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
paddingBottom: 90,
}, },
desktopContainer: { desktopContainer: {
borderLeftWidth: 1, borderLeftWidth: 1,
borderRightWidth: 1, borderRightWidth: 1,
paddingBottom: 40,
}, },
titleSection: { titleSection: {
paddingBottom: 30, paddingBottom: 30,
@@ -190,6 +159,7 @@ const styles = StyleSheet.create({
}, },
cardsContainer: { cardsContainer: {
paddingHorizontal: 20, paddingHorizontal: 20,
paddingVertical: 16,
}, },
card: { card: {
padding: 16, padding: 16,
+3 -1
View File
@@ -205,6 +205,7 @@ export function ProfileFeedScreenInner({
_( _(
msg`There was an an issue updating your feeds, please check your internet connection and try again.`, msg`There was an an issue updating your feeds, please check your internet connection and try again.`,
), ),
'xmark',
) )
logger.error('Failed up update feeds', {message: err}) logger.error('Failed up update feeds', {message: err})
} }
@@ -231,7 +232,7 @@ export function ProfileFeedScreenInner({
]) ])
} }
} catch (e) { } catch (e) {
Toast.show(_(msg`There was an issue contacting the server`)) Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
logger.error('Failed to toggle pinned feed', {message: e}) logger.error('Failed to toggle pinned feed', {message: e})
} }
}, [ }, [
@@ -543,6 +544,7 @@ function AboutSection({
_( _(
msg`There was an an issue contacting the server, please check your internet connection and try again.`, msg`There was an an issue contacting the server, please check your internet connection and try again.`,
), ),
'xmark',
) )
logger.error('Failed up toggle like', {message: err}) logger.error('Failed up toggle like', {message: err})
} }
+2 -2
View File
@@ -300,7 +300,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
Toast.show(_(msg`Saved to your feeds`)) Toast.show(_(msg`Saved to your feeds`))
} }
} catch (e) { } catch (e) {
Toast.show(_(msg`There was an issue contacting the server`)) Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
logger.error('Failed to toggle pinned feed', {message: e}) logger.error('Failed to toggle pinned feed', {message: e})
} }
}, [ }, [
@@ -319,7 +319,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
await removeSavedFeed(savedFeedConfig) await removeSavedFeed(savedFeedConfig)
Toast.show(_(msg`Removed from your feeds`)) Toast.show(_(msg`Removed from your feeds`))
} catch (e) { } catch (e) {
Toast.show(_(msg`There was an issue contacting the server`)) Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
logger.error('Failed to remove pinned list', {message: e}) logger.error('Failed to remove pinned list', {message: e})
} }
}, [playHaptic, removeSavedFeed, _, savedFeedConfig]) }, [playHaptic, removeSavedFeed, _, savedFeedConfig])
+3 -3
View File
@@ -234,7 +234,7 @@ function ListItem({
}, },
]) ])
} catch (e) { } catch (e) {
Toast.show(_(msg`There was an issue contacting the server`)) Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
logger.error('Failed to toggle pinned feed', {message: e}) logger.error('Failed to toggle pinned feed', {message: e})
} }
}, [_, playHaptic, feed, updateSavedFeeds, resetSaveFeedsMutationState]) }, [_, playHaptic, feed, updateSavedFeeds, resetSaveFeedsMutationState])
@@ -260,7 +260,7 @@ function ListItem({
index: nextIndex, index: nextIndex,
}) })
} catch (e) { } catch (e) {
Toast.show(_(msg`There was an issue contacting the server`)) Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
logger.error('Failed to set pinned feed order', {message: e}) logger.error('Failed to set pinned feed order', {message: e})
} }
}, [feed, isPinned, overwriteSavedFeeds, currentFeeds, _]) }, [feed, isPinned, overwriteSavedFeeds, currentFeeds, _])
@@ -286,7 +286,7 @@ function ListItem({
index: nextIndex, index: nextIndex,
}) })
} catch (e) { } catch (e) {
Toast.show(_(msg`There was an issue contacting the server`)) Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
logger.error('Failed to set pinned feed order', {message: e}) logger.error('Failed to set pinned feed order', {message: e})
} }
}, [feed, isPinned, overwriteSavedFeeds, currentFeeds, _]) }, [feed, isPinned, overwriteSavedFeeds, currentFeeds, _])
@@ -43,7 +43,7 @@ export function ExportCarDialog({
} }
} catch (e) { } catch (e) {
logger.error('Error occurred while downloading CAR file', {message: e}) logger.error('Error occurred while downloading CAR file', {message: e})
Toast.show(_(msg`Error occurred while saving file`)) Toast.show(_(msg`Error occurred while saving file`), 'xmark')
} finally { } finally {
setLoading(false) setLoading(false)
control.close() control.close()
+4 -4
View File
@@ -34,10 +34,10 @@
jsonpointer "^5.0.0" jsonpointer "^5.0.0"
leven "^3.1.0" leven "^3.1.0"
"@atproto/api@^0.12.23": "@atproto/api@0.12.25":
version "0.12.23" version "0.12.25"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.23.tgz#b3409817d0b981a64f30d16e8257f0fe261338af" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.25.tgz#9eeb51484106a5e07f89f124e505674a3574f93b"
integrity sha512-fgQ30u+q9smX5g41eep7fISSkSAhRkX0inc81PZ82QwcHbFkC8ePaha/KP0CoTaPWKi7EsC89Z/8BEBCJo0oBA== integrity sha512-IV3vGPnDw9bmyP/JOd8YKbm8fOpRAgJpEUVnIZNVb/Vo8v+WOroOjrJxtzdHOcXTL9IEcTTyXSCc7yE7kwhN2A==
dependencies: dependencies:
"@atproto/common-web" "^0.3.0" "@atproto/common-web" "^0.3.0"
"@atproto/lexicon" "^0.4.0" "@atproto/lexicon" "^0.4.0"