diff --git a/App.tsx b/App.tsx index 2368376d8..38ebbd692 100644 --- a/App.tsx +++ b/App.tsx @@ -1,6 +1,9 @@ import "expo-dev-client"; import "./polyfills"; - +import { + configureReanimatedLogger, + ReanimatedLogLevel, +} from "react-native-reanimated"; import { configure as configureCoinbase } from "@coinbase/wallet-mobile-sdk"; import DebugButton from "@components/DebugButton"; import { BottomSheetModalProvider } from "@design-system/BottomSheet/BottomSheetModalProvider"; @@ -48,8 +51,21 @@ LogBox.ignoreLogs([ "Privy: Expected status code 200, received 400", // Privy "Error destroying session", // Privy 'event="noNetwork', // ethers + "[Reanimated] Reading from `value` during component render. Please ensure that you do not access the `value` property or use `get` method of a shared value while React is rendering a component.", + "Attempted to import the module", ]); +// This is the default configuration +configureReanimatedLogger({ + level: ReanimatedLogLevel.warn, + strict: /* + Ignores the following warning: + "[Reanimated] Reading from `value` during component render. Please ensure that you do not access the `value` property or use `get` method of a shared value while React is rendering a component.", +todo investigate + + */ false, +}); + configureCoinbase({ callbackURL: new URL(`https://${config.websiteDomain}/coinbase`), hostURL: new URL("https://wallet.coinbase.com/wsegue"), diff --git a/components/ClickableText.tsx b/components/ClickableText.tsx index 197e4f600..ac0493681 100644 --- a/components/ClickableText.tsx +++ b/components/ClickableText.tsx @@ -2,7 +2,7 @@ import { ParsedText } from "@components/ParsedText/ParsedText"; import Clipboard from "@react-native-clipboard/clipboard"; import { actionSheetColors } from "@styles/colors"; import * as Linking from "expo-linking"; -import { useCallback } from "react"; +import { useCallback, useEffect } from "react"; import { StyleProp, StyleSheet, TextStyle, useColorScheme } from "react-native"; import { navigate } from "../utils/navigation"; import { @@ -38,7 +38,7 @@ export function ClickableText({ children, style }: Props) { }, []); const handleNewConversationPress = useCallback((peer: string) => { - navigate("NewConversation", { peer }); + navigate("Conversation", { peer }); }, []); const showCopyActionSheet = useCallback( diff --git a/components/Onboarding/WarpcastConnect.tsx b/components/Onboarding/WarpcastConnect.tsx deleted file mode 100644 index e3548dc0f..000000000 --- a/components/Onboarding/WarpcastConnect.tsx +++ /dev/null @@ -1,66 +0,0 @@ -// Comment until we really use it - -// import logger from "@utils/logger"; -// import { useState } from "react"; -// import { Text } from "react-native"; - -// import DeprecatedOnboardingComponent from "./DeprecatedOnboardingComponent"; -// import config from "../../config"; -// import { invalidateProfileSocialsQuery } from "../../data/helpers/profiles/profilesUpdate"; -// import { -// useCurrentAccount, -// useSettingsStore, -// } from "../../data/store/accountsStore"; -// import { useSelect } from "../../data/store/storeHelpers"; -// import { notifyFarcasterLinked } from "../../utils/api"; -// import { useLinkFarcaster } from "../../utils/evm/privy"; -// import { refreshRecommendationsForAccount } from "../../utils/recommendations"; - -// export default function WarpcastConnect() { -// const [loading, setLoading] = useState(false); -// const [error, setError] = useState(""); -// const currentAccount = useCurrentAccount() as string; -// const { setSkipFarcaster } = useSettingsStore( -// useSelect(["setSkipFarcaster"]) -// ); -// const linkWithFarcaster = useLinkFarcaster({ -// onSuccess: async () => { -// try { -// await notifyFarcasterLinked(); -// await invalidateProfileSocialsQuery(currentAccount, currentAccount); -// await refreshRecommendationsForAccount(currentAccount); -// } catch (e: any) { -// logger.error(e); -// setError("An unknown error occurred"); -// } -// setLoading(false); -// }, -// onError: (error) => { -// setLoading(false); -// setError(error?.message); -// }, -// }); -// return ( -// { -// setLoading(true); -// setError(""); -// linkWithFarcaster({ -// relyingParty: `https://${config.websiteDomain}`, -// }); -// }} -// isLoading={loading} -// backButtonText="Skip" -// backButtonAction={() => { -// setSkipFarcaster(true); -// }} -// > -// Connect to Warpcast -// {error && {error}} -// -// ); -// } diff --git a/components/Recommendations/Recommendation.tsx b/components/Recommendations/Recommendation.tsx deleted file mode 100644 index 007ac045a..000000000 --- a/components/Recommendations/Recommendation.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { IProfileSocials } from "@/features/profiles/profile-types"; -import { - itemSeparatorColor, - textPrimaryColor, - textSecondaryColor, -} from "@styles/colors"; -import { AvatarSizes } from "@styles/sizes"; -import { - Image, - Platform, - StyleSheet, - Text, - useColorScheme, - View, -} from "react-native"; - -import IconLoading from "@assets/icon-loading.png"; -import { RecommendationData } from "@data/store/recommendationsStore"; -import { - getPreferredAvatar, - getPreferredName, - getPrimaryNames, -} from "@utils/profile"; -import { shortAddress } from "@utils/strings/shortAddress"; -import { Avatar } from "../Avatar"; -import { NavigationChatButton } from "@search/components/NavigationChatButton"; -import { useProfileSocials } from "@/hooks/useProfileSocials"; - -export function Recommendation({ - address, - // @todo => use only profile - recommendationData: { ens, farcasterUsernames, lensHandles, tags, profile }, - embedInChat, - isVisible, - groupMode, - addToGroup, -}: { - address: string; - recommendationData: RecommendationData; - embedInChat?: boolean; - isVisible: boolean; - groupMode?: boolean; - addToGroup?: (member: IProfileSocials & { address: string }) => void; -}) { - const styles = useStyles(); - let primaryNamesDisplay = [ - ...(lensHandles || []).map((l) => `${l} on lens`), - ...(farcasterUsernames || []).map((f) => `${f} on farcaster`), - ]; - const preferredName = profile // @todo => use only preferred name - ? getPreferredName(profile, address) - : ens || shortAddress(address); - if (profile) { - const primaryNames = getPrimaryNames(profile); - primaryNamesDisplay = [ - ...primaryNames.filter((name) => name !== preferredName), - shortAddress(address), - ]; - } - const textAlign = embedInChat ? "center" : "left"; - const socials = useProfileSocials(address); - - return ( - - {!embedInChat && ( - - )} - - - {preferredName} - - - {primaryNamesDisplay.length > 0 && ( - - {primaryNamesDisplay.join(" | ")} - - )} - {tags.map((t) => ( - - - - - {t.text} - - - ))} - - {!embedInChat && ( - - addToGroup({ ...socials, address }) : undefined - } - /> - - )} - - ); -} - -const useStyles = () => { - const colorScheme = useColorScheme(); - return StyleSheet.create({ - recommendation: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - ...Platform.select({ - default: { - paddingVertical: 15, - paddingLeft: Platform.OS === "android" ? 0 : 16, - }, - android: { paddingVertical: 12 }, - }), - }, - recommendationBorderBottom: { - ...Platform.select({ - default: { - borderBottomWidth: 0.5, - borderBottomColor: itemSeparatorColor(colorScheme), - }, - android: {}, - }), - }, - avatar: { - marginRight: Platform.OS === "ios" ? 13 : 16, - }, - recommendationLeft: { - flexGrow: 1, - flexShrink: 1, - justifyContent: "center", - }, - recommendationRight: { - marginLeft: Platform.OS === "ios" ? 10 : 0, - justifyContent: "center", - }, - recommendationTitle: { - width: "100%", - color: textPrimaryColor(colorScheme), - ...Platform.select({ - default: { - fontSize: 17, - fontWeight: "600", - marginBottom: 3, - marginRight: 110, - }, - android: { - fontSize: 16, - }, - }), - }, - recommendationText: { - color: textSecondaryColor(colorScheme), - ...Platform.select({ - default: { - fontSize: 15, - }, - android: { - fontSize: 14, - flexGrow: 1, - }, - }), - alignSelf: "flex-start", - }, - recommendationRow: { - flexDirection: "row", - alignItems: "center", - marginVertical: 3, - }, - recommendationImage: { - width: 15, - height: 15, - marginRight: 10, - }, - }); -}; diff --git a/components/Recommendations/Recommendations.tsx b/components/Recommendations/Recommendations.tsx deleted file mode 100644 index 575736065..000000000 --- a/components/Recommendations/Recommendations.tsx +++ /dev/null @@ -1,303 +0,0 @@ -import { IProfileSocials } from "@/features/profiles/profile-types"; -import { useCallback, useEffect, useState } from "react"; -import { - FlatList, - Keyboard, - Platform, - View, - ViewStyle, - TextStyle, -} from "react-native"; -import * as Linking from "expo-linking"; - -import { Recommendation } from "./Recommendation"; -import config from "@config"; -import { - useAccountsStore, - useCurrentAccount, - useRecommendationsStore, -} from "@data/store/accountsStore"; -import { useSelect } from "@data/store/storeHelpers"; -import { useRouter } from "@navigation/useNavigation"; -import { refreshRecommendationsForAccount } from "@utils/recommendations"; -import { translate } from "@/i18n"; -import { Text } from "@design-system/Text"; -import { Loader } from "@/design-system/loader"; -import { ThemedStyle, useAppTheme } from "@/theme/useAppTheme"; -import { textSizeStyles } from "@/design-system/Text/Text.styles"; - -const EXPIRE_AFTER = 86400000; // 1 DAY - -export default function Recommendations({ - visibility, - groupMode, - groupMembers, - addToGroup, - showTitle = true, -}: { - visibility: "FULL" | "EMBEDDED" | "HIDDEN"; - groupMode?: boolean; - groupMembers?: (IProfileSocials & { address: string })[]; - addToGroup?: (member: IProfileSocials & { address: string }) => void; - showTitle?: boolean; -}) { - const navigation = useRouter(); - - const userAddress = useCurrentAccount(); - const currentAccount = useAccountsStore((s) => s.currentAccount); - const { - frens, - setLoadingRecommendations, - setRecommendations, - loading, - updatedAt, - } = useRecommendationsStore( - useSelect([ - "frens", - "setLoadingRecommendations", - "setRecommendations", - "loading", - "updatedAt", - ]) - ); - const { themed } = useAppTheme(); - - const openSignalList = useCallback(() => { - Linking.openURL( - "https://converseapp.notion.site/Converse-MM-signals-af014ca135c04ce1aae362e536712461?pvs=4" - ); - }, []); - - const contactPol = useCallback(() => { - navigation.popToTop(); - setTimeout(() => { - navigation.navigate("Conversation", { - peer: config.contactAddress, - }); - }, 300); - }, [navigation]); - - const [viewableItems, setViewableItems] = useState<{ [key: string]: true }>( - {} - ); - - const onViewableItemsChanged = useCallback( - ({ viewableItems: items }: any) => { - const viewable: { [key: string]: true } = {}; - items.forEach((item: any) => { - viewable[item.item] = true; - }); - setViewableItems(viewable); - }, - [] - ); - - useEffect(() => { - // On load, let's load frens - const getRecommendations = async () => { - setLoadingRecommendations(); - await refreshRecommendationsForAccount(currentAccount); - }; - const now = new Date().getTime(); - if (!loading && userAddress && now - updatedAt >= EXPIRE_AFTER) { - getRecommendations(); - } - }, [ - loading, - setLoadingRecommendations, - setRecommendations, - userAddress, - updatedAt, - currentAccount, - ]); - - const keyExtractor = useCallback((address: string) => address, []); - - const renderItem = useCallback( - ({ item }: { item: string }) => { - if (item === "title") { - return ( - <> - {visibility === "FULL" && showTitle && ( - - <> - 👋 - - {translate("recommendations.title")} - - - - )} - {visibility === "EMBEDDED" && showTitle && ( - - - {translate("recommendations.section_title")} - - - )} - - ); - } - - // If address is in groupMembers, remove profile from recommendations - if ( - groupMembers?.some( - (member) => member.address.toLowerCase() === item.toLowerCase() - ) - ) { - return null; - } - - return ( - - ); - }, - [ - frens, - themed, - viewableItems, - visibility, - groupMembers, - groupMode, - addToGroup, - showTitle, - ] - ); - - if (visibility === "HIDDEN") return null; - - if (loading && Object.keys(frens).length === 0 && visibility === "FULL") { - return ( - - - - {translate("recommendations.loading")} - - - ); - } - - if (visibility === "FULL" && frens && Object.keys(frens).length === 0) { - return ( - <> - 😐 - - {translate("recommendations.no_recommendations")} - - {translate("recommendations.signal_list")} - - {translate("recommendations.please_feel_free_to")} - - {translate("recommendations.contact_pol")} - {" "} - {translate("recommendations.if_you_want_us_to_add_anything")} - - - ); - } - - return ( - - - - ); -} - -const $emoji: ThemedStyle = ({ spacing }) => ({ - ...textSizeStyles.xl, - textAlign: "center", - marginTop: spacing.xl, - marginBottom: spacing.sm, -}); - -const $title: ThemedStyle = ({ colors, spacing }) => ({ - ...textSizeStyles.sm, - color: colors.text.primary, - ...Platform.select({ - default: { - paddingHorizontal: spacing.xl, - }, - android: { - paddingHorizontal: spacing.xxl, - }, - }), - textAlign: "center", -}); - -const $recommendations: ThemedStyle = ({ colors, spacing }) => ({ - marginBottom: spacing.xl, - backgroundColor: colors.background.surface, - marginLeft: Platform.OS === "android" ? spacing.md : 0, -}); - -const $fetching: ThemedStyle = () => ({ - flexGrow: 1, - justifyContent: "center", - marginBottom: 40, -}); - -const $fetchingText: ThemedStyle = ({ colors, spacing }) => ({ - ...textSizeStyles.sm, - color: colors.text.primary, - textAlign: "center", - marginTop: spacing.lg, -}); - -const $clickableText: ThemedStyle = ({ colors }) => ({ - color: colors.text.action, - fontWeight: "500", -}); - -const $titleContainer: ThemedStyle = ({ - colors, - spacing, - borderWidth, -}) => ({ - paddingBottom: spacing.xl, - ...Platform.select({ - default: { - borderBottomWidth: borderWidth.xs, - borderBottomColor: colors.border.subtle, - }, - android: {}, - web: {}, - }), -}); - -const $sectionTitleContainer: ThemedStyle = ({ - colors, - spacing, - borderWidth, -}) => ({ - ...Platform.select({ - default: { - borderBottomWidth: borderWidth.xs, - borderBottomColor: colors.border.subtle, - paddingLeft: spacing.md, - }, - android: {}, - web: {}, - }), -}); - -const $sectionTitleSpacing: ThemedStyle = ({ colors, spacing }) => ({ - marginBottom: spacing.xs, - marginTop: spacing.lg, -}); diff --git a/containers/GroupScreenAddition.tsx b/containers/GroupScreenAddition.tsx index a16f3183b..717ae974c 100644 --- a/containers/GroupScreenAddition.tsx +++ b/containers/GroupScreenAddition.tsx @@ -42,6 +42,7 @@ import { saveInviteIdByGroupId, } from "../features/GroupInvites/groupInvites.utils"; import { captureErrorWithToast } from "@/utils/capture-error"; +import logger from "@/utils/logger"; type GroupScreenAdditionProps = { topic: ConversationTopic; @@ -52,20 +53,25 @@ const noop = () => {}; export const GroupScreenAddition: FC = ({ topic, }) => { + logger.debug("[GroupScreenAddition] Rendering component", { topic }); + const colorScheme = useColorScheme(); const currentAccount = useCurrentAccount() as string; const { members } = useGroupMembers(topic); - const { currentAccountIsAdmin, currentAccountIsSuperAdmin } = useMemo( - () => ({ + const { currentAccountIsAdmin, currentAccountIsSuperAdmin } = useMemo(() => { + logger.debug("[GroupScreenAddition] Calculating admin status", { + members, + currentAccount, + }); + return { currentAccountIsAdmin: getAddressIsAdmin(members, currentAccount), currentAccountIsSuperAdmin: getAddressIsSuperAdmin( members, currentAccount ), - }), - [currentAccount, members] - ); + }; + }, [currentAccount, members]); const styles = useStyles(); const [snackMessage, setSnackMessage] = useState(null); @@ -87,18 +93,25 @@ export const GroupScreenAddition: FC = ({ useChatStore(useSelect(["setGroupInviteLink", "deleteGroupInviteLink"])); const onAddMemberPress = useCallback(() => { - navigate("NewConversation", { addingToGroupTopic: topic }); + logger.debug("[GroupScreenAddition] Adding new member", { topic }); + navigate("InviteUsersToExistingGroup", { addingToGroupTopic: topic }); }, [topic]); const onCopyInviteLinkPress = useCallback(() => { if (!groupInviteLink) { + logger.warn("[GroupScreenAddition] No invite link to copy"); return; } + logger.debug("[GroupScreenAddition] Copying invite link"); setSnackMessage(translate("group_invite_link_copied")); Clipboard.setString(groupInviteLink); }, [groupInviteLink]); const onCreateInviteLinkPress = useCallback(() => { + logger.debug("[GroupScreenAddition] Creating invite link", { + groupName, + topic, + }); createGroupInvite(currentAccount, { groupName: groupName ?? translate("group_invite_default_group_name"), imageUrl: groupPhoto, @@ -106,6 +119,9 @@ export const GroupScreenAddition: FC = ({ groupId: getV3IdFromTopic(topic), }) .then((groupInvite) => { + logger.debug("[GroupScreenAddition] Created invite link successfully", { + inviteId: groupInvite.id, + }); saveInviteIdByGroupId(getV3IdFromTopic(topic), groupInvite.id); saveGroupInviteLink(groupInvite.id, getV3IdFromTopic(topic)); setGroupInviteLink(topic, groupInvite.inviteLink); @@ -113,6 +129,7 @@ export const GroupScreenAddition: FC = ({ setSnackMessage(translate("group_invite_link_created_copied")); }) .catch((err) => { + logger.error("[GroupScreenAddition] Failed to create invite link", err); captureErrorWithToast(err, { message: translate("group_opertation_an_error_occurred"), }); @@ -127,13 +144,18 @@ export const GroupScreenAddition: FC = ({ ]); const onDeleteInviteLink = useCallback(() => { + logger.debug("[GroupScreenAddition] Deleting invite link"); Haptics.impactAsync(); const groupId = getV3IdFromTopic(topic); const inviteId = getInviteIdByGroupId(groupId); if (!inviteId) { + logger.warn("[GroupScreenAddition] No invite ID found to delete"); return; } deleteGroupInvite(currentAccount, inviteId).then(() => { + logger.debug("[GroupScreenAddition] Deleted invite link successfully", { + inviteId, + }); setSnackMessage(translate("group_invite_link_deleted")); deleteLinkFromState(topic); deleteLinkFromStore(inviteId); @@ -142,10 +164,14 @@ export const GroupScreenAddition: FC = ({ }, [deleteLinkFromState, topic, currentAccount]); const dismissSnackBar = useCallback(() => { + logger.debug("[GroupScreenAddition] Dismissing snackbar"); setSnackMessage(null); }, [setSnackMessage]); if (!canAddMember) { + logger.debug( + "[GroupScreenAddition] User cannot add members, not rendering" + ); return null; } diff --git a/design-system/IconButton/IconButton.tsx b/design-system/IconButton/IconButton.tsx index 20e29da50..17b878369 100644 --- a/design-system/IconButton/IconButton.tsx +++ b/design-system/IconButton/IconButton.tsx @@ -4,6 +4,7 @@ import { PressableStateCallbackType, StyleProp, TextStyle, + View, ViewStyle, } from "react-native"; import { useAppTheme } from "../../theme/useAppTheme"; @@ -94,12 +95,16 @@ export function IconButton(props: IIconButtonProps) { const handlePress = useCallback( (e: GestureResponderEvent) => { + if (disabled) { + return; + } + if (withHaptics) { Haptics.softImpactAsync(); } onPress?.(e); }, - [withHaptics, onPress] + [withHaptics, onPress, disabled] ); return ( diff --git a/design-system/chip.tsx b/design-system/chip.tsx new file mode 100644 index 000000000..3fec7e3c4 --- /dev/null +++ b/design-system/chip.tsx @@ -0,0 +1,102 @@ +/** + * A Chip component that displays an avatar and text in a pill-shaped + * container. Used for user selection and filtering. + * + * @see https://www.figma.com/design/p6mt4tEDltI4mypD3TIgUk/Converse-App?node-id=5026-27031 + * + * @example + * // Basic usage with avatar + * {}} + * /> + * + * @example + * // Active state without avatar + * {}} + * /> + */ + +import { ViewStyle, TextStyle, ImageStyle } from "react-native"; +import { TouchableOpacity } from "./TouchableOpacity"; +import { HStack } from "./HStack"; +import { Text } from "./Text"; +import { ThemedStyle, useAppTheme } from "@/theme/useAppTheme"; +import { Avatar } from "@/components/Avatar"; +import { debugBorder } from "@/utils/debug-style"; + +export type IChipProps = { + text: string; + avatarUrl?: string; + isActive?: boolean; + onPress?: () => void; + style?: ViewStyle; +}; + +export function Chip({ + text, + avatarUrl, + isActive, + onPress, + style, +}: IChipProps) { + const { themed, theme } = useAppTheme(); + + return ( + + + {avatarUrl && ( + + )} + + {text} + + + + ); +} + +const $chip: ThemedStyle = ({ + spacing, + borderRadius, + borderWidth, + colors, +}) => ({ + borderRadius: borderRadius.sm, + borderWidth: borderWidth.sm, + borderColor: colors.border.subtle, + paddingVertical: spacing.xxs, + paddingHorizontal: spacing.xs, + minHeight: 36, + // ...debugBorder("orange"), +}); + +const $chipActive: ThemedStyle = ({ colors }) => ({ + backgroundColor: colors.border.subtle, +}); + +const $chipContainer: ThemedStyle = ({ spacing }) => ({ + alignItems: "center", + gap: spacing.xxs, +}); + +const $avatar: ThemedStyle = ({ borderRadius }) => ({ + width: 16, + height: 16, + borderRadius: borderRadius.message.bubble, +}); + +const $chipText: ThemedStyle = ({ colors }) => ({ + color: colors.text.secondary, +}); + +const $chipTextActive: ThemedStyle = ({ colors }) => ({ + color: colors.text.primary, +}); diff --git a/features/conversation-list/components/ChatNullState.tsx b/features/conversation-list/components/ChatNullState.tsx deleted file mode 100644 index 6e2c5817d..000000000 --- a/features/conversation-list/components/ChatNullState.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import Recommendations from "@components/Recommendations/Recommendations"; -import { - useSettingsStore, - useRecommendationsStore, -} from "@data/store/accountsStore"; -import { translate } from "@i18n/index"; -import { - backgroundColor, - itemSeparatorColor, - primaryColor, - tertiaryBackgroundColor, - textPrimaryColor, - textSecondaryColor, -} from "@styles/colors"; -import { BorderRadius, Margins, Paddings } from "@styles/sizes"; -import React from "react"; -import { Platform, StyleSheet, Text, useColorScheme, View } from "react-native"; - -import config from "../../../config"; -import { ShareProfileContent } from "../../../screens/ShareProfile"; -import NewConversationButton from "./NewConversationButton"; -import { usePreferredUsername } from "@/hooks/usePreferredUsername"; -import { usePreferredName } from "@/hooks/usePreferredName"; -import { usePreferredAvatarUri } from "@/hooks/usePreferredAvatarUri"; - -type ChatNullStateProps = { - currentAccount: string; - navigation: any; - route: any; -}; - -const ChatNullState: React.FC = ({ - currentAccount, - navigation, - route, -}) => { - const colorScheme = useColorScheme(); - const styles = useStyles(); - - const username = usePreferredUsername(currentAccount); - const displayName = usePreferredName(currentAccount); - const avatar = usePreferredAvatarUri(currentAccount); - - const profileUrl = `https://${config.websiteDomain}/dm/${ - username || currentAccount - }`; - - const frens = useRecommendationsStore((s) => s.frens); - const hasRecommendations = Object.keys(frens).length > 0; - - const hasUserDismissedBanner = useSettingsStore( - (s) => s.hasUserDismissedBanner - ); - - return ( - - {/* {!hasUserDismissedBanner && ( - { - Linking.openURL(config.alphaGroupChatUrl); - }} - /> - )} */} - - - - - {hasRecommendations - ? translate("connectWithYourNetwork") - : translate("shareYourQRCode")} - - - {hasRecommendations - ? translate("findContacts") - : translate("moveOrConnect")} - - - {hasRecommendations ? ( - - - - ) : ( - - - - )} - - {Platform.OS === "android" && } - - ); -}; - -const useStyles = () => { - const colorScheme = useColorScheme(); - return StyleSheet.create({ - container: { - flex: 1, - backgroundColor: backgroundColor(colorScheme), - borderTopWidth: Platform.OS === "android" ? 0 : 1, - borderTopColor: tertiaryBackgroundColor(colorScheme), - }, - contentContainer: { - flex: 1, - alignItems: "center", - justifyContent: "flex-start", - paddingTop: Paddings.default, - }, - titlesContainer: { - width: "100%", - borderBottomColor: tertiaryBackgroundColor(colorScheme), - }, - title: { - fontSize: 24, - fontWeight: "bold", - marginBottom: Margins.small, - color: textPrimaryColor(colorScheme), - textAlign: "center", - letterSpacing: -0.4, - marginTop: Margins.small, - }, - titleWithRecommendations: { - textAlign: "left", - marginLeft: Margins.default, - marginTop: 0, - }, - description: { - fontSize: 14, - textAlign: "center", - marginBottom: Margins.large, - color: textPrimaryColor(colorScheme), - letterSpacing: -0.3, - }, - descriptionWithRecommendations: { - textAlign: "left", - marginLeft: Margins.default, - }, - qrCodeContainer: { - paddingVertical: Paddings.default, - paddingHorizontal: Paddings.large, - backgroundColor: backgroundColor(colorScheme), - borderRadius: BorderRadius.large, - shadowColor: primaryColor(colorScheme), - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.2, - shadowRadius: 4, - elevation: 3, - marginTop: Platform.OS === "android" ? Margins.large : Margins.small, - }, - identityContainer: { - marginTop: Margins.large, - alignItems: "center", - }, - identity: { - color: textPrimaryColor(colorScheme), - fontSize: 24, - fontWeight: "600", - textAlign: "center", - }, - username: { - fontSize: 16, - color: textSecondaryColor(colorScheme), - marginTop: Margins.small, - }, - recommendationsContainer: { - width: "100%", - height: "100%", - }, - }); -}; - -export default ChatNullState; diff --git a/features/conversation-list/components/NewConversationButton.tsx b/features/conversation-list/components/NewConversationButton.tsx deleted file mode 100644 index ed88b26d4..000000000 --- a/features/conversation-list/components/NewConversationButton.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { PictoSizes } from "@styles/sizes"; -import { converseEventEmitter } from "@utils/events"; -import React, { useCallback } from "react"; -import { Platform, useColorScheme } from "react-native"; -import { FAB } from "react-native-paper"; - -import { IconButton } from "../../../design-system/IconButton/IconButton"; -import { navigate } from "../../../utils/navigation"; -import Picto from "../../../components/Picto/Picto"; - -export default function NewConversationButton() { - const colorScheme = useColorScheme(); - - const onPress = useCallback(() => { - navigate("NewConversation"); - }, []); - - const showDebug = useCallback(() => { - converseEventEmitter.emit("showDebugMenu"); - }, []); - - if (Platform.OS === "ios") { - return ( - - ); - } else { - return ( - ( - <> - - - )} - animated={false} - style={{ - position: "absolute", - margin: 0, - right: 16, - bottom: 20, - }} - onPress={onPress} - onLongPress={showDebug} - /> - ); - } -} diff --git a/features/conversation-list/conversation-list.screen.tsx b/features/conversation-list/conversation-list.screen.tsx index 854dfa4d9..a41b35a8e 100644 --- a/features/conversation-list/conversation-list.screen.tsx +++ b/features/conversation-list/conversation-list.screen.tsx @@ -48,7 +48,7 @@ import { import { useDisconnectActionSheet } from "@hooks/useDisconnectActionSheet"; import { useNavigation } from "@react-navigation/native"; import { NativeStackScreenProps } from "@react-navigation/native-stack"; -import React, { memo, useCallback, useMemo } from "react"; +import React, { memo, useCallback, useEffect, useMemo } from "react"; import { Alert, Platform, @@ -62,6 +62,7 @@ import { } from "react-native-ios-context-menu"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useConversationListRequestCount } from "./useConversationListRequestCount"; +import logger from "@/utils/logger"; type IConversationListProps = NativeStackScreenProps< NavigationParamList, @@ -347,7 +348,10 @@ function useHeaderWrapper() { }} icon="square.and.pencil" onPress={() => { - navigation.navigate("NewConversation", {}); + logger.debug( + "[ConversationListScreen] Navigating to NewConversation" + ); + navigation.navigate("NewConversation"); }} /> diff --git a/features/conversation/conversation-composer/conversation-composer.tsx b/features/conversation/conversation-composer/conversation-composer.tsx index be69a5f42..e43c0ed39 100644 --- a/features/conversation/conversation-composer/conversation-composer.tsx +++ b/features/conversation/conversation-composer/conversation-composer.tsx @@ -19,10 +19,12 @@ import { type IComposerProps = { onSend: (args: ISendMessageParams) => Promise; + hideAddAttachmentButton?: boolean; + disabled?: boolean; }; export const Composer = memo(function Composer(props: IComposerProps) { - const { onSend } = props; + const { onSend, disabled, hideAddAttachmentButton } = props; const { theme } = useAppTheme(); const store = useConversationComposerStore(); @@ -113,17 +115,39 @@ export const Composer = memo(function Composer(props: IComposerProps) { return ( - + {/* note(lustig): if we need any more modifications of this component, + we should take the time to create a composite component to allow + consumers to explicitly construct it the way they need. + + ie: + + + + + + + + this api would prevent us from getting into prop hell and we + could still create the "base" component that covers most normal + cases from it. + + I foresee this being required once we start building the composer + "swap sender" functionality. + + That functionality will be completely useless mid conversation, + but it will be useful when creating a new conversation, for example. + + figma: https://www.figma.com/design/p6mt4tEDltI4mypD3TIgUk/Converse-App?node-id=5026-26997&m=dev + */} + {!hideAddAttachmentButton && } - + @@ -151,8 +175,11 @@ export const Composer = memo(function Composer(props: IComposerProps) { ); }); -const SendButton = memo(function SendButton(props: { onPress: () => void }) { - const { onPress } = props; +const SendButton = memo(function SendButton(props: { + onPress: () => void; + disabled?: boolean; +}) { + const { onPress, disabled } = props; const { theme } = useAppTheme(); @@ -181,7 +208,7 @@ const SendButton = memo(function SendButton(props: { onPress: () => void }) { hitSlop={theme.spacing.xs} size="sm" onPress={onPress} - disabled={!canSend} + disabled={disabled || !canSend} iconName="arrow.up" /> diff --git a/features/conversation/hooks/use-send-message.ts b/features/conversation/hooks/use-send-message.ts index 2f72aa006..54b393fe9 100644 --- a/features/conversation/hooks/use-send-message.ts +++ b/features/conversation/hooks/use-send-message.ts @@ -47,6 +47,8 @@ export function sendMessage(args: { }); } + // todo: where do we optimistically handle adding the message + // to our query cache? return conversation.send( content.remoteAttachment ? { remoteAttachment: content.remoteAttachment } diff --git a/features/groups/invite-to-group/InviteUsersToExistingGroup.nav.tsx b/features/groups/invite-to-group/InviteUsersToExistingGroup.nav.tsx new file mode 100644 index 000000000..4caf62138 --- /dev/null +++ b/features/groups/invite-to-group/InviteUsersToExistingGroup.nav.tsx @@ -0,0 +1,37 @@ +import { + NativeStack, + navigationAnimation, +} from "@/screens/Navigation/Navigation"; +import { Platform } from "react-native"; +import { ConversationTopic } from "@xmtp/react-native-sdk"; +import { useColorScheme } from "react-native"; +import { InviteUsersToExistingGroupScreen } from "./invite-users-to-exisiting-group.screen"; +import { translate } from "@/i18n"; +import { + headerTitleStyle, + textPrimaryColor, + textSecondaryColor, +} from "@/styles/colors"; + +export type InviteUsersToExistingGroupParams = { + addingToGroupTopic: ConversationTopic; +}; + +export function InviteUsersToExistingGroupNav() { + const colorScheme = useColorScheme(); + return ( + ({ + headerTitle: translate("group_info"), + headerTintColor: + Platform.OS === "android" + ? textSecondaryColor(colorScheme) + : textPrimaryColor(colorScheme), + animation: navigationAnimation, + headerTitleStyle: headerTitleStyle(colorScheme), + })} + /> + ); +} diff --git a/features/groups/invite-to-group/invite-users-to-exisiting-group.screen.tsx b/features/groups/invite-to-group/invite-users-to-exisiting-group.screen.tsx new file mode 100644 index 000000000..92e4fd497 --- /dev/null +++ b/features/groups/invite-to-group/invite-users-to-exisiting-group.screen.tsx @@ -0,0 +1,506 @@ +import { Button } from "@design-system/Button/Button"; +import { NativeStackScreenProps } from "@react-navigation/native-stack"; +import { + backgroundColor, + itemSeparatorColor, + primaryColor, + textPrimaryColor, + textSecondaryColor, +} from "@styles/colors"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { + Alert, + Platform, + ScrollView, + StyleSheet, + Text, + TextInput, + View, + useColorScheme, +} from "react-native"; + +import { translate } from "@/i18n"; +import { getCleanAddress } from "@/utils/evm/getCleanAddress"; +import { useGroupQuery } from "@queries/useGroupQuery"; +import { SearchBar } from "@search/components/SearchBar"; +import { canMessageByAccount } from "@utils/xmtpRN/contacts"; +import { InboxId } from "@xmtp/react-native-sdk"; +import { NavigationParamList } from "@/screens/Navigation/Navigation"; +import { currentAccount } from "@/data/store/accountsStore"; +import { IProfileSocials } from "@/features/profiles/profile-types"; +import { useGroupMembers } from "@/hooks/useGroupMembers"; +import ActivityIndicator from "@/components/ActivityIndicator/ActivityIndicator"; +import { setProfileRecordSocialsQueryData } from "@/queries/useProfileSocialsQuery"; +import { searchProfiles } from "@/utils/api"; +import { isEmptyObject } from "@/utils/objects"; +import { getPreferredName } from "@/utils/profile"; +// import { OldProfileSearchResultsList } from "@/features/search/components/OldProfileSearchResultsList"; +import TableView from "@/components/TableView/TableView"; +import { TableViewPicto } from "@/components/TableView/TableViewImage"; +import AndroidBackAction from "@/components/AndroidBackAction"; +import { getAddressForPeer, isSupportedPeer } from "@/utils/evm/address"; +import config from "@/config"; +import { OldProfileSearchResultsList } from "@/features/search/components/OldProfileSearchResultsList"; + +export function InviteUsersToExistingGroupScreen({ + route, + navigation, +}: NativeStackScreenProps) { + const colorScheme = useColorScheme(); + const [group, setGroup] = useState({ + enabled: !!route.params?.addingToGroupTopic, + members: [] as (IProfileSocials & { address: string })[], + }); + + const { addMembers, members } = useGroupMembers( + route.params?.addingToGroupTopic! + ); + + const [loading, setLoading] = useState(false); + + const handleBack = useCallback(() => navigation.goBack(), [navigation]); + + const styles = useStyles(); + + const handleRightAction = useCallback(async () => { + setLoading(true); + try { + // TODO: Support multiple addresses + await addMembers(group.members.map((m) => m.address)); + navigation.goBack(); + } catch (e) { + setLoading(false); + console.error(e); + Alert.alert("An error occured"); + } + }, [addMembers, group.members, navigation]); + + useEffect(() => { + navigation.setOptions({ + headerLeft: () => + Platform.OS === "ios" ? ( +