[NEW] Canned responses (#3355)

Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Reinaldo Neto 2021-09-22 14:29:26 -03:00 committed by GitHub
parent 0871849de8
commit a52199a49c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1836 additions and 53 deletions

View File

@ -1322,6 +1322,593 @@ exports[`Storyshots BackgroundContainer text 1`] = `
</View> </View>
`; `;
exports[`Storyshots CannedResponseItem Itens 1`] = `
Array [
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"backgroundColor": "#ffffff",
"maxHeight": 141,
"minHeight": 117,
"opacity": 1,
"padding": 16,
}
}
>
<View
style={
Object {
"flexDirection": "row",
"height": 36,
}
}
>
<View
style={
Object {
"flex": 1,
}
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"flex": 1,
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "500",
"paddingBottom": 0,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#0d0e12",
},
]
}
>
!
!FAQ4
</Text>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"flex": 1,
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
Private
</Text>
</View>
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"backgroundColor": "#f3f4f5",
"borderRadius": 2,
"height": 28,
"justifyContent": "center",
"marginBottom": 12,
"marginLeft": 8,
"opacity": 1,
"paddingHorizontal": 14,
"width": 56,
}
}
>
<Text
accessibilityLabel="Use"
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 16,
"fontWeight": "500",
"textAlign": "center",
},
Object {
"color": "#0d0e12",
},
Object {
"fontSize": 12,
},
]
}
>
Use
</Text>
</View>
</View>
<Text
ellipsizeMode="tail"
numberOfLines={2}
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "400",
"marginTop": 8,
"paddingBottom": 0,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
ZCVXZVXCZVZXVZXCVZXCVXZCVZX
</Text>
<View
style={
Object {
"flexDirection": "row",
"overflow": "hidden",
}
}
/>
</View>,
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"backgroundColor": "#ffffff",
"maxHeight": 141,
"minHeight": 117,
"opacity": 1,
"padding": 16,
}
}
>
<View
style={
Object {
"flexDirection": "row",
"height": 36,
}
}
>
<View
style={
Object {
"flex": 1,
}
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"flex": 1,
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "500",
"paddingBottom": 0,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#0d0e12",
},
]
}
>
!
test4mobilePrivate
</Text>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"flex": 1,
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
Private
</Text>
</View>
<View
accessible={true}
focusable={true}
onClick={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"backgroundColor": "#f3f4f5",
"borderRadius": 2,
"height": 28,
"justifyContent": "center",
"marginBottom": 12,
"marginLeft": 8,
"opacity": 1,
"paddingHorizontal": 14,
"width": 56,
}
}
>
<Text
accessibilityLabel="Use"
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 16,
"fontWeight": "500",
"textAlign": "center",
},
Object {
"color": "#0d0e12",
},
Object {
"fontSize": 12,
},
]
}
>
Use
</Text>
</View>
</View>
<Text
ellipsizeMode="tail"
numberOfLines={2}
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 14,
"fontWeight": "400",
"marginTop": 8,
"paddingBottom": 0,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
test for mobile private
</Text>
<View
style={
Object {
"flexDirection": "row",
"overflow": "hidden",
}
}
>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 16,
"marginRight": 4,
"marginTop": 8,
},
Object {
"backgroundColor": "#E6E6E7",
},
]
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingHorizontal": 4,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
HQ
</Text>
</View>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 16,
"marginRight": 4,
"marginTop": 8,
},
Object {
"backgroundColor": "#E6E6E7",
},
]
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingHorizontal": 4,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
Closed
</Text>
</View>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 16,
"marginRight": 4,
"marginTop": 8,
},
Object {
"backgroundColor": "#E6E6E7",
},
]
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingHorizontal": 4,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
HQ
</Text>
</View>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 16,
"marginRight": 4,
"marginTop": 8,
},
Object {
"backgroundColor": "#E6E6E7",
},
]
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingHorizontal": 4,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
Problem in Product Y
</Text>
</View>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 16,
"marginRight": 4,
"marginTop": 8,
},
Object {
"backgroundColor": "#E6E6E7",
},
]
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingHorizontal": 4,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
HQ
</Text>
</View>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 16,
"marginRight": 4,
"marginTop": 8,
},
Object {
"backgroundColor": "#E6E6E7",
},
]
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingHorizontal": 4,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
Closed
</Text>
</View>
<View
style={
Array [
Object {
"borderRadius": 4,
"height": 16,
"marginRight": 4,
"marginTop": 8,
},
Object {
"backgroundColor": "#E6E6E7",
},
]
}
>
<Text
style={
Array [
Object {
"backgroundColor": "transparent",
"fontFamily": "System",
"fontSize": 12,
"fontWeight": "400",
"paddingBottom": 0,
"paddingHorizontal": 4,
"paddingTop": 0,
"textAlign": "left",
},
Object {
"color": "#6C727A",
},
]
}
>
Problem in Product Y
</Text>
</View>
</View>
</View>,
]
`;
exports[`Storyshots Header Buttons badge 1`] = ` exports[`Storyshots Header Buttons badge 1`] = `
<RNCSafeAreaView <RNCSafeAreaView
edges={ edges={

View File

@ -202,5 +202,8 @@ export default {
}, },
Jitsi_Enable_Channels: { Jitsi_Enable_Channels: {
type: 'valuesAsBoolean' type: 'valuesAsBoolean'
},
Canned_Responses_Enable: {
type: 'valueAsBoolean'
} }
}; };

View File

@ -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 (
<View style={styles.wrapMentionHeaderListRow}>
<ActivityIndicator style={styles.loadingPaddingHeader} size='small' />
<Text style={[styles.mentionHeaderList, { color: themes[theme].auxiliaryText }]}>{I18n.t('Searching')}</Text>
</View>
);
}
if (!hasMentions) {
return (
<TouchableOpacity style={[styles.wrapMentionHeaderListRow, styles.mentionNoMatchHeader]} onPress={onPressNoMatchCanned}>
<Text style={[styles.mentionHeaderListNoMatchFound, { color: themes[theme].auxiliaryText }]}>
{I18n.t('No_match_found')} <Text style={sharedStyles.textSemibold}>{I18n.t('Check_canned_responses')}</Text>
</Text>
<CustomIcon name='chevron-right' size={24} color={themes[theme].auxiliaryText} />
</TouchableOpacity>
);
}
}
return null;
};
MentionHeaderList.propTypes = {
trackingType: PropTypes.string,
hasMentions: PropTypes.bool,
theme: PropTypes.string,
loading: PropTypes.bool
};
export default MentionHeaderList;

View File

@ -6,7 +6,7 @@ import Avatar from '../../Avatar';
import MessageboxContext from '../Context'; import MessageboxContext from '../Context';
import FixedMentionItem from './FixedMentionItem'; import FixedMentionItem from './FixedMentionItem';
import MentionEmoji from './MentionEmoji'; 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 { themes } from '../../../constants/colors';
import { IEmoji } from '../../EmojiPicker/interfaces'; import { IEmoji } from '../../EmojiPicker/interfaces';
@ -17,6 +17,8 @@ interface IMessageBoxMentionItem {
username: string; username: string;
t: string; t: string;
id: string; id: string;
shortcut: string;
text: string;
} & IEmoji; } & IEmoji;
trackingType: string; trackingType: string;
theme: string; theme: string;
@ -32,6 +34,8 @@ const MentionItem = ({ item, trackingType, theme }: IMessageBoxMentionItem) => {
return `mention-item-${item.name || item}`; return `mention-item-${item.name || item}`;
case MENTIONS_TRACKING_TYPE_COMMANDS: case MENTIONS_TRACKING_TYPE_COMMANDS:
return `mention-item-${item.command || item}`; return `mention-item-${item.command || item}`;
case MENTIONS_TRACKING_TYPE_CANNED:
return `mention-item-${item.shortcut || item}`;
default: default:
return `mention-item-${item.username || item.name || item}`; 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 = (
<>
<Text style={[styles.cannedItem, { color: themes[theme].titleText }]}>!{item.shortcut}</Text>
<Text numberOfLines={1} style={[styles.cannedMentionText, { color: themes[theme].auxiliaryTintColor }]}>
{item.text}
</Text>
</>
);
}
return ( return (
<TouchableOpacity <TouchableOpacity
style={[ style={[

View File

@ -2,18 +2,20 @@ import React from 'react';
import { FlatList, View } from 'react-native'; import { FlatList, View } from 'react-native';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import MentionHeaderList from './MentionHeaderList';
import styles from '../styles'; import styles from '../styles';
import MentionItem from './MentionItem'; import MentionItem from './MentionItem';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
interface IMessageBoxMentions { interface IMessageBoxMentions {
mentions: []; mentions: any[];
trackingType: string; trackingType: string;
theme: string; theme: string;
loading: boolean;
} }
const Mentions = React.memo( const Mentions = React.memo(
({ mentions, trackingType, theme }: IMessageBoxMentions) => { ({ mentions, trackingType, theme, loading }: IMessageBoxMentions) => {
if (!trackingType) { if (!trackingType) {
return null; return null;
} }
@ -21,16 +23,22 @@ const Mentions = React.memo(
<View testID='messagebox-container'> <View testID='messagebox-container'>
<FlatList <FlatList
style={[styles.mentionList, { backgroundColor: themes[theme].auxiliaryBackground }]} style={[styles.mentionList, { backgroundColor: themes[theme].auxiliaryBackground }]}
ListHeaderComponent={() => (
<MentionHeaderList trackingType={trackingType} hasMentions={mentions.length > 0} theme={theme} loading={loading} />
)}
data={mentions} data={mentions}
extraData={mentions} extraData={mentions}
renderItem={({ item }) => <MentionItem item={item} trackingType={trackingType} theme={theme} />} renderItem={({ item }) => <MentionItem item={item} trackingType={trackingType} theme={theme} />}
keyExtractor={(item: any) => item.rid || item.name || item.command || item} keyExtractor={item => item.rid || item.name || item.command || item.shortcut || item}
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
/> />
</View> </View>
); );
}, },
(prevProps, nextProps) => { (prevProps, nextProps) => {
if (prevProps.loading !== nextProps.loading) {
return false;
}
if (prevProps.theme !== nextProps.theme) { if (prevProps.theme !== nextProps.theme) {
return false; return false;
} }

View File

@ -2,4 +2,5 @@ export const MENTIONS_TRACKING_TYPE_USERS = '@';
export const MENTIONS_TRACKING_TYPE_EMOJIS = ':'; export const MENTIONS_TRACKING_TYPE_EMOJIS = ':';
export const MENTIONS_TRACKING_TYPE_COMMANDS = '/'; export const MENTIONS_TRACKING_TYPE_COMMANDS = '/';
export const MENTIONS_TRACKING_TYPE_ROOMS = '#'; export const MENTIONS_TRACKING_TYPE_ROOMS = '#';
export const MENTIONS_TRACKING_TYPE_CANNED = '!';
export const MENTIONS_COUNT_TO_DISPLAY = 4; export const MENTIONS_COUNT_TO_DISPLAY = 4;

View File

@ -35,6 +35,7 @@ import Mentions from './Mentions';
import MessageboxContext from './Context'; import MessageboxContext from './Context';
import { import {
MENTIONS_COUNT_TO_DISPLAY, MENTIONS_COUNT_TO_DISPLAY,
MENTIONS_TRACKING_TYPE_CANNED,
MENTIONS_TRACKING_TYPE_COMMANDS, MENTIONS_TRACKING_TYPE_COMMANDS,
MENTIONS_TRACKING_TYPE_EMOJIS, MENTIONS_TRACKING_TYPE_EMOJIS,
MENTIONS_TRACKING_TYPE_ROOMS, MENTIONS_TRACKING_TYPE_ROOMS,
@ -107,6 +108,7 @@ interface IMessageBoxProps {
iOSScrollBehavior: number; iOSScrollBehavior: number;
sharing: boolean; sharing: boolean;
isActionsEnabled: boolean; isActionsEnabled: boolean;
usedCannedResponse: string;
} }
interface IMessageBoxState { interface IMessageBoxState {
@ -121,6 +123,7 @@ interface IMessageBoxState {
appId?: any; appId?: any;
}; };
tshow: boolean; tshow: boolean;
mentionLoading: boolean;
} }
class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> { class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
@ -175,7 +178,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
commandPreview: [], commandPreview: [],
showCommandPreview: false, showCommandPreview: false,
command: {}, command: {},
tshow: false tshow: false,
mentionLoading: false
}; };
this.text = ''; this.text = '';
this.selection = { start: 0, end: 0 }; this.selection = { start: 0, end: 0 };
@ -234,7 +238,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
async componentDidMount() { async componentDidMount() {
const db = database.active; const db = database.active;
const { rid, tmid, navigation, sharing } = this.props; const { rid, tmid, navigation, sharing, usedCannedResponse, isMasterDetail } = this.props;
let msg; let msg;
try { try {
const threadsCollection = db.get('threads'); const threadsCollection = db.get('threads');
@ -269,6 +273,10 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
EventEmiter.addEventListener(KEY_COMMAND, this.handleCommands); EventEmiter.addEventListener(KEY_COMMAND, this.handleCommands);
} }
if (isMasterDetail && usedCannedResponse) {
this.onChangeText('');
}
this.unsubscribeFocus = navigation.addListener('focus', () => { this.unsubscribeFocus = navigation.addListener('focus', () => {
// didFocus // didFocus
// We should wait pushed views be dismissed // We should wait pushed views be dismissed
@ -285,10 +293,13 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
} }
UNSAFE_componentWillReceiveProps(nextProps: any) { UNSAFE_componentWillReceiveProps(nextProps: any) {
const { isFocused, editing, replying, sharing } = this.props; const { isFocused, editing, replying, sharing, usedCannedResponse } = this.props;
if (!isFocused?.()) { if (!isFocused?.()) {
return; return;
} }
if (usedCannedResponse !== nextProps.usedCannedResponse) {
this.onChangeText(nextProps.usedCannedResponse ?? '');
}
if (sharing) { if (sharing) {
this.setInput(nextProps.message.msg ?? ''); this.setInput(nextProps.message.msg ?? '');
return; return;
@ -311,10 +322,9 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
} }
shouldComponentUpdate(nextProps: any, nextState: any) { shouldComponentUpdate(nextProps: any, nextState: any) {
const { showEmojiKeyboard, showSend, recording, mentions, commandPreview, tshow } = this.state; const { showEmojiKeyboard, showSend, recording, mentions, commandPreview, tshow, mentionLoading, trackingType } = this.state;
const { roomType, replying, editing, isFocused, message, theme } = this.props;
const { roomType, replying, editing, isFocused, message, theme, usedCannedResponse } = this.props;
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
return true; return true;
} }
@ -333,6 +343,12 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (nextState.showEmojiKeyboard !== showEmojiKeyboard) { if (nextState.showEmojiKeyboard !== showEmojiKeyboard) {
return true; return true;
} }
if (nextState.trackingType !== trackingType) {
return true;
}
if (nextState.mentionLoading !== mentionLoading) {
return true;
}
if (nextState.showSend !== showSend) { if (nextState.showSend !== showSend) {
return true; return true;
} }
@ -351,6 +367,9 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (!dequal(nextProps.message?.id, message?.id)) { if (!dequal(nextProps.message?.id, message?.id)) {
return true; return true;
} }
if (nextProps.usedCannedResponse !== usedCannedResponse) {
return true;
}
return false; return false;
} }
@ -371,6 +390,9 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (this.getSlashCommands && this.getSlashCommands.stop) { if (this.getSlashCommands && this.getSlashCommands.stop) {
this.getSlashCommands.stop(); this.getSlashCommands.stop();
} }
if (this.getCannedResponses && this.getCannedResponses.stop) {
this.getCannedResponses.stop();
}
if (this.unsubscribeFocus) { if (this.unsubscribeFocus) {
this.unsubscribeFocus(); this.unsubscribeFocus();
} }
@ -395,7 +417,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
debouncedOnChangeText = debounce(async (text: any) => { debouncedOnChangeText = debounce(async (text: any) => {
const { sharing } = this.props; const { sharing, roomType } = this.props;
const isTextEmpty = text.length === 0; const isTextEmpty = text.length === 0;
if (isTextEmpty) { if (isTextEmpty) {
this.stopTrackingMention(); this.stopTrackingMention();
@ -412,6 +434,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const channelMention = lastWord.match(/^#/); const channelMention = lastWord.match(/^#/);
const userMention = lastWord.match(/^@/); const userMention = lastWord.match(/^@/);
const emojiMention = lastWord.match(/^:/); const emojiMention = lastWord.match(/^:/);
const cannedMention = lastWord.match(/^!/);
if (commandMention && !sharing) { if (commandMention && !sharing) {
const command = text.substr(1); const command = text.substr(1);
@ -440,6 +463,9 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (emojiMention) { if (emojiMention) {
return this.identifyMentionKeyword(result, MENTIONS_TRACKING_TYPE_EMOJIS); return this.identifyMentionKeyword(result, MENTIONS_TRACKING_TYPE_EMOJIS);
} }
if (cannedMention && roomType === 'l') {
return this.identifyMentionKeyword(result, MENTIONS_TRACKING_TYPE_CANNED);
}
return this.stopTrackingMention(); return this.stopTrackingMention();
}, 100); }, 100);
@ -456,11 +482,17 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const { start, end } = this.selection; const { start, end } = this.selection;
const cursor = Math.max(start, end); const cursor = Math.max(start, end);
const regexp = /([a-z0-9._-]+)$/im; 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 = 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)}`; const text = `${result}${mentionName} ${msg.slice(cursor)}`;
if (trackingType === MENTIONS_TRACKING_TYPE_COMMANDS && item.providesPreview) { if (trackingType === MENTIONS_TRACKING_TYPE_COMMANDS && item.providesPreview) {
this.setState({ showCommandPreview: true }); this.setState({ showCommandPreview: true });
} }
@ -532,12 +564,12 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
getUsers = debounce(async (keyword: any) => { getUsers = debounce(async (keyword: any) => {
let res = await RocketChat.search({ text: keyword, filterRooms: false, filterUsers: true }); let res = await RocketChat.search({ text: keyword, filterRooms: false, filterUsers: true });
res = [...this.getFixedMentions(keyword), ...res]; res = [...this.getFixedMentions(keyword), ...res];
this.setState({ mentions: res }); this.setState({ mentions: res, mentionLoading: false });
}, 300); }, 300);
getRooms = debounce(async (keyword = '') => { getRooms = debounce(async (keyword = '') => {
const res = await RocketChat.search({ text: keyword, filterRooms: true, filterUsers: false }); const res = await RocketChat.search({ text: keyword, filterRooms: true, filterUsers: false });
this.setState({ mentions: res }); this.setState({ mentions: res, mentionLoading: false });
}, 300); }, 300);
getEmojis = debounce(async (keyword: any) => { getEmojis = debounce(async (keyword: any) => {
@ -552,7 +584,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
customEmojis = customEmojis.slice(0, MENTIONS_COUNT_TO_DISPLAY); customEmojis = customEmojis.slice(0, MENTIONS_COUNT_TO_DISPLAY);
const filteredEmojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).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); const mergedEmojis = [...customEmojis, ...filteredEmojis].slice(0, MENTIONS_COUNT_TO_DISPLAY);
this.setState({ mentions: mergedEmojis || [] }); this.setState({ mentions: mergedEmojis || [], mentionLoading: false });
}, 300); }, 300);
getSlashCommands = debounce(async (keyword: any) => { getSlashCommands = debounce(async (keyword: any) => {
@ -560,9 +592,14 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const commandsCollection = db.get('slash_commands'); const commandsCollection = db.get('slash_commands');
const likeString = sanitizeLikeString(keyword); const likeString = sanitizeLikeString(keyword);
const commands = await commandsCollection.query(Q.where('id', Q.like(`${likeString}%`))).fetch(); const commands = await commandsCollection.query(Q.where('id', Q.like(`${likeString}%`))).fetch();
this.setState({ mentions: commands || [] }); this.setState({ mentions: commands || [], mentionLoading: false });
}, 300); }, 300);
getCannedResponses = debounce(async (text?: string) => {
const res = await RocketChat.getListCannedResponse({ text });
this.setState({ mentions: res?.cannedResponses || [], mentionLoading: false });
}, 500);
focus = () => { focus = () => {
if (this.component && this.component.focus) { if (this.component && this.component.focus) {
this.component.focus(); this.component.focus();
@ -695,6 +732,16 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
} }
}; };
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) => { openShareView = (attachments: any) => {
const { message, replyCancel, replyWithMention } = this.props; const { message, replyCancel, replyWithMention } = this.props;
// Start a thread with an attachment // Start a thread with an attachment
@ -854,6 +901,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
this.getEmojis(keyword); this.getEmojis(keyword);
} else if (type === MENTIONS_TRACKING_TYPE_COMMANDS) { } else if (type === MENTIONS_TRACKING_TYPE_COMMANDS) {
this.getSlashCommands(keyword); this.getSlashCommands(keyword);
} else if (type === MENTIONS_TRACKING_TYPE_CANNED) {
this.getCannedResponses(keyword);
} else { } else {
this.getRooms(keyword); this.getRooms(keyword);
} }
@ -862,7 +911,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
identifyMentionKeyword = (keyword: any, type: string) => { identifyMentionKeyword = (keyword: any, type: string) => {
this.setState({ this.setState({
showEmojiKeyboard: false, showEmojiKeyboard: false,
trackingType: type trackingType: type,
mentionLoading: true
}); });
this.updateMentions(keyword, type); this.updateMentions(keyword, type);
}; };
@ -918,7 +968,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}; };
renderContent = () => { renderContent = () => {
const { recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview } = this.state; const { recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview, mentionLoading } =
this.state;
const { const {
editing, editing,
message, message,
@ -950,8 +1001,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const commandsPreviewAndMentions = !recording ? ( const commandsPreviewAndMentions = !recording ? (
<> <>
<CommandsPreview commandPreview={commandPreview} showCommandPreview={showCommandPreview} /> <CommandsPreview commandPreview={commandPreview} showCommandPreview={showCommandPreview} />
{/* @ts-ignore*/} <Mentions mentions={mentions} trackingType={trackingType} theme={theme} loading={mentionLoading} />
<Mentions mentions={mentions} trackingType={trackingType} theme={theme} />
</> </>
) : null; ) : null;
@ -1039,7 +1089,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
user, user,
baseUrl, baseUrl,
onPressMention: this.onPressMention, onPressMention: this.onPressMention,
onPressCommandPreview: this.onPressCommandPreview onPressCommandPreview: this.onPressCommandPreview,
onPressNoMatchCanned: this.onPressNoMatchCanned
}}> }}>
<KeyboardAccessoryView <KeyboardAccessoryView
ref={(ref: any) => (this.tracking = ref)} ref={(ref: any) => (this.tracking = ref)}

