diff --git a/README.md b/README.md index e4e7bdec..50e97ea1 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Also check the [#react-native](https://open.rocket.chat/channel/react-native) co Are you a dev and would like to help? Found a bug that you would like to report or a missing feature that you would like to work on? Great! We have written down a [Contribution guide](https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/CONTRIBUTING.md) so you can start easily. ## Whitelabel -Do you want to make the app run on your own server only? [Follow our whitelabel documentation.](https://docs.rocket.chat/guides/developer/mobile-apps/whitelabeling-mobile-apps) +Do you want to make the app run on your own server only? [Follow our whitelabel documentation.](https://developer.rocket.chat/mobile-app/mobile-app-white-labelling) ## Engage with us ### Share your story diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 56448a03..0ad82c0d 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -1322,6 +1322,595 @@ exports[`Storyshots BackgroundContainer text 1`] = ` `; +exports[`Storyshots CannedResponseItem Itens 1`] = ` +Array [ + + + + + ! + !FAQ4 + + + Private + + + + + Use + + + + + “ + ZCVXZVXCZVZXVZXCVZXCVXZCVZX + ” + + + , + + + + + ! + test4mobilePrivate + + + Private + + + + + Use + + + + + “ + test for mobile private + ” + + + + + HQ + + + + + Closed + + + + + HQ + + + + + Problem in Product Y + + + + + HQ + + + + + Closed + + + + + Problem in Product Y + + + + , +] +`; + exports[`Storyshots Header Buttons badge 1`] = ` diff --git a/android/app/build.gradle b/android/app/build.gradle index d18b7869..42a73ea3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -260,7 +260,6 @@ android { dependencies { addUnimodulesDependencies() - implementation project(':watermelondb') implementation project(':@react-native-community_viewpager') playImplementation project(':reactnativenotifications') playImplementation project(':@react-native-firebase_app') diff --git a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.java b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.java index 33e246b5..bf7dc5ef 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/MainApplication.java +++ b/android/app/src/main/java/chat/rocket/reactnative/MainApplication.java @@ -9,8 +9,9 @@ import com.facebook.react.ReactApplication; import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; import com.facebook.soloader.SoLoader; -import com.nozbe.watermelondb.WatermelonDBPackage; import com.reactnativecommunity.viewpager.RNCViewPagerPackage; +import com.facebook.react.bridge.JSIModulePackage; +import com.swmansion.reanimated.ReanimatedJSIModulePackage; import org.unimodules.adapters.react.ModuleRegistryAdapter; import org.unimodules.adapters.react.ReactModuleRegistryProvider; @@ -35,7 +36,6 @@ public class MainApplication extends Application implements ReactApplication { protected List getPackages() { @SuppressWarnings("UnnecessaryLocalVariable") List packages = new PackageList(this).getPackages(); - packages.add(new WatermelonDBPackage()); packages.add(new RNCViewPagerPackage()); packages.add(new SSLPinningPackage()); List unimodules = Arrays.asList( @@ -52,6 +52,11 @@ public class MainApplication extends Application implements ReactApplication { return "index"; } + @Override + protected JSIModulePackage getJSIModulePackage() { + return new ReanimatedJSIModulePackage(); // <- add + } + @Override protected @Nullable String getBundleAssetName() { return "app.bundle"; diff --git a/android/settings.gradle b/android/settings.gradle index d50ac213..98573a99 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -2,8 +2,6 @@ apply from: '../node_modules/react-native-unimodules/gradle.groovy' includeUnimodulesProjects() rootProject.name = 'RocketChatRN' -include ':watermelondb' -project(':watermelondb').projectDir = new File(rootProject.projectDir, '../node_modules/@nozbe/watermelondb/native/android') include ':reactnativenotifications' project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android/app') include ':@react-native-community_viewpager' diff --git a/app/AppContainer.tsx b/app/AppContainer.tsx index 00d85101..f7f08bb2 100644 --- a/app/AppContainer.tsx +++ b/app/AppContainer.tsx @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import Navigation from './lib/Navigation'; import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation'; -import { ROOT_INSIDE, ROOT_LOADING, ROOT_NEW_SERVER, ROOT_OUTSIDE, ROOT_SET_USERNAME } from './actions/app'; +import { ROOT_INSIDE, ROOT_LOADING, ROOT_OUTSIDE, ROOT_SET_USERNAME } from './actions/app'; // Stacks import AuthLoadingView from './views/AuthLoadingView'; // SetUsername Stack @@ -56,9 +56,7 @@ const App = React.memo(({ root, isMasterDetail }: { root: string; isMasterDetail <> {root === ROOT_LOADING ? : null} - {root === ROOT_OUTSIDE || root === ROOT_NEW_SERVER ? ( - - ) : null} + {root === ROOT_OUTSIDE ? : null} {root === ROOT_INSIDE && isMasterDetail ? ( ) : null} diff --git a/app/actions/app.js b/app/actions/app.js index 9ced0c6c..fe6981d6 100644 --- a/app/actions/app.js +++ b/app/actions/app.js @@ -3,7 +3,6 @@ import { APP } from './actionsTypes'; export const ROOT_OUTSIDE = 'outside'; export const ROOT_INSIDE = 'inside'; export const ROOT_LOADING = 'loading'; -export const ROOT_NEW_SERVER = 'newServer'; export const ROOT_SET_USERNAME = 'setUsername'; export function appStart({ root, ...args }) { diff --git a/app/constants/settings.ts b/app/constants/settings.ts index 14ad8fb3..fb9c7e6b 100644 --- a/app/constants/settings.ts +++ b/app/constants/settings.ts @@ -202,5 +202,8 @@ export default { }, Jitsi_Enable_Channels: { type: 'valuesAsBoolean' + }, + Canned_Responses_Enable: { + type: 'valueAsBoolean' } }; diff --git a/app/containers/ActionSheet/ActionSheet.tsx b/app/containers/ActionSheet/ActionSheet.tsx index f926f01e..35e69c70 100644 --- a/app/containers/ActionSheet/ActionSheet.tsx +++ b/app/containers/ActionSheet/ActionSheet.tsx @@ -3,7 +3,7 @@ import { Keyboard, Text } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { State, TapGestureHandler } from 'react-native-gesture-handler'; import ScrollBottomSheet from 'react-native-scroll-bottom-sheet'; -import Animated, { Easing, Extrapolate, Value, interpolate } from 'react-native-reanimated'; +import Animated, { Easing, Extrapolate, Value, interpolateNode } from 'react-native-reanimated'; import * as Haptics from 'expo-haptics'; import { useBackHandler } from '@react-native-community/hooks'; @@ -132,11 +132,12 @@ const ActionSheet = React.memo( const renderItem = ({ item }: any) => ; const animatedPosition = React.useRef(new Value(0)); - const opacity = interpolate(animatedPosition.current, { + // TODO: Similar to https://github.com/wcandillon/react-native-redash/issues/307#issuecomment-827442320 + const opacity = interpolateNode(animatedPosition.current, { inputRange: [0, 1], outputRange: [0, themes[theme].backdropOpacity], extrapolate: Extrapolate.CLAMP - }); + }) as any; return ( <> diff --git a/app/containers/Button/index.tsx b/app/containers/Button/index.tsx index 4f57776d..9e475a67 100644 --- a/app/containers/Button/index.tsx +++ b/app/containers/Button/index.tsx @@ -17,6 +17,7 @@ interface IButtonProps { color: string; fontSize: any; style: any; + styleText?: any; testID: string; } @@ -48,7 +49,8 @@ export default class Button extends React.PureComponent, a }; render() { - const { title, type, onPress, disabled, backgroundColor, color, loading, style, theme, fontSize, ...otherProps } = this.props; + const { title, type, onPress, disabled, backgroundColor, color, loading, style, theme, fontSize, styleText, ...otherProps } = + this.props; const isPrimary = type === 'primary'; let textColor = isPrimary ? themes[theme!].buttonText : themes[theme!].bodyText; @@ -72,7 +74,7 @@ export default class Button extends React.PureComponent, a {loading ? ( ) : ( - + {title} )} diff --git a/app/containers/MessageBox/Mentions/MentionHeaderList.js b/app/containers/MessageBox/Mentions/MentionHeaderList.js new file mode 100644 index 00000000..10b48e4e --- /dev/null +++ b/app/containers/MessageBox/Mentions/MentionHeaderList.js @@ -0,0 +1,49 @@ +import React, { useContext } from 'react'; +import { View, Text, ActivityIndicator, TouchableOpacity } from 'react-native'; +import PropTypes from 'prop-types'; + +import { MENTIONS_TRACKING_TYPE_CANNED } from '../constants'; +import styles from '../styles'; +import sharedStyles from '../../../views/Styles'; +import I18n from '../../../i18n'; +import { themes } from '../../../constants/colors'; +import { CustomIcon } from '../../../lib/Icons'; +import MessageboxContext from '../Context'; + +const MentionHeaderList = ({ trackingType, hasMentions, theme, loading }) => { + const context = useContext(MessageboxContext); + const { onPressNoMatchCanned } = context; + + if (trackingType === MENTIONS_TRACKING_TYPE_CANNED) { + if (loading) { + return ( + + + {I18n.t('Searching')} + + ); + } + + if (!hasMentions) { + return ( + + + {I18n.t('No_match_found')} {I18n.t('Check_canned_responses')} + + + + ); + } + } + + return null; +}; + +MentionHeaderList.propTypes = { + trackingType: PropTypes.string, + hasMentions: PropTypes.bool, + theme: PropTypes.string, + loading: PropTypes.bool +}; + +export default MentionHeaderList; diff --git a/app/containers/MessageBox/Mentions/MentionItem.tsx b/app/containers/MessageBox/Mentions/MentionItem.tsx index 5fc11609..c315b925 100644 --- a/app/containers/MessageBox/Mentions/MentionItem.tsx +++ b/app/containers/MessageBox/Mentions/MentionItem.tsx @@ -6,7 +6,7 @@ import Avatar from '../../Avatar'; import MessageboxContext from '../Context'; import FixedMentionItem from './FixedMentionItem'; import MentionEmoji from './MentionEmoji'; -import { MENTIONS_TRACKING_TYPE_COMMANDS, MENTIONS_TRACKING_TYPE_EMOJIS } from '../constants'; +import { MENTIONS_TRACKING_TYPE_EMOJIS, MENTIONS_TRACKING_TYPE_COMMANDS, MENTIONS_TRACKING_TYPE_CANNED } from '../constants'; import { themes } from '../../../constants/colors'; import { IEmoji } from '../../EmojiPicker/interfaces'; @@ -17,6 +17,8 @@ interface IMessageBoxMentionItem { username: string; t: string; id: string; + shortcut: string; + text: string; } & IEmoji; trackingType: string; theme: string; @@ -32,6 +34,8 @@ const MentionItem = ({ item, trackingType, theme }: IMessageBoxMentionItem) => { return `mention-item-${item.name || item}`; case MENTIONS_TRACKING_TYPE_COMMANDS: return `mention-item-${item.command || item}`; + case MENTIONS_TRACKING_TYPE_CANNED: + return `mention-item-${item.shortcut || item}`; default: return `mention-item-${item.username || item.name || item}`; } @@ -68,6 +72,17 @@ const MentionItem = ({ item, trackingType, theme }: IMessageBoxMentionItem) => { ); } + if (trackingType === MENTIONS_TRACKING_TYPE_CANNED) { + content = ( + <> + !{item.shortcut} + + {item.text} + + + ); + } + return ( { + ({ mentions, trackingType, theme, loading }: IMessageBoxMentions) => { if (!trackingType) { return null; } @@ -21,16 +23,22 @@ const Mentions = React.memo( ( + 0} theme={theme} loading={loading} /> + )} data={mentions} extraData={mentions} renderItem={({ item }) => } - keyExtractor={(item: any) => item.rid || item.name || item.command || item} + keyExtractor={item => item.rid || item.name || item.command || item.shortcut || item} keyboardShouldPersistTaps='always' /> ); }, (prevProps, nextProps) => { + if (prevProps.loading !== nextProps.loading) { + return false; + } if (prevProps.theme !== nextProps.theme) { return false; } diff --git a/app/containers/MessageBox/RecordAudio.tsx b/app/containers/MessageBox/RecordAudio.tsx index af2bcd83..fa6c509e 100644 --- a/app/containers/MessageBox/RecordAudio.tsx +++ b/app/containers/MessageBox/RecordAudio.tsx @@ -17,11 +17,11 @@ interface IMessageBoxRecordAudioProps { onFinish: Function; } -const RECORDING_EXTENSION = '.aac'; +const RECORDING_EXTENSION = '.m4a'; const RECORDING_SETTINGS = { android: { extension: RECORDING_EXTENSION, - outputFormat: Audio.RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AAC_ADTS, + outputFormat: Audio.RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG_4, audioEncoder: Audio.RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC, sampleRate: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.android.sampleRate, numberOfChannels: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.android.numberOfChannels, @@ -39,7 +39,7 @@ const RECORDING_SETTINGS = { const RECORDING_MODE = { allowsRecordingIOS: true, playsInSilentModeIOS: true, - staysActiveInBackground: false, + staysActiveInBackground: true, shouldDuckAndroid: true, playThroughEarpieceAndroid: false, interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX, @@ -63,20 +63,24 @@ export default class RecordAudio extends React.PureComponent { try { const permission = await Audio.getPermissionsAsync(); @@ -108,17 +116,20 @@ export default class RecordAudio extends React.PureComponent { logEvent(events.ROOM_AUDIO_RECORD); if (!this.isRecorderBusy) { this.isRecorderBusy = true; + this.LastDuration = 0; try { const canRecord = await this.isRecordingPermissionGranted(); if (canRecord) { await Audio.setAudioModeAsync(RECORDING_MODE); + this.setState({ isRecorderActive: true }); this.recording = new Audio.Recording(); await this.recording.prepareToRecordAsync(RECORDING_SETTINGS); this.recording.setOnRecordingStatusUpdate(this.onRecordingStatusUpdate); @@ -147,7 +158,7 @@ export default class RecordAudio extends React.PureComponent + + + + + {this.GetLastDuration} + + + + + + ); + } + return ( @@ -207,9 +244,10 @@ export default class RecordAudio extends React.PureComponent - + - {this.duration} + {this.duration} + - + ); diff --git a/app/containers/MessageBox/constants.ts b/app/containers/MessageBox/constants.ts index 8fe37fc8..dabaee49 100644 --- a/app/containers/MessageBox/constants.ts +++ b/app/containers/MessageBox/constants.ts @@ -2,4 +2,5 @@ export const MENTIONS_TRACKING_TYPE_USERS = '@'; export const MENTIONS_TRACKING_TYPE_EMOJIS = ':'; export const MENTIONS_TRACKING_TYPE_COMMANDS = '/'; export const MENTIONS_TRACKING_TYPE_ROOMS = '#'; +export const MENTIONS_TRACKING_TYPE_CANNED = '!'; export const MENTIONS_COUNT_TO_DISPLAY = 4; diff --git a/app/containers/MessageBox/index.tsx b/app/containers/MessageBox/index.tsx index 58d7fd43..047c128b 100644 --- a/app/containers/MessageBox/index.tsx +++ b/app/containers/MessageBox/index.tsx @@ -35,6 +35,7 @@ import Mentions from './Mentions'; import MessageboxContext from './Context'; import { MENTIONS_COUNT_TO_DISPLAY, + MENTIONS_TRACKING_TYPE_CANNED, MENTIONS_TRACKING_TYPE_COMMANDS, MENTIONS_TRACKING_TYPE_EMOJIS, MENTIONS_TRACKING_TYPE_ROOMS, @@ -107,6 +108,7 @@ interface IMessageBoxProps { iOSScrollBehavior: number; sharing: boolean; isActionsEnabled: boolean; + usedCannedResponse: string; } interface IMessageBoxState { @@ -121,6 +123,7 @@ interface IMessageBoxState { appId?: any; }; tshow: boolean; + mentionLoading: boolean; } class MessageBox extends Component { @@ -175,7 +178,8 @@ class MessageBox extends Component { commandPreview: [], showCommandPreview: false, command: {}, - tshow: false + tshow: false, + mentionLoading: false }; this.text = ''; this.selection = { start: 0, end: 0 }; @@ -234,7 +238,7 @@ class MessageBox extends Component { async componentDidMount() { const db = database.active; - const { rid, tmid, navigation, sharing } = this.props; + const { rid, tmid, navigation, sharing, usedCannedResponse, isMasterDetail } = this.props; let msg; try { const threadsCollection = db.get('threads'); @@ -269,6 +273,10 @@ class MessageBox extends Component { EventEmiter.addEventListener(KEY_COMMAND, this.handleCommands); } + if (isMasterDetail && usedCannedResponse) { + this.onChangeText(usedCannedResponse); + } + this.unsubscribeFocus = navigation.addListener('focus', () => { // didFocus // We should wait pushed views be dismissed @@ -285,10 +293,13 @@ class MessageBox extends Component { } UNSAFE_componentWillReceiveProps(nextProps: any) { - const { isFocused, editing, replying, sharing } = this.props; + const { isFocused, editing, replying, sharing, usedCannedResponse } = this.props; if (!isFocused?.()) { return; } + if (usedCannedResponse !== nextProps.usedCannedResponse) { + this.onChangeText(nextProps.usedCannedResponse ?? ''); + } if (sharing) { this.setInput(nextProps.message.msg ?? ''); return; @@ -311,10 +322,9 @@ class MessageBox extends Component { } shouldComponentUpdate(nextProps: any, nextState: any) { - const { showEmojiKeyboard, showSend, recording, mentions, commandPreview, tshow } = this.state; - - const { roomType, replying, editing, isFocused, message, theme } = this.props; + const { showEmojiKeyboard, showSend, recording, mentions, commandPreview, tshow, mentionLoading, trackingType } = this.state; + const { roomType, replying, editing, isFocused, message, theme, usedCannedResponse } = this.props; if (nextProps.theme !== theme) { return true; } @@ -333,6 +343,12 @@ class MessageBox extends Component { if (nextState.showEmojiKeyboard !== showEmojiKeyboard) { return true; } + if (nextState.trackingType !== trackingType) { + return true; + } + if (nextState.mentionLoading !== mentionLoading) { + return true; + } if (nextState.showSend !== showSend) { return true; } @@ -351,6 +367,9 @@ class MessageBox extends Component { if (!dequal(nextProps.message?.id, message?.id)) { return true; } + if (nextProps.usedCannedResponse !== usedCannedResponse) { + return true; + } return false; } @@ -371,6 +390,9 @@ class MessageBox extends Component { if (this.getSlashCommands && this.getSlashCommands.stop) { this.getSlashCommands.stop(); } + if (this.getCannedResponses && this.getCannedResponses.stop) { + this.getCannedResponses.stop(); + } if (this.unsubscribeFocus) { this.unsubscribeFocus(); } @@ -395,7 +417,7 @@ class MessageBox extends Component { // eslint-disable-next-line react/sort-comp debouncedOnChangeText = debounce(async (text: any) => { - const { sharing } = this.props; + const { sharing, roomType } = this.props; const isTextEmpty = text.length === 0; if (isTextEmpty) { this.stopTrackingMention(); @@ -412,6 +434,7 @@ class MessageBox extends Component { const channelMention = lastWord.match(/^#/); const userMention = lastWord.match(/^@/); const emojiMention = lastWord.match(/^:/); + const cannedMention = lastWord.match(/^!/); if (commandMention && !sharing) { const command = text.substr(1); @@ -440,6 +463,9 @@ class MessageBox extends Component { if (emojiMention) { return this.identifyMentionKeyword(result, MENTIONS_TRACKING_TYPE_EMOJIS); } + if (cannedMention && roomType === 'l') { + return this.identifyMentionKeyword(result, MENTIONS_TRACKING_TYPE_CANNED); + } return this.stopTrackingMention(); }, 100); @@ -456,11 +482,17 @@ class MessageBox extends Component { const { start, end } = this.selection; const cursor = Math.max(start, end); const regexp = /([a-z0-9._-]+)$/im; - const result = msg.substr(0, cursor).replace(regexp, ''); + let result = msg.substr(0, cursor).replace(regexp, ''); + // Remove the ! after select the canned response + if (trackingType === MENTIONS_TRACKING_TYPE_CANNED) { + const lastIndexOfExclamation = msg.lastIndexOf('!', cursor); + result = msg.substr(0, lastIndexOfExclamation).replace(regexp, ''); + } const mentionName = - trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ? `${item.name || item}:` : item.username || item.name || item.command; + trackingType === MENTIONS_TRACKING_TYPE_EMOJIS + ? `${item.name || item}:` + : item.username || item.name || item.command || item.text; const text = `${result}${mentionName} ${msg.slice(cursor)}`; - if (trackingType === MENTIONS_TRACKING_TYPE_COMMANDS && item.providesPreview) { this.setState({ showCommandPreview: true }); } @@ -532,12 +564,12 @@ class MessageBox extends Component { getUsers = debounce(async (keyword: any) => { let res = await RocketChat.search({ text: keyword, filterRooms: false, filterUsers: true }); res = [...this.getFixedMentions(keyword), ...res]; - this.setState({ mentions: res }); + this.setState({ mentions: res, mentionLoading: false }); }, 300); getRooms = debounce(async (keyword = '') => { const res = await RocketChat.search({ text: keyword, filterRooms: true, filterUsers: false }); - this.setState({ mentions: res }); + this.setState({ mentions: res, mentionLoading: false }); }, 300); getEmojis = debounce(async (keyword: any) => { @@ -552,7 +584,7 @@ class MessageBox extends Component { customEmojis = customEmojis.slice(0, MENTIONS_COUNT_TO_DISPLAY); const filteredEmojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, MENTIONS_COUNT_TO_DISPLAY); const mergedEmojis = [...customEmojis, ...filteredEmojis].slice(0, MENTIONS_COUNT_TO_DISPLAY); - this.setState({ mentions: mergedEmojis || [] }); + this.setState({ mentions: mergedEmojis || [], mentionLoading: false }); }, 300); getSlashCommands = debounce(async (keyword: any) => { @@ -560,9 +592,14 @@ class MessageBox extends Component { const commandsCollection = db.get('slash_commands'); const likeString = sanitizeLikeString(keyword); const commands = await commandsCollection.query(Q.where('id', Q.like(`${likeString}%`))).fetch(); - this.setState({ mentions: commands || [] }); + this.setState({ mentions: commands || [], mentionLoading: false }); }, 300); + getCannedResponses = debounce(async (text?: string) => { + const res = await RocketChat.getListCannedResponse({ text }); + this.setState({ mentions: res?.cannedResponses || [], mentionLoading: false }); + }, 500); + focus = () => { if (this.component && this.component.focus) { this.component.focus(); @@ -695,6 +732,16 @@ class MessageBox extends Component { } }; + onPressNoMatchCanned = () => { + const { isMasterDetail, rid } = this.props; + const params = { rid }; + if (isMasterDetail) { + Navigation.navigate('ModalStackNavigator', { screen: 'CannedResponsesListView', params }); + } else { + Navigation.navigate('CannedResponsesListView', params); + } + }; + openShareView = (attachments: any) => { const { message, replyCancel, replyWithMention } = this.props; // Start a thread with an attachment @@ -854,6 +901,8 @@ class MessageBox extends Component { this.getEmojis(keyword); } else if (type === MENTIONS_TRACKING_TYPE_COMMANDS) { this.getSlashCommands(keyword); + } else if (type === MENTIONS_TRACKING_TYPE_CANNED) { + this.getCannedResponses(keyword); } else { this.getRooms(keyword); } @@ -862,7 +911,8 @@ class MessageBox extends Component { identifyMentionKeyword = (keyword: any, type: string) => { this.setState({ showEmojiKeyboard: false, - trackingType: type + trackingType: type, + mentionLoading: true }); this.updateMentions(keyword, type); }; @@ -918,7 +968,8 @@ class MessageBox extends Component { }; renderContent = () => { - const { recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview } = this.state; + const { recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview, mentionLoading } = + this.state; const { editing, message, @@ -950,8 +1001,7 @@ class MessageBox extends Component { const commandsPreviewAndMentions = !recording ? ( <> - {/* @ts-ignore*/} - + ) : null; @@ -1039,7 +1089,8 @@ class MessageBox extends Component { user, baseUrl, onPressMention: this.onPressMention, - onPressCommandPreview: this.onPressCommandPreview + onPressCommandPreview: this.onPressCommandPreview, + onPressNoMatchCanned: this.onPressNoMatchCanned }}> (this.tracking = ref)} diff --git a/app/containers/MessageBox/styles.ts b/app/containers/MessageBox/styles.ts index 9c5a2ff5..6f459909 100644 --- a/app/containers/MessageBox/styles.ts +++ b/app/containers/MessageBox/styles.ts @@ -36,6 +36,30 @@ export default StyleSheet.create({ width: 60, height: 48 }, + wrapMentionHeaderList: { + height: MENTION_HEIGHT, + justifyContent: 'center' + }, + wrapMentionHeaderListRow: { + height: MENTION_HEIGHT, + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 12 + }, + loadingPaddingHeader: { + paddingRight: 12 + }, + mentionHeaderList: { + fontSize: 14, + ...sharedStyles.textMedium + }, + mentionHeaderListNoMatchFound: { + fontSize: 14, + ...sharedStyles.textRegular + }, + mentionNoMatchHeader: { + justifyContent: 'space-between' + }, mentionList: { maxHeight: MENTION_HEIGHT * 4 }, @@ -67,6 +91,18 @@ export default StyleSheet.create({ fontSize: 14, ...sharedStyles.textRegular }, + cannedMentionText: { + flex: 1, + fontSize: 14, + paddingRight: 12, + ...sharedStyles.textRegular + }, + cannedItem: { + fontSize: 14, + ...sharedStyles.textBold, + paddingLeft: 12, + paddingRight: 8 + }, emojiKeyboardContainer: { flex: 1, borderTopWidth: StyleSheet.hairlineWidth @@ -103,7 +139,8 @@ export default StyleSheet.create({ flex: 1, justifyContent: 'space-between' }, - recordingCancelText: { + recordingDurationText: { + width: 60, fontSize: 16, ...sharedStyles.textRegular }, diff --git a/app/containers/SearchHeader.js b/app/containers/SearchHeader.js new file mode 100644 index 00000000..e231d074 --- /dev/null +++ b/app/containers/SearchHeader.js @@ -0,0 +1,49 @@ +import React from 'react'; +import { StyleSheet, View } from 'react-native'; +import PropTypes from 'prop-types'; + +import { withTheme } from '../theme'; +import sharedStyles from '../views/Styles'; +import { themes } from '../constants/colors'; +import TextInput from '../presentation/TextInput'; +import { isIOS, isTablet } from '../utils/deviceInfo'; +import { useOrientation } from '../dimensions'; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + marginLeft: 0 + }, + title: { + ...sharedStyles.textSemibold + } +}); + +// TODO: it might be useful to refactor this component for reusage +const SearchHeader = ({ theme, onSearchChangeText }) => { + const titleColorStyle = { color: themes[theme].headerTitleColor }; + const isLight = theme === 'light'; + const { isLandscape } = useOrientation(); + const scale = isIOS && isLandscape && !isTablet ? 0.8 : 1; + const titleFontSize = 16 * scale; + + return ( + + + + ); +}; + +SearchHeader.propTypes = { + theme: PropTypes.string, + onSearchChangeText: PropTypes.func +}; +export default withTheme(SearchHeader); diff --git a/app/containers/TextInput.tsx b/app/containers/TextInput.tsx index ae11dcc1..f9c2236a 100644 --- a/app/containers/TextInput.tsx +++ b/app/containers/TextInput.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; -import { BorderlessButton } from 'react-native-gesture-handler'; +import Touchable from 'react-native-platform-touchable'; import sharedStyles from '../views/Styles'; import TextInput from '../presentation/TextInput'; @@ -95,9 +95,9 @@ export default class RCTextInput extends React.PureComponent + - + ); } @@ -105,14 +105,14 @@ export default class RCTextInput extends React.PureComponent + - + ); } diff --git a/app/containers/UIKit/MultiSelect/Input.tsx b/app/containers/UIKit/MultiSelect/Input.tsx index 611838c0..e03837a2 100644 --- a/app/containers/UIKit/MultiSelect/Input.tsx +++ b/app/containers/UIKit/MultiSelect/Input.tsx @@ -12,18 +12,19 @@ interface IInput { onPress: Function; theme: string; inputStyle: object; - disabled: boolean; - placeholder: string; - loading: boolean; + disabled?: boolean | object; + placeholder?: string; + loading?: boolean; + innerInputStyle?: object; } -const Input = ({ children, onPress, theme, loading, inputStyle, placeholder, disabled }: IInput) => ( +const Input = ({ children, onPress, theme, loading, inputStyle, placeholder, disabled, innerInputStyle }: IInput) => ( - + {placeholder ? {placeholder} : children} {loading ? ( diff --git a/app/containers/UIKit/MultiSelect/index.tsx b/app/containers/UIKit/MultiSelect/index.tsx index 95bedbeb..d50dede3 100644 --- a/app/containers/UIKit/MultiSelect/index.tsx +++ b/app/containers/UIKit/MultiSelect/index.tsx @@ -28,6 +28,7 @@ interface IMultiSelect { value?: any[]; disabled?: boolean | object; theme: string; + innerInputStyle?: object; } const ANIMATION_DURATION = 200; @@ -53,7 +54,8 @@ export const MultiSelect = React.memo( onClose = () => {}, disabled, inputStyle, - theme + theme, + innerInputStyle }: IMultiSelect) => { const [selected, select] = useState(Array.isArray(values) ? values : []); const [open, setOpen] = useState(false); @@ -143,8 +145,13 @@ export const MultiSelect = React.memo( let button = multiselect ? (