diff --git a/App.tsx b/App.tsx
index 4d93fe669b..0362aee6f5 100644
--- a/App.tsx
+++ b/App.tsx
@@ -22,6 +22,7 @@ import { Navigator } from "./packages/components/navigation/Navigator";
import { DropdownsContextProvider } from "./packages/context/DropdownsProvider";
import { FeedbacksContextProvider } from "./packages/context/FeedbacksProvider";
import { MediaPlayerContextProvider } from "./packages/context/MediaPlayerProvider";
+import { MessageContextProvider } from "./packages/context/MessageProvider";
import { SearchBarContextProvider } from "./packages/context/SearchBarProvider";
import { TNSMetaDataListContextProvider } from "./packages/context/TNSMetaDataListProvider";
import { TNSContextProvider } from "./packages/context/TNSProvider";
@@ -86,10 +87,12 @@ export default function App() {
-
-
-
-
+
+
+
+
+
+
diff --git a/assets/icons/Addplus.svg b/assets/icons/Addplus.svg
new file mode 100644
index 0000000000..c666722157
--- /dev/null
+++ b/assets/icons/Addplus.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/add-chat.svg b/assets/icons/add-chat.svg
new file mode 100644
index 0000000000..6d4426c348
--- /dev/null
+++ b/assets/icons/add-chat.svg
@@ -0,0 +1,44 @@
+
diff --git a/assets/icons/chaticon.svg b/assets/icons/chaticon.svg
new file mode 100644
index 0000000000..497ed9441a
--- /dev/null
+++ b/assets/icons/chaticon.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/chatplus.svg b/assets/icons/chatplus.svg
new file mode 100644
index 0000000000..eded3ddf15
--- /dev/null
+++ b/assets/icons/chatplus.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/dots.svg b/assets/icons/dots.svg
new file mode 100644
index 0000000000..90cf6bfb4d
--- /dev/null
+++ b/assets/icons/dots.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/doublecheck.svg b/assets/icons/doublecheck.svg
new file mode 100644
index 0000000000..634abe1b64
--- /dev/null
+++ b/assets/icons/doublecheck.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/forward-to.svg b/assets/icons/forward-to.svg
new file mode 100644
index 0000000000..7354514ace
--- /dev/null
+++ b/assets/icons/forward-to.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/assets/icons/forward.svg b/assets/icons/forward.svg
new file mode 100644
index 0000000000..98aac227d4
--- /dev/null
+++ b/assets/icons/forward.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/friend.svg b/assets/icons/friend.svg
new file mode 100644
index 0000000000..3012cb62f2
--- /dev/null
+++ b/assets/icons/friend.svg
@@ -0,0 +1,42 @@
+
diff --git a/assets/icons/friends.svg b/assets/icons/friends.svg
new file mode 100644
index 0000000000..26ea1ac421
--- /dev/null
+++ b/assets/icons/friends.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/group.svg b/assets/icons/group.svg
new file mode 100644
index 0000000000..0a8bebdc52
--- /dev/null
+++ b/assets/icons/group.svg
@@ -0,0 +1,44 @@
+
diff --git a/assets/icons/illustration.svg b/assets/icons/illustration.svg
new file mode 100644
index 0000000000..eb306604d5
--- /dev/null
+++ b/assets/icons/illustration.svg
@@ -0,0 +1,285 @@
+
+
diff --git a/assets/icons/sent.svg b/assets/icons/sent.svg
new file mode 100644
index 0000000000..264a57dba9
--- /dev/null
+++ b/assets/icons/sent.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/icons/space.svg b/assets/icons/space.svg
new file mode 100644
index 0000000000..4a9b64d8dc
--- /dev/null
+++ b/assets/icons/space.svg
@@ -0,0 +1,47 @@
+
diff --git a/assets/logos/logo-hexagon.png b/assets/logos/logo-hexagon.png
new file mode 100644
index 0000000000..415d69f10d
Binary files /dev/null and b/assets/logos/logo-hexagon.png differ
diff --git a/declarations.d.ts b/declarations.d.ts
index 96e1ed39ea..89e73b24fd 100644
--- a/declarations.d.ts
+++ b/declarations.d.ts
@@ -18,3 +18,4 @@ declare module "*.jpg" {
}
declare module "react-native-smooth-slider";
+declare module "react-native-keyboard-aware-scrollview";
diff --git a/package.json b/package.json
index 6f0b52d976..91d42b4bee 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,7 @@
"ethers": "^5.7.2",
"expo": "^49.0.16",
"expo-av": "~13.4.1",
+ "expo-barcode-scanner": "~12.5.3",
"expo-font": "~11.4.0",
"expo-linear-gradient": "~12.3.0",
"expo-optimize": "^0.2.20",
@@ -93,10 +94,12 @@
"react-native-gesture-handler": "~2.12.0",
"react-native-heroicons": "^3.2.0",
"react-native-hoverable": "^0.2.0",
+ "react-native-keyboard-aware-scrollview": "^2.1.0",
"react-native-paper": "^4.12.5",
"react-native-pell-rich-editor": "^1.8.8",
"react-native-pie-chart": "^3.0.1",
"react-native-popup-menu": "^0.16.1",
+ "react-native-qrcode-svg": "^6.2.0",
"react-native-reanimated": "~3.3.0",
"react-native-reanimated-carousel": "^3.0.3",
"react-native-redash": "^18.0.0",
diff --git a/packages/components/CopyToClipboard.tsx b/packages/components/CopyToClipboard.tsx
index bf4daa5d2e..0c879059e1 100644
--- a/packages/components/CopyToClipboard.tsx
+++ b/packages/components/CopyToClipboard.tsx
@@ -26,14 +26,16 @@ export const useCopyToClipboard = () => {
export const CopyToClipboard: React.FC<{
text: string;
squaresBackgroundColor?: string;
-}> = ({ text, squaresBackgroundColor }) => {
+ fullWidth?: boolean;
+}> = ({ text, squaresBackgroundColor, fullWidth }) => {
const { copyToClipboard } = useCopyToClipboard();
return (
copyToClipboard(text)}>
) => React.ReactNode);
+ triggerComponent?: React.ReactNode;
+ style?: ViewStyle;
+ onDropdownClosed?: () => void;
+ positionStyle?: ViewStyle;
+}
+
+export const Dropdown = ({
+ style,
+ children,
+ triggerComponent,
+ onDropdownClosed,
+ positionStyle = {},
+}: DropdownProps) => {
+ const [, setLayout] = useState({
+ height: 0,
+ width: 0,
+ });
+ const { onPressDropdownButton, isDropdownOpen, closeOpenedDropdown } =
+ useDropdowns();
+ const dropdownRef = useRef(null);
+
+ const isDropdownOpened = isDropdownOpen(dropdownRef);
+
+ const [isOpened, setIsOpened] = useState(false);
+
+ useEffect(() => {
+ if (isOpened && !isDropdownOpened) {
+ onDropdownClosed?.();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isDropdownOpened, isOpened]);
+
+ const handleLayout = ({ nativeEvent: { layout } }: LayoutChangeEvent) => {
+ setLayout(layout);
+ };
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const handleOpen = () => {
+ setIsOpened(true);
+ onPressDropdownButton(dropdownRef);
+ };
+
+ useEffect(() => {
+ if (!triggerComponent) {
+ handleOpen();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [triggerComponent]);
+
+ return (
+
+ {!!triggerComponent && (
+
+ {triggerComponent}
+
+ )}
+ {isDropdownOpened && (
+
+ {typeof children === "function"
+ ? children({ isDropdownOpen, closeOpenedDropdown })
+ : children}
+
+ )}
+
+ );
+};
diff --git a/packages/components/KeyboardAvoidingView.tsx b/packages/components/KeyboardAvoidingView.tsx
new file mode 100644
index 0000000000..9935339418
--- /dev/null
+++ b/packages/components/KeyboardAvoidingView.tsx
@@ -0,0 +1,32 @@
+import React from "react";
+import {
+ Platform,
+ KeyboardAvoidingView as KeyboardAvoiding,
+} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+
+interface KeyboardAvoidingViewProps {
+ extraVerticalOffset?: number;
+ children: React.ReactNode;
+}
+
+export const KeyboardAvoidingView = ({
+ extraVerticalOffset = 0,
+ children,
+}: KeyboardAvoidingViewProps) => {
+ const insets = useSafeAreaInsets();
+
+ if (Platform.OS === "web") {
+ return children;
+ }
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/components/modals/ModalBase.tsx b/packages/components/modals/ModalBase.tsx
index bd3f32eaa1..30fe11d282 100644
--- a/packages/components/modals/ModalBase.tsx
+++ b/packages/components/modals/ModalBase.tsx
@@ -1,4 +1,9 @@
-import React, { ComponentType, ReactNode, useEffect } from "react";
+import React, {
+ ComponentType,
+ ReactNode,
+ useEffect,
+ PropsWithChildren,
+} from "react";
import {
Modal,
View,
@@ -8,6 +13,7 @@ import {
StyleProp,
} from "react-native";
import { TouchableOpacity } from "react-native-gesture-handler";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
import chevronLeft from "../../../assets/icons/chevron-left.svg";
import closeSVG from "../../../assets/icons/hamburger-button-cross.svg";
@@ -46,6 +52,21 @@ type ModalBaseProps = {
children: ReactNode;
};
+const ScrollableComponent = ({
+ scrollable,
+ ...props
+}: PropsWithChildren<{
+ scrollable?: boolean;
+ style: StyleProp;
+ contentContainerStyle: StyleProp;
+}>) => {
+ if (scrollable) {
+ return ;
+ } else {
+ return ;
+ }
+};
+
// The base components for modals. You can provide children (Modal's content) and childrenBottom (Optional Modal's bottom content)
const ModalBase: React.FC = ({
label,
@@ -68,9 +89,11 @@ const ModalBase: React.FC = ({
verticalPosition = "center",
closeOnBlur,
}) => {
- const { width: windowWidth } = useWindowDimensions();
+ const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const navigation = useAppNavigation();
+ const insets = useSafeAreaInsets();
+
useEffect(() => {
if (closeOnBlur !== true) return;
const unsubscribe = navigation.addListener("blur", () => {
@@ -89,20 +112,28 @@ const ModalBase: React.FC = ({
onRequestClose={onClose}
>
{/*------ Modal background */}
- = ({
{/*------- Modal bottom content */}
{childrenBottom}
-
+
);
};
diff --git a/packages/components/modals/QRCodeScannerModal.tsx b/packages/components/modals/QRCodeScannerModal.tsx
new file mode 100644
index 0000000000..70ff85db59
--- /dev/null
+++ b/packages/components/modals/QRCodeScannerModal.tsx
@@ -0,0 +1,65 @@
+import { BarCodeScanner } from "expo-barcode-scanner";
+import React, { useEffect, useState } from "react";
+import {
+ View,
+ StyleSheet,
+ useWindowDimensions,
+ ActivityIndicator,
+} from "react-native";
+
+import ModalBase from "./ModalBase";
+import { useFeedbacks } from "../../context/FeedbacksProvider";
+
+export const QRCodeScannerModal = ({
+ onClose,
+}: {
+ onClose: (data?: string) => void;
+}) => {
+ const { setToastError } = useFeedbacks();
+ const { width, height } = useWindowDimensions();
+ const [permission, setPermission] = useState(false);
+ useEffect(() => {
+ const getBarCodeScannerPermissions = async () => {
+ const status = await BarCodeScanner.requestPermissionsAsync();
+
+ if (status.granted) {
+ setPermission(true);
+ }
+ };
+
+ getBarCodeScannerPermissions();
+ }, []);
+
+ const handleBarCodeScanned = ({ data }: { data: string }) => {
+ if (
+ typeof data === "string" &&
+ data.startsWith("https://app.teritori.com/contact")
+ ) {
+ onClose(data);
+ } else {
+ setToastError({
+ title: "QR Error",
+ message: "QR is not of Teritori contact",
+ });
+ }
+ };
+ return (
+
+
+ {permission ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
diff --git a/packages/components/navigation/Navigator.tsx b/packages/components/navigation/Navigator.tsx
index 2b4f987fbc..b5eeba2f1a 100644
--- a/packages/components/navigation/Navigator.tsx
+++ b/packages/components/navigation/Navigator.tsx
@@ -19,6 +19,9 @@ import { CollectionScreen } from "../../screens/Marketplace/CollectionScreen";
import { CollectionToolsScreen } from "../../screens/Marketplace/CollectionToolsScreen";
import { MarketplaceScreen } from "../../screens/Marketplace/MarketplaceScreen";
import { NFTDetailScreen } from "../../screens/Marketplace/NFTDetailScreen";
+import { MessageScreen } from "../../screens/Message/MessageScreen";
+import { ChatSectionScreen } from "../../screens/Message/components/ChatSection";
+import { FriendshipManagerScreen } from "../../screens/Message/components/FriendshipManager";
import { MultisigCreateScreen } from "../../screens/Multisig/MultisigCreateScreen";
import { MultisigScreen } from "../../screens/Multisig/MultisigScreen";
import { MultisigWalletDashboardScreen } from "../../screens/Multisig/MultisigWalletDashboardScreen";
@@ -315,6 +318,27 @@ export const Navigator: React.FC = () => {
component={CoreDAOScreen}
options={{ header: () => null, title: screenTitle("Core DAO") }}
/>
+ null, title: screenTitle("Message") }}
+ />
+ null,
+ title: screenTitle("Chat Message"),
+ }}
+ />
+ null,
+ title: screenTitle("Friends Add"),
+ }}
+ />
);
};
diff --git a/packages/context/DropdownsProvider.tsx b/packages/context/DropdownsProvider.tsx
index b1498f553d..fe301bfce7 100644
--- a/packages/context/DropdownsProvider.tsx
+++ b/packages/context/DropdownsProvider.tsx
@@ -8,7 +8,7 @@ import React, {
} from "react";
import { GestureResponderEvent, Pressable, StyleSheet } from "react-native";
-interface DefaultValue {
+export interface DefaultValue {
onPressDropdownButton: (dropdownRef: RefObject) => void;
closeOpenedDropdown: () => void;
isDropdownOpen: (dropdownRef: RefObject) => boolean;
diff --git a/packages/context/MessageProvider.tsx b/packages/context/MessageProvider.tsx
new file mode 100644
index 0000000000..af56663eeb
--- /dev/null
+++ b/packages/context/MessageProvider.tsx
@@ -0,0 +1,45 @@
+import React, {
+ PropsWithChildren,
+ createContext,
+ useContext,
+ useState,
+} from "react";
+
+import { CONVERSATION_TYPES, Conversation } from "../utils/types/message";
+
+interface DefaultValue {
+ activeConversationType: CONVERSATION_TYPES;
+ setActiveConversationType: (type: CONVERSATION_TYPES) => void;
+ activeConversation?: Conversation;
+ setActiveConversation: (conv: Conversation) => void;
+}
+const defaultValue: DefaultValue = {
+ activeConversationType: CONVERSATION_TYPES.ACTIVE,
+ setActiveConversationType: (type: CONVERSATION_TYPES) => {},
+ activeConversation: undefined,
+ setActiveConversation: (conv: Conversation) => {},
+};
+
+const MessageContext = createContext(defaultValue);
+
+export const MessageContextProvider = ({ children }: PropsWithChildren) => {
+ const [activeConversationType, setActiveConversationType] = useState(
+ CONVERSATION_TYPES.ACTIVE,
+ );
+ const [activeConversation, setActiveConversation] = useState();
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useMessage = () => useContext(MessageContext);
diff --git a/packages/screens/Message/MessageScreen.tsx b/packages/screens/Message/MessageScreen.tsx
new file mode 100644
index 0000000000..18157b8314
--- /dev/null
+++ b/packages/screens/Message/MessageScreen.tsx
@@ -0,0 +1,212 @@
+import React from "react";
+import {
+ View,
+ TouchableOpacity,
+ Platform,
+ ScrollView,
+ ActivityIndicator,
+} from "react-native";
+import { useSelector } from "react-redux";
+
+import { ChatSection } from "./components/ChatSection";
+import { CreateConversation } from "./components/CreateConversation";
+import { CreateGroup } from "./components/CreateGroup";
+import { FriendshipManager } from "./components/FriendshipManager";
+import { JoinGroup } from "./components/JoinGroup";
+import { MessageBlankFiller } from "./components/MessageBlankFiller";
+import MessageCard from "./components/MessageCard";
+import { MessageHeader } from "./components/MessageHeader";
+import { SideBarChats } from "./components/SideBarChats";
+import chat from "../../../assets/icons/add-chat.svg";
+import friend from "../../../assets/icons/friend.svg";
+import group from "../../../assets/icons/group.svg";
+import space from "../../../assets/icons/space.svg";
+import { BrandText } from "../../components/BrandText";
+import FlexRow from "../../components/FlexRow";
+import { ScreenContainer } from "../../components/ScreenContainer";
+import { Separator } from "../../components/separators/Separator";
+import { SpacerColumn, SpacerRow } from "../../components/spacer";
+import { useMessage } from "../../context/MessageProvider";
+import { selectIsWeshConnected } from "../../store/slices/message";
+import { useAppNavigation, ScreenFC } from "../../utils/navigation";
+import { fontSemibold14 } from "../../utils/style/fonts";
+import { layout } from "../../utils/style/layout";
+
+export const MessageScreen: ScreenFC<"Message"> = ({ route }) => {
+ const activeView = route?.params?.view;
+ const activeTab = route?.params?.tab;
+ const isWeshConnected = useSelector(selectIsWeshConnected);
+
+ const { activeConversation, setActiveConversation } = useMessage();
+
+ const navigation = useAppNavigation();
+ // const contactInfo = useSelector(selectContactInfo);
+
+ const HEADER_CONFIG = [
+ {
+ id: 1,
+ title: "Create a conversation",
+ icon: chat,
+ onPress: () => {
+ navigation.navigate("Message", { view: "CreateConversation" });
+ },
+ },
+ {
+ id: 2,
+ title: "Create a group",
+ icon: group,
+ onPress() {
+ navigation.navigate("Message", { view: "CreateGroup" });
+ },
+ },
+ {
+ id: 3,
+ title: "Add a friend",
+ icon: friend,
+ onPress() {
+ if (["android", "ios"].includes(Platform.OS)) {
+ navigation.navigate("FriendshipManager");
+ } else {
+ navigation.navigate("Message", { view: "AddFriend" });
+ }
+ },
+ },
+ {
+ id: 4,
+ title: "Join a group",
+ icon: group,
+ onPress() {
+ navigation.navigate("Message", { view: "JoinGroup" });
+ },
+ },
+ {
+ id: 5,
+ title: "Create a Teritori space",
+ icon: space,
+ subtitle: "coming soon",
+ onPress() {},
+ },
+ ];
+
+ if (!isWeshConnected) {
+ return (
+ }
+ responsive
+ fullWidth
+ footerChildren={<>>}
+ noScroll
+ >
+
+
+
+ We are currently in the process of setting up Weshnet, and it will
+ be ready within just a few short minutes.{"\n"} Thank you for your
+ understanding
+
+
+
+ );
+ }
+ return (
+ }
+ responsive
+ fullWidth
+ footerChildren={<>>}
+ noScroll
+ >
+
+
+
+
+ {HEADER_CONFIG.map((item) => (
+
+
+
+
+
+
+ ))}
+
+
+
+
+ {Platform.OS === "web" && }
+
+ {["android", "ios"].includes(Platform.OS) ? (
+
+ ) : (
+
+
+
+
+
+ {activeView === "AddFriend" ? (
+ {
+ setActiveConversation(conv);
+ navigation.navigate("Message");
+ }}
+ />
+ ) : (
+ <>
+ {activeConversation ? (
+
+ ) : (
+
+ )}
+ >
+ )}
+
+
+ )}
+
+ {activeView === "CreateGroup" && (
+ navigation.navigate("Message")} />
+ )}
+ {activeView === "CreateConversation" && (
+ navigation.navigate("Message")} />
+ )}
+ {activeView === "JoinGroup" && (
+ navigation.navigate("Message")} />
+ )}
+
+
+ );
+};
diff --git a/packages/screens/Message/components/ChatHeader.tsx b/packages/screens/Message/components/ChatHeader.tsx
new file mode 100644
index 0000000000..59470c7cb5
--- /dev/null
+++ b/packages/screens/Message/components/ChatHeader.tsx
@@ -0,0 +1,236 @@
+import Clipboard from "@react-native-clipboard/clipboard";
+import React, { useRef, useState } from "react";
+import { View, TouchableOpacity } from "react-native";
+import { useDispatch } from "react-redux";
+
+import { ConversationAvatar } from "./ConversationAvatar";
+import { SearchInput } from "./SearchInput";
+import dots from "../../../../assets/icons/dots.svg";
+import searchSVG from "../../../../assets/icons/search.svg";
+import { BrandText } from "../../../components/BrandText";
+import FlexRow from "../../../components/FlexRow";
+import { SVG } from "../../../components/SVG";
+import { TertiaryBox } from "../../../components/boxes/TertiaryBox";
+import { SpacerRow } from "../../../components/spacer";
+import { useDropdowns } from "../../../context/DropdownsProvider";
+import { useFeedbacks } from "../../../context/FeedbacksProvider";
+import { updateConversationById } from "../../../store/slices/message";
+import { neutral17, secondaryColor } from "../../../utils/style/colors";
+import { fontSemibold13, fontSemibold12 } from "../../../utils/style/fonts";
+import { layout } from "../../../utils/style/layout";
+import { Conversation } from "../../../utils/types/message";
+import { weshClient } from "../../../weshnet/client";
+import { subscribeMessages } from "../../../weshnet/message/subscriber";
+import { getConversationName } from "../../../weshnet/messageHelpers";
+import { createMultiMemberShareableLink } from "../../../weshnet/services";
+import { bytesFromString } from "../../../weshnet/utils";
+
+interface ChatHeaderProps {
+ searchInput: string;
+ setSearchInput: (input: string) => void;
+ conversation: Conversation;
+}
+
+export const ChatHeader = ({
+ searchInput,
+ setSearchInput,
+ conversation,
+}: ChatHeaderProps) => {
+ const dispatch = useDispatch();
+ const { setToastSuccess } = useFeedbacks();
+
+ const [showTextInput, setShowTextInput] = useState(false);
+
+ const { onPressDropdownButton, isDropdownOpen, closeOpenedDropdown } =
+ useDropdowns();
+ const dropdownRef = useRef(null);
+ const handleSearchIconPress = () => {
+ setShowTextInput(true);
+ };
+
+ const LIST_ITEMS = [
+ conversation.type === "group" && {
+ label: "Leave Group",
+ onPress: async () => {
+ await weshClient.client.MultiMemberGroupLeave({
+ groupPk: bytesFromString(conversation.id),
+ });
+ closeOpenedDropdown();
+ },
+ },
+ conversation.type === "group" && {
+ label: "Copy conversation link",
+ onPress: async () => {
+ const groupInfo = await weshClient.client.GroupInfo({
+ groupPk: bytesFromString(conversation.id),
+ });
+
+ if (!groupInfo.group) {
+ return;
+ }
+
+ const groupLink = createMultiMemberShareableLink(
+ groupInfo?.group,
+ conversation.name,
+ );
+ Clipboard.setString(groupLink || "");
+ setToastSuccess({
+ title: "Group link copied!",
+ message: "",
+ });
+ },
+ },
+ conversation.status === "archived" && {
+ label: "Unarchive Chat",
+ onPress: () => {
+ dispatch(
+ updateConversationById({
+ id: conversation.id,
+ status: "active",
+ }),
+ );
+ subscribeMessages(conversation.id);
+ closeOpenedDropdown();
+ },
+ },
+ conversation.status === "active" && {
+ label: "Archive Chat",
+ onPress: () => {
+ dispatch(
+ updateConversationById({
+ id: conversation.id,
+ status: "archived",
+ }),
+ );
+ closeOpenedDropdown();
+ },
+ },
+ conversation.status === "archived" && {
+ label: "Unarchive Chat",
+ onPress: () => {
+ dispatch(
+ updateConversationById({
+ id: conversation.id,
+ status: "active",
+ }),
+ );
+ closeOpenedDropdown();
+ },
+ },
+ ].filter(Boolean) as {
+ label: string;
+ onPress: () => void;
+ }[];
+
+ return (
+ <>
+
+
+
+
+
+
+ {getConversationName(conversation)}
+
+
+
+
+ {showTextInput ? (
+ setShowTextInput(false)}
+ />
+ ) : (
+
+
+
+
+
+
+
+ onPressDropdownButton(dropdownRef)}
+ >
+
+
+ {isDropdownOpen(dropdownRef) && (
+
+ {LIST_ITEMS.map((item) => {
+ return (
+
+
+
+ {item.label}
+
+
+
+ );
+ })}
+
+ )}
+
+
+
+
+ )}
+
+
+ >
+ );
+};
diff --git a/packages/screens/Message/components/ChatItem.tsx b/packages/screens/Message/components/ChatItem.tsx
new file mode 100644
index 0000000000..6ed37858bb
--- /dev/null
+++ b/packages/screens/Message/components/ChatItem.tsx
@@ -0,0 +1,135 @@
+import moment from "moment";
+import React, { useMemo } from "react";
+import { Platform, TouchableOpacity, View } from "react-native";
+import { useSelector } from "react-redux";
+
+import { MessageAvatar } from "./MessageAvatar";
+import { BrandText } from "../../../components/BrandText";
+import FlexRow from "../../../components/FlexRow";
+import { SpacerColumn, SpacerRow } from "../../../components/spacer";
+import {
+ selectConversationById,
+ selectLastContactMessageByGroupPk,
+ selectLastMessageByGroupPk,
+} from "../../../store/slices/message";
+import { useAppNavigation } from "../../../utils/navigation";
+import {
+ neutral00,
+ neutral22,
+ neutralA3,
+ secondaryColor,
+} from "../../../utils/style/colors";
+import {
+ fontMedium10,
+ fontSemibold11,
+ fontSemibold13,
+} from "../../../utils/style/fonts";
+import { layout } from "../../../utils/style/layout";
+import { Conversation } from "../../../utils/types/message";
+import { getConversationName } from "../../../weshnet/messageHelpers";
+interface ChatItemProps {
+ data: Conversation;
+ onPress: () => void;
+ isActive: boolean;
+ isLastItem: boolean;
+}
+
+export const ChatItem = ({
+ data,
+ onPress,
+ isActive,
+ isLastItem,
+}: ChatItemProps) => {
+ const navigation = useAppNavigation();
+ const lastMessage = useSelector(selectLastMessageByGroupPk(data.id));
+ const lastContactMessage = useSelector(
+ selectLastContactMessageByGroupPk(data.id),
+ );
+ const contactInfo = data.members?.[0];
+ const conversation = useSelector(selectConversationById(data.id));
+
+ const isAllMessageRead = useMemo(() => {
+ return lastContactMessage?.id === conversation.lastReadIdByMe;
+ }, [conversation.lastReadIdByMe, lastContactMessage?.id]);
+
+ return (
+
+ ["android", "ios"].includes(Platform.OS)
+ ? navigation.navigate("ChatSection", data)
+ : onPress()
+ }
+ >
+ {!isAllMessageRead && (
+
+ )}
+
+
+
+
+
+
+
+
+ {getConversationName(data)}
+
+
+
+
+ {lastMessage?.payload?.message}
+
+
+
+
+ {!!lastMessage && (
+
+
+
+ {moment(lastMessage?.timestamp).fromNow()}
+
+
+
+ )}
+
+
+ );
+};
diff --git a/packages/screens/Message/components/ChatSection.tsx b/packages/screens/Message/components/ChatSection.tsx
new file mode 100644
index 0000000000..748776aa9c
--- /dev/null
+++ b/packages/screens/Message/components/ChatSection.tsx
@@ -0,0 +1,558 @@
+import moment from "moment";
+import React, { RefObject, useEffect, useMemo, useRef, useState } from "react";
+import {
+ View,
+ TouchableOpacity,
+ useWindowDimensions,
+ FlatList,
+ Platform,
+} from "react-native";
+import { useDispatch, useSelector } from "react-redux";
+
+import { ChatHeader } from "./ChatHeader";
+import { Conversation } from "./Conversation";
+import { SearchConversation } from "./SearchConversation";
+import closeSVG from "../../../../assets/icons/close.svg";
+import sent from "../../../../assets/icons/sent.svg";
+import { BrandText } from "../../../components/BrandText";
+import { KeyboardAvoidingView } from "../../../components/KeyboardAvoidingView";
+import { SVG } from "../../../components/SVG";
+import { ScreenContainer } from "../../../components/ScreenContainer";
+import { TextInputCustom } from "../../../components/inputs/TextInputCustom";
+import { Separator } from "../../../components/separators/Separator";
+import { SpacerColumn, SpacerRow } from "../../../components/spacer";
+import { useFeedbacks } from "../../../context/FeedbacksProvider";
+import {
+ selectConversationById,
+ selectMessageListByGroupPk,
+ updateConversationById,
+} from "../../../store/slices/message";
+import {
+ ScreenFC,
+ useAppNavigation,
+ useAppRoute,
+} from "../../../utils/navigation";
+import {
+ neutral00,
+ neutral33,
+ neutral77,
+ neutralA3,
+ redDefault,
+} from "../../../utils/style/colors";
+import {
+ fontSemibold10,
+ fontSemibold12,
+ fontSemibold14,
+} from "../../../utils/style/fonts";
+import { layout } from "../../../utils/style/layout";
+import {
+ Conversation as IConversation,
+ Message,
+ MessageFileData,
+ ReplyTo,
+} from "../../../utils/types/message";
+import { weshConfig } from "../../../weshnet";
+import { getNewConversationText } from "../../../weshnet/messageHelpers";
+import { sendMessage } from "../../../weshnet/services";
+import { bytesFromString, stringFromBytes } from "../../../weshnet/utils";
+
+interface ChatSectionProps {
+ conversation: IConversation;
+}
+export interface HandleSendParams {
+ message: string;
+ files: MessageFileData[];
+}
+
+export const ChatSection = ({ conversation }: ChatSectionProps) => {
+ const [message, setMessage] = useState("");
+ const [inputHeight, setInputHeight] = useState(40);
+ const [replyTo, setReplyTo] = useState();
+ const [inputRef, setInputRef] = useState | null>(null);
+ const dispatch = useDispatch();
+
+ const [lastReadProcessedId, setLastReadProcessedId] = useState("");
+ const conversationItem = useSelector(selectConversationById(conversation.id));
+ const [lastReadMessage, setLastReadMessage] = useState();
+
+ const [searchInput, setSearchInput] = useState("");
+
+ const flatListRef = useRef(null);
+
+ const { setToastError } = useFeedbacks();
+ const messages = useSelector(selectMessageListByGroupPk(conversation.id));
+
+ const contactMessages = useMemo(
+ () =>
+ messages.filter(
+ (item) =>
+ item.senderId !== stringFromBytes(weshConfig.config?.accountPk),
+ ),
+ [messages],
+ );
+
+ const lastContactReadMessageIndex = useMemo(() => {
+ const message = messages.find(
+ (item) => item.id === conversationItem.lastReadIdByContact,
+ );
+ if (!message) {
+ return null;
+ }
+
+ const index = messages.findIndex((msg) => msg.id === message?.id);
+ if (index === -1) {
+ return null;
+ }
+ return index;
+ }, [conversationItem, messages]);
+
+ const searchResults = useMemo(() => {
+ if (!searchInput) {
+ return [];
+ }
+ return messages.filter(
+ (item) =>
+ item?.payload?.message
+ ?.toLowerCase()
+ .includes(searchInput?.toLowerCase()),
+ );
+ }, [messages, searchInput]);
+
+ const handleSend = async (data?: any) => {
+ if (!message && !data?.message) {
+ return;
+ }
+
+ try {
+ await sendMessage({
+ groupPk: bytesFromString(conversation.id),
+ message: {
+ type: "message",
+ parentId: replyTo?.id || "",
+ payload: {
+ message: message || data?.message,
+ files: [],
+ },
+ },
+ });
+
+ setMessage("");
+ setReplyTo(undefined);
+ inputRef?.current?.focus();
+ } catch (err: any) {
+ setToastError({
+ title: "Failed to send message",
+ message: err?.message,
+ });
+ }
+ };
+ const { height } = useWindowDimensions();
+
+ const handleRead = async (lastMessageId: string) => {
+ if (lastReadProcessedId === lastMessageId) {
+ return;
+ }
+
+ setLastReadProcessedId(lastMessageId);
+ try {
+ if (!lastMessageId) {
+ return;
+ }
+ if (conversation.type === "group") {
+ dispatch(
+ updateConversationById({
+ id: conversation.id,
+ lastReadIdByMe: lastMessageId,
+ }),
+ );
+ } else {
+ await sendMessage({
+ groupPk: bytesFromString(conversation.id),
+ message: {
+ type: "read",
+ payload: {
+ message: "-",
+ metadata: {
+ lastReadBy: stringFromBytes(weshConfig.config?.accountPk),
+ lastReadId: lastMessageId,
+ },
+ files: [],
+ },
+ },
+ });
+ }
+ } catch {}
+ };
+
+ useEffect(() => {
+ const lastReadMessageIndex = messages.findIndex(
+ (item) => item?.id === conversation?.lastReadIdByMe,
+ );
+ if (lastReadMessageIndex === -1) {
+ return;
+ }
+
+ const nextContactMessages = messages.filter(
+ (item, index) =>
+ index < lastReadMessageIndex &&
+ item.senderId !== stringFromBytes(weshConfig.config?.accountPk),
+ );
+
+ const nextMessage = nextContactMessages[nextContactMessages.length - 1];
+
+ if (
+ nextMessage &&
+ nextMessage.id !== conversation.lastReadIdByMe &&
+ lastReadMessage?.id !== nextMessage.id
+ ) {
+ setLastReadMessage(nextMessage);
+ }
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [conversation, messages]);
+
+ useEffect(() => {
+ if (!conversationItem?.id || !contactMessages?.length) {
+ return;
+ }
+
+ if (contactMessages?.[0]?.id !== conversationItem.lastReadIdByMe) {
+ handleRead(contactMessages?.[0]?.id);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [conversationItem, contactMessages]);
+
+ return (
+
+
+
+
+
+
+
+ {!!searchInput && (
+
+ {!searchResults.length && (
+
+
+ No result found
+
+
+ )}
+ {
+ return (
+ <>
+ {
+ setSearchInput("");
+ flatListRef.current?.scrollToIndex({
+ index: messages.findIndex(
+ (message) => message.id === item.id,
+ ),
+ });
+ }}
+ conversation={conversation}
+ message={item}
+ groupPk={bytesFromString(conversation.id)}
+ />
+ >
+ );
+ }}
+ keyExtractor={(item) => item.id}
+ />
+
+ )}
+
+ {!messages.length && (
+
+
+ {getNewConversationText(conversation)}
+
+
+ )}
+
+ {
+ const previousMessage =
+ index < messages.length - 1 ? messages[index + 1] : undefined;
+ const nextMessage = index > 0 ? messages[index - 1] : undefined;
+
+ const separatorDate = previousMessage
+ ? moment(item.timestamp).format("DD/MM/YYYY") !==
+ moment(previousMessage.timestamp).format("DD/MM/YYYY") &&
+ item.timestamp
+ : item.timestamp;
+
+ let isNewSeparator = false;
+
+ if (lastReadMessage?.id) {
+ if (lastReadMessage.id === contactMessages?.[0]?.id) {
+ isNewSeparator = false;
+ } else if (lastReadMessage.id === item?.id) {
+ isNewSeparator = true;
+ }
+ }
+ const isReadByContact =
+ lastContactReadMessageIndex === null
+ ? false
+ : index >= lastContactReadMessageIndex;
+
+ const parentMessage = item?.parentId
+ ? messages.find((msg) => msg.id === item.parentId)
+ : undefined;
+
+ if (item.type === "group-join") {
+ return null;
+ }
+
+ return (
+ <>
+ {item.type !== "accept-contact" && (
+
+ )}
+ {item.type === "accept-contact" && (
+
+
+ Contact accepted
+
+
+ )}
+ {(!!isNewSeparator || !!separatorDate) && (
+
+ {!!separatorDate && (
+
+ {moment(separatorDate).format("DD/MM/YYYY")}
+
+ )}
+
+ {!!isNewSeparator && (
+
+ New
+
+ )}
+
+
+ )}
+ >
+ );
+ }}
+ keyExtractor={(item) => item.id}
+ />
+
+
+
+ {!!messages.length && (
+
+
+
+ {!!replyTo?.message && (
+
+
+ Reply to: {replyTo?.message}
+
+ {
+ setReplyTo(undefined);
+ }}
+ activeOpacity={0.9}
+ >
+
+
+
+ )}
+ {
+ if (message.length) {
+ handleSend();
+ }
+ }}
+ onContentSizeChange={(event) => {
+ setInputHeight(event.nativeEvent.contentSize.height);
+ }}
+ onKeyPress={(e) => {
+ if (
+ Platform.OS === "web" &&
+ e.nativeEvent.key === "Enter" &&
+ //@ts-ignore
+ !e?.shiftKey
+ ) {
+ e.preventDefault();
+ if (message.length) {
+ handleSend();
+ }
+ }
+ }}
+ >
+
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+export const ChatSectionScreen: ScreenFC<"ChatSection"> = () => {
+ const { params } = useAppRoute();
+ const { navigate } = useAppNavigation();
+ return (
+ navigate("Message")}
+ footerChildren={<>>}
+ >
+
+
+ );
+};
diff --git a/packages/screens/Message/components/CheckboxGroup.tsx b/packages/screens/Message/components/CheckboxGroup.tsx
new file mode 100644
index 0000000000..3f7890aabd
--- /dev/null
+++ b/packages/screens/Message/components/CheckboxGroup.tsx
@@ -0,0 +1,103 @@
+import React, { useMemo, useState } from "react";
+import { View, TouchableOpacity } from "react-native";
+import { Avatar } from "react-native-paper";
+
+import { BrandText } from "../../../components/BrandText";
+import FlexRow from "../../../components/FlexRow";
+import { SpacerColumn, SpacerRow } from "../../../components/spacer";
+import { neutral77, secondaryColor } from "../../../utils/style/colors";
+import { fontSemibold14 } from "../../../utils/style/fonts";
+import { layout } from "../../../utils/style/layout";
+import { CheckboxDappStore } from "../../DAppStore/components/CheckboxDappStore";
+export interface CheckboxItem {
+ id: string;
+ name: string;
+ avatar: string;
+ checked: boolean;
+}
+interface CheckboxGroupProps {
+ items: CheckboxItem[];
+ onChange: (items: CheckboxItem[]) => void;
+ searchText: string;
+}
+
+const Checkbox = ({
+ item,
+ onPress,
+}: {
+ item: CheckboxItem;
+ onPress: () => void;
+}) => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {item.name}
+
+
+
+ >
+ );
+};
+
+export const CheckboxGroup: React.FC = ({
+ items,
+ onChange,
+ searchText,
+}) => {
+ const [checkboxItems, setCheckboxItems] = useState(items);
+ const handleCheckboxPress = (index: number) => {
+ const newItems = [...checkboxItems];
+ newItems[index].checked = !newItems[index].checked;
+ setCheckboxItems(newItems);
+ onChange(newItems);
+ };
+
+ const searchItems = useMemo(() => {
+ return checkboxItems
+ .filter((item) =>
+ item.name.toLowerCase().includes(searchText.toLowerCase()),
+ )
+ .filter((item) => !item.checked);
+ }, [searchText, checkboxItems]);
+
+ return (
+
+ {!searchItems.length && !!searchText.trim() && (
+
+
+ No records found
+
+
+ )}
+ {checkboxItems
+ .filter((item) => item.checked)
+ .map((item, index) => (
+ handleCheckboxPress(index)}
+ />
+ ))}
+ {searchItems.map((item, index) => (
+ handleCheckboxPress(index)}
+ />
+ ))}
+
+ );
+};
diff --git a/packages/screens/Message/components/Conversation.tsx b/packages/screens/Message/components/Conversation.tsx
new file mode 100644
index 0000000000..79331122b3
--- /dev/null
+++ b/packages/screens/Message/components/Conversation.tsx
@@ -0,0 +1,303 @@
+import { chain } from "lodash";
+import moment from "moment";
+import React, { useMemo, useState } from "react";
+import { View, TouchableOpacity } from "react-native";
+import { Avatar } from "react-native-paper";
+
+import { FileRenderer } from "./FileRenderer";
+import { GroupInvitationAction } from "./GroupInvitationAction";
+import { MessagePopup } from "./MessagePopup";
+import doubleCheckSVG from "../../../../assets/icons/doublecheck.svg";
+import replySVG from "../../../../assets/icons/reply.svg";
+import { BrandText } from "../../../components/BrandText";
+import { Dropdown } from "../../../components/Dropdown";
+import FlexCol from "../../../components/FlexCol";
+import FlexRow from "../../../components/FlexRow";
+import { SVG } from "../../../components/SVG";
+import { EmojiSelector } from "../../../components/socialFeed/EmojiSelector";
+import { Reactions } from "../../../components/socialFeed/SocialActions/Reactions";
+import { SpacerRow } from "../../../components/spacer";
+import {
+ neutral77,
+ secondaryColor,
+ purpleDark,
+ neutral17,
+ neutralA3,
+} from "../../../utils/style/colors";
+import {
+ fontBold9,
+ fontMedium10,
+ fontSemibold11,
+} from "../../../utils/style/fonts";
+import { layout } from "../../../utils/style/layout";
+import {
+ Conversation as IConversation,
+ Message,
+ ReplyTo,
+} from "../../../utils/types/message";
+import { weshConfig } from "../../../weshnet";
+import { getConversationAvatar } from "../../../weshnet/messageHelpers";
+import { sendMessage } from "../../../weshnet/services";
+import { stringFromBytes } from "../../../weshnet/utils";
+
+interface ConversationProps {
+ conversation: IConversation;
+ message: Message;
+ groupPk?: Uint8Array;
+ isMessageChain: boolean;
+ isNextMine: boolean;
+ onReply: (params: ReplyTo) => void;
+ parentMessage?: Message;
+ isReadByContact?: boolean;
+}
+
+export const Conversation = ({
+ conversation,
+ message,
+ groupPk,
+ isMessageChain,
+ isNextMine,
+ onReply,
+ parentMessage,
+ isReadByContact,
+}: ConversationProps) => {
+ const [showPopup, setShowPopup] = useState(false);
+ const [showMenu, setShowMenu] = useState(false);
+
+ const isSender =
+ message.senderId === stringFromBytes(weshConfig?.config?.accountPk);
+
+ const reactions = useMemo(() => {
+ if (!message?.reactions?.length) {
+ return [];
+ }
+ return chain(message.reactions || [])
+ .groupBy(message?.payload?.message)
+ .map((value, key) => ({
+ icon: value?.[0]?.payload?.message || "",
+ count: value.length,
+ }))
+ .value();
+ }, [message?.payload?.message, message?.reactions]);
+
+ const onEmojiSelected = async (emoji: string | null) => {
+ if (emoji) {
+ await sendMessage({
+ groupPk,
+ message: {
+ type: "reaction",
+ parentId: message.id,
+ payload: {
+ message: emoji,
+ files: [],
+ },
+ },
+ });
+ }
+ };
+
+ const receiverName = "Anon";
+
+ return (
+
+ {!isSender && (
+
+ {!isMessageChain && (
+
+ )}
+
+ )}
+
+ {(!isMessageChain || isSender) && (
+
+
+
+ {isSender ? "Me" : receiverName}
+
+
+ {moment(message.timestamp).local().format("HH:mm")}
+
+
+ {isSender && !!isReadByContact && (
+
+ )}
+
+ )}
+ setShowPopup(true)}
+ activeOpacity={0.9}
+ disabled={isSender}
+ >
+ {!!parentMessage?.id && (
+
+
+
+ {parentMessage?.payload?.message}
+
+
+ )}
+
+ {["message", "group-invite"].includes(message.type) && (
+ <>
+
+ {message?.payload?.message}
+
+ {!!message?.payload?.files?.length && (
+
+ )}
+ >
+ )}
+
+ {message?.type === "group-invite" && !isSender && (
+
+ )}
+
+ {!isSender && (
+ <>
+ {showPopup && (
+ setShowPopup(false)}
+ positionStyle={{
+ bottom: -10,
+ right: -100,
+ }}
+ >
+
+
+ {
+ onEmojiSelected(emoji);
+ setShowPopup(false);
+ }}
+ />
+
+ setShowMenu(true)}>
+
+
+
+
+
+ )}
+ {showMenu && (
+ setShowMenu(false)}
+ positionStyle={{
+ bottom: -10,
+ right: -240,
+ }}
+ >
+ setShowMenu(false)}
+ message={message?.payload?.message || ""}
+ onReply={() => {
+ onReply({
+ id: message.id,
+ message: message?.payload?.message || "",
+ });
+ }}
+ />
+
+ )}
+ >
+ )}
+
+
+ {}} />
+
+
+
+ );
+};
diff --git a/packages/screens/Message/components/ConversationAvatar.tsx b/packages/screens/Message/components/ConversationAvatar.tsx
new file mode 100644
index 0000000000..dcb029449e
--- /dev/null
+++ b/packages/screens/Message/components/ConversationAvatar.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+import { View } from "react-native";
+import { Avatar } from "react-native-paper";
+
+import { Conversation } from "../../../utils/types/message";
+import { getConversationAvatar } from "../../../weshnet/messageHelpers";
+
+interface ConversationAvatarProps {
+ conversation: Conversation;
+ size?: number;
+}
+
+export const ConversationAvatar = ({
+ conversation,
+ size = 30,
+}: ConversationAvatarProps) => {
+ if (conversation.members?.length > 1) {
+ return (
+
+ {conversation.members.map((_, index) => (
+
+ ))}
+
+ );
+ }
+ return (
+ <>
+
+ >
+ );
+};
diff --git a/packages/screens/Message/components/ConversationSelector.tsx b/packages/screens/Message/components/ConversationSelector.tsx
new file mode 100644
index 0000000000..f8ba76fc8b
--- /dev/null
+++ b/packages/screens/Message/components/ConversationSelector.tsx
@@ -0,0 +1,99 @@
+import React, { useRef } from "react";
+import { StyleProp, TouchableOpacity, View, ViewStyle } from "react-native";
+
+import chevronDownSVG from "../../../../assets/icons/chevron-down.svg";
+import chevronUpSVG from "../../../../assets/icons/chevron-up.svg";
+import { BrandText } from "../../../components/BrandText";
+import { SVG } from "../../../components/SVG";
+import { TertiaryBox } from "../../../components/boxes/TertiaryBox";
+import { SpacerRow } from "../../../components/spacer";
+import { useDropdowns } from "../../../context/DropdownsProvider";
+import { useMessage } from "../../../context/MessageProvider";
+import { neutral17, secondaryColor } from "../../../utils/style/colors";
+import { fontSemibold12 } from "../../../utils/style/fonts";
+import { layout } from "../../../utils/style/layout";
+import { CONVERSATION_TYPES } from "../../../utils/types/message";
+
+export const ConversationSelector: React.FC<{
+ style?: StyleProp;
+}> = ({ style }) => {
+ const { onPressDropdownButton, isDropdownOpen, closeOpenedDropdown } =
+ useDropdowns();
+ const dropdownRef = useRef(null);
+ const { activeConversationType, setActiveConversationType } = useMessage();
+
+ const onPressItem = (conversationType: CONVERSATION_TYPES) => {
+ setActiveConversationType(conversationType);
+ closeOpenedDropdown();
+ };
+
+ const fontSize = 14;
+
+ return (
+
+ onPressDropdownButton(dropdownRef)}
+ style={{
+ flexDirection: "row",
+ paddingHorizontal: 12,
+ }}
+ >
+
+
+ {activeConversationType}
+
+
+
+
+
+ {isDropdownOpen(dropdownRef) && (
+
+ {Object.values(CONVERSATION_TYPES).map((type, index) => {
+ return (
+ onPressItem(type)}
+ >
+
+
+ {type}
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+};
diff --git a/packages/screens/Message/components/CreateConversation.tsx b/packages/screens/Message/components/CreateConversation.tsx
new file mode 100644
index 0000000000..8ef7ab1a6e
--- /dev/null
+++ b/packages/screens/Message/components/CreateConversation.tsx
@@ -0,0 +1,245 @@
+import React, { useState, useEffect } from "react";
+import { Platform, View } from "react-native";
+import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scrollview";
+import QRCode from "react-native-qrcode-svg";
+import { useDispatch, useSelector } from "react-redux";
+
+import logoHexagonPNG from "../../../../assets/logos/logo-hexagon.png";
+import { BrandText } from "../../../components/BrandText";
+import { CopyToClipboard } from "../../../components/CopyToClipboard";
+import { ErrorText } from "../../../components/ErrorText";
+import { PrimaryButton } from "../../../components/buttons/PrimaryButton";
+import { SecondaryButton } from "../../../components/buttons/SecondaryButton";
+import { TextInputCustom } from "../../../components/inputs/TextInputCustom";
+import ModalBase from "../../../components/modals/ModalBase";
+import { QRCodeScannerModal } from "../../../components/modals/QRCodeScannerModal";
+import { Separator } from "../../../components/separators/Separator";
+import { SpacerColumn, SpacerRow } from "../../../components/spacer";
+import { useFeedbacks } from "../../../context/FeedbacksProvider";
+import { useIsMobile } from "../../../hooks/useIsMobile";
+import {
+ MessageState,
+ selectContactInfo,
+ setContactInfo,
+} from "../../../store/slices/message";
+import { neutral00, secondaryColor } from "../../../utils/style/colors";
+import { fontSemibold16 } from "../../../utils/style/fonts";
+import { layout } from "../../../utils/style/layout";
+import { weshServices } from "../../../weshnet";
+import { createSharableLink } from "../../../weshnet/services";
+interface CreateConversationProps {
+ onClose: () => void;
+}
+
+export const CreateConversation = ({ onClose }: CreateConversationProps) => {
+ const contactInfo = useSelector(selectContactInfo);
+ const { setToastSuccess, setToastError } = useFeedbacks();
+ const [contactLink, setContactLink] = useState("");
+ const [addContactLoading, setAddContactLoading] = useState(false);
+ const [error, setError] = useState("");
+ const [isScan, setIsScan] = useState(false);
+ const isMobile = useIsMobile();
+
+ const dispatch = useDispatch();
+
+ const handleAddContact = async (link = contactLink) => {
+ setAddContactLoading(true);
+ setError("");
+
+ try {
+ await weshServices.addContact(link, contactInfo);
+ setToastSuccess({
+ title: "Request sent",
+ message: "Contact Request sent successfully",
+ });
+ onClose();
+ } catch (err: any) {
+ setError(err?.message);
+ setToastError({
+ title: "Request sent error",
+ message: err?.message,
+ });
+ }
+
+ setAddContactLoading(false);
+ };
+
+ const handleContactInfoChange = (
+ key: keyof MessageState["contactInfo"],
+ value: string,
+ ) => {
+ dispatch(
+ setContactInfo({
+ [key]: value,
+ }),
+ );
+ };
+
+ useEffect(() => {
+ const shareLink = createSharableLink({
+ ...contactInfo,
+ });
+ dispatch(
+ setContactInfo({
+ shareLink,
+ }),
+ );
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [dispatch]);
+
+ const handleScan = async (link?: string) => {
+ if (link) {
+ await handleAddContact(link);
+ onClose();
+ } else {
+ setIsScan(false);
+ }
+ };
+
+ if (isScan) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ {!!contactInfo.shareLink && (
+
+ )}
+
+
+ {Platform.OS !== "web" && (
+ setIsScan(true)}
+ />
+ )}
+
+
+ Name
+
+
+ handleContactInfoChange("name", text)}
+ value={contactInfo.name}
+ containerStyle={{
+ flex: 1,
+ }}
+ placeholderTextColor={secondaryColor}
+ squaresBackgroundColor={neutral00}
+ />
+
+
+
+ Avatar
+
+
+ handleContactInfoChange("avatar", text)}
+ value={contactInfo.avatar}
+ containerStyle={{
+ flex: 1,
+ }}
+ placeholderTextColor={secondaryColor}
+ squaresBackgroundColor={neutral00}
+ />
+
+
+
+
+
+ Share my contact
+
+
+
+
+
+
+
+
+ Add contact
+
+
+
+
+
+
+ {isMobile && }
+
+
+ {!!error && (
+ <>
+
+ {error}
+ >
+ )}
+
+
+
+
+ );
+};
diff --git a/packages/screens/Message/components/CreateGroup.tsx b/packages/screens/Message/components/CreateGroup.tsx
new file mode 100644
index 0000000000..98e313b78b
--- /dev/null
+++ b/packages/screens/Message/components/CreateGroup.tsx
@@ -0,0 +1,180 @@
+import React, { useMemo, useState } from "react";
+import { View, ScrollView } from "react-native";
+import { useSelector } from "react-redux";
+
+import { CheckboxGroup, CheckboxItem } from "./CheckboxGroup";
+import { GroupInfo_Reply } from "../../../api/weshnet/protocoltypes";
+import { PrimaryButton } from "../../../components/buttons/PrimaryButton";
+import { TextInputCustom } from "../../../components/inputs/TextInputCustom";
+import ModalBase from "../../../components/modals/ModalBase";
+import { Separator } from "../../../components/separators/Separator";
+import { SeparatorGradient } from "../../../components/separators/SeparatorGradient";
+import { SearchInput } from "../../../components/sorts/SearchInput";
+import { SpacerColumn } from "../../../components/spacer";
+import { useFeedbacks } from "../../../context/FeedbacksProvider";
+import {
+ selectContactInfo,
+ selectConversationList,
+} from "../../../store/slices/message";
+import {
+ neutral00,
+ neutral33,
+ secondaryColor,
+} from "../../../utils/style/colors";
+import { weshClient } from "../../../weshnet/client";
+import { subscribeMessages } from "../../../weshnet/message/subscriber";
+import {
+ getConversationAvatar,
+ getConversationName,
+} from "../../../weshnet/messageHelpers";
+import { sendMessage } from "../../../weshnet/services";
+import { bytesFromString, stringFromBytes } from "../../../weshnet/utils";
+
+interface CreateGroupProps {
+ onClose: () => void;
+}
+
+export const CreateGroup = ({ onClose }: CreateGroupProps) => {
+ const [groupName, setGroupName] = useState("");
+ const [checkedContacts, setCheckedContacts] = useState([]);
+ const contactInfo = useSelector(selectContactInfo);
+ const { setToastError } = useFeedbacks();
+ const [loading, setLoading] = useState(false);
+ const [searchText, setSearchText] = useState("");
+ const conversations = useSelector(selectConversationList());
+ const handleChange = (items: CheckboxItem[]) => {
+ setCheckedContacts(
+ items.filter((item) => item.checked).map((item) => item.id),
+ );
+ };
+
+ const items: CheckboxItem[] = useMemo(() => {
+ return conversations
+ .filter((conv) => conv.type === "contact")
+ .map((item) => {
+ const contactPk = item?.members?.[0].id;
+
+ return {
+ id: contactPk,
+ name: getConversationName(item),
+ avatar: getConversationAvatar(item),
+ checked: checkedContacts.includes(contactPk),
+ };
+ });
+ }, [conversations, checkedContacts]);
+
+ const handleCreateGroup = async () => {
+ if (!groupName) {
+ return;
+ }
+ setLoading(true);
+ try {
+ const group = await weshClient.client.MultiMemberGroupCreate({});
+
+ const groupInfo = await weshClient.client.GroupInfo({
+ groupPk: group.groupPk,
+ });
+
+ await sendMessage({
+ groupPk: group.groupPk,
+ message: {
+ type: "group-create",
+ payload: {
+ message: "",
+ files: [],
+ metadata: {
+ groupName,
+ },
+ },
+ },
+ });
+
+ await Promise.all(
+ conversations
+ .filter((item) => checkedContacts.includes(item?.members?.[0]?.id))
+ .map(async (item) => {
+ const contactPk = bytesFromString(item.members[0].id);
+ const _group = await weshClient.client.GroupInfo({
+ contactPk,
+ });
+
+ await sendMessage({
+ groupPk: _group.group?.publicKey,
+ message: {
+ type: "group-invite",
+ payload: {
+ message: `${
+ contactInfo.name || "Anon"
+ } has invited you to join group ${groupName}`,
+ metadata: {
+ groupName,
+ group: (GroupInfo_Reply.toJSON(groupInfo) as any).group,
+ contact: item?.members?.[0],
+ },
+ files: [],
+ },
+ },
+ });
+ }),
+ );
+
+ subscribeMessages(stringFromBytes(groupInfo.group?.publicKey));
+
+ onClose();
+ } catch (err: any) {
+ console.error("create group err", err);
+ setToastError({
+ title: "Group creation failed",
+ message: err?.message,
+ });
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/screens/Message/components/FileRenderer.tsx b/packages/screens/Message/components/FileRenderer.tsx
new file mode 100644
index 0000000000..8730b7e1cf
--- /dev/null
+++ b/packages/screens/Message/components/FileRenderer.tsx
@@ -0,0 +1,42 @@
+import React, { useMemo } from "react";
+import { View } from "react-native";
+import { z } from "zod";
+
+import { ImagesViews } from "../../../components/FilePreview/ImagesViews";
+import { VideoView } from "../../../components/FilePreview/VideoView";
+import { ZodRemoteFileData } from "../../../utils/types/files";
+
+interface Props {
+ files: z.infer[];
+ maxWidth?: number;
+ waveFormMaxWidth?: number;
+}
+export const THUMBNAIL_WIDTH = 140;
+
+export const FileRenderer = ({ files, maxWidth, waveFormMaxWidth }: Props) => {
+ const imageFiles = useMemo(
+ () =>
+ files
+ ?.filter(
+ (file) => file.fileType === "image" || file.fileType === "base64",
+ )
+ .map((file) => ({
+ ...file,
+ })),
+ [files],
+ );
+ const videoFiles = useMemo(
+ () => files?.filter((file) => file.fileType === "video"),
+ [files],
+ );
+
+ return (
+
+ {!!imageFiles?.length && }
+
+ {videoFiles?.map((file, index) => (
+
+ ))}
+
+ );
+};
diff --git a/packages/screens/Message/components/Friends.tsx b/packages/screens/Message/components/Friends.tsx
new file mode 100644
index 0000000000..2d90ee43a3
--- /dev/null
+++ b/packages/screens/Message/components/Friends.tsx
@@ -0,0 +1,55 @@
+import React, { useState } from "react";
+import { View, Platform } from "react-native";
+
+import FriendList from "./FriendsList";
+import { MessageBlankFiller } from "./MessageBlankFiller";
+import { TextInputCustomBorder } from "../../../components/inputs/TextInputCustomBorder";
+import { SpacerColumn } from "../../../components/spacer";
+import { useAppNavigation } from "../../../utils/navigation";
+import { neutral00 } from "../../../utils/style/colors";
+import { Conversation } from "../../../utils/types/message";
+interface FriendsProps {
+ items: Conversation[];
+ setActiveConversation?: (item: Conversation) => void;
+}
+export const Friends = ({ items, setActiveConversation }: FriendsProps) => {
+ const [searchQuery, setSearchQuery] = useState("");
+ const { navigate } = useAppNavigation();
+
+ const filteredItems = items.filter((item) =>
+ item.name.toLowerCase().includes(searchQuery.toLowerCase()),
+ );
+
+ return (
+
+
+
+
+
+ {filteredItems?.length > 0 ? (
+ filteredItems?.map((item) => (
+ {
+ if (Platform.OS === "web") {
+ setActiveConversation?.(item);
+ navigate("Message");
+ } else {
+ navigate("ChatSection", item);
+ }
+ setActiveConversation?.(item);
+ }}
+ />
+ ))
+ ) : (
+
+ )}
+
+ );
+};
diff --git a/packages/screens/Message/components/FriendsBar.tsx b/packages/screens/Message/components/FriendsBar.tsx
new file mode 100644
index 0000000000..20011ffa5f
--- /dev/null
+++ b/packages/screens/Message/components/FriendsBar.tsx
@@ -0,0 +1,94 @@
+import React from "react";
+import { View, TouchableOpacity, Platform } from "react-native";
+import { useSelector } from "react-redux";
+
+import forwardSVG from "../../../../assets/icons/forward.svg";
+import friendsSVG from "../../../../assets/icons/friends.svg";
+import { BrandText } from "../../../components/BrandText";
+import FlexRow from "../../../components/FlexRow";
+import { SVG } from "../../../components/SVG";
+import { TertiaryBadge } from "../../../components/badges/TertiaryBadge";
+import { SpacerRow } from "../../../components/spacer";
+import {
+ selectConversationList,
+ selectContactRequestList,
+} from "../../../store/slices/message";
+import { useAppNavigation } from "../../../utils/navigation";
+import {
+ neutral22,
+ secondaryColor,
+ primaryColor,
+} from "../../../utils/style/colors";
+import { fontSemibold13 } from "../../../utils/style/fonts";
+
+export const FriendsBar = () => {
+ const contactRequests = useSelector(selectContactRequestList);
+ const conversations = useSelector(selectConversationList());
+ const { navigate } = useAppNavigation();
+ return (
+
+
+
+
+
+
+
+
+ Friends
+
+
+
+
+
+
+ {!!contactRequests?.length && (
+ {
+ if (Platform.OS === "web") {
+ navigate("Message", { view: "AddFriend", tab: "request" });
+ } else {
+ navigate("FriendshipManager", { tab: "request" });
+ }
+ }}
+ >
+
+
+ )}
+
+ {
+ if (Platform.OS === "web") {
+ navigate("Message", { view: "AddFriend", tab: "friends" });
+ } else {
+ navigate("FriendshipManager", { tab: "friends" });
+ }
+ }}
+ >
+
+ {conversations?.filter((conv) => conv.type === "contact")
+ ?.length || ""}
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/screens/Message/components/FriendsList.tsx b/packages/screens/Message/components/FriendsList.tsx
new file mode 100644
index 0000000000..941b9e1991
--- /dev/null
+++ b/packages/screens/Message/components/FriendsList.tsx
@@ -0,0 +1,60 @@
+import React from "react";
+import { View } from "react-native";
+import { TouchableOpacity } from "react-native-gesture-handler";
+
+import { MessageAvatar } from "./MessageAvatar";
+import chaticon from "../../../../assets/icons/chaticon.svg";
+import dots from "../../../../assets/icons/dots.svg";
+import { BrandText } from "../../../components/BrandText";
+import FlexRow from "../../../components/FlexRow";
+import { SVG } from "../../../components/SVG";
+import { Separator } from "../../../components/separators/Separator";
+import { SpacerColumn, SpacerRow } from "../../../components/spacer";
+import { neutral22, secondaryColor } from "../../../utils/style/colors";
+import { fontSemibold13 } from "../../../utils/style/fonts";
+import { Conversation } from "../../../utils/types/message";
+import { getConversationName } from "../../../weshnet/messageHelpers";
+
+type FriendListProps = {
+ item: Conversation;
+ handleChatPress: () => void;
+};
+
+const FriendList = ({ item, handleChatPress }: FriendListProps) => {
+ return (
+
+
+
+
+
+
+
+
+ {getConversationName(item)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default FriendList;
diff --git a/packages/screens/Message/components/FriendshipManager.tsx b/packages/screens/Message/components/FriendshipManager.tsx
new file mode 100644
index 0000000000..7ca74f8670
--- /dev/null
+++ b/packages/screens/Message/components/FriendshipManager.tsx
@@ -0,0 +1,111 @@
+import React, { useMemo } from "react";
+import { View, Platform } from "react-native";
+import { useSelector } from "react-redux";
+
+import { Friends } from "./Friends";
+import { Requests } from "./Requests";
+import plus from "../../../../assets/icons/Addplus.svg";
+import { ScreenContainer } from "../../../components/ScreenContainer";
+import { Separator } from "../../../components/separators/Separator";
+import { SpacerColumn } from "../../../components/spacer";
+import { Tabs } from "../../../components/tabs/Tabs";
+import {
+ selectContactRequestList,
+ selectConversationList,
+} from "../../../store/slices/message";
+import { ScreenFC, useAppNavigation } from "../../../utils/navigation";
+import { layout } from "../../../utils/style/layout";
+import {
+ Conversation,
+ MessageFriendsTabItem,
+} from "../../../utils/types/message";
+
+interface FriendshipManagerProps {
+ setActiveConversation?: (item: Conversation) => void;
+ activeTab?: string;
+}
+
+export const FriendshipManager = ({
+ setActiveConversation,
+ activeTab = "friends",
+}: FriendshipManagerProps) => {
+ const conversations = useSelector(selectConversationList());
+ const contactRequests = useSelector(selectContactRequestList);
+ const { navigate } = useAppNavigation();
+
+ const contactConversations = useMemo(() => {
+ return conversations.filter((item) => item.type === "contact");
+ }, [conversations]);
+
+ const tabs = {
+ friends: {
+ name: "Friends",
+ badgeCount: contactConversations.length,
+ icon: "",
+ },
+ request: {
+ name: "Requests",
+ badgeCount: contactRequests.length,
+ icon: "",
+ },
+ addFriend: {
+ name: "Add a friend",
+ icon: plus,
+ disabled: true,
+ },
+ };
+
+ const renderContentWeb = () => (
+ <>
+
+ {
+ if (Platform.OS === "web") {
+ navigate("Message", { view: "AddFriend", tab });
+ } else {
+ navigate("FriendshipManager", { tab });
+ }
+ }}
+ selected={activeTab as MessageFriendsTabItem}
+ tabContainerStyle={{
+ paddingBottom: layout.spacing_x1_5,
+ }}
+ style={{
+ height: 40,
+ }}
+ />
+
+
+ {activeTab === "friends" && (
+
+ )}
+ {activeTab === "request" && }
+ >
+ );
+ if (Platform.OS === "web") {
+ return (
+
+ {renderContentWeb()}
+
+ );
+ }
+ return (
+ navigate("Message")}>
+
+ {renderContentWeb()}
+
+
+ );
+};
+
+export const FriendshipManagerScreen: ScreenFC<"FriendshipManager"> = ({
+ route,
+}) => {
+ const activeTab = route?.params?.tab || "friends";
+
+ return ;
+};
diff --git a/packages/screens/Message/components/GroupInvitationAction.tsx b/packages/screens/Message/components/GroupInvitationAction.tsx
new file mode 100644
index 0000000000..c8e7ea7312
--- /dev/null
+++ b/packages/screens/Message/components/GroupInvitationAction.tsx
@@ -0,0 +1,123 @@
+import React, { useEffect, useState } from "react";
+import { useSelector } from "react-redux";
+
+import { GroupInfo_Reply } from "../../../api/weshnet/protocoltypes";
+import { BrandText } from "../../../components/BrandText";
+import FlexRow from "../../../components/FlexRow";
+import { PrimaryButton } from "../../../components/buttons/PrimaryButton";
+import { SpacerColumn, SpacerRow } from "../../../components/spacer";
+import { useFeedbacks } from "../../../context/FeedbacksProvider";
+import {
+ selectContactInfo,
+ selectConversationList,
+} from "../../../store/slices/message";
+import { purpleDark, successColor } from "../../../utils/style/colors";
+import { fontMedium10 } from "../../../utils/style/fonts";
+import { layout } from "../../../utils/style/layout";
+import { CONVERSATION_TYPES, Message } from "../../../utils/types/message";
+import { weshClient, weshConfig } from "../../../weshnet";
+import { sendMessage } from "../../../weshnet/services";
+import { stringFromBytes } from "../../../weshnet/utils";
+
+interface GroupInvitationActionProps {
+ message: Message;
+}
+
+export const GroupInvitationAction = ({
+ message,
+}: GroupInvitationActionProps) => {
+ const { setToastError } = useFeedbacks();
+
+ const contactInfo = useSelector(selectContactInfo);
+ const conversations = useSelector(
+ selectConversationList(CONVERSATION_TYPES.ALL),
+ );
+
+ const [isAccepted, setIsAccepted] = useState(false);
+
+ useEffect(() => {
+ if (!isAccepted && message.payload?.metadata?.group) {
+ const group = message.payload?.metadata?.group;
+ const groupInfo = GroupInfo_Reply.fromJSON({ group });
+
+ const hasAlreadyAccepted = conversations.find(
+ (item) => item.id === stringFromBytes(groupInfo.group?.publicKey),
+ );
+ if (hasAlreadyAccepted?.id) {
+ setIsAccepted(true);
+ }
+ }
+ }, [conversations, isAccepted, message.payload?.metadata?.group]);
+
+ const handleAcceptGroup = async () => {
+ try {
+ const group = message.payload?.metadata?.group;
+ const groupInfo = GroupInfo_Reply.fromJSON({ group });
+
+ await weshClient.client.MultiMemberGroupJoin({
+ group: groupInfo.group,
+ });
+
+ await weshClient.client.ActivateGroup({
+ groupPk: groupInfo.group?.publicKey,
+ });
+
+ await sendMessage({
+ groupPk: groupInfo.group?.publicKey,
+ message: {
+ type: "group-join",
+ payload: {
+ message: "",
+ files: [],
+ metadata: {
+ contact: {
+ id: stringFromBytes(weshConfig.config?.accountPk),
+ rdvSeed: stringFromBytes(weshConfig.metadata.rdvSeed),
+ tokenId: weshConfig.metadata.tokenId,
+ name: contactInfo.name,
+ avatar: contactInfo.avatar,
+ peerId: weshConfig.config?.peerId,
+ },
+ groupName: message?.payload?.metadata?.groupName,
+ },
+ },
+ },
+ });
+ } catch (err: any) {
+ setToastError({
+ title: "Failed to accept group",
+ message: err?.message,
+ });
+ }
+ };
+
+ if (isAccepted) {
+ return (
+
+ You have already accepted the group invitation
+
+ );
+ }
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+};
diff --git a/packages/screens/Message/components/JoinGroup.tsx b/packages/screens/Message/components/JoinGroup.tsx
new file mode 100644
index 0000000000..89b3a73831
--- /dev/null
+++ b/packages/screens/Message/components/JoinGroup.tsx
@@ -0,0 +1,114 @@
+import React, { useState } from "react";
+import { View } from "react-native";
+import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scrollview";
+import { useSelector } from "react-redux";
+
+import { BrandText } from "../../../components/BrandText";
+import { ErrorText } from "../../../components/ErrorText";
+import { PrimaryButton } from "../../../components/buttons/PrimaryButton";
+import { TextInputCustom } from "../../../components/inputs/TextInputCustom";
+import ModalBase from "../../../components/modals/ModalBase";
+import { SpacerColumn, SpacerRow } from "../../../components/spacer";
+import { useFeedbacks } from "../../../context/FeedbacksProvider";
+import { useIsMobile } from "../../../hooks/useIsMobile";
+import { selectContactInfo } from "../../../store/slices/message";
+import { neutral00, secondaryColor } from "../../../utils/style/colors";
+import { fontSemibold16 } from "../../../utils/style/fonts";
+import { layout } from "../../../utils/style/layout";
+import { weshServices } from "../../../weshnet";
+interface CreateConversationProps {
+ onClose: () => void;
+}
+
+export const JoinGroup = ({ onClose }: CreateConversationProps) => {
+ const contactInfo = useSelector(selectContactInfo);
+ const { setToastSuccess, setToastError } = useFeedbacks();
+ const [groupLink, setGroupLink] = useState("");
+ const [joinGroupLoading, setJoinGroupLoading] = useState(false);
+ const [error, setError] = useState("");
+ const isMobile = useIsMobile();
+
+ const handleJoinGroup = async (link = groupLink) => {
+ setJoinGroupLoading(true);
+ setError("");
+
+ try {
+ await weshServices.multiMemberGroupJoin(link, contactInfo);
+ setToastSuccess({
+ title: "Group joined!",
+ message: "Group joined successfully",
+ });
+ onClose();
+ } catch (err: any) {
+ setError(err?.message);
+ setToastError({
+ title: "Group join error",
+ message: err?.message,
+ });
+ }
+
+ setJoinGroupLoading(false);
+ };
+
+ return (
+
+
+
+
+ Join Group
+
+
+
+
+
+
+ {isMobile && }
+
+
+ {!!error && (
+ <>
+
+ {error}
+ >
+ )}
+
+
+
+
+ );
+};
diff --git a/packages/screens/Message/components/MessageAvatar.tsx b/packages/screens/Message/components/MessageAvatar.tsx
new file mode 100644
index 0000000000..592a10f1e0
--- /dev/null
+++ b/packages/screens/Message/components/MessageAvatar.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+import { View } from "react-native";
+import { Avatar, Badge } from "react-native-paper";
+import { useSelector } from "react-redux";
+
+import { selectPeerListById } from "../../../store/slices/message";
+import { Contact } from "../../../utils/types/message";
+
+type MessageAvatarProps = {
+ item: Contact;
+ size?: number;
+ disableStatus?: boolean;
+};
+
+export const MessageAvatar = ({
+ item,
+ size = 40,
+ disableStatus = false,
+}: MessageAvatarProps) => {
+ const peerStatus = useSelector(selectPeerListById(item?.peerId));
+ return (
+
+
+ {!disableStatus && (
+
+ )}
+
+ );
+};
diff --git a/packages/screens/Message/components/MessageBlankFiller.tsx b/packages/screens/Message/components/MessageBlankFiller.tsx
new file mode 100644
index 0000000000..97d501abfb
--- /dev/null
+++ b/packages/screens/Message/components/MessageBlankFiller.tsx
@@ -0,0 +1,21 @@
+import React from "react";
+import { View } from "react-native";
+
+import illustrationSVG from "../../../../assets/icons/illustration.svg";
+import { SVG } from "../../../components/SVG";
+import { neutral00 } from "../../../utils/style/colors";
+
+export const MessageBlankFiller = () => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/screens/Message/components/MessageCard.tsx b/packages/screens/Message/components/MessageCard.tsx
new file mode 100644
index 0000000000..d62c6b27d0
--- /dev/null
+++ b/packages/screens/Message/components/MessageCard.tsx
@@ -0,0 +1,47 @@
+import React, { FC } from "react";
+import { SvgProps } from "react-native-svg";
+
+import { BrandText } from "../../../components/BrandText";
+import FlexRow from "../../../components/FlexRow";
+import { SVG } from "../../../components/SVG";
+import { SpacerRow } from "../../../components/spacer";
+import {
+ neutral00,
+ neutral33,
+ neutral55,
+ secondaryColor,
+} from "../../../utils/style/colors";
+import { fontSemibold12, fontSemibold14 } from "../../../utils/style/fonts";
+import { layout } from "../../../utils/style/layout";
+interface CardProps {
+ icon: React.FC;
+ text: string;
+ subtext: string;
+}
+
+const MessageCard: FC = ({ icon, text, subtext }) => {
+ return (
+
+
+
+
+ {text}
+
+
+
+ {subtext}
+
+
+ );
+};
+
+export default MessageCard;
diff --git a/packages/screens/Message/components/MessageHeader.tsx b/packages/screens/Message/components/MessageHeader.tsx
new file mode 100644
index 0000000000..eb2d9b7f72
--- /dev/null
+++ b/packages/screens/Message/components/MessageHeader.tsx
@@ -0,0 +1,15 @@
+import React from "react";
+
+import { BrandText } from "../../../components/BrandText";
+import { secondaryColor } from "../../../utils/style/colors";
+import { fontSemibold20 } from "../../../utils/style/fonts";
+
+interface MessageHeaderProps {}
+
+export const MessageHeader: React.FC = () => {
+ return (
+
+ Messenger home
+
+ );
+};
diff --git a/packages/screens/Message/components/MessagePopup.tsx b/packages/screens/Message/components/MessagePopup.tsx
new file mode 100644
index 0000000000..57cd76b61b
--- /dev/null
+++ b/packages/screens/Message/components/MessagePopup.tsx
@@ -0,0 +1,81 @@
+import Clipboard from "@react-native-clipboard/clipboard";
+import React from "react";
+import { View, TouchableOpacity } from "react-native";
+
+import copy from "../../../../assets/icons/copy.svg";
+import reply from "../../../../assets/icons/reply.svg";
+import { BrandText } from "../../../components/BrandText";
+import FlexRow from "../../../components/FlexRow";
+import { SVG } from "../../../components/SVG";
+import { Separator } from "../../../components/separators/Separator";
+import { SpacerColumn, SpacerRow } from "../../../components/spacer";
+import { useFeedbacks } from "../../../context/FeedbacksProvider";
+import { neutralA3 } from "../../../utils/style/colors";
+import { fontSemibold13 } from "../../../utils/style/fonts";
+import { layout } from "../../../utils/style/layout";
+
+interface MessagePopupProps {
+ message: string;
+ onReply: () => void;
+ onClose: () => void;
+}
+
+export const MessagePopup = ({
+ onReply,
+ message,
+ onClose,
+}: MessagePopupProps) => {
+ const { setToastSuccess } = useFeedbacks();
+
+ return (
+
+ {
+ onReply();
+ onClose();
+ }}
+ >
+
+
+
+
+ Reply
+
+
+
+
+
+
+
+
+
+ {
+ Clipboard.setString(message);
+ setToastSuccess({
+ title: "Copied",
+ message: "",
+ });
+ onClose();
+ }}
+ >
+
+
+
+
+ Copy text
+
+
+
+
+
+ );
+};
diff --git a/packages/screens/Message/components/MessageTypeLineMessage.tsx b/packages/screens/Message/components/MessageTypeLineMessage.tsx
new file mode 100644
index 0000000000..e9b059ff43
--- /dev/null
+++ b/packages/screens/Message/components/MessageTypeLineMessage.tsx
@@ -0,0 +1,19 @@
+import React from "react";
+import { View } from "react-native";
+
+import { BrandText } from "../../../components/BrandText";
+import { neutralA3 } from "../../../utils/style/colors";
+import { fontSemibold10 } from "../../../utils/style/fonts";
+import { Message } from "../../../utils/types/message";
+
+export const MessageTypeLineMessage = ({ message }: { message: Message }) => {
+ if (message.type === "accept-contact") {
+ return (
+
+
+ Contact accepted
+
+
+ );
+ }
+};
diff --git a/packages/screens/Message/components/Request.tsx b/packages/screens/Message/components/Request.tsx
new file mode 100644
index 0000000000..784e5ad9fa
--- /dev/null
+++ b/packages/screens/Message/components/Request.tsx
@@ -0,0 +1,119 @@
+import React, { useState } from "react";
+import { View } from "react-native";
+import { Avatar, Badge } from "react-native-paper";
+
+import { BrandText } from "../../../components/BrandText";
+import FlexRow from "../../../components/FlexRow";
+import { PrimaryButton } from "../../../components/buttons/PrimaryButton";
+import { SecondaryButton } from "../../../components/buttons/SecondaryButton";
+import { Separator } from "../../../components/separators/Separator";
+import { SpacerColumn, SpacerRow } from "../../../components/spacer";
+import { useFeedbacks } from "../../../context/FeedbacksProvider";
+import { neutral22, secondaryColor } from "../../../utils/style/colors";
+import { fontSemibold13 } from "../../../utils/style/fonts";
+import { ContactRequest } from "../../../utils/types/message";
+import { weshClient } from "../../../weshnet/client";
+import {
+ acceptFriendRequest,
+ activateGroup,
+ sendMessage,
+} from "../../../weshnet/services";
+import { bytesFromString } from "../../../weshnet/utils";
+type Props = {
+ name: string;
+ isOnline: boolean;
+ avatar: any;
+ data: ContactRequest;
+};
+
+const RequestList = ({ isOnline, data }: Props) => {
+ const { setToastError } = useFeedbacks();
+ const [addLoading, setAddLoading] = useState(false);
+
+ const onlineStatusBadgeColor = isOnline ? "green" : "yellow";
+
+ const handleAddFriend = async () => {
+ setAddLoading(true);
+ try {
+ const contactPk = bytesFromString(data?.contactId);
+ await acceptFriendRequest(contactPk);
+ const groupInfo = await activateGroup({ contactPk });
+ await sendMessage({
+ groupPk: groupInfo?.group?.publicKey,
+ message: {
+ type: "accept-contact",
+ },
+ });
+ } catch (err) {
+ console.error("add friend err", err);
+ setToastError({
+ title: "Failed",
+ message: "Failed to accept contact. Please try again later.",
+ });
+ }
+ setAddLoading(false);
+ };
+
+ const handleCancelFriend = async () => {
+ try {
+ await weshClient.client.ContactRequestDiscard({
+ contactPk: bytesFromString(data?.contactId),
+ });
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ } catch (e) {
+ setToastError({
+ title: "Failed",
+ message: "Failed to reject contact. Please try again later.",
+ });
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {data?.name || "Anon"}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default RequestList;
diff --git a/packages/screens/Message/components/Requests.tsx b/packages/screens/Message/components/Requests.tsx
new file mode 100644
index 0000000000..5bf0e97c7b
--- /dev/null
+++ b/packages/screens/Message/components/Requests.tsx
@@ -0,0 +1,49 @@
+import React, { useState } from "react";
+import { ScrollView, View } from "react-native";
+import { useSelector } from "react-redux";
+
+import { MessageBlankFiller } from "./MessageBlankFiller";
+import RequestList from "./Request";
+import { TextInputCustomBorder } from "../../../components/inputs/TextInputCustomBorder";
+import { SpacerColumn } from "../../../components/spacer";
+import { selectContactRequestList } from "../../../store/slices/message";
+import { ContactRequest } from "../../../utils/types/message";
+
+interface RequestProps {
+ items: ContactRequest[];
+}
+
+export const Requests = ({ items }: RequestProps) => {
+ const [searchQuery, setSearchQuery] = useState("");
+ const contactRequestList = useSelector(selectContactRequestList);
+
+ return (
+
+
+
+
+
+ {items?.length > 0 ? (
+ contactRequestList?.map((item) => (
+
+
+
+
+
+ ))
+ ) : (
+
+ )}
+
+ );
+};
diff --git a/packages/screens/Message/components/SearchConversation.tsx b/packages/screens/Message/components/SearchConversation.tsx
new file mode 100644
index 0000000000..fe4ed4221b
--- /dev/null
+++ b/packages/screens/Message/components/SearchConversation.tsx
@@ -0,0 +1,142 @@
+import moment from "moment";
+import React from "react";
+import { View, TouchableOpacity, Image } from "react-native";
+import { Avatar } from "react-native-paper";
+
+import { FileRenderer } from "./FileRenderer";
+import { BrandText } from "../../../components/BrandText";
+import FlexCol from "../../../components/FlexCol";
+import FlexRow from "../../../components/FlexRow";
+import {
+ neutral77,
+ secondaryColor,
+ neutralA3,
+ neutral30,
+} from "../../../utils/style/colors";
+import {
+ fontBold9,
+ fontMedium10,
+ fontSemibold11,
+} from "../../../utils/style/fonts";
+import { layout } from "../../../utils/style/layout";
+import { Conversation, Message } from "../../../utils/types/message";
+import { getConversationAvatar } from "../../../weshnet/messageHelpers";
+
+interface SearchConversationProps {
+ conversation: Conversation;
+ message: Message;
+ groupPk?: Uint8Array;
+ parentMessage?: Message;
+ onPress: () => void;
+}
+
+export const SearchConversation = ({
+ conversation,
+ message,
+ parentMessage,
+ onPress,
+}: SearchConversationProps) => {
+ const receiverName = "Anon";
+
+ return (
+
+
+
+
+
+
+
+
+
+ {receiverName}
+
+
+ {moment(message.timestamp).local().format("MM/DD/YYYY hh:mm a")}
+
+
+
+
+ {!!parentMessage?.id && (
+
+
+
+ {parentMessage?.payload?.message}
+
+
+ )}
+
+ {["message", "group-invite"].includes(message.type) ? (
+ <>
+
+ {message?.payload?.message}
+
+ {!!message?.payload?.files?.length && (
+
+ )}
+ >
+ ) : (
+ <>>
+ )}
+
+ {message?.payload?.files?.[0]?.type === "image" && (
+
+ )}
+
+
+
+
+ );
+};
diff --git a/packages/screens/Message/components/SearchInput.tsx b/packages/screens/Message/components/SearchInput.tsx
new file mode 100644
index 0000000000..f83c6ca459
--- /dev/null
+++ b/packages/screens/Message/components/SearchInput.tsx
@@ -0,0 +1,57 @@
+import React from "react";
+import { TextInput, TouchableOpacity, View } from "react-native";
+
+import closeSVG from "../../../../assets/icons/close.svg";
+import searchSVG from "../../../../assets/icons/search.svg";
+import { SVG } from "../../../components/SVG";
+import { SpacerRow } from "../../../components/spacer";
+import { secondaryColor, neutral33 } from "../../../utils/style/colors";
+import { fontMedium14 } from "../../../utils/style/fonts";
+import { layout } from "../../../utils/style/layout";
+
+interface SearchInputProps {
+ onClose: () => void;
+ value: string;
+ setValue: (val: string) => void;
+}
+
+export const SearchInput = ({ onClose, value, setValue }: SearchInputProps) => {
+ return (
+
+ <>
+
+
+
+
+ >
+
+
+
+
+ );
+};
diff --git a/packages/screens/Message/components/SideBarChats.tsx b/packages/screens/Message/components/SideBarChats.tsx
new file mode 100644
index 0000000000..7f09f4f2d0
--- /dev/null
+++ b/packages/screens/Message/components/SideBarChats.tsx
@@ -0,0 +1,165 @@
+import React, { useEffect, useMemo, useState } from "react";
+import {
+ Platform,
+ ScrollView,
+ TouchableOpacity,
+ View,
+ useWindowDimensions,
+} from "react-native";
+import { useSelector } from "react-redux";
+
+import { ChatItem } from "./ChatItem";
+import { ConversationSelector } from "./ConversationSelector";
+import { FriendsBar } from "./FriendsBar";
+import { SearchInput } from "./SearchInput";
+import addSVG from "../../../../assets/icons/add-circle-filled.svg";
+import searchSVG from "../../../../assets/icons/search.svg";
+import { BrandText } from "../../../components/BrandText";
+import FlexRow from "../../../components/FlexRow";
+import { SVG } from "../../../components/SVG";
+import { Separator } from "../../../components/separators/Separator";
+import { SpacerColumn, SpacerRow } from "../../../components/spacer";
+import { useMessage } from "../../../context/MessageProvider";
+import { selectConversationList } from "../../../store/slices/message";
+import { setSearchText } from "../../../store/slices/search";
+import { useAppNavigation } from "../../../utils/navigation";
+import {
+ primaryColor,
+ secondaryColor,
+ neutral22,
+ neutral77,
+} from "../../../utils/style/colors";
+import { fontSemibold14 } from "../../../utils/style/fonts";
+import { layout } from "../../../utils/style/layout";
+import { getConversationName } from "../../../weshnet/messageHelpers";
+
+export const SideBarChats = () => {
+ const { activeConversationType, activeConversation, setActiveConversation } =
+ useMessage();
+ const conversationList = useSelector(
+ selectConversationList(activeConversationType),
+ );
+
+ const { navigate } = useAppNavigation();
+ const { width: windowWidth } = useWindowDimensions();
+
+ const [isSearch, setIsSearch] = useState(false);
+ const [searchInput, setSearchInput] = useState("");
+
+ useEffect(() => {
+ if (
+ (!activeConversation && conversationList.length) ||
+ !conversationList.find((conv) => conv.id === activeConversation?.id)
+ ) {
+ setActiveConversation?.(conversationList[0]);
+ }
+ }, [activeConversation, conversationList, setActiveConversation]);
+
+ const searchResults = useMemo(() => {
+ if (!searchInput) {
+ return conversationList;
+ }
+ return conversationList.filter((item) =>
+ getConversationName(item)
+ .toLowerCase()
+ .includes(searchInput?.toLowerCase()),
+ );
+ }, [conversationList, searchInput]);
+
+ return (
+
+ <>
+
+
+
+
+ {isSearch ? (
+ {
+ setIsSearch(false);
+ setSearchText("");
+ }}
+ />
+ ) : (
+ <>
+
+
+
+
+
+
+
+
+ navigate("Message", { view: "CreateConversation" })
+ }
+ >
+
+
+
+ setIsSearch(true)}
+ >
+
+
+
+
+ >
+ )}
+
+
+
+
+ >
+
+
+ {searchResults.map((item, index) => (
+ {
+ if (Platform.OS === "web") {
+ setActiveConversation?.(item);
+ navigate("Message");
+ } else {
+ navigate("ChatSection", item);
+ }
+ }}
+ isLastItem={index === conversationList.length - 1}
+ />
+ ))}
+
+
+ {!!searchInput && !searchResults.length && (
+
+ No records found
+
+ )}
+
+
+ );
+};
diff --git a/packages/store/slices/message.ts b/packages/store/slices/message.ts
index c014729279..9076e68848 100644
--- a/packages/store/slices/message.ts
+++ b/packages/store/slices/message.ts
@@ -59,7 +59,7 @@ export const selectMessageList = (groupPk: string) => (state: RootState) =>
export const selectPeerList = (state: RootState) => state.message.peerList;
-export const selectPeerListById = (id: string) => (state: RootState) =>
+export const selectPeerListById = (id?: string) => (state: RootState) =>
state.message.peerList.find((item) => item.id === id);
export const selectLastIdByKey = (key: string) => (state: RootState) =>
diff --git a/packages/utils/navigation.ts b/packages/utils/navigation.ts
index 185ed388d5..5679b3e4e7 100644
--- a/packages/utils/navigation.ts
+++ b/packages/utils/navigation.ts
@@ -2,6 +2,7 @@ import { RouteProp, useNavigation, useRoute } from "@react-navigation/native";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import React from "react";
+import { Conversation, MessageFriendsTabItem } from "./types/message";
import { NewPostFormValues } from "../components/socialFeed/NewsFeed/NewsFeed.type";
export type RouteName = keyof RootStackParamList;
@@ -65,6 +66,10 @@ export type RootStackParamList = {
DAppStore: undefined;
ToriPunks: { route: string };
+
+ Message: { view: string; tab?: string } | undefined;
+ ChatSection: Conversation;
+ FriendshipManager: { tab?: MessageFriendsTabItem } | undefined;
};
export type AppNavigationProp = NativeStackNavigationProp;
@@ -149,6 +154,11 @@ const navConfig: {
DAppStore: "dapp-store",
// === DApps
ToriPunks: "dapp/tori-punks/:route?",
+
+ // ==== Message
+ Message: "message/:view?",
+ ChatSection: "message/chat",
+ FriendshipManager: "/friends",
},
};
diff --git a/yarn.lock b/yarn.lock
index 6116edc002..c5deef995a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9355,6 +9355,13 @@ __metadata:
languageName: node
linkType: hard
+"dijkstrajs@npm:^1.0.1":
+ version: 1.0.3
+ resolution: "dijkstrajs@npm:1.0.3"
+ checksum: 82ff2c6633f235dd5e6bed04ec62cdfb1f327b4d7534557bd52f18991313f864ee50654543072fff4384a92b643ada4d5452f006b7098dbdfad6c8744a8c9e08
+ languageName: node
+ linkType: hard
+
"dir-glob@npm:^3.0.1":
version: 3.0.1
resolution: "dir-glob@npm:3.0.1"
@@ -9656,6 +9663,13 @@ __metadata:
languageName: node
linkType: hard
+"encode-utf8@npm:^1.0.3":
+ version: 1.0.3
+ resolution: "encode-utf8@npm:1.0.3"
+ checksum: 550224bf2a104b1d355458c8a82e9b4ea07f9fc78387bc3a49c151b940ad26473de8dc9e121eefc4e84561cb0b46de1e4cd2bc766f72ee145e9ea9541482817f
+ languageName: node
+ linkType: hard
+
"encodeurl@npm:^1.0.2, encodeurl@npm:~1.0.2":
version: 1.0.2
resolution: "encodeurl@npm:1.0.2"
@@ -10494,6 +10508,17 @@ __metadata:
languageName: node
linkType: hard
+"expo-barcode-scanner@npm:~12.5.3":
+ version: 12.5.3
+ resolution: "expo-barcode-scanner@npm:12.5.3"
+ dependencies:
+ expo-image-loader: ~4.3.0
+ peerDependencies:
+ expo: "*"
+ checksum: 10923168c29620c64935f9b171c37e2a22d86bedc7dd263460683c2e27c9620c857b3f852194f58cc7109a68380174a4c0faa502e715445aacec95f622d38e54
+ languageName: node
+ linkType: hard
+
"expo-constants@npm:~14.4.2":
version: 14.4.2
resolution: "expo-constants@npm:14.4.2"
@@ -10586,6 +10611,15 @@ __metadata:
languageName: node
linkType: hard
+"expo-image-loader@npm:~4.3.0":
+ version: 4.3.0
+ resolution: "expo-image-loader@npm:4.3.0"
+ peerDependencies:
+ expo: "*"
+ checksum: 0440ac80a7b2331cdfd0db88aac0ba2fe61689e6347e2c6bedba6bf3220b95410c724ac9004252486870b582fc36d47d4bff008bba7052dcee2830256950974f
+ languageName: node
+ linkType: hard
+
"expo-json-utils@npm:~0.7.0":
version: 0.7.1
resolution: "expo-json-utils@npm:0.7.1"
@@ -16072,6 +16106,13 @@ __metadata:
languageName: node
linkType: hard
+"pngjs@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "pngjs@npm:5.0.0"
+ checksum: 04e912cc45fb9601564e2284efaf0c5d20d131d9b596244f8a6789fc6cdb6b18d2975a6bbf7a001858d7e159d5c5c5dd7b11592e97629b7137f7f5cef05904c8
+ languageName: node
+ linkType: hard
+
"postcss-calc@npm:^8.2.3":
version: 8.2.4
resolution: "postcss-calc@npm:8.2.4"
@@ -16617,7 +16658,7 @@ __metadata:
languageName: node
linkType: hard
-"prop-types@npm:*, prop-types@npm:^15.5.6, prop-types@npm:^15.5.8, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
+"prop-types@npm:*, prop-types@npm:^15.5.6, prop-types@npm:^15.5.8, prop-types@npm:^15.7.2, prop-types@npm:^15.8.0, prop-types@npm:^15.8.1":
version: 15.8.1
resolution: "prop-types@npm:15.8.1"
dependencies:
@@ -16791,6 +16832,20 @@ __metadata:
languageName: node
linkType: hard
+"qrcode@npm:^1.5.1":
+ version: 1.5.3
+ resolution: "qrcode@npm:1.5.3"
+ dependencies:
+ dijkstrajs: ^1.0.1
+ encode-utf8: ^1.0.3
+ pngjs: ^5.0.0
+ yargs: ^15.3.1
+ bin:
+ qrcode: bin/qrcode
+ checksum: 9a8a20a0a9cb1d15de8e7b3ffa214e8b6d2a8b07655f25bd1b1d77f4681488f84d7bae569870c0652872d829d5f8ac4922c27a6bd14c13f0e197bf07b28dead7
+ languageName: node
+ linkType: hard
+
"qs@npm:6.11.0":
version: 6.11.0
resolution: "qs@npm:6.11.0"
@@ -17045,6 +17100,16 @@ __metadata:
languageName: node
linkType: hard
+"react-native-keyboard-aware-scrollview@npm:^2.1.0":
+ version: 2.1.0
+ resolution: "react-native-keyboard-aware-scrollview@npm:2.1.0"
+ peerDependencies:
+ react: ">=0.14.5"
+ react-native: ">=0.25.1"
+ checksum: 070c6b15154608228214df4b9511d316181da38b95061c00752caa0890d428680bd00f25586467006e96673de96d4900db6e94adfcc3df17973eed3cb385589e
+ languageName: node
+ linkType: hard
+
"react-native-paper@npm:^4.12.5":
version: 4.12.5
resolution: "react-native-paper@npm:4.12.5"
@@ -17090,6 +17155,20 @@ __metadata:
languageName: node
linkType: hard
+"react-native-qrcode-svg@npm:^6.2.0":
+ version: 6.2.0
+ resolution: "react-native-qrcode-svg@npm:6.2.0"
+ dependencies:
+ prop-types: ^15.8.0
+ qrcode: ^1.5.1
+ peerDependencies:
+ react: "*"
+ react-native: ">=0.63.4"
+ react-native-svg: ^13.2.0
+ checksum: e4e8a709900e16cf0116e9c5edd8ff7150cfbbb2c9763a5a7b171ba6efb1f84d8b93b919dedb915d16686d7a8b0c49c905c62dcbd86f98d471b1c0bcec6f1ae3
+ languageName: node
+ linkType: hard
+
"react-native-reanimated-carousel@npm:^3.0.3":
version: 3.5.1
resolution: "react-native-reanimated-carousel@npm:3.5.1"
@@ -19317,6 +19396,7 @@ __metadata:
ethers: ^5.7.2
expo: ^49.0.16
expo-av: ~13.4.1
+ expo-barcode-scanner: ~12.5.3
expo-dev-client: ~2.4.12
expo-doctor: ^1.1.3
expo-font: ~11.4.0
@@ -19348,10 +19428,12 @@ __metadata:
react-native-gesture-handler: ~2.12.0
react-native-heroicons: ^3.2.0
react-native-hoverable: ^0.2.0
+ react-native-keyboard-aware-scrollview: ^2.1.0
react-native-paper: ^4.12.5
react-native-pell-rich-editor: ^1.8.8
react-native-pie-chart: ^3.0.1
react-native-popup-menu: ^0.16.1
+ react-native-qrcode-svg: ^6.2.0
react-native-reanimated: ~3.3.0
react-native-reanimated-carousel: ^3.0.3
react-native-redash: ^18.0.0
@@ -21379,7 +21461,7 @@ __metadata:
languageName: node
linkType: hard
-"yargs@npm:^15.1.0":
+"yargs@npm:^15.1.0, yargs@npm:^15.3.1":
version: 15.4.1
resolution: "yargs@npm:15.4.1"
dependencies: