[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:
parent
0e4e174e25
commit
9e89316e2a
|
@ -31,7 +31,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
|
|||
'OPEN_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 MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
|
||||
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
|
||||
|
|
|
@ -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() {
|
||||
return {
|
||||
type: types.ROOM.REMOVED
|
||||
|
|
|
@ -68,6 +68,9 @@ export default {
|
|||
LDAP_Enable: {
|
||||
type: 'valueAsBoolean'
|
||||
},
|
||||
Livechat_request_comment_when_closing_conversation: {
|
||||
type: 'valueAsBoolean'
|
||||
},
|
||||
Jitsi_Enabled: {
|
||||
type: 'valueAsBoolean'
|
||||
},
|
||||
|
|
|
@ -12,7 +12,7 @@ import sharedStyles from '../views/Styles';
|
|||
import { themes } from '../constants/colors';
|
||||
import { loginRequest as loginRequestAction } from '../actions/login';
|
||||
import Button from './Button';
|
||||
import OnboardingSeparator from './OnboardingSeparator';
|
||||
import OrSeparator from './OrSeparator';
|
||||
import Touch from '../utils/touch';
|
||||
import I18n from '../i18n';
|
||||
import random from '../utils/random';
|
||||
|
@ -252,12 +252,12 @@ class LoginServices extends React.PureComponent {
|
|||
style={styles.options}
|
||||
color={themes[theme].actionTintColor}
|
||||
/>
|
||||
<OnboardingSeparator theme={theme} />
|
||||
<OrSeparator theme={theme} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (length > 0 && separator) {
|
||||
return <OnboardingSeparator theme={theme} />;
|
||||
return <OrSeparator theme={theme} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -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 text = { color: themes[theme].auxiliaryText };
|
||||
return (
|
||||
|
@ -36,8 +36,8 @@ const DateSeparator = React.memo(({ theme }) => {
|
|||
);
|
||||
});
|
||||
|
||||
DateSeparator.propTypes = {
|
||||
OrSeparator.propTypes = {
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
export default DateSeparator;
|
||||
export default OrSeparator;
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { Image, StyleSheet } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
import { themes } from '../constants/colors';
|
||||
import { STATUS_COLORS, themes } from '../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
style: {
|
||||
|
@ -15,7 +15,7 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
const RoomTypeIcon = React.memo(({
|
||||
type, size, isGroupChat, style, theme
|
||||
type, size, isGroupChat, status, style, theme
|
||||
}) => {
|
||||
if (!type) {
|
||||
return null;
|
||||
|
@ -36,7 +36,7 @@ const RoomTypeIcon = React.memo(({
|
|||
}
|
||||
return <CustomIcon name='at' size={13} style={[styles.style, styles.discussion, { color }]} />;
|
||||
} 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 }]} />;
|
||||
});
|
||||
|
@ -45,6 +45,7 @@ RoomTypeIcon.propTypes = {
|
|||
theme: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
isGroupChat: PropTypes.bool,
|
||||
status: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
style: PropTypes.object
|
||||
};
|
||||
|
|
|
@ -64,8 +64,10 @@ export default class RCTextInput extends React.PureComponent {
|
|||
inputRef: PropTypes.func,
|
||||
testID: PropTypes.string,
|
||||
iconLeft: PropTypes.string,
|
||||
iconRight: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
left: PropTypes.element,
|
||||
onIconRightPress: PropTypes.func,
|
||||
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() {
|
||||
const { showPassword } = this.state;
|
||||
const { testID, theme } = this.props;
|
||||
|
@ -117,7 +132,7 @@ export default class RCTextInput extends React.PureComponent {
|
|||
render() {
|
||||
const { showPassword } = this.state;
|
||||
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;
|
||||
const { dangerColor } = themes[theme];
|
||||
return (
|
||||
|
@ -140,7 +155,7 @@ export default class RCTextInput extends React.PureComponent {
|
|||
style={[
|
||||
styles.input,
|
||||
iconLeft && styles.inputIconLeft,
|
||||
secureTextEntry && styles.inputIconRight,
|
||||
(secureTextEntry || iconRight) && styles.inputIconRight,
|
||||
{
|
||||
backgroundColor: themes[theme].backgroundColor,
|
||||
borderColor: themes[theme].separatorColor,
|
||||
|
@ -165,6 +180,7 @@ export default class RCTextInput extends React.PureComponent {
|
|||
{...inputProps}
|
||||
/>
|
||||
{iconLeft ? this.iconLeft : null}
|
||||
{iconRight ? this.iconRight : null}
|
||||
{secureTextEntry ? this.iconPassword : null}
|
||||
{loading ? this.loading : null}
|
||||
{left}
|
||||
|
|
|
@ -12,11 +12,13 @@ import styles from './styles';
|
|||
|
||||
const keyExtractor = item => item.value.toString();
|
||||
|
||||
const Chip = ({ item, onSelect, theme }) => (
|
||||
const Chip = ({
|
||||
item, onSelect, style, theme
|
||||
}) => (
|
||||
<Touchable
|
||||
key={item.value}
|
||||
onPress={() => onSelect(item)}
|
||||
style={[styles.chip, { backgroundColor: themes[theme].auxiliaryBackground }]}
|
||||
style={[styles.chip, { backgroundColor: themes[theme].auxiliaryBackground }, style]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
>
|
||||
<>
|
||||
|
@ -29,17 +31,21 @@ const Chip = ({ item, onSelect, theme }) => (
|
|||
Chip.propTypes = {
|
||||
item: PropTypes.object,
|
||||
onSelect: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
const Chips = ({ items, onSelect, theme }) => (
|
||||
const Chips = ({
|
||||
items, onSelect, style, theme
|
||||
}) => (
|
||||
<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>
|
||||
);
|
||||
Chips.propTypes = {
|
||||
items: PropTypes.array,
|
||||
onSelect: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { View, Text } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
|
@ -9,16 +9,16 @@ import ActivityIndicator from '../../ActivityIndicator';
|
|||
import styles from './styles';
|
||||
|
||||
const Input = ({
|
||||
children, open, theme, loading, inputStyle, disabled
|
||||
children, onPress, theme, loading, inputStyle, placeholder, disabled
|
||||
}) => (
|
||||
<Touchable
|
||||
onPress={() => open(true)}
|
||||
onPress={onPress}
|
||||
style={[{ backgroundColor: themes[theme].backgroundColor }, inputStyle]}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<View style={[styles.input, { borderColor: themes[theme].separatorColor }]}>
|
||||
{children}
|
||||
{placeholder ? <Text style={[styles.pickerText, { color: themes[theme].auxiliaryText }]}>{placeholder}</Text> : children}
|
||||
{
|
||||
loading
|
||||
? <ActivityIndicator style={[styles.loading, styles.icon]} />
|
||||
|
@ -29,10 +29,11 @@ const Input = ({
|
|||
);
|
||||
Input.propTypes = {
|
||||
children: PropTypes.node,
|
||||
open: PropTypes.func,
|
||||
onPress: PropTypes.func,
|
||||
theme: PropTypes.string,
|
||||
inputStyle: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
loading: PropTypes.bool
|
||||
};
|
||||
|
||||
|
|
|
@ -136,7 +136,7 @@ export const MultiSelect = React.memo(({
|
|||
/>
|
||||
) : (
|
||||
<Input
|
||||
open={onShow}
|
||||
onPress={onShow}
|
||||
theme={theme}
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
|
@ -150,7 +150,7 @@ export const MultiSelect = React.memo(({
|
|||
const items = options.filter(option => selected.includes(option.value));
|
||||
button = (
|
||||
<Input
|
||||
open={onShow}
|
||||
onPress={onShow}
|
||||
theme={theme}
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -53,7 +53,6 @@ const User = React.memo(({
|
|||
<TouchableOpacity
|
||||
style={styles.titleContainer}
|
||||
onPress={() => navToRoomInfo(navParam)}
|
||||
style={styles.titleContainer}
|
||||
disabled={author._id === user.id}
|
||||
>
|
||||
<Text style={[styles.username, { color: themes[theme].titleText }]} numberOfLines={1}>
|
||||
|
|
|
@ -85,6 +85,7 @@ export default {
|
|||
Add_Server: 'Add Server',
|
||||
Add_users: 'Add users',
|
||||
Admin_Panel: 'Admin Panel',
|
||||
Agent: 'Agent',
|
||||
Alert: 'Alert',
|
||||
alert: 'alert',
|
||||
alerts: 'alerts',
|
||||
|
@ -133,7 +134,9 @@ export default {
|
|||
Click_to_join: 'Click to Join!',
|
||||
Close: 'Close',
|
||||
Close_emoji_selector: 'Close emoji selector',
|
||||
Closing_chat: 'Closing chat',
|
||||
Change_language_loading: 'Changing language.',
|
||||
Chat_closed_by_agent: 'Chat closed by agent',
|
||||
Choose: 'Choose',
|
||||
Choose_from_library: 'Choose from library',
|
||||
Choose_file: 'Choose file',
|
||||
|
@ -151,6 +154,7 @@ export default {
|
|||
Continue_with: 'Continue with',
|
||||
Copied_to_clipboard: 'Copied to clipboard!',
|
||||
Copy: 'Copy',
|
||||
Conversation: 'Conversation',
|
||||
Permalink: 'Permalink',
|
||||
Certificate_password: 'Certificate Password',
|
||||
Clear_cache: 'Clear local server cache',
|
||||
|
@ -169,6 +173,7 @@ export default {
|
|||
Default: 'Default',
|
||||
Default_browser: 'Default browser',
|
||||
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',
|
||||
|
@ -196,6 +201,7 @@ export default {
|
|||
Email: 'Email',
|
||||
EMAIL: 'EMAIL',
|
||||
email: 'e-mail',
|
||||
Empty_title: 'Empty title',
|
||||
Enable_Auto_Translate: 'Enable Auto-Translate',
|
||||
Enable_notifications: 'Enable notifications',
|
||||
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: 'Forgot your 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',
|
||||
Generate_New_Link: 'Generate New Link',
|
||||
Group_by_favorites: 'Group favorites',
|
||||
|
@ -235,6 +245,7 @@ export default {
|
|||
Message_HideType_subscription_role_removed: 'Role No Longer Defined',
|
||||
Message_HideType_room_archived: 'Room Archived',
|
||||
Message_HideType_room_unarchived: 'Room Unarchived',
|
||||
IP: 'IP',
|
||||
In_app: 'In-app',
|
||||
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',
|
||||
|
@ -260,6 +271,7 @@ export default {
|
|||
Light: 'Light',
|
||||
License: 'License',
|
||||
Livechat: 'Livechat',
|
||||
Livechat_edit: 'Livechat edit',
|
||||
Login: 'Login',
|
||||
Login_error: 'Your credentials were rejected! Please try again.',
|
||||
Login_with: 'Login with',
|
||||
|
@ -292,6 +304,7 @@ export default {
|
|||
N_users: '{{n}} users',
|
||||
name: 'name',
|
||||
Name: 'Name',
|
||||
Navigation_history: 'Navigation history',
|
||||
Never: 'Never',
|
||||
New_Message: 'New Message',
|
||||
New_Password: 'New Password',
|
||||
|
@ -318,6 +331,7 @@ export default {
|
|||
Notifications: 'Notifications',
|
||||
Notification_Duration: 'Notification Duration',
|
||||
Notification_Preferences: 'Notification Preferences',
|
||||
No_available_agents_to_transfer: 'No available agents to transfer',
|
||||
Offline: 'Offline',
|
||||
Oops: 'Oops!',
|
||||
Onboarding_description: 'A workspace is your team or organization’s 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_your_authentication_app_and_enter_the_code: 'Open your authentication app and enter the code.',
|
||||
OR: 'OR',
|
||||
OS: 'OS',
|
||||
Overwrites_the_server_configuration_and_use_room_config: 'Overwrites the server configuration and use room config',
|
||||
Password: 'Password',
|
||||
Parent_channel_or_group: 'Parent channel or group',
|
||||
Permalink_copied_to_clipboard: 'Permalink copied to clipboard!',
|
||||
Phone: 'Phone',
|
||||
Pin: 'Pin',
|
||||
Pinned_Messages: 'Pinned Messages',
|
||||
pinned: 'pinned',
|
||||
Pinned: 'Pinned',
|
||||
Please_add_a_comment: 'Please add a comment',
|
||||
Please_enter_your_password: 'Please enter your password',
|
||||
Please_wait: 'Please wait.',
|
||||
Preferences: 'Preferences',
|
||||
|
@ -380,6 +397,7 @@ export default {
|
|||
Reset_password: 'Reset password',
|
||||
resetting_password: 'resetting password',
|
||||
RESET: 'RESET',
|
||||
Return: 'Return',
|
||||
Review_app_title: 'Are you enjoying this app?',
|
||||
Review_app_desc: 'Give us 5 stars on {{store}}',
|
||||
Review_app_yes: 'Sure!',
|
||||
|
@ -401,6 +419,7 @@ export default {
|
|||
SAVE: 'SAVE',
|
||||
Save_Changes: 'Save Changes',
|
||||
Save: 'Save',
|
||||
Saved: 'Saved',
|
||||
saving_preferences: 'saving preferences',
|
||||
saving_profile: 'saving profile',
|
||||
saving_settings: 'saving settings',
|
||||
|
@ -415,7 +434,9 @@ export default {
|
|||
Select_Server: 'Select Server',
|
||||
Select_Users: 'Select Users',
|
||||
Select_a_Channel: 'Select a Channel',
|
||||
Select_a_Department: 'Select a Department',
|
||||
Select_an_option: 'Select an option',
|
||||
Select_a_User: 'Select a User',
|
||||
Send: 'Send',
|
||||
Send_audio_message: 'Send audio message',
|
||||
Send_crash_report: 'Send crash report',
|
||||
|
@ -453,6 +474,7 @@ export default {
|
|||
Started_call: 'Call started by {{userBy}}',
|
||||
Submit: 'Submit',
|
||||
Table: 'Table',
|
||||
Tags: 'Tags',
|
||||
Take_a_photo: 'Take a photo',
|
||||
Take_a_video: 'Take a video',
|
||||
tap_to_change_status: 'tap to change status',
|
||||
|
@ -488,6 +510,7 @@ export default {
|
|||
Updating: 'Updating...',
|
||||
Uploading: 'Uploading',
|
||||
Upload_file_question_mark: 'Upload file?',
|
||||
User: 'User',
|
||||
Users: 'Users',
|
||||
User_added_by: 'User {{userAdded}} added by {{userBy}}',
|
||||
User_Info: 'User Info',
|
||||
|
@ -518,8 +541,10 @@ export default {
|
|||
Whats_your_2fa: 'What\'s your 2FA code?',
|
||||
Without_Servers: 'Without Servers',
|
||||
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: 'Gallery Permission',
|
||||
Yes: 'Yes',
|
||||
Yes_action_it: 'Yes, {{action}} it!',
|
||||
Yesterday: 'Yesterday',
|
||||
You_are_in_preview_mode: 'You are in preview mode',
|
||||
|
|
|
@ -89,6 +89,7 @@ export default {
|
|||
Add_Reaction: 'Reagir',
|
||||
Add_Server: 'Adicionar servidor',
|
||||
Add_users: 'Adicionar usuário',
|
||||
Agent: 'Agente',
|
||||
Alert: 'Alerta',
|
||||
alert: 'alerta',
|
||||
alerts: 'alertas',
|
||||
|
@ -135,7 +136,9 @@ export default {
|
|||
Click_to_join: 'Clique para participar!',
|
||||
Close: 'Fechar',
|
||||
Close_emoji_selector: 'Fechar seletor de emojis',
|
||||
Closing_chat: 'Fechando conversa',
|
||||
Choose: 'Escolher',
|
||||
Chat_closed_by_agent: 'Conversa fechada por agente',
|
||||
Choose_from_library: 'Escolha da biblioteca',
|
||||
Choose_file: 'Enviar arquivo',
|
||||
Choose_where_you_want_links_be_opened: 'Escolha onde deseja que os links sejam abertos',
|
||||
|
@ -145,6 +148,7 @@ export default {
|
|||
Confirm: 'Confirmar',
|
||||
Connect: 'Conectar',
|
||||
Connected: 'Conectado',
|
||||
Conversation: 'Conversação',
|
||||
connecting_server: 'conectando no servidor',
|
||||
Connecting: 'Conectando...',
|
||||
Continue_with: 'Entrar com',
|
||||
|
@ -187,6 +191,7 @@ export default {
|
|||
Email_or_password_field_is_empty: 'Email ou senha estão vazios',
|
||||
Email: 'Email',
|
||||
email: 'e-mail',
|
||||
Empty_title: 'Título vazio',
|
||||
Enable_notifications: 'Habilitar notificações',
|
||||
Everyone_can_access_this_channel: 'Todos podem acessar este canal',
|
||||
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: 'Esqueceu sua 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',
|
||||
Generate_New_Link: 'Gerar novo convite',
|
||||
Group_by_favorites: 'Agrupar favoritos',
|
||||
|
@ -223,6 +232,7 @@ export default {
|
|||
Message_HideType_subscription_role_removed: 'Papel removido',
|
||||
Message_HideType_room_archived: 'Sala arquivada',
|
||||
Message_HideType_room_unarchived: 'Sala desarquivada',
|
||||
IP: 'IP',
|
||||
In_app: 'No app',
|
||||
Invisible: 'Invisível',
|
||||
Invite: 'Convidar',
|
||||
|
@ -269,6 +279,7 @@ export default {
|
|||
N_users: '{{n}} usuários',
|
||||
name: 'nome',
|
||||
Name: 'Nome',
|
||||
Navigation_history: 'Histórico de navegação',
|
||||
Never: 'Nunca',
|
||||
New_in_RocketChat_question_mark: 'Novo no Rocket.Chat?',
|
||||
New_Message: 'Nova Mensagem',
|
||||
|
@ -289,6 +300,7 @@ export default {
|
|||
Notify_active_in_this_room: 'Notificar usuários ativos nesta sala',
|
||||
Notify_all_in_this_room: 'Notificar todos nesta sala',
|
||||
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',
|
||||
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.',
|
||||
|
@ -305,6 +317,7 @@ export default {
|
|||
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.',
|
||||
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',
|
||||
Password: 'Senha',
|
||||
Parent_channel_or_group: 'Canal ou grupo pai',
|
||||
|
@ -315,6 +328,7 @@ export default {
|
|||
Pinned: 'Mensagens Fixadas',
|
||||
Please_wait: 'Por favor, aguarde.',
|
||||
Please_enter_your_password: 'Por favor, digite sua senha',
|
||||
Please_add_a_comment: 'Por favor, adicione um comentário',
|
||||
Preferences: 'Preferências',
|
||||
Preferences_saved: 'Preferências salvas!',
|
||||
Privacy_Policy: ' Política de Privacidade',
|
||||
|
@ -343,6 +357,7 @@ export default {
|
|||
Reset_password: 'Resetar senha',
|
||||
resetting_password: 'redefinindo senha',
|
||||
RESET: 'RESETAR',
|
||||
Return: 'Retornar',
|
||||
Review_app_title: 'Você está gostando do app?',
|
||||
Review_app_desc: 'Nos dê 5 estrelas na {{store}}',
|
||||
Review_app_yes: 'Claro!',
|
||||
|
@ -377,7 +392,9 @@ export default {
|
|||
Select_Server: 'Selecionar Servidor',
|
||||
Select_Users: 'Selecionar Usuários',
|
||||
Select_a_Channel: 'Selecione um canal',
|
||||
Select_a_Department: 'Selecione um Departamento',
|
||||
Select_an_option: 'Selecione uma opção',
|
||||
Select_a_User: 'Selecione um Usuário',
|
||||
Send: 'Enviar',
|
||||
Send_audio_message: 'Enviar mensagem de áudio',
|
||||
Send_message: 'Enviar mensagem',
|
||||
|
@ -436,6 +453,7 @@ export default {
|
|||
Updating: 'Atualizando...',
|
||||
Uploading: 'Subindo arquivo',
|
||||
Upload_file_question_mark: 'Enviar arquivo?',
|
||||
User: 'Usuário',
|
||||
Users: 'Usuários',
|
||||
User_added_by: 'Usuário {{userAdded}} adicionado por {{userBy}}',
|
||||
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_workspace: 'Sua workspace',
|
||||
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: '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.',
|
||||
Type_message: 'Digitar mensagem',
|
||||
Room_search: 'Busca de sala',
|
||||
|
|
|
@ -168,6 +168,15 @@ const ChatsStack = createStackNavigator({
|
|||
NotificationPrefView: {
|
||||
getScreen: () => require('./views/NotificationPreferencesView').default
|
||||
},
|
||||
VisitorNavigationView: {
|
||||
getScreen: () => require('./views/VisitorNavigationView').default
|
||||
},
|
||||
ForwardLivechatView: {
|
||||
getScreen: () => require('./views/ForwardLivechatView').default
|
||||
},
|
||||
LivechatEditView: {
|
||||
getScreen: () => require('./views/LivechatEditView').default
|
||||
},
|
||||
PickerView: {
|
||||
getScreen: () => require('./views/PickerView').default
|
||||
},
|
||||
|
|
|
@ -13,4 +13,14 @@ export default class Room extends Model {
|
|||
@field('encrypted') encrypted;
|
||||
|
||||
@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;
|
||||
}
|
||||
|
|
|
@ -97,4 +97,14 @@ export default class Subscription extends Model {
|
|||
@json('uids', sanitizer) uids;
|
||||
|
||||
@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;
|
||||
}
|
||||
|
|
|
@ -99,7 +99,22 @@ export default schemaMigrations({
|
|||
addColumns({
|
||||
table: 'subscriptions',
|
||||
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 }
|
||||
]
|
||||
})
|
||||
]
|
||||
|
|
|
@ -43,7 +43,12 @@ export default appSchema({
|
|||
{ name: 'hide_unread_status', type: 'boolean', isOptional: true },
|
||||
{ name: 'sys_mes', 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({
|
||||
|
@ -52,7 +57,12 @@ export default appSchema({
|
|||
{ name: 'custom_fields', type: 'string' },
|
||||
{ name: 'broadcast', 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({
|
||||
|
|
|
@ -44,7 +44,12 @@ export default async(subscriptions = [], rooms = []) => {
|
|||
autoTranslateLanguage: s.autoTranslateLanguage,
|
||||
lastMessage: s.lastMessage,
|
||||
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);
|
||||
|
||||
|
@ -65,7 +70,12 @@ export default async(subscriptions = [], rooms = []) => {
|
|||
ro: r.ro,
|
||||
broadcast: r.broadcast,
|
||||
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);
|
||||
} catch {
|
||||
|
|
|
@ -35,6 +35,21 @@ export const merge = (subscription, room) => {
|
|||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -75,7 +75,12 @@ const createOrUpdateSubscription = async(subscription, room) => {
|
|||
lastMessage: s.lastMessage,
|
||||
roles: s.roles,
|
||||
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) {
|
||||
try {
|
||||
|
@ -98,10 +103,15 @@ const createOrUpdateSubscription = async(subscription, room) => {
|
|||
// We have to create a plain obj so we can manipulate it on `merge`
|
||||
// Can we do it in a better way?
|
||||
room = {
|
||||
customFields: r.customFields,
|
||||
broadcast: r.broadcast,
|
||||
v: r.v,
|
||||
ro: r.ro,
|
||||
tags: r.tags,
|
||||
servedBy: r.servedBy,
|
||||
encrypted: r.encrypted,
|
||||
ro: r.ro
|
||||
broadcast: r.broadcast,
|
||||
customFields: r.customFields,
|
||||
departmentId: r.departmentId,
|
||||
livechatData: r.livechatData
|
||||
};
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
|
|
|
@ -757,6 +757,59 @@ const RocketChat = {
|
|||
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) {
|
||||
const { id: userId } = reduxStore.getState().login.user;
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ const TypeIcon = React.memo(({
|
|||
if (type === 'd' && !isGroupChat) {
|
||||
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 = {
|
||||
|
|
|
@ -209,12 +209,20 @@ RoomItem.defaultProps = {
|
|||
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,
|
||||
status:
|
||||
state.meteor.connected && ownProps.type === 'd'
|
||||
? state.activeUsers[ownProps.id] && state.activeUsers[ownProps.id].status
|
||||
: 'offline'
|
||||
});
|
||||
status
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(RoomItem);
|
||||
|
|
|
@ -31,6 +31,18 @@ export default function(state = initialState, action) {
|
|||
rid: action.rid,
|
||||
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:
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Alert } from 'react-native';
|
||||
import prompt from 'react-native-prompt-android';
|
||||
import {
|
||||
takeLatest, take, select, delay, race, put
|
||||
} from 'redux-saga/effects';
|
||||
|
@ -9,6 +10,7 @@ import { removedRoom } from '../actions/room';
|
|||
import RocketChat from '../lib/rocketchat';
|
||||
import log from '../utils/log';
|
||||
import I18n from '../i18n';
|
||||
import { showErrorAlert } from '../utils/info';
|
||||
|
||||
const watchUserTyping = function* watchUserTyping({ rid, status }) {
|
||||
const auth = yield select(state => state.login.isAuthenticated);
|
||||
|
@ -28,10 +30,8 @@ const watchUserTyping = function* watchUserTyping({ rid, status }) {
|
|||
}
|
||||
};
|
||||
|
||||
const handleRemovedRoom = function* handleLeaveRoom({ result }) {
|
||||
if (result.success) {
|
||||
const handleRemovedRoom = function* handleRemovedRoom() {
|
||||
yield Navigation.navigate('RoomsListView');
|
||||
}
|
||||
// types.ROOM.REMOVE is triggered by `subscriptions-changed` with `removed` arg
|
||||
const { timeout } = yield race({
|
||||
deleteFinished: take(types.ROOM.REMOVED),
|
||||
|
@ -45,7 +45,9 @@ const handleRemovedRoom = function* handleLeaveRoom({ result }) {
|
|||
const handleLeaveRoom = function* handleLeaveRoom({ rid, t }) {
|
||||
try {
|
||||
const result = yield RocketChat.leaveRoom(rid, t);
|
||||
yield handleRemovedRoom({ result });
|
||||
if (result.success) {
|
||||
yield handleRemovedRoom();
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.data && e.data.errorType === 'error-you-are-last-owner') {
|
||||
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 }) {
|
||||
try {
|
||||
const result = yield RocketChat.deleteRoom(rid, t);
|
||||
yield handleRemovedRoom({ result });
|
||||
if (result.success) {
|
||||
yield handleRemovedRoom();
|
||||
}
|
||||
} catch (e) {
|
||||
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() {
|
||||
yield takeLatest(types.ROOM.USER_TYPING, watchUserTyping);
|
||||
yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom);
|
||||
yield takeLatest(types.ROOM.DELETE, handleDeleteRoom);
|
||||
yield takeLatest(types.ROOM.CLOSE, handleCloseRoom);
|
||||
yield takeLatest(types.ROOM.FORWARD, handleForwardRoom);
|
||||
};
|
||||
export default root;
|
||||
|
|
|
@ -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));
|
|
@ -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));
|
|
@ -19,7 +19,7 @@ import { appStart as appStartAction } from '../actions';
|
|||
import sharedStyles from './Styles';
|
||||
import Button from '../containers/Button';
|
||||
import TextInput from '../containers/TextInput';
|
||||
import OnboardingSeparator from '../containers/OnboardingSeparator';
|
||||
import OrSeparator from '../containers/OrSeparator';
|
||||
import FormContainer, { FormContainerInner } from '../containers/FormContainer';
|
||||
import I18n from '../i18n';
|
||||
import { isIOS } from '../utils/deviceInfo';
|
||||
|
@ -324,7 +324,7 @@ class NewServerView extends React.Component {
|
|||
testID='new-server-view-button'
|
||||
theme={theme}
|
||||
/>
|
||||
<OnboardingSeparator theme={theme} />
|
||||
<OrSeparator theme={theme} />
|
||||
<Text style={[styles.description, { color: themes[theme].auxiliaryText }]}>{I18n.t('Onboarding_join_open_description')}</Text>
|
||||
<Button
|
||||
title={I18n.t('Join_our_open_workspace')}
|
||||
|
|
|
@ -1,20 +1,38 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FlatList, StyleSheet } from 'react-native';
|
||||
import {
|
||||
View, FlatList, StyleSheet, Text
|
||||
} from 'react-native';
|
||||
|
||||
import I18n from '../i18n';
|
||||
import { themedHeader } from '../utils/navigation';
|
||||
import { withTheme } from '../theme';
|
||||
import { themes } from '../constants/colors';
|
||||
import debounce from '../utils/debounce';
|
||||
import sharedStyles from './Styles';
|
||||
|
||||
import ListItem from '../containers/ListItem';
|
||||
import Check from '../containers/Check';
|
||||
import Separator from '../containers/Separator';
|
||||
import SearchBox from '../containers/SearchBox';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
check: {
|
||||
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 value = props.navigation.getParam('value');
|
||||
this.state = { data, value };
|
||||
|
||||
this.onSearch = props.navigation.getParam('onChangeText');
|
||||
}
|
||||
|
||||
onChangeValue = (value) => {
|
||||
const { navigation } = this.props;
|
||||
const goBack = navigation.getParam('goBack', true);
|
||||
const onChange = navigation.getParam('onChangeValue', () => {});
|
||||
onChange(value);
|
||||
if (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() {
|
||||
const { data, value } = this.state;
|
||||
const { theme } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.renderSearch()}
|
||||
<FlatList
|
||||
data={data}
|
||||
keyExtractor={item => item.value}
|
||||
|
@ -75,20 +119,23 @@ class PickerView extends React.PureComponent {
|
|||
<Item
|
||||
item={item}
|
||||
theme={theme}
|
||||
selected={(value || data[0]?.value) === item.value}
|
||||
selected={!this.onSearch && (value || data[0]?.value) === item.value}
|
||||
onItemPress={() => this.onChangeValue(item.value)}
|
||||
/>
|
||||
)}
|
||||
ItemSeparatorComponent={() => <Separator theme={theme} />}
|
||||
ListEmptyComponent={() => <Text style={[styles.noResult, { color: themes[theme].titleText }]}>{I18n.t('No_results_found')}</Text>}
|
||||
contentContainerStyle={[
|
||||
sharedStyles.listContentContainer,
|
||||
{
|
||||
backgroundColor: themes[theme].auxiliaryBackground,
|
||||
borderColor: themes[theme].separatorColor
|
||||
}
|
||||
},
|
||||
!data.length && styles.withoutBorder
|
||||
]}
|
||||
style={{ backgroundColor: themes[theme].auxiliaryBackground }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import _ from 'lodash';
|
|||
|
||||
import Touch from '../../utils/touch';
|
||||
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 sharedStyles from '../Styles';
|
||||
import Avatar from '../../containers/Avatar';
|
||||
|
@ -28,6 +28,7 @@ import { themedHeader } from '../../utils/navigation';
|
|||
import { CloseModalButton } from '../../containers/HeaderButton';
|
||||
import { getUserSelector } from '../../selectors/login';
|
||||
import Markdown from '../../containers/markdown';
|
||||
import { showConfirmationAlert, showErrorAlert } from '../../utils/info';
|
||||
|
||||
class RoomActionsView extends React.Component {
|
||||
static navigationOptions = ({ navigation, screenProps }) => {
|
||||
|
@ -51,6 +52,7 @@ class RoomActionsView extends React.Component {
|
|||
leaveRoom: PropTypes.func,
|
||||
jitsiEnabled: PropTypes.bool,
|
||||
setLoadingInvite: PropTypes.func,
|
||||
closeRoom: PropTypes.func,
|
||||
theme: PropTypes.string
|
||||
}
|
||||
|
||||
|
@ -69,7 +71,9 @@ class RoomActionsView extends React.Component {
|
|||
canViewMembers: false,
|
||||
canAutoTranslate: false,
|
||||
canAddUser: false,
|
||||
canInviteUser: false
|
||||
canInviteUser: false,
|
||||
canForwardGuest: false,
|
||||
canReturnQueue: false
|
||||
};
|
||||
if (room && room.observe && room.rid) {
|
||||
this.roomObservable = room.observe();
|
||||
|
@ -117,6 +121,12 @@ class RoomActionsView extends React.Component {
|
|||
|
||||
this.canAddUser();
|
||||
this.canInviteUser();
|
||||
|
||||
// livechat permissions
|
||||
if (room.t === 'l') {
|
||||
this.canForwardGuest();
|
||||
this.canReturnQueue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,9 +196,32 @@ class RoomActionsView extends React.Component {
|
|||
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() {
|
||||
const {
|
||||
room, member, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate
|
||||
room, member, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate, canForwardGuest, canReturnQueue
|
||||
} = this.state;
|
||||
const { jitsiEnabled } = this.props;
|
||||
const {
|
||||
|
@ -373,7 +406,42 @@ class RoomActionsView extends React.Component {
|
|||
});
|
||||
}
|
||||
} 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;
|
||||
|
@ -384,6 +452,28 @@ class RoomActionsView extends React.Component {
|
|||
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() => {
|
||||
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>
|
||||
: (
|
||||
<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>
|
||||
</View>
|
||||
)
|
||||
|
@ -583,6 +673,7 @@ const mapStateToProps = state => ({
|
|||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
leaveRoom: (rid, t) => dispatch(leaveRoomAction(rid, t)),
|
||||
closeRoom: rid => dispatch(closeRoomAction(rid)),
|
||||
setLoadingInvite: loading => dispatch(setLoadingAction(loading))
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
|
@ -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);
|
|
@ -3,19 +3,20 @@ import PropTypes from 'prop-types';
|
|||
import { View, Text, ScrollView } from 'react-native';
|
||||
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||
import { connect } from 'react-redux';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
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 Status from '../../containers/Status';
|
||||
import Avatar from '../../containers/Avatar';
|
||||
import styles from './styles';
|
||||
import sharedStyles from '../Styles';
|
||||
import database from '../../lib/database';
|
||||
import RocketChat from '../../lib/rocketchat';
|
||||
import RoomTypeIcon from '../../containers/RoomTypeIcon';
|
||||
import I18n from '../../i18n';
|
||||
import { CustomHeaderButtons, Item } from '../../containers/HeaderButton';
|
||||
import { CustomHeaderButtons } from '../../containers/HeaderButton';
|
||||
import StatusBar from '../../containers/StatusBar';
|
||||
import log from '../../utils/log';
|
||||
import { themes } from '../../constants/colors';
|
||||
|
@ -24,6 +25,11 @@ import { themedHeader } from '../../utils/navigation';
|
|||
import { getUserSelector } from '../../selectors/login';
|
||||
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 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}>
|
||||
<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>
|
||||
</View>
|
||||
)
|
||||
|
@ -43,16 +49,22 @@ const getRoomTitle = (room, type, name, username, statusText, theme) => (type ==
|
|||
|
||||
class RoomInfoView extends React.Component {
|
||||
static navigationOptions = ({ navigation, screenProps }) => {
|
||||
const showEdit = navigation.getParam('showEdit');
|
||||
const rid = navigation.getParam('rid');
|
||||
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 {
|
||||
title: t === 'd' ? I18n.t('User_Info') : I18n.t('Room_Info'),
|
||||
...themedHeader(screenProps.theme),
|
||||
headerRight: showEdit
|
||||
? (
|
||||
<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>
|
||||
)
|
||||
: null
|
||||
|
@ -66,7 +78,6 @@ class RoomInfoView extends React.Component {
|
|||
token: PropTypes.string
|
||||
}),
|
||||
baseUrl: PropTypes.string,
|
||||
Message_TimeFormat: PropTypes.string,
|
||||
theme: PropTypes.string
|
||||
}
|
||||
|
||||
|
@ -78,68 +89,42 @@ class RoomInfoView extends React.Component {
|
|||
this.t = props.navigation.getParam('t');
|
||||
this.state = {
|
||||
room: room || { rid: this.rid, t: this.t },
|
||||
roomUser: roomUser || {},
|
||||
parsedRoles: []
|
||||
roomUser: roomUser || {}
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const { roomUser, room: roomState } = this.state;
|
||||
if (this.t === 'd' && !_.isEmpty(roomUser)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
componentDidMount() {
|
||||
if (this.isDirect) {
|
||||
this.loadUser();
|
||||
} else {
|
||||
this.loadRoom();
|
||||
}
|
||||
|
||||
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 });
|
||||
this.willFocusListener = navigation.addListener('willFocus', () => {
|
||||
if (this.isLivechat) {
|
||||
this.loadVisitor();
|
||||
}
|
||||
});
|
||||
} 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() {
|
||||
if (this.subscription && 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) => {
|
||||
|
@ -154,86 +139,112 @@ class RoomInfoView extends React.Component {
|
|||
} catch (e) {
|
||||
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() => {
|
||||
const { roomUser } = this.state;
|
||||
const { username } = roomUser;
|
||||
loadUser = async() => {
|
||||
const { room: roomState, roomUser } = this.state;
|
||||
|
||||
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;
|
||||
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 {
|
||||
const result = await RocketChat.createDirectMessage(username);
|
||||
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');
|
||||
const rid = result.room._id;
|
||||
navigation.navigate('RoomView', { rid, name: RocketChat.getRoomTitle(roomUser), t: 'd' });
|
||||
navigation.navigate('RoomView', { rid: room.rid, name: RocketChat.getRoomTitle(roomUser), t: 'd' });
|
||||
}
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
videoCall = () => RocketChat.callJitsi(this.rid)
|
||||
|
||||
isDirect = () => this.t === 'd'
|
||||
|
||||
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;
|
||||
videoCall = () => {
|
||||
const { room } = this.state;
|
||||
RocketChat.callJitsi(room.rid);
|
||||
}
|
||||
|
||||
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) => {
|
||||
const { theme } = this.props;
|
||||
return (
|
||||
|
@ -309,37 +289,21 @@ class RoomInfoView extends React.Component {
|
|||
</View>
|
||||
)
|
||||
|
||||
renderChannel = () => {
|
||||
const { room } = this.state;
|
||||
const { description, topic, announcement } = room;
|
||||
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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
renderContent = () => {
|
||||
const { room, roomUser } = this.state;
|
||||
const { theme } = this.props;
|
||||
|
||||
renderDirect = () => {
|
||||
const { roomUser } = this.state;
|
||||
return (
|
||||
<>
|
||||
{this.renderRoles()}
|
||||
{this.renderTimezone()}
|
||||
{this.renderCustomFields(roomUser._id)}
|
||||
</>
|
||||
);
|
||||
if (this.isDirect) {
|
||||
return <Direct roomUser={roomUser} theme={theme} />;
|
||||
} else if (this.t === 'l') {
|
||||
return <Livechat room={room} roomUser={roomUser} theme={theme} />;
|
||||
}
|
||||
return <Channel room={room} theme={theme} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { room, roomUser } = this.state;
|
||||
const { theme } = this.props;
|
||||
const isDirect = this.isDirect();
|
||||
if (!room) {
|
||||
return <View />;
|
||||
}
|
||||
return (
|
||||
<ScrollView style={[styles.scroll, { backgroundColor: themes[theme].backgroundColor }]}>
|
||||
<StatusBar theme={theme} />
|
||||
|
@ -348,12 +312,12 @@ class RoomInfoView extends React.Component {
|
|||
forceInset={{ vertical: 'never' }}
|
||||
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)}
|
||||
<View style={styles.roomTitleContainer}>{ getRoomTitle(room, this.t, roomUser && roomUser.name, roomUser && roomUser.username, roomUser && roomUser.statusText, theme) }</View>
|
||||
{isDirect ? this.renderButtons() : null}
|
||||
<View style={styles.roomTitleContainer}>{ getRoomTitle(room, this.t, roomUser?.name, roomUser?.username, roomUser?.statusText, theme) }</View>
|
||||
{this.isDirect ? this.renderButtons() : null}
|
||||
</View>
|
||||
{isDirect ? this.renderDirect() : this.renderChannel()}
|
||||
{this.renderContent()}
|
||||
</SafeAreaView>
|
||||
</ScrollView>
|
||||
);
|
||||
|
@ -362,8 +326,7 @@ class RoomInfoView extends React.Component {
|
|||
|
||||
const mapStateToProps = state => ({
|
||||
baseUrl: state.server.server,
|
||||
user: getUserSelector(state),
|
||||
Message_TimeFormat: state.settings.Message_TimeFormat
|
||||
user: getUserSelector(state)
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(withTheme(RoomInfoView));
|
||||
|
|
|
@ -29,7 +29,7 @@ const Icon = React.memo(({
|
|||
}
|
||||
|
||||
let colorStyle = {};
|
||||
if (type === 'd' && roomUserId) {
|
||||
if (type === 'l') {
|
||||
colorStyle = { color: STATUS_COLORS[status] };
|
||||
} else {
|
||||
colorStyle = { color: isAndroid && theme === 'light' ? themes[theme].buttonText : themes[theme].auxiliaryText };
|
||||
|
|
|
@ -8,7 +8,6 @@ import Header from './Header';
|
|||
import RightButtons from './RightButtons';
|
||||
import { withTheme } from '../../../theme';
|
||||
import RoomHeaderLeft from './RoomHeaderLeft';
|
||||
import { getUserSelector } from '../../../selectors/login';
|
||||
|
||||
class RoomHeaderView extends Component {
|
||||
static propTypes = {
|
||||
|
@ -95,17 +94,15 @@ class RoomHeaderView extends Component {
|
|||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
let status;
|
||||
let statusText;
|
||||
const { roomUserId, type } = ownProps;
|
||||
if (type === 'd') {
|
||||
const user = getUserSelector(state);
|
||||
if (user.id) {
|
||||
if (state.activeUsers[roomUserId] && state.meteor.connected) {
|
||||
let status = 'offline';
|
||||
const { roomUserId, type, visitor = {} } = ownProps;
|
||||
|
||||
if (state.meteor.connected) {
|
||||
if (type === 'd' && state.activeUsers[roomUserId]) {
|
||||
({ status, statusText } = state.activeUsers[roomUserId]);
|
||||
} else {
|
||||
status = 'offline';
|
||||
}
|
||||
} else if (type === 'l' && visitor?.status) {
|
||||
({ status } = visitor);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ const stateAttrsUpdate = [
|
|||
'readOnly',
|
||||
'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 {
|
||||
static navigationOptions = ({ navigation, screenProps }) => {
|
||||
|
@ -87,6 +87,7 @@ class RoomView extends React.Component {
|
|||
const goRoomActionsView = navigation.getParam('goRoomActionsView', () => {});
|
||||
const unreadsCount = navigation.getParam('unreadsCount', null);
|
||||
const roomUserId = navigation.getParam('roomUserId');
|
||||
const visitor = navigation.getParam('visitor');
|
||||
if (!rid) {
|
||||
return {
|
||||
...themedHeader(screenProps.theme)
|
||||
|
@ -104,6 +105,7 @@ class RoomView extends React.Component {
|
|||
type={t}
|
||||
widthOffset={tmid ? 95 : 130}
|
||||
roomUserId={roomUserId}
|
||||
visitor={visitor}
|
||||
goRoomActionsView={goRoomActionsView}
|
||||
/>
|
||||
),
|
||||
|
@ -291,6 +293,12 @@ class RoomView extends React.Component {
|
|||
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) {
|
||||
navigation.setParams({ name: RocketChat.getRoomTitle(room) });
|
||||
}
|
||||
|
|
|
@ -421,7 +421,8 @@ class RoomsListView extends React.Component {
|
|||
type: item.t,
|
||||
prid: item.prid,
|
||||
uids: item.uids,
|
||||
usernames: item.usernames
|
||||
usernames: item.usernames,
|
||||
visitor: item.visitor
|
||||
}));
|
||||
|
||||
// unread
|
||||
|
@ -548,6 +549,7 @@ class RoomsListView extends React.Component {
|
|||
prid: item.prid,
|
||||
room: item,
|
||||
search: item.search,
|
||||
visitor: item.visitor,
|
||||
roomUserId: this.getUidDirectMessage(item)
|
||||
});
|
||||
}
|
||||
|
@ -816,6 +818,7 @@ class RoomsListView extends React.Component {
|
|||
useRealName={useRealName}
|
||||
getUserPresence={this.getUserPresence}
|
||||
isGroupChat={isGroupChat}
|
||||
visitor={item.visitor}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
|
@ -109,6 +109,7 @@
|
|||
"rn-root-view": "^1.0.3",
|
||||
"rn-user-defaults": "^1.8.1",
|
||||
"semver": "7.3.2",
|
||||
"ua-parser-js": "^0.7.21",
|
||||
"url-parse": "^1.4.7",
|
||||
"use-deep-compare-effect": "^1.3.1"
|
||||
},
|
||||
|
|
11
yarn.lock
11
yarn.lock
|
@ -2461,9 +2461,9 @@
|
|||
prop-types "^15.7.2"
|
||||
|
||||
"@react-native-community/async-storage@^1.9.0":
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-community/async-storage/-/async-storage-1.9.0.tgz#af26a8879bd2987970fbbe81a9623851d29a56f1"
|
||||
integrity sha512-TlGMr02JcmY4huH1P7Mt7p6wJecosPpW+09+CwCFLn875IhpRqU2XiVA+BQppZOYfQdHUfUzIKyCBeXOlCEbEg==
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-community/async-storage/-/async-storage-1.10.0.tgz#fd6a9737f3c227ef4e28858b8201ad793a022296"
|
||||
integrity sha512-kPJwhUpBKLXGrBnUjx0JVSJvSEl5nPO+puJ3Uy9pMvika9uWeniRGZPQjUWWQimU5M7xhQ41d5I1OP82Q3Xx9A==
|
||||
dependencies:
|
||||
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"
|
||||
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:
|
||||
version "3.3.9"
|
||||
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
|
||||
|
|
Loading…
Reference in New Issue