diff --git a/README.md b/README.md
index e4e7bdecf..50e97ea19 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 56448a036..0ad82c0d0 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 d18b7869f..42a73ea3b 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 33e246b55..bf7dc5ef7 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 d50ac2137..98573a99f 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 00d851010..f7f08bb27 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 9ced0c6c2..fe6981d67 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 14ad8fb36..fb9c7e6b5 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 f926f01e7..35e69c700 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 4f57776d1..9e475a679 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 000000000..10b48e4e9
--- /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 5fc11609e..c315b9250 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 af2bcd832..fa6c509ef 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 8fe37fc8c..dabaee497 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 58d7fd436..047c128bd 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 9c5a2ff52..6f4599090 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 000000000..e231d0748
--- /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 ae11dcc17..f9c2236a8 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 611838c08..e03837a2b 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 95bedbebd..d50dede31 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 ? (
) : (
- // @ts-ignore
-
+
{currentValue || placeholder.text}
@@ -154,8 +161,13 @@ export const MultiSelect = React.memo(
if (context === BLOCK_CONTEXT.FORM) {
const items: any = options.filter((option: any) => selected.includes(option.value));
button = (
- // @ts-ignore
-
+
{items.length ? (
(disabled ? {} : onSelect(item))} theme={theme} />
) : (
diff --git a/app/containers/message/Audio.tsx b/app/containers/message/Audio.tsx
index 7e4bb555b..958ae8ee8 100644
--- a/app/containers/message/Audio.tsx
+++ b/app/containers/message/Audio.tsx
@@ -43,7 +43,7 @@ interface IMessageAudioState {
const mode = {
allowsRecordingIOS: false,
playsInSilentModeIOS: true,
- staysActiveInBackground: false,
+ staysActiveInBackground: true,
shouldDuckAndroid: true,
playThroughEarpieceAndroid: false,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json
index cdb11b3e9..a1f3e29d0 100644
--- a/app/i18n/locales/en.json
+++ b/app/i18n/locales/en.json
@@ -773,5 +773,14 @@
"Converting_Team_To_Channel": "Converting Team to Channel",
"Select_Team_Channels_To_Delete": "Select the Team’s Channels you would like to delete, the ones you do not select will be moved to the Workspace. \n\nNotice that public Channels will be public and visible to everyone.",
"You_are_converting_the_team": "You are converting this Team to a Channel",
- "creating_discussion": "creating discussion"
+ "creating_discussion": "creating discussion",
+ "Canned_Responses": "Canned Responses",
+ "No_match_found": "No match found.",
+ "Check_canned_responses": "Check on canned responses.",
+ "Searching": "Searching",
+ "Use": "Use",
+ "Shortcut": "Shortcut",
+ "Content": "Content",
+ "Sharing": "Sharing",
+ "No_canned_responses": "No canned responses"
}
diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json
index 98675ab6d..57c31c067 100644
--- a/app/i18n/locales/pt-BR.json
+++ b/app/i18n/locales/pt-BR.json
@@ -672,5 +672,13 @@
"Left_The_Room_Successfully": "Saiu da sala com sucesso",
"Deleted_The_Team_Successfully": "Time deletado com sucesso",
"Deleted_The_Room_Successfully": "Sala deletada com sucesso",
- "Convert_to_Channel": "Converter para um Canal"
+ "Convert_to_Channel": "Converter para um Canal",
+ "Canned_Responses": "Respostas Predefinidas",
+ "No_match_found": "Nenhum resultado encontrado",
+ "Check_canned_responses": "Verifique nas respostas predefinidas",
+ "Searching": "Buscando",
+ "Use": "Use",
+ "Shortcut": "Atalho",
+ "Content": "Conteúdo",
+ "No_canned_responses": "Não há respostas predefinidas"
}
diff --git a/app/lib/database/index.js b/app/lib/database/index.js
index dfda88a98..ee02e6212 100644
--- a/app/lib/database/index.js
+++ b/app/lib/database/index.js
@@ -60,8 +60,7 @@ export const getDatabase = (database = '') => {
Permission,
SlashCommand,
User
- ],
- actionsEnabled: true
+ ]
});
};
@@ -73,8 +72,7 @@ class DB {
schema: serversSchema,
migrations: serversMigrations
}),
- modelClasses: [Server, LoggedUser, ServersHistory],
- actionsEnabled: true
+ modelClasses: [Server, LoggedUser, ServersHistory]
})
};
@@ -117,8 +115,7 @@ class DB {
FrequentlyUsedEmoji,
Setting,
User
- ],
- actionsEnabled: true
+ ]
});
}
diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js
index 0e5a996ca..d23268f39 100644
--- a/app/lib/methods/getPermissions.js
+++ b/app/lib/methods/getPermissions.js
@@ -50,7 +50,8 @@ const PERMISSIONS = [
'view-all-team-channels',
'convert-team',
'edit-omnichannel-contact',
- 'edit-livechat-room-customfields'
+ 'edit-livechat-room-customfields',
+ 'view-canned-responses'
];
export async function setPermissions() {
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index fa5167330..fa8a47b18 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -1049,7 +1049,7 @@ const RocketChat = {
}
return this.post('subscriptions.read', { rid: roomId });
},
- getRoomMembers({ rid, allUsers, roomType, type, filter, skip = 0, limit = 10 }) {
+ async getRoomMembers({ rid, allUsers, roomType, type, filter, skip = 0, limit = 10 }) {
const serverVersion = reduxStore.getState().server.version;
if (compareServerVersion(serverVersion, '3.16.0', methods.greaterThanOrEqualTo)) {
const params = {
@@ -1060,10 +1060,12 @@ const RocketChat = {
...(filter && { filter })
};
// RC 3.16.0
- return this.sdk.get(`${this.roomTypeToApiType(roomType)}.members`, params);
+ const result = await this.sdk.get(`${this.roomTypeToApiType(roomType)}.members`, params);
+ return result?.members;
}
// RC 0.42.0
- return this.methodCallWrapper('getUsersOfRoom', rid, allUsers, { skip, limit });
+ const result = await this.methodCallWrapper('getUsersOfRoom', rid, allUsers, { skip, limit });
+ return result?.records;
},
methodCallWrapper(method, ...params) {
const { API_Use_REST_For_DDP_Calls } = reduxStore.getState().settings;
@@ -1171,6 +1173,19 @@ const RocketChat = {
return this.sdk.get('livechat/custom-fields');
},
+ getListCannedResponse({ scope = '', departmentId = '', offset = 0, count = 25, text = '' }) {
+ const params = {
+ offset,
+ count,
+ ...(departmentId && { departmentId }),
+ ...(text && { text }),
+ ...(scope && { scope })
+ };
+
+ // RC 3.17.0
+ return this.sdk.get('canned-responses', params);
+ },
+
getUidDirectMessage(room) {
const { id: userId } = reduxStore.getState().login.user;
diff --git a/app/presentation/ImageViewer/ImageViewer.android.tsx b/app/presentation/ImageViewer/ImageViewer.android.tsx
index 4c8cf5959..cbb79c7fe 100644
--- a/app/presentation/ImageViewer/ImageViewer.android.tsx
+++ b/app/presentation/ImageViewer/ImageViewer.android.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { PanGestureHandler, PinchGestureHandler, State } from 'react-native-gesture-handler';
-import Animated, { Easing } from 'react-native-reanimated';
+import Animated, { EasingNode } from 'react-native-reanimated';
import { ImageComponent } from './ImageComponent';
import { themes } from '../../constants/colors';
@@ -94,7 +94,7 @@ function runTiming(clock: any, value: any, dest: any, startStopClock: any = true
const config = {
toValue: new Value(0),
duration: 300,
- easing: Easing.inOut(Easing.cubic)
+ easing: EasingNode.inOut(EasingNode.cubic)
};
return [
@@ -215,7 +215,9 @@ class Image extends React.PureComponent {
return ;
}
}
-const AnimatedImage = Animated.createAnimatedComponent(Image);
+
+// https://github.com/software-mansion/react-native-reanimated/issues/1717
+const AnimatedImage: any = Animated.createAnimatedComponent(Image);
// it was picked from https://github.com/software-mansion/react-native-reanimated/tree/master/Example/imageViewer
// and changed to use FastImage animated component
diff --git a/app/reducers/server.js b/app/reducers/server.js
index f3934a68d..14c7bbfdf 100644
--- a/app/reducers/server.js
+++ b/app/reducers/server.js
@@ -7,7 +7,6 @@ const initialState = {
server: '',
version: null,
loading: true,
- adding: false,
previousServer: null,
changingServer: false
};
@@ -58,13 +57,11 @@ export default function server(state = initialState, action) {
case SERVER.INIT_ADD:
return {
...state,
- adding: true,
previousServer: action.previousServer
};
case SERVER.FINISH_ADD:
return {
...state,
- adding: false,
previousServer: null
};
default:
diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js
index 959a0802d..efad7afae 100644
--- a/app/sagas/deepLinking.js
+++ b/app/sagas/deepLinking.js
@@ -8,7 +8,7 @@ import { inviteLinksRequest, inviteLinksSetToken } from '../actions/inviteLinks'
import database from '../lib/database';
import RocketChat from '../lib/rocketchat';
import EventEmitter from '../utils/events';
-import { ROOT_INSIDE, ROOT_NEW_SERVER, appInit, appStart } from '../actions/app';
+import { ROOT_INSIDE, ROOT_OUTSIDE, appInit, appStart } from '../actions/app';
import { localAuthenticate } from '../utils/localAuthentication';
import { goRoom } from '../utils/goRoom';
import { loginRequest } from '../actions/login';
@@ -180,7 +180,7 @@ const handleOpen = function* handleOpen({ params }) {
yield fallbackNavigation();
return;
}
- yield put(appStart({ root: ROOT_NEW_SERVER }));
+ yield put(appStart({ root: ROOT_OUTSIDE }));
yield put(serverInitAdd(server));
yield delay(1000);
EventEmitter.emit('NewServer', { server: host });
diff --git a/app/sagas/login.js b/app/sagas/login.js
index 516599c3d..c3f987054 100644
--- a/app/sagas/login.js
+++ b/app/sagas/login.js
@@ -118,8 +118,6 @@ const fetchRooms = function* fetchRooms() {
const handleLoginSuccess = function* handleLoginSuccess({ user }) {
try {
- const adding = yield select(state => state.server.adding);
-
RocketChat.getUserPresence(user.id);
const server = yield select(getServer);
@@ -170,24 +168,10 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
yield put(setUser(user));
EventEmitter.emit('connected');
- let currentRoot;
- if (adding) {
- yield put(serverFinishAdd());
- yield put(appStart({ root: ROOT_INSIDE }));
- } else {
- currentRoot = yield select(state => state.app.root);
- if (currentRoot !== ROOT_INSIDE) {
- yield put(appStart({ root: ROOT_INSIDE }));
- }
- }
-
- // after a successful login, check if it's been invited via invite link
- currentRoot = yield select(state => state.app.root);
- if (currentRoot === ROOT_INSIDE) {
- const inviteLinkToken = yield select(state => state.inviteLinks.token);
- if (inviteLinkToken) {
- yield put(inviteLinksRequest(inviteLinkToken));
- }
+ yield put(appStart({ root: ROOT_INSIDE }));
+ const inviteLinkToken = yield select(state => state.inviteLinks.token);
+ if (inviteLinkToken) {
+ yield put(inviteLinksRequest(inviteLinkToken));
}
} catch (e) {
log(e);
diff --git a/app/stacks/InsideStack.js b/app/stacks/InsideStack.js
index 9f11a4db5..bf01d551f 100644
--- a/app/stacks/InsideStack.js
+++ b/app/stacks/InsideStack.js
@@ -30,6 +30,8 @@ import ThreadMessagesView from '../views/ThreadMessagesView';
import TeamChannelsView from '../views/TeamChannelsView';
import MarkdownTableView from '../views/MarkdownTableView';
import ReadReceiptsView from '../views/ReadReceiptView';
+import CannedResponsesListView from '../views/CannedResponsesListView';
+import CannedResponseDetail from '../views/CannedResponseDetail';
import { themes } from '../constants/colors';
// Profile Stack
import ProfileView from '../views/ProfileView';
@@ -138,6 +140,16 @@ const ChatsStackNavigator = () => {
+
+
);
};
diff --git a/app/stacks/MasterDetailStack/index.js b/app/stacks/MasterDetailStack/index.js
index 02b5fe46c..9de407e54 100644
--- a/app/stacks/MasterDetailStack/index.js
+++ b/app/stacks/MasterDetailStack/index.js
@@ -24,6 +24,8 @@ import DirectoryView from '../../views/DirectoryView';
import NotificationPrefView from '../../views/NotificationPreferencesView';
import VisitorNavigationView from '../../views/VisitorNavigationView';
import ForwardLivechatView from '../../views/ForwardLivechatView';
+import CannedResponsesListView from '../../views/CannedResponsesListView';
+import CannedResponseDetail from '../../views/CannedResponseDetail';
import LivechatEditView from '../../views/LivechatEditView';
import PickerView from '../../views/PickerView';
import ThreadMessagesView from '../../views/ThreadMessagesView';
@@ -160,6 +162,16 @@ const ModalStackNavigator = React.memo(({ navigation }) => {
component={ForwardLivechatView}
options={ForwardLivechatView.navigationOptions}
/>
+
+
diff --git a/app/stacks/OutsideStack.js b/app/stacks/OutsideStack.js
index 0e8a8f62e..f23e65c3d 100644
--- a/app/stacks/OutsideStack.js
+++ b/app/stacks/OutsideStack.js
@@ -1,13 +1,11 @@
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
import { ThemeContext } from '../theme';
import { ModalAnimation, StackAnimation, defaultHeader, themedHeader } from '../utils/navigation';
// Outside Stack
-import OnboardingView from '../views/OnboardingView';
import NewServerView from '../views/NewServerView';
import WorkspaceView from '../views/WorkspaceView';
import LoginView from '../views/LoginView';
@@ -15,18 +13,14 @@ import ForgotPasswordView from '../views/ForgotPasswordView';
import RegisterView from '../views/RegisterView';
import LegalView from '../views/LegalView';
import AuthenticationWebView from '../views/AuthenticationWebView';
-import { ROOT_OUTSIDE } from '../actions/app';
// Outside
const Outside = createStackNavigator();
-const _OutsideStack = ({ root }) => {
+const _OutsideStack = () => {
const { theme } = React.useContext(ThemeContext);
return (
- {root === ROOT_OUTSIDE ? (
-
- ) : null}
@@ -41,10 +35,6 @@ const mapStateToProps = state => ({
root: state.app.root
});
-_OutsideStack.propTypes = {
- root: PropTypes.string
-};
-
const OutsideStack = connect(mapStateToProps)(_OutsideStack);
// OutsideStackModal
diff --git a/app/utils/log/events.js b/app/utils/log/events.js
index c39ccc424..4a3ec38df 100644
--- a/app/utils/log/events.js
+++ b/app/utils/log/events.js
@@ -1,9 +1,4 @@
export default {
- // ONBOARDING VIEW
- ONBOARD_JOIN_A_WORKSPACE: 'onboard_join_a_workspace',
- ONBOARD_CREATE_NEW_WORKSPACE: 'onboard_create_new_workspace',
- ONBOARD_CREATE_NEW_WORKSPACE_F: 'onboard_create_new_workspace_f',
-
// NEW SERVER VIEW
NS_CONNECT_TO_WORKSPACE: 'ns_connect_to_workspace',
NS_JOIN_OPEN_WORKSPACE: 'ns_join_open_workspace',
@@ -78,6 +73,7 @@ export default {
RL_GROUP_CHANNELS_BY_TYPE: 'rl_group_channels_by_type',
RL_GROUP_CHANNELS_BY_FAVORITE: 'rl_group_channels_by_favorite',
RL_GROUP_CHANNELS_BY_UNREAD: 'rl_group_channels_by_unread',
+ RL_CREATE_NEW_WORKSPACE: 'rl_create_new_workspace',
// QUEUE LIST VIEW
QL_GO_ROOM: 'ql_go_room',
diff --git a/app/utils/scaling.js b/app/utils/scaling.js
index f154a50a9..7cf33f1fc 100644
--- a/app/utils/scaling.js
+++ b/app/utils/scaling.js
@@ -1,14 +1,11 @@
-import { Dimensions } from 'react-native';
-
import { isTablet } from './deviceInfo';
-const { width, height } = Dimensions.get('window');
-
const guidelineBaseWidth = isTablet ? 600 : 375;
const guidelineBaseHeight = isTablet ? 800 : 667;
-const scale = size => (width / guidelineBaseWidth) * size;
-const verticalScale = size => (height / guidelineBaseHeight) * size;
-const moderateScale = (size, factor = 0.5) => size + (scale(size) - size) * factor;
+// TODO: we need to refactor this
+const scale = (size, width) => (width / guidelineBaseWidth) * size;
+const verticalScale = (size, height) => (height / guidelineBaseHeight) * size;
+const moderateScale = (size, factor = 0.5, width) => size + (scale(size, width) - size) * factor;
export { scale, verticalScale, moderateScale };
diff --git a/app/views/CannedResponseDetail.js b/app/views/CannedResponseDetail.js
new file mode 100644
index 000000000..67002bbdb
--- /dev/null
+++ b/app/views/CannedResponseDetail.js
@@ -0,0 +1,171 @@
+import React, { useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { StyleSheet, Text, View, ScrollView } from 'react-native';
+import { useSelector } from 'react-redux';
+
+import I18n from '../i18n';
+import SafeAreaView from '../containers/SafeAreaView';
+import StatusBar from '../containers/StatusBar';
+import Button from '../containers/Button';
+import { useTheme } from '../theme';
+import RocketChat from '../lib/rocketchat';
+import Navigation from '../lib/Navigation';
+import { goRoom } from '../utils/goRoom';
+import { themes } from '../constants/colors';
+import Markdown from '../containers/markdown';
+import sharedStyles from './Styles';
+
+const styles = StyleSheet.create({
+ scroll: {
+ flex: 1
+ },
+ container: {
+ flex: 1,
+ marginTop: 12,
+ marginHorizontal: 15
+ },
+ cannedText: {
+ marginTop: 8,
+ marginBottom: 16,
+ fontSize: 14,
+ paddingTop: 0,
+ paddingBottom: 0,
+ ...sharedStyles.textRegular
+ },
+ cannedTagWrap: {
+ borderRadius: 4,
+ marginRight: 4,
+ marginTop: 8,
+ height: 16
+ },
+ cannedTagContainer: {
+ flexDirection: 'row',
+ flexWrap: 'wrap'
+ },
+ cannedTag: {
+ fontSize: 12,
+ paddingTop: 0,
+ paddingBottom: 0,
+ paddingHorizontal: 4,
+ ...sharedStyles.textRegular
+ },
+ button: {
+ margin: 24,
+ marginBottom: 24
+ },
+ item: {
+ paddingVertical: 10,
+ justifyContent: 'center'
+ },
+ itemLabel: {
+ marginBottom: 10,
+ fontSize: 14,
+ ...sharedStyles.textMedium
+ },
+ itemContent: {
+ fontSize: 14,
+ ...sharedStyles.textRegular
+ }
+});
+
+const Item = ({ label, content, theme, testID }) =>
+ content ? (
+
+
+ {label}
+
+
+
+ ) : null;
+Item.propTypes = {
+ label: PropTypes.string,
+ content: PropTypes.string,
+ theme: PropTypes.string,
+ testID: PropTypes.string
+};
+
+const CannedResponseDetail = ({ navigation, route }) => {
+ const { cannedResponse } = route?.params;
+ const { theme } = useTheme();
+ const { isMasterDetail } = useSelector(state => state.app);
+ const { rooms } = useSelector(state => state.room);
+
+ useEffect(() => {
+ navigation.setOptions({
+ title: `!${cannedResponse?.shortcut}`
+ });
+ }, []);
+
+ const navigateToRoom = item => {
+ const { room } = route.params;
+ const { name, username } = room;
+ const params = {
+ rid: room.rid,
+ name: RocketChat.getRoomTitle({
+ t: room.t,
+ fname: name,
+ name: username
+ }),
+ t: room.t,
+ roomUserId: RocketChat.getUidDirectMessage(room),
+ usedCannedResponse: item.text
+ };
+
+ if (room.rid) {
+ // 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 });
+ } else {
+ let navigate = navigation.push;
+ // if this is a room focused
+ if (rooms.includes(room.rid)) {
+ ({ navigate } = navigation);
+ }
+ navigate('RoomView', params);
+ }
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ {I18n.t('Tags')}
+
+ {cannedResponse?.tags?.length > 0 ? (
+ cannedResponse.tags.map(t => (
+
+ {t}
+
+ ))
+ ) : (
+ -
+ )}
+
+
+
+
+
+ );
+};
+
+CannedResponseDetail.propTypes = {
+ navigation: PropTypes.object,
+ route: PropTypes.object
+};
+
+export default CannedResponseDetail;
diff --git a/app/views/CannedResponsesListView/CannedResponseItem.js b/app/views/CannedResponsesListView/CannedResponseItem.js
new file mode 100644
index 000000000..cc5f3c39a
--- /dev/null
+++ b/app/views/CannedResponsesListView/CannedResponseItem.js
@@ -0,0 +1,61 @@
+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 }) => (
+
+ <>
+
+
+ !{shortcut}
+ {scope}
+
+
+
+
+
+
+ “{text}”
+
+
+ {tags?.length > 0
+ ? tags.map(t => (
+
+ {t}
+
+ ))
+ : null}
+
+ >
+
+);
+
+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/CannedResponseItem.stories.js b/app/views/CannedResponsesListView/CannedResponseItem.stories.js
new file mode 100644
index 000000000..5e49f8507
--- /dev/null
+++ b/app/views/CannedResponsesListView/CannedResponseItem.stories.js
@@ -0,0 +1,64 @@
+/* eslint-disable import/no-extraneous-dependencies */
+import React from 'react';
+import { storiesOf } from '@storybook/react-native';
+
+import CannedResponseItem from './CannedResponseItem';
+
+const stories = storiesOf('CannedResponseItem', module);
+
+const item = [
+ {
+ _id: 'x1-x1-x1',
+ shortcut: '!FAQ4',
+ text: 'ZCVXZVXCZVZXVZXCVZXCVXZCVZX',
+ scope: 'user',
+ userId: 'xxx-x-xx-x-x-',
+ createdBy: {
+ _id: 'xxx-x-xx-x-x-',
+ username: 'rocket.cat'
+ },
+ _createdAt: '2021-08-11T01:23:17.379Z',
+ _updatedAt: '2021-08-11T01:23:17.379Z',
+ scopeName: 'Private'
+ },
+ {
+ _id: 'x1-1x-1x',
+ shortcut: 'test4mobilePrivate',
+ text: 'test for mobile private',
+ scope: 'user',
+ tags: ['HQ', 'Closed', 'HQ', 'Problem in Product Y', 'HQ', 'Closed', 'Problem in Product Y'],
+ userId: 'laslsaklasal',
+ createdBy: {
+ _id: 'laslsaklasal',
+ username: 'reinaldo.neto'
+ },
+ _createdAt: '2021-09-02T17:44:52.095Z',
+ _updatedAt: '2021-09-02T18:24:40.436Z',
+ scopeName: 'Private'
+ }
+];
+
+const theme = 'light';
+
+stories.add('Itens', () => (
+ <>
+ alert('navigation to CannedResponseDetail')}
+ onPressUse={() => alert('Back to RoomView and wrote in MessageBox')}
+ />
+ alert('navigation to CannedResponseDetail')}
+ onPressUse={() => alert('Back to RoomView and wrote in MessageBox')}
+ />
+ >
+));
diff --git a/app/views/CannedResponsesListView/Dropdown/DropdownItem.js b/app/views/CannedResponsesListView/Dropdown/DropdownItem.js
new file mode 100644
index 000000000..85b9aa703
--- /dev/null
+++ b/app/views/CannedResponsesListView/Dropdown/DropdownItem.js
@@ -0,0 +1,44 @@
+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/DropdownItemFilter.js b/app/views/CannedResponsesListView/Dropdown/DropdownItemFilter.js
new file mode 100644
index 000000000..d4e457805
--- /dev/null
+++ b/app/views/CannedResponsesListView/Dropdown/DropdownItemFilter.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import DropdownItem from './DropdownItem';
+
+const DropdownItemFilter = ({ currentDepartment, value, onPress }) => (
+ onPress(value)}
+ />
+);
+
+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
new file mode 100644
index 000000000..4f1f2b68f
--- /dev/null
+++ b/app/views/CannedResponsesListView/Dropdown/DropdownItemHeader.js
@@ -0,0 +1,15 @@
+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/index.js b/app/views/CannedResponsesListView/Dropdown/index.js
new file mode 100644
index 000000000..e2735e5c5
--- /dev/null
+++ b/app/views/CannedResponsesListView/Dropdown/index.js
@@ -0,0 +1,106 @@
+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 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
+ };
+
+ constructor(props) {
+ super(props);
+ this.animatedValue = new Animated.Value(0);
+ }
+
+ componentDidMount() {
+ Animated.timing(this.animatedValue, {
+ toValue: 1,
+ duration: ANIMATION_DURATION,
+ easing: Easing.inOut(Easing.quad),
+ useNativeDriver: true
+ }).start();
+ }
+
+ close = () => {
+ const { onClose } = this.props;
+ Animated.timing(this.animatedValue, {
+ toValue: 0,
+ duration: ANIMATION_DURATION,
+ easing: Easing.inOut(Easing.quad),
+ useNativeDriver: true
+ }).start(() => onClose());
+ };
+
+ render() {
+ const { isMasterDetail, insets, theme, currentDepartment, onDepartmentSelected, departments } = this.props;
+ const statusBarHeight = insets?.top ?? 0;
+ const heightDestination = isMasterDetail ? headerHeight + statusBarHeight : 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]
+ });
+
+ const maxRows = 5;
+ return (
+ <>
+
+
+
+
+
+
+ item._id}
+ renderItem={({ item }) => (
+
+ )}
+ keyboardShouldPersistTaps='always'
+ />
+
+ >
+ );
+ }
+}
+
+export default withTheme(withSafeAreaInsets(Dropdown));
diff --git a/app/views/CannedResponsesListView/index.js b/app/views/CannedResponsesListView/index.js
new file mode 100644
index 000000000..f9f515deb
--- /dev/null
+++ b/app/views/CannedResponsesListView/index.js
@@ -0,0 +1,363 @@
+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 database from '../../lib/database';
+import I18n from '../../i18n';
+import SafeAreaView from '../../containers/SafeAreaView';
+import StatusBar from '../../containers/StatusBar';
+import ActivityIndicator from '../../containers/ActivityIndicator';
+import SearchHeader from '../../containers/SearchHeader';
+import BackgroundContainer from '../../containers/BackgroundContainer';
+import { getHeaderTitlePosition } from '../../containers/Header';
+import { useTheme } from '../../theme';
+import RocketChat from '../../lib/rocketchat';
+import debounce from '../../utils/debounce';
+import Navigation from '../../lib/Navigation';
+import { goRoom } from '../../utils/goRoom';
+import * as HeaderButton from '../../containers/HeaderButton';
+import * as List from '../../containers/List';
+import { themes } from '../../constants/colors';
+import log from '../../utils/log';
+import CannedResponseItem from './CannedResponseItem';
+import Dropdown from './Dropdown';
+import DropdownItemHeader from './Dropdown/DropdownItemHeader';
+import styles from './styles';
+
+const COUNT = 25;
+
+const fixedScopes = [
+ {
+ _id: 'all',
+ name: I18n.t('All')
+ },
+ {
+ _id: 'global',
+ name: I18n.t('Public')
+ },
+ {
+ _id: 'user',
+ name: I18n.t('Private')
+ }
+];
+
+const CannedResponsesListView = ({ navigation, route }) => {
+ 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);
+ const [currentDepartment, setCurrentDepartment] = useState(fixedScopes[0]);
+ const [showFilterDropdown, setShowFilterDropDown] = useState(false);
+
+ // states used to do a fetch by onChangeText, onDepartmentSelect and onEndReached
+ const [searchText, setSearchText] = useState('');
+ const [scope, setScope] = useState('');
+ const [departmentId, setDepartmentId] = useState('');
+ const [loading, setLoading] = useState(true);
+ const [offset, setOffset] = useState(0);
+
+ const insets = useSafeAreaInsets();
+ const { theme } = useTheme();
+ const { isMasterDetail } = useSelector(state => state.app);
+ const { rooms } = useSelector(state => state.room);
+
+ const getRoomFromDb = async () => {
+ const { rid } = route.params;
+ const db = database.active;
+ const subsCollection = db.get('subscriptions');
+ try {
+ const r = await subsCollection.find(rid);
+ setRoom(r);
+ } catch (error) {
+ console.log('CannedResponsesListView: Room not found');
+ log(error);
+ }
+ };
+
+ const getDepartments = debounce(async () => {
+ try {
+ const res = await RocketChat.getDepartments();
+ if (res.success) {
+ setDepartments([...fixedScopes, ...res.departments]);
+ }
+ } catch (e) {
+ setDepartments(fixedScopes);
+ log(e);
+ }
+ }, 300);
+
+ const goToDetail = item => {
+ navigation.navigate('CannedResponseDetail', { cannedResponse: item, room });
+ };
+
+ const navigateToRoom = item => {
+ if (!room) {
+ return;
+ }
+ const { name, username } = room;
+ const params = {
+ rid: room.rid,
+ name: RocketChat.getRoomTitle({
+ t: room.t,
+ fname: name,
+ name: username
+ }),
+ t: room.t,
+ roomUserId: RocketChat.getUidDirectMessage(room),
+ usedCannedResponse: item.text
+ };
+
+ if (room.rid) {
+ // 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 });
+ } else {
+ let navigate = navigation.push;
+ // if this is a room focused
+ if (rooms.includes(room.rid)) {
+ ({ navigate } = navigation);
+ }
+ navigate('RoomView', params);
+ }
+ }
+ };
+
+ const getListCannedResponse = async ({ text, department, depId, debounced }) => {
+ try {
+ const res = await RocketChat.getListCannedResponse({
+ text,
+ offset,
+ count: COUNT,
+ departmentId: depId,
+ scope: department
+ });
+ if (res.success) {
+ // search with changes on text or scope are debounced
+ // the begin result and pagination aren't debounced
+ setCannedResponses(prevCanned => (debounced ? res.cannedResponses : [...prevCanned, ...res.cannedResponses]));
+ setLoading(false);
+ setOffset(prevOffset => prevOffset + COUNT);
+ }
+ } catch (e) {
+ log(e);
+ }
+ };
+
+ useEffect(() => {
+ if (departments.length > 0) {
+ const newCannedResponses = cannedResponses.map(cr => {
+ let scopeName = '';
+
+ if (cr?.departmentId) {
+ scopeName = departments.filter(dep => dep._id === cr.departmentId)[0]?.name || 'Department';
+ } else {
+ scopeName = departments.filter(dep => dep._id === cr.scope)[0]?.name;
+ }
+ cr.scopeName = scopeName;
+
+ return cr;
+ });
+ setCannedResponsesScopeName(newCannedResponses);
+ }
+ }, [departments, cannedResponses]);
+
+ const searchCallback = useCallback(
+ debounce(async (text = '', department = '', depId = '') => {
+ await getListCannedResponse({ text, department, depId, debounced: true });
+ }, 1000),
+ []
+ ); // use debounce with useCallback https://stackoverflow.com/a/58594890
+
+ useEffect(() => {
+ getRoomFromDb();
+ getDepartments();
+ getListCannedResponse({ text: '', department: '', depId: '', debounced: false });
+ }, []);
+
+ const newSearch = () => {
+ setCannedResponses([]);
+ setLoading(true);
+ setOffset(0);
+ };
+
+ const onChangeText = text => {
+ newSearch();
+ setSearchText(text);
+ searchCallback(text, scope, departmentId);
+ };
+
+ const onDepartmentSelect = value => {
+ let department = '';
+ let depId = '';
+
+ if (value._id === fixedScopes[0]._id) {
+ department = '';
+ } else if (value._id === fixedScopes[1]._id) {
+ department = 'global';
+ } else if (value._id === fixedScopes[2]._id) {
+ department = 'user';
+ } else {
+ department = 'department';
+ depId = value._id;
+ }
+
+ newSearch();
+ setCurrentDepartment(value);
+ setScope(department);
+ setDepartmentId(depId);
+ setShowFilterDropDown(false);
+ searchCallback(searchText, department, depId);
+ };
+
+ const onEndReached = async () => {
+ if (cannedResponses.length < offset || loading) {
+ return;
+ }
+ setLoading(true);
+ await getListCannedResponse({ text: searchText, department: scope, depId: departmentId, debounced: false });
+ };
+
+ const getHeader = () => {
+ if (isSearching) {
+ const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 1 });
+ return {
+ headerTitleAlign: 'left',
+ headerLeft: () => (
+
+ {
+ onChangeText();
+ setIsSearching(false);
+ }}
+ />
+
+ ),
+ headerTitle: () => ,
+ headerTitleContainerStyle: {
+ left: headerTitlePosition.left,
+ right: headerTitlePosition.right
+ },
+ headerRight: () => null
+ };
+ }
+
+ const options = {
+ headerLeft: () => (
+ navigation.pop()} tintColor={themes[theme].headerTintColor} />
+ ),
+ headerTitleAlign: 'center',
+ headerTitle: I18n.t('Canned_Responses'),
+ headerTitleContainerStyle: {
+ left: null,
+ right: null
+ }
+ };
+
+ if (isMasterDetail) {
+ options.headerLeft = () => ;
+ }
+
+ options.headerRight = () => (
+
+ setIsSearching(true)} />
+
+ );
+ return options;
+ };
+
+ const setHeader = () => {
+ const options = getHeader();
+ navigation.setOptions(options);
+ };
+
+ useEffect(() => {
+ setHeader();
+ }, [isSearching]);
+
+ const showDropdown = () => {
+ if (isSearching) {
+ setSearchText('');
+ setIsSearching(false);
+ }
+ setShowFilterDropDown(true);
+ };
+
+ const renderFlatListHeader = () => {
+ if (!departments.length) {
+ return null;
+ }
+ return (
+ <>
+
+
+ >
+ );
+ };
+
+ const renderContent = () => {
+ if (!cannedResponsesScopeName.length && !loading) {
+ return (
+ <>
+ {renderFlatListHeader()}
+
+ >
+ );
+ }
+ return (
+ (
+ goToDetail(item)}
+ onPressUse={() => navigateToRoom(item)}
+ />
+ )}
+ keyExtractor={item => item._id || item.shortcut}
+ ListHeaderComponent={renderFlatListHeader}
+ stickyHeaderIndices={[0]}
+ onEndReached={onEndReached}
+ onEndReachedThreshold={0.5}
+ ItemSeparatorComponent={List.Separator}
+ ListFooterComponent={loading ? : null}
+ />
+ );
+ };
+
+ return (
+
+
+ {renderContent()}
+ {showFilterDropdown ? (
+ setShowFilterDropDown(false)}
+ />
+ ) : null}
+
+ );
+};
+
+CannedResponsesListView.propTypes = {
+ navigation: PropTypes.object,
+ route: PropTypes.object
+};
+
+export default CannedResponsesListView;
diff --git a/app/views/CannedResponsesListView/styles.js b/app/views/CannedResponsesListView/styles.js
new file mode 100644
index 000000000..d9b8f580c
--- /dev/null
+++ b/app/views/CannedResponsesListView/styles.js
@@ -0,0 +1,73 @@
+import { StyleSheet } from 'react-native';
+
+import sharedStyles from '../Styles';
+
+export default StyleSheet.create({
+ list: {
+ flex: 1
+ },
+ dropdownContainer: {
+ width: '100%',
+ position: 'absolute',
+ top: 0,
+ borderBottomWidth: StyleSheet.hairlineWidth
+ },
+ backdrop: {
+ ...StyleSheet.absoluteFill
+ },
+ wrapCannedItem: {
+ minHeight: 117,
+ maxHeight: 141,
+ padding: 16
+ },
+ cannedRow: {
+ flexDirection: 'row',
+ height: 36
+ },
+ cannedWrapShortcutScope: {
+ flex: 1
+ },
+ cannedShortcut: {
+ flex: 1,
+ fontSize: 14,
+ paddingTop: 0,
+ paddingBottom: 0,
+ ...sharedStyles.textMedium
+ },
+ cannedScope: {
+ flex: 1,
+ fontSize: 12,
+ paddingTop: 0,
+ paddingBottom: 0,
+ ...sharedStyles.textRegular
+ },
+ cannedText: {
+ marginTop: 8,
+ fontSize: 14,
+ paddingTop: 0,
+ paddingBottom: 0,
+ ...sharedStyles.textRegular
+ },
+ cannedTagContainer: {
+ flexDirection: 'row',
+ overflow: 'hidden'
+ },
+ cannedTagWrap: {
+ borderRadius: 4,
+ marginRight: 4,
+ marginTop: 8,
+ height: 16
+ },
+ cannedTag: {
+ fontSize: 12,
+ paddingTop: 0,
+ paddingBottom: 0,
+ paddingHorizontal: 4,
+ ...sharedStyles.textRegular
+ },
+ cannedUseButton: {
+ height: 28,
+ width: 56,
+ marginLeft: 8
+ }
+});
diff --git a/app/views/NewServerView/ServerInput/index.js b/app/views/NewServerView/ServerInput/index.js
index 16d8159d1..ef80c4685 100644
--- a/app/views/NewServerView/ServerInput/index.js
+++ b/app/views/NewServerView/ServerInput/index.js
@@ -10,9 +10,7 @@ import Item from './Item';
const styles = StyleSheet.create({
container: {
- zIndex: 1,
- marginTop: 24,
- marginBottom: 32
+ zIndex: 1
},
inputContainer: {
marginTop: 0,
diff --git a/app/views/NewServerView/index.js b/app/views/NewServerView/index.js
index e7bffce67..8eaba349e 100644
--- a/app/views/NewServerView/index.js
+++ b/app/views/NewServerView/index.js
@@ -1,15 +1,16 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { BackHandler, Keyboard, StyleSheet, Text, View } from 'react-native';
+import { Text, Keyboard, StyleSheet, View, BackHandler, Image } from 'react-native';
import { connect } from 'react-redux';
import { Base64 } from 'js-base64';
import parse from 'url-parse';
import { Q } from '@nozbe/watermelondb';
import { TouchableOpacity } from 'react-native-gesture-handler';
+import Orientation from 'react-native-orientation-locker';
import UserPreferences from '../../lib/userPreferences';
import EventEmitter from '../../utils/events';
-import { selectServerRequest, serverRequest } from '../../actions/server';
+import { selectServerRequest, serverRequest, serverFinishAdd as serverFinishAddAction } from '../../actions/server';
import { inviteLinksClear as inviteLinksClearAction } from '../../actions/inviteLinks';
import sharedStyles from '../Styles';
import Button from '../../containers/Button';
@@ -27,31 +28,38 @@ import database from '../../lib/database';
import { sanitizeLikeString } from '../../lib/database/utils';
import SSLPinning from '../../utils/sslPinning';
import RocketChat from '../../lib/rocketchat';
+import { isTablet } from '../../utils/deviceInfo';
+import { verticalScale, moderateScale } from '../../utils/scaling';
+import { withDimensions } from '../../dimensions';
import ServerInput from './ServerInput';
const styles = StyleSheet.create({
+ onboardingImage: {
+ alignSelf: 'center',
+ resizeMode: 'contain'
+ },
title: {
...sharedStyles.textBold,
- fontSize: 22
+ letterSpacing: 0,
+ alignSelf: 'center'
+ },
+ subtitle: {
+ ...sharedStyles.textRegular,
+ alignSelf: 'center'
},
certificatePicker: {
- marginBottom: 32,
alignItems: 'center',
justifyContent: 'flex-end'
},
chooseCertificateTitle: {
- fontSize: 13,
...sharedStyles.textRegular
},
chooseCertificate: {
- fontSize: 13,
...sharedStyles.textSemibold
},
description: {
...sharedStyles.textRegular,
- fontSize: 14,
- textAlign: 'left',
- marginBottom: 24
+ textAlign: 'center'
},
connectButton: {
marginBottom: 0
@@ -59,23 +67,22 @@ const styles = StyleSheet.create({
});
class NewServerView extends React.Component {
- static navigationOptions = () => ({
- title: I18n.t('Workspaces')
- });
-
static propTypes = {
navigation: PropTypes.object,
theme: PropTypes.string,
connecting: PropTypes.bool.isRequired,
connectServer: PropTypes.func.isRequired,
selectServer: PropTypes.func.isRequired,
- adding: PropTypes.bool,
previousServer: PropTypes.string,
- inviteLinksClear: PropTypes.func
+ inviteLinksClear: PropTypes.func,
+ serverFinishAdd: PropTypes.func
};
constructor(props) {
super(props);
+ if (!isTablet) {
+ Orientation.lockToPortrait();
+ }
this.setHeader();
this.state = {
@@ -92,25 +99,27 @@ class NewServerView extends React.Component {
this.queryServerHistory();
}
- componentDidUpdate(prevProps) {
- const { adding } = this.props;
- if (prevProps.adding !== adding) {
- this.setHeader();
- }
- }
-
componentWillUnmount() {
EventEmitter.removeListener('NewServer', this.handleNewServerEvent);
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
+ const { previousServer, serverFinishAdd } = this.props;
+ if (previousServer) {
+ serverFinishAdd();
+ }
}
setHeader = () => {
- const { adding, navigation } = this.props;
- if (adding) {
- navigation.setOptions({
+ const { previousServer, navigation } = this.props;
+ if (previousServer) {
+ return navigation.setOptions({
+ headerTitle: I18n.t('Workspaces'),
headerLeft: () =>
});
}
+
+ return navigation.setOptions({
+ headerShown: false
+ });
};
handleBackPress = () => {
@@ -273,16 +282,26 @@ class NewServerView extends React.Component {
renderCertificatePicker = () => {
const { certificate } = this.state;
- const { theme } = this.props;
+ const { theme, width, height, previousServer } = this.props;
return (
-
-
+
+
{certificate ? I18n.t('Your_certificate') : I18n.t('Do_you_have_a_certificate')}
-
+
{certificate ?? I18n.t('Apply_Your_Certificate')}
@@ -291,12 +310,48 @@ class NewServerView extends React.Component {
};
render() {
- const { connecting, theme } = this.props;
+ const { connecting, theme, previousServer, width, height } = this.props;
const { text, connectingOpen, serversHistory } = this.state;
+ const marginTop = previousServer ? 0 : 35;
+
return (
- {I18n.t('Join_your_workspace')}
+
+
+ Rocket.Chat
+
+
+ {I18n.t('Onboarding_subtitle')}
+
-
+
{I18n.t('Onboarding_join_open_description')}