Browse Source

REFACTOR | Home screen & skeleton loading cache

stage
Vitalik 2 years ago
parent
commit
31a6e323c7
  1. 2
      index.js
  2. 4
      ios/taskme.xcodeproj/project.pbxproj
  3. 6
      package-lock.json
  4. 33
      src/api/tasks/transform.ts
  5. 57
      src/modules/chats/components/chats-list.component.tsx
  6. 2
      src/modules/chats/configs/chat-card-buttons.config.ts
  7. 52
      src/modules/chats/hooks/use-chats-list.hook.ts
  8. 27
      src/modules/chats/screens/chats.screen.tsx
  9. 26
      src/modules/chats/smart-components/chats-list.smart-component.tsx
  10. 67
      src/modules/chats/smart-components/swipable-chat-row-card.smart-component.tsx
  11. 1
      src/modules/executors/hooks/use-fetch-executors.hook.ts
  12. 153
      src/modules/executors/smart-components/executors-slider-list.smart-component.tsx
  13. 1
      src/modules/executors/smart-components/index.ts
  14. 1
      src/modules/groups/hooks/use-fetch-groups-with-tasks-count.hook.ts
  15. 52
      src/modules/groups/mock/groups-data.mock.ts
  16. 1
      src/modules/groups/mock/index.ts
  17. 148
      src/modules/groups/smart-components/groups-slider-list.smart-component.tsx
  18. 1
      src/modules/groups/smart-components/index.ts
  19. 50
      src/modules/home/atoms/home-executors.component.tsx
  20. 53
      src/modules/home/atoms/home-groups-block.componen.tsx
  21. 2
      src/modules/home/atoms/index.ts
  22. 24
      src/modules/home/helpers/index.ts
  23. 196
      src/modules/home/screens/home.screen.tsx
  24. 88
      src/modules/home/smart-components/my-tasks-list.smart-component.tsx
  25. 8
      src/modules/root/navigation-groups/tab-bar.group.tsx
  26. 14
      src/modules/tasks/hooks/use-fetch-tasks-by-filter.hook.ts
  27. 43
      src/services/domain/account.service.ts
  28. 2
      src/services/domain/permissions.service.ts
  29. 3
      src/services/system/index.ts
  30. 47
      src/services/system/skeleton-data.service.ts
  31. 32
      src/shared/abstract/service.ts
  32. 5
      src/shared/components/forms/form-search.component.tsx
  33. 3
      src/shared/components/headers/primary-header.component.tsx
  34. 17
      src/shared/components/layouts/screen-layout.component.tsx
  35. 3
      src/shared/enums/storage-keys.enum.ts
  36. 28
      src/shared/hooks/use-flat-list.hook.ts
  37. 4
      yarn.lock

2
index.js

