Merge remote-tracking branch 'upstream/main' into Improve-notification-localization
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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',
|
||||
)
|
||||
})
|
||||
}, [_])
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -145,6 +145,9 @@ export const atoms = {
|
||||
flex_shrink: {
|
||||
flexShrink: 1,
|
||||
},
|
||||
flex_shrink_0: {
|
||||
flexShrink: 0,
|
||||
},
|
||||
justify_start: {
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ let ConvoMenu = ({
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
Toast.show(_(msg`Could not mute chat`))
|
||||
Toast.show(_(msg`Could not mute chat`), 'xmark')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export function LeaveConvoPrompt({
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
Toast.show(_(msg`Could not leave chat`))
|
||||
Toast.show(_(msg`Could not leave chat`), 'xmark')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 => ({
|
||||
|
||||
@@ -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
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,3 +14,4 @@ export type Gate =
|
||||
| 'suggested_follows_interstitial'
|
||||
| 'ungroup_follow_backs'
|
||||
| 'videos'
|
||||
| 'small_avi_thumb'
|
||||
|
||||
+634
-548
File diff suppressed because it is too large
Load Diff
+923
-1235
File diff suppressed because it is too large
Load Diff
+635
-545
File diff suppressed because it is too large
Load Diff
+635
-545
File diff suppressed because it is too large
Load Diff
+635
-545
File diff suppressed because it is too large
Load Diff
+635
-553
File diff suppressed because it is too large
Load Diff
+635
-545
File diff suppressed because it is too large
Load Diff
+634
-548
File diff suppressed because it is too large
Load Diff
+635
-545
File diff suppressed because it is too large
Load Diff
+635
-545
File diff suppressed because it is too large
Load Diff
+628
-554
File diff suppressed because it is too large
Load Diff
+635
-545
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+635
-545
File diff suppressed because it is too large
Load Diff
+635
-545
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
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
]}>
|
||||
|
||||
@@ -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')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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]}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export interface FeedPage {
|
||||
cursor: string | undefined
|
||||
seenAt: Date
|
||||
items: FeedNotification[]
|
||||
priority: boolean
|
||||
}
|
||||
|
||||
export interface CachedFeedPage {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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, _],
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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, _])
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ function ListItem({
|
||||
onRemove?.(list.uri)
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.show(cleanError(e))
|
||||
Toast.show(cleanError(e), 'xmark')
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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, _])
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user