View File

@ -36,6 +36,30 @@ export default StyleSheet.create({
width: 60, width: 60,
height: 48 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: { mentionList: {
maxHeight: MENTION_HEIGHT * 4 maxHeight: MENTION_HEIGHT * 4
}, },
@ -67,6 +91,18 @@ export default StyleSheet.create({
fontSize: 14, fontSize: 14,
...sharedStyles.textRegular ...sharedStyles.textRegular
}, },
cannedMentionText: {
flex: 1,
fontSize: 14,
paddingRight: 12,
...sharedStyles.textRegular
},
cannedItem: {
fontSize: 14,
...sharedStyles.textBold,
paddingLeft: 12,
paddingRight: 8
},
emojiKeyboardContainer: { emojiKeyboardContainer: {
flex: 1, flex: 1,
borderTopWidth: StyleSheet.hairlineWidth borderTopWidth: StyleSheet.hairlineWidth

View File

@ -2,12 +2,12 @@ import React from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withTheme } from '../../theme'; import { withTheme } from '../theme';
import sharedStyles from '../Styles'; import sharedStyles from '../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../constants/colors';
import TextInput from '../../presentation/TextInput'; import TextInput from '../presentation/TextInput';
import { isIOS, isTablet } from '../../utils/deviceInfo'; import { isIOS, isTablet } from '../utils/deviceInfo';
import { useOrientation } from '../../dimensions'; import { useOrientation } from '../dimensions';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {

View File

@ -12,18 +12,19 @@ interface IInput {
onPress: Function; onPress: Function;
theme: string; theme: string;
inputStyle: object; inputStyle: object;
disabled: boolean; disabled?: boolean | object;
placeholder: string; placeholder?: string;
loading: boolean; 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) => (
<Touchable <Touchable
onPress={onPress} onPress={onPress}
style={[{ backgroundColor: themes[theme].backgroundColor }, inputStyle]} style={[{ backgroundColor: themes[theme].backgroundColor }, inputStyle]}
background={Touchable.Ripple(themes[theme].bannerBackground)} background={Touchable.Ripple(themes[theme].bannerBackground)}
disabled={disabled}> disabled={disabled}>
<View style={[styles.input, { borderColor: themes[theme].separatorColor }]}> <View style={[styles.input, { borderColor: themes[theme].separatorColor }, innerInputStyle]}>
{placeholder ? <Text style={[styles.pickerText, { color: themes[theme].auxiliaryText }]}>{placeholder}</Text> : children} {placeholder ? <Text style={[styles.pickerText, { color: themes[theme].auxiliaryText }]}>{placeholder}</Text> : children}
{loading ? ( {loading ? (
<ActivityIndicator style={[styles.loading, styles.icon]} /> <ActivityIndicator style={[styles.loading, styles.icon]} />

View File

@ -28,6 +28,7 @@ interface IMultiSelect {
value?: any[]; value?: any[];
disabled?: boolean | object; disabled?: boolean | object;
theme: string; theme: string;
innerInputStyle?: object;
} }
const ANIMATION_DURATION = 200; const ANIMATION_DURATION = 200;
@ -53,7 +54,8 @@ export const MultiSelect = React.memo(
onClose = () => {}, onClose = () => {},
disabled, disabled,
inputStyle, inputStyle,
theme theme,
innerInputStyle
}: IMultiSelect) => { }: IMultiSelect) => {
const [selected, select] = useState<any>(Array.isArray(values) ? values : []); const [selected, select] = useState<any>(Array.isArray(values) ? values : []);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -143,8 +145,13 @@ export const MultiSelect = React.memo(
let button = multiselect ? ( let button = multiselect ? (
<Button title={`${selected.length} selecteds`} onPress={onShow} loading={loading} theme={theme} /> <Button title={`${selected.length} selecteds`} onPress={onShow} loading={loading} theme={theme} />
) : ( ) : (
// @ts-ignore <Input
<Input onPress={onShow} theme={theme} loading={loading} disabled={disabled} inputStyle={inputStyle}> onPress={onShow}
theme={theme}
loading={loading}
disabled={disabled}
inputStyle={inputStyle}
innerInputStyle={innerInputStyle}>
<Text style={[styles.pickerText, { color: currentValue ? themes[theme].titleText : themes[theme].auxiliaryText }]}> <Text style={[styles.pickerText, { color: currentValue ? themes[theme].titleText : themes[theme].auxiliaryText }]}>
{currentValue || placeholder.text} {currentValue || placeholder.text}
</Text> </Text>
@ -154,8 +161,13 @@ export const MultiSelect = React.memo(
if (context === BLOCK_CONTEXT.FORM) { if (context === BLOCK_CONTEXT.FORM) {
const items: any = options.filter((option: any) => selected.includes(option.value)); const items: any = options.filter((option: any) => selected.includes(option.value));
button = ( button = (
// @ts-ignore <Input
<Input onPress={onShow} theme={theme} loading={loading} disabled={disabled} inputStyle={inputStyle}> onPress={onShow}
theme={theme}
loading={loading}
disabled={disabled}
inputStyle={inputStyle}
innerInputStyle={innerInputStyle}>
{items.length ? ( {items.length ? (
<Chips items={items} onSelect={(item: any) => (disabled ? {} : onSelect(item))} theme={theme} /> <Chips items={items} onSelect={(item: any) => (disabled ? {} : onSelect(item))} theme={theme} />
) : ( ) : (

View File

@ -772,5 +772,14 @@
"Converting_Team_To_Channel": "Converting Team to Channel", "Converting_Team_To_Channel": "Converting Team to Channel",
"Select_Team_Channels_To_Delete": "Select the Teams 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.", "Select_Team_Channels_To_Delete": "Select the Teams 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", "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"
} }

View File

@ -672,5 +672,13 @@
"Left_The_Room_Successfully": "Saiu da sala com sucesso", "Left_The_Room_Successfully": "Saiu da sala com sucesso",
"Deleted_The_Team_Successfully": "Time deletado com sucesso", "Deleted_The_Team_Successfully": "Time deletado com sucesso",
"Deleted_The_Room_Successfully": "Sala deletada 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"
} }

View File

@ -50,7 +50,8 @@ const PERMISSIONS = [
'view-all-team-channels', 'view-all-team-channels',
'convert-team', 'convert-team',
'edit-omnichannel-contact', 'edit-omnichannel-contact',
'edit-livechat-room-customfields' 'edit-livechat-room-customfields',
'view-canned-responses'
]; ];
export async function setPermissions() { export async function setPermissions() {

View File

@ -1161,6 +1161,19 @@ const RocketChat = {
return this.sdk.get('livechat/custom-fields'); 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) { getUidDirectMessage(room) {
const { id: userId } = reduxStore.getState().login.user; const { id: userId } = reduxStore.getState().login.user;

View File

@ -30,6 +30,8 @@ import ThreadMessagesView from '../views/ThreadMessagesView';
import TeamChannelsView from '../views/TeamChannelsView'; import TeamChannelsView from '../views/TeamChannelsView';
import MarkdownTableView from '../views/MarkdownTableView'; import MarkdownTableView from '../views/MarkdownTableView';
import ReadReceiptsView from '../views/ReadReceiptView'; import ReadReceiptsView from '../views/ReadReceiptView';
import CannedResponsesListView from '../views/CannedResponsesListView';
import CannedResponseDetail from '../views/CannedResponseDetail';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
// Profile Stack // Profile Stack
import ProfileView from '../views/ProfileView'; import ProfileView from '../views/ProfileView';
@ -136,6 +138,16 @@ const ChatsStackNavigator = () => {
<ChatsStack.Screen name='MarkdownTableView' component={MarkdownTableView} options={MarkdownTableView.navigationOptions} /> <ChatsStack.Screen name='MarkdownTableView' component={MarkdownTableView} options={MarkdownTableView.navigationOptions} />
<ChatsStack.Screen name='ReadReceiptsView' component={ReadReceiptsView} options={ReadReceiptsView.navigationOptions} /> <ChatsStack.Screen name='ReadReceiptsView' component={ReadReceiptsView} options={ReadReceiptsView.navigationOptions} />
<ChatsStack.Screen name='QueueListView' component={QueueListView} options={QueueListView.navigationOptions} /> <ChatsStack.Screen name='QueueListView' component={QueueListView} options={QueueListView.navigationOptions} />
<ChatsStack.Screen
name='CannedResponsesListView'
component={CannedResponsesListView}
options={CannedResponsesListView.navigationOptions}
/>
<ChatsStack.Screen
name='CannedResponseDetail'
component={CannedResponseDetail}
options={CannedResponseDetail.navigationOptions}
/>
</ChatsStack.Navigator> </ChatsStack.Navigator>
); );
}; };

View File

@ -24,6 +24,8 @@ import DirectoryView from '../../views/DirectoryView';
import NotificationPrefView from '../../views/NotificationPreferencesView'; import NotificationPrefView from '../../views/NotificationPreferencesView';
import VisitorNavigationView from '../../views/VisitorNavigationView'; import VisitorNavigationView from '../../views/VisitorNavigationView';
import ForwardLivechatView from '../../views/ForwardLivechatView'; import ForwardLivechatView from '../../views/ForwardLivechatView';
import CannedResponsesListView from '../../views/CannedResponsesListView';
import CannedResponseDetail from '../../views/CannedResponseDetail';
import LivechatEditView from '../../views/LivechatEditView'; import LivechatEditView from '../../views/LivechatEditView';
import PickerView from '../../views/PickerView'; import PickerView from '../../views/PickerView';
import ThreadMessagesView from '../../views/ThreadMessagesView'; import ThreadMessagesView from '../../views/ThreadMessagesView';
@ -159,6 +161,16 @@ const ModalStackNavigator = React.memo(({ navigation }) => {
component={ForwardLivechatView} component={ForwardLivechatView}
options={ForwardLivechatView.navigationOptions} options={ForwardLivechatView.navigationOptions}
/> />
<ModalStack.Screen
name='CannedResponsesListView'
component={CannedResponsesListView}
options={CannedResponsesListView.navigationOptions}
/>
<ModalStack.Screen
name='CannedResponseDetail'
component={CannedResponseDetail}
options={CannedResponseDetail.navigationOptions}
/>
<ModalStack.Screen name='LivechatEditView' component={LivechatEditView} options={LivechatEditView.navigationOptions} /> <ModalStack.Screen name='LivechatEditView' component={LivechatEditView} options={LivechatEditView.navigationOptions} />
<ModalStack.Screen name='PickerView' component={PickerView} options={PickerView.navigationOptions} /> <ModalStack.Screen name='PickerView' component={PickerView} options={PickerView.navigationOptions} />
<ModalStack.Screen name='ThreadMessagesView' component={ThreadMessagesView} /> <ModalStack.Screen name='ThreadMessagesView' component={ThreadMessagesView} />

View File

@ -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 ? (
<View style={styles.item} testID={testID}>
<Text accessibilityLabel={label} style={[styles.itemLabel, { color: themes[theme].titleText }]}>
{label}
</Text>
<Markdown style={[styles.itemContent, { color: themes[theme].auxiliaryText }]} msg={content} theme={theme} />
</View>
) : 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 (
<SafeAreaView>
<ScrollView contentContainerStyle={[styles.scroll, { backgroundColor: themes[theme].messageboxBackground }]}>
<StatusBar />
<View style={styles.container}>
<Item label={I18n.t('Shortcut')} content={`!${cannedResponse?.shortcut}`} theme={theme} />
<Item label={I18n.t('Content')} content={cannedResponse?.text} theme={theme} />
<Item label={I18n.t('Sharing')} content={cannedResponse?.scopeName} theme={theme} />
<View style={styles.item}>
<Text style={[styles.itemLabel, { color: themes[theme].titleText }]}>{I18n.t('Tags')}</Text>
<View style={styles.cannedTagContainer}>
{cannedResponse?.tags?.length > 0 ? (
cannedResponse.tags.map(t => (
<View style={[styles.cannedTagWrap, { backgroundColor: themes[theme].searchboxBackground }]}>
<Text style={[styles.cannedTag, { color: themes[theme].auxiliaryTintColor }]}>{t}</Text>
</View>
))
) : (
<Text style={[styles.cannedText, { color: themes[theme].auxiliaryTintColor }]}>-</Text>
)}
</View>
</View>
</View>
<Button
title={I18n.t('Use')}
theme={theme}
style={styles.button}
type='primary'
onPress={() => navigateToRoom(cannedResponse)}
/>
</ScrollView>
</SafeAreaView>
);
};
CannedResponseDetail.propTypes = {
navigation: PropTypes.object,
route: PropTypes.object
};
export default CannedResponseDetail;

View File

@ -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 }) => (
<Touchable onPress={onPressDetail} style={[styles.wrapCannedItem, { backgroundColor: themes[theme].messageboxBackground }]}>
<>
<View style={styles.cannedRow}>
<View style={styles.cannedWrapShortcutScope}>
<Text style={[styles.cannedShortcut, { color: themes[theme].titleText }]}>!{shortcut}</Text>
<Text style={[styles.cannedScope, { color: themes[theme].auxiliaryTintColor }]}>{scope}</Text>
</View>
<Button
title={I18n.t('Use')}
fontSize={12}
color={themes[theme].titleText}
style={[styles.cannedUseButton, { backgroundColor: themes[theme].chatComponentBackground }]}
theme={theme}
onPress={onPressUse}
/>
</View>
<Text ellipsizeMode='tail' numberOfLines={2} style={[styles.cannedText, { color: themes[theme].auxiliaryTintColor }]}>
{text}
</Text>
<View style={styles.cannedTagContainer}>
{tags?.length > 0
? tags.map(t => (
<View style={[styles.cannedTagWrap, { backgroundColor: themes[theme].searchboxBackground }]}>
<Text style={[styles.cannedTag, { color: themes[theme].auxiliaryTintColor }]}>{t}</Text>
</View>
))
: null}
</View>
</>
</Touchable>
);
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;

View File

@ -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', () => (
<>
<CannedResponseItem
theme={theme}
scope={item[0].scopeName}
shortcut={item[0].shortcut}
tags={item[0]?.tags}
text={item[0].text}
onPressDetail={() => alert('navigation to CannedResponseDetail')}
onPressUse={() => alert('Back to RoomView and wrote in MessageBox')}
/>
<CannedResponseItem
theme={theme}
scope={item[1].scopeName}
shortcut={item[1].shortcut}
tags={item[1]?.tags}
text={item[1].text}
onPressDetail={() => alert('navigation to CannedResponseDetail')}
onPressUse={() => alert('Back to RoomView and wrote in MessageBox')}
/>
</>
));

View File

@ -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 }) => (
<Touch theme={theme} onPress={onPress} style={{ backgroundColor: themes[theme].backgroundColor }}>
<View style={styles.container}>
<Text style={[styles.text, { color: themes[theme].auxiliaryText }]}>{text}</Text>
{iconName ? <CustomIcon name={iconName} size={22} color={themes[theme].auxiliaryText} /> : null}
</View>
</Touch>
));
DropdownItem.propTypes = {
text: PropTypes.string,
iconName: PropTypes.string,
theme: PropTypes.string,
onPress: PropTypes.func
};
export default withTheme(DropdownItem);

View File

@ -0,0 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import DropdownItem from './DropdownItem';
const DropdownItemFilter = ({ currentDepartment, value, onPress }) => (
<DropdownItem
text={value?.name}
iconName={currentDepartment?._id === value?._id ? 'check' : null}
onPress={() => onPress(value)}
/>
);
DropdownItemFilter.propTypes = {
currentDepartment: PropTypes.object,
value: PropTypes.string,
onPress: PropTypes.func
};
export default DropdownItemFilter;

View File

@ -0,0 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import DropdownItem from './DropdownItem';
const DropdownItemHeader = ({ department, onPress }) => (
<DropdownItem text={department?.name} iconName='filter' onPress={onPress} />
);
DropdownItemHeader.propTypes = {
department: PropTypes.object,
onPress: PropTypes.func
};
export default DropdownItemHeader;

View File

@ -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 (
<>
<TouchableWithoutFeedback onPress={this.close}>
<Animated.View
style={[
styles.backdrop,
{
backgroundColor: themes[theme].backdropColor,
opacity: backdropOpacity,
top: heightDestination
}
]}
/>
</TouchableWithoutFeedback>
<Animated.View
style={[
styles.dropdownContainer,
{
transform: [{ translateY }],
backgroundColor: themes[theme].backgroundColor,
borderColor: themes[theme].separatorColor
}
]}>
<DropdownItemHeader department={currentDepartment} onPress={this.close} />
<List.Separator />
<FlatList
style={{ maxHeight: maxRows * ROW_HEIGHT }}
data={departments}
keyExtractor={item => item._id}
renderItem={({ item }) => (
<DropdownItemFilter onPress={onDepartmentSelected} currentDepartment={currentDepartment} value={item} />
)}
keyboardShouldPersistTaps='always'
/>
</Animated.View>
</>
);
}
}
export default withTheme(withSafeAreaInsets(Dropdown));

View File

@ -0,0 +1,376 @@
import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { FlatList, RefreshControl } 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([]);
const [refreshing, setRefreshing] = useState(false);
// 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: () => (
<HeaderButton.Container left>
<HeaderButton.Item
iconName='close'
onPress={() => {
onChangeText();
setIsSearching(false);
}}
/>
</HeaderButton.Container>
),
headerTitle: () => <SearchHeader onSearchChangeText={onChangeText} />,
headerTitleContainerStyle: {
left: headerTitlePosition.left,
right: headerTitlePosition.right
},
headerRight: () => null
};
}
const options = {
headerLeft: () => (
<HeaderBackButton labelVisible={false} onPress={() => navigation.pop()} tintColor={themes[theme].headerTintColor} />
),
headerTitleAlign: 'center',
headerTitle: I18n.t('Canned_Responses'),
headerTitleContainerStyle: {
left: null,
right: null
}
};
if (isMasterDetail) {
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
}
options.headerRight = () => (
<HeaderButton.Container>
<HeaderButton.Item iconName='search' onPress={() => setIsSearching(true)} />
</HeaderButton.Container>
);
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 (
<>
<DropdownItemHeader department={currentDepartment} onPress={showDropdown} />
<List.Separator />
</>
);
};
const onRefresh = () => {
setRefreshing(true);
onChangeText('');
};
useEffect(() => {
if (refreshing) {
setRefreshing(false);
}
}, [cannedResponses]);
const renderContent = () => {
if (!cannedResponsesScopeName.length && !loading) {
return (
<>
{renderFlatListHeader()}
<BackgroundContainer text={I18n.t('No_canned_responses')} />
</>
);
}
return (
<FlatList
data={cannedResponsesScopeName}
extraData={cannedResponsesScopeName}
style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]}
renderItem={({ item }) => (
<CannedResponseItem
theme={theme}
scope={item.scopeName}
shortcut={item.shortcut}
tags={item?.tags}
text={item.text}
onPressDetail={() => goToDetail(item)}
onPressUse={() => navigateToRoom(item)}
/>
)}
keyExtractor={item => item._id || item.shortcut}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={themes[theme].auxiliaryText} />}
ListHeaderComponent={renderFlatListHeader}
stickyHeaderIndices={[0]}
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
ItemSeparatorComponent={List.Separator}
ListFooterComponent={loading && !refreshing ? <ActivityIndicator theme={theme} /> : null}
/>
);
};
return (
<SafeAreaView>
<StatusBar />
{renderContent()}
{showFilterDropdown ? (
<Dropdown
departments={departments}
currentDepartment={currentDepartment}
onDepartmentSelected={onDepartmentSelect}
onClose={() => setShowFilterDropDown(false)}
/>
) : null}
</SafeAreaView>
);
};
CannedResponsesListView.propTypes = {
navigation: PropTypes.object,
route: PropTypes.object
};
export default CannedResponsesListView;