@ -12,6 +12,8 @@ import {
setNativeExceptionHandler, setNativeExceptionHandler,
} from 'react-native-exception-handler' } from 'react-native-exception-handler'
import '@/services/system/skeleton-data.service'
const errorHandler = (e, isFatal) => { const errorHandler = (e, isFatal) => {
if (isFatal) { if (isFatal) {
Alert.alert( Alert.alert(

4
ios/taskme.xcodeproj/project.pbxproj

@ -764,7 +764,7 @@
CODE_SIGN_ENTITLEMENTS = taskme/taskme.entitlements; CODE_SIGN_ENTITLEMENTS = taskme/taskme.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 105; CURRENT_PROJECT_VERSION = 106;
DEVELOPMENT_TEAM = HQ3J3TDPR2; DEVELOPMENT_TEAM = HQ3J3TDPR2;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
@ -802,7 +802,7 @@
CODE_SIGN_ENTITLEMENTS = taskme/taskme.entitlements; CODE_SIGN_ENTITLEMENTS = taskme/taskme.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 105; CURRENT_PROJECT_VERSION = 106;
DEVELOPMENT_TEAM = HQ3J3TDPR2; DEVELOPMENT_TEAM = HQ3J3TDPR2;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";

6
package-lock.json generated

@ -14,6 +14,7 @@
"@react-native-async-storage/async-storage": "^1.15.8", "@react-native-async-storage/async-storage": "^1.15.8",
"@react-native-community/cameraroll": "^4.1.2", "@react-native-community/cameraroll": "^4.1.2",
"@react-native-community/clipboard": "^1.5.1", "@react-native-community/clipboard": "^1.5.1",
"@react-native-community/datetimepicker": "^6.1.3",
"@react-native-community/netinfo": "^9.0.0", "@react-native-community/netinfo": "^9.0.0",
"@react-native-community/push-notification-ios": "^1.10.1", "@react-native-community/push-notification-ios": "^1.10.1",
"@react-native-picker/picker": "^2.4.1", "@react-native-picker/picker": "^2.4.1",
@ -27,6 +28,7 @@
"axios": "^0.21.1", "axios": "^0.21.1",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"cachios": "^3.0.0", "cachios": "^3.0.0",
"deprecated-react-native-prop-types": "^2.3.0",
"events": "^3.3.0", "events": "^3.3.0",
"jet-tools": "^1.1.0", "jet-tools": "^1.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@ -2962,7 +2964,6 @@
"version": "6.1.3", "version": "6.1.3",
"resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-6.1.3.tgz", "resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-6.1.3.tgz",
"integrity": "sha512-3e20BPQy8QiQeIGJL4zoJaRsGWB31hl9i7oTZECWxnK6nVUeqGt/G+JRJlJinmqL6z4WePHC6RSUMgpC64OOwg==", "integrity": "sha512-3e20BPQy8QiQeIGJL4zoJaRsGWB31hl9i7oTZECWxnK6nVUeqGt/G+JRJlJinmqL6z4WePHC6RSUMgpC64OOwg==",
"peer": true,
"dependencies": { "dependencies": {
"invariant": "^2.2.4" "invariant": "^2.2.4"
} }
@ -5667,7 +5668,6 @@
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz", "resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz",
"integrity": "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==", "integrity": "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==",
"peer": true,
"dependencies": { "dependencies": {
"@react-native/normalize-color": "*", "@react-native/normalize-color": "*",
"invariant": "*", "invariant": "*",
@ -17883,7 +17883,6 @@
"version": "6.1.3", "version": "6.1.3",
"resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-6.1.3.tgz", "resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-6.1.3.tgz",
"integrity": "sha512-3e20BPQy8QiQeIGJL4zoJaRsGWB31hl9i7oTZECWxnK6nVUeqGt/G+JRJlJinmqL6z4WePHC6RSUMgpC64OOwg==", "integrity": "sha512-3e20BPQy8QiQeIGJL4zoJaRsGWB31hl9i7oTZECWxnK6nVUeqGt/G+JRJlJinmqL6z4WePHC6RSUMgpC64OOwg==",
"peer": true,
"requires": { "requires": {
"invariant": "^2.2.4" "invariant": "^2.2.4"
} }
@ -19994,7 +19993,6 @@
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz", "resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz",
"integrity": "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==", "integrity": "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==",
"peer": true,
"requires": { "requires": {
"@react-native/normalize-color": "*", "@react-native/normalize-color": "*",
"invariant": "*", "invariant": "*",

33
src/api/tasks/transform.ts

@ -1,4 +1,9 @@
import { createFullName, IFullTaskInfo, IShortUser } from '@/shared' import {
createFullName,
IFullTaskInfo,
IShortUser,
ITaskPreview,
} from '@/shared'
import { import {
IFetchTaskDetailsResponse, IFetchTaskDetailsResponse,
ITaskExecutorResponse, ITaskExecutorResponse,
@ -77,22 +82,22 @@ export const transformTaskDetails = (
export const transformTasksInList = ( export const transformTasksInList = (
items: (ITaskPreviewResponse | ITaskWithExecutorPreviewResponse)[], items: (ITaskPreviewResponse | ITaskWithExecutorPreviewResponse)[],
executor?: IShortUser, executor?: IShortUser,
) => { ): ITaskPreview[] => {
const transformedItems = items.map(item => { const transformedItems = items.map(item => {
const taskExecutor = executor const taskExecutor = executor
? executor ? executor
: { : {
id: (item as ITaskWithExecutorPreviewResponse).executor id: (item as ITaskWithExecutorPreviewResponse).executor
.userId, .userId,
fullName: createFullName( fullName: createFullName(
(item as ITaskWithExecutorPreviewResponse).executor (item as ITaskWithExecutorPreviewResponse).executor
.firstName, .firstName,
(item as ITaskWithExecutorPreviewResponse).executor (item as ITaskWithExecutorPreviewResponse).executor
.lastName, .lastName,
), ),
avatarUrl: (item as ITaskWithExecutorPreviewResponse) avatarUrl: (item as ITaskWithExecutorPreviewResponse)
.executor.avatarUrl, .executor.avatarUrl,
} }
const transformedItem = { const transformedItem = {
id: item.id, id: item.id,
@ -106,7 +111,7 @@ export const transformTasksInList = (
isFavorite: item.isFavorite, isFavorite: item.isFavorite,
hasUnreadComments: item.hasUnreadComments, hasUnreadComments: item.hasUnreadComments,
hasAttachments: item.hasDocuments, hasAttachments: item.hasDocuments,
events: item.events, events: item.events as any,
doneDate: item.doneDate, doneDate: item.doneDate,
} }

57
src/modules/chats/components/chats-list.component.tsx

@ -16,6 +16,7 @@ import {
ActivityIndicator, ActivityIndicator,
FlatList, FlatList,
StyleSheet, StyleSheet,
Text,
View, View,
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
@ -52,39 +53,29 @@ export const ChatsList: FC<IProps> = ({
}) => { }) => {
const { styles, theme } = useTheme(createStyles) const { styles, theme } = useTheme(createStyles)
const itemToRender = ({ item, index }: any) => { const itemToRender = useCallback(
const previewUrl = ({ item, index }: any) => {
item?.type === ChatType.Group return (
? item?.previewUrl <SwipableChatRowCardSmart
: item?.chatMembers[0]?.user?.avatarUrl id={item.id}
label={item.name}
const name = lastMessage={getMessagePreviewText(item.lastMessage)}
item?.type === ChatType.Group previewUrl={item.previewUrl}
? item.name isOnline={false}
: createFullName( isPinned={item.isChatFixed}
item?.chatMembers[0]?.user?.firstName, isUnread={item.isChatUnread}
item?.chatMembers[0]?.user?.lastName, unreadMessagesCount={item.unreadMessagesCount}
) style={createCardStyles(chats, index)}
sendDateTime={
return ( item.lastMessage?.createdAt || item.lastMessageDate
<SwipableChatRowCardSmart }
id={item.id} onPressCard={() => onPressItem(item)}
label={name} onPressActionBtn={onPressActionBtn}
lastMessage={getMessagePreviewText(item.lastMessage)} />
previewUrl={previewUrl} )
isOnline={false} },
isPinned={item.isChatFixed} [onPressItem, onPressActionBtn],
isUnread={item.isChatUnread} )
unreadMessagesCount={item.unreadMessagesCount}
style={createCardStyles(chats, index)}
sendDateTime={
item.lastMessage?.createdAt || item.lastMessageDate
}
onPressCard={() => onPressItem(item)}
onPressActionBtn={onPressActionBtn}
/>
)
}
const renderFooter = useCallback(() => { const renderFooter = useCallback(() => {
if (!isLoading && isLoadingNext) if (!isLoading && isLoadingNext)

2
src/modules/chats/configs/chat-card-buttons.config.ts

@ -13,7 +13,7 @@ type BtnsConfigT = {
interface configProps { interface configProps {
id: number id: number
theme: PartialTheme theme: PartialTheme
unreadMessagesCount: number unreadMessagesCount?: number
onPress: ( onPress: (
actionType: ChatCardActionEnum, actionType: ChatCardActionEnum,
chatId: number, chatId: number,

52
src/modules/chats/hooks/use-chats-list.hook.ts

@ -4,8 +4,10 @@ import {
appEvents, appEvents,
ChatMemberRole, ChatMemberRole,
ChatType, ChatType,
createFullName,
IChat, IChat,
RouteKey, RouteKey,
StorageKey,
useEventsListener, useEventsListener,
useFlatList, useFlatList,
useNav, useNav,
@ -31,6 +33,7 @@ import {
import { showUknowError } from '@/shared/helpers/alert.helper' import { showUknowError } from '@/shared/helpers/alert.helper'
import { getChatIdFromMessages, isChatInList } from '../helpers' import { getChatIdFromMessages, isChatInList } from '../helpers'
import { InteractionManager } from 'react-native'
export const useChatList = () => { export const useChatList = () => {
const nav = useNav() const nav = useNav()
@ -38,6 +41,25 @@ export const useChatList = () => {
const accountId = useSelector(selectId) const accountId = useSelector(selectId)
const [searchString, setSearchVal] = useState<string>(null) const [searchString, setSearchVal] = useState<string>(null)
const serrializatorItems = useCallback(
(items: any[]) =>
items.map(item => ({
...item,
name:
item?.type === ChatType.Group
? item.name
: createFullName(
item?.chatMembers[0]?.user?.firstName,
item?.chatMembers[0]?.user?.lastName,
),
previewUrl:
item?.type === ChatType.Group
? item?.previewUrl
: item?.chatMembers[0]?.user?.avatarUrl,
})),
[],
)
const { const {
items: chats, items: chats,
loadMore, loadMore,
@ -51,8 +73,34 @@ export const useChatList = () => {
needInit: true, needInit: true,
clearWhenReload: false, clearWhenReload: false,
limit: 10, limit: 10,
serrializatorItems: serrializatorItems,
}) })
useEffect(() => {
InteractionManager.runAfterInteractions(() => {
resetFlatList()
})
}, [])
// const cacheItems = async () => {
// const toSave = _.slice(chats, 0, 11)
// await storageService.set(StorageKey.Chats, JSON.stringify(toSave))
// }
// const getCache = async () => {
// const itemsJson = await storageService.get(StorageKey.Chats)
// if (!itemsJson) return
// _setItems(JSON.parse(itemsJson))
// }
// useEffect(() => {
// if (!isLoading) cacheItems()
// }, [isLoading])
// useEffect(() => {
// if (cachedItems) _setItems(cachedItems)
// }, [])
useEffect(() => { useEffect(() => {
if (searchString !== null) setLoadParams({ searchString }) if (searchString !== null) setLoadParams({ searchString })
}, [searchString]) }, [searchString])
@ -360,7 +408,7 @@ export const useChatList = () => {
resetFlatList: resetFlatList, resetFlatList: resetFlatList,
setSearchVal, setSearchVal,
handleChatAction, handleChatAction,
isLoading, isLoading: false,
isLoadingNext, isLoadingNext: false,
} }
} }

27
src/modules/chats/screens/chats.screen.tsx

@ -1,11 +1,17 @@
import React, { FC } from 'react' import React, { FC } from 'react'
import { $size, IRouteParams, RouteKey, ScreenLayout } from '@/shared' import {
$size,
IRouteParams,
PrimaryHeader,
RouteKey,
ScreenLayout,
} from '@/shared'
import { useTheme } from '@/shared/hooks/use-theme.hook' import { useTheme } from '@/shared/hooks/use-theme.hook'
import { Platform, StyleSheet } from 'react-native' import { StyleSheet } from 'react-native'
import { HeaderRightBtn } from '../atoms' import { HeaderRightBtn } from '../atoms'
import { SmartChatsList } from '../smart-components' import { SmartChatsList } from '../smart-components'
interface IProps extends IRouteParams { } interface IProps extends IRouteParams {}
export const ChatsScreen: FC<IProps> = ({ navigation }) => { export const ChatsScreen: FC<IProps> = ({ navigation }) => {
const { styles } = useTheme(createStyles) const { styles } = useTheme(createStyles)
@ -19,17 +25,16 @@ export const ChatsScreen: FC<IProps> = ({ navigation }) => {
useChatBtnColors: true, useChatBtnColors: true,
}) })
const props = {
title: 'Бесіди',
rightComponent: <HeaderRightBtn onPress={onPressHeaderBtn} />,
goBack: navigation.goBack,
style: styles.header,
}
return ( return (
<ScreenLayout <ScreenLayout
viewStyle={styles.container} viewStyle={styles.container}
header={{ headerComponent={<PrimaryHeader {...props} />}>
title: 'Бесіди',
rightComponents: () => (
<HeaderRightBtn onPress={onPressHeaderBtn} />
),
goBack: navigation.goBack,
style: styles.header,
}}>
<SmartChatsList /> <SmartChatsList />
</ScreenLayout> </ScreenLayout>
) )

26
src/modules/chats/smart-components/chats-list.smart-component.tsx

@ -1,21 +1,37 @@
import { $size, appEvents, IChat, RouteKey } from '@/shared' import { storageService } from '@/services/system'
import { $size, appEvents, IChat, RouteKey, StorageKey } from '@/shared'
import { SearchForm } from '@/shared/components/forms' import { SearchForm } from '@/shared/components/forms'
import { useTheme } from '@/shared/hooks/use-theme.hook' import { useTheme } from '@/shared/hooks/use-theme.hook'
import { SelectChat } from '@/store/chats' import { SelectChat } from '@/store/chats'
import { simpleDispatch } from '@/store/store-helpers' import { simpleDispatch } from '@/store/store-helpers'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack' import { StackNavigationProp } from '@react-navigation/stack'
import React, { FC } from 'react' import React, { FC, useEffect, useState } from 'react'
import { Platform, StyleSheet } from 'react-native' import { Platform, StyleSheet } from 'react-native'
import { ChatsList } from '../components/chats-list.component' import { ChatsList } from '../components/chats-list.component'
import { useChatList } from '../hooks' import { useChatList } from '../hooks'
type TConversationNav = StackNavigationProp<{ [RouteKey.Conversation] }> type TConversationNav = StackNavigationProp<{ [RouteKey.Conversation] }>
let cachedItems = []
// const getCache = async () => {
// const itemsJson = await storageService.get(StorageKey.Chats)
// if (!itemsJson) return
// cachedItems = JSON.parse(itemsJson)
// }
// getCache()
export const SmartChatsList: FC = () => { export const SmartChatsList: FC = () => {
const { styles } = useTheme(createStyles) const { styles } = useTheme(createStyles)
const { navigate } = useNavigation<TConversationNav>() const { navigate } = useNavigation<TConversationNav>()
// const [items, setItems] = useState([])
// useEffect(() => {
// setItems(cachedItems)
// }, [])
const { const {
chats, chats,
@ -68,9 +84,9 @@ const createStyles = () =>
container: { container: {
...Platform.select({ ...Platform.select({
android: { android: {
paddingBottom: $size(73) paddingBottom: $size(73),
} },
}) }),
}, },
searchField: { searchField: {
marginBottom: $size(20, 18), marginBottom: $size(20, 18),

67
src/modules/chats/smart-components/swipable-chat-row-card.smart-component.tsx

@ -2,7 +2,7 @@ import { IChat, SquareButton } from '@/shared'
import { useTheme } from '@/shared/hooks/use-theme.hook' import { useTheme } from '@/shared/hooks/use-theme.hook'
import { PartialTheme } from '@/shared/themes/interfaces' import { PartialTheme } from '@/shared/themes/interfaces'
import { useFocusEffect } from '@react-navigation/native' import { useFocusEffect } from '@react-navigation/native'
import React, { FC, useRef } from 'react' import React, { FC, useCallback, useRef } from 'react'
import { StyleSheet, View, ViewStyle } from 'react-native' import { StyleSheet, View, ViewStyle } from 'react-native'
import { Swipeable } from 'react-native-gesture-handler' import { Swipeable } from 'react-native-gesture-handler'
import { ChatRowCard } from '../components' import { ChatRowCard } from '../components'
@ -28,6 +28,12 @@ interface IProps {
style?: ViewStyle style?: ViewStyle
} }
const prepareBtnsToRender = (config: any) => {
return config.map((it, i) => (
<SquareButton key={i} style={{ width: '25%' }} {...it} />
))
}
export const SwipableChatRowCardSmart: FC<IProps> = ({ export const SwipableChatRowCardSmart: FC<IProps> = ({
id, id,
label, label,
@ -45,45 +51,46 @@ export const SwipableChatRowCardSmart: FC<IProps> = ({
const { styles, theme } = useTheme(createStyles) const { styles, theme } = useTheme(createStyles)
const swipableRef = useRef(null) const swipableRef = useRef(null)
const closeSwipeable = () => { const closeSwipeable = useCallback(() => {
swipableRef.current.close() swipableRef.current.close()
} }, [swipableRef.current])
useFocusEffect(() => closeSwipeable()) useFocusEffect(() => closeSwipeable())
const prepareBtnsToRender = (config: any) => { const rightBtnsToRender = useCallback(
const btnsToRender = config.map(it => { () =>
return <SquareButton style={{ width: '25%' }} {...it} /> prepareBtnsToRender(
}) chatRowCardButtonsConfig.rightBtn({
return btnsToRender id,
} theme,
onPress: onPressActionBtn,
const rightBtnsToRender = prepareBtnsToRender( afterPress: closeSwipeable,
chatRowCardButtonsConfig.rightBtn({ }),
id, ),
unreadMessagesCount, [id, unreadMessagesCount, closeSwipeable],
theme,
onPress: onPressActionBtn,
afterPress: closeSwipeable,
}),
) )
const leftBtnsToRender = prepareBtnsToRender(
chatRowCardButtonsConfig.leftBtn({ const leftBtnsToRender = useCallback(
id, () =>
unreadMessagesCount, prepareBtnsToRender(
theme, chatRowCardButtonsConfig.leftBtn({
onPress: onPressActionBtn, id,
afterPress: closeSwipeable, unreadMessagesCount,
isChatFixed: isPinned, theme,
isChatUnread: isUnread, onPress: onPressActionBtn,
}), afterPress: closeSwipeable,
isChatFixed: isPinned,
isChatUnread: isUnread,
}),
),
[id, unreadMessagesCount, theme, isPinned, isUnread, closeSwipeable],
) )
return ( return (
<Swipeable <Swipeable
ref={swipableRef} ref={swipableRef}
renderLeftActions={() => leftBtnsToRender} renderLeftActions={leftBtnsToRender}
renderRightActions={() => rightBtnsToRender}> renderRightActions={rightBtnsToRender}>
<View style={[styles.container, style]}> <View style={[styles.container, style]}>
<ChatRowCard <ChatRowCard
isOnline={isOnline} isOnline={isOnline}

1
src/modules/executors/hooks/use-fetch-executors.hook.ts

@ -1,6 +1,5 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { IShortUser, useFlatList } from '@/shared' import { IShortUser, useFlatList } from '@/shared'
import { usersService } from '@/services/domain/users.service'
import { IPermittedExecutorResponse } from '@/api/users/responses.interface' import { IPermittedExecutorResponse } from '@/api/users/responses.interface'
import { transformPermittedExecutors } from '@/api/users/transforms' import { transformPermittedExecutors } from '@/api/users/transforms'
import { fetchTPermittedExecutorsReq } from '@/api' import { fetchTPermittedExecutorsReq } from '@/api'

153
src/modules/executors/smart-components/executors-slider-list.smart-component.tsx

@ -0,0 +1,153 @@
import { fetchTPermittedExecutorsReq } from '@/api'
import { IPermittedExecutorResponse } from '@/api/users/responses.interface'
import { transformPermittedExecutors } from '@/api/users/transforms'
import { SkeletonDataKey } from '@/services/system'
import {
$size,
InfoRowCard,
IShortUser,
RouteKey,
useFlatList,
useNav,
} from '@/shared'
import { ContentBlock } from '@/shared/components/blocks'
import React, { FC, useCallback, useEffect } from 'react'
import {
ActivityIndicator,
Dimensions,
InteractionManager,
StyleSheet,
View,
} from 'react-native'
import Swiper from 'react-native-swiper'
import { useFetchPermittedExecutors } from '../hooks'
const { width: screenWidth } = Dimensions.get('screen')
interface ExecutorsSliderListSmartProps {
searchString: string
onLoaded?: (count: number) => void
}
export const ExecutorsSliderListSmart: FC<ExecutorsSliderListSmartProps> = ({
searchString,
onLoaded,
}) => {
const nav = useNav()
const {
items: executors,
isLoading,
setLoadParams,
count,
resetFlatList,
isEmpty,
} = useFlatList<IShortUser>({
fetchItems: fetchTPermittedExecutorsReq,
needInit: false,
serrializatorItems: (_items: IPermittedExecutorResponse[]) =>
transformPermittedExecutors(_items),
loadParams: {
sort: 'DESC',
sortField: 'id',
searchString,
},
limit: 5,
defaultLoading: false,
skeletonDataKey: SkeletonDataKey.HomeExecutors,
clearWhenReload: false,
defaultItems: [],
})
useEffect(() => {
InteractionManager.runAfterInteractions(() => {
resetFlatList()
})
}, [])
useEffect(() => {
if (onLoaded) onLoaded(count)
}, [count])
useEffect(() => {
if (searchString !== null) setLoadParams({ searchString })
}, [searchString])
const renderItem = item => {
return (
<View key={item.id} style={styles.container}>
<InfoRowCard
title={item.fullName}
info={item.position}
imageUri={item.avatarUrl}
containerStyle={styles.cardContainer}
onPressCard={() =>
nav.navigate(RouteKey.ExecutorTasks, {
userData: item,
})
}
onPressInfo={() =>
nav.navigate(RouteKey.ContactDetail, {
contactId: item.id,
})
}
/>
</View>
)
}
const pressExecutorsHandler = useCallback(() => {
nav.navigate(RouteKey.Executors)
}, [nav.navigate])
if (searchString !== null && !isLoading && isEmpty) return null
const renderContent = () => {
if (isLoading) {
return (
<View style={styles.loading}>
<ActivityIndicator />
</View>
)
}
return (
<Swiper
showsButtons={false}
horizontal
bounces={false}
loop={false}
automaticallyAdjustContentInsets
showsPagination={false}>
{executors.map(renderItem)}
</Swiper>
)
}
return (
<ContentBlock
title={'Виконавці'}
btnTitle={'Дивитись усі'}
onPressBtn={pressExecutorsHandler}
paddingHorizontal={$size(0)}
style={styles.content}>
{renderContent()}
</ContentBlock>
)
}
export const styles = StyleSheet.create({
loading: {
justifyContent: 'center',
alignItems: 'center',
},
container: {
width: screenWidth - 40,
marginLeft: 20,
},
cardContainer: {
width: '100%',
},
content: {
marginTop: $size(15, 10),
},
})

1
src/modules/executors/smart-components/index.ts

@ -0,0 +1 @@
export * from './executors-slider-list.smart-component'

1
src/modules/groups/hooks/use-fetch-groups-with-tasks-count.hook.ts

@ -1,6 +1,5 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { ITaxonomyWithTasksCount, useFlatList } from '@/shared' import { ITaxonomyWithTasksCount, useFlatList } from '@/shared'
import { taxonomiesService } from '@/services/domain'
import { fetchGroupsWithTasksCountReq } from '@/api' import { fetchGroupsWithTasksCountReq } from '@/api'
export const useFetchGroupsWithTasksCount = ( export const useFetchGroupsWithTasksCount = (

52
src/modules/groups/mock/groups-data.mock.ts

@ -1,42 +1,14 @@
export const groupsData = [ import { ITaxonomyWithTasksCount } from '@/shared'
{
title: 'Title', export const groupsData: ITaxonomyWithTasksCount[] = [
info: '25 Pflfx', {
}, id: null,
{ parentId: null,
title: 'Title', name: null,
info: 'Info', type: null,
}, isDeleted: null,
{ isDefault: null,
title: 'Title', iconUrl: null,
info: 'Info', tasksCount: null,
},
{
title: 'Title',
info: 'Info',
},
{
title: 'Title',
info: 'Info',
},
{
title: 'Title',
info: 'Info',
},
{
title: 'Title',
info: 'Info',
},
{
title: 'Title',
info: 'Info',
},
{
title: 'Title',
info: 'Info',
},
{
title: 'Title',
info: 'Info',
}, },
] ]

1
src/modules/groups/mock/index.ts

@ -0,0 +1 @@
export * from './groups-data.mock'

148
src/modules/groups/smart-components/groups-slider-list.smart-component.tsx

@ -0,0 +1,148 @@
import React, { FC, useCallback, useEffect } from 'react'
import {
InfoRowCard,
ITaxonomyWithTasksCount,
RouteKey,
useFlatList,
useNav,
} from '@/shared'
import { $size, getTitleByCount } from '@/shared/helpers'
import {
ActivityIndicator,
Dimensions,
InteractionManager,
StyleSheet,
View,
} from 'react-native'
import Swiper from 'react-native-swiper'
import { fetchGroupsWithTasksCountReq } from '@/api'
import { SkeletonDataKey } from '@/services/system'
import { ContentBlock } from '@/shared/components/blocks'
const { width: screenWidth } = Dimensions.get('screen')
interface GroupsSliderListProps {
searchString: string
onLoaded?: (count: number) => void
}
export const GroupsSliderList: FC<GroupsSliderListProps> = ({
searchString,
onLoaded,
}) => {
const nav = useNav()
const {
items: groups,
isLoading,
resetFlatList,
setLoadParams,
count,
isEmpty,
} = useFlatList<ITaxonomyWithTasksCount>({
fetchItems: fetchGroupsWithTasksCountReq,
needInit: false,
loadParams: {
withoutEmpty: true,
sort: 'DESC',
sortField: 'id',
},
limit: 30,
defaultLoading: false,
skeletonDataKey: SkeletonDataKey.HomeGroups,
clearWhenReload: false,
defaultItems: [],
})
useEffect(() => {
InteractionManager.runAfterInteractions(() => {
resetFlatList()
})
}, [])
useEffect(() => {
if (onLoaded) onLoaded(count)
}, [count])
useEffect(() => {
if (searchString !== null) setLoadParams({ searchString })
}, [searchString])
const renderItem = useCallback(item => {
if (!item || !item.tasksCount) return null
return (
<View key={item.id}>
<InfoRowCard
title={item.name}
info={getTitleByCount(item.tasksCount, [
'задача',
'задачі',
'задач',
])}
imageUri={item.iconUrl}
containerStyle={styles.cardContainer}
onPressCard={() =>
nav.navigate(RouteKey.GroupTasks, {
groupData: item,
})
}
onPressInfo={() =>
console.log('i press group info', item.name)
}
/>
</View>
)
}, [])
const pressGroupHandler = useCallback(() => {
nav.navigate(RouteKey.Group)
}, [nav.navigate])
if (searchString !== null && !isLoading && isEmpty) return null
const renderContent = () => {
if (isLoading) {
return (
<View style={styles.loading}>
<ActivityIndicator />
</View>
)
}
return (
<Swiper
showsButtons={false}
horizontal
bounces={false}
loop={false}
automaticallyAdjustContentInsets
showsPagination={false}>
{groups.map(renderItem)}
</Swiper>
)
}
return (
<ContentBlock
title={'Групи'}
btnTitle={'Дивитись усі'}
onPressBtn={pressGroupHandler}
paddingHorizontal={$size(0)}
style={styles.content}>
{renderContent()}
</ContentBlock>
)
}
const styles = StyleSheet.create({
loading: {
justifyContent: 'center',
alignItems: 'center',
},
cardContainer: {
width: screenWidth - 40,
marginLeft: 20,
},
content: {
marginTop: $size(15, 10),
},
})

1
src/modules/groups/smart-components/index.ts

@ -0,0 +1 @@
export * from './groups-slider-list.smart-component'

50
src/modules/home/atoms/home-executors.component.tsx

@ -1,50 +0,0 @@
import {
$size,
InfoRowCard,
IShortUser,
ITaxonomyWithTasksCount,
RouteKey,
useNav,
} from '@/shared'
import React, { FC } from 'react'
import { Dimensions, StyleSheet, View } from 'react-native'
import { HomeCarusel } from '../components'
interface Props {
items: IShortUser[]
}
const { width: screenWidth } = Dimensions.get('screen')
export const HomeExecutorsBlock: FC<Props> = ({ items }) => {
const { navigate } = useNav()
const renderItem = item => {
return (
<View key={item.id} style={styles.container}>
<InfoRowCard
title={item.fullName}
info={item.position}
imageUri={item.avatarUrl}
containerStyle={styles.cardContainer}
onPressCard={() =>
navigate(RouteKey.ExecutorTasks, { userData: item })
}
onPressInfo={() =>
navigate(RouteKey.ContactDetail, { contactId: item.id })
}
/>
</View>
)
}
return <HomeCarusel>{items.map(renderItem)}</HomeCarusel>
}
const styles = StyleSheet.create({
container: {
width: screenWidth - 40,
marginLeft: 20,
},
cardContainer: {
width: '100%',
},
})

53
src/modules/home/atoms/home-groups-block.componen.tsx

@ -1,53 +0,0 @@
import {
InfoRowCard,
ITaxonomyWithTasksCount,
RouteKey,
useNav,
} from '@/shared'
import { $size, getTitleByCount } from '@/shared/helpers'
import React, { FC } from 'react'
import { Dimensions, StyleSheet, View } from 'react-native'
import { HomeCarusel } from '../components'
interface Props {
items: ITaxonomyWithTasksCount[]
}
const { width: screenWidth } = Dimensions.get('screen')
export const HomeGroupsBlock: FC<Props> = ({ items }) => {
const { navigate } = useNav()
const renderItem = item => {
if (!item || !item.tasksCount) return null
return (
<View key={item.id}>
<InfoRowCard
title={item.name}
info={getTitleByCount(item.tasksCount, [
'задача',
'задачі',
'задач',
])}
imageUri={item.iconUrl}
containerStyle={styles.cardContainer}
onPressCard={() =>
navigate(RouteKey.GroupTasks, {
groupData: item,
})
}
onPressInfo={() =>
console.log('i press group info', item.name)
}
/>
</View>
)
}
return <HomeCarusel>{items.map(renderItem)}</HomeCarusel>
}
const styles = StyleSheet.create({
cardContainer: {
width: screenWidth - 40,
marginLeft: 20,
},
})

2
src/modules/home/atoms/index.ts

@ -1,2 +0,0 @@
export * from './home-groups-block.componen';
export * from './home-executors.component'

24
src/modules/home/helpers/index.ts

@ -0,0 +1,24 @@
import { getTitleByCount } from '@/shared/helpers'
import _ from 'lodash'
export const getHomeSearchResult = (
counts: Record<string, number>,
searchString: string,
) => {
if (
!searchString ||
!_.isNumber(counts.tasks) ||
!_.isNumber(counts.groups) ||
!_.isNumber(counts.executors)
)
return false
const count =
Number(counts.executors) + Number(counts.groups) + Number(counts.tasks)
return `Знайдено ${getTitleByCount(count, [
'результат',
'результата',
'результатів',
])}`
}

196
src/modules/home/screens/home.screen.tsx

@ -1,11 +1,12 @@
import React, { FC, useEffect, useMemo, useState } from 'react' import React, {
import { FC,
ActivityIndicator, useCallback,
ScrollView, useEffect,
StyleSheet, useMemo,
TouchableOpacity, useRef,
View, useState,
} from 'react-native' } from 'react'
import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native'
import { import {
$size, $size,
IconComponent, IconComponent,
@ -16,28 +17,30 @@ import {
useResetOnBlur, useResetOnBlur,
} from '@/shared' } from '@/shared'
import { SearchForm } from '@/shared/components/forms' import { SearchForm } from '@/shared/components/forms'
import { ContentBlock } from '@/shared/components/blocks'
import { PartialTheme } from '@/shared/themes/interfaces' import { PartialTheme } from '@/shared/themes/interfaces'
import { useTheme } from '@/shared/hooks/use-theme.hook' import { useTheme } from '@/shared/hooks/use-theme.hook'
import { MyTasksListSmart } from '../smart-components' import { MyTasksListSmart } from '../smart-components'
import { useFetchPermittedExecutors } from '@/modules/executors/hooks'
import { useFetchGroupsWithTasksCount } from '@/modules/groups/hooks'
import { useTaskFilter } from '@/modules/tasks/hooks' import { useTaskFilter } from '@/modules/tasks/hooks'
import { HomeExecutorsBlock, HomeGroupsBlock } from '../atoms'
import { needRedirect } from '@/services/system/notification.service' import { needRedirect } from '@/services/system/notification.service'
import { getTitleByCount } from '@/shared/helpers' import { GroupsSliderList } from '@/modules/groups/smart-components'
import { fetchTPermittedExecutorsReq } from '@/api' import { ExecutorsSliderListSmart } from '@/modules/executors/smart-components'
import _ from 'lodash'
import { getHomeSearchResult } from '../helpers'
interface IProps extends IRouteParams {} interface IProps extends IRouteParams {}
const defaultCountsState = {
tasks: null,
groups: null,
executors: null,
}
export const HomeScreen: FC<IProps> = ({ navigation }) => { export const HomeScreen: FC<IProps> = ({ navigation }) => {
const { styles, theme } = useTheme(createStyles) const { styles, theme } = useTheme(createStyles)
const [searchValue, setValue] = useState<string>(null) const [searchValue, setValue] = useState<string>(null)
const [isOpenDrawer, setDrawer] = useState<boolean>(false) const [isOpenDrawer, setDrawer] = useState<boolean>(false)
const [tasksCount, setTasksCount] = useState(0) const countsRef = useRef(defaultCountsState)
const [alreadyToLoadTasks, setAlreadyToLoad] = useState(false)
const taskFilter = useTaskFilter({ const taskFilter = useTaskFilter({
onSubmit: () => { onSubmit: () => {
setDrawer(false) setDrawer(false)
@ -50,98 +53,55 @@ export const HomeScreen: FC<IProps> = ({ navigation }) => {
useResetOnBlur(taskFilter.onClearFilter) useResetOnBlur(taskFilter.onClearFilter)
const respExecutors = useFetchPermittedExecutors(
{
sort: 'DESC',
sortField: 'id',
searchString: searchValue,
},
5,
)
const respGroups = useFetchGroupsWithTasksCount(
{
withoutEmpty: true,
sort: 'DESC',
sortField: 'id',
searchString: searchValue,
},
5,
)
useEffect(() => { useEffect(() => {
if (respExecutors.isLoading && !alreadyToLoadTasks) { const timmer = setTimeout(() => {
setTimeout(() => {
setAlreadyToLoad(true)
}, 100)
}
}, [respExecutors.count])
useEffect(() => {
if (searchValue !== null) {
respExecutors.setLoadParams({ searchString: searchValue })
respGroups.setLoadParams({ searchString: searchValue })
}
}, [searchValue])
useEffect(() => {
setTimeout(() => {
if (needRedirect.to) { if (needRedirect.to) {
navigation.navigate(needRedirect.to, needRedirect.payload) navigation.navigate(needRedirect.to, needRedirect.payload)
needRedirect.to = null needRedirect.to = null
needRedirect.payload = null needRedirect.payload = null
} }
}, 500) }, 500)
return () => clearTimeout(timmer)
}, []) }, [])
const groupsMemoRender = useMemo(() => { useEffect(() => {
if (respGroups.isLoading) { if (!searchValue) countsRef.current = defaultCountsState
return ( }, [searchValue])
<View style={styles.loading}>
<ActivityIndicator /> const searchResult = useMemo(
</View> () => getHomeSearchResult(countsRef.current, searchValue),
) [
} searchValue,
return <HomeGroupsBlock key="groups" items={respGroups.groups} /> countsRef.current.tasks,
}, [respGroups]) countsRef.current.executors,
countsRef.current.groups,
const executorsMemoRender = useMemo(() => { ],
if (respExecutors.isLoading) {
return (
<View style={styles.loading}>
<ActivityIndicator />
</View>
)
}
return (
<HomeExecutorsBlock
key="executors"
items={respExecutors.executors}
/>
)
}, [respExecutors])
const showResult = useMemo(
() =>
searchValue && !respExecutors?.isLoading && !respGroups?.isLoading,
[searchValue, respExecutors.isLoading, respGroups.isLoading],
) )
const resultCount = useMemo( const setTasksCount = useCallback(
() => respExecutors?.count + respGroups?.count + tasksCount, count => {
[showResult, respExecutors.count, respGroups.count, tasksCount], countsRef.current.tasks = count
},
[countsRef.current],
) )
const resultString = useMemo( const setExecutorsCount = useCallback(
() => count => {
`Знайдено ${getTitleByCount(resultCount, [ countsRef.current.executors = count
'результат', },
'результата', [countsRef.current],
'результатів',
])}`,
[resultCount],
) )
const onTasksLoaded = (count: number) => setTasksCount(count) const setGroupsCount = useCallback(
count => {
countsRef.current.groups = count
},
[countsRef.current],
)
const openDrawer = useCallback(() => {
setDrawer(true)
}, [setDrawer])
return ( return (
<ScreenLayout <ScreenLayout
@ -157,10 +117,9 @@ export const HomeScreen: FC<IProps> = ({ navigation }) => {
searchValue={searchValue} searchValue={searchValue}
placeholder="Знайдіть задачу, групу" placeholder="Знайдіть задачу, групу"
onChange={setValue} onChange={setValue}
onFocus={() => {}}
paddingHorizontal={20} paddingHorizontal={20}
leftComponent={() => ( leftComponent={() => (
<TouchableOpacity onPress={() => setDrawer(true)}> <TouchableOpacity onPress={openDrawer}>
<IconComponent <IconComponent
name="slidershorizontal-1" name="slidershorizontal-1"
size={$size(25, 23)} size={$size(25, 23)}
@ -171,42 +130,29 @@ export const HomeScreen: FC<IProps> = ({ navigation }) => {
leftContainerStyle={styles.searchContainer} leftContainerStyle={styles.searchContainer}
/> />
{showResult ? ( {searchResult ? (
<View style={styles.foundCount}> <View style={styles.foundCount}>
<Txt style={styles.foundCountTxt}>{resultString}</Txt> <Txt style={styles.foundCountTxt}>{searchResult}</Txt>
</View> </View>
) : null} ) : null}
<ScrollView <ScrollView
contentContainerStyle={styles.contentWrapper} contentContainerStyle={styles.contentWrapper}
showsVerticalScrollIndicator={false}> showsVerticalScrollIndicator={false}>
<ContentBlock <GroupsSliderList
title={'Групи'} searchString={searchValue}
btnTitle={'Дивитись усі'} onLoaded={setGroupsCount}
onPressBtn={() => navigation.navigate(RouteKey.Group)} />
paddingHorizontal={$size(0)} <ExecutorsSliderListSmart
style={styles.content}> searchString={searchValue}
{groupsMemoRender} onLoaded={setExecutorsCount}
</ContentBlock> />
<ContentBlock <MyTasksListSmart
title={'Виконавці'} searchString={searchValue}
btnTitle={'Дивитись усі'} onLoaded={setTasksCount}
onPressBtn={() => contentStyle={{ paddingHorizontal: 20 }}
navigation.navigate(RouteKey.Executors) />
}
paddingHorizontal={$size(0)}
style={styles.content}>
{executorsMemoRender}
</ContentBlock>
<View style={{ paddingHorizontal: 20 }}>
<MyTasksListSmart
searchString={searchValue}
onLoaded={onTasksLoaded}
alreadyToLoad={alreadyToLoadTasks}
/>
</View>
</ScrollView> </ScrollView>
</> </>
</ScreenLayout> </ScreenLayout>

88
src/modules/home/smart-components/my-tasks-list.smart-component.tsx

@ -1,5 +1,12 @@
import React, { useEffect, FC } from 'react' import React, { useEffect, FC, useCallback } from 'react'
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native' import {
ActivityIndicator,
InteractionManager,
StyleSheet,
Text,
View,
ViewStyle,
} from 'react-native'
import { import {
TasksListHeadSmart, TasksListHeadSmart,
TasksListSmart, TasksListSmart,
@ -9,7 +16,9 @@ import {
$size, $size,
ITaskPreview, ITaskPreview,
RouteKey, RouteKey,
TaskStatus,
useEventsListener, useEventsListener,
useFlatList,
useNav, useNav,
} from '@/shared' } from '@/shared'
import { PartialTheme } from '@/shared/themes/interfaces' import { PartialTheme } from '@/shared/themes/interfaces'
@ -19,19 +28,20 @@ import * as _ from 'lodash'
import { import {
useFetchTasksByFilter, useFetchTasksByFilter,
useTaskActions, useTaskActions,
useTaskListEvents,
useTaskSelector, useTaskSelector,
} from '@/modules/tasks/hooks' } from '@/modules/tasks/hooks'
import { tasksService } from '@/services/domain'
import { transformTasksInList } from '@/api/tasks/transform'
import { SkeletonDataKey } from '@/services/system'
interface IProps { interface IProps {
searchString?: string searchString?: string
onLoaded?: (count: number) => void onLoaded?: (count: number) => void
alreadyToLoad?: boolean contentStyle?: ViewStyle
} }
export const MyTasksListSmart: FC<IProps> = ({ export const MyTasksListSmart: FC<IProps> = ({ contentStyle, ...props }) => {
alreadyToLoad = true,
...props
}) => {
const nav = useNav() const nav = useNav()
const { styles } = useTheme(createStyles) const { styles } = useTheme(createStyles)
const { handlePressOneTaskAction } = useTaskActions() const { handlePressOneTaskAction } = useTaskActions()
@ -39,17 +49,42 @@ export const MyTasksListSmart: FC<IProps> = ({
const { selectedTasks, handleSelectTask, handleResetSelected } = const { selectedTasks, handleSelectTask, handleResetSelected } =
useTaskSelector() useTaskSelector()
const { const { items, isLoading, setLoadParams, resetFlatList, _setItems, count } =
items: myTasks, useFlatList<ITaskPreview>({
isLoading, fetchItems: tasksService.getTasks,
setLoadParams, serrializatorItems: transformTasksInList,
resetTaskList, needInit: false,
count, loadParams: {
} = useFetchTasksByFilter({ sort: 'DESC',
limit: 5, sortField: 'id',
alreadyToLoad, },
limit: 5,
defaultLoading: false,
skeletonDataKey: SkeletonDataKey.HomeTasks,
clearWhenReload: false,
defaultItems: [],
})
useEffect(() => {
const {} = InteractionManager.runAfterInteractions(() => {
resetFlatList()
})
}, [])
const onAction = (key: string) => {
if (key === 'delete') resetFlatList()
}
useTaskListEvents({
items,
onReload: () => resetFlatList(),
setItems: (items: ITaskPreview[]) => _setItems(items),
onAction,
taskFilterStatus: TaskStatus.Active,
}) })
useEventsListener('reloadTask', resetFlatList)
useEffect(() => { useEffect(() => {
if (props.onLoaded) props.onLoaded(count) if (props.onLoaded) props.onLoaded(count)
}, [count]) }, [count])
@ -59,20 +94,21 @@ export const MyTasksListSmart: FC<IProps> = ({
setLoadParams({ searchString: props.searchString }) setLoadParams({ searchString: props.searchString })
}, [props.searchString]) }, [props.searchString])
const onPressTask = (task: ITaskPreview) => { const onPressTask = useCallback(
nav.navigate(RouteKey.TaskDetails, { (task: ITaskPreview) => {
taskId: task.id, nav.navigate(RouteKey.TaskDetails, {
}) taskId: task.id,
} })
},
useEventsListener('reloadTask', resetTaskList) [nav.navigate],
)
const renderList = ( const renderList = (
items: ITaskPreview[], items: ITaskPreview[],
renderItem: TRenderTaskPreviewItem, renderItem: TRenderTaskPreviewItem,
) => { ) => {
return ( return (
<View style={styles.content} key="III"> <View style={[styles.content, contentStyle]} key="III">
{_.isEmpty(selectedTasks) ? ( {_.isEmpty(selectedTasks) ? (
<ContentBlockHeader <ContentBlockHeader
title="Задачі" title="Задачі"
@ -98,11 +134,11 @@ export const MyTasksListSmart: FC<IProps> = ({
if (isLoading) return <ActivityIndicator /> if (isLoading) return <ActivityIndicator />
if (_.isEmpty(myTasks)) return <Text style={styles.text}>Немає задач</Text> if (_.isEmpty(items)) return <Text style={styles.text}>Немає задач</Text>
return ( return (
<TasksListSmart <TasksListSmart
items={myTasks} items={items}
renderList={renderList} renderList={renderList}
onPressItem={onPressTask} onPressItem={onPressTask}
onSelectItem={task => handleSelectTask(task)} onSelectItem={task => handleSelectTask(task)}

8
src/modules/root/navigation-groups/tab-bar.group.tsx

@ -23,13 +23,17 @@ export const TabNavigator: FC = () => {
return ( return (
<Tab.Navigator <Tab.Navigator
screenOptions={{ headerShown: false }} screenOptions={{ headerShown: false, lazy: true }}
initialRouteName={RouteKey.Home} initialRouteName={RouteKey.Home}
tabBar={({ state, navigation: { navigate } }) => ( tabBar={({ state, navigation: { navigate } }) => (
<TabBarSmart state={state} navigate={navigate} /> <TabBarSmart state={state} navigate={navigate} />
)}> )}>
<Tab.Screen name={RouteKey.Home} component={HomeGroup} /> <Tab.Screen name={RouteKey.Home} component={HomeGroup} />
<Tab.Screen name={RouteKey.Chats} component={ChatsScreen} /> <Tab.Screen
name={RouteKey.Chats}
component={ChatsScreen}
// options={{ lazy: false }}
/>
<Tab.Screen <Tab.Screen
name={RouteKey.AddTask} name={RouteKey.AddTask}
component={AddUpdateTaskScreen} component={AddUpdateTaskScreen}

14
src/modules/tasks/hooks/use-fetch-tasks-by-filter.hook.ts

@ -6,7 +6,7 @@ import { transformTasksInList } from '@/api/tasks/transform'
import { IFetchTasksParams } from '@/api/tasks/requests.interface' import { IFetchTasksParams } from '@/api/tasks/requests.interface'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { selectId } from '@/store/account' import { selectId } from '@/store/account'
import _ from 'lodash' import _, { noop } from 'lodash'
import { useTaskFilter } from './use-task-filter.hook' import { useTaskFilter } from './use-task-filter.hook'
import { useTaskListEvents } from './use-task-list-events.hook' import { useTaskListEvents } from './use-task-list-events.hook'
@ -65,7 +65,7 @@ export const useFetchTasksByFilter = ({
fetchItems: params => tasksService.getTasks(params), fetchItems: params => tasksService.getTasks(params),
serrializatorItems: (items: ITaskPreviewResponse[]) => serrializatorItems: (items: ITaskPreviewResponse[]) =>
transformTasksInList(items) as any, transformTasksInList(items) as any,
needInit: false, needInit: true,
loadParams: { loadParams: {
sort: 'DESC', sort: 'DESC',
sortField: 'id', sortField: 'id',
@ -76,10 +76,6 @@ export const useFetchTasksByFilter = ({
clearWhenReload: false, clearWhenReload: false,
}) })
useEffect(() => {
if (alreadyToLoad) resetFlatList()
}, [alreadyToLoad])
useEffect(() => { useEffect(() => {
if (searchString) setLoadParams({ ...getFetchParams(), searchString }) if (searchString) setLoadParams({ ...getFetchParams(), searchString })
}, [searchString]) }, [searchString])
@ -88,15 +84,11 @@ export const useFetchTasksByFilter = ({
setLoadParams({ ...getFetchParams(), searchString }) setLoadParams({ ...getFetchParams(), searchString })
} }
const onAction = (key: string) => {
if (key === 'delete' && limit === 5) resetFlatList()
}
useTaskListEvents({ useTaskListEvents({
items, items,
onReload: () => resetFlatList(), onReload: () => resetFlatList(),
setItems: (items: ITaskPreview[]) => _setItems(items), setItems: (items: ITaskPreview[]) => _setItems(items),
onAction, onAction: noop,
taskFilterStatus: filter?.status, taskFilterStatus: filter?.status,
}) })

43
src/services/domain/account.service.ts

@ -2,41 +2,26 @@ import { simpleDispatch } from '@/store/store-helpers'
import { SetLoadingAccount, SaveAccount } from '@/store/account' import { SetLoadingAccount, SaveAccount } from '@/store/account'
import { fetchAccount } from '@/api' import { fetchAccount } from '@/api'
import { IUser } from '@/shared/interfaces' import { IUser } from '@/shared/interfaces'
import AsyncStorage from '@react-native-async-storage/async-storage' import { Service } from '@/shared/abstract'
const loadAccount = async (version?: string) => { class AccountService extends Service {
simpleDispatch(new SetLoadingAccount({ isLoading: true })) protected prefix = 'account'
try {
let account: IUser
if (version) { public async loadAccount(version?: string) {
const cached = await AsyncStorage.getItem(`acc${version}`) simpleDispatch(new SetLoadingAccount({ isLoading: true }))
if (cached) account = JSON.parse(cached)
}
if (!account) { try {
const { data } = await fetchAccount() const account = await this.fetchData(fetchAccount)
account = data simpleDispatch(new SaveAccount({ account }))
if (version) } catch (e) {
await AsyncStorage.setItem( } finally {
`acc${version}`, simpleDispatch(new SetLoadingAccount({ isLoading: false }))
JSON.stringify(data),
)
} }
}
public setAccount(account: IUser) {
simpleDispatch(new SaveAccount({ account })) simpleDispatch(new SaveAccount({ account }))
} catch (e: any) {
console.log(e)
} finally {
simpleDispatch(new SetLoadingAccount({ isLoading: false }))
} }
} }
const setAccount = async (account: IUser) => { export const accountService = new AccountService()
simpleDispatch(new SaveAccount({ account }))
}
export const accountService = {
loadAccount,
setAccount,
}

2
src/services/domain/permissions.service.ts

@ -13,6 +13,8 @@ import AsyncStorage from '@react-native-async-storage/async-storage'
import _ from 'lodash' import _ from 'lodash'
class PermissionsService extends Service { class PermissionsService extends Service {
protected prefix = 'prefix'
private getPermissionsForUsers(): IPermissionsState['permissionsForUsers'] { private getPermissionsForUsers(): IPermissionsState['permissionsForUsers'] {
return this.select(selectPermissionsForUsers) return this.select(selectPermissionsForUsers)
} }

3
src/services/system/index.ts

@ -5,4 +5,5 @@ export * from './storage.service'
export * from './device-info.service' export * from './device-info.service'
export * from './real-time.service' export * from './real-time.service'
export * from './media.service' export * from './media.service'
export * from './fs.service' export * from './fs.service'
export * from './skeleton-data.service'

47
src/services/system/skeleton-data.service.ts

@ -0,0 +1,47 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
import _ from 'lodash'
export enum SkeletonDataKey {
HomeGroups = 'homeGroups',
HomeExecutors = 'homeExecutors',
HomeTasks = 'homeTasks',
}
export class SkeletonData {
private dataKeys = [
SkeletonDataKey.HomeGroups,
SkeletonDataKey.HomeExecutors,
SkeletonDataKey.HomeTasks,
]
private loadedData: Partial<Record<SkeletonDataKey, any>> = {}
constructor() {
this.init()
}
private async init() {
for await (const key of this.dataKeys) {
await this.load(key)
}
console.log('LOAD FINISH')
}
private async load(key: SkeletonDataKey) {
const data = await AsyncStorage.getItem(key)
if (!data) return
this.loadedData[key] = JSON.parse(data)
console.log(key, this.loadedData)
}
public set(key: SkeletonDataKey, data: any) {
console.log('set key', key)
AsyncStorage.setItem(key, JSON.stringify(data))
}
public get(key: SkeletonDataKey, defaultValue?: any) {
return _.defaultTo(this.loadedData[key], defaultValue)
}
}
export const skeletonDataService = new SkeletonData()

32
src/shared/abstract/service.ts

@ -1,7 +1,11 @@
import store, { RootState } from '@/store' import store, { RootState } from '@/store'
import { selectId } from '@/store/account' import { selectId } from '@/store/account'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { AxiosResponse } from 'axios'
export abstract class Service {
protected abstract prefix: string
export class Service {
protected select(selector: (store: RootState) => any) { protected select(selector: (store: RootState) => any) {
return selector(store.getState()) return selector(store.getState())
} }
@ -9,4 +13,30 @@ export class Service {
protected selectUserId() { protected selectUserId() {
return this.select(selectId) return this.select(selectId)
} }
protected async fetchData<T>(
req: (...args: any[]) => Promise<AxiosResponse<T>>,
version?: string,
): Promise<T> {
let result: T
if (version) {
const cached = await AsyncStorage.getItem(
`${this.prefix}${version}`,
)
if (cached) result = JSON.parse(cached)
}
if (!result) {
const { data } = await req()
result = data
if (version)
await AsyncStorage.setItem(
`${this.prefix}${version}`,
JSON.stringify(data),
)
}
return result
}
} }

5
src/shared/components/forms/form-search.component.tsx

@ -10,6 +10,7 @@ import { IconComponent } from '../elements'
import { $size } from '@/shared/helpers' import { $size } from '@/shared/helpers'
import { PartialTheme } from '@/shared/themes/interfaces' import { PartialTheme } from '@/shared/themes/interfaces'
import { useTheme } from '@/shared/hooks/use-theme.hook' import { useTheme } from '@/shared/hooks/use-theme.hook'
import { noop } from 'lodash'
interface IProps { interface IProps {
searchValue: string searchValue: string
@ -35,8 +36,8 @@ export const SearchForm: FC<IProps> = ({
containerStyle, containerStyle,
borderBottom = true, borderBottom = true,
onChange, onChange,
onFocus = () => {}, onFocus = noop,
onBlur = () => {}, onBlur = noop,
leftComponent, leftComponent,
leftContainerStyle, leftContainerStyle,
rightComponent, rightComponent,

3
src/shared/components/headers/primary-header.component.tsx

@ -11,6 +11,7 @@ interface IProps {
bottomBorder?: boolean bottomBorder?: boolean
goBack?: () => void goBack?: () => void
rightComponents?: () => ReactElement | ReactElement[] rightComponents?: () => ReactElement | ReactElement[]
rightComponent?: ReactElement
} }
export const PrimaryHeader: FC<IProps> = ({ export const PrimaryHeader: FC<IProps> = ({
@ -19,6 +20,7 @@ export const PrimaryHeader: FC<IProps> = ({
bottomBorder = true, bottomBorder = true,
goBack, goBack,
rightComponents, rightComponents,
rightComponent = null,
}) => { }) => {
const { styles, theme } = useTheme(createStyles) const { styles, theme } = useTheme(createStyles)
@ -45,6 +47,7 @@ export const PrimaryHeader: FC<IProps> = ({
<View style={styles.rightComponent}> <View style={styles.rightComponent}>
{rightComponents && rightComponents()} {rightComponents && rightComponents()}
{rightComponent}
</View> </View>
</View> </View>
) )

17
src/shared/components/layouts/screen-layout.component.tsx

@ -1,4 +1,4 @@
import React, { ReactElement } from 'react' import React, { ReactElement, useCallback } from 'react'
import { StatusBar, StyleSheet, View, ViewStyle } from 'react-native' import { StatusBar, StyleSheet, View, ViewStyle } from 'react-native'
import { ScreenLayoutContent } from './screen-layout-content.component' import { ScreenLayoutContent } from './screen-layout-content.component'
import { PrimaryHeader } from '../headers' import { PrimaryHeader } from '../headers'
@ -42,6 +42,13 @@ interface ScreenLayoutProps {
export const ScreenLayout = (props: ScreenLayoutProps) => { export const ScreenLayout = (props: ScreenLayoutProps) => {
const { styles, themeTitle } = useTheme(createStyles) const { styles, themeTitle } = useTheme(createStyles)
const header = useCallback(() => {
if (props.header) return <PrimaryHeader {...props.header} />
if (props.headerComponent) return props.headerComponent
return null
}, [props.header, props.headerComponent])
const layout = ( const layout = (
<View style={styles.container} key="base"> <View style={styles.container} key="base">
<StatusBar <StatusBar
@ -50,13 +57,7 @@ export const ScreenLayout = (props: ScreenLayoutProps) => {
} }
/> />
<ScreenLayoutContent <ScreenLayoutContent {...props} header={header} />
{...props}
header={() => {
if (props.header) return <PrimaryHeader {...props.header} />
if (props.headerComponent) return props.headerComponent
}}
/>
{props.footer ? props.footer() : null} {props.footer ? props.footer() : null}
</View> </View>

3
src/shared/enums/storage-keys.enum.ts

@ -3,5 +3,6 @@ export enum StorageKey {
RefreshToken = 'RefreshToken', RefreshToken = 'RefreshToken',
Theme = 'Theme', Theme = 'Theme',
ChatBgId = 'ChatBgId', ChatBgId = 'ChatBgId',
CustomChatBg = 'CustomChatBg' CustomChatBg = 'CustomChatBg',
Chats = 'chats',
} }

28
src/shared/hooks/use-flat-list.hook.ts

@ -1,3 +1,5 @@
import { SkeletonDataKey, skeletonDataService } from '@/services/system'
import _ from 'lodash'
import { useState, useRef, useEffect, useMemo } from 'react' import { useState, useRef, useEffect, useMemo } from 'react'
interface IProps<T> { interface IProps<T> {
@ -9,6 +11,9 @@ interface IProps<T> {
loadParams?: { [key: string]: string | boolean } loadParams?: { [key: string]: string | boolean }
needInit: boolean needInit: boolean
clearWhenReload?: boolean clearWhenReload?: boolean
defaultItems?: T[]
defaultLoading?: boolean
skeletonDataKey?: SkeletonDataKey
} }
const defaultProps: IProps<any[]> = { const defaultProps: IProps<any[]> = {
@ -20,6 +25,9 @@ const defaultProps: IProps<any[]> = {
loadParams: {}, loadParams: {},
needInit: true, needInit: true,
clearWhenReload: true, clearWhenReload: true,
defaultItems: null,
defaultLoading: true,
skeletonDataKey: null,
} }
const getDefaultProps = () => { const getDefaultProps = () => {
@ -41,17 +49,25 @@ export const useFlatList = <T>(props: IProps<T>) => {
const blockLoadingRef = useRef(false) const blockLoadingRef = useRef(false)
const [items, setItems] = useState<T[]>(null) const [items, setItems] = useState<T[]>(props.defaultItems)
const [totalCount, setTotalCount] = useState(0) const [totalCount, setTotalCount] = useState(0)
const [isLoading, setLoading] = useState(true) const [isLoading, setLoading] = useState(props.defaultLoading)
const [isLoadingNext, setIsLoadingNext] = useState(false) const [isLoadingNext, setIsLoadingNext] = useState(false)
const [isInit, setIsInit] = useState(false) const [isInit, setIsInit] = useState(false)
useEffect(() => {
if (props.skeletonDataKey) {
const items = skeletonDataService.get(props.skeletonDataKey)
if (items) setItems(_.defaultTo(items, []))
else setLoading(true)
}
}, [])
const fetchItems = async (firstFetch = false) => { const fetchItems = async (firstFetch = false) => {
const { count, page, limit } = loadParams.current const { count, page, limit } = loadParams.current
if (firstFetch) { if (firstFetch) {
setLoading(true) if (isInit || !props.skeletonDataKey) setLoading(true)
loadParams.current.page = defaultProps.page loadParams.current.page = defaultProps.page
loadParams.current.count = undefined loadParams.current.count = undefined
} else if (blockLoadingRef.current) return } else if (blockLoadingRef.current) return
@ -82,7 +98,11 @@ export const useFlatList = <T>(props: IProps<T>) => {
setTotalCount(response.data.count) setTotalCount(response.data.count)
if (!isInit) setIsInit(true) if (!isInit) {
if (props.skeletonDataKey)
skeletonDataService.set(props.skeletonDataKey, fetchedItems)
setIsInit(true)
}
} catch (e) { } catch (e) {
console.log(e) console.log(e)
setItems([]) setItems([])

4
yarn.lock

@ -1540,7 +1540,7 @@
"resolved" "https://registry.npmjs.org/@react-native-community/clipboard/-/clipboard-1.5.1.tgz" "resolved" "https://registry.npmjs.org/@react-native-community/clipboard/-/clipboard-1.5.1.tgz"
"version" "1.5.1" "version" "1.5.1"
"@react-native-community/datetimepicker@>=6.1.3": "@react-native-community/datetimepicker@^6.1.3", "@react-native-community/datetimepicker@>=6.1.3":
"integrity" "sha512-3e20BPQy8QiQeIGJL4zoJaRsGWB31hl9i7oTZECWxnK6nVUeqGt/G+JRJlJinmqL6z4WePHC6RSUMgpC64OOwg==" "integrity" "sha512-3e20BPQy8QiQeIGJL4zoJaRsGWB31hl9i7oTZECWxnK6nVUeqGt/G+JRJlJinmqL6z4WePHC6RSUMgpC64OOwg=="
"resolved" "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-6.1.3.tgz" "resolved" "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-6.1.3.tgz"
"version" "6.1.3" "version" "6.1.3"
@ -3480,7 +3480,7 @@
"resolved" "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" "resolved" "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz"
"version" "2.0.0" "version" "2.0.0"
"deprecated-react-native-prop-types@>=2.3.0": "deprecated-react-native-prop-types@^2.3.0", "deprecated-react-native-prop-types@>=2.3.0":
"integrity" "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==" "integrity" "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA=="
"resolved" "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz" "resolved" "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz"
"version" "2.3.0" "version" "2.3.0"

Loading…
Cancel
Save