diff --git a/app/containers/SearchHeader.tsx b/app/containers/SearchHeader.tsx index f79451355..8d3c81416 100644 --- a/app/containers/SearchHeader.tsx +++ b/app/containers/SearchHeader.tsx @@ -22,7 +22,7 @@ const styles = StyleSheet.create({ interface ISearchHeaderProps { onSearchChangeText?: (text: string) => void; - testID: string; + testID?: string; } const SearchHeader = ({ onSearchChangeText, testID }: ISearchHeaderProps): JSX.Element => { diff --git a/app/definitions/ICannedResponse.ts b/app/definitions/ICannedResponse.ts new file mode 100644 index 000000000..fcf531c0c --- /dev/null +++ b/app/definitions/ICannedResponse.ts @@ -0,0 +1,31 @@ +export interface IDepartment { + _id: string; + enabled: boolean; + name: string; + description: string; + showOnRegistration: boolean; + showOnOfflineForm: boolean; + requestTagBeforeClosingChat: boolean; + email: string; + chatClosingTags: string[]; + offlineMessageChannelName: string; + maxNumberSimultaneousChat: number; + abandonedRoomsCloseCustomMessage: string; + waitingQueueMessage: string; + departmentsAllowedToForward: string; + _updatedAt: Date; + numAgents: number; + ancestors: string[]; +} + +export interface ICannedResponse { + _id: string; + shortcut: string; + text: string; + scope: string; + tags: string[]; + createdBy: { _id: string; username: string }; + userId: string; + scopeName: string; + departmentId?: string; +} diff --git a/app/stacks/types.ts b/app/stacks/types.ts index aef4d9bf3..002f5e6d7 100644 --- a/app/stacks/types.ts +++ b/app/stacks/types.ts @@ -8,6 +8,7 @@ import { IServer } from '../definitions/IServer'; import { IAttachment } from '../definitions/IAttachment'; import { IMessage } from '../definitions/IMessage'; import { ISubscription, SubscriptionType, TSubscriptionModel } from '../definitions/ISubscription'; +import { ICannedResponse } from '../definitions/ICannedResponse'; export type ChatsStackParamList = { RoomsListView: undefined; @@ -137,12 +138,7 @@ export type ChatsStackParamList = { rid: string; }; CannedResponseDetail: { - cannedResponse: { - shortcut: string; - text: string; - scopeName: string; - tags: string[]; - }; + cannedResponse: ICannedResponse; room: ISubscription; }; }; diff --git a/app/utils/goRoom.ts b/app/utils/goRoom.ts index e2623a47e..23913496a 100644 --- a/app/utils/goRoom.ts +++ b/app/utils/goRoom.ts @@ -44,6 +44,7 @@ export const goRoom = async ({ isMasterDetail: boolean; navigationMethod?: any; jumpToMessageId?: string; + usedCannedResponse?: string; }): Promise => { if (item.t === SubscriptionType.DIRECT && item?.search) { // if user is using the search we need first to join/create room @@ -54,7 +55,7 @@ export const goRoom = async ({ return navigate({ item: { rid: result.room._id, - name: username, + name: username || '', t: SubscriptionType.DIRECT }, isMasterDetail, diff --git a/app/views/CannedResponseDetail.js b/app/views/CannedResponseDetail.tsx similarity index 80% rename from app/views/CannedResponseDetail.js rename to app/views/CannedResponseDetail.tsx index 67002bbdb..8c42b7aff 100644 --- a/app/views/CannedResponseDetail.js +++ b/app/views/CannedResponseDetail.tsx @@ -1,5 +1,6 @@ import React, { useEffect } from 'react'; -import PropTypes from 'prop-types'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; import { StyleSheet, Text, View, ScrollView } from 'react-native'; import { useSelector } from 'react-redux'; @@ -13,6 +14,8 @@ import Navigation from '../lib/Navigation'; import { goRoom } from '../utils/goRoom'; import { themes } from '../constants/colors'; import Markdown from '../containers/markdown'; +import { ICannedResponse } from '../definitions/ICannedResponse'; +import { ChatsStackParamList } from '../stacks/types'; import sharedStyles from './Styles'; const styles = StyleSheet.create({ @@ -68,27 +71,34 @@ const styles = StyleSheet.create({ } }); -const Item = ({ label, content, theme, testID }) => +interface IItem { + label: string; + content?: string; + theme: string; + testID?: string; +} + +const Item = ({ label, content, theme, testID }: IItem) => content ? ( {label} + {/* @ts-ignore */} ) : null; -Item.propTypes = { - label: PropTypes.string, - content: PropTypes.string, - theme: PropTypes.string, - testID: PropTypes.string -}; -const CannedResponseDetail = ({ navigation, route }) => { +interface ICannedResponseDetailProps { + navigation: StackNavigationProp; + route: RouteProp; +} + +const CannedResponseDetail = ({ navigation, route }: ICannedResponseDetailProps): JSX.Element => { const { cannedResponse } = route?.params; const { theme } = useTheme(); - const { isMasterDetail } = useSelector(state => state.app); - const { rooms } = useSelector(state => state.room); + const { isMasterDetail } = useSelector((state: any) => state.app); + const { rooms } = useSelector((state: any) => state.room); useEffect(() => { navigation.setOptions({ @@ -96,15 +106,14 @@ const CannedResponseDetail = ({ navigation, route }) => { }); }, []); - const navigateToRoom = item => { + const navigateToRoom = (item: ICannedResponse) => { const { room } = route.params; - const { name, username } = room; + const { name } = room; const params = { rid: room.rid, name: RocketChat.getRoomTitle({ t: room.t, - fname: name, - name: username + fname: name }), t: room.t, roomUserId: RocketChat.getUidDirectMessage(room), @@ -115,7 +124,7 @@ const CannedResponseDetail = ({ navigation, route }) => { // if it's on master detail layout, we close the modal and replace RoomView if (isMasterDetail) { Navigation.navigate('DrawerNavigator'); - goRoom({ item: params, isMasterDetail, usedCannedResponse: item.text }); + goRoom({ item: params, isMasterDetail }); } else { let navigate = navigation.push; // if this is a room focused @@ -163,9 +172,4 @@ const CannedResponseDetail = ({ navigation, route }) => { ); }; -CannedResponseDetail.propTypes = { - navigation: PropTypes.object, - route: PropTypes.object -}; - export default CannedResponseDetail; diff --git a/app/views/CannedResponsesListView/CannedResponseItem.js b/app/views/CannedResponsesListView/CannedResponseItem.tsx similarity index 77% rename from app/views/CannedResponsesListView/CannedResponseItem.js rename to app/views/CannedResponsesListView/CannedResponseItem.tsx index cc5f3c39a..d81e0e366 100644 --- a/app/views/CannedResponsesListView/CannedResponseItem.js +++ b/app/views/CannedResponsesListView/CannedResponseItem.tsx @@ -1,14 +1,31 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { View, Text } from 'react-native'; - import Touchable from 'react-native-platform-touchable'; + import { themes } from '../../constants/colors'; import Button from '../../containers/Button'; import I18n from '../../i18n'; import styles from './styles'; -const CannedResponseItem = ({ theme, onPressDetail, shortcut, scope, onPressUse, text, tags }) => ( +interface ICannedResponseItem { + theme: string; + onPressDetail: () => void; + shortcut: string; + scope: string; + onPressUse: () => void; + text: string; + tags: string[]; +} + +const CannedResponseItem = ({ + theme, + onPressDetail = () => {}, + shortcut, + scope, + onPressUse = () => {}, + text, + tags +}: ICannedResponseItem): JSX.Element => ( <> @@ -43,19 +60,4 @@ const CannedResponseItem = ({ theme, onPressDetail, shortcut, scope, onPressUse, ); -CannedResponseItem.propTypes = { - theme: PropTypes.string, - onPressDetail: PropTypes.func, - shortcut: PropTypes.string, - scope: PropTypes.string, - onPressUse: PropTypes.func, - text: PropTypes.string, - tags: PropTypes.array -}; - -CannedResponseItem.defaultProps = { - onPressDetail: () => {}, - onPressUse: () => {} -}; - export default CannedResponseItem; diff --git a/app/views/CannedResponsesListView/Dropdown/DropdownItem.js b/app/views/CannedResponsesListView/Dropdown/DropdownItem.js deleted file mode 100644 index 85b9aa703..000000000 --- a/app/views/CannedResponsesListView/Dropdown/DropdownItem.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { StyleSheet, Text, View } from 'react-native'; - -import { themes } from '../../../constants/colors'; -import { withTheme } from '../../../theme'; -import Touch from '../../../utils/touch'; -import { CustomIcon } from '../../../lib/Icons'; -import sharedStyles from '../../Styles'; - -export const ROW_HEIGHT = 44; - -const styles = StyleSheet.create({ - container: { - paddingVertical: 11, - height: ROW_HEIGHT, - paddingHorizontal: 16, - flexDirection: 'row', - alignItems: 'center' - }, - text: { - flex: 1, - fontSize: 16, - ...sharedStyles.textRegular - } -}); - -const DropdownItem = React.memo(({ theme, onPress, iconName, text }) => ( - - - {text} - {iconName ? : null} - - -)); - -DropdownItem.propTypes = { - text: PropTypes.string, - iconName: PropTypes.string, - theme: PropTypes.string, - onPress: PropTypes.func -}; - -export default withTheme(DropdownItem); diff --git a/app/views/CannedResponsesListView/Dropdown/DropdownItem.tsx b/app/views/CannedResponsesListView/Dropdown/DropdownItem.tsx new file mode 100644 index 000000000..ebae90378 --- /dev/null +++ b/app/views/CannedResponsesListView/Dropdown/DropdownItem.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { StyleSheet, Text, View } from 'react-native'; + +import { themes } from '../../../constants/colors'; +import { useTheme } from '../../../theme'; +import Touch from '../../../utils/touch'; +import { CustomIcon } from '../../../lib/Icons'; +import sharedStyles from '../../Styles'; + +export const ROW_HEIGHT = 44; + +const styles = StyleSheet.create({ + container: { + paddingVertical: 11, + height: ROW_HEIGHT, + paddingHorizontal: 16, + flexDirection: 'row', + alignItems: 'center' + }, + text: { + flex: 1, + fontSize: 16, + ...sharedStyles.textRegular + } +}); + +interface IDropdownItem { + text: string; + iconName: string | null; + onPress: () => void; +} + +const DropdownItem = React.memo(({ onPress, iconName, text }: IDropdownItem) => { + const { theme } = useTheme(); + + return ( + + + {text} + {iconName ? : null} + + + ); +}); + +export default DropdownItem; diff --git a/app/views/CannedResponsesListView/Dropdown/DropdownItemFilter.js b/app/views/CannedResponsesListView/Dropdown/DropdownItemFilter.tsx similarity index 57% rename from app/views/CannedResponsesListView/Dropdown/DropdownItemFilter.js rename to app/views/CannedResponsesListView/Dropdown/DropdownItemFilter.tsx index d4e457805..a2d68c528 100644 --- a/app/views/CannedResponsesListView/Dropdown/DropdownItemFilter.js +++ b/app/views/CannedResponsesListView/Dropdown/DropdownItemFilter.tsx @@ -1,9 +1,15 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import { IDepartment } from '../../../definitions/ICannedResponse'; import DropdownItem from './DropdownItem'; -const DropdownItemFilter = ({ currentDepartment, value, onPress }) => ( +interface IDropdownItemFilter { + currentDepartment: IDepartment; + value: IDepartment; + onPress: (value: IDepartment) => void; +} + +const DropdownItemFilter = ({ currentDepartment, value, onPress }: IDropdownItemFilter): JSX.Element => ( ( /> ); -DropdownItemFilter.propTypes = { - currentDepartment: PropTypes.object, - value: PropTypes.string, - onPress: PropTypes.func -}; - export default DropdownItemFilter; diff --git a/app/views/CannedResponsesListView/Dropdown/DropdownItemHeader.js b/app/views/CannedResponsesListView/Dropdown/DropdownItemHeader.js deleted file mode 100644 index 4f1f2b68f..000000000 --- a/app/views/CannedResponsesListView/Dropdown/DropdownItemHeader.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import DropdownItem from './DropdownItem'; - -const DropdownItemHeader = ({ department, onPress }) => ( - -); - -DropdownItemHeader.propTypes = { - department: PropTypes.object, - onPress: PropTypes.func -}; - -export default DropdownItemHeader; diff --git a/app/views/CannedResponsesListView/Dropdown/DropdownItemHeader.tsx b/app/views/CannedResponsesListView/Dropdown/DropdownItemHeader.tsx new file mode 100644 index 000000000..ecfa95e8a --- /dev/null +++ b/app/views/CannedResponsesListView/Dropdown/DropdownItemHeader.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import { IDepartment } from '../../../definitions/ICannedResponse'; +import DropdownItem from './DropdownItem'; + +interface IDropdownItemHeader { + department: IDepartment; + onPress: () => void; +} + +const DropdownItemHeader = ({ department, onPress }: IDropdownItemHeader): JSX.Element => ( + +); + +export default DropdownItemHeader; diff --git a/app/views/CannedResponsesListView/Dropdown/index.js b/app/views/CannedResponsesListView/Dropdown/index.tsx similarity index 72% rename from app/views/CannedResponsesListView/Dropdown/index.js rename to app/views/CannedResponsesListView/Dropdown/index.tsx index e2735e5c5..d723bc4c8 100644 --- a/app/views/CannedResponsesListView/Dropdown/index.js +++ b/app/views/CannedResponsesListView/Dropdown/index.tsx @@ -1,31 +1,30 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Animated, Easing, FlatList, TouchableWithoutFeedback } from 'react-native'; import { withSafeAreaInsets } from 'react-native-safe-area-context'; import styles from '../styles'; import { themes } from '../../../constants/colors'; import { withTheme } from '../../../theme'; -import { headerHeight } from '../../../containers/Header'; import * as List from '../../../containers/List'; +import { IDepartment } from '../../../definitions/ICannedResponse'; import DropdownItemFilter from './DropdownItemFilter'; import DropdownItemHeader from './DropdownItemHeader'; import { ROW_HEIGHT } from './DropdownItem'; const ANIMATION_DURATION = 200; -class Dropdown extends React.Component { - static propTypes = { - isMasterDetail: PropTypes.bool, - theme: PropTypes.string, - insets: PropTypes.object, - currentDepartment: PropTypes.object, - onClose: PropTypes.func, - onDepartmentSelected: PropTypes.func, - departments: PropTypes.array - }; +interface IDropdownProps { + theme?: string; + currentDepartment: IDepartment; + onClose: () => void; + onDepartmentSelected: (value: IDepartment) => void; + departments: IDepartment[]; +} - constructor(props) { +class Dropdown extends React.Component { + private animatedValue: Animated.Value; + + constructor(props: IDropdownProps) { super(props); this.animatedValue = new Animated.Value(0); } @@ -50,16 +49,15 @@ class Dropdown extends React.Component { }; render() { - const { isMasterDetail, insets, theme, currentDepartment, onDepartmentSelected, departments } = this.props; - const statusBarHeight = insets?.top ?? 0; - const heightDestination = isMasterDetail ? headerHeight + statusBarHeight : 0; + const { theme, currentDepartment, onDepartmentSelected, departments } = this.props; + const heightDestination = 0; const translateY = this.animatedValue.interpolate({ inputRange: [0, 1], outputRange: [-300, heightDestination] // approximated height of the component when closed/open }); const backdropOpacity = this.animatedValue.interpolate({ inputRange: [0, 1], - outputRange: [0, themes[theme].backdropOpacity] + outputRange: [0, themes[theme!].backdropOpacity] }); const maxRows = 5; @@ -70,7 +68,7 @@ class Dropdown extends React.Component { style={[ styles.backdrop, { - backgroundColor: themes[theme].backdropColor, + backgroundColor: themes[theme!].backdropColor, opacity: backdropOpacity, top: heightDestination } @@ -82,8 +80,8 @@ class Dropdown extends React.Component { styles.dropdownContainer, { transform: [{ translateY }], - backgroundColor: themes[theme].backgroundColor, - borderColor: themes[theme].separatorColor + backgroundColor: themes[theme!].backgroundColor, + borderColor: themes[theme!].separatorColor } ]}> diff --git a/app/views/CannedResponsesListView/index.js b/app/views/CannedResponsesListView/index.tsx similarity index 83% rename from app/views/CannedResponsesListView/index.js rename to app/views/CannedResponsesListView/index.tsx index f9f515deb..3c7b0dc9f 100644 --- a/app/views/CannedResponsesListView/index.js +++ b/app/views/CannedResponsesListView/index.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useState, useCallback } from 'react'; -import PropTypes from 'prop-types'; import { FlatList } from 'react-native'; import { useSelector } from 'react-redux'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { HeaderBackButton } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native'; +import { HeaderBackButton, StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack'; import database from '../../lib/database'; import I18n from '../../i18n'; @@ -26,6 +26,9 @@ import CannedResponseItem from './CannedResponseItem'; import Dropdown from './Dropdown'; import DropdownItemHeader from './Dropdown/DropdownItemHeader'; import styles from './styles'; +import { ICannedResponse, IDepartment } from '../../definitions/ICannedResponse'; +import { ChatsStackParamList } from '../../stacks/types'; +import { ISubscription } from '../../definitions/ISubscription'; const COUNT = 25; @@ -42,14 +45,19 @@ const fixedScopes = [ _id: 'user', name: I18n.t('Private') } -]; +] as IDepartment[]; -const CannedResponsesListView = ({ navigation, route }) => { - const [room, setRoom] = useState(null); +interface ICannedResponsesListViewProps { + navigation: StackNavigationProp; + route: RouteProp; +} - const [cannedResponses, setCannedResponses] = useState([]); - const [cannedResponsesScopeName, setCannedResponsesScopeName] = useState([]); - const [departments, setDepartments] = useState([]); +const CannedResponsesListView = ({ navigation, route }: ICannedResponsesListViewProps): JSX.Element => { + const [room, setRoom] = useState(null); + + const [cannedResponses, setCannedResponses] = useState([]); + const [cannedResponsesScopeName, setCannedResponsesScopeName] = useState([]); + const [departments, setDepartments] = useState([]); // states used by the filter in Header and Dropdown const [isSearching, setIsSearching] = useState(false); @@ -65,8 +73,8 @@ const CannedResponsesListView = ({ navigation, route }) => { const insets = useSafeAreaInsets(); const { theme } = useTheme(); - const { isMasterDetail } = useSelector(state => state.app); - const { rooms } = useSelector(state => state.room); + const { isMasterDetail } = useSelector((state: any) => state.app); + const { rooms } = useSelector((state: any) => state.room); const getRoomFromDb = async () => { const { rid } = route.params; @@ -93,21 +101,22 @@ const CannedResponsesListView = ({ navigation, route }) => { } }, 300); - const goToDetail = item => { - navigation.navigate('CannedResponseDetail', { cannedResponse: item, room }); + const goToDetail = (item: ICannedResponse) => { + if (room) { + navigation.navigate('CannedResponseDetail', { cannedResponse: item, room }); + } }; - const navigateToRoom = item => { + const navigateToRoom = (item: ICannedResponse) => { if (!room) { return; } - const { name, username } = room; + const { name } = room; const params = { rid: room.rid, name: RocketChat.getRoomTitle({ t: room.t, - fname: name, - name: username + fname: name }), t: room.t, roomUserId: RocketChat.getUidDirectMessage(room), @@ -118,7 +127,7 @@ const CannedResponsesListView = ({ navigation, route }) => { // if it's on master detail layout, we close the modal and replace RoomView if (isMasterDetail) { Navigation.navigate('DrawerNavigator'); - goRoom({ item: params, isMasterDetail, usedCannedResponse: item.text }); + goRoom({ item: params, isMasterDetail }); } else { let navigate = navigation.push; // if this is a room focused @@ -130,7 +139,17 @@ const CannedResponsesListView = ({ navigation, route }) => { } }; - const getListCannedResponse = async ({ text, department, depId, debounced }) => { + const getListCannedResponse = async ({ + text, + department, + depId, + debounced + }: { + text: string; + department: string; + depId: string; + debounced: boolean; + }) => { try { const res = await RocketChat.getListCannedResponse({ text, @@ -188,13 +207,13 @@ const CannedResponsesListView = ({ navigation, route }) => { setOffset(0); }; - const onChangeText = text => { + const onChangeText = (text: string) => { newSearch(); setSearchText(text); searchCallback(text, scope, departmentId); }; - const onDepartmentSelect = value => { + const onDepartmentSelect = (value: IDepartment) => { let department = ''; let depId = ''; @@ -225,7 +244,7 @@ const CannedResponsesListView = ({ navigation, route }) => { await getListCannedResponse({ text: searchText, department: scope, depId: departmentId, debounced: false }); }; - const getHeader = () => { + const getHeader = (): StackNavigationOptions => { if (isSearching) { const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 1 }); return { @@ -235,7 +254,7 @@ const CannedResponsesListView = ({ navigation, route }) => { { - onChangeText(); + onChangeText(''); setIsSearching(false); }} /> @@ -250,7 +269,7 @@ const CannedResponsesListView = ({ navigation, route }) => { }; } - const options = { + const options: StackNavigationOptions = { headerLeft: () => ( navigation.pop()} tintColor={themes[theme].headerTintColor} /> ), @@ -355,9 +374,4 @@ const CannedResponsesListView = ({ navigation, route }) => { ); }; -CannedResponsesListView.propTypes = { - navigation: PropTypes.object, - route: PropTypes.object -}; - export default CannedResponsesListView; diff --git a/app/views/CannedResponsesListView/styles.js b/app/views/CannedResponsesListView/styles.ts similarity index 97% rename from app/views/CannedResponsesListView/styles.js rename to app/views/CannedResponsesListView/styles.ts index d9b8f580c..f0e971867 100644 --- a/app/views/CannedResponsesListView/styles.js +++ b/app/views/CannedResponsesListView/styles.ts @@ -13,7 +13,7 @@ export default StyleSheet.create({ borderBottomWidth: StyleSheet.hairlineWidth }, backdrop: { - ...StyleSheet.absoluteFill + ...StyleSheet.absoluteFillObject }, wrapCannedItem: { minHeight: 117,