[NEW] Livechat (#2004)

* [WIP][NEW] Livechat info/actions

* [IMPROVEMENT] RoomActionsView

* [NEW] Visitor Navigation

* [NEW] Get Department REST

* [FIX] Borders

* [IMPROVEMENT] Refactor RoomInfo View

* [FIX] Error while navigate from mention -> roomInfo

* [NEW] Livechat Fields

* [NEW] Close Livechat

* [WIP] Forward livechat

* [NEW] Return inquiry

* [WIP] Comment when close livechat

* [WIP] Improve roomInfo

* [IMPROVEMENT] Forward room

* [FIX] Department picker

* [FIX] Picker without results

* [FIX] Superfluous argument

* [FIX] Check permissions on RoomActionsView

* [FIX] Livechat permissions

* [WIP] Show edit to livechat

* [I18N] Add pt-br translations

* [WIP] Livechat Info

* [IMPROVEMENT] Livechat info

* [WIP] Livechat Edit

* [WIP] Livechat edit

* [WIP] Livechat Edit

* [WIP] Livechat edit scroll

* [FIX] Edit customFields

* [FIX] Clean livechat customField

* [FIX] Visitor Navigation

* [NEW] Next input logic LivechatEdit

* [FIX] Add livechat data to subscription

* [FIX] Revert change

* [NEW] Livechat user Status

* [WIP] Livechat tags

* [NEW] Edit livechat tags

* [FIX] Prevent some crashes

* [FIX] Forward

* [FIX] Return Livechat error

* [FIX] Prevent livechat info crash

* [IMPROVEMENT] Use input style on forward chat

* OnboardingSeparator -> OrSeparator

* [FIX] Go to next input

* [NEW] Added some icons

* [NEW] Livechat close

* [NEW] Forward Room Action

* [FIX] Livechat edit style

* [FIX] Change status logic

* [CHORE] Remove unnecessary logic

* [CHORE] Remove unnecessary code

* [CHORE] Remove unecessary case

* [FIX] Superfluous argument

* [IMPROVEMENT] Submit livechat edit

* [CHORE] Remove textInput type

* [FIX] Livechat edit

* [FIX] Livechat Edit

* [FIX] Use same effect

* [IMPROVEMENT] Tags input

* [FIX] Add empty tag

* Fix minor issues

* Fix typo

* insert livechat room data to our room object

* review

* add method calls server version

Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Djorkaeff Alexandre 2020-05-08 14:36:10 -03:00 committed by GitHub
parent 0e4e174e25
commit 9e89316e2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1534 additions and 281 deletions

View File

@ -31,7 +31,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
'OPEN_SEARCH_HEADER', 'OPEN_SEARCH_HEADER',
'CLOSE_SEARCH_HEADER' 'CLOSE_SEARCH_HEADER'
]); ]);
export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'USER_TYPING']); export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'CLOSE', 'FORWARD', 'USER_TYPING']);
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS']); export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS']);
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']); export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]); export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);

View File

@ -30,6 +30,21 @@ export function deleteRoom(rid, t) {
}; };
} }
export function closeRoom(rid) {
return {
type: types.ROOM.CLOSE,
rid
};
}
export function forwardRoom(rid, transferData) {
return {
type: types.ROOM.FORWARD,
transferData,
rid
};
}
export function removedRoom() { export function removedRoom() {
return { return {
type: types.ROOM.REMOVED type: types.ROOM.REMOVED

View File

@ -68,6 +68,9 @@ export default {
LDAP_Enable: { LDAP_Enable: {
type: 'valueAsBoolean' type: 'valueAsBoolean'
}, },
Livechat_request_comment_when_closing_conversation: {
type: 'valueAsBoolean'
},
Jitsi_Enabled: { Jitsi_Enabled: {
type: 'valueAsBoolean' type: 'valueAsBoolean'
}, },

View File

@ -12,7 +12,7 @@ import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { loginRequest as loginRequestAction } from '../actions/login'; import { loginRequest as loginRequestAction } from '../actions/login';
import Button from './Button'; import Button from './Button';
import OnboardingSeparator from './OnboardingSeparator'; import OrSeparator from './OrSeparator';
import Touch from '../utils/touch'; import Touch from '../utils/touch';
import I18n from '../i18n'; import I18n from '../i18n';
import random from '../utils/random'; import random from '../utils/random';
@ -252,12 +252,12 @@ class LoginServices extends React.PureComponent {
style={styles.options} style={styles.options}
color={themes[theme].actionTintColor} color={themes[theme].actionTintColor}
/> />
<OnboardingSeparator theme={theme} /> <OrSeparator theme={theme} />
</> </>
); );
} }
if (length > 0 && separator) { if (length > 0 && separator) {
return <OnboardingSeparator theme={theme} />; return <OrSeparator theme={theme} />;
} }
return null; return null;
} }

View File

