Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# @baseapp-frontend/components

## 1.4.8

### Minor Changes

- Added roomslist components and illustrations

### Patch Changes

- Updated dependencies
- @baseapp-frontend/design-system@1.1.5

## 1.4.7

### Patch Changes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Text } from '@baseapp-frontend/design-system/components/native/typographies'
import { View } from '@baseapp-frontend/design-system/components/native/views'
import { useTheme } from '@baseapp-frontend/design-system/providers/native'

import { createStyles } from './styles'
import { EmptyViewProps } from './types'

const EmptyView = ({ icon, title, message, style }: EmptyViewProps) => {
const theme = useTheme()
const styles = createStyles(theme)

return (
<View style={[styles.container, style]}>
{icon && <View style={styles.imageWrapper}>{icon}</View>}
<View style={styles.textWrapper}>
<Text variant="subtitle2" style={styles.text}>
{title}
</Text>
{message && (
<Text variant="caption" style={styles.text}>
{message}
</Text>
)}
</View>
</View>
)
}

export default EmptyView
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Theme } from '@baseapp-frontend/design-system/styles/native'

import { StyleSheet } from 'react-native'

export const createStyles = (theme: Theme) =>
StyleSheet.create({
container: {
flex: 1,
padding: 32,
flexDirection: 'column',
gap: 12,
justifyContent: 'center',
alignItems: 'center',
},
imageWrapper: {
flex: 1,
alignItems: 'center',
},
textWrapper: {
flex: 1,
maxWidth: '100%',
padding: 16,
justifyContent: 'flex-end',
alignItems: 'center',
gap: 10,
},
text: {
color: theme.colors.object.low,
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ReactNode } from 'react'

import { StyleProp, ViewStyle } from 'react-native'

export interface EmptyViewProps {
icon?: ReactNode
title: string
message?: string
style?: StyleProp<ViewStyle>
}
2 changes: 2 additions & 0 deletions packages/components/modules/__shared__/native/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as EmptyView } from './EmptyView'
export type * from './EmptyView/types'
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { graphql } from 'react-relay'

export const ChatRoomFragment = graphql`
fragment ChatRoomFragment on ChatRoom @refetchable(queryName: "ChatRoomFragmentRefetchQuery") {
id
isGroup
isArchived
unreadMessages {
count
markedUnread
}
participantsCount
...TitleFragment
...MessagesListFragment
}
`
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const ChatRoomQuery = graphql`
participantsCount
...TitleFragment
...MessagesListFragment
...ChatRoomFragment
}
}
`
139 changes: 139 additions & 0 deletions packages/components/modules/messages/native/ChatRooms/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { FC, Suspense, useState, useTransition } from 'react'

import { useCurrentProfile } from '@baseapp-frontend/authentication'
import { LoadingScreen } from '@baseapp-frontend/design-system/components/native/displays'
import {
DEFAULT_FORM_VALUES,
FORM_VALUES,
SearchInput,
SearchInputFormValues,
} from '@baseapp-frontend/design-system/components/native/inputs'
import { Tab, Tabs } from '@baseapp-frontend/design-system/components/native/tabs'
import { Text } from '@baseapp-frontend/design-system/components/native/typographies'
import { PageViewWithHeader, View } from '@baseapp-frontend/design-system/components/native/views'
import { useTheme } from '@baseapp-frontend/design-system/providers/native'
import { useAppStateSubscription } from '@baseapp-frontend/utils/hooks/useAppStateSubscription'

import { useForm } from 'react-hook-form'
import { useLazyLoadQuery, useRelayEnvironment } from 'react-relay'

import { ChatRoomsQuery as ChatRoomsQueryType } from '../../../../__generated__/ChatRoomsQuery.graphql'
import { RoomsListFragment$key } from '../../../../__generated__/RoomsListFragment.graphql'
import { useRoomsList } from '../../common'
import { ChatRoomsQuery } from '../../common/graphql/queries/ChatRoomsQuery'
import { NewChatButton } from '../NewChatButton'
import RoomsList from '../RoomsList'
import { CHAT_TAB_LABEL, CHAT_TAB_VALUES } from '../RoomsList/RoomsListComponent/constants'
import { ChatTabValues } from '../RoomsList/RoomsListComponent/types'
import { useRoomListSubscription } from '../graphql/subscriptions/useRoomListSubscription'
import { createStyles } from './styles'

const ChatRooms: FC = () => {
const theme = useTheme()
const styles = createStyles(theme)
const { control, watch, setValue, reset } = useForm<SearchInputFormValues>({
defaultValues: DEFAULT_FORM_VALUES,
})
const searchParam = watch(FORM_VALUES.search)
const [selectedTab, setSelectedTab] = useState<string>(CHAT_TAB_VALUES.active)
const [, startTransition] = useTransition()
const { currentProfile } = useCurrentProfile()
const chatRoomQueryData = useLazyLoadQuery<ChatRoomsQueryType>(
ChatRoomsQuery,
{ profileId: currentProfile?.id || '' },
{ fetchPolicy: 'store-and-network' },
)
const { data, refetch } = useRoomsList(chatRoomQueryData?.profile as RoomsListFragment$key)
Comment on lines +41 to +46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find ChatRoomsQuery definition
fd -t f -i "chatrooms" | head -20

Repository: silverlogic/baseapp-frontend

Length of output: 149


🏁 Script executed:

#!/bin/bash
# Read the ChatRoomsQuery definition
cat -n packages/components/modules/messages/common/graphql/queries/ChatRoomsQuery.ts

Repository: silverlogic/baseapp-frontend

Length of output: 377


🏁 Script executed:

#!/bin/bash
# Read the ChatRooms component around the query usage
sed -n '35,50p' packages/components/modules/messages/native/ChatRooms/index.tsx

Repository: silverlogic/baseapp-frontend

Length of output: 730


🏁 Script executed:

#!/bin/bash
# Check the full file to understand component structure and error handling
wc -l packages/components/modules/messages/native/ChatRooms/index.tsx

Repository: silverlogic/baseapp-frontend

Length of output: 139


🏁 Script executed:

#!/bin/bash
# Read the entire ChatRooms component
cat -n packages/components/modules/messages/native/ChatRooms/index.tsx

Repository: silverlogic/baseapp-frontend

Length of output: 5808


🏁 Script executed:

#!/bin/bash
# Search for useCurrentProfile hook to understand when it returns null/undefined
fd -t f "useCurrentProfile" | head -10

Repository: silverlogic/baseapp-frontend

Length of output: 166


🏁 Script executed:

#!/bin/bash
# Find the useCurrentProfile implementation
fd -t f "useCurrentProfile.ts" -o "useCurrentProfile.tsx" | grep -v test | grep -v __generated__

Repository: silverlogic/baseapp-frontend

Length of output: 233


🏁 Script executed:

#!/bin/bash
# Find the useCurrentProfile implementation
find packages/authentication -name "useCurrentProfile.ts*" -not -path "*test*" -not -path "*__generated__*"

Repository: silverlogic/baseapp-frontend

Length of output: 54


🏁 Script executed:

#!/bin/bash
# List the useCurrentProfile directory
ls -la packages/authentication/modules/profile/useCurrentProfile/

Repository: silverlogic/baseapp-frontend

Length of output: 479


🏁 Script executed:

#!/bin/bash
# Read the useCurrentProfile implementation
cat -n packages/authentication/modules/profile/useCurrentProfile/index.tsx

Repository: silverlogic/baseapp-frontend

Length of output: 1403


🏁 Script executed:

#!/bin/bash
# Read the types for CurrentProfileState
cat -n packages/authentication/modules/profile/useCurrentProfile/types.ts

Repository: silverlogic/baseapp-frontend

Length of output: 593


🏁 Script executed:

#!/bin/bash
# Search for useLazyLoadQuery usage patterns in codebase to see how others handle conditional queries
rg -n "useLazyLoadQuery" -A 3 -B 1 --type=ts --type=tsx | head -50

Repository: silverlogic/baseapp-frontend

Length of output: 99


🏁 Script executed:

#!/bin/bash
# Search for useLazyLoadQuery usage patterns
rg -n "useLazyLoadQuery" -A 3 -B 1 | head -60

Repository: silverlogic/baseapp-frontend

Length of output: 7115


🏁 Script executed:

#!/bin/bash
# Check if there's any error handling or try-catch around the useLazyLoadQuery call
sed -n '31,50p' packages/components/modules/messages/native/ChatRooms/index.tsx

Repository: silverlogic/baseapp-frontend

Length of output: 903


The empty string fallback for profileId violates the GraphQL schema contract and causes runtime errors.

The GraphQL query requires profileId: ID! (non-nullable ID), but passing an empty string when currentProfile is null is not a valid ID value. Since currentProfile can be null (defined in CurrentProfileState as MinimalProfile | null), this will cause useLazyLoadQuery to fail when executed without proper validation.

Recommended fix: Guard the component to only render when currentProfile?.id exists, or conditionally skip the query execution until the profile ID is available.

// Example guard pattern:
if (!currentProfile?.id) {
  return <LoadingScreen /> // or null
}

const chatRoomQueryData = useLazyLoadQuery<ChatRoomsQueryType>(
  ChatRoomsQuery,
  { profileId: currentProfile.id }, // now guaranteed non-null
  { fetchPolicy: 'store-and-network' },
)
🤖 Prompt for AI Agents
In packages/components/modules/messages/native/ChatRooms/index.tsx around lines
41 to 46, the useLazyLoadQuery is passed an empty string when currentProfile is
null which violates the GraphQL ID! contract; change the component to guard
until currentProfile?.id exists (e.g. return null or a loading UI before
executing the query) so you can call useLazyLoadQuery with a guaranteed non-null
profileId, or alternatively skip/conditionally execute the query until the id is
available; ensure downstream useRoomsList is only called with a valid
RoomsListFragment reference.


const handleSearchChange = (text: string) => {
startTransition(() => {
setValue(FORM_VALUES.search, text)
})
}

const resetInput = () => {
setValue(FORM_VALUES.search, DEFAULT_FORM_VALUES.search)
reset(DEFAULT_FORM_VALUES)
}

const environment = useRelayEnvironment()
useRoomListSubscription({ profileId: data?.id, connections: [], environment })

const handleChange = (newValue: string) => {
setSelectedTab(newValue as ChatTabValues)
}

useAppStateSubscription(() => {
refetch(
{
q: searchParam,
unreadMessages: selectedTab === CHAT_TAB_VALUES.unread,
archived: selectedTab === CHAT_TAB_VALUES.archived,
},
{ fetchPolicy: 'store-and-network' },
)
})

return (
<PageViewWithHeader>
<View style={styles.container}>
<Text variant="h4">Messages</Text>
<SearchInput
placeholder="Search"
onChangeText={handleSearchChange}
control={control}
name={FORM_VALUES.search}
searchParam={searchParam}
resetInput={resetInput}
autoComplete="off"
autoCorrect={false}
/>

<Tabs value={selectedTab} onChange={handleChange} style={styles.tabs}>
<Tab
label={CHAT_TAB_LABEL.active}
value={CHAT_TAB_VALUES.active}
aria-label="Active messages tab"
/>
<Tab
label={CHAT_TAB_LABEL.unread}
value={CHAT_TAB_VALUES.unread}
aria-label="Unread messages tab"
/>
<Tab
label={CHAT_TAB_LABEL.archived}
value={CHAT_TAB_VALUES.archived}
aria-label="Archived messages tab"
/>
</Tabs>
{
// TODO: Handle groups tab separately will be implemented later

Check warning on line 110 in packages/components/modules/messages/native/ChatRooms/index.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=silverlogic_baseapp-frontend&issues=AZl4aBRVUf3kDgi9onna&open=AZl4aBRVUf3kDgi9onna&pullRequest=293
selectedTab === CHAT_TAB_VALUES.groups ? (
<Text>Groups tab is not implemented yet.</Text>
) : (
<RoomsList
targetRef={chatRoomQueryData}
searchParam={searchParam}
selectedTab={selectedTab}
/>
)
}
<NewChatButton />
</View>
</PageViewWithHeader>
)
}

const SuspendedChatRooms = () => (
<Suspense
fallback={
<View style={{ flex: 1 }}>
<LoadingScreen />
</View>
}
>
<ChatRooms />
</Suspense>
)

export default SuspendedChatRooms
20 changes: 20 additions & 0 deletions packages/components/modules/messages/native/ChatRooms/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Theme } from '@baseapp-frontend/design-system/styles/native'

import { StyleSheet } from 'react-native'

export const createStyles = (theme: Theme) =>
StyleSheet.create({
container: {
backgroundColor: theme.colors.surface.background,
flexGrow: 1,
flex: 1,
position: 'relative',
},
chatRoomsContainer: {
flexGrow: 1,
},
tabs: {
justifyContent: 'space-evenly',
gap: 0,
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'

import { FabButton } from '@baseapp-frontend/design-system/components/native/buttons'
import { useTheme } from '@baseapp-frontend/design-system/providers/native'

import { useRouter } from 'expo-router'

const NewChatButton = () => {
const theme = useTheme()
const router = useRouter()

const navigateToCreateRoom = () => router.push('/create-room')

return (
<FabButton
onPress={navigateToCreateRoom}
iconName="add"
iconSize={28}
iconColor={theme.colors.primary.contrast}
/>
)
}

export { NewChatButton }
Loading
Loading