diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js
index 1dae08222..69b3f916e 100644
--- a/app/actions/actionsTypes.js
+++ b/app/actions/actionsTypes.js
@@ -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]);
diff --git a/app/actions/room.js b/app/actions/room.js
index 3b3b213f2..59916bec0 100644
--- a/app/actions/room.js
+++ b/app/actions/room.js
@@ -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
diff --git a/app/constants/settings.js b/app/constants/settings.js
index ace1e91a9..ba63d4cab 100644
--- a/app/constants/settings.js
+++ b/app/constants/settings.js
@@ -68,6 +68,9 @@ export default {
LDAP_Enable: {
type: 'valueAsBoolean'
},
+ Livechat_request_comment_when_closing_conversation: {
+ type: 'valueAsBoolean'
+ },
Jitsi_Enabled: {
type: 'valueAsBoolean'
},
diff --git a/app/containers/LoginServices.js b/app/containers/LoginServices.js
index e6e685b34..2cf06d327 100644
--- a/app/containers/LoginServices.js
+++ b/app/containers/LoginServices.js
@@ -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}
/>
-
+
>
);
}
if (length > 0 && separator) {
- return ;
+ return ;
}
return null;
}
diff --git a/app/containers/OnboardingSeparator.js b/app/containers/OrSeparator.js
similarity index 89%
rename from app/containers/OnboardingSeparator.js
rename to app/containers/OrSeparator.js
index ff18bf6a9..665c14538 100644
--- a/app/containers/OnboardingSeparator.js
+++ b/app/containers/OrSeparator.js
@@ -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;
diff --git a/app/containers/RoomTypeIcon.js b/app/containers/RoomTypeIcon.js
index 1055a0b21..b35d83a60 100644
--- a/app/containers/RoomTypeIcon.js
+++ b/app/containers/RoomTypeIcon.js
@@ -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 ;
} if (type === 'l') {
- return ;
+ return ;
}
return ;
});
@@ -45,6 +45,7 @@ RoomTypeIcon.propTypes = {
theme: PropTypes.string,
type: PropTypes.string,
isGroupChat: PropTypes.bool,
+ status: PropTypes.string,
size: PropTypes.number,
style: PropTypes.object
};
diff --git a/app/containers/TextInput.js b/app/containers/TextInput.js
index dc071971f..a7c7d473c 100644
--- a/app/containers/TextInput.js
+++ b/app/containers/TextInput.js
@@ -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 (
+
+
+
+ );
+ }
+
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}
diff --git a/app/containers/UIKit/MultiSelect/Chips.js b/app/containers/UIKit/MultiSelect/Chips.js
index 59264db0a..496ba2261 100644
--- a/app/containers/UIKit/MultiSelect/Chips.js
+++ b/app/containers/UIKit/MultiSelect/Chips.js
@@ -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
+}) => (
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
+}) => (
- {items.map(item => )}
+ {items.map(item => )}
);
Chips.propTypes = {
items: PropTypes.array,
onSelect: PropTypes.func,
+ style: PropTypes.object,
theme: PropTypes.string
};
diff --git a/app/containers/UIKit/MultiSelect/Input.js b/app/containers/UIKit/MultiSelect/Input.js
index d4de6917c..6a7085fae 100644
--- a/app/containers/UIKit/MultiSelect/Input.js
+++ b/app/containers/UIKit/MultiSelect/Input.js
@@ -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
}) => (
open(true)}
+ onPress={onPress}
style={[{ backgroundColor: themes[theme].backgroundColor }, inputStyle]}
background={Touchable.Ripple(themes[theme].bannerBackground)}
disabled={disabled}
>
- {children}
+ {placeholder ? {placeholder} : children}
{
loading
?
@@ -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
};
diff --git a/app/containers/UIKit/MultiSelect/index.js b/app/containers/UIKit/MultiSelect/index.js
index 6cd584b1b..b88330e4e 100644
--- a/app/containers/UIKit/MultiSelect/index.js
+++ b/app/containers/UIKit/MultiSelect/index.js
@@ -136,7 +136,7 @@ export const MultiSelect = React.memo(({
/>
) : (
selected.includes(option.value));
button = (
navToRoomInfo(navParam)}
- style={styles.titleContainer}
disabled={author._id === user.id}
>
diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js
index f795354d5..4d1a88a2c 100644
--- a/app/i18n/locales/en.js
+++ b/app/i18n/locales/en.js
@@ -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',
diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js
index 139c0b510..6c7e06b50 100644
--- a/app/i18n/locales/pt-BR.js
+++ b/app/i18n/locales/pt-BR.js
@@ -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',
diff --git a/app/index.js b/app/index.js
index ad738c323..824172707 100644
--- a/app/index.js
+++ b/app/index.js
@@ -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
},
diff --git a/app/lib/database/model/Room.js b/app/lib/database/model/Room.js
index 4a1097a0e..0a8beab12 100644
--- a/app/lib/database/model/Room.js
+++ b/app/lib/database/model/Room.js
@@ -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;
}
diff --git a/app/lib/database/model/Subscription.js b/app/lib/database/model/Subscription.js
index ba928da6e..77efd3783 100644
--- a/app/lib/database/model/Subscription.js
+++ b/app/lib/database/model/Subscription.js
@@ -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;
}
diff --git a/app/lib/database/model/migrations.js b/app/lib/database/model/migrations.js
index ab5b14848..260209427 100644
--- a/app/lib/database/model/migrations.js
+++ b/app/lib/database/model/migrations.js
@@ -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 }
]
})
]
diff --git a/app/lib/database/schema/app.js b/app/lib/database/schema/app.js
index f740501bd..97784b3f9 100644
--- a/app/lib/database/schema/app.js
+++ b/app/lib/database/schema/app.js
@@ -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({
diff --git a/app/lib/methods/helpers/findSubscriptionsRooms.js b/app/lib/methods/helpers/findSubscriptionsRooms.js
index a0a9c5c22..457fc3b5f 100644
--- a/app/lib/methods/helpers/findSubscriptionsRooms.js
+++ b/app/lib/methods/helpers/findSubscriptionsRooms.js
@@ -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 {
diff --git a/app/lib/methods/helpers/mergeSubscriptionsRooms.js b/app/lib/methods/helpers/mergeSubscriptionsRooms.js
index 2ab12f652..6d9f6624d 100644
--- a/app/lib/methods/helpers/mergeSubscriptionsRooms.js
+++ b/app/lib/methods/helpers/mergeSubscriptionsRooms.js
@@ -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;
}
diff --git a/app/lib/methods/subscriptions/rooms.js b/app/lib/methods/subscriptions/rooms.js
index da1b50d95..d6ea0f7b6 100644
--- a/app/lib/methods/subscriptions/rooms.js
+++ b/app/lib/methods/subscriptions/rooms.js
@@ -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
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index e9306358e..036604072 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -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;
diff --git a/app/presentation/RoomItem/TypeIcon.js b/app/presentation/RoomItem/TypeIcon.js
index 19537c07e..d42e31a26 100644
--- a/app/presentation/RoomItem/TypeIcon.js
+++ b/app/presentation/RoomItem/TypeIcon.js
@@ -11,7 +11,7 @@ const TypeIcon = React.memo(({
if (type === 'd' && !isGroupChat) {
return ;
}
- return ;
+ return ;
});
TypeIcon.propTypes = {
diff --git a/app/presentation/RoomItem/index.js b/app/presentation/RoomItem/index.js
index dc39fe303..9251717cb 100644
--- a/app/presentation/RoomItem/index.js
+++ b/app/presentation/RoomItem/index.js
@@ -209,12 +209,20 @@ RoomItem.defaultProps = {
getUserPresence: () => {}
};
-const mapStateToProps = (state, ownProps) => ({
- connected: state.meteor.connected,
- status:
- state.meteor.connected && ownProps.type === 'd'
- ? state.activeUsers[ownProps.id] && state.activeUsers[ownProps.id].status
- : 'offline'
-});
+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
+ };
+};
export default connect(mapStateToProps)(RoomItem);
diff --git a/app/reducers/room.js b/app/reducers/room.js
index 762905fea..2b5cfcb8a 100644
--- a/app/reducers/room.js
+++ b/app/reducers/room.js
@@ -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,
diff --git a/app/sagas/room.js b/app/sagas/room.js
index a5cd48b46..9fab31386 100644
--- a/app/sagas/room.js
+++ b/app/sagas/room.js
@@ -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) {
- yield Navigation.navigate('RoomsListView');
- }
+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;
diff --git a/app/views/ForwardLivechatView.js b/app/views/ForwardLivechatView.js
new file mode 100644
index 000000000..d9c97b8bb
--- /dev/null
+++ b/app/views/ForwardLivechatView.js
@@ -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 (
+
+
+
+
+
+ );
+};
+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));
diff --git a/app/views/LivechatEditView.js b/app/views/LivechatEditView.js
new file mode 100644
index 000000000..5ac1692e1
--- /dev/null
+++ b/app/views/LivechatEditView.js
@@ -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 ? {title} : 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 (
+
+
+
+
+ onChangeText('name', text)}
+ onSubmitEditing={() => { inputs.name.focus(); }}
+ theme={theme}
+ />
+ { inputs.name = e; }}
+ defaultValue={visitor?.visitorEmails && visitor?.visitorEmails[0]?.address}
+ onChangeText={text => onChangeText('email', text)}
+ onSubmitEditing={() => { inputs.phone.focus(); }}
+ theme={theme}
+ />
+ { 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) => (
+ { 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}
+ />
+ ))}
+
+ { inputs.topic = e; }}
+ defaultValue={livechat?.topic}
+ onChangeText={text => onChangeText('topic', text)}
+ onSubmitEditing={() => inputs.tags.focus()}
+ theme={theme}
+ />
+
+ { 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}
+ />
+ ({ 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) => (
+ { inputs[key] = e; }}
+ onChangeText={text => onChangeText(key, text)}
+ onSubmitEditing={() => {
+ if (array.length - 1 > index) {
+ return inputs[array[index + 1]].focus();
+ }
+ submit();
+ }}
+ theme={theme}
+ />
+ ))}
+
+
+
+
+
+ );
+};
+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));
diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js
index edbbd15f0..efea48c1d 100644
--- a/app/views/NewServerView.js
+++ b/app/views/NewServerView.js
@@ -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}
/>
-
+
{I18n.t('Onboarding_join_open_description')}
)
- 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 ;
+ } else if (this.t === 'l') {
+ return ;
+ }
+ return ;
}
render() {
const { room, roomUser } = this.state;
const { theme } = this.props;
- const isDirect = this.isDirect();
- if (!room) {
- return ;
- }
return (
@@ -348,12 +312,12 @@ class RoomInfoView extends React.Component {
forceInset={{ vertical: 'never' }}
testID='room-info-view'
>
-
+
{this.renderAvatar(room, roomUser)}
- { getRoomTitle(room, this.t, roomUser && roomUser.name, roomUser && roomUser.username, roomUser && roomUser.statusText, theme) }
- {isDirect ? this.renderButtons() : null}
+ { getRoomTitle(room, this.t, roomUser?.name, roomUser?.username, roomUser?.statusText, theme) }
+ {this.isDirect ? this.renderButtons() : null}
- {isDirect ? this.renderDirect() : this.renderChannel()}
+ {this.renderContent()}
);
@@ -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));
diff --git a/app/views/RoomView/Header/Icon.js b/app/views/RoomView/Header/Icon.js
index 12ab5572e..d47f75603 100644
--- a/app/views/RoomView/Header/Icon.js
+++ b/app/views/RoomView/Header/Icon.js
@@ -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 };
diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js
index 5d3dc2592..8b1bec52f 100644
--- a/app/views/RoomView/Header/index.js
+++ b/app/views/RoomView/Header/index.js
@@ -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) {
- ({ status, statusText } = state.activeUsers[roomUserId]);
- } else {
- status = 'offline';
- }
+ let status = 'offline';
+ const { roomUserId, type, visitor = {} } = ownProps;
+
+ if (state.meteor.connected) {
+ if (type === 'd' && state.activeUsers[roomUserId]) {
+ ({ status, statusText } = state.activeUsers[roomUserId]);
+ } else if (type === 'l' && visitor?.status) {
+ ({ status } = visitor);
}
}
diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js
index 345709b7f..dc8317fea 100644
--- a/app/views/RoomView/index.js
+++ b/app/views/RoomView/index.js
@@ -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) });
}
diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js
index 454734300..699bd3b9b 100644
--- a/app/views/RoomsListView/index.js
+++ b/app/views/RoomsListView/index.js
@@ -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}
/>
);
};
diff --git a/app/views/VisitorNavigationView.js b/app/views/VisitorNavigationView.js
new file mode 100644
index 000000000..4cf4f66e2
--- /dev/null
+++ b/app/views/VisitorNavigationView.js
@@ -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 }) => (
+ 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 (
+ }
+ ItemSeparatorComponent={() => }
+ contentContainerStyle={[
+ sharedStyles.listContentContainer,
+ {
+ backgroundColor: themes[theme].auxiliaryBackground,
+ borderColor: themes[theme].separatorColor
+ },
+ !pages.length && styles.withoutBorder
+ ]}
+ style={{ backgroundColor: themes[theme].auxiliaryBackground }}
+ ListEmptyComponent={() => {I18n.t('No_results_found')}}
+ 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);
diff --git a/package.json b/package.json
index 37f4077d1..88e602b5a 100644
--- a/package.json
+++ b/package.json
@@ -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"
},
diff --git a/yarn.lock b/yarn.lock
index 1aa317fbd..449fa8e0a 100644
--- a/yarn.lock
+++ b/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"