@ -24,7 +24,7 @@ const styles = StyleSheet.create({
} }
}); });
const DateSeparator = React.memo(({ theme }) => { const OrSeparator = React.memo(({ theme }) => {
const line = { backgroundColor: themes[theme].borderColor }; const line = { backgroundColor: themes[theme].borderColor };
const text = { color: themes[theme].auxiliaryText }; const text = { color: themes[theme].auxiliaryText };
return ( return (
@ -36,8 +36,8 @@ const DateSeparator = React.memo(({ theme }) => {
); );
}); });
DateSeparator.propTypes = { OrSeparator.propTypes = {
theme: PropTypes.string theme: PropTypes.string
}; };
export default DateSeparator; export default OrSeparator;

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Image, StyleSheet } from 'react-native'; import { Image, StyleSheet } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import { themes } from '../constants/colors'; import { STATUS_COLORS, themes } from '../constants/colors';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
style: { style: {
@ -15,7 +15,7 @@ const styles = StyleSheet.create({
}); });
const RoomTypeIcon = React.memo(({ const RoomTypeIcon = React.memo(({
type, size, isGroupChat, style, theme type, size, isGroupChat, status, style, theme
}) => { }) => {
if (!type) { if (!type) {
return null; return null;
@ -36,7 +36,7 @@ const RoomTypeIcon = React.memo(({
} }
return <CustomIcon name='at' size={13} style={[styles.style, styles.discussion, { color }]} />; return <CustomIcon name='at' size={13} style={[styles.style, styles.discussion, { color }]} />;
} if (type === 'l') { } if (type === 'l') {
return <CustomIcon name='omnichannel' size={13} style={[styles.style, styles.discussion, { color }]} />; return <CustomIcon name='omnichannel' size={13} style={[styles.style, styles.discussion, { color: STATUS_COLORS[status] }]} />;
} }
return <Image source={{ uri: 'lock' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />; return <Image source={{ uri: 'lock' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />;
}); });
@ -45,6 +45,7 @@ RoomTypeIcon.propTypes = {
theme: PropTypes.string, theme: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
isGroupChat: PropTypes.bool, isGroupChat: PropTypes.bool,
status: PropTypes.string,
size: PropTypes.number, size: PropTypes.number,
style: PropTypes.object style: PropTypes.object
}; };

View File

@ -64,8 +64,10 @@ export default class RCTextInput extends React.PureComponent {
inputRef: PropTypes.func, inputRef: PropTypes.func,
testID: PropTypes.string, testID: PropTypes.string,
iconLeft: PropTypes.string, iconLeft: PropTypes.string,
iconRight: PropTypes.string,
placeholder: PropTypes.string, placeholder: PropTypes.string,
left: PropTypes.element, left: PropTypes.element,
onIconRightPress: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string
} }
@ -90,6 +92,19 @@ export default class RCTextInput extends React.PureComponent {
); );
} }
get iconRight() {
const { iconRight, onIconRightPress, theme } = this.props;
return (
<BorderlessButton onPress={onIconRightPress} style={[styles.iconContainer, styles.iconRight]}>
<CustomIcon
name={iconRight}
style={{ color: themes[theme].bodyText }}
size={20}
/>
</BorderlessButton>
);
}
get iconPassword() { get iconPassword() {
const { showPassword } = this.state; const { showPassword } = this.state;
const { testID, theme } = this.props; const { testID, theme } = this.props;
@ -117,7 +132,7 @@ export default class RCTextInput extends React.PureComponent {
render() { render() {
const { showPassword } = this.state; const { showPassword } = this.state;
const { const {
label, left, error, loading, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, testID, placeholder, theme, ...inputProps label, left, error, loading, secureTextEntry, containerStyle, inputRef, iconLeft, iconRight, inputStyle, testID, placeholder, theme, ...inputProps
} = this.props; } = this.props;
const { dangerColor } = themes[theme]; const { dangerColor } = themes[theme];
return ( return (
@ -140,7 +155,7 @@ export default class RCTextInput extends React.PureComponent {
style={[ style={[
styles.input, styles.input,
iconLeft && styles.inputIconLeft, iconLeft && styles.inputIconLeft,
secureTextEntry && styles.inputIconRight, (secureTextEntry || iconRight) && styles.inputIconRight,
{ {
backgroundColor: themes[theme].backgroundColor, backgroundColor: themes[theme].backgroundColor,
borderColor: themes[theme].separatorColor, borderColor: themes[theme].separatorColor,
@ -165,6 +180,7 @@ export default class RCTextInput extends React.PureComponent {
{...inputProps} {...inputProps}
/> />
{iconLeft ? this.iconLeft : null} {iconLeft ? this.iconLeft : null}
{iconRight ? this.iconRight : null}
{secureTextEntry ? this.iconPassword : null} {secureTextEntry ? this.iconPassword : null}
{loading ? this.loading : null} {loading ? this.loading : null}
{left} {left}

View File

@ -12,11 +12,13 @@ import styles from './styles';
const keyExtractor = item => item.value.toString(); const keyExtractor = item => item.value.toString();
const Chip = ({ item, onSelect, theme }) => ( const Chip = ({
item, onSelect, style, theme
}) => (
<Touchable <Touchable
key={item.value} key={item.value}
onPress={() => onSelect(item)} onPress={() => onSelect(item)}
style={[styles.chip, { backgroundColor: themes[theme].auxiliaryBackground }]} style={[styles.chip, { backgroundColor: themes[theme].auxiliaryBackground }, style]}
background={Touchable.Ripple(themes[theme].bannerBackground)} background={Touchable.Ripple(themes[theme].bannerBackground)}
> >
<> <>
@ -29,17 +31,21 @@ const Chip = ({ item, onSelect, theme }) => (
Chip.propTypes = { Chip.propTypes = {
item: PropTypes.object, item: PropTypes.object,
onSelect: PropTypes.func, onSelect: PropTypes.func,
style: PropTypes.object,
theme: PropTypes.string theme: PropTypes.string
}; };
const Chips = ({ items, onSelect, theme }) => ( const Chips = ({
items, onSelect, style, theme
}) => (
<View style={styles.chips}> <View style={styles.chips}>
{items.map(item => <Chip key={keyExtractor(item)} item={item} onSelect={onSelect} theme={theme} />)} {items.map(item => <Chip key={keyExtractor(item)} item={item} onSelect={onSelect} style={style} theme={theme} />)}
</View> </View>
); );
Chips.propTypes = { Chips.propTypes = {
items: PropTypes.array, items: PropTypes.array,
onSelect: PropTypes.func, onSelect: PropTypes.func,
style: PropTypes.object,
theme: PropTypes.string theme: PropTypes.string
}; };

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { View, Text } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
@ -9,16 +9,16 @@ import ActivityIndicator from '../../ActivityIndicator';
import styles from './styles'; import styles from './styles';
const Input = ({ const Input = ({
children, open, theme, loading, inputStyle, disabled children, onPress, theme, loading, inputStyle, placeholder, disabled
}) => ( }) => (
<Touchable <Touchable
onPress={() => open(true)} 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 }]}>
{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]} />
@ -29,10 +29,11 @@ const Input = ({
); );
Input.propTypes = { Input.propTypes = {
children: PropTypes.node, children: PropTypes.node,
open: PropTypes.func, onPress: PropTypes.func,
theme: PropTypes.string, theme: PropTypes.string,
inputStyle: PropTypes.object, inputStyle: PropTypes.object,
disabled: PropTypes.bool, disabled: PropTypes.bool,
placeholder: PropTypes.string,
loading: PropTypes.bool loading: PropTypes.bool
}; };

View File

@ -136,7 +136,7 @@ export const MultiSelect = React.memo(({
/> />
) : ( ) : (
<Input <Input
open={onShow} onPress={onShow}
theme={theme} theme={theme}
loading={loading} loading={loading}
disabled={disabled} disabled={disabled}
@ -150,7 +150,7 @@ export const MultiSelect = React.memo(({
const items = options.filter(option => selected.includes(option.value)); const items = options.filter(option => selected.includes(option.value));
button = ( button = (
<Input <Input
open={onShow} onPress={onShow}
theme={theme} theme={theme}
loading={loading} loading={loading}
disabled={disabled} disabled={disabled}

View File

@ -53,7 +53,6 @@ const User = React.memo(({
<TouchableOpacity <TouchableOpacity
style={styles.titleContainer} style={styles.titleContainer}
onPress={() => navToRoomInfo(navParam)} onPress={() => navToRoomInfo(navParam)}
style={styles.titleContainer}
disabled={author._id === user.id} disabled={author._id === user.id}
> >
<Text style={[styles.username, { color: themes[theme].titleText }]} numberOfLines={1}> <Text style={[styles.username, { color: themes[theme].titleText }]} numberOfLines={1}>

View File

@ -85,6 +85,7 @@ export default {
Add_Server: 'Add Server', Add_Server: 'Add Server',
Add_users: 'Add users', Add_users: 'Add users',
Admin_Panel: 'Admin Panel', Admin_Panel: 'Admin Panel',
Agent: 'Agent',
Alert: 'Alert', Alert: 'Alert',
alert: 'alert', alert: 'alert',
alerts: 'alerts', alerts: 'alerts',
@ -133,7 +134,9 @@ export default {
Click_to_join: 'Click to Join!', Click_to_join: 'Click to Join!',
Close: 'Close', Close: 'Close',
Close_emoji_selector: 'Close emoji selector', Close_emoji_selector: 'Close emoji selector',
Closing_chat: 'Closing chat',
Change_language_loading: 'Changing language.', Change_language_loading: 'Changing language.',
Chat_closed_by_agent: 'Chat closed by agent',
Choose: 'Choose', Choose: 'Choose',
Choose_from_library: 'Choose from library', Choose_from_library: 'Choose from library',
Choose_file: 'Choose file', Choose_file: 'Choose file',
@ -151,6 +154,7 @@ export default {
Continue_with: 'Continue with', Continue_with: 'Continue with',
Copied_to_clipboard: 'Copied to clipboard!', Copied_to_clipboard: 'Copied to clipboard!',
Copy: 'Copy', Copy: 'Copy',
Conversation: 'Conversation',
Permalink: 'Permalink', Permalink: 'Permalink',
Certificate_password: 'Certificate Password', Certificate_password: 'Certificate Password',
Clear_cache: 'Clear local server cache', Clear_cache: 'Clear local server cache',
@ -169,6 +173,7 @@ export default {
Default: 'Default', Default: 'Default',
Default_browser: 'Default browser', Default_browser: 'Default browser',
Delete_Room_Warning: 'Deleting a room will delete all messages posted within the room. This cannot be undone.', Delete_Room_Warning: 'Deleting a room will delete all messages posted within the room. This cannot be undone.',
Department: 'Department',
delete: 'delete', delete: 'delete',
Delete: 'Delete', Delete: 'Delete',
DELETE: 'DELETE', DELETE: 'DELETE',
@ -196,6 +201,7 @@ export default {
Email: 'Email', Email: 'Email',
EMAIL: 'EMAIL', EMAIL: 'EMAIL',
email: 'e-mail', email: 'e-mail',
Empty_title: 'Empty title',
Enable_Auto_Translate: 'Enable Auto-Translate', Enable_Auto_Translate: 'Enable Auto-Translate',
Enable_notifications: 'Enable notifications', Enable_notifications: 'Enable notifications',
Everyone_can_access_this_channel: 'Everyone can access this channel', Everyone_can_access_this_channel: 'Everyone can access this channel',
@ -212,6 +218,10 @@ export default {
Forgot_password_If_this_email_is_registered: 'If this email is registered, we\'ll send instructions on how to reset your password. If you do not receive an email shortly, please come back and try again.', Forgot_password_If_this_email_is_registered: 'If this email is registered, we\'ll send instructions on how to reset your password. If you do not receive an email shortly, please come back and try again.',
Forgot_password: 'Forgot your password?', Forgot_password: 'Forgot your password?',
Forgot_Password: 'Forgot Password', Forgot_Password: 'Forgot Password',
Forward: 'Forward',
Forward_Chat: 'Forward Chat',
Forward_to_department: 'Forward to department',
Forward_to_user: 'Forward to user',
Full_table: 'Click to see full table', Full_table: 'Click to see full table',
Generate_New_Link: 'Generate New Link', Generate_New_Link: 'Generate New Link',
Group_by_favorites: 'Group favorites', Group_by_favorites: 'Group favorites',
@ -235,6 +245,7 @@ export default {
Message_HideType_subscription_role_removed: 'Role No Longer Defined', Message_HideType_subscription_role_removed: 'Role No Longer Defined',
Message_HideType_room_archived: 'Room Archived', Message_HideType_room_archived: 'Room Archived',
Message_HideType_room_unarchived: 'Room Unarchived', Message_HideType_room_unarchived: 'Room Unarchived',
IP: 'IP',
In_app: 'In-app', In_app: 'In-app',
IN_APP_AND_DESKTOP: 'IN-APP AND DESKTOP', IN_APP_AND_DESKTOP: 'IN-APP AND DESKTOP',
In_App_and_Desktop_Alert_info: 'Displays a banner at the top of the screen when app is open, and displays a notification on desktop', In_App_and_Desktop_Alert_info: 'Displays a banner at the top of the screen when app is open, and displays a notification on desktop',
@ -260,6 +271,7 @@ export default {
Light: 'Light', Light: 'Light',
License: 'License', License: 'License',
Livechat: 'Livechat', Livechat: 'Livechat',
Livechat_edit: 'Livechat edit',
Login: 'Login', Login: 'Login',
Login_error: 'Your credentials were rejected! Please try again.', Login_error: 'Your credentials were rejected! Please try again.',
Login_with: 'Login with', Login_with: 'Login with',
@ -292,6 +304,7 @@ export default {
N_users: '{{n}} users', N_users: '{{n}} users',
name: 'name', name: 'name',
Name: 'Name', Name: 'Name',
Navigation_history: 'Navigation history',
Never: 'Never', Never: 'Never',
New_Message: 'New Message', New_Message: 'New Message',
New_Password: 'New Password', New_Password: 'New Password',
@ -318,6 +331,7 @@ export default {
Notifications: 'Notifications', Notifications: 'Notifications',
Notification_Duration: 'Notification Duration', Notification_Duration: 'Notification Duration',
Notification_Preferences: 'Notification Preferences', Notification_Preferences: 'Notification Preferences',
No_available_agents_to_transfer: 'No available agents to transfer',
Offline: 'Offline', Offline: 'Offline',
Oops: 'Oops!', Oops: 'Oops!',
Onboarding_description: 'A workspace is your team or organizations space to collaborate. Ask the workspace admin for address to join or create one for your team.', Onboarding_description: 'A workspace is your team or organizations space to collaborate. Ask the workspace admin for address to join or create one for your team.',
@ -334,14 +348,17 @@ export default {
Open_Source_Communication: 'Open Source Communication', Open_Source_Communication: 'Open Source Communication',
Open_your_authentication_app_and_enter_the_code: 'Open your authentication app and enter the code.', Open_your_authentication_app_and_enter_the_code: 'Open your authentication app and enter the code.',
OR: 'OR', OR: 'OR',
OS: 'OS',
Overwrites_the_server_configuration_and_use_room_config: 'Overwrites the server configuration and use room config', Overwrites_the_server_configuration_and_use_room_config: 'Overwrites the server configuration and use room config',
Password: 'Password', Password: 'Password',
Parent_channel_or_group: 'Parent channel or group', Parent_channel_or_group: 'Parent channel or group',
Permalink_copied_to_clipboard: 'Permalink copied to clipboard!', Permalink_copied_to_clipboard: 'Permalink copied to clipboard!',
Phone: 'Phone',
Pin: 'Pin', Pin: 'Pin',
Pinned_Messages: 'Pinned Messages', Pinned_Messages: 'Pinned Messages',
pinned: 'pinned', pinned: 'pinned',
Pinned: 'Pinned', Pinned: 'Pinned',
Please_add_a_comment: 'Please add a comment',
Please_enter_your_password: 'Please enter your password', Please_enter_your_password: 'Please enter your password',
Please_wait: 'Please wait.', Please_wait: 'Please wait.',
Preferences: 'Preferences', Preferences: 'Preferences',
@ -380,6 +397,7 @@ export default {
Reset_password: 'Reset password', Reset_password: 'Reset password',
resetting_password: 'resetting password', resetting_password: 'resetting password',
RESET: 'RESET', RESET: 'RESET',
Return: 'Return',
Review_app_title: 'Are you enjoying this app?', Review_app_title: 'Are you enjoying this app?',
Review_app_desc: 'Give us 5 stars on {{store}}', Review_app_desc: 'Give us 5 stars on {{store}}',
Review_app_yes: 'Sure!', Review_app_yes: 'Sure!',
@ -401,6 +419,7 @@ export default {
SAVE: 'SAVE', SAVE: 'SAVE',
Save_Changes: 'Save Changes', Save_Changes: 'Save Changes',
Save: 'Save', Save: 'Save',
Saved: 'Saved',
saving_preferences: 'saving preferences', saving_preferences: 'saving preferences',
saving_profile: 'saving profile', saving_profile: 'saving profile',
saving_settings: 'saving settings', saving_settings: 'saving settings',
@ -415,7 +434,9 @@ export default {
Select_Server: 'Select Server', Select_Server: 'Select Server',
Select_Users: 'Select Users', Select_Users: 'Select Users',
Select_a_Channel: 'Select a Channel', Select_a_Channel: 'Select a Channel',
Select_a_Department: 'Select a Department',
Select_an_option: 'Select an option', Select_an_option: 'Select an option',
Select_a_User: 'Select a User',
Send: 'Send', Send: 'Send',
Send_audio_message: 'Send audio message', Send_audio_message: 'Send audio message',
Send_crash_report: 'Send crash report', Send_crash_report: 'Send crash report',
@ -453,6 +474,7 @@ export default {
Started_call: 'Call started by {{userBy}}', Started_call: 'Call started by {{userBy}}',
Submit: 'Submit', Submit: 'Submit',
Table: 'Table', Table: 'Table',
Tags: 'Tags',
Take_a_photo: 'Take a photo', Take_a_photo: 'Take a photo',
Take_a_video: 'Take a video', Take_a_video: 'Take a video',
tap_to_change_status: 'tap to change status', tap_to_change_status: 'tap to change status',
@ -488,6 +510,7 @@ export default {
Updating: 'Updating...', Updating: 'Updating...',
Uploading: 'Uploading', Uploading: 'Uploading',
Upload_file_question_mark: 'Upload file?', Upload_file_question_mark: 'Upload file?',
User: 'User',
Users: 'Users', Users: 'Users',
User_added_by: 'User {{userAdded}} added by {{userBy}}', User_added_by: 'User {{userAdded}} added by {{userBy}}',
User_Info: 'User Info', User_Info: 'User Info',
@ -518,8 +541,10 @@ export default {
Whats_your_2fa: 'What\'s your 2FA code?', Whats_your_2fa: 'What\'s your 2FA code?',
Without_Servers: 'Without Servers', Without_Servers: 'Without Servers',
Workspaces: 'Workspaces', Workspaces: 'Workspaces',
Would_you_like_to_return_the_inquiry: 'Would you like to return the inquiry?',
Write_External_Permission_Message: 'Rocket Chat needs access to your gallery so you can save images.', Write_External_Permission_Message: 'Rocket Chat needs access to your gallery so you can save images.',
Write_External_Permission: 'Gallery Permission', Write_External_Permission: 'Gallery Permission',
Yes: 'Yes',
Yes_action_it: 'Yes, {{action}} it!', Yes_action_it: 'Yes, {{action}} it!',
Yesterday: 'Yesterday', Yesterday: 'Yesterday',
You_are_in_preview_mode: 'You are in preview mode', You_are_in_preview_mode: 'You are in preview mode',

View File

@ -89,6 +89,7 @@ export default {
Add_Reaction: 'Reagir', Add_Reaction: 'Reagir',
Add_Server: 'Adicionar servidor', Add_Server: 'Adicionar servidor',
Add_users: 'Adicionar usuário', Add_users: 'Adicionar usuário',
Agent: 'Agente',
Alert: 'Alerta', Alert: 'Alerta',
alert: 'alerta', alert: 'alerta',
alerts: 'alertas', alerts: 'alertas',
@ -135,7 +136,9 @@ export default {
Click_to_join: 'Clique para participar!', Click_to_join: 'Clique para participar!',
Close: 'Fechar', Close: 'Fechar',
Close_emoji_selector: 'Fechar seletor de emojis', Close_emoji_selector: 'Fechar seletor de emojis',
Closing_chat: 'Fechando conversa',
Choose: 'Escolher', Choose: 'Escolher',
Chat_closed_by_agent: 'Conversa fechada por agente',
Choose_from_library: 'Escolha da biblioteca', Choose_from_library: 'Escolha da biblioteca',
Choose_file: 'Enviar arquivo', Choose_file: 'Enviar arquivo',
Choose_where_you_want_links_be_opened: 'Escolha onde deseja que os links sejam abertos', Choose_where_you_want_links_be_opened: 'Escolha onde deseja que os links sejam abertos',
@ -145,6 +148,7 @@ export default {
Confirm: 'Confirmar', Confirm: 'Confirmar',
Connect: 'Conectar', Connect: 'Conectar',
Connected: 'Conectado', Connected: 'Conectado',
Conversation: 'Conversação',
connecting_server: 'conectando no servidor', connecting_server: 'conectando no servidor',
Connecting: 'Conectando...', Connecting: 'Conectando...',
Continue_with: 'Entrar com', Continue_with: 'Entrar com',
@ -187,6 +191,7 @@ export default {
Email_or_password_field_is_empty: 'Email ou senha estão vazios', Email_or_password_field_is_empty: 'Email ou senha estão vazios',
Email: 'Email', Email: 'Email',
email: 'e-mail', email: 'e-mail',
Empty_title: 'Título vazio',
Enable_notifications: 'Habilitar notificações', Enable_notifications: 'Habilitar notificações',
Everyone_can_access_this_channel: 'Todos podem acessar este canal', Everyone_can_access_this_channel: 'Todos podem acessar este canal',
Error_uploading: 'Erro subindo', Error_uploading: 'Erro subindo',
@ -201,6 +206,10 @@ export default {
Forgot_password_If_this_email_is_registered: 'Se este e-mail estiver cadastrado, enviaremos instruções sobre como redefinir sua senha. Se você não receber um e-mail em breve, volte e tente novamente.', Forgot_password_If_this_email_is_registered: 'Se este e-mail estiver cadastrado, enviaremos instruções sobre como redefinir sua senha. Se você não receber um e-mail em breve, volte e tente novamente.',
Forgot_password: 'Esqueceu sua senha?', Forgot_password: 'Esqueceu sua senha?',
Forgot_Password: 'Esqueci minha senha', Forgot_Password: 'Esqueci minha senha',
Forward: 'Encaminhar',
Forward_Chat: 'Encaminhar Conversa',
Forward_to_department: 'Encaminhar para departamento',
Forward_to_user: 'Encaminhar para usuário',
Full_table: 'Clique para ver a tabela completa', Full_table: 'Clique para ver a tabela completa',
Generate_New_Link: 'Gerar novo convite', Generate_New_Link: 'Gerar novo convite',
Group_by_favorites: 'Agrupar favoritos', Group_by_favorites: 'Agrupar favoritos',
@ -223,6 +232,7 @@ export default {
Message_HideType_subscription_role_removed: 'Papel removido', Message_HideType_subscription_role_removed: 'Papel removido',
Message_HideType_room_archived: 'Sala arquivada', Message_HideType_room_archived: 'Sala arquivada',
Message_HideType_room_unarchived: 'Sala desarquivada', Message_HideType_room_unarchived: 'Sala desarquivada',
IP: 'IP',
In_app: 'No app', In_app: 'No app',
Invisible: 'Invisível', Invisible: 'Invisível',
Invite: 'Convidar', Invite: 'Convidar',
@ -269,6 +279,7 @@ export default {
N_users: '{{n}} usuários', N_users: '{{n}} usuários',
name: 'nome', name: 'nome',
Name: 'Nome', Name: 'Nome',
Navigation_history: 'Histórico de navegação',
Never: 'Nunca', Never: 'Nunca',
New_in_RocketChat_question_mark: 'Novo no Rocket.Chat?', New_in_RocketChat_question_mark: 'Novo no Rocket.Chat?',
New_Message: 'Nova Mensagem', New_Message: 'Nova Mensagem',
@ -289,6 +300,7 @@ export default {
Notify_active_in_this_room: 'Notificar usuários ativos nesta sala', Notify_active_in_this_room: 'Notificar usuários ativos nesta sala',
Notify_all_in_this_room: 'Notificar todos nesta sala', Notify_all_in_this_room: 'Notificar todos nesta sala',
Not_RC_Server: 'Este não é um servidor Rocket.Chat.\n{{contact}}', Not_RC_Server: 'Este não é um servidor Rocket.Chat.\n{{contact}}',
No_available_agents_to_transfer: 'Nenhum agente disponível para transferência',
Offline: 'Offline', Offline: 'Offline',
Oops: 'Ops!', Oops: 'Ops!',
Onboarding_description: 'Workspace é o espaço de colaboração do seu time ou organização. Peça um convite ou o endereço ao seu administrador ou crie uma workspace para o seu time.', Onboarding_description: 'Workspace é o espaço de colaboração do seu time ou organização. Peça um convite ou o endereço ao seu administrador ou crie uma workspace para o seu time.',
@ -305,6 +317,7 @@ export default {
Open_Source_Communication: 'Comunicação Open Source', Open_Source_Communication: 'Comunicação Open Source',
Open_your_authentication_app_and_enter_the_code: 'Abra seu aplicativo de autenticação e digite o código.', Open_your_authentication_app_and_enter_the_code: 'Abra seu aplicativo de autenticação e digite o código.',
OR: 'OU', OR: 'OU',
OS: 'SO',
Overwrites_the_server_configuration_and_use_room_config: 'Substituir a configuração do servidor e usar a configuração da sala', Overwrites_the_server_configuration_and_use_room_config: 'Substituir a configuração do servidor e usar a configuração da sala',
Password: 'Senha', Password: 'Senha',
Parent_channel_or_group: 'Canal ou grupo pai', Parent_channel_or_group: 'Canal ou grupo pai',
@ -315,6 +328,7 @@ export default {
Pinned: 'Mensagens Fixadas', Pinned: 'Mensagens Fixadas',
Please_wait: 'Por favor, aguarde.', Please_wait: 'Por favor, aguarde.',
Please_enter_your_password: 'Por favor, digite sua senha', Please_enter_your_password: 'Por favor, digite sua senha',
Please_add_a_comment: 'Por favor, adicione um comentário',
Preferences: 'Preferências', Preferences: 'Preferências',
Preferences_saved: 'Preferências salvas!', Preferences_saved: 'Preferências salvas!',
Privacy_Policy: ' Política de Privacidade', Privacy_Policy: ' Política de Privacidade',
@ -343,6 +357,7 @@ export default {
Reset_password: 'Resetar senha', Reset_password: 'Resetar senha',
resetting_password: 'redefinindo senha', resetting_password: 'redefinindo senha',
RESET: 'RESETAR', RESET: 'RESETAR',
Return: 'Retornar',
Review_app_title: 'Você está gostando do app?', Review_app_title: 'Você está gostando do app?',
Review_app_desc: 'Nos dê 5 estrelas na {{store}}', Review_app_desc: 'Nos dê 5 estrelas na {{store}}',
Review_app_yes: 'Claro!', Review_app_yes: 'Claro!',
@ -377,7 +392,9 @@ export default {
Select_Server: 'Selecionar Servidor', Select_Server: 'Selecionar Servidor',
Select_Users: 'Selecionar Usuários', Select_Users: 'Selecionar Usuários',
Select_a_Channel: 'Selecione um canal', Select_a_Channel: 'Selecione um canal',
Select_a_Department: 'Selecione um Departamento',
Select_an_option: 'Selecione uma opção', Select_an_option: 'Selecione uma opção',
Select_a_User: 'Selecione um Usuário',
Send: 'Enviar', Send: 'Enviar',
Send_audio_message: 'Enviar mensagem de áudio', Send_audio_message: 'Enviar mensagem de áudio',
Send_message: 'Enviar mensagem', Send_message: 'Enviar mensagem',
@ -436,6 +453,7 @@ export default {
Updating: 'Atualizando...', Updating: 'Atualizando...',
Uploading: 'Subindo arquivo', Uploading: 'Subindo arquivo',
Upload_file_question_mark: 'Enviar arquivo?', Upload_file_question_mark: 'Enviar arquivo?',
User: 'Usuário',
Users: 'Usuários', Users: 'Usuários',
User_added_by: 'Usuário {{userAdded}} adicionado por {{userBy}}', User_added_by: 'Usuário {{userAdded}} adicionado por {{userBy}}',
User_has_been_key: 'Usuário foi {{key}}!', User_has_been_key: 'Usuário foi {{key}}!',
@ -479,8 +497,10 @@ export default {
Your_invite_link_will_never_expire: 'Seu link de convite nunca irá vencer.', Your_invite_link_will_never_expire: 'Seu link de convite nunca irá vencer.',
Your_workspace: 'Sua workspace', Your_workspace: 'Sua workspace',
You_will_not_be_able_to_recover_this_message: 'Você não será capaz de recuperar essa mensagem!', You_will_not_be_able_to_recover_this_message: 'Você não será capaz de recuperar essa mensagem!',
Would_you_like_to_return_the_inquiry: 'Deseja retornar a consulta?',
Write_External_Permission_Message: 'Rocket Chat precisa de acesso à sua galeria para salvar imagens', Write_External_Permission_Message: 'Rocket Chat precisa de acesso à sua galeria para salvar imagens',
Write_External_Permission: 'Acesso à Galeria', Write_External_Permission: 'Acesso à Galeria',
Yes: 'Sim',
Crash_report_disclaimer: 'Nós não rastreamos o conteúdo das suas conversas. O relatório de erros apenas contém informações relevantes para identificarmos problemas e corrigí-los.', Crash_report_disclaimer: 'Nós não rastreamos o conteúdo das suas conversas. O relatório de erros apenas contém informações relevantes para identificarmos problemas e corrigí-los.',
Type_message: 'Digitar mensagem', Type_message: 'Digitar mensagem',
Room_search: 'Busca de sala', Room_search: 'Busca de sala',

View File

@ -168,6 +168,15 @@ const ChatsStack = createStackNavigator({
NotificationPrefView: { NotificationPrefView: {
getScreen: () => require('./views/NotificationPreferencesView').default getScreen: () => require('./views/NotificationPreferencesView').default
}, },
VisitorNavigationView: {
getScreen: () => require('./views/VisitorNavigationView').default
},
ForwardLivechatView: {
getScreen: () => require('./views/ForwardLivechatView').default
},
LivechatEditView: {
getScreen: () => require('./views/LivechatEditView').default
},
PickerView: { PickerView: {
getScreen: () => require('./views/PickerView').default getScreen: () => require('./views/PickerView').default
}, },

View File

@ -13,4 +13,14 @@ export default class Room extends Model {
@field('encrypted') encrypted; @field('encrypted') encrypted;
@field('ro') ro; @field('ro') ro;
@json('v', sanitizer) v;
@json('served_by', sanitizer) servedBy;
@field('department_id') departmentId;
@json('livechat_data', sanitizer) livechatData;
@json('tags', sanitizer) tags;
} }

View File

@ -97,4 +97,14 @@ export default class Subscription extends Model {
@json('uids', sanitizer) uids; @json('uids', sanitizer) uids;
@json('usernames', sanitizer) usernames; @json('usernames', sanitizer) usernames;
@json('visitor', sanitizer) visitor;
@field('department_id') departmentId;
@json('served_by', sanitizer) servedBy;
@json('livechat_data', sanitizer) livechatData;
@json('tags', sanitizer) tags;
} }

View File

@ -99,7 +99,22 @@ export default schemaMigrations({
addColumns({ addColumns({
table: 'subscriptions', table: 'subscriptions',
columns: [ columns: [
{ name: 'banner_closed', type: 'boolean', isOptional: true } { name: 'banner_closed', type: 'boolean', isOptional: true },
{ name: 'visitor', type: 'string', isOptional: true },
{ name: 'department_id', type: 'string', isOptional: true },
{ name: 'served_by', type: 'string', isOptional: true },
{ name: 'livechat_data', type: 'string', isOptional: true },
{ name: 'tags', type: 'string', isOptional: true }
]
}),
addColumns({
table: 'rooms',
columns: [
{ name: 'v', type: 'string', isOptional: true },
{ name: 'department_id', type: 'string', isOptional: true },
{ name: 'served_by', type: 'string', isOptional: true },
{ name: 'livechat_data', type: 'string', isOptional: true },
{ name: 'tags', type: 'string', isOptional: true }
] ]
}) })
] ]

View File

@ -43,7 +43,12 @@ export default appSchema({
{ name: 'hide_unread_status', type: 'boolean', isOptional: true }, { name: 'hide_unread_status', type: 'boolean', isOptional: true },
{ name: 'sys_mes', type: 'string', isOptional: true }, { name: 'sys_mes', type: 'string', isOptional: true },
{ name: 'uids', type: 'string', isOptional: true }, { name: 'uids', type: 'string', isOptional: true },
{ name: 'usernames', type: 'string', isOptional: true } { name: 'usernames', type: 'string', isOptional: true },
{ name: 'visitor', type: 'string', isOptional: true },
{ name: 'department_id', type: 'string', isOptional: true },
{ name: 'served_by', type: 'string', isOptional: true },
{ name: 'livechat_data', type: 'string', isOptional: true },
{ name: 'tags', type: 'string', isOptional: true }
] ]
}), }),
tableSchema({ tableSchema({
@ -52,7 +57,12 @@ export default appSchema({
{ name: 'custom_fields', type: 'string' }, { name: 'custom_fields', type: 'string' },
{ name: 'broadcast', type: 'boolean' }, { name: 'broadcast', type: 'boolean' },
{ name: 'encrypted', type: 'boolean' }, { name: 'encrypted', type: 'boolean' },
{ name: 'ro', type: 'boolean' } { name: 'ro', type: 'boolean' },
{ name: 'v', type: 'string', isOptional: true },
{ name: 'department_id', type: 'string', isOptional: true },
{ name: 'served_by', type: 'string', isOptional: true },
{ name: 'livechat_data', type: 'string', isOptional: true },
{ name: 'tags', type: 'string', isOptional: true }
] ]
}), }),
tableSchema({ tableSchema({

View File

@ -44,7 +44,12 @@ export default async(subscriptions = [], rooms = []) => {
autoTranslateLanguage: s.autoTranslateLanguage, autoTranslateLanguage: s.autoTranslateLanguage,
lastMessage: s.lastMessage, lastMessage: s.lastMessage,
usernames: s.usernames, usernames: s.usernames,
uids: s.uids uids: s.uids,
visitor: s.visitor,
departmentId: s.departmentId,
servedBy: s.servedBy,
livechatData: s.livechatData,
tags: s.tags
})); }));
subscriptions = subscriptions.concat(existingSubs); subscriptions = subscriptions.concat(existingSubs);
@ -65,7 +70,12 @@ export default async(subscriptions = [], rooms = []) => {
ro: r.ro, ro: r.ro,
broadcast: r.broadcast, broadcast: r.broadcast,
muted: r.muted, muted: r.muted,
sysMes: r.sysMes sysMes: r.sysMes,
v: r.v,
departmentId: r.departmentId,
servedBy: r.servedBy,
livechatData: r.livechatData,
tags: r.tags
})); }));
rooms = rooms.concat(existingRooms); rooms = rooms.concat(existingRooms);
} catch { } catch {

View File

@ -35,6 +35,21 @@ export const merge = (subscription, room) => {
} else { } else {
subscription.muted = []; subscription.muted = [];
} }
if (room.v) {
subscription.visitor = room.v;
}
if (room.departmentId) {
subscription.departmentId = room.departmentId;
}
if (room.servedBy) {
subscription.servedBy = room.servedBy;
}
if (room.livechatData) {
subscription.livechatData = room.livechatData;
}
if (room.tags) {
subscription.tags = room.tags;
}
subscription.sysMes = room.sysMes; subscription.sysMes = room.sysMes;
} }

View File

@ -75,7 +75,12 @@ const createOrUpdateSubscription = async(subscription, room) => {
lastMessage: s.lastMessage, lastMessage: s.lastMessage,
roles: s.roles, roles: s.roles,
usernames: s.usernames, usernames: s.usernames,
uids: s.uids uids: s.uids,
visitor: s.visitor,
departmentId: s.departmentId,
servedBy: s.servedBy,
livechatData: s.livechatData,
tags: s.tags
}; };
} catch (error) { } catch (error) {
try { try {
@ -98,10 +103,15 @@ const createOrUpdateSubscription = async(subscription, room) => {
// We have to create a plain obj so we can manipulate it on `merge` // We have to create a plain obj so we can manipulate it on `merge`
// Can we do it in a better way? // Can we do it in a better way?
room = { room = {
customFields: r.customFields, v: r.v,
broadcast: r.broadcast, ro: r.ro,
tags: r.tags,
servedBy: r.servedBy,
encrypted: r.encrypted, encrypted: r.encrypted,
ro: r.ro broadcast: r.broadcast,
customFields: r.customFields,
departmentId: r.departmentId,
livechatData: r.livechatData
}; };
} catch (error) { } catch (error) {
// Do nothing // Do nothing

View File

@ -757,6 +757,59 @@ const RocketChat = {
return this.sdk.get('rooms.info', { roomId }); return this.sdk.get('rooms.info', { roomId });
}, },
getVisitorInfo(visitorId) {
// RC 2.3.0
return this.sdk.get('livechat/visitors.info', { visitorId });
},
closeLivechat(rid, comment) {
// RC 0.29.0
return this.methodCall('livechat:closeRoom', rid, comment, { clientAction: true });
},
editLivechat(userData, roomData) {
// RC 0.55.0
return this.methodCall('livechat:saveInfo', userData, roomData);
},
returnLivechat(rid) {
// RC 0.72.0
return this.methodCall('livechat:returnAsInquiry', rid);
},
forwardLivechat(transferData) {
// RC 0.36.0
return this.methodCall('livechat:transfer', transferData);
},
getPagesLivechat(rid, offset) {
// RC 2.3.0
return this.sdk.get(`livechat/visitors.pagesVisited/${ rid }?count=50&offset=${ offset }`);
},
getDepartmentInfo(departmentId) {
// RC 2.2.0
return this.sdk.get(`livechat/department/${ departmentId }?includeAgents=false`);
},
getDepartments() {
// RC 2.2.0
return this.sdk.get('livechat/department');
},
usersAutoComplete(selector) {
// RC 2.4.0
return this.sdk.get('users.autocomplete', { selector });
},
getRoutingConfig() {
// RC 2.0.0
return this.methodCall('livechat:getRoutingConfig');
},
getTagsList() {
// RC 2.0.0
return this.methodCall('livechat:getTagsList');
},
getAgentDepartments(uid) {
// RC 2.4.0
return this.sdk.get(`livechat/agents/${ uid }/departments`);
},
getCustomFields() {
// RC 2.2.0
return this.sdk.get('livechat/custom-fields');
},
getUidDirectMessage(room) { getUidDirectMessage(room) {
const { id: userId } = reduxStore.getState().login.user; const { id: userId } = reduxStore.getState().login.user;

View File

@ -11,7 +11,7 @@ const TypeIcon = React.memo(({
if (type === 'd' && !isGroupChat) { if (type === 'd' && !isGroupChat) {
return <Status style={styles.status} size={10} status={status} />; return <Status style={styles.status} size={10} status={status} />;
} }
return <RoomTypeIcon theme={theme} type={prid ? 'discussion' : type} isGroupChat={isGroupChat} />; return <RoomTypeIcon theme={theme} type={prid ? 'discussion' : type} isGroupChat={isGroupChat} status={status} />;
}); });
TypeIcon.propTypes = { TypeIcon.propTypes = {

View File

@ -209,12 +209,20 @@ RoomItem.defaultProps = {
getUserPresence: () => {} getUserPresence: () => {}
}; };
const mapStateToProps = (state, ownProps) => ({ const mapStateToProps = (state, ownProps) => {
let status = 'offline';
const { id, type, visitor = {} } = ownProps;
if (state.meteor.connected) {
if (type === 'd') {
status = state.activeUsers[id]?.status || 'offline';
} else if (type === 'l' && visitor?.status) {
({ status } = visitor);
}
}
return {
connected: state.meteor.connected, connected: state.meteor.connected,
status: status
state.meteor.connected && ownProps.type === 'd' };
? state.activeUsers[ownProps.id] && state.activeUsers[ownProps.id].status };
: 'offline'
});
export default connect(mapStateToProps)(RoomItem); export default connect(mapStateToProps)(RoomItem);

View File

@ -31,6 +31,18 @@ export default function(state = initialState, action) {
rid: action.rid, rid: action.rid,
isDeleting: true isDeleting: true
}; };
case ROOM.CLOSE:
return {
...state,
rid: action.rid,
isDeleting: true
};
case ROOM.FORWARD:
return {
...state,
rid: action.rid,
isDeleting: true
};
case ROOM.REMOVED: case ROOM.REMOVED:
return { return {
...state, ...state,

View File

@ -1,4 +1,5 @@
import { Alert } from 'react-native'; import { Alert } from 'react-native';
import prompt from 'react-native-prompt-android';
import { import {
takeLatest, take, select, delay, race, put takeLatest, take, select, delay, race, put
} from 'redux-saga/effects'; } from 'redux-saga/effects';
@ -9,6 +10,7 @@ import { removedRoom } from '../actions/room';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import log from '../utils/log'; import log from '../utils/log';
import I18n from '../i18n'; import I18n from '../i18n';
import { showErrorAlert } from '../utils/info';
const watchUserTyping = function* watchUserTyping({ rid, status }) { const watchUserTyping = function* watchUserTyping({ rid, status }) {
const auth = yield select(state => state.login.isAuthenticated); const auth = yield select(state => state.login.isAuthenticated);
@ -28,10 +30,8 @@ const watchUserTyping = function* watchUserTyping({ rid, status }) {
} }
}; };
const handleRemovedRoom = function* handleLeaveRoom({ result }) { const handleRemovedRoom = function* handleRemovedRoom() {
if (result.success) {
yield Navigation.navigate('RoomsListView'); yield Navigation.navigate('RoomsListView');
}
// types.ROOM.REMOVE is triggered by `subscriptions-changed` with `removed` arg // types.ROOM.REMOVE is triggered by `subscriptions-changed` with `removed` arg
const { timeout } = yield race({ const { timeout } = yield race({
deleteFinished: take(types.ROOM.REMOVED), deleteFinished: take(types.ROOM.REMOVED),
@ -45,7 +45,9 @@ const handleRemovedRoom = function* handleLeaveRoom({ result }) {
const handleLeaveRoom = function* handleLeaveRoom({ rid, t }) { const handleLeaveRoom = function* handleLeaveRoom({ rid, t }) {
try { try {
const result = yield RocketChat.leaveRoom(rid, t); const result = yield RocketChat.leaveRoom(rid, t);
yield handleRemovedRoom({ result }); if (result.success) {
yield handleRemovedRoom();
}
} catch (e) { } catch (e) {
if (e.data && e.data.errorType === 'error-you-are-last-owner') { if (e.data && e.data.errorType === 'error-you-are-last-owner') {
Alert.alert(I18n.t('Oops'), I18n.t(e.data.errorType)); Alert.alert(I18n.t('Oops'), I18n.t(e.data.errorType));
@ -58,15 +60,65 @@ const handleLeaveRoom = function* handleLeaveRoom({ rid, t }) {
const handleDeleteRoom = function* handleDeleteRoom({ rid, t }) { const handleDeleteRoom = function* handleDeleteRoom({ rid, t }) {
try { try {
const result = yield RocketChat.deleteRoom(rid, t); const result = yield RocketChat.deleteRoom(rid, t);
yield handleRemovedRoom({ result }); if (result.success) {
yield handleRemovedRoom();
}
} catch (e) { } catch (e) {
Alert.alert(I18n.t('Oops'), I18n.t('There_was_an_error_while_action', { action: I18n.t('deleting_room') })); Alert.alert(I18n.t('Oops'), I18n.t('There_was_an_error_while_action', { action: I18n.t('deleting_room') }));
} }
}; };
const handleCloseRoom = function* handleCloseRoom({ rid }) {
const requestComment = yield select(state => state.settings.Livechat_request_comment_when_closing_conversation);
const closeRoom = async(comment = '') => {
try {
await RocketChat.closeLivechat(rid, comment);
Navigation.navigate('RoomsListView');
} catch {
// do nothing
}
};
if (!requestComment) {
const comment = I18n.t('Chat_closed_by_agent');
return closeRoom(comment);
}
prompt(
I18n.t('Closing_chat'),
I18n.t('Please_add_a_comment'),
[
{ text: I18n.t('Cancel'), onPress: () => { }, style: 'cancel' },
{
text: I18n.t('Submit'),
onPress: comment => closeRoom(comment)
}
],
{
cancelable: true
}
);
};
const handleForwardRoom = function* handleForwardRoom({ transferData }) {
try {
const result = yield RocketChat.forwardLivechat(transferData);
if (result === true) {
Navigation.navigate('RoomsListView');
} else {
showErrorAlert(I18n.t('No_available_agents_to_transfer'), I18n.t('Oops'));
}
} catch (e) {
showErrorAlert(e.reason, I18n.t('Oops'));
}
};
const root = function* root() { const root = function* root() {
yield takeLatest(types.ROOM.USER_TYPING, watchUserTyping); yield takeLatest(types.ROOM.USER_TYPING, watchUserTyping);
yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom); yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom);
yield takeLatest(types.ROOM.DELETE, handleDeleteRoom); yield takeLatest(types.ROOM.DELETE, handleDeleteRoom);
yield takeLatest(types.ROOM.CLOSE, handleCloseRoom);
yield takeLatest(types.ROOM.FORWARD, handleForwardRoom);
}; };
export default root; export default root;

View File

@ -0,0 +1,150 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { View, StyleSheet } from 'react-native';
import { connect } from 'react-redux';
import I18n from '../i18n';
import { withTheme } from '../theme';
import { themes } from '../constants/colors';
import RocketChat from '../lib/rocketchat';
import OrSeparator from '../containers/OrSeparator';
import Input from '../containers/UIKit/MultiSelect/Input';
import { forwardRoom as forwardRoomAction } from '../actions/room';
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16
}
});
const ForwardLivechatView = ({ forwardRoom, navigation, theme }) => {
const [departments, setDepartments] = useState([]);
const [departmentId, setDepartment] = useState();
const [users, setUsers] = useState([]);
const [userId, setUser] = useState();
const [room, setRoom] = useState();
const rid = navigation.getParam('rid');
const getDepartments = async() => {
try {
const result = await RocketChat.getDepartments();
if (result.success) {
setDepartments(result.departments.map(department => ({ label: department.name, value: department._id })));
}
} catch {
// do nothing
}
};
const getUsers = async(term = '') => {
try {
const { servedBy: { _id: agentId } = {} } = room;
const _id = agentId && { $ne: agentId };
const result = await RocketChat.usersAutoComplete({ conditions: { _id, status: { $ne: 'offline' }, statusLivechat: 'available' }, term });
if (result.success) {
const parsedUsers = result.items.map(user => ({ label: user.username, value: user._id }));
setUsers(parsedUsers);
return parsedUsers;
}
} catch {
// do nothing
}
return [];
};
const getRoom = async() => {
try {
const result = await RocketChat.getRoomInfo(rid);
if (result.success) {
setRoom(result.room);
}
} catch {
// do nothing
}
};
const submit = () => {
const transferData = { roomId: rid };
if (!departmentId && !userId) {
return;
}
if (userId) {
transferData.userId = userId;
} else {
transferData.departmentId = departmentId;
}
forwardRoom(rid, transferData);
};
useEffect(() => {
getRoom();
}, []);
useEffect(() => {
if (room) {
getUsers();
getDepartments();
}
}, [room]);
useEffect(() => {
if (departmentId || userId) {
submit();
}
}, [departmentId, userId]);
const onPressDepartment = () => {
navigation.navigate('PickerView', {
title: I18n.t('Forward_to_department'),
value: room?.departmentId,
data: departments,
onChangeValue: setDepartment,
goBack: false
});
};
const onPressUser = () => {
navigation.navigate('PickerView', {
title: I18n.t('Forward_to_user'),
data: users,
onChangeValue: setUser,
onChangeText: getUsers,
goBack: false
});
};
return (
<View style={[styles.container, { backgroundColor: themes[theme].auxiliaryBackground }]}>
<Input
onPress={onPressDepartment}
placeholder={I18n.t('Select_a_Department')}
theme={theme}
/>
<OrSeparator theme={theme} />
<Input
onPress={onPressUser}
placeholder={I18n.t('Select_a_User')}
theme={theme}
/>
</View>
);
};
ForwardLivechatView.propTypes = {
forwardRoom: PropTypes.func,
navigation: PropTypes.object,
theme: PropTypes.string
};
ForwardLivechatView.navigationOptions = {
title: I18n.t('Forward_Chat')
};
const mapDispatchToProps = dispatch => ({
forwardRoom: (rid, transferData) => dispatch(forwardRoomAction(rid, transferData))
});
export default connect(null, mapDispatchToProps)(withTheme(ForwardLivechatView));

View File

@ -0,0 +1,285 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Text, StyleSheet, ScrollView } from 'react-native';
import { SafeAreaView } from 'react-navigation';
import { connect } from 'react-redux';
import { withTheme } from '../theme';
import { themes } from '../constants/colors';
import TextInput from '../containers/TextInput';
import KeyboardView from '../presentation/KeyboardView';
import RocketChat from '../lib/rocketchat';
import I18n from '../i18n';
import sharedStyles from './Styles';
import { LISTENER } from '../containers/Toast';
import EventEmitter from '../utils/events';
import scrollPersistTaps from '../utils/scrollPersistTaps';
import { getUserSelector } from '../selectors/login';
import Chips from '../containers/UIKit/MultiSelect/Chips';
import Button from '../containers/Button';
const styles = StyleSheet.create({
container: {
padding: 16
},
title: {
fontSize: 20,
paddingVertical: 10,
...sharedStyles.textMedium
}
});
const Title = ({ title, theme }) => (title ? <Text style={[styles.title, { color: themes[theme].titleText }]}>{title}</Text> : null);
Title.propTypes = {
title: PropTypes.string,
theme: PropTypes.string
};
const LivechatEditView = ({ user, navigation, theme }) => {
const [customFields, setCustomFields] = useState({});
const [availableUserTags, setAvailableUserTags] = useState([]);
const params = {};
const inputs = {};
const livechat = navigation.getParam('room', {});
const visitor = navigation.getParam('roomUser', {});
const getCustomFields = async() => {
const result = await RocketChat.getCustomFields();
if (result.success && result.customFields?.length) {
const visitorCustomFields = result.customFields
.filter(field => field.visibility !== 'hidden' && field.scope === 'visitor')
.map(field => ({ [field._id]: (visitor.livechatData && visitor.livechatData[field._id]) || '' }))
.reduce((ret, field) => ({ [field]: field, ...ret }));
const livechatCustomFields = result.customFields
.filter(field => field.visibility !== 'hidden' && field.scope === 'room')
.map(field => ({ [field._id]: (livechat.livechatData && livechat.livechatData[field._id]) || '' }))
.reduce((ret, field) => ({ [field]: field, ...ret }));
return setCustomFields({ visitor: visitorCustomFields, livechat: livechatCustomFields });
}
};
const [tagParam, setTags] = useState(livechat?.tags || []);
useEffect(() => {
setTags([...tagParam, ...availableUserTags]);
}, [availableUserTags]);
const getTagsList = async(agentDepartments) => {
const tags = await RocketChat.getTagsList();
const isAdmin = ['admin', 'livechat-manager'].find(role => user.roles.includes(role));
const availableTags = tags
.filter(({ departments }) => isAdmin || (departments.length === 0 || departments.some(i => agentDepartments.indexOf(i) > -1)))
.map(({ name }) => name);
setAvailableUserTags(availableTags);
};
const getAgentDepartments = async() => {
const result = await RocketChat.getAgentDepartments(visitor?._id);
if (result.success) {
const agentDepartments = result.departments.map(dept => dept.departmentId);
getTagsList(agentDepartments);
}
};
const submit = async() => {
const userData = { _id: visitor?._id };
const { rid, sms } = livechat;
const roomData = { _id: rid };
if (params.name) {
userData.name = params.name;
}
if (params.email) {
userData.email = params.email;
}
if (params.phone) {
userData.phone = params.phone;
}
userData.livechatData = {};
Object.entries(customFields?.visitor || {}).forEach(([key]) => {
if (params[key] || params[key] === '') {
userData.livechatData[key] = params[key];
}
});
if (params.topic) {
roomData.topic = params.topic;
}
roomData.tags = tagParam;
roomData.livechatData = {};
Object.entries(customFields?.livechat || {}).forEach(([key]) => {
if (params[key] || params[key] === '') {
roomData.livechatData[key] = params[key];
}
});
if (sms) {
delete userData.phone;
}
const { error } = await RocketChat.editLivechat(userData, roomData);
if (error) {
EventEmitter.emit(LISTENER, { message: error });
} else {
EventEmitter.emit(LISTENER, { message: I18n.t('Saved') });
navigation.goBack();
}
};
const onChangeText = (key, text) => { params[key] = text; };
useEffect(() => {
getAgentDepartments();
getCustomFields();
}, []);
return (
<KeyboardView
style={{ backgroundColor: themes[theme].auxiliaryBackground }}
contentContainerStyle={sharedStyles.container}
keyboardVerticalOffset={128}
>
<ScrollView {...scrollPersistTaps}>
<SafeAreaView style={[sharedStyles.container, styles.container]} forceInset={{ vertical: 'never' }}>
<Title
title={visitor?.username}
theme={theme}
/>
<TextInput
label={I18n.t('Name')}
defaultValue={visitor?.name}
onChangeText={text => onChangeText('name', text)}
onSubmitEditing={() => { inputs.name.focus(); }}
theme={theme}
/>
<TextInput
label={I18n.t('Email')}
inputRef={(e) => { inputs.name = e; }}
defaultValue={visitor?.visitorEmails && visitor?.visitorEmails[0]?.address}
onChangeText={text => onChangeText('email', text)}
onSubmitEditing={() => { inputs.phone.focus(); }}
theme={theme}
/>
<TextInput
label={I18n.t('Phone')}
inputRef={(e) => { inputs.phone = e; }}
defaultValue={visitor?.phone && visitor?.phone[0]?.phoneNumber}
onChangeText={text => onChangeText('phone', text)}
onSubmitEditing={() => {
const keys = Object.keys(customFields?.visitor || {});
if (keys.length > 0) {
const key = keys.pop();
inputs[key].focus();
} else {
inputs.topic.focus();
}
}}
theme={theme}
/>
{Object.entries(customFields?.visitor || {}).map(([key, value], index, array) => (
<TextInput
label={key}
defaultValue={value}
inputRef={(e) => { inputs[key] = e; }}
onChangeText={text => onChangeText(key, text)}
onSubmitEditing={() => {
if (array.length - 1 > index) {
return inputs[array[index + 1]].focus();
}
inputs.topic.focus();
}}
theme={theme}
/>
))}
<Title
title={I18n.t('Conversation')}
theme={theme}
/>
<TextInput
label={I18n.t('Topic')}
inputRef={(e) => { inputs.topic = e; }}
defaultValue={livechat?.topic}
onChangeText={text => onChangeText('topic', text)}
onSubmitEditing={() => inputs.tags.focus()}
theme={theme}
/>
<TextInput
inputRef={(e) => { inputs.tags = e; }}
label={I18n.t('Tags')}
iconRight='plus'
onIconRightPress={() => {
const lastText = inputs.tags._lastNativeText || '';
if (lastText.length) {
setTags([...tagParam.filter(t => t !== lastText), lastText]);
inputs.tags.clear();
}
}}
onSubmitEditing={() => {
const keys = Object.keys(customFields?.livechat || {});
if (keys.length > 0) {
const key = keys.pop();
inputs[key].focus();
} else {
submit();
}
}}
theme={theme}
/>
<Chips
items={tagParam.map(tag => ({ text: { text: tag }, value: tag }))}
onSelect={tag => setTags(tagParam.filter(t => t !== tag.value) || [])}
style={{ backgroundColor: themes[theme].backgroundColor }}
theme={theme}
/>
{Object.entries(customFields?.livechat || {}).map(([key, value], index, array) => (
<TextInput
label={key}
defaultValue={value}
inputRef={(e) => { inputs[key] = e; }}
onChangeText={text => onChangeText(key, text)}
onSubmitEditing={() => {
if (array.length - 1 > index) {
return inputs[array[index + 1]].focus();
}
submit();
}}
theme={theme}
/>
))}
<Button
title={I18n.t('Save')}
onPress={submit}
theme={theme}
/>
</SafeAreaView>
</ScrollView>
</KeyboardView>
);
};
LivechatEditView.propTypes = {
user: PropTypes.object,
navigation: PropTypes.object,
theme: PropTypes.string
};
LivechatEditView.navigationOptions = ({
title: I18n.t('Livechat_edit')
});
const mapStateToProps = state => ({
server: state.server.server,
user: getUserSelector(state)
});
export default connect(mapStateToProps)(withTheme(LivechatEditView));

View File

@ -19,7 +19,7 @@ import { appStart as appStartAction } from '../actions';
import sharedStyles from './Styles'; import sharedStyles from './Styles';
import Button from '../containers/Button'; import Button from '../containers/Button';
import TextInput from '../containers/TextInput'; import TextInput from '../containers/TextInput';
import OnboardingSeparator from '../containers/OnboardingSeparator'; import OrSeparator from '../containers/OrSeparator';
import FormContainer, { FormContainerInner } from '../containers/FormContainer'; import FormContainer, { FormContainerInner } from '../containers/FormContainer';
import I18n from '../i18n'; import I18n from '../i18n';
import { isIOS } from '../utils/deviceInfo'; import { isIOS } from '../utils/deviceInfo';
@ -324,7 +324,7 @@ class NewServerView extends React.Component {
testID='new-server-view-button' testID='new-server-view-button'
theme={theme} theme={theme}
/> />
<OnboardingSeparator theme={theme} /> <OrSeparator theme={theme} />
<Text style={[styles.description, { color: themes[theme].auxiliaryText }]}>{I18n.t('Onboarding_join_open_description')}</Text> <Text style={[styles.description, { color: themes[theme].auxiliaryText }]}>{I18n.t('Onboarding_join_open_description')}</Text>
<Button <Button
title={I18n.t('Join_our_open_workspace')} title={I18n.t('Join_our_open_workspace')}

View File

@ -1,20 +1,38 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FlatList, StyleSheet } from 'react-native'; import {
View, FlatList, StyleSheet, Text
} from 'react-native';
import I18n from '../i18n'; import I18n from '../i18n';
import { themedHeader } from '../utils/navigation'; import { themedHeader } from '../utils/navigation';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import debounce from '../utils/debounce';
import sharedStyles from './Styles'; import sharedStyles from './Styles';
import ListItem from '../containers/ListItem'; import ListItem from '../containers/ListItem';
import Check from '../containers/Check'; import Check from '../containers/Check';
import Separator from '../containers/Separator'; import Separator from '../containers/Separator';
import SearchBox from '../containers/SearchBox';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
check: { check: {
marginHorizontal: 0 marginHorizontal: 0
},
search: {
width: '100%',
height: 56
},
noResult: {
fontSize: 16,
paddingVertical: 56,
...sharedStyles.textAlignCenter,
...sharedStyles.textSemibold
},
withoutBorder: {
borderBottomWidth: 0,
borderTopWidth: 0
} }
}); });
@ -54,20 +72,46 @@ class PickerView extends React.PureComponent {
const data = props.navigation.getParam('data', []); const data = props.navigation.getParam('data', []);
const value = props.navigation.getParam('value'); const value = props.navigation.getParam('value');
this.state = { data, value }; this.state = { data, value };
this.onSearch = props.navigation.getParam('onChangeText');
} }
onChangeValue = (value) => { onChangeValue = (value) => {
const { navigation } = this.props; const { navigation } = this.props;
const goBack = navigation.getParam('goBack', true);
const onChange = navigation.getParam('onChangeValue', () => {}); const onChange = navigation.getParam('onChangeValue', () => {});
onChange(value); onChange(value);
if (goBack) {
navigation.goBack(); navigation.goBack();
} }
}
onChangeText = debounce(async(text) => {
if (this.onSearch) {
const data = await this.onSearch(text);
this.setState({ data });
}
}, 300, true)
renderSearch() {
if (!this.onSearch) {
return null;
}
return (
<View style={styles.search}>
<SearchBox onChangeText={this.onChangeText} />
</View>
);
}
render() { render() {
const { data, value } = this.state; const { data, value } = this.state;
const { theme } = this.props; const { theme } = this.props;
return ( return (
<>
{this.renderSearch()}
<FlatList <FlatList
data={data} data={data}
keyExtractor={item => item.value} keyExtractor={item => item.value}
@ -75,20 +119,23 @@ class PickerView extends React.PureComponent {
<Item <Item
item={item} item={item}
theme={theme} theme={theme}
selected={(value || data[0]?.value) === item.value} selected={!this.onSearch && (value || data[0]?.value) === item.value}
onItemPress={() => this.onChangeValue(item.value)} onItemPress={() => this.onChangeValue(item.value)}
/> />
)} )}
ItemSeparatorComponent={() => <Separator theme={theme} />} ItemSeparatorComponent={() => <Separator theme={theme} />}
ListEmptyComponent={() => <Text style={[styles.noResult, { color: themes[theme].titleText }]}>{I18n.t('No_results_found')}</Text>}
contentContainerStyle={[ contentContainerStyle={[
sharedStyles.listContentContainer, sharedStyles.listContentContainer,
{ {
backgroundColor: themes[theme].auxiliaryBackground, backgroundColor: themes[theme].auxiliaryBackground,
borderColor: themes[theme].separatorColor borderColor: themes[theme].separatorColor
} },
!data.length && styles.withoutBorder
]} ]}
style={{ backgroundColor: themes[theme].auxiliaryBackground }} style={{ backgroundColor: themes[theme].auxiliaryBackground }}
/> />
</>
); );
} }
} }

View File

@ -9,7 +9,7 @@ import _ from 'lodash';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import { setLoading as setLoadingAction } from '../../actions/selectedUsers'; import { setLoading as setLoadingAction } from '../../actions/selectedUsers';
import { leaveRoom as leaveRoomAction } from '../../actions/room'; import { leaveRoom as leaveRoomAction, closeRoom as closeRoomAction } from '../../actions/room';
import styles from './styles'; import styles from './styles';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
import Avatar from '../../containers/Avatar'; import Avatar from '../../containers/Avatar';
@ -28,6 +28,7 @@ import { themedHeader } from '../../utils/navigation';
import { CloseModalButton } from '../../containers/HeaderButton'; import { CloseModalButton } from '../../containers/HeaderButton';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import Markdown from '../../containers/markdown'; import Markdown from '../../containers/markdown';
import { showConfirmationAlert, showErrorAlert } from '../../utils/info';
class RoomActionsView extends React.Component { class RoomActionsView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => { static navigationOptions = ({ navigation, screenProps }) => {
@ -51,6 +52,7 @@ class RoomActionsView extends React.Component {
leaveRoom: PropTypes.func, leaveRoom: PropTypes.func,
jitsiEnabled: PropTypes.bool, jitsiEnabled: PropTypes.bool,
setLoadingInvite: PropTypes.func, setLoadingInvite: PropTypes.func,
closeRoom: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string
} }
@ -69,7 +71,9 @@ class RoomActionsView extends React.Component {
canViewMembers: false, canViewMembers: false,
canAutoTranslate: false, canAutoTranslate: false,
canAddUser: false, canAddUser: false,
canInviteUser: false canInviteUser: false,
canForwardGuest: false,
canReturnQueue: false
}; };
if (room && room.observe && room.rid) { if (room && room.observe && room.rid) {
this.roomObservable = room.observe(); this.roomObservable = room.observe();
@ -117,6 +121,12 @@ class RoomActionsView extends React.Component {
this.canAddUser(); this.canAddUser();
this.canInviteUser(); this.canInviteUser();
// livechat permissions
if (room.t === 'l') {
this.canForwardGuest();
this.canReturnQueue();
}
} }
} }
@ -186,9 +196,32 @@ class RoomActionsView extends React.Component {
return result; return result;
} }
canForwardGuest = async() => {
const { room } = this.state;
const { rid } = room;
let result = true;
const transferLivechatGuest = 'transfer-livechat-guest';
const permissions = await RocketChat.hasPermission([transferLivechatGuest], rid);
if (!permissions[transferLivechatGuest]) {
result = false;
}
this.setState({ canForwardGuest: result });
}
canReturnQueue = async() => {
try {
const { returnQueue } = await RocketChat.getRoutingConfig();
this.setState({ canReturnQueue: returnQueue });
} catch {
// do nothing
}
}
get sections() { get sections() {
const { const {
room, member, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate room, member, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate, canForwardGuest, canReturnQueue
} = this.state; } = this.state;
const { jitsiEnabled } = this.props; const { jitsiEnabled } = this.props;
const { const {
@ -373,7 +406,42 @@ class RoomActionsView extends React.Component {
}); });
} }
} else if (t === 'l') { } else if (t === 'l') {
sections[2].data = [notificationsAction]; sections[2].data = [];
sections[2].data.push({
icon: 'circle-cross',
name: I18n.t('Close'),
event: this.closeLivechat
});
if (canForwardGuest) {
sections[2].data.push({
icon: 'reply',
name: I18n.t('Forward'),
route: 'ForwardLivechatView',
params: { rid }
});
}
if (canReturnQueue) {
sections[2].data.push({
icon: 'back',
name: I18n.t('Return'),
event: this.returnLivechat
});
}
sections[2].data.push({
icon: 'reload',
name: I18n.t('Navigation_history'),
route: 'VisitorNavigationView',
params: { rid }
});
sections.push({
data: [notificationsAction],
renderItem: this.renderItem
});
} }
return sections; return sections;
@ -384,6 +452,28 @@ class RoomActionsView extends React.Component {
return <View style={[styles.separator, { backgroundColor: themes[theme].separatorColor }]} />; return <View style={[styles.separator, { backgroundColor: themes[theme].separatorColor }]} />;
} }
closeLivechat = () => {
const { room: { rid } } = this.state;
const { closeRoom } = this.props;
closeRoom(rid);
}
returnLivechat = () => {
const { room: { rid } } = this.state;
showConfirmationAlert({
message: I18n.t('Would_you_like_to_return_the_inquiry'),
callToAction: I18n.t('Yes'),
onPress: async() => {
try {
await RocketChat.returnLivechat(rid);
} catch (e) {
showErrorAlert(e.reason, I18n.t('Oops'));
}
}
});
}
updateRoomMember = async() => { updateRoomMember = async() => {
const { room } = this.state; const { room } = this.state;
@ -485,7 +575,7 @@ class RoomActionsView extends React.Component {
? <Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{room.fname}</Text> ? <Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{room.fname}</Text>
: ( : (
<View style={styles.roomTitleRow}> <View style={styles.roomTitleRow}>
<RoomTypeIcon type={room.prid ? 'discussion' : room.t} theme={theme} /> <RoomTypeIcon type={room.prid ? 'discussion' : room.t} status={room.visitor?.status} theme={theme} />
<Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{RocketChat.getRoomTitle(room)}</Text> <Text style={[styles.roomTitle, { color: themes[theme].titleText }]} numberOfLines={1}>{RocketChat.getRoomTitle(room)}</Text>
</View> </View>
) )
@ -583,6 +673,7 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
leaveRoom: (rid, t) => dispatch(leaveRoomAction(rid, t)), leaveRoom: (rid, t) => dispatch(leaveRoomAction(rid, t)),
closeRoom: rid => dispatch(closeRoomAction(rid)),
setLoadingInvite: loading => dispatch(setLoadingAction(loading)) setLoadingInvite: loading => dispatch(setLoadingAction(loading))
}); });

View File

@ -0,0 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import I18n from '../../i18n';
import Item from './Item';
const Channel = ({ room, theme }) => {
const { description, topic, announcement } = room;
return (
<>
<Item
label={I18n.t('Description')}
content={description || `__${ I18n.t('No_label_provided', { label: 'description' }) }__`}
theme={theme}
/>
<Item
label={I18n.t('Topic')}
content={topic || `__${ I18n.t('No_label_provided', { label: 'topic' }) }__`}
theme={theme}
/>
<Item
label={I18n.t('Announcement')}
content={announcement || `__${ I18n.t('No_label_provided', { label: 'announcement' }) }__`}
theme={theme}
/>
<Item
label={I18n.t('Broadcast_Channel')}
content={room.broadcast && I18n.t('Broadcast_channel_Description')}
theme={theme}
/>
</>
);
};
Channel.propTypes = {
room: PropTypes.object,
theme: PropTypes.string
};
export default Channel;

View File

@ -0,0 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
import Item from './Item';
const CustomFields = ({ customFields, theme }) => {
if (customFields) {
return (
Object.keys(customFields).map((title) => {
if (!customFields[title]) {
return;
}
return (
<Item
label={title}
content={customFields[title]}
theme={theme}
/>
);
})
);
}
return null;
};
CustomFields.propTypes = {
customFields: PropTypes.object,
theme: PropTypes.string
};
export default CustomFields;

View File

@ -0,0 +1,42 @@
import React from 'react';
import { View, Text } from 'react-native';
import PropTypes from 'prop-types';
import { themes } from '../../constants/colors';
import I18n from '../../i18n';
import Timezone from './Timezone';
import CustomFields from './CustomFields';
import styles from './styles';
const Roles = ({ roles, theme }) => (roles && roles.length ? (
<View style={styles.item}>
<Text style={[styles.itemLabel, { color: themes[theme].titleText }]}>{I18n.t('Roles')}</Text>
<View style={styles.rolesContainer}>
{roles.map(role => (role ? (
<View style={[styles.roleBadge, { backgroundColor: themes[theme].auxiliaryBackground }]} key={role}>
<Text style={styles.role}>{role}</Text>
</View>
) : null))}
</View>
</View>
) : null);
Roles.propTypes = {
roles: PropTypes.array,
theme: PropTypes.string
};
const Direct = ({ roomUser, theme }) => (
<>
<Roles roles={roomUser.parsedRoles} theme={theme} />
<Timezone utcOffset={roomUser.utcOffset} theme={theme} />
<CustomFields customFields={roomUser.customFields} theme={theme} />
</>
);
Direct.propTypes = {
roomUser: PropTypes.object,
theme: PropTypes.string
};
export default Direct;

View File

@ -0,0 +1,27 @@
import React from 'react';
import { View, Text } from 'react-native';
import PropTypes from 'prop-types';
import styles from './styles';
import Markdown from '../../containers/markdown';
import { themes } from '../../constants/colors';
const Item = ({ label, content, theme }) => (
content ? (
<View style={styles.item}>
<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
};
export default Item;

View File

@ -0,0 +1,140 @@
import React, { useState, useEffect } from 'react';
import { Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import RocketChat from '../../lib/rocketchat';
import { withTheme } from '../../theme';
import CustomFields from './CustomFields';
import Item from './Item';
import Timezone from './Timezone';
import sharedStyles from '../Styles';
import { themes } from '../../constants/colors';
import I18n from '../../i18n';
const styles = StyleSheet.create({
title: {
fontSize: 16,
paddingHorizontal: 20,
...sharedStyles.textMedium
}
});
const Title = ({ title, theme }) => <Text style={[styles.title, { color: themes[theme].titleText }]}>{title}</Text>;
Title.propTypes = {
title: PropTypes.string,
theme: PropTypes.string
};
const Livechat = ({ room, roomUser, theme }) => {
const [department, setDepartment] = useState({});
const getDepartment = async(id) => {
if (id) {
const result = await RocketChat.getDepartmentInfo(id);
if (result.success) {
setDepartment(result.department);
}
}
};
const getRoom = () => {
if (room.departmentId) {
getDepartment(room.departmentId);
}
};
useEffect(() => { getRoom(); }, []);
return (
<>
<Title
title={I18n.t('User')}
theme={theme}
/>
<Timezone
utcOffset={roomUser.utc}
theme={theme}
/>
<Item
label={I18n.t('Username')}
content={roomUser.username}
theme={theme}
/>
<Item
label={I18n.t('Email')}
content={roomUser.visitorEmails?.map(email => email.address).reduce((ret, item) => `${ ret }${ item }\n`)}
theme={theme}
/>
<Item
label={I18n.t('Phone')}
content={roomUser.phone?.map(phone => phone.phoneNumber).reduce((ret, item) => `${ ret }${ item }\n`)}
theme={theme}
/>
<Item
label={I18n.t('IP')}
content={roomUser.ip}
theme={theme}
/>
<Item
label={I18n.t('OS')}
content={roomUser.os}
theme={theme}
/>
<Item
label={I18n.t('Browser')}
content={roomUser.browser}
theme={theme}
/>
<CustomFields
customFields={roomUser.livechatData}
theme={theme}
/>
<Title
title={I18n.t('Conversation')}
theme={theme}
/>
<Item
label={I18n.t('Agent')}
content={room.servedBy?.username}
theme={theme}
/>
<Item
label={I18n.t('Facebook')}
content={room.facebook?.page.name}
theme={theme}
/>
<Item
label={I18n.t('SMS')}
content={room.sms && 'SMS Enabled'}
theme={theme}
/>
<Item
label={I18n.t('Topic')}
content={room.topic}
theme={theme}
/>
<Item
label={I18n.t('Tags')}
content={room.tags?.join(', ')}
theme={theme}
/>
<Item
label={I18n.t('Department')}
content={department.name}
theme={theme}
/>
<CustomFields
customFields={room.livechatData}
theme={theme}
/>
</>
);
};
Livechat.propTypes = {
room: PropTypes.object,
roomUser: PropTypes.object,
theme: PropTypes.string
};
export default withTheme(Livechat);

View File

@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import moment from 'moment';
import I18n from '../../i18n';
import Item from './Item';
const Timezone = ({ utcOffset, Message_TimeFormat, theme }) => (utcOffset ? (
<Item
label={I18n.t('Timezone')}
content={`${ moment().utcOffset(utcOffset).format(Message_TimeFormat) } (UTC ${ utcOffset })`}
theme={theme}
/>
) : null);
Timezone.propTypes = {
utcOffset: PropTypes.number,
Message_TimeFormat: PropTypes.string,
theme: PropTypes.string
};
const mapStateToProps = state => ({
Message_TimeFormat: state.settings.Message_TimeFormat
});
export default connect(mapStateToProps)(Timezone);

View File

@ -3,19 +3,20 @@ import PropTypes from 'prop-types';
import { View, Text, ScrollView } from 'react-native'; import { View, Text, ScrollView } from 'react-native';
import { BorderlessButton } from 'react-native-gesture-handler'; import { BorderlessButton } from 'react-native-gesture-handler';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import moment from 'moment';
import _ from 'lodash';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView } from 'react-navigation';
import UAParser from 'ua-parser-js';
import _ from 'lodash';
import database from '../../lib/database';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import Status from '../../containers/Status'; import Status from '../../containers/Status';
import Avatar from '../../containers/Avatar'; import Avatar from '../../containers/Avatar';
import styles from './styles'; import styles from './styles';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
import database from '../../lib/database';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import RoomTypeIcon from '../../containers/RoomTypeIcon'; import RoomTypeIcon from '../../containers/RoomTypeIcon';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { CustomHeaderButtons, Item } from '../../containers/HeaderButton'; import { CustomHeaderButtons } from '../../containers/HeaderButton';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import log from '../../utils/log'; import log from '../../utils/log';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
@ -24,6 +25,11 @@ import { themedHeader } from '../../utils/navigation';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import Markdown from '../../containers/markdown'; import Markdown from '../../containers/markdown';
import Livechat from './Livechat';
import Channel from './Channel';
import Item from './Item';
import Direct from './Direct';
const PERMISSION_EDIT_ROOM = 'edit-room'; const PERMISSION_EDIT_ROOM = 'edit-room';
const getRoomTitle = (room, type, name, username, statusText, theme) => (type === 'd' const getRoomTitle = (room, type, name, username, statusText, theme) => (type === 'd'
? ( ? (
@ -35,7 +41,7 @@ const getRoomTitle = (room, type, name, username, statusText, theme) => (type ==
) )
: ( : (
<View style={styles.roomTitleRow}> <View style={styles.roomTitleRow}>
<RoomTypeIcon type={room.prid ? 'discussion' : room.t} key='room-info-type' theme={theme} /> <RoomTypeIcon type={room.prid ? 'discussion' : room.t} key='room-info-type' status={room.visitor?.status} theme={theme} />
<Text testID='room-info-view-name' style={[styles.roomTitle, { color: themes[theme].titleText }]} key='room-info-name'>{RocketChat.getRoomTitle(room)}</Text> <Text testID='room-info-view-name' style={[styles.roomTitle, { color: themes[theme].titleText }]} key='room-info-name'>{RocketChat.getRoomTitle(room)}</Text>
</View> </View>
) )
@ -43,16 +49,22 @@ const getRoomTitle = (room, type, name, username, statusText, theme) => (type ==
class RoomInfoView extends React.Component { class RoomInfoView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => { static navigationOptions = ({ navigation, screenProps }) => {
const showEdit = navigation.getParam('showEdit');
const rid = navigation.getParam('rid');
const t = navigation.getParam('t'); const t = navigation.getParam('t');
const rid = navigation.getParam('rid');
const room = navigation.getParam('room');
const roomUser = navigation.getParam('roomUser');
const showEdit = navigation.getParam('showEdit', t === 'l');
return { return {
title: t === 'd' ? I18n.t('User_Info') : I18n.t('Room_Info'), title: t === 'd' ? I18n.t('User_Info') : I18n.t('Room_Info'),
...themedHeader(screenProps.theme), ...themedHeader(screenProps.theme),
headerRight: showEdit headerRight: showEdit
? ( ? (
<CustomHeaderButtons> <CustomHeaderButtons>
<Item iconName='edit' onPress={() => navigation.navigate('RoomInfoEditView', { rid })} testID='room-info-view-edit-button' /> <Item
iconName='edit'
onPress={() => navigation.navigate(t === 'l' ? 'LivechatEditView' : 'RoomInfoEditView', { rid, room, roomUser })}
testID='room-info-view-edit-button'
/>
</CustomHeaderButtons> </CustomHeaderButtons>
) )
: null : null
@ -66,7 +78,6 @@ class RoomInfoView extends React.Component {
token: PropTypes.string token: PropTypes.string
}), }),
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
Message_TimeFormat: PropTypes.string,
theme: PropTypes.string theme: PropTypes.string
} }
@ -78,68 +89,42 @@ class RoomInfoView extends React.Component {
this.t = props.navigation.getParam('t'); this.t = props.navigation.getParam('t');
this.state = { this.state = {
room: room || { rid: this.rid, t: this.t }, room: room || { rid: this.rid, t: this.t },
roomUser: roomUser || {}, roomUser: roomUser || {}
parsedRoles: []
}; };
} }
async componentDidMount() { componentDidMount() {
const { roomUser, room: roomState } = this.state; if (this.isDirect) {
if (this.t === 'd' && !_.isEmpty(roomUser)) { this.loadUser();
return; } else {
} this.loadRoom();
if (this.t === 'd') {
try {
const roomUserId = RocketChat.getUidDirectMessage(roomState);
const result = await RocketChat.getUserInfo(roomUserId);
if (result.success) {
const { roles } = result.user;
let parsedRoles = [];
if (roles && roles.length) {
parsedRoles = await Promise.all(roles.map(async(role) => {
const description = await this.getRoleDescription(role);
return description;
}));
}
this.setState({ roomUser: result.user, parsedRoles });
}
} catch (e) {
log(e);
}
return;
} }
const { navigation } = this.props; const { navigation } = this.props;
let room = navigation.getParam('room'); this.willFocusListener = navigation.addListener('willFocus', () => {
if (room && room.observe) { if (this.isLivechat) {
this.roomObservable = room.observe(); this.loadVisitor();
this.subscription = this.roomObservable }
.subscribe((changes) => {
this.setState({ room: changes });
}); });
} else {
try {
const result = await RocketChat.getRoomInfo(this.rid);
if (result.success) {
// eslint-disable-next-line prefer-destructuring
room = result.room;
this.setState({ room });
}
} catch (e) {
log(e);
}
}
const permissions = await RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid);
if (permissions[PERMISSION_EDIT_ROOM] && !room.prid && this.t !== 'l') {
navigation.setParams({ showEdit: true });
}
} }
componentWillUnmount() { componentWillUnmount() {
if (this.subscription && this.subscription.unsubscribe) { if (this.subscription && this.subscription.unsubscribe) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
if (this.willFocusListener && this.willFocusListener.remove) {
this.willFocusListener.remove();
}
}
get isDirect() {
const { room } = this.state;
return room.t === 'd';
}
get isLivechat() {
const { room } = this.state;
return room.t === 'l';
} }
getRoleDescription = async(id) => { getRoleDescription = async(id) => {
@ -154,86 +139,112 @@ class RoomInfoView extends React.Component {
} catch (e) { } catch (e) {
return null; return null;
} }
};
loadVisitor = async() => {
const { room } = this.state;
const { navigation } = this.props;
try {
const result = await RocketChat.getVisitorInfo(room?.visitor?._id);
if (result.success) {
const { visitor } = result;
if (visitor.userAgent) {
const ua = new UAParser();
ua.setUA(visitor.userAgent);
visitor.os = `${ ua.getOS().name } ${ ua.getOS().version }`;
visitor.browser = `${ ua.getBrowser().name } ${ ua.getBrowser().version }`;
}
this.setState({ roomUser: visitor });
navigation.setParams({ roomUser: visitor });
}
} catch (error) {
// Do nothing
}
} }
goRoom = async() => { loadUser = async() => {
const { roomUser } = this.state; const { room: roomState, roomUser } = this.state;
const { username } = roomUser;
if (_.isEmpty(roomUser)) {
try {
const roomUserId = RocketChat.getUidDirectMessage(roomState);
const result = await RocketChat.getUserInfo(roomUserId);
if (result.success) {
const { user } = result;
const { roles } = user;
if (roles && roles.length) {
user.parsedRoles = await Promise.all(roles.map(async(role) => {
const description = await this.getRoleDescription(role);
return description;
}));
}
const room = await this.getDirect(user.username);
this.setState({ roomUser: user, room: { ...roomState, rid: room.rid } });
}
} catch {
// do nothing
}
}
}
loadRoom = async() => {
const { navigation } = this.props; const { navigation } = this.props;
let room = navigation.getParam('room');
if (room && room.observe) {
this.roomObservable = room.observe();
this.subscription = this.roomObservable
.subscribe((changes) => {
this.setState({ room: changes });
navigation.setParams({ room: changes });
});
} else {
try {
const result = await RocketChat.getRoomInfo(this.rid);
if (result.success) {
({ room } = result);
this.setState({ room });
}
} catch (e) {
log(e);
}
}
const permissions = await RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid);
if (permissions[PERMISSION_EDIT_ROOM] && !room.prid) {
navigation.setParams({ showEdit: true });
}
}
getDirect = async(username) => {
try { try {
const result = await RocketChat.createDirectMessage(username); const result = await RocketChat.createDirectMessage(username);
if (result.success) { if (result.success) {
return result.room;
}
} catch {
// do nothing
}
}
goRoom = async() => {
const { roomUser, room } = this.state;
const { navigation } = this.props;
try {
if (room.rid) {
await navigation.navigate('RoomsListView'); await navigation.navigate('RoomsListView');
const rid = result.room._id; navigation.navigate('RoomView', { rid: room.rid, name: RocketChat.getRoomTitle(roomUser), t: 'd' });
navigation.navigate('RoomView', { rid, name: RocketChat.getRoomTitle(roomUser), t: 'd' });
} }
} catch (e) { } catch (e) {
// do nothing // do nothing
} }
} }
videoCall = () => RocketChat.callJitsi(this.rid) videoCall = () => {
const { room } = this.state;
isDirect = () => this.t === 'd' RocketChat.callJitsi(room.rid);
renderItem = ({ label, content }) => {
const { theme } = this.props;
return (
<View style={styles.item}>
<Text accessibilityLabel={label} style={[styles.itemLabel, { color: themes[theme].titleText }]}>{label}</Text>
<Markdown
style={[styles.itemContent, { color: themes[theme].auxiliaryText }]}
msg={content || `__${ I18n.t('No_label_provided', { label: label.toLowerCase() }) }__`}
theme={theme}
/>
</View>
);
}
renderRole = (description) => {
const { theme } = this.props;
if (description) {
return (
<View style={[styles.roleBadge, { backgroundColor: themes[theme].auxiliaryBackground }]} key={description}>
<Text style={styles.role}>{ description }</Text>
</View>
);
}
return null;
}
renderRoles = () => {
const { parsedRoles } = this.state;
const { theme } = this.props;
if (parsedRoles && parsedRoles.length) {
return (
<View style={styles.item}>
<Text style={[styles.itemLabel, { color: themes[theme].titleText }]}>{I18n.t('Roles')}</Text>
<View style={styles.rolesContainer}>
{parsedRoles.map(role => this.renderRole(role))}
</View>
</View>
);
}
return null;
}
renderTimezone = () => {
const { roomUser } = this.state;
const { Message_TimeFormat } = this.props;
if (roomUser) {
const { utcOffset } = roomUser;
if (!utcOffset) {
return null;
}
return this.renderItem({
label: I18n.t('Timezone'),
content: `${ moment().utcOffset(utcOffset).format(Message_TimeFormat) } (UTC ${ utcOffset })`
});
}
return null;
} }
renderAvatar = (room, roomUser) => { renderAvatar = (room, roomUser) => {
@ -254,37 +265,6 @@ class RoomInfoView extends React.Component {
); );
} }
renderBroadcast = () => this.renderItem({
label: I18n.t('Broadcast_Channel'),
content: I18n.t('Broadcast_channel_Description')
});
renderCustomFields = () => {
const { roomUser } = this.state;
if (roomUser) {
const { customFields } = roomUser;
if (!roomUser.customFields) {
return null;
}
return (
Object.keys(customFields).map((title) => {
if (!customFields[title]) {
return;
}
return (
<View style={styles.item} key={title}>
<Text style={styles.itemLabel}>{title}</Text>
<Text style={styles.itemContent}>{customFields[title]}</Text>
</View>
);
})
);
}
return null;
}
renderButton = (onPress, iconName, text) => { renderButton = (onPress, iconName, text) => {
const { theme } = this.props; const { theme } = this.props;
return ( return (
@ -309,37 +289,21 @@ class RoomInfoView extends React.Component {
</View> </View>
) )
renderChannel = () => { renderContent = () => {
const { room } = this.state; const { room, roomUser } = this.state;
const { description, topic, announcement } = room; const { theme } = this.props;
return (
<>
{this.renderItem({ label: I18n.t('Description'), content: description })}
{this.renderItem({ label: I18n.t('Topic'), content: topic })}
{this.renderItem({ label: I18n.t('Announcement'), content: announcement })}
{room.broadcast ? this.renderBroadcast() : null}
</>
);
}
renderDirect = () => { if (this.isDirect) {
const { roomUser } = this.state; return <Direct roomUser={roomUser} theme={theme} />;
return ( } else if (this.t === 'l') {
<> return <Livechat room={room} roomUser={roomUser} theme={theme} />;
{this.renderRoles()} }
{this.renderTimezone()} return <Channel room={room} theme={theme} />;
{this.renderCustomFields(roomUser._id)}
</>
);
} }
render() { render() {
const { room, roomUser } = this.state; const { room, roomUser } = this.state;
const { theme } = this.props; const { theme } = this.props;
const isDirect = this.isDirect();
if (!room) {
return <View />;
}
return ( return (
<ScrollView style={[styles.scroll, { backgroundColor: themes[theme].backgroundColor }]}> <ScrollView style={[styles.scroll, { backgroundColor: themes[theme].backgroundColor }]}>
<StatusBar theme={theme} /> <StatusBar theme={theme} />
@ -348,12 +312,12 @@ class RoomInfoView extends React.Component {
forceInset={{ vertical: 'never' }} forceInset={{ vertical: 'never' }}
testID='room-info-view' testID='room-info-view'
> >
<View style={[styles.avatarContainer, isDirect && styles.avatarContainerDirectRoom, { backgroundColor: themes[theme].auxiliaryBackground }]}> <View style={[styles.avatarContainer, this.isDirect && styles.avatarContainerDirectRoom, { backgroundColor: themes[theme].auxiliaryBackground }]}>
{this.renderAvatar(room, roomUser)} {this.renderAvatar(room, roomUser)}
<View style={styles.roomTitleContainer}>{ getRoomTitle(room, this.t, roomUser && roomUser.name, roomUser && roomUser.username, roomUser && roomUser.statusText, theme) }</View> <View style={styles.roomTitleContainer}>{ getRoomTitle(room, this.t, roomUser?.name, roomUser?.username, roomUser?.statusText, theme) }</View>
{isDirect ? this.renderButtons() : null} {this.isDirect ? this.renderButtons() : null}
</View> </View>
{isDirect ? this.renderDirect() : this.renderChannel()} {this.renderContent()}
</SafeAreaView> </SafeAreaView>
</ScrollView> </ScrollView>
); );
@ -362,8 +326,7 @@ class RoomInfoView extends React.Component {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
baseUrl: state.server.server, baseUrl: state.server.server,
user: getUserSelector(state), user: getUserSelector(state)
Message_TimeFormat: state.settings.Message_TimeFormat
}); });
export default connect(mapStateToProps)(withTheme(RoomInfoView)); export default connect(mapStateToProps)(withTheme(RoomInfoView));

View File

@ -29,7 +29,7 @@ const Icon = React.memo(({
} }
let colorStyle = {}; let colorStyle = {};
if (type === 'd' && roomUserId) { if (type === 'l') {
colorStyle = { color: STATUS_COLORS[status] }; colorStyle = { color: STATUS_COLORS[status] };
} else { } else {
colorStyle = { color: isAndroid && theme === 'light' ? themes[theme].buttonText : themes[theme].auxiliaryText }; colorStyle = { color: isAndroid && theme === 'light' ? themes[theme].buttonText : themes[theme].auxiliaryText };

View File

@ -8,7 +8,6 @@ import Header from './Header';
import RightButtons from './RightButtons'; import RightButtons from './RightButtons';
import { withTheme } from '../../../theme'; import { withTheme } from '../../../theme';
import RoomHeaderLeft from './RoomHeaderLeft'; import RoomHeaderLeft from './RoomHeaderLeft';
import { getUserSelector } from '../../../selectors/login';
class RoomHeaderView extends Component { class RoomHeaderView extends Component {
static propTypes = { static propTypes = {
@ -95,17 +94,15 @@ class RoomHeaderView extends Component {
} }
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
let status;
let statusText; let statusText;
const { roomUserId, type } = ownProps; let status = 'offline';
if (type === 'd') { const { roomUserId, type, visitor = {} } = ownProps;
const user = getUserSelector(state);
if (user.id) { if (state.meteor.connected) {
if (state.activeUsers[roomUserId] && state.meteor.connected) { if (type === 'd' && state.activeUsers[roomUserId]) {
({ status, statusText } = state.activeUsers[roomUserId]); ({ status, statusText } = state.activeUsers[roomUserId]);
} else { } else if (type === 'l' && visitor?.status) {
status = 'offline'; ({ status } = visitor);
}
} }
} }

View File

@ -69,7 +69,7 @@ const stateAttrsUpdate = [
'readOnly', 'readOnly',
'member' 'member'
]; ];
const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'muted', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname', 'roles', 'bannerClosed']; const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'muted', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname', 'roles', 'bannerClosed', 'visitor'];
class RoomView extends React.Component { class RoomView extends React.Component {
static navigationOptions = ({ navigation, screenProps }) => { static navigationOptions = ({ navigation, screenProps }) => {
@ -87,6 +87,7 @@ class RoomView extends React.Component {
const goRoomActionsView = navigation.getParam('goRoomActionsView', () => {}); const goRoomActionsView = navigation.getParam('goRoomActionsView', () => {});
const unreadsCount = navigation.getParam('unreadsCount', null); const unreadsCount = navigation.getParam('unreadsCount', null);
const roomUserId = navigation.getParam('roomUserId'); const roomUserId = navigation.getParam('roomUserId');
const visitor = navigation.getParam('visitor');
if (!rid) { if (!rid) {
return { return {
...themedHeader(screenProps.theme) ...themedHeader(screenProps.theme)
@ -104,6 +105,7 @@ class RoomView extends React.Component {
type={t} type={t}
widthOffset={tmid ? 95 : 130} widthOffset={tmid ? 95 : 130}
roomUserId={roomUserId} roomUserId={roomUserId}
visitor={visitor}
goRoomActionsView={goRoomActionsView} goRoomActionsView={goRoomActionsView}
/> />
), ),
@ -291,6 +293,12 @@ class RoomView extends React.Component {
this.setReadOnly(); this.setReadOnly();
} }
} }
// If it's a livechat room
if (this.t === 'l') {
if (!isEqual(prevState.roomUpdate.visitor, roomUpdate.visitor)) {
navigation.setParams({ visitor: roomUpdate.visitor });
}
}
if (((roomUpdate.fname !== prevState.roomUpdate.fname) || (roomUpdate.name !== prevState.roomUpdate.name)) && !this.tmid) { if (((roomUpdate.fname !== prevState.roomUpdate.fname) || (roomUpdate.name !== prevState.roomUpdate.name)) && !this.tmid) {
navigation.setParams({ name: RocketChat.getRoomTitle(room) }); navigation.setParams({ name: RocketChat.getRoomTitle(room) });
} }

View File

@ -421,7 +421,8 @@ class RoomsListView extends React.Component {
type: item.t, type: item.t,
prid: item.prid, prid: item.prid,
uids: item.uids, uids: item.uids,
usernames: item.usernames usernames: item.usernames,
visitor: item.visitor
})); }));
// unread // unread
@ -548,6 +549,7 @@ class RoomsListView extends React.Component {
prid: item.prid, prid: item.prid,
room: item, room: item,
search: item.search, search: item.search,
visitor: item.visitor,
roomUserId: this.getUidDirectMessage(item) roomUserId: this.getUidDirectMessage(item)
}); });
} }
@ -816,6 +818,7 @@ class RoomsListView extends React.Component {
useRealName={useRealName} useRealName={useRealName}
getUserPresence={this.getUserPresence} getUserPresence={this.getUserPresence}
isGroupChat={isGroupChat} isGroupChat={isGroupChat}
visitor={item.visitor}
/> />
); );
}; };

View File

@ -0,0 +1,98 @@
import React, { useEffect, useState } from 'react';
import { FlatList, StyleSheet, Text } from 'react-native';
import PropTypes from 'prop-types';
import { withTheme } from '../theme';
import RocketChat from '../lib/rocketchat';
import { themes } from '../constants/colors';
import Separator from '../containers/Separator';
import openLink from '../utils/openLink';
import I18n from '../i18n';
import debounce from '../utils/debounce';
import sharedStyles from './Styles';
import ListItem from '../containers/ListItem';
const styles = StyleSheet.create({
noResult: {
fontSize: 16,
paddingVertical: 56,
...sharedStyles.textAlignCenter,
...sharedStyles.textSemibold
},
withoutBorder: {
borderBottomWidth: 0,
borderTopWidth: 0
}
});
const Item = ({ item, theme }) => (
<ListItem
title={item.navigation?.page?.title || I18n.t('Empty_title')}
onPress={() => openLink(item.navigation?.page?.location?.href)}
theme={theme}
/>
);
Item.propTypes = {
item: PropTypes.object,
theme: PropTypes.string
};
const VisitorNavigationView = ({ navigation, theme }) => {
let offset;
let total = 0;
const [pages, setPages] = useState([]);
const getPages = async() => {
const rid = navigation.getParam('rid');
if (rid) {
try {
const result = await RocketChat.getPagesLivechat(rid, offset);
if (result.success) {
setPages(result.pages);
offset = result.pages.length;
({ total } = result);
}
} catch {
// do nothig
}
}
};
useEffect(() => { getPages(); }, []);
const onEndReached = debounce(() => {
if (pages.length <= total) {
getPages();
}
}, 300);
return (
<FlatList
data={pages}
renderItem={({ item }) => <Item item={item} theme={theme} />}
ItemSeparatorComponent={() => <Separator theme={theme} />}
contentContainerStyle={[
sharedStyles.listContentContainer,
{
backgroundColor: themes[theme].auxiliaryBackground,
borderColor: themes[theme].separatorColor
},
!pages.length && styles.withoutBorder
]}
style={{ backgroundColor: themes[theme].auxiliaryBackground }}
ListEmptyComponent={() => <Text style={[styles.noResult, { color: themes[theme].titleText }]}>{I18n.t('No_results_found')}</Text>}
keyExtractor={item => item}
onEndReached={onEndReached}
onEndReachedThreshold={5}
/>
);
};
VisitorNavigationView.propTypes = {
theme: PropTypes.string,
navigation: PropTypes.object
};
VisitorNavigationView.navigationOptions = {
title: I18n.t('Navigation_history')
};
export default withTheme(VisitorNavigationView);

View File

@ -109,6 +109,7 @@
"rn-root-view": "^1.0.3", "rn-root-view": "^1.0.3",
"rn-user-defaults": "^1.8.1", "rn-user-defaults": "^1.8.1",
"semver": "7.3.2", "semver": "7.3.2",
"ua-parser-js": "^0.7.21",
"url-parse": "^1.4.7", "url-parse": "^1.4.7",
"use-deep-compare-effect": "^1.3.1" "use-deep-compare-effect": "^1.3.1"
}, },

View File

@ -2461,9 +2461,9 @@
prop-types "^15.7.2" prop-types "^15.7.2"
"@react-native-community/async-storage@^1.9.0": "@react-native-community/async-storage@^1.9.0":
version "1.9.0" version "1.10.0"
resolved "https://registry.yarnpkg.com/@react-native-community/async-storage/-/async-storage-1.9.0.tgz#af26a8879bd2987970fbbe81a9623851d29a56f1" resolved "https://registry.yarnpkg.com/@react-native-community/async-storage/-/async-storage-1.10.0.tgz#fd6a9737f3c227ef4e28858b8201ad793a022296"
integrity sha512-TlGMr02JcmY4huH1P7Mt7p6wJecosPpW+09+CwCFLn875IhpRqU2XiVA+BQppZOYfQdHUfUzIKyCBeXOlCEbEg== integrity sha512-kPJwhUpBKLXGrBnUjx0JVSJvSEl5nPO+puJ3Uy9pMvika9uWeniRGZPQjUWWQimU5M7xhQ41d5I1OP82Q3Xx9A==
dependencies: dependencies:
deep-assign "^3.0.0" deep-assign "^3.0.0"
@ -15862,6 +15862,11 @@ ua-parser-js@^0.7.18:
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b"
integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ== integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==
ua-parser-js@^0.7.21:
version "0.7.21"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777"
integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==
uglify-es@^3.1.9: uglify-es@^3.1.9:
version "3.3.9" version "3.3.9"
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"