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 : (
+
+ ),
+ }),
+ [clearAllSpam, clearingAll, styles.headerContainer, styles.headerText]
+ );
+
+ // Navigate back to the main screen when no request to display
+ useEffect(() => {
+ const unsubscribe = navRef.current?.addListener("focus", () => {
+ if (allRequests.length === 0) {
+ navRef.current?.goBack();
+ }
+ });
+ return unsubscribe;
+ }, [allRequests]);
+
+ const handleSegmentChange = (index: number) => {
+ setSelectedSegment(index);
+ };
+
+ const renderSegmentedController = () => {
+ if (likelyNotSpam.length > 0 && likelySpam.length > 0) {
+ return (
+
+ );
+ }
+ return null;
+ };
+
+ const renderSuggestionText = () => {
+ if (likelyNotSpam.length > 0) {
+ return (
+
+ Based on your onchain history, we've made some suggestions on who you
+ may know.
+
+ );
+ }
+ return null;
+ };
+
+ const renderContent = (navigationProps: {
+ route: RouteProp;
+ navigation: any;
+ }) => {
+ if (likelyNotSpam.length === 0 && likelySpam.length === 0) {
+ return (
+
+
+ No message requests at this time.
+
+
+ );
+ }
+
+ if (likelyNotSpam.length === 0 && likelySpam.length > 0) {
+ return (
+ <>
+
+ You have some message requests that might be spam. Review them
+ carefully.
+
+
+ >
+ );
+ }
+
+ return (
+ <>
+ {renderSegmentedController()}
+ {renderSuggestionText()}
+
+ >
+ );
+ };
+
+ return (
+
+ {(navigationProps) => {
+ navRef.current = navigationProps.navigation;
+ return (
+ <>
+
+
+ {renderContent(navigationProps)}
+
+
+ >
+ );
+ }}
+
+ );
+}
+
+const useStyles = () => {
+ const colorScheme = useColorScheme();
+ return StyleSheet.create({
+ container: {
+ paddingTop: 4,
+ flex: 1,
+ },
+ root: {
+ flex: 1,
+ backgroundColor: backgroundColor(colorScheme),
+ },
+ headerContainer: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "center",
+ width: Platform.OS === "ios" ? 110 : 130,
+ },
+ headerText: {
+ marginLeft: 10,
+ color: textPrimaryColor(colorScheme),
+ ...Platform.select({
+ android: { fontSize: 22, fontFamily: "Roboto" },
+ }),
+ },
+ suggestionText: {
+ fontSize: 12,
+ color: textSecondaryColor(colorScheme),
+ textAlign: "center",
+ marginTop: 14,
+ marginBottom: 10,
+ marginHorizontal: 16,
+ },
+ emptyContainer: {
+ flex: 1,
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ emptyText: {
+ fontSize: 12,
+ color: textSecondaryColor(colorScheme),
+ textAlign: "center",
+ },
+ spamOnlyText: {
+ fontSize: 12,
+ color: textSecondaryColor(colorScheme),
+ textAlign: "center",
+ marginTop: 14,
+ marginBottom: 10,
+ marginHorizontal: 16,
+ },
+ });
+};
diff --git a/styles/colors/index.ts b/styles/colors/index.ts
index 4996a55db..34f1703ff 100644
--- a/styles/colors/index.ts
+++ b/styles/colors/index.ts
@@ -1,8 +1,8 @@
import { ColorSchemeName, Platform } from "react-native";
import { MD3DarkTheme, MD3LightTheme } from "react-native-paper";
-const BACKGROUND_LIGHT = "#FFF";
-const BACKGROUND_DARK = "#111";
+export const BACKGROUND_LIGHT = "#FFF";
+export const BACKGROUND_DARK = "#111";
export const backgroundColor = (colorScheme: ColorSchemeName) => {
if (colorScheme === "dark")
@@ -39,8 +39,8 @@ export const chatInputBackgroundColor = (colorScheme: ColorSchemeName) => {
: NAVIGATION_SECONDARY_BACKGROUND_LIGHT;
};
-const TEXT_PRIMARY_COLOR_LIGHT = "#000";
-const TEXT_PRIMARY_COLOR_DARK = "#FFF";
+export const TEXT_PRIMARY_COLOR_LIGHT = "#000";
+export const TEXT_PRIMARY_COLOR_DARK = "#FFF";
export const textPrimaryColor = (colorScheme: ColorSchemeName) => {
if (colorScheme === "dark")
@@ -214,6 +214,14 @@ export const dangerColor = (colorScheme: ColorSchemeName) => {
return colorScheme === "dark" ? "#FF453A" : "#FF3B30";
};
+const REQUESTS_TEXT_LIGHT = "rgba(126, 0, 204, 1)";
+const REQUESTS_TEXT_DARK = "rgba(126, 0, 204, 1)";
+
+export const requestsTextColor = (colorScheme: ColorSchemeName) => {
+ if (colorScheme === "dark") return REQUESTS_TEXT_DARK;
+ return REQUESTS_TEXT_LIGHT;
+};
+
// Generated using https://callstack.github.io/react-native-paper/docs/guides/theming#theme-properties
const MaterialLightColors = {