diff --git a/.eslintrc b/.eslintrc index 8eaa768f..ff52b25e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,6 +10,7 @@ "extends": [ "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", + "plugin:react-hooks/recommended", "prettier" ], "rules": { diff --git a/package.json b/package.json index 72c0c38b..3a83a857 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "eslint": "8.22.0", "eslint-config-prettier": "8.5.0", "eslint-plugin-prettier": "4.2.1", + "eslint-plugin-react-hooks": "4.6.0", "husky": "8.0.1", "lint-staged": "13.0.3", "npm-run-all": "4.1.5", diff --git a/samples/react-native-group-chat/App.tsx b/samples/react-native-group-chat/App.tsx index a8c556a8..c4fc6dcb 100644 --- a/samples/react-native-group-chat/App.tsx +++ b/samples/react-native-group-chat/App.tsx @@ -1,15 +1,21 @@ import { useContext, useEffect, useState } from "react" -import { View, StyleSheet, ActivityIndicator, KeyboardAvoidingView, Platform } from "react-native" +import { + View, + StyleSheet, + ActivityIndicator, + KeyboardAvoidingView, + Platform, + LogBox, +} from "react-native" import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context" import { GestureHandlerRootView } from "react-native-gesture-handler" import { BottomSheetModalProvider } from "@gorhom/bottom-sheet" import { NavigationContainer } from "@react-navigation/native" import { createBottomTabNavigator } from "@react-navigation/bottom-tabs" import { createStackNavigator, StackScreenProps } from "@react-navigation/stack" -import { PaperProvider } from "react-native-paper" import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons" import { StatusBar } from "expo-status-bar" -import { Chat, Membership, User } from "@pubnub/chat" +import { Channel, Chat, Membership, User } from "@pubnub/chat" import { useFonts, Roboto_400Regular, @@ -24,6 +30,8 @@ import { ChatContext } from "./context" import { RootStackParamList, BottomTabsParamList } from "./types" import { defaultTheme, colorPalette as colors } from "./ui-components" +LogBox.ignoreLogs(["Require cycle:", "Sending"]) + const Tab = createBottomTabNavigator() const MainStack = createStackNavigator() @@ -34,8 +42,10 @@ function TabNavigator({ route }: StackScreenProps) { useEffect(() => { async function init() { const chat = await Chat.init({ - publishKey: process.env.EXPO_PUBLIC_PUBNUB_PUB_KEY || "pub-c-0457cb83-0786-43df-bc70-723b16a6e816", - subscribeKey: process.env.EXPO_PUBLIC_PUBNUB_SUB_KEY || "sub-c-e654122d-85b5-49a6-a3dd-8ebc93c882de", + publishKey: + process.env.EXPO_PUBLIC_PUBNUB_PUB_KEY || "pub-c-0457cb83-0786-43df-bc70-723b16a6e816", + subscribeKey: + process.env.EXPO_PUBLIC_PUBNUB_SUB_KEY || "sub-c-e654122d-85b5-49a6-a3dd-8ebc93c882de", userId: name || "test-user", typingTimeout: 2000, storeUserActivityTimestamps: true, @@ -45,7 +55,7 @@ function TabNavigator({ route }: StackScreenProps) { } init() - }, [name]) + }, [name, setChat]) if (!chat) { return ( @@ -119,8 +129,28 @@ function TabNavigator({ route }: StackScreenProps) { function App() { const [chat, setChat] = useState(null) const [users, setUsers] = useState([]) + const [loading, setLoading] = useState(false) + const [currentChannel, setCurrentChannel] = useState() + const [currentChannelMembers, setCurrentChannelMembers] = useState([]) const [userMemberships, setUserMemberships] = useState([]) + async function setCurrentChannelWithMembers(channel: Channel) { + const { members } = await channel.getMembers() + setCurrentChannelMembers(members) + setCurrentChannel(channel) + } + + function getUser(userId: string) { + const existingUser = users.find((u) => u.id === userId) + if (!existingUser) { + chat?.getUser(userId).then((fetchedUser) => { + if (fetchedUser) setUsers((users) => [...users, fetchedUser]) + }) + return null + } + return existingUser + } + const [fontsLoaded] = useFonts({ Roboto_400Regular, Roboto_500Medium, @@ -134,38 +164,42 @@ function App() { return ( - - - - + + + {/* TODO: for some reason KeyboardAvoidingView doesn't work on any page other than login */} + - {/* TODO: for some reason KeyboardAvoidingView doesn't work on any page other than login */} - - - - - - - - - - - + + + + + + + + + diff --git a/samples/react-native-group-chat/babel.config.js b/samples/react-native-group-chat/babel.config.js index 21b7fee8..53b18f42 100644 --- a/samples/react-native-group-chat/babel.config.js +++ b/samples/react-native-group-chat/babel.config.js @@ -2,11 +2,6 @@ module.exports = function (api) { api.cache(true) return { presets: ["babel-preset-expo"], - env: { - production: { - plugins: ["react-native-paper/babel"], - }, - }, plugins: ["@babel/plugin-proposal-export-namespace-from", "react-native-reanimated/plugin"], } } diff --git a/samples/react-native-group-chat/components/actions-menu/ActionsMenu.tsx b/samples/react-native-group-chat/components/actions-menu/ActionsMenu.tsx index 07a303f6..51f5a8c6 100644 --- a/samples/react-native-group-chat/components/actions-menu/ActionsMenu.tsx +++ b/samples/react-native-group-chat/components/actions-menu/ActionsMenu.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useMemo, useRef, useState } from "react" import { StyleSheet, View } from "react-native" import { BottomSheetModal, BottomSheetBackdrop } from "@gorhom/bottom-sheet" -import { Gap, Text, usePNTheme, Button } from "../../ui-components" +import { Gap, Text, colorPalette as colors, Button } from "../../ui-components" import Emoji1 from "../../assets/emojis/emoji1.svg" import Emoji2 from "../../assets/emojis/emoji2.svg" import Emoji3 from "../../assets/emojis/emoji3.svg" @@ -12,17 +12,20 @@ import Emoji7 from "../../assets/emojis/emoji7.svg" import { useNavigation } from "@react-navigation/native" import { EnhancedIMessage } from "../../utils" import { HomeStackNavigation } from "../../types" -import { Message } from "@pubnub/chat" +import { Message, ThreadMessage } from "@pubnub/chat" type UseActionsMenuParams = { onQuote: (message: Message) => void removeThreadReply?: boolean - onPinMessage: (message: Message) => void + onPinMessage: (message: Message | ThreadMessage) => void } -export function useActionsMenu({ onQuote, removeThreadReply = false, onPinMessage }: UseActionsMenuParams) { +export function useActionsMenu({ + onQuote, + removeThreadReply = false, + onPinMessage, +}: UseActionsMenuParams) { const bottomSheetModalRef = useRef(null) - const theme = usePNTheme() const navigation = useNavigation() const [currentlyFocusedMessage, setCurrentlyFocusedMessage] = useState( null @@ -48,7 +51,7 @@ export function useActionsMenu({ onQuote, removeThreadReply = false, onPinMessag onChange={handleSheetChanges} style={styles.container} backdropComponent={BottomSheetBackdrop} - handleIndicatorStyle={[styles.handleIndicator, { backgroundColor: theme.colors.neutral500 }]} + handleIndicatorStyle={[styles.handleIndicator, { backgroundColor: colors.neutral500 }]} > diff --git a/samples/react-native-group-chat/components/avatar/Avatar.tsx b/samples/react-native-group-chat/components/avatar/Avatar.tsx new file mode 100644 index 00000000..272a0c82 --- /dev/null +++ b/samples/react-native-group-chat/components/avatar/Avatar.tsx @@ -0,0 +1,96 @@ +import { Channel, User } from "@pubnub/chat" +import { View, Image, StyleSheet, ViewStyle } from "react-native" + +import Avatar1 from "../../assets/avatars/avatar1.png" +import Avatar2 from "../../assets/avatars/avatar2.png" +import Avatar3 from "../../assets/avatars/avatar3.png" +import Avatar4 from "../../assets/avatars/avatar4.png" +import Avatar5 from "../../assets/avatars/avatar5.png" +import Avatar6 from "../../assets/avatars/avatar6.png" +import Avatar7 from "../../assets/avatars/avatar7.png" +import Avatar8 from "../../assets/avatars/avatar8.png" +import Avatar9 from "../../assets/avatars/avatar9.png" +import Avatar10 from "../../assets/avatars/avatar10.png" +import Avatar11 from "../../assets/avatars/avatar11.png" +import Avatar12 from "../../assets/avatars/avatar12.png" +import Avatar13 from "../../assets/avatars/avatar13.png" +import Avatar14 from "../../assets/avatars/avatar14.png" +import Avatar15 from "../../assets/avatars/avatar15.png" +import Avatar16 from "../../assets/avatars/avatar16.png" +import { colorPalette as colors } from "../../ui-components" + +const avatars = [ + Avatar1, + Avatar2, + Avatar3, + Avatar4, + Avatar5, + Avatar6, + Avatar7, + Avatar8, + Avatar9, + Avatar10, + Avatar11, + Avatar12, + Avatar13, + Avatar14, + Avatar15, + Avatar16, +] + +type AvatarProps = { + source: User | Channel + showIndicator?: boolean + size?: "sm" | "md" | "lg" | "xl" + style?: ViewStyle +} + +export function Avatar({ source, showIndicator = false, size = "sm", style }: AvatarProps) { + const isUser = source && "active" in source + const hash = source.id + .split("") + .map((c) => c.charCodeAt(0)) + .reduce((a, b) => a + b) + const styles = createStyles({ size }) + + return ( + + + {isUser && showIndicator && ( + + )} + + ) +} + +function createStyles({ size }: Required>) { + return StyleSheet.create({ + image: { + width: { sm: 27, md: 36, lg: 64, xl: 88 }[size], + height: { sm: 27, md: 36, lg: 64, xl: 88 }[size], + borderRadius: { sm: 27, md: 36, lg: 64, xl: 88 }[size], + }, + indicator: { + borderColor: colors.neutral0, + borderRadius: { sm: 12, md: 18, lg: 22, xl: 64 }[size], + borderWidth: { sm: 2, md: 3, lg: 3, xl: 4 }[size], + top: { sm: 16, md: 20, lg: 42, xl: 64 }[size], + height: { sm: 12, md: 18, lg: 22, xl: 28 }[size], + position: "absolute", + left: { sm: 16, md: 20, lg: 42, xl: 64 }[size], + width: { sm: 12, md: 18, lg: 22, xl: 28 }[size], + }, + }) +} diff --git a/samples/react-native-group-chat/components/avatar/index.ts b/samples/react-native-group-chat/components/avatar/index.ts new file mode 100644 index 00000000..c5ddb58f --- /dev/null +++ b/samples/react-native-group-chat/components/avatar/index.ts @@ -0,0 +1 @@ +export * from "./Avatar" diff --git a/samples/react-native-group-chat/components/channels-section/ChannelsSection.tsx b/samples/react-native-group-chat/components/channels-section/ChannelsSection.tsx deleted file mode 100644 index 2bd10958..00000000 --- a/samples/react-native-group-chat/components/channels-section/ChannelsSection.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, { useState } from "react" -import { TouchableOpacity, View, StyleSheet } from "react-native" -import { Channel } from "@pubnub/chat" -import { List } from "react-native-paper" - -import { Icon, usePNTheme } from "../../ui-components" -import { ListItem } from "../list-item" - -type ChannelsSectionProps = { - channels: Channel[] - title: string - onAddIconPress: () => void - onChannelPress: (channelId: string) => void -} - -export function ChannelsSection({ - title, - channels, - onAddIconPress, - onChannelPress, -}: ChannelsSectionProps) { - const [isSectionExpanded, setIsSectionExpanded] = useState(true) - const theme = usePNTheme() - - return ( - ( - - {/* - - */} - setIsSectionExpanded(!isSectionExpanded)}> - {isSectionExpanded ? ( - - ) : ( - - )} - - - )} - > - {channels.map((channel) => ( - onChannelPress(channel.id)} - /> - ))} - - ) -} - -const styles = StyleSheet.create({ - container: { - paddingVertical: 0, - }, - sectionIcons: { - flexDirection: "row", - right: -16, - }, - accordionTitleStyle: { - left: -16, - }, -}) diff --git a/samples/react-native-group-chat/components/channels-section/index.ts b/samples/react-native-group-chat/components/channels-section/index.ts deleted file mode 100644 index 29b59fb8..00000000 --- a/samples/react-native-group-chat/components/channels-section/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./ChannelsSection" diff --git a/samples/react-native-group-chat/components/direct-channels/DirectChannels.tsx b/samples/react-native-group-chat/components/direct-channels/DirectChannels.tsx new file mode 100644 index 00000000..fac7be98 --- /dev/null +++ b/samples/react-native-group-chat/components/direct-channels/DirectChannels.tsx @@ -0,0 +1,72 @@ +import React, { useContext } from "react" +import { View } from "react-native" +import { Channel, User } from "@pubnub/chat" +import { useNavigation } from "@react-navigation/native" + +import { ChatContext } from "../../context" +import { ListItem, Avatar } from "../../components" + +type DirectChannelsProps = { + searchText?: string + sortByActive?: boolean + showIndicators?: boolean +} + +export function DirectChannels({ + searchText = "", + sortByActive = false, + showIndicators = false, +}: DirectChannelsProps) { + const { chat, users, memberships, setCurrentChannel } = useContext(ChatContext) + const navigation = useNavigation() + + const entries = memberships.flatMap((m) => { + if (m.channel.type !== "direct") return [] + const user = getInterlocutor(m.channel) + if (!user) return [] + return { channel: m.channel, user } + }) + + function userName(user: User) { + return user.name || user.id + } + + function getInterlocutor(channel: Channel) { + if (!chat) return + const userId = channel.id + .replace("direct.", "") + .replace(chat?.currentUser.id, "") + .replace("&", "") + return users.find((u) => u.id === userId) + } + + function openChat(channel: Channel) { + setCurrentChannel(channel) + // TODO: fix navigation type error + navigation.navigate("Chat") + } + + function sortEntries(a: User, b: User) { + if (sortByActive && a.active !== b.active) return a.active ? -1 : 1 + return userName(a).localeCompare(userName(b)) + } + + return ( + chat && ( + + {entries + .filter(({ user }) => userName(user).toLowerCase().includes(searchText.toLowerCase())) + .sort((a, b) => sortEntries(a.user, b.user)) + .map(({ user, channel }) => ( + } + title={userName(user)} + onPress={() => openChat(channel)} + // TODO: unread messages count badge + /> + ))} + + ) + ) +} diff --git a/samples/react-native-group-chat/components/direct-channels/index.ts b/samples/react-native-group-chat/components/direct-channels/index.ts new file mode 100644 index 00000000..934c91ff --- /dev/null +++ b/samples/react-native-group-chat/components/direct-channels/index.ts @@ -0,0 +1 @@ +export * from "./DirectChannels" diff --git a/samples/react-native-group-chat/components/index.ts b/samples/react-native-group-chat/components/index.ts index 627e9858..082c7090 100644 --- a/samples/react-native-group-chat/components/index.ts +++ b/samples/react-native-group-chat/components/index.ts @@ -1,6 +1,6 @@ export * from "./actions-menu" -export * from "./channels-section" -export * from "./unread-channels-section" export * from "./list-item" export * from "./user-suggestion-box" export * from "./quote" +export * from "./avatar" +export * from "./direct-channels" diff --git a/samples/react-native-group-chat/components/list-item/ListItem.tsx b/samples/react-native-group-chat/components/list-item/ListItem.tsx index 8c2aac23..2f7b853e 100644 --- a/samples/react-native-group-chat/components/list-item/ListItem.tsx +++ b/samples/react-native-group-chat/components/list-item/ListItem.tsx @@ -1,27 +1,19 @@ +import { ReactNode } from "react" import { View, StyleSheet, TouchableHighlight } from "react-native" import { MaterialIcons } from "@expo/vector-icons" -import { Text, colorPalette as colors, RandomAvatar } from "../../ui-components" +import { Text, colorPalette as colors } from "../../ui-components" type ListItemProps = { title: string - showActive?: boolean - active?: boolean + avatar?: ReactNode badge?: string onPress?: () => unknown showCheckbox?: boolean checked?: boolean } -export function ListItem({ - title, - showActive, - active, - badge, - onPress, - showCheckbox, - checked, -}: ListItemProps) { +export function ListItem({ title, avatar, badge, onPress, showCheckbox, checked }: ListItemProps) { return ( @@ -34,15 +26,7 @@ export function ListItem({ /> ) : null} - - {showActive ? ( - - ) : null} + {avatar} {title} @@ -70,16 +54,6 @@ const styles = StyleSheet.create({ alignItems: "center", flexDirection: "row", }, - indicator: { - borderColor: colors.neutral0, - borderRadius: 12, - borderWidth: 2, - bottom: -1, - height: 12, - left: 21, - position: "absolute", - width: 12, - }, checkbox: { paddingRight: 12, }, diff --git a/samples/react-native-group-chat/components/message-text/MessageText.tsx b/samples/react-native-group-chat/components/message-text/MessageText.tsx index fdc23426..92a7870e 100644 --- a/samples/react-native-group-chat/components/message-text/MessageText.tsx +++ b/samples/react-native-group-chat/components/message-text/MessageText.tsx @@ -1,11 +1,11 @@ -import React, {useCallback, useContext} from "react" -import {Bubble} from "react-native-gifted-chat"; -import {EnhancedIMessage} from "../../utils"; -import {Linking, View} from "react-native"; -import {Quote} from "../quote"; -import {Text} from "../../ui-components"; -import {Message, MixedTextTypedElement} from "@pubnub/chat"; -import {ChatContext} from "../../context"; +import React, { useCallback, useContext } from "react" +import { Bubble } from "react-native-gifted-chat" +import { EnhancedIMessage } from "../../utils" +import { Linking, View } from "react-native" +import { Quote } from "../quote" +import { Text } from "../../ui-components" +import { Message, MixedTextTypedElement } from "@pubnub/chat" +import { ChatContext } from "../../context" type MessageTextProps = { onGoToMessage: (message: Message) => void @@ -80,7 +80,9 @@ export function MessageText({ onGoToMessage, messageProps }: MessageTextProps) { // onGoToMessage={() => { // scrollToMessage(props.currentMessage?.originalPnMessage.quotedMessage) // }} - onGoToMessage={() => onGoToMessage(messageProps.currentMessage?.originalPnMessage.quotedMessage)} + onGoToMessage={() => + onGoToMessage(messageProps.currentMessage?.originalPnMessage.quotedMessage) + } charactersLimit={50} /> ) : null} @@ -88,7 +90,11 @@ export function MessageText({ onGoToMessage, messageProps }: MessageTextProps) { {messageProps.currentMessage.originalPnMessage .getLinkedText() .map((msgPart, index) => - renderMessagePart(msgPart, index, messageProps.currentMessage?.user._id || "") + renderMessagePart( + msgPart, + index, + messageProps.currentMessage?.originalPnMessage.userId || "" + ) )} diff --git a/samples/react-native-group-chat/components/unread-channels-section/UnreadChannelsSection.tsx b/samples/react-native-group-chat/components/unread-channels-section/UnreadChannelsSection.tsx deleted file mode 100644 index 26e403cd..00000000 --- a/samples/react-native-group-chat/components/unread-channels-section/UnreadChannelsSection.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React, { useState } from "react" -import { Channel, Membership } from "@pubnub/chat" -import { TouchableOpacity, View, StyleSheet } from "react-native" -import { List, useTheme } from "react-native-paper" - -import { ListItem } from "../list-item" -import { Icon, defaultTheme } from "../../ui-components" - -type UnreadChannelsSection = { - onPress: (channelId: string) => void - unreadChannels: { channel: Channel; count: number; membership: Membership }[] - markAllMessagesAsRead: () => void -} - -export function UnreadChannelsSection({ - onPress, - unreadChannels, - markAllMessagesAsRead, -}: UnreadChannelsSection) { - const [isSectionExpanded, setIsSectionExpanded] = useState(true) - const theme = useTheme() as typeof defaultTheme - - return ( - ( - - - - - setIsSectionExpanded(!isSectionExpanded)}> - {isSectionExpanded ? ( - - ) : ( - - )} - - - )} - > - {unreadChannels.map((unreadChannel) => ( - onPress(unreadChannel.channel.id)} - badge={String(unreadChannel.count)} - /> - ))} - - ) -} - -const styles = StyleSheet.create({ - container: { - paddingVertical: 0, - }, - sectionIcons: { - flexDirection: "row", - right: -16, - }, - accordionTitleStyle: { - left: -16, - }, -}) diff --git a/samples/react-native-group-chat/components/unread-channels-section/index.ts b/samples/react-native-group-chat/components/unread-channels-section/index.ts deleted file mode 100644 index b712cdf3..00000000 --- a/samples/react-native-group-chat/components/unread-channels-section/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./UnreadChannelsSection" diff --git a/samples/react-native-group-chat/context.ts b/samples/react-native-group-chat/context.ts index ca8e938b..7c21d7a5 100644 --- a/samples/react-native-group-chat/context.ts +++ b/samples/react-native-group-chat/context.ts @@ -1,20 +1,32 @@ import * as React from "react" -import { Chat, User, Membership } from "@pubnub/chat" +import { Chat, User, Membership, Channel } from "@pubnub/chat" type ChatContextParams = { + loading: boolean + setLoading: (state: boolean) => void chat: null | Chat setChat: (chat: Chat | null) => void + currentChannel?: Channel + setCurrentChannel: (channel: Channel) => void + currentChannelMembers: Membership[] users: User[] setUsers: (users: User[]) => void + getUser: (userId: string) => User | null memberships: Membership[] setMemberships: (memberships: Membership[]) => void } -export const ChatContext = React.createContext({ +export const ChatContext = React.createContext({ + loading: false, + setLoading: () => null, chat: null as Chat | null, setChat: () => null, + currentChannel: undefined, + setCurrentChannel: () => null, + currentChannelMembers: [], users: [], setUsers: () => null, + getUser: () => null, memberships: [], setMemberships: () => null, -} as ChatContextParams) +}) diff --git a/samples/react-native-group-chat/hooks/useCommonChatRenderers.tsx b/samples/react-native-group-chat/hooks/useCommonChatRenderers.tsx index 637ef41e..72473f42 100644 --- a/samples/react-native-group-chat/hooks/useCommonChatRenderers.tsx +++ b/samples/react-native-group-chat/hooks/useCommonChatRenderers.tsx @@ -1,16 +1,16 @@ -import { FlatList, Linking, View, StyleSheet } from "react-native" +import { FlatList, View, StyleSheet } from "react-native" import React, { useCallback } from "react" -import { Chat, Message, MessageDraft, MixedTextTypedElement, User } from "@pubnub/chat" +import { Message, MessageDraft, User } from "@pubnub/chat" import { Text } from "../ui-components" import { Bubble } from "react-native-gifted-chat" import { EnhancedIMessage } from "../utils" import { Quote, UserSuggestionBox } from "../components" -import {MessageText} from "../components/message-text"; +import { MessageText } from "../components/message-text" type UseCommonChatRenderersProps = { typingData: string[] users: Map - messageDraft: MessageDraft + messageDraft: MessageDraft | null lastAffectedNameOccurrenceIndex: number setText: (text: string) => void setShowSuggestedUsers: (value: boolean) => void @@ -66,7 +66,7 @@ export function useCommonChatRenderers({ ) const renderChatFooter = useCallback(() => { - if (!messageDraft || !messageDraft.quotedMessage) { + if (!messageDraft) { return null } @@ -78,7 +78,7 @@ export function useCommonChatRenderers({ quotedMessageComponent = ( scrollToMessage(quotedMessage)} /> @@ -97,7 +97,7 @@ export function useCommonChatRenderers({ {userSuggestionComponent} ) - }, [messageDraft, showSuggestedUsers, scrollToMessage]) + }, [messageDraft, showSuggestedUsers, scrollToMessage, suggestedUsers]) const renderFooter = useCallback(() => { if (!typingData.length) { diff --git a/samples/react-native-group-chat/package.json b/samples/react-native-group-chat/package.json index 65fd79b0..ef502a0c 100644 --- a/samples/react-native-group-chat/package.json +++ b/samples/react-native-group-chat/package.json @@ -26,7 +26,6 @@ "react-native-gesture-handler": "~2.12.0", "react-native-get-random-values": "1.9.0", "react-native-gifted-chat": "^2.4.0", - "react-native-paper": "^5.10.3", "react-native-reanimated": "~3.3.0", "react-native-safe-area-context": "4.6.3", "react-native-screens": "~3.22.0", diff --git a/samples/react-native-group-chat/screens/ordinary/chat-settings/ChatSettings.tsx b/samples/react-native-group-chat/screens/ordinary/chat-settings/ChatSettings.tsx new file mode 100644 index 00000000..3cf1829b --- /dev/null +++ b/samples/react-native-group-chat/screens/ordinary/chat-settings/ChatSettings.tsx @@ -0,0 +1,171 @@ +import { useState, useContext, useEffect, useRef } from "react" +import { View, ScrollView, StyleSheet, Switch } from "react-native" +import { StackScreenProps } from "@react-navigation/stack" +import { BottomSheetModal, BottomSheetBackdrop } from "@gorhom/bottom-sheet" + +import { HomeStackParamList } from "../../../types" +import { ChatContext } from "../../../context" +import { Gap, Button, Line, Text, TextInput, colorPalette as colors } from "../../../ui-components" +import { Avatar } from "../../../components" + +export function ChatSettings({ navigation }: StackScreenProps) { + const { chat, currentChannel, setCurrentChannel, currentChannelMembers } = useContext(ChatContext) + const isDirect = currentChannel?.type === "direct" + const [mute, setMute] = useState(true) + const bottomSheetModalRef = useRef(null) + const [nameInput, setNameInput] = useState(currentChannel?.name) + const members = currentChannelMembers + .filter((m) => m.user.id !== chat?.currentUser.id) + .map((m) => m.user) + + async function leaveOrRemove() { + if (!currentChannel) return + isDirect ? await currentChannel.delete() : await currentChannel.leave() + navigation.popToTop() + } + + async function saveName() { + if (!currentChannel) return + await currentChannel.update({ name: nameInput }) + bottomSheetModalRef.current?.dismiss() + } + + useEffect(() => { + if (!currentChannel) return + return currentChannel.streamUpdates(setCurrentChannel) + }, [currentChannel, setCurrentChannel]) + + return ( + currentChannel && ( + + + {members.map((user) => ( + + ))} + + + {!isDirect && ( + <> + + + + Name + + {currentChannel.name} + + + + + + + + + + )} + + + Members + + + {members.map((user) => ( + + {user.name || user.id} + + ))} + + + + + + + + Mute channel + + + + + + Mute notifications about new messages and mentions from this chat + + + + + + + + + + + + Change chat name + + + + + + + + + + ) + ) +} + +const styles = StyleSheet.create({ + container: { + backgroundColor: colors.neutral0, + flex: 1, + paddingHorizontal: 32, + }, + row: { + alignItems: "center", + flexDirection: "row", + justifyContent: "space-between", + justifySelf: "center", + }, + avatars: { + flexDirection: "row", + margin: 32, + justifyContent: "center", + }, + avatar: { + borderWidth: 2, + borderColor: colors.neutral0, + borderRadius: 100, + marginLeft: -12, + }, +}) diff --git a/samples/react-native-group-chat/screens/ordinary/chat-settings/index.ts b/samples/react-native-group-chat/screens/ordinary/chat-settings/index.ts new file mode 100644 index 00000000..935d0000 --- /dev/null +++ b/samples/react-native-group-chat/screens/ordinary/chat-settings/index.ts @@ -0,0 +1 @@ +export * from "./ChatSettings" diff --git a/samples/react-native-group-chat/screens/ordinary/chat/Chat.tsx b/samples/react-native-group-chat/screens/ordinary/chat/Chat.tsx index d281ddd0..8a87b13e 100644 --- a/samples/react-native-group-chat/screens/ordinary/chat/Chat.tsx +++ b/samples/react-native-group-chat/screens/ordinary/chat/Chat.tsx @@ -1,22 +1,22 @@ -import React, { useState, useCallback, useEffect, useContext, useMemo, useRef } from "react" +import React, { useState, useCallback, useEffect, useContext, useRef } from "react" import { StyleSheet, View, ActivityIndicator, TouchableOpacity, FlatList } from "react-native" import { GiftedChat, Bubble } from "react-native-gifted-chat" import { StackScreenProps } from "@react-navigation/stack" -import { Channel, User, MessageDraft, Message } from "@pubnub/chat" +import { User, MessageDraft, Message } from "@pubnub/chat" import { EnhancedIMessage, mapPNMessageToGChatMessage } from "../../../utils" import { ChatContext } from "../../../context" import { HomeStackParamList } from "../../../types" -import { Quote, useActionsMenu, UserSuggestionBox } from "../../../components" -import { getRandomAvatar, colorPalette as colors } from "../../../ui-components" +import { Avatar, useActionsMenu } from "../../../components" +import { colorPalette as colors, Text } from "../../../ui-components" import { useNavigation } from "@react-navigation/native" -import { Icon, Text, usePNTheme } from "../../../ui-components" import { useCommonChatRenderers } from "../../../hooks" +import { MaterialCommunityIcons } from "@expo/vector-icons" -export function ChatScreen({ route }: StackScreenProps) { - const { channelId } = route.params +export function ChatScreen({}: StackScreenProps) { + const { chat, setCurrentChannel, currentChannel, getUser, currentChannelMembers } = + useContext(ChatContext) const navigation = useNavigation() - const [currentChannel, setCurrentChannel] = useState(null) const [isMoreMessages, setIsMoreMessages] = useState(true) const [isLoadingMoreMessages, setIsLoadingMoreMessages] = useState(false) const [giftedChatMappedMessages, setGiftedChatMappedMessages] = useState([]) @@ -28,14 +28,10 @@ export function ChatScreen({ route }: StackScreenProps>(null) const [lastAffectedNameOccurrenceIndex, setLastAffectedNameOccurrenceIndex] = useState(-1) const [text, setText] = useState("") - const { chat, memberships } = useContext(ChatContext) - const theme = usePNTheme() - const currentChannelMembership = useMemo( - () => memberships.find((membership) => membership.channel.id === channelId), - [memberships, channelId] + const currentChannelMembership = currentChannelMembers.find( + (m) => m.user.id === currentChannel?.id ) const { renderFooter, renderMessageText, renderChatFooter } = useCommonChatRenderers({ - chat, typingData, users, messageDraft, @@ -55,7 +51,7 @@ export function ChatScreen({ route }: StackScreenProps navigation.navigate("PinnedMessage", { channelId: currentChannel?.id })} style={{ paddingRight: 24 }} > - + ) }, @@ -81,9 +77,11 @@ export function ChatScreen({ route }: StackScreenProps { newUsers.set(user.id, { ...user, - thumbnail: getRandomAvatar(), }) }) @@ -106,7 +103,7 @@ export function ChatScreen({ route }: StackScreenProps { @@ -115,15 +112,6 @@ export function ChatScreen({ route }: StackScreenProps { updateUsersMap("1", usersObject.users) }) @@ -132,7 +120,7 @@ export function ChatScreen({ route }: StackScreenProps { if (!giftedChatMappedMessages.length) { @@ -149,7 +137,7 @@ export function ChatScreen({ route }: StackScreenProps { if (!currentChannel) { @@ -219,7 +207,7 @@ export function ChatScreen({ route }: StackScreenProps { if (!currentChannel) { @@ -275,6 +263,7 @@ export function ChatScreen({ route }: StackScreenProps { + console.log("suggestionObject", suggestionObject) setSuggestedUsers(suggestionObject.users.suggestedUsers) setLastAffectedNameOccurrenceIndex(suggestionObject.users.nameOccurrenceIndex) }) @@ -291,8 +280,8 @@ export function ChatScreen({ route }: StackScreenProps {props.currentMessage?.originalPnMessage.hasThread ? ( @@ -304,7 +293,7 @@ export function ChatScreen({ route }: StackScreenProps - + replies @@ -337,6 +326,10 @@ export function ChatScreen({ route }: StackScreenProps { + const user = getUser(props.currentMessage?.originalPnMessage.userId) + return user && + }} user={{ _id: chat.currentUser.id, }} diff --git a/samples/react-native-group-chat/screens/ordinary/index.ts b/samples/react-native-group-chat/screens/ordinary/index.ts index c6b1f3b7..1975ade5 100644 --- a/samples/react-native-group-chat/screens/ordinary/index.ts +++ b/samples/react-native-group-chat/screens/ordinary/index.ts @@ -3,3 +3,5 @@ export * from "./login-screen" export * from "./new-chat-screen" export * from "./new-group-screen" export * from "./pinned-message" +export * from "./chat-settings" +export * from "./thread-reply" diff --git a/samples/react-native-group-chat/screens/ordinary/new-chat-screen/NewChatScreen.tsx b/samples/react-native-group-chat/screens/ordinary/new-chat-screen/NewChatScreen.tsx index d8dd68aa..dbee1854 100644 --- a/samples/react-native-group-chat/screens/ordinary/new-chat-screen/NewChatScreen.tsx +++ b/samples/react-native-group-chat/screens/ordinary/new-chat-screen/NewChatScreen.tsx @@ -3,20 +3,23 @@ import { View, StyleSheet, FlatList } from "react-native" import { StackScreenProps } from "@react-navigation/stack" import { User } from "@pubnub/chat" +import { Gap, Button, Line, TextInput, colorPalette as colors } from "../../../ui-components" +import { ListItem, Avatar } from "../../../components" import { HomeStackParamList } from "../../../types" import { ChatContext } from "../../../context" -import { ListItem } from "../../../components" -import { Gap, Button, Line, TextInput, colorPalette as colors } from "../../../ui-components" export function NewChatScreen({ navigation }: StackScreenProps) { + const { chat, users, setLoading, setCurrentChannel } = useContext(ChatContext) const [searchText, setSearchText] = useState("") - const { chat, users } = useContext(ChatContext) async function openChat(user: User) { - // TODO: this should ideally navigate first and create channel in the background if (!chat) return + setLoading(true) + navigation.popToTop() + navigation.navigate("Chat") const { channel } = await chat.createDirectConversation({ user, channelData: {} }) - navigation.navigate("Chat", { channelId: channel.id }) + setCurrentChannel(channel) + setLoading(false) } return ( @@ -28,7 +31,9 @@ export function NewChatScreen({ navigation }: StackScreenProps + + + + user.name?.toLowerCase().includes(searchText.toLowerCase()))} renderItem={({ item: user }) => ( - openChat(user)} /> + } + title={user.name || user.id} + onPress={() => openChat(user)} + /> )} keyExtractor={(user) => user.id} /> diff --git a/samples/react-native-group-chat/screens/ordinary/new-group-screen/NewGroupScreen.tsx b/samples/react-native-group-chat/screens/ordinary/new-group-screen/NewGroupScreen.tsx index 179c6129..5cb0a82f 100644 --- a/samples/react-native-group-chat/screens/ordinary/new-group-screen/NewGroupScreen.tsx +++ b/samples/react-native-group-chat/screens/ordinary/new-group-screen/NewGroupScreen.tsx @@ -6,14 +6,14 @@ import { User } from "@pubnub/chat" import { HomeStackParamList } from "../../../types" import { ChatContext } from "../../../context" -import { ListItem } from "../../../components" +import { ListItem, Avatar } from "../../../components" import { Button, Gap, Line, TextInput, colorPalette as colors } from "../../../ui-components" export function NewGroupScreen({ navigation }: StackScreenProps) { + const { chat, users, setCurrentChannel, setLoading } = useContext(ChatContext) const [searchText, setSearchText] = useState("") const [groupName, setGroupName] = useState("") const [selectedUsers, setSelectedUsers] = useState([]) - const { chat, users } = useContext(ChatContext) function toggleUser(user: User) { setSelectedUsers((selectedUsers) => { @@ -24,14 +24,17 @@ export function NewGroupScreen({ navigation }: StackScreenProps user.name?.toLowerCase().includes(searchText.toLowerCase()))} renderItem={({ item: user }) => ( } title={user.name || user.id} onPress={() => toggleUser(user)} showCheckbox diff --git a/samples/react-native-group-chat/screens/ordinary/pinned-message/PinnedMessage.tsx b/samples/react-native-group-chat/screens/ordinary/pinned-message/PinnedMessage.tsx index 8b9b4e68..25da9f43 100644 --- a/samples/react-native-group-chat/screens/ordinary/pinned-message/PinnedMessage.tsx +++ b/samples/react-native-group-chat/screens/ordinary/pinned-message/PinnedMessage.tsx @@ -1,24 +1,17 @@ -import React, {useCallback, useContext, useEffect, useState} from "react" -import { View, StyleSheet } from "react-native"; -import { Text } from "../../../ui-components"; +import React, { useCallback, useContext, useEffect, useState } from "react" +import { View, StyleSheet } from "react-native" import { Message } from "@pubnub/chat" -import {ChatContext} from "../../../context"; -import {Bubble} from "react-native-gifted-chat"; -import {EnhancedIMessage} from "../../../utils"; -import {Quote} from "../../../components"; -import {useCommonChatRenderers} from "../../../hooks"; -import {MessageText} from "../../../components/message-text"; -import {StackScreenProps} from "@react-navigation/stack"; -import {HomeStackParamList} from "../../../types"; - -type PinnedMessageProps = { - channelId: string - messageTimetoken: string -} +import { ChatContext } from "../../../context" +import { Bubble } from "react-native-gifted-chat" +import { EnhancedIMessage } from "../../../utils" +import { Avatar } from "../../../components" +import { MessageText } from "../../../components/message-text" +import { StackScreenProps } from "@react-navigation/stack" +import { HomeStackParamList } from "../../../types" export function PinnedMessage({ route }: StackScreenProps) { const [message, setMessage] = useState(null) - const { chat } = useContext(ChatContext) + const { chat, getUser } = useContext(ChatContext) const { channelId } = route.params useEffect(() => { @@ -34,25 +27,32 @@ export function PinnedMessage({ route }: StackScreenProps["props"]) => { - return ( - null} messageProps={props} />} - // renderTime={() => null} - // containerToNextStyle={{ right: { marginRight: 0 } }} - // containerStyle={{ right: { marginRight: 0 } }} - // wrapperStyle={{ - // right: [styles.ownBubbleBackground, { backgroundColor: theme.colors.teal100 }], - // left: [styles.otherBubbleBackground], - // }} - // textStyle={{ right: [styles.ownBubbleText, theme.textStyles.body] }} - /> - ) - }, []) + const renderMessageBubble = useCallback( + (props: Bubble["props"]) => { + if (!message) { + return null + } + + const sender = getUser(props.currentMessage?.originalPnMessage.userId) + + return ( + + {sender && } + + ( + null} messageProps={props} /> + )} + /> + + ) + }, + [message] + ) if (!message) { return null @@ -60,11 +60,8 @@ export function PinnedMessage({ route }: StackScreenProps - - Pinned message: - - {renderMessageBubble({ currentMessage: { originalPnMessage: message } })} + {renderMessageBubble({ currentMessage: { originalPnMessage: message, text: "example" } })} ) @@ -75,8 +72,7 @@ const styles = StyleSheet.create({ flex: 1, }, content: { - backgroundColor: "violet", - width: 100, - height: 100, + paddingLeft: 16, + paddingVertical: 16, }, }) diff --git a/samples/react-native-group-chat/screens/ordinary/thread-reply/ThreadReply.tsx b/samples/react-native-group-chat/screens/ordinary/thread-reply/ThreadReply.tsx index de799e08..423c1d93 100644 --- a/samples/react-native-group-chat/screens/ordinary/thread-reply/ThreadReply.tsx +++ b/samples/react-native-group-chat/screens/ordinary/thread-reply/ThreadReply.tsx @@ -3,25 +3,20 @@ import { StackScreenProps } from "@react-navigation/stack" import { HomeStackParamList } from "../../../types" import { ChatContext } from "../../../context" import { EnhancedIMessage, mapPNMessageToGChatMessage } from "../../../utils" -import { Message, MessageDraft, ThreadChannel, User } from "@pubnub/chat" +import { Message, MessageDraft, ThreadChannel, ThreadMessage, User } from "@pubnub/chat" import { Bubble, GiftedChat } from "react-native-gifted-chat" import { FlatList, StyleSheet, TouchableOpacity, View } from "react-native" -import { - Gap, - Line, - RandomAvatar, - usePNTheme, - Text, - Icon, - colorPalette, -} from "../../../ui-components" +import { Gap, Line, Text, defaultTheme } from "../../../ui-components" import { useNavigation } from "@react-navigation/native" import { useCommonChatRenderers } from "../../../hooks" -import { useActionsMenu } from "../../../components" +import { Avatar, useActionsMenu } from "../../../components" +import { MaterialCommunityIcons } from "@expo/vector-icons" + +const { colors, textStyles } = defaultTheme export function ThreadReply({ route }: StackScreenProps) { const { parentMessage } = route.params - const { chat } = useContext(ChatContext) + const { chat, getUser } = useContext(ChatContext) const navigation = useNavigation() const [currentThreadChannel, setCurrentThreadChannel] = useState(null) const [messageDraft, setMessageDraft] = useState(null) @@ -35,10 +30,8 @@ export function ThreadReply({ route }: StackScreenProps([]) const [showSuggestedUsers, setShowSuggestedUsers] = useState(false) const [lastAffectedNameOccurrenceIndex, setLastAffectedNameOccurrenceIndex] = useState(-1) - const theme = usePNTheme() const { renderFooter, renderMessageText, renderChatFooter } = useCommonChatRenderers({ - chat, typingData, users: new Map(), setText, @@ -63,9 +56,22 @@ export function ThreadReply({ route }: StackScreenProps { + if (!chat) { + return + } + if (message instanceof ThreadMessage) { + await message.pinToParentChannel() + } + }, + [chat] + ) + const { ActionsMenuComponent, handlePresentModalPress } = useActionsMenu({ onQuote: handleQuote, removeThreadReply: true, + onPinMessage: handlePin, }) useEffect(() => { @@ -174,7 +180,7 @@ export function ThreadReply({ route }: StackScreenProps { @@ -218,18 +224,17 @@ export function ThreadReply({ route }: StackScreenProps ) }, []) const renderParentMessageBubble = useCallback(() => { - if (!chat) { - return null - } + if (!chat) return null + const sender = getUser(parentMessage.originalPnMessage.userId) return ( @@ -239,7 +244,7 @@ export function ThreadReply({ route }: StackScreenProps - + {sender && } {renderMessageBubble({ currentMessage: parentMessage })} @@ -247,7 +252,7 @@ export function ThreadReply({ route }: StackScreenProps setIsParentMessageCollapsed(!isParentMessageCollapsed)} > - + {isParentMessageCollapsed ? "Expand" : "Collapse"} @@ -282,7 +287,10 @@ export function ThreadReply({ route }: StackScreenProps null} renderTime={() => null} - renderAvatar={() => } + renderAvatar={(props) => { + const user = getUser(props.currentMessage?.originalPnMessage.userId) + return user && + }} user={{ _id: chat.currentUser.id, }} @@ -299,8 +307,8 @@ export function ThreadReply({ route }: StackScreenProps) { - const { chat, setMemberships, setUsers } = useContext(ChatContext) + const { chat, memberships, setCurrentChannel, setMemberships, setUsers } = useContext(ChatContext) const [searchText, setSearchText] = useState("") - const [currentUserGroupChannels, setCurrentUserGroupChannels] = useState([]) - const [currentUserDirectChannels, setCurrentUserDirectChannels] = useState([]) - const [currentUserPublicChannels, setCurrentUserPublicChannels] = useState([]) const [unreadChannels, setUnreadChannels] = useState< { channel: Channel; count: number; membership: Membership }[] >([]) - const fetchUnreadMessagesCount = useCallback(async () => { - if (!chat) { - return - } + const channels = memberships.map((m) => m.channel) + const currentUserGroupChannels = channels.filter((c) => c.type === "group") + const currentUserPublicChannels = channels.filter((c) => c.type === "public") + function navigateToChat(channel: Channel) { + setCurrentChannel(channel) + navigation.navigate("Chat") + } + + const fetchUnreadMessagesCount = useCallback(async () => { + if (!chat) return const unreadMessagesCounts = await chat.getUnreadMessagesCounts({ filter: "channel.type == 'group'", }) @@ -35,28 +37,19 @@ export function HomeScreen({ navigation }: StackScreenProps { async function init() { - if (!chat) { - return - } - - const [, membershipsObject, usersObject] = await Promise.all([ + if (!chat) return + const [, { memberships }, { users }] = await Promise.all([ fetchUnreadMessagesCount(), chat.currentUser.getMemberships(), chat.getUsers({}), ]) - setUsers(usersObject.users) - - const channels = membershipsObject.memberships.map((m) => m.channel) - setMemberships(membershipsObject.memberships) - - setCurrentUserGroupChannels(channels.filter((c) => c.type === "group")) - setCurrentUserDirectChannels(channels.filter((c) => c.type === "direct")) - setCurrentUserPublicChannels(channels.filter((c) => c.type === "public")) + setUsers(users) + setMemberships(memberships) } init() - }, [chat]) + }, [chat, fetchUnreadMessagesCount, setMemberships, setUsers]) useFocusEffect( React.useCallback(() => { @@ -83,13 +76,10 @@ export function HomeScreen({ navigation }: StackScreenProps { - if (!chat) { - return - } - + if (!chat) return await chat.markAllMessagesAsRead() await fetchUnreadMessagesCount() - }, [chat]) + }, [chat, fetchUnreadMessagesCount]) return ( <> @@ -101,42 +91,71 @@ export function HomeScreen({ navigation }: StackScreenProps + - - navigation.navigate("Chat", { channelId })} - unreadChannels={getFilteredUnreadChannels(unreadChannels)} - markAllMessagesAsRead={markAllMessagesAsRead} - /> + + + + + + } + > + {getFilteredUnreadChannels(unreadChannels).map(({ channel, count }) => ( + } + onPress={() => navigateToChat(channel)} + badge={String(count)} + /> + ))} + + - - null} - onChannelPress={(channelId) => navigation.navigate("Chat", { channelId })} - /> + + + + {getFilteredChannels(currentUserPublicChannels).map((channel) => ( + } + onPress={() => navigateToChat(channel)} + /> + ))} + + - - null} - onChannelPress={(channelId) => navigation.navigate("Chat", { channelId })} - /> + + + + {getFilteredChannels(currentUserGroupChannels).map((channel) => ( + } + onPress={() => navigateToChat(channel)} + /> + ))} + + - - null} - onChannelPress={(channelId) => navigation.navigate("Chat", { channelId })} - /> + + + + + + + + () export function HomeStackScreen({ route }: StackScreenProps) { + const { chat, currentChannel, currentChannelMembers } = useContext(ChatContext) + const interlocutor = + currentChannel?.type === "direct" && + currentChannelMembers.map((m) => m.user).filter((u) => u.id !== chat?.currentUser.id)[0] + return ( - + ({ + headerTitle: () => + currentChannel && ( + navigation.navigate("ChatSettings")} + style={{ paddingVertical: 8, paddingHorizontal: 30, borderRadius: 6 }} + > + + + + {interlocutor ? interlocutor.name : currentChannel?.name} + + + + ), + })} + /> + ) diff --git a/samples/react-native-group-chat/screens/tabs/people/PeopleScreen.tsx b/samples/react-native-group-chat/screens/tabs/people/PeopleScreen.tsx index 9cba341e..cad76beb 100644 --- a/samples/react-native-group-chat/screens/tabs/people/PeopleScreen.tsx +++ b/samples/react-native-group-chat/screens/tabs/people/PeopleScreen.tsx @@ -1,128 +1,89 @@ import React, { useContext, useState } from "react" -import { View, StyleSheet, FlatList, TouchableOpacity } from "react-native" +import { ScrollView, View, StyleSheet, TouchableOpacity } from "react-native" import { BottomTabScreenProps } from "@react-navigation/bottom-tabs" import { MaterialIcons } from "@expo/vector-icons" -import { Channel } from "@pubnub/chat" import { BottomTabsParamList } from "../../../types" import { ChatContext } from "../../../context" -import { ListItem } from "../../../components" +import { DirectChannels } from "../../../components" import { Gap, Line, TextInput, Button, Text, colorPalette as colors } from "../../../ui-components" -type ListEntry = { - title: string - active: boolean - channelId: string -} - -export function PeopleScreen({ navigation }: BottomTabScreenProps) { +export function PeopleScreen({}: BottomTabScreenProps) { + const { chat } = useContext(ChatContext) const [searchText, setSearchText] = useState("") const [tooltipShown, setTooltipShown] = useState(false) const [sortByActive, setSortByActive] = useState(true) - const { chat, memberships, users } = useContext(ChatContext) - const directChannels = memberships.map((m) => m.channel).filter((c) => c.type === "direct") - const entries: ListEntry[] = directChannels.flatMap((c) => { - const userId = getInterlocutorId(c) - if (!userId) return [] - const user = users.find((u) => u.id === userId) - return user - ? { title: user.name || user.id, active: !!user.active, channelId: c.id } - : { title: userId, active: false, channelId: c.id } - }) - - function getInterlocutorId(channel: Channel) { - if (!chat) return - return channel.id.replace("direct.", "").replace(chat.currentUser.id, "").replace("&", "") - } - - function sortEntries(a: ListEntry, b: ListEntry) { - if (sortByActive && a.active !== b.active) return a.active ? -1 : 1 - return a.title.localeCompare(b.title) - } return ( - - + chat && ( + + - - - + + + - - DIRECT MESSAGES + + DIRECT MESSAGES - setTooltipShown(!tooltipShown)}> - - + setTooltipShown(!tooltipShown)}> + + - {tooltipShown ? ( - - - - Sort by - + {tooltipShown ? ( + + + + Sort by + + + + - - - - ) : null} - - + ) : null} + + + + + - entry.title && entry.title.toLowerCase().includes(searchText.toLowerCase()) - ) - .sort(sortEntries)} - renderItem={({ item: entry }) => ( - navigation.navigate("Chat", { channelId: entry.channelId })} - // TODO: unread messages count badge - /> - )} - keyExtractor={(entry) => entry.channelId} - /> - + + + ) ) } const styles = StyleSheet.create({ container: { backgroundColor: colors.neutral0, - flex: 1, padding: 16, }, row: { diff --git a/samples/react-native-group-chat/screens/tabs/profile/ProfileScreen.tsx b/samples/react-native-group-chat/screens/tabs/profile/ProfileScreen.tsx index 265902d1..8c1b26e1 100644 --- a/samples/react-native-group-chat/screens/tabs/profile/ProfileScreen.tsx +++ b/samples/react-native-group-chat/screens/tabs/profile/ProfileScreen.tsx @@ -5,15 +5,8 @@ import { BottomSheetModal, BottomSheetBackdrop } from "@gorhom/bottom-sheet" import { BottomTabsParamList } from "../../../types" import { ChatContext } from "../../../context" -import { - Line, - Button, - Text, - Gap, - RandomAvatar, - TextInput, - colorPalette as colors, -} from "../../../ui-components" +import { Line, Button, Text, Gap, TextInput, colorPalette as colors } from "../../../ui-components" +import { Avatar } from "../../../components" export function ProfileScreen({ navigation, @@ -41,106 +34,106 @@ export function ProfileScreen({ } return ( - - + chat?.currentUser && ( + + + + - - - + + + + + + Name + + {userName} + + + + - + + + - - + - Name + Notifications - {userName} + - - + - - - - - - - Notifications + + Get notified about new messages and mentions from chats - - - + + + - - Get notified about new messages and mentions from chats - + + + Read receipts + + + - - - + - - - Read receipts + + You will see send or receive receipts - - - - - - - You will see send or receive receipts - - - - - - - - - - Change your name - - - - - - - - - + + + + + Change your name + + + + + + + + + + ) ) } @@ -148,7 +141,7 @@ const styles = StyleSheet.create({ container: { backgroundColor: colors.neutral0, flex: 1, - paddingHorizontal: 32, + padding: 32, }, row: { alignItems: "center", diff --git a/samples/react-native-group-chat/types.ts b/samples/react-native-group-chat/types.ts index ba4501f5..6b59cb83 100644 --- a/samples/react-native-group-chat/types.ts +++ b/samples/react-native-group-chat/types.ts @@ -1,5 +1,6 @@ import type { StackScreenProps } from "@react-navigation/stack" import { NavigationProp, NavigatorScreenParams } from "@react-navigation/native" + import { EnhancedIMessage } from "./utils" export type RootStackParamList = { @@ -9,10 +10,11 @@ export type RootStackParamList = { export type HomeStackParamList = { Home: { name: string } - Chat: { channelId: string } + Chat: undefined NewChat: undefined NewGroup: undefined ThreadReply: { parentMessage: EnhancedIMessage } + ChatSettings: undefined PinnedMessage: { channelId: string } } diff --git a/samples/react-native-group-chat/ui-components/accordion/Accordion.tsx b/samples/react-native-group-chat/ui-components/accordion/Accordion.tsx new file mode 100644 index 00000000..717e5a67 --- /dev/null +++ b/samples/react-native-group-chat/ui-components/accordion/Accordion.tsx @@ -0,0 +1,43 @@ +import { ReactNode, useState } from "react" +import { View, StyleSheet, TouchableOpacity } from "react-native" + +import { Text } from "../text" +import { MaterialCommunityIcons } from "@expo/vector-icons" +import { colorPalette } from "../defaultTheme" + +type AccordionProps = { + title: string + icons?: ReactNode + children: ReactNode +} + +export function Accordion({ title, children, icons, ...rest }: AccordionProps) { + const [expanded, setExpanded] = useState(true) + + return ( + + + {title} + + {icons} + setExpanded(!expanded)}> + + + + + {expanded && children} + + ) +} +const styles = StyleSheet.create({ + row: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + marginBottom: 16, + }, +}) diff --git a/samples/react-native-group-chat/ui-components/accordion/index.ts b/samples/react-native-group-chat/ui-components/accordion/index.ts new file mode 100644 index 00000000..e1447afd --- /dev/null +++ b/samples/react-native-group-chat/ui-components/accordion/index.ts @@ -0,0 +1 @@ +export * from "./Accordion" diff --git a/samples/react-native-group-chat/ui-components/button/Button.tsx b/samples/react-native-group-chat/ui-components/button/Button.tsx index dae8a309..dee1c89a 100644 --- a/samples/react-native-group-chat/ui-components/button/Button.tsx +++ b/samples/react-native-group-chat/ui-components/button/Button.tsx @@ -1,5 +1,5 @@ import { TouchableHighlight, TouchableHighlightProps, StyleSheet, View } from "react-native" -import { MaterialIcons } from "@expo/vector-icons" +import { MaterialIcons, MaterialCommunityIcons } from "@expo/vector-icons" import { Text } from "../text" import { colorPalette as colors } from "../defaultTheme" @@ -9,7 +9,9 @@ type ButtonProps = { size?: "lg" | "md" | "sm" align?: "center" | "left" icon?: keyof typeof MaterialIcons.glyphMap + iconCommunity?: keyof typeof MaterialCommunityIcons.glyphMap iconRight?: keyof typeof MaterialIcons.glyphMap + iconRightCommunity?: keyof typeof MaterialCommunityIcons.glyphMap } export function Button(props: ButtonProps & TouchableHighlightProps) { @@ -19,7 +21,9 @@ export function Button(props: ButtonProps & TouchableHighlightProps) { align = "center", children, icon, + iconCommunity, iconRight, + iconRightCommunity, style, disabled = false, ...rest @@ -35,21 +39,10 @@ export function Button(props: ButtonProps & TouchableHighlightProps) { {...rest} > - {icon ? ( - - ) : null} + {icon && } + {iconCommunity && ( + + )} - {iconRight ? ( - - ) : null} + {iconRight && } + {iconRightCommunity && ( + + )} ) @@ -117,14 +99,16 @@ const createStyles = ({ justifyContent: align === "center" ? "center" : "flex-start", }, icon: { - width: 20, - height: 20, marginLeft: -2, marginRight: 10, + color: { + base: colors.neutral50, + danger: colors.badge, + outlined: colors.navy700, + list: colors.teal800, + }[variant], }, iconRight: { - width: 20, - height: 20, marginLeft: 10, }, }) diff --git a/samples/react-native-group-chat/ui-components/defaultTheme.ts b/samples/react-native-group-chat/ui-components/defaultTheme.ts index 8bd912e8..954698f4 100644 --- a/samples/react-native-group-chat/ui-components/defaultTheme.ts +++ b/samples/react-native-group-chat/ui-components/defaultTheme.ts @@ -1,5 +1,4 @@ import { TextStyle } from "react-native" -import { useTheme, MD3LightTheme as DefaultTheme } from "react-native-paper" export const colorPalette = { sky50: "#F0F9FF", @@ -69,15 +68,9 @@ const textStyles = { } as { [variant: string]: TextStyle } export const defaultTheme = { - ...DefaultTheme, colors: { - ...DefaultTheme.colors, ...colorPalette, primary: colorPalette.neutral900, }, textStyles, } - -export function usePNTheme() { - return useTheme() as typeof defaultTheme -} diff --git a/samples/react-native-group-chat/ui-components/icon/Icon.tsx b/samples/react-native-group-chat/ui-components/icon/Icon.tsx deleted file mode 100644 index b416be17..00000000 --- a/samples/react-native-group-chat/ui-components/icon/Icon.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react" -import { StyleSheet } from "react-native" -import { IconButton } from "react-native-paper" -import { IconSource } from "react-native-paper/lib/typescript/components/Icon" -import { colorPalette, usePNTheme } from "../defaultTheme" - -type IconProps = { - icon: IconSource - iconColor?: keyof typeof colorPalette -} - -export function Icon({ icon, iconColor }: IconProps) { - const theme = usePNTheme() - - return ( - - ) -} - -const styles = StyleSheet.create({ - container: { - width: 24, - height: 24, - }, -}) diff --git a/samples/react-native-group-chat/ui-components/icon/index.ts b/samples/react-native-group-chat/ui-components/icon/index.ts deleted file mode 100644 index 03d66fa8..00000000 --- a/samples/react-native-group-chat/ui-components/icon/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./Icon" diff --git a/samples/react-native-group-chat/ui-components/index.ts b/samples/react-native-group-chat/ui-components/index.ts index 85ee68cf..6d22e096 100644 --- a/samples/react-native-group-chat/ui-components/index.ts +++ b/samples/react-native-group-chat/ui-components/index.ts @@ -1,8 +1,7 @@ export * from "./button" export * from "./gap" -export * from "./icon" export * from "./line" -export * from "./random-avatar" export * from "./text" export * from "./defaultTheme" export * from "./text-input" +export * from "./accordion" diff --git a/samples/react-native-group-chat/ui-components/random-avatar/RandomAvatar.tsx b/samples/react-native-group-chat/ui-components/random-avatar/RandomAvatar.tsx deleted file mode 100644 index 1ac2a083..00000000 --- a/samples/react-native-group-chat/ui-components/random-avatar/RandomAvatar.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { useMemo } from "react" -import Avatar1 from "../../assets/avatars/avatar1.png" -import Avatar2 from "../../assets/avatars/avatar2.png" -import Avatar3 from "../../assets/avatars/avatar3.png" -import Avatar4 from "../../assets/avatars/avatar4.png" -import Avatar5 from "../../assets/avatars/avatar5.png" -import Avatar6 from "../../assets/avatars/avatar6.png" -import Avatar7 from "../../assets/avatars/avatar7.png" -import Avatar8 from "../../assets/avatars/avatar8.png" -import Avatar9 from "../../assets/avatars/avatar9.png" -import Avatar10 from "../../assets/avatars/avatar10.png" -import Avatar11 from "../../assets/avatars/avatar11.png" -import Avatar12 from "../../assets/avatars/avatar12.png" -import Avatar13 from "../../assets/avatars/avatar13.png" -import Avatar14 from "../../assets/avatars/avatar14.png" -import Avatar15 from "../../assets/avatars/avatar15.png" -import Avatar16 from "../../assets/avatars/avatar16.png" -import { Avatar } from "react-native-paper" - -const avatars = { - 1: Avatar1, - 2: Avatar2, - 3: Avatar3, - 4: Avatar4, - 5: Avatar5, - 6: Avatar6, - 7: Avatar7, - 8: Avatar8, - 9: Avatar9, - 10: Avatar10, - 11: Avatar11, - 12: Avatar12, - 13: Avatar13, - 14: Avatar14, - 15: Avatar15, - 16: Avatar16, -} - -export function getRandomAvatar() { - return avatars[Math.floor(Math.random() * 17) as keyof typeof avatars] -} - -type RandomAvatarProps = { - size?: number -} - -export function RandomAvatar({ size = 27 }: RandomAvatarProps) { - const randomSource = useMemo(() => { - return getRandomAvatar() - }, []) - - return -} diff --git a/samples/react-native-group-chat/ui-components/random-avatar/index.ts b/samples/react-native-group-chat/ui-components/random-avatar/index.ts deleted file mode 100644 index fd368843..00000000 --- a/samples/react-native-group-chat/ui-components/random-avatar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./RandomAvatar" diff --git a/yarn.lock b/yarn.lock index bd8aadac..b0bd5e17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2323,14 +2323,6 @@ resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@callstack/react-theme-provider@^3.0.9": - version "3.0.9" - resolved "https://registry.npmjs.org/@callstack/react-theme-provider/-/react-theme-provider-3.0.9.tgz" - integrity sha512-tTQ0uDSCL0ypeMa8T/E9wAZRGKWj8kXP7+6RYgPTfOPs9N07C9xM8P02GJ3feETap4Ux5S69D9nteq9mEj86NA== - dependencies: - deepmerge "^3.2.0" - hoist-non-react-statics "^3.3.0" - "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz" @@ -7160,7 +7152,7 @@ collect-v8-coverage@^1.0.0: resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz" integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== -color-convert@^1.9.0, color-convert@^1.9.3: +color-convert@^1.9.0: version "1.9.3" resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -7184,7 +7176,7 @@ color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.6.0, color-string@^1.9.0: +color-string@^1.9.0: version "1.9.1" resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz" integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== @@ -7197,14 +7189,6 @@ color-support@^1.1.3: resolved "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== -color@^3.1.2: - version "3.2.1" - resolved "https://registry.npmjs.org/color/-/color-3.2.1.tgz" - integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== - dependencies: - color-convert "^1.9.3" - color-string "^1.6.0" - color@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/color/-/color-4.2.3.tgz" @@ -7855,11 +7839,6 @@ deepmerge-ts@^4.2.2: resolved "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-4.3.0.tgz" integrity sha512-if3ZYdkD2dClhnXR5reKtG98cwyaRT1NeugQoAPTTfsOpV9kqyeiBF9Qa5RHjemb3KzD5ulqygv6ED3t5j9eJw== -deepmerge@^3.2.0: - version "3.3.0" - resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-3.3.0.tgz" - integrity sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA== - deepmerge@^4.2.2, deepmerge@^4.3.0, deepmerge@^4.3.1: version "4.3.1" resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz" @@ -14803,15 +14782,6 @@ react-native-lightbox-v2@0.9.0: resolved "https://registry.npmjs.org/react-native-lightbox-v2/-/react-native-lightbox-v2-0.9.0.tgz" integrity sha512-Fc5VFHFj2vokS+OegyTsANKb1CYoUlOtAv+EBH5wtpJn1b5cey6jVXH7136G5+8OC9JmKWSgKHc5thFwOoZTUg== -react-native-paper@^5.10.3: - version "5.10.3" - resolved "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.10.3.tgz" - integrity sha512-UXT+cOnycpepAhE1t7E4PqFk5mSLtCA5BSQkt6xGi7XJxnpBF3v4orcHwG/OQEOWx7TpWKGPCjhDewnZodOl5Q== - dependencies: - "@callstack/react-theme-provider" "^3.0.9" - color "^3.1.2" - use-latest-callback "^0.1.5" - react-native-parsed-text@0.0.22: version "0.0.22" resolved "https://registry.npmjs.org/react-native-parsed-text/-/react-native-parsed-text-0.0.22.tgz"