Browse Source

create-personal-chat (#15) (#17)

Co-authored-by: Vitalik Yatsenko <vitalik@noreply.localhost>
Reviewed-on: #15
Co-authored-by: YaroslavBerkuta <yaroslavberkuta@gmail.com>
Co-committed-by: YaroslavBerkuta <yaroslavberkuta@gmail.com>
Co-authored-by: YaroslavBerkuta <yaroslavberkuta@gmail.com>
Reviewed-on: #17
pull/18/head
Vitalik Yatsenko 9 months ago
parent
commit
d4ac2eb0e3
  1. 19
      src/api/tasks/transform.ts
  2. 12
      src/modules/chats/components/chats-list.component.tsx
  3. 50
      src/modules/chats/screens/chats.screen.tsx
  4. 180
      src/modules/chats/screens/create-personal.screen.tsx
  5. 3
      src/modules/chats/screens/index.ts
  6. 7
      src/modules/root/navigation-groups/users.group.tsx
  7. 7
      src/modules/users/hooks/use-fetch-users.hook.ts
  8. 2
      src/modules/users/screens/select-user-list.screen.tsx
  9. 1
      src/shared/enums/route-key.enum.ts
  10. 22
      src/shared/helpers/configs.helpers.ts
  11. 5
      src/shared/helpers/date.helpers.ts
  12. 7
      src/shared/helpers/exceptions.helpers.ts
  13. 8
      src/shared/helpers/is-soon-birthday.helper.ts
  14. 16
      src/shared/helpers/linking.helper.ts
  15. 6
      src/shared/helpers/name.helpers.ts
  16. 10
      src/shared/helpers/theme-helpers.ts
  17. 25
      src/shared/helpers/title-by-count.helper.ts
  18. 23
      src/shared/helpers/touch-event.helpers.ts
  19. 4
      src/shared/helpers/url.helpers.ts
  20. 23
      src/shared/helpers/versions.helper.ts

19
src/api/tasks/transform.ts

@ -13,15 +13,18 @@ import { @@ -13,15 +13,18 @@ import {
export const transformExecutorsToShortUsers = (
items: ITaskExecutorResponse[],
execludeUserId?: number[],
): IShortUser[] => {
const transformedItems = items.map(it => {
return {
id: it.userId,
firstName: it.firstName,
fullName: createFullName(it.lastName, it.firstName),
avatarUrl: it.avatarUrl,
}
})
const transformedItems = items
.filter(obj => !execludeUserId?.includes(obj.userId))
.map(it => {
return {
id: it.userId,
firstName: it.firstName,
fullName: createFullName(it.lastName, it.firstName),
avatarUrl: it.avatarUrl,
}
})
return transformedItems
}

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

@ -1,13 +1,5 @@ @@ -1,13 +1,5 @@
import { createCardStyles } from '@/modules/tasks/helpers'
import {
$size,
ChatType,
createFullName,
IChat,
IChatMessage,
MessageType,
Txt,
} from '@/shared'
import { $size, IChat, Txt } from '@/shared'
import { getMessagePreviewText } from '@/shared/helpers'
import { useTheme } from '@/shared/hooks/use-theme.hook'
import { PartialTheme } from '@/shared/themes/interfaces'
@ -17,11 +9,9 @@ import { @@ -17,11 +9,9 @@ import {
FlatList,
Platform,
StyleSheet,
Text,
View,
ViewStyle,
} from 'react-native'
import Animated from 'react-native-reanimated'
import { ChatCardActionEnum } from '../enums'
import { SwipableChatRowCardSmart } from '../smart-components'

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

@ -1,13 +1,11 @@ @@ -1,13 +1,11 @@
import React, { FC } from 'react'
import {
$size,
IRouteParams,
PrimaryHeader,
RouteKey,
ScreenLayout,
appEvents,
} from '@/shared'
import { useTheme } from '@/shared/hooks/use-theme.hook'
import { StyleSheet, View } from 'react-native'
import { HeaderRightBtn } from '../atoms'
import { SmartChatsList } from '../smart-components'
import { useSelector } from 'react-redux'
@ -16,26 +14,37 @@ import { selectId } from '@/store/account' @@ -16,26 +14,37 @@ import { selectId } from '@/store/account'
interface IProps extends IRouteParams {}
export const ChatsScreen: FC<IProps> = ({ navigation }) => {
const { styles } = useTheme(createStyles)
const accountId = useSelector(selectId)
const onPressHeaderBtn = () =>
navigation.navigate(RouteKey.SelectUsersScreen, {
title: 'Нова група',
isLoading: false,
footerBtnTitle: 'Далі',
onSubmit: () => navigation.navigate(RouteKey.CreateGroup),
useChatBtnColors: true,
excludeIds: [accountId],
resetOnGoBack: true,
type: 'all',
const createChat = () => {
appEvents.emit('openActionSheet', {
items: [
{
name: 'Персональний',
onPress: () => navigation.navigate(RouteKey.CreatePersonal),
},
{
name: 'Груповий',
onPress: () =>
navigation.navigate(RouteKey.SelectUsersScreen, {
title: 'Нова група',
isLoading: false,
footerBtnTitle: 'Далі',
onSubmit: () =>
navigation.navigate(RouteKey.CreateGroup),
useChatBtnColors: true,
excludeIds: [accountId],
resetOnGoBack: true,
type: 'all',
}),
},
],
})
}
const props = {
title: 'Бесіди',
rightComponent: <HeaderRightBtn onPress={onPressHeaderBtn} />,
// goBack: navigation.goBack,
//style: styles.header,
rightComponent: <HeaderRightBtn onPress={createChat} />,
}
return (
<ScreenLayout headerComponent={<PrimaryHeader {...props} />}>
@ -43,10 +52,3 @@ export const ChatsScreen: FC<IProps> = ({ navigation }) => { @@ -43,10 +52,3 @@ export const ChatsScreen: FC<IProps> = ({ navigation }) => {
</ScreenLayout>
)
}
const createStyles = () =>
StyleSheet.create({
header: {
//marginBottom: $size(10, 8),
},
})

180
src/modules/chats/screens/create-personal.screen.tsx

@ -0,0 +1,180 @@ @@ -0,0 +1,180 @@
/* eslint-disable react/no-unstable-nested-components */
import React, { useCallback } from 'react'
import {
$size,
Avatar,
IShortUser,
IconComponent,
RouteKey,
ScreenLayout,
SearchForm,
Txt,
appEvents,
hasImageUrl,
useNav,
useTheme,
} from '@/shared'
import { useFetchUsersList } from '@/modules/users/hooks'
import {
TouchableOpacity,
StyleSheet,
FlatList,
ActivityIndicator,
Platform,
} from 'react-native'
import { PartialTheme } from '@/shared/themes/interfaces'
import { chatManager } from '@/managers'
import { simpleDispatch } from '@/store/store-helpers'
import { SelectChat } from '@/store/chats'
import { useSelector } from 'react-redux'
import { selectAccount } from '@/store/account'
export const CreatePersonalScreen = () => {
const account = useSelector(selectAccount)
const nav = useNav()
const { styles, theme } = useTheme(createStyles)
const {
items,
searchString,
setSearchVal,
loadMore,
resetFlatList,
isLoading,
isLoadingNext,
} = useFetchUsersList({
type: 'all',
execludeUserId: [account.id],
})
const onPressMessage = async userId => {
try {
const chatId = await chatManager.getPersonalChatId({
userId,
})
simpleDispatch(new SelectChat({ id: chatId }))
chatManager.readChat.bind(chatManager)(chatId)
nav.navigate(RouteKey.Conversation)
} catch (e) {
appEvents.emit('openInfoModal', {
title: 'Сталась помилка!',
message: 'Спробуйте будь-ласка пізніше.',
onPressOk: () => {},
})
}
}
const renderItem = useCallback(({ item }: { item: IShortUser }) => {
return (
<TouchableOpacity
style={styles.container}
onPress={() => onPressMessage(item.id)}>
<Avatar
imageUrl={hasImageUrl(item.avatarUrl, item.fullName)}
maxHeight={$size(35, 32)}
maxWidth={$size(35, 32)}
textStyle={styles.avatarLabel}
/>
<Txt style={styles.userName}>{item.fullName}</Txt>
<IconComponent
name="chatcircledots-1"
size={24}
color="#9F2843"
/>
</TouchableOpacity>
)
}, [])
const renderFooter = useCallback(() => {
if (!isLoading && isLoadingNext)
return (
<ActivityIndicator
color={theme.$loaderPrimary}
style={{ marginTop: 20 }}
/>
)
else return null
}, [isLoading, isLoadingNext, theme])
const renderEmpty = useCallback(() => {
if (isLoading || isLoadingNext) {
return <ActivityIndicator color={theme.$loaderPrimary} />
} else {
return <Txt style={styles.emptyText}>Користувачі відсутні</Txt>
}
}, [isLoading, isLoadingNext])
const keyExtractor = useCallback(item => `${item.id}`, [])
return (
<ScreenLayout
horizontalPadding={0}
header={{
goBack: nav.goBack,
title: 'Новий персональний чат',
style: {
marginBottom: $size(20, 18),
paddingTop: $size(10, 10),
},
}}>
<>
<SearchForm
containerStyle={styles.searchContainer}
searchValue={searchString}
placeholder={'Знайдіть контакт'}
onChange={setSearchVal}
/>
<FlatList
style={{ flex: 1 }}
data={items}
renderItem={renderItem}
contentContainerStyle={{ paddingHorizontal: $size(16) }}
keyExtractor={keyExtractor}
initialNumToRender={10}
onEndReachedThreshold={0.4}
refreshing={Platform.select({
ios: false,
android: isLoading,
})}
onEndReached={loadMore}
onRefresh={resetFlatList}
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
ListEmptyComponent={renderEmpty}
ListFooterComponent={renderFooter}
/>
</>
</ScreenLayout>
)
}
const createStyles = (theme: PartialTheme) =>
StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
height: $size(65, 60),
borderTopWidth: 0.3,
borderTopColor: theme.$border,
},
userName: {
fontSize: $size(16, 14),
fontWeight: '400',
marginLeft: $size(20, 16),
marginRight: 'auto',
color: theme.$textPrimary,
},
checkBox: {
position: 'absolute',
right: 0,
alignItems: 'center',
justifyContent: 'center',
},
searchContainer: {
borderBottomWidth: 0,
},
avatarLabel: {
fontSize: $size(20, 18),
fontWeight: '500',
},
})

3
src/modules/chats/screens/index.ts

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
export * from './chats.screen'
export * from './group-chat-detail.screen'
export * from './create-group.screen'
export * from './forward-message.screen'
export * from './forward-message.screen'
export * from './create-personal.screen'

7
src/modules/root/navigation-groups/users.group.tsx

@ -21,6 +21,7 @@ import { ChatConversation } from '@/modules/chats/screens/chat' @@ -21,6 +21,7 @@ import { ChatConversation } from '@/modules/chats/screens/chat'
import { Comments } from '@/modules/comments/screens/comments.screen'
import {
CreateChatScreen,
CreatePersonalScreen,
ForwardMessageScreen,
GroupChatDetailScreen,
} from '@/modules/chats'
@ -63,6 +64,12 @@ export const UsersGroup: FC = () => ( @@ -63,6 +64,12 @@ export const UsersGroup: FC = () => (
name={RouteKey.CreateGroup}
component={CreateChatScreen}
/>
<Stack.Screen
name={RouteKey.CreatePersonal}
component={CreatePersonalScreen}
/>
<Stack.Screen
name={RouteKey.ForwardMessage}
component={ForwardMessageScreen}

7
src/modules/users/hooks/use-fetch-users.hook.ts

@ -8,6 +8,7 @@ import _ from 'lodash' @@ -8,6 +8,7 @@ import _ from 'lodash'
interface IProps {
type?: 'executors' | 'all'
execludeUserId?: number[]
}
const api = {
@ -15,7 +16,7 @@ const api = { @@ -15,7 +16,7 @@ const api = {
all: params => chatsService.getUsersForChat.bind(chatsService)(params),
}
export const useFetchUsersList = ({ type }: IProps) => {
export const useFetchUsersList = ({ type, execludeUserId }: IProps) => {
const [searchString, setSearchVal] = useState<string>(null)
const {
@ -26,11 +27,12 @@ export const useFetchUsersList = ({ type }: IProps) => { @@ -26,11 +27,12 @@ export const useFetchUsersList = ({ type }: IProps) => {
setLoadParams,
loadAll,
isLoadedAll,
resetFlatList,
} = useFlatList<IShortUser>({
fetchItems: api[_.defaultTo(type, 'executors')],
needInit: true,
serrializatorItems: (_items: ITaskExecutorResponse[]) =>
transformExecutorsToShortUsers(_items),
transformExecutorsToShortUsers(_items, execludeUserId),
loadParams: {
sort: 'ASC',
sortField: 'lastName',
@ -52,5 +54,6 @@ export const useFetchUsersList = ({ type }: IProps) => { @@ -52,5 +54,6 @@ export const useFetchUsersList = ({ type }: IProps) => {
setSearchVal,
loadAll,
isLoadedAll,
resetFlatList,
}
}

2
src/modules/users/screens/select-user-list.screen.tsx

@ -58,6 +58,7 @@ export const SelectUserList: FC<IProps> = ({ @@ -58,6 +58,7 @@ export const SelectUserList: FC<IProps> = ({
if (isLoading) return <ActivityIndicator color={theme.$loaderPrimary} />
// eslint-disable-next-line react-hooks/rules-of-hooks
const renderSelectedUserRow = useMemo(() => {
return (
<SelectedUsersRow
@ -86,6 +87,7 @@ export const SelectUserList: FC<IProps> = ({ @@ -86,6 +87,7 @@ export const SelectUserList: FC<IProps> = ({
paddingTop: $size(10, 10),
},
}}
// eslint-disable-next-line react/no-unstable-nested-components
footer={() => (
<FooterWithBtn btnTitle={footerBtnTitle} onPress={onSubmit} />
)}

1
src/shared/enums/route-key.enum.ts

@ -30,6 +30,7 @@ export enum RouteKey { @@ -30,6 +30,7 @@ export enum RouteKey {
Group = 'Group',
CreateGroup = 'CreateGroup',
CreatePersonal = 'CreatePersonal',
GroupChatDetail = 'GroupChatDetail',
ForwardMessage = 'ForwardMessage',

22
src/shared/helpers/configs.helpers.ts

@ -1,16 +1,16 @@ @@ -1,16 +1,16 @@
import _ from "lodash";
import { IFilesConfig } from "../interfaces";
import _ from 'lodash'
import { IFilesConfig } from '../interfaces'
export const transformFilesLimitsConfig = (
data: Partial<Record<keyof IFilesConfig, string>>
data: Partial<Record<keyof IFilesConfig, string>>,
) => {
const config: Partial<IFilesConfig> = {};
const config: Partial<IFilesConfig> = {}
if (_.isEmpty(data)) return config;
if (_.isEmpty(data)) return config
Object.keys(data).map(it => {
if (!_.isNaN(Number(data[it]))) config[it] = Number(data[it]);
else config[it] = data[it].toLowerCase();
});
return config;
};
Object.keys(data).map(it => {
if (!_.isNaN(Number(data[it]))) config[it] = Number(data[it])
else config[it] = data[it].toLowerCase()
})
return config
}

5
src/shared/helpers/date.helpers.ts

@ -9,7 +9,10 @@ export const checkIsSameDate = (val1: string | Date, val2: string | Date) => { @@ -9,7 +9,10 @@ export const checkIsSameDate = (val1: string | Date, val2: string | Date) => {
)
}
export const getDayOfYear = (date: Date | string = new Date(), daysInterval = 0): number => {
export const getDayOfYear = (
date: Date | string = new Date(),
daysInterval = 0,
): number => {
const currDate = new Date(date)
currDate.setDate(currDate.getDate() + daysInterval)
const startOfYear = new Date(currDate.getFullYear(), 0, 0)

7
src/shared/helpers/exceptions.helpers.ts

@ -6,8 +6,9 @@ const exceptionsDictionary = { @@ -6,8 +6,9 @@ const exceptionsDictionary = {
[ExceptionKeys.Forbidden]:
'Вашу IP-адресу заблоковано. Для розблокування зверніться до адміністратора',
[ExceptionKeys.PhoneNumberExist]: 'Вказаний робочий телефон вже зайнятий',
[ExceptionKeys.PersonalPhoneNumberExist]: 'Вказаний персональний телефон вже зайнятий',
[ExceptionKeys.EmailExist]: 'Вказана електронна пошта вже зайнята'
[ExceptionKeys.PersonalPhoneNumberExist]:
'Вказаний персональний телефон вже зайнятий',
[ExceptionKeys.EmailExist]: 'Вказана електронна пошта вже зайнята',
}
export const getMessageByExceptionKey = (key: ExceptionKeys) => {
@ -15,7 +16,7 @@ export const getMessageByExceptionKey = (key: ExceptionKeys) => { @@ -15,7 +16,7 @@ export const getMessageByExceptionKey = (key: ExceptionKeys) => {
}
const errorCodesDictionary = {
403: 'Відсутні повноваження на операцію'
403: 'Відсутні повноваження на операцію',
}
export const getMessageByErrorCode = (code: number) => {

8
src/shared/helpers/is-soon-birthday.helper.ts

@ -1,9 +1,11 @@ @@ -1,9 +1,11 @@
import moment from "moment"
import moment from 'moment'
export const isSoonBirthday = (dateOfBirthday: string): boolean => {
const birthWithCurrentYear = moment(dateOfBirthday).year(moment().year()).toDate()
const birthWithCurrentYear = moment(dateOfBirthday)
.year(moment().year())
.toDate()
const diffInDays = moment(birthWithCurrentYear).diff(new Date, 'days')
const diffInDays = moment(birthWithCurrentYear).diff(new Date(), 'days')
return diffInDays >= 0 && diffInDays < 3
}

16
src/shared/helpers/linking.helper.ts

@ -1,14 +1,12 @@ @@ -1,14 +1,12 @@
import { Alert, Linking } from "react-native";
import { Alert, Linking } from 'react-native'
export const callPhoneNumber = (phoneNumber: string) => Linking.openURL(`tel:${phoneNumber}`)
export const callPhoneNumber = (phoneNumber: string) =>
Linking.openURL(`tel:${phoneNumber}`)
export const sendEmail = (to: string) => Linking.openURL(`mailto:${to}`)
export const clearLink = (url: string) => {
if (url.includes('utm_source')) {
return `${url.split('utm_source')[0]}...`
} else return url
}
if (url.includes('utm_source')) {
return `${url.split('utm_source')[0]}...`
} else return url
}

6
src/shared/helpers/name.helpers.ts

@ -2,6 +2,10 @@ export const createFullName = (firstName: string, lastName: string) => { @@ -2,6 +2,10 @@ export const createFullName = (firstName: string, lastName: string) => {
return [firstName, lastName].filter(it => it).join(' ')
}
export const createContactName = (firstName?: string, middleName?: string, lastName?: string) => {
export const createContactName = (
firstName?: string,
middleName?: string,
lastName?: string,
) => {
return [lastName, firstName, middleName].filter(it => it).join(' ')
}

10
src/shared/helpers/theme-helpers.ts

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
import { ThemeContext } from '@/shared/themes';
import { useContext } from 'react';
import { ThemeContext } from '@/shared/themes'
import { useContext } from 'react'
export const getCurrentThemeType = () => {
const { themeTitle } = useContext(ThemeContext)
const { themeTitle } = useContext(ThemeContext)
return themeTitle
}
return themeTitle
}

25
src/shared/helpers/title-by-count.helper.ts

@ -1,8 +1,17 @@ @@ -1,8 +1,17 @@
export const getTitleByCount = (count: number, [form1, form2, form3]: string[]) => {
const tens = Math.abs(count) % 100
const units = tens % 10
if (tens > 10 && tens < 20) { return `${count} ${form3}` }
if (units > 1 && units < 5) { return `${count} ${form2}` }
if (units == 1) { return `${count} ${form1}` }
return `${count} ${form3}`
}
export const getTitleByCount = (
count: number,
[form1, form2, form3]: string[],
) => {
const tens = Math.abs(count) % 100
const units = tens % 10
if (tens > 10 && tens < 20) {
return `${count} ${form3}`
}
if (units > 1 && units < 5) {
return `${count} ${form2}`
}
if (units == 1) {
return `${count} ${form1}`
}
return `${count} ${form3}`
}

23
src/shared/helpers/touch-event.helpers.ts

@ -1,15 +1,20 @@ @@ -1,15 +1,20 @@
import { NativeTouchEvent } from "react-native";
import { NativeTouchEvent } from 'react-native'
const maxDeviationX = 60
const minOffsetY = 140
export const getSwipeDirection = ({ locationX, locationY }: NativeTouchEvent, touchStart: { locationX: number, locationY: number }): 'up' | 'down' | 'none' => {
const offsetX = Math.abs(touchStart.locationX - locationX)
const offsetY = touchStart.locationY - locationY
const absOffsetY = Math.abs(offsetY)
export const getSwipeDirection = (
{ locationX, locationY }: NativeTouchEvent,
touchStart: { locationX: number; locationY: number },
): 'up' | 'down' | 'none' => {
const offsetX = Math.abs(touchStart.locationX - locationX)
const offsetY = touchStart.locationY - locationY
const absOffsetY = Math.abs(offsetY)
if (offsetX < maxDeviationX && offsetY < 0 && absOffsetY >= minOffsetY) return 'up'
if (offsetX < maxDeviationX && offsetY > 0 && absOffsetY >= minOffsetY) return 'down'
if (offsetX < maxDeviationX && offsetY < 0 && absOffsetY >= minOffsetY)
return 'up'
if (offsetX < maxDeviationX && offsetY > 0 && absOffsetY >= minOffsetY)
return 'down'
return 'none'
}
return 'none'
}

4
src/shared/helpers/url.helpers.ts

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
export const isImgUrl = (url: string) => {
return url.toLowerCase().match(/\.(jpeg|jpg|gif|png|heic)$/) != null;
return url.toLowerCase().match(/\.(jpeg|jpg|gif|png|heic)$/) != null
}
export const isVideo = (url: string) =>
/\.(amv|mp4|m4p|m4v|mpg|mpeg|avi)$/i.test(url)
/\.(amv|mp4|m4p|m4v|mpg|mpeg|avi)$/i.test(url)

23
src/shared/helpers/versions.helper.ts

@ -1,15 +1,16 @@ @@ -1,15 +1,16 @@
import _ from "lodash"
import _ from 'lodash'
export const compare = (basis: Record<string, string>, compared: Record<string, string>) => {
const diffIds = []
export const compare = (
basis: Record<string, string>,
compared: Record<string, string>,
) => {
const diffIds = []
if (!compared || _.isEmpty(compared))
return Object.keys(basis)
if (!compared || _.isEmpty(compared)) return Object.keys(basis)
Object.keys(basis).map(it => {
if (basis[it] !== compared[it])
diffIds.push(it)
})
Object.keys(basis).map(it => {
if (basis[it] !== compared[it]) diffIds.push(it)
})
return diffIds
}
return diffIds
}

Loading…
Cancel
Save