View File

@ -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
}
});

View File

@ -64,7 +64,8 @@ class RoomActionsView extends React.Component {
transferLivechatGuestPermission: PropTypes.array, transferLivechatGuestPermission: PropTypes.array,
createTeamPermission: PropTypes.array, createTeamPermission: PropTypes.array,
addTeamChannelPermission: PropTypes.array, addTeamChannelPermission: PropTypes.array,
convertTeamPermission: PropTypes.array convertTeamPermission: PropTypes.array,
viewCannedResponsesPermission: PropTypes.array
}; };
constructor(props) { constructor(props) {
@ -89,7 +90,8 @@ class RoomActionsView extends React.Component {
canToggleEncryption: false, canToggleEncryption: false,
canCreateTeam: false, canCreateTeam: false,
canAddChannelToTeam: false, canAddChannelToTeam: false,
canConvertTeam: false canConvertTeam: false,
canViewCannedResponse: false
}; };
if (room && room.observe && room.rid) { if (room && room.observe && room.rid) {
this.roomObservable = room.observe(); this.roomObservable = room.observe();
@ -157,7 +159,8 @@ class RoomActionsView extends React.Component {
if (room.t === 'l') { if (room.t === 'l') {
const canForwardGuest = await this.canForwardGuest(); const canForwardGuest = await this.canForwardGuest();
const canReturnQueue = await this.canReturnQueue(); const canReturnQueue = await this.canReturnQueue();
this.setState({ canForwardGuest, canReturnQueue }); const canViewCannedResponse = await this.canViewCannedResponse();
this.setState({ canForwardGuest, canReturnQueue, canViewCannedResponse });
} }
} }
} }
@ -294,6 +297,14 @@ class RoomActionsView extends React.Component {
return permissions[0]; return permissions[0];
}; };
canViewCannedResponse = async () => {
const { room } = this.state;
const { viewCannedResponsesPermission } = this.props;
const { rid } = room;
const permissions = await RocketChat.hasPermission([viewCannedResponsesPermission], rid);
return permissions[0];
};
canReturnQueue = async () => { canReturnQueue = async () => {
try { try {
const { returnQueue } = await RocketChat.getRoutingConfig(); const { returnQueue } = await RocketChat.getRoutingConfig();
@ -927,7 +938,8 @@ class RoomActionsView extends React.Component {
joined, joined,
canAutoTranslate, canAutoTranslate,
canForwardGuest, canForwardGuest,
canReturnQueue canReturnQueue,
canViewCannedResponse
} = this.state; } = this.state;
const { rid, t } = room; const { rid, t } = room;
const isGroupChat = RocketChat.isGroupChat(room); const isGroupChat = RocketChat.isGroupChat(room);
@ -1125,6 +1137,18 @@ class RoomActionsView extends React.Component {
{this.teamChannelActions(t, room)} {this.teamChannelActions(t, room)}
{this.teamToChannelActions(t, room)} {this.teamToChannelActions(t, room)}
{['l'].includes(t) && !this.isOmnichannelPreview && canViewCannedResponse ? (
<>
<List.Item
title='Canned_Responses'
onPress={() => this.onPressTouchable({ route: 'CannedResponsesListView', params: { rid, room } })}
left={() => <List.Icon name='canned-response' />}
showActionIndicator
/>
<List.Separator />
</>
) : null}
{['l'].includes(t) && !this.isOmnichannelPreview ? ( {['l'].includes(t) && !this.isOmnichannelPreview ? (
<> <>
<List.Item <List.Item
@ -1216,7 +1240,8 @@ const mapStateToProps = state => ({
transferLivechatGuestPermission: state.permissions['transfer-livechat-guest'], transferLivechatGuestPermission: state.permissions['transfer-livechat-guest'],
createTeamPermission: state.permissions['create-team'], createTeamPermission: state.permissions['create-team'],
addTeamChannelPermission: state.permissions['add-team-channel'], addTeamChannelPermission: state.permissions['add-team-channel'],
convertTeamPermission: state.permissions['convert-team'] convertTeamPermission: state.permissions['convert-team'],
viewCannedResponsesPermission: state.permissions['view-canned-responses']
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({

View File

@ -1057,8 +1057,10 @@ class RoomView extends React.Component {
}; };
renderFooter = () => { renderFooter = () => {
const { joined, room, selectedMessage, editing, replying, replyWithMention, readOnly } = this.state; const { joined, room, selectedMessage, editing, replying, replyWithMention, readOnly, loading } = this.state;
const { navigation, theme } = this.props; const { navigation, theme, route } = this.props;
const usedCannedResponse = route?.params?.usedCannedResponse;
if (!this.rid) { if (!this.rid) {
return null; return null;
@ -1074,6 +1076,7 @@ class RoomView extends React.Component {
<Touch <Touch
onPress={this.joinRoom} onPress={this.joinRoom}
style={[styles.joinRoomButton, { backgroundColor: themes[theme].actionTintColor }]} style={[styles.joinRoomButton, { backgroundColor: themes[theme].actionTintColor }]}
enabled={!loading}
theme={theme}> theme={theme}>
<Text style={[styles.joinRoomText, { color: themes[theme].buttonText }]} testID='room-view-join-button'> <Text style={[styles.joinRoomText, { color: themes[theme].buttonText }]} testID='room-view-join-button'>
{I18n.t(this.isOmnichannel ? 'Take_it' : 'Join')} {I18n.t(this.isOmnichannel ? 'Take_it' : 'Join')}
@ -1118,6 +1121,7 @@ class RoomView extends React.Component {
replyCancel={this.onReplyCancel} replyCancel={this.onReplyCancel}
getCustomEmoji={this.getCustomEmoji} getCustomEmoji={this.getCustomEmoji}
navigation={navigation} navigation={navigation}
usedCannedResponse={usedCannedResponse}
/> />
); );
}; };

View File

@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import { withSafeAreaInsets } from 'react-native-safe-area-context'; import { withSafeAreaInsets } from 'react-native-safe-area-context';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { HeaderBackButton } from '@react-navigation/stack';
import StatusBar from '../containers/StatusBar'; import StatusBar from '../containers/StatusBar';
import RoomHeader from '../containers/RoomHeader'; import RoomHeader from '../containers/RoomHeader';
@ -16,6 +17,7 @@ import * as HeaderButton from '../containers/HeaderButton';
import BackgroundContainer from '../containers/BackgroundContainer'; import BackgroundContainer from '../containers/BackgroundContainer';
import SafeAreaView from '../containers/SafeAreaView'; import SafeAreaView from '../containers/SafeAreaView';
import ActivityIndicator from '../containers/ActivityIndicator'; import ActivityIndicator from '../containers/ActivityIndicator';
import SearchHeader from '../containers/SearchHeader';
import RoomItem, { ROW_HEIGHT } from '../presentation/RoomItem'; import RoomItem, { ROW_HEIGHT } from '../presentation/RoomItem';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { withDimensions } from '../dimensions'; import { withDimensions } from '../dimensions';
@ -28,7 +30,6 @@ import { withActionSheet } from '../containers/ActionSheet';
import { deleteRoom as deleteRoomAction } from '../actions/room'; import { deleteRoom as deleteRoomAction } from '../actions/room';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import SearchHeader from './ThreadMessagesView/SearchHeader';
const API_FETCH_COUNT = 25; const API_FETCH_COUNT = 25;
const PERMISSION_DELETE_C = 'delete-c'; const PERMISSION_DELETE_C = 'delete-c';
@ -157,7 +158,7 @@ class TeamChannelsView extends React.Component {
setHeader = () => { setHeader = () => {
const { isSearching, showCreate, data } = this.state; const { isSearching, showCreate, data } = this.state;
const { navigation, isMasterDetail, insets } = this.props; const { navigation, isMasterDetail, insets, theme } = this.props;
const { team } = this; const { team } = this;
if (!team) { if (!team) {
@ -167,7 +168,7 @@ class TeamChannelsView extends React.Component {
const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 2 }); const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 2 });
if (isSearching) { if (isSearching) {
return { const options = {
headerTitleAlign: 'left', headerTitleAlign: 'left',
headerLeft: () => ( headerLeft: () => (
<HeaderButton.Container left> <HeaderButton.Container left>
@ -181,6 +182,7 @@ class TeamChannelsView extends React.Component {
}, },
headerRight: () => null headerRight: () => null
}; };
return navigation.setOptions(options);
} }
const options = { const options = {
@ -190,6 +192,9 @@ class TeamChannelsView extends React.Component {
left: headerTitlePosition.left, left: headerTitlePosition.left,
right: headerTitlePosition.right right: headerTitlePosition.right
}, },
headerLeft: () => (
<HeaderBackButton labelVisible={false} onPress={() => navigation.pop()} tintColor={themes[theme].headerTintColor} />
),
headerTitle: () => ( headerTitle: () => (
<RoomHeader <RoomHeader
title={RocketChat.getRoomTitle(team)} title={RocketChat.getRoomTitle(team)}

View File

@ -29,7 +29,7 @@ import { getBadgeColor, makeThreadName } from '../../utils/room';
import { getHeaderTitlePosition } from '../../containers/Header'; import { getHeaderTitlePosition } from '../../containers/Header';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import { LISTENER } from '../../containers/Toast'; import { LISTENER } from '../../containers/Toast';
import SearchHeader from './SearchHeader'; import SearchHeader from '../../containers/SearchHeader';
import { FILTER } from './filters'; import { FILTER } from './filters';
import DropdownItemHeader from './Dropdown/DropdownItemHeader'; import DropdownItemHeader from './Dropdown/DropdownItemHeader';
import Dropdown from './Dropdown'; import Dropdown from './Dropdown';

View File

@ -15,6 +15,7 @@ import './Avatar';
import '../../app/containers/BackgroundContainer/index.stories.js'; import '../../app/containers/BackgroundContainer/index.stories.js';
import '../../app/containers/RoomHeader/RoomHeader.stories.js'; import '../../app/containers/RoomHeader/RoomHeader.stories.js';
import '../../app/views/RoomView/LoadMore/LoadMore.stories'; import '../../app/views/RoomView/LoadMore/LoadMore.stories';
import '../../app/views/CannedResponsesListView/CannedResponseItem.stories';
import '../../app/containers/TextInput.stories'; import '../../app/containers/TextInput.stories';
// Change here to see themed storybook // Change here to see themed storybook