diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b11c963c7ef13ea581a5d2f0a7503ae0db42c7a5..bbd2b555704e607c8ef5477d9667ea916fc145d0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,7 @@ variables: YARN_CACHE_FOLDER: '$CI_PROJECT_DIR/.yarn/cache' YARN_ENABLE_GLOBAL_CACHE: 'false' + GIT_DEPTH: 50 .yarn-cache-rw: key: @@ -97,6 +98,7 @@ lint_compile_test: - yarn format:check - yarn check-types - git fetch origin main:main # needed for 'yarn workspaces foreach ... (lint_all task)' + - git fetch origin $CI_COMMIT_BRANCH:$CI_COMMIT_BRANCH - yarn lint_all - yarn test interruptible: true diff --git a/apps/mobile/notifications/chatNotifications.ts b/apps/mobile/notifications/chatNotifications.ts index f5f14bb00e261ebab696636a37b91e44d095e480..7157b868fc8374ea0cdba5d54395a1ae76cc3ffd 100644 --- a/apps/mobile/notifications/chatNotifications.ts +++ b/apps/mobile/notifications/chatNotifications.ts @@ -1,11 +1,11 @@ import * as Notifications from 'expo-notifications' import type { NotificationContentInput } from 'expo-notifications' -import { type Route } from '@react-navigation/native' +import type { Route } from '@react-navigation/native' import { getLogger } from '@holi/core/helpers/logging' import { RouteName } from '@holi/mobile/navigation/routeName' -import { NotificationData } from '@holi/mobile/notifications' +import type { NotificationData } from '@holi/mobile/notifications' const logger = getLogger('Notifications') @@ -107,7 +107,7 @@ const createAndroidRoomMemberChangePush = (event: AndroidRoomMemberChange): Noti if (event.membership === 'invite') { return { title: 'Invitation', - body: 'Someone invited you to a ' + (event.content.is_direct ? 'direct chat' : 'chat') + '.', + body: 'Someone invited you to a ' + (event?.content?.is_direct ? 'direct chat' : 'chat') + '.', data: { room_id: event.room_id, }, @@ -115,7 +115,7 @@ const createAndroidRoomMemberChangePush = (event: AndroidRoomMemberChange): Noti } else { return { title: 'Channel Update', - body: 'Someone updated ' + (event.content.is_direct ? 'your direct chat' : 'a chat you are part of') + '.', + body: 'Someone updated ' + (event?.content?.is_direct ? 'your direct chat' : 'a chat you are part of') + '.', data: { room_id: event.room_id, }, diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 54ea47e0681a92e681d4ac7a743fc401ebf6460e..7ec0502e00d2ab1e59a9351e48bd755eb90656bb 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -68,7 +68,7 @@ "react-native-get-random-values": "^1.11.0", "react-native-image-crop-picker": "^0.41.2", "react-native-image-pan-zoom": "^2.1.12", - "react-native-keyboard-controller": "^1.15.2", + "react-native-keyboard-controller": "^1.16.3", "react-native-media-query": "^2.0.1", "react-native-reanimated": "~3.10.1", "react-native-reanimated-carousel": "^3.5.1", diff --git a/apps/web/__tests__/sitemap/static.xml.test.ts b/apps/web/__tests__/sitemap/static.xml.test.ts index 5cb3ee783426853eee62fcebcc68b9c383328236..88414dc98a1fd5cd4e894361f2908193efd58e81 100644 --- a/apps/web/__tests__/sitemap/static.xml.test.ts +++ b/apps/web/__tests__/sitemap/static.xml.test.ts @@ -1,6 +1,6 @@ import { HttpResponse } from 'msw' import { setupServer } from 'msw/node' -import { NextPageContext } from 'next' +import type { NextPageContext } from 'next' import { createPageContextMock, graphqlLink } from '@holi/web/__tests__/helpers' import { getServerSideProps, topicSlugsQuery } from '@holi/web/pages/sitemap/static.xml' @@ -8,6 +8,7 @@ import { getServerSideProps, topicSlugsQuery } from '@holi/web/pages/sitemap/sta jest.mock('@holi/core/helpers/isSSR', () => ({ isSSR: true, })) +jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')) const server = setupServer( graphqlLink.query(topicSlugsQuery, () => { diff --git a/core/components/Screen/index.tsx b/core/components/Screen/index.tsx index fa60a5ec991a68e8cf6681f293a2744b5489846e..4cfb9e8b4c34ee2aa0e8222ab53e56d4a0352487 100644 --- a/core/components/Screen/index.tsx +++ b/core/components/Screen/index.tsx @@ -108,15 +108,20 @@ type BottomComponentWrapperProps = { StickyBottomComponent: ScreenProps['StickyBottomComponent'] padding?: ScreenProps['padding'] onLayout?: ((event: LayoutChangeEvent) => void) | undefined + footerHidden?: boolean } -const BottomComponentWrapper = ({ StickyBottomComponent, padding, onLayout }: BottomComponentWrapperProps) => { +const BottomComponentWrapper = ({ + StickyBottomComponent, + padding, + onLayout, + footerHidden, +}: BottomComponentWrapperProps) => { const { styles } = useStyles(stylesheet) const { bottomDistance } = useGradualAnimation() - const { bottom } = useSafeAreaInsets() - const toSubtract = dimensions.bottomBarHeight + (isIos ? bottom : 0) + const bottomBar = footerHidden ? 0 : dimensions.bottomBarHeight const bottomInputStyle = useAnimatedStyle( () => ({ - bottom: bottomDistance.value - toSubtract < 0 ? 0 : bottomDistance.value - toSubtract, + bottom: bottomDistance.value - bottomBar < 0 ? 0 : bottomDistance.value - bottomBar, }), [] ) @@ -128,6 +133,7 @@ const BottomComponentWrapper = ({ StickyBottomComponent, padding, onLayout }: Bo ) } +const DEFAULT_VERTICAL_OFFSET = 100 export const Screen = (props: ScreenProps) => { const { preset = 'fixed', @@ -140,7 +146,7 @@ export const Screen = (props: ScreenProps) => { headerOptions, StickyTopComponent, padding, - contentContainerPadding = { left: 'xs', right: 'xs' }, + contentContainerPadding, testID, ...otherProps } = props @@ -152,6 +158,11 @@ export const Screen = (props: ScreenProps) => { }) const safeAreaInsets = useSafeAreaInsets() const navigationOptions = useScreenOptions() + const footerHidden = headerOptions?.navigationCustomOptions?.footerShown === false + const _keyboardVerticalOffset = keyboardVerticalOffset + ? keyboardVerticalOffset + : DEFAULT_VERTICAL_OFFSET + (Platform.OS === 'ios' ? stickyBottomOffset || 0 : 0) + return ( <SafeAreaView edges={safeAreaEdges} @@ -167,15 +178,15 @@ export const Screen = (props: ScreenProps) => { <StatusBar {...statusBarProps} barStyle={themeName === 'dark' ? 'light-content' : 'dark-content'} /> <KeyboardAvoidingView - behavior={isIos ? 'padding' : 'height'} - keyboardVerticalOffset={keyboardVerticalOffset} {...keyboardAvoidingViewProps} + behavior={isIos ? 'padding' : 'height'} + keyboardVerticalOffset={_keyboardVerticalOffset} style={[ keyboardAvoidingViewProps?.style, styles.flex1, styles.relative, styles.contentContainerPadding(navigationOptions.headerShown, safeAreaInsets.top, contentContainerPadding), - StickyBottomComponent ? styles.stickyBottomOffset(stickyBottomOffset) : null, + StickyBottomComponent && preset === 'fixed' ? styles.stickyBottomOffset(stickyBottomOffset) : null, StickyTopComponent ? styles.stickyTopOffset(stickyTopOffset) : null, ]} > @@ -201,6 +212,7 @@ export const Screen = (props: ScreenProps) => { {StickyBottomComponent && ( <BottomComponentWrapper StickyBottomComponent={StickyBottomComponent} + footerHidden={footerHidden} padding={padding} onLayout={stickyBottomLayout} /> diff --git a/core/components/Screen/index.web.tsx b/core/components/Screen/index.web.tsx index cd5a92ae7a9257e7985099ddda0f828272510e9b..9cb3e8109386b43a2abfd7c2693ca1331474782a 100644 --- a/core/components/Screen/index.web.tsx +++ b/core/components/Screen/index.web.tsx @@ -14,7 +14,7 @@ export const Screen = (props: ScreenProps) => { StickyBottomComponent, StickyTopComponent, padding, - contentContainerPadding = { left: 'xs', right: 'xs' }, + contentContainerPadding, headerOptions, testID, children, @@ -26,24 +26,25 @@ export const Screen = (props: ScreenProps) => { const { styles, theme } = useStyles(stylesheet) const { stickyBottomLayout, stickyBottomOffset, stickyTopLayout, stickyTopOffset, headerHeight } = useScreenComponentInternal({ headerOptions }) + return ( <View testID={testID} style={[styles.padding(padding), styles.container, { backgroundColor: theme.colors.bg[backgroundColor] }]} > - <HoliContainer flex={1} noPadding> + <HoliContainer flex={1}> <View style={[ styles.flex1, - styles.webContainer, keyboardAvoidingViewProps?.style, + styles.webContainer, StickyTopComponent ? styles.stickyTopOffset(stickyTopOffset) : null, StickyBottomComponent ? styles.stickyBottomOffset(stickyBottomOffset) : null, ]} > {StickyTopComponent && ( <View style={[styles.stickyTopWeb(headerHeight), { position: 'fixed' }]} onLayout={stickyTopLayout}> - <HoliContainer noPadding>{StickyTopComponent}</HoliContainer> + <HoliContainer>{StickyTopComponent}</HoliContainer> </View> )} @@ -56,8 +57,14 @@ export const Screen = (props: ScreenProps) => { </ScrollViewRefreshable> {StickyBottomComponent && ( - <View style={[styles.stickyBottomWeb, { position: 'fixed' }]} onLayout={stickyBottomLayout}> - <HoliContainer noPadding>{StickyBottomComponent}</HoliContainer> + <View + style={[ + styles.stickyBottomWeb(headerOptions?.navigationCustomOptions?.footerShown === false), + { position: 'fixed' }, + ]} + onLayout={stickyBottomLayout} + > + <HoliContainer>{StickyBottomComponent}</HoliContainer> </View> )} </View> diff --git a/core/components/Screen/mock.ts b/core/components/Screen/mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..5f564d4dc5c6c9a26a3150788d5c7c58653d096a --- /dev/null +++ b/core/components/Screen/mock.ts @@ -0,0 +1,11 @@ +jest.mock('react-native-safe-area-context', () => { + const mock = jest.requireActual('react-native-safe-area-context/jest/mock') + return { + ...mock.default, + } +}) + +jest.mock('react-native-keyboard-controller', () => ({ + ...require('react-native-keyboard-controller/jest'), + useReanimatedKeyboardAnimation: () => ({ height: 0 }), +})) diff --git a/core/components/Screen/styles.ts b/core/components/Screen/styles.ts index dddbafdf6d9511974eae0386ec97176f14974569..7d9aaea16e44fc44f9c8df23db41e8ba491f450d 100644 --- a/core/components/Screen/styles.ts +++ b/core/components/Screen/styles.ts @@ -2,6 +2,7 @@ import type { ScreenPadding } from '@holi/core/components/Screen/types' import { dimensions } from '@holi/ui/styles/globalVars' import { Spacing } from 'holi-bricks/tokens' import { createStyleSheet } from 'holi-bricks/utils' +import { Platform } from 'react-native' const makePadding = (padding?: ScreenPadding) => ({ paddingTop: padding?.top ? Spacing[padding?.top] : 0, @@ -40,7 +41,8 @@ export const stylesheet = createStyleSheet(() => { }, stickyBottomOffset: (offset: number) => { return { - paddingBottom: offset, + marginBottom: Platform.OS === 'ios' ? offset : 0, + paddingBottom: Platform.OS === 'android' || Platform.OS === 'web' ? offset : 0, } }, stickyTopOffset: (offset: number) => { @@ -64,15 +66,15 @@ export const stylesheet = createStyleSheet(() => { zIndex: 10, } }, - stickyBottomWeb: { + stickyBottomWeb: (footerHidden) => ({ width: '100%', maxWidth: dimensions.maxScreenWidth, - bottom: dimensions.bottomBarHeight, + bottom: footerHidden ? 0 : dimensions.bottomBarHeight, left: 0, right: 0, marginHorizontal: 'auto', zIndex: 10, - }, + }), stickyBottom: { position: 'absolute', bottom: 0, diff --git a/core/components/Screen/useScreenComponentInternal.ts b/core/components/Screen/useScreenComponentInternal.ts index 2b2a2eca5cbff80add2964d7ff4665a349078773..74cc7f12fb5123c713eea92e87182e15dce3bd64 100644 --- a/core/components/Screen/useScreenComponentInternal.ts +++ b/core/components/Screen/useScreenComponentInternal.ts @@ -12,13 +12,20 @@ export const useScreenComponentInternal = ({ headerOptions }: UseScreenComponent const setScreenOptions = useSetScreenOptions() const [stickyBottomOffset, setStickyBottomOffset] = useState(0) - const stickyBottomLayout = useCallback((e: LayoutChangeEvent) => { - setStickyBottomOffset(e.nativeEvent.layout.height) - }, []) + const stickyBottomLayout = useCallback( + (e: LayoutChangeEvent) => { + if (e.nativeEvent.layout.height !== stickyBottomOffset) setStickyBottomOffset(e.nativeEvent.layout.height) + }, + [stickyBottomOffset] + ) + const [stickyTopOffset, setStickyTopOffset] = useState(0) - const stickyTopLayout = useCallback((e: LayoutChangeEvent) => { - setStickyTopOffset(e.nativeEvent.layout.height) - }, []) + const stickyTopLayout = useCallback( + (e: LayoutChangeEvent) => { + if (e.nativeEvent.layout.height !== stickyTopOffset) setStickyTopOffset(e.nativeEvent.layout.height) + }, + [stickyTopOffset] + ) useEffect(() => { if (headerOptions) { diff --git a/core/package.json b/core/package.json index 6e94eeb6ddb5a3447a40a8f2f8f4fe93b254ae57..da0af1e594f8d499cd455fff1697084cf12bdb04 100644 --- a/core/package.json +++ b/core/package.json @@ -7,7 +7,7 @@ "graphql-tag": "^2.12.6", "react": "18.2.0", "react-native": "0.74.6", - "react-native-keyboard-controller": "^1.15.2", + "react-native-keyboard-controller": "^1.16.3", "react-native-phone-number-input": "^2.1.0", "react-native-reanimated-carousel": "^3.5.1", "react-native-url-polyfill": "^1.3.0", diff --git a/core/screens/act/__tests__/ActHome.test.tsx b/core/screens/act/__tests__/ActHome.test.tsx index 1bfa02e0754cf0e4b706d0f92aab67f0e7cda331..65015dc3427fc4e4f7d985c51557ffe3937c1896 100644 --- a/core/screens/act/__tests__/ActHome.test.tsx +++ b/core/screens/act/__tests__/ActHome.test.tsx @@ -10,6 +10,7 @@ jest.mock('@holi/core/tracking/PosthogCrossPlatform', () => ({ })) jest.mock('@holi/core/navigation/hooks/useParam') +jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')) const mockActSection = jest.fn() jest.mock('@holi/core/screens/act/ActSection', () => { diff --git a/core/screens/chat/ChatHome.tsx b/core/screens/chat/ChatHome.tsx index 36a4325a27088de66a437655034455b846dee2fb..1f5ff6cd0a8c370e34f0e309f3678b1e35d01ce7 100644 --- a/core/screens/chat/ChatHome.tsx +++ b/core/screens/chat/ChatHome.tsx @@ -47,11 +47,11 @@ const ChatHome = () => { headerRight: () => <NotificationAndSearchHeader />, }) }, [navigate, setScreenOptions, t, title, colors.background20]) - if (chatFeatureFlag.inMaintenance) { + if (chatFeatureFlag.inMaintenance || chatFeatureFlag.isOff) { return <ChatMaintenance /> } else if (chatFeatureFlag.isOn) { return <ChatHomeContent /> - } else { + } else if (chatFeatureFlag.isUnknown) { // unknown status (= missing feature toggle) || disabled return ( <ScrollView style={{ backgroundColor: colors.background20 }}> diff --git a/core/screens/chat/ChatRoomView.tsx b/core/screens/chat/ChatRoomView.tsx index a9fdfaab331d3d3f2b2b6b41d7fbae5c784721ee..247be363ab73bbe0e1004548b6a1eef8663dd10d 100644 --- a/core/screens/chat/ChatRoomView.tsx +++ b/core/screens/chat/ChatRoomView.tsx @@ -2,7 +2,7 @@ import { useAtomValue } from 'jotai' import React, { useCallback, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' // eslint-disable-next-line no-restricted-imports -import { Keyboard, Platform, SafeAreaView, View } from 'react-native' +import { Keyboard, Platform } from 'react-native' import { useChatInitialized } from '@holi/chat/src/client/chatState' import ChatProfileCard from '@holi/chat/src/components/ChatProfileCard' @@ -26,22 +26,15 @@ import { useLoggedInUser } from '@holi/core/auth/hooks/useLoggedInUser' import PermissionsGuard, { PermissionsType } from '@holi/core/components/PermissionsGuard' import { logError } from '@holi/core/errors/helpers' import { getInitials } from '@holi/core/helpers' -import useIsomorphicLayoutEffect from '@holi/core/helpers/useIsomorphicLayoutEffect' import { useShowHeaderOnScreenFocus } from '@holi/core/helpers/useShowHeaderOnScreenFocus' import HeaderBackButton from '@holi/core/navigation/components/HeaderBackButton' import createParamHooks from '@holi/core/navigation/hooks/useParam' import useRouting from '@holi/core/navigation/hooks/useRouting' -import { useSetScreenOptions } from '@holi/core/navigation/hooks/useScreenOptions' -import HoliContainer from '@holi/ui/components/atoms/HoliContainer' -import { HoliTransition } from '@holi/ui/components/atoms/HoliTransition' import HoliLoader from '@holi/ui/components/molecules/HoliLoader' import { HoliToastType, useToast } from '@holi/ui/components/molecules/HoliToastProvider' -import HoliKeyboardSafeAreaView from '@holi/ui/components/organisms/HoliKeyboardSafeAreaView' -import { dimensions } from '@holi/ui/styles/globalVars' -import { useStyles } from 'holi-bricks/hooks' import { TrackingEvent } from '@holi/core/tracking' import useTracking from '@holi/core/tracking/hooks/useTracking' -import { createStyleSheet } from 'holi-bricks/utils' +import { Screen } from '@holi/core/components/Screen' const WAIT_FOR_CHATROOM_DELAY = 5000 @@ -51,13 +44,13 @@ export type ChatRoomPageParams = { const { useParam } = createParamHooks<ChatRoomPageParams>() -const NUMBER_OF_TEXT_MESSAGES_NEEDED_TO_FILL_A_SCREEN_AND_CAUSE_SCROLLBARS = 40 +// number of text messages needed to fill a screen and cause scrollbars +const NUMBER_OF_INITIAL_MSGS = 40 const ChatRoomView = () => { const { t } = useTranslation() const { navigateBack, replaceRoute } = useRouting() const chatInitialized = useChatInitialized() - const { styles } = useStyles(stylesheet) const { track } = useTracking() const [roomId = ''] = useParam('roomId') @@ -65,8 +58,7 @@ const ChatRoomView = () => { const { user: loggedInUser } = useLoggedInUser() const chatPartner = useAtomValue(useMemo(() => chatPartnerAtomCreator(roomId, loggedInUser), [loggedInUser, roomId])) - const requestMessages = useRequestRoomMessages(NUMBER_OF_TEXT_MESSAGES_NEEDED_TO_FILL_A_SCREEN_AND_CAUSE_SCROLLBARS) - const setScreenOptions = useSetScreenOptions() + const requestMessages = useRequestRoomMessages(NUMBER_OF_INITIAL_MSGS) const roomMemberIds = room?.members.map((m) => m.id) ?? [] const { matrixIdToHoliUserRecord, loading: loadingUsers } = useMatrixIdToHoliUserRecord(roomMemberIds) @@ -97,62 +89,44 @@ const ChatRoomView = () => { } }, [navigateBack, roomId]) - useIsomorphicLayoutEffect(() => { - setScreenOptions({ - headerRight: () => - !!chatPartner && - !isSpaceRoom && ( - <ChatRoomActionDrawer - roomId={roomId} - chatPartner={chatPartner} - chatPartnerFullName={chatPartnerHoliUser?.fullName ?? chatPartner.name} + const headerTitle = () => { + if (loadingUsers) return null + switch (room?.type) { + case 'private': + return chatPartnerHoliUser ? ( + <ChatRoomHeaderTitle + href={'/profile/' + chatPartnerHoliUser.id} + name={room.name} + imageSrc={chatPartner?.avatar} + imageBlurhash={chatPartnerHoliUser.avatarBlurhash} + initials={getChatInitials(chatPartner?.name)} + defaultColor={getDefaultAvatarColor(chatPartner?.name)} /> - ), - headerLeft: ({ tintColor }) => <HeaderBackButton tintColor={tintColor} onPress={handleBackPress} />, - headerTitle: () => { - if (loadingUsers) return null - - switch (room?.type) { - case 'private': - return chatPartnerHoliUser ? ( - <ChatRoomHeaderTitle - href={'/profile/' + chatPartnerHoliUser.id} - name={room.name} - imageSrc={chatPartner?.avatar} - imageBlurhash={chatPartnerHoliUser.avatarBlurhash} - initials={getChatInitials(chatPartner?.name)} - defaultColor={getDefaultAvatarColor(chatPartner?.name)} - /> - ) : ( - <ChatRoomHeaderTitle - name={room.name} - imageSrc={chatPartner?.avatar} - initials={getChatInitials(chatPartner?.name)} - defaultColor={getDefaultAvatarColor(chatPartner?.name)} - /> - ) - - case 'space.child': - return ( - <ChatRoomHeaderTitle - href={'/spaces/' + room.content.holiSpaceId} - name={room.content.spaceName} - initials={getInitials(room.content.spaceName)} - imageSrc={room.avatar} - defaultColor={room.content.avatarDefaultColor} - shape="square" - /> - ) + ) : ( + <ChatRoomHeaderTitle + name={room.name} + imageSrc={chatPartner?.avatar} + initials={getChatInitials(chatPartner?.name)} + defaultColor={getDefaultAvatarColor(chatPartner?.name)} + /> + ) + + case 'space.child': + return ( + <ChatRoomHeaderTitle + href={'/spaces/' + room.content.holiSpaceId} + name={room.content.spaceName} + initials={getInitials(room.content.spaceName)} + imageSrc={room.avatar} + defaultColor={room.content.avatarDefaultColor} + shape="square" + /> + ) - default: - return null - } - }, - navigationCustomOptions: { - footerShown: false, - }, - }) - }, [handleBackPress, chatPartner, setScreenOptions, chatPartnerHoliUser, roomId, room, isSpaceRoom, t, loadingUsers]) + default: + return null + } + } useEffect(() => { if (chatInitialized && !room) { @@ -208,95 +182,66 @@ const ChatRoomView = () => { if (!chatInitialized || !room || loadingUsers) return <HoliLoader testID={'chat-room-loader'} /> return ( - <HoliKeyboardSafeAreaView> - <View style={styles.layoutWrapper}> - <PermissionsGuard permissionType={PermissionsType.AUTH} redirectRoute="/chat"> - <HoliTransition.Fade visible style={{ flex: 1 }}> - <SafeAreaView style={styles.safeArea}> - <View style={styles.container}> - <View style={styles.listContainer}> - {room.hasInvitation ? ( - <ChatProfileCard - member={chatPartner} - roomId={roomId} - user={chatPartnerHoliUser} - userIsLoading={loadingUsers} - /> - ) : ( - <RoomMessageList - roomId={room.id} - messages={messages || []} - matrixIdToHoliUserRecord={matrixIdToHoliUserRecord} - starter={ - amIRoomCreator && - chatPartnerHoliUser && ( - <View style={styles.listStage}> - <RoomMessageListStart member={chatPartner} chatPartnerHoliUser={chatPartnerHoliUser} /> - </View> - ) - } - onScroll={({ nativeEvent: { contentOffset, contentSize, layoutMeasurement } }) => { - const needToLoad = contentSize.height - contentOffset.y < layoutMeasurement.height * 2 - - if (needToLoad) { - requestMessages(roomId, false) - } - }} - /> - )} - </View> - - <HoliContainer noPadding> - <ChatTextInput - isVisible={!room.hasInvitation} - userName={chatPartnerHoliUser?.fullName} - roomId={roomId} - onMessageSent={sendTrackingEvent} - /> - </HoliContainer> - </View> - </SafeAreaView> - </HoliTransition.Fade> - </PermissionsGuard> - </View> - </HoliKeyboardSafeAreaView> + <PermissionsGuard permissionType={PermissionsType.AUTH} redirectRoute="/chat"> + <Screen + preset="fixed" + keyboardVerticalOffset={Platform.OS == 'ios' ? 168 : undefined} //FIXME: This is needed cause of a bug inside screen component that must be addressed. + StickyBottomComponent={ + <ChatTextInput + isVisible={!room.hasInvitation} + userName={chatPartnerHoliUser?.fullName} + roomId={roomId} + onMessageSent={sendTrackingEvent} + /> + } + contentContainerStyle={{ flex: 1 }} + headerOptions={{ + headerTitle, + headerLeft: ({ tintColor }) => <HeaderBackButton tintColor={tintColor} onPress={handleBackPress} />, + headerRight: () => + !!chatPartner && + !isSpaceRoom && ( + <ChatRoomActionDrawer + roomId={roomId} + chatPartner={chatPartner} + chatPartnerFullName={chatPartnerHoliUser?.fullName ?? chatPartner.name} + /> + ), + navigationCustomOptions: { + footerShown: false, + }, + }} + > + {room.hasInvitation ? ( + <ChatProfileCard + member={chatPartner} + roomId={roomId} + user={chatPartnerHoliUser} + userIsLoading={loadingUsers} + /> + ) : ( + <RoomMessageList + roomId={room.id} + messages={messages || []} + matrixIdToHoliUserRecord={matrixIdToHoliUserRecord} + starter={ + amIRoomCreator && + chatPartnerHoliUser && ( + <RoomMessageListStart member={chatPartner} chatPartnerHoliUser={chatPartnerHoliUser} /> + ) + } + onScroll={({ nativeEvent: { contentOffset, contentSize, layoutMeasurement } }) => { + const needToLoad = contentSize.height - contentOffset.y < layoutMeasurement.height * 2 + + if (needToLoad) { + requestMessages(roomId, false) + } + }} + /> + )} + </Screen> + </PermissionsGuard> ) } export default ChatRoomView - -const stylesheet = createStyleSheet((theme) => ({ - layoutWrapper: { - ...Platform.select({ - web: { - height: '100%', - }, - native: { - flex: 1, - }, - }), - }, - safeArea: { - flex: 1, - }, - headerTitle: { - alignItems: 'center', - flexDirection: 'row', - gap: 8, - }, - container: { - flex: 1, - alignItems: 'center', - backgroundColor: theme.colors.bg.container, - }, - listContainer: { - flex: 1, - height: '100%', - width: '100%', - }, - listStage: { - alignSelf: 'center', - paddingHorizontal: 16, - maxWidth: dimensions.maxScreenWidth, - }, -})) diff --git a/core/screens/comments/components/CommentText.tsx b/core/screens/comments/components/CommentText.tsx index 2b00d4381e4b28e7f88b1b66ff36f4886bc02c04..d1caa55ac89d3dd04748b47c5cfcc684ab9d1196 100644 --- a/core/screens/comments/components/CommentText.tsx +++ b/core/screens/comments/components/CommentText.tsx @@ -2,12 +2,11 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import HoliTranslate from '@holi/core/components/HoliTranslate' -import { Comment } from '@holi/core/screens/comments/types' +import type { Comment } from '@holi/core/screens/comments/types' import { TrackingEvent } from '@holi/core/tracking' -import { HoliTextProps } from '@holi/ui/components/atoms/HoliText' -import HoliRichTextOutput from '@holi/ui/components/organisms/HoliRichTextOutput' +import HoliRichTextOutput, { type HoliRichTextProps } from '@holi/ui/components/organisms/HoliRichTextOutput' -type CommentTextProps = HoliTextProps & { +type CommentTextProps = Omit<HoliRichTextProps, 'text'> & { comment: Comment linkMentionsToProfile?: boolean } @@ -22,9 +21,9 @@ const CommentText = ({ comment, ...textProps }: CommentTextProps) => { return ( <> <HoliRichTextOutput + {...textProps} text={text} users={comment.mentions || []} - {...textProps} allowAllLinks loading={loadingTranslation} skeletonOptions={{ numberOfLines: 3 }} diff --git a/core/screens/individualPosts/components/GenericPostCard.tsx b/core/screens/individualPosts/components/GenericPostCard.tsx index 3f9280a0d1dd9c25cbcb6bdba05d554eb819bdc0..517b07548381814e12efb290c2beb15e72eaed7e 100644 --- a/core/screens/individualPosts/components/GenericPostCard.tsx +++ b/core/screens/individualPosts/components/GenericPostCard.tsx @@ -171,7 +171,7 @@ const GenericPostCard = ({ post, showVisibilityBadge, allTopics }: GenericPostCa onPressText={() => { goToDetailView() }} - color="gray300" + color="support" users={feedPost.mentionedUsers} /> </HoliReadMore> @@ -327,7 +327,7 @@ const GenericPostCard = ({ post, showVisibilityBadge, allTopics }: GenericPostCa onPressText={() => { goToDetailView() }} - color="gray300" + color="support" users={spacePost.mentions} /> </HoliReadMore> diff --git a/core/screens/individualPosts/components/PostCard.tsx b/core/screens/individualPosts/components/PostCard.tsx index f08a09e67a43cead804bfbafbe77706013ad4bbe..04345efa3bc49d35da71dc002e8205b34b2b4fd0 100644 --- a/core/screens/individualPosts/components/PostCard.tsx +++ b/core/screens/individualPosts/components/PostCard.tsx @@ -5,11 +5,11 @@ import { StyleSheet, View } from 'react-native' import { useLoggedInUser } from '@holi/core/auth/hooks/useLoggedInUser' import HoliMediaAuthorHeader from '@holi/core/components/HoliMediaAuthorHeader' -import { Term, User } from '@holi/core/domain/shared/types' +import type { Term, User } from '@holi/core/domain/shared/types' import useRouting from '@holi/core/navigation/hooks/useRouting' import FeedPostReactions from '@holi/core/screens/individualPosts/components/FeedPostReactions' import PostDrawer from '@holi/core/screens/individualPosts/components/PostDrawer' -import { FeedPost, VisibilityLevel } from '@holi/core/screens/individualPosts/types' +import { type FeedPost, VisibilityLevel } from '@holi/core/screens/individualPosts/types' import { TrackingEvent } from '@holi/core/tracking' import useTracking from '@holi/core/tracking/hooks/useTracking' import { HoliIcon } from '@holi/icons/src/HoliIcon' @@ -24,7 +24,7 @@ import HoliRichTextOutput from '@holi/ui/components/organisms/HoliRichTextOutput import HoliUserAvatarPile from '@holi/ui/components/organisms/HoliUserAvatarPile' import Spacing from '@holi/ui/foundations/Spacing' import { colors } from '@holi/ui/styles/globalVars' -import { HoliTheme, useTheme } from '@holi/ui/styles/theme' +import { type HoliTheme, useTheme } from '@holi/ui/styles/theme' type PostCardProps = { post: FeedPost @@ -118,7 +118,7 @@ const PostCard = ({ post, showVisibilityBadge, allTopics }: PostCardProps) => { onPressText={() => { goToDetailView() }} - color="gray300" + color="support" users={post.mentionedUsers} /> </HoliReadMore> diff --git a/core/screens/insights/discussions/DiscussionCard.tsx b/core/screens/insights/discussions/DiscussionCard.tsx index 4b41c51f03748e20c64b4b0f0629f4e522992148..c41c63e0554aa1f0aaecca0b1656ca49b3204a3a 100644 --- a/core/screens/insights/discussions/DiscussionCard.tsx +++ b/core/screens/insights/discussions/DiscussionCard.tsx @@ -94,8 +94,8 @@ const DiscussionCard = ({ discussion, insightId }: DiscussionCardProps) => { </HoliText> <CommentText comment={topComment} - size="small" - color="gray300" + size="sm" + color="support" numberOfLines={2} linkMentionsToProfile={false} /> diff --git a/core/screens/notifications/components/__tests__/NotificationsOverview.test.tsx b/core/screens/notifications/components/__tests__/NotificationsOverview.test.tsx index c3b85ce0def6ec5ddff08f6093c583f3458efd1a..765760969da595f152e4a433f251dd41a22d5a93 100644 --- a/core/screens/notifications/components/__tests__/NotificationsOverview.test.tsx +++ b/core/screens/notifications/components/__tests__/NotificationsOverview.test.tsx @@ -25,6 +25,7 @@ const loggedOut = (): LoginState => ({ }) const useLoginStateMock = useLoginState as jest.Mock const mockUseNotifications = jest.fn() +jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')) jest.mock('@holi/core/auth/loginState', () => ({ useLoginState: jest.fn(), diff --git a/core/screens/onboarding/__tests__/Onboarding.test.tsx b/core/screens/onboarding/__tests__/Onboarding.test.tsx index e679d438682664a11ae7c9b7e7811cb5e6df477a..596efdbad94ca8e05793165fce3877946af67db8 100644 --- a/core/screens/onboarding/__tests__/Onboarding.test.tsx +++ b/core/screens/onboarding/__tests__/Onboarding.test.tsx @@ -36,6 +36,7 @@ const mockUseRedirect = useRedirect as jest.Mock jest.mock('@holi/core/navigation/hooks/useRedirect', () => ({ useRedirect: jest.fn(), })) +jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')) const mockUseTracking = useTracking as jest.Mock jest.mock('@holi/core/tracking/hooks/useTracking', () => jest.fn()) diff --git a/core/screens/polls/components/PollResults.tsx b/core/screens/polls/components/PollResults.tsx index c16696dc37c78e2a7613b0891377884ef0b22a39..7df6bbc4cbd48be1d2a8977f65b7c77287cf989f 100644 --- a/core/screens/polls/components/PollResults.tsx +++ b/core/screens/polls/components/PollResults.tsx @@ -1,10 +1,11 @@ import React, { useMemo, useRef } from 'react' import { Animated, StyleSheet, View } from 'react-native' -import { Choice } from '@holi/core/screens/polls/types' -import HoliText from '@holi/ui/components/atoms/HoliText' +import type { Choice } from '@holi/core/screens/polls/types' import HoliRichTextOutput from '@holi/ui/components/organisms/HoliRichTextOutput' -import { HoliTheme, useTheme } from '@holi/ui/styles/theme' +import { type HoliTheme, useTheme } from '@holi/ui/styles/theme' +import { Text } from 'holi-bricks/components/text' +import Spacing from '@holi/ui/foundations/Spacing' interface PollResultsProps { choice: Choice @@ -35,11 +36,11 @@ const PollResults = ({ choice, votesSum, selected }: PollResultsProps) => { users={[]} links={[]} allowAllLinks - size="small" + size="sm" /> - <HoliText style={styles.percentText} size="small" testID="poll-result-percentage"> + <Text size="sm" testID="poll-result-percentage"> {percentage}% - </HoliText> + </Text> </View> <View style={styles.progressBar}> <Animated.View style={{ ...styles.innerBar, width: progressWidth }} /> @@ -63,6 +64,7 @@ const getStyles = (theme: HoliTheme) => alignItems: 'flex-start', flex: 1, marginBottom: 5, + gap: Spacing['lg'], }, optionsTextStyle: { flexWrap: 'wrap', @@ -84,11 +86,6 @@ const getStyles = (theme: HoliTheme) => backgroundColor: theme.colors.brandColorB30, borderRadius: 12, }, - percentText: { - fontSize: 14, - marginLeft: 30, - color: theme.colors.foreground30, - }, }) export default PollResults diff --git a/core/screens/polls/components/PollSelection.tsx b/core/screens/polls/components/PollSelection.tsx index 4f39f9218fddabf614332b30e5efe1fe0a20d985..4e8d2d914e83953c8a566f33cf88baf6a0958233 100644 --- a/core/screens/polls/components/PollSelection.tsx +++ b/core/screens/polls/components/PollSelection.tsx @@ -1,12 +1,12 @@ import React, { useCallback, useMemo, useState } from 'react' import { Platform, StyleSheet, View } from 'react-native' -import { Choice } from '@holi/core/screens/polls/types' +import type { Choice } from '@holi/core/screens/polls/types' import HoliBox from '@holi/ui/components/atoms/HoliBox' import HoliButton from '@holi/ui/components/molecules/HoliButton' import HoliCheckbox from '@holi/ui/components/molecules/HoliCheckbox' import HoliRichTextOutput from '@holi/ui/components/organisms/HoliRichTextOutput' -import { HoliTheme, useTheme } from '@holi/ui/styles/theme' +import { type HoliTheme, useTheme } from '@holi/ui/styles/theme' export type PollSelectionProps = { answerOptions: Choice[] @@ -49,7 +49,7 @@ const PollSelection = ({ answerOptions, onSelected }: PollSelectionProps) => { /> </HoliBox> <View style={[styles.userItemContent]}> - <HoliRichTextOutput text={answer.choiceText} users={[]} links={[]} allowAllLinks size="small" /> + <HoliRichTextOutput text={answer.choiceText} users={[]} links={[]} allowAllLinks size="sm" /> </View> </HoliButton> </View> diff --git a/core/screens/polls/components/__tests__/__snapshots__/PollResults.test.tsx.snap b/core/screens/polls/components/__tests__/__snapshots__/PollResults.test.tsx.snap index 0b3799c4e722835d4f646b493d8eb57d92cef3ff..f0bb384fe33cb2b6475f2055cdfb23e55763bfe9 100644 --- a/core/screens/polls/components/__tests__/__snapshots__/PollResults.test.tsx.snap +++ b/core/screens/polls/components/__tests__/__snapshots__/PollResults.test.tsx.snap @@ -20,31 +20,28 @@ exports[`PollResults renders correctly as expected 1`] = ` "alignItems": "flex-start", "flex": 1, "flexDirection": "row", + "gap": 32, "marginBottom": 5, } } > <Text accessible={false} - collapsable={false} - jestAnimatedStyle={ - { - "value": {}, - } - } - numberOfLines={2} + role="none" selectable={true} style={ { - "color": "#262424", - "flexWrap": "wrap", + "color": "#676565", "fontFamily": "InstrumentSans-Regular", - "fontSize": 14, + "fontSize": 15, + "fontVariant": [ + "lining-nums", + "tabular-nums", + ], "fontWeight": "400", - "lineHeight": 19.599999999999998, - "marginRight": "auto", - "maxWidth": "80%", - "paddingRight": "auto", + "letterSpacing": -0.12, + "lineHeight": 21, + "textAlign": undefined, "userSelect": "auto", } } @@ -53,21 +50,21 @@ exports[`PollResults renders correctly as expected 1`] = ` </Text> <Text accessible={false} - collapsable={false} - jestAnimatedStyle={ - { - "value": {}, - } - } + role="none" selectable={true} style={ { "color": "#262424", "fontFamily": "InstrumentSans-Regular", - "fontSize": 14, + "fontSize": 15, + "fontVariant": [ + "lining-nums", + "tabular-nums", + ], "fontWeight": "400", - "lineHeight": 19.599999999999998, - "marginLeft": 30, + "letterSpacing": -0.12, + "lineHeight": 21, + "textAlign": undefined, "userSelect": "auto", } } diff --git a/core/screens/polls/components/__tests__/__snapshots__/PollSelection.test.tsx.snap b/core/screens/polls/components/__tests__/__snapshots__/PollSelection.test.tsx.snap index 8f05f9db25dd70e6fb64233d4b9441dbf41c3dce..0aa3171ac6d36e057b9138cc8c8e356c43fe3edc 100644 --- a/core/screens/polls/components/__tests__/__snapshots__/PollSelection.test.tsx.snap +++ b/core/screens/polls/components/__tests__/__snapshots__/PollSelection.test.tsx.snap @@ -166,20 +166,21 @@ exports[`PollSelection renders poll selection correctly 1`] = ` > <Text accessible={false} - collapsable={false} - jestAnimatedStyle={ - { - "value": {}, - } - } + role="none" selectable={true} style={ { - "color": "#262424", + "color": "#676565", "fontFamily": "InstrumentSans-Regular", - "fontSize": 14, + "fontSize": 15, + "fontVariant": [ + "lining-nums", + "tabular-nums", + ], "fontWeight": "400", - "lineHeight": 19.599999999999998, + "letterSpacing": -0.12, + "lineHeight": 21, + "textAlign": undefined, "userSelect": "auto", } } @@ -354,20 +355,21 @@ exports[`PollSelection renders poll selection correctly 1`] = ` > <Text accessible={false} - collapsable={false} - jestAnimatedStyle={ - { - "value": {}, - } - } + role="none" selectable={true} style={ { - "color": "#262424", + "color": "#676565", "fontFamily": "InstrumentSans-Regular", - "fontSize": 14, + "fontSize": 15, + "fontVariant": [ + "lining-nums", + "tabular-nums", + ], "fontWeight": "400", - "lineHeight": 19.599999999999998, + "letterSpacing": -0.12, + "lineHeight": 21, + "textAlign": undefined, "userSelect": "auto", } } @@ -548,20 +550,21 @@ exports[`PollSelection renders poll selection correctly with one option 1`] = ` > <Text accessible={false} - collapsable={false} - jestAnimatedStyle={ - { - "value": {}, - } - } + role="none" selectable={true} style={ { - "color": "#262424", + "color": "#676565", "fontFamily": "InstrumentSans-Regular", - "fontSize": 14, + "fontSize": 15, + "fontVariant": [ + "lining-nums", + "tabular-nums", + ], "fontWeight": "400", - "lineHeight": 19.599999999999998, + "letterSpacing": -0.12, + "lineHeight": 21, + "textAlign": undefined, "userSelect": "auto", } } diff --git a/core/screens/posts/components/PostPreview.tsx b/core/screens/posts/components/PostPreview.tsx index 51f1b40a23bbfa93f05564fe4ca2ba56de32764d..0eb0b2894df7abe71839268420a940b46436c994 100644 --- a/core/screens/posts/components/PostPreview.tsx +++ b/core/screens/posts/components/PostPreview.tsx @@ -235,8 +235,7 @@ const PostPreview = ({ users={post?.mentions || []} links={post?.links || []} allowAllLinks - size="default" - color="gray300" + color="default" /> </HoliReadMore> )} diff --git a/core/screens/search/typesense/Search.tsx b/core/screens/search/typesense/Search.tsx index 49b3b649aa7709e32e649669869c9fae45d74790..a79b8007fe27f1c009e82588655d8b0e98e69607 100644 --- a/core/screens/search/typesense/Search.tsx +++ b/core/screens/search/typesense/Search.tsx @@ -65,7 +65,6 @@ export const Search = () => { <Screen backgroundColor="page" preset="fixed" - contentContainerPadding={{}} contentContainerStyle={{ flex: 1 }} // this is needed because flash list has no initial size, it calculates it while rendering. style={styles.screen} headerOptions={{ diff --git a/core/screens/spaces/appointments/AppointmentDetails.tsx b/core/screens/spaces/appointments/AppointmentDetails.tsx index 849627efc31bc539e0431d9bff97bc5c5176b651..43aa3ca1e233a704b3ea5f270d478c92b71e03e4 100644 --- a/core/screens/spaces/appointments/AppointmentDetails.tsx +++ b/core/screens/spaces/appointments/AppointmentDetails.tsx @@ -320,8 +320,8 @@ const AppointmentDetails = () => { <HoliRichTextOutput style={styles.description} text={appointment.description} - color="gray300" - size="small" + color="support" + size="sm" allowAllLinks testID="appointment-description" /> diff --git a/core/screens/spaces/details/__tests__/SpaceDetails.test.tsx b/core/screens/spaces/details/__tests__/SpaceDetails.test.tsx index d98faf6196cc7a81a04ddff69d7fe3f7416a504b..14d7f7b1bac91d8570dafd70a998b90b71e7d109 100644 --- a/core/screens/spaces/details/__tests__/SpaceDetails.test.tsx +++ b/core/screens/spaces/details/__tests__/SpaceDetails.test.tsx @@ -72,6 +72,9 @@ jest.mock('@holi/core/featureFlags/hooks/useFeatureFlag', () => ({ useFeatureFlag: (flag: FeatureFlagKey) => mockUseFeatureFlag(flag), })) +jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')) +jest.useFakeTimers() + jest.mock('@holi/core/navigation/hooks/useParam') describe('SpaceDetails', () => { diff --git a/core/screens/spaces/details/components/__tests__/SpaceFollow.test.tsx b/core/screens/spaces/details/components/__tests__/SpaceFollow.test.tsx index c14ab059ac59157e26e9a47020718b8233c0a569..f72c61b65ef0f7a090a9a0f71576e92d6cebf395 100644 --- a/core/screens/spaces/details/components/__tests__/SpaceFollow.test.tsx +++ b/core/screens/spaces/details/components/__tests__/SpaceFollow.test.tsx @@ -2,7 +2,7 @@ import { MockedProvider } from '@apollo/client/testing' import { fireEvent, render, screen, waitFor } from '@testing-library/react-native' import React from 'react' -import { LoginState } from '@holi/core/auth/loginState' +import type { LoginState } from '@holi/core/auth/loginState' import { authenticatedUserV2Query } from '@holi/core/domain/shared/queries' import { mockParameters } from '@holi/core/navigation/hooks/useParam/__mocks__/mockParameters' import { space } from '@holi/core/screens/spaces/__tests__/testData' @@ -24,6 +24,7 @@ jest.mock('@holi/core/errors/hooks/useErrorHandling', () => ({ displayError: jest.fn(), }), })) +jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')) const authenticatedUserRequest = { request: { diff --git a/core/screens/spaces/details/components/__tests__/__snapshots__/SpaceInvolvement.test.tsx.snap b/core/screens/spaces/details/components/__tests__/__snapshots__/SpaceInvolvement.test.tsx.snap index 3395ef5373c8983e16ef10dd531dcaf5efc0ffd7..84a383e35c676d216daa22e4a9a227e7bbc1ba88 100644 --- a/core/screens/spaces/details/components/__tests__/__snapshots__/SpaceInvolvement.test.tsx.snap +++ b/core/screens/spaces/details/components/__tests__/__snapshots__/SpaceInvolvement.test.tsx.snap @@ -784,20 +784,21 @@ exports[`SpaceInvolvement renders correctly for the public 1`] = ` /> <Text accessible={false} - collapsable={false} - jestAnimatedStyle={ - { - "value": {}, - } - } + role="none" selectable={true} style={ { - "color": "#262424", + "color": "#676565", "fontFamily": "InstrumentSans-Regular", - "fontSize": 14, + "fontSize": 15, + "fontVariant": [ + "lining-nums", + "tabular-nums", + ], "fontWeight": "400", - "lineHeight": 19.599999999999998, + "letterSpacing": -0.12, + "lineHeight": 21, + "textAlign": undefined, "userSelect": "auto", } } @@ -1489,20 +1490,21 @@ exports[`SpaceInvolvement renders correctly for the public 1`] = ` /> <Text accessible={false} - collapsable={false} - jestAnimatedStyle={ - { - "value": {}, - } - } + role="none" selectable={true} style={ { - "color": "#262424", + "color": "#676565", "fontFamily": "InstrumentSans-Regular", - "fontSize": 14, + "fontSize": 15, + "fontVariant": [ + "lining-nums", + "tabular-nums", + ], "fontWeight": "400", - "lineHeight": 19.599999999999998, + "letterSpacing": -0.12, + "lineHeight": 21, + "textAlign": undefined, "userSelect": "auto", } } diff --git a/core/screens/spaces/discover/__tests__/Spaces.test.tsx b/core/screens/spaces/discover/__tests__/Spaces.test.tsx index 58ee3080393a13356f8e17ab13f70dfbe1adea7a..cd40db713a0dbec80cc870274971ef241b59c993 100644 --- a/core/screens/spaces/discover/__tests__/Spaces.test.tsx +++ b/core/screens/spaces/discover/__tests__/Spaces.test.tsx @@ -27,6 +27,7 @@ const loginStateLoggedIn = { jest.mock('@holi/core/auth/hooks/useLoggedInUser', () => ({ useLoggedInUser: () => loginStateLoggedIn, })) +jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')) jest.mock('@holi/core/helpers/useLogout', () => ({ useLogout: () => ({ logout: jest.fn() }), diff --git a/core/screens/spaces/edit/components/__tests__/EditSpaceCollaborators.test.tsx b/core/screens/spaces/edit/components/__tests__/EditSpaceCollaborators.test.tsx index dc29bba83127a0a8053b15702a0f5326a1518dbc..59b38af08fbe19b0c810092c262da1bf2ac18628 100644 --- a/core/screens/spaces/edit/components/__tests__/EditSpaceCollaborators.test.tsx +++ b/core/screens/spaces/edit/components/__tests__/EditSpaceCollaborators.test.tsx @@ -1,5 +1,5 @@ import { ApolloError } from '@apollo/client' -import { GraphQLErrors } from '@apollo/client/errors' +import type { GraphQLErrors } from '@apollo/client/errors' import { MockedProvider } from '@apollo/client/testing' import { act, render, screen, userEvent, waitFor, waitForElementToBeRemoved } from '@testing-library/react-native' import React from 'react' @@ -18,6 +18,7 @@ import { import EditSpaceCollaborators from '@holi/core/screens/spaces/edit/components/EditSpaceCollaborators' import { spaceMembershipByIdQuery } from '@holi/core/screens/spaces/queries' import { SpaceConnectionType } from '@holi/core/screens/spaces/types' +jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')) const spaceForAdmin = { ...space, diff --git a/core/screens/spaces/filter/components/__tests__/FilteredSpaces.test.tsx b/core/screens/spaces/filter/components/__tests__/FilteredSpaces.test.tsx index 6ca704608e1cf6f3217d9ea07ded96feb54b08c2..c220ea0975af12ede00f1492f88d83842c049897 100644 --- a/core/screens/spaces/filter/components/__tests__/FilteredSpaces.test.tsx +++ b/core/screens/spaces/filter/components/__tests__/FilteredSpaces.test.tsx @@ -1,8 +1,8 @@ -import { MockedProvider, MockedResponse } from '@apollo/client/testing' +import { MockedProvider, type MockedResponse } from '@apollo/client/testing' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react-native' import React from 'react' -import { NavigationRouteContext, Route } from '@react-navigation/native' +import { NavigationRouteContext, type Route } from '@react-navigation/native' import { createCache } from '@holi/api/graphql/client' import { HoliError } from '@holi/core/errors/classes/HoliError' @@ -14,6 +14,7 @@ import { spacesQuery } from '@holi/core/screens/spaces/filter/queries' import { HoliToastType, ToastProvider } from '@holi/ui/components/molecules/HoliToastProvider' jest.spyOn(window, 'requestAnimationFrame').mockImplementation(() => Date.now()) +jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')) // prevent logging about incomplete apollo mocks, // because we didn't flesh out the response data diff --git a/core/screens/spaces/tasks/components/SpaceTaskTile/Full.tsx b/core/screens/spaces/tasks/components/SpaceTaskTile/Full.tsx index 0396429d26c3cff2c70e61edc880ed16d61b7daf..a0f62a7cfe97790187e26b71a505316e428dd92c 100644 --- a/core/screens/spaces/tasks/components/SpaceTaskTile/Full.tsx +++ b/core/screens/spaces/tasks/components/SpaceTaskTile/Full.tsx @@ -54,7 +54,7 @@ const List = ({ loading, task, space }: PropsWithSkeleton<BaseProps>) => { </HoliLink> <HoliGap size={'s'} /> - <Content task={task} richTextOutputColor="gray300" descriptionWithDetails /> + <Content task={task} richTextOutputColor="support" descriptionWithDetails /> </HoliCardComponent.ContentContainer> </HoliCardComponent.CardContainer> ) diff --git a/core/screens/spaces/tasks/components/SpaceTaskTile/components/Content.tsx b/core/screens/spaces/tasks/components/SpaceTaskTile/components/Content.tsx index 7f491cc4d967bf433b419b2f45a030bcacede3de..448607319b73a2169e63cb332d7bb4271ae6016d 100644 --- a/core/screens/spaces/tasks/components/SpaceTaskTile/components/Content.tsx +++ b/core/screens/spaces/tasks/components/SpaceTaskTile/components/Content.tsx @@ -15,22 +15,23 @@ import { TimeFilled, } from '@holi/icons/src/generated' import { HoliGap } from '@holi/ui/components/atoms/HoliGap' -import HoliText, { type HoliTextProps } from '@holi/ui/components/atoms/HoliText' +import HoliText from '@holi/ui/components/atoms/HoliText' import HoliButton from '@holi/ui/components/molecules/HoliButton' import HoliRichTextOutput from '@holi/ui/components/organisms/HoliRichTextOutput' import { useTheme } from '@holi/ui/styles/theme' import { Avatar } from 'holi-bricks/components/avatar' +import type { TextColor } from 'holi-bricks/components/text' export interface Props { task: Task - richTextOutputColor?: HoliTextProps['color'] + richTextOutputColor?: TextColor descriptionWithDetails?: boolean recommendation?: boolean } const Content = ({ task, - richTextOutputColor = 'black300', + richTextOutputColor = 'support', descriptionWithDetails = false, recommendation = false, }: Props) => { @@ -92,7 +93,7 @@ const Content = ({ {...(recommendation ? { numberOfLines: 2 } : {})} text={task.description} color={richTextOutputColor} - size="small" + size="sm" allowAllLinks testID="task-description" /> diff --git a/core/screens/spaces/tasks/components/__tests__/__snapshots__/SpaceTaskList.test.tsx.snap b/core/screens/spaces/tasks/components/__tests__/__snapshots__/SpaceTaskList.test.tsx.snap index dc7791096da2ae259ca1e1511bdccdab6c707413..c4f61ddd6dbaa8370c09f242bf9461d97e654a75 100644 --- a/core/screens/spaces/tasks/components/__tests__/__snapshots__/SpaceTaskList.test.tsx.snap +++ b/core/screens/spaces/tasks/components/__tests__/__snapshots__/SpaceTaskList.test.tsx.snap @@ -649,20 +649,21 @@ exports[`SpaceTaskList renders correctly when there are tasks within space 1`] = /> <Text accessible={false} - collapsable={false} - jestAnimatedStyle={ - { - "value": {}, - } - } + role="none" selectable={true} style={ { - "color": "#262424", + "color": "#676565", "fontFamily": "InstrumentSans-Regular", - "fontSize": 14, + "fontSize": 15, + "fontVariant": [ + "lining-nums", + "tabular-nums", + ], "fontWeight": "400", - "lineHeight": 19.599999999999998, + "letterSpacing": -0.12, + "lineHeight": 21, + "textAlign": undefined, "userSelect": "auto", } } @@ -1354,20 +1355,21 @@ exports[`SpaceTaskList renders correctly when there are tasks within space 1`] = /> <Text accessible={false} - collapsable={false} - jestAnimatedStyle={ - { - "value": {}, - } - } + role="none" selectable={true} style={ { - "color": "#262424", + "color": "#676565", "fontFamily": "InstrumentSans-Regular", - "fontSize": 14, + "fontSize": 15, + "fontVariant": [ + "lining-nums", + "tabular-nums", + ], "fontWeight": "400", - "lineHeight": 19.599999999999998, + "letterSpacing": -0.12, + "lineHeight": 21, + "textAlign": undefined, "userSelect": "auto", } } @@ -2059,20 +2061,21 @@ exports[`SpaceTaskList renders correctly when there are tasks within space 1`] = /> <Text accessible={false} - collapsable={false} - jestAnimatedStyle={ - { - "value": {}, - } - } + role="none" selectable={true} style={ { - "color": "#262424", + "color": "#676565", "fontFamily": "InstrumentSans-Regular", - "fontSize": 14, + "fontSize": 15, + "fontVariant": [ + "lining-nums", + "tabular-nums", + ], "fontWeight": "400", - "lineHeight": 19.599999999999998, + "letterSpacing": -0.12, + "lineHeight": 21, + "textAlign": undefined, "userSelect": "auto", } } diff --git a/global.d.ts b/global.d.ts index c9d67a6f12a19a933af6d626fd03ccfdabdaafa2..367a01f4bc770f6a5187a126d92be87a10a04419 100644 --- a/global.d.ts +++ b/global.d.ts @@ -1,8 +1,8 @@ /* eslint-disable no-var */ import type { ScreenOptions } from '@react-navigation/native' -import { TrackingEvent } from '@holi/core/tracking' -import { HoliToastType } from '@holi/ui/components/molecules/HoliToastProvider' +import type { TrackingEvent } from '@holi/core/tracking' +import type { HoliToastType } from '@holi/ui/components/molecules/HoliToastProvider' declare global { var mockNavigate: (routeName: string) => void diff --git a/holi-apps/volunteering/screens/__tests__/VolunteeringSimilarRecos.test.tsx b/holi-apps/volunteering/screens/__tests__/VolunteeringSimilarRecos.test.tsx index 736d7ac9c51dc58c27e6f10665a5daad7471e629..94412518c802fd3aeea00883addf057c48b749b9 100644 --- a/holi-apps/volunteering/screens/__tests__/VolunteeringSimilarRecos.test.tsx +++ b/holi-apps/volunteering/screens/__tests__/VolunteeringSimilarRecos.test.tsx @@ -1,20 +1,20 @@ import { render, screen, waitFor } from '@testing-library/react-native' import React from 'react' -import { MockedProvider, MockedResponse } from '@apollo/client/testing' +import { MockedProvider, type MockedResponse } from '@apollo/client/testing' import { allSkills, allTopics } from '@holi-apps/volunteering/__tests__/helpers/fixtures' import { makeEngagement } from '@holi-apps/volunteering/__tests__/helpers/generators' import { - AppVolunteeringEngagementByIdResponse, - AppVolunteeringEngagementByIdVariables, - VolunteeringSimilarRecosQueryResponse, - VolunteeringSimilarRecosQueryVariables, + type AppVolunteeringEngagementByIdResponse, + type AppVolunteeringEngagementByIdVariables, + type VolunteeringSimilarRecosQueryResponse, + type VolunteeringSimilarRecosQueryVariables, engagementByIdQuery, trackEngagementViewMutation, volunteeringSimilarRecosQuery, } from '@holi-apps/volunteering/queries' import { ExportedForTesting } from '@holi-apps/volunteering/screens/VolunteeringEngagementDetail' -import { +import type { AppVolunteering_TrackEngagementViewResponse, MutationAppVolunteering_TrackEngagementViewArgs, } from '@holi/api/graphql/graphql-codegen' @@ -23,6 +23,8 @@ import { mockParameters } from '@holi/core/navigation/hooks/useParam/__mocks__/m const mockEngagementId = '123' +jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')) + const mockEngagementDetailQuery: MockedResponse< AppVolunteeringEngagementByIdResponse, AppVolunteeringEngagementByIdVariables diff --git a/holi-bricks/components/text/Text.tsx b/holi-bricks/components/text/Text.tsx index b46f357bfa22ddc1fdd7593504cd33142effd156..e5282ad1dde15b87796f31f5889b4536c6d1f9c7 100644 --- a/holi-bricks/components/text/Text.tsx +++ b/holi-bricks/components/text/Text.tsx @@ -46,6 +46,7 @@ export interface TextProps { textAlign?: TextStyle['textAlign'] ellipsizeMode?: RNTextProps['ellipsizeMode'] headingLevel?: TextHeadingLevel + testID?: string } // Change import in HOLI-10369 @@ -86,6 +87,7 @@ export const Text = ({ ellipsizeMode, headingLevel, children, + testID, ...rest }: React.PropsWithChildren<TextProps>) => { const { styles } = useStyles(stylesheet, { size }) @@ -100,6 +102,7 @@ export const Text = ({ <RNText {...textAccessibilityProps} {...accessibilityProps(rest)} + testID={testID} selectable={Platform.OS !== 'web' ? selectable : undefined} numberOfLines={numberOfLines} ellipsizeMode={ellipsizeMode} diff --git a/holi-bricks/jest.config.ts b/holi-bricks/jest.config.ts index 23a4c52c5589dbcf439a1c13a4e1798db55e4d20..f93549688c1b88017b54967357718410391b2d34 100644 --- a/holi-bricks/jest.config.ts +++ b/holi-bricks/jest.config.ts @@ -7,9 +7,7 @@ import type { Config } from 'jest' const config: Config = { preset: 'jest-expo', setupFiles: ['<rootDir>/jest.setup.ts'], - clearMocks: true, - // collectCoverage: true, - // coverageDirectory: ".coverage", + clearMocks: false, testEnvironment: 'jsdom', moduleNameMapper: { 'holi-bricks/(.*)': '<rootDir>/$1', diff --git a/holi-bricks/utils/index.ts b/holi-bricks/utils/index.ts index 40cc838f426da58ce24598459ed59845890a835b..71cfa4b074178f1de828b225e4e0823c243f4f3f 100644 --- a/holi-bricks/utils/index.ts +++ b/holi-bricks/utils/index.ts @@ -1,3 +1,4 @@ -import { createStyleSheet as unistylesCreateStylesheet } from 'react-native-unistyles' +import { createStyleSheet as unistylesCreateStylesheet, mq as unistylesMq } from 'react-native-unistyles' export const createStyleSheet = unistylesCreateStylesheet +export const mq = unistylesMq diff --git a/jest.setup.ts b/jest.setup.ts index 239e42bc8a825fc058a14dc4e786b31700f70bdd..553f12670b5c95267671384ea824602cf297ac75 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -5,7 +5,6 @@ import mockSafeAreaContext from 'react-native-safe-area-context/jest/mock' import i18n from '@holi/core/i18n' import type { TrackingEventHandler, TrackingHook } from '@holi/core/tracking/types' -import { TrackingEvent } from '@holi/core/tracking' // Added to fix the following error: https://github.com/facebook/react-native/issues/42904#issuecomment-2306382117 // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unused-vars @@ -52,8 +51,6 @@ jest.mock('react-native/Libraries/Linking/Linking', () => ({ openURL: global.mockOpenURL, })) -jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')) - jest.mock('expo-linking', () => { const module: typeof import('expo-linking') = { ...jest.requireActual('expo-linking'), diff --git a/packages/chat/src/components/ChatTextInput.tsx b/packages/chat/src/components/ChatTextInput.tsx index d0afc308abf9ccef2ede4e5215d82e64108f1710..f1e6bc05d402cba5b2316a052f3bedca6f003522 100644 --- a/packages/chat/src/components/ChatTextInput.tsx +++ b/packages/chat/src/components/ChatTextInput.tsx @@ -6,7 +6,6 @@ import { Platform, StyleSheet, type TextInputKeyPressEventData, View } from 'rea import { sendMessage } from '@holi/chat/src/store' import { useErrorHandling } from '@holi/core/errors/hooks' import { SendPlane } from '@holi/icons/src/generated' -import { HoliTransition } from '@holi/ui/components/atoms/HoliTransition' import { ButtonIcon } from 'holi-bricks/components/button' import HoliTextInput from '@holi/ui/components/molecules/HoliTextInput' import { type HoliTheme, useTheme } from '@holi/ui/styles/theme' @@ -59,11 +58,9 @@ export const ChatTextInput = ({ roomId, isVisible, userName = '', onMessageSent } } - const PLACEHOLDER_MIN_HIGHT = SEND_BUTTON_SIZE + Spacing.xxs * 2 + BORDER_WIDTH * 2 - - return ( - <View style={{ minHeight: PLACEHOLDER_MIN_HIGHT, width: '100%' }}> - <HoliTransition.SlideDown visible={isVisible} style={styles.container}> + return isVisible ? ( + <View style={styles.wrapper}> + <View style={styles.container}> <HoliTextInput aria-label={t('chat.room.input.placeholder', { fullName: userName })} placeholder={t('chat.room.input.placeholder', { fullName: userName })} @@ -88,24 +85,24 @@ export const ChatTextInput = ({ roomId, isVisible, userName = '', onMessageSent /> <View style={styles.buttonWrapper}> - {inputValue.trim().length > 0 && ( - <HoliTransition.Fade visible> - <ButtonIcon - label={t('chat.room.sendButton.label')} - disabled={inputValue.trim().length === 0} - onPress={handleSendMessage} - icon={SendPlane} - /> - </HoliTransition.Fade> - )} + <ButtonIcon + label={t('chat.room.sendButton.label')} + disabled={inputValue.trim().length === 0} + onPress={handleSendMessage} + icon={SendPlane} + /> </View> - </HoliTransition.SlideDown> + </View> </View> - ) + ) : null } const getStyles = (theme: HoliTheme) => StyleSheet.create({ + wrapper: { + minHeight: SEND_BUTTON_SIZE + Spacing.xxs * 2 + BORDER_WIDTH * 2, + width: '100%', + }, container: { flexDirection: 'row', alignItems: 'center', diff --git a/packages/chat/src/components/RoomMessageList/RoomMessage.tsx b/packages/chat/src/components/RoomMessageList/RoomMessage.tsx index bcc075cf1e469b77ae0f71f30f6adcf50bba76ae..6b4135e9452d6b66b54f7a1720b954e1fb7ea9ec 100644 --- a/packages/chat/src/components/RoomMessageList/RoomMessage.tsx +++ b/packages/chat/src/components/RoomMessageList/RoomMessage.tsx @@ -5,7 +5,8 @@ import type { ChatRoomMessage } from '@holi/chat/src/client/types' import RoomMembershipBadge from '@holi/chat/src/components/RoomMessageList/RoomMembershipBadge' import RoomTextMessage from '@holi/chat/src/components/RoomMessageList/RoomTextMessage' import { roomMembersAtomCreator } from '@holi/chat/src/store' -import { User } from '@holi/core/domain/shared/types' +import type { User } from '@holi/core/domain/shared/types' +import { useLoggedInUser } from '@holi/core/auth/hooks/useLoggedInUser' interface Props { message: ChatRoomMessage @@ -20,10 +21,21 @@ interface Props { const RoomMessage = ({ holiUser, message, roomId, ...props }: Props) => { const membersRecord = useAtomValue(useMemo(() => roomMembersAtomCreator(roomId), [roomId])) const member = membersRecord[message.sender ?? ''] + const { isLoading, user: loggedInUser } = useLoggedInUser() + + if (isLoading) return null switch (message.type) { case 'message': - return <RoomTextMessage member={member} holiUser={holiUser} message={message} {...props} /> + return ( + <RoomTextMessage + member={member} + isSender={loggedInUser?.id === holiUser?.id} + holiUser={holiUser} + message={message} + {...props} + /> + ) case 'membership': return <RoomMembershipBadge member={member} user={holiUser} message={message} /> } diff --git a/packages/chat/src/components/RoomMessageList/RoomTextMessage.tsx b/packages/chat/src/components/RoomMessageList/RoomTextMessage.tsx index 6b6784066730b308bbf8d4dce6b9cbfb4230b6ad..28aff8e6a778b784b0730b4670545acbad4a5076 100644 --- a/packages/chat/src/components/RoomMessageList/RoomTextMessage.tsx +++ b/packages/chat/src/components/RoomMessageList/RoomTextMessage.tsx @@ -1,7 +1,7 @@ import { useAtomValue } from 'jotai' -import React, { useMemo } from 'react' +import React from 'react' import { useTranslation } from 'react-i18next' -import { StyleSheet, View } from 'react-native' +import { View } from 'react-native' import type { ChatRoomMember, ChatRoomMessage } from '@holi/chat/src/client/types' import { isMsgTextContent } from '@holi/chat/src/client/utils' @@ -10,11 +10,13 @@ import { getChatInitials, getChatMemberName } from '@holi/chat/src/utils' import { getTimeFormat } from '@holi/core/i18n/helpers/dateHelper' import { getLocale } from '@holi/core/i18n/helpers/dateHelper' import HoliBox from '@holi/ui/components/atoms/HoliBox' -import HoliText from '@holi/ui/components/atoms/HoliText' import HoliRichTextOutput from '@holi/ui/components/organisms/HoliRichTextOutput' -import { type HoliTheme, useTheme } from '@holi/ui/styles/theme' import { Avatar } from 'holi-bricks/components/avatar' import type { User } from '@holi/core/domain/shared/types' +import { createStyleSheet } from 'holi-bricks/utils' +import { useStyles } from 'holi-bricks/hooks' +import { BorderRadius } from 'holi-bricks/tokens' +import { Text } from 'holi-bricks/components/text' interface Props { message: ChatRoomMessage @@ -24,6 +26,7 @@ interface Props { isPrevMessageFromSameUser?: boolean isNextMessageFromSameUser?: boolean testID?: string + isSender?: boolean } const RoomTextMessage = ({ @@ -33,12 +36,12 @@ const RoomTextMessage = ({ showAvatar = false, isPrevMessageFromSameUser = false, isNextMessageFromSameUser = false, + isSender, testID, }: Props) => { const { i18n, t } = useTranslation() const locale = getLocale(i18n.language) - const { theme } = useTheme() - const styles = useMemo(() => getStyles(theme), [theme]) + const { styles } = useStyles(stylesheet) const myId = useAtomValue(loggedInMemberIdAtom) const isMyMessage = message.sender === myId @@ -67,24 +70,26 @@ const RoomTextMessage = ({ <HoliBox padding={[0, 16]} /> )} - <View testID={testID} style={styles.wrapper}> + <View testID={testID} style={styles.wrapper(isSender)}> <HoliBox padding={[4, 8, 8, 8]}> {!isMyMessage && !isPrevMessageFromSameUser && ( <HoliBox padding={[0, 0, 4, 0]}> - <HoliText bold>{getChatMemberName(t, member?.name)}</HoliText> + <Text size="sm" color="support"> + {getChatMemberName(t, member?.name)} + </Text> </HoliBox> )} {content.body ? ( - <HoliRichTextOutput text={content.body} allowAllLinks /> + <HoliRichTextOutput size="md" text={content.body} allowAllLinks /> ) : ( - <HoliText>{t('chat.message.deleted')}</HoliText> + <Text size="md">{t('chat.message.deleted')}</Text> )} <View style={styles.date}> - <HoliText size="extrasmall" color="gray300"> + <Text size="xs" color={'default'}> {getTimeFormat(locale, timestamp)} - </HoliText> + </Text> </View> </HoliBox> </View> @@ -95,21 +100,20 @@ const RoomTextMessage = ({ export default RoomTextMessage -const getStyles = (theme: HoliTheme) => - StyleSheet.create({ - wrapper: { - borderColor: theme.colors.background30, - borderWidth: 1, - borderRadius: 12, - backgroundColor: theme.colors.white100, - flexShrink: 1, - }, - rowWrapper: { - flexDirection: 'row', - alignItems: 'flex-end', - justifyContent: 'flex-start', - }, - date: { - alignItems: 'flex-end', - }, - }) +const stylesheet = createStyleSheet((theme) => ({ + wrapper: (isSender?: boolean) => ({ + borderColor: isSender ? theme.colors.decorative.purpleLight : theme.colors.bg.container, + borderWidth: 1, + borderRadius: BorderRadius.sm, + backgroundColor: isSender ? theme.colors.decorative.purpleLight : theme.colors.bg.container, + flexShrink: 1, + }), + rowWrapper: { + flexDirection: 'row', + alignItems: 'flex-end', + justifyContent: 'flex-start', + }, + date: { + alignItems: 'flex-end', + }, +})) diff --git a/packages/chat/src/components/RoomMessageList/index.tsx b/packages/chat/src/components/RoomMessageList/index.tsx index 082409091d4d5179e125e64b1215ed66ae7496a3..c1d06d3be3604a6ccf2f9f33b8fd5970172b1864 100644 --- a/packages/chat/src/components/RoomMessageList/index.tsx +++ b/packages/chat/src/components/RoomMessageList/index.tsx @@ -1,18 +1,17 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { View } from 'react-native' import type { ChatRoomMessage } from '@holi/chat/src/client/types' import RoomMessageSection from '@holi/chat/src/components/RoomMessageList/RoomMessageSection' import { type MessageItem, isMessageSection, isTextMessageItem } from '@holi/chat/src/components/RoomMessageList/types' +import { FlashList } from '@shopify/flash-list' import { getSectionDisplayDate } from '@holi/chat/src/utils/date' import { getLocale } from '@holi/core/i18n/helpers/dateHelper' -import { dimensions } from '@holi/ui/styles/globalVars' -import React from 'react' -import { useTranslation } from 'react-i18next' -import { View } from 'react-native' -import Animated, { LayoutAnimationConfig, ZoomInEasyDown, LinearTransition } from 'react-native-reanimated' - -import { useStyles } from 'holi-bricks/hooks' -import { createStyleSheet } from 'holi-bricks/utils' import RoomMessage from './RoomMessage' import type { RoomMessageListProps } from './types' +import { createStyleSheet } from 'holi-bricks/utils' +import { useStyles } from 'holi-bricks/hooks' +import { Spacing } from 'holi-bricks/tokens' const groupByDate = (messages: ChatRoomMessage[], locale: string): MessageItem[] => { const groupedMessages: Record<string, MessageItem[]> = {} @@ -20,21 +19,21 @@ const groupByDate = (messages: ChatRoomMessage[], locale: string): MessageItem[] for (const message of messages) { const { timestamp } = message - let formatedDate = '' + let formattedDate = '' if (timestamp) { const date = new Date(timestamp) - formatedDate = Intl.DateTimeFormat(locale, { + formattedDate = Intl.DateTimeFormat(locale, { year: 'numeric', month: 'numeric', day: 'numeric', }).format(date) } - groupedMessages[formatedDate] = groupedMessages[formatedDate] ?? [ + groupedMessages[formattedDate] = groupedMessages[formattedDate] ?? [ { title: getSectionDisplayDate(locale, timestamp) }, ] - groupedMessages[formatedDate].unshift(message) + groupedMessages[formattedDate].unshift(message) } return Object.values(groupedMessages).reverse().flat() @@ -51,47 +50,41 @@ const RoomMessageList = ({ const { styles } = useStyles(stylesheet) const groupedMessages = groupByDate(messages, i18n.language) - return ( - // skip entering animation for the first render - <LayoutAnimationConfig skipEntering> - <Animated.FlatList - data={groupedMessages} - renderItem={({ item: message, index }) => { - if (isMessageSection(message)) return <RoomMessageSection messageSection={message} /> + <FlashList + renderItem={({ item: message, index }) => { + if (isMessageSection(message)) return <RoomMessageSection messageSection={message} /> - const isFirstMessageInSection = index === 0 - const prevMessage = groupedMessages[index + 1] - const isPrevMessageFromSameUser = - !!prevMessage && isTextMessageItem(prevMessage) && prevMessage?.sender === message.sender - const nextMessage = groupedMessages[index - 1] - const isNextMessageFromSameUser = - !!nextMessage && isTextMessageItem(nextMessage) && nextMessage?.sender === message.sender + const isFirstMessageInSection = index === 0 + const prevMessage = groupedMessages[index + 1] + const isPrevMessageFromSameUser = + !!prevMessage && isTextMessageItem(prevMessage) && prevMessage?.sender === message.sender + const nextMessage = groupedMessages[index - 1] + const isNextMessageFromSameUser = + !!nextMessage && isTextMessageItem(nextMessage) && nextMessage?.sender === message.sender - return ( - <Animated.View entering={ZoomInEasyDown}> - <RoomMessage - roomId={roomId} - message={message} - holiUser={matrixIdToHoliUserRecord[message.sender ?? '']} - showAvatar={isFirstMessageInSection || !isNextMessageFromSameUser} - isPrevMessageFromSameUser={isPrevMessageFromSameUser} - isNextMessageFromSameUser={isNextMessageFromSameUser} - /> - </Animated.View> - ) - }} - initialNumToRender={15} - // BE CAREFUL: this section list is inverted! So, ListHeaderComponent is shown bellow and ListFooterComponent above in the list - // use ListFooterComponent to add padding bottom for HeaderAnimated - inverted={true} - onScroll={onScroll} - keyboardDismissMode="interactive" - ListHeaderComponent={() => <View style={styles.listHeaderSpacer} />} - ListFooterComponent={() => <View style={styles.listFooterSpacer}>{starter}</View>} - itemLayoutAnimation={LinearTransition.springify().damping(14).stiffness(90)} - /> - </LayoutAnimationConfig> + return ( + <RoomMessage + roomId={roomId} + message={message} + holiUser={matrixIdToHoliUserRecord[message.sender ?? '']} + showAvatar={isFirstMessageInSection || !isNextMessageFromSameUser} + isPrevMessageFromSameUser={isPrevMessageFromSameUser} + isNextMessageFromSameUser={isNextMessageFromSameUser} + /> + ) + }} + data={groupedMessages} + // BE CAREFUL: this section list is inverted! So, ListHeaderComponent is shown bellow and ListFooterComponent above in the list + // use ListFooterComponent to add padding bottom for HeaderAnimated + inverted={true} + onScroll={onScroll} + estimatedItemSize={52} + removeClippedSubviews + keyboardDismissMode="interactive" + ListHeaderComponent={() => <View style={styles.listHeaderSpacer} />} + ListFooterComponent={() => <View style={styles.listFooterSpacer}>{starter}</View>} + /> ) } @@ -99,16 +92,9 @@ export default RoomMessageList const stylesheet = createStyleSheet(() => ({ listFooterSpacer: { - paddingTop: dimensions.topBarHeight, + paddingHorizontal: Spacing['xs'], }, listHeaderSpacer: { height: 20, }, - container: { - flex: 1, - }, - contentContainer: { - justifyContent: 'flex-end', - flexGrow: 1, - }, })) diff --git a/packages/chat/src/components/RoomMessageList/index.web.tsx b/packages/chat/src/components/RoomMessageList/index.web.tsx deleted file mode 100644 index 48cd00dec47023ca29966cb198960a76e91c4375..0000000000000000000000000000000000000000 --- a/packages/chat/src/components/RoomMessageList/index.web.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React, { useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { SectionList, View } from 'react-native' -import { StyleSheet } from 'react-native' - -import type { ChatRoomMessage } from '@holi/chat/src/client/types' -import RoomMessageSection from '@holi/chat/src/components/RoomMessageList/RoomMessageSection' -import { getSectionDisplayDate } from '@holi/chat/src/utils/date' -import { getLocale } from '@holi/core/i18n/helpers/dateHelper' -import { dimensions } from '@holi/ui/styles/globalVars' -import { HoliTheme, useTheme } from '@holi/ui/styles/theme' - -import RoomMessage from './RoomMessage' -import { RoomMessageListProps, isTextMessageItem } from './types' - -interface MessageDateGroup { - title: string - data: ChatRoomMessage[] -} - -const groupByDate = (messages: ChatRoomMessage[], locale: string): MessageDateGroup[] => { - const groupedMessages: Record<string, MessageDateGroup> = {} - locale = getLocale(locale) - - for (const message of messages) { - const { timestamp } = message - let formattedDate = '' - - if (timestamp) { - const date = new Date(timestamp) - formattedDate = Intl.DateTimeFormat(locale, { - year: 'numeric', - month: 'numeric', - day: 'numeric', - }).format(date) - } - - groupedMessages[formattedDate] = groupedMessages[formattedDate] ?? { - title: getSectionDisplayDate(locale, timestamp), - data: [], - } - groupedMessages[formattedDate].data.unshift(message) - } - - return Object.values(groupedMessages).reverse() -} - -const RoomMessageList = ({ - messages = [], - matrixIdToHoliUserRecord, - starter, - onScroll, - roomId, -}: RoomMessageListProps) => { - const { i18n } = useTranslation() - const { theme } = useTheme() - const styles = useMemo(() => getStyles(theme), [theme]) - - const groupedMessages = groupByDate(messages, i18n.language) - - return ( - <SectionList - sections={groupedMessages} - keyExtractor={(item) => item.id} - renderItem={({ item: message, index, section: { data: list } }) => { - const isFirstMessageInSection = index === 0 - const prevMessage = list[index + 1] - const isPrevMessageFromSameUser = - !!prevMessage && isTextMessageItem(prevMessage) && prevMessage.sender === message.sender - const nextMessage = list[index - 1] - const isNextMessageFromSameUser = - !!nextMessage && isTextMessageItem(prevMessage) && nextMessage.sender === message.sender - - return ( - <View style={styles.listItemContainer}> - <RoomMessage - roomId={roomId} - message={message} - holiUser={matrixIdToHoliUserRecord[message.sender ?? '']} - showAvatar={isFirstMessageInSection || !isNextMessageFromSameUser} - isPrevMessageFromSameUser={isPrevMessageFromSameUser} - isNextMessageFromSameUser={isNextMessageFromSameUser} - /> - </View> - ) - }} - renderSectionFooter={({ section }) => <RoomMessageSection messageSection={section} />} - // BE CAREFUL: this section list is inverted! So, ListHeaderComponent is shown bellow and ListFooterComponent above in the list - // use ListFooterComponent to add padding bottom for HeaderAnimated - ListHeaderComponent={() => <View style={styles.listHeaderSpacer} />} - ListFooterComponent={() => <View style={styles.listFooterSpacer}>{starter}</View>} - stickySectionHeadersEnabled={false} - inverted={true} - style={styles.list} - removeClippedSubviews - updateCellsBatchingPeriod={20} - windowSize={11} - onScroll={onScroll} - /> - ) -} - -export default RoomMessageList - -const getStyles = (theme: HoliTheme) => - StyleSheet.create({ - list: { - width: '100%', - height: '100%', - backgroundColor: theme.colors.background20, - }, - listItemContainer: { - alignSelf: 'center', - width: '100%', - maxWidth: dimensions.maxScreenWidth, - }, - listFooterSpacer: { - paddingTop: dimensions.topBarHeight, - }, - listHeaderSpacer: { - height: 20, - }, - }) diff --git a/packages/chat/src/components/__tests__/ChatRoomView.test.tsx b/packages/chat/src/components/__tests__/ChatRoomView.test.tsx index 64fef6c2cc3ff19092aa767098a98f43903d59db..22494e9757645d7289ab8c6835ed00184a30c66b 100644 --- a/packages/chat/src/components/__tests__/ChatRoomView.test.tsx +++ b/packages/chat/src/components/__tests__/ChatRoomView.test.tsx @@ -1,3 +1,4 @@ +import '@holi/core/components/Screen/mock' import type { DefaultOptions } from '@apollo/client' import { MockedProvider } from '@apollo/client/testing' import { act, render, screen, waitForElementToBeRemoved } from '@testing-library/react-native' diff --git a/packages/ui/components/organisms/HoliRichTextOutput.tsx b/packages/ui/components/organisms/HoliRichTextOutput.tsx index cb613d6c69ada1978935cdfd6bbcacbefa7bf620..39ea2c06fecfa9a0f21408053cd1a6ae5a544497 100644 --- a/packages/ui/components/organisms/HoliRichTextOutput.tsx +++ b/packages/ui/components/organisms/HoliRichTextOutput.tsx @@ -1,16 +1,17 @@ -import { Text } from 'holi-bricks/components/text' +import { Text, type TextLinkProps, type TextLinkSize } from 'holi-bricks/components/text' import React from 'react' -import { Platform, type StyleProp, type TextStyle, type ViewStyle } from 'react-native' +import { Platform, View, type StyleProp, type TextStyle, type ViewStyle } from 'react-native' import type { User } from '@holi/core/domain/shared/types' import { createUUID } from '@holi/core/helpers/strings' import { HoliTextLink } from '@holi/core/navigation/components/HoliTextLink' import type { Link } from '@holi/core/screens/posts/types' -import HoliText from '@holi/ui/components/atoms/HoliText' -import type { HoliTextProps } from '@holi/ui/components/atoms/HoliText' import { REGEX_URL, REGEX_USER_MENTIONS, ensureProtocolPrefix, isValidUrl } from '@holi/ui/helper/richText' +import type { SkeletonTextOptions } from '@holi/ui/components/atoms/HoliSkeleton/types' +import { defaultTextOptionsAsProps } from '@holi/ui/components/atoms/HoliSkeleton/defaults' +import HoliSkeleton from '@holi/ui/components/atoms/HoliSkeleton' -export interface Props extends HoliTextProps { +export interface HoliRichTextProps extends Omit<Omit<TextLinkProps, 'variant'>, 'size'> { text: string users?: User[] links?: Link[] @@ -20,6 +21,9 @@ export interface Props extends HoliTextProps { allowAllLinks?: boolean testID?: string onPressText?: () => void + loading?: boolean + size?: TextLinkSize + skeletonOptions?: SkeletonTextOptions } const HoliRichTextOutput = ({ @@ -27,14 +31,16 @@ const HoliRichTextOutput = ({ users = [], links = [], testID, - size, + size = 'md', allowAllLinks = false, linkMentionsToProfile = true, + loading, + skeletonOptions, + numberOfLines, onPressText, + color = 'support', ...props -}: Props) => { - const linkSize = size === undefined || size === 'extralarge' || size === 'large' ? 'md' : 'sm' - +}: HoliRichTextProps) => { /** * Replaces each @<USER-ID> * with the highlighted fullName @@ -61,14 +67,14 @@ const HoliRichTextOutput = ({ label={user.fullName ?? ''} href={`/profile/${user.id}`} key={index} - size={linkSize} + size={size} color="informative" > {mentionPrefix} {replaced} </HoliTextLink> ) : ( - <Text key={index} size={linkSize}> + <Text key={index} size={size} color={color}> {mentionPrefix} {replaced} </Text> @@ -102,7 +108,7 @@ const HoliRichTextOutput = ({ <HoliTextLink href={link.requestedUrl} label={link.title} - size={linkSize} + size={size} key={link.requestedUrl} external={isExternalLink(link.requestedUrl)} color="informative" @@ -110,9 +116,9 @@ const HoliRichTextOutput = ({ {link.title} </HoliTextLink> ) : !!textPart.trim() && onPressText ? ( - <HoliText key={createUUID()} onPress={onPressText} {...props}> + <Text size={size} key={createUUID()} onPress={onPressText} color={color} {...props}> {textPart} - </HoliText> + </Text> ) : ( textPart ) @@ -120,10 +126,26 @@ const HoliRichTextOutput = ({ }) })() + const sizesToSkeletonHeight = { + md: 16, + sm: 15, + xs: 12, + } + + if (loading) { + return ( + <View style={props?.style}> + <HoliSkeleton + {...defaultTextOptionsAsProps(skeletonOptions, numberOfLines, sizesToSkeletonHeight[size], '60%')} + /> + </View> + ) + } + return ( - <HoliText testID={testID} size={size} {...props}> + <Text testID={testID} size={size} color={color} {...props}> {textWithMentions} - </HoliText> + </Text> ) } diff --git a/packages/ui/components/organisms/__tests__/__snapshots__/HoliRichTextOutput.test.tsx.snap b/packages/ui/components/organisms/__tests__/__snapshots__/HoliRichTextOutput.test.tsx.snap index b6805475aebd6f2524d4c61b0b2789645f674342..259823342b0b0a7b838af3af8aebd5320a523436 100644 --- a/packages/ui/components/organisms/__tests__/__snapshots__/HoliRichTextOutput.test.tsx.snap +++ b/packages/ui/components/organisms/__tests__/__snapshots__/HoliRichTextOutput.test.tsx.snap @@ -3,20 +3,21 @@ exports[`HoliRichTextOutput full text starting with word followed by colon is not a link 1`] = ` <Text accessible={false} - collapsable={false} - jestAnimatedStyle={ - { - "value": {}, - } - } + role="none" selectable={true} style={ { - "color": "#262424", + "color": "#676565", "fontFamily": "InstrumentSans-Regular", "fontSize": 16, + "fontVariant": [ + "lining-nums", + "tabular-nums", + ], "fontWeight": "400", - "lineHeight": 22.4, + "letterSpacing": -0.128, + "lineHeight": 23.68, + "textAlign": undefined, "userSelect": "auto", } } @@ -29,20 +30,21 @@ exports[`HoliRichTextOutput full text starting with word followed by colon is no exports[`HoliRichTextOutput link surrounded by whitespace is rendered as a link 1`] = ` <Text accessible={false} - collapsable={false} - jestAnimatedStyle={ - { - "value": {}, - } - } + role="none" selectable={true} style={ { - "color": "#262424", + "color": "#676565", "fontFamily": "InstrumentSans-Regular", "fontSize": 16, + "fontVariant": [ + "lining-nums", + "tabular-nums", + ], "fontWeight": "400", - "lineHeight": 22.4, + "letterSpacing": -0.128, + "lineHeight": 23.68, + "textAlign": undefined, "userSelect": "auto", } } diff --git a/yarn.lock b/yarn.lock index 483c061a8b5de9e575f4be8eca5c50439bf387df..0f8cc57659d616e927ed0a610598c675bfa992fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5684,7 +5684,7 @@ __metadata: jest-offline: "npm:^1.0.1" react: "npm:18.2.0" react-native: "npm:0.74.6" - react-native-keyboard-controller: "npm:^1.15.2" + react-native-keyboard-controller: "npm:^1.16.3" react-native-phone-number-input: "npm:^2.1.0" react-native-reanimated-carousel: "npm:^3.5.1" react-native-url-polyfill: "npm:^1.3.0" @@ -5837,7 +5837,7 @@ __metadata: react-native-get-random-values: "npm:^1.11.0" react-native-image-crop-picker: "npm:^0.41.2" react-native-image-pan-zoom: "npm:^2.1.12" - react-native-keyboard-controller: "npm:^1.15.2" + react-native-keyboard-controller: "npm:^1.16.3" react-native-media-query: "npm:^2.0.1" react-native-reanimated: "npm:~3.10.1" react-native-reanimated-carousel: "npm:^3.5.1" @@ -29402,16 +29402,16 @@ __metadata: languageName: node linkType: hard -"react-native-keyboard-controller@npm:^1.15.2": - version: 1.15.2 - resolution: "react-native-keyboard-controller@npm:1.15.2" +"react-native-keyboard-controller@npm:^1.16.3": + version: 1.16.3 + resolution: "react-native-keyboard-controller@npm:1.16.3" dependencies: react-native-is-edge-to-edge: "npm:^1.1.6" peerDependencies: react: "*" react-native: "*" react-native-reanimated: ">=3.0.0" - checksum: 10c0/7603bdcbeef5c453e385517d97efce829495fb67d3e18ba5044f159ec734941893a33d6e368177f32a31e4c6a71d73efad62dfc0b28469080ef36b4a4747c4a7 + checksum: 10c0/5c2e52214b3b537e424dca5368116970e5cd87e136c47cae40c3b6c05872c3c16a5deea6c22d08733d2cc1c056faca1a7dd0d534d272b5765883fb388f87ee06 languageName: node linkType: hard