Browse Source

selected-messages-actions (#28)

BANK-1145: Масові дії з повідомленнями в додатку
Reviewed-on: #28
Co-authored-by: Oksana Stepanenko <oksana.stepanenko@jetup.team>
Co-committed-by: Oksana Stepanenko <oksana.stepanenko@jetup.team>
fix/conversation
Oksana Stepanenko 9 months ago committed by Vitalik Yatsenko
parent
commit
040fbff260
  1. 14995
      package-lock.json
  2. 4
      package.json
  3. 29
      src/App.tsx
  4. 33
      src/config/toast.config.tsx
  5. 76
      src/modules/chats/atoms/chat-info-row.atom.tsx
  6. 4
      src/modules/chats/atoms/index.ts
  7. 91
      src/modules/chats/atoms/selected-messages-info-row.atom.tsx
  8. 116
      src/modules/chats/components/chat-header.component.tsx
  9. 6
      src/modules/chats/configs/chat-message-menu-options.config.ts
  10. 3
      src/modules/chats/configs/index.ts
  11. 59
      src/modules/chats/configs/selected-messages-menu-options.config.ts
  12. 6
      src/modules/chats/consts/index.ts
  13. 3
      src/modules/chats/enums/chat-message-action.enum.ts
  14. 4
      src/modules/chats/enums/chat-view-mode.enum.ts
  15. 3
      src/modules/chats/enums/index.ts
  16. 38
      src/modules/chats/helpers/get-copied-messages-content.helper.ts
  17. 1
      src/modules/chats/helpers/index.ts
  18. 2
      src/modules/chats/hooks/index.ts
  19. 22
      src/modules/chats/hooks/use-chat-messages.hook.ts
  20. 14
      src/modules/chats/hooks/use-chat-view-mode-state.hook.ts
  21. 211
      src/modules/chats/hooks/use-selected-messages.hook.ts
  22. 60
      src/modules/chats/layouts/chat-layout.layout.tsx
  23. 31
      src/modules/chats/screens/chat.tsx
  24. 19
      src/modules/chats/screens/forward-message.screen.tsx
  25. 3
      src/modules/chats/transforms/chat-messages.transforms.ts
  26. 30
      src/services/domain/chat-messages.service.ts
  27. 18
      src/shared/components/plugins/chat/chat-item-audio.component.tsx
  28. 14
      src/shared/components/plugins/chat/chat-item-file.component.tsx
  29. 23
      src/shared/components/plugins/chat/chat-item-image.component.tsx
  30. 70
      src/shared/components/plugins/chat/chat-item-video.component.tsx
  31. 69
      src/shared/components/plugins/chat/chat-item.component.tsx
  32. 32
      src/shared/components/plugins/chat/chat-messages.component.tsx
  33. 1
      src/shared/components/plugins/chat/interfaces.ts

14995
package-lock.json generated

File diff suppressed because it is too large Load Diff

4
package.json

@ -92,6 +92,7 @@ @@ -92,6 +92,7 @@
"react-native-svg": "^13.10.0",
"react-native-swiper": "^1.6.0",
"react-native-tab-view": "^3.5.2",
"react-native-toast-message": "^2.2.0",
"react-native-vector-icons": "^10.0.0",
"react-native-video": "^5.2.1",
"react-native-video-controls": "^2.8.1",
@ -104,7 +105,8 @@ @@ -104,7 +105,8 @@
"rn-fetch-blob": "^0.12.0",
"socket.io-client": "^4.5.0",
"validate.js": "^0.13.1",
"world-countries": "^5.0.0"
"world-countries": "^5.0.0",
"zustand": "^4.5.2"
},
"devDependencies": {
"@babel/core": "^7.20.0",

29
src/App.tsx

@ -2,7 +2,7 @@ import React, { FC, useEffect } from 'react' @@ -2,7 +2,7 @@ import React, { FC, useEffect } from 'react'
import { Provider } from 'react-redux'
import { Navigation } from './modules/root'
import store from './store'
// import './services/system/reactron.service'
import './services/system/reactron.service'
import { ThemeProvider } from './shared/themes'
// import Orientation from 'react-native-orientation-locker'
import { AppState, LogBox } from 'react-native'
@ -13,6 +13,8 @@ import { BottomSheetModalProvider } from '@gorhom/bottom-sheet' @@ -13,6 +13,8 @@ import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { appEvents } from './shared'
import Toast from 'react-native-toast-message'
import { toastConfig } from './config/toast.config'
LogBox.ignoreLogs(['Warning: ...', 'Require cycle: ...']) // Ignore log notification by message
LogBox.ignoreAllLogs() //Ignore all log notifications
@ -41,17 +43,20 @@ const App: FC = () => { @@ -41,17 +43,20 @@ const App: FC = () => {
}, [])
return (
<SafeAreaProvider>
<ThemeProvider key="theme-provider">
<BottomSheetModalProvider key="bottom-sheet">
<GestureHandlerRootView style={{ flex: 1 }}>
<Provider store={store} key="provider">
<Navigation />
</Provider>
</GestureHandlerRootView>
</BottomSheetModalProvider>
</ThemeProvider>
</SafeAreaProvider>
<>
<SafeAreaProvider>
<ThemeProvider key="theme-provider">
<BottomSheetModalProvider key="bottom-sheet">
<GestureHandlerRootView style={{ flex: 1 }}>
<Provider store={store} key="provider">
<Navigation />
</Provider>
</GestureHandlerRootView>
</BottomSheetModalProvider>
</ThemeProvider>
</SafeAreaProvider>
<Toast config={toastConfig} />
</>
)
}

33
src/config/toast.config.tsx

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
import React from 'react'
import { $size, Txt } from '@/shared'
import { StyleSheet, View } from 'react-native'
import { ToastConfig, ToastConfigParams } from 'react-native-toast-message'
interface IToastMessages {
text1: string
text2: string
}
export const toastConfig: ToastConfig = {
rwsToast: ({ text1, text2 }: ToastConfigParams<IToastMessages>) => (
<View style={styles.container}>
<Txt>{text1}</Txt>
{text2 ? <Txt>{text2}</Txt> : null}
</View>
),
}
const styles = StyleSheet.create({
container: {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: $size(12),
paddingVertical: $size(6),
marginTop: 100,
height: $size(30, 26),
borderRadius: 4,
backgroundColor: '#ffffff',
},
})

