Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lib): resolve certain issues with the sample and fix unread messages #138

Merged
merged 9 commits into from
Oct 23, 2023
27 changes: 15 additions & 12 deletions lib/src/entities/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -960,21 +960,28 @@ export class Chat {
const membershipsWithTimetokens = userMemberships.memberships.filter(
(membership) => membership.lastReadMessageTimetoken
)
const membershipsWithoutTimetokens = userMemberships.memberships.filter(
(membership) => !membership.lastReadMessageTimetoken
)
const relevantTimetokens = membershipsWithTimetokens.map((m) => m.lastReadMessageTimetoken)
const relevantChannelIds = membershipsWithTimetokens.map((m) => m.channel.id)
const channelIdsWithTimetokens = membershipsWithTimetokens.map((m) => m.channel.id)
const channelIdsWithoutTimetokens = membershipsWithoutTimetokens.map((m) => m.channel.id)

if (!relevantChannelIds.length) {
if (!channelIdsWithTimetokens.length && !channelIdsWithoutTimetokens.length) {
return []
}

const response = await this.sdk.messageCounts({
channels: relevantChannelIds,
channelTimetokens: relevantTimetokens as string[],
channels: [...channelIdsWithTimetokens, ...channelIdsWithoutTimetokens],
channelTimetokens: [
...relevantTimetokens,
...channelIdsWithoutTimetokens.map(() => "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}`
Expand All @@ -992,11 +999,7 @@ export class Chat {
async markAllMessagesAsRead(params: Omit<GetMembershipsParametersv2, "include"> = {}) {
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
Expand All @@ -1012,12 +1015,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,
},
}
Expand Down
244 changes: 155 additions & 89 deletions samples/react-native-group-chat/App.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<BottomTabsParamList>()
const MainStack = createStackNavigator<RootStackParamList>()

function TabNavigator({ route }: StackScreenProps<RootStackParamList, "tabs">) {
function MainRoutesNavigator({ route }: StackScreenProps<RootStackParamList, "mainRoutes">) {
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() {
Expand Down Expand Up @@ -65,65 +76,93 @@ function TabNavigator({ route }: StackScreenProps<RootStackParamList, "tabs">) {
}

return (
<Tab.Navigator
<MainStack.Navigator
screenOptions={{
headerStyle: {
height: 64,
backgroundColor: colors.navy900,
},
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,
}}
>
<Tab.Screen
<MainStack.Screen
name="HomeStack"
component={HomeStackScreen}
initialParams={{ name }}
options={{
tabBarLabel: "Home",
headerShown: false,
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons name="home-outline" size={24} color={color} />
),
headerTitle: "Home",
}}
/>
<Tab.Screen
name="Mentions"
component={MentionsScreen}
options={{
tabBarLabel: "Mentions",
tabBarIcon: ({ color }) => (
<MaterialIcons name="alternate-email" size={24} color={color} />
),
}}
<MainStack.Screen
name="Chat"
component={ChatScreen}
options={({ navigation }) => ({
headerTitle: () =>
currentChannel && (
<TouchableHighlight
underlayColor={colors.navy700}
onPress={() => navigation.navigate("ChatSettings")}
style={{ paddingVertical: 8, paddingHorizontal: 30, borderRadius: 6 }}
>
<View style={{ flexDirection: "row" }}>
<Avatar
source={interlocutor ? interlocutor : currentChannel}
showIndicator={!!interlocutor}
style={{ marginRight: 10 }}
/>
<Text variant="headline" color="neutral0">
{interlocutor ? interlocutor.name : currentChannel?.name}
</Text>
</View>
</TouchableHighlight>
),
headerRight: () => {
return (
<TouchableOpacity
onPress={() =>
navigation.navigate("PinnedMessage", { channelId: currentChannel?.id })
}
style={{ paddingRight: 24 }}
>
<MaterialCommunityIcons name="pin-outline" color={colors.neutral0} size={26} />
</TouchableOpacity>
)
},
})}
/>
<Tab.Screen
name="Profile"
component={ProfileScreen}
options={{
tabBarLabel: "Profile",
tabBarIcon: ({ color }) => (
<MaterialIcons name="person-outline" size={24} color={color} />
),
}}
<MainStack.Screen name="NewChat" component={NewChatScreen} options={{ title: "New chat" }} />
<MainStack.Screen
name="NewGroup"
component={NewGroupScreen}
options={{ title: "Group chat" }}
/>
<MainStack.Screen name="ThreadReply" component={ThreadReply} />
<MainStack.Screen
name="ChatSettings"
component={ChatSettings}
options={{ title: "Chat settings" }}
/>
</Tab.Navigator>
<MainStack.Screen
name="PinnedMessage"
component={PinnedMessage}
options={{ title: "Pinned message" }}
/>
</MainStack.Navigator>
)
}

function App() {
const [chat, setChat] = useState<Chat | null>(null)
const [users, setUsers] = useState<User[]>([])
const [interlocutors, setInterlocutors] = useState<{ [channelId: string]: User }>({})
const [loading, setLoading] = useState(false)
const [currentChannel, setCurrentChannel] = useState<Channel | null>()
const [currentChannelMembers, setCurrentChannelMembers] = useState<Membership[]>([])
const [userMemberships, setUserMemberships] = useState<Membership[]>([])

async function setCurrentChannelWithMembers(channel: Channel | null) {
const setCurrentChannelWithMembers = useCallback(async (channel: Channel | null) => {
if (!channel) {
setCurrentChannelMembers([])
setCurrentChannel(null)
Expand All @@ -133,56 +172,89 @@ 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,
Roboto_500Medium,
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 (
<ChatContext.Provider
value={{
loading,
setLoading,
chat,
setChat,
currentChannel,
setCurrentChannel: setCurrentChannelWithMembers,
currentChannelMembers,
users,
setUsers,
getUser,
getInterlocutor,
memberships: userMemberships,
setMemberships: setUserMemberships,
}}
>
<ChatContext.Provider value={contextValue}>
<GestureHandlerRootView style={{ flex: 1 }}>
<BottomSheetModalProvider>
<StatusBar style="inverted" backgroundColor={defaultTheme.colors.navy800} />
Expand All @@ -191,18 +263,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 */}
<KeyboardAvoidingView
{...(Platform.OS === "ios" ? { behavior: "padding" } : {})}
style={{ flex: 1 }}
>
<NavigationContainer>
<MainStack.Navigator screenOptions={{ headerShown: false }}>
<MainStack.Screen name="login" component={LoginScreen} />
<MainStack.Screen name="tabs" component={TabNavigator} />
</MainStack.Navigator>
</NavigationContainer>
</KeyboardAvoidingView>
<NavigationContainer>
<MainStack.Navigator screenOptions={{ headerShown: false }}>
<MainStack.Screen name="login" component={LoginScreen} />
<MainStack.Screen name="mainRoutes" component={MainRoutesNavigator} />
</MainStack.Navigator>
</NavigationContainer>
</SafeAreaView>
</SafeAreaProvider>
</BottomSheetModalProvider>
Expand Down
Loading
Loading