diff --git a/components/ConversationList/RequestsButton.tsx b/components/ConversationList/RequestsButton.tsx index 90faee16a..e88b0f114 100644 --- a/components/ConversationList/RequestsButton.tsx +++ b/components/ConversationList/RequestsButton.tsx @@ -1,16 +1,14 @@ import { NativeStackScreenProps } from "@react-navigation/native-stack"; import { - clickedItemBackgroundColor, - listItemSeparatorColor, - primaryColor, - textPrimaryColor, + actionSecondaryColor, + requestsTextColor, textSecondaryColor, } from "@styles/colors"; import { Platform, + Pressable, StyleSheet, Text, - TouchableHighlight, useColorScheme, View, } from "react-native"; @@ -22,21 +20,28 @@ type Props = { requestsCount: number } & NativeStackScreenProps< "Chats" | "ShareFrame" | "Blocked" >; export default function RequestsButton({ navigation, requestsCount }: Props) { - const colorScheme = useColorScheme(); const styles = useStyles(); + return ( - { - navigation.push("ChatsRequests"); - }} + onPress={() => navigation.push("ChatsRequests")} + style={styles.requestsHeader} > - - Requests - {requestsCount} - - + {({ pressed }) => ( + + + {requestsCount === 0 ? "Requests" : `Requests (${requestsCount})`} + + + )} + ); } @@ -44,55 +49,32 @@ const useStyles = () => { const colorScheme = useColorScheme(); return StyleSheet.create({ requestsHeader: { - flexDirection: "row", - alignItems: "center", ...Platform.select({ - default: { - paddingVertical: 8, - paddingRight: 8, - paddingLeft: 24, - height: 40, - borderBottomWidth: 0.25, - borderBottomColor: listItemSeparatorColor(colorScheme), - borderTopWidth: 0.25, - borderTopColor: listItemSeparatorColor(colorScheme), - }, android: { - paddingVertical: 12, paddingLeft: 24, paddingRight: 14, }, }), }, - requestsHeaderTitle: { - color: textPrimaryColor(colorScheme), - ...Platform.select({ - default: { - fontSize: 17, - fontWeight: "600", - marginBottom: 3, - marginRight: 110, - }, - android: { - fontSize: 16, - }, - }), - }, requestsCount: { - marginLeft: "auto", - + fontWeight: "500", + color: requestsTextColor(colorScheme), ...Platform.select({ default: { - marginRight: 16, - fontSize: 15, - color: textSecondaryColor(colorScheme), + fontSize: 14, + marginBottom: 3, }, android: { marginRight: 1, fontSize: 11, - color: primaryColor(colorScheme), }, }), }, + zeroRequests: { + color: textSecondaryColor(colorScheme), + }, + pressedText: { + color: actionSecondaryColor(colorScheme), + }, }); }; diff --git a/components/ConversationList/RequestsSegmentedController.tsx b/components/ConversationList/RequestsSegmentedController.tsx new file mode 100644 index 000000000..96dfdfc6a --- /dev/null +++ b/components/ConversationList/RequestsSegmentedController.tsx @@ -0,0 +1,98 @@ +import { + tertiaryBackgroundColor, + textPrimaryColor, + BACKGROUND_LIGHT, + TEXT_PRIMARY_COLOR_LIGHT, +} from "@styles/colors"; +import React from "react"; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + useColorScheme, +} from "react-native"; + +interface SegmentedControllerProps { + options: string[]; + selectedIndex: number; + onSelect: (index: number) => void; +} + +const RequestsSegmentedController: React.FC = ({ + options, + selectedIndex, + onSelect, +}) => { + const styles = useStyles(); + + return ( + + {options.map((option, index) => ( + onSelect(index)} + > + + {option} + + + ))} + + ); +}; + +const useStyles = () => { + const colorScheme = useColorScheme(); + return StyleSheet.create({ + container: { + flexDirection: "row", + backgroundColor: tertiaryBackgroundColor(colorScheme), + borderRadius: 8, + padding: 2, + height: 32, + marginTop: 10, + marginBottom: 2, + marginHorizontal: 16, + }, + option: { + flex: 1, + paddingVertical: 6, + alignItems: "center", + justifyContent: "center", + }, + selectedOption: { + backgroundColor: BACKGROUND_LIGHT, + borderRadius: 6, + }, + firstOption: { + borderTopLeftRadius: 6, + borderBottomLeftRadius: 6, + }, + lastOption: { + borderTopRightRadius: 6, + borderBottomRightRadius: 6, + }, + optionText: { + fontSize: 13, + color: textPrimaryColor(colorScheme), + }, + selectedOptionText: { + fontWeight: "500", + color: TEXT_PRIMARY_COLOR_LIGHT, + }, + }); +}; + +export default RequestsSegmentedController; diff --git a/screens/ConversationList.tsx b/screens/ConversationList.tsx index 527e58f15..95abac86d 100644 --- a/screens/ConversationList.tsx +++ b/screens/ConversationList.tsx @@ -2,6 +2,7 @@ import { NativeStackScreenProps } from "@react-navigation/native-stack"; import { backgroundColor, itemSeparatorColor, + listItemSeparatorColor, textPrimaryColor, } from "@styles/colors"; import React, { useCallback, useEffect, useState } from "react"; @@ -162,14 +163,16 @@ function ConversationList({ navigation, route, searchBarRef }: Props) { key="pinnedConversations" />, ]; + const showSearchTitleHeader = ((Platform.OS === "ios" && searchBarFocused && !showNoResult) || (Platform.OS === "android" && searchBarFocused)) && !sharingMode; + if (showSearchTitleHeader) { ListHeaderComponents.push( - Chats + Messages ); } else if ( @@ -177,12 +180,21 @@ function ConversationList({ navigation, route, searchBarRef }: Props) { !sharingMode ) { ListHeaderComponents.push( - + + Messages + + + ); + } else if (!sharingMode) { + ListHeaderComponents.push( + + Messages + ); } @@ -254,10 +266,39 @@ const useStyles = () => { color: textPrimaryColor(colorScheme), }, android: { - fontSize: 11, - textTransform: "uppercase", - fontWeight: "bold", - color: textPrimaryColor(colorScheme), + fontSize: 16, + }, + }), + }, + headerTitleContainer: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingTop: 12, + paddingBottom: 8, + paddingHorizontal: 16, + ...Platform.select({ + default: { + backgroundColor: backgroundColor(colorScheme), + borderTopWidth: 0.25, + borderTopColor: listItemSeparatorColor(colorScheme), + }, + android: { + borderBottomWidth: 0, + }, + }), + }, + headerTitle: { + color: textPrimaryColor(colorScheme), + ...Platform.select({ + default: { + fontSize: 16, + fontWeight: "600", + marginBottom: 3, + marginRight: 110, + }, + android: { + fontSize: 16, }, }), }, diff --git a/screens/Navigation/ConversationRequestsListNav.ios.tsx b/screens/Navigation/ConversationRequestsListNav.ios.tsx new file mode 100644 index 000000000..3084f8ac2 --- /dev/null +++ b/screens/Navigation/ConversationRequestsListNav.ios.tsx @@ -0,0 +1,261 @@ +import RequestsSegmentedController from "@components/ConversationList/RequestsSegmentedController"; +import { RouteProp } from "@react-navigation/native"; +import { + actionSheetColors, + backgroundColor, + textPrimaryColor, + textSecondaryColor, +} from "@styles/colors"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { Platform, StyleSheet, Text, useColorScheme, View } from "react-native"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { StackAnimationTypes } from "react-native-screens"; + +import { + NativeStack, + navigationAnimation, + NavigationParamList, +} from "./Navigation"; +import ActivityIndicator from "../../components/ActivityIndicator/ActivityIndicator"; +import AndroidBackAction from "../../components/AndroidBackAction"; +import Button from "../../components/Button/Button"; +import ConversationFlashList from "../../components/ConversationFlashList"; +import { showActionSheetWithOptions } from "../../components/StateHandlers/ActionSheetStateHandler"; +import { + useChatStore, + useCurrentAccount, +} from "../../data/store/accountsStore"; +import { + consentToPeersOnProtocol, + sortRequestsBySpamScore, + updateConsentStatus, +} from "../../utils/xmtpRN/conversations"; + +export default function ConversationRequestsListNav() { + const sortedConversationsWithPreview = useChatStore( + (s) => s.sortedConversationsWithPreview + ); + const colorScheme = useColorScheme(); + const account = useCurrentAccount() as string; + const navRef = useRef(); + const [clearingAll, setClearingAll] = useState(false); + + const [selectedSegment, setSelectedSegment] = useState(0); + const allRequests = sortedConversationsWithPreview.conversationsRequests; + const { likelySpam, likelyNotSpam } = sortRequestsBySpamScore(allRequests); + const styles = useStyles(); + + const clearAllSpam = useCallback(() => { + const methods = { + "Clear all!": async () => { + setClearingAll(true); + // @todo => handle groups here + const peers = Array.from( + new Set(allRequests.map((c) => c.peerAddress)) + ).filter((peer) => !!peer) as string[]; + await consentToPeersOnProtocol(account, peers, "deny"); + await updateConsentStatus(account); + setClearingAll(false); + navRef.current?.goBack(); + }, + Cancel: () => {}, + }; + + const options = Object.keys(methods); + + showActionSheetWithOptions( + { + options, + destructiveButtonIndex: options.indexOf("Clear all"), + cancelButtonIndex: options.indexOf("Cancel"), + title: + "Do you confirm? This will block all accounts that are currently tagged as requests.", + ...actionSheetColors(colorScheme), + }, + (selectedIndex?: number) => { + if (selectedIndex === undefined) return; + const method = (methods as any)[options[selectedIndex]]; + if (method) { + method(); + } + } + ); + }, [account, colorScheme, allRequests]); + + const navigationOptions = useCallback( + ({ + navigation, + }: { + route: RouteProp; + navigation: any; + }) => ({ + animation: navigationAnimation as StackAnimationTypes, + headerTitle: clearingAll + ? Platform.OS === "ios" + ? () => ( + + + Clearing + + ) + : "Clearing..." + : "Message requests", + headerLeft: + Platform.OS === "ios" + ? undefined + : () => , + headerRight: () => + clearingAll ? undefined : ( +