From eb38313075eef8f73315a7a88dfa20a1ffc5224b Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Tue, 27 Apr 2021 13:18:15 -0300 Subject: [PATCH 01/44] [REGRESSION] Fallback not working when device's language is available (#3091) * Always add 'en' i18n * Add tests --- app/i18n/index.js | 1 + app/i18n/locales/nl.json | 1 - e2e/tests/assorted/04-setting.spec.js | 13 --- e2e/tests/assorted/12-i18n.spec.js | 114 ++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 14 deletions(-) create mode 100644 e2e/tests/assorted/12-i18n.spec.js diff --git a/app/i18n/index.js b/app/i18n/index.js index c82e9e17e..ebe430769 100644 --- a/app/i18n/index.js +++ b/app/i18n/index.js @@ -95,6 +95,7 @@ export const setLanguage = (l) => { moment.locale(toMomentLocale(locale)); }; +i18n.translations = { en: translations.en?.() }; const defaultLanguage = { languageTag: 'en', isRTL: false }; const availableLanguages = Object.keys(translations); const { languageTag } = RNLocalize.findBestAvailableLanguage(availableLanguages) || defaultLanguage; diff --git a/app/i18n/locales/nl.json b/app/i18n/locales/nl.json index 903da5e1f..fee4b826f 100644 --- a/app/i18n/locales/nl.json +++ b/app/i18n/locales/nl.json @@ -123,7 +123,6 @@ "creating_invite": "uitnodiging maken", "Channel_Name": "Kanaal Name", "Channels": "Kanalen", - "Chats": "Chats", "Call_already_ended": "Gesprek al beeïndigd!", "Click_to_join": "Klik om lid te worden!", "Close": "Sluiten", diff --git a/e2e/tests/assorted/04-setting.spec.js b/e2e/tests/assorted/04-setting.spec.js index a83b5b653..14bdbe71f 100644 --- a/e2e/tests/assorted/04-setting.spec.js +++ b/e2e/tests/assorted/04-setting.spec.js @@ -63,19 +63,6 @@ describe('Settings screen', () => { }); describe('Usage', async() => { - it('should navigate to language view', async() => { - await element(by.id('settings-view-language')).tap(); - await waitFor(element(by.id('language-view'))).toBeVisible().withTimeout(60000); - await expect(element(by.id('language-view-zh-CN'))).toExist(); - await expect(element(by.id('language-view-de'))).toExist(); - await expect(element(by.id('language-view-en'))).toExist(); - await expect(element(by.id('language-view-fr'))).toExist(); - await expect(element(by.id('language-view-pt-BR'))).toExist(); - await expect(element(by.id('language-view-pt-PT'))).toExist(); - await expect(element(by.id('language-view-ru'))).toExist(); - await tapBack(); - }); - it('should tap clear cache and navigate to roomslistview', async() => { await waitFor(element(by.id('settings-view'))).toBeVisible().withTimeout(2000); await element(by.id('settings-view-clear-cache')).tap(); diff --git a/e2e/tests/assorted/12-i18n.spec.js b/e2e/tests/assorted/12-i18n.spec.js new file mode 100644 index 000000000..13e7c79be --- /dev/null +++ b/e2e/tests/assorted/12-i18n.spec.js @@ -0,0 +1,114 @@ +const { + device, element, by, waitFor +} = require('detox'); +const { navigateToLogin, login, sleep } = require('../../helpers/app'); +const { post } = require('../../helpers/data_setup'); + +const data = require('../../data'); +const testuser = data.users.regular +const defaultLaunchArgs = { permissions: { notifications: 'YES' } }; + +const navToLanguage = async() => { + await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000); + await element(by.id('rooms-list-view-sidebar')).tap(); + await waitFor(element(by.id('sidebar-view'))).toBeVisible().withTimeout(2000); + await waitFor(element(by.id('sidebar-settings'))).toBeVisible().withTimeout(2000); + await element(by.id('sidebar-settings')).tap(); + await waitFor(element(by.id('settings-view'))).toBeVisible().withTimeout(2000); + await element(by.id('settings-view-language')).tap(); + await waitFor(element(by.id('language-view'))).toBeVisible().withTimeout(10000); +}; + +describe('i18n', () => { + describe('OS language', () => { + it('OS set to \'en\' and proper translate to \'en\'', async() => { + await device.launchApp({ + ...defaultLaunchArgs, + languageAndLocale: { + language: "en", + locale: "en" + }, + delete: true + }); + await waitFor(element(by.id('onboarding-view'))).toBeVisible().withTimeout(20000); + await expect(element(by.id('join-workspace').and(by.label('Join a workspace')))).toBeVisible(); + await expect(element(by.id('create-workspace-button').and(by.label('Create a new workspace')))).toBeVisible(); + }); + + it('OS set to unavailable language and fallback to \'en\'', async() => { + await device.launchApp({ + ...defaultLaunchArgs, + languageAndLocale: { + language: "es-MX", + locale: "es-MX" + } + }); + await waitFor(element(by.id('onboarding-view'))).toBeVisible().withTimeout(20000); + await expect(element(by.id('join-workspace').and(by.label('Join a workspace')))).toBeVisible(); + await expect(element(by.id('create-workspace-button').and(by.label('Create a new workspace')))).toBeVisible(); + }); + + /** + * This test might become outdated as soon as we support the language + * Although this seems to be a bad approach, that's the intention for having fallback enabled + */ + it('OS set to available language and fallback to \'en\' on strings missing translation', async() => { + await device.launchApp({ + ...defaultLaunchArgs, + languageAndLocale: { + language: "nl", + locale: "nl" + } + }); + await waitFor(element(by.id('onboarding-view'))).toBeVisible().withTimeout(20000); + await expect(element(by.id('join-workspace').and(by.label('Join a workspace')))).toBeVisible(); // Missing nl translation + await expect(element(by.id('create-workspace-button').and(by.label('Een nieuwe workspace maken')))).toBeVisible(); + }); + }); + + describe('Rocket.Chat language', () => { + before(async() => { + await device.launchApp(defaultLaunchArgs); + await navigateToLogin(); + await login(testuser.username, testuser.password); + }); + + it('should select \'en\'', async() => { + await navToLanguage(); + await element(by.id('language-view-en')).tap(); + await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000); + await element(by.id('rooms-list-view-sidebar')).tap(); + await waitFor(element(by.id('sidebar-view'))).toBeVisible().withTimeout(2000); + await expect(element(by.id('sidebar-chats').withDescendant(by.label('Chats')))).toBeVisible(); + await expect(element(by.id('sidebar-profile').withDescendant(by.label('Profile')))).toBeVisible(); + await expect(element(by.id('sidebar-settings').withDescendant(by.label('Settings')))).toBeVisible(); + await element(by.id('sidebar-close-drawer')).tap(); + }); + + it('should select \'nl\' and fallback to \'en\'', async() => { + await navToLanguage(); + await element(by.id('language-view-nl')).tap(); + await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000); + await element(by.id('rooms-list-view-sidebar')).tap(); + await waitFor(element(by.id('sidebar-view'))).toBeVisible().withTimeout(2000); + await expect(element(by.id('sidebar-chats').withDescendant(by.label('Chats')))).toBeVisible(); // fallback to en + await expect(element(by.id('sidebar-profile').withDescendant(by.label('Profiel')))).toBeVisible(); + await expect(element(by.id('sidebar-settings').withDescendant(by.label('Instellingen')))).toBeVisible(); + await element(by.id('sidebar-close-drawer')).tap(); + }); + + it('should set unsupported language and fallback to \'en\'', async() => { + await post('users.setPreferences', { data: { language: 'eo' } }); // Set language to Esperanto + await device.launchApp(defaultLaunchArgs); + await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000); + await element(by.id('rooms-list-view-sidebar')).tap(); + await waitFor(element(by.id('sidebar-view'))).toBeVisible().withTimeout(2000); + // give the app some time to apply new language + await sleep(3000); + await expect(element(by.id('sidebar-chats').withDescendant(by.label('Chats')))).toBeVisible(); + await expect(element(by.id('sidebar-profile').withDescendant(by.label('Profile')))).toBeVisible(); + await expect(element(by.id('sidebar-settings').withDescendant(by.label('Settings')))).toBeVisible(); + await post('users.setPreferences', { data: { language: 'en' } }); // Set back to english + }); + }) +}); \ No newline at end of file From 6798f039f3ba7565bd61930083ad582fa7dda8d0 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Tue, 27 Apr 2021 13:21:22 -0300 Subject: [PATCH 02/44] Bump version to 4.16.2 (#3092) --- android/app/build.gradle | 2 +- ios/RocketChatRN.xcodeproj/project.pbxproj | 4 ++-- ios/RocketChatRN/Info.plist | 2 +- ios/ShareRocketChatRN/Info.plist | 2 +- package.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index e3da57bb9..69975e5d7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -144,7 +144,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode VERSIONCODE as Integer - versionName "4.17.0" + versionName "4.16.2" vectorDrawables.useSupportLibrary = true if (!isFoss) { manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj index bbebd2ee7..13f5b8cac 100644 --- a/ios/RocketChatRN.xcodeproj/project.pbxproj +++ b/ios/RocketChatRN.xcodeproj/project.pbxproj @@ -1694,7 +1694,7 @@ INFOPLIST_FILE = NotificationService/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 4.17.0; + MARKETING_VERSION = 4.16.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService; @@ -1731,7 +1731,7 @@ INFOPLIST_FILE = NotificationService/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 4.17.0; + MARKETING_VERSION = 4.16.2; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/ios/RocketChatRN/Info.plist b/ios/RocketChatRN/Info.plist index 4357a8abb..e6129268e 100644 --- a/ios/RocketChatRN/Info.plist +++ b/ios/RocketChatRN/Info.plist @@ -23,7 +23,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 4.17.0 + 4.16.2 CFBundleSignature ???? CFBundleURLTypes diff --git a/ios/ShareRocketChatRN/Info.plist b/ios/ShareRocketChatRN/Info.plist index e38798d73..38a2e4cf6 100644 --- a/ios/ShareRocketChatRN/Info.plist +++ b/ios/ShareRocketChatRN/Info.plist @@ -21,7 +21,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 4.17.0 + 4.16.2 CFBundleVersion 1 KeychainGroup diff --git a/package.json b/package.json index 7e0e5d6da..855b1df73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket-chat-reactnative", - "version": "4.17.0", + "version": "4.16.2", "private": true, "scripts": { "start": "react-native start", From fc52286bfa9e256e97941a06b7df0d5c7adac9af Mon Sep 17 00:00:00 2001 From: Noach Magedman Date: Wed, 5 May 2021 19:02:26 +0300 Subject: [PATCH 03/44] [FIX] Connecting stream listener not being cleared (#3008) Co-authored-by: Diego Mello --- app/lib/rocketchat.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index fc73e375f..3d8c50b7c 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -196,6 +196,10 @@ const RocketChat = { clearTimeout(this.connectTimeout); } + if (this.connectingListener) { + this.connectingListener.then(this.stopListener); + } + if (this.connectedListener) { this.connectedListener.then(this.stopListener); } @@ -243,7 +247,7 @@ const RocketChat = { sdkConnect(); - this.connectedListener = this.sdk.onStreamData('connecting', () => { + this.connectingListener = this.sdk.onStreamData('connecting', () => { reduxStore.dispatch(connectRequest()); }); From 8f571fd0292dedbadbd0934626b763ab625bcf51 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Thu, 6 May 2021 11:06:52 -0300 Subject: [PATCH 04/44] [FIX] App making calls to DDP after socket was killed by OS (#3062) Co-authored-by: Gerzon Z --- app/lib/rocketchat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 3d8c50b7c..84f85a6f2 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -1140,7 +1140,7 @@ const RocketChat = { methodCall(...args) { return new Promise(async(resolve, reject) => { try { - const result = await this.sdk.methodCall(...args, this.code || ''); + const result = await this.sdk?.methodCall(...args, this.code || ''); return resolve(result); } catch (e) { if (e.error && (e.error === 'totp-required' || e.error === 'totp-invalid')) { From 1f0ff830a331170c7da7156576db7f651cee6552 Mon Sep 17 00:00:00 2001 From: Gerzon Z Date: Wed, 12 May 2021 15:01:29 -0400 Subject: [PATCH 05/44] [NEW] Create Team (#3082) Co-authored-by: Diego Mello --- app/definition/ITeam.js | 5 ++ app/i18n/locales/en.json | 11 +++- app/lib/rocketchat.js | 20 ++++++- app/sagas/createChannel.js | 36 ++++++++++-- app/utils/log/events.js | 1 + app/utils/room.js | 2 + app/views/CreateChannelView.js | 72 +++++++++++++++--------- app/views/NewMessageView.js | 14 ++++- app/views/RoomView/RightButtons.js | 10 ++-- app/views/RoomView/index.js | 10 +++- app/views/SelectedUsersView.js | 15 ++++- e2e/data.js | 5 ++ e2e/helpers/data_setup.js | 26 +++++++++ e2e/tests/team/01-createteam.spec.js | 82 ++++++++++++++++++++++++++++ 14 files changed, 265 insertions(+), 44 deletions(-) create mode 100644 app/definition/ITeam.js create mode 100644 e2e/tests/team/01-createteam.spec.js diff --git a/app/definition/ITeam.js b/app/definition/ITeam.js new file mode 100644 index 000000000..10919715d --- /dev/null +++ b/app/definition/ITeam.js @@ -0,0 +1,5 @@ +// https://github.com/RocketChat/Rocket.Chat/blob/develop/definition/ITeam.ts +export const TEAM_TYPE = { + PUBLIC: 0, + PRIVATE: 1 +}; diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index ec289d27d..58438f319 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -709,5 +709,12 @@ "This_room_encryption_has_been_disabled_by__username_": "This room's encryption has been disabled by {{username}}", "Teams": "Teams", "No_team_channels_found": "No channels found", - "Team_not_found": "Team not found" -} \ No newline at end of file + "Team_not_found": "Team not found", + "Create_Team": "Create Team", + "Team_Name": "Team Name", + "Private_Team": "Private Team", + "Read_Only_Team": "Read Only Team", + "Broadcast_Team": "Broadcast Team", + "creating_team" : "creating team", + "team-name-already-exists": "A team with that name already exists" +} diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 84f85a6f2..adc7f80ff 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -60,6 +60,7 @@ import UserPreferences from './userPreferences'; import { Encryption } from './encryption'; import EventEmitter from '../utils/events'; import { sanitizeLikeString } from './database/utils'; +import { TEAM_TYPE } from '../definition/ITeam'; const TOKEN_KEY = 'reactnativemeteor_usertoken'; const CURRENT_SERVER = 'currentServer'; @@ -732,7 +733,24 @@ const RocketChat = { prid, pmid, t_name, reply, users, encrypted }); }, - + createTeam({ + name, users, type, readOnly, broadcast, encrypted + }) { + const params = { + name, + users, + type: type ? TEAM_TYPE.PRIVATE : TEAM_TYPE.PUBLIC, + room: { + readOnly, + extraData: { + broadcast, + encrypted + } + } + }; + // RC 3.13.0 + return this.post('teams.create', params); + }, joinRoom(roomId, joinCode, type) { // TODO: join code // RC 0.48.0 diff --git a/app/sagas/createChannel.js b/app/sagas/createChannel.js index 613aedec2..f2ecfe76d 100644 --- a/app/sagas/createChannel.js +++ b/app/sagas/createChannel.js @@ -21,6 +21,10 @@ const createGroupChat = function createGroupChat() { return RocketChat.createGroupChat(); }; +const createTeam = function createTeam(data) { + return RocketChat.createTeam(data); +}; + const handleRequest = function* handleRequest({ data }) { try { const auth = yield select(state => state.login.isAuthenticated); @@ -29,7 +33,21 @@ const handleRequest = function* handleRequest({ data }) { } let sub; - if (data.group) { + if (data.isTeam) { + const { + type, + readOnly, + broadcast, + encrypted + } = data; + logEvent(events.CR_CREATE, { + type, + readOnly, + broadcast, + encrypted + }); + sub = yield call(createTeam, data); + } else if (data.group) { logEvent(events.SELECTED_USERS_CREATE_GROUP); const result = yield call(createGroupChat); if (result.success) { @@ -56,7 +74,7 @@ const handleRequest = function* handleRequest({ data }) { const subCollection = db.get('subscriptions'); yield db.action(async() => { await subCollection.create((s) => { - s._raw = sanitizedRaw({ id: sub.rid }, subCollection.schema); + s._raw = sanitizedRaw({ id: sub.team ? sub.team.roomId : sub.rid }, subCollection.schema); Object.assign(s, sub); }); }); @@ -64,7 +82,17 @@ const handleRequest = function* handleRequest({ data }) { // do nothing } - yield put(createChannelSuccess(sub)); + let successParams = {}; + if (data.isTeam) { + successParams = { + ...sub.team, + rid: sub.team.roomId, + t: sub.team.type ? 'p' : 'c' + }; + } else { + successParams = data; + } + yield put(createChannelSuccess(successParams)); } catch (err) { logEvent(events[data.group ? 'SELECTED_USERS_CREATE_GROUP_F' : 'CR_CREATE_F']); yield put(createChannelFailure(err)); @@ -81,7 +109,7 @@ const handleSuccess = function* handleSuccess({ data }) { const handleFailure = function handleFailure({ err }) { setTimeout(() => { - const msg = err.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') }); + const msg = err.data ? I18n.t(err.data.error) : err.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') }); showErrorAlert(msg); }, 300); }; diff --git a/app/utils/log/events.js b/app/utils/log/events.js index c9c9579f8..490f0dfd3 100644 --- a/app/utils/log/events.js +++ b/app/utils/log/events.js @@ -88,6 +88,7 @@ export default { // NEW MESSAGE VIEW NEW_MSG_CREATE_CHANNEL: 'new_msg_create_channel', + NEW_MSG_CREATE_TEAM: 'new_msg_create_team', NEW_MSG_CREATE_GROUP_CHAT: 'new_msg_create_group_chat', NEW_MSG_CREATE_DISCUSSION: 'new_msg_create_discussion', NEW_MSG_CHAT_WITH_USER: 'new_msg_chat_with_user', diff --git a/app/utils/room.js b/app/utils/room.js index 7077c73dc..fef926d5f 100644 --- a/app/utils/room.js +++ b/app/utils/room.js @@ -45,3 +45,5 @@ export const getBadgeColor = ({ subscription, messageId, theme }) => { }; export const makeThreadName = messageRecord => messageRecord.msg || messageRecord?.attachments[0]?.title; + +export const isTeamRoom = ({ teamId, joined }) => teamId && joined; diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js index 9d1e450ff..8090ef4fe 100644 --- a/app/views/CreateChannelView.js +++ b/app/views/CreateChannelView.js @@ -68,12 +68,9 @@ const styles = StyleSheet.create({ }); class CreateChannelView extends React.Component { - static navigationOptions = () => ({ - title: I18n.t('Create_Channel') - }); - static propTypes = { navigation: PropTypes.object, + route: PropTypes.object, baseUrl: PropTypes.string, create: PropTypes.func.isRequired, removeUser: PropTypes.func.isRequired, @@ -89,12 +86,19 @@ class CreateChannelView extends React.Component { theme: PropTypes.string }; - state = { - channelName: '', - type: true, - readOnly: false, - encrypted: false, - broadcast: false + constructor(props) { + super(props); + const { route } = this.props; + const isTeam = route?.params?.isTeam || false; + this.state = { + channelName: '', + type: true, + readOnly: false, + encrypted: false, + broadcast: false, + isTeam + }; + this.setHeader(); } shouldComponentUpdate(nextProps, nextState) { @@ -134,6 +138,15 @@ class CreateChannelView extends React.Component { return false; } + setHeader = () => { + const { navigation } = this.props; + const { isTeam } = this.state; + + navigation.setOptions({ + title: isTeam ? I18n.t('Create_Team') : I18n.t('Create_Channel') + }); + } + toggleRightButton = (channelName) => { const { navigation } = this.props; navigation.setOptions({ @@ -152,9 +165,11 @@ class CreateChannelView extends React.Component { submit = () => { const { - channelName, type, readOnly, broadcast, encrypted + channelName, type, readOnly, broadcast, encrypted, isTeam } = this.state; - const { users: usersProps, isFetching, create } = this.props; + const { + users: usersProps, isFetching, create + } = this.props; if (!channelName.trim() || isFetching) { return; @@ -163,9 +178,9 @@ class CreateChannelView extends React.Component { // transform users object into array of usernames const users = usersProps.map(user => user.name); - // create channel + // create channel or team create({ - name: channelName, users, type, readOnly, broadcast, encrypted + name: channelName, users, type, readOnly, broadcast, encrypted, isTeam }); Review.pushPositiveEvent(); @@ -196,11 +211,12 @@ class CreateChannelView extends React.Component { } renderType() { - const { type } = this.state; + const { type, isTeam } = this.state; + return this.renderSwitch({ id: 'type', value: type, - label: 'Private_Channel', + label: isTeam ? 'Private_Team' : 'Private_Channel', onValueChange: (value) => { logEvent(events.CR_TOGGLE_TYPE); // If we set the channel as public, encrypted status should be false @@ -210,11 +226,12 @@ class CreateChannelView extends React.Component { } renderReadOnly() { - const { readOnly, broadcast } = this.state; + const { readOnly, broadcast, isTeam } = this.state; + return this.renderSwitch({ id: 'readonly', value: readOnly, - label: 'Read_Only_Channel', + label: isTeam ? 'Read_Only_Team' : 'Read_Only_Channel', onValueChange: (value) => { logEvent(events.CR_TOGGLE_READ_ONLY); this.setState({ readOnly: value }); @@ -244,11 +261,12 @@ class CreateChannelView extends React.Component { } renderBroadcast() { - const { broadcast, readOnly } = this.state; + const { broadcast, readOnly, isTeam } = this.state; + return this.renderSwitch({ id: 'broadcast', value: broadcast, - label: 'Broadcast_Channel', + label: isTeam ? 'Broadcast_Team' : 'Broadcast_Channel', onValueChange: (value) => { logEvent(events.CR_TOGGLE_BROADCAST); this.setState({ @@ -301,8 +319,10 @@ class CreateChannelView extends React.Component { } render() { - const { channelName } = this.state; - const { users, isFetching, theme } = this.props; + const { channelName, isTeam } = this.state; + const { + users, isFetching, theme + } = this.props; const userCount = users.length; return ( @@ -312,18 +332,18 @@ class CreateChannelView extends React.Component { keyboardVerticalOffset={128} > - + navigation.navigate('CreateChannelView') }); } + createTeam = () => { + logEvent(events.NEW_MSG_CREATE_TEAM); + const { navigation } = this.props; + navigation.navigate('SelectedUsersViewCreateChannel', { nextAction: () => navigation.navigate('CreateChannelView', { isTeam: true }) }); + } + createGroupChat = () => { logEvent(events.NEW_MSG_CREATE_GROUP_CHAT); const { createChannel, maxUsers, navigation } = this.props; @@ -172,6 +178,12 @@ class NewMessageView extends React.Component { testID: 'new-message-view-create-channel', first: true })} + {this.renderButton({ + onPress: this.createTeam, + title: I18n.t('Create_Team'), + icon: 'teams', + testID: 'new-message-view-create-team' + })} {maxUsers > 2 ? this.renderButton({ onPress: this.createGroupChat, title: I18n.t('Create_Direct_Messages'), @@ -253,7 +265,7 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ - createChannel: params => dispatch(createChannelRequest(params)) + create: params => dispatch(createChannelRequest(params)) }); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(NewMessageView)); diff --git a/app/views/RoomView/RightButtons.js b/app/views/RoomView/RightButtons.js index debc3edb9..81b8f153b 100644 --- a/app/views/RoomView/RightButtons.js +++ b/app/views/RoomView/RightButtons.js @@ -7,6 +7,7 @@ import * as HeaderButton from '../../containers/HeaderButton'; import database from '../../lib/database'; import { getUserSelector } from '../../selectors/login'; import { logEvent, events } from '../../utils/log'; +import { isTeamRoom } from '../../utils/room'; class RightButtonsContainer extends Component { static propTypes = { @@ -15,10 +16,11 @@ class RightButtonsContainer extends Component { rid: PropTypes.string, t: PropTypes.string, tmid: PropTypes.string, - teamId: PropTypes.bool, + teamId: PropTypes.string, navigation: PropTypes.object, isMasterDetail: PropTypes.bool, - toggleFollowThread: PropTypes.func + toggleFollowThread: PropTypes.func, + joined: PropTypes.bool }; constructor(props) { @@ -163,7 +165,7 @@ class RightButtonsContainer extends Component { isFollowingThread, tunread, tunreadUser, tunreadGroup } = this.state; const { - t, tmid, threadsEnabled, teamId + t, tmid, threadsEnabled, teamId, joined } = this.props; if (t === 'l') { return null; @@ -181,7 +183,7 @@ class RightButtonsContainer extends Component { } return ( - {teamId ? ( + {isTeamRoom({ teamId, joined }) ? ( { const { - room, unreadsCount, roomUserId + room, unreadsCount, roomUserId, joined } = this.state; const { navigation, isMasterDetail, theme, baseUrl, user, insets, route @@ -331,7 +333,7 @@ class RoomView extends React.Component { let numIconsRight = 2; if (tmid) { numIconsRight = 1; - } else if (teamId) { + } else if (isTeamRoom({ teamId, joined })) { numIconsRight = 3; } const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight }); @@ -380,6 +382,8 @@ class RoomView extends React.Component { rid={rid} tmid={tmid} teamId={teamId} + teamMain={teamMain} + joined={joined} t={t} navigation={navigation} toggleFollowThread={this.toggleFollowThread} diff --git a/app/views/SelectedUsersView.js b/app/views/SelectedUsersView.js index f9d14e169..bd6740e1f 100644 --- a/app/views/SelectedUsersView.js +++ b/app/views/SelectedUsersView.js @@ -17,7 +17,6 @@ import sharedStyles from './Styles'; import * as HeaderButton from '../containers/HeaderButton'; import StatusBar from '../containers/StatusBar'; import { themes } from '../constants/colors'; -import { animateNextTransition } from '../utils/layoutAnimation'; import { withTheme } from '../theme'; import { getUserSelector } from '../selectors/login'; import { @@ -28,6 +27,9 @@ import { import { showErrorAlert } from '../utils/info'; import SafeAreaView from '../containers/SafeAreaView'; +const ITEM_WIDTH = 250; +const getItemLayout = (_, index) => ({ length: ITEM_WIDTH, offset: ITEM_WIDTH * index, index }); + class SelectedUsersView extends React.Component { static propTypes = { baseUrl: PropTypes.string, @@ -50,7 +52,7 @@ class SelectedUsersView extends React.Component { constructor(props) { super(props); this.init(); - + this.flatlist = React.createRef(); const maxUsers = props.route.params?.maxUsers; this.state = { maxUsers, @@ -151,7 +153,6 @@ class SelectedUsersView extends React.Component { return; } - animateNextTransition(); if (!this.isChecked(user.name)) { if (this.isGroupChat() && users.length === maxUsers) { return showErrorAlert(I18n.t('Max_number_of_users_allowed_is_number', { maxUsers }), I18n.t('Oops')); @@ -184,15 +185,23 @@ class SelectedUsersView extends React.Component { ); } + setFlatListRef = ref => this.flatlist = ref; + + onContentSizeChange = () => this.flatlist.scrollToEnd({ animated: true }); + renderSelected = () => { const { users, theme } = this.props; if (users.length === 0) { return null; } + return ( item._id} style={[sharedStyles.separatorTop, { borderColor: themes[theme].separatorColor }]} contentContainerStyle={{ marginVertical: 5 }} diff --git a/e2e/data.js b/e2e/data.js index 79e7842e8..c69b72515 100644 --- a/e2e/data.js +++ b/e2e/data.js @@ -42,6 +42,11 @@ const data = { name: `detox-private-${ value }` } }, + teams: { + private: { + name: `detox-team-${ value }` + } + }, registeringUser: { username: `newuser${ value }`, password: `password${ value }`, diff --git a/e2e/helpers/data_setup.js b/e2e/helpers/data_setup.js index 1f8f8fb65..ce1d5083e 100644 --- a/e2e/helpers/data_setup.js +++ b/e2e/helpers/data_setup.js @@ -1,5 +1,6 @@ const axios = require('axios').default; const data = require('../data'); +const { TEAM_TYPE } = require('../../app/definition/ITeam'); let server = data.server @@ -57,6 +58,24 @@ const createChannelIfNotExists = async (channelname) => { } } +const createTeamIfNotExists = async (teamname) => { + console.log(`Creating private team ${teamname}`) + try { + await rocketchat.post('teams.create', { + "name": teamname, + "type": TEAM_TYPE.PRIVATE + }) + } catch (createError) { + try { //Maybe it exists already? + await rocketchat.get(`teams.info?teamName=${teamname}`) + } catch (infoError) { + console.log(JSON.stringify(createError)) + console.log(JSON.stringify(infoError)) + throw "Failed to find or create private team" + } + } +} + const createGroupIfNotExists = async (groupname) => { console.log(`Creating private group ${groupname}`) try { @@ -133,6 +152,13 @@ const setup = async () => { } } + for (var teamKey in data.teams) { + if (data.teams.hasOwnProperty(teamKey)) { + const team = data.teams[teamKey] + await createTeamIfNotExists(team.name) + } + } + return } diff --git a/e2e/tests/team/01-createteam.spec.js b/e2e/tests/team/01-createteam.spec.js new file mode 100644 index 000000000..4dfe17ca3 --- /dev/null +++ b/e2e/tests/team/01-createteam.spec.js @@ -0,0 +1,82 @@ +const { + device, expect, element, by, waitFor +} = require('detox'); +const data = require('../../data'); +const { tapBack, sleep, navigateToLogin, login, tryTapping } = require('../../helpers/app'); + + + +describe('Create team screen', () => { + before(async() => { + await device.launchApp({ permissions: { notifications: 'YES' }, delete: true }); + await navigateToLogin(); + await login(data.users.regular.username, data.users.regular.password); + }); + + describe('New Message', async() => { + before(async() => { + await element(by.id('rooms-list-view-create-channel')).tap(); + }); + + describe('Render', async() => { + it('should have team button', async() => { + await waitFor(element(by.id('new-message-view-create-channel'))).toBeVisible().withTimeout(2000); + }); + }) + + describe('Usage', async() => { + it('should navigate to select users', async() => { + await element(by.id('new-message-view-create-channel')).tap(); + await waitFor(element(by.id('select-users-view'))).toExist().withTimeout(5000); + }); + }) + }); + + describe('Select Users', async() => { + it('should search users', async() => { + await element(by.id('select-users-view-search')).replaceText('rocket.cat'); + await waitFor(element(by.id(`select-users-view-item-rocket.cat`))).toBeVisible().withTimeout(10000); + }); + + it('should select/unselect user', async() => { + // Spotlight issues + await element(by.id('select-users-view-item-rocket.cat')).tap(); + await waitFor(element(by.id('selected-user-rocket.cat'))).toBeVisible().withTimeout(10000); + await element(by.id('selected-user-rocket.cat')).tap(); + await waitFor(element(by.id('selected-user-rocket.cat'))).toBeNotVisible().withTimeout(10000); + // Spotlight issues + await element(by.id('select-users-view-item-rocket.cat')).tap(); + await waitFor(element(by.id('selected-user-rocket.cat'))).toBeVisible().withTimeout(10000); + }); + + it('should create team', async() => { + await element(by.id('selected-users-view-submit')).tap(); + await waitFor(element(by.id('create-channel-view'))).toExist().withTimeout(10000); + }); + }) + + describe('Create Team', async() => { + describe('Usage', async() => { + it('should get invalid team name', async() => { + await element(by.id('create-channel-name')).typeText(`${data.teams.private.name}`); + await element(by.id('create-channel-submit')).tap(); + await element(by.text('OK')).tap(); + }); + + it('should create private team', async() => { + const room = `private${ data.random }`; + await element(by.id('create-channel-name')).replaceText(''); + await element(by.id('create-channel-name')).typeText(room); + await element(by.id('create-channel-submit')).tap(); + await waitFor(element(by.id('room-view'))).toExist().withTimeout(20000); + await expect(element(by.id('room-view'))).toExist(); + await waitFor(element(by.id(`room-view-title-${ room }`))).toExist().withTimeout(6000); + await expect(element(by.id(`room-view-title-${ room }`))).toExist(); + await tapBack(); + await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(10000); + await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toExist().withTimeout(6000); + await expect(element(by.id(`rooms-list-view-item-${ room }`))).toExist(); + }); + }) + }); +}); From b701913478f52fd39ef1d2a7ea2cea4f3baeb55d Mon Sep 17 00:00:00 2001 From: "lingohub[bot]" <69908207+lingohub[bot]@users.noreply.github.com> Date: Tue, 18 May 2021 15:58:05 -0300 Subject: [PATCH 06/44] =?UTF-8?q?Language=20update=20from=20LingoHub=20?= =?UTF-8?q?=F0=9F=A4=96=20(#3139)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Project Name: Rocket.Chat.ReactNative Project Link: https://translate.lingohub.com/rocketchat/dashboard/rocket-dot-chat-dot-reactnative User: Robot LingoHub Easy language translations with LingoHub 🚀 Co-authored-by: Robot LingoHub --- app/i18n/locales/ar.json | 10 +---- app/i18n/locales/de.json | 8 ++-- app/i18n/locales/en.json | 6 +-- app/i18n/locales/es-ES.json | 5 +-- app/i18n/locales/fr.json | 2 +- app/i18n/locales/it.json | 4 +- app/i18n/locales/ja.json | 5 +-- app/i18n/locales/nl.json | 4 +- app/i18n/locales/pt-BR.json | 86 ++++++++++++++++++++----------------- app/i18n/locales/pt-PT.json | 6 +-- app/i18n/locales/ru.json | 26 ++++++++--- app/i18n/locales/tr.json | 15 +++---- app/i18n/locales/zh-CN.json | 8 ++-- app/i18n/locales/zh-TW.json | 6 +-- 14 files changed, 100 insertions(+), 91 deletions(-) diff --git a/app/i18n/locales/ar.json b/app/i18n/locales/ar.json index 0b53315f9..8d2f1094c 100644 --- a/app/i18n/locales/ar.json +++ b/app/i18n/locales/ar.json @@ -33,7 +33,7 @@ "error-invalid-date": "التاريخ غير صالح", "error-invalid-description": "الوصف غير صالح", "error-invalid-domain": "عنوان الموقع غير صالح", - "error-invalid-email": "عنوان البريد اﻹلكتروني غير صالح {{emai}}", + "error-invalid-email": "عنوان البريد اﻹلكتروني غير صالح {{email}}", "error-invalid-email-address": "عنوان البريد اﻹلكتروني غير صالح", "error-invalid-file-height": "ارتفاع الملف غير صالح", "error-invalid-file-type": "نوع الملف غير صالح", @@ -100,7 +100,6 @@ "announcement": "إعلان", "Announcement": "إعلان", "Apply_Your_Certificate": "طبق شهادتك", - "Applying_a_theme_will_change_how_the_app_looks": "سيؤدي تطبيق السمة إلى تغيير شكل التطبيق", "ARCHIVE": "أرشفة", "archive": "أرشفة", "are_typing": "يكتب", @@ -184,8 +183,6 @@ "deleting_room": "حذف الغرفة", "description": "وصف", "Description": "وصف", - "DESKTOP_OPTIONS": "خيارات سطح المكتب", - "DESKTOP_NOTIFICATIONS": "إشعارات سطح المكتب", "Desktop_Alert_info": "هذه الإشعارات ترسل لسطح المكتب", "Directory": "مجلد", "Direct_Messages": "رسالة مباشرة", @@ -213,7 +210,6 @@ "Email_Notification_Mode_Disabled": "معطل", "Email_or_password_field_is_empty": "حقل البريد الإلكتروني أو كلمة المرور فارغ", "Email": "البريد الإلكتروني", - "EMAIL": "البريد الإلكتروني", "email": "البريد الإلكتروني", "Empty_title": "عنوان فارغ", "Enable_Auto_Translate": "تمكين الترجمة التلقائية", @@ -270,7 +266,6 @@ "I_Saved_My_E2E_Password": "قمت بحفظ كلمة المرور الطرفية", "IP": " عنوان بروتوكول الإنترنت (الآيبي)", "In_app": "في التطبيق", - "IN_APP_AND_DESKTOP": "داخل التطبيق وسطح المكتب", "In_App_and_Desktop_Alert_info": "يعرض شعاراً أعلى الشاشة عندما يكون التطبيق مفتوحًا، ويعرض إشعاراً على سطح المكتب", "Invisible": "غير مرئي", "Invite": "دعوة", @@ -398,7 +393,6 @@ "Profile": "الملف الشخصي", "Public_Channel": "قناة عامة", "Public": "عام", - "PUSH_NOTIFICATIONS": "الإشعارات", "Push_Notifications_Alert_Info": "يتم إرسال هذه الإشعارات إليك عندما لا يكون التطبيق مفتوحاً", "Quote": "اقتباس", "Reactions_are_disabled": "التفاعل معطل", @@ -446,9 +440,9 @@ "Room_Members": "أعضاء الغرفة", "Room_name_changed": "تم تغيير اسم الغرفة إلى: {{name}} من قبل {{userBy}}", "SAVE": "حفظ", - "Saved": "تم الحفظ", "Save_Changes": "حفظ التغيرات", "Save": "حفظ", + "Saved": "تم الحفظ", "saving_preferences": "حفظ التفضيلات", "saving_profile": "حفظ الملف الشخصي", "saving_settings": "حفظ الإعدادات", diff --git a/app/i18n/locales/de.json b/app/i18n/locales/de.json index 8bdf42d09..1074ff730 100644 --- a/app/i18n/locales/de.json +++ b/app/i18n/locales/de.json @@ -33,7 +33,7 @@ "error-invalid-date": "Ungültiges Datum angegeben", "error-invalid-description": "Ungültige Beschreibung", "error-invalid-domain": "Ungültige Domain", - "error-invalid-email": "Ungültige E-Mail {{emai}}", + "error-invalid-email": "Ungültige E-Mail {{email}}", "error-invalid-email-address": "Ungültige E-Mail-Adresse", "error-invalid-file-height": "Ungültige Dateihöhe", "error-invalid-file-type": "Ungültiger Dateityp", @@ -69,7 +69,7 @@ "error-role-in-use": "Rolle kann nicht gelöscht werden, da sie gerade verwendet wird", "error-role-name-required": "Der Rollenname ist erforderlich", "error-the-field-is-required": "Das Feld {{field}} ist erforderlich.", - "error-too-many-requests": "Fehler, zu viele Anfragen. Du musst {{Sekunden}} Sekunden warten, bevor du es erneut versuchst.", + "error-too-many-requests": "Fehler, zu viele Anfragen. Du musst {{seconds}} Sekunden warten, bevor du es erneut versuchst.", "error-user-is-not-activated": "Benutzer ist nicht aktiviert", "error-user-has-no-roles": "Benutzer hat keine Rollen", "error-user-limit-exceeded": "Die Anzahl der Benutzer, die du zu #channel_name einladen möchtest, überschreitet die vom Administrator festgelegte Grenze", @@ -185,6 +185,7 @@ "Description": "Beschreibung", "Desktop_Options": "Desktop-Einstellungen", "Desktop_Notifications": "Desktop-Benachrichtigungen", + "Desktop_Alert_info": "Diese Benachrichtigungen werden auf dem Desktop angezeigt", "Directory": "Verzeichnis", "Direct_Messages": "Direkte Nachrichten", "Disable_notifications": "Benachrichtigungen deaktiveren", @@ -276,7 +277,7 @@ "is_not_a_valid_RocketChat_instance": "ist keine gültige Rocket.Chat-Instanz", "is_typing": "schreibt", "Invalid_or_expired_invite_token": "Ungültiger oder abgelaufener Einladungscode", - "Invalid_server_version": "Der Server, zu dem du dich verbinden möchtest, verwendet eine Version, die von der App nicht mehr unterstützt wird: {{currentVersion}}.\n\nWir benötigen Version {{MinVersion}}.", + "Invalid_server_version": "Der Server, zu dem du dich verbinden möchtest, verwendet eine Version, die von der App nicht mehr unterstützt wird: {{currentVersion}}.\n\nWir benötigen Version {{minVersion}}.", "Invite_Link": "Einladungs-Link", "Invite_users": "Benutzer einladen", "Join": "Beitreten", @@ -400,7 +401,6 @@ "Public": "Öffentlich", "Push_Notifications": "Push-Benachrichtigungen", "Push_Notifications_Alert_Info": "Diese Benachrichtigungen werden dir zugestellt, wenn die App nicht geöffnet ist.", - "Desktop_Alert_info": "Diese Benachrichtigungen werden auf dem Desktop angezeigt", "Quote": "Zitat", "Reactions_are_disabled": "Reaktionen sind deaktiviert", "Reactions_are_enabled": "Reaktionen sind aktiviert", diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 58438f319..bd4e1f8a2 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -33,7 +33,7 @@ "error-invalid-date": "Invalid date provided.", "error-invalid-description": "Invalid description", "error-invalid-domain": "Invalid domain", - "error-invalid-email": "Invalid email {{emai}}", + "error-invalid-email": "Invalid email {{email}}", "error-invalid-email-address": "Invalid email address", "error-invalid-file-height": "Invalid file height", "error-invalid-file-type": "Invalid file type", @@ -715,6 +715,6 @@ "Private_Team": "Private Team", "Read_Only_Team": "Read Only Team", "Broadcast_Team": "Broadcast Team", - "creating_team" : "creating team", + "creating_team": "creating team", "team-name-already-exists": "A team with that name already exists" -} +} \ No newline at end of file diff --git a/app/i18n/locales/es-ES.json b/app/i18n/locales/es-ES.json index e03f4f6c3..feb18f1a7 100644 --- a/app/i18n/locales/es-ES.json +++ b/app/i18n/locales/es-ES.json @@ -30,7 +30,7 @@ "error-invalid-date": "La fecha proporcionada no es correcta.", "error-invalid-description": "La descipción no es correcta", "error-invalid-domain": "El dominio no es correcto", - "error-invalid-email": "El email {{emai}} no es correcto", + "error-invalid-email": "El email {{email}} no es correcto", "error-invalid-email-address": "La dirección de correo no es correcta", "error-invalid-file-height": "La altura de la imagen no es correcta", "error-invalid-file-type": "El formato del archivo no es correcto", @@ -44,7 +44,7 @@ "error-invalid-redirectUri": "La URL de redirección no es correcta.", "error-invalid-role": "El rol no es correcto", "error-invalid-room": "La sala no es correcta", - "error-invalid-room-name": "No se puede asignar el nombre {{name}} a una sala.", + "error-invalid-room-name": "No se puede asignar el nombre {{room_name}} a una sala.", "error-invalid-room-type": "No se puede asginar el tipo {{type}} a una sala.", "error-invalid-settings": "La configuración proporcionada no es correcta", "error-invalid-subscription": "La subscripción no es correcta", @@ -80,7 +80,6 @@ "Activity": "Actividad", "Add_Reaction": "Reaccionar", "Add_Server": "Añadir servidor", - "Add_user": "Añadir usuario", "Admin_Panel": "Panel de Control", "Alert": "Alerta", "alert": "alerta", diff --git a/app/i18n/locales/fr.json b/app/i18n/locales/fr.json index 1920045b1..c61e22f24 100644 --- a/app/i18n/locales/fr.json +++ b/app/i18n/locales/fr.json @@ -33,7 +33,7 @@ "error-invalid-date": "Date fournie invalide.", "error-invalid-description": "Description invalide", "error-invalid-domain": "Domaine invalide", - "error-invalid-email": "Adresse e-mail non valide {{emai}}", + "error-invalid-email": "Adresse e-mail non valide {{email}}", "error-invalid-email-address": "Adresse e-mail invalide", "error-invalid-file-height": "Hauteur de fichier non valide", "error-invalid-file-type": "Type de fichier invalide", diff --git a/app/i18n/locales/it.json b/app/i18n/locales/it.json index 12e02e3a3..4c6ee1acf 100644 --- a/app/i18n/locales/it.json +++ b/app/i18n/locales/it.json @@ -33,7 +33,7 @@ "error-invalid-date": "Data fornita non valida.", "error-invalid-description": "Descrizione non valida", "error-invalid-domain": "Dominio non valido", - "error-invalid-email": "E-mail {{emai}} non valida", + "error-invalid-email": "E-mail {{email}} non valida", "error-invalid-email-address": "Indirizzo e-mail non valido", "error-invalid-file-height": "Altezza del file non valida", "error-invalid-file-type": "Tipo di file non valido", @@ -157,8 +157,8 @@ "Continue_with": "Continua con", "Copied_to_clipboard": "Copiato negli appunti!", "Copy": "Copia", - "Permalink": "Permalink", "Conversation": "Conversazione", + "Permalink": "Permalink", "Certificate_password": "Password certificato", "Clear_cache": "Cancella la cache locale", "Clear_cache_loading": "Cancellando la cache.", diff --git a/app/i18n/locales/ja.json b/app/i18n/locales/ja.json index 83b0d9d5e..d758e135a 100644 --- a/app/i18n/locales/ja.json +++ b/app/i18n/locales/ja.json @@ -31,7 +31,7 @@ "error-invalid-date": "不正な日時です", "error-invalid-description": "不正な詳細です", "error-invalid-domain": "不正なドメインです", - "error-invalid-email": "不正なメールアドレスです。 {{emai}}", + "error-invalid-email": "不正なメールアドレスです。 {{email}}", "error-invalid-email-address": "不正なメールアドレスです", "error-invalid-file-height": "ファイルの高さが不正です", "error-invalid-file-type": "ファイルの種類が不正です", @@ -179,7 +179,6 @@ "Email": "メールアドレス", "email": "メールアドレス", "Enable_Auto_Translate": "自動翻訳を有効にする", - "Enable_markdown": "マークダウンを有効にする", "Enable_notifications": "通知を有効にする", "Everyone_can_access_this_channel": "全員このチャンネルにアクセスできます", "Error_uploading": "アップロードエラー", @@ -432,7 +431,7 @@ "Users": "ユーザー", "User_added_by": "{{userBy}} が {{userAdded}} を追加しました", "User_Info": "ユーザー情報", - "User_has_been_key": "ユーザーは{{ key }}", + "User_has_been_key": "ユーザーは{{key}}", "User_is_no_longer_role_by_": "{{userBy}} は {{user}} のロール {{role}} を削除しました。", "User_muted_by": "{{userBy}} は {{userMuted}} をミュートしました。", "User_removed_by": "{{userBy}} は {{userRemoved}} を退出させました。", diff --git a/app/i18n/locales/nl.json b/app/i18n/locales/nl.json index fee4b826f..7756a910d 100644 --- a/app/i18n/locales/nl.json +++ b/app/i18n/locales/nl.json @@ -1,7 +1,7 @@ { "1_person_reacted": "1 persoon heeft gereageerd", "1_user": "1 gebruiker", - "error-action-not-allowed": "{{actie}} is niet toegestaan", + "error-action-not-allowed": "{{action}} is niet toegestaan", "error-application-not-found": "Applicatie niet gevonden", "error-archived-duplicate-name": "Er is een gearchiveerd kanaal met de naam {{room_name}}", "error-avatar-invalid-url": "Foutieve avatar URL: {{url}}", @@ -31,7 +31,7 @@ "error-invalid-date": "Ongeldige datum opgegeven.", "error-invalid-description": "Ongeldige beschrijving", "error-invalid-domain": "Ongeldig domein", - "error-invalid-email": "Ongeldige email {{emai}}", + "error-invalid-email": "Ongeldige email {{email}}", "error-invalid-email-address": "Ongeldig emailadres", "error-invalid-file-height": "Ongeldige file height", "error-invalid-file-type": "Ongeldig bestandstype", diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index 4002b2d6e..64369e781 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -62,22 +62,14 @@ "error-no-tokens-for-this-user": "Não existem tokens para este usuário", "error-not-allowed": "Não permitido", "error-not-authorized": "Não autorizado", - "error-password-policy-not-met": "A senha não atende a política do servidor", - "error-password-policy-not-met-maxLength": "A senha não está de acordo com a política de comprimento máximo do servidor (senha muito longa)", - "error-password-policy-not-met-minLength": "A senha não está de acordo com a política de comprimento mínimo do servidor (senha muito curta)", - "error-password-policy-not-met-oneLowercase": "A senha não está de acordo com a política do servidor de pelo menos um caractere minúsculo.", - "error-password-policy-not-met-oneNumber": "A senha não está de acordo com a política do servidor, de pelo menos um caractere numérico.", - "error-password-policy-not-met-oneSpecial": "A senha não está de acordo com a política do servidor, de pelo menos um caractere especial.", - "error-password-policy-not-met-oneUppercase": "A senha não está de acordo com a política do servidor, de pelo menos um caractere maiúsculo.", - "error-password-policy-not-met-repeatingCharacters": "A senha não está de acordo com a política do servidor, relativamente aos caracteres proibidos repetidos (existem vários caracteres proibidos próximos uns dos outros)", "error-push-disabled": "Notificações push desativadas", "error-remove-last-owner": "Este é o último proprietário. Por favor, defina um novo proprietário antes de remover este.", "error-role-in-use": "Não é possível remover o papel pois ele está em uso", "error-role-name-required": "Nome do papel é obrigatório", "error-the-field-is-required": "O campo {{field}} é obrigatório.", "error-too-many-requests": "Erro, muitas solicitações. Por favor, diminua a velocidade. Você deve esperar {{seconds}} segundos antes de tentar novamente.", - "error-user-has-no-roles": "O usuário não possui permissões", "error-user-is-not-activated": "O usuário não está ativo", + "error-user-has-no-roles": "O usuário não possui permissões", "error-user-limit-exceeded": "O número de usuários que você está tentando convidar para #channel_name excede o limite determindado pelo administrador", "error-user-not-in-room": "O usuário não está nesta sala", "error-user-registration-disabled": "O registro do usuário está desativado", @@ -102,6 +94,7 @@ "and": "e", "announcement": "anúncio", "Announcement": "Anúncio", + "Apply_Your_Certificate": "Aplicar certificado", "ARCHIVE": "ARQUIVAR", "archive": "arquivar", "are_typing": "estão digitando", @@ -131,10 +124,7 @@ "Channel_Name": "Nome do Canal", "Channels": "Canais", "Chats": "Conversas", - "Change_Language": "Alterar idioma", - "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", @@ -143,8 +133,9 @@ "Close": "Fechar", "Close_emoji_selector": "Fechar seletor de emojis", "Closing_chat": "Fechando conversa", - "Choose": "Escolher", + "Change_language_loading": "Alterando idioma.", "Chat_closed_by_agent": "Conversa fechada por agente", + "Choose": "Escolher", "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", @@ -154,15 +145,16 @@ "Confirm": "Confirmar", "Connect": "Conectar", "Connected": "Conectado", - "Conversation": "Conversação", "connecting_server": "conectando no servidor", "Connecting": "Conectando...", "Contact_us": "Entre em contato", - "Continue_with": "Entrar com", "Contact_your_server_admin": "Contate o administrador do servidor.", + "Continue_with": "Entrar com", "Copied_to_clipboard": "Copiado para a área de transferência!", "Copy": "Copiar", + "Conversation": "Conversação", "Permalink": "Link-Permanente", + "Clear_cache_loading": "Limpando cache.", "Create_account": "Criar conta", "Create_Channel": "Criar Canal", "Create_Direct_Messages": "Criar Mensagens Diretas", @@ -172,19 +164,21 @@ "Create": "Criar", "Dark": "Escuro", "Dark_level": "Nível escuro", + "Default": "Padrão", "Default_browser": "Navegador padrão", "Delete_Room_Warning": "A exclusão de uma sala irá apagar todas as mensagens postadas na sala. Isso não pode ser desfeito.", + "Department": "Departamento", "delete": "excluir", "Delete": "Excluir", "DELETE": "EXCLUIR", "deleting_room": "excluindo sala", - "Direct_Messages": "Mensagens Diretas", + "description": "descrição", + "Description": "Descrição", "Desktop_Options": "Opções De Área De Trabalho", "Desktop_Notifications": "Notificações da Área de Trabalho", "Desktop_Alert_info": "Essas notificações são entregues a você na área de trabalho", "Directory": "Diretório", - "description": "descrição", - "Description": "Descrição", + "Direct_Messages": "Mensagens Diretas", "Disable_notifications": "Desabilitar notificações", "Discussions": "Discussões", "Discussion_Desc": "Ajude a manter uma visão geral sobre o que está acontecendo! Ao criar uma discussão, um sub-canal do que você selecionou é criado e os dois são vinculados.", @@ -192,6 +186,7 @@ "Done": "Pronto", "Dont_Have_An_Account": "Não tem uma conta?", "Do_you_have_an_account": "Você tem uma conta?", + "Do_you_have_a_certificate": "Você tem um certificado?", "Do_you_really_want_to_key_this_room_question_mark": "Você quer realmente {{key}} esta sala?", "E2E_Encryption": "Encriptação ponta a ponta", "E2E_How_It_Works_info1": "Agora você pode criar grupos privados criptografados e mensagens diretas. Você também pode alterar grupos privados existentes ou DMs para criptografados.", @@ -201,16 +196,16 @@ "edit": "editar", "edited": "editado", "Edit": "Editar", - "Edit_Invite": "Editar convite", "Edit_Status": "Editar Status", + "Edit_Invite": "Editar convite", "End_to_end_encrypted_room": "Sala criptografada de ponta a ponta", "end_to_end_encryption": "criptografia de ponta a ponta", + "Email_Notification_Mode_All": "Cada Menção / Mensagem Direta", + "Email_Notification_Mode_Disabled": "Desativado", "Email_or_password_field_is_empty": "Email ou senha estão vazios", "Email": "E-mail", "email": "e-mail", "Empty_title": "Título vazio", - "Email_Notification_Mode_All": "Cada Menção / Mensagem Direta", - "Email_Notification_Mode_Disabled": "Desativado", "Enable_Auto_Translate": "Ativar a tradução automática", "Enable_notifications": "Habilitar notificações", "Encrypted": "Criptografado", @@ -223,6 +218,7 @@ "Everyone_can_access_this_channel": "Todos podem acessar este canal", "Error_uploading": "Erro subindo", "Expiration_Days": "Expira em (dias)", + "Favorite": "Adicionar aos Favoritos", "Favorites": "Favoritos", "Files": "Arquivos", "File_description": "Descrição do arquivo", @@ -241,6 +237,7 @@ "Generate_New_Link": "Gerar novo convite", "Group_by_favorites": "Agrupar favoritos", "Group_by_type": "Agrupar por tipo", + "Hide": "Ocultar", "Has_joined_the_channel": "entrou no canal", "Has_joined_the_conversation": "entrou na conversa", "Has_left_the_channel": "saiu da conversa", @@ -286,8 +283,8 @@ "Login": "Entrar", "Login_error": "Suas credenciais foram rejeitadas. Tente novamente por favor!", "Login_with": "Login with", - "Logout": "Sair", "Logging_out": "Saindo.", + "Logout": "Sair", "Max_number_of_uses": "Número máximo de usos", "Max_number_of_users_allowed_is_number": "Número máximo de usuários é {{maxUsers}}", "Members": "Membros", @@ -300,6 +297,7 @@ "Message_removed": "Mensagem removida", "message": "mensagem", "messages": "mensagens", + "Message": "Mensagem", "Messages": "Mensagens", "Microphone_Permission_Message": "Rocket.Chat precisa de acesso ao seu microfone para enviar mensagens de áudio.", "Microphone_Permission": "Acesso ao Microfone", @@ -311,7 +309,6 @@ "Name": "Nome", "Navigation_history": "Histórico de navegação", "Never": "Nunca", - "New_in_RocketChat_question_mark": "Novo no Rocket.Chat?", "New_Message": "Nova Mensagem", "New_Password": "Nova Senha", "Next": "Próximo", @@ -326,19 +323,20 @@ "No_Message": "Não há mensagens", "No_messages_yet": "Não há mensagens ainda", "No_Reactions": "Sem reações", + "Not_RC_Server": "Este não é um servidor Rocket.Chat.\n{{contact}}", + "Nothing": "Nada", "Nothing_to_save": "Nada para salvar!", "Notify_active_in_this_room": "Notificar usuários ativos nesta sala", "Notify_all_in_this_room": "Notificar todos nesta sala", "Notifications": "Notificações", "Notification_Duration": "Duração da notificação", "Notification_Preferences": "Preferências de notificação", - "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!", "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.", "Onboarding_join_workspace": "Entre numa workspace", "Onboarding_subtitle": "Além da colaboração em equipe", @@ -358,13 +356,14 @@ "Password": "Senha", "Parent_channel_or_group": "Canal ou grupo pai", "Permalink_copied_to_clipboard": "Link-permanente copiado para a área de transferência!", + "Phone": "Telefone", "Pin": "Fixar", "Pinned_Messages": "Mensagens Fixadas", "pinned": "fixada", "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", + "Please_enter_your_password": "Por favor, digite sua senha", + "Please_wait": "Por favor, aguarde.", "Preferences": "Preferências", "Preferences_saved": "Preferências salvas!", "Privacy_Policy": " Política de Privacidade", @@ -386,15 +385,16 @@ "Read_External_Permission": "Permissão de acesso à arquivos", "Read_Only_Channel": "Canal Somente Leitura", "Read_Only": "Somente Leitura", + "Read_Receipt": "Lida por", "Receive_Group_Mentions": "Receber menções de grupo", "Receive_Group_Mentions_Info": "Receber menções @all e @here", "Register": "Registrar", - "Read_Receipt": "Lida por", "Repeat_Password": "Repetir Senha", "Replied_on": "Respondido em:", "replies": "respostas", "reply": "resposta", "Reply": "Responder", + "Report": "Reportar", "Receive_Notification": "Receber Notificação", "Receive_notifications_from": "Receber notificação de {{name}}", "Resend": "Reenviar", @@ -424,6 +424,7 @@ "SAVE": "SALVAR", "Save_Changes": "Salvar Alterações", "Save": "Salvar", + "Saved": "Salvo", "saving_preferences": "salvando preferências", "saving_profile": "salvando perfil", "saving_settings": "salvando configurações", @@ -472,12 +473,14 @@ "starred": "favoritou", "Starred": "Mensagens Favoritas", "Start_of_conversation": "Início da conversa", - "Started_call": "Chamada iniciada por {{userBy}}", + "Start_a_Discussion": "Iniciar uma Discussão", "Started_discussion": "Iniciou uma discussão:", + "Started_call": "Chamada iniciada por {{userBy}}", "Submit": "Enviar", "Table": "Tabela", "Take_a_photo": "Tirar uma foto", "Take_a_video": "Gravar um vídeo", + "Take_it": "Pegue!", "Terms_of_Service": " Termos de Serviço ", "Theme": "Tema", "The_user_wont_be_able_to_type_in_roomName": "O usuário não poderá digitar em {{roomName}}", @@ -491,12 +494,14 @@ "To": "Para", "topic": "tópico", "Topic": "Tópico", + "Translate": "Traduzir", "Try_again": "Tentar novamente", "Two_Factor_Authentication": "Autenticação de dois fatores", "Type_the_channel_name_here": "Digite o nome do canal", "unarchive": "desarquivar", "UNARCHIVE": "DESARQUIVAR", "Unblock_user": "Desbloquear usuário", + "Unfavorite": "Remover dos Favoritos", "Unfollowed_thread": "Parou de seguir tópico", "Unmute": "Permitir que o usuário fale", "unmuted": "permitiu que o usuário fale", @@ -511,6 +516,7 @@ "User": "Usuário", "Users": "Usuários", "User_added_by": "Usuário {{userAdded}} adicionado por {{userBy}}", + "User_Info": "Informações do usuário", "User_has_been_key": "Usuário foi {{key}}", "User_is_no_longer_role_by_": "{{user}} não pertence mais à {{role}} por {{userBy}}", "User_muted_by": "User {{userMuted}} muted por {{userBy}}", @@ -527,25 +533,31 @@ "Verify_email_desc": "Nós lhe enviamos um e-mail para confirmar o seu registro. Se você não receber um e-mail em breve, por favor retorne e tente novamente.", "Verify_your_email_for_the_code_we_sent": "Verifique em seu e-mail o código que enviamos", "Video_call": "Chamada de vídeo", + "View_Original": "Visualizar original", "Voice_call": "Chamada de voz", "Waiting_for_network": "Aguardando rede...", "Websocket_disabled": "Websocket está desativado para esse servidor.\n{{contact}}", "Welcome": "Bem vindo", - "Whats_your_2fa": "Qual seu código de autenticação?", "What_are_you_doing_right_now": "O que você está fazendo agora?", + "Whats_your_2fa": "Qual seu código de autenticação?", "Without_Servers": "Sem Servidores", "Workspaces": "Workspaces", + "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", "Yes_action_it": "Sim, {{action}}!", "Yesterday": "Ontem", "You_are_in_preview_mode": "Está é uma prévia do canal", "You_are_offline": "Você está offline", "You_can_search_using_RegExp_eg": "Você pode usar expressões regulares, por exemplo `/^text$/i`", - "You_need_to_verifiy_your_email_address_to_get_notications": "Você precisa confirmar seu endereço de e-mail para obter notificações", "You_colon": "Você: ", "you_were_mentioned": "você foi mencionado", "You_were_removed_from_channel": "Você foi removido de {{channel}}", "you": "você", "You": "Você", + "You_need_to_verifiy_your_email_address_to_get_notications": "Você precisa confirmar seu endereço de e-mail para obter notificações", + "Your_certificate": "Seu certificado", "Your_invite_link_will_expire_after__usesLeft__uses": "Seu link de convite irá vencer depois de {{usesLeft}} usos.", "Your_invite_link_will_expire_on__date__or_after__usesLeft__uses": "Seu link de convite irá vencer em {{date}} ou depois de {{usesLeft}} usos.", "Your_invite_link_will_expire_on__date__": "Seu link de convite irá vencer em {{date}}.", @@ -553,10 +565,7 @@ "Your_workspace": "Sua workspace", "You_will_not_be_able_to_recover_this_message": "Você não será capaz de recuperar essa mensagem!", "You_will_unset_a_certificate_for_this_server": "Você cancelará a configuração de um certificado para este servidor", - "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", + "Change_Language": "Alterar idioma", "Crash_report_disclaimer": "Nós não rastreamos o conteúdo das suas conversas. O relatório de erros e os eventos do analytics apenas contém informações relevantes para identificarmos problemas e corrigí-los.", "Type_message": "Digitar mensagem", "Room_search": "Busca de sala", @@ -568,6 +577,7 @@ "Search_messages": "Buscar mensagens", "Scroll_messages": "Rolar mensagens", "Reply_latest": "Responder para última mensagem", + "Reply_in_Thread": "Responder por Tópico", "Server_selection": "Seleção de servidor", "Server_selection_numbers": "Selecionar servidor 1...9", "Add_server": "Adicionar servidor", @@ -654,10 +664,8 @@ "Workspace_URL_Example": "Ex. sua-empresa.rocket.chat", "This_room_encryption_has_been_enabled_by__username_": "A criptografia para essa sala foi habilitada por {{username}}", "This_room_encryption_has_been_disabled_by__username_": "A criptografia para essa sala foi desabilitada por {{username}}", - "Apply_Your_Certificate": "Aplicar certificado", - "Do_you_have_a_certificate": "Você tem um certificado?", - "Your_certificate": "Seu certificado", "Teams": "Times", "No_team_channels_found": "Nenhum canal encontrado", - "Team_not_found": "Time não encontrado" + "Team_not_found": "Time não encontrado", + "Private_Team": "Equipe Privada" } \ No newline at end of file diff --git a/app/i18n/locales/pt-PT.json b/app/i18n/locales/pt-PT.json index 7a5f40bd8..ca7d4dc59 100644 --- a/app/i18n/locales/pt-PT.json +++ b/app/i18n/locales/pt-PT.json @@ -30,7 +30,7 @@ "error-invalid-date": "Data inválida fornecida.", "error-invalid-description": "Descrição inválida", "error-invalid-domain": "Domínio inválido", - "error-invalid-email": "E-mail inválido {{emai}}", + "error-invalid-email": "E-mail inválido {{email}}", "error-invalid-email-address": "Endereço de e-mail invalido", "error-invalid-file-height": "Altura de ficheiro inválida", "error-invalid-file-type": "Tipo de ficheiro inválido", @@ -137,14 +137,14 @@ "delete": "apagar", "Delete": "Apagar", "DELETE": "APAGAR", + "deleting_room": "apagando sala", "description": "descrição", "Description": "Descrição", - "Disable_notifications": "Desactivar notificações", "Direct_Messages": "Mensagens Directas", + "Disable_notifications": "Desactivar notificações", "Dont_Have_An_Account": "Não tem uma conta?", "Do_you_really_want_to_key_this_room_question_mark": "Você quer mesmo {{key}} esta sala?", "edit": "editar", - "deleting_room": "apagando sala", "Edit": "Editar", "Email_or_password_field_is_empty": "O campo de e-mail ou palavra-passe está vazio", "Email": "E-mail", diff --git a/app/i18n/locales/ru.json b/app/i18n/locales/ru.json index d63a078a5..8b904c0ff 100644 --- a/app/i18n/locales/ru.json +++ b/app/i18n/locales/ru.json @@ -29,7 +29,7 @@ "error-invalid-channel": "Недействительный канал.", "error-invalid-channel-start-with-chars": "Недействительный канал. Начните с @ или #", "error-invalid-custom-field": "Неверное настраиваемое поле", - "error-invalid-custom-field-name": "Неверное имя настраиваемого поля. Используйте только буквы, цифры, дефисы и символы подчеркивания.", + "error-invalid-custom-field-name": "Неверное имя настраиваемого поля. Используйте только буквы, цифры, дефис и символ подчеркивания.", "error-invalid-date": "Указана недопустимая дата.", "error-invalid-description": "Недопустимое описание", "error-invalid-domain": "Недопустимый домен", @@ -46,9 +46,9 @@ "error-invalid-password": "Неверный пароль", "error-invalid-redirectUri": "Недопустимый redirectUri", "error-invalid-role": "Недопустимая роль", - "error-invalid-room": "Недопустимый канал", - "error-invalid-room-name": "{{room_name}} не является допустимым именем канала", - "error-invalid-room-type": "{{type}} не является допустимым типом канала.", + "error-invalid-room": "Недопустимый чат", + "error-invalid-room-name": "{{room_name}} не является допустимым именем чата", + "error-invalid-room-type": "{{type}} не является допустимым типом чата.", "error-invalid-settings": "Недопустимые параметры", "error-invalid-subscription": "Недействительная подписка", "error-invalid-token": "Недопустимый токен", @@ -77,7 +77,7 @@ "error-user-registration-custom-field": "error-user-registration-custom-field", "error-user-registration-disabled": "Регистрация пользователей отключена", "error-user-registration-secret": "Регистрация пользователей разрешена только через секретный URL", - "error-you-are-last-owner": "Вы последний владелец. Пожалуйста, установите нового владельца, прежде чем покинуть комнату.", + "error-you-are-last-owner": "Вы последний владелец. Пожалуйста, назначьте нового владельца, прежде чем покинуть чат.", "Actions": "Действия", "activity": "активности", "Activity": "По активности", @@ -281,6 +281,8 @@ "Invite_Link": "Ссылка Приглашения", "Invite_users": "Приглашение пользователей", "Join": "Присоединиться", + "Join_Code": "Код присоединения", + "Insert_Join_Code": "Вставить код присоединения", "Join_our_open_workspace": "Присоединиться к нашему открытому серверу", "Join_your_workspace": "Присоединиться к вашему серверу", "Just_invited_people_can_access_this_channel": "Только приглашенные люди могут получить доступ к этому каналу", @@ -322,7 +324,7 @@ "Mute": "Заглушить", "muted": "Заглушен", "My_servers": "Мои серверы", - "N_person_reacted": "{{n}} людей отреагировало", + "N_people_reacted": "отреагировало {{n}} человек", "N_users": "{{n}} пользователи", "name": "имя", "Name": "Имя", @@ -704,5 +706,15 @@ "Enter_workspace_URL": "Введите URL вашего рабочего пространства", "Workspace_URL_Example": "Например, your-company.rocket.chat", "This_room_encryption_has_been_enabled_by__username_": "Шифрование для этого чата включено {{username}}", - "This_room_encryption_has_been_disabled_by__username_": "Шифрование для этого чата выключено {{username}}" + "This_room_encryption_has_been_disabled_by__username_": "Шифрование для этого чата выключено {{username}}", + "Teams": "Команды", + "No_team_channels_found": "Каналы не найдены", + "Team_not_found": "Команда не найдена", + "Create_Team": "Создать Команду", + "Team_Name": "Имя Команды", + "Private_Team": "Приватная Команда", + "Read_Only_Team": "Команда только для чтения", + "Broadcast_Team": "Широковещательная Команда", + "creating_team": "создание Команды", + "team-name-already-exists": "Команда с таким названием уже существует" } \ No newline at end of file diff --git a/app/i18n/locales/tr.json b/app/i18n/locales/tr.json index 1029893ad..4de4c1ecf 100644 --- a/app/i18n/locales/tr.json +++ b/app/i18n/locales/tr.json @@ -440,7 +440,6 @@ "Room_changed_announcement": "Oda duyurusu, {{userBy}} tarafından {{announcement}} olarak değiştirildi", "Room_changed_avatar": "Oda profil fotoğrafı {{userBy}} tarafından değiştirildi", "Room_changed_description": "Oda açıklaması, {{userBy}} tarafından {{description}} olarak değiştirildi", - "Room_changed_privacy": "Oda açıklaması, {{userBy}} tarafından {{description}} olarak değiştirildi", "Room_changed_topic": "Oda konusu, {{userBy}} tarafından {{topic}} olarak değiştirildi", "Room_Files": "Oda Dosyaları", "Room_Info_Edit": "Oda Bilgilerini Düzenle", @@ -565,7 +564,6 @@ "Username": "Kullanıcı adı", "Username_or_email": "Kullanıcı adı ya da e-posta", "Uses_server_configuration": "Sunucu yapılandırmasını kullanır", - "Usually_a_discussion_starts_with_a_question_like_How_do_I_upload_a_picture": "Genellikle tartışma, \"Nasıl resim yüklerim?\" gibi bir soruyla başlar.", "Validating": "Doğrulanıyor", "Registration_Succeeded": "Kayıt Başarılı!", "Verify": "Onayla", @@ -600,7 +598,6 @@ "You_need_to_access_at_least_one_RocketChat_server_to_share_something": "Bir şeyler paylaşmak için Rocket.Chat sunucusuna erişmeniz gerekir.", "You_need_to_verifiy_your_email_address_to_get_notications": "Bildirim almak için e-posta adresinizi doğrulamanız gerekiyor", "Your_certificate": "Sertifikanız", - "Your_message": "İletiınız", "Your_invite_link_will_expire_after__usesLeft__uses": "Davet bağlantınızın geçerliliği {{usesLeft}} kullanımdan sonra sona erecek.", "Your_invite_link_will_expire_on__date__or_after__usesLeft__uses": "Davet bağlantınızın geçerliliği {{date}} tarihinde veya {{usesLeft}} kullanımdan sonra sona erecek.", "Your_invite_link_will_expire_on__date__": "Davet bağlantınızın geçerlilik süresi {{date}} tarihinde sona erecek.", @@ -695,12 +692,12 @@ "User_has_been_ignored": "Kullanıcı yok sayıldı.", "User_has_been_unignored": "Kullanıcı artık yok sayılmıyor.", "User_has_been_removed_from_s": "Kullanıcı {{s}} alanından kaldırıldı.", - "User__username__is_now_a_leader_of__room_name_": "{{Username}} kullanıcısı artık {{room_name}} lideridir.", - "User__username__is_now_a_moderator_of__room_name_": "{{Username}} kullanıcısı artık bir {{room_name}} moderatörüdür.", - "User__username__is_now_a_owner_of__room_name_": "{{Username}} kullanıcısı artık {{room_name}} adlı odanın sahibidir.", - "User__username__removed_from__room_name__leaders": "{{Username}} adlı kullanıcı, {{room_name}} liderlerinden kaldırıldı.", - "User__username__removed_from__room_name__moderators": "{{Username}} adlı kullanıcı, {{room_name}} moderatörlerinden kaldırıldı.", - "User__username__removed_from__room_name__owners": "{{Username}} adlı kullanıcı, {{room_name}} sahiplerinden kaldırıldı.", + "User__username__is_now_a_leader_of__room_name_": "{{username}} kullanıcısı artık {{room_name}} lideridir.", + "User__username__is_now_a_moderator_of__room_name_": "{{username}} kullanıcısı artık bir {{room_name}} moderatörüdür.", + "User__username__is_now_a_owner_of__room_name_": "{{username}} kullanıcısı artık {{room_name}} adlı odanın sahibidir.", + "User__username__removed_from__room_name__leaders": "{{username}} adlı kullanıcı, {{room_name}} liderlerinden kaldırıldı.", + "User__username__removed_from__room_name__moderators": "{{username}} adlı kullanıcı, {{room_name}} moderatörlerinden kaldırıldı.", + "User__username__removed_from__room_name__owners": "{{username}} adlı kullanıcı, {{room_name}} sahiplerinden kaldırıldı.", "The_user_will_be_removed_from_s": "Kullanıcı, {{s}} alanından kaldırılacak!", "Yes_remove_user": "Evet, kullanıcıyı kaldır!", "Direct_message": "Özel ileti", diff --git a/app/i18n/locales/zh-CN.json b/app/i18n/locales/zh-CN.json index 3253216e1..1b6fe974c 100644 --- a/app/i18n/locales/zh-CN.json +++ b/app/i18n/locales/zh-CN.json @@ -33,7 +33,7 @@ "error-invalid-date": "无效的日期", "error-invalid-description": "无效的描述", "error-invalid-domain": "无效的域名", - "error-invalid-email": "无效的电子邮件{{emai}}", + "error-invalid-email": "无效的电子邮件{{email}}", "error-invalid-email-address": "无效的邮件地址", "error-invalid-file-height": "无效的文件长度", "error-invalid-file-type": "无效的文件类型", @@ -278,11 +278,11 @@ "is_typing": "正在输入", "Invalid_or_expired_invite_token": "无效或到期的邀请 token", "Invalid_server_version": "此 App 版本已不支援您正在连线之服务器版本。当前版本: {{currentVersion}}.\\n\\n最低版本要求: {{minVersion}}", - "Join_your_workspace": "加入您的工作区", "Invite_Link": "邀请链接", "Invite_users": "邀请用戶", "Join": "加入", "Join_our_open_workspace": "加入开放工作区", + "Join_your_workspace": "加入您的工作区", "Just_invited_people_can_access_this_channel": "仅有被邀请人能进入这个频道", "Language": "语言", "last_message": "最后一条信息", @@ -300,7 +300,7 @@ "Logging_out": "正在登出", "Logout": "注销", "Max_number_of_uses": "最大使用次数", - "Max_number_of_users_allowed_is_number": "允许使用者上限数量", + "Max_number_of_users_allowed_is_number": "允许使用者上限数量{{maxUsers}}", "members": "成员", "Members": "成员", "Mentioned_Messages": "被提及的信息", @@ -444,7 +444,7 @@ "Room_Info_Edit": "聊天室信息编辑", "Room_Info": "聊天室信息", "Room_Members": "聊天室成员", - "Room_name_changed": "{{userBy}} 将聊天室名称改为:{{{name}}", + "Room_name_changed": "{{userBy}} 将聊天室名称改为:{{name}}", "SAVE": "保存", "Save_Changes": "保存更改", "Save": "保存", diff --git a/app/i18n/locales/zh-TW.json b/app/i18n/locales/zh-TW.json index 271e56418..dc5748c27 100644 --- a/app/i18n/locales/zh-TW.json +++ b/app/i18n/locales/zh-TW.json @@ -278,11 +278,11 @@ "is_typing": "正在輸入", "Invalid_or_expired_invite_token": "無效或到期的邀請 token", "Invalid_server_version": "此 App 版本已不支援您正在連線之伺服器版本。當前版本: {{currentVersion}}.\\n\\n最低版本要求: {{minVersion}}", - "Join_your_workspace": "加入您的工作區", "Invite_Link": "邀請連結", "Invite_users": "邀請使用者", "Join": "加入", "Join_our_open_workspace": "加入開放工作區", + "Join_your_workspace": "加入您的工作區", "Just_invited_people_can_access_this_channel": "僅有受邀者能存取此頻道", "Language": "語言", "last_message": "最後一則訊息", @@ -300,7 +300,7 @@ "Logging_out": "正在登出", "Logout": "登出", "Max_number_of_uses": "最大使用次數", - "Max_number_of_users_allowed_is_number": "允許使用者上限數量", + "Max_number_of_users_allowed_is_number": "允許使用者上限數量 {{maxUsers}}", "members": "成員", "Members": "成員", "Mentioned_Messages": "被提及的訊息", @@ -444,7 +444,7 @@ "Room_Info_Edit": "修改聊天室資訊", "Room_Info": "聊天室資訊", "Room_Members": "聊天室成員", - "Room_name_changed": "{{userBy}} 將聊天室名稱改為:{{{name}}", + "Room_name_changed": "{{userBy}} 將聊天室名稱改為:{{name}}", "SAVE": "儲存", "Save_Changes": "儲存更改", "Save": "儲存", From 9670fa623a9c5d5989fecac25c8b3ea5867fd386 Mon Sep 17 00:00:00 2001 From: Gerzon Z Date: Wed, 19 May 2021 17:14:42 -0400 Subject: [PATCH 07/44] [NEW] Add/Create/Remove channel on a team (#3090) * Added Create Team * Added actionTypes, actions, ENG strings for Teams and updated NewMessageView * Added createTeam sagas, createTeam reducer, new Team string and update CreateChannelView * Remove unnecessary actionTypes, reducers and sagas, e2e tests and navigation to team view * Minor tweaks * Show TeamChannelsView only if joined the team * Minor tweak * Added AddChannelTeamView * Added permissions, translations strings for teams, deleteTeamRoom and addTeamRooms, AddExistingChannelView, updated CreateChannelView, TeamChannelsView * Refactor touch component and update removeRoom and deleteRoom methods * Minor tweaks * Minor tweaks for removing channels and addExistingChannelView * Added missing events and fixed channels list * Minor tweaks for refactored touch component * Minor tweaks * Remove unnecesary changes, update TeamChannelsView, AddExistingChannelView, AddChannelTeamView, createChannel, goRoom and Touchable * Add screens to ModalStack, events, autoJoin, update createChannel, addRoomsToTeam and Touchable * Minor tweak * Update loadMessagesForRoom.js * Updated schema, tag component, touch, AddChannelTeamView, AddExistingChannelView, ActionSheet Item * Fix unnecessary changes * Add i18n, update createChannel, AddExistingChannelTeamView, AddChannelTeamView, RightButton and TeamChannelsView * Updated styles, added tag story * Minor tweak * Minor tweaks * Auto-join tweak * Minor tweaks * Minor tweak on search * One way to refactor :P * Next level refactor :) * Fix create group dm * Refactor renderItem * Minor bug fixes * Fix stories Co-authored-by: Diego Mello --- .../__snapshots__/Storyshots.test.js.snap | 23079 +++++++++------- app/containers/ActionSheet/Item.js | 24 +- app/containers/ActionSheet/styles.js | 6 + app/containers/RoomTypeIcon.js | 1 + app/i18n/locales/en.json | 14 +- app/lib/methods/getPermissions.js | 7 +- app/lib/rocketchat.js | 30 +- app/presentation/RoomItem/RoomItem.js | 17 +- app/presentation/RoomItem/Tag.js | 30 + app/presentation/RoomItem/Touchable.js | 105 +- app/presentation/RoomItem/index.js | 17 +- app/presentation/RoomItem/styles.js | 11 + app/sagas/createChannel.js | 37 +- app/stacks/InsideStack.js | 12 + app/stacks/MasterDetailStack/index.js | 12 + app/utils/log/events.js | 8 + app/views/AddChannelTeamView.js | 72 + app/views/AddExistingChannelView.js | 209 + app/views/CreateChannelView.js | 6 +- app/views/NewMessageView.js | 6 +- app/views/TeamChannelsView.js | 187 +- storybook/stories/RoomItem.js | 10 +- 22 files changed, 13185 insertions(+), 10715 deletions(-) create mode 100644 app/presentation/RoomItem/Tag.js create mode 100644 app/views/AddChannelTeamView.js create mode 100644 app/views/AddExistingChannelView.js diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 52cde22be..68b171855 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -44122,46 +44122,119 @@ exports[`Storyshots Room Item Alerts 1`] = ` > - + + + + +  + + + Read + + + + + - @@ -44170,7 +44243,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -44183,7 +44256,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` ] } > -  +  - Read + Favorite - - - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - - - - - - - - -  - - - rocket.cat - - - - - - - - - @@ -44558,7 +44300,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -44571,7 +44313,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` ] } > -  +  - Read + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - -  - - - unread - +  + + - 1 + rocket.cat @@ -44939,46 +44512,119 @@ exports[`Storyshots Room Item Alerts 1`] = ` - + + + + +  + + + Read + + + + + - @@ -44987,7 +44633,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -45000,7 +44646,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - -  - - - unread - +  + + - +999 + unread + + + 1 + + @@ -45368,46 +44943,119 @@ exports[`Storyshots Room Item Alerts 1`] = ` - + + + + +  + + + Read + + + + + - @@ -45416,7 +45064,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -45429,7 +45077,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - -  - - - user mentions - +  + + - 1 + unread + + + +999 + + @@ -45797,46 +45374,119 @@ exports[`Storyshots Room Item Alerts 1`] = ` - + + + + +  + + + Read + + + + + - @@ -45845,7 +45495,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -45858,7 +45508,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - -  - - - group mentions - +  + + - 1 + user mentions + + + 1 + + @@ -46226,46 +45805,119 @@ exports[`Storyshots Room Item Alerts 1`] = ` - + + + + +  + + + Read + + + + + - @@ -46274,7 +45926,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -46287,7 +45939,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - -  - - - thread unread - +  + + - 1 + group mentions + + + 1 + + @@ -46655,46 +46236,119 @@ exports[`Storyshots Room Item Alerts 1`] = ` - + + + + +  + + + Read + + + + + - @@ -46703,7 +46357,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -46716,7 +46370,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - -  - - - thread unread user - +  + + - 1 + thread unread + + + 1 + + @@ -47084,46 +46667,119 @@ exports[`Storyshots Room Item Alerts 1`] = ` - + + + + +  + + + Read + + + + + - @@ -47132,7 +46788,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -47145,7 +46801,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - -  - - - thread unread group - +  + + - 1 + thread unread user + + + 1 + + @@ -47513,46 +47098,119 @@ exports[`Storyshots Room Item Alerts 1`] = ` - + + + + +  + + + Read + + + + + - @@ -47561,7 +47219,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -47574,7 +47232,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - -  - - - user mentions priority 1 - +  + + - 1 + thread unread group + + + 1 + + @@ -47942,46 +47529,119 @@ exports[`Storyshots Room Item Alerts 1`] = ` - + + + + +  + + + Read + + + + + - @@ -47990,7 +47650,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -48003,7 +47663,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - -  - - - group mentions priority 2 - +  + + - 1 + user mentions priority 1 + + + 1 + + @@ -48371,46 +47960,119 @@ exports[`Storyshots Room Item Alerts 1`] = ` - + + + + +  + + + Read + + + + + - @@ -48419,7 +48081,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -48432,7 +48094,7 @@ exports[`Storyshots Room Item Alerts 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - + + +  + + + group mentions priority 2 + + + + 1 + + + + + + + + + + + + + -  +  - thread unread priority 3 + Read + + + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + + + + + + +  + + - 1 + thread unread priority 3 + + + 1 + + @@ -48813,46 +48835,119 @@ exports[`Storyshots Room Item Basic 1`] = ` > - + + + + +  + + + Read + + + + + - @@ -48861,7 +48956,7 @@ exports[`Storyshots Room Item Basic 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -48874,7 +48969,7 @@ exports[`Storyshots Room Item Basic 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - + -  - - - rocket.cat - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + rocket.cat + + @@ -49209,312 +49233,47 @@ exports[`Storyshots Room Item Last Message 1`] = ` > - + - -  - - - Read - - - - - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - - - - - - @@ -49523,30 +49282,10 @@ exports[`Storyshots Room Item Last Message 1`] = ` style={ Array [ Object { - "color": "#cbced1", - "fontSize": 16, + "color": "white", + "fontSize": 20, }, - Array [ - Object { - "height": 16, - "textAlignVertical": "center", - "width": 16, - }, - Array [ - Array [ - Object { - "marginRight": 4, - }, - Object { - "color": "#0d0e12", - }, - undefined, - ], - Object { - "color": "#cbced1", - }, - ], - ], + undefined, Object { "fontFamily": "custom", "fontStyle": "normal", @@ -49556,1899 +49295,319 @@ exports[`Storyshots Room Item Last Message 1`] = ` ] } > -  +  - rocket.cat - - - 10:00 - - - - - No Message - - - - - - - - - - - -  - - - Read - - - - - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - - - - - - - - -  - - - rocket.cat - - - 10:00 - - - - - 2 - - - - - - - - - - - -  - - - Read - - - - - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - - - - - - - - -  - - - rocket.cat - - - 10:00 - - - - - You: 1 - - - - - - - - - - - -  - - - Read - - - - - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - - - - - - - - -  - - - rocket.cat - - - 10:00 - - - - - Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries - - - - - - - - - - - -  - - - Read - - - - - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - - - - - - - - -  - - - rocket.cat - - - 10:00 + Read - + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + - - Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries - + + + + + +  + + + rocket.cat + + + 10:00 + + + + - 1 + No Message @@ -51475,46 +49677,119 @@ exports[`Storyshots Room Item Last Message 1`] = ` - + + + + +  + + + Read + + + + + - @@ -51523,7 +49798,7 @@ exports[`Storyshots Room Item Last Message 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -51536,7 +49811,7 @@ exports[`Storyshots Room Item Last Message 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - - - - - - -  - - - rocket.cat - - - 10:00 - - - - - Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries - + + + + + +  + + + rocket.cat + + + 10:00 + + + + - +999 + 2 @@ -51973,46 +50121,119 @@ exports[`Storyshots Room Item Last Message 1`] = ` - + + + + +  + + + Read + + + + + - @@ -52021,7 +50242,7 @@ exports[`Storyshots Room Item Last Message 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -52034,7 +50255,7 @@ exports[`Storyshots Room Item Last Message 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + + + + + + > + +  + + + rocket.cat + + + 10:00 + + + + + You: 1 + + + + + + + + @@ -52287,30 +50614,10 @@ exports[`Storyshots Room Item Last Message 1`] = ` style={ Array [ Object { - "color": "#cbced1", - "fontSize": 16, + "color": "white", + "fontSize": 20, }, - Array [ - Object { - "height": 16, - "textAlignVertical": "center", - "width": 16, - }, - Array [ - Array [ - Object { - "marginRight": 4, - }, - Object { - "color": "#0d0e12", - }, - undefined, - ], - Object { - "color": "#cbced1", - }, - ], - ], + undefined, Object { "fontFamily": "custom", "fontStyle": "normal", @@ -52320,131 +50627,319 @@ exports[`Storyshots Room Item Last Message 1`] = ` ] } > -  +  - rocket.cat - - - 10:00 + Read - + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + - - Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries - + + + + + +  + + + rocket.cat + + + 10:00 + + + + - 1 + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + + + + + + + + + + +  + + + Read + + + + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + + + + + + + + +  + + + rocket.cat + + + 10:00 + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + 1 + + + + + + + + + + + + + + +  + + + Read + + + + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + + + + + + + + +  + + + rocket.cat + + + 10:00 + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + +999 + + + + + + + + + + + + + + +  + + + Read + + + + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + + + + + + + + +  + + + rocket.cat + + + 10:00 + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + 1 + + + + + + + + + + +`; + +exports[`Storyshots Room Item Tag 1`] = ` + + + + + + + + +  + + + Read + + + + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + + + + + + + + +  + + + rocket.cat + + + + Auto-join + + + + + + + + + + + + + + +  + + + Read + + + + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + + + + + + + + +  + + + rocket.cat + + + + Auto-join + + + + 10:00 + + + + + No Message + + + + + + + + + + + + + +  + + + Read + + + + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + + + + + + + + +  + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + Auto-join + + + + + + + + + + + + + + +  + + + Read + + + + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + + + + + + + + +  + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + Auto-join + + + + 10:00 + + + + + No Message @@ -52484,46 +54337,119 @@ exports[`Storyshots Room Item Type 1`] = ` > - + + + + +  + + + Read + + + + + - @@ -52532,7 +54458,7 @@ exports[`Storyshots Room Item Type 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -52545,7 +54471,7 @@ exports[`Storyshots Room Item Type 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - + +  + + + rocket.cat + + + + + + + + + + + + + -  +  - rocket.cat + Read - - - - - @@ -52915,7 +54843,7 @@ exports[`Storyshots Room Item Type 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -52928,7 +54856,7 @@ exports[`Storyshots Room Item Type 1`] = ` ] } > -  +  - Read + Favorite - - - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - - - - - - - - -  - - - rocket.cat - - - - - - - - - @@ -53286,7 +54900,7 @@ exports[`Storyshots Room Item Type 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -53299,7 +54913,7 @@ exports[`Storyshots Room Item Type 1`] = ` ] } > -  +  - Read + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - + + +  + + + rocket.cat + + + + + + + + + + + + -  +  - rocket.cat + Read - - - - - @@ -53657,7 +55216,7 @@ exports[`Storyshots Room Item Type 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -53670,7 +55229,7 @@ exports[`Storyshots Room Item Type 1`] = ` ] } > -  +  - Read + Favorite - - - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - - - - - - - - -  - - - rocket.cat - - - - - - - - - @@ -54028,7 +55273,7 @@ exports[`Storyshots Room Item Type 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -54041,7 +55286,7 @@ exports[`Storyshots Room Item Type 1`] = ` ] } > -  +  - Read + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - + + +  + + + rocket.cat + + + + + + + + + + + + -  +  - rocket.cat + Read - - - - - @@ -54399,7 +55589,7 @@ exports[`Storyshots Room Item Type 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -54412,7 +55602,7 @@ exports[`Storyshots Room Item Type 1`] = ` ] } > -  +  - Read + Favorite - - - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - - - - - - - - -  - - - rocket.cat - - - - - - - - - @@ -54770,7 +55646,7 @@ exports[`Storyshots Room Item Type 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -54783,7 +55659,7 @@ exports[`Storyshots Room Item Type 1`] = ` ] } > -  +  - Read + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - + + +  + + + rocket.cat + + + + + + + + + + + + -  +  - rocket.cat + Read + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + + + + + + + + +  + + + rocket.cat + + + + + + + + + + + + + +  + + + Read + + + + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + + + + + + + + +  + + + rocket.cat + + + + + + + + + + + + + +  + + + Read + + + + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + + + + + + + + +  + + + rocket.cat + + + + + @@ -55106,46 +56973,119 @@ exports[`Storyshots Room Item User 1`] = ` > - + + + + +  + + + Read + + + + + - @@ -55154,7 +57094,7 @@ exports[`Storyshots Room Item User 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -55167,7 +57107,7 @@ exports[`Storyshots Room Item User 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - + -  - - - diego.mello - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + diego.mello + + - + + + + +  + + + Read + + + + + - @@ -55537,7 +57479,7 @@ exports[`Storyshots Room Item User 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -55550,7 +57492,7 @@ exports[`Storyshots Room Item User 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - + -  - - - Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + @@ -55885,46 +57756,119 @@ exports[`Storyshots Room Item User status 1`] = ` > - + + + + +  + + + Read + + + + + - @@ -55933,7 +57877,7 @@ exports[`Storyshots Room Item User status 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -55946,7 +57890,7 @@ exports[`Storyshots Room Item User status 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - + -  - - - rocket.cat - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + rocket.cat + + - + + + + +  + + + Read + + + + + - @@ -56316,7 +58262,7 @@ exports[`Storyshots Room Item User status 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -56329,7 +58275,7 @@ exports[`Storyshots Room Item User status 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - + -  - - - rocket.cat - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + rocket.cat + + - + + + + +  + + + Read + + + + + - @@ -56699,7 +58647,7 @@ exports[`Storyshots Room Item User status 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -56712,7 +58660,7 @@ exports[`Storyshots Room Item User status 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - + -  - - - rocket.cat - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + rocket.cat + + - + + + + +  + + + Read + + + + + - @@ -57082,7 +59032,7 @@ exports[`Storyshots Room Item User status 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -57095,7 +59045,7 @@ exports[`Storyshots Room Item User status 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - + -  - - - rocket.cat - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + rocket.cat + + - + + + + +  + + + Read + + + + + - @@ -57465,7 +59417,7 @@ exports[`Storyshots Room Item User status 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -57478,7 +59430,7 @@ exports[`Storyshots Room Item User status 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - + -  - - - rocket.cat - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + rocket.cat + + - + + + + +  + + + Read + + + + + - @@ -57848,7 +59802,7 @@ exports[`Storyshots Room Item User status 1`] = ` style={ Array [ Object { - "color": "white", + "color": "#ffffff", "fontSize": 20, }, undefined, @@ -57861,7 +59815,7 @@ exports[`Storyshots Room Item User status 1`] = ` ] } > -  +  - Read + Favorite + + + + +  + + + Hide - - - -  - - - Favorite - - - - -  - - - Hide - - - - - - + > + + - - - + -  - - - rocket.cat - + Object { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + Object {}, + ] + } + > +  + + + rocket.cat + + diff --git a/app/containers/ActionSheet/Item.js b/app/containers/ActionSheet/Item.js index 7cd5e7b4d..aa76da8bb 100644 --- a/app/containers/ActionSheet/Item.js +++ b/app/containers/ActionSheet/Item.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Text } from 'react-native'; +import { Text, View } from 'react-native'; import { themes } from '../../constants/colors'; import { CustomIcon } from '../../lib/Icons'; @@ -20,12 +20,19 @@ export const Item = React.memo(({ item, hide, theme }) => { theme={theme} > - - {item.title} - + + + {item.title} + + + { item.right ? ( + + {item.right ? item.right() : null} + + ) : null } ); }); @@ -34,7 +41,8 @@ Item.propTypes = { title: PropTypes.string, icon: PropTypes.string, danger: PropTypes.bool, - onPress: PropTypes.func + onPress: PropTypes.func, + right: PropTypes.func }), hide: PropTypes.func, theme: PropTypes.string diff --git a/app/containers/ActionSheet/styles.js b/app/containers/ActionSheet/styles.js index 57fe0bc82..1b9397dc9 100644 --- a/app/containers/ActionSheet/styles.js +++ b/app/containers/ActionSheet/styles.js @@ -22,6 +22,9 @@ export default StyleSheet.create({ content: { paddingTop: 16 }, + titleContainer: { + flex: 1 + }, title: { fontSize: 16, marginLeft: 16, @@ -58,5 +61,8 @@ export default StyleSheet.create({ fontSize: 16, ...sharedStyles.textMedium, ...sharedStyles.textAlignCenter + }, + rightContainer: { + paddingLeft: 12 } }); diff --git a/app/containers/RoomTypeIcon.js b/app/containers/RoomTypeIcon.js index 55294310d..7c0e32c13 100644 --- a/app/containers/RoomTypeIcon.js +++ b/app/containers/RoomTypeIcon.js @@ -30,6 +30,7 @@ const RoomTypeIcon = React.memo(({ return ; } + // TODO: move this to a separate function let icon = 'channel-private'; if (teamMain) { icon = `teams${ type === 'p' ? '-private' : '' }`; diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index bd4e1f8a2..8cc6cb278 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -435,6 +435,7 @@ "Review_app_unable_store": "Unable to open {{store}}", "Review_this_app": "Review this app", "Remove": "Remove", + "remove": "remove", "Roles": "Roles", "Room_actions": "Room actions", "Room_changed_announcement": "Room announcement changed to: {{announcement}} by {{userBy}}", @@ -716,5 +717,14 @@ "Read_Only_Team": "Read Only Team", "Broadcast_Team": "Broadcast Team", "creating_team": "creating team", - "team-name-already-exists": "A team with that name already exists" -} \ No newline at end of file + "team-name-already-exists": "A team with that name already exists", + "Add_Channel_to_Team": "Add Channel to Team", + "Create_New": "Create New", + "Add_Existing": "Add Existing", + "Add_Existing_Channel": "Add Existing Channel", + "Remove_from_Team": "Remove from Team", + "Auto-join": "Auto-join", + "Delete_Team_Room_Warning": "Woud you like to remove this channel from the team? The channel will be moved back to the workspace", + "Confirmation": "Confirmation", + "invalid-room": "Invalid room" +} diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js index 09b91aa63..1f4bcb9cb 100644 --- a/app/lib/methods/getPermissions.js +++ b/app/lib/methods/getPermissions.js @@ -13,6 +13,7 @@ const PERMISSIONS = [ 'add-user-to-any-c-room', 'add-user-to-any-p-room', 'add-user-to-joined-room', + 'add-team-channel', 'archive-room', 'auto-translate', 'create-invite-links', @@ -21,11 +22,13 @@ const PERMISSIONS = [ 'delete-p', 'edit-message', 'edit-room', + 'edit-team-channel', 'force-delete-message', 'mute-user', 'pin-message', 'post-readonly', 'remove-user', + 'remove-team-channel', 'set-leader', 'set-moderator', 'set-owner', @@ -38,7 +41,9 @@ const PERMISSIONS = [ 'view-privileged-setting', 'view-room-administration', 'view-statistics', - 'view-user-administration' + 'view-user-administration', + 'view-all-teams', + 'view-all-team-channels' ]; export async function setPermissions() { diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index adc7f80ff..57c6e0c71 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -95,10 +95,19 @@ const RocketChat = { }, canOpenRoom, createChannel({ - name, users, type, readOnly, broadcast, encrypted + name, users, type, readOnly, broadcast, encrypted, teamId }) { - // RC 0.51.0 - return this.methodCallWrapper(type ? 'createPrivateGroup' : 'createChannel', name, users, readOnly, {}, { broadcast, encrypted }); + const params = { + name, + members: users, + readOnly, + extraData: { + broadcast, + encrypted, + ...(teamId && { teamId }) + } + }; + return this.post(type ? 'groups.create' : 'channels.create', params); }, async getWebsocketInfo({ server }) { const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) }); @@ -648,7 +657,8 @@ const RocketChat = { avatarETag: sub.avatarETag, t: sub.t, encrypted: sub.encrypted, - lastMessage: sub.lastMessage + lastMessage: sub.lastMessage, + ...(sub.teamId && { teamId: sub.teamId }) })); return data; @@ -751,6 +761,18 @@ const RocketChat = { // RC 3.13.0 return this.post('teams.create', params); }, + addRoomsToTeam({ teamId, rooms }) { + // RC 3.13.0 + return this.post('teams.addRooms', { teamId, rooms }); + }, + removeTeamRoom({ roomId, teamId }) { + // RC 3.13.0 + return this.post('teams.removeRoom', { roomId, teamId }); + }, + updateTeamRoom({ roomId, isDefault }) { + // RC 3.13.0 + return this.post('teams.updateRoom', { roomId, isDefault }); + }, joinRoom(roomId, joinCode, type) { // TODO: join code // RC 0.48.0 diff --git a/app/presentation/RoomItem/RoomItem.js b/app/presentation/RoomItem/RoomItem.js index b3922787b..065e91331 100644 --- a/app/presentation/RoomItem/RoomItem.js +++ b/app/presentation/RoomItem/RoomItem.js @@ -10,6 +10,8 @@ import LastMessage from './LastMessage'; import Title from './Title'; import UpdatedAt from './UpdatedAt'; import Touchable from './Touchable'; +import Tag from './Tag'; +import I18n from '../../i18n'; const RoomItem = ({ rid, @@ -42,13 +44,16 @@ const RoomItem = ({ testID, swipeEnabled, onPress, + onLongPress, toggleFav, toggleRead, hideChannel, - teamMain + teamMain, + autoJoin }) => ( + { + autoJoin ? : null + } + { + autoJoin ? : null + } { + const { theme } = useTheme(); + + return ( + + + {name} + + + ); +}); + +Tag.propTypes = { + name: PropTypes.string +}; + +export default Tag; diff --git a/app/presentation/RoomItem/Touchable.js b/app/presentation/RoomItem/Touchable.js index defb5e105..bbf7cbf86 100644 --- a/app/presentation/RoomItem/Touchable.js +++ b/app/presentation/RoomItem/Touchable.js @@ -1,7 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Animated } from 'react-native'; -import { PanGestureHandler, State } from 'react-native-gesture-handler'; +import { + LongPressGestureHandler, PanGestureHandler, State +} from 'react-native-gesture-handler'; import Touch from '../../utils/touch'; import { @@ -17,6 +19,7 @@ class Touchable extends React.Component { static propTypes = { type: PropTypes.string.isRequired, onPress: PropTypes.func, + onLongPress: PropTypes.func, testID: PropTypes.string, width: PropTypes.number, favorite: PropTypes.bool, @@ -59,6 +62,12 @@ class Touchable extends React.Component { } } + onLongPressHandlerStateChange = ({ nativeEvent }) => { + if (nativeEvent.state === State.ACTIVE) { + this.onLongPress(); + } + } + _handleRelease = (nativeEvent) => { const { translationX } = nativeEvent; @@ -203,54 +212,70 @@ class Touchable extends React.Component { } }; + onLongPress = () => { + const { rowState } = this.state; + const { onLongPress } = this.props; + if (rowState !== 0) { + this.close(); + return; + } + + if (onLongPress) { + onLongPress(); + } + }; + render() { const { testID, isRead, width, favorite, children, theme, isFocused, swipeEnabled } = this.props; return ( - - + - - - - - {children} - - - + + + + + + {children} + + + - + + + ); } } diff --git a/app/presentation/RoomItem/index.js b/app/presentation/RoomItem/index.js index 80bcf063b..d56194f8b 100644 --- a/app/presentation/RoomItem/index.js +++ b/app/presentation/RoomItem/index.js @@ -16,7 +16,8 @@ const attrs = [ 'theme', 'isFocused', 'forceUpdate', - 'showLastMessage' + 'showLastMessage', + 'autoJoin' ]; class RoomItemContainer extends React.Component { @@ -25,6 +26,7 @@ class RoomItemContainer extends React.Component { showLastMessage: PropTypes.bool, id: PropTypes.string, onPress: PropTypes.func, + onLongPress: PropTypes.func, username: PropTypes.string, avatarSize: PropTypes.number, width: PropTypes.number, @@ -41,7 +43,8 @@ class RoomItemContainer extends React.Component { getRoomAvatar: PropTypes.func, getIsGroupChat: PropTypes.func, getIsRead: PropTypes.func, - swipeEnabled: PropTypes.bool + swipeEnabled: PropTypes.bool, + autoJoin: PropTypes.bool }; static defaultProps = { @@ -112,6 +115,11 @@ class RoomItemContainer extends React.Component { return onPress(item); } + onLongPress = () => { + const { item, onLongPress } = this.props; + return onLongPress(item); + } + render() { const { item, @@ -129,7 +137,8 @@ class RoomItemContainer extends React.Component { showLastMessage, username, useRealName, - swipeEnabled + swipeEnabled, + autoJoin } = this.props; const name = getRoomTitle(item); const testID = `rooms-list-view-item-${ name }`; @@ -160,6 +169,7 @@ class RoomItemContainer extends React.Component { isGroupChat={this.isGroupChat} isRead={isRead} onPress={this.onPress} + onLongPress={this.onLongPress} date={date} accessibilityLabel={accessibilityLabel} width={width} @@ -189,6 +199,7 @@ class RoomItemContainer extends React.Component { tunreadGroup={item.tunreadGroup} swipeEnabled={swipeEnabled} teamMain={item.teamMain} + autoJoin={autoJoin} /> ); } diff --git a/app/presentation/RoomItem/styles.js b/app/presentation/RoomItem/styles.js index 80bf0c90b..787546c76 100644 --- a/app/presentation/RoomItem/styles.js +++ b/app/presentation/RoomItem/styles.js @@ -96,5 +96,16 @@ export default StyleSheet.create({ height: '100%', alignItems: 'center', justifyContent: 'center' + }, + tagContainer: { + alignSelf: 'center', + alignItems: 'center', + borderRadius: 4, + marginHorizontal: 4 + }, + tagText: { + fontSize: 13, + paddingHorizontal: 4, + ...sharedStyles.textSemibold } }); diff --git a/app/sagas/createChannel.js b/app/sagas/createChannel.js index f2ecfe76d..8979e230e 100644 --- a/app/sagas/createChannel.js +++ b/app/sagas/createChannel.js @@ -40,18 +40,26 @@ const handleRequest = function* handleRequest({ data }) { broadcast, encrypted } = data; - logEvent(events.CR_CREATE, { + logEvent(events.CT_CREATE, { type, readOnly, broadcast, encrypted }); - sub = yield call(createTeam, data); + const result = yield call(createTeam, data); + sub = { + rid: result?.team?.roomId, + ...result.team, + t: result.team.type ? 'p' : 'c' + }; } else if (data.group) { logEvent(events.SELECTED_USERS_CREATE_GROUP); const result = yield call(createGroupChat); if (result.success) { - ({ room: sub } = result); + sub = { + rid: result.room?._id, + ...result.room + }; } } else { const { @@ -66,33 +74,26 @@ const handleRequest = function* handleRequest({ data }) { broadcast, encrypted }); - sub = yield call(createChannel, data); + const result = yield call(createChannel, data); + sub = { + rid: result?.channel?._id || result?.group?._id, + ...result?.channel, + ...result?.group + }; } - try { const db = database.active; const subCollection = db.get('subscriptions'); yield db.action(async() => { await subCollection.create((s) => { - s._raw = sanitizedRaw({ id: sub.team ? sub.team.roomId : sub.rid }, subCollection.schema); + s._raw = sanitizedRaw({ id: sub.rid }, subCollection.schema); Object.assign(s, sub); }); }); } catch { // do nothing } - - let successParams = {}; - if (data.isTeam) { - successParams = { - ...sub.team, - rid: sub.team.roomId, - t: sub.team.type ? 'p' : 'c' - }; - } else { - successParams = data; - } - yield put(createChannelSuccess(successParams)); + yield put(createChannelSuccess(sub)); } catch (err) { logEvent(events[data.group ? 'SELECTED_USERS_CREATE_GROUP_F' : 'CR_CREATE_F']); yield put(createChannelFailure(err)); diff --git a/app/stacks/InsideStack.js b/app/stacks/InsideStack.js index bda56f0d2..75758960b 100644 --- a/app/stacks/InsideStack.js +++ b/app/stacks/InsideStack.js @@ -71,6 +71,8 @@ import ShareView from '../views/ShareView'; import CreateDiscussionView from '../views/CreateDiscussionView'; import QueueListView from '../ee/omnichannel/views/QueueListView'; +import AddChannelTeamView from '../views/AddChannelTeamView'; +import AddExistingChannelView from '../views/AddExistingChannelView'; // ChatsStackNavigator const ChatsStack = createStackNavigator(); @@ -174,6 +176,16 @@ const ChatsStackNavigator = () => { component={TeamChannelsView} options={TeamChannelsView.navigationOptions} /> + + { component={InviteUsersView} options={InviteUsersView.navigationOptions} /> + + { + const options = { + headerTitle: I18n.t('Add_Channel_to_Team') + }; + + if (isMasterDetail) { + options.headerLeft = () => ; + } + + navigation.setOptions(options); +}; + +const AddChannelTeamView = ({ + navigation, route, isMasterDetail +}) => { + const { teamId, teamChannels } = route.params; + const { theme } = useTheme(); + + useEffect(() => { + setHeader(navigation, isMasterDetail); + }, []); + + return ( + + + + + navigation.navigate('NewMessageStackNavigator', { screen: 'SelectedUsersViewCreateChannel', params: { nextAction: () => navigation.navigate('CreateChannelView', { teamId }) } })} + testID='add-channel-team-view-create-channel' + left={() => } + right={() => } + theme={theme} + /> + + navigation.navigate('AddExistingChannelView', { teamId, teamChannels })} + testID='add-channel-team-view-create-channel' + left={() => } + right={() => } + theme={theme} + /> + + + + ); +}; + +AddChannelTeamView.propTypes = { + route: PropTypes.object, + navigation: PropTypes.object, + isMasterDetail: PropTypes.bool +}; + +const mapStateToProps = state => ({ + isMasterDetail: state.app.isMasterDetail +}); + +export default connect(mapStateToProps)(AddChannelTeamView); diff --git a/app/views/AddExistingChannelView.js b/app/views/AddExistingChannelView.js new file mode 100644 index 000000000..a3acf9e23 --- /dev/null +++ b/app/views/AddExistingChannelView.js @@ -0,0 +1,209 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + View, FlatList +} from 'react-native'; +import { connect } from 'react-redux'; +import { Q } from '@nozbe/watermelondb'; + +import * as List from '../containers/List'; +import database from '../lib/database'; +import RocketChat from '../lib/rocketchat'; +import I18n from '../i18n'; +import log, { events, logEvent } from '../utils/log'; +import SearchBox from '../containers/SearchBox'; +import * as HeaderButton from '../containers/HeaderButton'; +import StatusBar from '../containers/StatusBar'; +import { themes } from '../constants/colors'; +import { withTheme } from '../theme'; +import SafeAreaView from '../containers/SafeAreaView'; +import { animateNextTransition } from '../utils/layoutAnimation'; +import { goRoom } from '../utils/goRoom'; +import Loading from '../containers/Loading'; + +const QUERY_SIZE = 50; + +class AddExistingChannelView extends React.Component { + static propTypes = { + navigation: PropTypes.object, + route: PropTypes.object, + theme: PropTypes.string, + isMasterDetail: PropTypes.bool, + addTeamChannelPermission: PropTypes.array + }; + + constructor(props) { + super(props); + this.init(); + this.teamId = props.route?.params?.teamId; + this.state = { + search: [], + channels: [], + selected: [], + loading: false + }; + this.setHeader(); + } + + setHeader = () => { + const { navigation, isMasterDetail } = this.props; + const { selected } = this.state; + + const options = { + headerTitle: I18n.t('Add_Existing_Channel') + }; + + if (isMasterDetail) { + options.headerLeft = () => ; + } + + options.headerRight = () => selected.length > 0 && ( + + + + ); + + navigation.setOptions(options); + } + + init = async() => { + try { + const { addTeamChannelPermission } = this.props; + const db = database.active; + const channels = await db.collections + .get('subscriptions') + .query( + Q.where('team_id', ''), + Q.where('t', Q.oneOf(['c', 'p'])), + Q.experimentalTake(QUERY_SIZE), + Q.experimentalSortBy('room_updated_at', Q.desc) + ) + .fetch(); + const filteredChannels = channels.filter(async(channel) => { + const permissions = await RocketChat.hasPermission([addTeamChannelPermission], channel.rid); + if (!permissions[0]) { + return; + } + return channel; + }); + this.setState({ channels: filteredChannels }); + } catch (e) { + log(e); + } + } + + onSearchChangeText(text) { + this.search(text); + } + + dismiss = () => { + const { navigation } = this.props; + return navigation.pop(); + } + + search = async(text) => { + const result = await RocketChat.search({ text, filterUsers: false }); + this.setState({ + search: result + }); + } + + submit = async() => { + const { selected } = this.state; + const { isMasterDetail } = this.props; + + this.setState({ loading: true }); + try { + logEvent(events.CT_ADD_ROOM_TO_TEAM); + const result = await RocketChat.addRoomsToTeam({ rooms: selected, teamId: this.teamId }); + if (result.success) { + this.setState({ loading: false }); + goRoom({ item: result, isMasterDetail }); + } + } catch (e) { + logEvent(events.CT_ADD_ROOM_TO_TEAM_F); + this.setState({ loading: false }); + } + } + + renderHeader = () => { + const { theme } = this.props; + return ( + + this.onSearchChangeText(text)} testID='add-existing-channel-view-search' /> + + ); + } + + isChecked = (rid) => { + const { selected } = this.state; + return selected.includes(rid); + } + + toggleChannel = (rid) => { + const { selected } = this.state; + + animateNextTransition(); + if (!this.isChecked(rid)) { + logEvent(events.EXISTING_CHANNEL_ADD_CHANNEL); + this.setState({ selected: [...selected, rid] }, () => this.setHeader()); + } else { + logEvent(events.EXISTING_CHANNEL_REMOVE_CHANNEL); + const filterSelected = selected.filter(el => el !== rid); + this.setState({ selected: filterSelected }, () => this.setHeader()); + } + } + + renderItem = ({ item }) => { + const isChecked = this.isChecked(item.rid); + // TODO: reuse logic inside RoomTypeIcon + const icon = item.t === 'p' && !item.teamId ? 'channel-private' : 'channel-public'; + return ( + this.toggleChannel(item.rid)} + testID='add-existing-channel-view-item' + left={() => } + right={() => (isChecked ? : null)} + /> + + ); + } + + renderList = () => { + const { search, channels } = this.state; + const { theme } = this.props; + return ( + 0 ? search : channels} + extraData={this.state} + keyExtractor={item => item._id} + ListHeaderComponent={this.renderHeader} + renderItem={this.renderItem} + ItemSeparatorComponent={List.Separator} + contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }} + keyboardShouldPersistTaps='always' + /> + ); + } + + render() { + const { loading } = this.state; + + return ( + + + {this.renderList()} + + + ); + } +} + +const mapStateToProps = state => ({ + isMasterDetail: state.app.isMasterDetail, + addTeamChannelPermission: state.permissions['add-team-channel'] +}); + +export default connect(mapStateToProps)(withTheme(AddExistingChannelView)); diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js index 8090ef4fe..54f402c63 100644 --- a/app/views/CreateChannelView.js +++ b/app/views/CreateChannelView.js @@ -83,13 +83,15 @@ class CreateChannelView extends React.Component { id: PropTypes.string, token: PropTypes.string }), - theme: PropTypes.string + theme: PropTypes.string, + teamId: PropTypes.string }; constructor(props) { super(props); const { route } = this.props; const isTeam = route?.params?.isTeam || false; + this.teamId = route?.params?.teamId; this.state = { channelName: '', type: true, @@ -180,7 +182,7 @@ class CreateChannelView extends React.Component { // create channel or team create({ - name: channelName, users, type, readOnly, broadcast, encrypted, isTeam + name: channelName, users, type, readOnly, broadcast, encrypted, isTeam, teamId: this.teamId }); Review.pushPositiveEvent(); diff --git a/app/views/NewMessageView.js b/app/views/NewMessageView.js index ae0628f19..cca5d5842 100644 --- a/app/views/NewMessageView.js +++ b/app/views/NewMessageView.js @@ -60,7 +60,7 @@ class NewMessageView extends React.Component { id: PropTypes.string, token: PropTypes.string }), - createChannel: PropTypes.func, + create: PropTypes.func, maxUsers: PropTypes.number, theme: PropTypes.string, isMasterDetail: PropTypes.bool @@ -124,9 +124,9 @@ class NewMessageView extends React.Component { createGroupChat = () => { logEvent(events.NEW_MSG_CREATE_GROUP_CHAT); - const { createChannel, maxUsers, navigation } = this.props; + const { create, maxUsers, navigation } = this.props; navigation.navigate('SelectedUsersViewCreateChannel', { - nextAction: () => createChannel({ group: true }), + nextAction: () => create({ group: true }), buttonText: I18n.t('Create'), maxUsers }); diff --git a/app/views/TeamChannelsView.js b/app/views/TeamChannelsView.js index 15724ab5c..03398edc9 100644 --- a/app/views/TeamChannelsView.js +++ b/app/views/TeamChannelsView.js @@ -1,11 +1,10 @@ import React from 'react'; -import { Keyboard } from 'react-native'; +import { Keyboard, Alert } from 'react-native'; import PropTypes from 'prop-types'; import { Q } from '@nozbe/watermelondb'; import { withSafeAreaInsets } from 'react-native-safe-area-context'; import { connect } from 'react-redux'; import { FlatList } from 'react-native-gesture-handler'; -import { HeaderBackButton } from '@react-navigation/stack'; import StatusBar from '../containers/StatusBar'; import RoomHeader from '../containers/RoomHeader'; @@ -23,11 +22,14 @@ import RoomItem, { ROW_HEIGHT } from '../presentation/RoomItem'; import RocketChat from '../lib/rocketchat'; import { withDimensions } from '../dimensions'; import { isIOS } from '../utils/deviceInfo'; -import { themes } from '../constants/colors'; import debounce from '../utils/debounce'; import { showErrorAlert } from '../utils/info'; import { goRoom } from '../utils/goRoom'; import I18n from '../i18n'; +import { withActionSheet } from '../containers/ActionSheet'; +import { deleteRoom as deleteRoomAction } from '../actions/room'; +import { CustomIcon } from '../lib/Icons'; +import { themes } from '../constants/colors'; const API_FETCH_COUNT = 25; @@ -47,7 +49,11 @@ class TeamChannelsView extends React.Component { theme: PropTypes.string, useRealName: PropTypes.bool, width: PropTypes.number, - StoreLastMessage: PropTypes.bool + StoreLastMessage: PropTypes.bool, + addTeamChannelPermission: PropTypes.array, + removeTeamChannelPermission: PropTypes.array, + showActionSheet: PropTypes.func, + deleteRoom: PropTypes.func } constructor(props) { @@ -60,9 +66,11 @@ class TeamChannelsView extends React.Component { isSearching: false, searchText: '', search: [], - end: false + end: false, + showCreate: false }; this.loadTeam(); + this.setHeader(); } componentDidMount() { @@ -70,6 +78,9 @@ class TeamChannelsView extends React.Component { } loadTeam = async() => { + const { addTeamChannelPermission } = this.props; + const { loading, data } = this.state; + const db = database.active; try { const subCollection = db.get('subscriptions'); @@ -82,6 +93,15 @@ class TeamChannelsView extends React.Component { if (!this.team) { throw new Error(); } + + const permissions = await RocketChat.hasPermission([addTeamChannelPermission], this.team.rid); + if (permissions[0]) { + this.setState({ showCreate: true }, () => this.setHeader()); + } + + if (loading && data.length) { + this.setState({ loading: false }); + } } catch { const { navigation } = this.props; navigation.pop(); @@ -115,14 +135,11 @@ class TeamChannelsView extends React.Component { loadingMore: false, end: result.rooms.length < API_FETCH_COUNT }; - const rooms = result.rooms.map((room) => { - const record = this.teamChannels?.find(c => c.rid === room._id); - return record ?? room; - }); + if (isSearching) { - newState.search = [...search, ...rooms]; + newState.search = [...search, ...result.rooms]; } else { - newState.data = [...data, ...rooms]; + newState.data = [...data, ...result.rooms]; } this.setState(newState); @@ -135,18 +152,16 @@ class TeamChannelsView extends React.Component { } }, 300) - getHeader = () => { - const { isSearching } = this.state; - const { - navigation, isMasterDetail, insets, theme - } = this.props; + setHeader = () => { + const { isSearching, showCreate, data } = this.state; + const { navigation, isMasterDetail, insets } = this.props; const { team } = this; if (!team) { return; } - const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 1 }); + const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight: 2 }); if (isSearching) { return { @@ -188,27 +203,16 @@ class TeamChannelsView extends React.Component { if (isMasterDetail) { options.headerLeft = () => ; - } else { - options.headerLeft = () => ( - navigation.pop()} - tintColor={themes[theme].headerTintColor} - /> - ); } options.headerRight = () => ( + { showCreate + ? navigation.navigate('AddChannelTeamView', { teamId: this.teamId, teamChannels: data })} /> + : null} ); - return options; - } - - setHeader = () => { - const { navigation } = this.props; - const options = this.getHeader(); navigation.setOptions(options); } @@ -287,6 +291,115 @@ class TeamChannelsView extends React.Component { } }, 1000, true); + options = (item) => { + const { theme } = this.props; + const isAutoJoinChecked = item.teamDefault; + const autoJoinIcon = isAutoJoinChecked ? 'checkbox-checked' : 'checkbox-unchecked'; + const autoJoinIconColor = isAutoJoinChecked ? themes[theme].tintActive : themes[theme].auxiliaryTintColor; + return ([ + { + title: I18n.t('Auto-join'), + icon: item.t === 'p' ? 'channel-private' : 'channel-public', + onPress: () => this.toggleAutoJoin(item), + right: () => + }, + { + title: I18n.t('Remove_from_Team'), + icon: 'close', + danger: true, + onPress: () => this.remove(item) + }, + { + title: I18n.t('Delete'), + icon: 'delete', + danger: true, + onPress: () => this.delete(item) + } + ]); + } + + toggleAutoJoin = async(item) => { + try { + const { data } = this.state; + const result = await RocketChat.updateTeamRoom({ roomId: item._id, isDefault: !item.teamDefault }); + if (result.success) { + const newData = data.map((i) => { + if (i._id === item._id) { + i.teamDefault = !i.teamDefault; + } + return i; + }); + this.setState({ data: newData }); + } + } catch (e) { + log(e); + } + } + + remove = (item) => { + Alert.alert( + I18n.t('Confirmation'), + I18n.t('Delete_Team_Room_Warning'), + [ + { + text: I18n.t('Cancel'), + style: 'cancel' + }, + { + text: I18n.t('Yes_action_it', { action: I18n.t('remove') }), + style: 'destructive', + onPress: () => this.removeRoom(item) + } + ], + { cancelable: false } + ); + } + + removeRoom = async(item) => { + try { + const { data } = this.state; + const result = await RocketChat.removeTeamRoom({ roomId: item._id, teamId: this.team.teamId }); + if (result.success) { + const newData = data.filter(room => result.room._id !== room._id); + this.setState({ data: newData }); + } + } catch (e) { + log(e); + } + } + + delete = (item) => { + const { deleteRoom } = this.props; + + Alert.alert( + I18n.t('Are_you_sure_question_mark'), + I18n.t('Delete_Room_Warning'), + [ + { + text: I18n.t('Cancel'), + style: 'cancel' + }, + { + text: I18n.t('Yes_action_it', { action: I18n.t('delete') }), + style: 'destructive', + onPress: () => deleteRoom(item._id, item.t) + } + ], + { cancelable: false } + ); + } + + showChannelActions = async(item) => { + logEvent(events.ROOM_SHOW_BOX_ACTIONS); + const { showActionSheet, removeTeamChannelPermission } = this.props; + + const permissions = await RocketChat.hasPermission([removeTeamChannelPermission], this.team.rid); + if (!permissions[0]) { + return; + } + showActionSheet({ options: this.options(item) }); + } + renderItem = ({ item }) => { const { StoreLastMessage, @@ -302,10 +415,12 @@ class TeamChannelsView extends React.Component { showLastMessage={StoreLastMessage} onPress={this.onPressItem} width={width} + onLongPress={this.showChannelActions} useRealName={useRealName} getRoomTitle={this.getRoomTitle} getRoomAvatar={this.getRoomAvatar} swipeEnabled={false} + autoJoin={item.teamDefault} /> ); }; @@ -365,7 +480,13 @@ const mapStateToProps = state => ({ user: getUserSelector(state), useRealName: state.settings.UI_Use_Real_Name, isMasterDetail: state.app.isMasterDetail, - StoreLastMessage: state.settings.Store_Last_Message + StoreLastMessage: state.settings.Store_Last_Message, + addTeamChannelPermission: state.permissions['add-team-channel'], + removeTeamChannelPermission: state.permissions['remove-team-channel'] }); -export default connect(mapStateToProps)(withDimensions(withSafeAreaInsets(withTheme(TeamChannelsView)))); +const mapDispatchToProps = dispatch => ({ + deleteRoom: (rid, t) => dispatch(deleteRoomAction(rid, t)) +}); + +export default connect(mapStateToProps, mapDispatchToProps)(withDimensions(withSafeAreaInsets(withTheme(withActionSheet(TeamChannelsView))))); diff --git a/storybook/stories/RoomItem.js b/storybook/stories/RoomItem.js index d39e92082..025fbf7f5 100644 --- a/storybook/stories/RoomItem.js +++ b/storybook/stories/RoomItem.js @@ -3,7 +3,6 @@ import React from 'react'; import { ScrollView, Dimensions } from 'react-native'; import { storiesOf } from '@storybook/react-native'; import { Provider } from 'react-redux'; -// import moment from 'moment'; import { themes } from '../../app/constants/colors'; import RoomItemComponent from '../../app/presentation/RoomItem/RoomItem'; @@ -94,6 +93,15 @@ stories.add('Alerts', () => ( )); +stories.add('Tag', () => ( + <> + + + + + +)); + stories.add('Last Message', () => ( <> Date: Thu, 20 May 2021 09:50:42 -0400 Subject: [PATCH 08/44] [FIX] E2E Tests not working because of ES6 import (#3147) * Update ITeam.js * Minor tweak --- e2e/helpers/data_setup.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/e2e/helpers/data_setup.js b/e2e/helpers/data_setup.js index ce1d5083e..7e744f54a 100644 --- a/e2e/helpers/data_setup.js +++ b/e2e/helpers/data_setup.js @@ -1,6 +1,10 @@ const axios = require('axios').default; const data = require('../data'); -const { TEAM_TYPE } = require('../../app/definition/ITeam'); + +const TEAM_TYPE = { + PUBLIC: 0, + PRIVATE: 1 +}; let server = data.server From 62336c6d3ac5e3f756e21d8a67052da5c2be74a4 Mon Sep 17 00:00:00 2001 From: Gerzon Z Date: Tue, 25 May 2021 14:04:05 -0400 Subject: [PATCH 09/44] [NEW] Leave Teams (#3116) * Added Create Team * Added actionTypes, actions, ENG strings for Teams and updated NewMessageView * Added createTeam sagas, createTeam reducer, new Team string and update CreateChannelView * Remove unnecessary actionTypes, reducers and sagas, e2e tests and navigation to team view * Minor tweaks * Show TeamChannelsView only if joined the team * Minor tweak * Added AddChannelTeamView * Added permissions, translations strings for teams, deleteTeamRoom and addTeamRooms, AddExistingChannelView, updated CreateChannelView, TeamChannelsView * Refactor touch component and update removeRoom and deleteRoom methods * Minor tweaks * Minor tweaks for removing channels and addExistingChannelView * Added missing events and fixed channels list * Minor tweaks for refactored touch component * Added SelectListView and logic for leaving team * Minor tweak * Minor tweak * Minor tweaks * Remove unnecesary changes, update TeamChannelsView, AddExistingChannelView, AddChannelTeamView, createChannel, goRoom and Touchable * Remove unnecesary prop * Add screens to ModalStack, events, autoJoin, update createChannel, addRoomsToTeam and Touchable * Minor tweak * Update loadMessagesForRoom.js * Updated schema, tag component, touch, AddChannelTeamView, AddExistingChannelView, ActionSheet Item * Fix unnecessary changes * Add i18n, update createChannel, AddExistingChannelTeamView, AddChannelTeamView, RightButton and TeamChannelsView * Updated styles, added tag story * Minor tweak * Minor tweaks * Auto-join tweak * Minor tweaks * Minor tweak on search * Minor refactor to ListItem, add SelectListView to ModalStack, update handleLeaveTeam * Minor tweaks * Update SelectListView * Update handleLeaveTeam, remove unnecessary method, add story * Minor tweak * Minor visual tweaks * Updated SelectListView, RoomActionsView, leaveTeam method and string translations * Update SelectListVIew * Minor tweak * Update SelectListView * Minor tweak * Fix for List.Item subtitles being pushed down by title's flex * Minor tweaks * Update RoomActionsView * Use showConfirmationAlert and showErrorAlert * Lint Co-authored-by: Diego Mello --- .../__snapshots__/Storyshots.test.js.snap | 2420 ++++++++++++----- app/containers/List/ListIcon.js | 3 +- app/containers/List/ListItem.js | 23 +- app/containers/List/constants.js | 1 + app/i18n/locales/en.json | 11 +- app/lib/rocketchat.js | 4 + app/stacks/InsideStack.js | 6 + app/stacks/MasterDetailStack/index.js | 6 + app/views/RoomActionsView/index.js | 83 +- app/views/SelectListView.js | 146 + storybook/stories/List.js | 14 + 11 files changed, 1974 insertions(+), 743 deletions(-) create mode 100644 app/views/SelectListView.js diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 68b171855..36433392c 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -3993,6 +3993,536 @@ Array [ ] `; +exports[`Storyshots List alert 1`] = ` + + + + + + + + + Chats + + +  + + + + + + + + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + +  + + + + + + + + + + + + Chats + + +  + + + + + + +  + + + + + + + + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + +  + + + + + + +  + + + + + + + + +`; + exports[`Storyshots List header 1`] = ` - - Press me - + + Press me + + - - I'm disabled - + + I'm disabled + + - - Chats - + + Chats + + @@ -4440,25 +5000,35 @@ exports[`Storyshots List title and subtitle 1`] = ` } } > - - Chats - + + Chats + + - - Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries - + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + - - 0 - + + 0 + + @@ -4796,25 +5386,35 @@ exports[`Storyshots List with FlatList 1`] = ` } } > - - 1 - + + 1 + + @@ -4868,25 +5468,35 @@ exports[`Storyshots List with FlatList 1`] = ` } } > - - 2 - + + 2 + + @@ -4940,25 +5550,35 @@ exports[`Storyshots List with FlatList 1`] = ` } } > - - 3 - + + 3 + + @@ -5012,25 +5632,35 @@ exports[`Storyshots List with FlatList 1`] = ` } } > - - 4 - + + 4 + + @@ -5084,25 +5714,35 @@ exports[`Storyshots List with FlatList 1`] = ` } } > - - 5 - + + 5 + + @@ -5156,25 +5796,35 @@ exports[`Storyshots List with FlatList 1`] = ` } } > - - 6 - + + 6 + + @@ -5228,25 +5878,35 @@ exports[`Storyshots List with FlatList 1`] = ` } } > - - 7 - + + 7 + + @@ -5300,25 +5960,35 @@ exports[`Storyshots List with FlatList 1`] = ` } } > - - 8 - + + 8 + + @@ -5372,25 +6042,35 @@ exports[`Storyshots List with FlatList 1`] = ` } } > - - 9 - + + 9 + + @@ -5586,25 +6266,35 @@ exports[`Storyshots List with bigger font 1`] = ` } } > - - Chats - + + Chats + + - - Chats - + + Chats + + - - Chats - + + Chats + + - - Chats - + + Chats + + - - Chats - + + Chats + + - - Chats - + + Chats + + - - Chats - + + Chats + + - - Chats - + + Chats + + - - Chats - + + Chats + + @@ -7217,25 +7987,35 @@ exports[`Storyshots List with custom colors 1`] = ` } } > - - Press me! - + + Press me! + + - - Chats - + + Chats + + - - Chats - + + Chats + + - - Chats - + + Chats + + - - Chats - + + Chats + + - - Icon Left - + + Icon Left + + @@ -8253,25 +9083,35 @@ exports[`Storyshots List with icon 1`] = ` } } > - - Icon Right - + + Icon Right + + - - Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries - + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + - - Show Action Indicator - + + Show Action Indicator + + - - Section Item - + + Section Item + + @@ -8760,25 +9630,35 @@ exports[`Storyshots List with section and info 1`] = ` } } > - - Section Item - + + Section Item + + @@ -8848,25 +9728,35 @@ exports[`Storyshots List with section and info 1`] = ` } } > - - Section Item - + + Section Item + + @@ -8915,25 +9805,35 @@ exports[`Storyshots List with section and info 1`] = ` } } > - - Section Item - + + Section Item + + @@ -9031,25 +9931,35 @@ exports[`Storyshots List with section and info 1`] = ` } } > - - Section Item - + + Section Item + + @@ -9098,25 +10008,35 @@ exports[`Storyshots List with section and info 1`] = ` } } > - - Section Item - + + Section Item + + @@ -9241,25 +10161,35 @@ exports[`Storyshots List with section and info 1`] = ` } } > - - Section Item - + + Section Item + + @@ -9308,25 +10238,35 @@ exports[`Storyshots List with section and info 1`] = ` } } > - - Section Item - + + Section Item + + @@ -9525,25 +10465,35 @@ exports[`Storyshots List with small font 1`] = ` } } > - - Chats - + + Chats + + - - Chats - + + Chats + + - - Chats - + + Chats + + - - Chats - + + Chats + + )); diff --git a/app/containers/List/ListItem.js b/app/containers/List/ListItem.js index 6ce7bb6fc..aa3ecbdf0 100644 --- a/app/containers/List/ListItem.js +++ b/app/containers/List/ListItem.js @@ -10,8 +10,9 @@ import sharedStyles from '../../views/Styles'; import { withTheme } from '../../theme'; import I18n from '../../i18n'; import { Icon } from '.'; -import { BASE_HEIGHT, PADDING_HORIZONTAL } from './constants'; +import { BASE_HEIGHT, ICON_SIZE, PADDING_HORIZONTAL } from './constants'; import { withDimensions } from '../../dimensions'; +import { CustomIcon } from '../../lib/Icons'; const styles = StyleSheet.create({ container: { @@ -34,7 +35,15 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: 'center' }, + textAlertContainer: { + flexDirection: 'row', + alignItems: 'center' + }, + alertIcon: { + paddingLeft: 4 + }, title: { + flexShrink: 1, fontSize: 16, ...sharedStyles.textRegular }, @@ -50,7 +59,7 @@ const styles = StyleSheet.create({ }); const Content = React.memo(({ - title, subtitle, disabled, testID, left, right, color, theme, translateTitle, translateSubtitle, showActionIndicator, fontScale + title, subtitle, disabled, testID, left, right, color, theme, translateTitle, translateSubtitle, showActionIndicator, fontScale, alert }) => ( {left @@ -61,7 +70,12 @@ const Content = React.memo(({ ) : null} - {translateTitle ? I18n.t(title) : title} + + {translateTitle ? I18n.t(title) : title} + {alert ? ( + + ) : null} + {subtitle ? {translateSubtitle ? I18n.t(subtitle) : subtitle} : null @@ -123,7 +137,8 @@ Content.propTypes = { translateTitle: PropTypes.bool, translateSubtitle: PropTypes.bool, showActionIndicator: PropTypes.bool, - fontScale: PropTypes.number + fontScale: PropTypes.number, + alert: PropTypes.bool }; Content.defaultProps = { diff --git a/app/containers/List/constants.js b/app/containers/List/constants.js index b69a04f95..8144096d3 100644 --- a/app/containers/List/constants.js +++ b/app/containers/List/constants.js @@ -1,2 +1,3 @@ export const PADDING_HORIZONTAL = 12; export const BASE_HEIGHT = 46; +export const ICON_SIZE = 20; diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 8cc6cb278..2e8391956 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -290,6 +290,7 @@ "last_message": "last message", "Leave_channel": "Leave channel", "leaving_room": "leaving room", + "Leave": "Leave", "leave": "leave", "Legal": "Legal", "Light": "Light", @@ -726,5 +727,13 @@ "Auto-join": "Auto-join", "Delete_Team_Room_Warning": "Woud you like to remove this channel from the team? The channel will be moved back to the workspace", "Confirmation": "Confirmation", - "invalid-room": "Invalid room" + "invalid-room": "Invalid room", + "You_are_leaving_the_team": "You are leaving the team '{{team}}'", + "Leave_Team": "Leave Team", + "Select_Team_Channels": "Select the Team's channels you would like to leave.", + "Cannot_leave": "Cannot leave", + "Last_owner_team_room": "You are the last owner of this channel. Once you leave the team, the channel will be kept inside the team but you will be managing it from outside.", + "last-owner-can-not-be-removed": "Last owner cannot be removed", + "leaving_team": "leaving team", + "member-does-not-exist": "Member does not exist" } diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 57c6e0c71..3891d88cb 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -769,6 +769,10 @@ const RocketChat = { // RC 3.13.0 return this.post('teams.removeRoom', { roomId, teamId }); }, + leaveTeam({ teamName, rooms }) { + // RC 3.13.0 + return this.post('teams.leave', { teamName, rooms }); + }, updateTeamRoom({ roomId, isDefault }) { // RC 3.13.0 return this.post('teams.updateRoom', { roomId, isDefault }); diff --git a/app/stacks/InsideStack.js b/app/stacks/InsideStack.js index 75758960b..18517368c 100644 --- a/app/stacks/InsideStack.js +++ b/app/stacks/InsideStack.js @@ -73,6 +73,7 @@ import CreateDiscussionView from '../views/CreateDiscussionView'; import QueueListView from '../ee/omnichannel/views/QueueListView'; import AddChannelTeamView from '../views/AddChannelTeamView'; import AddExistingChannelView from '../views/AddExistingChannelView'; +import SelectListView from '../views/SelectListView'; // ChatsStackNavigator const ChatsStack = createStackNavigator(); @@ -93,6 +94,11 @@ const ChatsStackNavigator = () => { component={RoomActionsView} options={RoomActionsView.navigationOptions} /> + { component={RoomInfoView} options={RoomInfoView.navigationOptions} /> + leaveRoom(room.rid, room.t) + showConfirmationAlert({ + message: I18n.t('Are_you_sure_you_want_to_leave_the_room', { room: RocketChat.getRoomTitle(room) }), + confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }), + onPress: () => leaveRoom(room.rid, room.t) + }); + } + + handleLeaveTeam = async(selected) => { + try { + const { room } = this.state; + const { navigation, isMasterDetail } = this.props; + const result = await RocketChat.leaveTeam({ teamName: room.name, ...(selected && { rooms: selected }) }); + + if (result.success) { + if (isMasterDetail) { + navigation.navigate('DrawerNavigator'); + } else { + navigation.navigate('RoomsListView'); } - ] - ); + } + } catch (e) { + log(e); + showErrorAlert( + e.data.error + ? I18n.t(e.data.error) + : I18n.t('There_was_an_error_while_action', { action: I18n.t('leaving_team') }), + I18n.t('Cannot_leave') + ); + } + } + + leaveTeam = async() => { + const { room } = this.state; + const { navigation } = this.props; + + try { + const db = database.active; + const subCollection = db.get('subscriptions'); + const teamChannels = await subCollection.query( + Q.where('team_id', room.teamId), + Q.where('team_main', null) + ); + + if (teamChannels.length) { + navigation.navigate('SelectListView', { + title: 'Leave_Team', + data: teamChannels, + infoText: 'Select_Team_Channels', + nextAction: data => this.handleLeaveTeam(data), + showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_leave')) + }); + } else { + showConfirmationAlert({ + message: I18n.t('You_are_leaving_the_team', { team: RocketChat.getRoomTitle(room) }), + confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }), + onPress: () => this.handleLeaveTeam() + }); + } + } catch (e) { + log(e); + } } renderRoomInfo = () => { @@ -568,9 +616,9 @@ class RoomActionsView extends React.Component { this.onPressTouchable({ - event: this.leaveChannel + event: room.teamMain ? this.leaveTeam : this.leaveChannel })} testID='room-actions-leave-channel' left={() => } @@ -880,6 +928,7 @@ const mapStateToProps = state => ({ jitsiEnabled: state.settings.Jitsi_Enabled || false, encryptionEnabled: state.encryption.enabled, serverVersion: state.server.version, + isMasterDetail: state.app.isMasterDetail, addUserToJoinedRoomPermission: state.permissions['add-user-to-joined-room'], addUserToAnyCRoomPermission: state.permissions['add-user-to-any-c-room'], addUserToAnyPRoomPermission: state.permissions['add-user-to-any-p-room'], diff --git a/app/views/SelectListView.js b/app/views/SelectListView.js new file mode 100644 index 000000000..9c886da80 --- /dev/null +++ b/app/views/SelectListView.js @@ -0,0 +1,146 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + View, StyleSheet, FlatList, Text +} from 'react-native'; +import { connect } from 'react-redux'; + +import * as List from '../containers/List'; +import sharedStyles from './Styles'; +import I18n from '../i18n'; +import * as HeaderButton from '../containers/HeaderButton'; +import StatusBar from '../containers/StatusBar'; +import { themes } from '../constants/colors'; +import { withTheme } from '../theme'; +import SafeAreaView from '../containers/SafeAreaView'; +import { animateNextTransition } from '../utils/layoutAnimation'; +import Loading from '../containers/Loading'; + +const styles = StyleSheet.create({ + buttonText: { + fontSize: 16, + margin: 16, + ...sharedStyles.textRegular + } +}); + +class SelectListView extends React.Component { + static propTypes = { + navigation: PropTypes.object, + route: PropTypes.object, + theme: PropTypes.string, + isMasterDetail: PropTypes.bool + }; + + constructor(props) { + super(props); + const data = props.route?.params?.data; + this.title = props.route?.params?.title; + this.infoText = props.route?.params?.infoText; + this.nextAction = props.route?.params?.nextAction; + this.showAlert = props.route?.params?.showAlert; + this.state = { + data, + selected: [], + loading: false + }; + this.setHeader(); + } + + setHeader = () => { + const { navigation, isMasterDetail } = this.props; + const { selected } = this.state; + + const options = { + headerTitle: I18n.t(this.title) + }; + + if (isMasterDetail) { + options.headerLeft = () => ; + } + + options.headerRight = () => ( + + this.nextAction(selected)} testID='select-list-view-submit' /> + + ); + + navigation.setOptions(options); + } + + renderInfoText = () => { + const { theme } = this.props; + return ( + + {I18n.t(this.infoText)} + + ); + } + + isChecked = (rid) => { + const { selected } = this.state; + return selected.includes(rid); + } + + toggleItem = (rid) => { + const { selected } = this.state; + + animateNextTransition(); + if (!this.isChecked(rid)) { + this.setState({ selected: [...selected, rid] }, () => this.setHeader()); + } else { + const filterSelected = selected.filter(el => el !== rid); + this.setState({ selected: filterSelected }, () => this.setHeader()); + } + } + + renderItem = ({ item }) => { + const { theme } = this.props; + const alert = item.roles.length; + + const icon = item.t === 'p' ? 'channel-private' : 'channel-public'; + const checked = this.isChecked(item.rid, item.roles) ? 'check' : null; + + return ( + <> + + (alert ? this.showAlert() : this.toggleItem(item.rid))} + alert={alert} + left={() => } + right={() => (checked ? : null)} + /> + + ); + } + + render() { + const { loading, data } = this.state; + const { theme } = this.props; + + return ( + + + item.rid} + renderItem={this.renderItem} + ListHeaderComponent={this.renderInfoText} + contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }} + keyboardShouldPersistTaps='always' + /> + + + ); + } +} + +const mapStateToProps = state => ({ + isMasterDetail: state.app.isMasterDetail +}); + +export default connect(mapStateToProps)(withTheme(SelectListView)); diff --git a/storybook/stories/List.js b/storybook/stories/List.js index 632018054..b445a1972 100644 --- a/storybook/stories/List.js +++ b/storybook/stories/List.js @@ -23,6 +23,20 @@ stories.add('title and subtitle', () => ( )); +stories.add('alert', () => ( + + + + + + + } alert /> + + } alert /> + + +)); + stories.add('pressable', () => ( From 3ef4ef531726c05dfef0b86563f8cbaf5f869eca Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 26 May 2021 14:24:54 -0300 Subject: [PATCH 10/44] [NEW] Jump to message (#3099) * Scrolling * Add loadMore button at the end of loadMessagesForRoom * Delete dummy item on tap * Only insert loadMore dummy if there's more data * load surrounding messages * fixes and load next * First dummy and dummy-next * Save load next messages * Check if message exists before fetching surroundings * Refactoring List * Jumping to message :) * Showing blocking loader while scrolling/fetching message * Check if message exists on local db before inserting dummy * Delete dummies automatically when the message sent to updateMessages again * Minor cleanup * Fix scroll * Highlight message * Jump to bottom * Load more on scroll * Adding stories to LoadMore * Refactoring * Add loading indicator to LoadMore * Small refactor * Add LoadMore to threads * getMoreMessages * chat.getThreadMessages -> getThreadMessages * Start jumping to threads * Add jumpToMessageId on RoomView * Nav to correct channel * Fix PK issue on thread_messages * Disable jump to thread from another room * Fix nav to thread params * Add navToRoom * Refactor styles * Test notch * Fix Android border * Fix thread message on title * Fix NavBottomFAB on threads * Minor cleanup * Workaround for readThreads being called too often * Lint * Update tests * Jump from search * Go to threads from search * Remove getItemLayout and rely on viewable items * Fix load older * stash working * Fix infinite loading * Lower itemVisiblePercentThreshhold to 10, so very long messages behave as viewable * Add generateLoadMoreId util * Minor cleanup * Jump to message from notification/deep linking * Add getMessageInfo * Nav to threads from other rooms * getThreadName * Unnecessary logic * getRoomInfo * Colocate getMessageInfo closer to RoomView * Minor cleanup * Remove search from RoomActionsView * Minor fix for search on not joined public channels * Jump to any link * Fix tablets * Jump to message from MessagesView and other bug fixes * Fix issue on Urls * Adds race condition to cancel jump to message if it's stuck or after 5 seconds * Jump from message search quote * lint * Stop onPress * Small refactor on load methods * Minor fixes for loadThreadMessages * Minor typo * LoadMore i18n * Minor cleanup --- .../__snapshots__/Storyshots.test.js.snap | 3791 +++++++++++++++++ app/constants/messageTypeLoad.js | 5 + app/containers/markdown/Link.js | 8 +- app/containers/markdown/index.js | 6 +- app/containers/message/Content.js | 3 +- app/containers/message/Message.js | 6 +- app/containers/message/RepliedThread.js | 42 +- app/containers/message/Reply.js | 5 +- app/containers/message/Urls.js | 2 +- app/containers/message/index.js | 82 +- app/i18n/locales/en.json | 5 +- app/lib/database/model/Message.js | 4 +- app/lib/database/model/Subscription.js | 4 +- app/lib/database/model/Thread.js | 4 +- app/lib/database/model/ThreadMessage.js | 4 +- app/lib/database/services/Message.js | 15 + app/lib/database/services/Subscription.js | 15 + app/lib/database/services/Thread.js | 15 + app/lib/database/services/ThreadMessage.js | 15 + app/lib/methods/getRoomInfo.js | 29 + app/lib/methods/getSingleMessage.js | 15 + app/lib/methods/getThreadName.js | 49 + app/lib/methods/loadMessagesForRoom.js | 26 +- app/lib/methods/loadNextMessages.js | 42 + app/lib/methods/loadSurroundingMessages.js | 65 + app/lib/methods/loadThreadMessages.js | 18 +- app/lib/methods/updateMessages.js | 26 +- app/lib/rocketchat.js | 7 +- app/lib/utils.js | 2 + app/notifications/push/index.js | 3 +- app/sagas/deepLinking.js | 7 +- app/utils/goRoom.js | 1 - app/views/MessagesView/index.js | 37 +- app/views/RoomActionsView/index.js | 20 +- app/views/RoomView/List/List.js | 42 + app/views/RoomView/List/NavBottomFAB.js | 75 + app/views/RoomView/{List.js => List/index.js} | 222 +- .../RoomView/LoadMore/LoadMore.stories.js | 62 + app/views/RoomView/LoadMore/index.js | 76 + app/views/RoomView/RightButtons.js | 4 +- app/views/RoomView/index.js | 324 +- app/views/RoomView/services/getMessageInfo.js | 41 + app/views/RoomView/services/getMessages.js | 10 + .../RoomView/services/getMoreMessages.js | 19 + .../RoomView/services/getThreadMessages.js | 6 + app/views/RoomView/services/index.js | 13 + app/views/RoomView/services/readMessages.js | 5 + app/views/RoomView/styles.js | 6 - app/views/SearchMessagesView/index.js | 41 +- storybook/stories/Message.js | 12 +- storybook/stories/index.js | 1 + 51 files changed, 4972 insertions(+), 365 deletions(-) create mode 100644 app/constants/messageTypeLoad.js create mode 100644 app/lib/database/services/Message.js create mode 100644 app/lib/database/services/Subscription.js create mode 100644 app/lib/database/services/Thread.js create mode 100644 app/lib/database/services/ThreadMessage.js create mode 100644 app/lib/methods/getRoomInfo.js create mode 100644 app/lib/methods/getSingleMessage.js create mode 100644 app/lib/methods/getThreadName.js create mode 100644 app/lib/methods/loadNextMessages.js create mode 100644 app/lib/methods/loadSurroundingMessages.js create mode 100644 app/views/RoomView/List/List.js create mode 100644 app/views/RoomView/List/NavBottomFAB.js rename app/views/RoomView/{List.js => List/index.js} (59%) create mode 100644 app/views/RoomView/LoadMore/LoadMore.stories.js create mode 100644 app/views/RoomView/LoadMore/index.js create mode 100644 app/views/RoomView/services/getMessageInfo.js create mode 100644 app/views/RoomView/services/getMessages.js create mode 100644 app/views/RoomView/services/getMoreMessages.js create mode 100644 app/views/RoomView/services/getThreadMessages.js create mode 100644 app/views/RoomView/services/index.js create mode 100644 app/views/RoomView/services/readMessages.js diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 36433392c..eec5b535a 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -11185,6 +11185,3689 @@ exports[`Storyshots List with small font 1`] = ` `; +exports[`Storyshots LoadMore basic 1`] = ` +Array [ + + Load More + , + + Load More + , + + Load Older + , + + Load Newer + , +] +`; + +exports[`Storyshots LoadMore black theme 1`] = ` + + + + Load Older + + + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + Hey! + + + + + + + + + + + + + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + + + + + + + + + + + + + + Older message + + + + + + + + + + Load Newer + + + Load More + + + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + + + + + + + + + + + + + + This is the third message + + + + + + + + + + + + + + + + + This is the second message + + + + + + + + + + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + This is the first message + + + + + + + + + + +`; + +exports[`Storyshots LoadMore dark theme 1`] = ` + + + + Load Older + + + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + Hey! + + + + + + + + + + + + + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + + + + + + + + + + + + + + Older message + + + + + + + + + + Load Newer + + + Load More + + + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + + + + + + + + + + + + + + This is the third message + + + + + + + + + + + + + + + + + This is the second message + + + + + + + + + + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + This is the first message + + + + + + + + + + +`; + +exports[`Storyshots LoadMore light theme 1`] = ` + + + + Load Older + + + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + Hey! + + + + + + + + + + + + + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + + + + + + + + + + + + + + Older message + + + + + + + + + + Load Newer + + + Load More + + + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries + + + + + + + + + + + + + + + + + This is the third message + + + + + + + + + + + + + + + + + This is the second message + + + + + + + + + + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + This is the first message + + + + + + + + + + +`; + exports[`Storyshots Markdown Block quote 1`] = ` { const handlePress = () => { if (!link) { return; } - openLink(link, theme); + onLinkPress(link); }; const childLength = React.Children.toArray(children).filter(o => o).length; @@ -40,7 +39,8 @@ const Link = React.memo(({ Link.propTypes = { children: PropTypes.node, link: PropTypes.string, - theme: PropTypes.string + theme: PropTypes.string, + onLinkPress: PropTypes.func }; export default Link; diff --git a/app/containers/markdown/index.js b/app/containers/markdown/index.js index dfbae1841..bc2fdba73 100644 --- a/app/containers/markdown/index.js +++ b/app/containers/markdown/index.js @@ -82,7 +82,8 @@ class Markdown extends PureComponent { preview: PropTypes.bool, theme: PropTypes.string, testID: PropTypes.string, - style: PropTypes.array + style: PropTypes.array, + onLinkPress: PropTypes.func }; constructor(props) { @@ -218,11 +219,12 @@ class Markdown extends PureComponent { }; renderLink = ({ children, href }) => { - const { theme } = this.props; + const { theme, onLinkPress } = this.props; return ( {children} diff --git a/app/containers/message/Content.js b/app/containers/message/Content.js index 2f29bf4ac..90af48acd 100644 --- a/app/containers/message/Content.js +++ b/app/containers/message/Content.js @@ -45,7 +45,7 @@ const Content = React.memo((props) => { } else if (props.isEncrypted) { content = {I18n.t('Encrypted_message')}; } else { - const { baseUrl, user } = useContext(MessageContext); + const { baseUrl, user, onLinkPress } = useContext(MessageContext); content = ( { tmid={props.tmid} useRealName={props.useRealName} theme={props.theme} + onLinkPress={onLinkPress} /> ); } diff --git a/app/containers/message/Message.js b/app/containers/message/Message.js index 104244740..4bc03c000 100644 --- a/app/containers/message/Message.js +++ b/app/containers/message/Message.js @@ -19,6 +19,7 @@ import Discussion from './Discussion'; import Content from './Content'; import ReadReceipt from './ReadReceipt'; import CallButton from './CallButton'; +import { themes } from '../../constants/colors'; const MessageInner = React.memo((props) => { if (props.type === 'discussion-created') { @@ -120,6 +121,7 @@ const MessageTouchable = React.memo((props) => { onLongPress={onLongPress} onPress={onPress} disabled={(props.isInfo && !props.isThreadReply) || props.archived || props.isTemp} + style={{ backgroundColor: props.highlighted ? themes[props.theme].headerBackground : null }} > @@ -134,7 +136,9 @@ MessageTouchable.propTypes = { isInfo: PropTypes.bool, isThreadReply: PropTypes.bool, isTemp: PropTypes.bool, - archived: PropTypes.bool + archived: PropTypes.bool, + highlighted: PropTypes.bool, + theme: PropTypes.string }; Message.propTypes = { diff --git a/app/containers/message/RepliedThread.js b/app/containers/message/RepliedThread.js index 733315485..46be5b1f6 100644 --- a/app/containers/message/RepliedThread.js +++ b/app/containers/message/RepliedThread.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { memo, useEffect, useState } from 'react'; import { View } from 'react-native'; import PropTypes from 'prop-types'; @@ -8,24 +8,29 @@ import { themes } from '../../constants/colors'; import I18n from '../../i18n'; import Markdown from '../markdown'; -const RepliedThread = React.memo(({ +const RepliedThread = memo(({ tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted, theme }) => { if (!tmid || !isHeader) { return null; } - if (!tmsg) { - fetchThreadName(tmid, id); + const [msg, setMsg] = useState(isEncrypted ? I18n.t('Encrypted_message') : tmsg); + const fetch = async() => { + const threadName = await fetchThreadName(tmid, id); + setMsg(threadName); + }; + + useEffect(() => { + if (!msg) { + fetch(); + } + }, []); + + if (!msg) { return null; } - let msg = tmsg; - - if (isEncrypted) { - msg = I18n.t('Encrypted_message'); - } - return ( @@ -45,23 +50,6 @@ const RepliedThread = React.memo(({ ); -}, (prevProps, nextProps) => { - if (prevProps.tmid !== nextProps.tmid) { - return false; - } - if (prevProps.tmsg !== nextProps.tmsg) { - return false; - } - if (prevProps.isEncrypted !== nextProps.isEncrypted) { - return false; - } - if (prevProps.isHeader !== nextProps.isHeader) { - return false; - } - if (prevProps.theme !== nextProps.theme) { - return false; - } - return true; }); RepliedThread.propTypes = { diff --git a/app/containers/message/Reply.js b/app/containers/message/Reply.js index 4acecbc42..5dcf0447f 100644 --- a/app/containers/message/Reply.js +++ b/app/containers/message/Reply.js @@ -142,10 +142,13 @@ const Reply = React.memo(({ if (!attachment) { return null; } - const { baseUrl, user } = useContext(MessageContext); + const { baseUrl, user, jumpToMessage } = useContext(MessageContext); const onPress = () => { let url = attachment.title_link || attachment.author_link; + if (attachment.message_link) { + return jumpToMessage(attachment.message_link); + } if (!url) { return; } diff --git a/app/containers/message/Urls.js b/app/containers/message/Urls.js index 742b2f478..b82d029af 100644 --- a/app/containers/message/Urls.js +++ b/app/containers/message/Urls.js @@ -80,7 +80,7 @@ const UrlContent = React.memo(({ title, description, theme }) => ( }); const Url = React.memo(({ url, index, theme }) => { - if (!url) { + if (!url || url?.ignoreParse) { return null; } diff --git a/app/containers/message/index.js b/app/containers/message/index.js index b4339ff0e..467c634a6 100644 --- a/app/containers/message/index.js +++ b/app/containers/message/index.js @@ -9,6 +9,7 @@ import { SYSTEM_MESSAGES, getMessageTranslation } from './utils'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants'; import messagesStatus from '../../constants/messagesStatus'; import { withTheme } from '../../theme'; +import openLink from '../../utils/openLink'; class MessageContainer extends React.Component { static propTypes = { @@ -33,6 +34,7 @@ class MessageContainer extends React.Component { autoTranslateLanguage: PropTypes.string, status: PropTypes.number, isIgnored: PropTypes.bool, + highlighted: PropTypes.bool, getCustomEmoji: PropTypes.func, onLongPress: PropTypes.func, onReactionPress: PropTypes.func, @@ -50,7 +52,9 @@ class MessageContainer extends React.Component { blockAction: PropTypes.func, theme: PropTypes.string, threadBadgeColor: PropTypes.string, - toggleFollowThread: PropTypes.func + toggleFollowThread: PropTypes.func, + jumpToMessage: PropTypes.func, + onPress: PropTypes.func } static defaultProps = { @@ -89,10 +93,15 @@ class MessageContainer extends React.Component { shouldComponentUpdate(nextProps, nextState) { const { isManualUnignored } = this.state; - const { theme, threadBadgeColor, isIgnored } = this.props; + const { + theme, threadBadgeColor, isIgnored, highlighted + } = this.props; if (nextProps.theme !== theme) { return true; } + if (nextProps.highlighted !== highlighted) { + return true; + } if (nextProps.threadBadgeColor !== threadBadgeColor) { return true; } @@ -112,10 +121,15 @@ class MessageContainer extends React.Component { } onPress = debounce(() => { + const { onPress } = this.props; if (this.isIgnored) { return this.onIgnoredMessagePress(); } + if (onPress) { + return onPress(); + } + const { item, isThreadRoom } = this.props; Keyboard.dismiss(); @@ -265,12 +279,69 @@ class MessageContainer extends React.Component { } } + onLinkPress = (link) => { + const { item, theme, jumpToMessage } = this.props; + const isMessageLink = item?.attachments?.findIndex(att => att?.message_link === link) !== -1; + if (isMessageLink) { + return jumpToMessage(link); + } + openLink(link, theme); + } + render() { const { - item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, showAttachment, timeFormat, isReadReceiptEnabled, autoTranslateRoom, autoTranslateLanguage, navToRoomInfo, getCustomEmoji, isThreadRoom, callJitsi, blockAction, rid, theme, threadBadgeColor, toggleFollowThread + item, + user, + style, + archived, + baseUrl, + useRealName, + broadcast, + fetchThreadName, + showAttachment, + timeFormat, + isReadReceiptEnabled, + autoTranslateRoom, + autoTranslateLanguage, + navToRoomInfo, + getCustomEmoji, + isThreadRoom, + callJitsi, + blockAction, + rid, + theme, + threadBadgeColor, + toggleFollowThread, + jumpToMessage, + highlighted } = this.props; const { - id, msg, ts, attachments, urls, reactions, t, avatar, emoji, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels, unread, blocks, autoTranslate: autoTranslateMessage, replies + id, + msg, + ts, + attachments, + urls, + reactions, + t, + avatar, + emoji, + u, + alias, + editedBy, + role, + drid, + dcount, + dlm, + tmid, + tcount, + tlm, + tmsg, + mentions, + channels, + unread, + blocks, + autoTranslate: autoTranslateMessage, + replies } = item; let message = msg; @@ -294,6 +365,8 @@ class MessageContainer extends React.Component { onEncryptedPress: this.onEncryptedPress, onDiscussionPress: this.onDiscussionPress, onReactionLongPress: this.onReactionLongPress, + onLinkPress: this.onLinkPress, + jumpToMessage, threadBadgeColor, toggleFollowThread, replies @@ -347,6 +420,7 @@ class MessageContainer extends React.Component { callJitsi={callJitsi} blockAction={blockAction} theme={theme} + highlighted={highlighted} /> ); diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 2e8391956..1648d3bf2 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -735,5 +735,8 @@ "Last_owner_team_room": "You are the last owner of this channel. Once you leave the team, the channel will be kept inside the team but you will be managing it from outside.", "last-owner-can-not-be-removed": "Last owner cannot be removed", "leaving_team": "leaving team", - "member-does-not-exist": "Member does not exist" + "member-does-not-exist": "Member does not exist", + "Load_More": "Load More", + "Load_Newer": "Load Newer", + "Load_Older": "Load Older" } diff --git a/app/lib/database/model/Message.js b/app/lib/database/model/Message.js index bf776fc73..52cf63f0c 100644 --- a/app/lib/database/model/Message.js +++ b/app/lib/database/model/Message.js @@ -5,8 +5,10 @@ import { import { sanitizer } from '../utils'; +export const TABLE_NAME = 'messages'; + export default class Message extends Model { - static table = 'messages'; + static table = TABLE_NAME; static associations = { subscriptions: { type: 'belongs_to', key: 'rid' } diff --git a/app/lib/database/model/Subscription.js b/app/lib/database/model/Subscription.js index 5b1ebd141..275dae217 100644 --- a/app/lib/database/model/Subscription.js +++ b/app/lib/database/model/Subscription.js @@ -4,8 +4,10 @@ import { } from '@nozbe/watermelondb/decorators'; import { sanitizer } from '../utils'; +export const TABLE_NAME = 'subscriptions'; + export default class Subscription extends Model { - static table = 'subscriptions'; + static table = TABLE_NAME; static associations = { messages: { type: 'has_many', foreignKey: 'rid' }, diff --git a/app/lib/database/model/Thread.js b/app/lib/database/model/Thread.js index e0179fc35..04e658392 100644 --- a/app/lib/database/model/Thread.js +++ b/app/lib/database/model/Thread.js @@ -5,8 +5,10 @@ import { import { sanitizer } from '../utils'; +export const TABLE_NAME = 'threads'; + export default class Thread extends Model { - static table = 'threads'; + static table = TABLE_NAME; static associations = { subscriptions: { type: 'belongs_to', key: 'rid' } diff --git a/app/lib/database/model/ThreadMessage.js b/app/lib/database/model/ThreadMessage.js index b3b4216b5..687e09f96 100644 --- a/app/lib/database/model/ThreadMessage.js +++ b/app/lib/database/model/ThreadMessage.js @@ -5,8 +5,10 @@ import { import { sanitizer } from '../utils'; +export const TABLE_NAME = 'thread_messages'; + export default class ThreadMessage extends Model { - static table = 'thread_messages'; + static table = TABLE_NAME; static associations = { subscriptions: { type: 'belongs_to', key: 'subscription_id' } diff --git a/app/lib/database/services/Message.js b/app/lib/database/services/Message.js new file mode 100644 index 000000000..5999446ba --- /dev/null +++ b/app/lib/database/services/Message.js @@ -0,0 +1,15 @@ +import database from '..'; +import { TABLE_NAME } from '../model/Message'; + +const getCollection = db => db.get(TABLE_NAME); + +export const getMessageById = async(messageId) => { + const db = database.active; + const messageCollection = getCollection(db); + try { + const result = await messageCollection.find(messageId); + return result; + } catch (error) { + return null; + } +}; diff --git a/app/lib/database/services/Subscription.js b/app/lib/database/services/Subscription.js new file mode 100644 index 000000000..925bb97e4 --- /dev/null +++ b/app/lib/database/services/Subscription.js @@ -0,0 +1,15 @@ +import database from '..'; +import { TABLE_NAME } from '../model/Subscription'; + +const getCollection = db => db.get(TABLE_NAME); + +export const getSubscriptionByRoomId = async(rid) => { + const db = database.active; + const subCollection = getCollection(db); + try { + const result = await subCollection.find(rid); + return result; + } catch (error) { + return null; + } +}; diff --git a/app/lib/database/services/Thread.js b/app/lib/database/services/Thread.js new file mode 100644 index 000000000..4c4208609 --- /dev/null +++ b/app/lib/database/services/Thread.js @@ -0,0 +1,15 @@ +import database from '..'; +import { TABLE_NAME } from '../model/Thread'; + +const getCollection = db => db.get(TABLE_NAME); + +export const getThreadById = async(tmid) => { + const db = database.active; + const threadCollection = getCollection(db); + try { + const result = await threadCollection.find(tmid); + return result; + } catch (error) { + return null; + } +}; diff --git a/app/lib/database/services/ThreadMessage.js b/app/lib/database/services/ThreadMessage.js new file mode 100644 index 000000000..ca1e5fc83 --- /dev/null +++ b/app/lib/database/services/ThreadMessage.js @@ -0,0 +1,15 @@ +import database from '..'; +import { TABLE_NAME } from '../model/ThreadMessage'; + +const getCollection = db => db.get(TABLE_NAME); + +export const getThreadMessageById = async(messageId) => { + const db = database.active; + const threadMessageCollection = getCollection(db); + try { + const result = await threadMessageCollection.find(messageId); + return result; + } catch (error) { + return null; + } +}; diff --git a/app/lib/methods/getRoomInfo.js b/app/lib/methods/getRoomInfo.js new file mode 100644 index 000000000..293d97c56 --- /dev/null +++ b/app/lib/methods/getRoomInfo.js @@ -0,0 +1,29 @@ +import { getSubscriptionByRoomId } from '../database/services/Subscription'; +import RocketChat from '../rocketchat'; + +const getRoomInfo = async(rid) => { + let result; + result = await getSubscriptionByRoomId(rid); + if (result) { + return { + rid, + name: result.name, + fname: result.fname, + t: result.t + }; + } + + result = await RocketChat.getRoomInfo(rid); + if (result?.success) { + return { + rid, + name: result.room.name, + fname: result.room.fname, + t: result.room.t + }; + } + + return null; +}; + +export default getRoomInfo; diff --git a/app/lib/methods/getSingleMessage.js b/app/lib/methods/getSingleMessage.js new file mode 100644 index 000000000..56ecb3e63 --- /dev/null +++ b/app/lib/methods/getSingleMessage.js @@ -0,0 +1,15 @@ +import RocketChat from '../rocketchat'; + +const getSingleMessage = messageId => new Promise(async(resolve, reject) => { + try { + const result = await RocketChat.getSingleMessage(messageId); + if (result.success) { + return resolve(result.message); + } + return reject(); + } catch (e) { + return reject(); + } +}); + +export default getSingleMessage; diff --git a/app/lib/methods/getThreadName.js b/app/lib/methods/getThreadName.js new file mode 100644 index 000000000..1eb1fbc78 --- /dev/null +++ b/app/lib/methods/getThreadName.js @@ -0,0 +1,49 @@ +import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; + +import database from '../database'; +import { getMessageById } from '../database/services/Message'; +import { getThreadById } from '../database/services/Thread'; +import log from '../../utils/log'; +import getSingleMessage from './getSingleMessage'; +import { Encryption } from '../encryption'; + +const buildThreadName = thread => thread.msg || thread?.attachments?.[0]?.title; + +const getThreadName = async(rid, tmid, messageId) => { + let tmsg; + try { + const db = database.active; + const threadCollection = db.get('threads'); + const messageRecord = await getMessageById(messageId); + const threadRecord = await getThreadById(tmid); + if (threadRecord) { + tmsg = buildThreadName(threadRecord); + await db.action(async() => { + await messageRecord?.update((m) => { + m.tmsg = tmsg; + }); + }); + } else { + let thread = await getSingleMessage(tmid); + thread = await Encryption.decryptMessage(thread); + tmsg = buildThreadName(thread); + await db.action(async() => { + await db.batch( + threadCollection?.prepareCreate((t) => { + t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema); + t.subscription.id = rid; + Object.assign(t, thread); + }), + messageRecord?.prepareUpdate((m) => { + m.tmsg = tmsg; + }) + ); + }); + } + } catch (e) { + log(e); + } + return tmsg; +}; + +export default getThreadName; diff --git a/app/lib/methods/loadMessagesForRoom.js b/app/lib/methods/loadMessagesForRoom.js index 012e1ea32..a8dc733ac 100644 --- a/app/lib/methods/loadMessagesForRoom.js +++ b/app/lib/methods/loadMessagesForRoom.js @@ -1,8 +1,15 @@ +import moment from 'moment'; + +import { MESSAGE_TYPE_LOAD_MORE } from '../../constants/messageTypeLoad'; import log from '../../utils/log'; +import { getMessageById } from '../database/services/Message'; import updateMessages from './updateMessages'; +import { generateLoadMoreId } from '../utils'; + +const COUNT = 50; async function load({ rid: roomId, latest, t }) { - let params = { roomId, count: 50 }; + let params = { roomId, count: COUNT }; if (latest) { params = { ...params, latest: new Date(latest).toISOString() }; } @@ -24,9 +31,20 @@ export default function loadMessagesForRoom(args) { return new Promise(async(resolve, reject) => { try { const data = await load.call(this, args); - - if (data && data.length) { - await updateMessages({ rid: args.rid, update: data }); + if (data?.length) { + const lastMessage = data[data.length - 1]; + const lastMessageRecord = await getMessageById(lastMessage._id); + if (!lastMessageRecord && data.length === COUNT) { + const loadMoreItem = { + _id: generateLoadMoreId(lastMessage._id), + rid: lastMessage.rid, + ts: moment(lastMessage.ts).subtract(1, 'millisecond'), + t: MESSAGE_TYPE_LOAD_MORE, + msg: lastMessage.msg + }; + data.push(loadMoreItem); + } + await updateMessages({ rid: args.rid, update: data, loaderItem: args.loaderItem }); return resolve(data); } else { return resolve([]); diff --git a/app/lib/methods/loadNextMessages.js b/app/lib/methods/loadNextMessages.js new file mode 100644 index 000000000..3a5e5e6ff --- /dev/null +++ b/app/lib/methods/loadNextMessages.js @@ -0,0 +1,42 @@ +import EJSON from 'ejson'; +import moment from 'moment'; +import orderBy from 'lodash/orderBy'; + +import log from '../../utils/log'; +import updateMessages from './updateMessages'; +import { getMessageById } from '../database/services/Message'; +import { MESSAGE_TYPE_LOAD_NEXT_CHUNK } from '../../constants/messageTypeLoad'; +import { generateLoadMoreId } from '../utils'; + +const COUNT = 50; + +export default function loadNextMessages(args) { + return new Promise(async(resolve, reject) => { + try { + const data = await this.methodCallWrapper('loadNextMessages', args.rid, args.ts, COUNT); + let messages = EJSON.fromJSONValue(data?.messages); + messages = orderBy(messages, 'ts'); + if (messages?.length) { + const lastMessage = messages[messages.length - 1]; + const lastMessageRecord = await getMessageById(lastMessage._id); + if (!lastMessageRecord && messages.length === COUNT) { + const loadMoreItem = { + _id: generateLoadMoreId(lastMessage._id), + rid: lastMessage.rid, + tmid: args.tmid, + ts: moment(lastMessage.ts).add(1, 'millisecond'), + t: MESSAGE_TYPE_LOAD_NEXT_CHUNK + }; + messages.push(loadMoreItem); + } + await updateMessages({ rid: args.rid, update: messages, loaderItem: args.loaderItem }); + return resolve(messages); + } else { + return resolve([]); + } + } catch (e) { + log(e); + reject(e); + } + }); +} diff --git a/app/lib/methods/loadSurroundingMessages.js b/app/lib/methods/loadSurroundingMessages.js new file mode 100644 index 000000000..74c345c2f --- /dev/null +++ b/app/lib/methods/loadSurroundingMessages.js @@ -0,0 +1,65 @@ +import EJSON from 'ejson'; +import moment from 'moment'; +import orderBy from 'lodash/orderBy'; + +import log from '../../utils/log'; +import updateMessages from './updateMessages'; +import { getMessageById } from '../database/services/Message'; +import { MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../constants/messageTypeLoad'; +import { generateLoadMoreId } from '../utils'; + +const COUNT = 50; + +export default function loadSurroundingMessages({ messageId, rid }) { + return new Promise(async(resolve, reject) => { + try { + const data = await this.methodCallWrapper('loadSurroundingMessages', { _id: messageId, rid }, COUNT); + let messages = EJSON.fromJSONValue(data?.messages); + messages = orderBy(messages, 'ts'); + + const message = messages.find(m => m._id === messageId); + const { tmid } = message; + + if (messages?.length) { + if (data?.moreBefore) { + const firstMessage = messages[0]; + const firstMessageRecord = await getMessageById(firstMessage._id); + if (!firstMessageRecord) { + const loadMoreItem = { + _id: generateLoadMoreId(firstMessage._id), + rid: firstMessage.rid, + tmid, + ts: moment(firstMessage.ts).subtract(1, 'millisecond'), + t: MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK, + msg: firstMessage.msg + }; + messages.unshift(loadMoreItem); + } + } + + if (data?.moreAfter) { + const lastMessage = messages[messages.length - 1]; + const lastMessageRecord = await getMessageById(lastMessage._id); + if (!lastMessageRecord) { + const loadMoreItem = { + _id: generateLoadMoreId(lastMessage._id), + rid: lastMessage.rid, + tmid, + ts: moment(lastMessage.ts).add(1, 'millisecond'), + t: MESSAGE_TYPE_LOAD_NEXT_CHUNK, + msg: lastMessage.msg + }; + messages.push(loadMoreItem); + } + } + await updateMessages({ rid, update: messages }); + return resolve(messages); + } else { + return resolve([]); + } + } catch (e) { + log(e); + reject(e); + } + }); +} diff --git a/app/lib/methods/loadThreadMessages.js b/app/lib/methods/loadThreadMessages.js index de6f244c6..d170635e8 100644 --- a/app/lib/methods/loadThreadMessages.js +++ b/app/lib/methods/loadThreadMessages.js @@ -1,5 +1,6 @@ import { Q } from '@nozbe/watermelondb'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; +import EJSON from 'ejson'; import buildMessage from './helpers/buildMessage'; import database from '../database'; @@ -7,30 +8,27 @@ import log from '../../utils/log'; import protectedFunction from './helpers/protectedFunction'; import { Encryption } from '../encryption'; -async function load({ tmid, offset }) { +async function load({ tmid }) { try { // RC 1.0 - const result = await this.sdk.get('chat.getThreadMessages', { - tmid, count: 50, offset, sort: { ts: -1 }, query: { _hidden: { $ne: true } } - }); - if (!result || !result.success) { + const result = await this.methodCallWrapper('getThreadMessages', { tmid }); + if (!result) { return []; } - return result.messages; + return EJSON.fromJSONValue(result); } catch (error) { console.log(error); return []; } } -export default function loadThreadMessages({ tmid, rid, offset = 0 }) { +export default function loadThreadMessages({ tmid, rid }) { return new Promise(async(resolve, reject) => { try { - let data = await load.call(this, { tmid, offset }); - + let data = await load.call(this, { tmid }); if (data && data.length) { try { - data = data.map(m => buildMessage(m)); + data = data.filter(m => m.tmid).map(m => buildMessage(m)); data = await Encryption.decryptMessages(data); const db = database.active; const threadMessagesCollection = db.get('thread_messages'); diff --git a/app/lib/methods/updateMessages.js b/app/lib/methods/updateMessages.js index 5f0db0f66..0b6b6c7c0 100644 --- a/app/lib/methods/updateMessages.js +++ b/app/lib/methods/updateMessages.js @@ -6,8 +6,12 @@ import log from '../../utils/log'; import database from '../database'; import protectedFunction from './helpers/protectedFunction'; import { Encryption } from '../encryption'; +import { MESSAGE_TYPE_ANY_LOAD } from '../../constants/messageTypeLoad'; +import { generateLoadMoreId } from '../utils'; -export default function updateMessages({ rid, update = [], remove = [] }) { +export default function updateMessages({ + rid, update = [], remove = [], loaderItem +}) { try { if (!((update && update.length) || (remove && remove.length))) { return; @@ -30,7 +34,13 @@ export default function updateMessages({ rid, update = [], remove = [] }) { const threadCollection = db.get('threads'); const threadMessagesCollection = db.get('thread_messages'); const allMessagesRecords = await msgCollection - .query(Q.where('rid', rid), Q.where('id', Q.oneOf(messagesIds))) + .query( + Q.where('rid', rid), + Q.or( + Q.where('id', Q.oneOf(messagesIds)), + Q.where('t', Q.oneOf(MESSAGE_TYPE_ANY_LOAD)) + ) + ) .fetch(); const allThreadsRecords = await threadCollection .query(Q.where('rid', rid), Q.where('id', Q.oneOf(messagesIds))) @@ -55,6 +65,9 @@ export default function updateMessages({ rid, update = [], remove = [] }) { let threadMessagesToCreate = allThreadMessages.filter(i1 => !allThreadMessagesRecords.find(i2 => i1._id === i2.id)); let threadMessagesToUpdate = allThreadMessagesRecords.filter(i1 => allThreadMessages.find(i2 => i1.id === i2._id)); + // filter loaders to delete + let loadersToDelete = allMessagesRecords.filter(i1 => update.find(i2 => i1.id === generateLoadMoreId(i2._id))); + // Create msgsToCreate = msgsToCreate.map(message => msgCollection.prepareCreate(protectedFunction((m) => { m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema); @@ -121,6 +134,12 @@ export default function updateMessages({ rid, update = [], remove = [] }) { threadMessagesToDelete = threadMessagesToDelete.map(tm => tm.prepareDestroyPermanently()); } + // Delete loaders + loadersToDelete = loadersToDelete.map(m => m.prepareDestroyPermanently()); + if (loaderItem) { + loadersToDelete.push(loaderItem.prepareDestroyPermanently()); + } + const allRecords = [ ...msgsToCreate, ...msgsToUpdate, @@ -130,7 +149,8 @@ export default function updateMessages({ rid, update = [], remove = [] }) { ...threadsToDelete, ...threadMessagesToCreate, ...threadMessagesToUpdate, - ...threadMessagesToDelete + ...threadMessagesToDelete, + ...loadersToDelete ]; try { diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 3891d88cb..561e02c73 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -1,4 +1,5 @@ import { InteractionManager } from 'react-native'; +import EJSON from 'ejson'; import { Rocketchat as RocketchatClient, settings as RocketChatSettings @@ -41,6 +42,8 @@ import canOpenRoom from './methods/canOpenRoom'; import triggerBlockAction, { triggerSubmitView, triggerCancel } from './methods/actions'; import loadMessagesForRoom from './methods/loadMessagesForRoom'; +import loadSurroundingMessages from './methods/loadSurroundingMessages'; +import loadNextMessages from './methods/loadNextMessages'; import loadMissedMessages from './methods/loadMissedMessages'; import loadThreadMessages from './methods/loadThreadMessages'; @@ -624,6 +627,8 @@ const RocketChat = { }, loadMissedMessages, loadMessagesForRoom, + loadSurroundingMessages, + loadNextMessages, loadThreadMessages, sendMessage, getRooms, @@ -938,7 +943,7 @@ const RocketChat = { methodCallWrapper(method, ...params) { const { API_Use_REST_For_DDP_Calls } = reduxStore.getState().settings; if (API_Use_REST_For_DDP_Calls) { - return this.post(`method.call/${ method }`, { message: JSON.stringify({ method, params }) }); + return this.post(`method.call/${ method }`, { message: EJSON.stringify({ method, params }) }); } return this.methodCall(method, ...params); }, diff --git a/app/lib/utils.js b/app/lib/utils.js index 769fd6d76..615b353ae 100644 --- a/app/lib/utils.js +++ b/app/lib/utils.js @@ -20,3 +20,5 @@ export const methods = { }; export const compareServerVersion = (currentServerVersion, versionToCompare, func) => currentServerVersion && func(coerce(currentServerVersion), versionToCompare); + +export const generateLoadMoreId = id => `load-more-${ id }`; diff --git a/app/notifications/push/index.js b/app/notifications/push/index.js index df4ac152d..13e929164 100644 --- a/app/notifications/push/index.js +++ b/app/notifications/push/index.js @@ -10,7 +10,7 @@ export const onNotification = (notification) => { if (data) { try { const { - rid, name, sender, type, host, messageType + rid, name, sender, type, host, messageType, messageId } = EJSON.parse(data.ejson); const types = { @@ -24,6 +24,7 @@ export const onNotification = (notification) => { const params = { host, rid, + messageId, path: `${ types[type] }/${ roomName }`, isCall: messageType === 'jitsi_call_started' }; diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js index 184a4d96e..985a28556 100644 --- a/app/sagas/deepLinking.js +++ b/app/sagas/deepLinking.js @@ -60,18 +60,19 @@ const navigate = function* navigate({ params }) { const isMasterDetail = yield select(state => state.app.isMasterDetail); const focusedRooms = yield select(state => state.room.rooms); + const jumpToMessageId = params.messageId; if (focusedRooms.includes(room.rid)) { // if there's one room on the list or last room is the one if (focusedRooms.length === 1 || focusedRooms[0] === room.rid) { - yield goRoom({ item, isMasterDetail }); + yield goRoom({ item, isMasterDetail, jumpToMessageId }); } else { popToRoot({ isMasterDetail }); - yield goRoom({ item, isMasterDetail }); + yield goRoom({ item, isMasterDetail, jumpToMessageId }); } } else { popToRoot({ isMasterDetail }); - yield goRoom({ item, isMasterDetail }); + yield goRoom({ item, isMasterDetail, jumpToMessageId }); } if (params.isCall) { diff --git a/app/utils/goRoom.js b/app/utils/goRoom.js index 94adfde49..e9811e651 100644 --- a/app/utils/goRoom.js +++ b/app/utils/goRoom.js @@ -14,7 +14,6 @@ const navigate = ({ item, isMasterDetail, ...props }) => { t: item.t, prid: item.prid, room: item, - search: item.search, visitor: item.visitor, roomUserId: RocketChat.getUidDirectMessage(item), ...props diff --git a/app/views/MessagesView/index.js b/app/views/MessagesView/index.js index fc840b2d6..f6ea91942 100644 --- a/app/views/MessagesView/index.js +++ b/app/views/MessagesView/index.js @@ -16,6 +16,7 @@ import { withTheme } from '../../theme'; import { getUserSelector } from '../../selectors/login'; import { withActionSheet } from '../../containers/ActionSheet'; import SafeAreaView from '../../containers/SafeAreaView'; +import getThreadName from '../../lib/methods/getThreadName'; class MessagesView extends React.Component { static propTypes = { @@ -26,7 +27,8 @@ class MessagesView extends React.Component { customEmojis: PropTypes.object, theme: PropTypes.string, showActionSheet: PropTypes.func, - useRealName: PropTypes.bool + useRealName: PropTypes.bool, + isMasterDetail: PropTypes.bool } constructor(props) { @@ -81,6 +83,32 @@ class MessagesView extends React.Component { navigation.navigate('RoomInfoView', navParam); } + jumpToMessage = async({ item }) => { + const { navigation, isMasterDetail } = this.props; + let params = { + rid: this.rid, + jumpToMessageId: item._id, + t: this.t, + room: this.room + }; + if (item.tmid) { + if (isMasterDetail) { + navigation.navigate('DrawerNavigator'); + } else { + navigation.pop(2); + } + params = { + ...params, + tmid: item.tmid, + name: await getThreadName(this.rid, item.tmid, item._id), + t: 'thread' + }; + navigation.push('RoomView', params); + } else { + navigation.navigate('RoomView', params); + } + } + defineMessagesViewContent = (name) => { const { user, baseUrl, theme, useRealName @@ -93,11 +121,13 @@ class MessagesView extends React.Component { timeFormat: 'MMM Do YYYY, h:mm:ss a', isEdited: !!item.editedAt, isHeader: true, + isThreadRoom: true, attachments: item.attachments || [], useRealName, showAttachment: this.showAttachment, getCustomEmoji: this.getCustomEmoji, - navToRoomInfo: this.navToRoomInfo + navToRoomInfo: this.navToRoomInfo, + onPress: () => this.jumpToMessage({ item }) }); return ({ @@ -315,7 +345,8 @@ const mapStateToProps = state => ({ baseUrl: state.server.server, user: getUserSelector(state), customEmojis: state.customEmojis, - useRealName: state.settings.UI_Use_Real_Name + useRealName: state.settings.UI_Use_Real_Name, + isMasterDetail: state.app.isMasterDetail }); export default connect(mapStateToProps)(withTheme(withActionSheet(MessagesView))); diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index 72ec2548b..3256dd0b9 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -636,7 +636,7 @@ class RoomActionsView extends React.Component { room, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate, canForwardGuest, canReturnQueue } = this.state; const { - rid, t, encrypted + rid, t } = room; const isGroupChat = RocketChat.isGroupChat(room); @@ -761,24 +761,6 @@ class RoomActionsView extends React.Component { ) : null} - {['c', 'p', 'd'].includes(t) - ? ( - <> - this.onPressTouchable({ - route: 'SearchMessagesView', - params: { rid, encrypted } - })} - testID='room-actions-search' - left={() => } - showActionIndicator - /> - - - ) - : null} - {['c', 'p', 'd'].includes(t) ? ( <> diff --git a/app/views/RoomView/List/List.js b/app/views/RoomView/List/List.js new file mode 100644 index 000000000..407dcbf10 --- /dev/null +++ b/app/views/RoomView/List/List.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { FlatList, StyleSheet } from 'react-native'; +import Animated from 'react-native-reanimated'; +import PropTypes from 'prop-types'; + +import { isIOS } from '../../../utils/deviceInfo'; +import scrollPersistTaps from '../../../utils/scrollPersistTaps'; + +const AnimatedFlatList = Animated.createAnimatedComponent(FlatList); + +const styles = StyleSheet.create({ + list: { + flex: 1 + }, + contentContainer: { + paddingTop: 10 + } +}); + +const List = ({ listRef, ...props }) => ( + item.id} + contentContainerStyle={styles.contentContainer} + style={styles.list} + inverted + removeClippedSubviews={isIOS} + initialNumToRender={7} + onEndReachedThreshold={0.5} + maxToRenderPerBatch={5} + windowSize={10} + {...props} + {...scrollPersistTaps} + /> +); + +List.propTypes = { + listRef: PropTypes.object +}; + +export default List; diff --git a/app/views/RoomView/List/NavBottomFAB.js b/app/views/RoomView/List/NavBottomFAB.js new file mode 100644 index 000000000..5c5aee746 --- /dev/null +++ b/app/views/RoomView/List/NavBottomFAB.js @@ -0,0 +1,75 @@ +import React, { useCallback, useState } from 'react'; +import { View, StyleSheet } from 'react-native'; +import PropTypes from 'prop-types'; +import Animated, { + call, cond, greaterOrEq, useCode +} from 'react-native-reanimated'; + +import { themes } from '../../../constants/colors'; +import { CustomIcon } from '../../../lib/Icons'; +import { useTheme } from '../../../theme'; +import Touch from '../../../utils/touch'; +import { hasNotch } from '../../../utils/deviceInfo'; + +const SCROLL_LIMIT = 200; +const SEND_TO_CHANNEL_HEIGHT = 40; + +const styles = StyleSheet.create({ + container: { + position: 'absolute', + right: 15 + }, + button: { + borderRadius: 25 + }, + content: { + width: 50, + height: 50, + borderRadius: 25, + borderWidth: 1, + alignItems: 'center', + justifyContent: 'center' + } +}); + +const NavBottomFAB = ({ y, onPress, isThread }) => { + const { theme } = useTheme(); + const [show, setShow] = useState(false); + const handleOnPress = useCallback(() => onPress()); + const toggle = v => setShow(v); + + useCode(() => cond(greaterOrEq(y, SCROLL_LIMIT), + call([y], () => toggle(true)), + call([y], () => toggle(false))), + [y]); + + if (!show) { + return null; + } + + let bottom = hasNotch ? 100 : 60; + if (isThread) { + bottom += SEND_TO_CHANNEL_HEIGHT; + } + return ( + + + + + + + + ); +}; + +NavBottomFAB.propTypes = { + y: Animated.Value, + onPress: PropTypes.func, + isThread: PropTypes.bool +}; + +export default NavBottomFAB; diff --git a/app/views/RoomView/List.js b/app/views/RoomView/List/index.js similarity index 59% rename from app/views/RoomView/List.js rename to app/views/RoomView/List/index.js index 41d424fa3..19a8ccb90 100644 --- a/app/views/RoomView/List.js +++ b/app/views/RoomView/List/index.js @@ -1,30 +1,39 @@ import React from 'react'; -import { FlatList, RefreshControl } from 'react-native'; +import { RefreshControl } from 'react-native'; import PropTypes from 'prop-types'; import { Q } from '@nozbe/watermelondb'; import moment from 'moment'; import { dequal } from 'dequal'; +import { Value, event } from 'react-native-reanimated'; -import styles from './styles'; -import database from '../../lib/database'; -import scrollPersistTaps from '../../utils/scrollPersistTaps'; -import RocketChat from '../../lib/rocketchat'; -import log from '../../utils/log'; -import EmptyRoom from './EmptyRoom'; -import { isIOS } from '../../utils/deviceInfo'; -import { animateNextTransition } from '../../utils/layoutAnimation'; -import ActivityIndicator from '../../containers/ActivityIndicator'; -import { themes } from '../../constants/colors'; +import database from '../../../lib/database'; +import RocketChat from '../../../lib/rocketchat'; +import log from '../../../utils/log'; +import EmptyRoom from '../EmptyRoom'; +import { animateNextTransition } from '../../../utils/layoutAnimation'; +import ActivityIndicator from '../../../containers/ActivityIndicator'; +import { themes } from '../../../constants/colors'; +import List from './List'; +import NavBottomFAB from './NavBottomFAB'; +import debounce from '../../../utils/debounce'; const QUERY_SIZE = 50; -class List extends React.Component { +const onScroll = ({ y }) => event( + [ + { + nativeEvent: { + contentOffset: { y } + } + } + ], + { useNativeDriver: true } +); + +class ListContainer extends React.Component { static propTypes = { - onEndReached: PropTypes.func, - renderFooter: PropTypes.func, renderRow: PropTypes.func, rid: PropTypes.string, - t: PropTypes.string, tmid: PropTypes.string, theme: PropTypes.string, loading: PropTypes.bool, @@ -36,34 +45,28 @@ class List extends React.Component { showMessageInMainThread: PropTypes.bool }; - // this.state.loading works for this.onEndReached and RoomView.init - static getDerivedStateFromProps(props, state) { - if (props.loading !== state.loading) { - return { - loading: props.loading - }; - } - return null; - } - constructor(props) { super(props); console.time(`${ this.constructor.name } init`); console.time(`${ this.constructor.name } mount`); this.count = 0; - this.needsFetch = false; this.mounted = false; this.animated = false; + this.jumping = false; this.state = { - loading: true, - end: false, messages: [], - refreshing: false + refreshing: false, + highlightedMessage: null }; + this.y = new Value(0); + this.onScroll = onScroll({ y: this.y }); this.query(); this.unsubscribeFocus = props.navigation.addListener('focus', () => { this.animated = true; }); + this.viewabilityConfig = { + itemVisiblePercentThreshold: 10 + }; console.timeEnd(`${ this.constructor.name } init`); } @@ -73,17 +76,17 @@ class List extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - const { loading, end, refreshing } = this.state; + const { refreshing, highlightedMessage } = this.state; const { - hideSystemMessages, theme, tunread, ignored + hideSystemMessages, theme, tunread, ignored, loading } = this.props; if (theme !== nextProps.theme) { return true; } - if (loading !== nextState.loading) { + if (loading !== nextProps.loading) { return true; } - if (end !== nextState.end) { + if (highlightedMessage !== nextState.highlightedMessage) { return true; } if (refreshing !== nextState.refreshing) { @@ -116,32 +119,14 @@ class List extends React.Component { if (this.unsubscribeFocus) { this.unsubscribeFocus(); } + this.clearHighlightedMessageTimeout(); console.countReset(`${ this.constructor.name }.render calls`); } - fetchData = async() => { - const { - loading, end, messages, latest = messages[messages.length - 1]?.ts - } = this.state; - if (loading || end) { - return; - } - - this.setState({ loading: true }); - const { rid, t, tmid } = this.props; - try { - let result; - if (tmid) { - // `offset` is `messages.length - 1` because we append thread start to `messages` obj - result = await RocketChat.loadThreadMessages({ tmid, rid, offset: messages.length - 1 }); - } else { - result = await RocketChat.loadMessagesForRoom({ rid, t, latest }); - } - - this.setState({ end: result.length < QUERY_SIZE, loading: false, latest: result[result.length - 1]?.ts }, () => this.loadMoreMessages(result)); - } catch (e) { - this.setState({ loading: false }); - log(e); + clearHighlightedMessageTimeout = () => { + if (this.highlightedMessageTimeout) { + clearTimeout(this.highlightedMessageTimeout); + this.highlightedMessageTimeout = false; } } @@ -198,9 +183,6 @@ class List extends React.Component { this.unsubscribeMessages(); this.messagesSubscription = this.messagesObservable .subscribe((messages) => { - if (messages.length <= this.count) { - this.needsFetch = true; - } if (tmid && this.thread) { messages = [...messages, this.thread]; } @@ -211,6 +193,7 @@ class List extends React.Component { } else { this.state.messages = messages; } + // TODO: move it away from here this.readThreads(); }); } @@ -221,7 +204,7 @@ class List extends React.Component { this.query(); } - readThreads = async() => { + readThreads = debounce(async() => { const { tmid } = this.props; if (tmid) { @@ -231,39 +214,9 @@ class List extends React.Component { // Do nothing } } - } + }, 300) - onEndReached = async() => { - if (this.needsFetch) { - this.needsFetch = false; - await this.fetchData(); - } - this.query(); - } - - loadMoreMessages = (result) => { - const { end } = this.state; - - if (end) { - return; - } - - // handle servers with version < 3.0.0 - let { hideSystemMessages = [] } = this.props; - if (!Array.isArray(hideSystemMessages)) { - hideSystemMessages = []; - } - - if (!hideSystemMessages.length) { - return; - } - - const hasReadableMessages = result.filter(message => !message.t || (message.t && !hideSystemMessages.includes(message.t))).length > 0; - // if this batch doesn't contain any messages that will be displayed, we'll request a new batch - if (!hasReadableMessages) { - this.onEndReached(); - } - } + onEndReached = () => this.query() onRefresh = () => this.setState({ refreshing: true }, async() => { const { messages } = this.state; @@ -272,7 +225,7 @@ class List extends React.Component { if (messages.length) { try { if (tmid) { - await RocketChat.loadThreadMessages({ tmid, rid, offset: messages.length - 1 }); + await RocketChat.loadThreadMessages({ tmid, rid }); } else { await RocketChat.loadMissedMessages({ rid, lastOpen: moment().subtract(7, 'days').toDate() }); } @@ -284,7 +237,6 @@ class List extends React.Component { this.setState({ refreshing: false }); }) - // eslint-disable-next-line react/sort-comp update = () => { if (this.animated) { animateNextTransition(); @@ -306,9 +258,53 @@ class List extends React.Component { return null; } + handleScrollToIndexFailed = (params) => { + const { listRef } = this.props; + listRef.current.getNode().scrollToIndex({ index: params.highestMeasuredFrameIndex, animated: false }); + } + + jumpToMessage = messageId => new Promise(async(resolve) => { + this.jumping = true; + const { messages } = this.state; + const { listRef } = this.props; + const index = messages.findIndex(item => item.id === messageId); + if (index > -1) { + listRef.current.getNode().scrollToIndex({ index, viewPosition: 0.5 }); + await new Promise(res => setTimeout(res, 300)); + if (!this.viewableItems.map(vi => vi.key).includes(messageId)) { + if (!this.jumping) { + return resolve(); + } + await setTimeout(() => resolve(this.jumpToMessage(messageId)), 300); + return; + } + this.setState({ highlightedMessage: messageId }); + this.clearHighlightedMessageTimeout(); + this.highlightedMessageTimeout = setTimeout(() => { + this.setState({ highlightedMessage: null }); + }, 10000); + await setTimeout(() => resolve(), 300); + } else { + listRef.current.getNode().scrollToIndex({ index: messages.length - 1, animated: false }); + if (!this.jumping) { + return resolve(); + } + await setTimeout(() => resolve(this.jumpToMessage(messageId)), 300); + } + }); + + // this.jumping is checked in between operations to make sure we're not stuck + cancelJumpToMessage = () => { + this.jumping = false; + } + + jumpToBottom = () => { + const { listRef } = this.props; + listRef.current.getNode().scrollToOffset({ offset: -100 }); + } + renderFooter = () => { - const { loading } = this.state; - const { rid, theme } = this.props; + const { rid, theme, loading } = this.props; if (loading && rid) { return ; } @@ -316,36 +312,34 @@ class List extends React.Component { } renderItem = ({ item, index }) => { - const { messages } = this.state; + const { messages, highlightedMessage } = this.state; const { renderRow } = this.props; - return renderRow(item, messages[index + 1]); + return renderRow(item, messages[index + 1], highlightedMessage); + } + + onViewableItemsChanged = ({ viewableItems }) => { + this.viewableItems = viewableItems; } render() { console.count(`${ this.constructor.name }.render calls`); - const { rid, listRef } = this.props; + const { rid, tmid, listRef } = this.props; const { messages, refreshing } = this.state; const { theme } = this.props; return ( <> - item.id} + )} - {...scrollPersistTaps} /> + ); } } -export default List; +export default ListContainer; diff --git a/app/views/RoomView/LoadMore/LoadMore.stories.js b/app/views/RoomView/LoadMore/LoadMore.stories.js new file mode 100644 index 000000000..1f110a9cf --- /dev/null +++ b/app/views/RoomView/LoadMore/LoadMore.stories.js @@ -0,0 +1,62 @@ +/* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions, react/prop-types, react/destructuring-assignment */ +import React from 'react'; +import { ScrollView } from 'react-native'; +import { storiesOf } from '@storybook/react-native'; + +import LoadMore from './index'; +import { longText } from '../../../../storybook/utils'; +import { ThemeContext } from '../../../theme'; +import { + Message, StoryProvider, MessageDecorator +} from '../../../../storybook/stories/Message'; +import { themes } from '../../../constants/colors'; +import { MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../../constants/messageTypeLoad'; + +const stories = storiesOf('LoadMore', module); + +// FIXME: for some reason, this promise never resolves on Storybook (it works on the app, so maybe the issue isn't on the component) +const load = () => new Promise(res => setTimeout(res, 1000)); + +stories.add('basic', () => ( + <> + + + + + +)); + +const ThemeStory = ({ theme }) => ( + + + + + + + + + + + + + + +); + +stories + .addDecorator(StoryProvider) + .addDecorator(MessageDecorator) + .add('light theme', () => ); + +stories + .addDecorator(StoryProvider) + .addDecorator(MessageDecorator) + .add('dark theme', () => ); + +stories + .addDecorator(StoryProvider) + .addDecorator(MessageDecorator) + .add('black theme', () => ); + diff --git a/app/views/RoomView/LoadMore/index.js b/app/views/RoomView/LoadMore/index.js new file mode 100644 index 000000000..04b922835 --- /dev/null +++ b/app/views/RoomView/LoadMore/index.js @@ -0,0 +1,76 @@ +import React, { useEffect, useCallback, useState } from 'react'; +import { Text, StyleSheet, ActivityIndicator } from 'react-native'; +import PropTypes from 'prop-types'; + +import { themes } from '../../../constants/colors'; +import { MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../../constants/messageTypeLoad'; +import { useTheme } from '../../../theme'; +import Touch from '../../../utils/touch'; +import sharedStyles from '../../Styles'; +import I18n from '../../../i18n'; + +const styles = StyleSheet.create({ + button: { + paddingVertical: 16, + alignItems: 'center', + justifyContent: 'center' + }, + text: { + fontSize: 16, + ...sharedStyles.textMedium + } +}); + +const LoadMore = ({ load, type, runOnRender }) => { + const { theme } = useTheme(); + const [loading, setLoading] = useState(false); + + const handleLoad = useCallback(async() => { + try { + if (loading) { + return; + } + setLoading(true); + await load(); + } finally { + setLoading(false); + } + }, [loading]); + + useEffect(() => { + if (runOnRender) { + handleLoad(); + } + }, []); + + let text = 'Load_More'; + if (type === MESSAGE_TYPE_LOAD_NEXT_CHUNK) { + text = 'Load_Newer'; + } + if (type === MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK) { + text = 'Load_Older'; + } + + return ( + + { + loading + ? + : {I18n.t(text)} + } + + ); +}; + +LoadMore.propTypes = { + load: PropTypes.func, + type: PropTypes.string, + runOnRender: PropTypes.bool +}; + +export default LoadMore; diff --git a/app/views/RoomView/RightButtons.js b/app/views/RoomView/RightButtons.js index 81b8f153b..f61488b5b 100644 --- a/app/views/RoomView/RightButtons.js +++ b/app/views/RoomView/RightButtons.js @@ -142,12 +142,12 @@ class RightButtonsContainer extends Component { goSearchView = () => { logEvent(events.ROOM_GO_SEARCH); const { - rid, navigation, isMasterDetail + rid, t, navigation, isMasterDetail } = this.props; if (isMasterDetail) { navigation.navigate('ModalStackNavigator', { screen: 'SearchMessagesView', params: { rid, showCloseModal: true } }); } else { - navigation.navigate('SearchMessagesView', { rid }); + navigation.navigate('SearchMessagesView', { rid, t }); } } diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index b6b700db3..18d123ec4 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -2,8 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Text, View, InteractionManager } from 'react-native'; import { connect } from 'react-redux'; +import parse from 'url-parse'; -import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import moment from 'moment'; import * as Haptics from 'expo-haptics'; import { Q } from '@nozbe/watermelondb'; @@ -17,7 +17,6 @@ import { import List from './List'; import database from '../../lib/database'; import RocketChat from '../../lib/rocketchat'; -import { Encryption } from '../../lib/encryption'; import Message from '../../containers/message'; import MessageActions from '../../containers/MessageActions'; import MessageErrorActions from '../../containers/MessageErrorActions'; @@ -35,6 +34,7 @@ import RightButtons from './RightButtons'; import StatusBar from '../../containers/StatusBar'; import Separator from './Separator'; import { themes } from '../../constants/colors'; +import { MESSAGE_TYPE_ANY_LOAD, MESSAGE_TYPE_LOAD_MORE } from '../../constants/messageTypeLoad'; import debounce from '../../utils/debounce'; import ReactionsModal from '../../containers/ReactionsModal'; import { LISTENER } from '../../containers/Toast'; @@ -64,6 +64,12 @@ import { getHeaderTitlePosition } from '../../containers/Header'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants'; import { takeInquiry } from '../../ee/omnichannel/lib'; +import Loading from '../../containers/Loading'; +import LoadMore from './LoadMore'; +import RoomServices from './services'; +import { goRoom } from '../../utils/goRoom'; +import getThreadName from '../../lib/methods/getThreadName'; +import getRoomInfo from '../../lib/methods/getRoomInfo'; const stateAttrsUpdate = [ 'joined', @@ -76,7 +82,8 @@ const stateAttrsUpdate = [ 'replying', 'reacting', 'readOnly', - 'member' + 'member', + 'showingBlockingLoader' ]; const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'tunread', 'muted', 'ignored', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname', 'roles', 'bannerClosed', 'visitor', 'joinCodeRequired']; @@ -117,11 +124,11 @@ class RoomView extends React.Component { const selectedMessage = props.route.params?.message; const name = props.route.params?.name; const fname = props.route.params?.fname; - const search = props.route.params?.search; const prid = props.route.params?.prid; const room = props.route.params?.room ?? { rid: this.rid, t: this.t, name, fname, prid }; + this.jumpToMessageId = props.route.params?.jumpToMessageId; const roomUserId = props.route.params?.roomUserId ?? RocketChat.getUidDirectMessage(room); this.state = { joined: true, @@ -133,6 +140,7 @@ class RoomView extends React.Component { selectedMessage: selectedMessage || {}, canAutoTranslate: false, loading: true, + showingBlockingLoader: false, editing: false, replying: !!selectedMessage, replyWithMention: false, @@ -151,13 +159,10 @@ class RoomView extends React.Component { this.setReadOnly(); - if (search) { - this.updateRoom(); - } - this.messagebox = React.createRef(); this.list = React.createRef(); this.joinCode = React.createRef(); + this.flatList = React.createRef(); this.mounted = false; // we don't need to subscribe to threads @@ -181,6 +186,9 @@ class RoomView extends React.Component { EventEmitter.addEventListener('connected', this.handleConnected); } } + if (this.jumpToMessageId) { + this.jumpToMessage(this.jumpToMessageId); + } if (isIOS && this.rid) { this.updateUnreadCount(); } @@ -195,7 +203,9 @@ class RoomView extends React.Component { shouldComponentUpdate(nextProps, nextState) { const { state } = this; const { roomUpdate, member } = state; - const { appState, theme, insets } = this.props; + const { + appState, theme, insets, route + } = this.props; if (theme !== nextProps.theme) { return true; } @@ -212,12 +222,19 @@ class RoomView extends React.Component { if (!dequal(nextProps.insets, insets)) { return true; } + if (!dequal(nextProps.route?.params, route?.params)) { + return true; + } return roomAttrsUpdate.some(key => !dequal(nextState.roomUpdate[key], roomUpdate[key])); } componentDidUpdate(prevProps, prevState) { const { roomUpdate } = this.state; - const { appState, insets } = this.props; + const { appState, insets, route } = this.props; + + if (route?.params?.jumpToMessageId !== prevProps.route?.params?.jumpToMessageId) { + this.jumpToMessage(route?.params?.jumpToMessageId); + } if (appState === 'foreground' && appState !== prevProps.appState && this.rid) { // Fire List.query() just to keep observables working @@ -417,34 +434,15 @@ class RoomView extends React.Component { this.setState({ readOnly }); } - updateRoom = async() => { - const db = database.active; - - try { - const subCollection = db.get('subscriptions'); - const sub = await subCollection.find(this.rid); - - const { room } = await RocketChat.getRoomInfo(this.rid); - - await db.action(async() => { - await sub.update((s) => { - Object.assign(s, room); - }); - }); - } catch { - // do nothing - } - } - init = async() => { try { this.setState({ loading: true }); const { room, joined } = this.state; if (this.tmid) { - await this.getThreadMessages(); + await RoomServices.getThreadMessages(this.tmid, this.rid); } else { const newLastOpen = new Date(); - await this.getMessages(room); + await RoomServices.getMessages(room); // if room is joined if (joined) { @@ -453,7 +451,7 @@ class RoomView extends React.Component { } else { this.setLastOpen(null); } - RocketChat.readMessages(room.rid, newLastOpen, true).catch(e => console.log(e)); + RoomServices.readMessages(room.rid, newLastOpen, true).catch(e => console.log(e)); } } @@ -660,26 +658,69 @@ class RoomView extends React.Component { }); }; - onThreadPress = debounce(async(item) => { - const { roomUserId } = this.state; - const { navigation } = this.props; - if (item.tmid) { - if (!item.tmsg) { - await this.fetchThreadName(item.tmid, item.id); - } - let name = item.tmsg; - if (item.t === E2E_MESSAGE_TYPE && item.e2e !== E2E_STATUS.DONE) { - name = I18n.t('Encrypted_message'); - } - navigation.push('RoomView', { - rid: item.subscription.id, tmid: item.tmid, name, t: 'thread', roomUserId - }); - } else if (item.tlm) { - navigation.push('RoomView', { - rid: item.subscription.id, tmid: item.id, name: makeThreadName(item), t: 'thread', roomUserId - }); + onThreadPress = debounce(item => this.navToThread(item), 1000, true) + + shouldNavigateToRoom = (message) => { + if (message.tmid && message.tmid === this.tmid) { + return false; } - }, 1000, true) + if (!message.tmid && message.rid === this.rid) { + return false; + } + return true; + } + + jumpToMessageByUrl = async(messageUrl) => { + if (!messageUrl) { + return; + } + try { + this.setState({ showingBlockingLoader: true }); + const parsedUrl = parse(messageUrl, true); + const messageId = parsedUrl.query.msg; + await this.jumpToMessage(messageId); + this.setState({ showingBlockingLoader: false }); + } catch (e) { + this.setState({ showingBlockingLoader: false }); + log(e); + } + } + + jumpToMessage = async(messageId) => { + try { + this.setState({ showingBlockingLoader: true }); + const message = await RoomServices.getMessageInfo(messageId); + + if (!message) { + return; + } + + if (this.shouldNavigateToRoom(message)) { + if (message.rid !== this.rid) { + this.navToRoom(message); + } else { + this.navToThread(message); + } + } else { + /** + * if it's from server, we don't have it saved locally and so we fetch surroundings + * we test if it's not from threads because we're fetching from threads currently with `getThreadMessages` + */ + if (message.fromServer && !message.tmid) { + await RocketChat.loadSurroundingMessages({ messageId, rid: this.rid }); + } + await Promise.race([ + this.list.current.jumpToMessage(message.id), + new Promise(res => setTimeout(res, 5000)) + ]); + this.list.current.cancelJumpToMessage(); + } + } catch (e) { + log(e); + } finally { + this.setState({ showingBlockingLoader: false }); + } + } replyBroadcast = (message) => { const { replyBroadcast } = this.props; @@ -718,17 +759,6 @@ class RoomView extends React.Component { }); }; - getMessages = () => { - const { room } = this.state; - if (room.lastOpen) { - return RocketChat.loadMissedMessages(room); - } else { - return RocketChat.loadMessagesForRoom(room); - } - } - - getThreadMessages = () => RocketChat.loadThreadMessages({ tmid: this.tmid, rid: this.rid }) - getCustomEmoji = (name) => { const { customEmojis } = this.props; const emoji = customEmojis[name]; @@ -767,45 +797,7 @@ class RoomView extends React.Component { } } - // eslint-disable-next-line react/sort-comp - fetchThreadName = async(tmid, messageId) => { - try { - const db = database.active; - const threadCollection = db.get('threads'); - const messageCollection = db.get('messages'); - const messageRecord = await messageCollection.find(messageId); - let threadRecord; - try { - threadRecord = await threadCollection.find(tmid); - } catch (error) { - console.log('Thread not found. We have to search for it.'); - } - if (threadRecord) { - await db.action(async() => { - await messageRecord.update((m) => { - m.tmsg = threadRecord.msg || (threadRecord.attachments && threadRecord.attachments.length && threadRecord.attachments[0].title); - }); - }); - } else { - let { message: thread } = await RocketChat.getSingleMessage(tmid); - thread = await Encryption.decryptMessage(thread); - await db.action(async() => { - await db.batch( - threadCollection.prepareCreate((t) => { - t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema); - t.subscription.id = this.rid; - Object.assign(t, thread); - }), - messageRecord.prepareUpdate((m) => { - m.tmsg = thread.msg || (thread.attachments && thread.attachments.length && thread.attachments[0].title); - }) - ); - }); - } - } catch (e) { - // log(e); - } - } + getThreadName = (tmid, messageId) => getThreadName(this.rid, tmid, messageId) toggleFollowThread = async(isFollowingThread, tmid) => { try { @@ -836,6 +828,38 @@ class RoomView extends React.Component { } } + navToThread = async(item) => { + const { roomUserId } = this.state; + const { navigation } = this.props; + + if (item.tmid) { + let name = item.tmsg; + if (!name) { + name = await this.getThreadName(item.tmid, item.id); + } + if (item.t === E2E_MESSAGE_TYPE && item.e2e !== E2E_STATUS.DONE) { + name = I18n.t('Encrypted_message'); + } + return navigation.push('RoomView', { + rid: this.rid, tmid: item.tmid, name, t: 'thread', roomUserId, jumpToMessageId: item.id + }); + } + + if (item.tlm) { + return navigation.push('RoomView', { + rid: this.rid, tmid: item.id, name: makeThreadName(item), t: 'thread', roomUserId + }); + } + } + + navToRoom = async(message) => { + const { navigation, isMasterDetail } = this.props; + const roomInfo = await getRoomInfo(message.rid); + return goRoom({ + item: roomInfo, isMasterDetail, navigationMethod: navigation.push, jumpToMessageId: message.id + }); + } + callJitsi = () => { const { room } = this.state; const { jitsiTimeout } = room; @@ -900,7 +924,11 @@ class RoomView extends React.Component { return room?.ignored?.includes?.(message?.u?._id) ?? false; } - renderItem = (item, previousItem) => { + onLoadMoreMessages = loaderItem => RoomServices.getMoreMessages({ + rid: this.rid, tmid: this.tmid, t: this.t, loaderItem + }) + + renderItem = (item, previousItem, highlightedMessage) => { const { room, lastOpen, canAutoTranslate } = this.state; const { user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, Message_Read_Receipt_Enabled, theme @@ -920,48 +948,55 @@ class RoomView extends React.Component { } } - const message = ( - - ); + let content = null; + if (MESSAGE_TYPE_ANY_LOAD.includes(item.t)) { + content = this.onLoadMoreMessages(item)} type={item.t} runOnRender={item.t === MESSAGE_TYPE_LOAD_MORE && !previousItem} />; + } else { + content = ( + + ); + } if (showUnreadSeparator || dateSeparator) { return ( <> - {message} + {content} { @@ -1057,12 +1092,10 @@ class RoomView extends React.Component { ); } - setListRef = ref => this.flatList = ref; - render() { console.count(`${ this.constructor.name }.render calls`); const { - room, reactionsModalVisible, selectedMessage, loading, reacting + room, reactionsModalVisible, selectedMessage, loading, reacting, showingBlockingLoader } = this.state; const { user, baseUrl, theme, navigation, Hide_System_Messages, width, height @@ -1087,7 +1120,7 @@ class RoomView extends React.Component { /> + ); } diff --git a/app/views/RoomView/services/getMessageInfo.js b/app/views/RoomView/services/getMessageInfo.js new file mode 100644 index 000000000..f7f008c46 --- /dev/null +++ b/app/views/RoomView/services/getMessageInfo.js @@ -0,0 +1,41 @@ +import { getMessageById } from '../../../lib/database/services/Message'; +import { getThreadMessageById } from '../../../lib/database/services/ThreadMessage'; +import getSingleMessage from '../../../lib/methods/getSingleMessage'; + +const getMessageInfo = async(messageId) => { + let result; + result = await getMessageById(messageId); + if (result) { + return { + id: result.id, + rid: result.subscription.id, + tmid: result.tmid, + msg: result.msg + }; + } + + result = await getThreadMessageById(messageId); + if (result) { + return { + id: result.id, + rid: result.subscription.id, + tmid: result.rid, + msg: result.msg + }; + } + + result = await getSingleMessage(messageId); + if (result) { + return { + id: result._id, + rid: result.rid, + tmid: result.tmid, + msg: result.msg, + fromServer: true + }; + } + + return null; +}; + +export default getMessageInfo; diff --git a/app/views/RoomView/services/getMessages.js b/app/views/RoomView/services/getMessages.js new file mode 100644 index 000000000..7e9c03de0 --- /dev/null +++ b/app/views/RoomView/services/getMessages.js @@ -0,0 +1,10 @@ +import RocketChat from '../../../lib/rocketchat'; + +const getMessages = (room) => { + if (room.lastOpen) { + return RocketChat.loadMissedMessages(room); + } else { + return RocketChat.loadMessagesForRoom(room); + } +}; +export default getMessages; diff --git a/app/views/RoomView/services/getMoreMessages.js b/app/views/RoomView/services/getMoreMessages.js new file mode 100644 index 000000000..6d16f69c2 --- /dev/null +++ b/app/views/RoomView/services/getMoreMessages.js @@ -0,0 +1,19 @@ +import { MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_NEXT_CHUNK, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK } from '../../../constants/messageTypeLoad'; +import RocketChat from '../../../lib/rocketchat'; + +const getMoreMessages = ({ + rid, t, tmid, loaderItem +}) => { + if ([MESSAGE_TYPE_LOAD_MORE, MESSAGE_TYPE_LOAD_PREVIOUS_CHUNK].includes(loaderItem.t)) { + return RocketChat.loadMessagesForRoom({ + rid, t, latest: loaderItem.ts, loaderItem + }); + } + + if (loaderItem.t === MESSAGE_TYPE_LOAD_NEXT_CHUNK) { + return RocketChat.loadNextMessages({ + rid, tmid, ts: loaderItem.ts, loaderItem + }); + } +}; +export default getMoreMessages; diff --git a/app/views/RoomView/services/getThreadMessages.js b/app/views/RoomView/services/getThreadMessages.js new file mode 100644 index 000000000..0f9529cfc --- /dev/null +++ b/app/views/RoomView/services/getThreadMessages.js @@ -0,0 +1,6 @@ +import RocketChat from '../../../lib/rocketchat'; + +// unlike getMessages, sync isn't required for threads, because loadMissedMessages does it already +const getThreadMessages = (tmid, rid) => RocketChat.loadThreadMessages({ tmid, rid }); + +export default getThreadMessages; diff --git a/app/views/RoomView/services/index.js b/app/views/RoomView/services/index.js new file mode 100644 index 000000000..f8799cda8 --- /dev/null +++ b/app/views/RoomView/services/index.js @@ -0,0 +1,13 @@ +import getMessages from './getMessages'; +import getMoreMessages from './getMoreMessages'; +import getThreadMessages from './getThreadMessages'; +import readMessages from './readMessages'; +import getMessageInfo from './getMessageInfo'; + +export default { + getMessages, + getMoreMessages, + getThreadMessages, + readMessages, + getMessageInfo +}; diff --git a/app/views/RoomView/services/readMessages.js b/app/views/RoomView/services/readMessages.js new file mode 100644 index 000000000..060d9aa7e --- /dev/null +++ b/app/views/RoomView/services/readMessages.js @@ -0,0 +1,5 @@ +import RocketChat from '../../../lib/rocketchat'; + +const readMessages = (rid, newLastOpen) => RocketChat.readMessages(rid, newLastOpen, true); + +export default readMessages; diff --git a/app/views/RoomView/styles.js b/app/views/RoomView/styles.js index fdbb61a7b..4f84d8f7a 100644 --- a/app/views/RoomView/styles.js +++ b/app/views/RoomView/styles.js @@ -9,12 +9,6 @@ export default StyleSheet.create({ safeAreaView: { flex: 1 }, - list: { - flex: 1 - }, - contentContainer: { - paddingTop: 10 - }, readOnly: { justifyContent: 'flex-end', alignItems: 'center', diff --git a/app/views/SearchMessagesView/index.js b/app/views/SearchMessagesView/index.js index e11a5b83d..09c9e6c15 100644 --- a/app/views/SearchMessagesView/index.js +++ b/app/views/SearchMessagesView/index.js @@ -23,6 +23,8 @@ import SafeAreaView from '../../containers/SafeAreaView'; import * as HeaderButton from '../../containers/HeaderButton'; import database from '../../lib/database'; import { sanitizeLikeString } from '../../lib/database/utils'; +import getThreadName from '../../lib/methods/getThreadName'; +import getRoomInfo from '../../lib/methods/getRoomInfo'; class SearchMessagesView extends React.Component { static navigationOptions = ({ navigation, route }) => { @@ -54,9 +56,14 @@ class SearchMessagesView extends React.Component { searchText: '' }; this.rid = props.route.params?.rid; + this.t = props.route.params?.t; this.encrypted = props.route.params?.encrypted; } + async componentDidMount() { + this.room = await getRoomInfo(this.rid); + } + shouldComponentUpdate(nextProps, nextState) { const { loading, searchText, messages } = this.state; const { theme } = this.props; @@ -126,6 +133,11 @@ class SearchMessagesView extends React.Component { return null; } + showAttachment = (attachment) => { + const { navigation } = this.props; + navigation.navigate('AttachmentView', { attachment }); + } + navToRoomInfo = (navParam) => { const { navigation, user } = this.props; if (navParam.rid === user.id) { @@ -134,6 +146,28 @@ class SearchMessagesView extends React.Component { navigation.navigate('RoomInfoView', navParam); } + jumpToMessage = async({ item }) => { + const { navigation } = this.props; + let params = { + rid: this.rid, + jumpToMessageId: item._id, + t: this.t, + room: this.room + }; + if (item.tmid) { + navigation.pop(); + params = { + ...params, + tmid: item.tmid, + name: await getThreadName(this.rid, item.tmid, item._id), + t: 'thread' + }; + navigation.push('RoomView', params); + } else { + navigation.navigate('RoomView', params); + } + } + renderEmpty = () => { const { theme } = this.props; return ( @@ -152,13 +186,16 @@ class SearchMessagesView extends React.Component { item={item} baseUrl={baseUrl} user={user} - timeFormat='LLL' + timeFormat='MMM Do YYYY, h:mm:ss a' isHeader - showAttachment={() => {}} + isThreadRoom + showAttachment={this.showAttachment} getCustomEmoji={this.getCustomEmoji} navToRoomInfo={this.navToRoomInfo} useRealName={useRealName} theme={theme} + onPress={() => this.jumpToMessage({ item })} + jumpToMessage={() => this.jumpToMessage({ item })} /> ); } diff --git a/storybook/stories/Message.js b/storybook/stories/Message.js index 82cfa1b03..6d6d2b4f1 100644 --- a/storybook/stories/Message.js +++ b/storybook/stories/Message.js @@ -40,7 +40,7 @@ const getCustomEmoji = (content) => { return customEmoji; }; -const messageDecorator = story => ( +export const MessageDecorator = story => ( ( ); -const Message = props => ( +export const Message = props => ( ( /> ); +export const StoryProvider = story => {story()}; +const MessageScrollView = story => {story()}; const stories = storiesOf('Message', module) - .addDecorator(story => {story()}) - .addDecorator(story => {story()}) - .addDecorator(messageDecorator); + .addDecorator(StoryProvider) + .addDecorator(MessageScrollView) + .addDecorator(MessageDecorator); stories.add('Basic', () => ( <> diff --git a/storybook/stories/index.js b/storybook/stories/index.js index 20bfc4f25..b695d49d6 100644 --- a/storybook/stories/index.js +++ b/storybook/stories/index.js @@ -14,6 +14,7 @@ import '../../app/views/ThreadMessagesView/Item.stories.js'; import './Avatar'; import '../../app/containers/BackgroundContainer/index.stories.js'; import '../../app/containers/RoomHeader/RoomHeader.stories.js'; +import '../../app/views/RoomView/LoadMore/LoadMore.stories'; // Change here to see themed storybook export const theme = 'light'; From 6d3bcfbd67658abeb33c70bc2eb8e8b9cc2119c6 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Wed, 26 May 2021 16:32:30 -0300 Subject: [PATCH 11/44] [FIX] Method calls not sending date params as EJSON (#3159) --- app/lib/rocketchat.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 561e02c73..c1a7655f4 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -945,7 +945,13 @@ const RocketChat = { if (API_Use_REST_For_DDP_Calls) { return this.post(`method.call/${ method }`, { message: EJSON.stringify({ method, params }) }); } - return this.methodCall(method, ...params); + const parsedParams = params.map((param) => { + if (param instanceof Date) { + return { $date: new Date(param).getTime() }; + } + return param; + }); + return this.methodCall(method, ...parsedParams); }, getUserRoles() { From 17c28e0b1b931b623ebb0127389fdf5e372ac270 Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Wed, 26 May 2021 17:40:46 -0300 Subject: [PATCH 12/44] [FIX] Read receipt not displaying full date (#3133) Co-authored-by: Diego Mello --- app/views/ReadReceiptView/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/ReadReceiptView/index.js b/app/views/ReadReceiptView/index.js index f41d7c873..b413a7674 100644 --- a/app/views/ReadReceiptView/index.js +++ b/app/views/ReadReceiptView/index.js @@ -30,7 +30,7 @@ class ReadReceiptView extends React.Component { static propTypes = { route: PropTypes.object, - Message_TimeFormat: PropTypes.string, + Message_TimeAndDateFormat: PropTypes.string, theme: PropTypes.string } @@ -94,8 +94,8 @@ class ReadReceiptView extends React.Component { } renderItem = ({ item }) => { - const { Message_TimeFormat, theme } = this.props; - const time = moment(item.ts).format(Message_TimeFormat); + const { theme, Message_TimeAndDateFormat } = this.props; + const time = moment(item.ts).format(Message_TimeAndDateFormat); if (!item?.user?.username) { return null; } @@ -156,7 +156,7 @@ class ReadReceiptView extends React.Component { } const mapStateToProps = state => ({ - Message_TimeFormat: state.settings.Message_TimeFormat + Message_TimeAndDateFormat: state.settings.Message_TimeAndDateFormat }); export default connect(mapStateToProps)(withTheme(ReadReceiptView)); From 6dcb9a51f182b76a0cac35ec74e244c529704a80 Mon Sep 17 00:00:00 2001 From: Gerzon Z Date: Wed, 26 May 2021 17:01:06 -0400 Subject: [PATCH 13/44] [NEW] Remove member from team (#3117) * Added Create Team * Added actionTypes, actions, ENG strings for Teams and updated NewMessageView * Added createTeam sagas, createTeam reducer, new Team string and update CreateChannelView * Remove unnecessary actionTypes, reducers and sagas, e2e tests and navigation to team view * Minor tweaks * Show TeamChannelsView only if joined the team * Minor tweak * Added AddChannelTeamView * Added permissions, translations strings for teams, deleteTeamRoom and addTeamRooms, AddExistingChannelView, updated CreateChannelView, TeamChannelsView * Refactor touch component and update removeRoom and deleteRoom methods * Minor tweaks * Minor tweaks for removing channels and addExistingChannelView * Added missing events and fixed channels list * Minor tweaks for refactored touch component * Added SelectListView and logic for leaving team * Added addTeamMember and removeTeamMember * Minor tweak * Minor tweak * Minor tweaks * Remove unnecesary changes, update TeamChannelsView, AddExistingChannelView, AddChannelTeamView, createChannel, goRoom and Touchable * Remove unnecesary prop * Add screens to ModalStack, events, autoJoin, update createChannel, addRoomsToTeam and Touchable * Minor tweak * Update loadMessagesForRoom.js * Updated schema, tag component, touch, AddChannelTeamView, AddExistingChannelView, ActionSheet Item * Fix unnecessary changes * Add i18n, update createChannel, AddExistingChannelTeamView, AddChannelTeamView, RightButton and TeamChannelsView * Updated styles, added tag story * Minor tweak * Minor tweaks * Auto-join tweak * Minor tweaks * Minor tweak on search * Minor refactor to ListItem, add SelectListView to ModalStack, update handleLeaveTeam * Minor tweaks * Update SelectListView * Update handleLeaveTeam, remove unnecessary method, add story * Minor tweak * Minor visual tweaks * Update SelectListView.js * Update RoomMembersView * Updated SelectListView, RoomActionsView, leaveTeam method and string translations * Update SelectListVIew * Minor tweak * Update SelectListView * Minor tweak * Minor tweaks * Fix for List.Item subtitles being pushed down by title's flex * Minor tweaks * Update RoomActionsView * Use showConfirmationAlert and showErrorAlert * Remove addTeamMember, update removeTeamMember * Update Alert * Minor tweaks * Minor tweaks * Minor tweak * Update showActionSheet on RoomMembersView * Remove team main from query and move code around * Fetch roles * Update RoomMembersView and SelectListView * Updated leaveTeam and handleRemoveFromTeam * Fix validation * Remove unnecessary function * Added confirmationAlert for missing permissions case Co-authored-by: Diego Mello --- app/i18n/locales/en.json | 14 ++- app/lib/methods/getPermissions.js | 1 + app/lib/rocketchat.js | 12 ++ app/views/RoomActionsView/index.js | 22 ++-- app/views/RoomMembersView/index.js | 182 ++++++++++++++++++++++------- app/views/SelectListView.js | 15 +-- 6 files changed, 180 insertions(+), 66 deletions(-) diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 1648d3bf2..71403821a 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -683,12 +683,9 @@ "No_threads_following": "You are not following any threads", "No_threads_unread": "There are no unread threads", "Messagebox_Send_to_channel": "Send to channel", - "Set_as_leader": "Set as leader", - "Set_as_moderator": "Set as moderator", - "Set_as_owner": "Set as owner", - "Remove_as_leader": "Remove as leader", - "Remove_as_moderator": "Remove as moderator", - "Remove_as_owner": "Remove as owner", + "Leader": "Leader", + "Moderator": "Moderator", + "Owner": "Owner", "Remove_from_room": "Remove from room", "Ignore": "Ignore", "Unignore": "Unignore", @@ -732,9 +729,14 @@ "Leave_Team": "Leave Team", "Select_Team_Channels": "Select the Team's channels you would like to leave.", "Cannot_leave": "Cannot leave", + "Cannot_remove": "Cannot remove", "Last_owner_team_room": "You are the last owner of this channel. Once you leave the team, the channel will be kept inside the team but you will be managing it from outside.", "last-owner-can-not-be-removed": "Last owner cannot be removed", + "Removing_user_from_this_team": "You are removing {{user}} from this team", + "Remove_User_Team_Channels": "Select the channels you want the user to be removed from.", + "Remove_Member": "Remove Member", "leaving_team": "leaving team", + "removing_team": "removing from team", "member-does-not-exist": "Member does not exist", "Load_More": "Load More", "Load_Newer": "Load Newer", diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js index 1f4bcb9cb..cc1dd46e0 100644 --- a/app/lib/methods/getPermissions.js +++ b/app/lib/methods/getPermissions.js @@ -22,6 +22,7 @@ const PERMISSIONS = [ 'delete-p', 'edit-message', 'edit-room', + 'edit-team-member', 'edit-team-channel', 'force-delete-message', 'mute-user', diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index c1a7655f4..3660b3195 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -778,10 +778,22 @@ const RocketChat = { // RC 3.13.0 return this.post('teams.leave', { teamName, rooms }); }, + removeTeamMember({ + teamId, teamName, userId, rooms + }) { + // RC 3.13.0 + return this.post('teams.removeMember', { + teamId, teamName, userId, rooms + }); + }, updateTeamRoom({ roomId, isDefault }) { // RC 3.13.0 return this.post('teams.updateRoom', { roomId, isDefault }); }, + teamListRoomsOfUser({ teamId, userId }) { + // RC 3.13.0 + return this.sdk.get('teams.listRoomsOfUser', { teamId, userId }); + }, joinRoom(roomId, joinCode, type) { // TODO: join code // RC 0.48.0 diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index 3256dd0b9..89d34ed07 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -5,7 +5,6 @@ import { } from 'react-native'; import { connect } from 'react-redux'; import isEmpty from 'lodash/isEmpty'; -import { Q } from '@nozbe/watermelondb'; import { compareServerVersion, methods } from '../../lib/utils'; import Touch from '../../utils/touch'; @@ -433,14 +432,15 @@ class RoomActionsView extends React.Component { const { navigation } = this.props; try { - const db = database.active; - const subCollection = db.get('subscriptions'); - const teamChannels = await subCollection.query( - Q.where('team_id', room.teamId), - Q.where('team_main', null) - ); + const result = await RocketChat.teamListRoomsOfUser({ teamId: room.teamId, userId: room.u._id }); - if (teamChannels.length) { + if (result.rooms?.length) { + const teamChannels = result.rooms.map(r => ({ + rid: r._id, + name: r.name, + teamId: r.teamId, + alert: r.isLastOwner + })); navigation.navigate('SelectListView', { title: 'Leave_Team', data: teamChannels, @@ -456,7 +456,11 @@ class RoomActionsView extends React.Component { }); } } catch (e) { - log(e); + showConfirmationAlert({ + message: I18n.t('You_are_leaving_the_team', { team: RocketChat.getRoomTitle(room) }), + confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }), + onPress: () => this.handleLeaveTeam() + }); } } diff --git a/app/views/RoomMembersView/index.js b/app/views/RoomMembersView/index.js index 9a47b60f7..59d298d94 100644 --- a/app/views/RoomMembersView/index.js +++ b/app/views/RoomMembersView/index.js @@ -23,9 +23,10 @@ import { withTheme } from '../../theme'; import { themes } from '../../constants/colors'; import { getUserSelector } from '../../selectors/login'; import { withActionSheet } from '../../containers/ActionSheet'; -import { showConfirmationAlert } from '../../utils/info'; +import { showConfirmationAlert, showErrorAlert } from '../../utils/info'; import SafeAreaView from '../../containers/SafeAreaView'; import { goRoom } from '../../utils/goRoom'; +import { CustomIcon } from '../../lib/Icons'; const PAGE_SIZE = 25; @@ -34,6 +35,9 @@ const PERMISSION_SET_LEADER = 'set-leader'; const PERMISSION_SET_OWNER = 'set-owner'; const PERMISSION_SET_MODERATOR = 'set-moderator'; const PERMISSION_REMOVE_USER = 'remove-user'; +const PERMISSION_EDIT_TEAM_MEMBER = 'edit-team-member'; +const PERMISION_VIEW_ALL_TEAMS = 'view-all-teams'; +const PERMISSION_VIEW_ALL_TEAM_CHANNELS = 'view-all-team-channels'; class RoomMembersView extends React.Component { static propTypes = { @@ -55,7 +59,10 @@ class RoomMembersView extends React.Component { setLeaderPermission: PropTypes.array, setOwnerPermission: PropTypes.array, setModeratorPermission: PropTypes.array, - removeUserPermission: PropTypes.array + removeUserPermission: PropTypes.array, + editTeamMemberPermission: PropTypes.array, + viewAllTeamChannelsPermission: PropTypes.array, + viewAllTeamsPermission: PropTypes.array } constructor(props) { @@ -94,10 +101,11 @@ class RoomMembersView extends React.Component { const { room } = this.state; const { - muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission + muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission, editTeamMemberPermission, viewAllTeamChannelsPermission, viewAllTeamsPermission } = this.props; + const result = await RocketChat.hasPermission([ - muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission + muteUserPermission, setLeaderPermission, setOwnerPermission, setModeratorPermission, removeUserPermission, ...(room.teamMain ? [editTeamMemberPermission, viewAllTeamChannelsPermission, viewAllTeamsPermission] : []) ], room.rid); this.permissions = { @@ -105,7 +113,12 @@ class RoomMembersView extends React.Component { [PERMISSION_SET_LEADER]: result[1], [PERMISSION_SET_OWNER]: result[2], [PERMISSION_SET_MODERATOR]: result[3], - [PERMISSION_REMOVE_USER]: result[4] + [PERMISSION_REMOVE_USER]: result[4], + ...(room.teamMain ? { + [PERMISSION_EDIT_TEAM_MEMBER]: result[5], + [PERMISSION_VIEW_ALL_TEAM_CHANNELS]: result[6], + [PERMISION_VIEW_ALL_TEAMS]: result[7] + } : {}) }; const hasSinglePermission = Object.values(this.permissions).some(p => !!p); @@ -163,9 +176,80 @@ class RoomMembersView extends React.Component { } } + handleRemoveFromTeam = async(selectedUser) => { + try { + const { navigation } = this.props; + const { room } = this.state; + + const result = await RocketChat.teamListRoomsOfUser({ teamId: room.teamId, userId: selectedUser._id }); + + if (result.rooms?.length) { + const teamChannels = result.rooms.map(r => ({ + rid: r._id, + name: r.name, + teamId: r.teamId, + alert: r.isLastOwner + })); + navigation.navigate('SelectListView', { + title: 'Remove_Member', + infoText: 'Remove_User_Team_Channels', + data: teamChannels, + nextAction: selected => this.removeFromTeam(selectedUser, selected), + showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_remove')) + }); + } else { + showConfirmationAlert({ + message: I18n.t('Removing_user_from_this_team', { user: selectedUser.username }), + confirmationText: I18n.t('Yes_action_it', { action: I18n.t('remove') }), + onPress: () => this.removeFromTeam(selectedUser) + }); + } + } catch (e) { + showConfirmationAlert({ + message: I18n.t('Removing_user_from_this_team', { user: selectedUser.username }), + confirmationText: I18n.t('Yes_action_it', { action: I18n.t('remove') }), + onPress: () => this.removeFromTeam(selectedUser) + }); + } + } + + removeFromTeam = async(selectedUser, selected) => { + try { + const { members, membersFiltered, room } = this.state; + const { navigation } = this.props; + + const userId = selectedUser._id; + const result = await RocketChat.removeTeamMember({ + teamId: room.teamId, + teamName: room.name, + userId, + ...(selected && { rooms: selected }) + }); + if (result.success) { + const message = I18n.t('User_has_been_removed_from_s', { s: RocketChat.getRoomTitle(room) }); + EventEmitter.emit(LISTENER, { message }); + const newMembers = members.filter(member => member._id !== userId); + const newMembersFiltered = membersFiltered.filter(member => member._id !== userId); + this.setState({ + members: newMembers, + membersFiltered: newMembersFiltered + }); + navigation.navigate('RoomMembersView'); + } + } catch (e) { + log(e); + showErrorAlert( + e.data.error + ? I18n.t(e.data.error) + : I18n.t('There_was_an_error_while_action', { action: I18n.t('removing_team') }), + I18n.t('Cannot_remove') + ); + } + } + onPressUser = (selectedUser) => { const { room } = this.state; - const { showActionSheet, user } = this.props; + const { showActionSheet, user, theme } = this.props; const options = [{ icon: 'message', @@ -173,39 +257,6 @@ class RoomMembersView extends React.Component { onPress: () => this.navToDirectMessage(selectedUser) }]; - // Owner - if (this.permissions['set-owner']) { - const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id); - const isOwner = userRoleResult?.roles.includes('owner'); - options.push({ - icon: 'shield-check', - title: I18n.t(isOwner ? 'Remove_as_owner' : 'Set_as_owner'), - onPress: () => this.handleOwner(selectedUser, !isOwner) - }); - } - - // Leader - if (this.permissions['set-leader']) { - const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id); - const isLeader = userRoleResult?.roles.includes('leader'); - options.push({ - icon: 'shield-alt', - title: I18n.t(isLeader ? 'Remove_as_leader' : 'Set_as_leader'), - onPress: () => this.handleLeader(selectedUser, !isLeader) - }); - } - - // Moderator - if (this.permissions['set-moderator']) { - const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id); - const isModerator = userRoleResult?.roles.includes('moderator'); - options.push({ - icon: 'shield', - title: I18n.t(isModerator ? 'Remove_as_moderator' : 'Set_as_moderator'), - onPress: () => this.handleModerator(selectedUser, !isModerator) - }); - } - // Ignore if (selectedUser._id !== user.id) { const { ignored } = room; @@ -236,8 +287,54 @@ class RoomMembersView extends React.Component { }); } + // Owner + if (this.permissions['set-owner']) { + const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id); + const isOwner = userRoleResult?.roles.includes('owner'); + options.push({ + icon: 'shield-check', + title: I18n.t('Owner'), + onPress: () => this.handleOwner(selectedUser, !isOwner), + right: () => + }); + } + + // Leader + if (this.permissions['set-leader']) { + const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id); + const isLeader = userRoleResult?.roles.includes('leader'); + options.push({ + icon: 'shield-alt', + title: I18n.t('Leader'), + onPress: () => this.handleLeader(selectedUser, !isLeader), + right: () => + }); + } + + // Moderator + if (this.permissions['set-moderator']) { + const userRoleResult = this.roomRoles.find(r => r.u._id === selectedUser._id); + const isModerator = userRoleResult?.roles.includes('moderator'); + options.push({ + icon: 'shield', + title: I18n.t('Moderator'), + onPress: () => this.handleModerator(selectedUser, !isModerator), + right: () => + }); + } + + // Remove from team + if (this.permissions['edit-team-member']) { + options.push({ + icon: 'logout', + danger: true, + title: I18n.t('Remove_from_Team'), + onPress: () => this.handleRemoveFromTeam(selectedUser) + }); + } + // Remove from room - if (this.permissions['remove-user']) { + if (this.permissions['remove-user'] && !room.teamMain) { options.push({ icon: 'logout', title: I18n.t('Remove_from_room'), @@ -477,7 +574,10 @@ const mapStateToProps = state => ({ setLeaderPermission: state.permissions[PERMISSION_SET_LEADER], setOwnerPermission: state.permissions[PERMISSION_SET_OWNER], setModeratorPermission: state.permissions[PERMISSION_SET_MODERATOR], - removeUserPermission: state.permissions[PERMISSION_REMOVE_USER] + removeUserPermission: state.permissions[PERMISSION_REMOVE_USER], + editTeamMemberPermission: state.permissions[PERMISSION_EDIT_TEAM_MEMBER], + viewAllTeamChannelsPermission: state.permissions[PERMISSION_VIEW_ALL_TEAM_CHANNELS], + viewAllTeamsPermission: state.permissions[PERMISION_VIEW_ALL_TEAMS] }); export default connect(mapStateToProps)(withActionSheet(withTheme(RoomMembersView))); diff --git a/app/views/SelectListView.js b/app/views/SelectListView.js index 9c886da80..bcbb0923f 100644 --- a/app/views/SelectListView.js +++ b/app/views/SelectListView.js @@ -14,7 +14,6 @@ import { themes } from '../constants/colors'; import { withTheme } from '../theme'; import SafeAreaView from '../containers/SafeAreaView'; import { animateNextTransition } from '../utils/layoutAnimation'; -import Loading from '../containers/Loading'; const styles = StyleSheet.create({ buttonText: { @@ -41,8 +40,7 @@ class SelectListView extends React.Component { this.showAlert = props.route?.params?.showAlert; this.state = { data, - selected: [], - loading: false + selected: [] }; this.setHeader(); } @@ -96,10 +94,8 @@ class SelectListView extends React.Component { renderItem = ({ item }) => { const { theme } = this.props; - const alert = item.roles.length; - const icon = item.t === 'p' ? 'channel-private' : 'channel-public'; - const checked = this.isChecked(item.rid, item.roles) ? 'check' : null; + const checked = this.isChecked(item.rid) ? 'check' : null; return ( <> @@ -108,8 +104,8 @@ class SelectListView extends React.Component { title={item.name} translateTitle={false} testID={`select-list-view-item-${ item.name }`} - onPress={() => (alert ? this.showAlert() : this.toggleItem(item.rid))} - alert={alert} + onPress={() => (item.alert ? this.showAlert() : this.toggleItem(item.rid))} + alert={item.alert} left={() => } right={() => (checked ? : null)} /> @@ -118,7 +114,7 @@ class SelectListView extends React.Component { } render() { - const { loading, data } = this.state; + const { data } = this.state; const { theme } = this.props; return ( @@ -133,7 +129,6 @@ class SelectListView extends React.Component { contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }} keyboardShouldPersistTaps='always' /> - ); } From 5c4772427a9d9195ba1bce2597168baaf41ee1e2 Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Wed, 26 May 2021 18:10:20 -0300 Subject: [PATCH 14/44] [FIX] Add Existing Channel screen showing discussions and channels without permission (#3151) * [Fix] the filter to show the existing channel * [Refactor] the function that filter to isolate it * Refactor how to wsearch properly Co-authored-by: Diego Mello --- app/views/AddExistingChannelView.js | 46 ++++++++++++++++------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/app/views/AddExistingChannelView.js b/app/views/AddExistingChannelView.js index a3acf9e23..03cfaa4f1 100644 --- a/app/views/AddExistingChannelView.js +++ b/app/views/AddExistingChannelView.js @@ -17,9 +17,10 @@ import StatusBar from '../containers/StatusBar'; import { themes } from '../constants/colors'; import { withTheme } from '../theme'; import SafeAreaView from '../containers/SafeAreaView'; +import Loading from '../containers/Loading'; import { animateNextTransition } from '../utils/layoutAnimation'; import { goRoom } from '../utils/goRoom'; -import Loading from '../containers/Loading'; +import debounce from '../utils/debounce'; const QUERY_SIZE = 50; @@ -34,7 +35,7 @@ class AddExistingChannelView extends React.Component { constructor(props) { super(props); - this.init(); + this.query(); this.teamId = props.route?.params?.teamId; this.state = { search: [], @@ -66,7 +67,7 @@ class AddExistingChannelView extends React.Component { navigation.setOptions(options); } - init = async() => { + query = async(stringToSearch = '') => { try { const { addTeamChannelPermission } = this.props; const db = database.active; @@ -75,39 +76,42 @@ class AddExistingChannelView extends React.Component { .query( Q.where('team_id', ''), Q.where('t', Q.oneOf(['c', 'p'])), + Q.where('name', Q.like(`%${ stringToSearch }%`)), Q.experimentalTake(QUERY_SIZE), Q.experimentalSortBy('room_updated_at', Q.desc) ) .fetch(); - const filteredChannels = channels.filter(async(channel) => { - const permissions = await RocketChat.hasPermission([addTeamChannelPermission], channel.rid); - if (!permissions[0]) { - return; - } - return channel; - }); - this.setState({ channels: filteredChannels }); + + const asyncFilter = async(channelsArray) => { + const results = await Promise.all(channelsArray.map(async(channel) => { + if (channel.prid) { + return false; + } + const permissions = await RocketChat.hasPermission([addTeamChannelPermission], channel.rid); + if (!permissions[0]) { + return false; + } + return true; + })); + + return channelsArray.filter((_v, index) => results[index]); + }; + const channelFiltered = await asyncFilter(channels); + this.setState({ channels: channelFiltered }); } catch (e) { log(e); } } - onSearchChangeText(text) { - this.search(text); - } + onSearchChangeText = debounce((text) => { + this.query(text); + }, 300) dismiss = () => { const { navigation } = this.props; return navigation.pop(); } - search = async(text) => { - const result = await RocketChat.search({ text, filterUsers: false }); - this.setState({ - search: result - }); - } - submit = async() => { const { selected } = this.state; const { isMasterDetail } = this.props; From c10bd5fd9332bcb76f6e04e3eaa088149fa25837 Mon Sep 17 00:00:00 2001 From: Arkadyuti Bandyopadhyay Date: Thu, 27 May 2021 02:44:42 +0530 Subject: [PATCH 15/44] [FIX] Member search not trimming search text (#3129) * Fixed logout toast bug for the iOS * Removing callToAction and replacing with confirmationText * Handling member search with spaces to the left and right of name/username * Changing location of string trimmer Co-authored-by: Diego Mello --- app/views/RoomMembersView/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/RoomMembersView/index.js b/app/views/RoomMembersView/index.js index 59d298d94..5819cf512 100644 --- a/app/views/RoomMembersView/index.js +++ b/app/views/RoomMembersView/index.js @@ -150,6 +150,7 @@ class RoomMembersView extends React.Component { onSearchChangeText = protectedFunction((text) => { const { members } = this.state; let membersFiltered = []; + text = text.trim(); if (members && members.length > 0 && text) { membersFiltered = members.filter(m => m.username.toLowerCase().match(text.toLowerCase()) || m.name.toLowerCase().match(text.toLowerCase())); From 0bbeb422711c71e35e0c74b8ac1c3a0f157579ee Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Thu, 27 May 2021 14:23:17 -0300 Subject: [PATCH 16/44] [FIX] Discussions not subscribing properly to messages when opened from inside the room (#3149) * [FIX] Promise at subscription Room * E2E - Update previous roomView count after send msg in discussion * Not needed rn Co-authored-by: Diego Mello --- app/lib/methods/subscriptions/room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/methods/subscriptions/room.js b/app/lib/methods/subscriptions/room.js index 885c2469a..2d9ba8f90 100644 --- a/app/lib/methods/subscriptions/room.js +++ b/app/lib/methods/subscriptions/room.js @@ -159,7 +159,7 @@ export default class RoomSubscription { updateMessage = message => ( new Promise(async(resolve) => { if (this.rid !== message.rid) { - return; + return resolve(); } const db = database.active; From 981b3688f1912d5c8b41567f34687769a34c1b68 Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Thu, 27 May 2021 14:27:24 -0300 Subject: [PATCH 17/44] [FIX] Team creation not raising error if something unexpected happens (#3152) * [IMPROVEMENT] Add error to AddExistingChannel * Fix the alert error when create a channel * Fix the error alert box when create channel and teams Co-authored-by: Diego Mello --- app/actions/createChannel.js | 5 +++-- app/i18n/locales/en.json | 1 + app/i18n/locales/pt-BR.json | 3 ++- app/sagas/createChannel.js | 8 ++++---- app/views/AddExistingChannelView.js | 2 ++ 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/actions/createChannel.js b/app/actions/createChannel.js index 60a8cca30..c93b47ef4 100644 --- a/app/actions/createChannel.js +++ b/app/actions/createChannel.js @@ -14,9 +14,10 @@ export function createChannelSuccess(data) { }; } -export function createChannelFailure(err) { +export function createChannelFailure(err, isTeam) { return { type: types.CREATE_CHANNEL.FAILURE, - err + err, + isTeam }; } diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 71403821a..76d60f21f 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -61,6 +61,7 @@ "error-message-editing-blocked": "Message editing is blocked", "error-message-size-exceeded": "Message size exceeds Message_MaxAllowedSize", "error-missing-unsubscribe-link": "You must provide the [unsubscribe] link.", + "error-no-owner-channel":"You don't own the channel", "error-no-tokens-for-this-user": "There are no tokens for this user", "error-not-allowed": "Not allowed", "error-not-authorized": "Not authorized", diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index 64369e781..7eca8b049 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -667,5 +667,6 @@ "Teams": "Times", "No_team_channels_found": "Nenhum canal encontrado", "Team_not_found": "Time não encontrado", - "Private_Team": "Equipe Privada" + "Private_Team": "Equipe Privada", + "Add_Existing_Channel": "Adicionar Canal Existente" } \ No newline at end of file diff --git a/app/sagas/createChannel.js b/app/sagas/createChannel.js index 8979e230e..a768916c8 100644 --- a/app/sagas/createChannel.js +++ b/app/sagas/createChannel.js @@ -96,7 +96,7 @@ const handleRequest = function* handleRequest({ data }) { yield put(createChannelSuccess(sub)); } catch (err) { logEvent(events[data.group ? 'SELECTED_USERS_CREATE_GROUP_F' : 'CR_CREATE_F']); - yield put(createChannelFailure(err)); + yield put(createChannelFailure(err, data.isTeam)); } }; @@ -108,10 +108,10 @@ const handleSuccess = function* handleSuccess({ data }) { goRoom({ item: data, isMasterDetail }); }; -const handleFailure = function handleFailure({ err }) { +const handleFailure = function handleFailure({ err, isTeam }) { setTimeout(() => { - const msg = err.data ? I18n.t(err.data.error) : err.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') }); - showErrorAlert(msg); + const msg = err.data.errorType ? I18n.t(err.data.errorType, { room_name: err.data.details.channel_name }) : err.reason || I18n.t('There_was_an_error_while_action', { action: isTeam ? I18n.t('creating_team') : I18n.t('creating_channel') }); + showErrorAlert(msg, isTeam ? I18n.t('Create_Team') : I18n.t('Create_Channel')); }, 300); }; diff --git a/app/views/AddExistingChannelView.js b/app/views/AddExistingChannelView.js index 03cfaa4f1..c1c94fec8 100644 --- a/app/views/AddExistingChannelView.js +++ b/app/views/AddExistingChannelView.js @@ -20,6 +20,7 @@ import SafeAreaView from '../containers/SafeAreaView'; import Loading from '../containers/Loading'; import { animateNextTransition } from '../utils/layoutAnimation'; import { goRoom } from '../utils/goRoom'; +import { showErrorAlert } from '../utils/info'; import debounce from '../utils/debounce'; const QUERY_SIZE = 50; @@ -125,6 +126,7 @@ class AddExistingChannelView extends React.Component { goRoom({ item: result, isMasterDetail }); } } catch (e) { + showErrorAlert(I18n.t(e.data.error), I18n.t('Add_Existing_Channel'), () => {}); logEvent(events.CT_ADD_ROOM_TO_TEAM_F); this.setState({ loading: false }); } From 6e32d1dc6c699f83ff1f31732d401cc6a7cda7a9 Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Thu, 27 May 2021 15:07:21 -0300 Subject: [PATCH 18/44] [FIX] Check permissions on team channels action sheet (#3155) * [IMPROVEMENT] Show only the option that user can manage in TeamChannelsView * Refactor the showActionSheet function * Added remove team channel permission * Cleanup Co-authored-by: Diego Mello --- app/views/TeamChannelsView.js | 87 ++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 33 deletions(-) diff --git a/app/views/TeamChannelsView.js b/app/views/TeamChannelsView.js index 03398edc9..1d4b09246 100644 --- a/app/views/TeamChannelsView.js +++ b/app/views/TeamChannelsView.js @@ -32,6 +32,12 @@ import { CustomIcon } from '../lib/Icons'; import { themes } from '../constants/colors'; const API_FETCH_COUNT = 25; +const PERMISSION_DELETE_C = 'delete-c'; +const PERMISSION_DELETE_P = 'delete-p'; +const PERMISSION_EDIT_TEAM_CHANNEL = 'edit-team-channel'; +const PERMISSION_REMOVE_TEAM_CHANNEL = 'remove-team-channel'; +const PERMISSION_ADD_TEAM_CHANNEL = 'add-team-channel'; + const getItemLayout = (data, index) => ({ length: data.length, @@ -51,7 +57,10 @@ class TeamChannelsView extends React.Component { width: PropTypes.number, StoreLastMessage: PropTypes.bool, addTeamChannelPermission: PropTypes.array, + editTeamChannelPermission: PropTypes.array, removeTeamChannelPermission: PropTypes.array, + deleteCPermission: PropTypes.array, + deletePPermission: PropTypes.array, showActionSheet: PropTypes.func, deleteRoom: PropTypes.func } @@ -291,33 +300,6 @@ class TeamChannelsView extends React.Component { } }, 1000, true); - options = (item) => { - const { theme } = this.props; - const isAutoJoinChecked = item.teamDefault; - const autoJoinIcon = isAutoJoinChecked ? 'checkbox-checked' : 'checkbox-unchecked'; - const autoJoinIconColor = isAutoJoinChecked ? themes[theme].tintActive : themes[theme].auxiliaryTintColor; - return ([ - { - title: I18n.t('Auto-join'), - icon: item.t === 'p' ? 'channel-private' : 'channel-public', - onPress: () => this.toggleAutoJoin(item), - right: () => - }, - { - title: I18n.t('Remove_from_Team'), - icon: 'close', - danger: true, - onPress: () => this.remove(item) - }, - { - title: I18n.t('Delete'), - icon: 'delete', - danger: true, - onPress: () => this.delete(item) - } - ]); - } - toggleAutoJoin = async(item) => { try { const { data } = this.state; @@ -391,13 +373,49 @@ class TeamChannelsView extends React.Component { showChannelActions = async(item) => { logEvent(events.ROOM_SHOW_BOX_ACTIONS); - const { showActionSheet, removeTeamChannelPermission } = this.props; + const { + showActionSheet, editTeamChannelPermission, deleteCPermission, deletePPermission, theme, removeTeamChannelPermission + } = this.props; + const isAutoJoinChecked = item.teamDefault; + const autoJoinIcon = isAutoJoinChecked ? 'checkbox-checked' : 'checkbox-unchecked'; + const autoJoinIconColor = isAutoJoinChecked ? themes[theme].tintActive : themes[theme].auxiliaryTintColor; - const permissions = await RocketChat.hasPermission([removeTeamChannelPermission], this.team.rid); - if (!permissions[0]) { + const options = []; + + const permissionsTeam = await RocketChat.hasPermission([editTeamChannelPermission], this.team.rid); + if (permissionsTeam[0]) { + options.push({ + title: I18n.t('Auto-join'), + icon: item.t === 'p' ? 'channel-private' : 'channel-public', + onPress: () => this.toggleAutoJoin(item), + right: () => + }); + } + + const permissionsRemoveTeam = await RocketChat.hasPermission([removeTeamChannelPermission], this.team.rid); + if (permissionsRemoveTeam[0]) { + options.push({ + title: I18n.t('Remove_from_Team'), + icon: 'close', + danger: true, + onPress: () => this.remove(item) + }); + } + + const permissionsChannel = await RocketChat.hasPermission([item.t === 'c' ? deleteCPermission : deletePPermission], item._id); + if (permissionsChannel[0]) { + options.push({ + title: I18n.t('Delete'), + icon: 'delete', + danger: true, + onPress: () => this.delete(item) + }); + } + + if (options.length === 0) { return; } - showActionSheet({ options: this.options(item) }); + showActionSheet({ options }); } renderItem = ({ item }) => { @@ -481,8 +499,11 @@ const mapStateToProps = state => ({ useRealName: state.settings.UI_Use_Real_Name, isMasterDetail: state.app.isMasterDetail, StoreLastMessage: state.settings.Store_Last_Message, - addTeamChannelPermission: state.permissions['add-team-channel'], - removeTeamChannelPermission: state.permissions['remove-team-channel'] + addTeamChannelPermission: state.permissions[PERMISSION_ADD_TEAM_CHANNEL], + editTeamChannelPermission: state.permissions[PERMISSION_EDIT_TEAM_CHANNEL], + removeTeamChannelPermission: state.permissions[PERMISSION_REMOVE_TEAM_CHANNEL], + deleteCPermission: state.permissions[PERMISSION_DELETE_C], + deletePPermission: state.permissions[PERMISSION_DELETE_P] }); const mapDispatchToProps = dispatch => ({ From 5697a136c23c3ae03d31fa40cf9dda1e98d1c95c Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Thu, 27 May 2021 17:41:33 -0300 Subject: [PATCH 19/44] [FIX] Add channels to team's flow using different navigators (#3157) * [FIX] the navigation to AddChannelTeamView and next screens * Fix the order inside the NewMessageStackNavigator * Delete spaces after arrow function in onPress * Adjusted InsideStackNavigator to a conditional animation * Fixed route for iPad * Small change Co-authored-by: Diego Mello --- app/stacks/InsideStack.js | 5 +++++ app/views/AddChannelTeamView.js | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/stacks/InsideStack.js b/app/stacks/InsideStack.js index 18517368c..83fc22240 100644 --- a/app/stacks/InsideStack.js +++ b/app/stacks/InsideStack.js @@ -182,6 +182,11 @@ const ChatsStackNavigator = () => { component={TeamChannelsView} options={TeamChannelsView.navigationOptions} /> + navigation.navigate('NewMessageStackNavigator', { screen: 'SelectedUsersViewCreateChannel', params: { nextAction: () => navigation.navigate('CreateChannelView', { teamId }) } })} + onPress={() => (isMasterDetail + ? navigation.navigate('SelectedUsersViewCreateChannel', { nextAction: () => navigation.navigate('CreateChannelView', { teamId }) }) + : navigation.navigate('SelectedUsersView', { nextAction: () => navigation.navigate('ChatsStackNavigator', { screen: 'CreateChannelView', params: { teamId } }) })) + } testID='add-channel-team-view-create-channel' left={() => } right={() => } From b833a2f8aaf4ced12d18cbe7b48552fe77db7137 Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Thu, 27 May 2021 17:44:35 -0300 Subject: [PATCH 20/44] [IMPROVEMENT] Allow discussions to be edited (#3137) * Refactored the filter to work the edit for channel and discussion * Removed the filter which type of room can be edit Co-authored-by: Diego Mello * Fix tests Co-authored-by: Diego Mello --- app/views/RoomInfoView/index.js | 2 +- e2e/tests/room/04-discussion.spec.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js index a49aab5b1..93a7e3a16 100644 --- a/app/views/RoomInfoView/index.js +++ b/app/views/RoomInfoView/index.js @@ -214,7 +214,7 @@ class RoomInfoView extends React.Component { } const permissions = await RocketChat.hasPermission([editRoomPermission], room.rid); - if (permissions[0] && !room.prid) { + if (permissions[0]) { this.setState({ showEdit: true }, () => this.setHeader()); } } diff --git a/e2e/tests/room/04-discussion.spec.js b/e2e/tests/room/04-discussion.spec.js index 0a0bc8e75..e5705b26b 100644 --- a/e2e/tests/room/04-discussion.spec.js +++ b/e2e/tests/room/04-discussion.spec.js @@ -131,8 +131,8 @@ describe('Discussion', () => { await expect(element(by.id('room-info-view'))).toExist(); }); - it('should not have edit button', async() => { - await expect(element(by.id('room-info-view-edit-button'))).toBeNotVisible(); + it('should have edit button', async() => { + await expect(element(by.id('room-info-view-edit-button'))).toBeVisible(); }); }); }); From 69c39b14af42ea95ad4d48626eb872512025ddb4 Mon Sep 17 00:00:00 2001 From: Gerzon Z Date: Fri, 28 May 2021 10:22:41 -0400 Subject: [PATCH 21/44] [NEW] Delete Teams (#3123) * Added Create Team * Added actionTypes, actions, ENG strings for Teams and updated NewMessageView * Added createTeam sagas, createTeam reducer, new Team string and update CreateChannelView * Remove unnecessary actionTypes, reducers and sagas, e2e tests and navigation to team view * Minor tweaks * Show TeamChannelsView only if joined the team * Minor tweak * Added AddChannelTeamView * Added permissions, translations strings for teams, deleteTeamRoom and addTeamRooms, AddExistingChannelView, updated CreateChannelView, TeamChannelsView * Refactor touch component and update removeRoom and deleteRoom methods * Minor tweaks * Minor tweaks for removing channels and addExistingChannelView * Added missing events and fixed channels list * Minor tweaks for refactored touch component * Added SelectListView and logic for leaving team * Added addTeamMember and removeTeamMember * Minor tweak * Added deleteTeam function * Minor tweak * Minor tweaks * Remove unnecesary changes, update TeamChannelsView, AddExistingChannelView, AddChannelTeamView, createChannel, goRoom and Touchable * Remove unnecesary prop * Add screens to ModalStack, events, autoJoin, update createChannel, addRoomsToTeam and Touchable * Minor tweak * Update loadMessagesForRoom.js * Updated schema, tag component, touch, AddChannelTeamView, AddExistingChannelView, ActionSheet Item * Fix unnecessary changes * Add i18n, update createChannel, AddExistingChannelTeamView, AddChannelTeamView, RightButton and TeamChannelsView * Updated styles, added tag story * Minor tweak * Minor tweaks * Auto-join tweak * Minor tweaks * Minor tweak on search * Minor refactor to ListItem, add SelectListView to ModalStack, update handleLeaveTeam * Minor tweaks * Update SelectListView * Update handleLeaveTeam, remove unnecessary method, add story * Minor tweak * Minor visual tweaks * Update SelectListView.js * Update index.js * Update RoomMembersView * Updated SelectListView, RoomActionsView, leaveTeam method and string translations * Update SelectListVIew * Minor tweak * Update SelectListView * Minor tweak * Minor tweaks * Fix for List.Item subtitles being pushed down by title's flex * Minor tweaks * Update RoomActionsView * Use showConfirmationAlert and showErrorAlert * Remove addTeamMember, update removeTeamMember * Update Alert * Minor tweaks * Minor tweaks * Minor tweak * Update showActionSheet on RoomMembersView * Remove team main from query and move code around * Fetch roles * Update RoomMembersView and SelectListView * Update rocketchat.js * Updated leaveTeam and handleRemoveFromTeam * Fix validation * Remove unnecessary function * Update RoomActionsView * Update en.json * updated deleteTeam function and permissions * Added showConfirmationAlert * Added string translations for teams * Fix permission * Minor tweaks * Typo Co-authored-by: Diego Mello --- app/i18n/locales/en.json | 11 ++- app/lib/methods/getPermissions.js | 1 + app/lib/rocketchat.js | 4 + app/views/RoomInfoEditView/index.js | 111 ++++++++++++++++++++++++---- app/views/TeamChannelsView.js | 2 +- 5 files changed, 113 insertions(+), 16 deletions(-) diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 76d60f21f..29a3b2422 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -91,6 +91,7 @@ "alert": "alert", "alerts": "alerts", "All_users_in_the_channel_can_write_new_messages": "All users in the channel can write new messages", + "All_users_in_the_team_can_write_new_messages": "All users in the team can write new messages", "A_meaningful_name_for_the_discussion_room": "A meaningful name for the discussion room", "All": "All", "All_Messages": "All Messages", @@ -226,6 +227,7 @@ "Encryption_error_title": "Your encryption password seems wrong", "Encryption_error_desc": "It wasn't possible to decode your encryption key to be imported.", "Everyone_can_access_this_channel": "Everyone can access this channel", + "Everyone_can_access_this_team": "Everyone can access this team", "Error_uploading": "Error uploading", "Expiration_Days": "Expiration (Days)", "Favorite": "Favorite", @@ -287,6 +289,7 @@ "Join_our_open_workspace": "Join our open workspace", "Join_your_workspace": "Join your workspace", "Just_invited_people_can_access_this_channel": "Just invited people can access this channel", + "Just_invited_people_can_access_this_team": "Just invited people can access this team", "Language": "Language", "last_message": "last message", "Leave_channel": "Leave channel", @@ -723,7 +726,7 @@ "Add_Existing_Channel": "Add Existing Channel", "Remove_from_Team": "Remove from Team", "Auto-join": "Auto-join", - "Delete_Team_Room_Warning": "Woud you like to remove this channel from the team? The channel will be moved back to the workspace", + "Remove_Team_Room_Warning": "Woud you like to remove this channel from the team? The channel will be moved back to the workspace", "Confirmation": "Confirmation", "invalid-room": "Invalid room", "You_are_leaving_the_team": "You are leaving the team '{{team}}'", @@ -731,13 +734,19 @@ "Select_Team_Channels": "Select the Team's channels you would like to leave.", "Cannot_leave": "Cannot leave", "Cannot_remove": "Cannot remove", + "Cannot_delete": "Cannot delete", "Last_owner_team_room": "You are the last owner of this channel. Once you leave the team, the channel will be kept inside the team but you will be managing it from outside.", "last-owner-can-not-be-removed": "Last owner cannot be removed", + "Remove_User_Teams": "Select channels you want the user to be removed from.", + "Delete_Team": "Delete Team", + "Select_channels_to_delete": "This can't be undone. Once you delete a team, all chat content and configuration will be deleted. \n\nSelect the channels you would like to delete. The ones you decide to keep will be available on your workspace. Notice that public channels will still be public and visible to everyone.", + "You_are_deleting_the_team": "You are deleting this team.", "Removing_user_from_this_team": "You are removing {{user}} from this team", "Remove_User_Team_Channels": "Select the channels you want the user to be removed from.", "Remove_Member": "Remove Member", "leaving_team": "leaving team", "removing_team": "removing from team", + "deleting_team": "deleting team", "member-does-not-exist": "Member does not exist", "Load_More": "Load More", "Load_Newer": "Load Newer", diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js index cc1dd46e0..99a18a6c0 100644 --- a/app/lib/methods/getPermissions.js +++ b/app/lib/methods/getPermissions.js @@ -20,6 +20,7 @@ const PERMISSIONS = [ 'delete-c', 'delete-message', 'delete-p', + 'delete-team', 'edit-message', 'edit-room', 'edit-team-member', diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 3660b3195..4774b4272 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -790,6 +790,10 @@ const RocketChat = { // RC 3.13.0 return this.post('teams.updateRoom', { roomId, isDefault }); }, + deleteTeam({ teamId, roomsToRemove }) { + // RC 3.13.0 + return this.post('teams.delete', { teamId, roomsToRemove }); + }, teamListRoomsOfUser({ teamId, userId }) { // RC 3.13.0 return this.sdk.get('teams.listRoomsOfUser', { teamId, userId }); diff --git a/app/views/RoomInfoEditView/index.js b/app/views/RoomInfoEditView/index.js index 7552b093a..90d8b28f5 100644 --- a/app/views/RoomInfoEditView/index.js +++ b/app/views/RoomInfoEditView/index.js @@ -8,15 +8,16 @@ import { BLOCK_CONTEXT } from '@rocket.chat/ui-kit'; import ImagePicker from 'react-native-image-crop-picker'; import { dequal } from 'dequal'; import isEmpty from 'lodash/isEmpty'; -import { compareServerVersion, methods } from '../../lib/utils'; +import { Q } from '@nozbe/watermelondb'; +import { compareServerVersion, methods } from '../../lib/utils'; import database from '../../lib/database'; import { deleteRoom as deleteRoomAction } from '../../actions/room'; import KeyboardView from '../../presentation/KeyboardView'; import sharedStyles from '../Styles'; import styles from './styles'; import scrollPersistTaps from '../../utils/scrollPersistTaps'; -import { showErrorAlert } from '../../utils/info'; +import { showConfirmationAlert, showErrorAlert } from '../../utils/info'; import { LISTENER } from '../../containers/Toast'; import EventEmitter from '../../utils/events'; import RocketChat from '../../lib/rocketchat'; @@ -41,6 +42,7 @@ const PERMISSION_ARCHIVE = 'archive-room'; const PERMISSION_UNARCHIVE = 'unarchive-room'; const PERMISSION_DELETE_C = 'delete-c'; const PERMISSION_DELETE_P = 'delete-p'; +const PERMISSION_DELETE_TEAM = 'delete-team'; class RoomInfoEditView extends React.Component { static navigationOptions = () => ({ @@ -48,6 +50,7 @@ class RoomInfoEditView extends React.Component { }) static propTypes = { + navigation: PropTypes.object, route: PropTypes.object, deleteRoom: PropTypes.func, serverVersion: PropTypes.string, @@ -58,7 +61,9 @@ class RoomInfoEditView extends React.Component { archiveRoomPermission: PropTypes.array, unarchiveRoomPermission: PropTypes.array, deleteCPermission: PropTypes.array, - deletePPermission: PropTypes.array + deletePPermission: PropTypes.array, + deleteTeamPermission: PropTypes.array, + isMasterDetail: PropTypes.bool }; constructor(props) { @@ -100,7 +105,8 @@ class RoomInfoEditView extends React.Component { archiveRoomPermission, unarchiveRoomPermission, deleteCPermission, - deletePPermission + deletePPermission, + deleteTeamPermission } = this.props; const rid = route.params?.rid; if (!rid) { @@ -122,7 +128,8 @@ class RoomInfoEditView extends React.Component { archiveRoomPermission, unarchiveRoomPermission, deleteCPermission, - deletePPermission + deletePPermission, + ...(this.room.teamMain ? [deleteTeamPermission] : []) ], rid); this.setState({ @@ -132,7 +139,8 @@ class RoomInfoEditView extends React.Component { [PERMISSION_ARCHIVE]: result[2], [PERMISSION_UNARCHIVE]: result[3], [PERMISSION_DELETE_C]: result[4], - [PERMISSION_DELETE_P]: result[5] + [PERMISSION_DELETE_P]: result[5], + ...(this.room.teamMain && { [PERMISSION_DELETE_TEAM]: result[6] }) } }); } catch (e) { @@ -284,6 +292,72 @@ class RoomInfoEditView extends React.Component { }, 100); } + handleDeleteTeam = async(selected) => { + const { navigation, isMasterDetail } = this.props; + const { room } = this.state; + try { + const result = await RocketChat.deleteTeam({ teamId: room.teamId, ...(selected && { roomsToRemove: selected }) }); + if (result.success) { + if (isMasterDetail) { + navigation.navigate('DrawerNavigator'); + } else { + navigation.navigate('RoomsListView'); + } + } + } catch (e) { + log(e); + showErrorAlert( + e.data.error + ? I18n.t(e.data.error) + : I18n.t('There_was_an_error_while_action', { action: I18n.t('deleting_team') }), + I18n.t('Cannot_delete') + ); + } + } + + deleteTeam = async() => { + const { room } = this.state; + const { navigation } = this.props; + + try { + const db = database.active; + const subCollection = db.get('subscriptions'); + const teamChannels = await subCollection.query( + Q.where('team_id', room.teamId), + Q.where('team_main', null) + ); + + if (teamChannels.length) { + navigation.navigate('SelectListView', { + title: 'Delete_Team', + data: teamChannels, + infoText: 'Select_channels_to_delete', + nextAction: (selected) => { + showConfirmationAlert({ + message: I18n.t('You_are_deleting_the_team', { team: RocketChat.getRoomTitle(room) }), + confirmationText: I18n.t('Yes_action_it', { action: I18n.t('delete') }), + onPress: () => this.handleDeleteTeam(selected) + }); + } + }); + } else { + showConfirmationAlert({ + message: I18n.t('You_are_deleting_the_team', { team: RocketChat.getRoomTitle(room) }), + confirmationText: I18n.t('Yes_action_it', { action: I18n.t('delete') }), + onPress: () => this.handleDeleteTeam() + }); + } + } catch (e) { + log(e); + showErrorAlert( + e.data.error + ? I18n.t(e.data.error) + : I18n.t('There_was_an_error_while_action', { action: I18n.t('deleting_team') }), + I18n.t('Cannot_delete') + ); + } + } + delete = () => { const { room } = this.state; const { deleteRoom } = this.props; @@ -339,9 +413,16 @@ class RoomInfoEditView extends React.Component { hasDeletePermission = () => { const { room, permissions } = this.state; - return ( - room.t === 'p' ? permissions[PERMISSION_DELETE_P] : permissions[PERMISSION_DELETE_C] - ); + + if (room.teamMain) { + return permissions[PERMISSION_DELETE_TEAM]; + } + + if (room.t === 'p') { + return permissions[PERMISSION_DELETE_P]; + } + + return permissions[PERMISSION_DELETE_C]; } hasArchivePermission = () => { @@ -513,9 +594,9 @@ class RoomInfoEditView extends React.Component { @@ -678,7 +759,9 @@ const mapStateToProps = state => ({ archiveRoomPermission: state.permissions[PERMISSION_ARCHIVE], unarchiveRoomPermission: state.permissions[PERMISSION_UNARCHIVE], deleteCPermission: state.permissions[PERMISSION_DELETE_C], - deletePPermission: state.permissions[PERMISSION_DELETE_P] + deletePPermission: state.permissions[PERMISSION_DELETE_P], + deleteTeamPermission: state.permissions[PERMISSION_DELETE_TEAM], + isMasterDetail: state.app.isMasterDetail }); const mapDispatchToProps = dispatch => ({ diff --git a/app/views/TeamChannelsView.js b/app/views/TeamChannelsView.js index 1d4b09246..15905a9ee 100644 --- a/app/views/TeamChannelsView.js +++ b/app/views/TeamChannelsView.js @@ -321,7 +321,7 @@ class TeamChannelsView extends React.Component { remove = (item) => { Alert.alert( I18n.t('Confirmation'), - I18n.t('Delete_Team_Room_Warning'), + I18n.t('Remove_Team_Room_Warning'), [ { text: I18n.t('Cancel'), From 852a893a00a59ea5b8355a31142381f54f527a5d Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Fri, 28 May 2021 15:29:21 -0300 Subject: [PATCH 22/44] [FIX] Android navigation bar color when Loading modal appears (#3165) * [FIX] Modal appearance * Undo and only add android:navigationBarColor Co-authored-by: Diego Mello --- android/app/src/main/res/values/styles.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 1377b4820..5b3a9bbdd 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -26,4 +26,14 @@ @color/splashBackground @color/splashBackground + + + From 2b51f37384a78b5cecde448188dd1cde5aa5b64f Mon Sep 17 00:00:00 2001 From: Gerzon Z Date: Fri, 28 May 2021 15:06:20 -0400 Subject: [PATCH 23/44] [FIX] Check for old servers for Teams (#3171) Co-authored-by: Diego Mello --- app/views/NewMessageView.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/views/NewMessageView.js b/app/views/NewMessageView.js index cca5d5842..8d5f62ceb 100644 --- a/app/views/NewMessageView.js +++ b/app/views/NewMessageView.js @@ -25,6 +25,7 @@ import Navigation from '../lib/Navigation'; import { createChannelRequest } from '../actions/createChannel'; import { goRoom } from '../utils/goRoom'; import SafeAreaView from '../containers/SafeAreaView'; +import { compareServerVersion, methods } from '../lib/utils'; const QUERY_SIZE = 50; @@ -63,7 +64,8 @@ class NewMessageView extends React.Component { create: PropTypes.func, maxUsers: PropTypes.number, theme: PropTypes.string, - isMasterDetail: PropTypes.bool + isMasterDetail: PropTypes.bool, + serverVersion: PropTypes.string }; constructor(props) { @@ -166,7 +168,7 @@ class NewMessageView extends React.Component { } renderHeader = () => { - const { maxUsers, theme } = this.props; + const { maxUsers, theme, serverVersion } = this.props; return ( this.onSearchChangeText(text)} testID='new-message-view-search' /> @@ -178,12 +180,13 @@ class NewMessageView extends React.Component { testID: 'new-message-view-create-channel', first: true })} - {this.renderButton({ - onPress: this.createTeam, - title: I18n.t('Create_Team'), - icon: 'teams', - testID: 'new-message-view-create-team' - })} + {compareServerVersion(serverVersion, '3.13.0', methods.greaterThanOrEqualTo) + ? (this.renderButton({ + onPress: this.createTeam, + title: I18n.t('Create_Team'), + icon: 'teams', + testID: 'new-message-view-create-team' + })) : null} {maxUsers > 2 ? this.renderButton({ onPress: this.createGroupChat, title: I18n.t('Create_Direct_Messages'), @@ -258,6 +261,7 @@ class NewMessageView extends React.Component { } const mapStateToProps = state => ({ + serverVersion: state.server.version, isMasterDetail: state.app.isMasterDetail, baseUrl: state.server.server, maxUsers: state.settings.DirectMesssage_maxUsers || 1, From 5fd7981d07f64e17c251415e7963d9463033936c Mon Sep 17 00:00:00 2001 From: Gerzon Z Date: Wed, 2 Jun 2021 09:44:19 -0400 Subject: [PATCH 24/44] [NEW] Convert/Move Channel to Team (#3164) * Added Create Team * Added actionTypes, actions, ENG strings for Teams and updated NewMessageView * Added createTeam sagas, createTeam reducer, new Team string and update CreateChannelView * Remove unnecessary actionTypes, reducers and sagas, e2e tests and navigation to team view * Minor tweaks * Show TeamChannelsView only if joined the team * Minor tweak * Added AddChannelTeamView * Added permissions, translations strings for teams, deleteTeamRoom and addTeamRooms, AddExistingChannelView, updated CreateChannelView, TeamChannelsView * Refactor touch component and update removeRoom and deleteRoom methods * Minor tweaks * Minor tweaks for removing channels and addExistingChannelView * Added missing events and fixed channels list * Minor tweaks for refactored touch component * Added SelectListView and logic for leaving team * Added addTeamMember and removeTeamMember * Minor tweak * Added deleteTeam function * Minor tweak * Minor tweaks * Remove unnecesary changes, update TeamChannelsView, AddExistingChannelView, AddChannelTeamView, createChannel, goRoom and Touchable * Remove unnecesary prop * Add screens to ModalStack, events, autoJoin, update createChannel, addRoomsToTeam and Touchable * Minor tweak * Update loadMessagesForRoom.js * Updated schema, tag component, touch, AddChannelTeamView, AddExistingChannelView, ActionSheet Item * Fix unnecessary changes * Add i18n, update createChannel, AddExistingChannelTeamView, AddChannelTeamView, RightButton and TeamChannelsView * Updated styles, added tag story * Minor tweak * Minor tweaks * Auto-join tweak * Minor tweaks * Minor tweak on search * Minor refactor to ListItem, add SelectListView to ModalStack, update handleLeaveTeam * Minor tweaks * Update SelectListView * Update handleLeaveTeam, remove unnecessary method, add story * Minor tweak * Minor visual tweaks * Update SelectListView.js * Update index.js * Update RoomMembersView * Updated SelectListView, RoomActionsView, leaveTeam method and string translations * Update SelectListVIew * Minor tweak * Update SelectListView * Minor tweak * Minor tweaks * Fix for List.Item subtitles being pushed down by title's flex * Minor tweaks * Update RoomActionsView * Use showConfirmationAlert and showErrorAlert * Remove addTeamMember, update removeTeamMember * Update Alert * Minor tweaks * Minor tweaks * Minor tweak * Update showActionSheet on RoomMembersView * Remove team main from query and move code around * Fetch roles * Update RoomMembersView and SelectListView * Update rocketchat.js * Updated leaveTeam and handleRemoveFromTeam * Fix validation * Remove unnecessary function * Update RoomActionsView * Update en.json * updated deleteTeam function and permissions * Added showConfirmationAlert * Added string translations for teams * Fix permission * Added moveChannelToTeam and convertToTeam functionality * Fix SelectListView RadioButton * Fix moveToTeam * Added searchBar to SelectListVIew * Update RoomView , SelectListVIew and string translation for error Co-authored-by: Diego Mello --- app/containers/RoomHeader/index.js | 5 +- app/i18n/locales/en.json | 12 +- app/lib/methods/getPermissions.js | 1 + app/lib/rocketchat.js | 14 +++ app/views/RoomActionsView/index.js | 190 ++++++++++++++++++++++++++++- app/views/RoomView/RightButtons.js | 4 + app/views/RoomView/index.js | 7 +- app/views/SelectListView.js | 53 ++++++-- 8 files changed, 270 insertions(+), 16 deletions(-) diff --git a/app/containers/RoomHeader/index.js b/app/containers/RoomHeader/index.js index 4eeab701f..7d4d22de8 100644 --- a/app/containers/RoomHeader/index.js +++ b/app/containers/RoomHeader/index.js @@ -32,7 +32,7 @@ class RoomHeaderContainer extends Component { shouldComponentUpdate(nextProps) { const { - type, title, subtitle, status, statusText, connecting, connected, onPress, usersTyping, width, height + type, title, subtitle, status, statusText, connecting, connected, onPress, usersTyping, width, height, teamMain } = this.props; if (nextProps.type !== type) { return true; @@ -67,6 +67,9 @@ class RoomHeaderContainer extends Component { if (nextProps.onPress !== onPress) { return true; } + if (nextProps.teamMain !== teamMain) { + return true; + } return false; } diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 29a3b2422..090bf5cd0 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -14,7 +14,7 @@ "error-delete-protected-role": "Cannot delete a protected role", "error-department-not-found": "Department not found", "error-direct-message-file-upload-not-allowed": "File sharing not allowed in direct messages", - "error-duplicate-channel-name": "A channel with name {{channel_name}} exists", + "error-duplicate-channel-name": "A channel with name {{room_name}} exists", "error-email-domain-blacklisted": "The email domain is blacklisted", "error-email-send-failed": "Error trying to send email: {{message}}", "error-save-image": "Error while saving image", @@ -182,6 +182,7 @@ "delete": "delete", "Delete": "Delete", "DELETE": "DELETE", + "move": "move", "deleting_room": "deleting room", "description": "description", "Description": "Description", @@ -731,6 +732,7 @@ "invalid-room": "Invalid room", "You_are_leaving_the_team": "You are leaving the team '{{team}}'", "Leave_Team": "Leave Team", + "Select_Team": "Select Team", "Select_Team_Channels": "Select the Team's channels you would like to leave.", "Cannot_leave": "Cannot leave", "Cannot_remove": "Cannot remove", @@ -746,8 +748,16 @@ "Remove_Member": "Remove Member", "leaving_team": "leaving team", "removing_team": "removing from team", + "moving_channel_to_team": "moving channel to team", "deleting_team": "deleting team", "member-does-not-exist": "Member does not exist", + "Convert": "Convert", + "Convert_to_Team": "Convert to Team", + "Convert_to_Team_Warning": "This can't be undone. Once you convert a channel to a team, you can not turn it back to a channel.", + "Move_to_Team": "Move to Team", + "Move_Channel_to_Team": "Move Channel to Team", + "Move_Channel_Paragraph": "Moving a channel inside a team means that this channel will be added in the team’s context, however, all channel’s members, which are not members of the respective team, will still have access to this channel, but will not be added as team’s members. \n\nAll channel’s management will still be made by the owners of this channel.\n\nTeam’s members and even team’s owners, if not a member of this channel, can not have access to the channel’s content. \n\nPlease notice that the Team’s owner will be able remove members from the Channel.", + "Move_to_Team_Warning": "After reading the previous intructions about this behavior, do you still want to move this channel to the selected team?", "Load_More": "Load More", "Load_Newer": "Load Newer", "Load_Older": "Load Older" diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js index 99a18a6c0..2b7da4765 100644 --- a/app/lib/methods/getPermissions.js +++ b/app/lib/methods/getPermissions.js @@ -17,6 +17,7 @@ const PERMISSIONS = [ 'archive-room', 'auto-translate', 'create-invite-links', + 'create-team', 'delete-c', 'delete-message', 'delete-p', diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 4774b4272..f174aa1eb 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -798,6 +798,20 @@ const RocketChat = { // RC 3.13.0 return this.sdk.get('teams.listRoomsOfUser', { teamId, userId }); }, + convertChannelToTeam({ rid, name, type }) { + const params = { + ...(type === 'c' + ? { + channelId: rid, + channelName: name + } + : { + roomId: rid, + roomName: name + }) + }; + return this.sdk.post(type === 'c' ? 'channels.convertToTeam' : 'groups.convertToTeam', params); + }, joinRoom(roomId, joinCode, type) { // TODO: join code // RC 0.48.0 diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index 89d34ed07..7199843c8 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -5,8 +5,9 @@ import { } from 'react-native'; import { connect } from 'react-redux'; import isEmpty from 'lodash/isEmpty'; -import { compareServerVersion, methods } from '../../lib/utils'; +import { Q } from '@nozbe/watermelondb'; +import { compareServerVersion, methods } from '../../lib/utils'; import Touch from '../../utils/touch'; import { setLoading as setLoadingAction } from '../../actions/selectedUsers'; import { leaveRoom as leaveRoomAction, closeRoom as closeRoomAction } from '../../actions/room'; @@ -61,7 +62,9 @@ class RoomActionsView extends React.Component { editRoomPermission: PropTypes.array, toggleRoomE2EEncryptionPermission: PropTypes.array, viewBroadcastMemberListPermission: PropTypes.array, - transferLivechatGuestPermission: PropTypes.array + transferLivechatGuestPermission: PropTypes.array, + createTeamPermission: PropTypes.array, + addTeamChannelPermission: PropTypes.array } constructor(props) { @@ -83,7 +86,9 @@ class RoomActionsView extends React.Component { canForwardGuest: false, canReturnQueue: false, canEdit: false, - canToggleEncryption: false + canToggleEncryption: false, + canCreateTeam: false, + canAddChannelToTeam: false }; if (room && room.observe && room.rid) { this.roomObservable = room.observe(); @@ -132,9 +137,11 @@ class RoomActionsView extends React.Component { const canEdit = await this.canEdit(); const canToggleEncryption = await this.canToggleEncryption(); const canViewMembers = await this.canViewMembers(); + const canCreateTeam = await this.canCreateTeam(); + const canAddChannelToTeam = await this.canAddChannelToTeam(); this.setState({ - canAutoTranslate, canAddUser, canInviteUser, canEdit, canToggleEncryption, canViewMembers + canAutoTranslate, canAddUser, canInviteUser, canEdit, canToggleEncryption, canViewMembers, canCreateTeam, canAddChannelToTeam }); // livechat permissions @@ -210,6 +217,26 @@ class RoomActionsView extends React.Component { return canEdit; } + canCreateTeam = async() => { + const { room } = this.state; + const { createTeamPermission } = this.props; + const { rid } = room; + const permissions = await RocketChat.hasPermission([createTeamPermission], rid); + + const canCreateTeam = permissions[0]; + return canCreateTeam; + } + + canAddChannelToTeam = async() => { + const { room } = this.state; + const { addTeamChannelPermission } = this.props; + const { rid } = room; + const permissions = await RocketChat.hasPermission([addTeamChannelPermission], rid); + + const canAddChannelToTeam = permissions[0]; + return canAddChannelToTeam; + } + canToggleEncryption = async() => { const { room } = this.state; const { toggleRoomE2EEncryptionPermission } = this.props; @@ -464,6 +491,111 @@ class RoomActionsView extends React.Component { } } + handleConvertToTeam = async() => { + try { + const { room } = this.state; + const { navigation } = this.props; + const result = await RocketChat.convertChannelToTeam({ rid: room.rid, name: room.name, type: room.t }); + + if (result.success) { + navigation.navigate('RoomView'); + } + } catch (e) { + log(e); + } + } + + convertToTeam = () => { + showConfirmationAlert({ + title: I18n.t('Confirmation'), + message: I18n.t('Convert_to_Team_Warning'), + confirmationText: I18n.t('Convert'), + onPress: () => this.handleConvertToTeam() + }); + } + + handleMoveToTeam = async(selected) => { + try { + const { room } = this.state; + const { navigation } = this.props; + const result = await RocketChat.addRoomsToTeam({ teamId: selected.teamId, rooms: [room.rid] }); + if (result.success) { + navigation.navigate('RoomView'); + } + } catch (e) { + log(e); + showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t('moving_channel_to_team') })); + } + } + + moveToTeam = async() => { + try { + const { navigation } = this.props; + const db = database.active; + const subCollection = db.get('subscriptions'); + const teamRooms = await subCollection.query( + Q.where('team_main', Q.notEq(null)) + ); + + if (teamRooms.length) { + navigation.navigate('SelectListView', { + title: 'Move_to_Team', + infoText: 'Move_Channel_Paragraph', + nextAction: () => { + navigation.push('SelectListView', { + title: 'Select_Team', + data: teamRooms, + isRadio: true, + isSearch: true, + onSearch: onChangeText => this.searchTeam(onChangeText), + nextAction: selected => showConfirmationAlert({ + title: I18n.t('Confirmation'), + message: I18n.t('Move_to_Team_Warning'), + confirmationText: I18n.t('Yes_action_it', { action: I18n.t('move') }), + onPress: () => this.handleMoveToTeam(selected) + }) + + }); + } + }); + } + } catch (e) { + log(e); + } + } + + searchTeam = async(onChangeText) => { + try { + const { addTeamChannelPermission, createTeamPermission } = this.props; + const QUERY_SIZE = 50; + const db = database.active; + const teams = await db.collections + .get('subscriptions') + .query( + Q.where('team_main', Q.notEq(null)), + Q.where('name', Q.like(`%${ onChangeText }%`)), + Q.experimentalTake(QUERY_SIZE), + Q.experimentalSortBy('room_updated_at', Q.desc) + ); + + const asyncFilter = async(teamArray) => { + const results = await Promise.all(teamArray.map(async(team) => { + const permissions = await RocketChat.hasPermission([addTeamChannelPermission, createTeamPermission], team.rid); + if (!permissions[0]) { + return false; + } + return true; + })); + + return teamArray.filter((_v, index) => results[index]); + }; + const teamsFiltered = await asyncFilter(teams); + return teamsFiltered; + } catch (e) { + log(e); + } + } + renderRoomInfo = () => { const { room, member } = this.state; const { @@ -635,6 +767,50 @@ class RoomActionsView extends React.Component { } } + teamChannelActions = (t, room) => { + const { canEdit, canCreateTeam, canAddChannelToTeam } = this.state; + const canConvertToTeam = canEdit && canCreateTeam && !room.teamMain; + const canMoveToTeam = canEdit && canAddChannelToTeam && !room.teamId; + + return ( + <> + {['c', 'p'].includes(t) && canConvertToTeam + ? ( + <> + this.onPressTouchable({ + event: this.convertToTeam + })} + testID='room-actions-convert-to-team' + left={() => } + showActionIndicator + /> + + + ) + : null} + + {['c', 'p'].includes(t) && canMoveToTeam + ? ( + <> + this.onPressTouchable({ + event: this.moveToTeam + })} + testID='room-actions-convert-to-team' + left={() => } + showActionIndicator + /> + + + ) + : null} + + ); + } + render() { const { room, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate, canForwardGuest, canReturnQueue @@ -836,6 +1012,8 @@ class RoomActionsView extends React.Component { ) : null} + { this.teamChannelActions(t, room) } + {['l'].includes(t) && !this.isOmnichannelPreview ? ( <> @@ -922,7 +1100,9 @@ const mapStateToProps = state => ({ editRoomPermission: state.permissions['edit-room'], toggleRoomE2EEncryptionPermission: state.permissions['toggle-room-e2e-encryption'], viewBroadcastMemberListPermission: state.permissions['view-broadcast-member-list'], - transferLivechatGuestPermission: state.permissions['transfer-livechat-guest'] + transferLivechatGuestPermission: state.permissions['transfer-livechat-guest'], + createTeamPermission: state.permissions['create-team'], + addTeamChannelPermission: state.permissions['add-team-channel'] }); const mapDispatchToProps = dispatch => ({ diff --git a/app/views/RoomView/RightButtons.js b/app/views/RoomView/RightButtons.js index f61488b5b..5b283b4ad 100644 --- a/app/views/RoomView/RightButtons.js +++ b/app/views/RoomView/RightButtons.js @@ -59,6 +59,10 @@ class RightButtonsContainer extends Component { const { isFollowingThread, tunread, tunreadUser, tunreadGroup } = this.state; + const { teamId } = this.props; + if (nextProps.teamId !== teamId) { + return true; + } if (nextState.isFollowingThread !== isFollowingThread) { return true; } diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index 18d123ec4..13de89c87 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -85,7 +85,7 @@ const stateAttrsUpdate = [ 'member', 'showingBlockingLoader' ]; -const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'tunread', 'muted', 'ignored', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname', 'roles', 'bannerClosed', 'visitor', 'joinCodeRequired']; +const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'tunread', 'muted', 'ignored', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname', 'roles', 'bannerClosed', 'visitor', 'joinCodeRequired', 'teamMain', 'teamId']; class RoomView extends React.Component { static propTypes = { @@ -254,7 +254,10 @@ class RoomView extends React.Component { this.setHeader(); } } - if (((roomUpdate.fname !== prevState.roomUpdate.fname) || (roomUpdate.name !== prevState.roomUpdate.name)) && !this.tmid) { + if ((roomUpdate.teamMain !== prevState.roomUpdate.teamMain) || (roomUpdate.teamId !== prevState.roomUpdate.teamId)) { + this.setHeader(); + } + if (((roomUpdate.fname !== prevState.roomUpdate.fname) || (roomUpdate.name !== prevState.roomUpdate.name) || (roomUpdate.teamMain !== prevState.roomUpdate.teamMain) || (roomUpdate.teamId !== prevState.roomUpdate.teamId)) && !this.tmid) { this.setHeader(); } if (insets.left !== prevProps.insets.left || insets.right !== prevProps.insets.right) { diff --git a/app/views/SelectListView.js b/app/views/SelectListView.js index bcbb0923f..a3ad3f889 100644 --- a/app/views/SelectListView.js +++ b/app/views/SelectListView.js @@ -4,7 +4,9 @@ import { View, StyleSheet, FlatList, Text } from 'react-native'; import { connect } from 'react-redux'; +import { RadioButton } from 'react-native-ui-lib'; +import log from '../utils/log'; import * as List from '../containers/List'; import sharedStyles from './Styles'; import I18n from '../i18n'; @@ -14,6 +16,9 @@ import { themes } from '../constants/colors'; import { withTheme } from '../theme'; import SafeAreaView from '../containers/SafeAreaView'; import { animateNextTransition } from '../utils/layoutAnimation'; +import { ICON_SIZE } from '../containers/List/constants'; +import SearchBox from '../containers/SearchBox'; + const styles = StyleSheet.create({ buttonText: { @@ -38,8 +43,13 @@ class SelectListView extends React.Component { this.infoText = props.route?.params?.infoText; this.nextAction = props.route?.params?.nextAction; this.showAlert = props.route?.params?.showAlert; + this.isSearch = props.route?.params?.isSearch; + this.onSearch = props.route?.params?.onSearch; + this.isRadio = props.route?.params?.isRadio; this.state = { data, + dataFiltered: [], + isSearching: false, selected: [] }; this.setHeader(); @@ -75,6 +85,25 @@ class SelectListView extends React.Component { ); } + renderSearch = () => { + const { theme } = this.props; + return ( + + this.search(text)} testID='select-list-view-search' onCancelPress={() => this.setState({ isSearching: false })} /> + + ); + } + + search = async(text) => { + try { + this.setState({ isSearching: true }); + const result = await this.onSearch(text); + this.setState({ dataFiltered: result }); + } catch (e) { + log(e); + } + } + isChecked = (rid) => { const { selected } = this.state; return selected.includes(rid); @@ -84,7 +113,11 @@ class SelectListView extends React.Component { const { selected } = this.state; animateNextTransition(); - if (!this.isChecked(rid)) { + if (this.isRadio) { + if (!this.isChecked(rid)) { + this.setState({ selected: [rid] }, () => this.setHeader()); + } + } else if (!this.isChecked(rid)) { this.setState({ selected: [...selected, rid] }, () => this.setHeader()); } else { const filterSelected = selected.filter(el => el !== rid); @@ -94,9 +127,16 @@ class SelectListView extends React.Component { renderItem = ({ item }) => { const { theme } = this.props; - const icon = item.t === 'p' ? 'channel-private' : 'channel-public'; + const { selected } = this.state; + + const channelIcon = item.t === 'p' ? 'channel-private' : 'channel-public'; + const teamIcon = item.t === 'p' ? 'teams-private' : 'teams'; + const icon = item.teamMain ? teamIcon : channelIcon; const checked = this.isChecked(item.rid) ? 'check' : null; + const showRadio = () => ; + const showCheck = () => ; + return ( <> @@ -107,25 +147,24 @@ class SelectListView extends React.Component { onPress={() => (item.alert ? this.showAlert() : this.toggleItem(item.rid))} alert={item.alert} left={() => } - right={() => (checked ? : null)} + right={() => (this.isRadio ? showRadio() : showCheck())} /> ); } render() { - const { data } = this.state; + const { data, isSearching, dataFiltered } = this.state; const { theme } = this.props; - return ( item.rid} renderItem={this.renderItem} - ListHeaderComponent={this.renderInfoText} + ListHeaderComponent={this.isSearch ? this.renderSearch : this.renderInfoText} contentContainerStyle={{ backgroundColor: themes[theme].backgroundColor }} keyboardShouldPersistTaps='always' /> From c087780ccfa848be8ce744b8607195f510209e35 Mon Sep 17 00:00:00 2001 From: Gerzon Z Date: Fri, 4 Jun 2021 12:16:05 -0400 Subject: [PATCH 25/44] [TEST] E2E Tests for Teams (#3178) * Added Create Team * Added actionTypes, actions, ENG strings for Teams and updated NewMessageView * Added createTeam sagas, createTeam reducer, new Team string and update CreateChannelView * Remove unnecessary actionTypes, reducers and sagas, e2e tests and navigation to team view * Minor tweaks * Show TeamChannelsView only if joined the team * Minor tweak * Added AddChannelTeamView * Added permissions, translations strings for teams, deleteTeamRoom and addTeamRooms, AddExistingChannelView, updated CreateChannelView, TeamChannelsView * Refactor touch component and update removeRoom and deleteRoom methods * Minor tweaks * Minor tweaks for removing channels and addExistingChannelView * Added missing events and fixed channels list * Minor tweaks for refactored touch component * Added SelectListView and logic for leaving team * Added addTeamMember and removeTeamMember * Minor tweak * Added deleteTeam function * Minor tweak * Minor tweaks * Remove unnecesary changes, update TeamChannelsView, AddExistingChannelView, AddChannelTeamView, createChannel, goRoom and Touchable * Remove unnecesary prop * Add screens to ModalStack, events, autoJoin, update createChannel, addRoomsToTeam and Touchable * Minor tweak * Update loadMessagesForRoom.js * Updated schema, tag component, touch, AddChannelTeamView, AddExistingChannelView, ActionSheet Item * Fix unnecessary changes * Add i18n, update createChannel, AddExistingChannelTeamView, AddChannelTeamView, RightButton and TeamChannelsView * Updated styles, added tag story * Minor tweak * Minor tweaks * Auto-join tweak * Minor tweaks * Minor tweak on search * Minor refactor to ListItem, add SelectListView to ModalStack, update handleLeaveTeam * Minor tweaks * Update SelectListView * Update handleLeaveTeam, remove unnecessary method, add story * Minor tweak * Minor visual tweaks * Update SelectListView.js * Update index.js * Update RoomMembersView * Updated SelectListView, RoomActionsView, leaveTeam method and string translations * Update SelectListVIew * Minor tweak * Update SelectListView * Minor tweak * Minor tweaks * Fix for List.Item subtitles being pushed down by title's flex * Minor tweaks * Update RoomActionsView * Use showConfirmationAlert and showErrorAlert * Remove addTeamMember, update removeTeamMember * Update Alert * Minor tweaks * Minor tweaks * Minor tweak * Update showActionSheet on RoomMembersView * Remove team main from query and move code around * Fetch roles * Update RoomMembersView and SelectListView * Update rocketchat.js * Updated leaveTeam and handleRemoveFromTeam * Fix validation * Remove unnecessary function * Update RoomActionsView * Update en.json * updated deleteTeam function and permissions * Added showConfirmationAlert * Added string translations for teams * Fix permission * Added moveChannelToTeam and convertToTeam functionality * Fix SelectListView RadioButton * Fix moveToTeam * Added searchBar to SelectListVIew * Update RoomView , SelectListVIew and string translation for error * E2E for Teams * Fix tests and cleanup * Minor refactor * Wrong label * Move/convert * Fix convert Co-authored-by: Diego Mello --- .../__snapshots__/Storyshots.test.js.snap | 2 + app/containers/ActionSheet/Item.js | 4 +- app/containers/List/ListIcon.js | 7 +- app/i18n/locales/en.json | 1 - app/presentation/RoomItem/RoomItem.js | 2 +- app/presentation/RoomItem/Tag.js | 6 +- app/views/AddChannelTeamView.js | 2 +- app/views/AddExistingChannelView.js | 4 +- app/views/CreateChannelView.js | 4 +- app/views/RoomActionsView/index.js | 14 +- app/views/RoomMembersView/index.js | 21 +- app/views/SelectListView.js | 4 +- app/views/TeamChannelsView.js | 13 +- e2e/data/data.cloud.js | 10 + e2e/data/data.docker.js | 10 + e2e/helpers/app.js | 4 +- e2e/tests/assorted/05-joinpublicroom.spec.js | 5 - e2e/tests/room/02-room.spec.js | 12 +- e2e/tests/room/03-roomactions.spec.js | 64 ++-- e2e/tests/room/04-discussion.spec.js | 4 - e2e/tests/team/01-createteam.spec.js | 70 ++--- e2e/tests/team/02-team.spec.js | 296 ++++++++++++++++++ e2e/tests/team/03-moveconvert.spec.js | 89 ++++++ 23 files changed, 526 insertions(+), 122 deletions(-) create mode 100644 e2e/tests/team/02-team.spec.js create mode 100644 e2e/tests/team/03-moveconvert.spec.js diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index eec5b535a..52fa5d01d 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -58119,6 +58119,7 @@ exports[`Storyshots Room Item Tag 1`] = ` }, ] } + testID="auto-join-tag" > Auto-join @@ -59020,6 +59021,7 @@ exports[`Storyshots Room Item Tag 1`] = ` }, ] } + testID="auto-join-tag" > Auto-join diff --git a/app/containers/ActionSheet/Item.js b/app/containers/ActionSheet/Item.js index aa76da8bb..2cacd0855 100644 --- a/app/containers/ActionSheet/Item.js +++ b/app/containers/ActionSheet/Item.js @@ -18,6 +18,7 @@ export const Item = React.memo(({ item, hide, theme }) => { onPress={onPress} style={[styles.item, { backgroundColor: themes[theme].focusedBackground }]} theme={theme} + testID={item.testID} > @@ -42,7 +43,8 @@ Item.propTypes = { icon: PropTypes.string, danger: PropTypes.bool, onPress: PropTypes.func, - right: PropTypes.func + right: PropTypes.func, + testID: PropTypes.string }), hide: PropTypes.func, theme: PropTypes.string diff --git a/app/containers/List/ListIcon.js b/app/containers/List/ListIcon.js index 5ab6c3bc9..44941f2ca 100644 --- a/app/containers/List/ListIcon.js +++ b/app/containers/List/ListIcon.js @@ -18,13 +18,15 @@ const ListIcon = React.memo(({ theme, name, color, - style + style, + testID }) => ( )); @@ -33,7 +35,8 @@ ListIcon.propTypes = { theme: PropTypes.string, name: PropTypes.string, color: PropTypes.string, - style: PropTypes.object + style: PropTypes.object, + testID: PropTypes.string }; ListIcon.displayName = 'List.Icon'; diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 090bf5cd0..a8f8d9635 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -755,7 +755,6 @@ "Convert_to_Team": "Convert to Team", "Convert_to_Team_Warning": "This can't be undone. Once you convert a channel to a team, you can not turn it back to a channel.", "Move_to_Team": "Move to Team", - "Move_Channel_to_Team": "Move Channel to Team", "Move_Channel_Paragraph": "Moving a channel inside a team means that this channel will be added in the team’s context, however, all channel’s members, which are not members of the respective team, will still have access to this channel, but will not be added as team’s members. \n\nAll channel’s management will still be made by the owners of this channel.\n\nTeam’s members and even team’s owners, if not a member of this channel, can not have access to the channel’s content. \n\nPlease notice that the Team’s owner will be able remove members from the Channel.", "Move_to_Team_Warning": "After reading the previous intructions about this behavior, do you still want to move this channel to the selected team?", "Load_More": "Load More", diff --git a/app/presentation/RoomItem/RoomItem.js b/app/presentation/RoomItem/RoomItem.js index 065e91331..2ac73a0bb 100644 --- a/app/presentation/RoomItem/RoomItem.js +++ b/app/presentation/RoomItem/RoomItem.js @@ -94,7 +94,7 @@ const RoomItem = ({ alert={alert} /> { - autoJoin ? : null + autoJoin ? : null } { +const Tag = React.memo(({ name, testID }) => { const { theme } = useTheme(); return ( @@ -16,6 +16,7 @@ const Tag = React.memo(({ name }) => { styles.tagText, { color: themes[theme].infoText } ]} numberOfLines={1} + testID={testID} > {name} @@ -24,7 +25,8 @@ const Tag = React.memo(({ name }) => { }); Tag.propTypes = { - name: PropTypes.string + name: PropTypes.string, + testID: PropTypes.string }; export default Tag; diff --git a/app/views/AddChannelTeamView.js b/app/views/AddChannelTeamView.js index fdb166caf..76439b412 100644 --- a/app/views/AddChannelTeamView.js +++ b/app/views/AddChannelTeamView.js @@ -51,7 +51,7 @@ const AddChannelTeamView = ({ navigation.navigate('AddExistingChannelView', { teamId, teamChannels })} - testID='add-channel-team-view-create-channel' + testID='add-channel-team-view-add-existing' left={() => } right={() => } theme={theme} diff --git a/app/views/AddExistingChannelView.js b/app/views/AddExistingChannelView.js index c1c94fec8..e7a590a54 100644 --- a/app/views/AddExistingChannelView.js +++ b/app/views/AddExistingChannelView.js @@ -61,7 +61,7 @@ class AddExistingChannelView extends React.Component { options.headerRight = () => selected.length > 0 && ( - + ); @@ -169,7 +169,7 @@ class AddExistingChannelView extends React.Component { title={RocketChat.getRoomTitle(item)} translateTitle={false} onPress={() => this.toggleChannel(item.rid)} - testID='add-existing-channel-view-item' + testID={`add-existing-channel-view-item-${ item.name }`} left={() => } right={() => (isChecked ? : null)} /> diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js index 54f402c63..78f4ea42a 100644 --- a/app/views/CreateChannelView.js +++ b/app/views/CreateChannelView.js @@ -334,7 +334,7 @@ class CreateChannelView extends React.Component { keyboardVerticalOffset={128} > - + ({ + rid: team.teamId, + t: team.t, + name: team.name + })); navigation.navigate('SelectListView', { title: 'Move_to_Team', infoText: 'Move_Channel_Paragraph', nextAction: () => { navigation.push('SelectListView', { title: 'Select_Team', - data: teamRooms, + data, isRadio: true, isSearch: true, onSearch: onChangeText => this.searchTeam(onChangeText), @@ -554,7 +559,6 @@ class RoomActionsView extends React.Component { confirmationText: I18n.t('Yes_action_it', { action: I18n.t('move') }), onPress: () => this.handleMoveToTeam(selected) }) - }); } }); @@ -795,11 +799,11 @@ class RoomActionsView extends React.Component { ? ( <> this.onPressTouchable({ event: this.moveToTeam })} - testID='room-actions-convert-to-team' + testID='room-actions-move-to-team' left={() => } showActionIndicator /> diff --git a/app/views/RoomMembersView/index.js b/app/views/RoomMembersView/index.js index 5819cf512..5f0deb326 100644 --- a/app/views/RoomMembersView/index.js +++ b/app/views/RoomMembersView/index.js @@ -265,7 +265,8 @@ class RoomMembersView extends React.Component { options.push({ icon: 'ignore', title: I18n.t(isIgnored ? 'Unignore' : 'Ignore'), - onPress: () => this.handleIgnore(selectedUser, !isIgnored) + onPress: () => this.handleIgnore(selectedUser, !isIgnored), + testID: 'action-sheet-ignore-user' }); } @@ -284,7 +285,8 @@ class RoomMembersView extends React.Component { confirmationText: I18n.t(userIsMuted ? 'Unmute' : 'Mute'), onPress: () => this.handleMute(selectedUser) }); - } + }, + testID: 'action-sheet-mute-user' }); } @@ -296,7 +298,8 @@ class RoomMembersView extends React.Component { icon: 'shield-check', title: I18n.t('Owner'), onPress: () => this.handleOwner(selectedUser, !isOwner), - right: () => + right: () => , + testID: 'action-sheet-set-owner' }); } @@ -308,7 +311,8 @@ class RoomMembersView extends React.Component { icon: 'shield-alt', title: I18n.t('Leader'), onPress: () => this.handleLeader(selectedUser, !isLeader), - right: () => + right: () => , + testID: 'action-sheet-set-leader' }); } @@ -320,7 +324,8 @@ class RoomMembersView extends React.Component { icon: 'shield', title: I18n.t('Moderator'), onPress: () => this.handleModerator(selectedUser, !isModerator), - right: () => + right: () => , + testID: 'action-sheet-set-moderator' }); } @@ -330,7 +335,8 @@ class RoomMembersView extends React.Component { icon: 'logout', danger: true, title: I18n.t('Remove_from_Team'), - onPress: () => this.handleRemoveFromTeam(selectedUser) + onPress: () => this.handleRemoveFromTeam(selectedUser), + testID: 'action-sheet-remove-from-team' }); } @@ -346,7 +352,8 @@ class RoomMembersView extends React.Component { confirmationText: I18n.t('Yes_remove_user'), onPress: () => this.handleRemoveUserFromRoom(selectedUser) }); - } + }, + testID: 'action-sheet-remove-from-room' }); } diff --git a/app/views/SelectListView.js b/app/views/SelectListView.js index a3ad3f889..5767df3f2 100644 --- a/app/views/SelectListView.js +++ b/app/views/SelectListView.js @@ -134,8 +134,8 @@ class SelectListView extends React.Component { const icon = item.teamMain ? teamIcon : channelIcon; const checked = this.isChecked(item.rid) ? 'check' : null; - const showRadio = () => ; - const showCheck = () => ; + const showRadio = () => ; + const showCheck = () => ; return ( <> diff --git a/app/views/TeamChannelsView.js b/app/views/TeamChannelsView.js index 15905a9ee..cee7fe13c 100644 --- a/app/views/TeamChannelsView.js +++ b/app/views/TeamChannelsView.js @@ -217,9 +217,9 @@ class TeamChannelsView extends React.Component { options.headerRight = () => ( { showCreate - ? navigation.navigate('AddChannelTeamView', { teamId: this.teamId, teamChannels: data })} /> + ? navigation.navigate('AddChannelTeamView', { teamId: this.teamId, teamChannels: data })} /> : null} - + ); navigation.setOptions(options); @@ -388,7 +388,8 @@ class TeamChannelsView extends React.Component { title: I18n.t('Auto-join'), icon: item.t === 'p' ? 'channel-private' : 'channel-public', onPress: () => this.toggleAutoJoin(item), - right: () => + right: () => , + testID: 'action-sheet-auto-join' }); } @@ -398,7 +399,8 @@ class TeamChannelsView extends React.Component { title: I18n.t('Remove_from_Team'), icon: 'close', danger: true, - onPress: () => this.remove(item) + onPress: () => this.remove(item), + testID: 'action-sheet-remove-from-team' }); } @@ -408,7 +410,8 @@ class TeamChannelsView extends React.Component { title: I18n.t('Delete'), icon: 'delete', danger: true, - onPress: () => this.delete(item) + onPress: () => this.delete(item), + testID: 'action-sheet-delete' }); } diff --git a/e2e/data/data.cloud.js b/e2e/data/data.cloud.js index 381d939f1..c69b72515 100644 --- a/e2e/data/data.cloud.js +++ b/e2e/data/data.cloud.js @@ -42,6 +42,11 @@ const data = { name: `detox-private-${ value }` } }, + teams: { + private: { + name: `detox-team-${ value }` + } + }, registeringUser: { username: `newuser${ value }`, password: `password${ value }`, @@ -57,6 +62,11 @@ const data = { password: `passwordthree${ value }`, email: `mobile+registeringthree${ value }@rocket.chat` }, + registeringUser4: { + username: `newuserfour${ value }`, + password: `passwordfour${ value }`, + email: `mobile+registeringfour${ value }@rocket.chat` + }, random: value } module.exports = data; diff --git a/e2e/data/data.docker.js b/e2e/data/data.docker.js index 77f9f82c0..1a6bb1569 100644 --- a/e2e/data/data.docker.js +++ b/e2e/data/data.docker.js @@ -42,6 +42,11 @@ const data = { name: `detox-private-${ value }` } }, + teams: { + private: { + name: `detox-team-${ value }` + } + }, registeringUser: { username: `newuser${ value }`, password: `password${ value }`, @@ -57,6 +62,11 @@ const data = { password: `passwordthree${ value }`, email: `mobile+registeringthree${ value }@rocket.chat` }, + registeringUser4: { + username: `newuserfour${ value }`, + password: `passwordfour${ value }`, + email: `mobile+registeringfour${ value }@rocket.chat` + }, random: value } module.exports = data; diff --git a/e2e/helpers/app.js b/e2e/helpers/app.js index af72f73d1..7c71c9f3e 100644 --- a/e2e/helpers/app.js +++ b/e2e/helpers/app.js @@ -67,7 +67,7 @@ async function starMessage(message){ await expect(element(by.id('action-sheet'))).toExist(); await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); - await element(by.label('Star')).tap(); + await element(by.label('Star')).atIndex(0).tap(); await waitFor(element(by.id('action-sheet'))).not.toExist().withTimeout(5000); }; @@ -78,7 +78,7 @@ async function pinMessage(message){ await expect(element(by.id('action-sheet'))).toExist(); await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); - await element(by.label('Pin')).tap(); + await element(by.label('Pin')).atIndex(0).tap(); await waitFor(element(by.id('action-sheet'))).not.toExist().withTimeout(5000); } diff --git a/e2e/tests/assorted/05-joinpublicroom.spec.js b/e2e/tests/assorted/05-joinpublicroom.spec.js index 9b92d33b3..15acbda9c 100644 --- a/e2e/tests/assorted/05-joinpublicroom.spec.js +++ b/e2e/tests/assorted/05-joinpublicroom.spec.js @@ -98,10 +98,6 @@ describe('Join public room', () => { await expect(element(by.id('room-actions-starred'))).toBeVisible(); }); - it('should have search', async() => { - await expect(element(by.id('room-actions-search'))).toBeVisible(); - }); - it('should have share', async() => { await expect(element(by.id('room-actions-share'))).toBeVisible(); }); @@ -150,7 +146,6 @@ describe('Join public room', () => { await expect(element(by.id('room-actions-files'))).toBeVisible(); await expect(element(by.id('room-actions-mentioned'))).toBeVisible(); await expect(element(by.id('room-actions-starred'))).toBeVisible(); - await expect(element(by.id('room-actions-search'))).toBeVisible(); await element(by.type('UIScrollView')).atIndex(1).swipe('down'); await expect(element(by.id('room-actions-share'))).toBeVisible(); await expect(element(by.id('room-actions-pinned'))).toBeVisible(); diff --git a/e2e/tests/room/02-room.spec.js b/e2e/tests/room/02-room.spec.js index 820536fb8..02d874963 100644 --- a/e2e/tests/room/02-room.spec.js +++ b/e2e/tests/room/02-room.spec.js @@ -168,7 +168,7 @@ describe('Room screen', () => { await expect(element(by.id('action-sheet'))).toExist(); await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); - await element(by.label('Permalink')).tap(); + await element(by.label('Permalink')).atIndex(0).tap(); // TODO: test clipboard }); @@ -178,7 +178,7 @@ describe('Room screen', () => { await expect(element(by.id('action-sheet'))).toExist(); await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); - await element(by.label('Copy')).tap(); + await element(by.label('Copy')).atIndex(0).tap(); // TODO: test clipboard }); @@ -191,7 +191,7 @@ describe('Room screen', () => { await expect(element(by.id('action-sheet'))).toExist(); await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await element(by.id('action-sheet-handle')).swipe('up', 'slow', 0.5); - await waitFor(element(by.label('Unstar'))).toBeVisible().withTimeout(6000); + await waitFor(element(by.label('Unstar')).atIndex(0)).toBeVisible().withTimeout(6000); await element(by.id('action-sheet-handle')).swipe('down', 'fast', 0.8); }); @@ -243,7 +243,7 @@ describe('Room screen', () => { await expect(element(by.id('action-sheet'))).toExist(); await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); - await element(by.label('Edit')).tap(); + await element(by.label('Edit')).atIndex(0).tap(); await element(by.id('messagebox-input')).typeText('ed'); await element(by.id('messagebox-send-message')).tap(); await waitFor(element(by.label(`${ data.random }edited (edited)`)).atIndex(0)).toExist().withTimeout(60000); @@ -255,7 +255,7 @@ describe('Room screen', () => { await expect(element(by.id('action-sheet'))).toExist(); await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); - await element(by.label('Quote')).tap(); + await element(by.label('Quote')).atIndex(0).tap(); await element(by.id('messagebox-input')).typeText(`${ data.random }quoted`); await element(by.id('messagebox-send-message')).tap(); @@ -285,7 +285,7 @@ describe('Room screen', () => { await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); await waitFor(element(by.label('Delete'))).toExist().withTimeout(1000); - await element(by.label('Delete')).tap(); + await element(by.label('Delete')).atIndex(0).tap(); const deleteAlertMessage = 'You will not be able to recover this message!'; await waitFor(element(by.text(deleteAlertMessage)).atIndex(0)).toExist().withTimeout(10000); diff --git a/e2e/tests/room/03-roomactions.spec.js b/e2e/tests/room/03-roomactions.spec.js index fcfc86711..204235705 100644 --- a/e2e/tests/room/03-roomactions.spec.js +++ b/e2e/tests/room/03-roomactions.spec.js @@ -77,10 +77,6 @@ describe('Room actions screen', () => { await expect(element(by.id('room-actions-starred'))).toExist(); }); - it('should have search', async() => { - await expect(element(by.id('room-actions-search'))).toExist(); - }); - it('should have share', async() => { await waitFor(element(by.id('room-actions-share'))).toExist(); await expect(element(by.id('room-actions-share'))).toExist(); @@ -147,10 +143,6 @@ describe('Room actions screen', () => { await expect(element(by.id('room-actions-starred'))).toExist(); }); - it('should have search', async() => { - await expect(element(by.id('room-actions-search'))).toExist(); - }); - it('should have share', async() => { await waitFor(element(by.id('room-actions-share'))).toExist(); await expect(element(by.id('room-actions-share'))).toExist(); @@ -229,7 +221,7 @@ describe('Room actions screen', () => { await element(by.label(`${ data.random }messageToStar`)).atIndex(0).longPress(); await expect(element(by.id('action-sheet'))).toExist(); await expect(element(by.id('action-sheet-handle'))).toBeVisible(); - await element(by.label('Unstar')).tap(); + await element(by.label('Unstar')).atIndex(0).tap(); await waitFor(element(by.label(`${ data.random }messageToStar`).withAncestor(by.id('starred-messages-view')))).toBeNotVisible().withTimeout(60000); await backToActions(); @@ -256,29 +248,29 @@ describe('Room actions screen', () => { await expect(element(by.id('action-sheet'))).toExist(); await expect(element(by.id('action-sheet-handle'))).toBeVisible(); - await element(by.label('Unpin')).tap(); + await element(by.label('Unpin')).atIndex(0).tap(); await waitFor(element(by.label(`${ data.random }messageToPin`).withAncestor(by.id('pinned-messages-view')))).not.toExist().withTimeout(6000); await backToActions(); }); - it('should search and find a message', async() => { + // it('should search and find a message', async() => { - //Go back to room and send a message - await tapBack(); - await mockMessage('messageToFind'); + // //Go back to room and send a message + // await tapBack(); + // await mockMessage('messageToFind'); - //Back into Room Actions - await element(by.id('room-header')).tap(); - await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000); + // //Back into Room Actions + // await element(by.id('room-header')).tap(); + // await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000); - await element(by.id('room-actions-search')).tap(); - await waitFor(element(by.id('search-messages-view'))).toExist().withTimeout(2000); - await expect(element(by.id('search-message-view-input'))).toExist(); - await element(by.id('search-message-view-input')).replaceText(`/${ data.random }messageToFind/`); - await waitFor(element(by.label(`${ data.random }messageToFind`).withAncestor(by.id('search-messages-view')))).toExist().withTimeout(60000); - await backToActions(); - }); + // await element(by.id('room-actions-search')).tap(); + // await waitFor(element(by.id('search-messages-view'))).toExist().withTimeout(2000); + // await expect(element(by.id('search-message-view-input'))).toExist(); + // await element(by.id('search-message-view-input')).replaceText(`/${ data.random }messageToFind/`); + // await waitFor(element(by.label(`${ data.random }messageToFind`).withAncestor(by.id('search-messages-view')))).toExist().withTimeout(60000); + // await backToActions(); + // }); }); describe('Notification', async() => { @@ -419,46 +411,46 @@ describe('Room actions screen', () => { it('should set/remove as owner', async() => { await openActionSheet(user.username); - await element(by.label('Set as owner')).tap(); + await element(by.id('action-sheet-set-owner')).tap(); await waitForToast(); await openActionSheet(user.username); - await element(by.label('Remove as owner')).tap(); + await waitFor(element(by.id('action-sheet-set-owner-checked'))).toBeVisible().withTimeout(6000); + await element(by.id('action-sheet-set-owner')).tap(); await waitForToast(); await openActionSheet(user.username); - // Tests if Remove as owner worked - await waitFor(element(by.label('Set as owner'))).toExist().withTimeout(5000); + await waitFor(element(by.id('action-sheet-set-owner-unchecked'))).toBeVisible().withTimeout(60000); await closeActionSheet(); }); it('should set/remove as leader', async() => { await openActionSheet(user.username); - await element(by.label('Set as leader')).tap(); + await element(by.id('action-sheet-set-leader')).tap(); await waitForToast(); await openActionSheet(user.username); - await element(by.label('Remove as leader')).tap(); + await waitFor(element(by.id('action-sheet-set-leader-checked'))).toBeVisible().withTimeout(6000); + await element(by.id('action-sheet-set-leader')).tap(); await waitForToast(); await openActionSheet(user.username); - // Tests if Remove as leader worked - await waitFor(element(by.label('Set as leader'))).toExist().withTimeout(5000); + await waitFor(element(by.id('action-sheet-set-owner-unchecked'))).toBeVisible().withTimeout(60000); await closeActionSheet(); }); it('should set/remove as moderator', async() => { await openActionSheet(user.username); - await element(by.label('Set as moderator')).tap(); + await element(by.id('action-sheet-set-moderator')).tap(); await waitForToast(); await openActionSheet(user.username); - await element(by.label('Remove as moderator')).tap(); + await waitFor(element(by.id('action-sheet-set-moderator-checked'))).toBeVisible().withTimeout(6000); + await element(by.id('action-sheet-set-moderator')).tap(); await waitForToast(); await openActionSheet(user.username); - // Tests if Remove as moderator worked - await waitFor(element(by.label('Set as moderator'))).toExist().withTimeout(5000); + await waitFor(element(by.id('action-sheet-set-moderator-unchecked'))).toBeVisible().withTimeout(60000); await closeActionSheet(); }); diff --git a/e2e/tests/room/04-discussion.spec.js b/e2e/tests/room/04-discussion.spec.js index e5705b26b..fccc96185 100644 --- a/e2e/tests/room/04-discussion.spec.js +++ b/e2e/tests/room/04-discussion.spec.js @@ -102,10 +102,6 @@ describe('Discussion', () => { await expect(element(by.id('room-actions-starred'))).toBeVisible(); }); - it('should have search', async() => { - await expect(element(by.id('room-actions-search'))).toBeVisible(); - }); - it('should have share', async() => { await element(by.type('UIScrollView')).atIndex(1).swipe('up'); await expect(element(by.id('room-actions-share'))).toBeVisible(); diff --git a/e2e/tests/team/01-createteam.spec.js b/e2e/tests/team/01-createteam.spec.js index 4dfe17ca3..3c9a2260c 100644 --- a/e2e/tests/team/01-createteam.spec.js +++ b/e2e/tests/team/01-createteam.spec.js @@ -2,9 +2,9 @@ const { device, expect, element, by, waitFor } = require('detox'); const data = require('../../data'); -const { tapBack, sleep, navigateToLogin, login, tryTapping } = require('../../helpers/app'); - +const { navigateToLogin, login } = require('../../helpers/app'); +const teamName = `team-${ data.random }`; describe('Create team screen', () => { before(async() => { @@ -18,38 +18,18 @@ describe('Create team screen', () => { await element(by.id('rooms-list-view-create-channel')).tap(); }); - describe('Render', async() => { - it('should have team button', async() => { - await waitFor(element(by.id('new-message-view-create-channel'))).toBeVisible().withTimeout(2000); - }); - }) + it('should have team button', async() => { + await waitFor(element(by.id('new-message-view-create-team'))).toBeVisible().withTimeout(2000); + }); - describe('Usage', async() => { - it('should navigate to select users', async() => { - await element(by.id('new-message-view-create-channel')).tap(); - await waitFor(element(by.id('select-users-view'))).toExist().withTimeout(5000); - }); - }) + it('should navigate to select users', async() => { + await element(by.id('new-message-view-create-team')).tap(); + await waitFor(element(by.id('select-users-view'))).toExist().withTimeout(5000); + }); }); describe('Select Users', async() => { - it('should search users', async() => { - await element(by.id('select-users-view-search')).replaceText('rocket.cat'); - await waitFor(element(by.id(`select-users-view-item-rocket.cat`))).toBeVisible().withTimeout(10000); - }); - - it('should select/unselect user', async() => { - // Spotlight issues - await element(by.id('select-users-view-item-rocket.cat')).tap(); - await waitFor(element(by.id('selected-user-rocket.cat'))).toBeVisible().withTimeout(10000); - await element(by.id('selected-user-rocket.cat')).tap(); - await waitFor(element(by.id('selected-user-rocket.cat'))).toBeNotVisible().withTimeout(10000); - // Spotlight issues - await element(by.id('select-users-view-item-rocket.cat')).tap(); - await waitFor(element(by.id('selected-user-rocket.cat'))).toBeVisible().withTimeout(10000); - }); - - it('should create team', async() => { + it('should nav to create team', async() => { await element(by.id('selected-users-view-submit')).tap(); await waitFor(element(by.id('create-channel-view'))).toExist().withTimeout(10000); }); @@ -64,19 +44,33 @@ describe('Create team screen', () => { }); it('should create private team', async() => { - const room = `private${ data.random }`; await element(by.id('create-channel-name')).replaceText(''); - await element(by.id('create-channel-name')).typeText(room); + await element(by.id('create-channel-name')).typeText(teamName); await element(by.id('create-channel-submit')).tap(); await waitFor(element(by.id('room-view'))).toExist().withTimeout(20000); await expect(element(by.id('room-view'))).toExist(); - await waitFor(element(by.id(`room-view-title-${ room }`))).toExist().withTimeout(6000); - await expect(element(by.id(`room-view-title-${ room }`))).toExist(); - await tapBack(); - await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(10000); - await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toExist().withTimeout(6000); - await expect(element(by.id(`rooms-list-view-item-${ room }`))).toExist(); + await waitFor(element(by.id(`room-view-title-${ teamName }`))).toExist().withTimeout(6000); + await expect(element(by.id(`room-view-title-${ teamName }`))).toExist(); }); }) }); + + describe('Delete Team', async() => { + it('should navigate to room info edit view', async() => { + await element(by.id('room-header')).tap(); + await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000); + await element(by.id('room-actions-info')).tap(); + await waitFor(element(by.id('room-info-view'))).toExist().withTimeout(2000); + }); + + it('should delete team', async() => { + await element(by.id('room-info-view-edit-button')).tap(); + await element(by.id('room-info-edit-view-list')).swipe('up', 'fast', 0.5); + await element(by.id('room-info-edit-view-delete')).tap(); + await waitFor(element(by.text('Yes, delete it!'))).toExist().withTimeout(5000); + await element(by.text('Yes, delete it!')).tap(); + await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(10000); + await waitFor(element(by.id(`rooms-list-view-item-${ teamName }`))).toBeNotVisible().withTimeout(60000); + }); + }); }); diff --git a/e2e/tests/team/02-team.spec.js b/e2e/tests/team/02-team.spec.js new file mode 100644 index 000000000..f6f4d0329 --- /dev/null +++ b/e2e/tests/team/02-team.spec.js @@ -0,0 +1,296 @@ +const { + device, expect, element, by, waitFor +} = require('detox'); +const data = require('../../data'); +const { navigateToLogin, login, tapBack, sleep, searchRoom } = require('../../helpers/app'); + +async function navigateToRoom(roomName) { + await searchRoom(`${ roomName }`); + await element(by.id(`rooms-list-view-item-${ roomName }`)).tap(); + await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000); +} + +async function openActionSheet(username) { + await waitFor(element(by.id(`room-members-view-item-${ username }`))).toExist().withTimeout(5000); + await element(by.id(`room-members-view-item-${ username }`)).tap(); + await sleep(300); + await expect(element(by.id('action-sheet'))).toExist(); + await expect(element(by.id('action-sheet-handle'))).toBeVisible(); +} + +async function navigateToRoomActions() { + await waitFor(element(by.id('room-view'))).toExist().withTimeout(2000); + await element(by.id('room-header')).tap(); + await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000); +} + +async function backToActions() { + await tapBack(); + await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(2000); +} +async function closeActionSheet() { + await element(by.id('action-sheet-handle')).swipe('down', 'fast', 0.6); +} + +async function waitForToast() { + await sleep(1000); +} + +describe('Team', () => { + const team = data.teams.private.name; + const user = data.users.alternate; + const room = `private${ data.random }`; + const existingRoom = data.groups.private.name; + + before(async() => { + await device.launchApp({ permissions: { notifications: 'YES' }, delete: true }); + await navigateToLogin(); + await login(data.users.regular.username, data.users.regular.password); + await navigateToRoom(team); + }); + + describe('Team Room', async() => { + describe('Team Header', async() => { + it('should have actions button ', async() => { + await expect(element(by.id('room-header'))).toExist(); + }); + + it('should have team channels button ', async() => { + await expect(element(by.id('room-view-header-team-channels'))).toExist(); + }); + + it('should have threads button ', async() => { + await expect(element(by.id('room-view-header-threads'))).toExist(); + }); + + + it('should have threads button ', async() => { + await expect(element(by.id('room-view-search'))).toExist(); + }); + }); + + describe('Team Header Usage', async() => { + it('should navigate to team channels view', async() => { + await element(by.id('room-view-header-team-channels')).tap(); + await waitFor(element(by.id('team-channels-view'))).toExist().withTimeout(5000); + }); + }) + + describe('Team Channels Header', async() => { + it('should have actions button ', async() => { + await expect(element(by.id('room-header'))).toExist(); + }); + + it('should have team channels button ', async() => { + await expect(element(by.id('team-channels-view-create'))).toExist(); + }); + + it('should have threads button ', async() => { + await expect(element(by.id('team-channels-view-search'))).toExist(); + }); + }); + + describe('Team Channels Header Usage', async() => { + it('should navigate to add team channels view', async() => { + await element(by.id('team-channels-view-create')).tap(); + await waitFor(element(by.id('add-channel-team-view'))).toExist().withTimeout(5000); + }); + + it('should have create new button', async() => { + await waitFor(element(by.id('add-channel-team-view-create-channel'))).toExist().withTimeout(5000); + }); + + it('should add existing button', async() => { + await waitFor(element(by.id('add-channel-team-view-add-existing'))).toExist().withTimeout(5000); + }); + }) + + describe('Channels', async() => { + it('should create new channel for team', async() => { + + await element(by.id('add-channel-team-view-create-channel')).tap(); + + await element(by.id('select-users-view-search')).replaceText('rocket.cat'); + await element(by.id('select-users-view-item-rocket.cat')).tap(); + await waitFor(element(by.id('selected-user-rocket.cat'))).toBeVisible().withTimeout(10000); + await element(by.id('selected-users-view-submit')).tap(); + + await waitFor(element(by.id('create-channel-view'))).toExist().withTimeout(10000); + await element(by.id('create-channel-name')).replaceText(''); + await element(by.id('create-channel-name')).typeText(room); + await element(by.id('create-channel-submit')).tap(); + + await waitFor(element(by.id('room-view'))).toExist().withTimeout(20000); + await expect(element(by.id('room-view'))).toExist(); + await expect(element(by.id('room-view-header-team-channels'))).toExist(); + await element(by.id('room-view-header-team-channels')).tap(); + + await waitFor(element(by.id('team-channels-view'))).toExist().withTimeout(5000); + await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toExist().withTimeout(6000); + await expect(element(by.id(`rooms-list-view-item-${ room }`))).toExist(); + await element(by.id(`rooms-list-view-item-${ room }`)).tap(); + await waitFor(element(by.id(`room-view-title-${ room }`))).toExist().withTimeout(60000); + await expect(element(by.id(`room-view-title-${ room }`))).toExist(); + await expect(element(by.id('room-view-header-team-channels'))).toExist(); + await expect(element(by.id('room-view-header-threads'))).toExist(); + await expect(element(by.id('room-view-search'))).toExist(); + await tapBack(); + }); + + it('should add existing channel to team', async() => { + + await element(by.id('team-channels-view-create')).tap(); + await waitFor(element(by.id('add-channel-team-view'))).toExist().withTimeout(5000); + + await element(by.id('add-channel-team-view-add-existing')).tap(); + await waitFor(element(by.id('add-existing-channel-view'))).toExist().withTimeout(60000) + await expect(element(by.id(`add-existing-channel-view-item-${ existingRoom }`))).toExist(); + await element(by.id(`add-existing-channel-view-item-${ existingRoom }`)).tap(); + await waitFor(element(by.id('add-existing-channel-view-submit'))).toExist().withTimeout(6000); + await element(by.id('add-existing-channel-view-submit')).tap(); + + await waitFor(element(by.id('room-view'))).toExist().withTimeout(20000); + await expect(element(by.id('room-view'))).toExist(); + await expect(element(by.id('room-view-header-team-channels'))).toExist(); + await element(by.id('room-view-header-team-channels')).tap(); + + await waitFor(element(by.id(`rooms-list-view-item-${ existingRoom }`))).toExist().withTimeout(10000); + }); + + it('should activate/deactivate auto-join to channel', async() => { + await element(by.id(`rooms-list-view-item-${ existingRoom }`)).atIndex(0).longPress(); + + await waitFor(element(by.id('action-sheet-auto-join'))).toBeVisible().withTimeout(5000); + await waitFor(element(by.id('auto-join-unchecked'))).toBeVisible().withTimeout(5000); + await waitFor(element(by.id('action-sheet-remove-from-team'))).toBeVisible().withTimeout(5000); + await waitFor(element(by.id('action-sheet-delete'))).toBeVisible().withTimeout(5000); + + await element(by.id('auto-join-unchecked')).tap(); + await waitFor(element(by.id('auto-join-tag'))).toBeVisible().withTimeout(5000); + await element(by.id(`rooms-list-view-item-${ existingRoom }`)).atIndex(0).longPress(); + + await waitFor(element(by.id('auto-join-checked'))).toBeVisible().withTimeout(5000); + await element(by.id('auto-join-checked')).tap(); + await waitFor(element(by.id('auto-join-tag'))).toBeNotVisible().withTimeout(5000); + await waitFor(element(by.id(`rooms-list-view-item-${ existingRoom }`))).toExist().withTimeout(6000); + }); + }) + + describe('Team actions', () => { + before(async() => { + await tapBack(); + await navigateToRoomActions(); + }); + + it('should add users to the team', async() => { + await waitFor(element(by.id('room-actions-add-user'))).toExist().withTimeout(10000); + await element(by.id('room-actions-add-user')).tap(); + + const rocketCat = 'rocket.cat'; + await element(by.id('select-users-view-search')).replaceText('rocket.cat'); + await waitFor(element(by.id(`select-users-view-item-${ rocketCat }`))).toExist().withTimeout(10000); + await element(by.id(`select-users-view-item-${ rocketCat }`)).tap(); + await waitFor(element(by.id(`selected-user-${ rocketCat }`))).toExist().withTimeout(5000); + + await waitFor(element(by.id('select-users-view-search'))).toExist().withTimeout(4000); + await element(by.id('select-users-view-search')).tap(); + await element(by.id('select-users-view-search')).replaceText(user.username); + await waitFor(element(by.id(`select-users-view-item-${ user.username }`))).toExist().withTimeout(10000); + await element(by.id(`select-users-view-item-${ user.username }`)).tap(); + await waitFor(element(by.id(`selected-user-${ user.username }`))).toExist().withTimeout(5000); + + await element(by.id('selected-users-view-submit')).tap(); + await sleep(300); + await waitFor(element(by.id('room-actions-members'))).toExist().withTimeout(10000); + await element(by.id('room-actions-members')).tap(); + await element(by.id('room-members-view-toggle-status')).tap(); + await waitFor(element(by.id(`room-members-view-item-${ user.username }`))).toExist().withTimeout(60000); + await backToActions(); + }); + + it('should try to leave to leave team and raise alert', async() => { + await element(by.id('room-actions-scrollview')).scrollTo('bottom'); + await waitFor(element(by.id('room-actions-leave-channel'))).toExist().withTimeout(2000); + await element(by.id('room-actions-leave-channel')).tap(); + + await waitFor(element(by.id('select-list-view'))).toExist().withTimeout(2000); + await waitFor(element(by.id(`select-list-view-item-${room}`))).toExist().withTimeout(2000); + await waitFor(element(by.id(`select-list-view-item-${existingRoom}`))).toExist().withTimeout(2000); + await element(by.id(`select-list-view-item-${room}`)).tap(); + + await waitFor(element(by.label('You are the last owner of this channel. Once you leave the team, the channel will be kept inside the team but you will be managing it from outside.'))).toExist().withTimeout(2000); + await element(by.text('OK')).tap(); + await waitFor(element(by.id('select-list-view-submit'))).toExist().withTimeout(2000); + await element(by.id('select-list-view-submit')).tap(); + await waitFor(element(by.text('Last owner cannot be removed'))).toExist().withTimeout(8000); + await element(by.text('OK')).tap(); + await tapBack(); + await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(2000); + }); + + describe('Room Members', async() => { + before(async() => { + await element(by.id('room-actions-members')).tap(); + await waitFor(element(by.id('room-members-view'))).toExist().withTimeout(2000); + }); + + it('should show all users', async() => { + await element(by.id('room-members-view-toggle-status')).tap(); + await waitFor(element(by.id(`room-members-view-item-${ user.username }`))).toExist().withTimeout(60000); + }); + + it('should filter user', async() => { + await waitFor(element(by.id(`room-members-view-item-${ user.username }`))).toExist().withTimeout(60000); + await element(by.id('room-members-view-search')).replaceText('rocket'); + await waitFor(element(by.id(`room-members-view-item-${ user.username }`))).toBeNotVisible().withTimeout(60000); + await element(by.id('room-members-view-search')).tap(); + await element(by.id('room-members-view-search')).clearText(''); + await waitFor(element(by.id(`room-members-view-item-${ user.username }`))).toExist().withTimeout(60000); + }); + + it('should remove member from team', async() => { + await openActionSheet('rocket.cat'); + await element(by.id('action-sheet-remove-from-team')).tap(); + await waitFor(element(by.id('select-list-view'))).toExist().withTimeout(5000); + await waitFor(element(by.id(`select-list-view-item-${ room }`))).toExist().withTimeout(5000); + await element(by.id(`select-list-view-item-${ room }`)).tap(); + await waitFor(element(by.id(`${ room }-checked`))).toExist().withTimeout(5000); + await element(by.id(`select-list-view-item-${ room }`)).tap(); + await waitFor(element(by.id(`${ room }-unchecked`))).toExist().withTimeout(5000); + await element(by.id('select-list-view-submit')).tap(); + await waitFor(element(by.id('room-members-view-item-rocket.cat'))).toBeNotVisible().withTimeout(60000); + }); + + it('should set member as owner', async() => { + await openActionSheet(user.username); + await element(by.id('action-sheet-set-owner')).tap(); + await waitForToast(); + + await openActionSheet(user.username); + await waitFor(element(by.id('action-sheet-set-owner-checked'))).toBeVisible().withTimeout(6000); + await closeActionSheet(); + }); + + it('should leave team', async() => { + await tapBack(); + await element(by.id('room-actions-scrollview')).scrollTo('bottom'); + await waitFor(element(by.id('room-actions-leave-channel'))).toExist().withTimeout(2000); + await element(by.id('room-actions-leave-channel')).tap(); + + await waitFor(element(by.id('select-list-view'))).toExist().withTimeout(2000); + await waitFor(element(by.id(`select-list-view-item-${room}`))).toExist().withTimeout(2000); + await waitFor(element(by.id(`select-list-view-item-${existingRoom}`))).toExist().withTimeout(2000); + await element(by.id(`select-list-view-item-${room}`)).tap(); + + await waitFor(element(by.label('You are the last owner of this channel. Once you leave the team, the channel will be kept inside the team but you will be managing it from outside.'))).toExist().withTimeout(2000); + await element(by.text('OK')).tap(); + await waitFor(element(by.id('select-list-view-submit'))).toExist().withTimeout(2000); + await element(by.id('select-list-view-submit')).tap(); + await waitFor(element(by.text(`You were removed from ${ team }`))).toExist().withTimeout(8000); + await element(by.text('OK')).tap(); + await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(5000); + }); + }); + }); + }); +}); \ No newline at end of file diff --git a/e2e/tests/team/03-moveconvert.spec.js b/e2e/tests/team/03-moveconvert.spec.js new file mode 100644 index 000000000..17143c609 --- /dev/null +++ b/e2e/tests/team/03-moveconvert.spec.js @@ -0,0 +1,89 @@ +const { + device, expect, element, by, waitFor +} = require('detox'); +const data = require('../../data'); +const { navigateToLogin, login, tapBack, searchRoom, sleep } = require('../../helpers/app'); + +const toBeConverted = `to-be-converted-${ data.random }`; +const toBeMoved = `to-be-moved-${ data.random }`; + +const createChannel = async(room) => { + await element(by.id('rooms-list-view-create-channel')).tap(); + await waitFor(element(by.id('new-message-view'))).toExist().withTimeout(5000); + await element(by.id('new-message-view-create-channel')).tap(); + await waitFor(element(by.id('select-users-view'))).toExist().withTimeout(5000); + await element(by.id('selected-users-view-submit')).tap(); + await waitFor(element(by.id('create-channel-view'))).toExist().withTimeout(10000); + await element(by.id('create-channel-name')).typeText(room); + await element(by.id('create-channel-submit')).tap(); + await waitFor(element(by.id('room-view'))).toExist().withTimeout(60000); + await waitFor(element(by.id(`room-view-title-${ room }`))).toExist().withTimeout(60000); + await tapBack(); + await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(2000); + await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toExist().withTimeout(60000); +} + +async function navigateToRoom(room) { + await searchRoom(`${ room }`); + await element(by.id(`rooms-list-view-item-${ room }`)).tap(); + await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000); +} + +async function navigateToRoomActions(room) { + await navigateToRoom(room); + await element(by.id('room-header')).tap(); + await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000); +} + +describe('Move/Convert Team', () => { + before(async() => { + await device.launchApp({ permissions: { notifications: 'YES' }, delete: true }); + await navigateToLogin(); + await login(data.users.regular.username, data.users.regular.password); + }); + + describe('Convert', async() => { + before(async() => { + await createChannel(toBeConverted); + }); + + it('should convert channel to a team', async() => { + await navigateToRoomActions(toBeConverted); + await element(by.id('room-actions-scrollview')).scrollTo('bottom'); + await waitFor(element(by.id('room-actions-convert-to-team'))).toExist().withTimeout(2000); + await element(by.id('room-actions-convert-to-team')).tap(); + await waitFor(element(by.label('This can\'t be undone. Once you convert a channel to a team, you can not turn it back to a channel.'))).toExist().withTimeout(2000); + await element(by.text('Convert')).tap(); + await waitFor(element(by.id('room-view'))).toExist().withTimeout(20000); + await waitFor(element(by.id(`room-view-title-${ toBeConverted }`))).toExist().withTimeout(6000); + }); + + after(async() => { + await tapBack(); + await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(2000); + }) + }); + + describe('Move', async() => { + before(async() => { + await createChannel(toBeMoved); + }); + + it('should move channel to a team', async() => { + await navigateToRoomActions(toBeMoved); + await element(by.id('room-actions-scrollview')).scrollTo('bottom'); + await waitFor(element(by.id('room-actions-move-to-team'))).toExist().withTimeout(2000); + await element(by.id('room-actions-move-to-team')).tap(); + await waitFor(element(by.id('select-list-view'))).toExist().withTimeout(2000); + await element(by.id('select-list-view-submit')).tap(); + await sleep(2000); + await waitFor(element(by.id('select-list-view'))).toExist().withTimeout(2000); + await waitFor(element(by.id(`select-list-view-item-${toBeConverted}`))).toExist().withTimeout(2000); + await element(by.id(`select-list-view-item-${toBeConverted}`)).tap(); + await element(by.id('select-list-view-submit')).atIndex(0).tap(); + await waitFor(element(by.label('After reading the previous intructions about this behavior, do you still want to move this channel to the selected team?'))).toExist().withTimeout(2000); + await element(by.text('Yes, move it!')).tap(); + await waitFor(element(by.id('room-view-header-team-channels'))).toExist().withTimeout(10000); + }); + }) +}); From 91371e88d95138e60e435d573b25bcf5d236d8f6 Mon Sep 17 00:00:00 2001 From: Gerzon Z Date: Fri, 4 Jun 2021 14:07:26 -0400 Subject: [PATCH 26/44] [NEW] Add Teams to Directory (#3181) * Added Teams to DirectoryView * Fix icon * Minor tweaks * add tests Co-authored-by: Diego Mello --- app/i18n/locales/en.json | 1 + app/lib/rocketchat.js | 4 ++ app/presentation/DirectoryItem/index.js | 7 ++-- app/views/DirectoryView/Options.js | 6 +++ app/views/DirectoryView/index.js | 40 +++++++++++++++++-- .../assorted/09-joinfromdirectory.spec.js | 16 ++++++-- 6 files changed, 63 insertions(+), 11 deletions(-) diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index a8f8d9635..e23bb33b7 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -332,6 +332,7 @@ "My_servers": "My servers", "N_people_reacted": "{{n}} people reacted", "N_users": "{{n}} users", + "N_channels": "{{n}} channels", "name": "name", "Name": "Name", "Navigation_history": "Navigation history", diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index f174aa1eb..6e7b3bcb2 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -798,6 +798,10 @@ const RocketChat = { // RC 3.13.0 return this.sdk.get('teams.listRoomsOfUser', { teamId, userId }); }, + getTeamInfo({ teamId }) { + // RC 3.13.0 + return this.sdk.get('teams.info', { teamId }); + }, convertChannelToTeam({ rid, name, type }) { const params = { ...(type === 'c' diff --git a/app/presentation/DirectoryItem/index.js b/app/presentation/DirectoryItem/index.js index 75b944c5e..9f98969af 100644 --- a/app/presentation/DirectoryItem/index.js +++ b/app/presentation/DirectoryItem/index.js @@ -18,7 +18,7 @@ const DirectoryItemLabel = React.memo(({ text, theme }) => { }); const DirectoryItem = ({ - title, description, avatar, onPress, testID, style, rightLabel, type, rid, theme + title, description, avatar, onPress, testID, style, rightLabel, type, rid, theme, teamMain }) => ( - + {title} { description ? {description} : null } @@ -56,7 +56,8 @@ DirectoryItem.propTypes = { style: PropTypes.any, rightLabel: PropTypes.string, rid: PropTypes.string, - theme: PropTypes.string + theme: PropTypes.string, + teamMain: PropTypes.bool }; DirectoryItemLabel.propTypes = { diff --git a/app/views/DirectoryView/Options.js b/app/views/DirectoryView/Options.js index ad8de1da1..a88bf42be 100644 --- a/app/views/DirectoryView/Options.js +++ b/app/views/DirectoryView/Options.js @@ -64,6 +64,11 @@ export default class DirectoryOptions extends PureComponent { icon = 'channel-public'; } + if (itemType === 'teams') { + text = 'Teams'; + icon = 'teams'; + } + return ( changeType(itemType)} @@ -105,6 +110,7 @@ export default class DirectoryOptions extends PureComponent { {this.renderItem('channels')} {this.renderItem('users')} + {this.renderItem('teams')} {isFederationEnabled ? ( <> diff --git a/app/views/DirectoryView/index.js b/app/views/DirectoryView/index.js index f78e74a21..c5cacb64d 100644 --- a/app/views/DirectoryView/index.js +++ b/app/views/DirectoryView/index.js @@ -121,6 +121,8 @@ class DirectoryView extends React.Component { logEvent(events.DIRECTORY_SEARCH_USERS); } else if (type === 'channels') { logEvent(events.DIRECTORY_SEARCH_CHANNELS); + } else if (type === 'teams') { + logEvent(events.DIRECTORY_SEARCH_TEAMS); } } @@ -149,17 +151,34 @@ class DirectoryView extends React.Component { if (result.success) { this.goRoom({ rid: result.room._id, name: item.username, t: 'd' }); } - } else { + } else if (['p', 'c'].includes(item.t) && !item.teamMain) { const { room } = await RocketChat.getRoomInfo(item._id); this.goRoom({ rid: item._id, name: item.name, joinCodeRequired: room.joinCodeRequired, t: 'c', search: true }); + } else { + this.goRoom({ + rid: item._id, name: item.name, t: item.t, search: true, teamMain: item.teamMain, teamId: item.teamId + }); } } renderHeader = () => { const { type } = this.state; const { theme } = this.props; + let text = 'Users'; + let icon = 'user'; + + if (type === 'channels') { + text = 'Channels'; + icon = 'channel-public'; + } + + if (type === 'teams') { + text = 'Teams'; + icon = 'teams'; + } + return ( <> - - {type === 'users' ? I18n.t('Users') : I18n.t('Channels')} + + {I18n.t(text)} @@ -217,12 +236,25 @@ class DirectoryView extends React.Component { /> ); } + + if (type === 'teams') { + return ( + + ); + } return ( ); diff --git a/e2e/tests/assorted/09-joinfromdirectory.spec.js b/e2e/tests/assorted/09-joinfromdirectory.spec.js index 308c4db2c..039ef823c 100644 --- a/e2e/tests/assorted/09-joinfromdirectory.spec.js +++ b/e2e/tests/assorted/09-joinfromdirectory.spec.js @@ -32,16 +32,24 @@ describe('Join room from directory', () => { await navigateToRoom(data.channels.detoxpublic.name); }) - it('should back and tap directory', async() => { + it('should search user and navigate', async() => { await tapBack(); await element(by.id('rooms-list-view-directory')).tap(); - }) - - it('should search user and navigate', async() => { + await waitFor(element(by.id('directory-view'))).toExist().withTimeout(2000); await element(by.id('directory-view-dropdown')).tap(); await element(by.label('Users')).tap(); await element(by.label('Search by')).tap(); await navigateToRoom(data.users.alternate.username); }) + + it('should search user and navigate', async() => { + await tapBack(); + await element(by.id('rooms-list-view-directory')).tap(); + await waitFor(element(by.id('directory-view'))).toExist().withTimeout(2000); + await element(by.id('directory-view-dropdown')).tap(); + await element(by.label('Teams')).tap(); + await element(by.label('Search by')).tap(); + await navigateToRoom(data.teams.private.name); + }) }); }); From cf6ddf63528424535c516a26a2c2b770379b4c22 Mon Sep 17 00:00:00 2001 From: Gerzon Z Date: Fri, 4 Jun 2021 14:08:37 -0400 Subject: [PATCH 27/44] [CHORE] Add logEvents for Teams (#3182) * added events for team channels view and add existing channel view * add logevents for room actions view and room info edit view Co-authored-by: Diego Mello --- app/utils/log/events.js | 19 ++++++++++++++++--- app/views/AddExistingChannelView.js | 6 +++--- app/views/RoomActionsView/index.js | 7 +++++++ app/views/RoomInfoEditView/index.js | 2 ++ app/views/TeamChannelsView.js | 5 +++++ 5 files changed, 33 insertions(+), 6 deletions(-) diff --git a/app/utils/log/events.js b/app/utils/log/events.js index a6d2eaa80..a183735ba 100644 --- a/app/utils/log/events.js +++ b/app/utils/log/events.js @@ -100,8 +100,8 @@ export default { SELECTED_USERS_CREATE_GROUP_F: 'selected_users_create_group_f', // ADD EXISTING CHANNEL VIEW - EXISTING_CHANNEL_ADD_CHANNEL: 'existing_channel_add_channel', - EXISTING_CHANNEL_REMOVE_CHANNEL: 'existing_channel_remove_channel', + AEC_ADD_CHANNEL: 'aec_add_channel', + AEC_REMOVE_CHANNEL: 'aec_remove_channel', // CREATE CHANNEL VIEW CR_CREATE: 'cr_create', @@ -255,6 +255,13 @@ export default { RA_TOGGLE_BLOCK_USER_F: 'ra_toggle_block_user_f', RA_TOGGLE_ENCRYPTED: 'ra_toggle_encrypted', RA_TOGGLE_ENCRYPTED_F: 'ra_toggle_encrypted_f', + RA_LEAVE_TEAM: 'ra_leave_team', + RA_LEAVE_TEAM_F: 'ra_leave_team_f', + RA_CONVERT_TO_TEAM: 'ra_convert_to_team', + RA_CONVERT_TO_TEAM_F: 'ra_convert_to_team_f', + RA_MOVE_TO_TEAM: 'ra_move_to_team', + RA_MOVE_TO_TEAM_F: 'ra_move_to_team_f', + RA_SEARCH_TEAM: 'ra_search_team', // ROOM INFO VIEW RI_GO_RI_EDIT: 'ri_go_ri_edit', @@ -274,6 +281,8 @@ export default { RI_EDIT_TOGGLE_ARCHIVE_F: 'ri_edit_toggle_archive_f', RI_EDIT_DELETE: 'ri_edit_delete', RI_EDIT_DELETE_F: 'ri_edit_delete_f', + RI_EDIT_DELETE_TEAM: 'ri_edit_delete_team', + RI_EDIT_DELETE_TEAM_F: 'ri_edit_delete_team_f', // JITSI MEET VIEW JM_CONFERENCE_JOIN: 'jm_conference_join', @@ -327,5 +336,9 @@ export default { TC_SEARCH: 'tc_search', TC_CANCEL_SEARCH: 'tc_cancel_search', TC_GO_ACTIONS: 'tc_go_actions', - TC_GO_ROOM: 'tc_go_room' + TC_GO_ROOM: 'tc_go_room', + TC_DELETE_ROOM: 'tc_delete_room', + TC_DELETE_ROOM_F: 'tc_delete_room_f', + TC_TOGGLE_AUTOJOIN: 'tc_toggle_autojoin', + TC_TOGGLE_AUTOJOIN_F: 'tc_toggle_autojoin_f' }; diff --git a/app/views/AddExistingChannelView.js b/app/views/AddExistingChannelView.js index e7a590a54..0edced4b0 100644 --- a/app/views/AddExistingChannelView.js +++ b/app/views/AddExistingChannelView.js @@ -126,8 +126,8 @@ class AddExistingChannelView extends React.Component { goRoom({ item: result, isMasterDetail }); } } catch (e) { - showErrorAlert(I18n.t(e.data.error), I18n.t('Add_Existing_Channel'), () => {}); logEvent(events.CT_ADD_ROOM_TO_TEAM_F); + showErrorAlert(I18n.t(e.data.error), I18n.t('Add_Existing_Channel'), () => {}); this.setState({ loading: false }); } } @@ -151,10 +151,10 @@ class AddExistingChannelView extends React.Component { animateNextTransition(); if (!this.isChecked(rid)) { - logEvent(events.EXISTING_CHANNEL_ADD_CHANNEL); + logEvent(events.AEC_ADD_CHANNEL); this.setState({ selected: [...selected, rid] }, () => this.setHeader()); } else { - logEvent(events.EXISTING_CHANNEL_REMOVE_CHANNEL); + logEvent(events.AEC_REMOVE_CHANNEL); const filterSelected = selected.filter(el => el !== rid); this.setState({ selected: filterSelected }, () => this.setHeader()); } diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index 74dc308fa..0748cf783 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -431,6 +431,7 @@ class RoomActionsView extends React.Component { } handleLeaveTeam = async(selected) => { + logEvent(events.RA_LEAVE_TEAM); try { const { room } = this.state; const { navigation, isMasterDetail } = this.props; @@ -444,6 +445,7 @@ class RoomActionsView extends React.Component { } } } catch (e) { + logEvent(events.RA_LEAVE_TEAM_F); log(e); showErrorAlert( e.data.error @@ -492,6 +494,7 @@ class RoomActionsView extends React.Component { } handleConvertToTeam = async() => { + logEvent(events.RA_CONVERT_TO_TEAM); try { const { room } = this.state; const { navigation } = this.props; @@ -501,6 +504,7 @@ class RoomActionsView extends React.Component { navigation.navigate('RoomView'); } } catch (e) { + logEvent(events.RA_CONVERT_TO_TEAM_F); log(e); } } @@ -515,6 +519,7 @@ class RoomActionsView extends React.Component { } handleMoveToTeam = async(selected) => { + logEvent(events.RA_MOVE_TO_TEAM); try { const { room } = this.state; const { navigation } = this.props; @@ -523,6 +528,7 @@ class RoomActionsView extends React.Component { navigation.navigate('RoomView'); } } catch (e) { + logEvent(events.RA_MOVE_TO_TEAM_F); log(e); showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t('moving_channel_to_team') })); } @@ -569,6 +575,7 @@ class RoomActionsView extends React.Component { } searchTeam = async(onChangeText) => { + logEvent(events.RA_SEARCH_TEAM); try { const { addTeamChannelPermission, createTeamPermission } = this.props; const QUERY_SIZE = 50; diff --git a/app/views/RoomInfoEditView/index.js b/app/views/RoomInfoEditView/index.js index 90d8b28f5..8905e5ee1 100644 --- a/app/views/RoomInfoEditView/index.js +++ b/app/views/RoomInfoEditView/index.js @@ -293,6 +293,7 @@ class RoomInfoEditView extends React.Component { } handleDeleteTeam = async(selected) => { + logEvent(events.RI_EDIT_DELETE_TEAM); const { navigation, isMasterDetail } = this.props; const { room } = this.state; try { @@ -305,6 +306,7 @@ class RoomInfoEditView extends React.Component { } } } catch (e) { + logEvent(events.RI_EDIT_DELETE_TEAM_F); log(e); showErrorAlert( e.data.error diff --git a/app/views/TeamChannelsView.js b/app/views/TeamChannelsView.js index cee7fe13c..82e4ecdc3 100644 --- a/app/views/TeamChannelsView.js +++ b/app/views/TeamChannelsView.js @@ -301,6 +301,7 @@ class TeamChannelsView extends React.Component { }, 1000, true); toggleAutoJoin = async(item) => { + logEvent(events.TC_TOGGLE_AUTOJOIN); try { const { data } = this.state; const result = await RocketChat.updateTeamRoom({ roomId: item._id, isDefault: !item.teamDefault }); @@ -314,6 +315,7 @@ class TeamChannelsView extends React.Component { this.setState({ data: newData }); } } catch (e) { + logEvent(events.TC_TOGGLE_AUTOJOIN_F); log(e); } } @@ -338,6 +340,7 @@ class TeamChannelsView extends React.Component { } removeRoom = async(item) => { + logEvent(events.TC_DELETE_ROOM); try { const { data } = this.state; const result = await RocketChat.removeTeamRoom({ roomId: item._id, teamId: this.team.teamId }); @@ -346,11 +349,13 @@ class TeamChannelsView extends React.Component { this.setState({ data: newData }); } } catch (e) { + logEvent(events.TC_DELETE_ROOM_F); log(e); } } delete = (item) => { + logEvent(events.TC_DELETE_ROOM); const { deleteRoom } = this.props; Alert.alert( From 5f2aba3d74d4cb4db06f4547050d5e46ea20764f Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Fri, 4 Jun 2021 15:10:01 -0300 Subject: [PATCH 28/44] [FIX] Disable jitsi call for teams (#3183) Co-authored-by: Diego Mello --- app/views/RoomActionsView/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index 0748cf783..0c7267d0a 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -681,7 +681,7 @@ class RoomActionsView extends React.Component { renderJitsi = () => { const { room } = this.state; const { jitsiEnabled } = this.props; - if (!jitsiEnabled) { + if (!jitsiEnabled || room.teamMain) { return null; } return ( From abf3945f32344834f27c789b51f92ed3d5eac470 Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Fri, 4 Jun 2021 15:40:50 -0300 Subject: [PATCH 29/44] [FIX] Show alert `Not allowed` when click on a private channel that you don't be invited before (#3177) * [FIX] Showing only channel you joined * [FIX] How to get the params to mnavigation to other room from TeamChannelList * Show alert Not allowed when trying access private channel that you don't joined Co-authored-by: Diego Mello --- app/views/TeamChannelsView.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/app/views/TeamChannelsView.js b/app/views/TeamChannelsView.js index 82e4ecdc3..40c8c05ac 100644 --- a/app/views/TeamChannelsView.js +++ b/app/views/TeamChannelsView.js @@ -282,21 +282,20 @@ class TeamChannelsView extends React.Component { logEvent(events.TC_GO_ROOM); const { navigation, isMasterDetail } = this.props; try { - let params = {}; - if (item.rid) { - params = item; - } else { - const { room } = await RocketChat.getRoomInfo(item._id); - params = { - rid: item._id, name: RocketChat.getRoomTitle(room), joinCodeRequired: room.joinCodeRequired, t: room.t, teamId: room.teamId - }; - } + const { room } = await RocketChat.getRoomInfo(item._id); + const params = { + rid: item._id, name: RocketChat.getRoomTitle(room), joinCodeRequired: room.joinCodeRequired, t: room.t, teamId: room.teamId + }; if (isMasterDetail) { navigation.pop(); } goRoom({ item: params, isMasterDetail, navigationMethod: navigation.push }); } catch (e) { - // do nothing + if (e.data.error === 'not-allowed') { + showErrorAlert(I18n.t('error-not-allowed')); + } else { + showErrorAlert(e.data.error); + } } }, 1000, true); From fa00ef92efa45fef3938afbb92be52b97cb16358 Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Fri, 4 Jun 2021 17:23:31 -0300 Subject: [PATCH 30/44] [IMPROVEMENT] Load team's rooms from local database on team leave (#3185) * [IMPROVEMENT] Search team list rooms of user in watermelon db * Minor nitpick Co-authored-by: Diego Mello --- app/views/RoomActionsView/index.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index 0c7267d0a..77a4cc59b 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -461,11 +461,16 @@ class RoomActionsView extends React.Component { const { navigation } = this.props; try { - const result = await RocketChat.teamListRoomsOfUser({ teamId: room.teamId, userId: room.u._id }); + const db = database.active; + const subCollection = db.get('subscriptions'); + const rooms = await subCollection.query( + Q.where('team_id', Q.eq(room.teamId)), + Q.where('team_main', Q.notEq(true)) + ); - if (result.rooms?.length) { - const teamChannels = result.rooms.map(r => ({ - rid: r._id, + if (rooms.length) { + const teamChannels = rooms.map(r => ({ + rid: r.id, name: r.name, teamId: r.teamId, alert: r.isLastOwner From 2502b27564926f3a15bfa0ed04ef9c9ede292c21 Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Fri, 4 Jun 2021 17:53:39 -0300 Subject: [PATCH 31/44] [FIX] Option to prevent users from using Invisible status (#3186) * [FIX] Option to prevent users from using Invisible status * Added error to pt-BR Co-authored-by: Diego Mello --- app/constants/settings.js | 3 +++ app/i18n/locales/en.json | 1 + app/i18n/locales/pt-BR.json | 1 + app/views/StatusView.js | 14 +++++++++++--- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/constants/settings.js b/app/constants/settings.js index 9f0df8865..d684a11c1 100644 --- a/app/constants/settings.js +++ b/app/constants/settings.js @@ -193,5 +193,8 @@ export default { }, Allow_Save_Media_to_Gallery: { type: 'valueAsBoolean' + }, + Accounts_AllowInvisibleStatusOption: { + type: 'valueAsString' } }; diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index e23bb33b7..1f37628d6 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -79,6 +79,7 @@ "error-user-registration-disabled": "User registration is disabled", "error-user-registration-secret": "User registration is only allowed via Secret URL", "error-you-are-last-owner": "You are the last owner. Please set new owner before leaving the room.", + "error-status-not-allowed": "Invisible status is disabled", "Actions": "Actions", "activity": "activity", "Activity": "Activity", diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index 7eca8b049..293959613 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -75,6 +75,7 @@ "error-user-registration-disabled": "O registro do usuário está desativado", "error-user-registration-secret": "O registro de usuário é permitido somente via URL secreta", "error-you-are-last-owner": "Você é o último proprietário da sala. Por favor defina um novo proprietário antes de sair.", + "error-status-not-allowed": "O status invisível está desativado", "Actions": "Ações", "activity": "atividade", "Activity": "Atividade", diff --git a/app/views/StatusView.js b/app/views/StatusView.js index 3f5a48c12..2ac358343 100644 --- a/app/views/StatusView.js +++ b/app/views/StatusView.js @@ -8,6 +8,7 @@ import * as List from '../containers/List'; import Status from '../containers/Status/Status'; import TextInput from '../containers/TextInput'; import EventEmitter from '../utils/events'; +import { showErrorAlert } from '../utils/info'; import Loading from '../containers/Loading'; import RocketChat from '../lib/rocketchat'; import log, { logEvent, events } from '../utils/log'; @@ -58,7 +59,8 @@ class StatusView extends React.Component { theme: PropTypes.string, navigation: PropTypes.object, isMasterDetail: PropTypes.bool, - setUser: PropTypes.func + setUser: PropTypes.func, + Accounts_AllowInvisibleStatusOption: PropTypes.bool } constructor(props) { @@ -168,6 +170,7 @@ class StatusView extends React.Component { setUser({ status: item.id }); } } catch (e) { + showErrorAlert(I18n.t(e.data.errorType)); logEvent(events.SET_STATUS_FAIL); log(e); } @@ -181,10 +184,14 @@ class StatusView extends React.Component { render() { const { loading } = this.state; + const { Accounts_AllowInvisibleStatusOption } = this.props; + + const status = Accounts_AllowInvisibleStatusOption ? STATUS : STATUS.filter(s => s.id !== 'offline'); + return ( item.id} renderItem={this.renderItem} ListHeaderComponent={this.renderHeader} @@ -199,7 +206,8 @@ class StatusView extends React.Component { const mapStateToProps = state => ({ user: getUserSelector(state), - isMasterDetail: state.app.isMasterDetail + isMasterDetail: state.app.isMasterDetail, + Accounts_AllowInvisibleStatusOption: state.settings.Accounts_AllowInvisibleStatusOption ?? true }); const mapDispatchToProps = dispatch => ({ From 87dd31a91adb13011019e527e8fe78126adf2b1b Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Mon, 7 Jun 2021 11:13:48 -0300 Subject: [PATCH 32/44] [FIX] Item not animating on tap on team's channels view (#3187) --- app/views/TeamChannelsView.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/TeamChannelsView.js b/app/views/TeamChannelsView.js index 40c8c05ac..64822aeae 100644 --- a/app/views/TeamChannelsView.js +++ b/app/views/TeamChannelsView.js @@ -1,10 +1,9 @@ import React from 'react'; -import { Keyboard, Alert } from 'react-native'; +import { Keyboard, Alert, FlatList } from 'react-native'; import PropTypes from 'prop-types'; import { Q } from '@nozbe/watermelondb'; import { withSafeAreaInsets } from 'react-native-safe-area-context'; import { connect } from 'react-redux'; -import { FlatList } from 'react-native-gesture-handler'; import StatusBar from '../containers/StatusBar'; import RoomHeader from '../containers/RoomHeader'; From 29ccb4745670751938244dee7ed1eb8c494ced40 Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Mon, 7 Jun 2021 11:56:11 -0300 Subject: [PATCH 33/44] [FIX] Directory sending incorrect room type (#3188) Co-authored-by: Diego Mello --- app/views/DirectoryView/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/DirectoryView/index.js b/app/views/DirectoryView/index.js index c5cacb64d..6e60b44bc 100644 --- a/app/views/DirectoryView/index.js +++ b/app/views/DirectoryView/index.js @@ -154,7 +154,7 @@ class DirectoryView extends React.Component { } else if (['p', 'c'].includes(item.t) && !item.teamMain) { const { room } = await RocketChat.getRoomInfo(item._id); this.goRoom({ - rid: item._id, name: item.name, joinCodeRequired: room.joinCodeRequired, t: 'c', search: true + rid: item._id, name: item.name, joinCodeRequired: room.joinCodeRequired, t: item.t, search: true }); } else { this.goRoom({ From 0b7461e800b1870a5a725b33367c4f70982fbef3 Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Mon, 7 Jun 2021 13:18:14 -0300 Subject: [PATCH 34/44] [FIX] App not showing proper alert on team leave (#3161) * [IMPROVEMENT] refactoring how to leave team * Fix the data passed to leaveTeam * Fixed the lint error in i18n, the path of i18n, merged two ifs in one * Fixed the Saga's flow when try to leave a room * Fixed params passed to leaveRoom * Fix the function name of leaveTeam Co-authored-by: Diego Mello --- app/actions/room.js | 7 ++--- app/i18n/locales/en.json | 1 + app/reducers/room.js | 2 +- app/sagas/room.js | 24 +++++++++++++---- app/views/RoomActionsView/index.js | 43 +++++++----------------------- 5 files changed, 34 insertions(+), 43 deletions(-) diff --git a/app/actions/room.js b/app/actions/room.js index 59916bec0..4ad7e87f6 100644 --- a/app/actions/room.js +++ b/app/actions/room.js @@ -14,11 +14,12 @@ export function unsubscribeRoom(rid) { }; } -export function leaveRoom(rid, t) { +export function leaveRoom(roomType, room, selected) { return { type: types.ROOM.LEAVE, - rid, - t + room, + roomType, + selected }; } diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 1f37628d6..80722f82d 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -724,6 +724,7 @@ "creating_team": "creating team", "team-name-already-exists": "A team with that name already exists", "Add_Channel_to_Team": "Add Channel to Team", + "Left_The_Team_Successfully": "Left the team successfully", "Create_New": "Create New", "Add_Existing": "Add Existing", "Add_Existing_Channel": "Add Existing Channel", diff --git a/app/reducers/room.js b/app/reducers/room.js index 20f86d1cc..de47fcede 100644 --- a/app/reducers/room.js +++ b/app/reducers/room.js @@ -22,7 +22,7 @@ export default function(state = initialState, action) { case ROOM.LEAVE: return { ...state, - rid: action.rid, + rid: action.room.rid, isDeleting: true }; case ROOM.DELETE: diff --git a/app/sagas/room.js b/app/sagas/room.js index 3b1b321ad..978944786 100644 --- a/app/sagas/room.js +++ b/app/sagas/room.js @@ -4,6 +4,7 @@ import { takeLatest, take, select, delay, race, put } from 'redux-saga/effects'; +import EventEmitter from '../utils/events'; import Navigation from '../lib/Navigation'; import * as types from '../actions/actionsTypes'; import { removedRoom } from '../actions/room'; @@ -11,6 +12,7 @@ import RocketChat from '../lib/rocketchat'; import log, { logEvent, events } from '../utils/log'; import I18n from '../i18n'; import { showErrorAlert } from '../utils/info'; +import { LISTENER } from '../containers/Toast'; const watchUserTyping = function* watchUserTyping({ rid, status }) { const auth = yield select(state => state.login.isAuthenticated); @@ -30,13 +32,18 @@ const watchUserTyping = function* watchUserTyping({ rid, status }) { } }; -const handleRemovedRoom = function* handleRemovedRoom() { +const handleRemovedRoom = function* handleRemovedRoom(roomType) { const isMasterDetail = yield select(state => state.app.isMasterDetail); if (isMasterDetail) { yield Navigation.navigate('DrawerNavigator'); } else { yield Navigation.navigate('RoomsListView'); } + + if (roomType === 'team') { + EventEmitter.emit(LISTENER, { message: I18n.t('Left_The_Team_Successfully') }); + } + // types.ROOM.REMOVE is triggered by `subscriptions-changed` with `removed` arg const { timeout } = yield race({ deleteFinished: take(types.ROOM.REMOVED), @@ -47,12 +54,19 @@ const handleRemovedRoom = function* handleRemovedRoom() { } }; -const handleLeaveRoom = function* handleLeaveRoom({ rid, t }) { +const handleLeaveRoom = function* handleLeaveRoom({ room, roomType, selected }) { logEvent(events.RA_LEAVE); try { - const result = yield RocketChat.leaveRoom(rid, t); - if (result.success) { - yield handleRemovedRoom(); + let result = {}; + + if (roomType === 'channel') { + result = yield RocketChat.leaveRoom(room.rid, room.t); + } else if (roomType === 'team') { + result = yield RocketChat.leaveTeam({ teamName: room.name, ...(selected && { rooms: selected }) }); + } + + if (result?.success) { + yield handleRemovedRoom(roomType); } } catch (e) { logEvent(events.RA_LEAVE_F); diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index 77a4cc59b..7ed2e1a57 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -10,7 +10,9 @@ import { Q } from '@nozbe/watermelondb'; import { compareServerVersion, methods } from '../../lib/utils'; import Touch from '../../utils/touch'; import { setLoading as setLoadingAction } from '../../actions/selectedUsers'; -import { leaveRoom as leaveRoomAction, closeRoom as closeRoomAction } 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'; @@ -54,7 +56,6 @@ class RoomActionsView extends React.Component { theme: PropTypes.string, fontScale: PropTypes.number, serverVersion: PropTypes.string, - isMasterDetail: PropTypes.bool, addUserToJoinedRoomPermission: PropTypes.array, addUserToAnyCRoomPermission: PropTypes.array, addUserToAnyPRoomPermission: PropTypes.array, @@ -426,39 +427,13 @@ class RoomActionsView extends React.Component { showConfirmationAlert({ message: I18n.t('Are_you_sure_you_want_to_leave_the_room', { room: RocketChat.getRoomTitle(room) }), confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }), - onPress: () => leaveRoom(room.rid, room.t) + onPress: () => leaveRoom('channel', room) }); } - handleLeaveTeam = async(selected) => { - logEvent(events.RA_LEAVE_TEAM); - try { - const { room } = this.state; - const { navigation, isMasterDetail } = this.props; - const result = await RocketChat.leaveTeam({ teamName: room.name, ...(selected && { rooms: selected }) }); - - if (result.success) { - if (isMasterDetail) { - navigation.navigate('DrawerNavigator'); - } else { - navigation.navigate('RoomsListView'); - } - } - } catch (e) { - logEvent(events.RA_LEAVE_TEAM_F); - log(e); - showErrorAlert( - e.data.error - ? I18n.t(e.data.error) - : I18n.t('There_was_an_error_while_action', { action: I18n.t('leaving_team') }), - I18n.t('Cannot_leave') - ); - } - } - leaveTeam = async() => { const { room } = this.state; - const { navigation } = this.props; + const { navigation, leaveRoom } = this.props; try { const db = database.active; @@ -479,21 +454,21 @@ class RoomActionsView extends React.Component { title: 'Leave_Team', data: teamChannels, infoText: 'Select_Team_Channels', - nextAction: data => this.handleLeaveTeam(data), + nextAction: data => leaveRoom('team', room, data), showAlert: () => showErrorAlert(I18n.t('Last_owner_team_room'), I18n.t('Cannot_leave')) }); } else { showConfirmationAlert({ message: I18n.t('You_are_leaving_the_team', { team: RocketChat.getRoomTitle(room) }), confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }), - onPress: () => this.handleLeaveTeam() + onPress: () => leaveRoom('team', room) }); } } catch (e) { showConfirmationAlert({ message: I18n.t('You_are_leaving_the_team', { team: RocketChat.getRoomTitle(room) }), confirmationText: I18n.t('Yes_action_it', { action: I18n.t('leave') }), - onPress: () => this.handleLeaveTeam() + onPress: () => leaveRoom('team', room) }); } } @@ -1122,7 +1097,7 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ - leaveRoom: (rid, t) => dispatch(leaveRoomAction(rid, t)), + leaveRoom: (roomType, room, selected) => dispatch(leaveRoomAction(roomType, room, selected)), closeRoom: rid => dispatch(closeRoomAction(rid)), setLoadingInvite: loading => dispatch(setLoadingAction(loading)) }); From b2c60e7b53b769cf02b63439ad821d484b841af7 Mon Sep 17 00:00:00 2001 From: "lingohub[bot]" <69908207+lingohub[bot]@users.noreply.github.com> Date: Mon, 7 Jun 2021 13:19:26 -0300 Subject: [PATCH 35/44] =?UTF-8?q?Language=20update=20from=20LingoHub=20?= =?UTF-8?q?=F0=9F=A4=96=20(#3192)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Project Name: Rocket.Chat.ReactNative Project Link: https://translate.lingohub.com/rocketchat/dashboard/rocket-dot-chat-dot-reactnative User: Robot LingoHub Easy language translations with LingoHub 🚀 Co-authored-by: Robot LingoHub Co-authored-by: Diego Mello --- app/i18n/locales/ar.json | 4 +- app/i18n/locales/de.json | 97 ++++- app/i18n/locales/en.json | 4 +- app/i18n/locales/es-ES.json | 174 ++++---- app/i18n/locales/fr.json | 586 +++++++++++++++++---------- app/i18n/locales/it.json | 11 +- app/i18n/locales/ja.json | 4 +- app/i18n/locales/nl.json | 783 ++++++++++++++++++++++++------------ app/i18n/locales/pt-BR.json | 10 +- app/i18n/locales/ru.json | 59 ++- app/i18n/locales/tr.json | 10 +- app/i18n/locales/zh-TW.json | 5 +- 12 files changed, 1134 insertions(+), 613 deletions(-) diff --git a/app/i18n/locales/ar.json b/app/i18n/locales/ar.json index 8d2f1094c..6f188ca55 100644 --- a/app/i18n/locales/ar.json +++ b/app/i18n/locales/ar.json @@ -284,6 +284,7 @@ "last_message": "الرسالة الأخيرة", "Leave_channel": "مغادرة القناة", "leaving_room": "مغادرة الغرفة", + "Leave": "مغادرة الغرفة", "leave": "مغادرة", "Legal": "قانوني", "Light": "ساطع", @@ -651,5 +652,6 @@ "You_will_be_logged_out_from_other_locations": "سيتم تسجيل خروج من الأماكن الأخرى", "Logged_out_of_other_clients_successfully": "تم تسجيل الخروج من الأماكن الأخرى بنجاح", "Logout_failed": "فشل تسجيل الخروج!", - "Log_analytics_events": "تحليلات سجل الأحداث" + "Log_analytics_events": "تحليلات سجل الأحداث", + "invalid-room": "غرفة غير صالحة" } \ No newline at end of file diff --git a/app/i18n/locales/de.json b/app/i18n/locales/de.json index 1074ff730..0a7f7e1ff 100644 --- a/app/i18n/locales/de.json +++ b/app/i18n/locales/de.json @@ -14,7 +14,7 @@ "error-delete-protected-role": "Eine geschützte Rolle kann nicht gelöscht werden", "error-department-not-found": "Abteilung nicht gefunden", "error-direct-message-file-upload-not-allowed": "Dateifreigabe in direkten Nachrichten nicht zulässig", - "error-duplicate-channel-name": "Ein Kanal mit dem Namen {{channel_name}} ist bereits vorhanden", + "error-duplicate-channel-name": "Ein Kanal mit dem Namen {{room_name}} ist bereits vorhanden", "error-email-domain-blacklisted": "Die E-Mail-Domain wird auf die schwarze Liste gesetzt", "error-email-send-failed": "Fehler beim Versuch, eine E-Mail zu senden: {{message}}", "error-save-image": "Fehler beim Speichern des Bildes", @@ -61,6 +61,7 @@ "error-message-editing-blocked": "Die Bearbeitung von Nachrichten ist gesperrt", "error-message-size-exceeded": "Die Nachrichtengröße überschreitet Message_MaxAllowedSize", "error-missing-unsubscribe-link": "Du musst den Link [abbestellen] angeben.", + "error-no-owner-channel": "Dieser Raum gehört dir nicht", "error-no-tokens-for-this-user": "Für diesen Benutzer gibt es keine Token", "error-not-allowed": "Nicht erlaubt", "error-not-authorized": "Nicht berechtigt", @@ -78,6 +79,7 @@ "error-user-registration-disabled": "Die Benutzerregistrierung ist deaktiviert", "error-user-registration-secret": "Die Benutzerregistrierung ist nur über eine geheime URL möglich", "error-you-are-last-owner": "Du bist der letzte Besitzer. Bitte setze einen neuen Besitzer, bevor du den Raum verlässt.", + "error-status-not-allowed": "Unsichtbar-Status ist deaktiviert", "Actions": "Aktionen", "activity": "Aktivität", "Activity": "Aktivität", @@ -90,6 +92,7 @@ "alert": "Benachrichtigung", "alerts": "Benachrichtigungen", "All_users_in_the_channel_can_write_new_messages": "Alle Benutzer im Kanal können neue Nachrichten schreiben", + "All_users_in_the_team_can_write_new_messages": "Alle Mitglieder eines Teams können neue Nachrichten schreiben", "A_meaningful_name_for_the_discussion_room": "Ein aussagekräftiger Name für den Diskussionsraum", "All": "alle", "All_Messages": "Alle Nachrichten", @@ -117,7 +120,7 @@ "Block_user": "Benutzer blockieren", "Browser": "Browser", "Broadcast_channel_Description": "Nur autorisierte Benutzer können neue Nachrichten schreiben, die anderen Benutzer können jedoch antworten", - "Broadcast_Channel": "Broadcastkanal", + "Broadcast_Channel": "Broadcast-Kanal", "Busy": "Beschäftigt", "By_proceeding_you_are_agreeing": "Indem du fortfährst, stimmst du zu unserem", "Cancel_editing": "Bearbeitung abbrechen", @@ -134,7 +137,7 @@ "Clear_cookies_desc": "Diese Aktion wird alle Login-Cookies löschen und erlaubt es dir, dich mit einem anderen Konto anzumelden.", "Clear_cookies_yes": "Ja, Cookies löschen", "Clear_cookies_no": "Nein, Cookies behalten", - "Click_to_join": "Klicken um teilzunehmen!", + "Click_to_join": "Klicken um beizutreten!", "Close": "Schließen", "Close_emoji_selector": "Schließe die Emoji-Auswahl", "Closing_chat": "Chat schließen", @@ -167,10 +170,10 @@ "Create_Channel": "Kanal erstellen", "Create_Direct_Messages": "Direkt-Nachricht erstellen", "Create_Discussion": "Diskussion erstellen", - "Created_snippet": "Erstellt ein Snippet", + "Created_snippet": "ein Snippet erstellt", "Create_a_new_workspace": "Erstelle einen neuen Arbeitsbereich", "Create": "Erstellen", - "Custom_Status": "eigener Status", + "Custom_Status": "Eigener Status", "Dark": "Dunkel", "Dark_level": "Dunkelstufe", "Default": "Standard", @@ -180,6 +183,7 @@ "delete": "löschen", "Delete": "Löschen", "DELETE": "LÖSCHEN", + "move": "verschieben", "deleting_room": "lösche Raum", "description": "Beschreibung", "Description": "Beschreibung", @@ -187,7 +191,7 @@ "Desktop_Notifications": "Desktop-Benachrichtigungen", "Desktop_Alert_info": "Diese Benachrichtigungen werden auf dem Desktop angezeigt", "Directory": "Verzeichnis", - "Direct_Messages": "Direkte Nachrichten", + "Direct_Messages": "Direktnachrichten", "Disable_notifications": "Benachrichtigungen deaktiveren", "Discussions": "Diskussionen", "Discussion_Desc": "Hilft dir die Übersicht zu behalten! Durch das Erstellen einer Diskussion wird ein Unter-Kanal im ausgewählten Raum erzeugt und beide verknüpft.", @@ -208,7 +212,7 @@ "Edit_Status": "Status ändern", "Edit_Invite": "Einladung bearbeiten", "End_to_end_encrypted_room": "Ende-zu-Ende-verschlüsselter Raum", - "end_to_end_encryption": "Nicht mehr Ende-zu-Ende-verschlüsseln", + "end_to_end_encryption": "Nicht mehr Ende-zu-Ende verschlüsseln", "Email_Notification_Mode_All": "Jede Erwähnung/Direktnachricht", "Email_Notification_Mode_Disabled": "Deaktiviert", "Email_or_password_field_is_empty": "Das E-Mail- oder Passwortfeld ist leer", @@ -225,6 +229,7 @@ "Encryption_error_title": "Dein Verschlüsselungs-Passwort scheint falsch zu sein", "Encryption_error_desc": "Es war nicht möglich deinen Verschlüsselungs-Key zu importieren.", "Everyone_can_access_this_channel": "Jeder kann auf diesen Kanal zugreifen", + "Everyone_can_access_this_team": "Jeder kann auf dieses Team zugreifen", "Error_uploading": "Fehler beim Hochladen", "Expiration_Days": "läuft ab (Tage)", "Favorite": "Favorisieren", @@ -269,7 +274,7 @@ "I_Saved_My_E2E_Password": "Ich habe mein Ende-zu-Ende-Passwort gesichert", "IP": "IP", "In_app": "In-App-Browser", - "In_App_And_Desktop": "In-app und Desktop", + "In_App_And_Desktop": "In-App und Desktop", "In_App_and_Desktop_Alert_info": "Zeigt ein Banner oben am Bildschirm, wenn die App geöffnet ist und eine Benachrichtigung auf dem Desktop.", "Invisible": "Unsichtbar", "Invite": "Einladen", @@ -286,16 +291,18 @@ "Join_our_open_workspace": "Tritt unserem offenen Arbeitsbereich bei", "Join_your_workspace": "Tritt deinem Arbeitsbereich bei", "Just_invited_people_can_access_this_channel": "Nur eingeladene Personen können auf diesen Kanal zugreifen", + "Just_invited_people_can_access_this_team": "Nur eingeladene Personen können auf das Team zugreifen", "Language": "Sprache", "last_message": "letzte Nachricht", "Leave_channel": "Kanal verlassen", "leaving_room": "Raum verlassen", + "Leave": "Raum verlassen", "leave": "verlassen", "Legal": "Rechtliches", "Light": "Hell", "License": "Lizenz", "Livechat": "Live-Chat", - "Livechat_edit": "Livechat bearbeiten", + "Livechat_edit": "Live-Chat bearbeiten", "Login": "Anmeldung", "Login_error": "Deine Zugangsdaten wurden abgelehnt! Bitte versuche es erneut.", "Login_with": "Einloggen mit", @@ -326,6 +333,7 @@ "My_servers": "Meine Server", "N_people_reacted": "{{n}} Leute haben reagiert", "N_users": "{{n}} Benutzer", + "N_channels": "{{n}} Kanäle", "name": "Name", "Name": "Name", "Navigation_history": "Navigations-Verlauf", @@ -435,6 +443,7 @@ "Review_app_unable_store": "Kann {{store}} nicht öffnen", "Review_this_app": "App bewerten", "Remove": "Entfernen", + "remove": "entfernen", "Roles": "Rollen", "Room_actions": "Raumaktionen", "Room_changed_announcement": "Raumansage geändert in: {{announcement}} von {{userBy}}", @@ -517,7 +526,7 @@ "Take_a_video": "Video aufnehmen", "Take_it": "Annehmen!", "tap_to_change_status": "Tippen um den Status zu ändern", - "Tap_to_view_servers_list": "Hier tippen, um die Serverliste anzuzeigen", + "Tap_to_view_servers_list": "Tippen, um die Serverliste anzuzeigen", "Terms_of_Service": " Nutzungsbedingungen", "Theme": "Erscheinungsbild", "The_user_wont_be_able_to_type_in_roomName": "Dem Nutzer wird es nicht möglich sein in {{roomName}} zu schreiben", @@ -592,7 +601,7 @@ "You_can_search_using_RegExp_eg": "Du kannst mit RegExp suchen. z.B. `/ ^ text $ / i`", "You_colon": "Du: ", "you_were_mentioned": "Du wurdest erwähnt", - "You_were_removed_from_channel": "Du wurdest aus dem Kanal {{channel}} entfernt", + "You_were_removed_from_channel": "Du wurdest aus {{channel}} entfernt", "you": "du", "You": "Du", "Logged_out_by_server": "Du bist vom Server abgemeldet worden. Bitte melde dich wieder an.", @@ -610,7 +619,7 @@ "You_will_unset_a_certificate_for_this_server": "Du entfernst ein Zertifikat für diesen Server", "Change_Language": "Sprache ändern", "Crash_report_disclaimer": "Wir verfolgen niemals den Inhalt deiner Chats. Der Crash-Report enthält nur für uns relevante Informationen um das Problem zu erkennen und zu beheben.", - "Type_message": "Type message", + "Type_message": "Nachricht schreiben", "Room_search": "Raum-Suche", "Room_selection": "Raum-Auswahl 1...9", "Next_room": "Nächster Raum", @@ -623,7 +632,7 @@ "Reply_in_Thread": "Im Thread antworten", "Server_selection": "Server-Auswahl", "Server_selection_numbers": "Server-Auswahl 1...9", - "Add_server": "Server hinufügen", + "Add_server": "Server hinzufügen", "New_line": "Zeilenumbruch", "You_will_be_logged_out_of_this_application": "Du wirst in dieser Anwendung vom Server abgemeldet.", "Clear": "Löschen", @@ -681,12 +690,9 @@ "No_threads_following": "Du folgst keinen Threads", "No_threads_unread": "Es gibt keine ungelesenen Threads", "Messagebox_Send_to_channel": "an Kanal senden", - "Set_as_leader": "Zum Diskussionsleiter ernennen", - "Set_as_moderator": "Zum Moderator ernennen", - "Set_as_owner": "Zum Besitzer machen", - "Remove_as_leader": "Als Diskussionsleiter entfernen", - "Remove_as_moderator": "Moderatorenrechte entfernen", - "Remove_as_owner": "Als Eigentümer entfernen", + "Leader": "Leiter", + "Moderator": "Moderator", + "Owner": "Eigentümer", "Remove_from_room": "Aus dem Raum entfernen", "Ignore": "Ignorieren", "Unignore": "Nicht mehr ignorieren", @@ -704,5 +710,56 @@ "Direct_message": "Direktnachricht", "Message_Ignored": "Nachricht ignoriert. Antippen um sie zu zeigen.", "Enter_workspace_URL": "Arbeitsbereich-URL", - "Workspace_URL_Example": "z.B. https://rocketchat.deine-firma.de" + "Workspace_URL_Example": "z.B. https://rocketchat.deine-firma.de", + "This_room_encryption_has_been_enabled_by__username_": "Die Verschlüsselung dieses Raums wurde von {{username}} aktiviert", + "This_room_encryption_has_been_disabled_by__username_": "Die Verschlüsselung dieses Raums wurde von {{username}} deaktiviert", + "Teams": "Teams", + "No_team_channels_found": "Keine Kanäle gefunden", + "Team_not_found": "Team nicht gefunden", + "Create_Team": "Team erstellen", + "Team_Name": "Team-Name", + "Private_Team": "Privates Team", + "Read_Only_Team": "Nur-Lesen-Team", + "Broadcast_Team": "Broadcast-Team", + "creating_team": "Team erstellen", + "team-name-already-exists": "Ein Team mit diesem Namen existiert bereits", + "Add_Channel_to_Team": "Kanal zum Team hinzufügen", + "Create_New": "Neu erstellen", + "Add_Existing": "Vorhandenes hinzufügen", + "Add_Existing_Channel": "Vorhandenen Kanal hinzufügen", + "Remove_from_Team": "Aus Team entfernen", + "Auto-join": "Automatischer Beitritt", + "Remove_Team_Room_Warning": "Möchten du diesen Kanal aus dem Team entfernen? Der Kanal wird zurück in den Arbeitsbereich verschoben.", + "Confirmation": "Bestätigung", + "invalid-room": "Ungültiger Raum", + "You_are_leaving_the_team": "Du verlässt das Team '{{team}}'", + "Leave_Team": "Team verlassen", + "Select_Team": "Team auswählen", + "Select_Team_Channels": "Wähle die Kanäle des Teams aus, die du verlassen möchtest.", + "Cannot_leave": "Verlassen nicht möglich", + "Cannot_remove": "Kann nicht entfernt werden", + "Cannot_delete": "Kann nicht gelöscht werden", + "Last_owner_team_room": "Du bist der letzte Eigentümer des Kanals. Wenn du das Team verlässt, bleibt der Kanal innerhalb des Teams aber du verwaltest ihn von außen.", + "last-owner-can-not-be-removed": "Letzter Besitzer kann nicht entfernt werden", + "Remove_User_Teams": "Wähle die Kanäle aus, aus denen der Benutzer entfernt werden soll.", + "Delete_Team": "Team löschen", + "Select_channels_to_delete": "Dies kann nicht rückgängig gemacht werden. Wenn du ein Team löschst, werden alle Chat-Inhalte und und Einstellungen gelöscht.\n\nWähle die Kanäle, die du löschen möchtest. Diejenigen, die du behalten möchtest, werden in deinem Arbeitsbereich verfügbar sein. Beachte, das öffentliche Kanäle öffentlich bleiben und für jeden sichtbar sein werden.", + "You_are_deleting_the_team": "Du löschst dieses Team", + "Removing_user_from_this_team": "Du entfernst {{user}} aus diesem Team", + "Remove_User_Team_Channels": "Wähle die Kanäle aus, aus denen der Benutzer entfernt werden soll.", + "Remove_Member": "Mitglied entfernen", + "leaving_team": "Team verlassen", + "removing_team": "Aus dem Team entfernen", + "moving_channel_to_team": "Kanal zu Team verschieben", + "deleting_team": "Team löschen", + "member-does-not-exist": "Mitglied existiert nicht", + "Convert": "Konvertieren", + "Convert_to_Team": "Zu Team konvertieren", + "Convert_to_Team_Warning": "Dies kann nicht rückgängig gemacht werden. Sobald du einen Kanal in ein Team umgewandelt hast, kannst du ihn nicht mehr zurück in einen Kanal verwandeln.", + "Move_to_Team": "Zu Team hinzufügen", + "Move_Channel_Paragraph": "Das Verschieben eines Kanals innerhalb eines Teams bedeutet, dass dieser Kanal im Kontext des Teams hinzugefügt wird, jedoch haben alle Mitglieder des Kanals, die nicht Mitglied des jeweiligen Teams sind, weiterhin Zugriff auf diesen Kanal, werden aber nicht als Teammitglieder hinzugefügt \n\nDie gesamte Verwaltung des Kanals wird weiterhin von den Eigentümern dieses Kanals vorgenommen.\n\nTeammitglieder und sogar Teameigentümer, die nicht Mitglied dieses Kanals sind, können keinen Zugriff auf den Inhalt des Kanals haben \n\nBitte beachte, dass der Besitzer des Teams in der Lage ist, Mitglieder aus dem Kanal zu entfernen.", + "Move_to_Team_Warning": "Nachdem du die vorherigen Anleitungen zu diesem Verhalten gelesen hast, möchtest du diesen Kanal immer noch in das ausgewählte Team verschieben?", + "Load_More": "Mehr laden", + "Load_Newer": "Neuere laden", + "Load_Older": "Ältere laden" } \ No newline at end of file diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 80722f82d..926b0c674 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -61,7 +61,7 @@ "error-message-editing-blocked": "Message editing is blocked", "error-message-size-exceeded": "Message size exceeds Message_MaxAllowedSize", "error-missing-unsubscribe-link": "You must provide the [unsubscribe] link.", - "error-no-owner-channel":"You don't own the channel", + "error-no-owner-channel": "You don't own the channel", "error-no-tokens-for-this-user": "There are no tokens for this user", "error-not-allowed": "Not allowed", "error-not-authorized": "Not authorized", @@ -763,4 +763,4 @@ "Load_More": "Load More", "Load_Newer": "Load Newer", "Load_Older": "Load Older" -} +} \ No newline at end of file diff --git a/app/i18n/locales/es-ES.json b/app/i18n/locales/es-ES.json index feb18f1a7..ac6657042 100644 --- a/app/i18n/locales/es-ES.json +++ b/app/i18n/locales/es-ES.json @@ -13,7 +13,7 @@ "error-delete-protected-role": "No se puede eliminar un rol protegido", "error-department-not-found": "Departamento no encontrado", "error-direct-message-file-upload-not-allowed": "No se permite compartir archivos en mensajes directos", - "error-duplicate-channel-name": "Ya existe un canal con nombre {{channel_name}}", + "error-duplicate-channel-name": "Ya existe un canal con nombre {{room_name}}", "error-email-domain-blacklisted": "El dominio del correo electrónico está en la lista negra", "error-email-send-failed": "Error al enviar el correo electrónico: {{message}}", "error-field-unavailable": "{{field}} ya está en uso :(", @@ -25,10 +25,10 @@ "error-invalid-asset": "El archivo archivo no es correcto", "error-invalid-channel": "El canal no es correcto.", "error-invalid-channel-start-with-chars": "Canal incorrecto. Debe comenzar con @ o #", - "error-invalid-custom-field": "Invalid custom field", - "error-invalid-custom-field-name": "Nombre inválido para el campo personalizado. Utilice sólo letras, números, guiones o guión bajo", + "error-invalid-custom-field": "Campo personalizado no válido", + "error-invalid-custom-field-name": "Nombre no válido para el campo personalizado. Utilice sólo letras, números, guiones o guión bajo", "error-invalid-date": "La fecha proporcionada no es correcta.", - "error-invalid-description": "La descipción no es correcta", + "error-invalid-description": "La descripción no es correcta", "error-invalid-domain": "El dominio no es correcto", "error-invalid-email": "El email {{email}} no es correcto", "error-invalid-email-address": "La dirección de correo no es correcta", @@ -45,9 +45,9 @@ "error-invalid-role": "El rol no es correcto", "error-invalid-room": "La sala no es correcta", "error-invalid-room-name": "No se puede asignar el nombre {{room_name}} a una sala.", - "error-invalid-room-type": "No se puede asginar el tipo {{type}} a una sala.", + "error-invalid-room-type": "No se puede asignar el tipo {{type}} a una sala.", "error-invalid-settings": "La configuración proporcionada no es correcta", - "error-invalid-subscription": "La subscripción no es correcta", + "error-invalid-subscription": "La suscripción no es correcta", "error-invalid-token": "El token no es correcto", "error-invalid-triggerWords": "El triggerWords no es correcto", "error-invalid-urls": "Las URLs no son correctas", @@ -62,23 +62,23 @@ "error-not-allowed": "No permitido", "error-not-authorized": "No autorizado", "error-push-disabled": "El Push está desactivado", - "error-remove-last-owner": "El usuario el único propietario existente. Debes establecer un nuevo propietario antes de eliminarlo.", + "error-remove-last-owner": "El usuario es el único propietario existente. Debes establecer un nuevo propietario antes de eliminarlo.", "error-role-in-use": "No puedes eliminar el rol dado que está en uso", "error-role-name-required": "Debes indicar el nombre del rol", "error-the-field-is-required": "El campo {{field}} es obligatorio.", - "error-too-many-requests": "Hemos recibido demasiadas peticiones. Debes esperar {{seconds}} segundos antes de continuar. Por favor, sé paciente.", + "error-too-many-requests": "Error, demasiadas peticiones. Debes esperar {{seconds}} segundos antes de continuar. Por favor, sé paciente.", "error-user-is-not-activated": "El usuario no está activo", "error-user-has-no-roles": "El usuario no tiene roles", - "error-user-limit-exceeded": "El número de usuarios que quieres invitiar al canal #channel_name supera el límite establecido por el adminitrador.", + "error-user-limit-exceeded": "El número de usuarios que quieres invitar al canal #channel_name supera el límite establecido por el administrador.", "error-user-not-in-room": "El usuario no está en la sala", "error-user-registration-custom-field": "error-user-registration-custom-field", - "error-user-registration-disabled": "El registro de usuario está deshabilitador", + "error-user-registration-disabled": "El registro de usuario está deshabilitado", "error-user-registration-secret": "El registro de usuarios sólo está permitido por URL secretas", - "error-you-are-last-owner": "El usuario el único propietario existente. Debes establecer un nuevo propietario antes de abandonar la sala.", + "error-you-are-last-owner": "Eres el único propietario existente. Debes establecer un nuevo propietario antes de abandonar la sala.", "Actions": "Acciones", "activity": "actividad", "Activity": "Actividad", - "Add_Reaction": "Reaccionar", + "Add_Reaction": "Añadir reacción", "Add_Server": "Añadir servidor", "Admin_Panel": "Panel de Control", "Alert": "Alerta", @@ -89,27 +89,27 @@ "All_Messages": "Todos los mensajes", "Allow_Reactions": "Permitir reacciones", "Alphabetical": "Alfabético", - "and_more": "más", + "and_more": "y más", "and": "y", "announcement": "anuncio", "Announcement": "Anuncio", - "Apply_Your_Certificate": "Applica tu Certificación", + "Apply_Your_Certificate": "Aplica tu certificado", "ARCHIVE": "FICHERO", - "archive": "Fichero", - "are_typing": "escribiendo", + "archive": "fichero", + "are_typing": "están escribiendo", "Are_you_sure_question_mark": "¿Estás seguro?", "Are_you_sure_you_want_to_leave_the_room": "¿Deseas salir de la sala {{room}}?", "Audio": "Audio", "Authenticating": "Autenticando", "Automatic": "Automático", - "Auto_Translate": "Auto-Translate", - "Avatar_changed_successfully": "Has cambiado tu Avatar!", + "Auto_Translate": "Traducción automática", + "Avatar_changed_successfully": "¡Avatar modificado correctamente!", "Avatar_Url": "URL del Avatar", "Away": "Ausente", "Back": "Volver", - "Black": "Black", + "Black": "Negro", "Block_user": "Bloquear usuario", - "Broadcast_channel_Description": "Sólo los usuario permitidos pueden escribir nuevos mensajes, el resto podrán responder sobre los mismos.", + "Broadcast_channel_Description": "Sólo los usuarios autorizados pueden escribir nuevos mensajes, el resto podrán responder sobre los mismos.", "Broadcast_Channel": "Canal de Transmisión", "Busy": "Ocupado", "By_proceeding_you_are_agreeing": "Al proceder estarás de acuerdo", @@ -121,35 +121,35 @@ "Channel_Name": "Nombre sala", "Channels": "Salas", "Chats": "Chats", - "Call_already_ended": "La llamada ya ha finalizado!", - "Click_to_join": "Unirme!", + "Call_already_ended": "¡!La llamada ya ha finalizado!", + "Click_to_join": "¡Unirme!", "Close": "Cerrar", "Close_emoji_selector": "Cerrar selector de emojis", "Choose": "Seleccionar", - "Choose_from_library": "Seleccionar desde Galería", - "Choose_file": "Seleccionar Archivo", + "Choose_from_library": "Seleccionar desde galería", + "Choose_file": "Seleccionar archivo", "Code": "Código", "Collaborative": "Colaborativo", "Confirm": "Confirmar", "Connect": "Conectar", "Connected": "Conectado", - "connecting_server": "conectando a servidor", + "connecting_server": "conectando al servidor", "Connecting": "Conectando...", - "Contact_us": "Contactar", + "Contact_us": "Contacta con nosotros", "Contact_your_server_admin": "Contacta con el administrador.", "Continue_with": "Continuar con", - "Copied_to_clipboard": "Copiado al portapapeles!", + "Copied_to_clipboard": "¡Copiado al portapapeles!", "Copy": "Copiar", "Permalink": "Enlace permanente", "Certificate_password": "Contraseña del certificado", - "Whats_the_password_for_your_certificate": "¿Cuál es la contraseña de tu cerficiado?", + "Whats_the_password_for_your_certificate": "¿Cuál es la contraseña de tu certificado?", "Create_account": "Crear una cuenta", - "Create_Channel": "Crear Sala", - "Created_snippet": "crear snippet", - "Create_a_new_workspace": "Crear un Workspace", + "Create_Channel": "Crear sala", + "Created_snippet": "crear mensaje en bloque", + "Create_a_new_workspace": "Crear un nuevo espacio de trabajo", "Create": "Crear", - "Dark": "Óscuro", - "Dark_level": "Nivel", + "Dark": "Oscuro", + "Dark_level": "Nivel de oscuridad", "Default": "Por defecto", "Delete_Room_Warning": "Eliminar a un usuario causará la eliminación de todos los mensajes creados por dicho usuario. Esta operación no se puede deshacer.", "delete": "eliminar", @@ -158,9 +158,9 @@ "deleting_room": "eliminando sala", "description": "descripción", "Description": "Descripción", - "Desktop_Options": "Opciones De Escritorio", + "Desktop_Options": "Opciones de escritorio", "Directory": "Directorio", - "Direct_Messages": "Mensajes directo", + "Direct_Messages": "Mensajes directos", "Disable_notifications": "Desactivar notificaciones", "Discussions": "Conversaciones", "Dont_Have_An_Account": "¿Todavía no tienes una cuenta?", @@ -169,7 +169,7 @@ "edit": "editar", "edited": "editado", "Edit": "Editar", - "Email_or_password_field_is_empty": "El email o la contraseña están vacios", + "Email_or_password_field_is_empty": "El email o la contraseña están vacíos", "Email": "E-mail", "email": "e-mail", "Enable_Auto_Translate": "Permitir Auto-Translate", @@ -184,9 +184,9 @@ "Finish_recording": "Finalizar grabación", "Following_thread": "Siguiendo hilo", "For_your_security_you_must_enter_your_current_password_to_continue": "Por seguridad, debes introducir tu contraseña para continuar", - "Forgot_password_If_this_email_is_registered": "Si este email está registrado, te enviaremos las instrucciones para resetear tu contraseña.Si no recibes un email en un rato, vuelve aquí e inténtalo de nuevo.", - "Forgot_password": "Restablecer mi contraseña", - "Forgot_Password": "Restabler mi Contraseña", + "Forgot_password_If_this_email_is_registered": "Si este email está registrado, te enviaremos las instrucciones para resetear tu contraseña. Si no recibes un email en breve, vuelve aquí e inténtalo de nuevo.", + "Forgot_password": "¿Ha olvidado su contraseña?", + "Forgot_Password": "Olvidé la contraseña", "Full_table": "Click para ver la tabla completa", "Group_by_favorites": "Agrupar por favoritos", "Group_by_type": "Agrupar por tipo", @@ -194,29 +194,29 @@ "Has_joined_the_channel": "se ha unido al canal", "Has_joined_the_conversation": "se ha unido a la conversación", "Has_left_the_channel": "ha dejado el canal", - "In_App_And_Desktop": "In-app and Desktop", - "In_App_and_Desktop_Alert_info": "Muestra un banner en la parte superior de la pantalla cuando la aplicación está abierta y muestra una notificación en el escritorio", + "In_App_And_Desktop": "En la aplicación y en el escritorio", + "In_App_and_Desktop_Alert_info": "Muestra un banner en la parte superior de la pantalla cuando la aplicación esté abierta y muestra una notificación en el escritorio", "Invisible": "Invisible", "Invite": "Invitar", - "is_a_valid_RocketChat_instance": "es una instancia válida Rocket.Chat", - "is_not_a_valid_RocketChat_instance": "no es una instancia válida Rocket.Chat", + "is_a_valid_RocketChat_instance": "es una instancia válida de Rocket.Chat", + "is_not_a_valid_RocketChat_instance": "no es una instancia válida de Rocket.Chat", "is_typing": "escribiendo", - "Invalid_server_version": "El servidor que intentas conectar está usando una versión que ya no es soportada por la aplicación : {{currentVersion}}. Requerimos una versión {{minVersion}}.", + "Invalid_server_version": "El servidor que intentas conectar está usando una versión que ya no está soportada por la aplicación : {{currentVersion}}. Se requiere una versión {{minVersion}}.", "Join": "Conectar", "Just_invited_people_can_access_this_channel": "Sólo gente invitada puede acceder a este canal.", "Language": "Idioma", "last_message": "último mensaje", - "Leave_channel": "Abandonar canal", + "Leave_channel": "Abandonar el canal", "leaving_room": "abandonando sala", "leave": "abandonar", "Legal": "Legal", "Light": "Claro", "License": "Licencia", - "Livechat": "Livechat", - "Login": "Acceder", + "Livechat": "LiveChat", + "Login": "Inicio de sesión", "Login_error": "¡Sus credenciales fueron rechazadas! Por favor, inténtelo de nuevo.", - "Login_with": "Acceder con", - "Logout": "Salir", + "Login_with": "Iniciar sesión con", + "Logout": "Cerrar sesión", "members": "miembros", "Members": "Miembros", "Mentioned_Messages": "Mensajes mencionados", @@ -248,19 +248,19 @@ "No_pinned_messages": "No hay mensajes fijados", "No_results_found": "No hay resultados", "No_starred_messages": "No hay mensajes destacados", - "No_thread_messages": "No hay hilots", + "No_thread_messages": "No hay hilos", "No_Message": "Sin mensajes", - "No_messages_yet": "No hay todavía mensajes", + "No_messages_yet": "No hay mensajes todavía", "No_Reactions": "No hay reacciones", "No_Read_Receipts": "No hay confirmaciones de lectura", - "Not_logged": "No logueado", + "Not_logged": "No ha iniciado sesión", "Not_RC_Server": "Esto no es un servidor de Rocket.Chat.\n{{contact}}", "Nothing": "Nada", - "Nothing_to_save": "No hay nada para guardar!", - "Notify_active_in_this_room": "Notificar usuarios activos en esta sala", + "Nothing_to_save": "¡No hay nada por guardar!", + "Notify_active_in_this_room": "Notificar a los usuarios activos en esta sala", "Notify_all_in_this_room": "Notificar a todos en esta sala", "Notifications": "Notificaciones", - "Notification_Duration": "Duración notificación", + "Notification_Duration": "Duración de la notificación", "Notification_Preferences": "Configuración de notificaciones", "Offline": "Sin conexión", "Oops": "Oops!", @@ -270,28 +270,28 @@ "Open_emoji_selector": "Abrir selector de emojis", "Open_Source_Communication": "Comunicación Open Source", "Password": "Contraseña", - "Permalink_copied_to_clipboard": "Enlace permanente copiado al portapapeles!", + "Permalink_copied_to_clipboard": "¡Enlace permanente copiado al portapapeles!", "Pin": "Fijar", "Pinned_Messages": "Mensajes fijados", "pinned": "fijado", "Pinned": "Fijado", - "Please_enter_your_password": "Por favor introduce tu contraseña", - "Preferences": "Configuración", - "Preferences_saved": "Configuración guardada!", - "Privacy_Policy": "Política de Privacidad", + "Please_enter_your_password": "Por favor introduce la contraseña", + "Preferences": "Preferencias", + "Preferences_saved": "¡Preferencias guardadas!", + "Privacy_Policy": "Política de privacidad", "Private_Channel": "Canal privado", "Private_Groups": "Grupos privados", "Private": "Privado", "Processing": "Procesando...", - "Profile_saved_successfully": "Perfil guardado correctamente!", + "Profile_saved_successfully": "¡Perfil guardado correctamente!", "Profile": "Perfil", "Public_Channel": "Canal público", "Public": "Público", - "Push_Notifications": "Push Notifications", + "Push_Notifications": "Notificaciones Push", "Push_Notifications_Alert_Info": "Estas notificaciones se le entregan cuando la aplicación no está abierta", "Quote": "Citar", "Reactions_are_disabled": "Las reacciones están desactivadas", - "Reactions_are_enabled": "Las reacciones están habilitadas", + "Reactions_are_enabled": "Las reacciones están activadas", "Reactions": "Reacciones", "Read": "Leer", "Read_Only_Channel": "Canal de sólo lectura", @@ -323,12 +323,12 @@ "Room_Info": "Información de la sala", "Room_Members": "Miembros de la sala", "Room_name_changed": "El nombre de la sala cambió a: {{name}} por {{userBy}}", - "SAVE": "SAVE", + "SAVE": "GUARDAR", "Save_Changes": "Guardar cambios", "Save": "Guardar", "saving_preferences": "guardando preferencias", "saving_profile": "guardando perfil", - "saving_settings": "guardando confiración", + "saving_settings": "guardando configuración", "Search_Messages": "Buscar mensajes", "Search": "Buscar", "Search_by": "Buscar por", @@ -349,14 +349,14 @@ "Server_version": "Versión servidor: {{version}}", "Set_username_subtitle": "El nombre de usuario se utiliza para permitir que otros le mencionen en los mensajes", "Settings": "Configuración", - "Settings_succesfully_changed": "Configuración cambiada correctamente!", + "Settings_succesfully_changed": "¡Configuración cambiada correctamente!", "Share": "Compartir", - "Share_this_app": "Compartir esta App", - "Show_Unread_Counter": "Mostrar contador No leídos", + "Share_this_app": "Compartir esta aplicación", + "Show_Unread_Counter": "Mostrar contador de no leídos", "Show_Unread_Counter_Info": "El contador de no leídos se muestra como una insignia a la derecha del canal, en la lista", "Sign_in_your_server": "Accede a tu servidor", - "Sign_Up": "Acceder", - "Some_field_is_invalid_or_empty": "Algún campo es incorrecto o vacío", + "Sign_Up": "Registrarse", + "Some_field_is_invalid_or_empty": "Algún campo no es correcto o está vacío", "Sorting_by": "Ordenado por {{key}}", "Sound": "Sonido", "Star_room": "Destacar sala", @@ -364,18 +364,18 @@ "Starred_Messages": "Mensajes destacados", "starred": "destacado", "Starred": "Destacado", - "Start_of_conversation": "Comiezo de la conversación", + "Start_of_conversation": "Comienzo de la conversación", "Started_discussion": "Comenzar una conversación:", "Started_call": "Llamada iniciada por {{userBy}}", "Submit": "Enviar", "Table": "Tabla", - "Take_a_photo": "Enviar Foto", - "Take_a_video": "Enviar Vídeo", + "Take_a_photo": "Enviar una foto", + "Take_a_video": "Enviar un vídeo", "tap_to_change_status": "pulsa para cambiar el estado", "Tap_to_view_servers_list": "Pulsa para ver la lista de servidores", "Terms_of_Service": "Términos de servicio", "Theme": "Tema", - "There_was_an_error_while_action": "Ha habido un error mientras {{action}}!", + "There_was_an_error_while_action": "¡Ha habido un error mientras {{action}}!", "This_room_is_blocked": "La sala está bloqueada", "This_room_is_read_only": "Esta sala es de sólo lectura", "Thread": "Hilo", @@ -388,21 +388,21 @@ "Try_again": "Intentar de nuevo", "Two_Factor_Authentication": "Autenticación de doble factor", "Type_the_channel_name_here": "Escribe el nombre del canal aquí", - "unarchive": "reactivar", - "UNARCHIVE": "UNARCHIVE", + "unarchive": "desarchivar", + "UNARCHIVE": "DESARCHIVAR", "Unblock_user": "Desbloquear usuario", - "Unfavorite": "Quitar Favorito", - "Unfollowed_thread": "Dejar de seguir el Hilo", + "Unfavorite": "Quitar favorito", + "Unfollowed_thread": "Dejar de seguir el hilo", "Unmute": "Desmutear", "unmuted": "Desmuteado", - "Unpin": "Quitar estado Fijado", - "unread_messages": "marcar como No leído", - "Unread": "Marcar como No leído", - "Unread_on_top": "Mensajes No leídos en la parte superior", - "Unstar": "Quitar Destacado", + "Unpin": "Quitar estado fijado", + "unread_messages": "marcar como no leído", + "Unread": "Marcar como no leído", + "Unread_on_top": "Mensajes no leídos en la parte superior", + "Unstar": "Quitar destacado", "Updating": "Actualizando...", "Uploading": "Subiendo", - "Upload_file_question_mark": "Subir fichero?", + "Upload_file_question_mark": "¿Subir fichero?", "Users": "Usuarios", "User_added_by": "Usuario {{userAdded}} añadido por {{userBy}}", "User_has_been_key": "El usuario ha sido {{key}}", @@ -423,9 +423,9 @@ "Welcome": "Bienvenido", "Whats_your_2fa": "¿Cuál es tu código 2FA?", "Without_Servers": "Sin servidores", - "Yes_action_it": "Sí, {{action}}!", + "Yes_action_it": "Sí, ¡{{action}}!", "Yesterday": "Ayer", - "You_are_in_preview_mode": "Estás en modo Vista Previa", + "You_are_in_preview_mode": "Estás en modo vista previa", "You_are_offline": "Estás desconectado", "You_can_search_using_RegExp_eg": "Puedes usar expresiones regulares. Por ejemplo, `/^text$/i`", "You_colon": "Tú: ", @@ -435,7 +435,7 @@ "You_need_to_access_at_least_one_RocketChat_server_to_share_something": "Necesita acceder al menos a un servidor Rocket.Chat para compartir algo.", "Your_certificate": "Tu certificado", "Version_no": "Versión: {{version}}", - "You_will_not_be_able_to_recover_this_message": "No podrás recuperar este mensaje!", + "You_will_not_be_able_to_recover_this_message": "¡No podrás recuperar este mensaje!", "Change_Language": "Cambiar idioma", "Crash_report_disclaimer": "Nunca rastreamos el contenido de sus conversaciones. El informe del error sólo contiene información relevante para nosotros con el fin de identificar los problemas y solucionarlos.", "Type_message": "Escribir mensaje", diff --git a/app/i18n/locales/fr.json b/app/i18n/locales/fr.json index c61e22f24..91388e2de 100644 --- a/app/i18n/locales/fr.json +++ b/app/i18n/locales/fr.json @@ -3,45 +3,45 @@ "1_user": "1 utilisateur", "error-action-not-allowed": "{{action}} n'est pas autorisé", "error-application-not-found": "Application non trouvée", - "error-archived-duplicate-name": "Il y a un canal archivé avec nom {{room_name}}", - "error-avatar-invalid-url": "URL d'avatar invalide: {{url}}", + "error-archived-duplicate-name": "Il y a un canal archivé avec le nom {{room_name}}", + "error-avatar-invalid-url": "URL d'avatar invalide : {{url}}", "error-avatar-url-handling": "Erreur lors de la gestion du paramètre d'avatar à partir d'une URL ({{url}}) pour {{username}}", - "error-cant-invite-for-direct-room": "Impossible d'inviter l'utilisateur aux salles direct", + "error-cant-invite-for-direct-room": "Impossible d'inviter l'utilisateur aux salons directs", "error-could-not-change-email": "Impossible de changer l'adresse e-mail", "error-could-not-change-name": "Impossible de changer le nom", "error-could-not-change-username": "Impossible de changer le nom d'utilisateur", "error-could-not-change-status": "Impossible de changer le statut", "error-delete-protected-role": "Impossible de supprimer un rôle protégé", "error-department-not-found": "Département introuvable", - "error-direct-message-file-upload-not-allowed": "Le partage de fichiers n'est pas autorisé dans les messages directs", - "error-duplicate-channel-name": "un canal avec nom {{channel_name}} existe", + "error-direct-message-file-upload-not-allowed": "Partage de fichiers non autorisé dans les messages privés", + "error-duplicate-channel-name": "Un canal avec nom {{room_name}} existe", "error-email-domain-blacklisted": "Le domaine de messagerie est sur liste noire", - "error-email-send-failed": "Erreur lors de la tentative d'envoi d'un courrier électronique: {{message}}", - "error-save-image": "Erreur en sauvegardant l'image", - "error-save-video": "Erreur en sauvegardant la video", + "error-email-send-failed": "Erreur lors de la tentative d'envoi de l'e-mail : {{message}}", + "error-save-image": "Erreur lors de l'enregistrement de l'image", + "error-save-video": "Erreur en sauvegardant la vidéo", "error-field-unavailable": "{{field}} est déjà utilisé: (", - "error-file-too-large": "Le fichier est trop volumineux", - "error-importer-not-defined": "L'importateur n'a pas été défini correctement, il manque la classe import.", - "error-input-is-not-a-valid-field": "{{input}} N'est pas valide {{field}}", - "error-invalid-actionlink": "Lien d'action invalide", - "error-invalid-arguments": "Invalid arguments", - "error-invalid-asset": "élément incorrect", + "error-file-too-large": "Le fichier est trop grand", + "error-importer-not-defined": "L'importateur n'a pas été défini correctement, il manque la classe Import.", + "error-input-is-not-a-valid-field": "{{input}} n'est pas un {{field}} valide", + "error-invalid-actionlink": "Lien d'action non valide", + "error-invalid-arguments": "Arguments non valides", + "error-invalid-asset": "Elément non valide", "error-invalid-channel": "Canal invalide.", - "error-invalid-channel-start-with-chars": "Canal invalide. Commence par @ ou #", - "error-invalid-custom-field": "Champ personnalisé incorrect", - "error-invalid-custom-field-name": "Nom de champ personnalisé non valide. Utilisez uniquement des lettres, des chiffres, des traits d'union et de soulignement.", - "error-invalid-date": "Date fournie invalide.", + "error-invalid-channel-start-with-chars": "Canal non valide. Commencez par @ ou #", + "error-invalid-custom-field": "Champ personnalisé non valide", + "error-invalid-custom-field-name": "Nom de champ personnalisé non valide. Utilisez uniquement des lettres, des chiffres, des traits d'union et des traits de soulignement.", + "error-invalid-date": "Date fournie non valide.", "error-invalid-description": "Description invalide", "error-invalid-domain": "Domaine invalide", - "error-invalid-email": "Adresse e-mail non valide {{email}}", + "error-invalid-email": "E-mail {{email}} invalide", "error-invalid-email-address": "Adresse e-mail invalide", "error-invalid-file-height": "Hauteur de fichier non valide", "error-invalid-file-type": "Type de fichier invalide", - "error-invalid-file-width": "Largeur de fichier invalide", - "error-invalid-from-address": "Vous avez informé une adresse FROM invalide.", + "error-invalid-file-width": "Largeur de fichier non valide", + "error-invalid-from-address": "Vous avez renseigné une adresse FROM invalide.", "error-invalid-integration": "Intégration invalide", "error-invalid-message": "Message invalide", - "error-invalid-method": "Méthode invalide", + "error-invalid-method": "Méthode non valide", "error-invalid-name": "Nom incorrect", "error-invalid-password": "Mot de passe incorrect", "error-invalid-redirectUri": "RedirectUri invalide", @@ -50,47 +50,50 @@ "error-invalid-room-name": "{{room_name}} n'est pas un nom de salon valide", "error-invalid-room-type": "{{type}} n'est pas un type de salon valide.", "error-invalid-settings": "Paramètres fournis non valides", - "error-invalid-subscription": "Subscription invalide", + "error-invalid-subscription": "Abonnement invalide", "error-invalid-token": "Jeton invalide", "error-invalid-triggerWords": "Mots déclencheurs invalides", "error-invalid-urls": "URL non valides", "error-invalid-user": "Utilisateur invalide", "error-invalid-username": "Nom d'utilisateur invalide", - "error-invalid-webhook-response": "L'URL webhook a répondu avec un statut autre que 200", + "error-invalid-webhook-response": "L'URL du webhook a répondu avec un statut autre que 200", "error-message-deleting-blocked": "La suppression du message est bloquée", "error-message-editing-blocked": "La modification du message est bloquée", "error-message-size-exceeded": "La taille du message dépasse Message_MaxAllowedSize", - "error-missing-unsubscribe-link": "Vous devez fournir le [unsubscribe] lien.", + "error-missing-unsubscribe-link": "Vous devez fournir le lien [unsubscribe].", + "error-no-owner-channel": "Vous n'êtes pas propriétaire du canal", "error-no-tokens-for-this-user": "Il n'y a pas de jetons pour cet utilisateur", - "error-not-allowed": "Non autorisé", - "error-not-authorized": "Non autorisé", + "error-not-allowed": "Interdit", + "error-not-authorized": "Pas autorisé", "error-push-disabled": "Push est désactivé", - "error-remove-last-owner": "Ceci est le dernier propriétaire. Veuillez définir un nouveau propriétaire avant de supprimer celui-ci.", - "error-role-in-use": "Impossible de supprimer le rôle car est utilisé", + "error-remove-last-owner": "C'est le dernier propriétaire. Veuillez définir un nouveau propriétaire avant de supprimer celui-ci.", + "error-role-in-use": "Impossible de supprimer le rôle car il est en cours d'utilisation", "error-role-name-required": "Le nom du rôle est requis", "error-the-field-is-required": "Le champ {{field}} est requis.", - "error-too-many-requests": "Erreur, trop de demandes. Ralentissez, s'il vous plaît. Vous devez attendre {{seconds}} secondes avant d'essayer à nouveau.", + "error-too-many-requests": "Erreur, trop de demandes. Ralentissez, s'il vous plaît. Vous devez attendre {{seconds}} secondes avant de réessayer.", "error-user-is-not-activated": "L'utilisateur n'est pas activé", - "error-user-has-no-roles": "L'utilisateur ne dispose pas d'un rôle", + "error-user-has-no-roles": "L'utilisateur n'a aucun rôle", "error-user-limit-exceeded": "Le nombre d'utilisateurs que vous essayez d'inviter à #channel_name dépasse la limite définie par l'administrateur", - "error-user-not-in-room": "L'utilisateur n'est pas dans cette salle", + "error-user-not-in-room": "L'utilisateur n'est pas dans ce salon", "error-user-registration-custom-field": "error-user-registration-custom-field", "error-user-registration-disabled": "L'enregistrement de l'utilisateur est désactivé", - "error-user-registration-secret": "Enregistrement de l'utilisateur est autorisée uniquement via l'URL secret", - "error-you-are-last-owner": "Vous êtes le dernier propriétaire. Veuillez définir un nouveau propriétaire avant de quitter la salle.", + "error-user-registration-secret": "L'enregistrement de l'utilisateur n'est autorisé que via l'URL secrète", + "error-you-are-last-owner": "Vous êtes le dernier propriétaire. Veuillez définir un nouveau propriétaire avant de quitter le salon.", + "error-status-not-allowed": "Le statut invisible est désactivé", "Actions": "Actions", "activity": "activité", "Activity": "Activité", "Add_Reaction": "Ajouter une réaction", "Add_Server": "Ajouter un serveur", "Add_users": "Ajouter des utilisateurs", - "Admin_Panel": "Panneau d'Administration", + "Admin_Panel": "Panneau d'administration", "Agent": "Agent", "Alert": "Alerte", "alert": "alerte", "alerts": "alertes", "All_users_in_the_channel_can_write_new_messages": "Tous les utilisateurs du canal peuvent écrire de nouveaux messages", - "A_meaningful_name_for_the_discussion_room": "Un nom explicite pour la salle de discussion", + "All_users_in_the_team_can_write_new_messages": "Tous les utilisateurs de l'équipe peuvent écrire de nouveaux messages", + "A_meaningful_name_for_the_discussion_room": "Un nom significatif pour le salon de discussion", "All": "Tout", "All_Messages": "Tous les messages", "Allow_Reactions": "Autoriser les réactions", @@ -99,113 +102,137 @@ "and": "et", "announcement": "annonce", "Announcement": "Annonce", - "Apply_Your_Certificate": "Valider le Certificat", + "Apply_Your_Certificate": "Appliquer votre certificat", "ARCHIVE": "ARCHIVER", "archive": "archiver", "are_typing": "sont en train d'écrire", "Are_you_sure_question_mark": "Êtes-vous sûr ?", - "Are_you_sure_you_want_to_leave_the_room": "Êtes-vous sûr de vouloir quitter le salon {{room}}?", + "Are_you_sure_you_want_to_leave_the_room": "Êtes-vous sûr de vouloir quitter le salon {{room}} ?", "Audio": "Audio", - "Authenticating": "Authentifier", + "Authenticating": "Authentification", "Automatic": "Automatique", - "Auto_Translate": "Traduction-Auto", - "Avatar_changed_successfully": "Avatar changé avec succès!", + "Auto_Translate": "Traduction automatique", + "Avatar_changed_successfully": "Avatar changé avec succès !", "Avatar_Url": "URL de l'avatar", - "Away": "absent", - "Back": "Arrière", + "Away": "Absent", + "Back": "Retour", "Black": "Noir", - "Block_user": "Bloquer l'Utilisateur", + "Block_user": "Bloquer l'utilisateur", "Browser": "Navigateur", "Broadcast_channel_Description": "Seuls les utilisateurs autorisés peuvent écrire de nouveaux messages, mais les autres utilisateurs pourront répondre.", "Broadcast_Channel": "Canal de diffusion", "Busy": "Occupé", - "By_proceeding_you_are_agreeing": "En procédant, vous acceptez nos", + "By_proceeding_you_are_agreeing": "En poursuivant, vous acceptez nos", "Cancel_editing": "Annuler la modification", "Cancel_recording": "Annuler l'enregistrement", "Cancel": "Annuler", "changing_avatar": "changer d'avatar", - "creating_channel": "créer un canal", + "creating_channel": "création d'un canal", "creating_invite": "création d'une invitation", "Channel_Name": "Nom du canal", "Channels": "Canaux", "Chats": "Chats", - "Call_already_ended": "L'appel a déjà terminé !", - "Click_to_join": "Cliquez pour rejoindre!", + "Call_already_ended": "Appel déjà terminé !", + "Clear_cookies_alert": "Voulez-vous effacer tous les cookies ?", + "Clear_cookies_desc": "Cette action effacera tous les cookies de connexion ce qui vous permettra de vous connecter à d'autres comptes.", + "Clear_cookies_yes": "Oui, effacez les cookies", + "Clear_cookies_no": "Non, gardez les cookies", + "Click_to_join": "Cliquez pour rejoindre !", "Close": "Fermer", - "Close_emoji_selector": "Fermer le sélecteur d'emoji", - "Closing_chat": "Fermeture du Salon de discussion", + "Close_emoji_selector": "Fermer le sélecteur d'émoji", + "Closing_chat": "Fermeture du chat", "Change_language_loading": "Changement de la langue.", - "Chat_closed_by_agent": "Le salon de discussion a été fermé", + "Chat_closed_by_agent": "Chat fermé par l'agent", "Choose": "Choisir", - "Choose_from_library": "Choisissez parmi la bibliothèque", - "Choose_file": "Choisir un fichier", - "Choose_where_you_want_links_be_opened": "Choisissez ou vous souhaitez ouvrir vos liens", + "Choose_from_library": "Choisissez dans la bibliothèque", + "Choose_file": "Choisir le fichier", + "Choose_where_you_want_links_be_opened": "Choisissez oµ vous souhaitez ouvrir les liens", "Code": "Code", "Code_or_password_invalid": "Code ou mot de passe invalide", - "Collaborative": "Collaborative", + "Collaborative": "Collaboratif", "Confirm": "Confirmer", - "Connect": "Se connecter", + "Connect": "Connecter", "Connected": "Connecté", "connecting_server": "connexion en cours au serveur", - "Connecting": "Connexion ...", - "Contact_us": "Contactez nous", - "Contact_your_server_admin": "Contactez l'administrateur de votre serveur.", + "Connecting": "Connexion...", + "Contact_us": "Contactez-nous", + "Contact_your_server_admin": "Contactez votre administrateur de serveur.", "Continue_with": "Continuer avec", - "Copied_to_clipboard": "Copié dans le presse-papier!", + "Copied_to_clipboard": "Copié dans le presse-papier !", "Copy": "Copier", "Conversation": "Conversation", "Permalink": "Lien permanent", - "Certificate_password": "Mot de passe du Certificat", - "Clear_cache": "Effacer le cache local", + "Certificate_password": "Mot de passe du certificat", + "Clear_cache": "Effacer le cache du serveur local", "Clear_cache_loading": "Effacement du cache.", - "Whats_the_password_for_your_certificate": "Quel est le mot de passe du Certificat ?", + "Whats_the_password_for_your_certificate": "Quel est le mot de passe de votre certificat ?", "Create_account": "Créer un compte", "Create_Channel": "Créer un canal", - "Create_Direct_Messages": "Créer un message direct", - "Create_Discussion": "Créer une Discussion", + "Create_Direct_Messages": "Créer des messages directs", + "Create_Discussion": "Créer une discussion", "Created_snippet": "créé un extrait", "Create_a_new_workspace": "Créer un nouvel espace de travail", "Create": "Créer", - "Custom_Status": "Statut Personnalisé", + "Custom_Status": "Statut personnalisé", "Dark": "Sombre", - "Dark_level": "Niveau d'assombrissement", + "Dark_level": "Niveau d'obscurité", "Default": "Défaut", "Default_browser": "Navigateur par défaut", - "Delete_Room_Warning": "Supprimer une salle supprimera tous les messages postés dans la salle. Ça ne peut pas être annulé.", + "Delete_Room_Warning": "Supprimer une salon supprimera tous les messages publiés dans le salon. Ça ne peut pas être annulé.", "Department": "Département", "delete": "supprimer", "Delete": "Supprimer", "DELETE": "SUPPRIMER", - "deleting_room": "effacement de la salle", + "move": "déplacer", + "deleting_room": "suppression du salon", "description": "la description", "Description": "La description", - "Desktop_Options": "Desktop Options", + "Desktop_Options": "Options de bureau", + "Desktop_Notifications": "Notifications de bureau", + "Desktop_Alert_info": "Ces notifications sont transmises sur le bureau", "Directory": "Répertoire", "Direct_Messages": "Messages directs", "Disable_notifications": "Désactiver les notifications", "Discussions": "Discussions", - "Discussion_Desc": "Aide à garder un aperçu de ce qui se passe! En créant une discussion, un sous-canal de celui que vous avez sélectionné est créé et les deux sont liés.", + "Discussion_Desc": "Aide à garder une vue d'ensemble sur ce qui se passe ! En créant une discussion, un sous-canal de celui que vous avez sélectionné est créé et les deux sont liés.", "Discussion_name": "Nom de la discussion", "Done": "Fait", - "Dont_Have_An_Account": "Vous n'avez pas de compte?", - "Do_you_have_an_account": "Avez-vous un compte?", - "Do_you_have_a_certificate": "Avez-vous un certificat?", - "Do_you_really_want_to_key_this_room_question_mark": "Voulez-vous vraiment {{key}} cette salle?", + "Dont_Have_An_Account": "Vous n'avez pas de compte ?", + "Do_you_have_an_account": "Avez-vous un compte ?", + "Do_you_have_a_certificate": "Avez-vous un certificat ?", + "Do_you_really_want_to_key_this_room_question_mark": "Voulez-vous vraiment {{key}} ce salon ?", + "E2E_Encryption": "Cryptage E2E", + "E2E_How_It_Works_info1": "Vous pouvez désormais créer des groupes privés et des messages directs chiffrés. Vous pouvez également modifier les groupes privés ou DM existants pour les crypter.", + "E2E_How_It_Works_info2": "Il s'agit du *chiffrement de bout en bout*, la clé permettant de coder/décoder vos messages ne sera pas enregistrée sur le serveur. C'est pourquoi *vous devez stocker ce mot de passe à un endroit sûr* auquel vous pourrez accéder plus tard si vous en avez besoin.", + "E2E_How_It_Works_info3": "Si vous continuez, un mot de passe E2E sera automatiquement généré.", + "E2E_How_It_Works_info4": "Vous pouvez également configurer un nouveau mot de passe pour votre clé de cryptage à tout moment à partir de n'importe quel navigateur dans lequel vous avez entré le mot de passe E2E existant.", "edit": "modifier", - "edited": "édité", + "edited": "modifié", "Edit": "Modifier", - "Edit_Status": "Modifier le Statut", + "Edit_Status": "Modifier le statut", "Edit_Invite": "Modifier l'invitation", + "End_to_end_encrypted_room": "Salon crypté de bout en bout", + "end_to_end_encryption": "chiffrement de bout en bout", + "Email_Notification_Mode_All": "Chaque mention/MD", + "Email_Notification_Mode_Disabled": "Désactivé", "Email_or_password_field_is_empty": "Le champ e-mail ou mot de passe est vide", "Email": "E-mail", "email": "e-mail", "Empty_title": "Titre vide", - "Enable_Auto_Translate": "Activer la traduction-auto", + "Enable_Auto_Translate": "Activer la traduction automatique", "Enable_notifications": "Activer les notifications", + "Encrypted": "Crypté", + "Encrypted_message": "Message crypté", + "Enter_Your_E2E_Password": "Entrez votre mot de passe E2E", + "Enter_Your_Encryption_Password_desc1": "Cela vous permettra d'accéder à vos groupes privés cryptés et à vos messages directs.", + "Enter_Your_Encryption_Password_desc2": "Vous devez entrer le mot de passe pour coder/décoder les messages à chaque endroit où vous utilisez le chat.", + "Encryption_error_title": "Votre mot de passe de cryptage semble erroné", + "Encryption_error_desc": "Il n'a pas été possible de décoder votre clé de cryptage pour être importé.", "Everyone_can_access_this_channel": "Tout le monde peut accéder à ce canal", - "Error_uploading": "Erreur lors du téléchargement", + "Everyone_can_access_this_team": "Tout le monde peut accéder à cette équipe", + "Error_uploading": "Erreur lors de l'envoi", "Expiration_Days": "Expiration (Jours)", - "Favorite": "Favoris", + "Favorite": "Favori", "Favorites": "Favoris", "Files": "Fichiers", "File_description": "Description du fichier", @@ -214,38 +241,40 @@ "Following_thread": "Suivre le fil", "For_your_security_you_must_enter_your_current_password_to_continue": "Pour votre sécurité, vous devez entrer votre mot de passe actuel pour continuer.", "Forgot_password_If_this_email_is_registered": "Si cet e-mail est enregistré, nous vous enverrons des instructions pour réinitialiser votre mot de passe. Si vous ne recevez pas d'e-mail sous peu, veuillez revenir et réessayer.", - "Forgot_password": "Mot de passe oublié", + "Forgot_password": "Mot de passe oublié ?", "Forgot_Password": "Mot de passe oublié", - "Forward": "Faire suivre", - "Forward_Chat": "Faire suivre le canal de discussion", - "Forward_to_department": "Faire suivre au département", - "Forward_to_user": "Faire suivre a l'utilisateur", - "Full_table": "Cliquez pour voir la table complète", + "Forward": "Transmettre", + "Forward_Chat": "Transmettre la conversation", + "Forward_to_department": "Transmettre au département", + "Forward_to_user": "Transmettre à l'utilisateur", + "Full_table": "Cliquez pour voir le tableau complet", "Generate_New_Link": "Générer un nouveau lien", "Group_by_favorites": "Grouper par favoris", "Group_by_type": "Grouper par type", "Hide": "Cacher", "Has_joined_the_channel": "a rejoint le canal", "Has_joined_the_conversation": "a rejoint la conversation", - "Has_left_the_channel": "a quitté la chaîne", + "Has_left_the_channel": "a quitté le canal", "Hide_System_Messages": "Masquer les messages système", "Hide_type_messages": "Masquer les messages \"{{type}}\"", + "How_It_Works": "Comment cela fonctionne", "Message_HideType_uj": "L'utilisateur a rejoint", "Message_HideType_ul": "L'utilisateur est parti", - "Message_HideType_ru": "Utilisateur éjecté", + "Message_HideType_ru": "Utilisateur supprimé", "Message_HideType_au": "Utilisateur ajouté", "Message_HideType_mute_unmute": "Utilisateur rendu muet / a retrouvé la parole", - "Message_HideType_r": "Le nom du salon a été changé", - "Message_HideType_ut": "L'Utilisateur a rejoint la conversation", + "Message_HideType_r": "Nom du salon modifié", + "Message_HideType_ut": "L'utilisateur a rejoint la conversation", "Message_HideType_wm": "Bienvenue", "Message_HideType_rm": "Message supprimé", - "Message_HideType_subscription_role_added": "a été défini avec ce Rôle", - "Message_HideType_subscription_role_removed": "Ce Rôle n'est plus défini", - "Message_HideType_room_archived": "Salon Archivé", - "Message_HideType_room_unarchived": "Salon Désarchivé", + "Message_HideType_subscription_role_added": "Rôle assigné", + "Message_HideType_subscription_role_removed": "Le rôle n'est plus défini", + "Message_HideType_room_archived": "Salon archivé", + "Message_HideType_room_unarchived": "Salon désarchivé", + "I_Saved_My_E2E_Password": "J'ai enregistré mon mot de passe E2E", "IP": "IP", - "In_app": "In-app", - "In_App_And_Desktop": "In-app et Bureau", + "In_app": "Dans l'app", + "In_App_And_Desktop": "Dans l'application et sur le bureau", "In_App_and_Desktop_Alert_info": "Affiche une bannière en haut de l'écran lorsque l'application est ouverte et affiche une notification sur le bureau", "Invisible": "Invisible", "Invite": "Inviter", @@ -253,25 +282,29 @@ "is_not_a_valid_RocketChat_instance": "n'est pas une instance valide de Rocket.Chat", "is_typing": "est en train d'écrire", "Invalid_or_expired_invite_token": "Jeton d'invitation non valide ou expiré", - "Invalid_server_version": "Le serveur que vous essayez de connecter utilise une version qui n'est plus prise en charge par l'application: {{currentVersion}}.\n\nNous exigeons la version {{minVersion}}", + "Invalid_server_version": "Le serveur auquel vous essayez de vous connecter utilise une version qui n'est plus prise en charge par l'application : {{currentVersion}}.\n\nNous exigeons la version {{minVersion}}", "Invite_Link": "Lien d'invitation", - "Invite_users": "Inviter utilisateur", + "Invite_users": "Inviter des utilisateurs", "Join": "Rejoindre", + "Join_Code": "Code d'adhésion", + "Insert_Join_Code": "Insérer le code d'adhésion", "Join_our_open_workspace": "Rejoignez notre espace de travail ouvert", "Join_your_workspace": "Rejoignez votre espace de travail", - "Just_invited_people_can_access_this_channel": "Seuls les invités peuvent accéder à ce canal", + "Just_invited_people_can_access_this_channel": "Seuls les personnes invitées peuvent accéder à ce canal", + "Just_invited_people_can_access_this_team": "Seules les personnes invitées peuvent accéder à cette équipe", "Language": "Langue", - "last_message": "Dernier message", + "last_message": "dernier message", "Leave_channel": "Quitter le canal", - "leaving_room": "En quittent le canal", + "leaving_room": "quittant le salon", + "Leave": "Quitter", "leave": "quitter", - "Legal": "Légale", - "Light": "Lumière", + "Legal": "Légal", + "Light": "Clair", "License": "Licence", - "Livechat": "Livechat", - "Livechat_edit": "Livechat modification", + "Livechat": "Chat en direct", + "Livechat_edit": "Modifier le chat en direct", "Login": "Connexion", - "Login_error": "Vos identifiants ont été rejetés! Veuillez réessayer.", + "Login_error": "Vos identifiants ont été rejetés ! Veuillez réessayer.", "Login_with": "Se connecter avec", "Logging_out": "Déconnexion.", "Logout": "Se déconnecter", @@ -282,7 +315,7 @@ "Mentioned_Messages": "Messages mentionnés", "mentioned": "mentionné", "Mentions": "Mentions", - "Message_accessibility": "message de {{user}} à {{time}}: {{message}}", + "Message_accessibility": "Message de {{user}} à {{time}} : {{message}}", "Message_actions": "Actions de message", "Message_pinned": "Message épinglé", "Message_removed": "Message supprimé", @@ -293,13 +326,14 @@ "Message": "Message", "Messages": "Messages", "Message_Reported": "Message signalé", - "Microphone_Permission_Message": "Rocket.Chat doit avoir accès à votre microphone pour pouvoir envoyer un message audio.", + "Microphone_Permission_Message": "Rocket.Chat a besoin d'accéder à votre microphone pour que vous puissiez envoyer un message audio.", "Microphone_Permission": "Permission de microphone", "Mute": "Rendre muet", - "muted": "Rendu muet", + "muted": "muet", "My_servers": "Mes serveurs", "N_people_reacted": "{{n}} personnes ont réagi", "N_users": "{{n}} utilisateurs", + "N_channels": "{{n}} canaux", "name": "nom", "Name": "Nom", "Navigation_history": "Historique de navigation", @@ -313,26 +347,28 @@ "No_mentioned_messages": "Aucun message mentionné", "No_pinned_messages": "Aucun message épinglé", "No_results_found": "Aucun résultat trouvé", - "No_starred_messages": "Pas de messages suivis", - "No_thread_messages": "Aucun fil de discussion", + "No_starred_messages": "Aucun message suivi", + "No_thread_messages": "Aucun message de fil de discussion", "No_label_provided": "Aucun {{label}} fourni.", "No_Message": "Aucun message", "No_messages_yet": "Pas encore de messages", "No_Reactions": "Aucune réaction", - "No_Read_Receipts": "Pas d'accusé de lecture", + "No_Read_Receipts": "Aucun accusé de lecture", "Not_logged": "Non connecté", "Not_RC_Server": "Ce n'est pas un serveur Rocket.Chat.\n{{contact}}", "Nothing": "Rien", - "Nothing_to_save": "Rien à enregistrer!", - "Notify_active_in_this_room": "Notifier les utilisateurs actifs dans cette salle", - "Notify_all_in_this_room": "Notifier tous dans cette salle", + "Nothing_to_save": "Rien à enregistrer !", + "Notify_active_in_this_room": "Notifier les utilisateurs actifs dans ce salon", + "Notify_all_in_this_room": "Avertir tout le monde dans ce salon", "Notifications": "Notifications", - "Notification_Duration": "Durée de Notification", - "Notification_Preferences": "Préférences de Notification", - "No_available_agents_to_transfer": "Aucun agent disponible à qui transférer", + "Notification_Duration": "Durée des notifications", + "Notification_Preferences": "Préférences de notification", + "No_available_agents_to_transfer": "Aucun agent disponible pour le transfert", "Offline": "Hors ligne", - "Oops": "Oops!", - "Omnichannel": "Omnichannel", + "Oops": "Oups !", + "Omnichannel": "Omnicanal", + "Open_Livechats": "Discussions en cours", + "Omnichannel_enable_alert": "Vous n'êtes pas disponible sur Omnicanal. Souhaitez-vous être disponible ?", "Onboarding_description": "Un espace de travail est l'espace de collaboration de votre équipe ou organisation. Demandez à l'administrateur de l'espace de travail l'adresse pour rejoindre ou créez-en une pour votre équipe.", "Onboarding_join_workspace": "Rejoindre un espace de travail", "Onboarding_subtitle": "Au-delà de la collaboration d'équipe", @@ -343,15 +379,15 @@ "Onboarding_more_options": "Plus d'options", "Online": "En ligne", "Only_authorized_users_can_write_new_messages": "Seuls les utilisateurs autorisés peuvent écrire de nouveaux messages.", - "Open_emoji_selector": "Ouvrir sélecteur emoji", + "Open_emoji_selector": "Ouvrir le sélecteur d'émoji", "Open_Source_Communication": "Communication Open Source", "Open_your_authentication_app_and_enter_the_code": "Ouvrez votre application d'authentification et entrez le code.", "OR": "OU", "OS": "OS", "Overwrites_the_server_configuration_and_use_room_config": "Écrase la configuration du serveur et utilise la configuration du salon", "Password": "Mot de passe", - "Parent_channel_or_group": "Chaîne ou groupe parent", - "Permalink_copied_to_clipboard": "Lien permanent copié dans le presse-papier!", + "Parent_channel_or_group": "Canal ou groupe parent", + "Permalink_copied_to_clipboard": "Lien permanent copié dans le presse-papiers !", "Phone": "Téléphone", "Pin": "Épingler", "Pinned_Messages": "Messages épinglés", @@ -359,20 +395,20 @@ "Pinned": "Épinglé", "Please_add_a_comment": "Veuillez ajouter un commentaire", "Please_enter_your_password": "Veuillez entrer votre mot de passe", - "Please_wait": "Attendez s'il vous plaît", + "Please_wait": "Veuillez patienter.", "Preferences": "Préférences", - "Preferences_saved": "Préférences sauvegardées!", + "Preferences_saved": "Préférences sauvegardées !", "Privacy_Policy": " Politique de confidentialité", "Private_Channel": "Canal privé", "Private_Groups": "Groupes privés", "Private": "Privé", - "Processing": "En traitement...", - "Profile_saved_successfully": "Profil enregistré avec succès!", + "Processing": "Traitement...", + "Profile_saved_successfully": "Profil enregistré avec succès !", "Profile": "Profil", - "Public_Channel": "Canal Public", + "Public_Channel": "Canal public", "Public": "Public", "Push_Notifications": "Notifications Push", - "Push_Notifications_Alert_Info": "Ces notifications vous sont livrées lorsque l'application n'est pas ouverte", + "Push_Notifications_Alert_Info": "Ces notifications vous sont envoyées lorsque l'application n'est pas ouverte", "Quote": "Citation", "Reactions_are_disabled": "Les réactions sont désactivées", "Reactions_are_enabled": "Les réactions sont activées", @@ -380,61 +416,68 @@ "Read": "Lecture", "Read_External_Permission_Message": "Rocket.Chat doit accéder aux photos, aux médias et aux fichiers sur votre appareil", "Read_External_Permission": "Permission de lecture des fichiers", - "Read_Only_Channel": "Chaîne en lecture seule", + "Read_Only_Channel": "Canal en lecture seule", "Read_Only": "Lecture seule", "Read_Receipt": "Accusé de réception", "Receive_Group_Mentions": "Recevoir des mentions de groupe", - "Receive_Group_Mentions_Info": "Recevoir les mentions @all et @here", + "Receive_Group_Mentions_Info": "Recevoir des mentions @all et @here", "Register": "S'inscrire", "Repeat_Password": "Répéter le mot de passe", - "Replied_on": "Répondu le:", + "Replied_on": "A répondu le :", "replies": "réponses", "reply": "répondre", "Reply": "Répondre", "Report": "Signaler", "Receive_Notification": "Recevoir une notification", - "Receive_notifications_from": "Recevoir les notifications de {{name}}", + "Receive_notifications_from": "Recevoir des notifications de {{name}}", "Resend": "Renvoyer", "Reset_password": "Réinitialiser le mot de passe", "resetting_password": "réinitialisation du mot de passe", "RESET": "RÉINITIALISER", "Return": "Retour", - "Review_app_title": "Appréciez-vous cette application?", + "Review_app_title": "Appréciez-vous cette application ?", "Review_app_desc": "Donnez-nous 5 étoiles sur {{store}}", - "Review_app_yes": "Bien sur!", + "Review_app_yes": "Bien sûr !", "Review_app_no": "Non", - "Review_app_later": "plus tard", + "Review_app_later": "Peut-être plus tard", "Review_app_unable_store": "Impossible d'ouvrir {{store}}", "Review_this_app": "Donnez votre avis sur cette application", - "Remove": "Retirer", + "Remove": "Supprimer", + "remove": "supprimer", "Roles": "Rôles", - "Room_actions": "Actions de canal", - "Room_changed_announcement": "Annonce de canal est changée en: {{announcement}} par {{userBy}}", - "Room_changed_description": "Description de canal est changée en: {{description}} par {{userBy}}", - "Room_changed_privacy": "Type de canal est changé en: {{type}} par {{userBy}}", - "Room_changed_topic": "Le sujet de canal est changé en: {{topic}} par {{userBy}}", - "Room_Files": "Fichiers de canal", - "Room_Info_Edit": "Infos sur le canal Modifier", - "Room_Info": "Info sur le canal", - "Room_Members": "Membres de canal", - "Room_name_changed": "Nom de canal est changé en: {{name}} par {{userBy}}", - "SAVE": "ENREGISTRER", + "Room_actions": "Actions du salon", + "Room_changed_announcement": "Annonce du salon changé en : {{announcement}} par {{userBy}}", + "Room_changed_avatar": "Avatar du salon modifié par {{userBy}}", + "Room_changed_description": "Description du salon changé en : {{description}} par {{userBy}}", + "Room_changed_privacy": "Type de salon changé en : {{type}} par {{userBy}}", + "Room_changed_topic": "Le sujet de salon est changé en : {{topic}} par {{userBy}}", + "Room_Files": "Fichiers du salon", + "Room_Info_Edit": "Modifier les informations du salon", + "Room_Info": "Info sur le salon", + "Room_Members": "Membres du salon", + "Room_name_changed": "Nom de salon changé en : {{name}} par {{userBy}}", + "SAVE": "SAUVEGARDER", "Save_Changes": "Sauvegarder les modifications", "Save": "Sauvegarder", - "Saved": "Sauvé", - "saving_preferences": "sauvegardant les préférences", + "Saved": "Enregistré", + "saving_preferences": "enregistrement des préférences", "saving_profile": "enregistrement du profil", "saving_settings": "enregistrement des paramètres", - "saved_to_gallery": "Sauvé dans la galerie", + "saved_to_gallery": "Enregistré dans la galerie", + "Save_Your_E2E_Password": "Enregistrez votre mot de passe E2E", + "Save_Your_Encryption_Password": "Enregistrez votre mot de passe de cryptage", + "Save_Your_Encryption_Password_warning": "Ce mot de passe n'est stocké nulle part, enregistrez-le donc soigneusement ailleurs.", + "Save_Your_Encryption_Password_info": "Si vous perdez le mot de passe, il n'y a aucun moyen de le récupérer et vous perdrez l'accès à vos messages.", "Search_Messages": "Rechercher des messages", "Search": "Recherche", - "Search_by": "Recherche par", + "Search_by": "Rechercher par", "Search_global_users": "Rechercher des utilisateurs mondiaux", "Search_global_users_description": "Si vous activez, vous pouvez rechercher n'importe quel utilisateur d'autres sociétés ou serveurs.", "Seconds": "{{second}} secondes", + "Security_and_privacy": "Sécurité et vie privée", "Select_Avatar": "Sélectionnez un avatar", "Select_Server": "Sélectionnez un serveur", - "Select_Users": "Sélectionner des utilisateurs", + "Select_Users": "Sélectionner les utilisateurs", "Select_a_Channel": "Sélectionnez un canal", "Select_a_Department": "Sélectionnez un département", "Select_an_option": "Sélectionnez une option", @@ -449,50 +492,50 @@ "Sent_an_attachment": "Envoyé une pièce jointe", "Server": "Serveur", "Servers": "Serveurs", - "Server_version": "Version du serveur: {{version}}", + "Server_version": "Version du serveur : {{version}}", "Set_username_subtitle": "Le nom d'utilisateur est utilisé pour permettre aux autres de vous mentionner dans les messages", - "Set_custom_status": "Définir un statut personnalisé", + "Set_custom_status": "Définir le statut personnalisé", "Set_status": "Définir le statut", - "Status_saved_successfully": "Statut enregistré avec succès!", + "Status_saved_successfully": "Statut enregistré avec succès !", "Settings": "Paramètres", - "Settings_succesfully_changed": "Paramètres modifiés avec succès!", + "Settings_succesfully_changed": "Paramètres modifiés avec succès !", "Share": "Partager", "Share_Link": "Partager le lien", "Share_this_app": "Partager cette application", "Show_more": "Afficher plus..", "Show_Unread_Counter": "Afficher le compteur non lu", - "Show_Unread_Counter_Info": "Le compteur non-lu est affiché sous forme de badge à droite de la chaîne, dans la liste", + "Show_Unread_Counter_Info": "Le compteur non lu est affiché sous forme de badge à droite du canal, dans la liste", "Sign_in_your_server": "Connectez-vous à votre serveur", "Sign_Up": "S'inscrire", "Some_field_is_invalid_or_empty": "Certains champs sont invalides ou vides", "Sorting_by": "Tri par {{key}}", "Sound": "Son", - "Star_room": "Favoriser canal", - "Star": "Favoris", - "Starred_Messages": "Les messages favorisé", - "starred": "favorisé", - "Starred": "Favorisé", + "Star_room": "Canal favoris", + "Star": "Mettre en favoris", + "Starred_Messages": "Les messages favoris", + "starred": "favoris", + "Starred": "Favoris", "Start_of_conversation": "Début de conversation", "Start_a_Discussion": "Lancer une discussion", - "Started_discussion": "A commencé une discussion:", + "Started_discussion": "A commencé une discussion :", "Started_call": "Appel lancé par {{userBy}}", "Submit": "Soumettre", - "Table": "Table", + "Table": "Tableau", "Tags": "Mots clés", "Take_a_photo": "Prendre une photo", "Take_a_video": "Prendre une vidéo", - "Take_it": "Prends-le!", - "tap_to_change_status": "Appuyez pour changer de statut", + "Take_it": "Prends-le !", + "tap_to_change_status": "appuyez pour changer de statut", "Tap_to_view_servers_list": "Appuyez pour afficher la liste des serveurs", "Terms_of_Service": " Conditions d'utilisation ", "Theme": "Thème", "The_user_wont_be_able_to_type_in_roomName": "L'utilisateur ne pourra pas écrire dans {{roomName}}", "The_user_will_be_able_to_type_in_roomName": "L'utilisateur pourra écrire dans {{roomName}}", - "There_was_an_error_while_action": "Il y avait une erreur en {{action}}!", - "This_room_is_blocked": "Cette canal est bloquée", - "This_room_is_read_only": "Cette canal est en lecture seule", - "Thread": "Fil de discutions", - "Threads": "Fils de discutions", + "There_was_an_error_while_action": "Une erreur s'est produite lors de {{action}} !", + "This_room_is_blocked": "Ce salon est bloqué", + "This_room_is_read_only": "Ce salon est en lecture seule", + "Thread": "Fil de discussion", + "Threads": "Fils de discussions", "Timezone": "Fuseau horaire", "To": "A", "topic": "sujet", @@ -506,101 +549,103 @@ "Unblock_user": "Débloquer l'utilisateur", "Unfavorite": "Supprimer des favoris", "Unfollowed_thread": "Ne plus suivre ce fil", - "Unmute": "Rendre La parole", - "unmuted": "Rendu la parole", + "Unmute": "Rendre la parole", + "unmuted": "rendu la parole", "Unpin": "Détacher", - "unread_messages": "non lus", + "unread_messages": "non lu", "Unread": "Non lu", - "Unread_on_top": "Non lu sur le dessus", - "Unstar": "Unstar", + "Unread_on_top": "Non lu en haut", + "Unstar": "Enlever des favoris", "Updating": "Mise à jour...", - "Uploading": "Téléchargement", - "Upload_file_question_mark": "Télécharger le fichier?", + "Uploading": "Envoyer", + "Upload_file_question_mark": "Téléverser un fichier ?", "User": "Utilisateur", "Users": "Utilisateurs", - "User_added_by": "L'utilisateur {{userAdded}} a été ajouté par {{userBy}}", + "User_added_by": "Utilisateur {{userAdded}} ajouté par {{userBy}}", "User_Info": "Info d'utilisateur", "User_has_been_key": "L'utilisateur a été {{key}}", "User_is_no_longer_role_by_": "{{user}} n'est plus {{role}} par {{userBy}}", "User_muted_by": "L'utilisateur {{userMuted}} a été rendu muet par {{userBy}}", - "User_removed_by": "L'utilisateur {{userRemoved}} a été retiré par {{userBy}}", - "User_sent_an_attachment": "{{user}} envoyé une pièce jointe", - "User_unmuted_by": "L'utilisateur {{userBy}} a rendu la parole a {{userUnmuted}}", - "User_was_set_role_by_": "{{user}} l'utilisateur a été défini {{role}} par {{userBy}}", + "User_removed_by": "Utilisateur {{userRemoved}} supprimé par {{userBy}}", + "User_sent_an_attachment": "{{user}} a envoyé une pièce jointe", + "User_unmuted_by": "L'utilisateur {{userBy}} a rendu la parole à {{userUnmuted}}", + "User_was_set_role_by_": "{{user}} a été défini {{role}} par {{userBy}}", "Username_is_empty": "Nom d'utilisateur est vide", "Username": "Nom d'utilisateur", - "Username_or_email": "Nom d'utilisateur ou address e-mail", + "Username_or_email": "Nom d'utilisateur ou e-mail", "Uses_server_configuration": "Utilise la configuration du serveur", "Validating": "Validation", - "Registration_Succeeded": "Inscription réussie!", + "Registration_Succeeded": "Inscription réussie !", "Verify": "Vérifier", - "Verify_email_title": "Inscription réussie!", + "Verify_email_title": "Inscription réussie !", "Verify_email_desc": "Nous vous avons envoyé un e-mail pour confirmer votre inscription. Si vous ne recevez pas d'e-mail sous peu, veuillez revenir et réessayer.", "Verify_your_email_for_the_code_we_sent": "Vérifiez votre e-mail pour le code que nous avons envoyé", "Video_call": "Appel vidéo", "View_Original": "Voir l'original", "Voice_call": "Appel vocal", - "Waiting_for_network": "En attente du réseau ...", + "Waiting_for_network": "En attente du réseau...", "Websocket_disabled": "Le Websocket est désactivé pour ce serveur.\n{{contact}}", "Welcome": "Bienvenue", - "What_are_you_doing_right_now": "Qu'es ce que vous faites actuellement?", - "Whats_your_2fa": "Quel est votre code 2FA?", + "What_are_you_doing_right_now": "Que fais-tu en ce moment ?", + "Whats_your_2fa": "Quel est votre code 2FA ?", "Without_Servers": "Sans serveurs", "Workspaces": "Espaces de travail", - "Would_you_like_to_return_the_inquiry": "Souhaitez-vous retourner la demande?", + "Would_you_like_to_return_the_inquiry": "Souhaitez-vous retourner la demande ?", "Write_External_Permission_Message": "Rocket.Chat a besoin d'accéder à votre galerie pour que vous puissiez enregistrer des images.", "Write_External_Permission": "Autorisation de la galerie", "Yes": "Oui", - "Yes_action_it": "Oui, {{action}} le!", + "Yes_action_it": "Oui, {{action}} le !", "Yesterday": "Hier", - "You_are_in_preview_mode": "Vous êtes en mode de prévisualisation", + "You_are_in_preview_mode": "Vous êtes en mode aperçu", "You_are_offline": "Vous êtes hors ligne", - "You_can_search_using_RegExp_eg": "Vous pouvez rechercher à l'aide de RegExp. e.g. `/^text$/i`", + "You_can_search_using_RegExp_eg": "Vous pouvez utiliser RegExp., par exemple `/^texte$/i`", "You_colon": "Vous: ", "you_were_mentioned": "vous avez été mentionné", - "You_were_removed_from_channel": "Vous avez été retiré de{{channel}}", + "You_were_removed_from_channel": "Vous avez été retiré de {{channel}}", "you": "vous", "You": "Vous", - "Logged_out_by_server": "Vous avez été déconnecté par le serveur. Veuillez vous reconnecter.", + "Logged_out_by_server": "Vous avez été déconnecté du serveur. Veuillez vous reconnecter.", "You_need_to_access_at_least_one_RocketChat_server_to_share_something": "Vous devez accéder à au moins un serveur Rocket.Chat pour partager quelque chose.", - "Your_certificate": "Votre Certificat", + "You_need_to_verifiy_your_email_address_to_get_notications": "Vous devez vérifier votre adresse e-mail pour recevoir des notifications", + "Your_certificate": "Votre certificat", "Your_invite_link_will_expire_after__usesLeft__uses": "Votre lien d'invitation expirera après {{usesLeft}} utilisations.", "Your_invite_link_will_expire_on__date__or_after__usesLeft__uses": "Votre lien d'invitation expirera le {{date}} ou après {{usesLeft}} utilisations.", "Your_invite_link_will_expire_on__date__": "Votre lien d'invitation expirera le {{date}}.", "Your_invite_link_will_never_expire": "Votre lien d'invitation n'expirera jamais.", "Your_workspace": "Votre espace de travail", - "Version_no": "Version: {{version}}", - "You_will_not_be_able_to_recover_this_message": "Vous ne pourrez pas récupérer ce message!", - "You_will_unset_a_certificate_for_this_server": "Vous allez annuler un certificat pour ce serveur", - "Change_Language": "Changer la Langue", - "Crash_report_disclaimer": "Nous ne suivons jamais le contenu de vos chats. Le rapport de plantage ne contient que des informations pertinentes pour nous afin d'identifier les problèmes et de les résoudre.", - "Type_message": "Écrire un message", - "Room_search": "Recherche de salon", - "Room_selection": "Sélection du Salon 1...9", - "Next_room": "Salon Suivant", - "Previous_room": "Salon Précédent", + "Your_password_is": "Votre mot de passe est", + "Version_no": "Version : {{version}}", + "You_will_not_be_able_to_recover_this_message": "Vous ne pourrez pas récupérer ce message !", + "You_will_unset_a_certificate_for_this_server": "Vous allez supprimer un certificat pour ce serveur", + "Change_Language": "Changer la langue", + "Crash_report_disclaimer": "Nous ne suivons jamais le contenu de vos chats. Le rapport d'incident et les évènements d'analyse ne contiennent que des informations pertinentes pour nous afin d'identifier et de résoudre les problèmes.", + "Type_message": "Tapez le message", + "Room_search": "Recherche de salons", + "Room_selection": "Sélection de salon 1...9", + "Next_room": "Salon suivant", + "Previous_room": "Salon précédent", "New_room": "Nouveau salon", - "Upload_room": "Envoyer sur un salon", - "Search_messages": "Recherche de messages", - "Scroll_messages": "Défiler messages", + "Upload_room": "Envoyer dans un salon", + "Search_messages": "Rechercher des messages", + "Scroll_messages": "Faire défiler les messages", "Reply_latest": "Répondre au dernier", "Reply_in_Thread": "Répondre dans le fil", "Server_selection": "Sélection du serveur", - "Server_selection_numbers": "Sélection du Serveur 1...9", - "Add_server": "Ajouter serveur", + "Server_selection_numbers": "Sélection du serveur 1...9", + "Add_server": "Ajouter un serveur", "New_line": "Nouvelle ligne", "You_will_be_logged_out_of_this_application": "Vous serez déconnecté de cette application.", "Clear": "Effacer", - "This_will_clear_all_your_offline_data": "Cela effacera toutes vos données hors-ligne.", + "This_will_clear_all_your_offline_data": "Cela effacera toutes vos données hors ligne.", "This_will_remove_all_data_from_this_server": "Cela supprimera toutes les données de ce serveur.", "Mark_unread": "Marquer comme non lu", "Wait_activation_warning": "Avant de pouvoir vous connecter, votre compte doit être activé manuellement par un administrateur.", "Screen_lock": "Verrouillage d'écran", "Local_authentication_biometry_title": "Authentifier", - "Local_authentication_biometry_fallback": "Utiliser le mot de passe", - "Local_authentication_unlock_option": "Déverrouiller avec mot de passe", - "Local_authentication_change_passcode": "Changer le code", - "Local_authentication_info": "Remarque: si vous oubliez le code, vous devrez supprimer et réinstaller l'application.", + "Local_authentication_biometry_fallback": "Utiliser le code d'accès", + "Local_authentication_unlock_option": "Déverrouiller avec le code d'accès", + "Local_authentication_change_passcode": "Changer le code d'accès", + "Local_authentication_info": "Remarque : si vous oubliez le code d'accès, vous devrez supprimer et réinstaller l'application.", "Local_authentication_facial_recognition": "reconnaissance faciale", "Local_authentication_fingerprint": "empreinte digitale", "Local_authentication_unlock_with_label": "Déverrouiller avec {{label}}", @@ -609,15 +654,112 @@ "Local_authentication_auto_lock_900": "Après 15 minutes", "Local_authentication_auto_lock_1800": "Après 30 minutes", "Local_authentication_auto_lock_3600": "Après 1 heure", - "Passcode_enter_title": "Entrez votre mot de passe", - "Passcode_choose_title": "Choisissez votre nouveau mot de passe", - "Passcode_choose_confirm_title": "Confirmez votre nouveau mot de passe", - "Passcode_choose_error": "Les codes secrets ne correspondent pas. Réessayer.", + "Passcode_enter_title": "Entrez votre code d'accès", + "Passcode_choose_title": "Choisissez votre nouveau code d'accès", + "Passcode_choose_confirm_title": "Confirmez votre nouveau code d'accès", + "Passcode_choose_error": "Les codes d'accès ne correspondent pas. Réessayer.", "Passcode_choose_force_set": "Code d'accès requis par l'administrateur", "Passcode_app_locked_title": "App verrouillée", "Passcode_app_locked_subtitle": "Réessayez dans {{timeLeft}} secondes", "After_seconds_set_by_admin": "Après {{seconds}} secondes (défini par l'administrateur)", "Dont_activate": "Ne pas activer maintenant", "Queued_chats": "Discussions en file d'attente", - "Queue_is_empty": "La file d'attente est vide" + "Queue_is_empty": "La file d'attente est vide", + "Logout_from_other_logged_in_locations": "Déconnexion des autres emplacements connectés", + "You_will_be_logged_out_from_other_locations": "Vous serez déconnecté des autres emplacements.", + "Logged_out_of_other_clients_successfully": "Déconnexion réussie des autres clients", + "Logout_failed": "Echec de la déconnexion !", + "Log_analytics_events": "Journal des événements d'analyse", + "E2E_encryption_change_password_title": "Changer le mot de passe de cryptage", + "E2E_encryption_change_password_description": "Vous pouvez désormais créer des groupes privés et des messages directs chiffrés. Vous pouvez également modifier les groupes privés ou DM existants pour les crypter.\nIl s'agit du chiffrement de bout en bout, la clé permettant de coder/décoder vos messages ne sera pas enregistrée sur le serveur. Pour cette raison, vous devez stocker ce mot de passe à un endroit sûr. Vous devrez le saisir sur les autres appareils sur lesquels vous souhaitez utiliser le cryptage E2E.", + "E2E_encryption_change_password_error": "Erreur lors de la modification du mot de passe de la clé E2E", + "E2E_encryption_change_password_success": "Le mot de passe de la clé E2E a été changé avec succès !", + "E2E_encryption_change_password_message": "Assurez-vous de l'avoir enregistré soigneusement ailleurs.", + "E2E_encryption_change_password_confirmation": "Oui, changez-le", + "E2E_encryption_reset_title": "Réinitialiser la clé E2E", + "E2E_encryption_reset_description": "Cette option supprimera la clé E2E actuelle et vous déconnectera.\nLorsque vous vous reconnecterez, Rocket.Chat générera une nouvelle clé et restaurera votre accès aux salons cryptés qui a un ou plusieurs membres en ligne.\nEn raison de la nature du cryptage E2E, Rocket.Chat ne pourra pas restaurer l'accès à un salon crypté qui n'a aucun membre en ligne.", + "E2E_encryption_reset_button": "Réinitialiser la clé E2E", + "E2E_encryption_reset_error": "Erreur lors de la réinitialisation de la clé E2E !", + "E2E_encryption_reset_message": "Vous allez être déconnecté.", + "E2E_encryption_reset_confirmation": "Oui, réinitialisez-le", + "Following": "Suivant", + "Threads_displaying_all": "Tout afficher", + "Threads_displaying_following": "Affichage suivant", + "Threads_displaying_unread": "Affichage non lu", + "No_threads": "Il n'y a pas de fils", + "No_threads_following": "Vous ne suivez aucun fil de discussion", + "No_threads_unread": "Il n'y a pas de fils non lus", + "Messagebox_Send_to_channel": "Envoyer au canal", + "Leader": "Leader", + "Moderator": "Modérateur", + "Owner": "Propriétaire", + "Remove_from_room": "Retirer du salon", + "Ignore": "Ignorer", + "Unignore": "Ne pas ignorer", + "User_has_been_ignored": "L'utilisateur a été ignoré", + "User_has_been_unignored": "L'utilisateur n'est plus ignoré", + "User_has_been_removed_from_s": "L'utilisateur a été retiré de {{s}}", + "User__username__is_now_a_leader_of__room_name_": "L'utilisateur {{username}} est désormais un leader de {{room_name}}", + "User__username__is_now_a_moderator_of__room_name_": "L'utilisateur {{username}} est désormais un modérateur de {{room_name}}", + "User__username__is_now_a_owner_of__room_name_": "L'utilisateur {{username}} est désormais un propriétaire de {{room_name}}", + "User__username__removed_from__room_name__leaders": "L'utilisateur {{username}} a été supprimé des leaders de {{room_name}}", + "User__username__removed_from__room_name__moderators": "L'utilisateur {{username}} a été supprimé des modérateurs de {{room_name}}", + "User__username__removed_from__room_name__owners": "L'utilisateur {{username}} a été supprimé des propriétaires de {{room_name}}", + "The_user_will_be_removed_from_s": "L'utilisateur sera supprimé de {{s}}", + "Yes_remove_user": "Oui, supprimez l'utilisateur !", + "Direct_message": "Message direct", + "Message_Ignored": "Message ignoré. Touchez pour l'afficher.", + "Enter_workspace_URL": "Entrez l'URL de l'espace de travail", + "Workspace_URL_Example": "Ex. votre-société.rocket.chat", + "This_room_encryption_has_been_enabled_by__username_": "Le cryptage de ce salon a été activé par {{username}}", + "This_room_encryption_has_been_disabled_by__username_": "Le cryptage de ce salon a été désactivé par {{username}}", + "Teams": "Equipes", + "No_team_channels_found": "Aucun canal trouvé", + "Team_not_found": "Equipe non trouvée", + "Create_Team": "Créer une équipe", + "Team_Name": "Nom de l'équipe", + "Private_Team": "Equipe privée", + "Read_Only_Team": "Equipe en lecture seule", + "Broadcast_Team": "Equipe de diffusion", + "creating_team": "création de l'équipe", + "team-name-already-exists": "Une équipe portant ce nom existe déjà", + "Add_Channel_to_Team": "Ajouter un canal à l'équipe", + "Create_New": "Créer un nouveau", + "Add_Existing": "Ajouter existant", + "Add_Existing_Channel": "Ajouter un canal existant", + "Remove_from_Team": "Retirer de l'équipe", + "Auto-join": "Rejoindre automatiquement", + "Remove_Team_Room_Warning": "Souhaitez-vous supprimer ce canal de l'équipe ? Le canal sera déplacé vers l'espace de travail", + "Confirmation": "Confirmation", + "invalid-room": "Salon invalide", + "You_are_leaving_the_team": "Vous quittez l'équipe '{{team}}'", + "Leave_Team": "Quitter l'équipe", + "Select_Team": "Sélectionnez l'équipe", + "Select_Team_Channels": "Sélectionnez les canaux de l'équipe que vous souhaitez quitter.", + "Cannot_leave": "Ne peut pas partir", + "Cannot_remove": "Impossible d'enlever", + "Cannot_delete": "Impossible de supprimer", + "Last_owner_team_room": "Vous êtes le dernier propriétaire de ce canal. Une fois que vous quittez l'équipe, le canal sera conservé au sein de l'équipe mais vous le gérerez de l'extérieur.", + "last-owner-can-not-be-removed": "Le dernier propriétaire ne peut pas être supprimé", + "Remove_User_Teams": "Sélectionnez les canaux dont vous souhaitez supprimer l'utilisateur.", + "Delete_Team": "Supprimer l'équipe", + "Select_channels_to_delete": "Ceci ne peut pas être annulé. Une fois que vous supprimez une équipe, tout le contenu et la configuration du chat seront supprimés.\n\nSélectionnez les canaux que vous souhaitez supprimer. Ceux que vous décidez de conserver seront disponible dans votre espace de travail. Notez que les canaux publics seront toujours publics et visibles par tous.", + "You_are_deleting_the_team": "Vous supprimez cette équipe.", + "Removing_user_from_this_team": "Vous supprimez {{user}} de cette équipe", + "Remove_User_Team_Channels": "Sélectionnez les canaux dont vous souhaitez supprimer l'utilisateur.", + "Remove_Member": "Supprimer un membre", + "leaving_team": "quitter l'équipe", + "removing_team": "retirer de l'équipe", + "moving_channel_to_team": "transfert de canal à l'équipe", + "deleting_team": "suppression de l'équipe", + "member-does-not-exist": "Le membre n'existe pas", + "Convert": "Convertir", + "Convert_to_Team": "Convertir en équipe", + "Convert_to_Team_Warning": "Ceci ne peut pas être annulé. Une fois que vous avez converti un canal en équipe, vous ne pouvez pas le retransformer en canal.", + "Move_to_Team": "Déplacer vers l'équipe", + "Move_Channel_Paragraph": "Le déplacement d'un canal dans une équipe signifie que ce canal sera ajouté dans le contexte d'équipe. Cependant, tous les membres du canal, qui ne sont pas membres de l'équipe respective, auront toujours accès à ce canal, mais ne seront pas ajoutés comme membres de l'équipe.\n\nLa gestion de tout le canal sera toujours assurée par les propriétaires de ce canal.\n\nLes membres de l'équipe et même les propriétaires de l'équipe, s'ils ne sont pas membres de ce canal, ne peuvent pas avoir accès au contenu du canal.\n\nVeuillez noter que le propriétaire de l'équipe pourra supprimer des membres du canal.", + "Move_to_Team_Warning": "Après avoir lu les instructions précédentes sur ce comportement, voulez-vous toujours déplacer ce canal vers l'équipe sélectionnée ?", + "Load_More": "Charger plus", + "Load_Newer": "Charger plus récent", + "Load_Older": "Charger plus ancien" } \ No newline at end of file diff --git a/app/i18n/locales/it.json b/app/i18n/locales/it.json index 4c6ee1acf..505282dd5 100644 --- a/app/i18n/locales/it.json +++ b/app/i18n/locales/it.json @@ -290,11 +290,11 @@ "last_message": "ultimo messaggio", "Leave_channel": "Abbandona canale", "leaving_room": "abbandonando stanza", + "Leave": "Lasciare il canale", "leave": "abbandona", "Legal": "Informazioni", "Light": "Chiaro", "License": "Licenza", - "Livechat": "Livechat", "Livechat_edit": "Modifica Livechat", "Login": "Accedi", "Login_error": "Le tue credenziali sono state rifiutate! Prova di nuovo.", @@ -681,12 +681,6 @@ "No_threads_following": "Non stai seguendo alcun thread", "No_threads_unread": "Non ci sono thread non letti", "Messagebox_Send_to_channel": "Invia sul canale", - "Set_as_leader": "Rendi leader", - "Set_as_moderator": "Rendi moderatore", - "Set_as_owner": "Rendi proprietario", - "Remove_as_leader": "Rimuovi come leader", - "Remove_as_moderator": "Rimuovi come moderatore", - "Remove_as_owner": "Rimuovi come proprietario", "Remove_from_room": "Rimuovi dalla stanza", "Ignore": "Ignora", "Unignore": "Non ignorare", @@ -704,5 +698,6 @@ "Direct_message": "Messaggio diretto", "Message_Ignored": "Messaggio ignorato. Tocca per visualizzarlo.", "Enter_workspace_URL": "Inserisci la url del workspace", - "Workspace_URL_Example": "Es. tua-azienda.rocket.chat" + "Workspace_URL_Example": "Es. tua-azienda.rocket.chat", + "invalid-room": "Canale non valido" } \ No newline at end of file diff --git a/app/i18n/locales/ja.json b/app/i18n/locales/ja.json index d758e135a..994753fbb 100644 --- a/app/i18n/locales/ja.json +++ b/app/i18n/locales/ja.json @@ -219,6 +219,7 @@ "last_message": "最後のメッセージ", "Leave_channel": "チャンネルを退出", "leaving_room": "チャンネルを退出", + "Leave": "ルームを退出", "leave": "退出", "Legal": "法的項目", "Light": "ライト", @@ -487,5 +488,6 @@ "New_line": "新しい行", "You_will_be_logged_out_of_this_application": "アプリからログアウトします。", "Clear": "クリア", - "This_will_clear_all_your_offline_data": "オフラインデータをすべて削除します。" + "This_will_clear_all_your_offline_data": "オフラインデータをすべて削除します。", + "invalid-room": "無効なルーム" } \ No newline at end of file diff --git a/app/i18n/locales/nl.json b/app/i18n/locales/nl.json index 7756a910d..8a48d3c74 100644 --- a/app/i18n/locales/nl.json +++ b/app/i18n/locales/nl.json @@ -4,254 +4,343 @@ "error-action-not-allowed": "{{action}} is niet toegestaan", "error-application-not-found": "Applicatie niet gevonden", "error-archived-duplicate-name": "Er is een gearchiveerd kanaal met de naam {{room_name}}", - "error-avatar-invalid-url": "Foutieve avatar URL: {{url}}", - "error-avatar-url-handling": "Fout tijdens verwerken avatar instellingen vanaf een URL({{url}}) for {{username}}", - "error-cant-invite-for-direct-room": "Kan gebruikers niet in directe kamers toevoegen", - "error-could-not-change-email": "Kon email niet veranderen", - "error-could-not-change-name": "Kon naam niet veranderen", - "error-could-not-change-username": "Kon gebruikersnaam niet veranderen", - "error-delete-protected-role": "Beveiligde rollen kunnen niet verwijderd worden.", + "error-avatar-invalid-url": "Ongeldige avatar-URL: {{url}}", + "error-avatar-url-handling": "Fout bij het verwerken van de avatar-instelling van een URL ({{url}}) voor {{username}}", + "error-cant-invite-for-direct-room": "Kan gebruikers in directe kamers niet uitnodigen", + "error-could-not-change-email": "Kan e-mail niet veranderen", + "error-could-not-change-name": "Kan naam niet veranderen", + "error-could-not-change-username": "Kan gebruikersnaam niet veranderen", + "error-could-not-change-status": "Kan status niet wijzigen", + "error-delete-protected-role": "Kan een beveiligde rol niet verwijderen", "error-department-not-found": "Afdeling niet gevonden", - "error-direct-message-file-upload-not-allowed": "Delen van bestanden niet toegestaan in directe berichten", - "error-duplicate-channel-name": "Een kanaal met de naam {{channel_name}} bestaat", - "error-email-domain-blacklisted": "Het email domein is blacklisted", - "error-email-send-failed": "Fout tijdens verzenden van email: {{message}}", - "error-save-image": "Fout tijdens opslaan afbeelding", - "error-field-unavailable": "{{field}} is alr in gebruik :(", + "error-direct-message-file-upload-not-allowed": "Delen van bestanden in privéberichten niet toegestaan", + "error-duplicate-channel-name": "Een kanaal met naam {{room_name}} bestaat", + "error-email-domain-blacklisted": "Het e-maildomein staat op de zwarte lijst", + "error-email-send-failed": "Fout bij het verzenden van e-mail: {{message}}", + "error-save-image": "Fout bij het opslaan van afbeelding", + "error-save-video": "Fout bij het opslaan van video", + "error-field-unavailable": "{{field}} is al in gebruik :(", "error-file-too-large": "Bestand is te groot", - "error-importer-not-defined": "De importer is niet goed gedefinieerd, het mist de Import class.", + "error-importer-not-defined": "De importeur is niet correct gedefinieerd, de klasse Import ontbreekt.", "error-input-is-not-a-valid-field": "{{input}} is geen geldig {{field}}", - "error-invalid-actionlink": "Ongeldige action link", + "error-invalid-actionlink": "Ongeldige actielink", "error-invalid-arguments": "Ongeldige argumenten", - "error-invalid-asset": "Ongeldig asset", - "error-invalid-channel": "Ongeldig channel.", - "error-invalid-channel-start-with-chars": "Ongeldig channel. Begin met @ of #", - "error-invalid-custom-field": "Ongeldig custom veld", - "error-invalid-custom-field-name": "Ongeldige custom veld naam. Gebruik alleen letters, cijfers, - of _.", + "error-invalid-asset": "Ongeldig item", + "error-invalid-channel": "Ongeldig kanaal.", + "error-invalid-channel-start-with-chars": "Ongeldig kanaal. Begin met @ of #", + "error-invalid-custom-field": "Ongeldig aangepast veld", + "error-invalid-custom-field-name": "Ongeldige aangepaste veldnaam. Gebruik alleen letters, cijfers, koppeltekens en underscores.", "error-invalid-date": "Ongeldige datum opgegeven.", "error-invalid-description": "Ongeldige beschrijving", "error-invalid-domain": "Ongeldig domein", - "error-invalid-email": "Ongeldige email {{email}}", - "error-invalid-email-address": "Ongeldig emailadres", - "error-invalid-file-height": "Ongeldige file height", + "error-invalid-email": "Ongeldig e-mail {{email}}", + "error-invalid-email-address": "Ongeldig e-mailadres", + "error-invalid-file-height": "Ongeldige bestandshoogte", "error-invalid-file-type": "Ongeldig bestandstype", - "error-invalid-file-width": "Ongeldige file width", - "error-invalid-from-address": "Een ongeldig FROM adres is ingevuld.", - "error-invalid-integration": "Ongeldige integration", - "error-invalid-message": "Ongeldige message", - "error-invalid-method": "Ongeldige method", + "error-invalid-file-width": "Ongeldige bestandsbreedte", + "error-invalid-from-address": "Je hebt een ongeldig FROM adres opgegeven.", + "error-invalid-integration": "Ongeldige integratie", + "error-invalid-message": "Ongeldig bericht", + "error-invalid-method": "Ongeldige methode", "error-invalid-name": "Ongeldige naam", - "error-invalid-password": "Ongeldig password", + "error-invalid-password": "Ongeldig wachtwoord", "error-invalid-redirectUri": "Ongeldige redirectUri", - "error-invalid-role": "Ongeldige role", + "error-invalid-role": "Ongeldige rol", "error-invalid-room": "Ongeldige kamer", "error-invalid-room-name": "{{room_name}} is geen geldige kamernaam", "error-invalid-room-type": "{{type}} is geen geldig kamertype.", - "error-invalid-settings": "Ongeldige instellingen ingevuld", - "error-invalid-subscription": "Ongeldige subscription", - "error-invalid-token": "Ongeldig token", - "error-invalid-triggerWords": "Ongeldige triggerWords", - "error-invalid-urls": "Ongeldige URLs", - "error-invalid-user": "Ongeldige user", - "error-invalid-username": "Ongeldige username", - "error-invalid-webhook-response": "De webhook URL antwoorde met een andere status dan 200", - "error-message-deleting-blocked": "Berichten verwijderen is geblokkeerd.", - "error-message-editing-blocked": "Berichten aanpassen is geblokkeerd.", - "error-message-size-exceeded": "Berichtgrootte is meer dan Message_MaxAllowedSize", - "error-missing-unsubscribe-link": "De [unsubscribe] link moet gegeven worden.", - "error-no-tokens-for-this-user": "Er zijn geen tokens voor deze user", + "error-invalid-settings": "Ongeldige instellingen opgegeven", + "error-invalid-subscription": "Ongeldig abonnement", + "error-invalid-token": "Ongeldige token", + "error-invalid-triggerWords": "Ongeldige triggerWoorden", + "error-invalid-urls": "Ongeldige URL's", + "error-invalid-user": "Ongeldige gebruiker", + "error-invalid-username": "Ongeldige gebruikersnaam", + "error-invalid-webhook-response": "De webhook-URL heeft met een andere status dan 200 gereageerd", + "error-message-deleting-blocked": "Het verwijderen van berichten is geblokkeerd", + "error-message-editing-blocked": "Het aanpassen van berichten is geblokkeerd", + "error-message-size-exceeded": "Berichtgrootte is groter dan Message_MaxAllowedSize", + "error-missing-unsubscribe-link": "Je moet de link [unsubscribe] opgeven.", + "error-no-owner-channel": "Je bent niet de eigenaar van het kanaal", + "error-no-tokens-for-this-user": "Er zijn geen tokens voor deze gebruiker", "error-not-allowed": "Niet toegestaan", - "error-not-authorized": "Niet gemachtigd", - "error-push-disabled": "Push staat uit", - "error-remove-last-owner": "Dit is de laatste eigenaar. Kies een nieuwe eigenaar voor je deze verwijderd.", - "error-role-in-use": "Kan rol niet verwijderen omdat hij in gebruik is", - "error-role-name-required": "Rol naam verplicht", + "error-not-authorized": "Geen bevoegdheid", + "error-push-disabled": "Push is uitgeschakeld", + "error-remove-last-owner": "Dit is de laatste eigenaar. Stel een nieuwe eigenaar in voordat je deze verwijdert.", + "error-role-in-use": "Kan rol niet verwijderen omdat deze in gebruik is", + "error-role-name-required": "Rolnaam is vereist", "error-the-field-is-required": "Het veld {{field}} is verplicht.", - "error-too-many-requests": "Error, te veel requests. Doe alsjeblieft rustig aan. Je moet {{seconds}} wachten voor je het opnieuw kan proberen.", + "error-too-many-requests": "Fout, te veel verzoeken. Vertraag, alsjeblieft. Je moet {{seconds}} seconden wachten voordat je het opnieuw probeert.", "error-user-is-not-activated": "Gebruiker is niet geactiveerd", "error-user-has-no-roles": "Gebruiker heeft geen rollen", - "error-user-limit-exceeded": "De hoeveelheid gebruikers die je probeert uit te nodigen voor #channel_name is meer dan het limiet wat de admin gekozen heeft", + "error-user-limit-exceeded": "Het aantal gebruikers die je probeert uit te nodigen voor #channel_name overschrijdt de limiet ingesteld door de beheerder", "error-user-not-in-room": "Gebruiker is niet in deze kamer", "error-user-registration-custom-field": "error-user-registration-custom-field", - "error-user-registration-disabled": "Registratie van gebruikers staat uit", - "error-user-registration-secret": "Registratie van gebruikers kan alleen via Secret URL", - "error-you-are-last-owner": "Je bent de laatste eigenaar. Kies eerst een nieuwe voor je de kamer verlaat.", + "error-user-registration-disabled": "Gebruikersregistratie is uitgeschakeld", + "error-user-registration-secret": "Gebruikersregistratie is alleen via geheime URL toegestaan", + "error-you-are-last-owner": "Je bent de laatste eigenaar. Stel een nieuwe eigenaar in voordat je de kamer verlaat.", + "error-status-not-allowed": "Onzichtbare status is uitgeschakeld", "Actions": "Acties", "activity": "activiteit", "Activity": "Activiteit", - "Add_Reaction": "Voeg reactie toe", - "Add_Server": "Voeg server toe", - "Add_users": "Voeg gebruikers toe", + "Add_Reaction": "Reactie toevoegen", + "Add_Server": "Server toevoegen", + "Add_users": "Gebruikers toevoegen", "Admin_Panel": "Admin Paneel", - "Alert": "Alert", - "alert": "alert", - "alerts": "alerts", - "All_users_in_the_channel_can_write_new_messages": "Alle gebruikers in het kanaal kunnen nieuwe berichten sturen", + "Agent": "Agent", + "Alert": "Waarschuwing", + "alert": "waarschuwing", + "alerts": "waarschuwingen", + "All_users_in_the_channel_can_write_new_messages": "Alle gebruikers in het kanaal kunnen nieuwe berichten schrijven", + "All_users_in_the_team_can_write_new_messages": "Alle gebruikers in het team kunnen nieuwe berichten schrijven", + "A_meaningful_name_for_the_discussion_room": "Een betekenisvolle naam voor de discussieruimte", "All": "Alle", - "All_Messages": "Alle Berichten", - "Allow_Reactions": "Sta reacties toe", + "All_Messages": "Alle berichten", + "Allow_Reactions": "Reacties toestaan", "Alphabetical": "Alfabetisch", "and_more": "en meer", "and": "en", "announcement": "aankondiging", "Announcement": "Aankondiging", - "Apply_Your_Certificate": "Gebruik je certificaat", + "Apply_Your_Certificate": "Pas jouw certificaat toe", "ARCHIVE": "ARCHIVEER", "archive": "archiveer", "are_typing": "zijn aan het typen", "Are_you_sure_question_mark": "Weet je het zeker?", - "Are_you_sure_you_want_to_leave_the_room": "Weet je zeker dat je de kamer {{room}} wil verlaten?", + "Are_you_sure_you_want_to_leave_the_room": "Weet je zeker dat je de kamer {{room}} wilt verlaten?", "Audio": "Audio", - "Authenticating": "Authenticating", + "Authenticating": "Authenticatie", "Automatic": "Automatisch", - "Auto_Translate": "Auto-Vertalen", - "Avatar_changed_successfully": "Avatar succesvol aangepast!", - "Avatar_Url": "Avatar URL", - "Away": "Weg", + "Auto_Translate": "Automatisch vertalen", + "Avatar_changed_successfully": "Avatar succesvol gewijzigd!", + "Avatar_Url": "Avatar-URL", + "Away": "Afwezig", "Back": "Terug", "Black": "Zwart", "Block_user": "Blokkeer gebruiker", - "Broadcast_channel_Description": "Alleen toegestane gebruikers kunnen nieuwe berichten sturen, maar iedereen kan reageren", - "Broadcast_Channel": "Broadcast Kanaal", + "Browser": "Browser", + "Broadcast_channel_Description": "Alleen geautoriseerde gebruikers kunnen nieuwe berichten schrijven, maar de andere gebruikers zullen kunnen antwoorden", + "Broadcast_Channel": "Uitzendkanaal", "Busy": "Bezig", "By_proceeding_you_are_agreeing": "Door verder te gaan ga je akkoord met onze", - "Cancel_editing": "Stop bewerken", - "Cancel_recording": "Stop opnemen", - "Cancel": "Stop", + "Cancel_editing": "Bewerken annuleren", + "Cancel_recording": "Opname annuleren", + "Cancel": "Annuleren", "changing_avatar": "avatar aan het veranderen", "creating_channel": "kanaal aan het maken", "creating_invite": "uitnodiging maken", - "Channel_Name": "Kanaal Name", + "Channel_Name": "Kanaal naam", "Channels": "Kanalen", + "Chats": "Chats", "Call_already_ended": "Gesprek al beeïndigd!", - "Click_to_join": "Klik om lid te worden!", + "Clear_cookies_alert": "Wilt u alle cookies wissen?", + "Clear_cookies_desc": "Met deze actie worden alle inlogcookies gewist, zodat u op andere accounts kunt inloggen.", + "Clear_cookies_yes": "Ja, cookies wissen", + "Clear_cookies_no": "Nee, bewaar cookies", + "Click_to_join": "Klik om mee te doen!", "Close": "Sluiten", - "Close_emoji_selector": "Sluit emoji selector", + "Close_emoji_selector": "Emoji-kiezer sluiten", + "Closing_chat": "Chat sluiten", + "Change_language_loading": "Taal veranderen", + "Chat_closed_by_agent": "Chat gesloten door agent", "Choose": "Kies", "Choose_from_library": "Kies uit bibliotheek", "Choose_file": "Kies bestand", + "Choose_where_you_want_links_be_opened": "Kies waar je links wilt openen", "Code": "Code", + "Code_or_password_invalid": "Code of wachtwoord ongeldig", "Collaborative": "Samenwerkend", "Confirm": "Bevestig", - "Connect": "Verbind", + "Connect": "Verbinden", "Connected": "Verbonden", - "connecting_server": "Verbonden met een server", + "connecting_server": "verbonden met de server", "Connecting": "Aan het verbinden...", - "Contact_us": "Contact opnemen", - "Contact_your_server_admin": "Neem contact op met je server admin.", + "Contact_us": "Neem contact op", + "Contact_your_server_admin": "Neem contact op met je serverbeheerder.", "Continue_with": "Ga verder met", - "Copied_to_clipboard": "Gekopïeerd naar klembord!", - "Copy": "Kopïeer", + "Copied_to_clipboard": "Gekopieerd naar klembord!", + "Copy": "Kopiëren", + "Conversation": "Conversatie", "Permalink": "Permalink", - "Certificate_password": "Certificate Password", - "Whats_the_password_for_your_certificate": "Wat is het wachtwoord voor je certificate?", - "Create_account": "Maak een account", - "Create_Channel": "Maak een kanaal", - "Created_snippet": "snippet gemaakt", - "Create_a_new_workspace": "Een nieuwe workspace maken", - "Create": "Maken", + "Certificate_password": "Certificaat wachtwoord", + "Clear_cache": "Lokale server cache wissen", + "Clear_cache_loading": "Cache wissen.", + "Whats_the_password_for_your_certificate": "Wat is het wachtwoord voor jouw certificaat?", + "Create_account": "Account aanmaken", + "Create_Channel": "Kanaal aanmaken", + "Create_Direct_Messages": "Directe berichten aanmaken", + "Create_Discussion": "Discussie aanmaken", + "Created_snippet": "knipsel aangemaakt", + "Create_a_new_workspace": "Een nieuwe werkruimte aanmaken", + "Create": "Aanmaken", + "Custom_Status": "Aangepaste status", "Dark": "Donker", "Dark_level": "Donker niveau", "Default": "Standaard", - "Delete_Room_Warning": "Een kamer verwijderen verwijdert alle berichten erin. Dit kan niet ongedaan gemaakt worden.", - "delete": "delete", - "Delete": "Delete", - "DELETE": "DELETE", - "deleting_room": "kamer legen", - "description": "beschrijving", - "Description": "Beschrijving", - "Desktop_Options": "Desktop Opties", + "Default_browser": "Standaard browser", + "Delete_Room_Warning": "Als je een kamer verwijdert, worden alle berichten die in de kamer geplaatst zijn, verwijderd. Dit kan niet ongedaan worden gemaakt.", + "Department": "Afdeling", + "delete": "verwijderen", + "Delete": "Verwijderen", + "DELETE": "VERWIJDEREN", + "move": "verplaatsen", + "deleting_room": "kamer verwijderen", + "description": "omschrijving", + "Description": "Omschrijving", + "Desktop_Options": "Bureaubladopties", + "Desktop_Notifications": "Desktopmeldingen", + "Desktop_Alert_info": "Deze meldingen worden op desktop geleverd", "Directory": "Map", "Direct_Messages": "Directe berichten", "Disable_notifications": "Zet notificaties uit", "Discussions": "Discussies", + "Discussion_Desc": "Help het overzicht te houden over wat er aan de hand is! Door een discussie aan te maken, wordt een subkanaal van het geselecteerde kanaal aangemaakt en worden beide gekoppeld.", + "Discussion_name": "Discussienaam", + "Done": "Gedaan", "Dont_Have_An_Account": "Heb je geen account?", - "Do_you_have_a_certificate": "Heb je een certificate?", + "Do_you_have_an_account": "Heb je een account?", + "Do_you_have_a_certificate": "Heb je een certificaat?", "Do_you_really_want_to_key_this_room_question_mark": "Wil je deze kamer echt {{key}}?", + "E2E_Encryption": "E2E-codering", + "E2E_How_It_Works_info1": "U kunt nu versleutelde privégroepen en directe berichten aanmaken. U kunt ook bestaande privégroepen of DM's wijzigen in versleuteld.", + "E2E_How_It_Works_info2": "Dit is *end-to-end codering*, dus de sleutel om jouw berichten te coderen/decoderen en deze wordt niet op de server opgeslagen. Daarom *moet je dit wachtwoord op een veilige plaats opslaan* waar je later toegang hebt als je dat nodig hebt.", + "E2E_How_It_Works_info3": "Als je doorgaat, wordt er automatisch een E2E-wachtwoord gegenereerd.", + "E2E_How_It_Works_info4": "Je kan ook op elk moment een nieuw wachtwoord voor uw coderingssleutel instellen vanuit elke browser waarin u het bestaande E2E-wachtwoord hebt ingevoerd.", "edit": "bewerk", "edited": "bewerkt", "Edit": "Bewerk", + "Edit_Status": "Status bewerken", "Edit_Invite": "Bewerk uitnodiging", - "Email_or_password_field_is_empty": "Email of wachtwoord veld is leeg", + "End_to_end_encrypted_room": "End-to-end versleutelde kamer", + "end_to_end_encryption": "end-to-end encryptie", + "Email_Notification_Mode_All": "Elke vermelding/DM", + "Email_Notification_Mode_Disabled": "Uitgeschakeld", + "Email_or_password_field_is_empty": "E-mail of wachtwoordveld is leeg", "Email": "E-mail", "email": "e-mail", - "Enable_Auto_Translate": "Zet Auto-Translate aan", - "Enable_notifications": "Zet notifications aan", - "Everyone_can_access_this_channel": "Iedereen kan bij dit kanaal", - "Error_uploading": "Error tijdens uploaden", - "Expiration_Days": "Vervalt in (Dagen)", + "Empty_title": "Lege titel", + "Enable_Auto_Translate": "Automatisch vertalen inschakelen", + "Enable_notifications": "Notificaties aanzetten", + "Encrypted": "Versleuteld", + "Encrypted_message": "Versleuteld bericht", + "Enter_Your_E2E_Password": "Voer uw E2E-wachtwoord in", + "Enter_Your_Encryption_Password_desc1": "Hiermee krijg je toegang tot uw gecodeerde privégroepen en directe berichten.", + "Enter_Your_Encryption_Password_desc2": "Op elke plaats waar je de chat gebruikt, moet je het wachtwoord invoeren om berichten te coderen/decoderen.", + "Encryption_error_title": "Jouw coderingswachtwoord lijkt verkeerd", + "Encryption_error_desc": "Het was niet mogelijk om uw coderingssleutel te decoderen om te worden geïmporteerd.", + "Everyone_can_access_this_channel": "Iedereen heeft toegang tot dit kanaal", + "Everyone_can_access_this_team": "Iedereen heeft toegang tot dit team", + "Error_uploading": "Fout bij uploaden", + "Expiration_Days": "Vervaldatum (Dagen)", "Favorite": "Favoriet", "Favorites": "Favorieten", "Files": "Bestanden", "File_description": "Bestandsbeschrijving", "File_name": "Bestandsnaam", - "Finish_recording": "Beëindig opname", - "Following_thread": "Volg thread", - "For_your_security_you_must_enter_your_current_password_to_continue": "Voor je veiligheid moet je je wachtwoord invullen om door te gaan", - "Forgot_password_If_this_email_is_registered": "Als dit email adres bij ons bekend is, sturen we je instructies op om je wachtwoord te resetten. Als je geen email krijgt, probeer het dan nogmaals.", - "Forgot_password": "Wachtwoord vergeten", - "Forgot_Password": "Wachtwoord Vergeten", - "Full_table": "Klik om de hele tabel te zien", - "Generate_New_Link": "Genereer Nieuwe Link", - "Group_by_favorites": "Sorteer op favorieten", - "Group_by_type": "Sorteer op type", + "Finish_recording": "Opname beëindigen", + "Following_thread": "Volg discussie", + "For_your_security_you_must_enter_your_current_password_to_continue": "Voor je veiligheid moet je je huidige wachtwoord invoeren om door te gaan", + "Forgot_password_If_this_email_is_registered": "Als dit e-mailadres geregistreerd is, sturen we instructies om je wachtwoord opnieuw in te stellen. Als je geen e-mail ontvangt, kom dan terug en probeer het opnieuw.", + "Forgot_password": "Wachtwoord vergeten?", + "Forgot_Password": "Wachtwoord vergeten", + "Forward": "Doorsturen", + "Forward_Chat": "Chat doorsturen", + "Forward_to_department": "Doorsturen naar afdeling", + "Forward_to_user": "Doorsturen naar gebruiker", + "Full_table": "Klik om de volledige tabel te zien", + "Generate_New_Link": "Nieuwe link genereren", + "Group_by_favorites": "Groepeer favorieten", + "Group_by_type": "Groeperen op type", "Hide": "Verberg", "Has_joined_the_channel": "is bij het kanaal gekomen", - "Has_joined_the_conversation": "neemt deel aan het gesprek", + "Has_joined_the_conversation": "heeft zich bij het gesprek aangesloten", "Has_left_the_channel": "heeft het kanaal verlaten", - "In_App_And_Desktop": "In-app en Desktop", - "In_App_and_Desktop_Alert_info": "Laat een banner bovenaan het scherm zien als de app open is en geeft een notificatie op de desktop", + "Hide_System_Messages": "Verberg systeemberichten", + "Hide_type_messages": "Verberg \"{{type}}\" berichten", + "How_It_Works": "Hoe het werkt", + "Message_HideType_uj": "Gebruiker neemt deel", + "Message_HideType_ul": "Gebruiker vertrokken", + "Message_HideType_ru": "Gebruiker verwijderd", + "Message_HideType_au": "Gebruiker toegevoegd", + "Message_HideType_mute_unmute": "Gebruiker gedempt / kan weer praten", + "Message_HideType_r": "Kamernaam gewijzigd", + "Message_HideType_ut": "Gebruiker neemt deel aan gesprek", + "Message_HideType_wm": "Welkom", + "Message_HideType_rm": "Bericht verwijderd", + "Message_HideType_subscription_role_added": "Kreeg rol", + "Message_HideType_subscription_role_removed": "Rol niet langer gedefinieerd", + "Message_HideType_room_archived": "Kamer gearchiveerd", + "Message_HideType_room_unarchived": "Kamer niet gearchiveerd", + "I_Saved_My_E2E_Password": "Ik heb mijn E2E-wachtwoord opgeslagen", + "IP": "IP", + "In_app": "In-app", + "In_App_And_Desktop": "In-app en desktop", + "In_App_and_Desktop_Alert_info": "Geeft een banner boven aan het scherm weer wanneer de app geopend is, en geeft een melding op desktop weer", "Invisible": "Onzichtbaar", "Invite": "Nodig uit", "is_a_valid_RocketChat_instance": "is een geldige Rocket.Chat instantie", "is_not_a_valid_RocketChat_instance": "is geen geldige Rocket.Chat instantie", "is_typing": "is aan het typen", - "Invalid_or_expired_invite_token": "Ongeldig of verlopen uitnodigingstoken", - "Invalid_server_version": "De server die je probeert te bereiken gebruikt een versie die niet meer door de app ondersteunt wordt: {{currentVersion}}.\n\nMinimale versienummer {{minVersion}}", + "Invalid_or_expired_invite_token": "Ongeldige of verlopen uitnodigingstoken", + "Invalid_server_version": "De server waarmee je probeert te verbinden, gebruikt een versie die niet meer door de app wordt ondersteund: {{currentVersion}}.\n\nWe hebben versie {{minVersion}} nodig", "Invite_Link": "Uitnodigingslink", - "Invite_users": "Nodig gebruikers uit", - "Join": "Word lid", - "Just_invited_people_can_access_this_channel": "Alleen genodigden kunnen bij dit kanaal", + "Invite_users": "Gebruikers uitnodigen", + "Join": "Doe mee", + "Join_Code": "Deelnamecode", + "Insert_Join_Code": "Deelnamecode invoegen", + "Join_our_open_workspace": "Word lid van onze open werkruimte", + "Join_your_workspace": "Word lid van jouw werkruimte", + "Just_invited_people_can_access_this_channel": "Alleen uitgenodigde mensen hebben toegang tot dit kanaal", + "Just_invited_people_can_access_this_team": "Alleen uitgenodigde mensen hebben toegang tot dit team", "Language": "Taal", "last_message": "laatste bericht", - "Leave_channel": "Verlaat kanaal", + "Leave_channel": "Kanaal verlaten", "leaving_room": "ruimte verlaten", + "Leave": "Verlaten", "leave": "verlaten", "Legal": "Legaal", - "Light": "Light", - "License": "License", + "Light": "Licht", + "License": "Licentie", "Livechat": "Livechat", - "Login": "Login", - "Login_error": "Je inloggegevens zijn fout! Probeer het opnieuw.", - "Login_with": "Login met", - "Logout": "Logout", - "Max_number_of_uses": "Maximaal aantal gebruiksmogelijkheden ", + "Livechat_edit": "Livechat bewerken", + "Login": "Inloggen", + "Login_error": "Je inloggegevens zijn geweigerd! Probeer het opnieuw.", + "Login_with": "Inloggen met", + "Logging_out": "Uitloggen.", + "Logout": "Uitloggen", + "Max_number_of_uses": "Max aantal toepassingen", + "Max_number_of_users_allowed_is_number": "Max aantal toegestane gebruikers is {{maxUsers}}", "members": "leden", "Members": "Leden", - "Mentioned_Messages": "Vermelde Berichten", + "Mentioned_Messages": "Vermelde berichten", "mentioned": "vermeld", "Mentions": "Vermeldingen", "Message_accessibility": "Bericht van {{user}} om {{time}}: {{message}}", "Message_actions": "Berichtacties", "Message_pinned": "Bericht vastgezet", "Message_removed": "Bericht verwijderd", + "Message_starred": "Bericht met ster", + "Message_unstarred": "Bericht zonder ster", "message": "bericht", "messages": "berichten", + "Message": "Bericht", "Messages": "Berichten", "Message_Reported": "Bericht gerapporteerd", - "Microphone_Permission_Message": "Rocket.Chat heeft toegang tot je microfoon nodig voor geluidsberichten.", - "Microphone_Permission": "Microfoon toestemming", + "Microphone_Permission_Message": "Rocket.Chat heeft toegang tot je microfoon nodig zodat je een audiobericht kunt verzenden.", + "Microphone_Permission": "Microfoontoestemming", "Mute": "Dempen", "muted": "gedempt", "My_servers": "Mijn servers", - "N_people_reacted": "{{n}} mensen reageerden", + "N_people_reacted": "{{n}} mensen hebben gereageerd", "N_users": "{{n}} gebruikers", + "N_channels": "{{n}} kanalen", "name": "naam", "Name": "Naam", + "Navigation_history": "Navigatie geschiedenis", "Never": "Nooit", - "New_Message": "Nieuw Bericht", - "New_Password": "Nieuw Wachtwoord", - "New_Server": "Nieuwe Server", + "New_Message": "Nieuw bericht", + "New_Password": "Nieuw wachtwoord", + "New_Server": "Nieuwe server", "Next": "Volgende", "No_files": "Geen bestanden", "No_limit": "Geen limiet", @@ -259,160 +348,207 @@ "No_pinned_messages": "Geen vastgezette berichten", "No_results_found": "Geen resultaten gevonden", "No_starred_messages": "Geen berichten met ster gemarkeerd", - "No_thread_messages": "Geen thread berichten", + "No_thread_messages": "Geen discussieberichten", + "No_label_provided": "Geen {{label}} opgegeven.", "No_Message": "Geen bericht", "No_messages_yet": "Nog geen berichten", "No_Reactions": "Geen reacties", - "No_Read_Receipts": "Geen leesbevestiging", - "Not_logged": "Niet gelogged", - "Not_RC_Server": "Dit is geen Rocket.Chat server.\n{{contact}}", + "No_Read_Receipts": "Geen leesbevestigingen", + "Not_logged": "Niet ingelogd", + "Not_RC_Server": "Dit is geen Rocket.Chat-server.\n{{contact}}", "Nothing": "Niets", "Nothing_to_save": "Niets om op te slaan!", - "Notify_active_in_this_room": "Bericht de actieve gebruikers in deze kamer", - "Notify_all_in_this_room": "Bericht iedereen in deze kamer", + "Notify_active_in_this_room": "Waarschuw actieve gebruikers in deze kamer", + "Notify_all_in_this_room": "Breng iedereen in deze kamer op de hoogte", "Notifications": "Notificaties", - "Notification_Duration": "Notificatie Duur", + "Notification_Duration": "Duur van de notificatie", "Notification_Preferences": "Notificatievoorkeuren", + "No_available_agents_to_transfer": "Geen beschikbare agenten om door te sturen", "Offline": "Offline", "Oops": "Oeps!", + "Omnichannel": "Omnichannel", + "Open_Livechats": "Bezig met chatten", + "Omnichannel_enable_alert": "Je bent niet beschikbaar op Omnichannel. Wil je beschikbaar zijn?", + "Onboarding_description": "Een werkruimte is de ruimte van jouw team of organisatie om samen te werken. Vraag aan de beheerder van de werkruimte een adres om lid te worden of maak er een aan voor jouw team.", + "Onboarding_join_workspace": "Word lid van een werkruimte", + "Onboarding_subtitle": "Meer dan teamsamenwerking", "Onboarding_title": "Welkom bij Rocket.Chat", + "Onboarding_join_open_description": "Word lid van onze open werkruimte om met het Rocket.Chat team en de community te chatten.", + "Onboarding_agree_terms": "Door verder te gaan, ga je akkoord met Rocket.Chat", + "Onboarding_less_options": "Minder opties", + "Onboarding_more_options": "Meer opties", "Online": "Online", - "Only_authorized_users_can_write_new_messages": "Alleen gebruikers met toestemming mogen nieuwe berichten maken", - "Open_emoji_selector": "Open de emoji selector", - "Open_Source_Communication": "Open de Source Communication", + "Only_authorized_users_can_write_new_messages": "Alleen geautoriseerde gebruikers kunnen nieuwe berichten schrijven", + "Open_emoji_selector": "Emoji-kiezer openen", + "Open_Source_Communication": "Open Source Communicatie", + "Open_your_authentication_app_and_enter_the_code": "Open je authenticatie-app en voer de code in.", + "OR": "OF", + "OS": "OS", + "Overwrites_the_server_configuration_and_use_room_config": "Overschrijft de serverconfiguratie en gebruikt kamer config", "Password": "Wachtwoord", + "Parent_channel_or_group": "Bovenliggend kanaal of groep", "Permalink_copied_to_clipboard": "Permalink gekopiëerd naar klembord!", + "Phone": "Telefoon", "Pin": "Vastzetten", "Pinned_Messages": "Vastgezette berichten", "pinned": "vastgezet", "Pinned": "Vastgezet", - "Please_enter_your_password": "Vul je wachtwoord in", + "Please_add_a_comment": "Voeg een reactie toe", + "Please_enter_your_password": "Voer je wachtwoord in", + "Please_wait": "Even geduld.", "Preferences": "Voorkeuren", "Preferences_saved": "Voorkeuren opgeslagen!", - "Privacy_Policy": " Privacy Policy", - "Private_Channel": "Prive Kanaal", - "Private_Groups": "Prive Groepen", - "Private": "Prive", - "Processing": "Verwerken...", + "Privacy_Policy": " Privacybeleid", + "Private_Channel": "Privékanaal", + "Private_Groups": "Privé groepen", + "Private": "Privé", + "Processing": "Verwerking...", "Profile_saved_successfully": "Profiel succesvol opgeslagen!", "Profile": "Profiel", "Public_Channel": "Publiek kanaal", "Public": "Publiek", - "Push_Notifications": "Pushnotificaties", - "Push_Notifications_Alert_Info": "Deze notificaties krijg je als de app niet geopend is", - "Quote": "Quote", + "Push_Notifications": "Pushmeldingen", + "Push_Notifications_Alert_Info": "Deze notificaties worden aan jouw enkel geleverd als de app niet geopend is", + "Quote": "Citaat", "Reactions_are_disabled": "Reacties zijn uitgeschakeld", "Reactions_are_enabled": "Reacties zijn ingeschakeld", "Reactions": "Reacties", "Read": "Lezen", - "Read_Only_Channel": "Alleen-lezen Kanaal", - "Read_Only": "Alleen Lezen", + "Read_External_Permission_Message": "Rocket.Chat heeft toegang nodig tot foto's, media en bestanden op je apparaat", + "Read_External_Permission": "Lees toestemming voor media", + "Read_Only_Channel": "Alleen-lezen kanaal", + "Read_Only": "Alleen lezen", "Read_Receipt": "Leesbevestiging", - "Receive_Group_Mentions": "Ontvang Groepsvermeldingen", + "Receive_Group_Mentions": "Groepsvermeldingen ontvangen", "Receive_Group_Mentions_Info": "Ontvang @all en @here vermeldingen", - "Register": "Aanmelden", - "Repeat_Password": "Wachtwoord herhalen", - "Replied_on": "Gereageerd op:", - "replies": "reacties", - "reply": "reactie", - "Reply": "Reacties", + "Register": "Registreren", + "Repeat_Password": "Herhaal wachtwoord", + "Replied_on": "Beantwoord op:", + "replies": "antwoordt", + "reply": "antwoord", + "Reply": "Antwoord", "Report": "Rapporteren", - "Receive_Notification": "Ontvang notificatie", + "Receive_Notification": "Notificatie ontvangen", "Receive_notifications_from": "Ontvang notificaties van {{name}}", "Resend": "Opnieuw verzenden", - "Reset_password": "Wachtwoord reset", + "Reset_password": "Wachtwoord resetten", "resetting_password": "wachtwoord aan het resetten", "RESET": "RESET", - "Review_app_title": "Vind je dit een TOP app?", + "Return": "Terug", + "Review_app_title": "Geniet je van deze app?", "Review_app_desc": "Geef ons 5 sterren op {{store}}", - "Review_app_yes": "Doe ik!", + "Review_app_yes": "Zeker!", "Review_app_no": "Nee", "Review_app_later": "Misschien later", - "Review_app_unable_store": "Kon {{store}} niet openen", - "Review_this_app": "Review deze app", + "Review_app_unable_store": "Kan {{store}} niet openen", + "Review_this_app": "Beoordeel deze app", + "Remove": "Verwijderen", + "remove": "verwijderen", "Roles": "Rollen", - "Room_actions": "Kamer acties", - "Room_changed_announcement": "Kamer announcement veranderd naar: {{announcement}} door {{userBy}}", - "Room_changed_description": "Kamer beschrijving veranderd naar: {{description}} door {{userBy}}", - "Room_changed_privacy": "Kamer type veranderd naar: {{type}} door {{userBy}}", - "Room_changed_topic": "Kamer onderwerp veranderd naar: {{topic}} door {{userBy}}", - "Room_Files": "Kamer Bestanden", - "Room_Info_Edit": "Kamer Info Aanpassen", - "Room_Info": "Kamer Info", - "Room_Members": "Kamer Leden", - "Room_name_changed": "Kamer naam veranderd naar: {{name}} door {{userBy}}", + "Room_actions": "Kameracties", + "Room_changed_announcement": "Kameraankondiging gewijzigd in: {{announcement}} door {{userBy}}", + "Room_changed_avatar": "Kameravatar gewijzigd door {{userBy}}", + "Room_changed_description": "Kamerbeschrijving gewijzigd in: {{description}} door {{userBy}}", + "Room_changed_privacy": "Kamertype gewijzigd in: {{type}} door {{userBy}}", + "Room_changed_topic": "Oonderwerp van kamer gewijzigd in: {{topic}} door {{userBy}}", + "Room_Files": "Kamerbestanden", + "Room_Info_Edit": "Kamer info bewerken", + "Room_Info": "Kamer info", + "Room_Members": "Kamerleden", + "Room_name_changed": "Kamernaam gewijzigd in: {{name}} door {{userBy}}", "SAVE": "OPSLAAN", - "Save_Changes": "Sla wijzigingen op", + "Save_Changes": "Wijzigingen opslaan", "Save": "Opslaan", + "Saved": "Opgeslagen", "saving_preferences": "voorkeuren opslaan", "saving_profile": "profiel opslaan", "saving_settings": "instellingen opslaan", - "saved_to_gallery": "Aan galerij toegevoegd", - "Search_Messages": "Zoek Berichten", - "Search": "Zoek", - "Search_by": "Zoek op", - "Search_global_users": "Zoek voor algemene gebruikers", - "Search_global_users_description": "Als je dit aan zet, kan je gebruikers van andere bedrijven en servers zoeken.", + "saved_to_gallery": "Opgeslagen in galerij", + "Save_Your_E2E_Password": "Bewaar jouw E2E-wachtwoord", + "Save_Your_Encryption_Password": "Bewaar jouw versleutelingswachtwoord", + "Save_Your_Encryption_Password_warning": "Dit wachtwoord wordt nergens opgeslagen, bewaar het dus zorgvuldig ergens anders.", + "Save_Your_Encryption_Password_info": "Indien je je wachtwoord verliest, is er geen enkel manier om het te herstellen en verlies je toegang tot je berichten.", + "Search_Messages": "Berichten zoeken", + "Search": "Zoeken", + "Search_by": "Zoeken op", + "Search_global_users": "Zoeken naar wereldwijde gebruikers", + "Search_global_users_description": "Als je dit inschakelt, kan je gebruikers van andere bedrijven en servers opzoeken.", "Seconds": "{{second}} seconden", - "Select_Avatar": "Kies Avatar", - "Select_Server": "Kies Server", - "Select_Users": "Kies Gebruikers", - "Send": "Verstuur", - "Send_audio_message": "Verstuur geluidsbericht", - "Send_crash_report": "Verstuur crash report", - "Send_message": "Verstuur bericht", - "Send_to": "Verstuur naar...", - "Sent_an_attachment": "Verstuur een bijlage", + "Security_and_privacy": "Veiligheid en privacy", + "Select_Avatar": "Selecteer avatar", + "Select_Server": "Selecteer server", + "Select_Users": "Selecteer gebruikers", + "Select_a_Channel": "Selecteer een kanaal", + "Select_a_Department": "Selecteer een afdeling", + "Select_an_option": "Selecteer een optie", + "Select_a_User": "Selecteer een gebruiker", + "Send": "Verzenden", + "Send_audio_message": "Audiobericht verzenden", + "Send_crash_report": "Crashrapport verzenden", + "Send_message": "Bericht verzenden", + "Send_me_the_code_again": "Stuur me de code opnieuw", + "Send_to": "Verzenden naar...", + "Sending_to": "Verzenden naar", + "Sent_an_attachment": "Een bijlage verzonden", "Server": "Server", "Servers": "Servers", "Server_version": "Server versie: {{version}}", - "Set_username_subtitle": "De gebruikersnaam wordt gebruikt om anderen jou te vermelden in berichten", + "Set_username_subtitle": "De gebruikersnaam wordt gebruikt om anderen toe te staan jou in berichten te vermelden", + "Set_custom_status": "Aangepaste status instellen", + "Set_status": "Status instellen", + "Status_saved_successfully": "Status succesvol opgeslagen!", "Settings": "Instellingen", - "Settings_succesfully_changed": "Instellingen succesvol veranderd!", + "Settings_succesfully_changed": "Instellingen succesvol gewijzigd!", "Share": "Delen", - "Share_Link": "Deel Link", + "Share_Link": "Deel link", "Share_this_app": "Deel deze app", - "Show_Unread_Counter": "Laat Ongelezen Teller Zien", - "Show_Unread_Counter_Info": "De Ongelezen Tller is een badge aan de rechterkant van het kanaal in de lijst", - "Sign_in_your_server": "Log in bij je server", - "Sign_Up": "Inschrijven", - "Some_field_is_invalid_or_empty": "Een veld is ongeldig of leeg", + "Show_more": "Meer tonen..", + "Show_Unread_Counter": "Toon ongelezen teller", + "Show_Unread_Counter_Info": "Ongelezen teller wordt weergegeven als een badge aan de rechterkant van het kanaal, in de lijst", + "Sign_in_your_server": "Log in op je server", + "Sign_Up": "Registreren", + "Some_field_is_invalid_or_empty": "Sommige velden zijn ongeldig of leeg", "Sorting_by": "Sorteren op {{key}}", "Sound": "Geluid", - "Star_room": "Sterrenkamer", + "Star_room": "Favoriete kanalen", "Star": "Ster", "Starred_Messages": "Berichten met ster gemarkeerd", "starred": "met ster gemarkeerd", "Starred": "Met ster gemarkeerd", "Start_of_conversation": "Begin van een gesprek", + "Start_a_Discussion": "Start een discussie", "Started_discussion": "Begin van een discussie:", - "Started_call": "Gesprek gestart door {{userBy}}", - "Submit": "Verstuur", + "Started_call": "Oproep gestart door {{userBy}}", + "Submit": "Verzenden", "Table": "Tabel", + "Tags": "Tags", "Take_a_photo": "Neem een foto", - "Take_a_video": "Neem een video", - "tap_to_change_status": "tik om je status te veranderen", - "Tap_to_view_servers_list": "Tik om een server lijst te weergeven", + "Take_a_video": "Maak een video", + "Take_it": "Pak het!", + "tap_to_change_status": "tik om de status te wijzigen", + "Tap_to_view_servers_list": "Tik om de serverlijst te bekijken", "Terms_of_Service": " Servicevoorwaarden ", "Theme": "Thema", - "There_was_an_error_while_action": "Er was eer fout tijdens {{action}}!", + "The_user_wont_be_able_to_type_in_roomName": "De gebruiker zal in {{roomName}} niet kunnen typen", + "The_user_will_be_able_to_type_in_roomName": "De gebruiker zal in {{roomName}} kunnen typen", + "There_was_an_error_while_action": "Er is een fout opgetreden tijdens {{action}}!", "This_room_is_blocked": "Deze kamer is geblokkeerd", "This_room_is_read_only": "Deze kamer is alleen-lezen", - "Thread": "Thread", - "Threads": "Threads", + "Thread": "Draad", + "Threads": "Draden", "Timezone": "Tijdzone", "To": "Naar", "topic": "onderwerp", "Topic": "Onderwerp", "Translate": "Vertalen", - "Try_again": "Probeer opnieuw", - "Two_Factor_Authentication": "Tweee-factor Authenticatie", + "Try_again": "Probeer het opnieuw", + "Two_Factor_Authentication": "Twee-factor authenticatie", "Type_the_channel_name_here": "Typ hier de kanaalnaam", "unarchive": "dearchiveren", "UNARCHIVE": "DEARCHIVEREN", - "Unblock_user": "Gebruiker deblokkeren", + "Unblock_user": "Deblokkeer gebruiker", "Unfavorite": "Uit favorieten halen", - "Unfollowed_thread": "Thread ontvolgd", + "Unfollowed_thread": "Draad ontvolgd", "Unmute": "Dempen opheffen", "unmuted": "ongedempt", "Unpin": "Losmaken", @@ -423,60 +559,207 @@ "Updating": "Updaten...", "Uploading": "Uploaden", "Upload_file_question_mark": "Bestand uploaden?", + "User": "Gebruiker", "Users": "Gebruikers", "User_added_by": "Gebruiker {{userAdded}} toegevoegd door {{userBy}}", - "User_Info": "Gebruiker Info", + "User_Info": "Gebruikers info", "User_has_been_key": "Gebruiker is {{key}}", - "User_is_no_longer_role_by_": "{{user}} is geen {{role}} meer door {{userBy}}", + "User_is_no_longer_role_by_": "{{user}} is niet langer {{role}} door {{userBy}}", "User_muted_by": "Gebruiker {{userMuted}} gedempt door {{userBy}}", "User_removed_by": "Gebruiker {{userRemoved}} verwijderd door {{userBy}}", "User_sent_an_attachment": "{{user}} stuurde een bijlage", - "User_unmuted_by": "Dempen opgeheven voor {{userUnmuted}} door {{userBy}}", - "User_was_set_role_by_": "{{user}} is nu {{role}} door {{userBy}}", + "User_unmuted_by": "Dempen voor {{userUnmuted}} opgeheven door {{userBy}}", + "User_was_set_role_by_": "{{user}} is als {{role}} ingesteld door {{userBy}}", "Username_is_empty": "Gebruikersnaam is leeg", "Username": "Gebruikersnaam", - "Username_or_email": "Gebruikersnaam of email", - "Validating": "Aan het valideren", + "Username_or_email": "Gebruikersnaam of e-mail", + "Uses_server_configuration": "Gebruikt serverconfiguratie", + "Validating": "Valideren", + "Registration_Succeeded": "Registratie geslaagd!", + "Verify": "Verifiëren", + "Verify_email_title": "Registratie geslaagd!", + "Verify_email_desc": "We hebben je een e-mail gestuurd om je inschrijving te bevestigen. Als je binnenkort geen e-mail ontvangt, gelieve terug te komen en het opnieuw te proberen.", + "Verify_your_email_for_the_code_we_sent": "Verifieer je e-mail voor de code die we hebben gestuurd", "Video_call": "Videogesprek", "View_Original": "Bekijk origineel", - "Voice_call": "Audiogesprek", - "Websocket_disabled": "Websocket staat uit voor deze server.\n{{contact}}", + "Voice_call": "Spraakoproep", + "Waiting_for_network": "Wachten op netwerk...", + "Websocket_disabled": "Websocket is uitgeschakeld voor deze server.\n{{contact}}", "Welcome": "Welkom", + "What_are_you_doing_right_now": "Wat doe je op dit moment?", "Whats_your_2fa": "Wat is je 2FA code?", - "Without_Servers": "Zonder Servers", - "Write_External_Permission_Message": "Rocket.Chat moet bij je galerij kunnen om afbeeldingen op te slaan.", - "Write_External_Permission": "Galerij Toestemming", + "Without_Servers": "Zonder servers", + "Workspaces": "Werkruimten", + "Would_you_like_to_return_the_inquiry": "Wil je de aanvraag retourneren?", + "Write_External_Permission_Message": "Rocket.Chat heeft toegang nodig tot je galerij zodat je afbeeldingen kunt opslaan.", + "Write_External_Permission": "Galerij toestemming", + "Yes": "Ja", "Yes_action_it": "Ja, {{action}} het!", "Yesterday": "Gisteren", - "You_are_in_preview_mode": "Je bent in preview mode", + "You_are_in_preview_mode": "Je bent in voorbeeldmodus", "You_are_offline": "Je bent offline", - "You_can_search_using_RegExp_eg": "Je kan RegExp. gebruiken, bijv. `/^text$/i`", + "You_can_search_using_RegExp_eg": "Je kan RegExp. gebruiken, bijv. `/^tekst$/i`", "You_colon": "Jij: ", "you_were_mentioned": "je bent vermeld", + "You_were_removed_from_channel": "Je bent verwijderd uit {{channel}}", "you": "jij", "You": "Jij", - "You_need_to_access_at_least_one_RocketChat_server_to_share_something": "Je moet minimaal toegang hebben tot 1 Rocket.Chat server om iets te delen.", - "Your_certificate": "Jouw Certificaat", - "Your_invite_link_will_expire_after__usesLeft__uses": "Je uitnodigingslink wordt ongeldig over {{usesLeft}} keer.", - "Your_invite_link_will_expire_on__date__or_after__usesLeft__uses": "Je uitnodigingslink wordt ongeldig op {{date}} of na{{usesLeft}} keer.", - "Your_invite_link_will_expire_on__date__": "Je uitnodigingslink wordt ongeldig op {{date}}.", - "Your_invite_link_will_never_expire": "Je uitnodigingslink wordt nooit ongeldig.", + "Logged_out_by_server": "Je bent uitgelogd door de server. Gelieve opnieuw in te loggen.", + "You_need_to_access_at_least_one_RocketChat_server_to_share_something": "Je moet minstens toegang hebben tot één Rocket.Chat-server om iets te delen.", + "You_need_to_verifiy_your_email_address_to_get_notications": "Je moet je e-mailadres verifiëren om meldingen te ontvangen", + "Your_certificate": "Jouw certificaat", + "Your_invite_link_will_expire_after__usesLeft__uses": "Je uitnodigingslink verloopt na {{usesLeft}} keer.", + "Your_invite_link_will_expire_on__date__or_after__usesLeft__uses": "Je uitnodigingslink verloopt op {{date}} of na {{usesLeft}} keer.", + "Your_invite_link_will_expire_on__date__": "Je uitnodigingslink verloopt op {{date}}.", + "Your_invite_link_will_never_expire": "Je uitnodigingslink zal nooit verlopen.", + "Your_workspace": "Jouw werkruimte", + "Your_password_is": "Jouw wachtwoord is", "Version_no": "Versie: {{version}}", - "You_will_not_be_able_to_recover_this_message": "Je kan dit bericht niet meer terugkrijgen!", - "Change_Language": "Verander taal", - "Crash_report_disclaimer": "We kijken nooit naar de content van je chats. Het crashrapport bevat alleen relevante informatie voor ons om problemen te isoleren en op te lossen.", - "Type_message": "Type bericht", + "You_will_not_be_able_to_recover_this_message": "Je zal dit bericht niet meer kunnen herstellen!", + "You_will_unset_a_certificate_for_this_server": "Je zal een certificaat voor deze server uitschakelen", + "Change_Language": "Taal veranderen", + "Crash_report_disclaimer": "We volgen nooit nooit de inhoud van je chats. Het crashrapport en de analytische gebeurtenissen bevatten alleen relevante informatie voor ons om problemen te identificeren en op te lossen.", + "Type_message": "Typ bericht", "Room_search": "Kamers zoeken", - "Room_selection": "Kamerselectie 1...9", + "Room_selection": "Kamerkeuze 1...9", "Next_room": "Volgende kamer", "Previous_room": "Vorige kamer", - "New_room": "Nieuwe Kamer", - "Upload_room": "Upload naar kamer", - "Search_messages": "Doorzoek messages", - "Scroll_messages": "Scroll door messages", - "Reply_latest": "Beantwoord de laatste", + "New_room": "Nieuwe kamer", + "Upload_room": "Uploaden naar kamer", + "Search_messages": "Berichten zoeken", + "Scroll_messages": "Berichten scrollen", + "Reply_latest": "Antwoord op laatste", + "Reply_in_Thread": "Reageer in discussie", "Server_selection": "Server selectie", "Server_selection_numbers": "Server selectie 1...9", - "Add_server": "Voeg Server Toe", - "New_line": "Nieuwe Regel" + "Add_server": "Server toevoegen", + "New_line": "Nieuwe lijn", + "You_will_be_logged_out_of_this_application": "Je wordt uitgelogd van deze applicatie.", + "Clear": "Wissen", + "This_will_clear_all_your_offline_data": "Hiermee worden al jouw offline gegevens gewist.", + "This_will_remove_all_data_from_this_server": "Dit zal alle gegevens van deze server verwijderen.", + "Mark_unread": "Markeer als ongelezen", + "Wait_activation_warning": "Voordat u kunt inloggen, moet uw account handmatig worden geactiveerd door een beheerder.", + "Screen_lock": "Schermvergrendeling", + "Local_authentication_biometry_title": "Authenticeren", + "Local_authentication_biometry_fallback": "Gebruik toegangscode", + "Local_authentication_unlock_option": "Ontgrendelen met toegangscode", + "Local_authentication_change_passcode": "Wijzig toegangscode", + "Local_authentication_info": "Opmerking: als je de toegangscode vergeet, moet je de app verwijderen en opnieuw installeren.", + "Local_authentication_facial_recognition": "gezichtsherkenning", + "Local_authentication_fingerprint": "vingerafdruk", + "Local_authentication_unlock_with_label": "Ontgrendel met {{label}}", + "Local_authentication_auto_lock_60": "Na 1 minuut", + "Local_authentication_auto_lock_300": "Na 5 minuten", + "Local_authentication_auto_lock_900": "Na 15 minuten", + "Local_authentication_auto_lock_1800": "Na 30 minuten", + "Local_authentication_auto_lock_3600": "Na 1 uur", + "Passcode_enter_title": "Voer uw toegangscode in", + "Passcode_choose_title": "Kies je nieuwe toegangscode", + "Passcode_choose_confirm_title": "Bevestig je nieuwe toegangscode", + "Passcode_choose_error": "Toegangscodes komen niet overeen. Probeer het opnieuw.", + "Passcode_choose_force_set": "Toegangscode vereist door beheerder", + "Passcode_app_locked_title": "App vergrendeld", + "Passcode_app_locked_subtitle": "Probeer het over {{timeLeft}} seconden opnieuw", + "After_seconds_set_by_admin": "Na {{seconds}} seconden (ingesteld door beheerder)", + "Dont_activate": "Nu niet activeren", + "Queued_chats": "Chats in de wachtrij", + "Queue_is_empty": "Wachtrij is leeg", + "Logout_from_other_logged_in_locations": "Afmelden bij andere ingelogde locaties", + "You_will_be_logged_out_from_other_locations": "Je wordt uitgelogd van andere locaties.", + "Logged_out_of_other_clients_successfully": "Succesvol uitgelogd bij andere klanten", + "Logout_failed": "Uitloggen mislukt!", + "Log_analytics_events": "Analysegebeurtenissen loggen", + "E2E_encryption_change_password_title": "Versleutelingswachtwoord wijzigen", + "E2E_encryption_change_password_description": "Je kan nu versleutelde privégroepen en directe berichten aanmaken. Je kan ook bestaande privégroepen of DM's wijzigen in versleuteld.\nDit is end-to-end codering, dus de sleutel om jouw berichten te coderen/decoderen en deze wordt niet op de server opgeslagen. Daarom moet je dit wachtwoord op een veilige plaats opslaan. Je moet het invoeren op andere apparaten waarop je e2e-codering wilt gebruiken.", + "E2E_encryption_change_password_error": "Fout bij het wijzigen van het E2E-wachtwoord", + "E2E_encryption_change_password_success": "E2E-wachtwoord succesvol gewijzigd!", + "E2E_encryption_change_password_message": "Zorg ervoor dat je het zorgvuldig ergens anders hebt bewaard.", + "E2E_encryption_change_password_confirmation": "Ja, verander het", + "E2E_encryption_reset_title": "E2E-sleutel resetten", + "E2E_encryption_reset_description": "Deze optie zal je huidige E2E-sleutel verwijderen en je wordt uitgelogd.\nWanneer je opniew inlogt, genereert Rocket.Chat je een nieuwe sleutel en herstelt je toegang tot elke versleutelde kamer die een of meer leden heeft.\nDoor de aard van E2E-versleuteling kan Rocket.Chat de toegang tot een versleutelde kamer zonder online lid niet herstellen.", + "E2E_encryption_reset_button": "E2E-sleutel resetten", + "E2E_encryption_reset_error": "Fout bij het resetten van E2E-sleutel!", + "E2E_encryption_reset_message": "Je wordt uitgelogd.", + "E2E_encryption_reset_confirmation": "Ja, reset het", + "Following": "Volgend", + "Threads_displaying_all": "Alles weergeven", + "Threads_displaying_following": "Volgend weergeven", + "Threads_displaying_unread": "Ongelezen weergeven", + "No_threads": "Er zijn geen discussies", + "No_threads_following": "Je volgt geen discussies", + "No_threads_unread": "Er zijn geen ongelezen discussies", + "Messagebox_Send_to_channel": "Stuur naar kanaal", + "Leader": "Leider", + "Moderator": "Moderator", + "Owner": "Eigenaar", + "Remove_from_room": "Verwijderen uit kamer", + "Ignore": "Negeren", + "Unignore": "Niet meer negeren", + "User_has_been_ignored": "Gebruiker is genegeerd", + "User_has_been_unignored": "Gebruiker wordt niet langer genegeerd", + "User_has_been_removed_from_s": "Gebruiker is verwijderd van {{s}}", + "User__username__is_now_a_leader_of__room_name_": "Gebruiker {{username}} is nu een leider van {{room_name}}", + "User__username__is_now_a_moderator_of__room_name_": "Gebruiker {{username}} is nu een moderator van {{room_name}}", + "User__username__is_now_a_owner_of__room_name_": "Gebruiker {{username}} is nu eigenaar van {{room_name}}", + "User__username__removed_from__room_name__leaders": "Gebruiker {{username}} verwijderd uit {{room_name}} leiders", + "User__username__removed_from__room_name__moderators": "Gebruiker {{username}} verwijderd uit {{room_name}} moderators", + "User__username__removed_from__room_name__owners": "Gebruiker {{username}} verwijderd uit {{room_name}} eigenaars", + "The_user_will_be_removed_from_s": "De gebruiker wordt verwijderd uit {{s}}", + "Yes_remove_user": "Ja, verwijder gebruiker!", + "Direct_message": "Direct bericht", + "Message_Ignored": "Bericht genegeerd. Tik om het weer te geven.", + "Enter_workspace_URL": "Voer de werkruimte-URL in", + "Workspace_URL_Example": "Vb. uw-bedrijf.rocket.chat", + "This_room_encryption_has_been_enabled_by__username_": "De versleuteling van deze kamer is ingeschakeld door {{username}}", + "This_room_encryption_has_been_disabled_by__username_": "De versleuteling van deze kamer is uitgeschakeld door {{username}}", + "Teams": "Teams", + "No_team_channels_found": "Geen kanalen gevonden", + "Team_not_found": "Team niet gevonden", + "Create_Team": "Team aanmaken", + "Team_Name": "Teamnaam", + "Private_Team": "Privé team", + "Read_Only_Team": "Alleen-lezen team", + "Broadcast_Team": "Broadcast team", + "creating_team": "team maken", + "team-name-already-exists": "Er bestaat al een team met die naam", + "Add_Channel_to_Team": "Kanaal toevoegen aan team", + "Create_New": "Maak nieuw", + "Add_Existing": "Voeg bestaande", + "Add_Existing_Channel": "Bestaand kanaal toevoegen", + "Remove_from_Team": "Verwijderen uit team", + "Auto-join": "Automatisch deelnemen", + "Remove_Team_Room_Warning": "Wil je dit kanaal uit het team verwijderen? Het kanaal wordt terug naar de werkruimte verplaatst", + "Confirmation": "Bevestiging", + "invalid-room": "Ongeldige kamer", + "You_are_leaving_the_team": "Je verlaat het team '{{team}}'", + "Leave_Team": "Team verlaten", + "Select_Team": "Selecteer team", + "Select_Team_Channels": "Selecteer de kanalen van het team die je wilt verlaten.", + "Cannot_leave": "Kan niet weggaan", + "Cannot_remove": "Kan niet verwijderen", + "Cannot_delete": "Kan niet verwijderen", + "Last_owner_team_room": "Je bent de laatste eigenaar van dit kanaal. Zodra u het team verlaat, blijft het kanaal binnen het team, maar beheert u het van buitenaf.", + "last-owner-can-not-be-removed": "Laatste eigenaar kan niet worden verwijderd.", + "Remove_User_Teams": "Selecteer de kanalen waarvan je de gebruiker wilt verwijderen.", + "Delete_Team": "Team verwijderen", + "Select_channels_to_delete": "Dit kan niet ongedaan worden gemaakt. Zodra je een team verwijdert, worden alle chatinhoud en configuratie verwijderd.\n\nSelecteer de kanalen die je wilt verwijderen. Degene die je besluit te behouden, zullen in jouw werkruimte beschikbaar zijn. Hou er rekening mee dat openbare kanalen nog steeds openbaar en voor iedereen zichtbaar zijn.", + "You_are_deleting_the_team": "Je verwijdert dit team.", + "Removing_user_from_this_team": "Je verwijdert {{user}} uit dit team", + "Remove_User_Team_Channels": "Selecteer de kanalen waarvan je de gebruiker wilt verwijderen.", + "Remove_Member": "Lid verwijderen", + "leaving_team": "team verlaten", + "removing_team": "verwijderen uit team", + "moving_channel_to_team": "kanaal verplaatsen naar team", + "deleting_team": "team verwijderen", + "member-does-not-exist": "Lid bestaat niet", + "Convert": "Converteren", + "Convert_to_Team": "Converteren naar team", + "Convert_to_Team_Warning": "Dit kan niet ongedaan worden gemaakt. Eens je een kanaal naar een team hebt geconverteerd, kun je het niet meer naar een kanaal terugzetten.", + "Move_to_Team": "Verplaats naar team", + "Move_Channel_Paragraph": "Het verplaatsen van een kanaal binnen een team betekent dat dit kanaal wordt toegevoegd in de context van het team. Maar, alle leden van dit kanaal, die geen lid zijn van het respectieve team, zullen nog steeds toegang hebben tot dit kanaal, maar worden niet als teamleden toegevoegd.\n\nHet volledige beheer van dit kanaal wordt nog steeds door de eigenaren van dit kanaal gedaan.\n\nTeamleden en zelfs teameigenaren, wanneer ze geen lid zijn van dit kanaal, hebben geen toegang tot de content van het kanaal.\n\nHou er rekening mee dat de eigenaar van het team de leden uit het kanaal kan verwijderen.", + "Move_to_Team_Warning": "Wil je na het lezen van de vorige instructies over dit gedrag, dit kanaal nog steeds naar het geselecteerde team verplaatsen?", + "Load_More": "Meer laden", + "Load_Newer": "Nieuwer laden", + "Load_Older": "Ouder laden" } \ No newline at end of file diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index 293959613..9b34d87d3 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -277,6 +277,7 @@ "last_message": "última mensagem", "Leave_channel": "Sair do canal", "leaving_room": "saindo do canal", + "Leave": "Sair da sala", "leave": "sair", "Legal": "Legal", "Light": "Claro", @@ -639,12 +640,6 @@ "No_threads_following": "Você não está seguindo tópicos", "No_threads_unread": "Não há tópicos não lidos", "Messagebox_Send_to_channel": "Mostrar no canal", - "Set_as_leader": "Definir como líder", - "Set_as_moderator": "Definir como moderador", - "Set_as_owner": "Definir como proprietário", - "Remove_as_leader": "Remover como líder", - "Remove_as_moderator": "Remover como moderador", - "Remove_as_owner": "Remover como owner", "Remove_from_room": "Remover do canal", "Ignore": "Ignorar", "Unignore": "Deixar de ignorar", @@ -669,5 +664,6 @@ "No_team_channels_found": "Nenhum canal encontrado", "Team_not_found": "Time não encontrado", "Private_Team": "Equipe Privada", - "Add_Existing_Channel": "Adicionar Canal Existente" + "Add_Existing_Channel": "Adicionar Canal Existente", + "invalid-room": "Sala inválida" } \ No newline at end of file diff --git a/app/i18n/locales/ru.json b/app/i18n/locales/ru.json index 8b904c0ff..e5fca9504 100644 --- a/app/i18n/locales/ru.json +++ b/app/i18n/locales/ru.json @@ -61,6 +61,7 @@ "error-message-editing-blocked": "Правка сообщений заблокирована", "error-message-size-exceeded": "Размер сообщения превышает максимально разрешенный", "error-missing-unsubscribe-link": "Вы должны указать ссылку [отписаться].", + "error-no-owner-channel": "Вы не являетесь владельцем данного чата", "error-no-tokens-for-this-user": "Для этого пользователя нет токенов", "error-not-allowed": "Не допускается", "error-not-authorized": "Не разрешено", @@ -78,6 +79,7 @@ "error-user-registration-disabled": "Регистрация пользователей отключена", "error-user-registration-secret": "Регистрация пользователей разрешена только через секретный URL", "error-you-are-last-owner": "Вы последний владелец. Пожалуйста, назначьте нового владельца, прежде чем покинуть чат.", + "error-status-not-allowed": "Статус Невидимый отключён", "Actions": "Действия", "activity": "активности", "Activity": "По активности", @@ -90,6 +92,7 @@ "alert": "оповещение", "alerts": "оповещения", "All_users_in_the_channel_can_write_new_messages": "Все пользователи канала могут писать новые сообщения", + "All_users_in_the_team_can_write_new_messages": "Все пользователи в Команде могут писать новые сообщения", "A_meaningful_name_for_the_discussion_room": "Осмысленное имя для обсуждения", "All": "Все", "All_Messages": "Все сообщения", @@ -180,6 +183,7 @@ "delete": "удалить", "Delete": "Удалить", "DELETE": "УДАЛИТЬ", + "move": "переместить", "deleting_room": "удаление чата", "description": "описание", "Description": "Описание", @@ -225,6 +229,7 @@ "Encryption_error_title": "Введен не верный пароль шифрования", "Encryption_error_desc": "Невозможно расшифровать ваш ключ шифрования, чтобы импортировать его", "Everyone_can_access_this_channel": "Каждый может получить доступ к этому каналу", + "Everyone_can_access_this_team": "Каждый может получить доступ к этой Команде", "Error_uploading": "Ошибка загрузки", "Expiration_Days": "Срок действия (Дни)", "Favorite": "Избранное", @@ -286,10 +291,12 @@ "Join_our_open_workspace": "Присоединиться к нашему открытому серверу", "Join_your_workspace": "Присоединиться к вашему серверу", "Just_invited_people_can_access_this_channel": "Только приглашенные люди могут получить доступ к этому каналу", + "Just_invited_people_can_access_this_team": "Только приглашенные пользователи могут получить доступ к этой Команде", "Language": "Язык", "last_message": "последнее сообщение", "Leave_channel": "Покинуть канал", "leaving_room": "покинуть комнату", + "Leave": "Покинуть комнату", "leave": "покинуть", "Legal": "Правовые аспекты", "Light": "Светлая", @@ -326,6 +333,7 @@ "My_servers": "Мои серверы", "N_people_reacted": "отреагировало {{n}} человек", "N_users": "{{n}} пользователи", + "N_channels": "{{n}} каналов", "name": "имя", "Name": "Имя", "Navigation_history": "История навигации", @@ -435,6 +443,7 @@ "Review_app_unable_store": "Невозможно открыть {{store}}", "Review_this_app": "Оценить это приложение", "Remove": "Удалить", + "remove": "удалить", "Roles": "Роли", "Room_actions": "Действия с чатом", "Room_changed_announcement": "Объявление чата было изменено на: {{announcement}} пользователем {{userBy}}", @@ -681,12 +690,9 @@ "No_threads_following": "Нет тредов, за которыми вы следите", "No_threads_unread": "Непрочитанных тредов нет", "Messagebox_Send_to_channel": "Отправить в чат", - "Set_as_leader": "Назначить лидером", - "Set_as_moderator": "Назначить модератором", - "Set_as_owner": "Назначить владельцем", - "Remove_as_leader": "Удалить из лидеров", - "Remove_as_moderator": "Удалить из модераторов", - "Remove_as_owner": "Удалить из владельцев", + "Leader": "Лидер", + "Moderator": "Модератор", + "Owner": "Владелец", "Remove_from_room": "Удалить из чата", "Ignore": "Игнориновать", "Unignore": "Прекратить игнорировать", @@ -716,5 +722,44 @@ "Read_Only_Team": "Команда только для чтения", "Broadcast_Team": "Широковещательная Команда", "creating_team": "создание Команды", - "team-name-already-exists": "Команда с таким названием уже существует" + "team-name-already-exists": "Команда с таким названием уже существует", + "Add_Channel_to_Team": "Добавить канал в Команду", + "Create_New": "Создать", + "Add_Existing": "Добавить существующее", + "Add_Existing_Channel": "Добавить существующий канал", + "Remove_from_Team": "Удалить из Команды", + "Auto-join": "Автодобавление", + "Remove_Team_Room_Warning": "Хотите ли вы удалить этот канал из Команды? Канал будет перемещен обратно в рабочее пространство", + "Confirmation": "Подтверждение", + "invalid-room": "Такого канала не существует", + "You_are_leaving_the_team": "Вы покидаете Команду '{{team}}'", + "Leave_Team": "Покинуть команду", + "Select_Team": "Выберите Команду", + "Select_Team_Channels": "Выберите каналы Команды, которые вы хотите покинуть.", + "Cannot_leave": "Невозможно выйти", + "Cannot_remove": "Невозможно удалить", + "Cannot_delete": "Невозможно удалить", + "Last_owner_team_room": "Вы последний владелец этого чата. Как только вы покинете Команду, чат будет храниться внутри нее, но вы будете управлять ею снаружи.", + "last-owner-can-not-be-removed": "Последний владелец не может быть удален", + "Remove_User_Teams": "Выберите каналы, из которых вы хотите удалить пользователя.", + "Delete_Team": "Удалить Команду", + "Select_channels_to_delete": "Это нельзя отменить. После удаления Команды все содержимое чата и конфигурация будут удалены \n\nВыберите каналы, которые вы хотите удалить. Те, которые вы решите оставить, будут доступны в вашем рабочем пространстве. Обратите внимание, что публичные каналы по-прежнему будут открытыми и видимыми для всех.", + "You_are_deleting_the_team": "Вы удаляете эту Команду.", + "Removing_user_from_this_team": "Вы удаляете {{user}} из этой Команды", + "Remove_User_Team_Channels": "Выберите каналы, из которых вы хотите удалить пользователя.", + "Remove_Member": "Удалить участника", + "leaving_team": "выход из Команды", + "removing_team": "удаление из Команды", + "moving_channel_to_team": "перемещение канала в Команду", + "deleting_team": "удаление Команды", + "member-does-not-exist": "Участник не существует", + "Convert": "Конвертировать", + "Convert_to_Team": "Конвертировать в команду", + "Convert_to_Team_Warning": "Это нельзя отменить. После преобразования канала в Команду, вы не сможете преобразовать его обратно в канал.", + "Move_to_Team": "Перенести в команду", + "Move_Channel_Paragraph": "Перемещение канала внутрь Команды означает, что этот канал будет добавлен в контекст Команды, однако все участники канала, которые не являются членами соответствующей Команды, по-прежнему будут иметь доступ к этому каналу, но не будут добавлены как участники Команды \n\nВсе управление каналом по-прежнему будет осуществляться владельцами этого канала.\n\nЧлены Команды и даже владельцы Команды, если они не являются членами этого канала, не могут иметь доступ к содержимому канала \n\nОбратите внимание, что владелец Команды сможет удалять участников с канала.", + "Move_to_Team_Warning": "После прочтения предыдущих инструкций об этом поведении, вы все еще хотите переместить этот канал в выбранную Команду?", + "Load_More": "Загрузить еще", + "Load_Newer": "Загрузить более позднее", + "Load_Older": "Загрузить более раннее" } \ No newline at end of file diff --git a/app/i18n/locales/tr.json b/app/i18n/locales/tr.json index 4de4c1ecf..82959de8b 100644 --- a/app/i18n/locales/tr.json +++ b/app/i18n/locales/tr.json @@ -290,6 +290,7 @@ "last_message": "son ileti", "Leave_channel": "Kanaldan ayrıl", "leaving_room": "odadan ayrılıyor", + "Leave": "Odadan ayrıl", "leave": "ayrıl", "Legal": "Yasal", "Light": "Açık", @@ -680,12 +681,6 @@ "No_threads_following": "Herhangi bir konuyu takip etmiyorsunuz", "No_threads_unread": "Okunmamış konu yok", "Messagebox_Send_to_channel": "Kanala gönder", - "Set_as_leader": "Lider olarak ayarla", - "Set_as_moderator": "Moderatör olarak ayarla", - "Set_as_owner": "Sahip olarak ayarla", - "Remove_as_leader": "Lider olarak kaldır", - "Remove_as_moderator": "Moderatör olarak kaldır", - "Remove_as_owner": "Sahip olarak kaldır", "Remove_from_room": "Odadan çıkar", "Ignore": "Yok say", "Unignore": "Yok sayma", @@ -703,5 +698,6 @@ "Direct_message": "Özel ileti", "Message_Ignored": "İleti yok sayıldı. Görüntülemek için dokunun.", "Enter_workspace_URL": "Çalışma alanı URL'nizi girin", - "Workspace_URL_Example": "Örn. sirketiniz.rocket.chat" + "Workspace_URL_Example": "Örn. sirketiniz.rocket.chat", + "invalid-room": "Geçersiz oda" } \ No newline at end of file diff --git a/app/i18n/locales/zh-TW.json b/app/i18n/locales/zh-TW.json index dc5748c27..6f60d155a 100644 --- a/app/i18n/locales/zh-TW.json +++ b/app/i18n/locales/zh-TW.json @@ -288,6 +288,7 @@ "last_message": "最後一則訊息", "Leave_channel": "離開頻道", "leaving_room": "離開聊天室", + "Leave": "離開", "leave": "離開", "Legal": "合法", "Light": "淺色", @@ -678,5 +679,7 @@ "No_threads": "當前沒有討論串", "No_threads_following": "當前沒有正在追蹤的討論", "No_threads_unread": "當前沒有未讀的討論", - "Messagebox_Send_to_channel": "發送至頻道" + "Messagebox_Send_to_channel": "發送至頻道", + "Confirmation": "確認", + "invalid-room": "無效的房間" } \ No newline at end of file From 7e31ac75f177511163e7af44164bf069450c9614 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Mon, 7 Jun 2021 14:27:15 -0300 Subject: [PATCH 36/44] [NEW] Support Google OAuth from external browser (#3134) * Deep linking to the app * Handle deep linking --- app/containers/LoginServices.js | 20 +++++++++++++++----- app/sagas/deepLinking.js | 15 +++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/app/containers/LoginServices.js b/app/containers/LoginServices.js index 4ae647b5c..11d5dd575 100644 --- a/app/containers/LoginServices.js +++ b/app/containers/LoginServices.js @@ -1,6 +1,6 @@ import React from 'react'; import { - View, StyleSheet, Text, Animated, Easing + View, StyleSheet, Text, Animated, Easing, Linking } from 'react-native'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; @@ -24,6 +24,9 @@ const SERVICE_HEIGHT = 58; const BORDER_RADIUS = 2; const SERVICES_COLLAPSED_HEIGHT = 174; +const LOGIN_STYPE_POPUP = 'popup'; +const LOGIN_STYPE_REDIRECT = 'redirect'; + const styles = StyleSheet.create({ serviceButton: { borderRadius: BORDER_RADIUS, @@ -122,9 +125,9 @@ class LoginServices extends React.PureComponent { const endpoint = 'https://accounts.google.com/o/oauth2/auth'; const redirect_uri = `${ server }/_oauth/google?close`; const scope = 'email'; - const state = this.getOAuthState(); + const state = this.getOAuthState(LOGIN_STYPE_REDIRECT); const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; - this.openOAuth({ url: `${ endpoint }${ params }` }); + Linking.openURL(`${ endpoint }${ params }`); } onPressLinkedin = () => { @@ -219,9 +222,16 @@ class LoginServices extends React.PureComponent { } } - getOAuthState = () => { + getOAuthState = (loginStyle = LOGIN_STYPE_POPUP) => { const credentialToken = random(43); - return Base64.encodeURI(JSON.stringify({ loginStyle: 'popup', credentialToken, isCordova: true })); + let obj = { loginStyle, credentialToken, isCordova: true }; + if (loginStyle === LOGIN_STYPE_REDIRECT) { + obj = { + ...obj, + redirectUrl: 'rocketchat://auth' + }; + } + return Base64.encodeURI(JSON.stringify(obj)); } openOAuth = ({ url, ssoToken, authType = 'oauth' }) => { diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js index 985a28556..556573a54 100644 --- a/app/sagas/deepLinking.js +++ b/app/sagas/deepLinking.js @@ -16,6 +16,7 @@ import { import { localAuthenticate } from '../utils/localAuthentication'; import { goRoom } from '../utils/goRoom'; import { loginRequest } from '../actions/login'; +import log from '../utils/log'; const roomTypes = { channel: 'c', direct: 'd', group: 'p', channels: 'l' @@ -93,6 +94,15 @@ const fallbackNavigation = function* fallbackNavigation() { yield put(appInit()); }; +const handleOAuth = function* handleOAuth({ params }) { + const { credentialToken, credentialSecret } = params; + try { + yield RocketChat.loginOAuthOrSso({ oauth: { credentialToken, credentialSecret } }); + } catch (e) { + log(e); + } +}; + const handleOpen = function* handleOpen({ params }) { const serversDB = database.servers; const serversCollection = serversDB.get('servers'); @@ -108,6 +118,11 @@ const handleOpen = function* handleOpen({ params }) { }); } + if (params.type === 'oauth') { + yield handleOAuth({ params }); + return; + } + // If there's no host on the deep link params and the app is opened, just call appInit() if (!host) { yield fallbackNavigation(); From ec97d8417fc7edf72a27073dfd399c53f9e6d26d Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Mon, 7 Jun 2021 14:27:37 -0300 Subject: [PATCH 37/44] Bump version to 4.17.0 (#3093) --- android/app/build.gradle | 2 +- ios/RocketChatRN.xcodeproj/project.pbxproj | 4 ++-- ios/RocketChatRN/Info.plist | 2 +- ios/ShareRocketChatRN/Info.plist | 2 +- package.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 69975e5d7..e3da57bb9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -144,7 +144,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode VERSIONCODE as Integer - versionName "4.16.2" + versionName "4.17.0" vectorDrawables.useSupportLibrary = true if (!isFoss) { manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj index 13f5b8cac..bbebd2ee7 100644 --- a/ios/RocketChatRN.xcodeproj/project.pbxproj +++ b/ios/RocketChatRN.xcodeproj/project.pbxproj @@ -1694,7 +1694,7 @@ INFOPLIST_FILE = NotificationService/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 4.16.2; + MARKETING_VERSION = 4.17.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService; @@ -1731,7 +1731,7 @@ INFOPLIST_FILE = NotificationService/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 4.16.2; + MARKETING_VERSION = 4.17.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/ios/RocketChatRN/Info.plist b/ios/RocketChatRN/Info.plist index e6129268e..4357a8abb 100644 --- a/ios/RocketChatRN/Info.plist +++ b/ios/RocketChatRN/Info.plist @@ -23,7 +23,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 4.16.2 + 4.17.0 CFBundleSignature ???? CFBundleURLTypes diff --git a/ios/ShareRocketChatRN/Info.plist b/ios/ShareRocketChatRN/Info.plist index 38a2e4cf6..e38798d73 100644 --- a/ios/ShareRocketChatRN/Info.plist +++ b/ios/ShareRocketChatRN/Info.plist @@ -21,7 +21,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 4.16.2 + 4.17.0 CFBundleVersion 1 KeychainGroup diff --git a/package.json b/package.json index 855b1df73..7e0e5d6da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket-chat-reactnative", - "version": "4.16.2", + "version": "4.17.0", "private": true, "scripts": { "start": "react-native start", From dfe9e4fccb3b769084cc30d16faabf1311a29a60 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Mon, 7 Jun 2021 16:08:20 -0300 Subject: [PATCH 38/44] Revert "[IMPROVEMENT] Load team's rooms from local database on team leave (#3185)" (#3194) This reverts commit fa00ef92efa45fef3938afbb92be52b97cb16358. --- app/views/RoomActionsView/index.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index 7ed2e1a57..f055b3649 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -436,16 +436,11 @@ class RoomActionsView extends React.Component { const { navigation, leaveRoom } = this.props; try { - const db = database.active; - const subCollection = db.get('subscriptions'); - const rooms = await subCollection.query( - Q.where('team_id', Q.eq(room.teamId)), - Q.where('team_main', Q.notEq(true)) - ); + const result = await RocketChat.teamListRoomsOfUser({ teamId: room.teamId, userId: room.u._id }); - if (rooms.length) { - const teamChannels = rooms.map(r => ({ - rid: r.id, + if (result.rooms?.length) { + const teamChannels = result.rooms.map(r => ({ + rid: r._id, name: r.name, teamId: r.teamId, alert: r.isLastOwner From 267dfc547ef4fdc37dc4e3c2e8fd40210b1b90a5 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Mon, 7 Jun 2021 17:20:07 -0300 Subject: [PATCH 39/44] [FIX] Teams tests (#3196) * Make team_main not optional and fix tests * Undo isOptional and fix query * Comment --- app/lib/database/schema/app.js | 2 +- app/sagas/room.js | 2 ++ app/views/RoomActionsView/index.js | 4 ++-- app/views/RoomInfoEditView/index.js | 2 +- e2e/tests/team/02-team.spec.js | 4 +--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/lib/database/schema/app.js b/app/lib/database/schema/app.js index 883e6dfd9..ac7b97e78 100644 --- a/app/lib/database/schema/app.js +++ b/app/lib/database/schema/app.js @@ -59,7 +59,7 @@ export default appSchema({ { name: 'e2e_key_id', type: 'string', isOptional: true }, { name: 'avatar_etag', type: 'string', isOptional: true }, { name: 'team_id', type: 'string', isIndexed: true }, - { name: 'team_main', type: 'boolean', isOptional: true } + { name: 'team_main', type: 'boolean', isOptional: true } // Use `Q.notEq(true)` to get false or null ] }), tableSchema({ diff --git a/app/sagas/room.js b/app/sagas/room.js index 978944786..e17a06da9 100644 --- a/app/sagas/room.js +++ b/app/sagas/room.js @@ -72,6 +72,8 @@ const handleLeaveRoom = function* handleLeaveRoom({ room, roomType, selected }) logEvent(events.RA_LEAVE_F); if (e.data && e.data.errorType === 'error-you-are-last-owner') { Alert.alert(I18n.t('Oops'), I18n.t(e.data.errorType)); + } else if (e?.data?.error === 'last-owner-can-not-be-removed') { + Alert.alert(I18n.t('Oops'), I18n.t(e.data.error)); } else { Alert.alert(I18n.t('Oops'), I18n.t('There_was_an_error_while_action', { action: I18n.t('leaving_room') })); } diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index f055b3649..e5c630ebb 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -515,7 +515,7 @@ class RoomActionsView extends React.Component { const db = database.active; const subCollection = db.get('subscriptions'); const teamRooms = await subCollection.query( - Q.where('team_main', Q.notEq(null)) + Q.where('team_main', true) ); if (teamRooms.length) { @@ -558,7 +558,7 @@ class RoomActionsView extends React.Component { const teams = await db.collections .get('subscriptions') .query( - Q.where('team_main', Q.notEq(null)), + Q.where('team_main', true), Q.where('name', Q.like(`%${ onChangeText }%`)), Q.experimentalTake(QUERY_SIZE), Q.experimentalSortBy('room_updated_at', Q.desc) diff --git a/app/views/RoomInfoEditView/index.js b/app/views/RoomInfoEditView/index.js index 8905e5ee1..d5dd7f0c5 100644 --- a/app/views/RoomInfoEditView/index.js +++ b/app/views/RoomInfoEditView/index.js @@ -326,7 +326,7 @@ class RoomInfoEditView extends React.Component { const subCollection = db.get('subscriptions'); const teamChannels = await subCollection.query( Q.where('team_id', room.teamId), - Q.where('team_main', null) + Q.where('team_main', Q.notEq(true)) ); if (teamChannels.length) { diff --git a/e2e/tests/team/02-team.spec.js b/e2e/tests/team/02-team.spec.js index f6f4d0329..b10fc8206 100644 --- a/e2e/tests/team/02-team.spec.js +++ b/e2e/tests/team/02-team.spec.js @@ -286,9 +286,7 @@ describe('Team', () => { await element(by.text('OK')).tap(); await waitFor(element(by.id('select-list-view-submit'))).toExist().withTimeout(2000); await element(by.id('select-list-view-submit')).tap(); - await waitFor(element(by.text(`You were removed from ${ team }`))).toExist().withTimeout(8000); - await element(by.text('OK')).tap(); - await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(5000); + await waitFor(element(by.id(`rooms-list-view-item-${ team }`))).toBeNotVisible().withTimeout(60000); }); }); }); From 2d4bfa51f821e5e41aaed6aba5a6fa09aa00a12c Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Thu, 10 Jun 2021 14:06:03 -0300 Subject: [PATCH 40/44] [FIX] Wrong system messages being passed as parameters to room save (#3197) --- app/utils/messageTypes.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/utils/messageTypes.js b/app/utils/messageTypes.js index f874a437a..eacd2c514 100644 --- a/app/utils/messageTypes.js +++ b/app/utils/messageTypes.js @@ -27,10 +27,10 @@ export const MessageTypeValues = [ value: 'rm', text: 'Message_HideType_rm' }, { - value: 'subscription_role_added', + value: 'subscription-role-added', text: 'Message_HideType_subscription_role_added' }, { - value: 'subscription_role_removed', + value: 'subscription-role-removed', text: 'Message_HideType_subscription_role_removed' }, { value: 'room_archived', From 86ca76194dc1fdc77e99a29713c8ee1fb17d2539 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Thu, 10 Jun 2021 14:11:05 -0300 Subject: [PATCH 41/44] [FIX] RoomItem's long press crashing the app if prop is missing (#3199) * Check onLongPress prop * Add Touch stories --- .../__snapshots__/Storyshots.test.js.snap | 398 ++++++++++++++++++ app/presentation/RoomItem/index.js | 4 +- storybook/stories/RoomItem.js | 3 + 3 files changed, 404 insertions(+), 1 deletion(-) diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 52fa5d01d..3e3106d94 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -59100,6 +59100,404 @@ exports[`Storyshots Room Item Tag 1`] = ` `; +exports[`Storyshots Room Item Touch 1`] = ` + + + + + + + + +  + + + Read + + + + + + + +  + + + Favorite + + + + +  + + + Hide + + + + + + + + + + + + + +  + + + rocket.cat + + + + + + + + + +`; + exports[`Storyshots Room Item Type 1`] = ` { const { item, onLongPress } = this.props; - return onLongPress(item); + if (onLongPress) { + return onLongPress(item); + } } render() { diff --git a/storybook/stories/RoomItem.js b/storybook/stories/RoomItem.js index 025fbf7f5..c13eaa4d2 100644 --- a/storybook/stories/RoomItem.js +++ b/storybook/stories/RoomItem.js @@ -44,6 +44,9 @@ stories.add('Basic', () => ( )); +stories.add('Touch', () => ( + alert('on press')} onLongPress={() => alert('on long press')} /> +)); stories.add('User', () => ( <> From 303c26f71669c8f7e0e7e723716f4e4a0fe71cfe Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Thu, 10 Jun 2021 14:32:35 -0300 Subject: [PATCH 42/44] [FIX] Crashing on link press (#3204) --- app/containers/markdown/Link.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/containers/markdown/Link.js b/app/containers/markdown/Link.js index 054b3d4d9..008dc0ba8 100644 --- a/app/containers/markdown/Link.js +++ b/app/containers/markdown/Link.js @@ -12,7 +12,7 @@ const Link = React.memo(({ children, link, theme, onLinkPress }) => { const handlePress = () => { - if (!link) { + if (!link || !onLinkPress) { return; } onLinkPress(link); From 3177bc3e58cd30d6a3b9c33905eb01fc069ba29c Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Thu, 10 Jun 2021 14:52:00 -0300 Subject: [PATCH 43/44] [FIX] Don't show Block Button inside Group DM Actions (#3195) * [FIX] Don't show Block Button inside Group DM Actions * Use RocketChat.isGroupChat instead of simple if condition * Add return Co-authored-by: Diego Mello --- app/views/RoomActionsView/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index e5c630ebb..d16ea5768 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -714,7 +714,7 @@ class RoomActionsView extends React.Component { return null; } - if (t === 'd') { + if (t === 'd' && !RocketChat.isGroupChat(room)) { return ( @@ -751,6 +751,8 @@ class RoomActionsView extends React.Component { ); } + + return null; } teamChannelActions = (t, room) => { From c744672cbf8b61d2817d964d4026b026bcbf2f55 Mon Sep 17 00:00:00 2001 From: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com> Date: Fri, 11 Jun 2021 15:23:43 -0300 Subject: [PATCH 44/44] [TEST] Fixed E2E tests (#3201) * [FIX] Test E2E i18n * 01-createroom and 02-room fixed * 03-roomactions and 04-discussions * 05-threads and 07-markasunread from room * Test 07-markasunread * Set notifications 'YES' and delete true in 03-forgotpassword and 04-createuser * Fixed the data that 02-team uses and changed the message in 07-markasunread * Added group.alternate to data.docker and commented the test for the fallback language Co-authored-by: Diego Mello --- e2e/data.js | 3 +++ e2e/data/data.docker.js | 3 +++ e2e/tests/assorted/12-i18n.spec.js | 24 +++++++++---------- .../onboarding/03-forgotpassword.spec.js | 2 +- e2e/tests/onboarding/04-createuser.spec.js | 2 +- e2e/tests/room/01-createroom.spec.js | 4 ++-- e2e/tests/room/02-room.spec.js | 4 ++-- e2e/tests/room/03-roomactions.spec.js | 10 ++++---- e2e/tests/room/04-discussion.spec.js | 4 ++-- e2e/tests/room/05-threads.spec.js | 2 +- e2e/tests/room/07-markasunread.spec.js | 10 ++++---- e2e/tests/team/02-team.spec.js | 4 ++-- 12 files changed, 39 insertions(+), 33 deletions(-) diff --git a/e2e/data.js b/e2e/data.js index c69b72515..98257f71d 100644 --- a/e2e/data.js +++ b/e2e/data.js @@ -40,6 +40,9 @@ const data = { groups: { private: { name: `detox-private-${ value }` + }, + alternate: { + name: `detox-alternate-${ value }` } }, teams: { diff --git a/e2e/data/data.docker.js b/e2e/data/data.docker.js index 1a6bb1569..31fb5c8e2 100644 --- a/e2e/data/data.docker.js +++ b/e2e/data/data.docker.js @@ -40,6 +40,9 @@ const data = { groups: { private: { name: `detox-private-${ value }` + }, + alternate: { + name: `detox-alternate-${ value }` } }, teams: { diff --git a/e2e/tests/assorted/12-i18n.spec.js b/e2e/tests/assorted/12-i18n.spec.js index 13e7c79be..51273fd72 100644 --- a/e2e/tests/assorted/12-i18n.spec.js +++ b/e2e/tests/assorted/12-i18n.spec.js @@ -52,18 +52,18 @@ describe('i18n', () => { * This test might become outdated as soon as we support the language * Although this seems to be a bad approach, that's the intention for having fallback enabled */ - it('OS set to available language and fallback to \'en\' on strings missing translation', async() => { - await device.launchApp({ - ...defaultLaunchArgs, - languageAndLocale: { - language: "nl", - locale: "nl" - } - }); - await waitFor(element(by.id('onboarding-view'))).toBeVisible().withTimeout(20000); - await expect(element(by.id('join-workspace').and(by.label('Join a workspace')))).toBeVisible(); // Missing nl translation - await expect(element(by.id('create-workspace-button').and(by.label('Een nieuwe workspace maken')))).toBeVisible(); - }); + // it('OS set to available language and fallback to \'en\' on strings missing translation', async() => { + // await device.launchApp({ + // ...defaultLaunchArgs, + // languageAndLocale: { + // language: "nl", + // locale: "nl" + // } + // }); + // await waitFor(element(by.id('onboarding-view'))).toBeVisible().withTimeout(20000); + // await expect(element(by.id('join-workspace').and(by.label('Word lid van een werkruimte')))).toBeVisible(); + // await expect(element(by.id('create-workspace-button').and(by.label('Een nieuwe werkruimte aanmaken')))).toBeVisible(); + // }); }); describe('Rocket.Chat language', () => { diff --git a/e2e/tests/onboarding/03-forgotpassword.spec.js b/e2e/tests/onboarding/03-forgotpassword.spec.js index 8b7fe9b5a..03afe5925 100644 --- a/e2e/tests/onboarding/03-forgotpassword.spec.js +++ b/e2e/tests/onboarding/03-forgotpassword.spec.js @@ -6,7 +6,7 @@ const { navigateToLogin } = require('../../helpers/app'); describe('Forgot password screen', () => { before(async() => { - await device.launchApp({ newInstance: true }); + await device.launchApp({ permissions: { notifications: 'YES' }, delete: true }); await navigateToLogin(); await element(by.id('login-view-forgot-password')).tap(); await waitFor(element(by.id('forgot-password-view'))).toExist().withTimeout(2000); diff --git a/e2e/tests/onboarding/04-createuser.spec.js b/e2e/tests/onboarding/04-createuser.spec.js index aa75807f2..cf706d78b 100644 --- a/e2e/tests/onboarding/04-createuser.spec.js +++ b/e2e/tests/onboarding/04-createuser.spec.js @@ -6,7 +6,7 @@ const data = require('../../data'); describe('Create user screen', () => { before(async() => { - await device.launchApp({ newInstance: true }); + await device.launchApp({ permissions: { notifications: 'YES' }, delete: true }); await navigateToRegister(); }); diff --git a/e2e/tests/room/01-createroom.spec.js b/e2e/tests/room/01-createroom.spec.js index 5e7ae2ca5..874255e43 100644 --- a/e2e/tests/room/01-createroom.spec.js +++ b/e2e/tests/room/01-createroom.spec.js @@ -96,8 +96,8 @@ describe('Create room screen', () => { it('should get invalid room', async() => { await element(by.id('create-channel-name')).typeText('general'); await element(by.id('create-channel-submit')).tap(); - await waitFor(element(by.text(`A channel with name 'general' exists`))).toExist().withTimeout(60000); - await expect(element(by.text(`A channel with name 'general' exists`))).toExist(); + await waitFor(element(by.text(`A channel with name general exists`))).toExist().withTimeout(60000); + await expect(element(by.text(`A channel with name general exists`))).toExist(); await element(by.text('OK')).tap(); }); diff --git a/e2e/tests/room/02-room.spec.js b/e2e/tests/room/02-room.spec.js index 02d874963..d6a374185 100644 --- a/e2e/tests/room/02-room.spec.js +++ b/e2e/tests/room/02-room.spec.js @@ -191,7 +191,7 @@ describe('Room screen', () => { await expect(element(by.id('action-sheet'))).toExist(); await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await element(by.id('action-sheet-handle')).swipe('up', 'slow', 0.5); - await waitFor(element(by.label('Unstar')).atIndex(0)).toBeVisible().withTimeout(6000); + await waitFor(element(by.label('Unstar')).atIndex(0)).toExist().withTimeout(6000); await element(by.id('action-sheet-handle')).swipe('down', 'fast', 0.8); }); @@ -272,7 +272,7 @@ describe('Room screen', () => { await waitFor(element(by.id('action-sheet'))).toExist().withTimeout(1000); await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); - await waitFor(element(by.label('Unpin'))).toBeVisible().withTimeout(2000); + await waitFor(element(by.label('Unpin')).atIndex(0)).toExist().withTimeout(2000); await element(by.id('action-sheet-handle')).swipe('down', 'fast', 0.8); }); diff --git a/e2e/tests/room/03-roomactions.spec.js b/e2e/tests/room/03-roomactions.spec.js index 204235705..ecbac2233 100644 --- a/e2e/tests/room/03-roomactions.spec.js +++ b/e2e/tests/room/03-roomactions.spec.js @@ -385,7 +385,7 @@ describe('Room actions screen', () => { it('should remove user from room', async() => { await openActionSheet('rocket.cat'); - await element(by.label('Remove from room')).tap(); + await element(by.label('Remove from room')).atIndex(0).tap(); await waitFor(element(by.label('Are you sure?'))).toExist().withTimeout(5000); await element(by.label('Yes, remove user!').and(by.type('_UIAlertControllerActionView'))).tap(); await waitFor(element(by.id('room-members-view-item-rocket.cat'))).toBeNotVisible().withTimeout(60000); @@ -456,13 +456,13 @@ describe('Room actions screen', () => { it('should set/remove as mute', async() => { await openActionSheet(user.username); - await element(by.label('Mute')).tap(); + await element(by.label('Mute')).atIndex(0).tap(); await waitFor(element(by.label('Are you sure?'))).toExist().withTimeout(5000); await element(by.label('Mute').and(by.type('_UIAlertControllerActionView'))).tap(); await waitForToast(); await openActionSheet(user.username); - await element(by.label('Unmute')).tap(); + await element(by.label('Unmute')).atIndex(0).tap(); await waitFor(element(by.label('Are you sure?'))).toExist().withTimeout(5000); await element(by.label('Unmute').and(by.type('_UIAlertControllerActionView'))).tap(); await waitForToast(); @@ -478,7 +478,7 @@ describe('Room actions screen', () => { const channelName = `#${ data.groups.private.name }`; await sendMessage(user, channelName, message); await openActionSheet(user.username); - await element(by.label('Ignore')).tap(); + await element(by.label('Ignore')).atIndex(0).tap(); await waitForToast(); await backToActions(); await tapBack(); @@ -497,7 +497,7 @@ describe('Room actions screen', () => { await element(by.id('room-members-view-toggle-status')).tap(); await waitFor(element(by.id(`room-members-view-item-${ user.username }`))).toExist().withTimeout(60000); await openActionSheet(user.username); - await element(by.label('Direct message')).tap(); + await element(by.label('Direct message')).atIndex(0).tap(); await waitFor(element(by.id('room-view'))).toExist().withTimeout(60000); await waitFor(element(by.id(`room-view-title-${ user.username }`))).toExist().withTimeout(60000); await tapBack(); diff --git a/e2e/tests/room/04-discussion.spec.js b/e2e/tests/room/04-discussion.spec.js index fccc96185..aa39f2eaf 100644 --- a/e2e/tests/room/04-discussion.spec.js +++ b/e2e/tests/room/04-discussion.spec.js @@ -23,7 +23,7 @@ describe('Discussion', () => { const discussionName = `${data.random} Discussion NewMessageView`; await element(by.id('rooms-list-view-create-channel')).tap(); await waitFor(element(by.id('new-message-view'))).toExist().withTimeout(2000); - await element(by.label('Create Discussion')).tap(); + await element(by.label('Create Discussion')).atIndex(0).tap(); await waitFor(element(by.id('create-discussion-view'))).toExist().withTimeout(60000); await expect(element(by.id('create-discussion-view'))).toExist(); await element(by.label('Select a Channel...')).tap(); @@ -44,7 +44,7 @@ describe('Discussion', () => { await navigateToRoom(); await element(by.id('messagebox-actions')).tap(); await waitFor(element(by.id('action-sheet'))).toExist().withTimeout(2000); - await element(by.label('Create Discussion')).tap(); + await element(by.label('Create Discussion')).atIndex(0).tap(); await waitFor(element(by.id('create-discussion-view'))).toExist().withTimeout(2000); await element(by.id('multi-select-discussion-name')).replaceText(discussionName); await waitFor(element(by.id(`create-discussion-submit`))).toExist().withTimeout(10000); diff --git a/e2e/tests/room/05-threads.spec.js b/e2e/tests/room/05-threads.spec.js index 40652df7d..381fa2b07 100644 --- a/e2e/tests/room/05-threads.spec.js +++ b/e2e/tests/room/05-threads.spec.js @@ -72,7 +72,7 @@ describe('Threads', () => { await expect(element(by.id('action-sheet'))).toExist(); await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); - await element(by.label('Reply in Thread')).tap(); + await element(by.label('Reply in Thread')).atIndex(0).tap(); await element(by.id('messagebox-input')).typeText('replied'); await element(by.id('messagebox-send-message')).tap(); await waitFor(element(by.id(`message-thread-button-${ thread }`))).toExist().withTimeout(5000); diff --git a/e2e/tests/room/07-markasunread.spec.js b/e2e/tests/room/07-markasunread.spec.js index dcbc50367..383f49a7b 100644 --- a/e2e/tests/room/07-markasunread.spec.js +++ b/e2e/tests/room/07-markasunread.spec.js @@ -2,7 +2,7 @@ const { device, expect, element, by, waitFor } = require('detox'); const data = require('../../data'); -const { navigateToLogin, login, searchRoom } = require('../../helpers/app'); +const { navigateToLogin, login, searchRoom, sleep } = require('../../helpers/app'); const { sendMessage } = require('../../helpers/data_setup') async function navigateToRoom(user) { @@ -25,15 +25,15 @@ describe('Mark as unread', () => { describe('Usage', async() => { describe('Mark message as unread', async() => { it('should mark message as unread', async() => { - const message = `${ data.random }message`; + const message = `${ data.random }message-mark-as-unread`; const channelName = `@${ data.users.regular.username }`; await sendMessage(data.users.alternate, channelName, message); await waitFor(element(by.label(message)).atIndex(0)).toExist().withTimeout(30000); + await sleep(300); await element(by.label(message)).atIndex(0).longPress(); - await expect(element(by.id('action-sheet'))).toExist(); - await expect(element(by.id('action-sheet-handle'))).toBeVisible(); + await waitFor(element(by.id('action-sheet-handle'))).toBeVisible().withTimeout(3000); await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); - await element(by.label('Mark Unread')).tap(); + await element(by.label('Mark Unread')).atIndex(0).tap(); await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(5000); await expect(element(by.id(`rooms-list-view-item-${data.users.alternate.username}`))).toExist(); }); diff --git a/e2e/tests/team/02-team.spec.js b/e2e/tests/team/02-team.spec.js index b10fc8206..f9b819595 100644 --- a/e2e/tests/team/02-team.spec.js +++ b/e2e/tests/team/02-team.spec.js @@ -39,8 +39,8 @@ async function waitForToast() { describe('Team', () => { const team = data.teams.private.name; const user = data.users.alternate; - const room = `private${ data.random }`; - const existingRoom = data.groups.private.name; + const room = `private${ data.random }-channel-team`; + const existingRoom = data.groups.alternate.name; before(async() => { await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });