From 58a5827d2c2bdf403ab20b40792d3672d00a06ae Mon Sep 17 00:00:00 2001 From: Sakul Budhathoki Date: Tue, 21 Nov 2023 15:22:26 +0545 Subject: [PATCH] feat(messenger): message ui components --- App.tsx | 11 +- assets/icons/Addplus.svg | 5 + assets/icons/add-chat.svg | 44 ++ assets/icons/chaticon.svg | 5 + assets/icons/chatplus.svg | 4 + assets/icons/dots.svg | 5 + assets/icons/doublecheck.svg | 5 + assets/icons/forward-to.svg | 3 + assets/icons/forward.svg | 3 + assets/icons/friend.svg | 42 ++ assets/icons/friends.svg | 3 + assets/icons/group.svg | 44 ++ assets/icons/illustration.svg | 285 +++++++++ assets/icons/sent.svg | 4 + assets/icons/space.svg | 47 ++ assets/logos/logo-hexagon.png | Bin 0 -> 3028 bytes declarations.d.ts | 1 + package.json | 3 + packages/components/CopyToClipboard.tsx | 6 +- packages/components/Dropdown.tsx | 94 +++ packages/components/KeyboardAvoidingView.tsx | 32 + packages/components/modals/ModalBase.tsx | 45 +- .../components/modals/QRCodeScannerModal.tsx | 65 ++ packages/components/navigation/Navigator.tsx | 24 + packages/context/DropdownsProvider.tsx | 2 +- packages/context/MessageProvider.tsx | 45 ++ packages/screens/Message/MessageScreen.tsx | 212 +++++++ .../screens/Message/components/ChatHeader.tsx | 236 ++++++++ .../screens/Message/components/ChatItem.tsx | 135 +++++ .../Message/components/ChatSection.tsx | 558 ++++++++++++++++++ .../Message/components/CheckboxGroup.tsx | 103 ++++ .../Message/components/Conversation.tsx | 303 ++++++++++ .../Message/components/ConversationAvatar.tsx | 37 ++ .../components/ConversationSelector.tsx | 99 ++++ .../Message/components/CreateConversation.tsx | 245 ++++++++ .../Message/components/CreateGroup.tsx | 180 ++++++ .../Message/components/FileRenderer.tsx | 42 ++ .../screens/Message/components/Friends.tsx | 55 ++ .../screens/Message/components/FriendsBar.tsx | 94 +++ .../Message/components/FriendsList.tsx | 60 ++ .../Message/components/FriendshipManager.tsx | 111 ++++ .../components/GroupInvitationAction.tsx | 123 ++++ .../screens/Message/components/JoinGroup.tsx | 114 ++++ .../Message/components/MessageAvatar.tsx | 37 ++ .../Message/components/MessageBlankFiller.tsx | 21 + .../Message/components/MessageCard.tsx | 47 ++ .../Message/components/MessageHeader.tsx | 15 + .../Message/components/MessagePopup.tsx | 81 +++ .../components/MessageTypeLineMessage.tsx | 19 + .../screens/Message/components/Request.tsx | 119 ++++ .../screens/Message/components/Requests.tsx | 49 ++ .../Message/components/SearchConversation.tsx | 142 +++++ .../Message/components/SearchInput.tsx | 57 ++ .../Message/components/SideBarChats.tsx | 165 ++++++ packages/store/slices/message.ts | 2 +- packages/utils/navigation.ts | 10 + yarn.lock | 86 ++- 57 files changed, 4367 insertions(+), 17 deletions(-) create mode 100644 assets/icons/Addplus.svg create mode 100644 assets/icons/add-chat.svg create mode 100644 assets/icons/chaticon.svg create mode 100644 assets/icons/chatplus.svg create mode 100644 assets/icons/dots.svg create mode 100644 assets/icons/doublecheck.svg create mode 100644 assets/icons/forward-to.svg create mode 100644 assets/icons/forward.svg create mode 100644 assets/icons/friend.svg create mode 100644 assets/icons/friends.svg create mode 100644 assets/icons/group.svg create mode 100644 assets/icons/illustration.svg create mode 100644 assets/icons/sent.svg create mode 100644 assets/icons/space.svg create mode 100644 assets/logos/logo-hexagon.png create mode 100644 packages/components/Dropdown.tsx create mode 100644 packages/components/KeyboardAvoidingView.tsx create mode 100644 packages/components/modals/QRCodeScannerModal.tsx create mode 100644 packages/context/MessageProvider.tsx create mode 100644 packages/screens/Message/MessageScreen.tsx create mode 100644 packages/screens/Message/components/ChatHeader.tsx create mode 100644 packages/screens/Message/components/ChatItem.tsx create mode 100644 packages/screens/Message/components/ChatSection.tsx create mode 100644 packages/screens/Message/components/CheckboxGroup.tsx create mode 100644 packages/screens/Message/components/Conversation.tsx create mode 100644 packages/screens/Message/components/ConversationAvatar.tsx create mode 100644 packages/screens/Message/components/ConversationSelector.tsx create mode 100644 packages/screens/Message/components/CreateConversation.tsx create mode 100644 packages/screens/Message/components/CreateGroup.tsx create mode 100644 packages/screens/Message/components/FileRenderer.tsx create mode 100644 packages/screens/Message/components/Friends.tsx create mode 100644 packages/screens/Message/components/FriendsBar.tsx create mode 100644 packages/screens/Message/components/FriendsList.tsx create mode 100644 packages/screens/Message/components/FriendshipManager.tsx create mode 100644 packages/screens/Message/components/GroupInvitationAction.tsx create mode 100644 packages/screens/Message/components/JoinGroup.tsx create mode 100644 packages/screens/Message/components/MessageAvatar.tsx create mode 100644 packages/screens/Message/components/MessageBlankFiller.tsx create mode 100644 packages/screens/Message/components/MessageCard.tsx create mode 100644 packages/screens/Message/components/MessageHeader.tsx create mode 100644 packages/screens/Message/components/MessagePopup.tsx create mode 100644 packages/screens/Message/components/MessageTypeLineMessage.tsx create mode 100644 packages/screens/Message/components/Request.tsx create mode 100644 packages/screens/Message/components/Requests.tsx create mode 100644 packages/screens/Message/components/SearchConversation.tsx create mode 100644 packages/screens/Message/components/SearchInput.tsx create mode 100644 packages/screens/Message/components/SideBarChats.tsx 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 0000000000000000000000000000000000000000..415d69f10da3dc29bbc601419ce5370ba4f9e458 GIT binary patch literal 3028 zcmV;_3oG=AP)Lpdz4=$4 z_-TvxXO8!UZuV%6_cp`%F~|8^oA`Bp_C&P#XpQ$M(fVkO_eZbzNv-%P(E5UK^wst@vG;_dC1!hHdpI(E2FS`XrhtYEHdfT)-;K`g(fxcYF0_jrJ4H@|8&78llK=gY_kq$6=KBBhmXmx%q=@@E+Ft zC$#BuN4`;p;u_5OO{(}U#`=s;-+f==l0(^tNYOUJ`A(_$FN?-fq4#5Z=TMRCay+pv z%=#tN`VY(NjYQNErNt6>lpmSIOk}t=sq8PN;X0k;6u#j|cF13K+gEqjNQc--irs2- z%f|0_Z^7aKKJaA=sX5T&L5L1q7!-0NJ!|F!V) zqT}>1#ra0U+ef9X1_%!j4H*wZUm-s{N+fVuEr<|!lWLm$oJr{$jlT_xq-Qdm6WINO z?btF(z@@VNt@q$~=-qv5|3_EoaCV$-dG97aYByV$!qfdUB3LFY zG$~q&z2e_dMxAW4r<9PuPnpeDt*DmB(8KobZR6n}z5ju)zLcuiW3rJd>8U-yF6(uqW4MIQ`I43GS97;SP5g$2RIW!#@3nLpsMXHIlyprb%FNn-o^piH1jW>j-R`K05BRC9#)=a)8kpoaVX{X1IY z&H3o|{PUpM{QTIvpM{y3{g3V-A#)$pIdHenO{KwGl{;wGs$p8QV32;bRa{vGf z5&!@T5&_cPe*6Fc2i-|TK~z|Uwb*xbTh$#0@SpVNm7b)hz4w-^XGua%bK zzzC8fbGHZvBXGN2h6h{JC33E3oxW5G3?p|q_&gyqH-mB*&1UnwsHWMe1)9T%EG<