76
src/modules/chats/atoms/chat-info-row.atom.tsx

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
import React, { FC } from 'react'
import { $size, ChatType, useTheme } from '@/shared'
import { ActivityIndicator, Platform, StyleSheet, View } from 'react-native'
import {
ChatHeaderLabel,
ChatHeaderGoBackBtn,
ChatHeaderRightBtn,
} from '../atoms'
import { PartialTheme } from '@/shared/themes/interfaces'
interface IProps {
title: {
previewUrl: string
title: string
label: string
}
isLoading: boolean
chatType: ChatType
goBack: () => void
onPressRightBtn: () => void
onPressLeftBtn?: () => void
}
export const ChatInfoRowAtom: FC<IProps> = ({
title,
chatType,
isLoading,
goBack,
onPressRightBtn,
onPressLeftBtn,
}) => {
const { styles, theme } = useTheme(createStyles)
return (
<View style={styles.container}>
{isLoading ? (
<ActivityIndicator color={theme.$loaderPrimary} />
) : (
<>
<View style={styles.leftWrap}>
<ChatHeaderGoBackBtn goBack={goBack} />
<ChatHeaderLabel
imageUrl={title.previewUrl}
chatName={title.title}
info={title.label}
/>
</View>
<ChatHeaderRightBtn
chatType={chatType}
onPressRight={onPressRightBtn}
onPressLeft={onPressLeftBtn}
/>
</>
)}
</View>
)
}
const createStyles = (theme: PartialTheme) =>
StyleSheet.create({
container: {
paddingHorizontal: $size(16, 12),
marginTop: Platform.select({ android: $size(10) }),
flexDirection: 'row',
justifyContent: 'space-between',
paddingBottom: $size(10),
borderBottomWidth: 0.5,
borderBottomColor: theme?.$border,
},
leftWrap: {
alignItems: 'center',
flexDirection: 'row',
},
})

4
src/modules/chats/atoms/index.ts

@ -10,4 +10,6 @@ export * from './chat-header-label.atom' @@ -10,4 +10,6 @@ export * from './chat-header-label.atom'
export * from './chat-header-go-back-btn.atom'
export * from './chat-header-right-btn.atom'
export * from './add-group-photo-btn'
export * from './attachments-menu.atom'
export * from './attachments-menu.atom'
export * from './chat-info-row.atom'
export * from './selected-messages-info-row.atom'

91
src/modules/chats/atoms/selected-messages-info-row.atom.tsx

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
import React, { FC, useMemo } from 'react'
import { $size, IconComponent, Txt, useTheme } from '@/shared'
import { Platform, StyleSheet, TouchableOpacity, View } from 'react-native'
import { PartialTheme } from '@/shared/themes/interfaces'
import { getTitleByCount } from '@/shared/helpers'
interface IProps {
selectedCount: number
onPressCancel: () => void
onPressActions: () => void
}
export const ChatSelectedMessagesInfoRowAtom: FC<IProps> = ({
selectedCount,
onPressCancel,
onPressActions,
}) => {
const { styles, theme } = useTheme(createStyles)
const infoText = useMemo(() => {
if (selectedCount >= 1)
return `Вибрано ${getTitleByCount(selectedCount, [
'повідомлення',
'повідомлення',
'повідомлень',
])}`
return 'Виберіть повідомлення'
}, [selectedCount])
return (
<View style={styles.container}>
<TouchableOpacity onPress={onPressCancel} style={{ flexGrow: 1 }}>
<Txt style={styles.btnText}>Скасувати</Txt>
</TouchableOpacity>
<View style={{ flexGrow: 2 }}>
<Txt style={styles.infoText}>{infoText}</Txt>
</View>
<TouchableOpacity
onPress={onPressActions}
style={styles.actionsBtnBlock}>
{selectedCount > 0 && (
<>
<Txt style={[styles.btnText, { textAlign: 'right' }]}>
Дії
</Txt>
<IconComponent
color={theme.$errorText}
name="3-dots"
size={$size(20, 18)}
/>
</>
)}
</TouchableOpacity>
</View>
)
}
const createStyles = (theme: PartialTheme) =>
StyleSheet.create({
container: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: Platform.select({ android: $size(10) }),
paddingHorizontal: $size(16, 12),
paddingVertical: $size(15),
borderBottomWidth: 0.5,
borderBottomColor: theme?.$border,
},
btn: {},
btnText: {
fontSize: $size(14, 12),
color: theme.$errorText,
},
infoText: {
fontSize: $size(13, 11),
color: theme.$textPrimary,
fontWeight: '500',
textAlign: 'center',
},
actionsBtnBlock: {
flexGrow: 1,
minWidth: $size(32),
minHeight: $size(20),
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
},
})

116
src/modules/chats/components/chat-header.component.tsx

@ -1,76 +1,68 @@ @@ -1,76 +1,68 @@
import React, { FC } from 'react'
import { $size, ChatType, useTheme } from '@/shared'
import { ActivityIndicator, Platform, StyleSheet, View } from 'react-native'
import {
ChatHeaderLabel,
ChatHeaderGoBackBtn,
ChatHeaderRightBtn,
} from '../atoms'
import { PartialTheme } from '@/shared/themes/interfaces'
import { RouteKey, useNav } from '@/shared'
import { Alert } from 'react-native'
import { ChatInfoRowAtom, ChatSelectedMessagesInfoRowAtom } from '../atoms'
import { IHeaderChatInfo } from '../interfaces'
import { simpleDispatch } from '@/store/store-helpers'
import { UnselectChat } from '@/store/chats'
import { ChatViewModeEnum } from '../enums'
import { useChatSelectedMessagesState, useChatViewModeState } from '../hooks'
interface IProps {
title: {
previewUrl: string
title: string
label: string
}
chatInfo: IHeaderChatInfo
isLoading: boolean
chatType: ChatType
goBack: () => void
onPressRightBtn: () => void
onPressLeftBtn?: () => void
onPressActions: () => void
}
export const ChatHeader: FC<IProps> = ({
title,
chatType,
chatInfo,
isLoading,
goBack,
onPressRightBtn,
onPressLeftBtn,
onPressActions,
}) => {
const { styles, theme } = useTheme(createStyles)
const nav = useNav()
return (
<View style={styles.container}>
{isLoading ? (
<ActivityIndicator color={theme.$loaderPrimary} />
) : (
<>
<View style={styles.leftWrap}>
<ChatHeaderGoBackBtn goBack={goBack} />
const chatViewMode = useChatViewModeState(s => s.mode)
const { setMode } = useChatViewModeState()
const selectedMessages = useChatSelectedMessagesState(s => s.messages)
const { unselectAll: unselectAllMessages } = useChatSelectedMessagesState()
<ChatHeaderLabel
imageUrl={title.previewUrl}
chatName={title.title}
info={title.label}
/>
</View>
const cancelSelectMode = () => {
unselectAllMessages()
setMode(ChatViewModeEnum.DEFAULT)
}
const goBack = () => {
nav.goBack()
simpleDispatch(new UnselectChat())
}
<ChatHeaderRightBtn
chatType={chatType}
onPressRight={onPressRightBtn}
onPressLeft={onPressLeftBtn}
/>
</>
)}
</View>
const goToGroupDetails = () => nav.navigate(RouteKey.GroupChatDetail)
const makeCall = () =>
Alert.alert('Not implemented', 'Will be implemented in next version')
if (chatViewMode === ChatViewModeEnum.SELECT)
return (
<ChatSelectedMessagesInfoRowAtom
selectedCount={selectedMessages.length}
onPressCancel={cancelSelectMode}
onPressActions={onPressActions}
/>
)
return (
<ChatInfoRowAtom
title={{
previewUrl: chatInfo.previewUrl,
title: chatInfo.name,
label: chatInfo.label,
}}
isLoading={isLoading}
chatType={chatInfo.type}
goBack={goBack}
onPressRightBtn={goToGroupDetails}
onPressLeftBtn={makeCall}
/>
)
}
const createStyles = (theme: PartialTheme) =>
StyleSheet.create({
container: {
paddingHorizontal: $size(16, 12),
marginTop: Platform.select({ android: $size(10) }),
flexDirection: 'row',
justifyContent: 'space-between',
paddingBottom: $size(10),
borderBottomWidth: 0.5,
borderBottomColor: theme?.$border,
},
leftWrap: {
alignItems: 'center',
flexDirection: 'row',
},
})

6
src/modules/chats/configs/chat-message-menu-options.config.ts

@ -68,12 +68,17 @@ export const getChatMessageMenuOptions = ({ @@ -68,12 +68,17 @@ export const getChatMessageMenuOptions = ({
name: 'Скасувати надсилання',
onPress: () => onPress(ChatMessageActionEnum.CANCEL),
},
select: {
name: 'Вибрати',
onPress: () => onPress(ChatMessageActionEnum.SELECT),
},
}
if (isOfflineMessage) {
const offlineOptions = []
if (canCopy) offlineOptions.push(menuOptions.copy)
offlineOptions.push(menuOptions.cancel)
offlineOptions.push(menuOptions.select)
return offlineOptions
}
@ -86,6 +91,7 @@ export const getChatMessageMenuOptions = ({ @@ -86,6 +91,7 @@ export const getChatMessageMenuOptions = ({
optionsKeys.push('delete')
if (canDeleteForAll) optionsKeys.push('deleteForAll')
if (canEdit) optionsKeys.push('edit')
optionsKeys.push('select')
const options = optionsKeys.map(key => menuOptions[key])

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

@ -3,4 +3,5 @@ export * from './data-message' @@ -3,4 +3,5 @@ export * from './data-message'
export * from './member-card-btns.config'
export * from './chat-details-footer-btns.config'
export * from './attachments-menu.config'
export * from './chat-message-menu-options.config'
export * from './chat-message-menu-options.config'
export * from './selected-messages-menu-options.config'

59
src/modules/chats/configs/selected-messages-menu-options.config.ts

@ -0,0 +1,59 @@ @@ -0,0 +1,59 @@
import { ChatMessageActionEnum } from '../enums'
interface IProps {
onPress: (actionType: ChatMessageActionEnum) => void
canShare?: boolean
canCopy?: boolean
canDeleteForAll?: boolean
hasOfflineMessages?: boolean
}
export const getSelectedMessagesMenuOptions = ({
onPress,
canShare,
canCopy,
canDeleteForAll,
hasOfflineMessages,
}: IProps) => {
const menuOptions = {
forward: {
name: 'Переслати',
onPress: () => onPress(ChatMessageActionEnum.FORWARD),
},
share: {
name: 'Переслати назовні',
onPress: () => onPress(ChatMessageActionEnum.SHARE),
},
copy: {
name: 'Копіювати',
onPress: () => onPress(ChatMessageActionEnum.COPY),
},
delete: {
name: 'Видалити',
onPress: () => onPress(ChatMessageActionEnum.DELETE),
},
deleteForAll: {
name: 'Видалити для всіх',
onPress: () => onPress(ChatMessageActionEnum.DELETE_FOR_ALL),
},
noAction: {
name: 'Доступних дій нема',
onPress: () => {},
},
}
if (hasOfflineMessages) {
if (canCopy) return [menuOptions.copy]
return [menuOptions.noAction]
}
const optionsKeys = ['forward']
if (canShare) optionsKeys.push('share')
if (canCopy) optionsKeys.push('copy')
optionsKeys.push('delete')
if (canDeleteForAll) optionsKeys.push('deleteForAll')
const options = optionsKeys.map(key => menuOptions[key])
return options
}

6
src/modules/chats/consts/index.ts

@ -1,2 +1,6 @@ @@ -1,2 +1,6 @@
import { MessageType } from '@/shared'
export * from './chat-header.consts'
export * from './chat-detail.consts'
export * from './chat-detail.consts'
export const COPY_ENABLED_MESSAGES_TYPES = [MessageType.Text, MessageType.Image]

3
src/modules/chats/enums/chat-message-action.enum.ts

@ -9,5 +9,6 @@ export enum ChatMessageActionEnum { @@ -9,5 +9,6 @@ export enum ChatMessageActionEnum {
DELETE_FOR_ALL,
DOWNLOAD,
EDIT,
CANCEL
CANCEL,
SELECT,
}

4
src/modules/chats/enums/chat-view-mode.enum.ts

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
export enum ChatViewModeEnum {
DEFAULT,
SELECT,
}

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

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
export * from './member-card-action.enum'
export * from './chat-details-footer-action.enum'
export * from './chat-card-action.enum'
export * from './chat-message-action.enum'
export * from './chat-message-action.enum'
export * from './chat-view-mode.enum'

38
src/modules/chats/helpers/get-copied-messages-content.helper.ts

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
import { MessageType } from '@/shared'
import { IChatMessage } from '@/shared/components/plugins/chat'
import _ from 'lodash'
import moment from 'moment'
export const getCopiedContent = (items: IChatMessage[]) => {
if (items.length === 1 && items[0].type === MessageType.Image) {
return items[0].content.fileUrl
} else if (_.every(items, it => it.type === MessageType.Text)) {
return createCopiedMessagesContent(items)
}
}
function createCopiedMessagesContent(items: IChatMessage[]) {
const text = []
const sortedItems = _.orderBy(items, ['createdAt'], ['asc'])
for (const item of sortedItems) {
const formattedContent = getFormattedMessageContent(item)
text.push(formattedContent)
}
return text.join('\n')
}
function getFormattedMessageContent(item: IChatMessage) {
const dateAndTime = getDateAndTime(item)
const authorName = _.defaultTo(item.author?.name, '')
const text = _.defaultTo(item.content?.message, '')
return `[${dateAndTime}] ${authorName}: ${text}`
}
function getDateAndTime(item: IChatMessage) {
if (!item.createdAt) return ''
return moment(item.createdAt).format('DD.MM.YY, HH:mm')
}

1
src/modules/chats/helpers/index.ts

@ -3,3 +3,4 @@ export * from './chats-helpers.helper' @@ -3,3 +3,4 @@ export * from './chats-helpers.helper'
export * from './get-chat-info.helper'
export * from './get-header-chat-info.helper'
export * from './get-time-from-message-send.helper'
export * from './get-copied-messages-content.helper'

2
src/modules/chats/hooks/index.ts

@ -9,3 +9,5 @@ export * from './use-pined-messages.hook' @@ -9,3 +9,5 @@ export * from './use-pined-messages.hook'
export * from './use-selected-chats.hook'
export * from './use-send-files.hook'
export * from './use-send-sticker.hook'
export * from './use-selected-messages.hook'
export * from './use-chat-view-mode-state.hook'

22
src/modules/chats/hooks/use-chat-messages.hook.ts

@ -24,17 +24,19 @@ import { Platform } from 'react-native' @@ -24,17 +24,19 @@ import { Platform } from 'react-native'
import { useCallback, useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { getChatMessageMenuOptions } from '../configs'
import { ChatMessageActionEnum } from '../enums'
import { ChatMessageActionEnum, ChatViewModeEnum } from '../enums'
import Clipboard from '@react-native-community/clipboard'
import store from '@/store'
import { chatMessageManager } from '@/managers'
import { isMessageEqualURL } from '@/shared/helpers'
import { COPY_ENABLED_MESSAGES_TYPES } from '../consts'
export const useChatMessages = (
chatId: number | string,
firstMessageId: number | string,
lastMessageId: number | string,
chatMembers: IChatMember[],
setChatViewMode: (mode: ChatViewModeEnum) => void,
) => {
const accountId = useSelector(selectId)
const socket = useSocket()
@ -269,6 +271,8 @@ export const useChatMessages = ( @@ -269,6 +271,8 @@ export const useChatMessages = (
[],
)
const onSelect = () => setChatViewMode(ChatViewModeEnum.SELECT)
const actions = {
[ChatMessageActionEnum.COPY]: onCopy,
[ChatMessageActionEnum.DELETE]: onDelete,
@ -281,6 +285,7 @@ export const useChatMessages = ( @@ -281,6 +285,7 @@ export const useChatMessages = (
[ChatMessageActionEnum.DOWNLOAD]: onDownload,
[ChatMessageActionEnum.EDIT]: onEdit,
[ChatMessageActionEnum.CANCEL]: onCancel,
[ChatMessageActionEnum.SELECT]: onSelect,
}
const onLongPress = (
@ -288,11 +293,9 @@ export const useChatMessages = ( @@ -288,11 +293,9 @@ export const useChatMessages = (
role: ChatMemberRole,
isConnected: boolean,
) => {
const copyEnablesTypes = [MessageType.Text, MessageType.Image]
const canDeleteForAll = checkCanDeleteForAll(message, role)
const canCopy = isConnected
? copyEnablesTypes.includes(message.type)
? COPY_ENABLED_MESSAGES_TYPES.includes(message.type)
: message.type === MessageType.Text
const canPin = !message.isPined
const canUnpin = message.isPined
@ -436,8 +439,15 @@ export const useChatMessages = ( @@ -436,8 +439,15 @@ export const useChatMessages = (
offlineKey?: string | string[]
}) => {
if (_.isArray(data.message))
onNewMessages(data.message, data.offlineKey as string[])
else onNewOneMessage(data.message, data.offlineKey as string)
onNewMessages(
data.message as IChatMessage[],
data.offlineKey as string[],
)
else
onNewOneMessage(
data.message as IChatMessage,
data.offlineKey as string,
)
}
const onMessageDeleted = (

14
src/modules/chats/hooks/use-chat-view-mode-state.hook.ts

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
import { create } from 'zustand'
import { ChatViewModeEnum } from '../enums'
interface IChatViewModeState {
mode: ChatViewModeEnum
setMode: (mode: ChatViewModeEnum) => void
}
export const useChatViewModeState = create<IChatViewModeState>()(set => ({
mode: ChatViewModeEnum.DEFAULT,
setMode: (mode: ChatViewModeEnum) => {
set({ mode })
},
}))

211
src/modules/chats/hooks/use-selected-messages.hook.ts

@ -0,0 +1,211 @@ @@ -0,0 +1,211 @@
import _ from 'lodash'
import { IChatMessage } from '@/shared/components/plugins/chat'
import { create } from 'zustand'
import {
ChatMemberRole,
MessageType,
RouteKey,
appEvents,
useNav,
} from '@/shared'
import { COPY_ENABLED_MESSAGES_TYPES } from '../consts'
import { ChatMessageActionEnum, ChatViewModeEnum } from '../enums'
import { getSelectedMessagesMenuOptions } from '../configs'
import { isMessageEqualURL } from '@/shared/helpers'
import { fsService } from '@/services/system'
import { useChatViewModeState } from './use-chat-view-mode-state.hook'
import { chatMessagesService } from '@/services/domain'
import Clipboard from '@react-native-community/clipboard'
import { getCopiedContent } from '../helpers'
import Toast from 'react-native-toast-message'
interface IChatSelectedMessagesState {
messages: IChatMessage[]
selectMessage: (message: IChatMessage) => void
unselectAll: () => void
}
export const useChatSelectedMessagesState =
create<IChatSelectedMessagesState>()(set => ({
messages: [],
selectMessage(message: IChatMessage) {
set(state => {
if (_.find(state.messages, it => it.id === message.id))
return {
messages: state.messages.filter(
it => it.id !== message.id,
),
}
else
return {
messages: [
...state.messages,
_.pick(message, [
'id',
'type',
'content',
'authorId',
'chatId',
'read',
'isMy',
'author',
'isPined',
'createdAt',
]),
],
}
})
},
unselectAll() {
set({ messages: [] })
},
}))
export const useChatSelectedMessages = () => {
const nav = useNav()
const messages = useChatSelectedMessagesState(s => s.messages)
const { unselectAll } = useChatSelectedMessagesState()
const { setMode } = useChatViewModeState()
const actions = {
[ChatMessageActionEnum.FORWARD]: forwardMany,
[ChatMessageActionEnum.SHARE]: shareMessage,
[ChatMessageActionEnum.COPY]: copyMany,
[ChatMessageActionEnum.DELETE]: deleteMany,
[ChatMessageActionEnum.DELETE_FOR_ALL]: deleteForAllMany,
}
const afterAction = () => {
unselectAll()
setMode(ChatViewModeEnum.DEFAULT)
}
function forwardMany() {
nav.navigate(RouteKey.ForwardMessage, {
messageId: messages.map(it => it.id),
})
afterAction()
}
function shareMessage() {
const { type, author, content } = messages[0]
const title = author?.name
if (type === MessageType.Text) {
if (!isMessageEqualURL(content?.message)) {
fsService.shareText(title, content?.message)
} else fsService.shareUrl(title, content?.message)
}
if (
type === MessageType.Image ||
type === MessageType.Video ||
type === MessageType.Audio ||
type === MessageType.File
) {
fsService.shareFileOutside(content?.fileUrl, content?.name)
}
afterAction()
}
function copyMany() {
const copiedContent = getCopiedContent(messages)
Clipboard.setString(copiedContent)
Toast.show({
type: 'rwsToast',
text1: 'Повідомлення скопійовано',
})
afterAction()
}
function deleteMany() {
deleteMessages()
}
function deleteForAllMany() {
deleteMessages(true)
}
function deleteMessages(deleteForAll?: boolean) {
setTimeout(
() =>
appEvents.emit('openConfirmModal', {
title: `Ви впевнені, що хочете \n видалити вибрані повідомлення${
deleteForAll ? ' для всіх' : ''
}?`,
buttonToHighlight: 'allow',
allowBtnAction: async () => {
await chatMessagesService.deleteChatMessage(
messages as IChatMessage[],
deleteForAll,
),
afterAction()
},
notAllowBtnAction: () => {},
}),
300,
)
}
const onPressActions = (
userRoleInChat: ChatMemberRole,
isConnected: boolean,
) => {
const canShare = isConnected && messages.length === 1
const canCopy = checkCanCopy()
const canDeleteForAll = checkCanDeleteForAll()
const hasOfflineMessages = _.some(messages, it =>
_.isNaN(Number(it.id)),
)
function checkCanCopy() {
if (!isConnected)
return _.every(messages, it => it.type === MessageType.Text)
if (
messages.length === 1 &&
COPY_ENABLED_MESSAGES_TYPES.includes(messages[0].type)
)
return true
if (
messages.length > 1 &&
_.every(messages, it => it.type === MessageType.Text)
)
return true
return false
}
function checkCanDeleteForAll() {
if (userRoleInChat === ChatMemberRole.Admin) return true
return _.every(messages, it => {
if (!it.read && it.isMy) return true
return false
})
}
const options = getSelectedMessagesMenuOptions({
canDeleteForAll,
canCopy,
canShare,
hasOfflineMessages,
onPress: (actionType: ChatMessageActionEnum) =>
actions[actionType](messages),
})
appEvents.emit('openActionSheet', {
items: options,
})
}
return {
onPressActions,
}
}

60
src/modules/chats/layouts/chat-layout.layout.tsx

@ -1,40 +1,17 @@ @@ -1,40 +1,17 @@
import { defaultChatBgConfig } from '@/config/default-chat-bg.config'
import { storageService } from '@/services/system/storage.service'
import {
ChatBgKeys,
IRouteParams,
RouteKey,
ScreenLayout,
StorageKey,
useTheme,
} from '@/shared'
import { ChatBgKeys, ScreenLayout, StorageKey, useTheme } from '@/shared'
import { selectCurrentChatBgId } from '@/store/chat-bg'
import { UnselectChat } from '@/store/chats'
import { simpleDispatch } from '@/store/store-helpers'
import React, { FC, useEffect, useState } from 'react'
import {
Alert,
ImageBackground,
ImageSourcePropType,
StyleSheet,
} from 'react-native'
import React, { FC, ReactElement, useEffect, useState } from 'react'
import { ImageBackground, ImageSourcePropType, StyleSheet } from 'react-native'
import { useSelector } from 'react-redux'
import { ChatHeader } from '../components'
import { IHeaderChatInfo } from '../interfaces'
interface IProps {
children: JSX.Element | JSX.Element[]
navigation: IRouteParams['navigation']
chatInfo: IHeaderChatInfo
isLoading: boolean
headerComponent: ReactElement
}
export const ChatLayout: FC<IProps> = ({
children,
navigation,
chatInfo,
isLoading,
}) => {
export const ChatLayout: FC<IProps> = ({ children, headerComponent }) => {
const { styles } = useTheme(createStyles)
const [chanBg, setBg] = useState<ImageSourcePropType>()
const currentBgImgId = useSelector(selectCurrentChatBgId)
@ -55,37 +32,12 @@ export const ChatLayout: FC<IProps> = ({ @@ -55,37 +32,12 @@ export const ChatLayout: FC<IProps> = ({
setBg(defBg)
}
const onPressRightBtn = () => navigation.navigate(RouteKey.GroupChatDetail)
const onPressLeftBtn = () =>
Alert.alert('Not implemented', 'Will be implemented in next version')
useEffect(() => {
preparedBg()
}, [currentBgImgId])
const goBack = () => {
navigation.goBack()
simpleDispatch(new UnselectChat())
}
return (
<ScreenLayout
horizontalPadding={0}
headerComponent={
<ChatHeader
title={{
previewUrl: chatInfo.previewUrl,
title: chatInfo.name,
label: chatInfo.label,
}}
isLoading={isLoading}
chatType={chatInfo.type}
goBack={goBack}
onPressRightBtn={onPressRightBtn}
onPressLeftBtn={onPressLeftBtn}
/>
}>
<ScreenLayout horizontalPadding={0} headerComponent={headerComponent}>
<ImageBackground
style={styles.bg}
source={chanBg}

31
src/modules/chats/screens/chat.tsx

@ -2,7 +2,6 @@ import { @@ -2,7 +2,6 @@ import {
ChatMemberRole,
ChatType,
createFullName,
IRouteParams,
MessageType,
RouteKey,
useNav,
@ -12,6 +11,8 @@ import { AttachmentsMenu } from '@/modules/chats/atoms' @@ -12,6 +11,8 @@ import { AttachmentsMenu } from '@/modules/chats/atoms'
import {
useChatDetails,
useChatMessages,
useChatSelectedMessages,
useChatViewModeState,
useCreateTextMessage,
usePinedMessages,
useSendFiles,
@ -32,14 +33,20 @@ import _ from 'lodash' @@ -32,14 +33,20 @@ import _ from 'lodash'
import { chatMessagesService } from '@/services/domain'
import { SelectStickerSmart } from '@/modules/media'
import { useNetInfo } from '@react-native-community/netinfo'
import { ChatViewModeEnum } from '../enums'
import { ChatHeader } from '../components'
interface IProps extends IRouteParams {}
export const ChatConversation = () => {
const nav = useNav()
export const ChatConversation = ({ navigation }: IProps) => {
const accountId = useSelector(selectId)
const selectedChatId = useSelector(selectSelectedChatId)
const [isMenuOpen, setMenuOpen] = useState(false)
const nav = useNav()
const mode = useChatViewModeState(s => s.mode)
const { setMode } = useChatViewModeState()
const { isConnected } = useNetInfo()
const { headerChatInfo, chatDetails, isLoading } =
@ -63,8 +70,11 @@ export const ChatConversation = ({ navigation }: IProps) => { @@ -63,8 +70,11 @@ export const ChatConversation = ({ navigation }: IProps) => {
chatDetails?.firstMessageId,
chatDetails?.lastMessageId,
chatDetails?.chatMembers,
setMode,
)
const { onPressActions } = useChatSelectedMessages()
const onSendMessage = () => {
if (replyTo) setReplyToMessage(null)
}
@ -159,10 +169,11 @@ export const ChatConversation = ({ navigation }: IProps) => { @@ -159,10 +169,11 @@ export const ChatConversation = ({ navigation }: IProps) => {
const onLongPressMessage = useCallback(
id => {
if (mode === ChatViewModeEnum.SELECT) return
const item = _.find(messages, message => message?.id === id)
onLongPress(item, role, isConnected)
},
[chatDetails, messages, isConnected],
[chatDetails, messages, isConnected, mode],
)
const onPressMessageAuthor = useCallback(
@ -197,9 +208,13 @@ export const ChatConversation = ({ navigation }: IProps) => { @@ -197,9 +208,13 @@ export const ChatConversation = ({ navigation }: IProps) => {
return (
<ChatLayout
navigation={navigation}
isLoading={isLoading}
chatInfo={headerChatInfo}>
headerComponent={
<ChatHeader
chatInfo={headerChatInfo}
isLoading={isLoading}
onPressActions={() => onPressActions(role, isConnected)}
/>
}>
<>
<View style={{ flex: 1 }}>
<PinedMessage

19
src/modules/chats/screens/forward-message.screen.tsx

@ -24,7 +24,7 @@ import { chatMessageManager } from '@/managers/chat-message.manager' @@ -24,7 +24,7 @@ import { chatMessageManager } from '@/managers/chat-message.manager'
interface IProps extends IRouteParams {
route: {
params: {
messageId: number
messageId: number | number[]
}
}
}
@ -63,11 +63,20 @@ export const ForwardMessageScreen: FC<IProps> = ({ @@ -63,11 +63,20 @@ export const ForwardMessageScreen: FC<IProps> = ({
const chatsIds = selectedChats.map(it => it.id)
if (_.isEmpty(chatsIds)) return
const messages = _.isArray(messageId)
? (messageId as number[])
: [messageId as number]
try {
await chatMessageManager.forwardMessage.bind(chatMessageManager)({
messageId,
chatsIds,
})
for await (const id of messages) {
await chatMessageManager.forwardMessage.bind(
chatMessageManager,
)({
messageId: id,
chatsIds,
})
}
navigation.goBack()
if (

3
src/modules/chats/transforms/chat-messages.transforms.ts

@ -174,12 +174,13 @@ export const transformMessage = ( @@ -174,12 +174,13 @@ export const transformMessage = (
return {
id: item.id,
chatId: item.chatId,
text: item.content?.message,
createdAt: item.createdAt,
authorId: item.userId,
author: {
id: author?.userId,
name: item.userId !== accountId && fullName,
name: fullName,
avatar:
item.userId !== accountId &&
hasImageUrl(author?.user?.avatarUrl, fullName),

30
src/services/domain/chat-messages.service.ts

@ -8,6 +8,7 @@ import { @@ -8,6 +8,7 @@ import {
ISendTextMessage,
} from '@/api/chat-messages/requests.interfaces'
import { chatMessageManager } from '@/managers/chat-message.manager'
import { IChatMessage as IChatMessageItem } from '@/shared/components/plugins/chat'
import { appEvents, IChatMessage } from '@/shared'
import {
runActionByType,
@ -18,6 +19,7 @@ import { SetUnreadMessagesCount } from '@/store/chats' @@ -18,6 +19,7 @@ import { SetUnreadMessagesCount } from '@/store/chats'
import { simpleDispatch } from '@/store/store-helpers'
import { Alert } from 'react-native'
import { converterService, fsService } from '../system'
import _ from 'lodash'
const sendTextMessage = async (data: ISendTextMessage) => {
try {
@ -163,26 +165,32 @@ const clearAllChatsMessages = async () => { @@ -163,26 +165,32 @@ const clearAllChatsMessages = async () => {
}
const deleteChatMessage = async (
message: IChatMessage,
message: IChatMessage | IChatMessageItem[],
deleteForAll?: boolean,
) => {
const params: IDeleteMessageParams = {}
if (deleteForAll) params.deleteForAll = deleteForAll
const messages = _.isArray(message)
? (message as IChatMessageItem[])
: [message as IChatMessage]
try {
await chatMessageManager.deleteMessage.bind(chatMessageManager)({
messageId: Number(message.id),
params,
})
for await (const item of messages) {
await chatMessageManager.deleteMessage.bind(chatMessageManager)({
messageId: Number(item.id),
params,
})
appEvents.emit('onDeleteMessage', {
chatId: Number(message.chatId),
messageId: Number(message.id),
})
appEvents.emit('onDeleteMessage', {
chatId: Number(item.chatId),
messageId: Number(item.id),
})
}
if (message.isPined)
if (_.some(messages, it => it.isPined))
appEvents.emit('reloadChatDetail', {
chatId: Number(message.chatId),
chatId: Number(messages[0].chatId),
})
} catch (err) {
console.log('ERROR ON DELETE MESSAGE', err)

18
src/shared/components/plugins/chat/chat-item-audio.component.tsx

@ -8,7 +8,12 @@ import { ChatItem } from './chat-item.component' @@ -8,7 +8,12 @@ import { ChatItem } from './chat-item.component'
import { IChatMessage } from './interfaces'
import { Slider } from '@miblanchard/react-native-slider'
import { mediaService } from '@/services/system'
import {
useChatSelectedMessagesState,
useChatViewModeState,
} from '@/modules/chats/hooks'
import { ChatViewModeEnum } from '@/modules/chats/enums'
import _ from 'lodash'
interface ChatItemAudioProps extends IChatMessage {
onLongPress?: () => void
onProfilePress?: () => void
@ -24,6 +29,11 @@ export const ChatItemAudio: FC<ChatItemAudioProps> = props => { @@ -24,6 +29,11 @@ export const ChatItemAudio: FC<ChatItemAudioProps> = props => {
const [isPlaying, setIsPlying] = useState<boolean>(false)
const [isPause, setIsPause] = useState<boolean>(false)
const viewMode = useChatViewModeState(s => s.mode)
const { selectMessage } = useChatSelectedMessagesState()
const select = () => selectMessage(props)
const {
styles,
theme: { chats },
@ -62,6 +72,8 @@ export const ChatItemAudio: FC<ChatItemAudioProps> = props => { @@ -62,6 +72,8 @@ export const ChatItemAudio: FC<ChatItemAudioProps> = props => {
}
const actionHandler = async () => {
if (viewMode === ChatViewModeEnum.SELECT) return select()
if (props.onPressPlay) props.onPressPlay()
if (props.activeAudioId !== props.id) await mediaService.onStopPlay()
if (isPlaying) return await onPause()
@ -82,6 +94,10 @@ export const ChatItemAudio: FC<ChatItemAudioProps> = props => { @@ -82,6 +94,10 @@ export const ChatItemAudio: FC<ChatItemAudioProps> = props => {
if (props.activeAudioId !== props.id && isPlaying) onPause()
}, [props.activeAudioId])
useEffect(() => {
if (viewMode === ChatViewModeEnum.SELECT && isPlaying) onPause()
}, [viewMode])
return (
<ChatItem
{...props}

14
src/shared/components/plugins/chat/chat-item-file.component.tsx

@ -16,6 +16,11 @@ import { ChatItem } from './chat-item.component' @@ -16,6 +16,11 @@ import { ChatItem } from './chat-item.component'
import { IChatMessage } from './interfaces'
import { appEvents } from '@/shared/events'
import { FileType } from '@/shared/enums'
import {
useChatViewModeState,
useChatSelectedMessagesState,
} from '@/modules/chats/hooks'
import { ChatViewModeEnum } from '@/modules/chats/enums'
interface ChatItemFileProps extends IChatMessage {
onLongPress?: () => void
@ -28,6 +33,11 @@ interface ChatItemFileProps extends IChatMessage { @@ -28,6 +33,11 @@ interface ChatItemFileProps extends IChatMessage {
export const ChatItemFile: FC<ChatItemFileProps> = props => {
const { styles, theme } = useTheme(createStyles)
const viewMode = useChatViewModeState(s => s.mode)
const { selectMessage } = useChatSelectedMessagesState()
const select = () => selectMessage(props)
const sizePart = useMemo(() => {
if (!props.content?.size) return ''
@ -38,12 +48,14 @@ export const ChatItemFile: FC<ChatItemFileProps> = props => { @@ -38,12 +48,14 @@ export const ChatItemFile: FC<ChatItemFileProps> = props => {
}, [props.content.size])
const previewFile = useCallback(() => {
if (viewMode === ChatViewModeEnum.SELECT) return select()
const content = props.content
appEvents.emit('previewFile', {
fileUrl: content?.fileUrl,
mimeType: getUrlExtension(content?.name) as FileType,
})
}, [props.content])
}, [props.content, viewMode])
const iconName = getIconNameByExtension(props?.content?.fileUrl)

23
src/shared/components/plugins/chat/chat-item-image.component.tsx

@ -6,6 +6,12 @@ import { StyleSheet, Dimensions, TouchableOpacity } from 'react-native' @@ -6,6 +6,12 @@ import { StyleSheet, Dimensions, TouchableOpacity } from 'react-native'
import { RemoteImage } from '../../elements'
import { ChatItem } from './chat-item.component'
import { IChatMessage } from './interfaces'
import {
useChatViewModeState,
useChatSelectedMessagesState,
} from '@/modules/chats/hooks'
import { _ } from 'jet-tools'
import { ChatViewModeEnum } from '@/modules/chats/enums'
interface ChatItemImageProps extends IChatMessage {
onLongPress?: () => void
@ -21,6 +27,11 @@ export const ChatItemImage: FC<ChatItemImageProps> = props => { @@ -21,6 +27,11 @@ export const ChatItemImage: FC<ChatItemImageProps> = props => {
const { styles } = useTheme(createStyles)
// const [height, setHeight] = useState(width)
const viewMode = useChatViewModeState(s => s.mode)
const { selectMessage } = useChatSelectedMessagesState()
const select = () => selectMessage(props)
return (
<ChatItem
{...props}
@ -31,11 +42,13 @@ export const ChatItemImage: FC<ChatItemImageProps> = props => { @@ -31,11 +42,13 @@ export const ChatItemImage: FC<ChatItemImageProps> = props => {
paddingHorizontal={8}>
<TouchableOpacity
style={{ alignSelf: 'center' }}
onPress={() =>
appEvents.emit('openFancybox', {
url: props?.content?.fileUrl,
})
}>
onPress={() => {
if (viewMode === ChatViewModeEnum.SELECT) select()
else
appEvents.emit('openFancybox', {
url: props?.content?.fileUrl,
})
}}>
<RemoteImage
url={props?.content?.fileUrl || null}
resizeMode="cover"

70
src/shared/components/plugins/chat/chat-item-video.component.tsx

@ -10,6 +10,13 @@ import { appEvents } from '@/shared/events' @@ -10,6 +10,13 @@ import { appEvents } from '@/shared/events'
import { ForwardedMessageHeader } from './forwarded-message-header.component'
import { RepliedMessageInfo } from './replied-message-info.component'
import { MessageType } from '@/shared/enums'
import { FormCircleCheckbox } from '@/shared'
import {
useChatViewModeState,
useChatSelectedMessagesState,
} from '@/modules/chats/hooks'
import { _ } from 'jet-tools'
import { ChatViewModeEnum } from '@/modules/chats/enums'
interface ChatItemVideoProps extends IChatMessage {
onLongPress?: () => void
onProfilePress?: () => void
@ -21,8 +28,18 @@ interface ChatItemVideoProps extends IChatMessage { @@ -21,8 +28,18 @@ interface ChatItemVideoProps extends IChatMessage {
export const ChatItemVideo: FC<ChatItemVideoProps> = props => {
const { styles, theme } = useTheme(createStyles)
const viewMode = useChatViewModeState(s => s.mode)
const selectedMessages = useChatSelectedMessagesState(s => s.messages)
const { selectMessage } = useChatSelectedMessagesState()
const select = () => selectMessage(props)
const openVideo = () => {
appEvents.emit('openVideoFullscreen', { url: props.content?.fileUrl })
if (viewMode === ChatViewModeEnum.SELECT) select()
else
appEvents.emit('openVideoFullscreen', {
url: props.content?.fileUrl,
})
}
const [paused, setPaused] = useState(false)
@ -39,9 +56,17 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = props => { @@ -39,9 +56,17 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = props => {
: 0.7
return (
<View
style={[styles.container, props.isMy ? styles.myContainer : null]}>
{!props.isMy ? (
<TouchableOpacity
disabled={viewMode === ChatViewModeEnum.DEFAULT}
onPress={select}
style={[
styles.container,
props.isMy ? styles.myContainer : null,
props.isMy && viewMode === ChatViewModeEnum.SELECT
? styles.mySelectableContainer
: null,
]}>
{!props.isMy && viewMode === ChatViewModeEnum.DEFAULT ? (
<TouchableOpacity onPress={props.onProfilePress}>
<Avatar
imageUrl={props.author.avatar}
@ -52,13 +77,28 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = props => { @@ -52,13 +77,28 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = props => {
</TouchableOpacity>
) : null}
{viewMode === ChatViewModeEnum.SELECT ? (
<FormCircleCheckbox
key={props.id}
isChecked={_.find(
selectedMessages,
it => it.id === props.id,
)}
onChecked={select}
style={styles.checkBox}
title={''}
bottomBorder={false}
/>
) : null}
<View style={[props.forwardedFrom ? styles.withBorder : null]}>
{props.forwardedFrom && (
<ForwardedMessageHeader
title={props.author?.name}
onPress={() =>
props.onForwardedAuthorPress(props.author?.id)
}
onPress={() => {
if (viewMode === ChatViewModeEnum.SELECT) select()
else props.onForwardedAuthorPress(props.author?.id)
}}
/>
)}
@ -132,6 +172,10 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = props => { @@ -132,6 +172,10 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = props => {
]}
/>
<TouchableOpacity
onPress={() => {
if (viewMode === ChatViewModeEnum.SELECT)
select()
}}
onLongPress={props.onLongPress}
style={styles.playBtnWrapper}>
<TouchableOpacity
@ -163,7 +207,9 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = props => { @@ -163,7 +207,9 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = props => {
</View>
</View>
</View>
{!props.isMy && !!props.onForwardPressMessage ? (
{!props.isMy &&
viewMode === ChatViewModeEnum.DEFAULT &&
!!props.onForwardPressMessage ? (
<TouchableOpacity
style={styles.forwardBtn}
onPress={props.onForwardPressMessage}>
@ -174,7 +220,7 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = props => { @@ -174,7 +220,7 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = props => {
/>
</TouchableOpacity>
) : null}
</View>
</TouchableOpacity>
)
}
@ -212,6 +258,9 @@ const createStyles = (theme: PartialTheme) => @@ -212,6 +258,9 @@ const createStyles = (theme: PartialTheme) =>
myContainer: {
justifyContent: 'flex-end',
},
mySelectableContainer: {
justifyContent: 'space-between',
},
content: {
maxWidth: $size(280),
maxHeight: $size(280),
@ -285,4 +334,7 @@ const createStyles = (theme: PartialTheme) => @@ -285,4 +334,7 @@ const createStyles = (theme: PartialTheme) =>
justifyContent: 'center',
// backgroundColor: theme?.chats.message.$bgPrimary,
},
checkBox: {
alignSelf: 'center',
},
})

69
src/shared/components/plugins/chat/chat-item.component.tsx

@ -9,6 +9,12 @@ import { Avatar, IconComponent, Txt } from '../../elements' @@ -9,6 +9,12 @@ import { Avatar, IconComponent, Txt } from '../../elements'
import { ForwardedMessageHeader } from './forwarded-message-header.component'
import { IChatMessage } from './interfaces'
import { RepliedMessageInfo } from './replied-message-info.component'
import { FormCircleCheckbox } from '@/shared'
import {
useChatSelectedMessagesState,
useChatViewModeState,
} from '@/modules/chats/hooks'
import { ChatViewModeEnum } from '@/modules/chats/enums'
interface ChatItemProps extends IChatMessage {
containerStyle?: ViewStyle
@ -30,6 +36,21 @@ export const ChatItem: FC<ChatItemProps> = ({ @@ -30,6 +36,21 @@ export const ChatItem: FC<ChatItemProps> = ({
}) => {
const { styles, theme } = useTheme(createStyles)
const viewMode = useChatViewModeState(s => s.mode)
const selectedMessages = useChatSelectedMessagesState(s => s.messages)
const { selectMessage } = useChatSelectedMessagesState()
const isSelectable = useMemo(() => {
if (viewMode === ChatViewModeEnum.DEFAULT) return false
return (
props.type === MessageType.Text ||
props.type === MessageType.Image ||
props.type === MessageType.Video ||
props.type === MessageType.Audio ||
props.type === MessageType.File
)
}, [viewMode, props.type])
const repliedMessage =
props.content?.replyToMessage?.type === MessageType.Forwarded
? props.content?.replyToMessage?.content?.originalMessage
@ -40,10 +61,20 @@ export const ChatItem: FC<ChatItemProps> = ({ @@ -40,10 +61,20 @@ export const ChatItem: FC<ChatItemProps> = ({
[props.id],
)
const select = () => selectMessage(props)
return (
<View
style={[styles.container, props.isMy ? styles.myContainer : null]}>
{!props.isMy ? (
<TouchableOpacity
disabled={!isSelectable}
onPress={select}
style={[
styles.container,
props.isMy ? styles.myContainer : null,
props.isMy && isSelectable
? styles.mySelectableContainer
: null,
]}>
{!props.isMy && !isSelectable ? (
<TouchableOpacity onPress={props.onProfilePress}>
<Avatar
imageUrl={props.author.avatar}
@ -54,6 +85,20 @@ export const ChatItem: FC<ChatItemProps> = ({ @@ -54,6 +85,20 @@ export const ChatItem: FC<ChatItemProps> = ({
</TouchableOpacity>
) : null}
{isSelectable ? (
<FormCircleCheckbox
key={props.id}
isChecked={_.find(
selectedMessages,
it => it.id === props.id,
)}
onChecked={select}
style={styles.checkBox}
title={''}
bottomBorder={false}
/>
) : null}
<View
style={[
styles.content,
@ -63,14 +108,16 @@ export const ChatItem: FC<ChatItemProps> = ({ @@ -63,14 +108,16 @@ export const ChatItem: FC<ChatItemProps> = ({
{props.forwardedFrom && (
<ForwardedMessageHeader
title={props.author?.name}
onPress={() =>
props.onForwardedAuthorPress(props.author?.id)
}
onPress={() => {
if (isSelectable) select()
else props.onForwardedAuthorPress(props.author?.id)
}}
/>
)}
<TouchableOpacity
onLongPress={props.onLongPress}
onPress={isSelectable ? select : null}
style={[
props.isMy ? styles.right : styles.left,
hideBackground
@ -130,7 +177,7 @@ export const ChatItem: FC<ChatItemProps> = ({ @@ -130,7 +177,7 @@ export const ChatItem: FC<ChatItemProps> = ({
</TouchableOpacity>
</View>
{!props.isMy && !!props.onForwardPressMessage ? (
{!props.isMy && !isSelectable && !!props.onForwardPressMessage ? (
<TouchableOpacity
style={styles.forwardBtn}
onPress={props.onForwardPressMessage}>
@ -141,7 +188,7 @@ export const ChatItem: FC<ChatItemProps> = ({ @@ -141,7 +188,7 @@ export const ChatItem: FC<ChatItemProps> = ({
/>
</TouchableOpacity>
) : null}
</View>
</TouchableOpacity>
)
}
@ -164,6 +211,9 @@ const createStyles = (theme: PartialTheme) => @@ -164,6 +211,9 @@ const createStyles = (theme: PartialTheme) =>
myContainer: {
justifyContent: 'flex-end',
},
mySelectableContainer: {
justifyContent: 'space-between',
},
content: {
maxWidth: '75%',
},
@ -216,4 +266,7 @@ const createStyles = (theme: PartialTheme) => @@ -216,4 +266,7 @@ const createStyles = (theme: PartialTheme) =>
justifyContent: 'center',
// backgroundColor: theme?.chats.message.$bgPrimary,
},
checkBox: {
alignSelf: 'center',
},
})

32
src/shared/components/plugins/chat/chat-messages.component.tsx

@ -295,23 +295,25 @@ export const ChatMessages: FC<ChatMessagesProps> = ({ items, ...props }) => { @@ -295,23 +295,25 @@ export const ChatMessages: FC<ChatMessagesProps> = ({ items, ...props }) => {
return null
}
const setItemHeightOnLayout = ({ nativeEvent, index }) =>
setHeight(state => ({
...state,
[index]: nativeEvent.layout.height,
}))
const renderItem = useCallback(
({ item, index }) => {
const itemByType = getItem(item)
if (itemByType)
return (
<View
onLayout={({ nativeEvent }) =>
setHeight(state => ({
...state,
[index]: nativeEvent.layout.height,
}))
}>
{itemByType}
</View>
)
return null
if (!itemByType) return null
return (
<View
onLayout={({ nativeEvent }) =>
setItemHeightOnLayout({ nativeEvent, index })
}>
{itemByType}
</View>
)
},
[items, activeAudioId, props.onLongPressMessage],
)
@ -414,7 +416,7 @@ export const ChatMessages: FC<ChatMessagesProps> = ({ items, ...props }) => { @@ -414,7 +416,7 @@ export const ChatMessages: FC<ChatMessagesProps> = ({ items, ...props }) => {
// потрібно, щоб пофіксити момент на андроїді
// коли закріплено повідомлення на початку чату і користувач переходить до нього
// то на сторінці відображається тільки декілька повідомлень (або взагалі одне)
// в цьому випадку подія onScroll не відловлюється (на відміну від ios)
// в цьому випадку подія onScroll не відловлюється (навідміну від ios)
// і нема можливості догрузити свіжі повідомлення
if (isScrolling) return

1
src/shared/components/plugins/chat/interfaces.ts

@ -2,6 +2,7 @@ import { MessageType } from '@/shared/enums' @@ -2,6 +2,7 @@ import { MessageType } from '@/shared/enums'
export interface IChatMessage {
id: number | string
chatId: number | string
content: any
authorId?: number
readByUsersId?: number[]

Loading…
Cancel
Save