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"
- tapOn:
id: "e2eGotoFeeds"
- tapOn: "Good Ppl"
- tapOn:
id: "saved-feed-Good Ppl"
- assertVisible:
id: "feedItem-by-bob.test"
- tapOn:
@@ -168,7 +169,7 @@ appId: xyz.blueskyweb.app
id: "profilePager-selector"
direction: LEFT
- tapOn:
id: "profilePager-selector-5"
id: "profilePager-selector-6"
- tapOn: "Good Ppl"
- tapOn:
+1
View File
@@ -197,6 +197,7 @@ func serve(cctx *cli.Context) error {
e.GET("/search", server.WebGeneric)
e.GET("/feeds", server.WebGeneric)
e.GET("/notifications", server.WebGeneric)
e.GET("/notifications/settings", server.WebGeneric)
e.GET("/lists", server.WebGeneric)
e.GET("/moderation", 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"
},
"dependencies": {
"@atproto/api": "^0.12.23",
"@atproto/api": "0.12.25",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
+4 -1
View File
@@ -95,7 +95,10 @@ function InnerApp() {
useEffect(() => {
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(() => {
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 {NotFoundScreen} from './view/screens/NotFound'
import {NotificationsScreen} from './view/screens/Notifications'
import {NotificationsSettingsScreen} from './view/screens/NotificationsSettings'
import {PostLikedByScreen} from './view/screens/PostLikedBy'
import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
import {PostThreadScreen} from './view/screens/PostThread'
@@ -324,6 +325,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => MessagesSettingsScreen}
options={{title: title(msg`Chat settings`), requireAuth: true}}
/>
<Stack.Screen
name="NotificationsSettings"
getComponent={() => NotificationsSettingsScreen}
options={{title: title(msg`Notification settings`), requireAuth: true}}
/>
<Stack.Screen
name="Feeds"
getComponent={() => FeedsScreen}
+3
View File
@@ -145,6 +145,9 @@ export const atoms = {
flex_shrink: {
flexShrink: 1,
},
flex_shrink_0: {
flexShrink: 0,
},
justify_start: {
justifyContent: 'flex-start',
},
+3 -3
View File
@@ -264,9 +264,9 @@ function SaveButtonInner({
])
}
Toast.show(_(msg`Feeds updated!`))
} catch (e: any) {
logger.error(e, {context: `FeedCard: failed to update feeds`, pin})
Toast.show(_(msg`Failed to update feeds`))
} catch (err: any) {
logger.error(err, {context: `FeedCard: failed to update feeds`, pin})
Toast.show(_(msg`Failed to update feeds`), 'xmark')
}
},
[_, pin, saveFeeds, removeFeed, uri, savedFeedConfig, type],
+1 -1
View File
@@ -189,7 +189,7 @@ let ListMaybePlaceholder = ({
return (
<Error
title={errorTitle ?? _(msg`Oops!`)}
message={errorMessage ?? _(`Something went wrong!`)}
message={errorMessage ?? _(msg`Something went wrong!`)}
onRetry={onRetry}
onGoBack={onGoBack}
sideBorders={sideBorders}
+6 -6
View File
@@ -306,9 +306,9 @@ export function FollowButtonInner({
)}`,
),
)
} catch (e: any) {
if (e?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`))
} catch (err: any) {
if (err?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
}
}
}
@@ -326,9 +326,9 @@ export function FollowButtonInner({
)}`,
),
)
} catch (e: any) {
if (e?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`))
} catch (err: any) {
if (err?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
}
}
}
+4 -1
View File
@@ -67,7 +67,10 @@ export function QrCodeDialog({
try {
await createAssetAsync(`file://${uri}`)
} 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})
return
}
+2 -1
View File
@@ -66,6 +66,7 @@ function ShareDialogInner({
if (!res) {
Toast.show(
_(msg`You must grant access to your photo library to save the image.`),
'xmark',
)
return
}
@@ -75,7 +76,7 @@ function ShareDialogInner({
Toast.show(_(msg`Image saved to your camera roll!`))
control.close()
} 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})
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.`,
),
'xmark',
)
logger.error('Failed to edit threadgate', {message: err})
}
+1 -1
View File
@@ -85,7 +85,7 @@ let ConvoMenu = ({
}
},
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: () => {
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)
Toast.show(_(msg`Copied to clipboard`))
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
}, [_, message.text, message.facets])
const onPressTranslateMessage = React.useCallback(() => {
+1 -1
View File
@@ -57,7 +57,7 @@ function DialogInner({
const [initialized, setInitialzed] = React.useState(false)
const {mutate: updateDeclaration} = useUpdateActorDeclaration({
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 {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import {logEvent} from 'lib/statsig/statsig'
import {FAB} from '#/view/com/util/fab/FAB'
@@ -31,7 +32,8 @@ export function NewChat({
logEvent('chat:open', {logContext: 'NewChatDialog'})
},
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 {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import {logEvent} from 'lib/statsig/statsig'
import * as Toast from '#/view/com/util/Toast'
@@ -43,7 +44,11 @@ function SendViaChatDialogInner({
logEvent('chat:open', {logContext: 'SendViaChatDialog'})
},
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 {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 {
atoms as a,
@@ -244,6 +246,7 @@ export function LabelText({
<Text
style={[
a.font_bold,
a.leading_tight,
{
userSelect: 'none',
color: disabled
@@ -251,7 +254,7 @@ export function LabelText({
: t.atoms.text_contrast_high.color,
},
native({
paddingTop: 3,
paddingTop: 2,
}),
flatten(style),
]}>
@@ -344,10 +347,10 @@ export function Checkbox() {
style={[
a.justify_center,
a.align_center,
a.border,
a.rounded_xs,
t.atoms.border_contrast_high,
{
borderWidth: 1,
height: 20,
width: 20,
},
@@ -375,34 +378,35 @@ export function Switch() {
<View
style={[
a.relative,
a.border,
a.rounded_full,
t.atoms.bg,
t.atoms.border_contrast_high,
{
borderWidth: 1,
height: 20,
width: 30,
width: 32,
padding: 2,
},
baseStyles,
hovered ? baseHoverStyles : {},
]}>
<View
<Animated.View
layout={LinearTransition.duration(100)}
style={[
a.absolute,
a.rounded_full,
{
height: 12,
width: 12,
top: 3,
left: 3,
backgroundColor: t.palette.contrast_400,
height: 14,
width: 14,
},
selected
? {
backgroundColor: t.palette.primary_500,
left: 13,
alignSelf: 'flex-end',
}
: {},
: {
backgroundColor: t.palette.contrast_400,
alignSelf: 'flex-start',
},
indicatorStyles,
]}
/>
@@ -428,10 +432,10 @@ export function Radio() {
style={[
a.justify_center,
a.align_center,
a.border,
a.rounded_full,
t.atoms.border_contrast_high,
{
borderWidth: 1,
height: 20,
width: 20,
},
@@ -456,3 +460,5 @@ export function Radio() {
</View>
)
}
export const Platform = isNative ? Switch : Checkbox
+2 -2
View File
@@ -32,7 +32,7 @@ export function useFollowMethods({
} catch (e: any) {
logger.error(`useFollowMethods: failed to follow`, {message: String(e)})
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),
})
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 => {
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: () => {
control.close()
+6 -4
View File
@@ -1,10 +1,10 @@
import {timeout} from './timeout'
export async function until(
export async function until<T>(
retries: number,
delay: number,
cond: (v: any, err: any) => boolean,
fn: () => Promise<any>,
cond: (v: T, err: any) => boolean,
fn: () => Promise<T>,
): Promise<boolean> {
while (retries > 0) {
try {
@@ -13,7 +13,9 @@ export async function until(
return true
}
} 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
}
}
+2 -2
View File
@@ -17,14 +17,14 @@ export async function openPicker(opts?: ImagePickerOptions) {
})
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 ?? [])
.slice(0, 4)
.filter(asset => {
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
})
.map(image => ({
+6 -9
View File
@@ -42,14 +42,13 @@ export type CommonNavigatorParams = {
Hashtag: {tag: string; author?: string}
MessagesConversation: {conversation: string; embed?: string}
MessagesSettings: undefined
NotificationsSettings: undefined
Feeds: undefined
Start: {name: string; rkey: string}
StarterPack: {name: string; rkey: string; new?: boolean}
StarterPackShort: {code: string}
StarterPackWizard: undefined
StarterPackEdit: {
rkey?: string
}
StarterPackEdit: {rkey?: string}
}
export type BottomTabNavigatorParams = CommonNavigatorParams & {
@@ -69,7 +68,7 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & {
}
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
Notifications: undefined
Notifications: {show?: 'all'}
}
export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
@@ -84,7 +83,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & {
Home: undefined
Search: {q?: string}
Feeds: undefined
Notifications: undefined
Notifications: {show?: 'all'}
Hashtag: {tag: string; author?: string}
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
}
@@ -96,7 +95,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
Search: {q?: string}
Feeds: undefined
NotificationsTab: undefined
Notifications: undefined
Notifications: {show?: 'all'}
MyProfileTab: undefined
Hashtag: {tag: string; author?: string}
MessagesTab: undefined
@@ -105,9 +104,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
StarterPack: {name: string; rkey: string; new?: boolean}
StarterPackShort: {code: string}
StarterPackWizard: undefined
StarterPackEdit: {
rkey?: string
}
StarterPackEdit: {rkey?: string}
}
// 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
// has increasing but not full support, so default to clipboard
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'
| 'ungroup_follow_backs'
| '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',
Feeds: '/feeds',
Notifications: '/notifications',
NotificationsSettings: '/notifications/settings',
Settings: '/settings',
LanguageSettings: '/settings/language',
Lists: '/lists',
@@ -84,7 +84,7 @@ function DialogInner() {
},
onError: 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: () => {
control.close()
@@ -67,7 +67,7 @@ export function MessageInput({
return
}
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
}
clearDraft()
@@ -46,7 +46,7 @@ export function MessageInput({
return
}
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
}
clearDraft()
@@ -106,6 +106,7 @@ function Inner() {
title={_(msg`Something went wrong`)}
message={_(msg`We couldn't load this conversation`)}
onRetry={() => convoState.error.retry()}
sideBorders={false}
/>
</CenteredView>
)
+1 -1
View File
@@ -309,7 +309,7 @@ function DesktopHeader({
a.gap_lg,
a.px_lg,
a.pr_md,
a.py_md,
a.py_sm,
a.border_b,
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 * as Toast from '#/view/com/util/Toast'
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 {Divider} from '#/components/Divider'
import * as Toggle from '#/components/forms/Toggle'
@@ -32,7 +32,7 @@ export function MessagesSettingsScreen({}: Props) {
const {mutate: updateDeclaration} = useUpdateActorDeclaration({
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 (
<CenteredView sideBorders style={a.h_full_vh}>
<ScrollView stickyHeaderIndices={[0]}>
<ViewHeader title={_(msg`Chat Settings`)} showOnDesktop showBorder />
<View style={[a.p_lg, a.gap_md]}>
<Text style={[a.text_lg, a.font_bold]}>
@@ -107,7 +107,7 @@ export function MessagesSettingsScreen({}: Props) {
a.rounded_md,
t.atoms.bg_contrast_25,
]}>
<Text style={[t.atoms.text_contrast_high]}>
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
<Trans>
You can continue ongoing conversations regardless of which setting
you choose.
@@ -149,6 +149,6 @@ export function MessagesSettingsScreen({}: Props) {
</>
)}
</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.`,
),
'xmark',
)
logger.error(`Failed to toggle labeler like`, {message: e.message})
}
@@ -104,7 +104,7 @@ let ProfileHeaderStandard = ({
} catch (e: any) {
if (e?.name !== 'AbortError') {
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) {
if (e?.name !== 'AbortError') {
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) {
if (e?.name !== 'AbortError') {
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])
@@ -359,7 +359,7 @@ function Header({
captureAction(ProgressGuideAction.Follow, dids.length)
Toast.show(_(msg`All accounts have been followed!`))
} 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 {
setIsProcessing(false)
}
@@ -672,7 +672,7 @@ function InvalidStarterPack({rkey}: {rkey: string}) {
onError: e => {
setIsProcessing(false)
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
case 'AddProfile':
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 {
updatedState = {...state, profiles: [...state.profiles, action.profile]}
}
@@ -89,7 +92,7 @@ function reducer(state: State, action: Action): State {
break
case 'AddFeed':
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 {
updatedState = {...state, feeds: [...state.feeds, action.feed]}
}
+2 -2
View File
@@ -227,7 +227,7 @@ function WizardInner({
onError: e => {
logger.error('Failed to create starter pack', {safeMessage: e})
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({
@@ -235,7 +235,7 @@ function WizardInner({
onError: e => {
logger.error('Failed to edit starter pack', {safeMessage: e})
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(
(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,
})
@@ -81,19 +85,25 @@ export function useActorAutocompleteFn() {
}
}
return computeSuggestions(
res?.data.actors,
moderationOpts || DEFAULT_MOD_OPTS,
)
return computeSuggestions({
q: query,
searched: res?.data.actors,
moderationOpts: moderationOpts || DEFAULT_MOD_OPTS,
})
},
[queryClient, moderationOpts, agent],
)
}
function computeSuggestions(
searched: AppBskyActorDefs.ProfileViewBasic[] = [],
moderationOpts: ModerationOpts,
) {
function computeSuggestions({
q,
searched = [],
moderationOpts,
}: {
q?: string
searched?: AppBskyActorDefs.ProfileViewBasic[]
moderationOpts: ModerationOpts
}) {
let items: AppBskyActorDefs.ProfileViewBasic[] = []
for (const item of searched) {
if (!items.find(item2 => item2.handle === item.handle)) {
@@ -102,6 +112,7 @@ function computeSuggestions(
}
return items.filter(profile => {
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
const RQKEY_ROOT = 'notification-feed'
export function RQKEY() {
return [RQKEY_ROOT]
export function RQKEY(priority?: false) {
return [RQKEY_ROOT, priority]
}
export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
export function useNotificationFeedQuery(opts?: {
enabled?: boolean
overridePriorityNotifications?: boolean
}) {
const agent = useAgent()
const queryClient = useQueryClient()
const moderationOpts = useModerationOpts()
@@ -59,6 +62,10 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
const lastPageCountRef = useRef(0)
const gate = useGate()
// false: force showing all notifications
// undefined: let the server decide
const priority = opts?.overridePriorityNotifications ? false : undefined
const query = useInfiniteQuery<
FeedPage,
Error,
@@ -67,7 +74,7 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
RQPageParam
>({
staleTime: STALE.INFINITY,
queryKey: RQKEY(),
queryKey: RQKEY(priority),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
let page
if (!pageParam) {
@@ -75,17 +82,17 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
page = unreads.getCachedUnreadPage()
}
if (!page) {
page = (
await fetchPage({
agent,
limit: PAGE_SIZE,
cursor: pageParam,
queryClient,
moderationOpts,
fetchAdditionalData: true,
shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'),
})
).page
const {page: fetchedPage} = await fetchPage({
agent,
limit: PAGE_SIZE,
cursor: pageParam,
queryClient,
moderationOpts,
fetchAdditionalData: true,
shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'),
priority,
})
page = fetchedPage
}
// 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
seenAt: Date
items: FeedNotification[]
priority: boolean
}
export interface CachedFeedPage {
+7 -1
View File
@@ -39,10 +39,15 @@ export async function fetchPage({
moderationOpts: ModerationOpts | undefined
fetchAdditionalData: boolean
shouldUngroupFollowBacks?: () => boolean
}): Promise<{page: FeedPage; indexedAt: string | undefined}> {
priority?: boolean
}): Promise<{
page: FeedPage
indexedAt: string | undefined
}> {
const res = await agent.listNotifications({
limit,
cursor,
// priority,
})
const indexedAt = res.data.notifications[0]?.indexedAt
@@ -88,6 +93,7 @@ export async function fetchPage({
cursor: res.data.cursor,
seenAt,
items: notifsGrouped,
priority: res.data.priority ?? false,
},
indexedAt,
}
+1 -1
View File
@@ -24,7 +24,7 @@ export function useVideoState({setError}: {setError: (error: string) => void}) {
onError: (e: any) => {
// Don't log these errors in sentry, just let the user know
if (e instanceof VideoTooLargeError) {
Toast.show(_(msg`Videos cannot be larger than 100MB`))
Toast.show(_(msg`Videos cannot be larger than 100MB`), 'xmark')
return
}
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`))
} 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})
}
}, [_, feed, pinOnSave, addSavedFeeds, isSaved])
@@ -138,7 +138,7 @@ export function FeedSourceCardLoaded({
// await item.unsave()
Toast.show(_(msg`Removed from my feeds`))
} 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})
}
}, [_, removeFeed, savedFeedConfig])
+6 -2
View File
@@ -66,7 +66,10 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
const saveImageToAlbumWithToasts = React.useCallback(
async (uri: string) => {
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) {
requestPermission()
} 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.`,
),
'xmark',
)
}
return
@@ -83,7 +87,7 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
await saveImageToMediaLibrary({uri})
Toast.show(_(msg`Saved to your camera roll`))
} catch (e: any) {
Toast.show(_(msg`Failed to save image: ${String(e)}`))
Toast.show(_(msg`Failed to save image: ${String(e)}`), 'xmark')
}
},
[permissionResponse, requestPermission, _],
+7 -6
View File
@@ -77,7 +77,7 @@ export function Component({}: {}) {
const onCopy = React.useCallback(() => {
if (appPassword) {
setStringAsync(appPassword)
Toast.show(_(msg`Copied to clipboard`))
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
setWasCopied(true)
}
}, [appPassword, _])
@@ -93,7 +93,7 @@ export function Component({}: {}) {
_(
msg`Please enter a name for your app password. All spaces is not allowed.`,
),
'times',
'xmark',
)
return
}
@@ -101,13 +101,13 @@ export function Component({}: {}) {
if (name.length < 4) {
Toast.show(
_(msg`App Password names must be at least 4 characters long.`),
'times',
'xmark',
)
return
}
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
}
@@ -116,11 +116,11 @@ export function Component({}: {}) {
if (newPassword) {
setAppPassword(newPassword.password)
} else {
Toast.show(_(msg`Failed to create app password.`), 'times')
Toast.show(_(msg`Failed to create app password.`), 'xmark')
// TODO: better error handling (?)
}
} 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})
}
}
@@ -137,6 +137,7 @@ export function Component({}: {}) {
_(
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(() => {
setStringAsync(isDNSForm ? `did=${currentAccount.did}` : currentAccount.did)
Toast.show(_(msg`Copied to clipboard`))
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
}, [currentAccount, isDNSForm, _])
const onChangeHandle = React.useCallback(
(v: string) => {
+1 -1
View File
@@ -150,7 +150,7 @@ function InviteCode({
const onPress = React.useCallback(() => {
setStringAsync(invite.code)
Toast.show(_(msg`Copied to clipboard`))
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
setInviteCopied(invite.code)
}, [setInviteCopied, invite, _])
+19 -18
View File
@@ -7,32 +7,33 @@ import {
View,
} from 'react-native'
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
import {ScrollView, TextInput} from './util'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from '../util/text/Text'
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 {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {HITSLOP_20} from '#/lib/constants'
import {useModalControls} from '#/state/modals'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {
useDangerousListMembershipsQuery,
getMembership,
ListMembersip,
useDangerousListMembershipsQuery,
useListMembershipAddMutation,
useListMembershipRemoveMutation,
} 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%']
@@ -200,7 +201,7 @@ function UserResult({
onChange?.('remove', profile)
}
} catch (e) {
Toast.show(cleanError(e))
Toast.show(cleanError(e), 'xmark')
} finally {
setIsProcessing(false)
}
+1 -1
View File
@@ -166,7 +166,7 @@ function ListItem({
onRemove?.(list.uri)
}
} catch (e) {
Toast.show(cleanError(e))
Toast.show(cleanError(e), 'xmark')
} finally {
setIsProcessing(false)
}
+6 -1
View File
@@ -35,11 +35,13 @@ export function Feed({
onPressTryAgain,
onScrolledDownChange,
ListHeaderComponent,
overridePriorityNotifications,
}: {
scrollElRef?: ListRef
onPressTryAgain?: () => void
onScrolledDownChange: (isScrolledDown: boolean) => void
ListHeaderComponent?: () => JSX.Element
overridePriorityNotifications?: boolean
}) {
const initialNumToRender = useInitialNumToRender()
@@ -59,7 +61,10 @@ export function Feed({
hasNextPage,
isFetchingNextPage,
fetchNextPage,
} = useNotificationFeedQuery({enabled: !!moderationOpts})
} = useNotificationFeedQuery({
enabled: !!moderationOpts,
overridePriorityNotifications,
})
const isEmpty = !isFetching && !data?.pages[0]?.items.length
const items = React.useMemo(() => {
@@ -96,7 +96,7 @@ function PostThreadFollowBtnLoaded({
} catch (e: any) {
if (e?.name !== 'AbortError') {
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) {
if (e?.name !== 'AbortError') {
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(
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})
}
+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.`,
),
'exclamation-circle',
)
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.`,
),
'exclamation-circle',
)
logger.error('Failed up update feeds', {message: err})
}
+2 -2
View File
@@ -56,12 +56,12 @@ let FeedSlice = ({
record={slice.items[last].record}
reason={slice.items[last].reason}
feedContext={slice.items[last].feedContext}
parentAuthor={slice.items[2].parentAuthor}
parentAuthor={slice.items[last].parentAuthor}
showReplyTo={false}
moderation={slice.items[last].moderation}
isThreadParent={isThreadParentAt(slice.items, last)}
isThreadChild={isThreadChildAt(slice.items, last)}
isParentBlocked={slice.items[2].isParentBlocked}
isParentBlocked={slice.items[last].isParentBlocked}
isThreadLastChild
/>
</>
+2 -2
View File
@@ -33,7 +33,7 @@ export function FollowButton({
await queueFollow()
} catch (e: any) {
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()
} catch (e: any) {
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()
} catch (e: any) {
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, _])
@@ -195,7 +195,7 @@ function SuggestedFollow({
await queueUnfollow()
} catch (e: any) {
if (e?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`))
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
}
}
}, [queueUnfollow, _])
+6 -6
View File
@@ -108,7 +108,7 @@ let ProfileMenu = ({
} catch (e: any) {
if (e?.name !== 'AbortError') {
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 {
@@ -119,7 +119,7 @@ let ProfileMenu = ({
} catch (e: any) {
if (e?.name !== 'AbortError') {
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) {
if (e?.name !== 'AbortError') {
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 {
@@ -145,7 +145,7 @@ let ProfileMenu = ({
} catch (e: any) {
if (e?.name !== 'AbortError') {
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) {
if (e?.name !== 'AbortError') {
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])
@@ -172,7 +172,7 @@ let ProfileMenu = ({
} catch (e: any) {
if (e?.name !== 'AbortError') {
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])
+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 React from 'react'
import {Animated, StyleSheet, View} from 'react-native'
import {Props as FontAwesomeProps} from '@fortawesome/react-native-fontawesome'
import {Text} from './text/Text'
import {colors} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {
FontAwesomeIcon,
Props as FontAwesomeProps,
} from '@fortawesome/react-native-fontawesome'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {IS_TEST} from '#/env'
const TIMEOUT = 4e3
const TIMEOUT = 2e3
export function show(
message: string,
_icon: FontAwesomeProps['icon'] = 'check',
icon: FontAwesomeProps['icon'] = 'check',
) {
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(() => {
item.destroy()
}, TIMEOUT)
}, TIMEOUT + 1e3)
}
function Toast({message}: {message: string}) {
const theme = useTheme()
const pal = usePalette('default')
const interp = useAnimatedValue(0)
function Toast({
message,
icon,
}: {
message: string
icon: FontAwesomeProps['icon']
}) {
const t = useTheme()
const {top} = useSafeAreaInsets()
React.useEffect(() => {
Animated.sequence([
Animated.timing(interp, {
toValue: 1,
duration: 150,
useNativeDriver: true,
}),
Animated.delay(3700),
Animated.timing(interp, {
toValue: 0,
duration: 150,
useNativeDriver: true,
}),
]).start()
})
// for the exit animation to work on iOS the animated component
// must not be the root component
// so we need to wrap it in a view and unmount the toast ahead of time
const [alive, setAlive] = useState(true)
useEffect(() => {
setTimeout(() => {
setAlive(false)
}, TIMEOUT)
}, [])
const opacityStyle = {opacity: interp}
return (
<View style={styles.container} pointerEvents="none">
<Animated.View
style={[
pal.view,
pal.border,
styles.toast,
theme.colorScheme === 'dark' && styles.toastDark,
opacityStyle,
]}>
<Text type="lg-medium" style={pal.text}>
{message}
</Text>
</Animated.View>
<View
style={[a.absolute, {top: top + 15, left: 16, right: 16}]}
pointerEvents="none">
{alive && (
<Animated.View
entering={FadeInUp}
exiting={FadeOutUp}
style={[
a.flex_1,
t.atoms.bg,
a.shadow_lg,
a.rounded_sm,
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>
)
}
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
*/
import React, {useState, useEffect} from 'react'
import React, {useEffect, useState} from 'react'
import {StyleSheet, Text, View} from 'react-native'
import {
FontAwesomeIcon,
@@ -39,7 +39,7 @@ export const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
<View style={styles.container}>
<FontAwesomeIcon
icon={activeToast.icon}
size={24}
size={20}
style={styles.icon as FontAwesomeIconStyle}
/>
<Text style={styles.text}>{activeToast.text}</Text>
@@ -79,6 +79,7 @@ const styles = StyleSheet.create({
},
icon: {
color: '#fff',
flexShrink: 0,
},
text: {
color: '#fff',
+24 -2
View File
@@ -8,6 +8,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {usePalette} from 'lib/hooks/usePalette'
import {
@@ -179,6 +180,7 @@ let UserAvatar = ({
const pal = usePalette('default')
const backgroundColor = pal.colors.backgroundLight
const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square')
const gate = useGate()
const aviStyle = useMemo(() => {
if (finalShape === 'square') {
@@ -221,7 +223,12 @@ let UserAvatar = ({
testID="userAvatarImage"
style={aviStyle}
resizeMode="cover"
source={{uri: avatar}}
source={{
uri: hackModifyThumbnailPath(
avatar,
size < 90 && gate('small_avi_thumb'),
),
}}
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
/>
) : (
@@ -229,7 +236,12 @@ let UserAvatar = ({
testID="userAvatarImage"
style={aviStyle}
contentFit="cover"
source={{uri: avatar}}
source={{
uri: hackModifyThumbnailPath(
avatar,
size < 90 && gate('small_avi_thumb'),
),
}}
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
/>
)}
@@ -433,6 +445,16 @@ let PreviewableUserAvatar = ({
PreviewableUserAvatar = memo(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({
editButtonContainer: {
position: 'absolute',
+6 -3
View File
@@ -149,7 +149,7 @@ let PostDropdownBtn = ({
},
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) {
if (e?.name !== 'AbortError') {
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])
@@ -186,7 +189,7 @@ let PostDropdownBtn = ({
const str = richTextToString(richText, true)
Clipboard.setStringAsync(str)
Toast.show(_(msg`Copied to clipboard`))
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
}, [_, richText])
const onPressTranslate = React.useCallback(() => {
+1 -1
View File
@@ -354,7 +354,7 @@ let PostCtrls = ({
onPress={e => {
e.stopPropagation()
Clipboard.setStringAsync(feedContext)
Toast.show(_(msg`Copied to clipboard`))
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
}}>
<Text
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 {faCircleDot} from '@fortawesome/free-solid-svg-icons/faCircleDot'
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 {faCommentSlash} from '@fortawesome/free-solid-svg-icons/faCommentSlash'
import {faDownload} from '@fortawesome/free-solid-svg-icons/faDownload'
@@ -140,6 +141,7 @@ library.add(
faCircleExclamation,
faCirclePlay,
faCircleUser,
faClipboardCheck,
faClone,
farClone,
faComment,
+2 -2
View File
@@ -36,7 +36,7 @@ export function AccessibilitySettingsScreen({}: Props) {
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
const {screen} = useAnalytics()
const {isMobile} = useWebMediaQueries()
const {isMobile, isTabletOrMobile} = useWebMediaQueries()
const {_} = useLingui()
const requireAltTextEnabled = useRequireAltTextEnabled()
@@ -58,7 +58,7 @@ export function AccessibilitySettingsScreen({}: Props) {
return (
<View style={s.hContentRegion} testID="accessibilitySettingsScreen">
<SimpleViewHeader
showBackButton={isMobile}
showBackButton={isTabletOrMobile}
style={[
pal.border,
{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 {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {useAnalytics} from '#/lib/analytics/analytics'
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 {isNative} from '#/platform/detection'
import {emitSoftReset, listenSoftReset} from '#/state/events'
@@ -17,37 +25,32 @@ import {
import {truncateAndInvalidate} from '#/state/queries/util'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {ComposeIcon2} from 'lib/icons'
import {
NativeStackScreenProps,
NotificationsTabNavigatorParams,
} from 'lib/routes/types'
import {colors, s} from 'lib/styles'
import {TextLink} from 'view/com/util/Link'
import {Feed} from '#/view/com/notifications/Feed'
import {FAB} from '#/view/com/util/fab/FAB'
import {MainScrollProvider} from '#/view/com/util/MainScrollProvider'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {ListMethods} from 'view/com/util/List'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
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 {Feed} from '../com/notifications/Feed'
import {FAB} from '../com/util/fab/FAB'
import {MainScrollProvider} from '../com/util/MainScrollProvider'
import {ViewHeader} from '../com/util/ViewHeader'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<
NotificationsTabNavigatorParams,
'Notifications'
>
export function NotificationsScreen({}: Props) {
export function NotificationsScreen({route: {params}}: Props) {
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const [isLoadingLatest, setIsLoadingLatest] = React.useState(false)
const scrollElRef = React.useRef<ListMethods>(null)
const {screen} = useAnalytics()
const pal = usePalette('default')
const t = useTheme()
const {isDesktop} = useWebMediaQueries()
const queryClient = useQueryClient()
const unreadNotifs = useUnreadNotifications()
@@ -109,56 +112,87 @@ export function NotificationsScreen({}: Props) {
return listenSoftReset(onPressLoadLatest)
}, [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(() => {
if (isDesktop) {
return (
<View
style={[
pal.view,
{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 18,
paddingVertical: 12,
},
t.atoms.bg,
a.flex_row,
a.align_center,
a.justify_between,
a.gap_lg,
a.px_lg,
a.pr_md,
a.py_sm,
]}>
<TextLink
type="title-lg"
href="/notifications"
style={[pal.text, {fontWeight: 'bold'}]}
text={
<>
<Trans>Notifications</Trans>{' '}
<Button
label={_(msg`Notifications`)}
accessibilityHint={_(msg`Refresh notifications`)}
onPress={emitSoftReset}>
{({hovered, pressed}) => (
<Text
style={[
a.text_2xl,
a.font_bold,
(hovered || pressed) && a.underline,
]}>
<Trans>Notifications</Trans>
{hasNew && (
<View
style={{
left: 4,
top: -8,
backgroundColor: colors.blue3,
backgroundColor: t.palette.primary_500,
width: 8,
height: 8,
borderRadius: 4,
}}
/>
)}
</>
}
onPress={emitSoftReset}
/>
{isLoadingLatest ? <Loader size="md" /> : <></>}
</Text>
)}
</Button>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
{isLoadingLatest ? <Loader size="md" /> : <></>}
{renderButton()}
</View>
</View>
)
}
return <></>
}, [isDesktop, pal, hasNew, isLoadingLatest])
}, [isDesktop, t, hasNew, renderButton, _, isLoadingLatest])
const renderHeaderSpinner = React.useCallback(() => {
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} /> : <></>}
{renderButton()}
</View>
)
}, [isLoadingLatest])
}, [renderButton, isLoadingLatest])
return (
<CenteredView
@@ -176,6 +210,7 @@ export function NotificationsScreen({}: Props) {
onScrolledDownChange={setIsScrolledDown}
scrollElRef={scrollElRef}
ListHeaderComponent={ListHeaderComponent}
overridePriorityNotifications={params?.show === 'all'}
/>
</MainScrollProvider>
{(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 setMinimalShellMode = useSetMinimalShellMode()
const {screen} = useAnalytics()
const {isMobile} = useWebMediaQueries()
const {isTabletOrMobile} = useWebMediaQueries()
useFocusEffect(
React.useCallback(() => {
@@ -41,26 +41,23 @@ export function PreferencesExternalEmbeds({}: Props) {
return (
<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
// @ts-ignore web only -prf
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={styles.infoCard}>
<Text style={pal.text}>
+34 -59
View File
@@ -1,24 +1,24 @@
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 {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Slider} from '@miblanchard/react-native-slider'
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 {
usePreferencesQuery,
useSetFeedViewPreferencesMutation,
} from '#/state/queries/preferences'
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 {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'
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'
function RepliesThresholdInput({
enabled,
@@ -79,10 +79,10 @@ type Props = NativeStackScreenProps<
CommonNavigatorParams,
'PreferencesFollowingFeed'
>
export function PreferencesFollowingFeed({navigation}: Props) {
export function PreferencesFollowingFeed({}: Props) {
const pal = usePalette('default')
const {_} = useLingui()
const {isTabletOrDesktop} = useWebMediaQueries()
const {isTabletOrMobile} = useWebMediaQueries()
const {data: preferences} = usePreferencesQuery()
const {mutate: setFeedViewPref, variables} =
useSetFeedViewPreferencesMutation()
@@ -92,26 +92,25 @@ export function PreferencesFollowingFeed({navigation}: Props) {
)
return (
<CenteredView
testID="preferencesHomeFeedScreen"
style={[
pal.view,
pal.border,
styles.container,
isTabletOrDesktop && styles.desktopContainer,
]}>
<ViewHeader title={_(msg`Following Feed Preferences`)} showOnDesktop />
<View
style={[
styles.titleSection,
isTabletOrDesktop && {paddingTop: 20, paddingBottom: 20},
]}>
<Text type="xl" style={[pal.textLight, styles.description]}>
<Trans>Fine-tune the content you see on your Following feed.</Trans>
</Text>
</View>
<ScrollView>
<View testID="preferencesHomeFeedScreen" style={s.hContentRegion}>
<ScrollView
// @ts-ignore web only -sfn
dataSet={{'stable-gutters': 1}}
contentContainerStyle={{paddingBottom: 75}}>
<SimpleViewHeader
showBackButton={isTabletOrMobile}
style={[pal.border, {borderBottomWidth: 1}]}>
<View style={{flex: 1}}>
<Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
<Trans>Following Feed Preferences</Trans>
</Text>
<Text style={pal.textLight}>
<Trans>
Fine-tune the content you see on your Following feed.
</Trans>
</Text>
</View>
</SimpleViewHeader>
<View style={styles.cardsContainer}>
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
@@ -253,7 +252,7 @@ export function PreferencesFollowingFeed({navigation}: Props) {
<View style={[pal.viewLight, styles.card]}>
<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>
</Text>
<Text style={[pal.text, s.pb10]}>
@@ -288,42 +287,17 @@ export function PreferencesFollowingFeed({navigation}: Props) {
</View>
</View>
</ScrollView>
<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>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingBottom: 90,
},
desktopContainer: {
borderLeftWidth: 1,
borderRightWidth: 1,
paddingBottom: 40,
},
titleSection: {
paddingBottom: 30,
@@ -338,6 +312,7 @@ const styles = StyleSheet.create({
},
cardsContainer: {
paddingHorizontal: 20,
paddingVertical: 16,
},
card: {
padding: 16,
+39 -69
View File
@@ -1,33 +1,29 @@
import React from 'react'
import {
ActivityIndicator,
ScrollView,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from '../com/util/text/Text'
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 {msg, Trans} from '@lingui/macro'
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 {
usePreferencesQuery,
useSetThreadViewPreferencesMutation,
} 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'>
export function PreferencesThreads({navigation}: Props) {
export function PreferencesThreads({}: Props) {
const pal = usePalette('default')
const {_} = useLingui()
const {isTabletOrDesktop} = useWebMediaQueries()
const {isTabletOrMobile} = useWebMediaQueries()
const {data: preferences} = usePreferencesQuery()
const {mutate: setThreadViewPrefs, variables} =
useSetThreadViewPreferencesMutation()
@@ -42,27 +38,25 @@ export function PreferencesThreads({navigation}: Props) {
)
return (
<CenteredView
testID="preferencesThreadsScreen"
style={[
pal.view,
pal.border,
styles.container,
isTabletOrDesktop && styles.desktopContainer,
]}>
<ViewHeader title={_(msg`Thread Preferences`)} showOnDesktop />
<View
style={[
styles.titleSection,
isTabletOrDesktop && {paddingTop: 20, paddingBottom: 20},
]}>
<Text type="xl" style={[pal.textLight, styles.description]}>
<Trans>Fine-tune the discussion threads.</Trans>
</Text>
</View>
<View testID="preferencesThreadsScreen" style={s.hContentRegion}>
<ScrollView
// @ts-ignore web only -prf
dataSet={{'stable-gutters': 1}}
contentContainerStyle={{paddingBottom: 75}}>
<SimpleViewHeader
showBackButton={isTabletOrMobile}
style={[pal.border, {borderBottomWidth: 1}]}>
<View style={{flex: 1}}>
<Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
<Trans>Thread Preferences</Trans>
</Text>
<Text style={pal.textLight}>
<Trans>Fine-tune the discussion threads.</Trans>
</Text>
</View>
</SimpleViewHeader>
{preferences ? (
<ScrollView>
{preferences ? (
<View style={styles.cardsContainer}>
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
@@ -136,46 +130,21 @@ export function PreferencesThreads({navigation}: Props) {
/>
</View>
</View>
</ScrollView>
) : (
<ActivityIndicator />
)}
<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>
) : (
<ActivityIndicator style={a.flex_1} />
)}
</ScrollView>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingBottom: 90,
},
desktopContainer: {
borderLeftWidth: 1,
borderRightWidth: 1,
paddingBottom: 40,
},
titleSection: {
paddingBottom: 30,
@@ -190,6 +159,7 @@ const styles = StyleSheet.create({
},
cardsContainer: {
paddingHorizontal: 20,
paddingVertical: 16,
},
card: {
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.`,
),
'xmark',
)
logger.error('Failed up update feeds', {message: err})
}
@@ -231,7 +232,7 @@ export function ProfileFeedScreenInner({
])
}
} 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})
}
}, [
@@ -543,6 +544,7 @@ function AboutSection({
_(
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})
}
+2 -2
View File
@@ -300,7 +300,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
Toast.show(_(msg`Saved to your feeds`))
}
} 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})
}
}, [
@@ -319,7 +319,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
await removeSavedFeed(savedFeedConfig)
Toast.show(_(msg`Removed from your feeds`))
} 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})
}
}, [playHaptic, removeSavedFeed, _, savedFeedConfig])
+3 -3
View File
@@ -234,7 +234,7 @@ function ListItem({
},
])
} 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})
}
}, [_, playHaptic, feed, updateSavedFeeds, resetSaveFeedsMutationState])
@@ -260,7 +260,7 @@ function ListItem({
index: nextIndex,
})
} 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})
}
}, [feed, isPinned, overwriteSavedFeeds, currentFeeds, _])
@@ -286,7 +286,7 @@ function ListItem({
index: nextIndex,
})
} 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})
}
}, [feed, isPinned, overwriteSavedFeeds, currentFeeds, _])
@@ -43,7 +43,7 @@ export function ExportCarDialog({
}
} catch (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 {
setLoading(false)
control.close()
+4 -4
View File
@@ -34,10 +34,10 @@
jsonpointer "^5.0.0"
leven "^3.1.0"
"@atproto/api@^0.12.23":
version "0.12.23"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.23.tgz#b3409817d0b981a64f30d16e8257f0fe261338af"
integrity sha512-fgQ30u+q9smX5g41eep7fISSkSAhRkX0inc81PZ82QwcHbFkC8ePaha/KP0CoTaPWKi7EsC89Z/8BEBCJo0oBA==
"@atproto/api@0.12.25":
version "0.12.25"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.25.tgz#9eeb51484106a5e07f89f124e505674a3574f93b"
integrity sha512-IV3vGPnDw9bmyP/JOd8YKbm8fOpRAgJpEUVnIZNVb/Vo8v+WOroOjrJxtzdHOcXTL9IEcTTyXSCc7yE7kwhN2A==
dependencies:
"@atproto/common-web" "^0.3.0"
"@atproto/lexicon" "^0.4.0"