diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index 023db49f7..d73d3381f 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -35,7 +35,7 @@ platform :android do lane :beta do upload_to_play_store( track: 'internal', - aab: 'android/app/build/outputs/bundle/release/app-release.aab' + aab: 'android/app/build/outputs/bundle/playRelease/app-play-release.aab' ) end end diff --git a/app/containers/MessageActions/index.js b/app/containers/MessageActions/index.js index ff4c4dbf2..0ce32bb71 100644 --- a/app/containers/MessageActions/index.js +++ b/app/containers/MessageActions/index.js @@ -268,7 +268,7 @@ const MessageActions = React.memo(forwardRef(({ const handleDelete = (message) => { showConfirmationAlert({ message: I18n.t('You_will_not_be_able_to_recover_this_message'), - callToAction: I18n.t('Delete'), + confirmationText: I18n.t('Delete'), onPress: async() => { try { logEvent(events.ROOM_MSG_ACTION_DELETE); diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index ef35a18a7..50f2d2f6b 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -131,6 +131,10 @@ export default { Channels: 'Channels', Chats: 'Chats', Call_already_ended: 'Call already ended!', + Clear_cookies_alert: 'Do you want to clear all cookies?', + Clear_cookies_desc: 'This action will clear all login cookies, allowing you to login into other accounts.', + Clear_cookies_yes: 'Yes, clear cookies', + Clear_cookies_no: 'No, keep cookies', Click_to_join: 'Click to Join!', Close: 'Close', Close_emoji_selector: 'Close emoji selector', @@ -339,6 +343,7 @@ export default { Offline: 'Offline', Oops: 'Oops!', Omnichannel: 'Omnichannel', + Open_Livechats: 'Chats in Progress', Omnichannel_enable_alert: 'You\'re not available on Omnichannel. Would you like to be available?', 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.', Onboarding_join_workspace: 'Join a workspace', @@ -630,5 +635,9 @@ export default { After_seconds_set_by_admin: 'After {{seconds}} seconds (set by admin)', Dont_activate: 'Don\'t activate now', Queued_chats: 'Queued chats', - Queue_is_empty: 'Queue is empty' + Queue_is_empty: 'Queue is empty', + Logout_from_other_logged_in_locations: 'Logout from other logged in locations', + You_will_be_logged_out_from_other_locations: 'You\'ll be logged out from other locations.', + Logged_out_of_other_clients_successfully: 'Logged out of other clients successfully', + Logout_failed: 'Logout failed!' }; diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js index 792498fc0..e289f6abb 100644 --- a/app/i18n/locales/pt-BR.js +++ b/app/i18n/locales/pt-BR.js @@ -135,6 +135,10 @@ export default { Change_language_loading: 'Alterando idioma.', Call_already_ended: 'A chamada já terminou!', Clear_cache_loading: 'Limpando cache.', + Clear_cookies_alert: 'Você quer limpar seus cookies?', + Clear_cookies_desc: 'Esta ação limpará todos os cookies de login permitindo que você faça login em outras contas.', + Clear_cookies_yes: 'Sim, limpar cookies', + Clear_cookies_no: 'Não, manter cookies', Click_to_join: 'Clique para participar!', Close: 'Fechar', Close_emoji_selector: 'Fechar seletor de emojis', @@ -315,6 +319,7 @@ export default { No_available_agents_to_transfer: 'Nenhum agente disponível para transferência', Offline: 'Offline', Omnichannel: 'Omnichannel', + Open_Livechats: 'Bate-papos em Andamento', Omnichannel_enable_alert: 'Você não está disponível no Omnichannel. Você quer ficar disponível?', 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.', @@ -576,5 +581,9 @@ export default { After_seconds_set_by_admin: 'Após {{seconds}} segundos (Configurado pelo adm)', Dont_activate: 'Não ativar agora', Queued_chats: 'Bate-papos na fila', - Queue_is_empty: 'A fila está vazia' + Queue_is_empty: 'A fila está vazia', + Logout_from_other_logged_in_locations: 'Sair de outros locais logados', + You_will_be_logged_out_from_other_locations: 'Você perderá a sessão de outros clientes', + Logged_out_of_other_clients_successfully: 'Desconectado de outros clientes com sucesso', + Logout_failed: 'Falha ao desconectar!' }; diff --git a/app/lib/database/model/User.js b/app/lib/database/model/User.js index 5535ef440..3d7a7cf2f 100644 --- a/app/lib/database/model/User.js +++ b/app/lib/database/model/User.js @@ -18,5 +18,7 @@ export default class User extends Model { @field('statusText') statusText; + @field('login_email_password') loginEmailPassword; + @json('roles', sanitizer) roles; } diff --git a/app/lib/database/model/serversMigrations.js b/app/lib/database/model/serversMigrations.js index 86995e5c4..0e26fb632 100644 --- a/app/lib/database/model/serversMigrations.js +++ b/app/lib/database/model/serversMigrations.js @@ -48,6 +48,17 @@ export default schemaMigrations({ ] }) ] + }, + { + toVersion: 7, + steps: [ + addColumns({ + table: 'users', + columns: [ + { name: 'login_email_password', type: 'boolean', isOptional: true } + ] + }) + ] } ] }); diff --git a/app/lib/database/schema/servers.js b/app/lib/database/schema/servers.js index 11c115ade..9ff0baafa 100644 --- a/app/lib/database/schema/servers.js +++ b/app/lib/database/schema/servers.js @@ -1,7 +1,7 @@ import { appSchema, tableSchema } from '@nozbe/watermelondb'; export default appSchema({ - version: 6, + version: 7, tables: [ tableSchema({ name: 'users', @@ -12,7 +12,8 @@ export default appSchema({ { name: 'language', type: 'string', isOptional: true }, { name: 'status', type: 'string', isOptional: true }, { name: 'statusText', type: 'string', isOptional: true }, - { name: 'roles', type: 'string', isOptional: true } + { name: 'roles', type: 'string', isOptional: true }, + { name: 'login_email_password', type: 'boolean', isOptional: true } ] }), tableSchema({ diff --git a/app/lib/methods/subscriptions/room.js b/app/lib/methods/subscriptions/room.js index 320aab52e..a7a8471de 100644 --- a/app/lib/methods/subscriptions/room.js +++ b/app/lib/methods/subscriptions/room.js @@ -46,6 +46,7 @@ export default class RoomSubscription { unsubscribe = async() => { console.log(`[RCRN] Unsubscribing from room ${ this.rid }`); this.isAlive = false; + reduxStore.dispatch(unsubscribeRoom(this.rid)); if (this.promises) { try { const subscriptions = await this.promises || []; @@ -62,8 +63,6 @@ export default class RoomSubscription { if (this.timer) { clearTimeout(this.timer); } - - reduxStore.dispatch(unsubscribeRoom(this.rid)); } removeListener = async(promise) => { diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index b7acdd8a1..5d10e9eb5 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -348,10 +348,10 @@ const RocketChat = { return this.post('users.forgotPassword', { email }, false); }, - loginTOTP(params) { + loginTOTP(params, loginEmailPassword) { return new Promise(async(resolve, reject) => { try { - const result = await this.login(params); + const result = await this.login(params, loginEmailPassword); return resolve(result); } catch (e) { if (e.data?.error && (e.data.error === 'totp-required' || e.data.error === 'totp-invalid')) { @@ -390,7 +390,9 @@ const RocketChat = { }; } - return this.loginTOTP(params); + const loginEmailPassword = true; + + return this.loginTOTP(params, loginEmailPassword); }, async loginOAuthOrSso(params) { @@ -398,7 +400,7 @@ const RocketChat = { reduxStore.dispatch(loginRequest({ resume: result.token })); }, - async login(params) { + async login(params, loginEmailPassword) { const sdk = this.shareSDK || this.sdk; // RC 0.64.0 await sdk.login(params); @@ -414,11 +416,16 @@ const RocketChat = { customFields: result.me.customFields, statusLivechat: result.me.statusLivechat, emails: result.me.emails, - roles: result.me.roles + roles: result.me.roles, + loginEmailPassword }; return user; }, logout, + logoutOtherLocations() { + const { id: userId } = reduxStore.getState().login.user; + return this.sdk.post('users.removeOtherTokens', { userId }); + }, removeServer, async clearCache({ server }) { try { diff --git a/app/sagas/login.js b/app/sagas/login.js index eb6093246..cee9e81d6 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -123,11 +123,13 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) { language: user.language, status: user.status, statusText: user.statusText, - roles: user.roles + roles: user.roles, + loginEmailPassword: user.loginEmailPassword }; yield serversDB.action(async() => { try { const userRecord = await usersCollection.find(user.id); + u.loginEmailPassword = userRecord?.loginEmailPassword; await userRecord.update((record) => { record._raw = sanitizedRaw({ id: user.id, ...record._raw }, usersCollection.schema); Object.assign(record, u); diff --git a/app/utils/info.js b/app/utils/info.js index 8114ef2aa..685b2f948 100644 --- a/app/utils/info.js +++ b/app/utils/info.js @@ -3,17 +3,20 @@ import I18n from '../i18n'; export const showErrorAlert = (message, title, onPress = () => {}) => Alert.alert(title, message, [{ text: 'OK', onPress }], { cancelable: true }); -export const showConfirmationAlert = ({ message, callToAction, onPress }) => ( +export const showConfirmationAlert = ({ + title, message, confirmationText, dismissText = I18n.t('Cancel'), onPress, onCancel +}) => ( Alert.alert( - I18n.t('Are_you_sure_question_mark'), + title || I18n.t('Are_you_sure_question_mark'), message, [ { - text: I18n.t('Cancel'), + text: dismissText, + onPress: onCancel, style: 'cancel' }, { - text: callToAction, + text: confirmationText, style: 'destructive', onPress } diff --git a/app/utils/log/events.js b/app/utils/log/events.js index c6d51d7e2..9e1dfc9d6 100644 --- a/app/utils/log/events.js +++ b/app/utils/log/events.js @@ -119,6 +119,8 @@ export default { PROFILE_SAVE_AVATAR_F: 'profile_save_avatar_f', PROFILE_SAVE_CHANGES: 'profile_save_changes', PROFILE_SAVE_CHANGES_F: 'profile_save_changes_f', + PROFILE_LOGOUT_OTHER_LOCATIONS: 'profile_logout_other_locations', + PROFILE_LOGOUT_OTHER_LOCATIONS_F: 'profile_logout_other_locations_f', // SETTINGS VIEW SE_CONTACT_US: 'se_contact_us', diff --git a/app/views/DirectoryView/Options.js b/app/views/DirectoryView/Options.js index 18483bda9..ab69b11f4 100644 --- a/app/views/DirectoryView/Options.js +++ b/app/views/DirectoryView/Options.js @@ -111,8 +111,8 @@ export default class DirectoryOptions extends PureComponent { - {I18n.t('Search_global_users')} - {I18n.t('Search_global_users_description')} + {I18n.t('Search_global_users')} + {I18n.t('Search_global_users_description')} diff --git a/app/views/MessagesView/index.js b/app/views/MessagesView/index.js index 4e826d3fc..b6d910a68 100644 --- a/app/views/MessagesView/index.js +++ b/app/views/MessagesView/index.js @@ -25,7 +25,8 @@ class MessagesView extends React.Component { route: PropTypes.object, customEmojis: PropTypes.object, theme: PropTypes.string, - showActionSheet: PropTypes.func + showActionSheet: PropTypes.func, + useRealName: PropTypes.bool } constructor(props) { @@ -81,17 +82,19 @@ class MessagesView extends React.Component { } defineMessagesViewContent = (name) => { - const { user, baseUrl, theme } = this.props; + const { + user, baseUrl, theme, useRealName + } = this.props; const renderItemCommonProps = item => ({ item, baseUrl, user, author: item.u || item.user, - ts: item.ts || item.uploadedAt, timeFormat: 'MMM Do YYYY, h:mm:ss a', isEdited: !!item.editedAt, isHeader: true, attachments: item.attachments || [], + useRealName, showAttachment: this.showAttachment, getCustomEmoji: this.getCustomEmoji, navToRoomInfo: this.navToRoomInfo @@ -114,6 +117,7 @@ class MessagesView extends React.Component { item={{ ...item, u: item.user, + ts: item.ts || item.uploadedAt, attachments: [{ title: item.name, description: item.description, @@ -311,7 +315,8 @@ class MessagesView extends React.Component { const mapStateToProps = state => ({ baseUrl: state.server.server, user: getUserSelector(state), - customEmojis: state.customEmojis + customEmojis: state.customEmojis, + useRealName: state.settings.UI_Use_Real_Name }); export default connect(mapStateToProps)(withTheme(withActionSheet(MessagesView))); diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index 01ab944cc..8ddebdd6f 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -246,7 +246,7 @@ class NewServerView extends React.Component { handleRemove = () => { showConfirmationAlert({ message: I18n.t('You_will_unset_a_certificate_for_this_server'), - callToAction: I18n.t('Remove'), + confirmationText: I18n.t('Remove'), onPress: this.setState({ certificate: null }) // We not need delete file from DocumentPicker because it is a temp file }); } diff --git a/app/views/ProfileView/index.js b/app/views/ProfileView/index.js index 19d718768..7c96c5280 100644 --- a/app/views/ProfileView/index.js +++ b/app/views/ProfileView/index.js @@ -13,7 +13,7 @@ import KeyboardView from '../../presentation/KeyboardView'; import sharedStyles from '../Styles'; import styles from './styles'; import scrollPersistTaps from '../../utils/scrollPersistTaps'; -import { showErrorAlert } from '../../utils/info'; +import { showErrorAlert, showConfirmationAlert } from '../../utils/info'; import { LISTENER } from '../../containers/Toast'; import EventEmitter from '../../utils/events'; import RocketChat from '../../lib/rocketchat'; @@ -426,6 +426,23 @@ class ProfileView extends React.Component { } } + logoutOtherLocations = () => { + logEvent(events.PROFILE_LOGOUT_OTHER_LOCATIONS); + showConfirmationAlert({ + message: I18n.t('You_will_be_logged_out_from_other_locations'), + callToAction: I18n.t('Logout'), + onPress: async() => { + try { + await RocketChat.logoutOtherLocations(); + EventEmitter.emit(LISTENER, { message: I18n.t('Logged_out_of_other_clients_successfully') }); + } catch { + logEvent(events.PROFILE_LOGOUT_OTHER_LOCATIONS_F); + EventEmitter.emit(LISTENER, { message: I18n.t('Logout_failed') }); + } + } + }); + } + render() { const { name, username, email, newPassword, avatarUrl, customFields, avatar, saving @@ -552,6 +569,14 @@ class ProfileView extends React.Component { loading={saving} theme={theme} /> +