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

4
ios/taskme.xcodeproj/project.pbxproj

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

6
package-lock.json generated

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

33
src/api/tasks/transform.ts

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

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

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

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

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

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

@ -4,8 +4,10 @@ import { @@ -4,8 +4,10 @@ import {
appEvents,
ChatMemberRole,
ChatType,
createFullName,
IChat,
RouteKey,
StorageKey,
useEventsListener,
useFlatList,
useNav,
@ -31,6 +33,7 @@ import { @@ -31,6 +33,7 @@ import {
import { showUknowError } from '@/shared/helpers/alert.helper'
import { getChatIdFromMessages, isChatInList } from '../helpers'
import { InteractionManager } from 'react-native'
export const useChatList = () => {
const nav = useNav()
@ -38,6 +41,25 @@ export const useChatList = () => { @@ -38,6 +41,25 @@ export const useChatList = () => {
const accountId = useSelector(selectId)
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 {
items: chats,
loadMore,
@ -51,8 +73,34 @@ export const useChatList = () => { @@ -51,8 +73,34 @@ export const useChatList = () => {
needInit: true,
clearWhenReload: false,
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(() => {
if (searchString !== null) setLoadParams({ searchString })
}, [searchString])
@ -360,7 +408,7 @@ export const useChatList = () => { @@ -360,7 +408,7 @@ export const useChatList = () => {
resetFlatList: resetFlatList,
setSearchVal,
handleChatAction,
isLoading,
isLoadingNext,
isLoading: false,
isLoadingNext: false,
}
}

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

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

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

@ -1,21 +1,37 @@ @@ -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 { useTheme } from '@/shared/hooks/use-theme.hook'
import { SelectChat } from '@/store/chats'
import { simpleDispatch } from '@/store/store-helpers'
import { useNavigation } from '@react-navigation/native'
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 { ChatsList } from '../components/chats-list.component'
import { useChatList } from '../hooks'
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 = () => {
const { styles } = useTheme(createStyles)
const { navigate } = useNavigation<TConversationNav>()
// const [items, setItems] = useState([])
// useEffect(() => {
// setItems(cachedItems)
// }, [])
const {
chats,
@ -68,9 +84,9 @@ const createStyles = () => @@ -68,9 +84,9 @@ const createStyles = () =>
container: {
...Platform.select({
android: {
paddingBottom: $size(73)
}
})
paddingBottom: $size(73),
},
}),
},
searchField: {
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' @@ -2,7 +2,7 @@ import { IChat, SquareButton } from '@/shared'
import { useTheme } from '@/shared/hooks/use-theme.hook'
import { PartialTheme } from '@/shared/themes/interfaces'
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 { Swipeable } from 'react-native-gesture-handler'
import { ChatRowCard } from '../components'
@ -28,6 +28,12 @@ interface IProps { @@ -28,6 +28,12 @@ interface IProps {
style?: ViewStyle
}
const prepareBtnsToRender = (config: any) => {
return config.map((it, i) => (
<SquareButton key={i} style={{ width: '25%' }} {...it} />
))
}
export const SwipableChatRowCardSmart: FC<IProps> = ({
id,
label,
@ -45,45 +51,46 @@ export const SwipableChatRowCardSmart: FC<IProps> = ({ @@ -45,45 +51,46 @@ export const SwipableChatRowCardSmart: FC<IProps> = ({
const { styles, theme } = useTheme(createStyles)
const swipableRef = useRef(null)
const closeSwipeable = () => {
const closeSwipeable = useCallback(() => {
swipableRef.current.close()
}
}, [swipableRef.current])
useFocusEffect(() => closeSwipeable())
const prepareBtnsToRender = (config: any) => {
const btnsToRender = config.map(it => {
return <SquareButton style={{ width: '25%' }} {...it} />
})
return btnsToRender
}
const rightBtnsToRender = prepareBtnsToRender(
chatRowCardButtonsConfig.rightBtn({
id,
unreadMessagesCount,
theme,
onPress: onPressActionBtn,
afterPress: closeSwipeable,
}),
const rightBtnsToRender = useCallback(
() =>
prepareBtnsToRender(
chatRowCardButtonsConfig.rightBtn({
id,
theme,
onPress: onPressActionBtn,
afterPress: closeSwipeable,
}),
),
[id, unreadMessagesCount, closeSwipeable],
)
const leftBtnsToRender = prepareBtnsToRender(
chatRowCardButtonsConfig.leftBtn({
id,
unreadMessagesCount,
theme,
onPress: onPressActionBtn,
afterPress: closeSwipeable,
isChatFixed: isPinned,
isChatUnread: isUnread,
}),
const leftBtnsToRender = useCallback(
() =>
prepareBtnsToRender(
chatRowCardButtonsConfig.leftBtn({
id,
unreadMessagesCount,
theme,
onPress: onPressActionBtn,
afterPress: closeSwipeable,
isChatFixed: isPinned,
isChatUnread: isUnread,
}),
),
[id, unreadMessagesCount, theme, isPinned, isUnread, closeSwipeable],
)
return (
<Swipeable
ref={swipableRef}
renderLeftActions={() => leftBtnsToRender}
renderRightActions={() => rightBtnsToRender}>
renderLeftActions={leftBtnsToRender}
renderRightActions={rightBtnsToRender}>
<View style={[styles.container, style]}>
<ChatRowCard
isOnline={isOnline}

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

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

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

@ -0,0 +1,153 @@ @@ -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 @@ @@ -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 @@ @@ -1,6 +1,5 @@
import { useEffect, useState } from 'react'
import { ITaxonomyWithTasksCount, useFlatList } from '@/shared'
import { taxonomiesService } from '@/services/domain'
import { fetchGroupsWithTasksCountReq } from '@/api'
export const useFetchGroupsWithTasksCount = (

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

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

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

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

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

@ -0,0 +1,148 @@ @@ -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 @@ @@ -0,0 +1 @@
export * from './groups-slider-list.smart-component'

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

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

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

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

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

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

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

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

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

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

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

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

1
src/services/system/index.ts

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

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

@ -0,0 +1,47 @@ @@ -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 @@ @@ -1,7 +1,11 @@
import store, { RootState } from '@/store'
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) {
return selector(store.getState())
}
@ -9,4 +13,30 @@ export class Service { @@ -9,4 +13,30 @@ export class Service {
protected selectUserId() {
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' @@ -10,6 +10,7 @@ import { IconComponent } from '../elements'
import { $size } from '@/shared/helpers'
import { PartialTheme } from '@/shared/themes/interfaces'
import { useTheme } from '@/shared/hooks/use-theme.hook'
import { noop } from 'lodash'
interface IProps {
searchValue: string
@ -35,8 +36,8 @@ export const SearchForm: FC<IProps> = ({ @@ -35,8 +36,8 @@ export const SearchForm: FC<IProps> = ({
containerStyle,
borderBottom = true,
onChange,
onFocus = () => {},
onBlur = () => {},
onFocus = noop,
onBlur = noop,
leftComponent,
leftContainerStyle,
rightComponent,

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

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

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

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

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

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

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

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

4
yarn.lock

@ -1540,7 +1540,7 @@ @@ -1540,7 +1540,7 @@
"resolved" "https://registry.npmjs.org/@react-native-community/clipboard/-/clipboard-1.5.1.tgz"
"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=="
"resolved" "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-6.1.3.tgz"
"version" "6.1.3"
@ -3480,7 +3480,7 @@ @@ -3480,7 +3480,7 @@
"resolved" "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz"
"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=="
"resolved" "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz"
"version" "2.3.0"

Loading…
Cancel
Save