C;+H+14k~eRF~D&xx4%Cxu?I~-PF*d z>si^tFhK%J3zkAL6cWg!8mFS#Syg3pd+f`*?mfGC^R?ezx}4+DhfU5^7E5DLXed;Q zfM5yQW?WU(@$KPvk@7D_SWmsEPErJz{J_*SqnIyu0*43;{ASbplIC*xUp^6@wCBmjzy-HJ+F1i+_6c|K4b2Aqwx zl-0nJuCp&@6N$X|;p%1pyUd5wGV{V; z*4&{Ac(Hz=fzCj96djRutgO*wyw*!T(=7{3eY7yxC{EvW9D)?o1gfNM%g$b&NaXDW zMqiPzyU~({u#~`SRM zw*IcncD!?Pc-OAGuBlf^D}f&j&TLx_W~MM+`G(oovSY_kJihmd zM<0IV$MJZ4(+__5qn~^-w0rmNq3H<&-wo51cnpLV-V{R0i!of@d}^kU9s6J1zyHFv z1Twzq)5#-8jvPLG__K=_FFrL>%6O~TEouXb5l+`4EJLibcD5rsUfsF#wF4It$m@rG z``mMhN`CXo%g;PBgT(JH(?^94fD&@LFQPI~IF}sHLiQFy-e0vMN><;^3MeFpOMkgO$R3@C;vNg?ATLVXQm8XV+%m73}PUf+OK4_OH!JKr1GzS zq&IdXb8z4q=!2NUDkXSR`<1M0ASF4-;K{;BK+8JH#i{c^fM}^xE%Rj{zrL{l-+Q+0 z-TU{C$415uCXf$4`0P)a$O@kTt5l|*hJ}b~kc5&))Jk>cEwNA=DhK@^XJc>93Fms{PgM5$x1%`kk*lGY zDyb$bQil;DLV9`3rOZH*d1rXnu8*gB^7E{*Bx3D`MwEOvEYV5|7=Bs^+5#zZBy4CW|3HJuw7XYIhKof0?J03Y@a?)G;Sl;f92!Z$Q#46$Ve_? z_DO4%rS|EQjjV2>4Y|nRWe`7KB(Y9^VQ@fRRU|2lWRE?2l|JluwCiTRxDbizbkbZT zzO4{qj+AODRMUt6aFyF(z4Olnkms|9PAr@!t56GVGv_oEdA+>0jxQHLvNvpeW3R{B z3^ChmC0xBqAn17Wh3uJoW{~U+yYHOY6xBzxq2Ic>u$~M>@@QHmh6^1WDHnr zHg@*fCBU^|g0?oV&MD>SBLnex{C7FVzRtx>WVZ?gX?HgqGrB?MzAd*a$>iR6Ha<{K zDGUJo^t%!z=rJl?(gknlMRIax{bHZZQUlzqyC`0wc3G_pZh89h#FqQl_qw#mQ6$x5 zUD`-4p=+qI-u~D3w-zS$oj7+_uSo*J^6pZ|4gp1F8mq+j^)+u5G9K%ENN?5}WaW(# zD#ria`lN5%OofntQ?5}gceH;xbFv}WJ4=kBtBc5$CK60v!^X~=^hoJL_ALE~QYy5w}=oB_VLb^=Yjcz5o03l*Q|c)jqAt zgaAZjegqJQ5R|Ij5?5KINb0BItMgAxZTBr_Iv3j3wwnBEDL{=3K+dD`8>!GNmsO}^ zC5j%k!F}WW`PW}}-Nu_cALy;G?`vvus$DK!wXRLWD?}uoiliyMmV-#C#43{o7B5`b z+o~ux6w$PwMHEGOWQEtKLI4F}F-}sXKUxFgh+Uzu`c0;&OQKaNRUz%1e(;lApp?Z* zgg|LBmKiV(Nl`Rtt%z2syeb;a#Q-2kpu~E8)Mb$3DC7~7Btky_R2Qf@{^x-va1!AJ zLE?!8D!o1w^09~{>Ca&#o(Q4jA4aL#l_WR0K9M;aX&O!eP*c_y58~pfs{XG5fd2vT Wbn;9%G>LWq0000 { 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: