diff --git a/lib/src/entities/channel.ts b/lib/src/entities/channel.ts index d62c610c..a0f86b9f 100644 --- a/lib/src/entities/channel.ts +++ b/lib/src/entities/channel.ts @@ -420,11 +420,11 @@ export class Channel { this.disconnect = this.connect(callback) return { - membership: Membership.fromMembershipDTO( + membership: await Membership.fromMembershipDTO( this.chat, membershipsResponse.data[0], this.chat.currentUser as User - ), + ).setLastReadMessageTimetoken(String((await this.chat.sdk.time()).timetoken)), disconnect: this.disconnect, } } catch (error) { @@ -490,7 +490,11 @@ export class Channel { filter: `channel.id == '${this.id}'`, }) - return Membership.fromMembershipDTO(this.chat, response.data[0], user) + return await Membership.fromMembershipDTO( + this.chat, + response.data[0], + user + ).setLastReadMessageTimetoken(String((await this.chat.sdk.time()).timetoken)) } catch (error) { throw error } @@ -513,9 +517,14 @@ export class Channel { }, filter, }) + const { timetoken } = await this.chat.sdk.time() - return response.data.map((dataPoint) => - Membership.fromChannelMemberDTO(this.chat, dataPoint, this) + return await Promise.all( + response.data.map((dataPoint) => + Membership.fromChannelMemberDTO(this.chat, dataPoint, this).setLastReadMessageTimetoken( + String(timetoken) + ) + ) ) } catch (error) { throw error diff --git a/lib/src/entities/chat.ts b/lib/src/entities/chat.ts index 7cd4931e..38fb64ad 100644 --- a/lib/src/entities/chat.ts +++ b/lib/src/entities/chat.ts @@ -957,24 +957,20 @@ export class Chat { async getUnreadMessagesCounts(params: Omit = {}) { const userMemberships = await this.currentUser.getMemberships(params) - const membershipsWithTimetokens = userMemberships.memberships.filter( - (membership) => membership.lastReadMessageTimetoken - ) - const relevantTimetokens = membershipsWithTimetokens.map((m) => m.lastReadMessageTimetoken) - const relevantChannelIds = membershipsWithTimetokens.map((m) => m.channel.id) - - if (!relevantChannelIds.length) { + if (!userMemberships.memberships.length) { return [] } const response = await this.sdk.messageCounts({ - channels: relevantChannelIds, - channelTimetokens: relevantTimetokens as string[], + channels: userMemberships.memberships.map((m) => m.channel.id), + channelTimetokens: userMemberships.memberships.map( + (m) => m.lastReadMessageTimetoken || "0" + ) as string[], }) return Object.keys(response.channels) .map((key) => { - const relevantMembership = membershipsWithTimetokens.find((m) => m.channel.id === key) + const relevantMembership = userMemberships.memberships.find((m) => m.channel.id === key) if (!relevantMembership) { throw `Cannot find channel with id ${key}` @@ -992,11 +988,7 @@ export class Chat { async markAllMessagesAsRead(params: Omit = {}) { const userMemberships = await this.currentUser.getMemberships(params) - const membershipsWithTimetokens = userMemberships.memberships.filter( - (membership) => membership.lastReadMessageTimetoken - ) - - const relevantChannelIds = membershipsWithTimetokens.map((m) => m.channel.id) + const relevantChannelIds = userMemberships.memberships.map((m) => m.channel.id) if (!relevantChannelIds.length) { return @@ -1012,12 +1004,12 @@ export class Chat { lastMessagesFromMembershipChannels.channels[encodeURIComponent(relevantChannelId)] const relevantLastMessageTimetoken = - relevantLastMessage && relevantLastMessage[0] ? relevantLastMessage[0].timetoken : "" + relevantLastMessage && relevantLastMessage[0] ? relevantLastMessage[0].timetoken : "0" return { id: relevantChannelId, custom: { - ...membershipsWithTimetokens[i].custom, + ...userMemberships.memberships[i].custom, lastReadMessageTimetoken: relevantLastMessageTimetoken, }, } diff --git a/lib/src/entities/membership.ts b/lib/src/entities/membership.ts index 61f54cbb..c0d12441 100644 --- a/lib/src/entities/membership.ts +++ b/lib/src/entities/membership.ts @@ -135,16 +135,20 @@ export class Membership { } async setLastReadMessage(message: Message) { + return this.setLastReadMessageTimetoken(message.timetoken) + } + + async setLastReadMessageTimetoken(timetoken: string) { try { const response = await this.update({ - custom: { ...this.custom, lastReadMessageTimetoken: message.timetoken }, + custom: { ...this.custom, lastReadMessageTimetoken: timetoken }, }) await this.chat.emitEvent({ channel: this.channel.id, type: "receipt", method: "signal", - payload: { messageTimetoken: message.timetoken }, + payload: { messageTimetoken: timetoken }, }) return response diff --git a/lib/tests/channel.test.ts b/lib/tests/channel.test.ts index 0be79770..e809bc97 100644 --- a/lib/tests/channel.test.ts +++ b/lib/tests/channel.test.ts @@ -285,7 +285,7 @@ describe("Channel test", () => { expect(unreadCount).toBe(false) const { messages } = await channel.getHistory() - membership = await membership.setLastReadMessage(messages[0]) + membership = await membership.setLastReadMessageTimetoken(messages[0].timetoken) unreadCount = await membership.getUnreadMessagesCount() expect(unreadCount).toBe(1) @@ -651,7 +651,7 @@ describe("Channel test", () => { const { timetoken } = await channel.sendText("New message") await sleep(150) // history calls have around 130ms of cache time const message = await channel.getMessage(timetoken) - await membership.setLastReadMessage(message) + await membership.setLastReadMessageTimetoken(message.timetoken) await sleep(150) // history calls have around 130ms of cache time expect(mockCallback).toHaveBeenCalledTimes(2) diff --git a/samples/react-native-group-chat/.env.example b/samples/react-native-group-chat/.env.example index 8e1f18c9..da835efe 100644 --- a/samples/react-native-group-chat/.env.example +++ b/samples/react-native-group-chat/.env.example @@ -1,2 +1,2 @@ -EXPO_PUBNUB_SUB_KEY= -EXPO_PUBNUB_PUB_KEY= \ No newline at end of file +EXPO_PUBLIC_PUBNUB_SUB_KEY= +EXPO_PUBLIC_PUBNUB_PUB_KEY= diff --git a/samples/react-native-group-chat/App.tsx b/samples/react-native-group-chat/App.tsx index 8c0da957..7d604fa2 100644 --- a/samples/react-native-group-chat/App.tsx +++ b/samples/react-native-group-chat/App.tsx @@ -1,19 +1,18 @@ -import { useContext, useEffect, useState } from "react" +import React, { useCallback, useContext, useEffect, useMemo, useState } from "react" import { View, StyleSheet, ActivityIndicator, - KeyboardAvoidingView, - Platform, LogBox, + TouchableHighlight, + TouchableOpacity, } 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 { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons" +import { MaterialCommunityIcons } from "@expo/vector-icons" import { StatusBar } from "expo-status-bar" import { Channel, Chat, Membership, User } from "@pubnub/chat" import { @@ -24,20 +23,32 @@ import { } from "@expo-google-fonts/roboto" import "react-native-get-random-values" -import { MentionsScreen, HomeStackScreen, ProfileScreen } from "./screens/tabs" -import { LoginScreen } from "./screens/ordinary" +import { HomeStackScreen } from "./screens/tabs" +import { + ChatScreen, + ChatSettings, + LoginScreen, + NewChatScreen, + NewGroupScreen, + PinnedMessage, + ThreadReply, +} from "./screens/ordinary" import { ChatContext } from "./context" -import { RootStackParamList, BottomTabsParamList } from "./types" -import { defaultTheme, colorPalette as colors } from "./ui-components" +import { RootStackParamList } from "./types" +import { defaultTheme, colorPalette as colors, Text } from "./ui-components" +import { Avatar } from "./components" LogBox.ignoreLogs(["Require cycle:", "Sending"]) -const Tab = createBottomTabNavigator() const MainStack = createStackNavigator() -function TabNavigator({ route }: StackScreenProps) { +function MainRoutesNavigator({ route }: StackScreenProps) { const { name } = route.params - const { setChat, chat } = useContext(ChatContext) + const { setChat, chat, currentChannel, currentChannelMembers } = useContext(ChatContext) + + const interlocutor = + currentChannel?.type === "direct" && + currentChannelMembers.map((m) => m.user).filter((u) => u.id !== chat?.currentUser.id)[0] useEffect(() => { async function init() { @@ -65,7 +76,7 @@ function TabNavigator({ route }: StackScreenProps) { } return ( - ) { }, headerStatusBarHeight: 0, // there's some extra padding on top of the header without this headerTintColor: colors.neutral0, - tabBarStyle: { backgroundColor: colors.navy50 }, - tabBarActiveTintColor: colors.neutral900, - tabBarInactiveTintColor: colors.navy500, - tabBarHideOnKeyboard: true, }} > - ( - - ), + headerTitle: "Home", }} /> - ( - - ), - }} + ({ + headerTitle: () => + currentChannel && ( + navigation.navigate("ChatSettings")} + style={{ paddingVertical: 8, paddingHorizontal: 30, borderRadius: 6 }} + > + + + + {interlocutor ? interlocutor.name : currentChannel?.name} + + + + ), + headerRight: () => { + return ( + navigation.navigate("PinnedMessage")} + style={{ paddingRight: 24 }} + > + + + ) + }, + })} /> - ( - - ), - }} + + + + - + + ) } function App() { const [chat, setChat] = useState(null) const [users, setUsers] = useState([]) + const [interlocutors, setInterlocutors] = useState<{ [channelId: string]: User }>({}) const [loading, setLoading] = useState(false) const [currentChannel, setCurrentChannel] = useState() const [currentChannelMembers, setCurrentChannelMembers] = useState([]) const [userMemberships, setUserMemberships] = useState([]) - async function setCurrentChannelWithMembers(channel: Channel | null) { + const setCurrentChannelWithMembers = useCallback(async (channel: Channel | null) => { if (!channel) { setCurrentChannelMembers([]) setCurrentChannel(null) @@ -133,27 +170,48 @@ function App() { const { members } = await channel.getMembers() setCurrentChannelMembers(members) setCurrentChannel(channel) - } + }, []) + + const getUser = useCallback( + (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 + }, + [chat, users] + ) + + const getInterlocutor = useCallback( + (channel: Channel) => { + if (!chat) return null - 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]) + if (interlocutors[channel.id]) { + return getUser(interlocutors[channel.id].id) + } + + channel.getMembers().then(({ members }) => { + const filteredMembers = members.filter((m) => m.user.id !== chat.currentUser.id) + const user = filteredMembers.length ? filteredMembers[0].user : null + + if (!user) { + return + } + + setInterlocutors((currentInterlocutors) => ({ + ...currentInterlocutors, + [channel.id]: user, + })) }) - return null - } - return existingUser - } - function getInterlocutor(channel: Channel) { - if (!chat) return null - const userId = channel.id - .replace("direct.", "") - .replace(chat?.currentUser.id, "") - .replace("&", "") - return getUser(userId) - } + return null + }, + [chat, getUser, interlocutors] + ) const [fontsLoaded] = useFonts({ Roboto_400Regular, @@ -161,28 +219,40 @@ function App() { Roboto_700Bold, }) + const contextValue = useMemo( + () => ({ + loading, + setLoading, + chat, + setChat, + currentChannel, + setCurrentChannel: setCurrentChannelWithMembers, + currentChannelMembers, + users, + setUsers, + getUser, + getInterlocutor, + memberships: userMemberships, + setMemberships: setUserMemberships, + }), + [ + chat, + currentChannel, + currentChannelMembers, + getInterlocutor, + getUser, + loading, + userMemberships, + users, + ] + ) + if (!fontsLoaded) { return null } return ( - + @@ -191,18 +261,12 @@ function App() { style={[styles.safeArea, { backgroundColor: defaultTheme.colors.navy800 }]} edges={["top", "left", "right"]} > - {/* TODO: for some reason KeyboardAvoidingView doesn't work on any page other than login */} - - - - - - - - + + + + + + diff --git a/samples/react-native-group-chat/components/bottom-sheet/BottomSheetTextInput.tsx b/samples/react-native-group-chat/components/bottom-sheet/BottomSheetTextInput.tsx new file mode 100644 index 00000000..a16db906 --- /dev/null +++ b/samples/react-native-group-chat/components/bottom-sheet/BottomSheetTextInput.tsx @@ -0,0 +1,66 @@ +import React from "react" +import { BottomSheetTextInput as GorhomBottomSheetTextInput } from "@gorhom/bottom-sheet" +import { MaterialIcons } from "@expo/vector-icons" +import { StyleSheet, View, ViewStyle } from "react-native" +import { TextInputProps as RNTextInputProps } from "react-native/Libraries/Components/TextInput/TextInput" +import { colorPalette as colors, Gap, Text } from "../../ui-components" + +type BottomSheetTextInputProps = { + label?: string + icon?: keyof typeof MaterialIcons.glyphMap + variant?: "base" | "search" + containerStyle?: ViewStyle +} + +export const BottomSheetTextInput = ({ + variant = "base", + label, + icon, + containerStyle, + style, + ...rest +}: BottomSheetTextInputProps & RNTextInputProps) => { + const styles = createStyles({ variant }) + return ( + + {label ? ( + <> + {label} + + + ) : null} + + + + + + ) +} + +const createStyles = ({ variant }: Required>) => + StyleSheet.create({ + wrapper: { + alignItems: "center", + borderRadius: 6, + borderWidth: 1, + flexDirection: "row", + justifyContent: "center", + paddingHorizontal: 12, + borderColor: variant === "base" ? colors.navy300 : colors.neutral50, + height: variant === "base" ? 48 : 42, + backgroundColor: variant === "base" ? undefined : colors.neutral50, + }, + icon: { + marginRight: 8, + }, + input: { + alignSelf: "stretch", + flex: 1, + fontFamily: "Roboto_400Regular", + fontSize: 16, + }, + }) diff --git a/samples/react-native-group-chat/components/bottom-sheet/index.ts b/samples/react-native-group-chat/components/bottom-sheet/index.ts new file mode 100644 index 00000000..47f10efc --- /dev/null +++ b/samples/react-native-group-chat/components/bottom-sheet/index.ts @@ -0,0 +1 @@ +export * from "./BottomSheetTextInput" 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 0533627d..a17423f3 100644 --- a/samples/react-native-group-chat/screens/ordinary/chat/Chat.tsx +++ b/samples/react-native-group-chat/screens/ordinary/chat/Chat.tsx @@ -1,5 +1,12 @@ -import React, { useState, useCallback, useEffect, useContext, useRef } from "react" -import { StyleSheet, View, ActivityIndicator, TouchableOpacity, FlatList } from "react-native" +import React, { useState, useCallback, useEffect, useContext, useRef, useMemo } from "react" +import { + StyleSheet, + View, + ActivityIndicator, + TouchableOpacity, + FlatList, + SafeAreaView, +} from "react-native" import { GiftedChat, Bubble } from "react-native-gifted-chat" import { StackScreenProps } from "@react-navigation/stack" import { User, MessageDraft, Message, Channel } from "@pubnub/chat" @@ -27,8 +34,9 @@ export function ChatScreen({}: StackScreenProps) { const giftedChatRef = useRef>(null) const [lastAffectedNameOccurrenceIndex, setLastAffectedNameOccurrenceIndex] = useState(-1) const [text, setText] = useState("") - const currentChannelMembership = currentChannelMembers.find( - (m) => m.user.id === chat?.currentUser.id + const currentChannelMembership = useMemo( + () => currentChannelMembers.find((m) => m.user.id === chat?.currentUser.id), + [chat?.currentUser.id, currentChannelMembers] ) const { renderFooter, renderMessageText, renderChatFooter } = useCommonChatRenderers({ typingData, @@ -61,17 +69,12 @@ export function ChatScreen({}: StackScreenProps) { } await message.pin() - const refreshedChannel = await chat.getChannel(currentChannel.id) - if (refreshedChannel) { - setCurrentChannel(refreshedChannel) - } }, [chat, currentChannel, setCurrentChannel] ) const handleEmoji = useCallback( (message: Message) => { - console.log("message", message) const copiedMessages = [...giftedChatMappedMessages] const index = copiedMessages.findIndex( @@ -153,8 +156,8 @@ export function ChatScreen({}: StackScreenProps) { const historicalMessagesObject = await currentChannel.getHistory({ count: 5 }) if (currentChannelMembership && historicalMessagesObject.messages.length) { - await currentChannelMembership.setLastReadMessage( - historicalMessagesObject.messages[historicalMessagesObject.messages.length - 1] + await currentChannelMembership.setLastReadMessageTimetoken( + historicalMessagesObject.messages[historicalMessagesObject.messages.length - 1].timetoken ) } @@ -190,7 +193,7 @@ export function ChatScreen({}: StackScreenProps) { } switchChannelImplementation() - }, [currentChannel, currentChannelMembership, getUser]) + }, [currentChannel, currentChannelMembership]) useEffect(() => { if (!currentChannel) { @@ -199,7 +202,7 @@ export function ChatScreen({}: StackScreenProps) { const disconnect = currentChannel.connect((message) => { if (currentChannelMembership) { - currentChannelMembership.setLastReadMessage(message) + currentChannelMembership.setLastReadMessageTimetoken(message.timetoken) } setGiftedChatMappedMessages((currentMessages) => GiftedChat.append(currentMessages, [ @@ -211,7 +214,7 @@ export function ChatScreen({}: StackScreenProps) { return () => { disconnect() } - }, [currentChannel, currentChannelMembership, getUser]) + }, [currentChannel, currentChannelMembership]) const resetInput = () => { if (!messageDraft) { @@ -232,12 +235,13 @@ export function ChatScreen({}: StackScreenProps) { } const handleInputChange = useCallback( - (text: string) => { - if (!messageDraft || text === "") { + (giftedChatText: string) => { + if (!messageDraft || giftedChatText === "") { + setText("") return } - messageDraft.onChange(text).then((suggestionObject) => { + messageDraft.onChange(giftedChatText).then((suggestionObject) => { setSuggestedData( suggestionObject.users.suggestedUsers.length ? suggestionObject.users.suggestedUsers @@ -253,7 +257,7 @@ export function ChatScreen({}: StackScreenProps) { setText(messageDraft.value) setShowSuggestedData(true) }, - [messageDraft, currentChannel] + [messageDraft] ) const renderBubble = (props: Bubble["props"]) => { @@ -294,7 +298,7 @@ export function ChatScreen({}: StackScreenProps) { } return ( - + onSend(messages)} @@ -321,7 +325,7 @@ export function ChatScreen({}: StackScreenProps) { messageContainerRef={giftedChatRef} /> - + ) } diff --git a/samples/react-native-group-chat/screens/ordinary/login-screen/LoginScreen.tsx b/samples/react-native-group-chat/screens/ordinary/login-screen/LoginScreen.tsx index 86147ca3..74a17e2b 100644 --- a/samples/react-native-group-chat/screens/ordinary/login-screen/LoginScreen.tsx +++ b/samples/react-native-group-chat/screens/ordinary/login-screen/LoginScreen.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react" -import { View, StyleSheet } from "react-native" +import { View, StyleSheet, Platform, KeyboardAvoidingView } from "react-native" import { StackScreenProps } from "@react-navigation/stack" import { Text, Button, Gap, TextInput, colorPalette as colors } from "../../../ui-components" @@ -10,27 +10,32 @@ export function LoginScreen({ navigation }: StackScreenProps - - - - - Log in to Sample Chat App - - - - - Built with PubNub Chat SDK for JavaScript and TypeScript. - - - - - - - - + + + + + + + Log in to Sample Chat App + + + + + Built with PubNub Chat SDK for JavaScript and TypeScript. + + + + + + + + + ) } 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 7867e9ee..d6c8e1b3 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 @@ -21,6 +21,14 @@ export function NewChatScreen({ navigation }: StackScreenProps { + await chat.emitEvent({ + channel: u.id, + method: "publish", + payload: { + action: "GROUP_CONVERSATION_STARTED", + channelId: channel.id, + }, + }) + }) + ) setCurrentChannel(channel) setLoading(false) } 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 bf0aa8e4..df64e873 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 @@ -16,12 +16,23 @@ export function PinnedMessage({}: StackScreenProps { async function init() { if (!chat || !currentChannel) return - setMessage(await currentChannel.getPinnedMessage()) + const refreshedChannel = await chat.getChannel(currentChannel.id) + if (refreshedChannel) { + setMessage(await refreshedChannel.getPinnedMessage()) + } } init() }, [chat, currentChannel]) + useEffect(() => { + const unstream = currentChannel?.streamUpdates(async (channel) => { + setMessage(await channel.getPinnedMessage()) + }) + + return unstream + }, [currentChannel]) + const renderMessageBubble = useCallback( (props: Bubble["props"]) => { if (!message) { diff --git a/samples/react-native-group-chat/screens/tabs/home/HomeScreen.tsx b/samples/react-native-group-chat/screens/tabs/home/HomeScreen.tsx index 9cd674bf..10c76c46 100644 --- a/samples/react-native-group-chat/screens/tabs/home/HomeScreen.tsx +++ b/samples/react-native-group-chat/screens/tabs/home/HomeScreen.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useContext, useEffect, useState } from "react" +import React, { useCallback, useContext, useEffect, useMemo, useState } from "react" import { StyleSheet, ScrollView, TouchableHighlight, TouchableOpacity } from "react-native" import { useFocusEffect } from "@react-navigation/native" import { StackScreenProps } from "@react-navigation/stack" @@ -18,7 +18,8 @@ export function HomeScreen({ navigation }: StackScreenProps([]) - const channels = memberships.map((m) => m.channel) + const channels = useMemo(() => memberships.map((m) => m.channel), [memberships]) + const currentUserDirectChannels = channels.filter((c) => c.type === "direct") const currentUserGroupChannels = channels.filter((c) => c.type === "group") const currentUserPublicChannels = channels.filter((c) => c.type === "public") @@ -35,26 +36,72 @@ export function HomeScreen({ navigation }: StackScreenProps { - async function init() { - if (!chat) return - const [, { memberships }, { users }] = await Promise.all([ - fetchUnreadMessagesCount(), - chat.currentUser.getMemberships(), - chat.getUsers({}), - ]) - - setUsers(users) - setMemberships(memberships) + if (!chat) { + return } - init() - }, [chat, fetchUnreadMessagesCount, setMemberships, setUsers]) + const removeDirectChatListener = chat.listenForEvents({ + channel: chat.currentUser.id, + type: "custom", + method: "publish", + callback: async (evt) => { + if (evt.payload.action === "DIRECT_CONVERSATION_STARTED") { + const { memberships } = await chat.currentUser.getMemberships() + setMemberships(memberships) + } + }, + }) + + const removeGroupChatListener = chat.listenForEvents({ + channel: chat.currentUser.id, + type: "custom", + method: "publish", + callback: async (evt) => { + if (evt.payload.action === "GROUP_CONVERSATION_STARTED") { + const { memberships } = await chat.currentUser.getMemberships() + setMemberships(memberships) + } + }, + }) + + return () => { + removeDirectChatListener() + removeGroupChatListener() + } + }, [chat]) + + useEffect(() => { + const disconnectFuncs = channels.map((ch) => + ch.connect((message) => { + fetchUnreadMessagesCount() + }) + ) + + return () => { + disconnectFuncs.forEach((func) => func()) + } + }, [channels, memberships]) useFocusEffect( React.useCallback(() => { - fetchUnreadMessagesCount() - setCurrentChannel(null) - }, []) + async function handleScreenFocus() { + if (!chat) { + return + } + setCurrentChannel(null) + + const [, { memberships: refreshedMemberships }, { users }] = await Promise.all([ + fetchUnreadMessagesCount(), + chat.currentUser.getMemberships(), + chat.getUsers({}), + ]) + + setUsers(users) + setMemberships(refreshedMemberships) + } + + handleScreenFocus() + }, [chat, fetchUnreadMessagesCount, setCurrentChannel, setMemberships, setUsers]) ) const getFilteredChannels = useCallback( @@ -66,14 +113,11 @@ export function HomeScreen({ navigation }: StackScreenProps { - return unreadChannelCounts.filter( - (c) => c.channel.name && c.channel.name.toLowerCase().includes(searchText.toLowerCase()) - ) - }, - [searchText] - ) + const getFilteredUnreadChannels = useCallback(() => { + return unreadChannels.filter( + (c) => c.channel.name && c.channel.name.toLowerCase().includes(searchText.toLowerCase()) + ) + }, [searchText, unreadChannels]) const markAllMessagesAsRead = useCallback(async () => { if (!chat) return @@ -104,7 +148,7 @@ export function HomeScreen({ navigation }: StackScreenProps } > - {getFilteredUnreadChannels(unreadChannels).map(({ channel, count }) => { + {getFilteredUnreadChannels().map(({ channel, count }) => { const interlocutor = channel.type === "direct" && getInterlocutor(channel) const source = interlocutor || channel diff --git a/samples/react-native-group-chat/screens/tabs/home/Navigator.tsx b/samples/react-native-group-chat/screens/tabs/home/Navigator.tsx index 2cb56416..42f4139d 100644 --- a/samples/react-native-group-chat/screens/tabs/home/Navigator.tsx +++ b/samples/react-native-group-chat/screens/tabs/home/Navigator.tsx @@ -1,32 +1,24 @@ import React, { useContext } from "react" -import { createStackNavigator, StackScreenProps } from "@react-navigation/stack" -import { View, TouchableHighlight, TouchableOpacity } from "react-native" -import { MaterialCommunityIcons } from "@expo/vector-icons" +import { StackScreenProps } from "@react-navigation/stack" +import { View } from "react-native" +import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons" -import { BottomTabsParamList, HomeStackParamList } from "../../../types" +import { BottomTabsParamList, RootStackParamList } from "../../../types" import { Text, colorPalette as colors } from "../../../ui-components" import { ChatContext } from "../../../context" import { Avatar } from "../../../components" import { HomeScreen } from "./HomeScreen" -import { - ChatScreen, - NewChatScreen, - NewGroupScreen, - ThreadReply, - ChatSettings, - PinnedMessage, -} from "../../ordinary" +import { createBottomTabNavigator } from "@react-navigation/bottom-tabs" +import { MentionsScreen } from "../mentions" +import { ProfileScreen } from "../profile" -const HomeStack = createStackNavigator() +const Tab = createBottomTabNavigator() -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] +export function HomeStackScreen({ route }: StackScreenProps) { + const { chat } = useContext(ChatContext) return ( - - ), + tabBarLabel: "Home", + tabBarIcon: ({ color }) => ( + + ), })} /> - ({ - headerTitle: () => - currentChannel && ( - navigation.navigate("ChatSettings")} - style={{ paddingVertical: 8, paddingHorizontal: 30, borderRadius: 6 }} - > - - - - {interlocutor ? interlocutor.name : currentChannel?.name} - - - - ), - headerRight: () => { - return ( - - navigation.navigate("PinnedMessage", { channelId: currentChannel?.id }) - } - style={{ paddingRight: 24 }} - > - - - ) - }, - })} - /> - - - - ( + + ), + }} /> - ( + + ), + }} /> - + ) } 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 dba29d07..ed51af09 100644 --- a/samples/react-native-group-chat/screens/tabs/profile/ProfileScreen.tsx +++ b/samples/react-native-group-chat/screens/tabs/profile/ProfileScreen.tsx @@ -5,8 +5,9 @@ import { BottomSheetModal, BottomSheetBackdrop } from "@gorhom/bottom-sheet" import { BottomTabsParamList } from "../../../types" import { ChatContext } from "../../../context" -import { Button, Text, Gap, TextInput, colorPalette as colors } from "../../../ui-components" +import { Button, Text, Gap, colorPalette as colors } from "../../../ui-components" import { Avatar } from "../../../components" +import { BottomSheetTextInput } from "../../../components/bottom-sheet" export function ProfileScreen({ navigation, @@ -76,7 +77,7 @@ export function ProfileScreen({ Change your name - + @@ -107,4 +108,14 @@ const styles = StyleSheet.create({ borderRadius: 6, width: 120, }, + textInput: { + alignSelf: "stretch", + marginHorizontal: 12, + marginBottom: 12, + padding: 12, + borderRadius: 12, + backgroundColor: "white", + color: "black", + textAlign: "center", + }, }) diff --git a/samples/react-native-group-chat/types.ts b/samples/react-native-group-chat/types.ts index 56507cc8..6754e987 100644 --- a/samples/react-native-group-chat/types.ts +++ b/samples/react-native-group-chat/types.ts @@ -5,23 +5,24 @@ import { EnhancedIMessage } from "./utils" export type RootStackParamList = { login: undefined - tabs: { name: string } -} - -export type HomeStackParamList = { - Home: { name: string } + mainRoutes: { name: string } Chat: undefined NewChat: undefined NewGroup: undefined ThreadReply: { parentMessage: EnhancedIMessage } ChatSettings: undefined PinnedMessage: undefined + HomeStack: NavigatorScreenParams & { name: string } +} + +export type HomeStackParamList = { + Home: { name: string } } export type HomeStackNavigation = NavigationProp export type BottomTabsParamList = { - HomeStack: NavigatorScreenParams & { name: string } + Home: { name: string } Mentions: undefined Profile: undefined }