solve merge conflicts

This commit is contained in:
GOVINDDIXIT 2020-07-25 13:06:56 +05:30
commit 5139d0f5de
1502 changed files with 104427 additions and 43381 deletions

View File

@ -135,6 +135,10 @@ commands:
name: Test name: Test
command: | command: |
npx detox test << parameters.folder >> --configuration ios.sim.release --cleanup npx detox test << parameters.folder >> --configuration ios.sim.release --cleanup
when: always
- store_artifacts:
path: ./artifacts
# JOBS # JOBS
jobs: jobs:

View File

@ -1,7 +1,34 @@
<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR --> <!-- This is a pull request template, you do not need to uncomment or remove the comments, they won't show up in the PR text. -->
@RocketChat/ReactNative
<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> ## Proposed changes
Closes #ISSUE_NUMBER <!-- Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue below. -->
<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> ## Issue(s)
<!-- Link the issues being closed by or related to this PR. For example, you can use #594 if this PR closes issue number 594 -->
## How to test or reproduce
<!-- Mention how you would reproduce the bug if not mentioned on the issue page already. Also mention which screens are going to have the changes if applicable -->
## Screenshots
## Types of changes
<!-- What types of changes does your code introduce to Rocket.Chat? -->
<!-- Put an `x` in the boxes that apply -->
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] Improvement (non-breaking change which improves a current function)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Documentation update (if none of the other choices apply)
## Checklist
<!-- Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code. -->
- [ ] I have read the [CONTRIBUTING](https://github.com/RocketChat/Rocket.Chat/blob/develop/.github/CONTRIBUTING.md#contributing-to-rocketchat) doc
- [ ] I have signed the [CLA](https://cla-assistant.io/RocketChat/Rocket.Chat.ReactNative)
- [ ] Lint and unit tests pass locally with my changes
- [ ] I have added tests that prove my fix is effective or that my feature works (if applicable)
- [ ] I have added necessary documentation (if applicable)
- [ ] Any dependent changes have been merged and published in downstream modules
## Further comments
<!-- If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... -->

1
.gitignore vendored
View File

@ -58,6 +58,7 @@ buck-out/
coverage coverage
artifacts
.vscode/ .vscode/
e2e/docker/rc_test_env/docker-compose.yml e2e/docker/rc_test_env/docker-compose.yml
e2e/docker/data/db e2e/docker/data/db

View File

@ -0,0 +1,3 @@
export default {
crashlytics: null
};

View File

@ -4994,137 +4994,6 @@ exports[`Storyshots Message list message 1`] = `
</RCTScrollView> </RCTScrollView>
`; `;
exports[`Storyshots RoomItem list roomitem 1`] = `
<RCTScrollView
style={
Object {
"backgroundColor": "#efeff4",
}
}
>
<View>
<Text
style={
Array [
Object {
"fontSize": 20,
"fontWeight": "300",
"marginLeft": 10,
"marginTop": 30,
},
Object {
"backgroundColor": "#ffffff",
"color": "#0d0e12",
},
undefined,
]
}
>
Basic
</Text>
View
<Text
style={
Array [
Object {
"fontSize": 20,
"fontWeight": "300",
"marginLeft": 10,
"marginTop": 30,
},
Object {
"backgroundColor": "#ffffff",
"color": "#0d0e12",
},
undefined,
]
}
>
User
</Text>
View
View
<Text
style={
Array [
Object {
"fontSize": 20,
"fontWeight": "300",
"marginLeft": 10,
"marginTop": 30,
},
Object {
"backgroundColor": "#ffffff",
"color": "#0d0e12",
},
undefined,
]
}
>
Type
</Text>
View
View
View
View
View
<Text
style={
Array [
Object {
"fontSize": 20,
"fontWeight": "300",
"marginLeft": 10,
"marginTop": 30,
},
Object {
"backgroundColor": "#ffffff",
"color": "#0d0e12",
},
undefined,
]
}
>
Alerts
</Text>
View
View
View
View
View
View
View
View
View
<Text
style={
Array [
Object {
"fontSize": 20,
"fontWeight": "300",
"marginLeft": 10,
"marginTop": 30,
},
Object {
"backgroundColor": "#ffffff",
"color": "#0d0e12",
},
undefined,
]
}
>
Last Message
</Text>
View
View
View
View
View
View
</View>
</RCTScrollView>
`;
exports[`Storyshots UiKitMessage list uikitmessage 1`] = ` exports[`Storyshots UiKitMessage list uikitmessage 1`] = `
<RCTSafeAreaView <RCTSafeAreaView
emulateUnlessSupported={true} emulateUnlessSupported={true}

View File

@ -2,13 +2,19 @@ def taskRequests = getGradle().getStartParameter().getTaskRequests().toString().
def isPlay = !taskRequests.contains("foss") def isPlay = !taskRequests.contains("foss")
apply plugin: "com.android.application" apply plugin: "com.android.application"
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
<<<<<<< HEAD
if (isPlay) { if (isPlay) {
apply plugin: "io.fabric" apply plugin: "io.fabric"
apply plugin: "com.google.firebase.firebase-perf" apply plugin: "com.google.firebase.firebase-perf"
apply plugin: 'com.bugsnag.android.gradle' apply plugin: 'com.bugsnag.android.gradle'
} }
=======
apply plugin: 'com.bugsnag.android.gradle'
>>>>>>> e5aaa667e7921a827df304b9d64320e14c7a55d3
import com.android.build.OutputFile import com.android.build.OutputFile
@ -174,15 +180,18 @@ android {
minifyEnabled enableProguardInReleaseBuilds minifyEnabled enableProguardInReleaseBuilds
setProguardFiles([getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro']) setProguardFiles([getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'])
signingConfig signingConfigs.release signingConfig signingConfigs.release
firebaseCrashlytics {
nativeSymbolUploadEnabled true
}
} }
} }
packagingOptions { // packagingOptions {
pickFirst '**/armeabi-v7a/libc++_shared.so' // pickFirst '**/armeabi-v7a/libc++_shared.so'
pickFirst '**/x86/libc++_shared.so' // pickFirst '**/x86/libc++_shared.so'
pickFirst '**/arm64-v8a/libc++_shared.so' // pickFirst '**/arm64-v8a/libc++_shared.so'
pickFirst '**/x86_64/libc++_shared.so' // pickFirst '**/x86_64/libc++_shared.so'
} // }
// applicationVariants are e.g. debug, release // applicationVariants are e.g. debug, release
@ -270,7 +279,11 @@ task copyDownloadableDepsToLibs(type: Copy) {
into 'libs' into 'libs'
} }
<<<<<<< HEAD
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
if (isPlay) { if (isPlay) {
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
} }
=======
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
>>>>>>> e5aaa667e7921a827df304b9d64320e14c7a55d3

View File

@ -29,10 +29,6 @@ import com.wix.reactnativenotifications.core.notification.INotificationsApplicat
import com.wix.reactnativenotifications.core.notification.IPushNotification; import com.wix.reactnativenotifications.core.notification.IPushNotification;
import com.wix.reactnativekeyboardinput.KeyboardInputPackage; import com.wix.reactnativekeyboardinput.KeyboardInputPackage;
import io.invertase.firebase.fabric.crashlytics.RNFirebaseCrashlyticsPackage;
import io.invertase.firebase.analytics.RNFirebaseAnalyticsPackage;
import io.invertase.firebase.perf.RNFirebasePerformancePackage;
import com.nozbe.watermelondb.WatermelonDBPackage; import com.nozbe.watermelondb.WatermelonDBPackage;
import com.reactnativecommunity.viewpager.RNCViewPagerPackage; import com.reactnativecommunity.viewpager.RNCViewPagerPackage;

View File

@ -1,10 +1,10 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext { ext {
buildToolsVersion = "28.0.3" buildToolsVersion = "29.0.2"
minSdkVersion = 21 minSdkVersion = 21
compileSdkVersion = 28 compileSdkVersion = 29
targetSdkVersion = 28 targetSdkVersion = 29
glideVersion = "4.9.0" glideVersion = "4.9.0"
kotlin_version = "1.3.50" kotlin_version = "1.3.50"
supportLibVersion = "28.0.0" supportLibVersion = "28.0.0"
@ -24,11 +24,11 @@ buildscript {
dependencies { dependencies {
if (isPlay) { if (isPlay) {
classpath 'com.google.gms:google-services:4.2.0' classpath 'com.google.gms:google-services:4.2.0'
classpath 'io.fabric.tools:gradle:1.28.1'
classpath 'com.google.firebase:perf-plugin:1.2.1' classpath 'com.google.firebase:perf-plugin:1.2.1'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.0.0'
classpath 'com.bugsnag:bugsnag-android-gradle-plugin:4.+' classpath 'com.bugsnag:bugsnag-android-gradle-plugin:4.+'
} }
classpath 'com.android.tools.build:gradle:3.5.2' classpath 'com.android.tools.build:gradle:3.5.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

View File

@ -27,7 +27,7 @@ android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
# Version of flipper SDK to use with React Native # Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.33.1 FLIPPER_VERSION=0.37.0
# App properties # App properties
VERSIONCODE=999999999 VERSIONCODE=999999999

29
android/gradlew vendored
View File

@ -154,19 +154,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
else else
eval `echo args$i`="\"$arg\"" eval `echo args$i`="\"$arg\""
fi fi
i=$((i+1)) i=`expr $i + 1`
done done
case $i in case $i in
(0) set -- ;; 0) set -- ;;
(1) set -- "$args0" ;; 1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;; 2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;; 3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac esac
fi fi
@ -175,14 +175,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " " echo " "
} }
APP_ARGS=$(save "$@") APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules # Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"

5
android/gradlew.bat vendored
View File

@ -5,7 +5,7 @@
@rem you may not use this file except in compliance with the License. @rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at @rem You may obtain a copy of the License at
@rem @rem
@rem http://www.apache.org/licenses/LICENSE-2.0 @rem https://www.apache.org/licenses/LICENSE-2.0
@rem @rem
@rem Unless required by applicable law or agreed to in writing, software @rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS, @rem distributed under the License is distributed on an "AS IS" BASIS,
@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

View File

@ -15,6 +15,7 @@ import OrSeparator from './OrSeparator';
import Touch from '../utils/touch'; import Touch from '../utils/touch';
import I18n from '../i18n'; import I18n from '../i18n';
import random from '../utils/random'; import random from '../utils/random';
import { logEvent, events } from '../utils/log';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
const BUTTON_HEIGHT = 48; const BUTTON_HEIGHT = 48;
@ -77,6 +78,7 @@ class LoginServices extends React.PureComponent {
} }
onPressFacebook = () => { onPressFacebook = () => {
logEvent(events.LOGIN_WITH_FACEBOOK);
const { services, server } = this.props; const { services, server } = this.props;
const { clientId } = services.facebook; const { clientId } = services.facebook;
const endpoint = 'https://m.facebook.com/v2.9/dialog/oauth'; const endpoint = 'https://m.facebook.com/v2.9/dialog/oauth';
@ -88,6 +90,7 @@ class LoginServices extends React.PureComponent {
} }
onPressGithub = () => { onPressGithub = () => {
logEvent(events.LOGIN_WITH_GITHUB);
const { services, server } = this.props; const { services, server } = this.props;
const { clientId } = services.github; const { clientId } = services.github;
const endpoint = `https://github.com/login?client_id=${ clientId }&return_to=${ encodeURIComponent('/login/oauth/authorize') }`; const endpoint = `https://github.com/login?client_id=${ clientId }&return_to=${ encodeURIComponent('/login/oauth/authorize') }`;
@ -99,6 +102,7 @@ class LoginServices extends React.PureComponent {
} }
onPressGitlab = () => { onPressGitlab = () => {
logEvent(events.LOGIN_WITH_GITLAB);
const { services, server, Gitlab_URL } = this.props; const { services, server, Gitlab_URL } = this.props;
const { clientId } = services.gitlab; const { clientId } = services.gitlab;
const baseURL = Gitlab_URL ? Gitlab_URL.trim().replace(/\/*$/, '') : 'https://gitlab.com'; const baseURL = Gitlab_URL ? Gitlab_URL.trim().replace(/\/*$/, '') : 'https://gitlab.com';
@ -111,6 +115,7 @@ class LoginServices extends React.PureComponent {
} }
onPressGoogle = () => { onPressGoogle = () => {
logEvent(events.LOGIN_WITH_GOOGLE);
const { services, server } = this.props; const { services, server } = this.props;
const { clientId } = services.google; const { clientId } = services.google;
const endpoint = 'https://accounts.google.com/o/oauth2/auth'; const endpoint = 'https://accounts.google.com/o/oauth2/auth';
@ -122,6 +127,7 @@ class LoginServices extends React.PureComponent {
} }
onPressLinkedin = () => { onPressLinkedin = () => {
logEvent(events.LOGIN_WITH_LINKEDIN);
const { services, server } = this.props; const { services, server } = this.props;
const { clientId } = services.linkedin; const { clientId } = services.linkedin;
const endpoint = 'https://www.linkedin.com/oauth/v2/authorization'; const endpoint = 'https://www.linkedin.com/oauth/v2/authorization';
@ -133,6 +139,7 @@ class LoginServices extends React.PureComponent {
} }
onPressMeteor = () => { onPressMeteor = () => {
logEvent(events.LOGIN_WITH_METEOR);
const { services, server } = this.props; const { services, server } = this.props;
const { clientId } = services['meteor-developer']; const { clientId } = services['meteor-developer'];
const endpoint = 'https://www.meteor.com/oauth2/authorize'; const endpoint = 'https://www.meteor.com/oauth2/authorize';
@ -143,6 +150,7 @@ class LoginServices extends React.PureComponent {
} }
onPressTwitter = () => { onPressTwitter = () => {
logEvent(events.LOGIN_WITH_TWITTER);
const { server } = this.props; const { server } = this.props;
const state = this.getOAuthState(); const state = this.getOAuthState();
const url = `${ server }/_oauth/twitter/?requestTokenAndRedirect=true&state=${ state }`; const url = `${ server }/_oauth/twitter/?requestTokenAndRedirect=true&state=${ state }`;
@ -150,6 +158,7 @@ class LoginServices extends React.PureComponent {
} }
onPressWordpress = () => { onPressWordpress = () => {
logEvent(events.LOGIN_WITH_WORDPRESS);
const { services, server } = this.props; const { services, server } = this.props;
const { clientId, serverURL } = services.wordpress; const { clientId, serverURL } = services.wordpress;
const endpoint = `${ serverURL }/oauth/authorize`; const endpoint = `${ serverURL }/oauth/authorize`;
@ -161,6 +170,7 @@ class LoginServices extends React.PureComponent {
} }
onPressCustomOAuth = (loginService) => { onPressCustomOAuth = (loginService) => {
logEvent(events.LOGIN_WITH_CUSTOM_OAUTH);
const { server } = this.props; const { server } = this.props;
const { const {
serverURL, authorizePath, clientId, scope, service serverURL, authorizePath, clientId, scope, service
@ -175,6 +185,7 @@ class LoginServices extends React.PureComponent {
} }
onPressSaml = (loginService) => { onPressSaml = (loginService) => {
logEvent(events.LOGIN_WITH_SAML);
const { server } = this.props; const { server } = this.props;
const { clientConfig } = loginService; const { clientConfig } = loginService;
const { provider } = clientConfig; const { provider } = clientConfig;
@ -184,6 +195,7 @@ class LoginServices extends React.PureComponent {
} }
onPressCas = () => { onPressCas = () => {
logEvent(events.LOGIN_WITH_CAS);
const { server, CAS_login_url } = this.props; const { server, CAS_login_url } = this.props;
const ssoToken = random(17); const ssoToken = random(17);
const url = `${ CAS_login_url }?service=${ server }/_cas/${ ssoToken }`; const url = `${ CAS_login_url }?service=${ server }/_cas/${ ssoToken }`;

View File

@ -122,6 +122,7 @@ class MessageBox extends Component {
command: {} command: {}
}; };
this.text = ''; this.text = '';
this.selection = { start: 0, end: 0 };
this.focused = false; this.focused = false;
// MessageBox Actions // MessageBox Actions
@ -331,6 +332,10 @@ class MessageBox extends Component {
this.setInput(text); this.setInput(text);
} }
onSelectionChange = (e) => {
this.selection = e.nativeEvent.selection;
}
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
debouncedOnChangeText = debounce(async(text) => { debouncedOnChangeText = debounce(async(text) => {
const { sharing } = this.props; const { sharing } = this.props;
@ -358,9 +363,9 @@ class MessageBox extends Component {
if (!isTextEmpty) { if (!isTextEmpty) {
try { try {
const { start, end } = this.component?.lastNativeSelection; const { start, end } = this.selection;
const cursor = Math.max(start, end); const cursor = Math.max(start, end);
const lastNativeText = this.component?.lastNativeText || ''; const lastNativeText = this.text;
// matches if text either starts with '/' or have (@,#,:) then it groups whatever comes next of mention type // matches if text either starts with '/' or have (@,#,:) then it groups whatever comes next of mention type
let regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im; let regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im;
@ -399,7 +404,7 @@ class MessageBox extends Component {
} }
const { trackingType } = this.state; const { trackingType } = this.state;
const msg = this.text; const msg = this.text;
const { start, end } = this.component?.lastNativeSelection; const { start, end } = this.selection;
const cursor = Math.max(start, end); const cursor = Math.max(start, end);
const regexp = /([a-z0-9._-]+)$/im; const regexp = /([a-z0-9._-]+)$/im;
const result = msg.substr(0, cursor).replace(regexp, ''); const result = msg.substr(0, cursor).replace(regexp, '');
@ -410,7 +415,8 @@ class MessageBox extends Component {
if ((trackingType === MENTIONS_TRACKING_TYPE_COMMANDS) && item.providesPreview) { if ((trackingType === MENTIONS_TRACKING_TYPE_COMMANDS) && item.providesPreview) {
this.setState({ showCommandPreview: true }); this.setState({ showCommandPreview: true });
} }
this.setInput(text); const newCursor = cursor + mentionName.length;
this.setInput(text, { start: newCursor, end: newCursor });
this.focus(); this.focus();
requestAnimationFrame(() => this.stopTrackingMention()); requestAnimationFrame(() => this.stopTrackingMention());
} }
@ -443,15 +449,11 @@ class MessageBox extends Component {
let newText = ''; let newText = '';
// if messagebox has an active cursor // if messagebox has an active cursor
if (this.component?.lastNativeSelection) { const { start, end } = this.selection;
const { start, end } = this.component.lastNativeSelection;
const cursor = Math.max(start, end); const cursor = Math.max(start, end);
newText = `${ text.substr(0, cursor) }${ emoji }${ text.substr(cursor) }`; newText = `${ text.substr(0, cursor) }${ emoji }${ text.substr(cursor) }`;
} else { const newCursor = cursor + emoji.length;
// if messagebox doesn't have a cursor, just append selected emoji this.setInput(newText, { start: newCursor, end: newCursor });
newText = `${ text }${ emoji }`;
}
this.setInput(newText);
this.setShowSend(true); this.setShowSend(true);
} }
@ -551,11 +553,12 @@ class MessageBox extends Component {
this.setState({ commandPreview: [], showCommandPreview: true, command: {} }); this.setState({ commandPreview: [], showCommandPreview: true, command: {} });
} }
setInput = (text) => { setInput = (text, selection) => {
this.text = text; this.text = text;
if (this.component && this.component.setNativeProps) { if (selection) {
this.component.setNativeProps({ text }); return this.component.setTextAndSelection(text, selection);
} }
this.component.setNativeProps({ text });
} }
setShowSend = (showSend) => { setShowSend = (showSend) => {
@ -888,6 +891,7 @@ class MessageBox extends Component {
blurOnSubmit={false} blurOnSubmit={false}
placeholder={I18n.t('New_Message')} placeholder={I18n.t('New_Message')}
onChangeText={this.onChangeText} onChangeText={this.onChangeText}
onSelectionChange={this.onSelectionChange}
underlineColorAndroid='transparent' underlineColorAndroid='transparent'
defaultValue='' defaultValue=''
multiline multiline

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { View, Text } from 'react-native'; import { View, Text, InteractionManager } from 'react-native';
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { sha256 } from 'js-sha256'; import { sha256 } from 'js-sha256';
@ -99,6 +99,7 @@ const TwoFactor = React.memo(({ theme, isMasterDetail }) => {
<TextInput <TextInput
value={code} value={code}
theme={theme} theme={theme}
inputRef={e => InteractionManager.runAfterInteractions(() => e?.getNativeRef()?.focus())}
returnKeyType='send' returnKeyType='send'
autoCapitalize='none' autoCapitalize='none'
onChangeText={setCode} onChangeText={setCode}

View File

@ -14,9 +14,10 @@ export default StyleSheet.create({
borderRadius: 4 borderRadius: 4
}, },
title: { title: {
fontSize: 14, fontSize: 16,
paddingBottom: 8, paddingBottom: 8,
...sharedStyles.textBold ...sharedStyles.textBold,
...sharedStyles.textAlignCenter
}, },
subtitle: { subtitle: {
fontSize: 14, fontSize: 14,

View File

@ -845,6 +845,12 @@ const RocketChat = {
return other && other.length ? other[0] : me; return other && other.length ? other[0] : me;
}, },
isRead(item) {
let isUnread = item.archived !== true && item.open === true; // item is not archived and not opened
isUnread = isUnread && (item.unread > 0 || item.alert === true); // either its unread count > 0 or its alert
return !isUnread;
},
isGroupChat(room) { isGroupChat(room) {
return (room.uids && room.uids.length > 2) || (room.usernames && room.usernames.length > 2); return (room.uids && room.uids.length > 2) || (room.usernames && room.usernames.length > 2);
}, },

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import { KeyboardAwareScrollView } from '@codler/react-native-keyboard-aware-scroll-view';
import scrollPersistTaps from '../utils/scrollPersistTaps'; import scrollPersistTaps from '../utils/scrollPersistTaps';
export default class KeyboardView extends React.PureComponent { export default class KeyboardView extends React.PureComponent {

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -16,82 +16,79 @@ import { themes } from '../../constants/colors';
export { ROW_HEIGHT }; export { ROW_HEIGHT };
const attrs = [ const attrs = [
'name',
'unread',
'userMentions',
'showLastMessage',
'useRealName',
'alert',
'type',
'width', 'width',
'isRead',
'favorite',
'status', 'status',
'connected', 'connected',
'theme', 'theme',
'isFocused' 'isFocused',
'forceUpdate',
'showLastMessage'
]; ];
const arePropsEqual = (oldProps, newProps) => { const arePropsEqual = (oldProps, newProps) => attrs.every(key => oldProps[key] === newProps[key]);
const { _updatedAt: _updatedAtOld } = oldProps;
const { _updatedAt: _updatedAtNew } = newProps;
if (_updatedAtOld && _updatedAtNew && _updatedAtOld.toISOString() !== _updatedAtNew.toISOString()) {
return false;
}
return attrs.every(key => oldProps[key] === newProps[key]);
};
const RoomItem = React.memo(({ const RoomItem = React.memo(({
item,
onPress, onPress,
width, width,
favorite,
toggleFav, toggleFav,
isRead,
rid,
toggleRead, toggleRead,
hideChannel, hideChannel,
testID, testID,
unread,
userMentions,
name,
_updatedAt,
alert,
type,
avatarSize, avatarSize,
baseUrl, baseUrl,
userId, userId,
username, username,
token, token,
id, id,
prid,
showLastMessage, showLastMessage,
hideUnreadStatus,
lastMessage,
status, status,
avatar,
useRealName, useRealName,
getUserPresence, getUserPresence,
isGroupChat,
connected, connected,
theme, theme,
isFocused isFocused,
getRoomTitle,
getRoomAvatar,
getIsGroupChat,
getIsRead
}) => { }) => {
const [, setForceUpdate] = useState(1);
useEffect(() => { useEffect(() => {
if (connected && type === 'd' && id) { if (connected && item.t === 'd' && id) {
getUserPresence(id); getUserPresence(id);
} }
}, [connected]); }, [connected]);
const date = lastMessage && formatDate(lastMessage.ts); useEffect(() => {
if (item?.observe) {
const observable = item.observe();
const subscription = observable?.subscribe?.(() => {
setForceUpdate(prevForceUpdate => prevForceUpdate + 1);
});
return () => {
subscription?.unsubscribe?.();
};
}
}, []);
const name = getRoomTitle(item);
const avatar = getRoomAvatar(item);
const isGroupChat = getIsGroupChat(item);
const isRead = getIsRead(item);
const _onPress = () => onPress(item);
const date = item.lastMessage?.ts && formatDate(item.lastMessage.ts);
let accessibilityLabel = name; let accessibilityLabel = name;
if (unread === 1) { if (item.unread === 1) {
accessibilityLabel += `, ${ unread } ${ I18n.t('alert') }`; accessibilityLabel += `, ${ item.unread } ${ I18n.t('alert') }`;
} else if (unread > 1) { } else if (item.unread > 1) {
accessibilityLabel += `, ${ unread } ${ I18n.t('alerts') }`; accessibilityLabel += `, ${ item.unread } ${ I18n.t('alerts') }`;
} }
if (userMentions > 0) { if (item.userMentions > 0) {
accessibilityLabel += `, ${ I18n.t('you_were_mentioned') }`; accessibilityLabel += `, ${ I18n.t('you_were_mentioned') }`;
} }
@ -101,16 +98,16 @@ const RoomItem = React.memo(({
return ( return (
<Touchable <Touchable
onPress={onPress} onPress={_onPress}
width={width} width={width}
favorite={favorite} favorite={item.f}
toggleFav={toggleFav} toggleFav={toggleFav}
isRead={isRead} isRead={isRead}
rid={rid} rid={item.rid}
toggleRead={toggleRead} toggleRead={toggleRead}
hideChannel={hideChannel} hideChannel={hideChannel}
testID={testID} testID={testID}
type={type} type={item.t}
theme={theme} theme={theme}
isFocused={isFocused} isFocused={isFocused}
> >
@ -121,7 +118,7 @@ const RoomItem = React.memo(({
<Avatar <Avatar
text={avatar} text={avatar}
size={avatarSize} size={avatarSize}
type={type} type={item.t}
baseUrl={baseUrl} baseUrl={baseUrl}
style={styles.avatar} style={styles.avatar}
userId={userId} userId={userId}
@ -137,8 +134,8 @@ const RoomItem = React.memo(({
> >
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<TypeIcon <TypeIcon
type={type} type={item.t}
prid={prid} prid={item.prid}
status={status} status={status}
isGroupChat={isGroupChat} isGroupChat={isGroupChat}
theme={theme} theme={theme}
@ -146,7 +143,7 @@ const RoomItem = React.memo(({
<Text <Text
style={[ style={[
styles.title, styles.title,
alert && !hideUnreadStatus && styles.alert, item.alert && !item.hideUnreadStatus && styles.alert,
{ color: themes[theme].titleText } { color: themes[theme].titleText }
]} ]}
ellipsizeMode='tail' ellipsizeMode='tail'
@ -154,7 +151,7 @@ const RoomItem = React.memo(({
> >
{name} {name}
</Text> </Text>
{_updatedAt ? ( {item.roomUpdatedAt ? (
<Text <Text
style={[ style={[
styles.date, styles.date,
@ -163,7 +160,7 @@ const RoomItem = React.memo(({
themes[theme] themes[theme]
.auxiliaryText .auxiliaryText
}, },
alert && !hideUnreadStatus && [ item.alert && !item.hideUnreadStatus && [
styles.updateAlert, styles.updateAlert,
{ {
color: color:
@ -181,18 +178,18 @@ const RoomItem = React.memo(({
</View> </View>
<View style={styles.row}> <View style={styles.row}>
<LastMessage <LastMessage
lastMessage={lastMessage} lastMessage={item.lastMessage}
type={type} type={item.t}
showLastMessage={showLastMessage} showLastMessage={showLastMessage}
username={username} username={username}
alert={alert && !hideUnreadStatus} alert={item.alert && !item.hideUnreadStatus}
useRealName={useRealName} useRealName={useRealName}
theme={theme} theme={theme}
/> />
<UnreadBadge <UnreadBadge
unread={unread} unread={item.unread}
userMentions={userMentions} userMentions={item.userMentions}
type={type} type={item.t}
theme={theme} theme={theme}
/> />
</View> </View>
@ -203,17 +200,10 @@ const RoomItem = React.memo(({
}, arePropsEqual); }, arePropsEqual);
RoomItem.propTypes = { RoomItem.propTypes = {
type: PropTypes.string.isRequired, item: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired, baseUrl: PropTypes.string.isRequired,
showLastMessage: PropTypes.bool, showLastMessage: PropTypes.bool,
_updatedAt: PropTypes.string,
lastMessage: PropTypes.object,
alert: PropTypes.bool,
unread: PropTypes.number,
userMentions: PropTypes.number,
id: PropTypes.string, id: PropTypes.string,
prid: PropTypes.string,
onPress: PropTypes.func, onPress: PropTypes.func,
userId: PropTypes.string, userId: PropTypes.string,
username: PropTypes.string, username: PropTypes.string,
@ -221,27 +211,29 @@ RoomItem.propTypes = {
avatarSize: PropTypes.number, avatarSize: PropTypes.number,
testID: PropTypes.string, testID: PropTypes.string,
width: PropTypes.number, width: PropTypes.number,
favorite: PropTypes.bool,
isRead: PropTypes.bool,
rid: PropTypes.string,
status: PropTypes.string, status: PropTypes.string,
toggleFav: PropTypes.func, toggleFav: PropTypes.func,
toggleRead: PropTypes.func, toggleRead: PropTypes.func,
hideChannel: PropTypes.func, hideChannel: PropTypes.func,
avatar: PropTypes.bool,
hideUnreadStatus: PropTypes.bool,
useRealName: PropTypes.bool, useRealName: PropTypes.bool,
getUserPresence: PropTypes.func, getUserPresence: PropTypes.func,
connected: PropTypes.bool, connected: PropTypes.bool,
isGroupChat: PropTypes.bool,
theme: PropTypes.string, theme: PropTypes.string,
isFocused: PropTypes.bool isFocused: PropTypes.bool,
getRoomTitle: PropTypes.func,
getRoomAvatar: PropTypes.func,
getIsGroupChat: PropTypes.func,
getIsRead: PropTypes.func
}; };
RoomItem.defaultProps = { RoomItem.defaultProps = {
avatarSize: 48, avatarSize: 48,
status: 'offline', status: 'offline',
getUserPresence: () => {} getUserPresence: () => {},
getRoomTitle: () => 'title',
getRoomAvatar: () => '',
getIsGroupChat: () => false,
getIsRead: () => false
}; };
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {

View File

@ -5,12 +5,12 @@ import RNUserDefaults from 'rn-user-defaults';
import Navigation from '../lib/Navigation'; import Navigation from '../lib/Navigation';
import * as types from '../actions/actionsTypes'; import * as types from '../actions/actionsTypes';
import { selectServerRequest } from '../actions/server'; import { selectServerRequest, serverInitAdd } from '../actions/server';
import { inviteLinksSetToken, inviteLinksRequest } from '../actions/inviteLinks'; import { inviteLinksSetToken, inviteLinksRequest } from '../actions/inviteLinks';
import database from '../lib/database'; import database from '../lib/database';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import EventEmitter from '../utils/events'; import EventEmitter from '../utils/events';
import { appStart, ROOT_INSIDE } from '../actions/app'; import { appStart, ROOT_INSIDE, ROOT_NEW_SERVER } from '../actions/app';
import { localAuthenticate } from '../utils/localAuthentication'; import { localAuthenticate } from '../utils/localAuthentication';
import { goRoom } from '../utils/goRoom'; import { goRoom } from '../utils/goRoom';
@ -106,7 +106,8 @@ const handleOpen = function* handleOpen({ params }) {
if (!result.success) { if (!result.success) {
return; return;
} }
Navigation.navigate('NewServerView', { previousServer: server }); yield put(appStart({ root: ROOT_NEW_SERVER }));
yield put(serverInitAdd(server));
yield delay(1000); yield delay(1000);
EventEmitter.emit('NewServer', { server: host }); EventEmitter.emit('NewServer', { server: host });

View File

@ -17,7 +17,7 @@ import {
import { roomsRequest } from '../actions/rooms'; import { roomsRequest } from '../actions/rooms';
import { toMomentLocale } from '../utils/moment'; import { toMomentLocale } from '../utils/moment';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import log from '../utils/log'; import log, { logEvent, events } from '../utils/log';
import I18n from '../i18n'; import I18n from '../i18n';
import database from '../lib/database'; import database from '../lib/database';
import EventEmitter from '../utils/events'; import EventEmitter from '../utils/events';
@ -32,6 +32,7 @@ const loginCall = args => RocketChat.login(args);
const logoutCall = args => RocketChat.logout(args); const logoutCall = args => RocketChat.logout(args);
const handleLoginRequest = function* handleLoginRequest({ credentials, logoutOnError = false }) { const handleLoginRequest = function* handleLoginRequest({ credentials, logoutOnError = false }) {
logEvent(events.DEFAULT_LOGIN);
try { try {
let result; let result;
if (credentials.resume) { if (credentials.resume) {
@ -52,6 +53,7 @@ const handleLoginRequest = function* handleLoginRequest({ credentials, logoutOnE
if (logoutOnError && (e.data && e.data.message && /you've been logged out by the server/i.test(e.data.message))) { if (logoutOnError && (e.data && e.data.message && /you've been logged out by the server/i.test(e.data.message))) {
yield put(logout(true)); yield put(logout(true));
} else { } else {
logEvent(events.DEFAULT_LOGIN_FAIL);
yield put(loginFailure(e)); yield put(loginFailure(e));
} }
} }

View File

@ -112,7 +112,6 @@ const ChatsStackNavigator = () => {
<ChatsStack.Screen <ChatsStack.Screen
name='MessagesView' name='MessagesView'
component={MessagesView} component={MessagesView}
options={MessagesView.navigationOptions}
/> />
<ChatsStack.Screen <ChatsStack.Screen
name='AutoTranslateView' name='AutoTranslateView'

View File

@ -139,7 +139,6 @@ const ModalStackNavigator = React.memo(({ navigation }) => {
<ModalStack.Screen <ModalStack.Screen
name='MessagesView' name='MessagesView'
component={MessagesView} component={MessagesView}
options={MessagesView.navigationOptions}
/> />
<ModalStack.Screen <ModalStack.Screen
name='AutoTranslateView' name='AutoTranslateView'

24
app/utils/log/events.js Normal file
View File

@ -0,0 +1,24 @@
export default {
JOIN_A_WORKSPACE: 'join_a_workspace',
CREATE_NEW_WORKSPACE: 'create_new_workspace',
CREATE_NEW_WORKSPACE_FAIL: 'create_new_workspace_fail',
CONNECT_TO_WORKSPACE: 'connect_to_workspace',
CONNECT_TO_WORKSPACE_FAIL: 'connect_to_workspace_fail',
JOIN_OPEN_WORKSPACE: 'join_open_workspace',
DEFAULT_LOGIN: 'default_login',
DEFAULT_LOGIN_FAIL: 'default_login_fail',
DEFAULT_SIGN_UP: 'default_sign_up',
DEFAULT_SIGN_UP_FAIL: 'default_sign_up_fail',
FORGOT_PASSWORD: 'forgot_password',
LOGIN_WITH_FACEBOOK: 'login_with_facebook',
LOGIN_WITH_GITHUB: 'login_with_github',
LOGIN_WITH_GITLAB: 'login_with_gitlab',
LOGIN_WITH_LINKEDIN: 'login_with_linkedin',
LOGIN_WITH_GOOGLE: 'login_with_google',
LOGIN_WITH_METEOR: 'login_with_meteor',
LOGIN_WITH_TWITTER: 'login_with_twitter',
LOGIN_WITH_WORDPRESS: 'login_with_wordpress',
LOGIN_WITH_CUSTOM_OAUTH: 'login_with_custom_oauth',
LOGIN_WITH_SAML: 'login_with_saml',
LOGIN_WITH_CAS: 'login_with_cas'
};

View File

@ -1,16 +1,16 @@
import { Client } from 'bugsnag-react-native'; import { Client } from 'bugsnag-react-native';
import firebase from 'react-native-firebase'; import analytics from '@react-native-firebase/analytics';
import crashlytics from '@react-native-firebase/crashlytics';
import { isGooglePlayBuild } from '../constants/environment'; import { isGooglePlayBuild } from '../constants/environment';
import config from '../../config'; import config from '../../../config';
import events from './events';
const bugsnag = new Client(config.BUGSNAG_API_KEY); const bugsnag = new Client(config.BUGSNAG_API_KEY);
export const analytics = isGooglePlayBuild ? firebase.analytics : ({ export { analytics };
logEvent: () => { }
});
export const loggerConfig = bugsnag.config; export const loggerConfig = bugsnag.config;
export const { leaveBreadcrumb } = bugsnag; export const { leaveBreadcrumb } = bugsnag;
export { events };
let metadata = {}; let metadata = {};
@ -20,6 +20,11 @@ export const logServerVersion = (serverVersion) => {
}; };
}; };
export const logEvent = (eventName, payload) => {
analytics().logEvent(eventName, payload);
leaveBreadcrumb(eventName, payload);
};
export const setCurrentScreen = (currentScreen) => { export const setCurrentScreen = (currentScreen) => {
if (isGooglePlayBuild) { if (isGooglePlayBuild) {
analytics().setCurrentScreen(currentScreen); analytics().setCurrentScreen(currentScreen);
@ -36,6 +41,7 @@ export default (e) => {
} }
}; };
}); });
crashlytics().recordError(e);
} else { } else {
console.log(e); console.log(e);
} }

View File

@ -31,6 +31,8 @@ class AdminPanelView extends React.Component {
<SafeAreaView theme={theme}> <SafeAreaView theme={theme}>
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<WebView <WebView
// https://github.com/react-native-community/react-native-webview/issues/1311
onMessage={() => {}}
source={{ uri: `${ baseUrl }/admin/info?layout=embedded` }} source={{ uri: `${ baseUrl }/admin/info?layout=embedded` }}
injectedJavaScript={`Meteor.loginWithToken('${ token }', function() { })`} injectedJavaScript={`Meteor.loginWithToken('${ token }', function() { })`}
/> />

View File

@ -19,11 +19,11 @@ import SafeAreaView from '../containers/SafeAreaView';
const DEFAULT_BROWSERS = [ const DEFAULT_BROWSERS = [
{ {
title: I18n.t('In_app'), title: 'In_app',
value: 'inApp' value: 'inApp'
}, },
{ {
title: isIOS ? 'Safari' : I18n.t('Browser'), title: isIOS ? 'Safari' : 'Browser',
value: 'systemDefault:' value: 'systemDefault:'
} }
]; ];
@ -137,7 +137,7 @@ class DefaultBrowserView extends React.Component {
const { title, value } = item; const { title, value } = item;
return ( return (
<ListItem <ListItem
title={title} title={I18n.t(title, { defaultValue: title })}
onPress={() => this.changeDefaultBrowser(value)} onPress={() => this.changeDefaultBrowser(value)}
testID={`default-browser-view-${ title }`} testID={`default-browser-view-${ title }`}
right={this.isSelected(value) ? this.renderIcon : null} right={this.isSelected(value) ? this.renderIcon : null}

View File

@ -21,9 +21,6 @@ import SafeAreaView from '../../containers/SafeAreaView';
const OPTIONS = { const OPTIONS = {
days: [{ days: [{
label: I18n.t('Never'), value: 0
},
{
label: '1', value: 1 label: '1', value: 1
}, },
{ {
@ -36,9 +33,6 @@ const OPTIONS = {
label: '30', value: 30 label: '30', value: 30
}], }],
maxUses: [{ maxUses: [{
label: I18n.t('No_limit'), value: 0
},
{
label: '1', value: 1 label: '1', value: 1
}, },
{ {
@ -91,9 +85,12 @@ class InviteUsersView extends React.Component {
navigation.pop(); navigation.pop();
} }
renderPicker = (key) => { renderPicker = (key, first) => {
const { props } = this; const { props } = this;
const { theme } = props; const { theme } = props;
const firstEl = [{
label: I18n.t(first), value: 0
}];
return ( return (
<RNPickerSelect <RNPickerSelect
style={{ viewContainer: styles.viewContainer }} style={{ viewContainer: styles.viewContainer }}
@ -102,7 +99,7 @@ class InviteUsersView extends React.Component {
useNativeAndroidPickerStyle={false} useNativeAndroidPickerStyle={false}
placeholder={{}} placeholder={{}}
onValueChange={value => this.onValueChangePicker(key, value)} onValueChange={value => this.onValueChangePicker(key, value)}
items={OPTIONS[key]} items={firstEl.concat(OPTIONS[key])}
/> />
); );
} }
@ -121,13 +118,13 @@ class InviteUsersView extends React.Component {
<Separator theme={theme} /> <Separator theme={theme} />
<ListItem <ListItem
title={I18n.t('Expiration_Days')} title={I18n.t('Expiration_Days')}
right={() => this.renderPicker('days')} right={() => this.renderPicker('days', 'Never')}
theme={theme} theme={theme}
/> />
<Separator theme={theme} /> <Separator theme={theme} />
<ListItem <ListItem
title={I18n.t('Max_number_of_uses')} title={I18n.t('Max_number_of_uses')}
right={() => this.renderPicker('maxUses')} right={() => this.renderPicker('maxUses', 'No_limit')}
theme={theme} theme={theme}
/> />
<Separator theme={theme} /> <Separator theme={theme} />

View File

@ -6,7 +6,7 @@ import {
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import equal from 'deep-equal'; import equal from 'deep-equal';
import { analytics } from '../utils/log'; import { logEvent, events } from '../utils/log';
import sharedStyles from './Styles'; import sharedStyles from './Styles';
import Button from '../containers/Button'; import Button from '../containers/Button';
import I18n from '../i18n'; import I18n from '../i18n';
@ -17,7 +17,6 @@ import FormContainer, { FormContainerInner } from '../containers/FormContainer';
import TextInput from '../containers/TextInput'; import TextInput from '../containers/TextInput';
import { loginRequest as loginRequestAction } from '../actions/login'; import { loginRequest as loginRequestAction } from '../actions/login';
import LoginServices from '../containers/LoginServices'; import LoginServices from '../containers/LoginServices';
import { isGooglePlayBuild } from '../constants/environment';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
registerDisabled: { registerDisabled: {
@ -104,6 +103,7 @@ class LoginView extends React.Component {
} }
forgotPassword = () => { forgotPassword = () => {
logEvent(events.FORGOT_PASSWORD);
const { navigation, Site_Name } = this.props; const { navigation, Site_Name } = this.props;
navigation.navigate('ForgotPasswordView', { title: Site_Name }); navigation.navigate('ForgotPasswordView', { title: Site_Name });
} }
@ -122,9 +122,6 @@ class LoginView extends React.Component {
const { loginRequest } = this.props; const { loginRequest } = this.props;
Keyboard.dismiss(); Keyboard.dismiss();
loginRequest({ user, password }); loginRequest({ user, password });
if (isGooglePlayBuild) {
analytics().logEvent('login');
}
} }
renderUserForm = () => { renderUserForm = () => {

View File

@ -18,10 +18,6 @@ import { withActionSheet } from '../../containers/ActionSheet';
import SafeAreaView from '../../containers/SafeAreaView'; import SafeAreaView from '../../containers/SafeAreaView';
class MessagesView extends React.Component { class MessagesView extends React.Component {
static navigationOptions = ({ route }) => ({
title: I18n.t(route.params?.name)
});
static propTypes = { static propTypes = {
user: PropTypes.object, user: PropTypes.object,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
@ -39,6 +35,7 @@ class MessagesView extends React.Component {
messages: [], messages: [],
fileLoading: true fileLoading: true
}; };
this.setHeader();
this.rid = props.route.params?.rid; this.rid = props.route.params?.rid;
this.t = props.route.params?.t; this.t = props.route.params?.t;
this.content = this.defineMessagesViewContent(props.route.params?.name); this.content = this.defineMessagesViewContent(props.route.params?.name);
@ -65,10 +62,16 @@ class MessagesView extends React.Component {
if (fileLoading !== nextState.fileLoading) { if (fileLoading !== nextState.fileLoading) {
return true; return true;
} }
return false; return false;
} }
setHeader = () => {
const { route, navigation } = this.props;
navigation.setOptions({
title: I18n.t(route.params?.name)
});
}
navToRoomInfo = (navParam) => { navToRoomInfo = (navParam) => {
const { navigation, user } = this.props; const { navigation, user } = this.props;
if (navParam.rid === user.id) { if (navParam.rid === user.id) {

View File

@ -3,7 +3,7 @@ import { StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import { KeyboardAwareScrollView } from '@codler/react-native-keyboard-aware-scroll-view';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import EventEmitter from '../utils/events'; import EventEmitter from '../utils/events';

View File

@ -7,11 +7,12 @@ import { connect } from 'react-redux';
import * as FileSystem from 'expo-file-system'; import * as FileSystem from 'expo-file-system';
import DocumentPicker from 'react-native-document-picker'; import DocumentPicker from 'react-native-document-picker';
import RNUserDefaults from 'rn-user-defaults'; import RNUserDefaults from 'rn-user-defaults';
import { encode } from 'base-64'; import { Base64 } from 'js-base64';
import parse from 'url-parse'; import parse from 'url-parse';
import EventEmitter from '../utils/events'; import EventEmitter from '../utils/events';
import { selectServerRequest, serverRequest } from '../actions/server'; import { selectServerRequest, serverRequest } from '../actions/server';
import { inviteLinksClear as inviteLinksClearAction } from '../actions/inviteLinks';
import sharedStyles from './Styles'; import sharedStyles from './Styles';
import Button from '../containers/Button'; import Button from '../containers/Button';
import TextInput from '../containers/TextInput'; import TextInput from '../containers/TextInput';
@ -20,7 +21,7 @@ import FormContainer, { FormContainerInner } from '../containers/FormContainer';
import I18n from '../i18n'; import I18n from '../i18n';
import { isIOS } from '../utils/deviceInfo'; import { isIOS } from '../utils/deviceInfo';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import log from '../utils/log'; import log, { logEvent, events } from '../utils/log';
import { animateNextTransition } from '../utils/layoutAnimation'; import { animateNextTransition } from '../utils/layoutAnimation';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { setBasicAuth, BASIC_AUTH_KEY } from '../utils/fetch'; import { setBasicAuth, BASIC_AUTH_KEY } from '../utils/fetch';
@ -72,16 +73,13 @@ class NewServerView extends React.Component {
connectServer: PropTypes.func.isRequired, connectServer: PropTypes.func.isRequired,
selectServer: PropTypes.func.isRequired, selectServer: PropTypes.func.isRequired,
adding: PropTypes.bool, adding: PropTypes.bool,
previousServer: PropTypes.string previousServer: PropTypes.string,
inviteLinksClear: PropTypes.func
} }
constructor(props) { constructor(props) {
super(props); super(props);
if (props.adding) { this.setHeader();
props.navigation.setOptions({
headerLeft: () => <CloseModalButton navigation={props.navigation} onPress={this.close} testID='new-server-view-close' />
});
}
this.state = { this.state = {
text: '', text: '',
@ -92,11 +90,27 @@ class NewServerView extends React.Component {
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
} }
componentDidUpdate(prevProps) {
const { adding } = this.props;
if (prevProps.adding !== adding) {
this.setHeader();
}
}
componentWillUnmount() { componentWillUnmount() {
EventEmitter.removeListener('NewServer', this.handleNewServerEvent); EventEmitter.removeListener('NewServer', this.handleNewServerEvent);
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress); BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
} }
setHeader = () => {
const { adding, navigation } = this.props;
if (adding) {
navigation.setOptions({
headerLeft: () => <CloseModalButton navigation={navigation} onPress={this.close} testID='new-server-view-close' />
});
}
}
handleBackPress = () => { handleBackPress = () => {
const { navigation, previousServer } = this.props; const { navigation, previousServer } = this.props;
if (navigation.isFocused() && previousServer) { if (navigation.isFocused() && previousServer) {
@ -111,7 +125,8 @@ class NewServerView extends React.Component {
} }
close = () => { close = () => {
const { selectServer, previousServer } = this.props; const { selectServer, previousServer, inviteLinksClear } = this.props;
inviteLinksClear();
selectServer(previousServer); selectServer(previousServer);
} }
@ -124,6 +139,7 @@ class NewServerView extends React.Component {
} }
submit = async() => { submit = async() => {
logEvent(events.CONNECT_TO_WORKSPACE);
const { text, certificate } = this.state; const { text, certificate } = this.state;
const { connectServer } = this.props; const { connectServer } = this.props;
let cert = null; let cert = null;
@ -135,6 +151,7 @@ class NewServerView extends React.Component {
try { try {
await FileSystem.copyAsync({ from: certificate.path, to: certificatePath }); await FileSystem.copyAsync({ from: certificate.path, to: certificatePath });
} catch (e) { } catch (e) {
logEvent(events.CONNECT_TO_WORKSPACE_FAIL);
log(e); log(e);
} }
cert = { cert = {
@ -152,6 +169,7 @@ class NewServerView extends React.Component {
} }
connectOpen = () => { connectOpen = () => {
logEvent(events.JOIN_OPEN_WORKSPACE);
this.setState({ connectingOpen: true }); this.setState({ connectingOpen: true });
const { connectServer } = this.props; const { connectServer } = this.props;
connectServer('https://open.rocket.chat'); connectServer('https://open.rocket.chat');
@ -161,7 +179,7 @@ class NewServerView extends React.Component {
try { try {
const parsedUrl = parse(text, true); const parsedUrl = parse(text, true);
if (parsedUrl.auth.length) { if (parsedUrl.auth.length) {
const credentials = encode(parsedUrl.auth); const credentials = Base64.encode(parsedUrl.auth);
await RNUserDefaults.set(`${ BASIC_AUTH_KEY }-${ server }`, credentials); await RNUserDefaults.set(`${ BASIC_AUTH_KEY }-${ server }`, credentials);
setBasicAuth(credentials); setBasicAuth(credentials);
} }
@ -321,7 +339,8 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
connectServer: (server, certificate) => dispatch(serverRequest(server, certificate)), connectServer: (server, certificate) => dispatch(serverRequest(server, certificate)),
selectServer: server => dispatch(selectServerRequest(server)) selectServer: server => dispatch(selectServerRequest(server)),
inviteLinksClear: () => dispatch(inviteLinksClearAction())
}); });
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(NewServerView)); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(NewServerView));

View File

@ -71,58 +71,58 @@ Info.propTypes = {
const OPTIONS = { const OPTIONS = {
desktopNotifications: [{ desktopNotifications: [{
label: I18n.t('Default'), value: 'default' label: 'Default', value: 'default'
}, { }, {
label: I18n.t('All_Messages'), value: 'all' label: 'All_Messages', value: 'all'
}, { }, {
label: I18n.t('Mentions'), value: 'mentions' label: 'Mentions', value: 'mentions'
}, { }, {
label: I18n.t('Nothing'), value: 'nothing' label: 'Nothing', value: 'nothing'
}], }],
audioNotifications: [{ audioNotifications: [{
label: I18n.t('Default'), value: 'default' label: 'Default', value: 'default'
}, { }, {
label: I18n.t('All_Messages'), value: 'all' label: 'All_Messages', value: 'all'
}, { }, {
label: I18n.t('Mentions'), value: 'mentions' label: 'Mentions', value: 'mentions'
}, { }, {
label: I18n.t('Nothing'), value: 'nothing' label: 'Nothing', value: 'nothing'
}], }],
mobilePushNotifications: [{ mobilePushNotifications: [{
label: I18n.t('Default'), value: 'default' label: 'Default', value: 'default'
}, { }, {
label: I18n.t('All_Messages'), value: 'all' label: 'All_Messages', value: 'all'
}, { }, {
label: I18n.t('Mentions'), value: 'mentions' label: 'Mentions', value: 'mentions'
}, { }, {
label: I18n.t('Nothing'), value: 'nothing' label: 'Nothing', value: 'nothing'
}], }],
emailNotifications: [{ emailNotifications: [{
label: I18n.t('Default'), value: 'default' label: 'Default', value: 'default'
}, { }, {
label: I18n.t('All_Messages'), value: 'all' label: 'All_Messages', value: 'all'
}, { }, {
label: I18n.t('Mentions'), value: 'mentions' label: 'Mentions', value: 'mentions'
}, { }, {
label: I18n.t('Nothing'), value: 'nothing' label: 'Nothing', value: 'nothing'
}], }],
desktopNotificationDuration: [{ desktopNotificationDuration: [{
label: I18n.t('Default'), value: 0 label: 'Default', value: 0
}, { }, {
label: I18n.t('Seconds', { second: 1 }), value: 1 label: 'Seconds', second: 1, value: 1
}, { }, {
label: I18n.t('Seconds', { second: 2 }), value: 2 label: 'Seconds', second: 2, value: 2
}, { }, {
label: I18n.t('Seconds', { second: 3 }), value: 3 label: 'Seconds', second: 3, value: 3
}, { }, {
label: I18n.t('Seconds', { second: 4 }), value: 4 label: 'Seconds', second: 4, value: 4
}, { }, {
label: I18n.t('Seconds', { second: 5 }), value: 5 label: 'Seconds', second: 5, value: 5
}], }],
audioNotificationValue: [{ audioNotificationValue: [{
label: 'None', value: 'none None' label: 'None', value: 'none None'
}, { }, {
label: I18n.t('Default'), value: '0 Default' label: 'Default', value: '0 Default'
}, { }, {
label: 'Beep', value: 'beep Beep' label: 'Beep', value: 'beep Beep'
}, { }, {
@ -229,7 +229,7 @@ class NotificationPreferencesView extends React.Component {
const { room } = this.state; const { room } = this.state;
const { theme } = this.props; const { theme } = this.props;
const text = room[key] ? OPTIONS[key].find(option => option.value === room[key]) : OPTIONS[key][0]; const text = room[key] ? OPTIONS[key].find(option => option.value === room[key]) : OPTIONS[key][0];
return <Text style={[styles.pickerText, { color: themes[theme].actionTintColor }]}>{text?.label}</Text>; return <Text style={[styles.pickerText, { color: themes[theme].actionTintColor }]}>{I18n.t(text?.label, { defaultValue: text?.label, second: text?.second })}</Text>;
} }
renderSwitch = (key) => { renderSwitch = (key) => {

View File

@ -14,6 +14,7 @@ import { isTablet } from '../../utils/deviceInfo';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import FormContainer, { FormContainerInner } from '../../containers/FormContainer'; import FormContainer, { FormContainerInner } from '../../containers/FormContainer';
import { logEvent, events } from '../../utils/log';
class OnboardingView extends React.Component { class OnboardingView extends React.Component {
static navigationOptions = { static navigationOptions = {
@ -69,15 +70,17 @@ class OnboardingView extends React.Component {
} }
connectServer = () => { connectServer = () => {
logEvent(events.JOIN_A_WORKSPACE);
const { navigation } = this.props; const { navigation } = this.props;
navigation.navigate('NewServerView'); navigation.navigate('NewServerView');
} }
createWorkspace = async() => { createWorkspace = async() => {
logEvent(events.CREATE_NEW_WORKSPACE);
try { try {
await Linking.openURL('https://cloud.rocket.chat/trial'); await Linking.openURL('https://cloud.rocket.chat/trial');
} catch { } catch {
// do nothing logEvent(events.CREATE_NEW_WORKSPACE_FAIL);
} }
} }

View File

@ -42,7 +42,7 @@ const Item = React.memo(({
theme theme
}) => ( }) => (
<ListItem <ListItem
title={item.label} title={I18n.t(item.label, { defaultValue: item.label, second: item?.second })}
right={selected && (() => <Check theme={theme} style={styles.check} />)} right={selected && (() => <Check theme={theme} style={styles.check} />)}
onPress={onItemPress} onPress={onItemPress}
theme={theme} theme={theme}

View File

@ -6,7 +6,7 @@ import {
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import RNPickerSelect from 'react-native-picker-select'; import RNPickerSelect from 'react-native-picker-select';
import log from '../utils/log'; import log, { logEvent, events } from '../utils/log';
import sharedStyles from './Styles'; import sharedStyles from './Styles';
import Button from '../containers/Button'; import Button from '../containers/Button';
import I18n from '../i18n'; import I18n from '../i18n';
@ -114,6 +114,7 @@ class RegisterView extends React.Component {
} }
submit = async() => { submit = async() => {
logEvent(events.DEFAULT_SIGN_UP);
if (!this.valid()) { if (!this.valid()) {
return; return;
} }
@ -149,6 +150,7 @@ class RegisterView extends React.Component {
return loginRequest({ user: email, password }); return loginRequest({ user: email, password });
} }
if (e.data?.error) { if (e.data?.error) {
logEvent(events.DEFAULT_SIGN_UP_FAIL);
showErrorAlert(e.data.error, I18n.t('Oops')); showErrorAlert(e.data.error, I18n.t('Oops'));
} }
} }

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { FlatList, RefreshControl } from 'react-native'; import { FlatList, RefreshControl } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import orderBy from 'lodash/orderBy';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import moment from 'moment'; import moment from 'moment';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
@ -15,9 +14,10 @@ import EmptyRoom from './EmptyRoom';
import { isIOS } from '../../utils/deviceInfo'; import { isIOS } from '../../utils/deviceInfo';
import { animateNextTransition } from '../../utils/layoutAnimation'; import { animateNextTransition } from '../../utils/layoutAnimation';
import ActivityIndicator from '../../containers/ActivityIndicator'; import ActivityIndicator from '../../containers/ActivityIndicator';
import debounce from '../../utils/debounce';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
const QUERY_SIZE = 50;
class List extends React.Component { class List extends React.Component {
static propTypes = { static propTypes = {
onEndReached: PropTypes.func, onEndReached: PropTypes.func,
@ -47,7 +47,8 @@ class List extends React.Component {
super(props); super(props);
console.time(`${ this.constructor.name } init`); console.time(`${ this.constructor.name } init`);
console.time(`${ this.constructor.name } mount`); console.time(`${ this.constructor.name } mount`);
this.count = 0;
this.needsFetch = false;
this.mounted = false; this.mounted = false;
this.state = { this.state = {
loading: true, loading: true,
@ -56,7 +57,7 @@ class List extends React.Component {
refreshing: false, refreshing: false,
animated: false animated: false
}; };
this.init(); this.query();
this.unsubscribeFocus = props.navigation.addListener('focus', () => { this.unsubscribeFocus = props.navigation.addListener('focus', () => {
if (this.mounted) { if (this.mounted) {
this.setState({ animated: true }); this.setState({ animated: true });
@ -72,72 +73,6 @@ class List extends React.Component {
console.timeEnd(`${ this.constructor.name } mount`); console.timeEnd(`${ this.constructor.name } mount`);
} }
// eslint-disable-next-line react/sort-comp
async init() {
const { rid, tmid } = this.props;
const db = database.active;
// handle servers with version < 3.0.0
let { hideSystemMessages = [] } = this.props;
if (!Array.isArray(hideSystemMessages)) {
hideSystemMessages = [];
}
if (tmid) {
try {
this.thread = await db.collections
.get('threads')
.find(tmid);
} catch (e) {
console.log(e);
}
this.messagesObservable = db.collections
.get('thread_messages')
.query(Q.where('rid', tmid), Q.or(Q.where('t', Q.notIn(hideSystemMessages)), Q.where('t', Q.eq(null))))
.observe();
} else if (rid) {
this.messagesObservable = db.collections
.get('messages')
.query(Q.where('rid', rid), Q.or(Q.where('t', Q.notIn(hideSystemMessages)), Q.where('t', Q.eq(null))))
.observe();
}
if (rid) {
this.unsubscribeMessages();
this.messagesSubscription = this.messagesObservable
.subscribe((data) => {
if (tmid && this.thread) {
data = [this.thread, ...data];
}
const messages = orderBy(data, ['ts'], ['desc']);
if (this.mounted) {
this.setState({ messages }, () => this.update());
} else {
this.state.messages = messages;
}
this.readThreads();
});
}
}
// eslint-disable-next-line react/sort-comp
reload = () => {
this.unsubscribeMessages();
this.init();
}
readThreads = async() => {
const { tmid } = this.props;
if (tmid) {
try {
await RocketChat.readThreads(tmid);
} catch {
// Do nothing
}
}
}
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { loading, end, refreshing } = this.state; const { loading, end, refreshing } = this.state;
const { hideSystemMessages, theme } = this.props; const { hideSystemMessages, theme } = this.props;
@ -177,7 +112,7 @@ class List extends React.Component {
console.countReset(`${ this.constructor.name }.render calls`); console.countReset(`${ this.constructor.name }.render calls`);
} }
onEndReached = debounce(async() => { fetchData = async() => {
const { const {
loading, end, messages, latest = messages[messages.length - 1]?.ts loading, end, messages, latest = messages[messages.length - 1]?.ts
} = this.state; } = this.state;
@ -196,12 +131,99 @@ class List extends React.Component {
result = await RocketChat.loadMessagesForRoom({ rid, t, latest }); result = await RocketChat.loadMessagesForRoom({ rid, t, latest });
} }
this.setState({ end: result.length < 50, loading: false, latest: result[result.length - 1]?.ts }, () => this.loadMoreMessages(result)); this.setState({ end: result.length < QUERY_SIZE, loading: false, latest: result[result.length - 1]?.ts }, () => this.loadMoreMessages(result));
} catch (e) { } catch (e) {
this.setState({ loading: false }); this.setState({ loading: false });
log(e); log(e);
} }
}, 300) }
query = async() => {
this.count += QUERY_SIZE;
const { rid, tmid } = this.props;
const db = database.active;
// handle servers with version < 3.0.0
let { hideSystemMessages = [] } = this.props;
if (!Array.isArray(hideSystemMessages)) {
hideSystemMessages = [];
}
if (tmid) {
try {
this.thread = await db.collections
.get('threads')
.find(tmid);
} catch (e) {
console.log(e);
}
this.messagesObservable = db.collections
.get('thread_messages')
.query(
Q.where('rid', tmid),
Q.experimentalSortBy('ts', Q.desc),
Q.experimentalSkip(0),
Q.experimentalTake(this.count)
)
.observe();
} else if (rid) {
this.messagesObservable = db.collections
.get('messages')
.query(
Q.where('rid', rid),
Q.experimentalSortBy('ts', Q.desc),
Q.experimentalSkip(0),
Q.experimentalTake(this.count)
)
.observe();
}
if (rid) {
this.unsubscribeMessages();
this.messagesSubscription = this.messagesObservable
.subscribe((messages) => {
if (messages.length <= this.count) {
this.needsFetch = true;
}
if (tmid && this.thread) {
messages = [...messages, this.thread];
}
messages = messages.filter(m => !m.t || !hideSystemMessages?.includes(m.t));
if (this.mounted) {
this.setState({ messages }, () => this.update());
} else {
this.state.messages = messages;
}
this.readThreads();
});
}
}
reload = () => {
this.count = 0;
this.query();
}
readThreads = async() => {
const { tmid } = this.props;
if (tmid) {
try {
await RocketChat.readThreads(tmid);
} catch {
// Do nothing
}
}
}
onEndReached = async() => {
if (this.needsFetch) {
this.needsFetch = false;
await this.fetchData();
}
this.query();
}
loadMoreMessages = (result) => { loadMoreMessages = (result) => {
const { end } = this.state; const { end } = this.state;
@ -305,7 +327,7 @@ class List extends React.Component {
removeClippedSubviews={isIOS} removeClippedSubviews={isIOS}
initialNumToRender={7} initialNumToRender={7}
onEndReached={this.onEndReached} onEndReached={this.onEndReached}
onEndReachedThreshold={5} onEndReachedThreshold={0.5}
maxToRenderPerBatch={5} maxToRenderPerBatch={5}
windowSize={10} windowSize={10}
ListFooterComponent={this.renderFooter} ListFooterComponent={this.renderFooter}

View File

@ -206,12 +206,10 @@ class RoomView extends React.Component {
const { appState, insets } = this.props; const { appState, insets } = this.props;
if (appState === 'foreground' && appState !== prevProps.appState && this.rid) { if (appState === 'foreground' && appState !== prevProps.appState && this.rid) {
this.onForegroundInteraction = InteractionManager.runAfterInteractions(() => { // Fire List.query() just to keep observables working
// Fire List.init() just to keep observables working
if (this.list && this.list.current) { if (this.list && this.list.current) {
this.list.current.init(); this.list.current?.query?.();
} }
});
} }
// If it's not direct message // If it's not direct message
if (this.t !== 'd') { if (this.t !== 'd') {
@ -267,9 +265,6 @@ class RoomView extends React.Component {
if (this.didMountInteraction && this.didMountInteraction.cancel) { if (this.didMountInteraction && this.didMountInteraction.cancel) {
this.didMountInteraction.cancel(); this.didMountInteraction.cancel();
} }
if (this.onForegroundInteraction && this.onForegroundInteraction.cancel) {
this.onForegroundInteraction.cancel();
}
if (this.willBlurListener && this.willBlurListener.remove) { if (this.willBlurListener && this.willBlurListener.remove) {
this.willBlurListener.remove(); this.willBlurListener.remove();
} }

View File

@ -41,7 +41,7 @@ const Header = React.memo(({
const { isLandscape } = useOrientation(); const { isLandscape } = useOrientation();
const scale = isIOS && isLandscape && !isTablet ? 0.8 : 1; const scale = isIOS && isLandscape && !isTablet ? 0.8 : 1;
const titleFontSize = 16 * scale; const titleFontSize = 16 * scale;
const subTitleFontSize = 12 * scale; const subTitleFontSize = 14 * scale;
if (showSearchHeader) { if (showSearchHeader) {
return ( return (
@ -78,11 +78,11 @@ const Header = React.memo(({
<CustomIcon <CustomIcon
name='chevron-down' name='chevron-down'
color={themes[theme].headerTintColor} color={themes[theme].headerTintColor}
style={[showServerDropdown && styles.upsideDown, { fontSize: subTitleFontSize }]} style={[showServerDropdown && styles.upsideDown]}
size={18} size={18}
/> />
</View> </View>
{subtitle ? <Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>{subtitle}</Text> : null} {subtitle ? <Text style={[styles.subtitle, { color: themes[theme].auxiliaryText, fontSize: subTitleFontSize }]} numberOfLines={1}>{subtitle}</Text> : null}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
); );

View File

@ -9,7 +9,7 @@ import {
RefreshControl RefreshControl
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { isEqual, orderBy } from 'lodash'; import isEqual from 'react-fast-compare';
import Orientation from 'react-native-orientation-locker'; import Orientation from 'react-native-orientation-locker';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import { withSafeAreaInsets } from 'react-native-safe-area-context'; import { withSafeAreaInsets } from 'react-native-safe-area-context';
@ -71,6 +71,7 @@ const DISCUSSIONS_HEADER = 'Discussions';
const CHANNELS_HEADER = 'Channels'; const CHANNELS_HEADER = 'Channels';
const DM_HEADER = 'Direct_Messages'; const DM_HEADER = 'Direct_Messages';
const GROUPS_HEADER = 'Private_Groups'; const GROUPS_HEADER = 'Private_Groups';
const QUERY_SIZE = 20;
const filterIsUnread = s => (s.unread > 0 || s.alert) && !s.hideUnreadStatus; const filterIsUnread = s => (s.unread > 0 || s.alert) && !s.hideUnreadStatus;
const filterIsFavorite = s => s.f; const filterIsFavorite = s => s.f;
@ -140,11 +141,12 @@ class RoomsListView extends React.Component {
this.gotSubscriptions = false; this.gotSubscriptions = false;
this.animated = false; this.animated = false;
this.count = 0;
this.state = { this.state = {
searching: false, searching: false,
search: [], search: [],
loading: true, loading: true,
allChats: [], chatsOrder: [],
chats: [], chats: [],
item: {} item: {}
}; };
@ -211,7 +213,7 @@ class RoomsListView extends React.Component {
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { allChats, searching, item } = this.state; const { chatsOrder, searching, item } = this.state;
// eslint-disable-next-line react/destructuring-assignment // eslint-disable-next-line react/destructuring-assignment
const propsUpdated = shouldUpdateProps.some(key => nextProps[key] !== this.props[key]); const propsUpdated = shouldUpdateProps.some(key => nextProps[key] !== this.props[key]);
if (propsUpdated) { if (propsUpdated) {
@ -219,7 +221,7 @@ class RoomsListView extends React.Component {
} }
// Compare changes only once // Compare changes only once
const chatsNotEqual = !isEqual(nextState.allChats, allChats); const chatsNotEqual = !isEqual(nextState.chatsOrder, chatsOrder);
// If they aren't equal, set to update if focused // If they aren't equal, set to update if focused
if (chatsNotEqual) { if (chatsNotEqual) {
@ -290,7 +292,7 @@ class RoomsListView extends React.Component {
&& prevProps.showUnread === showUnread && prevProps.showUnread === showUnread
) )
) { ) {
this.getSubscriptions(true); this.getSubscriptions();
} else if ( } else if (
appState === 'foreground' appState === 'foreground'
&& appState !== prevProps.appState && appState !== prevProps.appState
@ -309,9 +311,7 @@ class RoomsListView extends React.Component {
} }
componentWillUnmount() { componentWillUnmount() {
if (this.querySubscription && this.querySubscription.unsubscribe) { this.unsubscribeQuery();
this.querySubscription.unsubscribe();
}
if (this.unsubscribeFocus) { if (this.unsubscribeFocus) {
this.unsubscribeFocus(); this.unsubscribeFocus();
} }
@ -396,17 +396,8 @@ class RoomsListView extends React.Component {
return allData; return allData;
} }
getSubscriptions = async(force = false) => { getSubscriptions = async() => {
if (this.gotSubscriptions && !force) { this.unsubscribeQuery();
return;
}
this.gotSubscriptions = true;
if (this.querySubscription && this.querySubscription.unsubscribe) {
this.querySubscription.unsubscribe();
}
this.setState({ loading: true });
const { const {
sortBy, sortBy,
@ -416,41 +407,49 @@ class RoomsListView extends React.Component {
} = this.props; } = this.props;
const db = database.active; const db = database.active;
const observable = await db.collections let observable;
.get('subscriptions')
.query( const defaultWhereClause = [
Q.where('archived', false), Q.where('archived', false),
Q.where('open', true) Q.where('open', true)
];
if (sortBy === 'alphabetical') {
defaultWhereClause.push(Q.experimentalSortBy(`${ this.useRealName ? 'fname' : 'name' }`, Q.asc));
} else {
defaultWhereClause.push(Q.experimentalSortBy('room_updated_at', Q.desc));
}
// When we're grouping by something
if (this.isGrouping) {
observable = await db.collections
.get('subscriptions')
.query(...defaultWhereClause)
.observe();
// When we're NOT grouping
} else {
this.count += QUERY_SIZE;
observable = await db.collections
.get('subscriptions')
.query(
...defaultWhereClause,
Q.experimentalSkip(0),
Q.experimentalTake(this.count)
) )
.observeWithColumns(['room_updated_at', 'unread', 'alert', 'user_mentions', 'f', 't']); .observe();
}
this.querySubscription = observable.subscribe((data) => { this.querySubscription = observable.subscribe((data) => {
let tempChats = []; let tempChats = [];
let chats = []; let chats = data;
if (sortBy === 'alphabetical') {
chats = orderBy(data, [`${ this.useRealName ? 'fname' : 'name' }`], ['asc']);
} else {
chats = orderBy(data, ['roomUpdatedAt'], ['desc']);
}
// it's better to map and test all subs altogether then testing them individually /**
const allChats = data.map(item => ({ * We trigger re-render only when chats order changes
alert: item.alert, * RoomItem handles its own re-render
unread: item.unread, */
userMentions: item.userMentions, const chatsOrder = data.map(item => item.rid);
isRead: this.getIsRead(item),
favorite: item.f,
lastMessage: item.lastMessage,
name: this.getRoomTitle(item),
_updatedAt: item.roomUpdatedAt,
key: item._id,
rid: item.rid,
type: item.t,
prid: item.prid,
uids: item.uids,
usernames: item.usernames,
visitor: item.visitor
}));
// unread // unread
if (showUnread) { if (showUnread) {
@ -484,12 +483,18 @@ class RoomsListView extends React.Component {
this.internalSetState({ this.internalSetState({
chats: tempChats, chats: tempChats,
allChats, chatsOrder,
loading: false loading: false
}); });
}); });
} }
unsubscribeQuery = () => {
if (this.querySubscription && this.querySubscription.unsubscribe) {
this.querySubscription.unsubscribe();
}
}
initSearching = () => { initSearching = () => {
const { openSearchHeader } = this.props; const { openSearchHeader } = this.props;
this.internalSetState({ searching: true }, () => { this.internalSetState({ searching: true }, () => {
@ -548,10 +553,19 @@ class RoomsListView extends React.Component {
getRoomAvatar = item => RocketChat.getRoomAvatar(item) getRoomAvatar = item => RocketChat.getRoomAvatar(item)
isGroupChat = item => RocketChat.isGroupChat(item)
isRead = item => RocketChat.isRead(item)
getUserPresence = uid => RocketChat.getUserPresence(uid) getUserPresence = uid => RocketChat.getUserPresence(uid)
getUidDirectMessage = room => RocketChat.getUidDirectMessage(room); getUidDirectMessage = room => RocketChat.getUidDirectMessage(room);
get isGrouping() {
const { showUnread, showFavorites, groupByType } = this.props;
return showUnread || showFavorites || groupByType;
}
onPressItem = (item = {}) => { onPressItem = (item = {}) => {
const { navigation, isMasterDetail } = this.props; const { navigation, isMasterDetail } = this.props;
if (!navigation.isFocused()) { if (!navigation.isFocused()) {
@ -743,6 +757,13 @@ class RoomsListView extends React.Component {
roomsRequest({ allData: true }); roomsRequest({ allData: true });
} }
onEndReached = () => {
// Run only when we're not grouping by anything
if (!this.isGrouping) {
this.getSubscriptions();
}
}
getScrollRef = ref => (this.scroll = ref); getScrollRef = ref => (this.scroll = ref);
renderListHeader = () => { renderListHeader = () => {
@ -774,12 +795,6 @@ class RoomsListView extends React.Component {
); );
} }
getIsRead = (item) => {
let isUnread = item.archived !== true && item.open === true; // item is not archived and not opened
isUnread = isUnread && (item.unread > 0 || item.alert === true); // either its unread count > 0 or its alert
return !isUnread;
};
renderItem = ({ item }) => { renderItem = ({ item }) => {
if (item.separator) { if (item.separator) {
return this.renderSectionHeader(item.rid); return this.renderSectionHeader(item.rid);
@ -800,32 +815,19 @@ class RoomsListView extends React.Component {
width width
} = this.props; } = this.props;
const id = this.getUidDirectMessage(item); const id = this.getUidDirectMessage(item);
const isGroupChat = RocketChat.isGroupChat(item);
return ( return (
<RoomItem <RoomItem
item={item}
theme={theme} theme={theme}
alert={item.alert}
unread={item.unread}
hideUnreadStatus={item.hideUnreadStatus}
userMentions={item.userMentions}
isRead={this.getIsRead(item)}
favorite={item.f}
avatar={this.getRoomAvatar(item)}
lastMessage={item.lastMessage}
name={this.getRoomTitle(item)}
_updatedAt={item.roomUpdatedAt}
key={item._id}
id={id} id={id}
type={item.t}
userId={userId} userId={userId}
username={username} username={username}
token={token} token={token}
rid={item.rid}
type={item.t}
baseUrl={server} baseUrl={server}
prid={item.prid}
showLastMessage={StoreLastMessage} showLastMessage={StoreLastMessage}
onPress={() => this.onPressItem(item)} onPress={this.onPressItem}
testID={`rooms-list-view-item-${ item.name }`} testID={`rooms-list-view-item-${ item.name }`}
width={isMasterDetail ? MAX_SIDEBAR_WIDTH : width} width={isMasterDetail ? MAX_SIDEBAR_WIDTH : width}
toggleFav={this.toggleFav} toggleFav={this.toggleFav}
@ -833,7 +835,10 @@ class RoomsListView extends React.Component {
hideChannel={this.hideChannel} hideChannel={this.hideChannel}
useRealName={useRealName} useRealName={useRealName}
getUserPresence={this.getUserPresence} getUserPresence={this.getUserPresence}
isGroupChat={isGroupChat} getRoomTitle={this.getRoomTitle}
getRoomAvatar={this.getRoomAvatar}
getIsGroupChat={this.isGroupChat}
getIsRead={this.isRead}
visitor={item.visitor} visitor={item.visitor}
isFocused={currentItem?.rid === item.rid} isFocused={currentItem?.rid === item.rid}
/> />
@ -880,6 +885,8 @@ class RoomsListView extends React.Component {
/> />
)} )}
windowSize={9} windowSize={9}
onEndReached={this.onEndReached}
onEndReachedThreshold={0.5}
/> />
); );
}; };

View File

@ -7,7 +7,7 @@ import ShareExtension from 'rn-extensions-share';
import * as FileSystem from 'expo-file-system'; import * as FileSystem from 'expo-file-system';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import * as mime from 'react-native-mime-types'; import * as mime from 'react-native-mime-types';
import { isEqual, orderBy } from 'lodash'; import isEqual from 'react-fast-compare';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import database from '../../lib/database'; import database from '../../lib/database';
@ -32,7 +32,6 @@ const permission = {
message: I18n.t('Read_External_Permission_Message') message: I18n.t('Read_External_Permission_Message')
}; };
const LIMIT = 50;
const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index }); const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index });
const keyExtractor = item => item.rid; const keyExtractor = item => item.rid;
@ -47,7 +46,7 @@ class ShareListView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.data = []; this.chats = [];
this.state = { this.state = {
searching: false, searching: false,
searchText: '', searchText: '',
@ -186,22 +185,36 @@ class ShareListView extends React.Component {
this.setState(...args); this.setState(...args);
} }
getSubscriptions = async(server) => { query = (text) => {
const db = database.active; const db = database.active;
const defaultWhereClause = [
Q.where('archived', false),
Q.where('open', true),
Q.experimentalSkip(0),
Q.experimentalTake(50),
Q.experimentalSortBy('room_updated_at', Q.desc)
];
if (text) {
return db.collections
.get('subscriptions')
.query(
...defaultWhereClause,
Q.or(
Q.where('name', Q.like(`%${ Q.sanitizeLikeString(text) }%`)),
Q.where('fname', Q.like(`%${ Q.sanitizeLikeString(text) }%`))
)
).fetch();
}
return db.collections.get('subscriptions').query(...defaultWhereClause).fetch();
}
getSubscriptions = async(server) => {
const serversDB = database.servers; const serversDB = database.servers;
if (server) { if (server) {
this.data = await db.collections this.chats = await this.query();
.get('subscriptions')
.query(
Q.where('archived', false),
Q.where('open', true)
).fetch();
this.data = orderBy(this.data, ['roomUpdatedAt'], ['desc']);
const serversCollection = serversDB.collections.get('servers'); const serversCollection = serversDB.collections.get('servers');
this.servers = await serversCollection.query().fetch(); this.servers = await serversCollection.query().fetch();
this.chats = this.data.slice(0, LIMIT);
let serverInfo = {}; let serverInfo = {};
try { try {
serverInfo = await serversCollection.find(server); serverInfo = await serversCollection.find(server);
@ -210,8 +223,8 @@ class ShareListView extends React.Component {
} }
this.internalSetState({ this.internalSetState({
chats: this.chats ? this.chats.slice() : [], chats: this.chats ?? [],
servers: this.servers ? this.servers.slice() : [], servers: this.servers ?? [],
loading: false, loading: false,
serverInfo serverInfo
}); });
@ -253,10 +266,10 @@ class ShareListView extends React.Component {
}); });
} }
search = (text) => { search = async(text) => {
const result = this.data.filter(item => item.name.includes(text)) || []; const result = await this.query(text);
this.internalSetState({ this.internalSetState({
searchResults: result.slice(0, LIMIT), searchResults: result,
searchText: text searchText: text
}); });
} }
@ -297,9 +310,26 @@ class ShareListView extends React.Component {
} }
renderItem = ({ item }) => { renderItem = ({ item }) => {
const { serverInfo } = this.state;
const { useRealName } = serverInfo;
const { const {
userId, token, server, theme userId, token, server, theme
} = this.props; } = this.props;
let description;
switch (item.t) {
case 'c':
description = item.topic || item.description;
break;
case 'p':
description = item.topic || item.description;
break;
case 'd':
description = useRealName ? item.name : item.fname;
break;
default:
description = item.fname;
break;
}
return ( return (
<DirectoryItem <DirectoryItem
user={{ user={{
@ -309,11 +339,7 @@ class ShareListView extends React.Component {
title={this.getRoomTitle(item)} title={this.getRoomTitle(item)}
baseUrl={server} baseUrl={server}
avatar={RocketChat.getRoomAvatar(item)} avatar={RocketChat.getRoomAvatar(item)}
description={ description={description}
item.t === 'c'
? (item.topic || item.description)
: item.fname
}
type={item.prid ? 'discussion' : item.t} type={item.prid ? 'discussion' : item.t}
onPress={() => this.shareMessage(item)} onPress={() => this.shareMessage(item)}
testID={`share-extension-item-${ item.name }`} testID={`share-extension-item-${ item.name }`}

View File

@ -24,16 +24,16 @@ import SafeAreaView from '../containers/SafeAreaView';
const STATUS = [{ const STATUS = [{
id: 'online', id: 'online',
name: I18n.t('Online') name: 'Online'
}, { }, {
id: 'busy', id: 'busy',
name: I18n.t('Busy') name: 'Busy'
}, { }, {
id: 'away', id: 'away',
name: I18n.t('Away') name: 'Away'
}, { }, {
id: 'offline', id: 'offline',
name: I18n.t('Invisible') name: 'Invisible'
}]; }];
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -164,7 +164,7 @@ class StatusView extends React.Component {
const { id, name } = item; const { id, name } = item;
return ( return (
<ListItem <ListItem
title={name} title={I18n.t(name)}
onPress={async() => { onPress={async() => {
if (user.status !== item.id) { if (user.status !== item.id) {
try { try {

View File

@ -21,28 +21,28 @@ const THEME_GROUP = 'THEME_GROUP';
const DARK_GROUP = 'DARK_GROUP'; const DARK_GROUP = 'DARK_GROUP';
const SYSTEM_THEME = { const SYSTEM_THEME = {
label: I18n.t('Automatic'), label: 'Automatic',
value: 'automatic', value: 'automatic',
group: THEME_GROUP group: THEME_GROUP
}; };
const THEMES = [ const THEMES = [
{ {
label: I18n.t('Light'), label: 'Light',
value: 'light', value: 'light',
group: THEME_GROUP group: THEME_GROUP
}, { }, {
label: I18n.t('Dark'), label: 'Dark',
value: 'dark', value: 'dark',
group: THEME_GROUP group: THEME_GROUP
}, { }, {
label: I18n.t('Dark'), label: 'Dark',
value: 'dark', value: 'dark',
separator: true, separator: true,
header: I18n.t('Dark_level'), header: 'Dark_level',
group: DARK_GROUP group: DARK_GROUP
}, { }, {
label: I18n.t('Black'), label: 'Black',
value: 'black', value: 'black',
group: DARK_GROUP group: DARK_GROUP
} }
@ -129,7 +129,7 @@ class ThemeView extends React.Component {
<> <>
{item.separator || isFirst ? this.renderSectionHeader(item.header) : null} {item.separator || isFirst ? this.renderSectionHeader(item.header) : null}
<ListItem <ListItem
title={label} title={I18n.t(label)}
onPress={() => this.onClick(item)} onPress={() => this.onClick(item)}
testID={`theme-view-${ value }`} testID={`theme-view-${ value }`}
right={this.isSelected(item) ? this.renderIcon : null} right={this.isSelected(item) ? this.renderIcon : null}
@ -139,12 +139,12 @@ class ThemeView extends React.Component {
); );
} }
renderSectionHeader = (header = I18n.t('Theme')) => { renderSectionHeader = (header = 'Theme') => {
const { theme } = this.props; const { theme } = this.props;
return ( return (
<> <>
<View style={styles.info}> <View style={styles.info}>
<Text style={[styles.infoText, { color: themes[theme].infoText }]}>{header}</Text> <Text style={[styles.infoText, { color: themes[theme].infoText }]}>{I18n.t(header)}</Text>
</View> </View>
{this.renderSeparator()} {this.renderSeparator()}
</> </>
@ -169,7 +169,7 @@ class ThemeView extends React.Component {
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<FlatList <FlatList
data={THEMES} data={THEMES}
keyExtractor={item => item.value} keyExtractor={item => item.value + item.group}
contentContainerStyle={[ contentContainerStyle={[
styles.list, styles.list,
{ borderColor: themes[theme].separatorColor } { borderColor: themes[theme].separatorColor }

View File

@ -9,34 +9,39 @@ const data = {
regular: { regular: {
username: `userone${ value }`, username: `userone${ value }`,
password: '123', password: '123',
email: `diego.mello+regular${ value }@rocket.chat` email: `mobile+regular${ value }@rocket.chat`
}, },
alternate: { alternate: {
username: `usertwo${ value }`, username: `usertwo${ value }`,
password: '123', password: '123',
email: `diego.mello+alternate${ value }@rocket.chat`, email: `mobile+alternate${ value }@rocket.chat`,
totpSecret: 'NA4GOMZGHBQSK6KEFRVT62DMGJJGSYZJFZIHO3ZOGVXWCYZ6MMZQ' totpSecret: 'NA4GOMZGHBQSK6KEFRVT62DMGJJGSYZJFZIHO3ZOGVXWCYZ6MMZQ'
}, },
profileChanges: { profileChanges: {
username: `userthree${ value }`, username: `userthree${ value }`,
password: '123', password: '123',
email: `diego.mello+profileChanges${ value }@rocket.chat` email: `mobile+profileChanges${ value }@rocket.chat`
}, },
existing: { existing: {
username: `existinguser${ value }`, username: `existinguser${ value }`,
password: '123', password: '123',
email: `diego.mello+existing${ value }@rocket.chat` email: `mobile+existing${ value }@rocket.chat`
} }
}, },
channels: { channels: {
public: { detoxpublic: {
name: 'detox-public' name: 'detox-public'
} }
}, },
groups: {
private: {
name: `detox-private-${ value }`
}
},
registeringUser: { registeringUser: {
username: `newuser${ value }`, username: `newuser${ value }`,
password: `password${ value }`, password: `password${ value }`,
email: `diego.mello+registering${ value }@rocket.chat` email: `mobile+registering${ value }@rocket.chat`
}, },
random: value random: value
} }

View File

@ -9,34 +9,39 @@ const data = {
regular: { regular: {
username: `userone${ value }`, username: `userone${ value }`,
password: '123', password: '123',
email: `diego.mello+regular${ value }@rocket.chat` email: `mobile+regular${ value }@rocket.chat`
}, },
alternate: { alternate: {
username: `usertwo${ value }`, username: `usertwo${ value }`,
password: '123', password: '123',
email: `diego.mello+alternate${ value }@rocket.chat`, email: `mobile+alternate${ value }@rocket.chat`,
totpSecret: 'NA4GOMZGHBQSK6KEFRVT62DMGJJGSYZJFZIHO3ZOGVXWCYZ6MMZQ' totpSecret: 'NA4GOMZGHBQSK6KEFRVT62DMGJJGSYZJFZIHO3ZOGVXWCYZ6MMZQ'
}, },
profileChanges: { profileChanges: {
username: `userthree${ value }`, username: `userthree${ value }`,
password: '123', password: '123',
email: `diego.mello+profileChanges${ value }@rocket.chat` email: `mobile+profileChanges${ value }@rocket.chat`
}, },
existing: { existing: {
username: `existinguser${ value }`, username: `existinguser${ value }`,
password: '123', password: '123',
email: `diego.mello+existing${ value }@rocket.chat` email: `mobile+existing${ value }@rocket.chat`
} }
}, },
channels: { channels: {
public: { detoxpublic: {
name: 'detox-public' name: 'detox-public'
} }
}, },
groups: {
private: {
name: `detox-private-${ value }`
}
},
registeringUser: { registeringUser: {
username: `newuser${ value }`, username: `newuser${ value }`,
password: `password${ value }`, password: `password${ value }`,
email: `diego.mello+registering${ value }@rocket.chat` email: `mobile+registering${ value }@rocket.chat`
}, },
random: value random: value
} }

View File

@ -9,34 +9,39 @@ const data = {
regular: { regular: {
username: `userone${ value }`, username: `userone${ value }`,
password: '123', password: '123',
email: `diego.mello+regular${ value }@rocket.chat` email: `mobile+regular${ value }@rocket.chat`
}, },
alternate: { alternate: {
username: `usertwo${ value }`, username: `usertwo${ value }`,
password: '123', password: '123',
email: `diego.mello+alternate${ value }@rocket.chat`, email: `mobile+alternate${ value }@rocket.chat`,
totpSecret: 'NA4GOMZGHBQSK6KEFRVT62DMGJJGSYZJFZIHO3ZOGVXWCYZ6MMZQ' totpSecret: 'NA4GOMZGHBQSK6KEFRVT62DMGJJGSYZJFZIHO3ZOGVXWCYZ6MMZQ'
}, },
profileChanges: { profileChanges: {
username: `userthree${ value }`, username: `userthree${ value }`,
password: '123', password: '123',
email: `diego.mello+profileChanges${ value }@rocket.chat` email: `mobile+profileChanges${ value }@rocket.chat`
}, },
existing: { existing: {
username: `existinguser${ value }`, username: `existinguser${ value }`,
password: '123', password: '123',
email: `diego.mello+existing${ value }@rocket.chat` email: `mobile+existing${ value }@rocket.chat`
} }
}, },
channels: { channels: {
public: { detoxpublic: {
name: 'detox-public' name: 'detox-public'
} }
}, },
groups: {
private: {
name: `detox-private-${ value }`
}
},
registeringUser: { registeringUser: {
username: `newuser${ value }`, username: `newuser${ value }`,
password: `password${ value }`, password: `password${ value }`,
email: `diego.mello+registering${ value }@rocket.chat` email: `mobile+registering${ value }@rocket.chat`
}, },
random: value random: value
} }

View File

@ -42,7 +42,7 @@ if [ "$COMMAND" == "start" ]; then
MAX_ATTEMPTS=60 MAX_ATTEMPTS=60
while [ $ATTEMPT_NUMBER -lt $MAX_ATTEMPTS ]; do # https://stackoverflow.com/a/21189312/399007 while [ $ATTEMPT_NUMBER -lt $MAX_ATTEMPTS ]; do # https://stackoverflow.com/a/21189312/399007
ATTEMPT_NUMBER=$((ATTEMPT_NUMBER + 1 )) ATTEMPT_NUMBER=$((ATTEMPT_NUMBER + 1 ))
echo "Waiting for server to be up ($ATTEMPT_NUMBER of $MAX_ATTEMPTS)" echo "Checking if servers are ready (attempt $ATTEMPT_NUMBER of $MAX_ATTEMPTS)"
LOGS=$(docker logs rc_test_env_rocketchat_1 2> /dev/null) LOGS=$(docker logs rc_test_env_rocketchat_1 2> /dev/null)
if grep -q 'SERVER RUNNING' <<< $LOGS ; then if grep -q 'SERVER RUNNING' <<< $LOGS ; then
echo "RocketChat is ready!" echo "RocketChat is ready!"

View File

@ -31,7 +31,6 @@ async function login(username, password) {
await waitFor(element(by.id('login-view'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('login-view'))).toBeVisible().withTimeout(2000);
await element(by.id('login-view-email')).replaceText(username); await element(by.id('login-view-email')).replaceText(username);
await element(by.id('login-view-password')).replaceText(password); await element(by.id('login-view-password')).replaceText(password);
await sleep(300);
await element(by.id('login-view-submit')).tap(); await element(by.id('login-view-submit')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000);
} }
@ -61,6 +60,33 @@ async function mockMessage(message) {
await element(by.label(`${ data.random }${ message }`)).atIndex(0).tap(); await element(by.label(`${ data.random }${ message }`)).atIndex(0).tap();
}; };
async function starMessage(message){
const messageLabel = `${ data.random }${ message }`
await waitFor(element(by.label(messageLabel))).toBeVisible().withTimeout(5000);
await element(by.label(messageLabel)).atIndex(0).longPress();
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 waitFor(element(by.id('action-sheet'))).toNotExist().withTimeout(5000);
};
async function pinMessage(message){
const messageLabel = `${ data.random }${ message }`
await waitFor(element(by.label(messageLabel)).atIndex(0)).toExist();
await element(by.label(messageLabel)).atIndex(0).longPress();
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 waitFor(element(by.id('action-sheet'))).toNotExist().withTimeout(5000);
}
async function dismissReviewNag(){
await waitFor(element(by.text('Are you enjoying this app?'))).toExist().withTimeout(60000);
await element(by.label('No').and(by.type('_UIAlertControllerActionView'))).tap(); // Tap `no` on ask for review alert
}
async function tapBack() { async function tapBack() {
await element(by.id('header-back')).atIndex(0).tap(); await element(by.id('header-back')).atIndex(0).tap();
} }
@ -74,7 +100,22 @@ async function searchRoom(room) {
await expect(element(by.id('rooms-list-view-search-input'))).toExist(); await expect(element(by.id('rooms-list-view-search-input'))).toExist();
await waitFor(element(by.id('rooms-list-view-search-input'))).toExist().withTimeout(5000); await waitFor(element(by.id('rooms-list-view-search-input'))).toExist().withTimeout(5000);
await element(by.id('rooms-list-view-search-input')).typeText(room); await element(by.id('rooms-list-view-search-input')).typeText(room);
await sleep(2000); }
async function tryTapping(theElement, timeout, longtap = false){
try {
if(longtap){
await theElement.longPress()
} else {
await theElement.tap()
}
} catch(e) {
if(timeout <= 0){ //TODO: Maths. How closely has the timeout been honoured here?
throw e
}
await sleep(100)
await tryTapping(theElement, timeout - 100)
}
} }
module.exports = { module.exports = {
@ -84,7 +125,11 @@ module.exports = {
login, login,
logout, logout,
mockMessage, mockMessage,
starMessage,
pinMessage,
dismissReviewNag,
tapBack, tapBack,
sleep, sleep,
searchRoom searchRoom,
tryTapping
}; };

View File

@ -38,7 +38,7 @@ const createUser = async (username, password, name, email) => {
} }
const createChannelIfNotExists = async (channelname) => { const createChannelIfNotExists = async (channelname) => {
console.log(`Creating channel ${channelname}`) console.log(`Creating public channel ${channelname}`)
try { try {
await rocketchat.post('channels.create', { await rocketchat.post('channels.create', {
"name": channelname "name": channelname
@ -49,7 +49,24 @@ const createChannelIfNotExists = async (channelname) => {
} catch (infoError) { } catch (infoError) {
console.log(JSON.stringify(createError)) console.log(JSON.stringify(createError))
console.log(JSON.stringify(infoError)) console.log(JSON.stringify(infoError))
throw "Failed to find or create channel" throw "Failed to find or create public channel"
}
}
}
const createGroupIfNotExists = async (groupname) => {
console.log(`Creating private group ${groupname}`)
try {
await rocketchat.post('groups.create', {
"name": groupname
})
} catch (createError) {
try { //Maybe it exists already?
await rocketchat.get(`group.info?roomName=${groupname}`)
} catch (infoError) {
console.log(JSON.stringify(createError))
console.log(JSON.stringify(infoError))
throw "Failed to find or create private group"
} }
} }
} }
@ -71,6 +88,15 @@ const setup = async () => {
} }
} }
await login(data.users.regular.username, data.users.regular.password)
for (var groupKey in data.groups) {
if (data.groups.hasOwnProperty(groupKey)) {
const group = data.groups[groupKey]
await createGroupIfNotExists(group.name)
}
}
return return
} }

View File

@ -9,12 +9,12 @@ const checkServer = async(server) => {
await element(by.id('rooms-list-view-sidebar')).tap(); 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-view'))).toBeVisible().withTimeout(2000);
await waitFor(element(by.label(label))).toBeVisible().withTimeout(60000); await waitFor(element(by.label(label))).toBeVisible().withTimeout(60000);
await expect(element(by.label(label))).toBeVisible();
await element(by.id('sidebar-close-drawer')).tap(); await element(by.id('sidebar-close-drawer')).tap();
} }
describe('Change server', () => { describe('Change server', () => {
before(async() => { before(async() => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
await navigateToLogin(); await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password); await login(data.users.regular.username, data.users.regular.password);
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000);
@ -28,8 +28,6 @@ describe('Change server', () => {
await sleep(5000); await sleep(5000);
await element(by.id('rooms-list-header-server-dropdown-button')).tap(); await element(by.id('rooms-list-header-server-dropdown-button')).tap();
await waitFor(element(by.id('rooms-list-header-server-dropdown'))).toBeVisible().withTimeout(5000); await waitFor(element(by.id('rooms-list-header-server-dropdown'))).toBeVisible().withTimeout(5000);
await expect(element(by.id('rooms-list-header-server-dropdown'))).toExist();
await sleep(1000);
await element(by.id('rooms-list-header-server-add')).tap(); await element(by.id('rooms-list-header-server-add')).tap();
// TODO: refactor // TODO: refactor
@ -37,19 +35,16 @@ describe('Change server', () => {
await element(by.id('new-server-view-input')).replaceText(data.alternateServer); await element(by.id('new-server-view-input')).replaceText(data.alternateServer);
await element(by.id('new-server-view-button')).tap(); await element(by.id('new-server-view-button')).tap();
await waitFor(element(by.id('workspace-view'))).toBeVisible().withTimeout(60000); await waitFor(element(by.id('workspace-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('workspace-view'))).toBeVisible();
await element(by.id('workspace-view-register')).tap(); await element(by.id('workspace-view-register')).tap();
await waitFor(element(by.id('register-view'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('register-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('register-view'))).toBeVisible();
// Register new user // Register new user
await element(by.id('register-view-name')).replaceText(data.registeringUser.username); await element(by.id('register-view-name')).replaceText(data.registeringUser.username);
await element(by.id('register-view-username')).replaceText(data.registeringUser.username); await element(by.id('register-view-username')).replaceText(data.registeringUser.username);
await element(by.id('register-view-email')).replaceText(data.registeringUser.email); await element(by.id('register-view-email')).replaceText(data.registeringUser.email);
await element(by.id('register-view-password')).replaceText(data.registeringUser.password); await element(by.id('register-view-password')).replaceText(data.registeringUser.password);
await sleep(1000);
await element(by.id('register-view-submit')).tap(); await element(by.id('register-view-submit')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(60000); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('rooms-list-view'))).toBeVisible();
// For a sanity test, to make sure roomslist is showing correct rooms // For a sanity test, to make sure roomslist is showing correct rooms
// app CANNOT show public room created on previous tests // app CANNOT show public room created on previous tests
@ -59,11 +54,8 @@ describe('Change server', () => {
}); });
it('should change back', async() => { it('should change back', async() => {
await sleep(5000);
await element(by.id('rooms-list-header-server-dropdown-button')).tap(); await element(by.id('rooms-list-header-server-dropdown-button')).tap();
await waitFor(element(by.id('rooms-list-header-server-dropdown'))).toBeVisible().withTimeout(5000); await waitFor(element(by.id('rooms-list-header-server-dropdown'))).toBeVisible().withTimeout(5000);
await expect(element(by.id('rooms-list-header-server-dropdown'))).toExist();
await sleep(1000);
await element(by.id(`rooms-list-header-server-${ data.server }`)).tap(); await element(by.id(`rooms-list-header-server-${ data.server }`)).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000);
await checkServer(data.server); await checkServer(data.server);

View File

@ -23,28 +23,16 @@ describe('Broadcast room', () => {
await waitFor(element(by.id('select-users-view'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('select-users-view'))).toBeVisible().withTimeout(2000);
await element(by.id('select-users-view-search')).replaceText(otheruser.username); await element(by.id('select-users-view-search')).replaceText(otheruser.username);
await waitFor(element(by.id(`select-users-view-item-${ otheruser.username }`))).toBeVisible().withTimeout(60000); await waitFor(element(by.id(`select-users-view-item-${ otheruser.username }`))).toBeVisible().withTimeout(60000);
await expect(element(by.id(`select-users-view-item-${ otheruser.username }`))).toBeVisible();
await element(by.id(`select-users-view-item-${ otheruser.username }`)).tap(); await element(by.id(`select-users-view-item-${ otheruser.username }`)).tap();
await waitFor(element(by.id(`selected-user-${ otheruser.username }`))).toBeVisible().withTimeout(5000); await waitFor(element(by.id(`selected-user-${ otheruser.username }`))).toBeVisible().withTimeout(5000);
await sleep(1000);
await element(by.id('selected-users-view-submit')).tap(); await element(by.id('selected-users-view-submit')).tap();
await sleep(1000);
await waitFor(element(by.id('create-channel-view'))).toExist().withTimeout(5000); await waitFor(element(by.id('create-channel-view'))).toExist().withTimeout(5000);
await element(by.id('create-channel-name')).replaceText(`broadcast${ data.random }`); await element(by.id('create-channel-name')).replaceText(`broadcast${ data.random }`);
await sleep(2000); await element(by.id('create-channel-broadcast')).longPress(); //https://github.com/facebook/react-native/issues/28032
await element(by.id('create-channel-broadcast')).tap();
if (device.getPlatform() === 'ios') { //Because this tap is FLAKY on iOS
await expect(element(by.id('create-channel-broadcast'))).toHaveValue('1')
}
await sleep(500);
await element(by.id('create-channel-submit')).tap(); await element(by.id('create-channel-submit')).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000); await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('room-view'))).toBeVisible();
await waitFor(element(by.id(`room-view-title-broadcast${ data.random }`))).toBeVisible().withTimeout(60000); await waitFor(element(by.id(`room-view-title-broadcast${ data.random }`))).toBeVisible().withTimeout(60000);
await expect(element(by.id(`room-view-title-broadcast${ data.random }`))).toBeVisible();
await sleep(1000);
await element(by.id('room-view-header-actions')).tap(); await element(by.id('room-view-header-actions')).tap();
await sleep(1000);
await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(5000); await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(5000);
await element(by.id('room-actions-info')).tap(); await element(by.id('room-actions-info')).tap();
await waitFor(element(by.id('room-info-view'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('room-info-view'))).toBeVisible().withTimeout(2000);
@ -64,25 +52,19 @@ describe('Broadcast room', () => {
it('should login as user without write message authorization and enter room', async() => { it('should login as user without write message authorization and enter room', async() => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true }); await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
await navigateToLogin(); await navigateToLogin();
await element(by.id('login-view-email')).replaceText(otheruser.username); await login(otheruser.username, otheruser.password);
await element(by.id('login-view-password')).replaceText(otheruser.password);
await sleep(1000);
await element(by.id('login-view-submit')).tap();
//await waitFor(element(by.id('two-factor'))).toBeVisible().withTimeout(5000); //await waitFor(element(by.id('two-factor'))).toBeVisible().withTimeout(5000);
//await expect(element(by.id('two-factor'))).toBeVisible(); //await expect(element(by.id('two-factor'))).toBeVisible();
//const code = GA.gen(data.alternateUserTOTPSecret); //const code = GA.gen(data.alternateUserTOTPSecret);
//await element(by.id('two-factor-input')).replaceText(code); //await element(by.id('two-factor-input')).replaceText(code);
//await sleep(1000);
//await element(by.id('two-factor-send')).tap(); //await element(by.id('two-factor-send')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000);
await searchRoom(`broadcast${ data.random }`); await searchRoom(`broadcast${ data.random }`);
await waitFor(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toExist().withTimeout(60000); await waitFor(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toExist().withTimeout(60000);
await expect(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toExist();
await element(by.id(`rooms-list-view-item-broadcast${ data.random }`)).tap(); await element(by.id(`rooms-list-view-item-broadcast${ data.random }`)).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000); await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
await waitFor(element(by.id(`room-view-title-broadcast${ data.random }`))).toBeVisible().withTimeout(60000); await waitFor(element(by.id(`room-view-title-broadcast${ data.random }`))).toBeVisible().withTimeout(60000);
await expect(element(by.id(`room-view-title-broadcast${ data.random }`))).toBeVisible();
await sleep(1000);
}); });
it('should not have messagebox', async() => { it('should not have messagebox', async() => {
@ -95,7 +77,6 @@ describe('Broadcast room', () => {
it('should have the message created earlier', async() => { it('should have the message created earlier', async() => {
await waitFor(element(by.label(`${ data.random }message`)).atIndex(0)).toBeVisible().withTimeout(60000); await waitFor(element(by.label(`${ data.random }message`)).atIndex(0)).toBeVisible().withTimeout(60000);
await expect(element(by.label(`${ data.random }message`)).atIndex(0)).toBeVisible();
}); });
it('should have reply button', async() => { it('should have reply button', async() => {
@ -104,9 +85,7 @@ describe('Broadcast room', () => {
it('should tap on reply button and navigate to direct room', async() => { it('should tap on reply button and navigate to direct room', async() => {
await element(by.id('message-broadcast-reply')).tap(); await element(by.id('message-broadcast-reply')).tap();
await sleep(1000);
await waitFor(element(by.id(`room-view-title-${ testuser.username }`))).toBeVisible().withTimeout(5000); await waitFor(element(by.id(`room-view-title-${ testuser.username }`))).toBeVisible().withTimeout(5000);
await expect(element(by.id(`room-view-title-${ testuser.username }`))).toBeVisible();
}); });
it('should reply broadcasted message', async() => { it('should reply broadcasted message', async() => {

View File

@ -13,7 +13,7 @@ async function waitForToast() {
// await expect(element(by.id('toast'))).toBeVisible(); // await expect(element(by.id('toast'))).toBeVisible();
// await waitFor(element(by.id('toast'))).toBeNotVisible().withTimeout(10000); // await waitFor(element(by.id('toast'))).toBeNotVisible().withTimeout(10000);
// await expect(element(by.id('toast'))).toBeNotVisible(); // await expect(element(by.id('toast'))).toBeNotVisible();
await sleep(5000); await sleep(1);
} }
describe('Profile screen', () => { describe('Profile screen', () => {
@ -24,7 +24,6 @@ describe('Profile screen', () => {
await element(by.id('rooms-list-view-sidebar')).tap(); 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-view'))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id('sidebar-profile'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('sidebar-profile'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('sidebar-profile'))).toBeVisible();
await element(by.id('sidebar-profile')).tap(); await element(by.id('sidebar-profile')).tap();
await waitFor(element(by.id('profile-view'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('profile-view'))).toBeVisible().withTimeout(2000);
}); });
@ -60,22 +59,18 @@ describe('Profile screen', () => {
it('should have reset avatar button', async() => { it('should have reset avatar button', async() => {
await waitFor(element(by.id('profile-view-reset-avatar'))).toExist().whileElement(by.id('profile-view-list')).scroll(scrollDown, 'down'); await waitFor(element(by.id('profile-view-reset-avatar'))).toExist().whileElement(by.id('profile-view-list')).scroll(scrollDown, 'down');
await expect(element(by.id('profile-view-reset-avatar'))).toExist();
}); });
it('should have upload avatar button', async() => { it('should have upload avatar button', async() => {
await waitFor(element(by.id('profile-view-upload-avatar'))).toExist().whileElement(by.id('profile-view-list')).scroll(scrollDown, 'down'); await waitFor(element(by.id('profile-view-upload-avatar'))).toExist().whileElement(by.id('profile-view-list')).scroll(scrollDown, 'down');
await expect(element(by.id('profile-view-upload-avatar'))).toExist();
}); });
it('should have avatar url button', async() => { it('should have avatar url button', async() => {
await waitFor(element(by.id('profile-view-avatar-url-button'))).toExist().whileElement(by.id('profile-view-list')).scroll(scrollDown, 'down'); await waitFor(element(by.id('profile-view-avatar-url-button'))).toExist().whileElement(by.id('profile-view-list')).scroll(scrollDown, 'down');
await expect(element(by.id('profile-view-avatar-url-button'))).toExist();
}); });
it('should have submit button', async() => { it('should have submit button', async() => {
await waitFor(element(by.id('profile-view-submit'))).toExist().whileElement(by.id('profile-view-list')).scroll(scrollDown, 'down'); await waitFor(element(by.id('profile-view-submit'))).toExist().whileElement(by.id('profile-view-list')).scroll(scrollDown, 'down');
await expect(element(by.id('profile-view-submit'))).toExist();
}); });
}); });
@ -84,15 +79,13 @@ describe('Profile screen', () => {
await element(by.type('UIScrollView')).atIndex(1).swipe('down'); await element(by.type('UIScrollView')).atIndex(1).swipe('down');
await element(by.id('profile-view-name')).replaceText(`${ profileChangeUser.username }new`); await element(by.id('profile-view-name')).replaceText(`${ profileChangeUser.username }new`);
await element(by.id('profile-view-username')).replaceText(`${ profileChangeUser.username }new`); await element(by.id('profile-view-username')).replaceText(`${ profileChangeUser.username }new`);
await sleep(1000);
await element(by.type('UIScrollView')).atIndex(1).swipe('up'); await element(by.type('UIScrollView')).atIndex(1).swipe('up');
await sleep(1000);
await element(by.id('profile-view-submit')).tap(); await element(by.id('profile-view-submit')).tap();
await waitForToast(); await waitForToast();
}); });
it('should change email and password', async() => { it('should change email and password', async() => {
await element(by.id('profile-view-email')).replaceText(`diego.mello+profileChangesNew${ data.random }@rocket.chat`); await element(by.id('profile-view-email')).replaceText(`mobile+profileChangesNew${ data.random }@rocket.chat`);
await element(by.id('profile-view-new-password')).replaceText(`${ profileChangeUser.password }new`); await element(by.id('profile-view-new-password')).replaceText(`${ profileChangeUser.password }new`);
await element(by.id('profile-view-submit')).tap(); await element(by.id('profile-view-submit')).tap();
await element(by.type('_UIAlertControllerTextField')).replaceText(`${ profileChangeUser.password }`) await element(by.type('_UIAlertControllerTextField')).replaceText(`${ profileChangeUser.password }`)
@ -103,7 +96,6 @@ describe('Profile screen', () => {
it('should reset avatar', async() => { it('should reset avatar', async() => {
await element(by.type('UIScrollView')).atIndex(1).swipe('up'); await element(by.type('UIScrollView')).atIndex(1).swipe('up');
await sleep(1000);
await element(by.id('profile-view-reset-avatar')).tap(); await element(by.id('profile-view-reset-avatar')).tap();
await waitForToast(); await waitForToast();
}); });

View File

@ -1,12 +1,18 @@
const { const {
device, expect, element, by, waitFor device, expect, element, by, waitFor
} = require('detox'); } = require('detox');
const { navigateToLogin, login } = require('../../helpers/app');
const data = require('../../data');
const testuser = data.users.regular
describe('Settings screen', () => { describe('Settings screen', () => {
before(async() => { before(async() => {
await device.launchApp({ newInstance: true }); await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
await navigateToLogin();
await login(testuser.username, testuser.password);
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000);
await expect(element(by.id('rooms-list-view'))).toBeVisible();
await element(by.id('rooms-list-view-sidebar')).tap(); 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-view'))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id('sidebar-settings'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('sidebar-settings'))).toBeVisible().withTimeout(2000);

View File

@ -2,12 +2,12 @@ const {
device, expect, element, by, waitFor device, expect, element, by, waitFor
} = require('detox'); } = require('detox');
const data = require('../../data'); const data = require('../../data');
const { mockMessage, tapBack, sleep, searchRoom } = require('../../helpers/app'); const { navigateToLogin, login, mockMessage, tapBack, sleep, searchRoom } = require('../../helpers/app');
const room = 'detox-public'; const testuser = data.users.regular
const room = data.channels.detoxpublic.name;
async function navigateToRoom() { async function navigateToRoom() {
await sleep(2000);
await searchRoom(room); await searchRoom(room);
await waitFor(element(by.id(`rooms-list-view-item-${ room }`)).atIndex(0)).toBeVisible().withTimeout(60000); await waitFor(element(by.id(`rooms-list-view-item-${ room }`)).atIndex(0)).toBeVisible().withTimeout(60000);
await element(by.id(`rooms-list-view-item-${ room }`)).atIndex(0).tap(); await element(by.id(`rooms-list-view-item-${ room }`)).atIndex(0).tap();
@ -15,15 +15,15 @@ async function navigateToRoom() {
} }
async function navigateToRoomActions() { async function navigateToRoomActions() {
await sleep(2000);
await element(by.id('room-view-header-actions')).tap(); await element(by.id('room-view-header-actions')).tap();
await sleep(2000);
await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(5000); await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(5000);
} }
describe('Join public room', () => { describe('Join public room', () => {
before(async() => { before(async() => {
await device.launchApp({ newInstance: true }); await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
await navigateToLogin();
await login(testuser.username, testuser.password);
await navigateToRoom(); await navigateToRoom();
}); });
@ -167,9 +167,7 @@ describe('Join public room', () => {
await element(by.text('Yes, leave it!')).tap(); await element(by.text('Yes, leave it!')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000);
// await element(by.id('rooms-list-view-search')).typeText(''); // await element(by.id('rooms-list-view-search')).typeText('');
await sleep(2000);
await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible().withTimeout(60000); await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible().withTimeout(60000);
await expect(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible();
}); });
}); });
}); });

View File

@ -1,14 +1,21 @@
const { const {
expect, element, by, waitFor expect, element, by, waitFor
} = require('detox'); } = require('detox');
const { sleep } = require('../../helpers/app'); const { navigateToLogin, login, sleep } = require('../../helpers/app');
const data = require('../../data');
const testuser = data.users.regular
async function waitForToast() { async function waitForToast() {
await sleep(5000); await sleep(1);
} }
describe('Status screen', () => { describe('Status screen', () => {
before(async () => { before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
await navigateToLogin();
await login(testuser.username, testuser.password);
await element(by.id('rooms-list-view-sidebar')).tap(); 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-view'))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id('sidebar-custom-status'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('sidebar-custom-status'))).toBeVisible().withTimeout(2000);
@ -29,15 +36,12 @@ describe('Status screen', () => {
describe('Usage', async () => { describe('Usage', async () => {
it('should change status', async () => { it('should change status', async () => {
await sleep(1000);
await element(by.id('status-view-busy')).tap(); await element(by.id('status-view-busy')).tap();
await sleep(1000);
await expect(element(by.id('status-view-current-busy'))).toExist(); await expect(element(by.id('status-view-current-busy'))).toExist();
}); });
it('should change status text', async () => { it('should change status text', async () => {
await element(by.id('status-view-input')).replaceText('status-text-new'); await element(by.id('status-view-input')).replaceText('status-text-new');
await sleep(1000);
await element(by.id('status-view-submit')).tap(); await element(by.id('status-view-submit')).tap();
await waitForToast(); await waitForToast();
await waitFor(element(by.label('status-text-new').withAncestor(by.id('sidebar-custom-status')))).toBeVisible().withTimeout(2000); await waitFor(element(by.label('status-text-new').withAncestor(by.id('sidebar-custom-status')))).toBeVisible().withTimeout(2000);

View File

@ -1,11 +1,21 @@
const detox = require('detox'); const detox = require('detox');
const config = require('../../package.json').detox; const config = require('../../package.json').detox;
const dataSetup = require('../helpers/data_setup') const dataSetup = require('../helpers/data_setup')
const adapter = require('detox/runners/mocha/adapter');
before(async() => { before(async() => {
await dataSetup() await Promise.all([dataSetup(), detox.init(config, { launchApp: false })])
await detox.init(config, { launchApp: false }); //await dataSetup()
await device.launchApp({ permissions: { notifications: 'YES' } }); //await detox.init(config, { launchApp: false });
//await device.launchApp({ permissions: { notifications: 'YES' } });
});
beforeEach(async function() {
await adapter.beforeEach(this);
});
afterEach(async function() {
await adapter.afterEach(this);
}); });
after(async() => { after(async() => {

View File

@ -31,7 +31,6 @@ describe('Onboarding', () => {
it('should navigate to join a workspace', async() => { it('should navigate to join a workspace', async() => {
await element(by.id('join-workspace')).tap(); await element(by.id('join-workspace')).tap();
await waitFor(element(by.id('new-server-view'))).toBeVisible().withTimeout(60000); await waitFor(element(by.id('new-server-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('new-server-view'))).toBeVisible();
}); });
it('should enter an invalid server and get error', async() => { it('should enter an invalid server and get error', async() => {
@ -39,14 +38,12 @@ describe('Onboarding', () => {
await element(by.id('new-server-view-button')).tap(); await element(by.id('new-server-view-button')).tap();
const errorText = 'Oops!'; const errorText = 'Oops!';
await waitFor(element(by.text(errorText))).toBeVisible().withTimeout(60000); await waitFor(element(by.text(errorText))).toBeVisible().withTimeout(60000);
await expect(element(by.text(errorText))).toBeVisible();
await element(by.text('OK')).tap(); await element(by.text('OK')).tap();
}); });
it('should tap on "Join our open workspace" and navigate', async() => { it('should tap on "Join our open workspace" and navigate', async() => {
await element(by.id('new-server-view-open')).tap(); await element(by.id('new-server-view-open')).tap();
await waitFor(element(by.id('workspace-view'))).toBeVisible().withTimeout(60000); await waitFor(element(by.id('workspace-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('workspace-view'))).toBeVisible();
}); });
it('should enter a valid server without login services and navigate to login', async() => { it('should enter a valid server without login services and navigate to login', async() => {
@ -57,7 +54,6 @@ describe('Onboarding', () => {
await element(by.id('new-server-view-input')).replaceText(data.server); await element(by.id('new-server-view-input')).replaceText(data.server);
await element(by.id('new-server-view-button')).tap(); await element(by.id('new-server-view-button')).tap();
await waitFor(element(by.id('workspace-view'))).toBeVisible().withTimeout(60000); await waitFor(element(by.id('workspace-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('workspace-view'))).toBeVisible();
}); });
}); });
}); });

View File

@ -4,32 +4,38 @@ const {
const { navigateToRegister, navigateToLogin } = require('../../helpers/app'); const { navigateToRegister, navigateToLogin } = require('../../helpers/app');
describe('Legal screen', () => { describe('Legal screen', () => {
it('should have legal button on login', async() => {
await device.launchApp({ newInstance: true }); describe('From Login', () => {
before(async() => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
await navigateToLogin(); await navigateToLogin();
});
it('should have legal button on login', async() => {
await waitFor(element(by.id('login-view-more'))).toBeVisible().withTimeout(60000); await waitFor(element(by.id('login-view-more'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('login-view-more'))).toBeVisible();
}); });
it('should navigate to legal from login', async() => { it('should navigate to legal from login', async() => {
await waitFor(element(by.id('login-view-more'))).toBeVisible().withTimeout(60000); await expect(element(by.id('login-view-more'))).toBeVisible();
await element(by.id('login-view-more')).tap(); await element(by.id('login-view-more')).tap();
await waitFor(element(by.id('legal-view'))).toBeVisible().withTimeout(4000)
});
});
describe('From Register', () => {
before(async() => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
await navigateToRegister();
}); });
it('should have legal button on register', async() => { it('should have legal button on register', async() => {
await device.launchApp({ newInstance: true });
await navigateToRegister();
await waitFor(element(by.id('register-view-more'))).toBeVisible().withTimeout(60000); await waitFor(element(by.id('register-view-more'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('register-view-more'))).toBeVisible();
}); });
it('should navigate to legal from register', async() => { it('should navigate to legal from register', async() => {
await waitFor(element(by.id('register-view-more'))).toBeVisible().withTimeout(60000); await expect(element(by.id('register-view-more'))).toBeVisible();
await element(by.id('register-view-more')).tap(); await element(by.id('register-view-more')).tap();
}); await waitFor(element(by.id('legal-view'))).toBeVisible().withTimeout(4000);
it('should have legal screen', async() => {
await expect(element(by.id('legal-view'))).toBeVisible();
}); });
it('should have terms of service button', async() => { it('should have terms of service button', async() => {
@ -40,18 +46,20 @@ describe('Legal screen', () => {
await expect(element(by.id('legal-privacy-button'))).toBeVisible(); await expect(element(by.id('legal-privacy-button'))).toBeVisible();
}); });
// We can't simulate how webview behaves, so I had to disable :( // We can't simulate how webview behaves, so I had to disable :(
// it('should navigate to terms', async() => { /*
// await element(by.id('legal-terms-button')).tap(); it('should navigate to terms', async() => {
// await waitFor(element(by.id('terms-view'))).toBeVisible().withTimeout(2000); await element(by.id('legal-terms-button')).tap();
// await expect(element(by.id('terms-view'))).toBeVisible(); await waitFor(element(by.id('terms-view'))).toBeVisible().withTimeout(2000);
// }); await expect(element(by.id('terms-view'))).toBeVisible();
});
// it('should navigate to privacy', async() => {
// await tapBack(); it('should navigate to privacy', async() => {
// await element(by.id('legal-privacy-button')).tap(); await tapBack();
// await waitFor(element(by.id('privacy-view'))).toBeVisible().withTimeout(2000); await element(by.id('legal-privacy-button')).tap();
// await expect(element(by.id('privacy-view'))).toBeVisible(); await waitFor(element(by.id('privacy-view'))).toBeVisible().withTimeout(2000);
// }); await expect(element(by.id('privacy-view'))).toBeVisible();
});
*/
});
}); });

View File

@ -32,7 +32,6 @@ describe('Forgot password screen', () => {
await element(by.id('forgot-password-view-submit')).tap(); await element(by.id('forgot-password-view-submit')).tap();
await element(by.text('OK')).tap(); await element(by.text('OK')).tap();
await waitFor(element(by.id('login-view'))).toBeVisible().withTimeout(60000); await waitFor(element(by.id('login-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('login-view'))).toBeVisible();
}); });
}); });
}); });

View File

@ -53,10 +53,8 @@ describe('Create user screen', () => {
await element(by.id('register-view-username')).replaceText(data.registeringUser.username); await element(by.id('register-view-username')).replaceText(data.registeringUser.username);
await element(by.id('register-view-email')).replaceText(data.users.existing.email); await element(by.id('register-view-email')).replaceText(data.users.existing.email);
await element(by.id('register-view-password')).replaceText(data.registeringUser.password); await element(by.id('register-view-password')).replaceText(data.registeringUser.password);
await sleep(300);
await element(by.id('register-view-submit')).tap(); await element(by.id('register-view-submit')).tap();
await waitFor(element(by.text('Email already exists. [403]')).atIndex(0)).toExist().withTimeout(10000); await waitFor(element(by.text('Email already exists. [403]')).atIndex(0)).toExist().withTimeout(10000);
await expect(element(by.text('Email already exists. [403]')).atIndex(0)).toExist();
await element(by.text('OK')).tap(); await element(by.text('OK')).tap();
}); });
@ -65,10 +63,8 @@ describe('Create user screen', () => {
await element(by.id('register-view-username')).replaceText(data.users.existing.username); await element(by.id('register-view-username')).replaceText(data.users.existing.username);
await element(by.id('register-view-email')).replaceText(data.registeringUser.email); await element(by.id('register-view-email')).replaceText(data.registeringUser.email);
await element(by.id('register-view-password')).replaceText(data.registeringUser.password); await element(by.id('register-view-password')).replaceText(data.registeringUser.password);
await sleep(300);
await element(by.id('register-view-submit')).tap(); await element(by.id('register-view-submit')).tap();
await waitFor(element(by.text('Username is already in use')).atIndex(0)).toExist().withTimeout(10000); await waitFor(element(by.text('Username is already in use')).atIndex(0)).toExist().withTimeout(10000);
await expect(element(by.text('Username is already in use')).atIndex(0)).toExist();
await element(by.text('OK')).tap(); await element(by.text('OK')).tap();
}); });
@ -77,10 +73,8 @@ describe('Create user screen', () => {
await element(by.id('register-view-username')).replaceText(data.registeringUser.username); await element(by.id('register-view-username')).replaceText(data.registeringUser.username);
await element(by.id('register-view-email')).replaceText(data.registeringUser.email); await element(by.id('register-view-email')).replaceText(data.registeringUser.email);
await element(by.id('register-view-password')).replaceText(data.registeringUser.password); await element(by.id('register-view-password')).replaceText(data.registeringUser.password);
await sleep(300);
await element(by.id('register-view-submit')).tap(); await element(by.id('register-view-submit')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(60000); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('rooms-list-view'))).toBeVisible();
}); });
}); });
}); });

View File

@ -44,33 +44,27 @@ describe('Login screen', () => {
it('should navigate to register', async() => { it('should navigate to register', async() => {
await element(by.id('login-view-register')).tap(); await element(by.id('login-view-register')).tap();
await waitFor(element(by.id('register-view'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('register-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('register-view'))).toBeVisible();
await tapBack(); await tapBack();
}); });
it('should navigate to forgot password', async() => { it('should navigate to forgot password', async() => {
await element(by.id('login-view-forgot-password')).tap(); await element(by.id('login-view-forgot-password')).tap();
await waitFor(element(by.id('forgot-password-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('forgot-password-view'))).toExist().withTimeout(2000);
await expect(element(by.id('forgot-password-view'))).toExist();
await tapBack(); await tapBack();
}); });
it('should insert wrong password and get error', async() => { it('should insert wrong password and get error', async() => {
await element(by.id('login-view-email')).replaceText(data.users.regular.username); await element(by.id('login-view-email')).replaceText(data.users.regular.username);
await element(by.id('login-view-password')).replaceText('NotMyActualPassword'); await element(by.id('login-view-password')).replaceText('NotMyActualPassword');
await sleep(300);
await element(by.id('login-view-submit')).tap(); await element(by.id('login-view-submit')).tap();
await waitFor(element(by.text('Your credentials were rejected! Please try again.'))).toBeVisible().withTimeout(10000); await waitFor(element(by.text('Your credentials were rejected! Please try again.'))).toBeVisible().withTimeout(10000);
await expect(element(by.text('Your credentials were rejected! Please try again.'))).toBeVisible();
await element(by.text('OK')).tap(); await element(by.text('OK')).tap();
}); });
it('should login with success', async() => { it('should login with success', async() => {
await element(by.id('login-view-password')).replaceText(data.users.regular.password); await element(by.id('login-view-password')).replaceText(data.users.regular.password);
await sleep(300);
await element(by.id('login-view-submit')).tap(); await element(by.id('login-view-submit')).tap();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(60000); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('rooms-list-view'))).toBeVisible();
}); });
}); });
}); });

View File

@ -1,9 +1,17 @@
const { const {
device, expect, element, by, waitFor device, expect, element, by, waitFor
} = require('detox'); } = require('detox');
const { logout, tapBack, sleep, searchRoom } = require('../../helpers/app'); const { login, navigateToLogin, logout, tapBack, sleep, searchRoom } = require('../../helpers/app');
const data = require('../../data');
describe('Rooms list screen', () => { describe('Rooms list screen', () => {
before(async() => {
await device.launchApp({ permissions: { notifications: 'YES' }, newInstance: true, delete: true });
await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password)
});
describe('Render', () => { describe('Render', () => {
it('should have rooms list screen', async() => { it('should have rooms list screen', async() => {
await expect(element(by.id('rooms-list-view'))).toBeVisible(); await expect(element(by.id('rooms-list-view'))).toBeVisible();
@ -29,18 +37,12 @@ describe('Rooms list screen', () => {
it('should search room and navigate', async() => { it('should search room and navigate', async() => {
await searchRoom('rocket.cat'); await searchRoom('rocket.cat');
await waitFor(element(by.id('rooms-list-view-item-rocket.cat'))).toBeVisible().withTimeout(60000); await waitFor(element(by.id('rooms-list-view-item-rocket.cat'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('rooms-list-view-item-rocket.cat'))).toBeVisible();
await element(by.id('rooms-list-view-item-rocket.cat')).tap(); await element(by.id('rooms-list-view-item-rocket.cat')).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(10000); await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(10000);
await expect(element(by.id('room-view'))).toBeVisible();
await waitFor(element(by.id('room-view-title-rocket.cat'))).toBeVisible().withTimeout(60000); await waitFor(element(by.id('room-view-title-rocket.cat'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('room-view-title-rocket.cat'))).toBeVisible();
await tapBack(); await tapBack();
await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('rooms-list-view'))).toBeVisible();
await sleep(2000);
await waitFor(element(by.id('rooms-list-view-item-rocket.cat'))).toExist().withTimeout(60000); await waitFor(element(by.id('rooms-list-view-item-rocket.cat'))).toExist().withTimeout(60000);
await expect(element(by.id('rooms-list-view-item-rocket.cat'))).toExist();
}); });
it('should logout', async() => { it('should logout', async() => {

View File

@ -2,48 +2,50 @@ const {
device, expect, element, by, waitFor device, expect, element, by, waitFor
} = require('detox'); } = require('detox');
const data = require('../../data'); const data = require('../../data');
const { tapBack, sleep, navigateToLogin, login } = require('../../helpers/app'); const { tapBack, sleep, navigateToLogin, login, tryTapping } = require('../../helpers/app');
describe('Create room screen', () => { describe('Create room screen', () => {
before(async() => { before(async() => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
await navigateToLogin(); await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password); await login(data.users.regular.username, data.users.regular.password);
await element(by.id('rooms-list-view-create-channel')).tap();
await waitFor(element(by.id('new-message-view'))).toExist().withTimeout(2000);
}); });
describe('New Message', async() => { describe('New Message', async() => {
before(async() => {
await element(by.id('rooms-list-view-create-channel')).tap();
});
describe('Render', async() => { describe('Render', async() => {
it('should have new message screen', async() => { it('should have new message screen', async() => {
await expect(element(by.id('new-message-view'))).toExist(); await waitFor(element(by.id('new-message-view'))).toBeVisible().withTimeout(2000);
}); });
it('should have search input', async() => { it('should have search input', async() => {
await waitFor(element(by.id('new-message-view-search'))).toExist().withTimeout(2000); await waitFor(element(by.id('new-message-view-search'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('new-message-view-search'))).toExist();
}); });
}) })
describe('Usage', async() => { describe('Usage', async() => {
it('should back to rooms list', async() => { it('should back to rooms list', async() => {
await sleep(1000); await waitFor(element(by.id('new-message-view-close'))).toBeVisible().withTimeout(2000);
await element(by.id('new-message-view-close')).tap(); await element(by.id('new-message-view-close')).tap();
await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(2000);
await expect(element(by.id('rooms-list-view'))).toExist(); await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
await element(by.id('rooms-list-view-create-channel')).tap();
await tryTapping(element(by.id('rooms-list-view-create-channel')), 3000);
//await element(by.id('rooms-list-view-create-channel')).tap();
await waitFor(element(by.id('new-message-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('new-message-view'))).toExist().withTimeout(2000);
await expect(element(by.id('new-message-view'))).toExist();
}); });
it('should search user and navigate', async() => { it('should search user and navigate', async() => {
await element(by.id('new-message-view-search')).replaceText('rocket.cat'); await element(by.id('new-message-view-search')).replaceText('rocket.cat');
await waitFor(element(by.id('new-message-view-item-rocket.cat'))).toExist().withTimeout(60000); await waitFor(element(by.id('new-message-view-item-rocket.cat'))).toExist().withTimeout(60000);
await expect(element(by.id('new-message-view-item-rocket.cat'))).toExist();
await element(by.id('new-message-view-item-rocket.cat')).tap(); await element(by.id('new-message-view-item-rocket.cat')).tap();
await waitFor(element(by.id('room-view'))).toExist().withTimeout(10000); await waitFor(element(by.id('room-view'))).toExist().withTimeout(10000);
await expect(element(by.id('room-view'))).toExist();
await waitFor(element(by.id('room-view-title-rocket.cat'))).toExist().withTimeout(60000); await waitFor(element(by.id('room-view-title-rocket.cat'))).toExist().withTimeout(60000);
await expect(element(by.id('room-view-title-rocket.cat'))).toExist();
await tapBack(); await tapBack();
await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(2000);
}); });
@ -51,11 +53,8 @@ describe('Create room screen', () => {
it('should navigate to select users', async() => { it('should navigate to select users', async() => {
await element(by.id('rooms-list-view-create-channel')).tap(); await element(by.id('rooms-list-view-create-channel')).tap();
await waitFor(element(by.id('new-message-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('new-message-view'))).toExist().withTimeout(2000);
await expect(element(by.id('new-message-view'))).toExist();
await sleep(1000);
await element(by.id('new-message-view-create-channel')).tap(); await element(by.id('new-message-view-create-channel')).tap();
await waitFor(element(by.id('select-users-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('select-users-view'))).toExist().withTimeout(2000);
await expect(element(by.id('select-users-view'))).toExist();
}); });
}) })
}); });
@ -108,7 +107,6 @@ describe('Create room screen', () => {
const room = `public${ data.random }`; const room = `public${ data.random }`;
await element(by.id('create-channel-name')).replaceText(room); await element(by.id('create-channel-name')).replaceText(room);
await element(by.id('create-channel-type')).tap(); await element(by.id('create-channel-type')).tap();
await sleep(1000);
await element(by.id('create-channel-submit')).tap(); 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'))).toExist().withTimeout(60000);
await expect(element(by.id('room-view'))).toExist(); await expect(element(by.id('room-view'))).toExist();
@ -123,20 +121,15 @@ describe('Create room screen', () => {
it('should create private room', async() => { it('should create private room', async() => {
const room = `private${ data.random }`; const room = `private${ data.random }`;
await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(2000);
// await device.launchApp({ newInstance: true });
await sleep(1000);
await element(by.id('rooms-list-view-create-channel')).tap(); await element(by.id('rooms-list-view-create-channel')).tap();
await waitFor(element(by.id('new-message-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('new-message-view'))).toExist().withTimeout(2000);
await sleep(1000);
await element(by.id('new-message-view-create-channel')).tap(); await element(by.id('new-message-view-create-channel')).tap();
await waitFor(element(by.id('select-users-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('select-users-view'))).toExist().withTimeout(2000);
await sleep(1000);
await element(by.id('select-users-view-item-rocket.cat')).tap(); await element(by.id('select-users-view-item-rocket.cat')).tap();
await waitFor(element(by.id('selected-user-rocket.cat'))).toExist().withTimeout(5000); await waitFor(element(by.id('selected-user-rocket.cat'))).toExist().withTimeout(5000);
await element(by.id('selected-users-view-submit')).tap(); await element(by.id('selected-users-view-submit')).tap();
await waitFor(element(by.id('create-channel-view'))).toExist().withTimeout(5000); await waitFor(element(by.id('create-channel-view'))).toExist().withTimeout(5000);
await element(by.id('create-channel-name')).replaceText(room); await element(by.id('create-channel-name')).replaceText(room);
await sleep(1000);
await element(by.id('create-channel-submit')).tap(); 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'))).toExist().withTimeout(60000);
await expect(element(by.id('room-view'))).toExist(); await expect(element(by.id('room-view'))).toExist();
@ -152,17 +145,13 @@ describe('Create room screen', () => {
const room = `empty${ data.random }`; const room = `empty${ data.random }`;
await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(2000);
// await device.launchApp({ newInstance: true }); // await device.launchApp({ newInstance: true });
await sleep(1000);
await element(by.id('rooms-list-view-create-channel')).tap(); await element(by.id('rooms-list-view-create-channel')).tap();
await waitFor(element(by.id('new-message-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('new-message-view'))).toExist().withTimeout(2000);
await sleep(1000);
await element(by.id('new-message-view-create-channel')).tap(); await element(by.id('new-message-view-create-channel')).tap();
await waitFor(element(by.id('select-users-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('select-users-view'))).toExist().withTimeout(2000);
await sleep(1000);
await element(by.id('selected-users-view-submit')).tap(); await element(by.id('selected-users-view-submit')).tap();
await waitFor(element(by.id('create-channel-view'))).toExist().withTimeout(5000); await waitFor(element(by.id('create-channel-view'))).toExist().withTimeout(5000);
await element(by.id('create-channel-name')).replaceText(room); await element(by.id('create-channel-name')).replaceText(room);
await sleep(1000);
await element(by.id('create-channel-submit')).tap(); 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'))).toExist().withTimeout(60000);
await expect(element(by.id('room-view'))).toExist(); await expect(element(by.id('room-view'))).toExist();

View File

@ -2,27 +2,29 @@ const {
device, expect, element, by, waitFor device, expect, element, by, waitFor
} = require('detox'); } = require('detox');
const data = require('../../data'); const data = require('../../data');
const { mockMessage, tapBack, sleep, searchRoom } = require('../../helpers/app'); const { navigateToLogin, login, mockMessage, tapBack, sleep, searchRoom, starMessage, pinMessage, dismissReviewNag, tryTapping } = require('../../helpers/app');
async function navigateToRoom() { async function navigateToRoom(roomName) {
await searchRoom(`private${ data.random }`); await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
await waitFor(element(by.id(`rooms-list-view-item-private${ data.random }`))).toExist().withTimeout(60000); await navigateToLogin();
await element(by.id(`rooms-list-view-item-private${ data.random }`)).tap(); await login(data.users.regular.username, data.users.regular.password);
await searchRoom(`${ roomName }`);
await waitFor(element(by.id(`rooms-list-view-item-${ roomName }`))).toExist().withTimeout(60000);
await element(by.id(`rooms-list-view-item-${ roomName }`)).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000); await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
} }
describe('Room screen', () => { describe('Room screen', () => {
const mainRoom = `private${ data.random }`; const mainRoom = data.groups.private.name;
before(async() => { before(async() => {
await navigateToRoom(); await navigateToRoom(mainRoom);
}); });
describe('Render', async() => { describe('Render', async() => {
it('should have room screen', async() => { it('should have room screen', async() => {
await expect(element(by.id('room-view'))).toExist(); await expect(element(by.id('room-view'))).toExist();
await waitFor(element(by.id(`room-view-title-${ mainRoom }`))).toExist().withTimeout(5000); await waitFor(element(by.id(`room-view-title-${ mainRoom }`))).toExist().withTimeout(5000);
await expect(element(by.id(`room-view-title-${ mainRoom }`))).toExist();
}); });
// Render - Header // Render - Header
@ -69,22 +71,15 @@ describe('Room screen', () => {
await expect(element(by.label(`${ data.random }message`)).atIndex(0)).toExist(); await expect(element(by.label(`${ data.random }message`)).atIndex(0)).toExist();
}); });
it('should ask for review', async() => {
await waitFor(element(by.text('Are you enjoying this app?'))).toExist().withTimeout(60000);
await expect(element(by.text('Are you enjoying this app?')).atIndex(0)).toExist();
await element(by.label('No').and(by.type('_UIAlertControllerActionView'))).tap(); // Tap `no` on ask for review alert
})
it('should show/hide emoji keyboard', async () => { it('should show/hide emoji keyboard', async () => {
if (device.getPlatform() === 'android') { if (device.getPlatform() === 'android') {
await element(by.id('messagebox-open-emoji')).tap(); await element(by.id('messagebox-open-emoji')).tap();
await waitFor(element(by.id('messagebox-keyboard-emoji'))).toExist().withTimeout(10000); await waitFor(element(by.id('messagebox-keyboard-emoji'))).toExist().withTimeout(10000);
await expect(element(by.id('messagebox-keyboard-emoji'))).toExist();
await expect(element(by.id('messagebox-close-emoji'))).toExist(); await expect(element(by.id('messagebox-close-emoji'))).toExist();
await expect(element(by.id('messagebox-open-emoji'))).toBeNotVisible(); await expect(element(by.id('messagebox-open-emoji'))).toBeNotVisible();
await element(by.id('messagebox-close-emoji')).tap(); await element(by.id('messagebox-close-emoji')).tap();
await waitFor(element(by.id('messagebox-keyboard-emoji'))).toBeNotVisible().withTimeout(10000); await waitFor(element(by.id('messagebox-keyboard-emoji'))).toBeNotVisible().withTimeout(10000);
await expect(element(by.id('messagebox-keyboard-emoji'))).toBeNotVisible();
await expect(element(by.id('messagebox-close-emoji'))).toBeNotVisible(); await expect(element(by.id('messagebox-close-emoji'))).toBeNotVisible();
await expect(element(by.id('messagebox-open-emoji'))).toExist(); await expect(element(by.id('messagebox-open-emoji'))).toExist();
} }
@ -94,10 +89,8 @@ describe('Room screen', () => {
await element(by.id('messagebox-input')).tap(); await element(by.id('messagebox-input')).tap();
await element(by.id('messagebox-input')).typeText(':joy'); await element(by.id('messagebox-input')).typeText(':joy');
await waitFor(element(by.id('messagebox-container'))).toExist().withTimeout(10000); await waitFor(element(by.id('messagebox-container'))).toExist().withTimeout(10000);
await expect(element(by.id('messagebox-container'))).toExist();
await element(by.id('messagebox-input')).clearText(); await element(by.id('messagebox-input')).clearText();
await waitFor(element(by.id('messagebox-container'))).toBeNotVisible().withTimeout(10000); await waitFor(element(by.id('messagebox-container'))).toBeNotVisible().withTimeout(10000);
await expect(element(by.id('messagebox-container'))).toBeNotVisible();
}); });
it('should show and tap on emoji autocomplete', async() => { it('should show and tap on emoji autocomplete', async() => {
@ -105,8 +98,6 @@ describe('Room screen', () => {
await element(by.id('messagebox-input')).replaceText(':'); await element(by.id('messagebox-input')).replaceText(':');
await element(by.id('messagebox-input')).typeText('joy'); // workaround for number keyboard await element(by.id('messagebox-input')).typeText('joy'); // workaround for number keyboard
await waitFor(element(by.id('messagebox-container'))).toExist().withTimeout(10000); await waitFor(element(by.id('messagebox-container'))).toExist().withTimeout(10000);
await expect(element(by.id('messagebox-container'))).toExist();
await sleep(1000);
await element(by.id('mention-item-joy')).tap(); await element(by.id('mention-item-joy')).tap();
await expect(element(by.id('messagebox-input'))).toHaveText(':joy: '); await expect(element(by.id('messagebox-input'))).toHaveText(':joy: ');
await element(by.id('messagebox-input')).clearText(); await element(by.id('messagebox-input')).clearText();
@ -116,25 +107,22 @@ describe('Room screen', () => {
const username = data.users.regular.username const username = data.users.regular.username
await element(by.id('messagebox-input')).tap(); await element(by.id('messagebox-input')).tap();
await element(by.id('messagebox-input')).typeText(`@${ username }`); await element(by.id('messagebox-input')).typeText(`@${ username }`);
await waitFor(element(by.id('messagebox-container'))).toExist().withTimeout(60000); await waitFor(element(by.id('messagebox-container'))).toExist().withTimeout(4000);
await expect(element(by.id('messagebox-container'))).toExist(); await waitFor(element(by.id(`mention-item-${ username }`))).toBeVisible().withTimeout(4000)
await sleep(1000); await tryTapping(element(by.id(`mention-item-${ username }`)), 2000, true);
await element(by.id(`mention-item-${ username }`)).tap();
await expect(element(by.id('messagebox-input'))).toHaveText(`@${ username } `); await expect(element(by.id('messagebox-input'))).toHaveText(`@${ username } `);
await element(by.id('messagebox-input')).tap(); await tryTapping(element(by.id('messagebox-input')), 2000)
await element(by.id('messagebox-input')).typeText(`${ data.random }mention`); await element(by.id('messagebox-input')).typeText(`${ data.random }mention`);
await element(by.id('messagebox-send-message')).tap(); await element(by.id('messagebox-send-message')).tap();
// await waitFor(element(by.label(`@${ data.user } ${ data.random }mention`)).atIndex(0)).toExist().withTimeout(60000); // await waitFor(element(by.label(`@${ data.user } ${ data.random }mention`)).atIndex(0)).toExist().withTimeout(60000);
await sleep(2000);
}); });
it('should show and tap on room autocomplete', async() => { it('should show and tap on room autocomplete', async() => {
await element(by.id('messagebox-input')).tap(); await element(by.id('messagebox-input')).tap();
await element(by.id('messagebox-input')).typeText('#general'); await element(by.id('messagebox-input')).typeText('#general');
await waitFor(element(by.id('messagebox-container'))).toExist().withTimeout(60000); //await waitFor(element(by.id('messagebox-container'))).toExist().withTimeout(4000);
await expect(element(by.id('messagebox-container'))).toExist(); await waitFor(element(by.id('mention-item-general'))).toBeVisible().withTimeout(4000);
await sleep(1000); await tryTapping(element(by.id('mention-item-general')), 2000, true)
await element(by.id('mention-item-general')).tap();
await expect(element(by.id('messagebox-input'))).toHaveText('#general '); await expect(element(by.id('messagebox-input'))).toHaveText('#general ');
await element(by.id('messagebox-input')).clearText(); await element(by.id('messagebox-input')).clearText();
}); });
@ -147,7 +135,6 @@ describe('Room screen', () => {
await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by.label('Permalink')).tap(); await element(by.label('Permalink')).tap();
await sleep(1000);
// TODO: test clipboard // TODO: test clipboard
}); });
@ -158,28 +145,20 @@ describe('Room screen', () => {
await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by.label('Copy')).tap(); await element(by.label('Copy')).tap();
await sleep(1000);
// TODO: test clipboard // TODO: test clipboard
}); });
it('should star message', async() => { it('should star message', async() => {
await element(by.label(`${ data.random }message`)).atIndex(0).longPress(); await 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 sleep(1000);
await waitFor(element(by.id('action-sheet'))).toNotExist().withTimeout(5000);
await sleep(1000) //https://github.com/RocketChat/Rocket.Chat.ReactNative/issues/2324
await element(by.label(`${ data.random }message`)).atIndex(0).longPress(); await element(by.label(`${ data.random }message`)).atIndex(0).longPress();
await expect(element(by.id('action-sheet'))).toExist(); await expect(element(by.id('action-sheet'))).toExist();
await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await waitFor(element(by.label('Unstar'))).toBeVisible().withTimeout(2000); await waitFor(element(by.label('Unstar'))).toBeVisible().withTimeout(2000);
await expect(element(by.label('Unstar'))).toBeVisible();
await element(by.id('action-sheet-backdrop')).tap(); await element(by.id('action-sheet-backdrop')).tap();
await sleep(1000);
}); });
it('should react to message', async() => { it('should react to message', async() => {
@ -189,14 +168,10 @@ describe('Room screen', () => {
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await element(by.id('add-reaction')).tap(); await element(by.id('add-reaction')).tap();
await waitFor(element(by.id('reaction-picker'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('reaction-picker'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('reaction-picker'))).toBeVisible();
await element(by.id('reaction-picker-😃')).tap(); await element(by.id('reaction-picker-😃')).tap();
await waitFor(element(by.id('reaction-picker-grinning'))).toExist().withTimeout(2000); await waitFor(element(by.id('reaction-picker-grinning'))).toExist().withTimeout(2000);
await expect(element(by.id('reaction-picker-grinning'))).toExist();
await element(by.id('reaction-picker-grinning')).tap(); await element(by.id('reaction-picker-grinning')).tap();
await waitFor(element(by.id('message-reaction-:grinning:'))).toExist().withTimeout(60000); await waitFor(element(by.id('message-reaction-:grinning:'))).toExist().withTimeout(60000);
await expect(element(by.id('message-reaction-:grinning:'))).toExist();
await sleep(1000);
}); });
it('should react to message with frequently used emoji', async() => { it('should react to message with frequently used emoji', async() => {
@ -205,30 +180,27 @@ describe('Room screen', () => {
await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
await waitFor(element(by.id('message-actions-emoji-+1'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('message-actions-emoji-+1'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('message-actions-emoji-+1'))).toBeVisible();
await element(by.id('message-actions-emoji-+1')).tap(); await element(by.id('message-actions-emoji-+1')).tap();
await waitFor(element(by.id('message-reaction-:+1:'))).toBeVisible().withTimeout(60000); await waitFor(element(by.id('message-reaction-:+1:'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('message-reaction-:+1:'))).toBeVisible();
await sleep(1000);
}); });
it('should show reaction picker on add reaction button pressed and have frequently used emoji', async() => { it('should show reaction picker on add reaction button pressed and have frequently used emoji', async() => {
await element(by.id('message-add-reaction')).tap(); await element(by.id('message-add-reaction')).tap();
await waitFor(element(by.id('reaction-picker'))).toExist().withTimeout(2000); await waitFor(element(by.id('reaction-picker'))).toExist().withTimeout(2000);
await expect(element(by.id('reaction-picker'))).toExist();
await waitFor(element(by.id('reaction-picker-grinning'))).toExist().withTimeout(2000); await waitFor(element(by.id('reaction-picker-grinning'))).toExist().withTimeout(2000);
await expect(element(by.id('reaction-picker-grinning'))).toExist();
await element(by.id('reaction-picker-😃')).tap(); await element(by.id('reaction-picker-😃')).tap();
await waitFor(element(by.id('reaction-picker-grimacing'))).toExist().withTimeout(2000); await waitFor(element(by.id('reaction-picker-grimacing'))).toExist().withTimeout(2000);
await element(by.id('reaction-picker-grimacing')).tap(); await element(by.id('reaction-picker-grimacing')).tap();
await waitFor(element(by.id('message-reaction-:grimacing:'))).toExist().withTimeout(60000); await waitFor(element(by.id('message-reaction-:grimacing:'))).toExist().withTimeout(60000);
await sleep(1000);
}); });
it('should ask for review', async() => {
await dismissReviewNag() //TODO: Create a proper test for this elsewhere.
})
it('should remove reaction', async() => { it('should remove reaction', async() => {
await element(by.id('message-reaction-:grinning:')).tap(); await element(by.id('message-reaction-:grinning:')).tap();
await waitFor(element(by.id('message-reaction-:grinning:'))).toBeNotVisible().withTimeout(60000); await waitFor(element(by.id('message-reaction-:grinning:'))).toBeNotVisible().withTimeout(60000);
await expect(element(by.id('message-reaction-:grinning:'))).toBeNotVisible();
}); });
it('should edit message', async() => { it('should edit message', async() => {
@ -241,7 +213,6 @@ describe('Room screen', () => {
await element(by.id('messagebox-input')).typeText('ed'); await element(by.id('messagebox-input')).typeText('ed');
await element(by.id('messagebox-send-message')).tap(); await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by.label(`${ data.random }edited (edited)`)).atIndex(0)).toExist().withTimeout(60000); await waitFor(element(by.label(`${ data.random }edited (edited)`)).atIndex(0)).toExist().withTimeout(60000);
await expect(element(by.label(`${ data.random }edited (edited)`)).atIndex(0)).toExist();
}); });
it('should quote message', async() => { it('should quote message', async() => {
@ -253,34 +224,29 @@ describe('Room screen', () => {
await element(by.label('Quote')).tap(); await element(by.label('Quote')).tap();
await element(by.id('messagebox-input')).typeText(`${ data.random }quoted`); await element(by.id('messagebox-input')).typeText(`${ data.random }quoted`);
await element(by.id('messagebox-send-message')).tap(); await element(by.id('messagebox-send-message')).tap();
await sleep(1000);
// TODO: test if quote was sent // TODO: test if quote was sent
}); });
it('should pin message', async() => { it('should pin message', async() => {
await waitFor(element(by.label(`${ data.random }edited (edited)`)).atIndex(0)).toExist(); await mockMessage('pin')
await element(by.label(`${ data.random }edited (edited)`)).atIndex(0).longPress(); await pinMessage('pin')
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 waitFor(element(by.id('action-sheet'))).toNotExist().withTimeout(5000);
await sleep(1500);
await waitFor(element(by.label(`${ data.random }edited (edited)`)).atIndex(0)).toBeVisible(); await waitFor(element(by.label(`${ data.random }pin`)).atIndex(0)).toBeVisible().withTimeout(2000);
await element(by.label(`${ data.random }edited (edited)`)).atIndex(0).longPress(); await waitFor(element(by.label('Message pinned')).atIndex(0)).toBeVisible().withTimeout(2000);
await expect(element(by.id('action-sheet'))).toExist(); await element(by.label(`${ data.random }pin`)).atIndex(0).longPress();
await waitFor(element(by.id('action-sheet'))).toExist().withTimeout(1000);
await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); 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'))).toBeVisible().withTimeout(2000);
await expect(element(by.label('Unpin'))).toBeVisible();
await element(by.id('action-sheet-backdrop')).tap(); await element(by.id('action-sheet-backdrop')).tap();
}); });
it('should delete message', async() => { it('should delete message', async() => {
await waitFor(element(by.label(`${ data.random }quoted`)).atIndex(0)).toBeVisible(); await mockMessage('delete')
await element(by.label(`${ data.random }quoted`)).atIndex(0).longPress();
await waitFor(element(by.label(`${ data.random }delete`)).atIndex(0)).toBeVisible();
await element(by.label(`${ data.random }delete`)).atIndex(0).longPress();
await expect(element(by.id('action-sheet'))).toExist(); await expect(element(by.id('action-sheet'))).toExist();
await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5); await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
@ -288,11 +254,9 @@ describe('Room screen', () => {
const deleteAlertMessage = 'You will not be able to recover this message!'; const deleteAlertMessage = 'You will not be able to recover this message!';
await waitFor(element(by.text(deleteAlertMessage)).atIndex(0)).toExist().withTimeout(10000); await waitFor(element(by.text(deleteAlertMessage)).atIndex(0)).toExist().withTimeout(10000);
await expect(element(by.text(deleteAlertMessage)).atIndex(0)).toExist();
await element(by.text('Delete')).tap(); await element(by.text('Delete')).tap();
await sleep(1000); await waitFor(element(by.label(`${ data.random }delete`)).atIndex(0)).toNotExist().withTimeout(2000);
await expect(element(by.label(`${ data.random }quoted`)).atIndex(0)).toNotExist();
}); });
}); });
@ -317,7 +281,6 @@ describe('Room screen', () => {
await waitFor(element(by.id(`room-view-title-${ thread }`))).toExist().withTimeout(5000); await waitFor(element(by.id(`room-view-title-${ thread }`))).toExist().withTimeout(5000);
await expect(element(by.id(`room-view-title-${ thread }`))).toExist(); await expect(element(by.id(`room-view-title-${ thread }`))).toExist();
await tapBack(); await tapBack();
await sleep(1000);
}); });
it('should toggle follow thread', async() => { it('should toggle follow thread', async() => {
@ -332,10 +295,13 @@ describe('Room screen', () => {
await waitFor(element(by.id('room-view-header-unfollow'))).toExist().withTimeout(60000); await waitFor(element(by.id('room-view-header-unfollow'))).toExist().withTimeout(60000);
await expect(element(by.id('room-view-header-unfollow'))).toExist(); await expect(element(by.id('room-view-header-unfollow'))).toExist();
await tapBack(); await tapBack();
await sleep(1000);
}); });
it('should navigate to thread from thread name', async() => { it('should navigate to thread from thread name', async() => {
await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ mainRoom }`)))).toBeVisible().withTimeout(2000);
await waitFor(element(by.id('room-view-header-actions').and(by.label(`${ data.random }thread`)))).toBeNotVisible().withTimeout(2000);
await sleep(500) //TODO: Find a better way to wait for the animation to finish and the messagebox-input to be available and usable :(
await mockMessage('dummymessagebetweenthethread'); await mockMessage('dummymessagebetweenthethread');
await element(by.label(thread)).atIndex(0).longPress(); await element(by.label(thread)).atIndex(0).longPress();
await expect(element(by.id('action-sheet'))).toExist(); await expect(element(by.id('action-sheet'))).toExist();
@ -352,10 +318,10 @@ describe('Room screen', () => {
await waitFor(element(by.id(`room-view-title-${ thread }`))).toExist().withTimeout(5000); await waitFor(element(by.id(`room-view-title-${ thread }`))).toExist().withTimeout(5000);
await expect(element(by.id(`room-view-title-${ thread }`))).toExist(); await expect(element(by.id(`room-view-title-${ thread }`))).toExist();
await tapBack(); await tapBack();
await sleep(1000);
}); });
it('should navigate to thread from threads view', async() => { it('should navigate to thread from threads view', async() => {
await waitFor(element(by.id('room-view-header-threads'))).toExist().withTimeout(1000);
await element(by.id('room-view-header-threads')).tap(); await element(by.id('room-view-header-threads')).tap();
await waitFor(element(by.id('thread-messages-view'))).toExist().withTimeout(5000); await waitFor(element(by.id('thread-messages-view'))).toExist().withTimeout(5000);
await expect(element(by.id('thread-messages-view'))).toExist(); await expect(element(by.id('thread-messages-view'))).toExist();

View File

@ -2,7 +2,7 @@ const {
device, expect, element, by, waitFor device, expect, element, by, waitFor
} = require('detox'); } = require('detox');
const data = require('../../data'); const data = require('../../data');
const { tapBack, sleep, searchRoom } = require('../../helpers/app'); const { navigateToLogin, login, tapBack, sleep, searchRoom, mockMessage, starMessage, pinMessage } = require('../../helpers/app');
const scrollDown = 200; const scrollDown = 200;
@ -11,13 +11,12 @@ async function navigateToRoomActions(type) {
if (type === 'd') { if (type === 'd') {
room = 'rocket.cat'; room = 'rocket.cat';
} else { } else {
room = `private${ data.random }`; room = data.groups.private.name;
} }
await searchRoom(room); await searchRoom(room);
await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toExist().withTimeout(60000); await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toExist().withTimeout(60000);
await element(by.id(`rooms-list-view-item-${ room }`)).tap(); await element(by.id(`rooms-list-view-item-${ room }`)).tap();
await waitFor(element(by.id('room-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-view'))).toExist().withTimeout(2000);
await sleep(1000);
await element(by.id('room-view-header-actions')).tap(); await element(by.id('room-view-header-actions')).tap();
await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000); await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000);
} }
@ -25,7 +24,6 @@ async function navigateToRoomActions(type) {
async function backToActions() { async function backToActions() {
await tapBack(); await tapBack();
await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(2000);
await expect(element(by.id('room-actions-view'))).toExist();
} }
async function backToRoomsList() { async function backToRoomsList() {
@ -36,10 +34,16 @@ async function backToRoomsList() {
} }
describe('Room actions screen', () => { describe('Room actions screen', () => {
before(async() => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password);
});
describe('Render', async() => { describe('Render', async() => {
describe('Direct', async() => { describe('Direct', async() => {
before(async() => { before(async() => {
await device.launchApp({ newInstance: true });
await navigateToRoomActions('d'); await navigateToRoomActions('d');
}); });
@ -197,65 +201,89 @@ describe('Room actions screen', () => {
it('should show mentioned messages', async() => { it('should show mentioned messages', async() => {
await element(by.id('room-actions-mentioned')).tap(); await element(by.id('room-actions-mentioned')).tap();
await waitFor(element(by.id('mentioned-messages-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('mentioned-messages-view'))).toExist().withTimeout(2000);
await expect(element(by.id('mentioned-messages-view'))).toExist();
// await waitFor(element(by.text(` ${ data.random }mention`))).toExist().withTimeout(60000); // await waitFor(element(by.text(` ${ data.random }mention`))).toExist().withTimeout(60000);
// await expect(element(by.text(` ${ data.random }mention`))).toExist();
await backToActions(); await backToActions();
}); });
it('should show starred message and unstar it', async() => { it('should show starred message and unstar it', async() => {
//Go back to room and send a message
await tapBack();
await mockMessage('messageToStar');
//Star the message
await starMessage('messageToStar')
//Back into Room Actions
await element(by.id('room-view-header-actions')).tap();
await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000);
//Go to starred messages
await element(by.id('room-actions-starred')).tap(); await element(by.id('room-actions-starred')).tap();
await waitFor(element(by.id('starred-messages-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('starred-messages-view'))).toExist().withTimeout(2000);
await sleep(1000); await waitFor(element(by.label(`${ data.random }messageToStar`).withAncestor(by.id('starred-messages-view')))).toBeVisible().withTimeout(60000);
await waitFor(element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toBeVisible().withTimeout(60000);
await expect(element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toBeVisible();
await element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view'))).longPress();
//Unstar message
await element(by.label(`${ data.random }messageToStar`).withAncestor(by.id('starred-messages-view'))).longPress();
await expect(element(by.id('action-sheet'))).toExist(); await expect(element(by.id('action-sheet'))).toExist();
await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.label('Unstar')).tap(); await element(by.label('Unstar')).tap();
await waitFor(element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toBeNotVisible().withTimeout(60000); await waitFor(element(by.label(`${ data.random }messageToStar`).withAncestor(by.id('starred-messages-view')))).toBeNotVisible().withTimeout(60000);
await expect(element(by.label(`${ data.random }message`).withAncestor(by.id('starred-messages-view')))).toBeNotVisible();
await backToActions(); await backToActions();
}); });
it('should show pinned message and unpin it', async() => { it('should show pinned message and unpin it', async() => {
//Go back to room and send a message
await tapBack();
await mockMessage('messageToPin');
//Pin the message
await pinMessage('messageToPin')
//Back into Room Actions
await element(by.id('room-view-header-actions')).tap();
await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000);
await waitFor(element(by.id('room-actions-pinned'))).toExist(); await waitFor(element(by.id('room-actions-pinned'))).toExist();
await element(by.id('room-actions-pinned')).tap(); await element(by.id('room-actions-pinned')).tap();
await waitFor(element(by.id('pinned-messages-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('pinned-messages-view'))).toExist().withTimeout(2000);
await sleep(1000); await waitFor(element(by.label(`${ data.random }messageToPin`).withAncestor(by.id('pinned-messages-view')))).toBeVisible().withTimeout(60000);
await waitFor(element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view')))).toBeVisible().withTimeout(60000); await element(by.label(`${ data.random }messageToPin`).withAncestor(by.id('pinned-messages-view'))).longPress();
await expect(element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view')))).toBeVisible();
await element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view'))).longPress();
await expect(element(by.id('action-sheet'))).toExist(); await expect(element(by.id('action-sheet'))).toExist();
await expect(element(by.id('action-sheet-handle'))).toBeVisible(); await expect(element(by.id('action-sheet-handle'))).toBeVisible();
await element(by.label('Unpin')).tap(); await element(by.label('Unpin')).tap();
await waitFor(element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view')))).toBeNotVisible().withTimeout(60000); await waitFor(element(by.label(`${ data.random }messageToPin`).withAncestor(by.id('pinned-messages-view')))).toBeNotVisible().withTimeout(60000);
await expect(element(by.label(`${ data.random }edited (edited)`).withAncestor(by.id('pinned-messages-view')))).toBeNotVisible();
await backToActions(); 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');
//Back into Room Actions
await element(by.id('room-view-header-actions')).tap();
await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000);
await element(by.id('room-actions-search')).tap(); await element(by.id('room-actions-search')).tap();
await waitFor(element(by.id('search-messages-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('search-messages-view'))).toExist().withTimeout(2000);
await expect(element(by.id('search-message-view-input'))).toExist(); await expect(element(by.id('search-message-view-input'))).toExist();
await element(by.id('search-message-view-input')).replaceText(`/${ data.random }message/`); await element(by.id('search-message-view-input')).replaceText(`/${ data.random }messageToFind/`);
await waitFor(element(by.label(`${ data.random }message`).withAncestor(by.id('search-messages-view')))).toExist().withTimeout(60000); await waitFor(element(by.label(`${ data.random }messageToFind`).withAncestor(by.id('search-messages-view')))).toExist().withTimeout(60000);
await expect(element(by.label(`${ data.random }message`).withAncestor(by.id('search-messages-view')))).toExist();
await backToActions(); await backToActions();
}); });
}); });
describe('Notification', async() => { describe('Notification', async() => {
it('should navigate to notification preference view', async() => { it('should navigate to notification preference view', async() => {
await waitFor(element(by.id('room-actions-notifications'))).toExist(); await waitFor(element(by.id('room-actions-notifications'))).toExist().withTimeout(2000);
await expect(element(by.id('room-actions-notifications'))).toExist();
await element(by.id('room-actions-notifications')).tap(); await element(by.id('room-actions-notifications')).tap();
await waitFor(element(by.id('notification-preference-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('notification-preference-view'))).toExist().withTimeout(2000);
await expect(element(by.id('notification-preference-view'))).toExist();
}); });
it('should have receive notification option', async() => { it('should have receive notification option', async() => {
@ -271,30 +299,25 @@ describe('Room actions screen', () => {
}); });
it('should have push notification option', async() => { it('should have push notification option', async() => {
await waitFor(element(by.id('notification-preference-view-push-notification'))).toExist(); await waitFor(element(by.id('notification-preference-view-push-notification'))).toExist().withTimeout(4000);
await expect(element(by.id('notification-preference-view-push-notification'))).toExist();
}); });
it('should have notification audio option', async() => { it('should have notification audio option', async() => {
await waitFor(element(by.id('notification-preference-view-audio'))).toExist(); await waitFor(element(by.id('notification-preference-view-audio'))).toExist().withTimeout(4000);
await expect(element(by.id('notification-preference-view-audio'))).toExist();
}); });
it('should have notification sound option', async() => { it('should have notification sound option', async() => {
// Ugly hack to scroll on detox // Ugly hack to scroll on detox
await element(by.type('UIScrollView')).atIndex(1).scrollTo('bottom'); await element(by.type('UIScrollView')).atIndex(1).scrollTo('bottom');
await waitFor(element(by.id('notification-preference-view-sound'))).toExist(); await waitFor(element(by.id('notification-preference-view-sound'))).toExist().withTimeout(4000);
await expect(element(by.id('notification-preference-view-sound'))).toExist();
}); });
it('should have notification duration option', async() => { it('should have notification duration option', async() => {
await waitFor(element(by.id('notification-preference-view-notification-duration'))).toExist(); await waitFor(element(by.id('notification-preference-view-notification-duration'))).toExist().withTimeout(4000);
await expect(element(by.id('notification-preference-view-notification-duration'))).toExist();
}); });
it('should have email alert option', async() => { it('should have email alert option', async() => {
await waitFor(element(by.id('notification-preference-view-email-alert'))).toExist(); await waitFor(element(by.id('notification-preference-view-email-alert'))).toExist().withTimeout(4000);
await expect(element(by.id('notification-preference-view-email-alert'))).toExist();
}); });
after(async() => { after(async() => {
@ -309,34 +332,28 @@ describe('Room actions screen', () => {
const user = data.users.alternate const user = data.users.alternate
it('should tap on leave channel and raise alert', async() => { it('should tap on leave channel and raise alert', async() => {
await waitFor(element(by.id('room-actions-leave-channel'))).toExist(); await waitFor(element(by.id('room-actions-leave-channel'))).toExist().withTimeout(2000);
await expect(element(by.id('room-actions-leave-channel'))).toExist();
await element(by.id('room-actions-leave-channel')).tap(); await element(by.id('room-actions-leave-channel')).tap();
await waitFor(element(by.text('Yes, leave it!'))).toExist().withTimeout(2000); await waitFor(element(by.text('Yes, leave it!'))).toExist().withTimeout(2000);
await expect(element(by.text('Yes, leave it!'))).toExist();
await element(by.text('Yes, leave it!')).tap(); await element(by.text('Yes, leave it!')).tap();
await waitFor(element(by.text('You are the last owner. Please set new owner before leaving the room.'))).toExist().withTimeout(60000); await waitFor(element(by.text('You are the last owner. Please set new owner before leaving the room.'))).toExist().withTimeout(8000);
await expect(element(by.text('You are the last owner. Please set new owner before leaving the room.'))).toExist();
await element(by.text('OK')).tap(); await element(by.text('OK')).tap();
await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(2000);
}); });
it('should add user to the room', async() => { it('should add user to the room', async() => {
await waitFor(element(by.id('room-actions-add-user'))).toExist(); await waitFor(element(by.id('room-actions-add-user'))).toExist().withTimeout(4000);
await element(by.id('room-actions-add-user')).tap(); await element(by.id('room-actions-add-user')).tap();
await element(by.id('select-users-view-search')).tap(); await element(by.id('select-users-view-search')).tap();
await element(by.id('select-users-view-search')).replaceText(user.username); await element(by.id('select-users-view-search')).replaceText(user.username);
await waitFor(element(by.id(`select-users-view-item-${ user.username }`))).toExist().withTimeout(60000); await waitFor(element(by.id(`select-users-view-item-${ user.username }`))).toExist().withTimeout(10000);
await expect(element(by.id(`select-users-view-item-${ user.username }`))).toExist();
await element(by.id(`select-users-view-item-${ user.username }`)).tap(); await element(by.id(`select-users-view-item-${ user.username }`)).tap();
await waitFor(element(by.id(`selected-user-${ user.username }`))).toExist().withTimeout(5000); await waitFor(element(by.id(`selected-user-${ user.username }`))).toExist().withTimeout(5000);
await expect(element(by.id(`selected-user-${ user.username }`))).toExist();
await element(by.id('selected-users-view-submit')).tap(); await element(by.id('selected-users-view-submit')).tap();
await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(2000);
await element(by.id('room-actions-members')).tap(); await element(by.id('room-actions-members')).tap();
await element(by.id('room-members-view-toggle-status')).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 waitFor(element(by.id(`room-members-view-item-${ user.username }`))).toExist().withTimeout(60000);
await expect(element(by.id(`room-members-view-item-${ user.username }`))).toExist();
await backToActions(1); await backToActions(1);
}); });
@ -344,26 +361,20 @@ describe('Room actions screen', () => {
before(async() => { before(async() => {
await element(by.id('room-actions-members')).tap(); await element(by.id('room-actions-members')).tap();
await waitFor(element(by.id('room-members-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-members-view'))).toExist().withTimeout(2000);
await expect(element(by.id('room-members-view'))).toExist();
}); });
it('should show all users', async() => { it('should show all users', async() => {
await sleep(1000);
await element(by.id('room-members-view-toggle-status')).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 waitFor(element(by.id(`room-members-view-item-${ user.username }`))).toExist().withTimeout(60000);
await expect(element(by.id(`room-members-view-item-${ user.username }`))).toExist();
}); });
it('should filter user', async() => { it('should filter user', async() => {
await waitFor(element(by.id(`room-members-view-item-${ user.username }`))).toExist().withTimeout(60000); await waitFor(element(by.id(`room-members-view-item-${ user.username }`))).toExist().withTimeout(60000);
await expect(element(by.id(`room-members-view-item-${ user.username }`))).toExist();
await element(by.id('room-members-view-search')).replaceText('rocket'); 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 waitFor(element(by.id(`room-members-view-item-${ user.username }`))).toBeNotVisible().withTimeout(60000);
await expect(element(by.id(`room-members-view-item-${ user.username }`))).toBeNotVisible();
await element(by.id('room-members-view-search')).tap(); await element(by.id('room-members-view-search')).tap();
await element(by.id('room-members-view-search')).clearText(''); await element(by.id('room-members-view-search')).clearText('');
await waitFor(element(by.id(`room-members-view-item-${ user.username }`))).toExist().withTimeout(60000); await waitFor(element(by.id(`room-members-view-item-${ user.username }`))).toExist().withTimeout(60000);
await expect(element(by.id(`room-members-view-item-${ user.username }`))).toExist();
}); });
// FIXME: mute/unmute isn't working // FIXME: mute/unmute isn't working
@ -391,9 +402,7 @@ describe('Room actions screen', () => {
await waitFor(element(by.id(`room-members-view-item-${ user.username }`))).toExist().withTimeout(5000); await waitFor(element(by.id(`room-members-view-item-${ user.username }`))).toExist().withTimeout(5000);
await element(by.id(`room-members-view-item-${ user.username }`)).tap(); await element(by.id(`room-members-view-item-${ user.username }`)).tap();
await waitFor(element(by.id('room-view'))).toExist().withTimeout(60000); await waitFor(element(by.id('room-view'))).toExist().withTimeout(60000);
await expect(element(by.id('room-view'))).toExist();
await waitFor(element(by.id(`room-view-title-${ user.username }`))).toExist().withTimeout(60000); await waitFor(element(by.id(`room-view-title-${ user.username }`))).toExist().withTimeout(60000);
await expect(element(by.id(`room-view-title-${ user.username }`))).toExist();
await tapBack(); await tapBack();
await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(2000);
}); });
@ -407,13 +416,10 @@ describe('Room actions screen', () => {
it('should block/unblock user', async() => { it('should block/unblock user', async() => {
await waitFor(element(by.id('room-actions-block-user'))).toExist(); await waitFor(element(by.id('room-actions-block-user'))).toExist();
await sleep(1000);
await element(by.id('room-actions-block-user')).tap(); await element(by.id('room-actions-block-user')).tap();
await waitFor(element(by.label('Unblock user'))).toExist().withTimeout(60000); await waitFor(element(by.label('Unblock user'))).toExist().withTimeout(60000);
await expect(element(by.label('Unblock user'))).toExist();
await element(by.id('room-actions-block-user')).tap(); await element(by.id('room-actions-block-user')).tap();
await waitFor(element(by.label('Block user'))).toExist().withTimeout(60000); await waitFor(element(by.label('Block user'))).toExist().withTimeout(60000);
await expect(element(by.label('Block user'))).toExist();
}); });
}); });
}); });

View File

@ -2,23 +2,23 @@ const {
device, expect, element, by, waitFor device, expect, element, by, waitFor
} = require('detox'); } = require('detox');
const data = require('../../data'); const data = require('../../data');
const { tapBack, sleep, searchRoom } = require('../../helpers/app'); const { navigateToLogin, login, tapBack, sleep, searchRoom } = require('../../helpers/app');
const privateRoomName = data.groups.private.name
async function navigateToRoomInfo(type) { async function navigateToRoomInfo(type) {
let room; let room;
if (type === 'd') { if (type === 'd') {
room = 'rocket.cat'; room = 'rocket.cat';
} else { } else {
room = `private${ data.random }`; room = privateRoomName;
} }
await searchRoom(room); await searchRoom(room);
await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toExist().withTimeout(60000); await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toExist().withTimeout(60000);
await element(by.id(`rooms-list-view-item-${ room }`)).tap(); await element(by.id(`rooms-list-view-item-${ room }`)).tap();
await waitFor(element(by.id('room-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-view'))).toExist().withTimeout(2000);
await sleep(1000);
await element(by.id('room-view-header-actions')).tap(); await element(by.id('room-view-header-actions')).tap();
await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000); await waitFor(element(by.id('room-actions-view'))).toExist().withTimeout(5000);
await sleep(1000);
await element(by.id('room-actions-info')).tap(); await element(by.id('room-actions-info')).tap();
await waitFor(element(by.id('room-info-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-info-view'))).toExist().withTimeout(2000);
} }
@ -28,13 +28,19 @@ async function waitForToast() {
// await expect(element(by.id('toast'))).toExist(); // await expect(element(by.id('toast'))).toExist();
// await waitFor(element(by.id('toast'))).toBeNotVisible().withTimeout(10000); // await waitFor(element(by.id('toast'))).toBeNotVisible().withTimeout(10000);
// await expect(element(by.id('toast'))).toBeNotVisible(); // await expect(element(by.id('toast'))).toBeNotVisible();
await sleep(5000); await sleep(1);
} }
describe('Room info screen', () => { describe('Room info screen', () => {
before(async() => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password);
});
describe('Direct', async() => { describe('Direct', async() => {
before(async() => { before(async() => {
await device.launchApp({ newInstance: true });
await navigateToRoomInfo('d'); await navigateToRoomInfo('d');
}); });
@ -42,11 +48,16 @@ describe('Room info screen', () => {
await expect(element(by.id('room-info-view'))).toExist(); await expect(element(by.id('room-info-view'))).toExist();
await expect(element(by.id('room-info-view-name'))).toExist(); await expect(element(by.id('room-info-view-name'))).toExist();
}); });
after(async() => {
await tapBack()
await tapBack()
await tapBack()
})
}); });
describe('Channel/Group', async() => { describe('Channel/Group', async() => {
before(async() => { before(async() => {
await device.launchApp({ newInstance: true });
await navigateToRoomInfo('c'); await navigateToRoomInfo('c');
}); });
@ -78,7 +89,6 @@ describe('Room info screen', () => {
describe('Render Edit', async() => { describe('Render Edit', async() => {
before(async() => { before(async() => {
await sleep(1000);
await waitFor(element(by.id('room-info-view-edit-button'))).toExist().withTimeout(10000); await waitFor(element(by.id('room-info-view-edit-button'))).toExist().withTimeout(10000);
await element(by.id('room-info-view-edit-button')).tap(); await element(by.id('room-info-view-edit-button')).tap();
await waitFor(element(by.id('room-info-edit-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-info-edit-view'))).toExist().withTimeout(2000);
@ -141,7 +151,6 @@ describe('Room info screen', () => {
}); });
describe('Usage', async() => { describe('Usage', async() => {
const room = `private${ data.random }`;
// it('should enter "invalid name" and get error', async() => { // it('should enter "invalid name" and get error', async() => {
// await element(by.type('UIScrollView')).atIndex(1).swipe('down'); // await element(by.type('UIScrollView')).atIndex(1).swipe('down');
// await element(by.id('room-info-edit-view-name')).replaceText('invalid name'); // await element(by.id('room-info-edit-view-name')).replaceText('invalid name');
@ -155,22 +164,17 @@ describe('Room info screen', () => {
// }); // });
it('should change room name', async() => { it('should change room name', async() => {
await element(by.id('room-info-edit-view-name')).replaceText(`${ room }new`); await element(by.id('room-info-edit-view-name')).replaceText(`${ privateRoomName }new`);
await element(by.type('UIScrollView')).atIndex(1).swipe('up'); await element(by.type('UIScrollView')).atIndex(1).swipe('up');
await element(by.id('room-info-edit-view-submit')).tap(); await element(by.id('room-info-edit-view-submit')).tap();
await sleep(5000);
await tapBack(); await tapBack();
await waitFor(element(by.id('room-info-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-info-view'))).toExist().withTimeout(2000);
await sleep(1000); await expect(element(by.id('room-info-view-name'))).toHaveLabel(`${ privateRoomName }new`);
await expect(element(by.id('room-info-view-name'))).toHaveLabel(`${ room }new`);
// change name to original // change name to original
await element(by.id('room-info-view-edit-button')).tap(); await element(by.id('room-info-view-edit-button')).tap();
await sleep(1000);
await waitFor(element(by.id('room-info-edit-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-info-edit-view'))).toExist().withTimeout(2000);
await sleep(1000); await element(by.id('room-info-edit-view-name')).replaceText(`${ privateRoomName }`);
await element(by.id('room-info-edit-view-name')).replaceText(`${ room }`);
await element(by.type('UIScrollView')).atIndex(1).swipe('up'); await element(by.type('UIScrollView')).atIndex(1).swipe('up');
await sleep(1000);
await element(by.id('room-info-edit-view-submit')).tap(); await element(by.id('room-info-edit-view-submit')).tap();
await waitForToast(); await waitForToast();
await element(by.type('UIScrollView')).atIndex(1).swipe('down'); await element(by.type('UIScrollView')).atIndex(1).swipe('down');
@ -184,14 +188,11 @@ describe('Room info screen', () => {
await element(by.id('room-info-edit-view-password')).replaceText('abc'); await element(by.id('room-info-edit-view-password')).replaceText('abc');
await element(by.type('UIScrollView')).atIndex(1).swipe('up'); await element(by.type('UIScrollView')).atIndex(1).swipe('up');
await element(by.id('room-info-edit-view-t')).tap(); await element(by.id('room-info-edit-view-t')).tap();
await sleep(1000); await element(by.id('room-info-edit-view-ro')).longPress(); //https://github.com/facebook/react-native/issues/28032
await element(by.id('room-info-edit-view-ro')).tap();
await sleep(1000);
await element(by.id('room-info-edit-view-react-when-ro')).tap(); await element(by.id('room-info-edit-view-react-when-ro')).tap();
await sleep(1000);
await element(by.id('room-info-edit-view-reset')).tap(); await element(by.id('room-info-edit-view-reset')).tap();
// after reset // after reset
await expect(element(by.id('room-info-edit-view-name'))).toHaveText(room); await expect(element(by.id('room-info-edit-view-name'))).toHaveText(privateRoomName);
await expect(element(by.id('room-info-edit-view-description'))).toHaveText(''); await expect(element(by.id('room-info-edit-view-description'))).toHaveText('');
await expect(element(by.id('room-info-edit-view-topic'))).toHaveText(''); await expect(element(by.id('room-info-edit-view-topic'))).toHaveText('');
await expect(element(by.id('room-info-edit-view-announcement'))).toHaveText(''); await expect(element(by.id('room-info-edit-view-announcement'))).toHaveText('');
@ -203,55 +204,45 @@ describe('Room info screen', () => {
}); });
it('should change room description', async() => { it('should change room description', async() => {
await sleep(1000);
await element(by.id('room-info-edit-view-description')).replaceText('new description'); await element(by.id('room-info-edit-view-description')).replaceText('new description');
await element(by.type('UIScrollView')).atIndex(1).swipe('up'); await element(by.type('UIScrollView')).atIndex(1).swipe('up');
await element(by.id('room-info-edit-view-submit')).tap(); await element(by.id('room-info-edit-view-submit')).tap();
await waitForToast(); await waitForToast();
await tapBack(); await tapBack();
await waitFor(element(by.id('room-info-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-info-view'))).toExist().withTimeout(2000);
await sleep(1000);
await expect(element(by.label('new description').withAncestor(by.id('room-info-view-description')))).toExist(); await expect(element(by.label('new description').withAncestor(by.id('room-info-view-description')))).toExist();
}); });
it('should change room topic', async() => { it('should change room topic', async() => {
await sleep(1000);
await waitFor(element(by.id('room-info-view-edit-button'))).toExist().withTimeout(10000); await waitFor(element(by.id('room-info-view-edit-button'))).toExist().withTimeout(10000);
await element(by.id('room-info-view-edit-button')).tap(); await element(by.id('room-info-view-edit-button')).tap();
await waitFor(element(by.id('room-info-edit-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-info-edit-view'))).toExist().withTimeout(2000);
await sleep(1000);
await element(by.id('room-info-edit-view-topic')).replaceText('new topic'); await element(by.id('room-info-edit-view-topic')).replaceText('new topic');
await element(by.type('UIScrollView')).atIndex(1).swipe('up'); await element(by.type('UIScrollView')).atIndex(1).swipe('up');
await element(by.id('room-info-edit-view-submit')).tap(); await element(by.id('room-info-edit-view-submit')).tap();
await waitForToast(); await waitForToast();
await tapBack(); await tapBack();
await waitFor(element(by.id('room-info-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-info-view'))).toExist().withTimeout(2000);
await sleep(1000);
await expect(element(by.label('new topic').withAncestor(by.id('room-info-view-topic')))).toExist(); await expect(element(by.label('new topic').withAncestor(by.id('room-info-view-topic')))).toExist();
}); });
it('should change room announcement', async() => { it('should change room announcement', async() => {
await sleep(1000);
await waitFor(element(by.id('room-info-view-edit-button'))).toExist().withTimeout(10000); await waitFor(element(by.id('room-info-view-edit-button'))).toExist().withTimeout(10000);
await element(by.id('room-info-view-edit-button')).tap(); await element(by.id('room-info-view-edit-button')).tap();
await waitFor(element(by.id('room-info-edit-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-info-edit-view'))).toExist().withTimeout(2000);
await sleep(1000);
await element(by.id('room-info-edit-view-announcement')).replaceText('new announcement'); await element(by.id('room-info-edit-view-announcement')).replaceText('new announcement');
await element(by.type('UIScrollView')).atIndex(1).swipe('up'); await element(by.type('UIScrollView')).atIndex(1).swipe('up');
await element(by.id('room-info-edit-view-submit')).tap(); await element(by.id('room-info-edit-view-submit')).tap();
await waitForToast(); await waitForToast();
await tapBack(); await tapBack();
await waitFor(element(by.id('room-info-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-info-view'))).toExist().withTimeout(2000);
await sleep(1000);
await expect(element(by.label('new announcement').withAncestor(by.id('room-info-view-announcement')))).toExist(); await expect(element(by.label('new announcement').withAncestor(by.id('room-info-view-announcement')))).toExist();
}); });
it('should change room password', async() => { it('should change room password', async() => {
await sleep(1000);
await waitFor(element(by.id('room-info-view-edit-button'))).toExist().withTimeout(10000); await waitFor(element(by.id('room-info-view-edit-button'))).toExist().withTimeout(10000);
await element(by.id('room-info-view-edit-button')).tap(); await element(by.id('room-info-view-edit-button')).tap();
await waitFor(element(by.id('room-info-edit-view'))).toExist().withTimeout(2000); await waitFor(element(by.id('room-info-edit-view'))).toExist().withTimeout(2000);
await sleep(1000);
await element(by.type('UIScrollView')).atIndex(1).swipe('up'); await element(by.type('UIScrollView')).atIndex(1).swipe('up');
await element(by.id('room-info-edit-view-password')).replaceText('password'); await element(by.id('room-info-edit-view-password')).replaceText('password');
await element(by.id('room-info-edit-view-submit')).tap(); await element(by.id('room-info-edit-view-submit')).tap();
@ -259,7 +250,6 @@ describe('Room info screen', () => {
}); });
it('should change room type', async() => { it('should change room type', async() => {
await sleep(1000);
await element(by.type('UIScrollView')).atIndex(1).swipe('up'); await element(by.type('UIScrollView')).atIndex(1).swipe('up');
await element(by.id('room-info-edit-view-t')).tap(); await element(by.id('room-info-edit-view-t')).tap();
await element(by.id('room-info-edit-view-submit')).tap(); await element(by.id('room-info-edit-view-submit')).tap();
@ -282,14 +272,11 @@ describe('Room info screen', () => {
// }); // });
it('should archive room', async() => { it('should archive room', async() => {
await sleep(1000);
await element(by.type('UIScrollView')).atIndex(1).swipe('up'); await element(by.type('UIScrollView')).atIndex(1).swipe('up');
await element(by.id('room-info-edit-view-archive')).tap(); await element(by.id('room-info-edit-view-archive')).tap();
await waitFor(element(by.text('Yes, archive it!'))).toExist().withTimeout(5000); await waitFor(element(by.text('Yes, archive it!'))).toExist().withTimeout(5000);
await expect(element(by.text('Yes, archive it!'))).toExist();
await element(by.text('Yes, archive it!')).tap(); await element(by.text('Yes, archive it!')).tap();
await waitFor(element(by.id('room-info-edit-view-unarchive'))).toExist().withTimeout(60000); await waitFor(element(by.id('room-info-edit-view-unarchive'))).toExist().withTimeout(60000);
await expect(element(by.id('room-info-edit-view-unarchive'))).toExist();
await expect(element(by.id('room-info-edit-view-archive'))).toBeNotVisible(); await expect(element(by.id('room-info-edit-view-archive'))).toBeNotVisible();
// TODO: needs permission to unarchive // TODO: needs permission to unarchive
// await element(by.id('room-info-edit-view-archive')).tap(); // await element(by.id('room-info-edit-view-archive')).tap();
@ -301,16 +288,12 @@ describe('Room info screen', () => {
}); });
it('should delete room', async() => { it('should delete room', async() => {
await sleep(1000);
await element(by.type('UIScrollView')).atIndex(1).swipe('up'); await element(by.type('UIScrollView')).atIndex(1).swipe('up');
await element(by.id('room-info-edit-view-delete')).tap(); await element(by.id('room-info-edit-view-delete')).tap();
await waitFor(element(by.text('Yes, delete it!'))).toExist().withTimeout(5000); await waitFor(element(by.text('Yes, delete it!'))).toExist().withTimeout(5000);
await expect(element(by.text('Yes, delete it!'))).toExist();
await element(by.text('Yes, delete it!')).tap(); 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'))).toExist().withTimeout(10000);
await sleep(2000); await waitFor(element(by.id(`rooms-list-view-item-${ privateRoomName }`))).toBeNotVisible().withTimeout(60000);
await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible().withTimeout(60000);
await expect(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible();
}); });
}); });
}); });

View File

@ -1,143 +1,23 @@
platform :ios, '11.0' platform :ios, '11.0'
require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
require_relative '../node_modules/react-native-unimodules/cocoapods.rb' require_relative '../node_modules/react-native-unimodules/cocoapods.rb'
def add_flipper_pods!(versions = {}) def all_pods
versions['Flipper'] ||= '~> 0.33.1' config = use_native_modules!
versions['DoubleConversion'] ||= '1.1.7' use_unimodules!
versions['Flipper-Folly'] ||= '~> 2.1' use_react_native!(:path => config["reactNativePath"])
versions['Flipper-Glog'] ||= '0.3.6' use_flipper!
versions['Flipper-PeerTalk'] ||= '~> 0.0.4'
versions['Flipper-RSocket'] ||= '~> 1.0'
pod 'FlipperKit', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FlipperKitLayoutPlugin', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/SKIOSNetworkPlugin', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FlipperKitUserDefaultsPlugin', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FlipperKitReactPlugin', versions['Flipper'], :configuration => 'Debug'
# List all transitive dependencies for FlipperKit pods
# to avoid them being linked in Release builds
pod 'Flipper', versions['Flipper'], :configuration => 'Debug'
pod 'Flipper-DoubleConversion', versions['DoubleConversion'], :configuration => 'Debug'
pod 'Flipper-Folly', versions['Flipper-Folly'], :configuration => 'Debug'
pod 'Flipper-Glog', versions['Flipper-Glog'], :configuration => 'Debug'
pod 'Flipper-PeerTalk', versions['Flipper-PeerTalk'], :configuration => 'Debug'
pod 'Flipper-RSocket', versions['Flipper-RSocket'], :configuration => 'Debug'
pod 'FlipperKit/Core', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/CppBridge', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FBCxxFollyDynamicConvert', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FBDefines', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FKPortForwarding', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FlipperKitHighlightOverlay', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FlipperKitLayoutTextSearchable', versions['Flipper'], :configuration => 'Debug'
pod 'FlipperKit/FlipperKitNetworkPlugin', versions['Flipper'], :configuration => 'Debug'
end
# Post Install processing for Flipper
def flipper_post_install(installer)
installer.pods_project.targets.each do |target|
if target.name == 'YogaKit'
target.build_configurations.each do |config|
config.build_settings['SWIFT_VERSION'] = '4.1'
end
end
end
file_name = Dir.glob("*.xcodeproj")[0]
app_project = Xcodeproj::Project.open(file_name)
app_project.native_targets.each do |target|
target.build_configurations.each do |config|
cflags = config.build_settings['OTHER_CFLAGS'] || '$(inherited) '
unless cflags.include? '-DFB_SONARKIT_ENABLED=1'
puts 'Adding -DFB_SONARKIT_ENABLED=1 in OTHER_CFLAGS...'
cflags << '-DFB_SONARKIT_ENABLED=1'
end
config.build_settings['OTHER_CFLAGS'] = cflags
end
app_project.save
end
installer.pods_project.save
end end
target 'RocketChatRN' do target 'RocketChatRN' do
pod 'FBLazyVector', :path => "../node_modules/react-native/Libraries/FBLazyVector" all_pods
pod 'FBReactNativeSpec', :path => "../node_modules/react-native/Libraries/FBReactNativeSpec"
pod 'RCTRequired', :path => "../node_modules/react-native/Libraries/RCTRequired"
pod 'RCTTypeSafety', :path => "../node_modules/react-native/Libraries/TypeSafety"
pod 'React', :path => '../node_modules/react-native/'
pod 'React-Core', :path => '../node_modules/react-native/'
pod 'React-CoreModules', :path => '../node_modules/react-native/React/CoreModules'
pod 'React-Core/DevSupport', :path => '../node_modules/react-native/'
pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS'
pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation'
pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob'
pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image'
pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS'
pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network'
pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings'
pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text'
pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration'
pod 'React-Core/RCTWebSocket', :path => '../node_modules/react-native/'
pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact'
pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi'
pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor'
pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector'
pod 'ReactCommon/callinvoker', :path => "../node_modules/react-native/ReactCommon"
pod 'ReactCommon/turbomodule/core', :path => "../node_modules/react-native/ReactCommon"
pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga', :modular_headers => true
pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec'
pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'
use_native_modules!
use_unimodules!
end end
target 'ShareRocketChatRN' do target 'ShareRocketChatRN' do
pod 'FBLazyVector', :path => "../node_modules/react-native/Libraries/FBLazyVector" all_pods
pod 'FBReactNativeSpec', :path => "../node_modules/react-native/Libraries/FBReactNativeSpec"
pod 'RCTRequired', :path => "../node_modules/react-native/Libraries/RCTRequired"
pod 'RCTTypeSafety', :path => "../node_modules/react-native/Libraries/TypeSafety"
pod 'React', :path => '../node_modules/react-native/'
pod 'React-Core', :path => '../node_modules/react-native/'
pod 'React-CoreModules', :path => '../node_modules/react-native/React/CoreModules'
pod 'React-Core/DevSupport', :path => '../node_modules/react-native/'
pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS'
pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation'
pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob'
pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image'
pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS'
pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network'
pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings'
pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text'
pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration'
pod 'React-Core/RCTWebSocket', :path => '../node_modules/react-native/'
pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact'
pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi'
pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor'
pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector'
pod 'ReactCommon/callinvoker', :path => "../node_modules/react-native/ReactCommon"
pod 'ReactCommon/turbomodule/core', :path => "../node_modules/react-native/ReactCommon"
pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga', :modular_headers => true
pod 'JitsiMeetSDK', :git => 'https://github.com/RocketChat/jitsi-meet-ios-sdk-releases.git'
pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec'
pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'
use_native_modules!
use_unimodules!
end end
# Enables Flipper.
#
# Note that if you have use_frameworks! enabled, Flipper will not work and
# you should disable these next few lines.
add_flipper_pods!
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
target.build_configurations.each do |config| target.build_configurations.each do |config|

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
We've now combined all our supported platforms into a single podspec. As a result, we moved our submit script to a new location for Cocoapods projects: ${PODS_ROOT}/Crashlytics/submit. To avoid breaking functionality that references the old location of the submit, we've placed this dummy script that calls to the correct location, while providing a helpful warning if it is invoked. This bridge for backwards compatibility will be removed in a future release, so please heed the warning!

View File

@ -1,6 +0,0 @@
if [[ -z $PODS_ROOT ]]; then
echo "error: The submit binary delivered by cocoapods is in a new location, under '$"{"PODS_ROOT"}"/Crashlytics/submit'. This script was put in place for backwards compatibility, but it relies on PODS_ROOT, which does not have a value in your current setup. Please update the path to the submit binary to fix this issue."
else
echo "warning: The submit script is now located at '$"{"PODS_ROOT"}"/Crashlytics/submit'. To remove this warning, update your path to point to this new location."
sh "${PODS_ROOT}/Crashlytics/submit" "$@"
fi

View File

@ -1,23 +0,0 @@
# Crashlytics
## Overview
[Crashlytics](https://firebase.google.com/docs/crashlytics/get-started?platform=ios) offers the most powerful, yet lightest weight crash reporting solution for iOS.
## Setup
To start using Crashlytics, there are two options:
1) The recommended way is to go to the [Firebase Crashlytics Docs](https://firebase.google.com/docs/crashlytics/get-started?platform=ios) and follow the directions there.
2) If you aren't using Firebase yet, go to [Fabric Kits](https://fabric.io/kits), and follow the directions for Crashlytics.
## Resources
* [API Reference](https://firebase.google.com/docs/reference/ios/crashlytics/api/reference/Classes)
* [Forums](https://stackoverflow.com/questions/tagged/google-fabric)
* [Website](https://firebase.google.com/docs/crashlytics)
* Follow us on Twitter: [@crashlytics](https://twitter.com/crashlytics)

Binary file not shown.

View File

@ -1,31 +0,0 @@
//
// ANSCompatibility.h
// AnswersKit
//
// Copyright (c) 2015 Crashlytics, Inc. All rights reserved.
//
#pragma once
#if !__has_feature(nullability)
#define nonnull
#define nullable
#define _Nullable
#define _Nonnull
#endif
#ifndef NS_ASSUME_NONNULL_BEGIN
#define NS_ASSUME_NONNULL_BEGIN
#endif
#ifndef NS_ASSUME_NONNULL_END
#define NS_ASSUME_NONNULL_END
#endif
#if __has_feature(objc_generics)
#define ANS_GENERIC_NSARRAY(type) NSArray<type>
#define ANS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary<key_type, object_key>
#else
#define ANS_GENERIC_NSARRAY(type) NSArray
#define ANS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary
#endif

View File

@ -1,210 +0,0 @@
//
// Answers.h
// Crashlytics
//
// Copyright (c) 2015 Crashlytics, Inc. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "ANSCompatibility.h"
NS_ASSUME_NONNULL_BEGIN
/**
* This class exposes the Answers Events API, allowing you to track key
* user user actions and metrics in your app.
*/
@interface Answers : NSObject
/**
* Log a Sign Up event to see users signing up for your app in real-time, understand how
* many users are signing up with different methods and their success rate signing up.
*
* @param signUpMethodOrNil The method by which a user logged in, e.g. Twitter or Digits.
* @param signUpSucceededOrNil The ultimate success or failure of the login
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logSignUpWithMethod:(nullable NSString *)signUpMethodOrNil
success:(nullable NSNumber *)signUpSucceededOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log an Log In event to see users logging into your app in real-time, understand how many
* users are logging in with different methods and their success rate logging into your app.
*
* @param loginMethodOrNil The method by which a user logged in, e.g. email, Twitter or Digits.
* @param loginSucceededOrNil The ultimate success or failure of the login
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logLoginWithMethod:(nullable NSString *)loginMethodOrNil
success:(nullable NSNumber *)loginSucceededOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Share event to see users sharing from your app in real-time, letting you
* understand what content they're sharing from the type or genre down to the specific id.
*
* @param shareMethodOrNil The method by which a user shared, e.g. email, Twitter, SMS.
* @param contentNameOrNil The human readable name for this piece of content.
* @param contentTypeOrNil The type of content shared.
* @param contentIdOrNil The unique identifier for this piece of content. Useful for finding the top shared item.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logShareWithMethod:(nullable NSString *)shareMethodOrNil
contentName:(nullable NSString *)contentNameOrNil
contentType:(nullable NSString *)contentTypeOrNil
contentId:(nullable NSString *)contentIdOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log an Invite Event to track how users are inviting other users into
* your application.
*
* @param inviteMethodOrNil The method of invitation, e.g. GameCenter, Twitter, email.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logInviteWithMethod:(nullable NSString *)inviteMethodOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Purchase event to see your revenue in real-time, understand how many users are making purchases, see which
* items are most popular, and track plenty of other important purchase-related metrics.
*
* @param itemPriceOrNil The purchased item's price.
* @param currencyOrNil The ISO4217 currency code. Example: USD
* @param purchaseSucceededOrNil Was the purchase successful or unsuccessful
* @param itemNameOrNil The human-readable form of the item's name. Example:
* @param itemTypeOrNil The type, or genre of the item. Example: Song
* @param itemIdOrNil The machine-readable, unique item identifier Example: SKU
* @param customAttributesOrNil A dictionary of custom attributes to associate with this purchase.
*/
+ (void)logPurchaseWithPrice:(nullable NSDecimalNumber *)itemPriceOrNil
currency:(nullable NSString *)currencyOrNil
success:(nullable NSNumber *)purchaseSucceededOrNil
itemName:(nullable NSString *)itemNameOrNil
itemType:(nullable NSString *)itemTypeOrNil
itemId:(nullable NSString *)itemIdOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Level Start Event to track where users are in your game.
*
* @param levelNameOrNil The level name
* @param customAttributesOrNil A dictionary of custom attributes to associate with this level start event.
*/
+ (void)logLevelStart:(nullable NSString *)levelNameOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Level End event to track how users are completing levels in your game.
*
* @param levelNameOrNil The name of the level completed, E.G. "1" or "Training"
* @param scoreOrNil The score the user completed the level with.
* @param levelCompletedSuccesfullyOrNil A boolean representing whether or not the level was completed successfully.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logLevelEnd:(nullable NSString *)levelNameOrNil
score:(nullable NSNumber *)scoreOrNil
success:(nullable NSNumber *)levelCompletedSuccesfullyOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log an Add to Cart event to see users adding items to a shopping cart in real-time, understand how
* many users start the purchase flow, see which items are most popular, and track plenty of other important
* purchase-related metrics.
*
* @param itemPriceOrNil The purchased item's price.
* @param currencyOrNil The ISO4217 currency code. Example: USD
* @param itemNameOrNil The human-readable form of the item's name. Example:
* @param itemTypeOrNil The type, or genre of the item. Example: Song
* @param itemIdOrNil The machine-readable, unique item identifier Example: SKU
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logAddToCartWithPrice:(nullable NSDecimalNumber *)itemPriceOrNil
currency:(nullable NSString *)currencyOrNil
itemName:(nullable NSString *)itemNameOrNil
itemType:(nullable NSString *)itemTypeOrNil
itemId:(nullable NSString *)itemIdOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Start Checkout event to see users moving through the purchase funnel in real-time, understand how many
* users are doing this and how much they're spending per checkout, and see how it related to other important
* purchase-related metrics.
*
* @param totalPriceOrNil The total price of the cart.
* @param currencyOrNil The ISO4217 currency code. Example: USD
* @param itemCountOrNil The number of items in the cart.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logStartCheckoutWithPrice:(nullable NSDecimalNumber *)totalPriceOrNil
currency:(nullable NSString *)currencyOrNil
itemCount:(nullable NSNumber *)itemCountOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Rating event to see users rating content within your app in real-time and understand what
* content is most engaging, from the type or genre down to the specific id.
*
* @param ratingOrNil The integer rating given by the user.
* @param contentNameOrNil The human readable name for this piece of content.
* @param contentTypeOrNil The type of content shared.
* @param contentIdOrNil The unique identifier for this piece of content. Useful for finding the top shared item.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logRating:(nullable NSNumber *)ratingOrNil
contentName:(nullable NSString *)contentNameOrNil
contentType:(nullable NSString *)contentTypeOrNil
contentId:(nullable NSString *)contentIdOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Content View event to see users viewing content within your app in real-time and
* understand what content is most engaging, from the type or genre down to the specific id.
*
* @param contentNameOrNil The human readable name for this piece of content.
* @param contentTypeOrNil The type of content shared.
* @param contentIdOrNil The unique identifier for this piece of content. Useful for finding the top shared item.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logContentViewWithName:(nullable NSString *)contentNameOrNil
contentType:(nullable NSString *)contentTypeOrNil
contentId:(nullable NSString *)contentIdOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Search event allows you to see users searching within your app in real-time and understand
* exactly what they're searching for.
*
* @param queryOrNil The user's query.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logSearchWithQuery:(nullable NSString *)queryOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Custom Event to see user actions that are uniquely important for your app in real-time, to see how often
* they're performing these actions with breakdowns by different categories you add. Use a human-readable name for
* the name of the event, since this is how the event will appear in Answers.
*
* @param eventName The human-readable name for the event.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event. Attribute keys
* must be <code>NSString</code> and values must be <code>NSNumber</code> or <code>NSString</code>.
* @discussion How we treat <code>NSNumbers</code>:
* We will provide information about the distribution of values over time.
*
* How we treat <code>NSStrings</code>:
* NSStrings are used as categorical data, allowing comparison across different category values.
* Strings are limited to a maximum length of 100 characters, attributes over this length will be
* truncated.
*
* When tracking the Tweet views to better understand user engagement, sending the tweet's length
* and the type of media present in the tweet allows you to track how tweet length and the type of media influence
* engagement.
*/
+ (void)logCustomEventWithName:(NSString *)eventName
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,33 +0,0 @@
//
// CLSAttributes.h
// Crashlytics
//
// Copyright (c) 2015 Crashlytics, Inc. All rights reserved.
//
#pragma once
#define CLS_DEPRECATED(x) __attribute__ ((deprecated(x)))
#if !__has_feature(nullability)
#define nonnull
#define nullable
#define _Nullable
#define _Nonnull
#endif
#ifndef NS_ASSUME_NONNULL_BEGIN
#define NS_ASSUME_NONNULL_BEGIN
#endif
#ifndef NS_ASSUME_NONNULL_END
#define NS_ASSUME_NONNULL_END
#endif
#if __has_feature(objc_generics)
#define CLS_GENERIC_NSARRAY(type) NSArray<type>
#define CLS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary<key_type, object_key>
#else
#define CLS_GENERIC_NSARRAY(type) NSArray
#define CLS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary
#endif

View File

@ -1,64 +0,0 @@
//
// CLSLogging.h
// Crashlytics
//
// Copyright (c) 2015 Crashlytics, Inc. All rights reserved.
//
#ifdef __OBJC__
#import "CLSAttributes.h"
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
#endif
/**
*
* The CLS_LOG macro provides as easy way to gather more information in your log messages that are
* sent with your crash data. CLS_LOG prepends your custom log message with the function name and
* line number where the macro was used. If your app was built with the DEBUG preprocessor macro
* defined CLS_LOG uses the CLSNSLog function which forwards your log message to NSLog and CLSLog.
* If the DEBUG preprocessor macro is not defined CLS_LOG uses CLSLog only.
*
* Example output:
* -[AppDelegate login:] line 134 $ login start
*
* If you would like to change this macro, create a new header file, unset our define and then define
* your own version. Make sure this new header file is imported after the Crashlytics header file.
*
* #undef CLS_LOG
* #define CLS_LOG(__FORMAT__, ...) CLSNSLog...
*
**/
#ifdef __OBJC__
#ifdef DEBUG
#define CLS_LOG(__FORMAT__, ...) CLSNSLog((@"%s line %d $ " __FORMAT__), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__)
#else
#define CLS_LOG(__FORMAT__, ...) CLSLog((@"%s line %d $ " __FORMAT__), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__)
#endif
#endif
/**
*
* Add logging that will be sent with your crash data. This logging will not show up in the system.log
* and will only be visible in your Crashlytics dashboard.
*
**/
#ifdef __OBJC__
OBJC_EXTERN void CLSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2);
OBJC_EXTERN void CLSLogv(NSString *format, va_list ap) NS_FORMAT_FUNCTION(1,0);
/**
*
* Add logging that will be sent with your crash data. This logging will show up in the system.log
* and your Crashlytics dashboard. It is not recommended for Release builds.
*
**/
OBJC_EXTERN void CLSNSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2);
OBJC_EXTERN void CLSNSLogv(NSString *format, va_list ap) NS_FORMAT_FUNCTION(1,0);
NS_ASSUME_NONNULL_END
#endif

View File

@ -1,103 +0,0 @@
//
// CLSReport.h
// Crashlytics
//
// Copyright (c) 2015 Crashlytics, Inc. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "CLSAttributes.h"
NS_ASSUME_NONNULL_BEGIN
/**
* The CLSCrashReport protocol is deprecated. See the CLSReport class and the CrashyticsDelegate changes for details.
**/
@protocol CLSCrashReport <NSObject>
@property (nonatomic, copy, readonly) NSString *identifier;
@property (nonatomic, copy, readonly) NSDictionary *customKeys;
@property (nonatomic, copy, readonly) NSString *bundleVersion;
@property (nonatomic, copy, readonly) NSString *bundleShortVersionString;
@property (nonatomic, readonly, nullable) NSDate *crashedOnDate;
@property (nonatomic, copy, readonly) NSString *OSVersion;
@property (nonatomic, copy, readonly) NSString *OSBuildVersion;
@end
/**
* The CLSReport exposes an interface to the phsyical report that Crashlytics has created. You can
* use this class to get information about the event, and can also set some values after the
* event has occurred.
**/
@interface CLSReport : NSObject <CLSCrashReport>
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
/**
* Returns the session identifier for the report.
**/
@property (nonatomic, copy, readonly) NSString *identifier;
/**
* Returns the custom key value data for the report.
**/
@property (nonatomic, copy, readonly) NSDictionary *customKeys;
/**
* Returns the CFBundleVersion of the application that generated the report.
**/
@property (nonatomic, copy, readonly) NSString *bundleVersion;
/**
* Returns the CFBundleShortVersionString of the application that generated the report.
**/
@property (nonatomic, copy, readonly) NSString *bundleShortVersionString;
/**
* Returns the date that the report was created.
**/
@property (nonatomic, copy, readonly) NSDate *dateCreated;
/**
* Returns the os version that the application crashed on.
**/
@property (nonatomic, copy, readonly) NSString *OSVersion;
/**
* Returns the os build version that the application crashed on.
**/
@property (nonatomic, copy, readonly) NSString *OSBuildVersion;
/**
* Returns YES if the report contains any crash information, otherwise returns NO.
**/
@property (nonatomic, assign, readonly) BOOL isCrash;
/**
* You can use this method to set, after the event, additional custom keys. The rules
* and semantics for this method are the same as those documented in Crashlytics.h. Be aware
* that the maximum size and count of custom keys is still enforced, and you can overwrite keys
* and/or cause excess keys to be deleted by using this method.
**/
- (void)setObjectValue:(nullable id)value forKey:(NSString *)key;
/**
* Record an application-specific user identifier. See Crashlytics.h for details.
**/
@property (nonatomic, copy, nullable) NSString * userIdentifier;
/**
* Record a user name. See Crashlytics.h for details.
**/
@property (nonatomic, copy, nullable) NSString * userName;
/**
* Record a user email. See Crashlytics.h for details.
**/
@property (nonatomic, copy, nullable) NSString * userEmail;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,38 +0,0 @@
//
// CLSStackFrame.h
// Crashlytics
//
// Copyright 2015 Crashlytics, Inc. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "CLSAttributes.h"
NS_ASSUME_NONNULL_BEGIN
/**
*
* This class is used in conjunction with -[Crashlytics recordCustomExceptionName:reason:frameArray:] to
* record information about non-ObjC/C++ exceptions. All information included here will be displayed
* in the Crashlytics UI, and can influence crash grouping. Be particularly careful with the use of the
* address property. If set, Crashlytics will attempt symbolication and could overwrite other properities
* in the process.
*
**/
@interface CLSStackFrame : NSObject
+ (instancetype)stackFrame;
+ (instancetype)stackFrameWithAddress:(NSUInteger)address;
+ (instancetype)stackFrameWithSymbol:(NSString *)symbol;
@property (nonatomic, copy, nullable) NSString *symbol;
@property (nonatomic, copy, nullable) NSString *rawSymbol;
@property (nonatomic, copy, nullable) NSString *library;
@property (nonatomic, copy, nullable) NSString *fileName;
@property (nonatomic, assign) uint32_t lineNumber;
@property (nonatomic, assign) uint64_t offset;
@property (nonatomic, assign) uint64_t address;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,288 +0,0 @@
//
// Crashlytics.h
// Crashlytics
//
// Copyright (c) 2015 Crashlytics, Inc. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "CLSAttributes.h"
#import "CLSLogging.h"
#import "CLSReport.h"
#import "CLSStackFrame.h"
#import "Answers.h"
NS_ASSUME_NONNULL_BEGIN
@protocol CrashlyticsDelegate;
/**
* Crashlytics. Handles configuration and initialization of Crashlytics.
*
* Note: The Crashlytics class cannot be subclassed. If this is causing you pain for
* testing, we suggest using either a wrapper class or a protocol extension.
*/
@interface Crashlytics : NSObject
@property (nonatomic, readonly, copy) NSString *APIKey;
@property (nonatomic, readonly, copy) NSString *version;
@property (nonatomic, assign) BOOL debugMode;
/**
*
* The delegate can be used to influence decisions on reporting and behavior, as well as reacting
* to previous crashes.
*
* Make certain that the delegate is setup before starting Crashlytics with startWithAPIKey:... or
* via +[Fabric with:...]. Failure to do will result in missing any delegate callbacks that occur
* synchronously during start.
*
**/
@property (nonatomic, assign, nullable) id <CrashlyticsDelegate> delegate;
/**
* The recommended way to install Crashlytics into your application is to place a call to +startWithAPIKey:
* in your -application:didFinishLaunchingWithOptions: or -applicationDidFinishLaunching:
* method.
*
* Note: Starting with 3.0, the submission process has been significantly improved. The delay parameter
* is no longer required to throttle submissions on launch, performance will be great without it.
*
* @param apiKey The Crashlytics API Key for this app
*
* @return The singleton Crashlytics instance
*/
+ (Crashlytics *)startWithAPIKey:(NSString *)apiKey;
+ (Crashlytics *)startWithAPIKey:(NSString *)apiKey afterDelay:(NSTimeInterval)delay CLS_DEPRECATED("Crashlytics no longer needs or uses the delay parameter. Please use +startWithAPIKey: instead.");
/**
* If you need the functionality provided by the CrashlyticsDelegate protocol, you can use
* these convenience methods to activate the framework and set the delegate in one call.
*
* @param apiKey The Crashlytics API Key for this app
* @param delegate A delegate object which conforms to CrashlyticsDelegate.
*
* @return The singleton Crashlytics instance
*/
+ (Crashlytics *)startWithAPIKey:(NSString *)apiKey delegate:(nullable id<CrashlyticsDelegate>)delegate;
+ (Crashlytics *)startWithAPIKey:(NSString *)apiKey delegate:(nullable id<CrashlyticsDelegate>)delegate afterDelay:(NSTimeInterval)delay CLS_DEPRECATED("Crashlytics no longer needs or uses the delay parameter. Please use +startWithAPIKey:delegate: instead.");
/**
* Access the singleton Crashlytics instance.
*
* @return The singleton Crashlytics instance
*/
+ (Crashlytics *)sharedInstance;
/**
* The easiest way to cause a crash - great for testing!
*/
- (void)crash;
/**
* The easiest way to cause a crash with an exception - great for testing.
*/
- (void)throwException;
/**
* Specify a user identifier which will be visible in the Crashlytics UI.
*
* Many of our customers have requested the ability to tie crashes to specific end-users of their
* application in order to facilitate responses to support requests or permit the ability to reach
* out for more information. We allow you to specify up to three separate values for display within
* the Crashlytics UI - but please be mindful of your end-user's privacy.
*
* We recommend specifying a user identifier - an arbitrary string that ties an end-user to a record
* in your system. This could be a database id, hash, or other value that is meaningless to a
* third-party observer but can be indexed and queried by you.
*
* Optionally, you may also specify the end-user's name or username, as well as email address if you
* do not have a system that works well with obscured identifiers.
*
* Pursuant to our EULA, this data is transferred securely throughout our system and we will not
* disseminate end-user data unless required to by law. That said, if you choose to provide end-user
* contact information, we strongly recommend that you disclose this in your application's privacy
* policy. Data privacy is of our utmost concern.
*
* @param identifier An arbitrary user identifier string which ties an end-user to a record in your system.
*/
- (void)setUserIdentifier:(nullable NSString *)identifier;
/**
* Specify a user name which will be visible in the Crashlytics UI.
* Please be mindful of your end-user's privacy and see if setUserIdentifier: can fulfil your needs.
* @see setUserIdentifier:
*
* @param name An end user's name.
*/
- (void)setUserName:(nullable NSString *)name;
/**
* Specify a user email which will be visible in the Crashlytics UI.
* Please be mindful of your end-user's privacy and see if setUserIdentifier: can fulfil your needs.
*
* @see setUserIdentifier:
*
* @param email An end user's email address.
*/
- (void)setUserEmail:(nullable NSString *)email;
+ (void)setUserIdentifier:(nullable NSString *)identifier CLS_DEPRECATED("Please access this method via +sharedInstance");
+ (void)setUserName:(nullable NSString *)name CLS_DEPRECATED("Please access this method via +sharedInstance");
+ (void)setUserEmail:(nullable NSString *)email CLS_DEPRECATED("Please access this method via +sharedInstance");
/**
* Set a value for a for a key to be associated with your crash data which will be visible in the Crashlytics UI.
* When setting an object value, the object is converted to a string. This is typically done by calling
* -[NSObject description].
*
* @param value The object to be associated with the key
* @param key The key with which to associate the value
*/
- (void)setObjectValue:(nullable id)value forKey:(NSString *)key;
/**
* Set an int value for a key to be associated with your crash data which will be visible in the Crashlytics UI.
*
* @param value The integer value to be set
* @param key The key with which to associate the value
*/
- (void)setIntValue:(int)value forKey:(NSString *)key;
/**
* Set an BOOL value for a key to be associated with your crash data which will be visible in the Crashlytics UI.
*
* @param value The BOOL value to be set
* @param key The key with which to associate the value
*/
- (void)setBoolValue:(BOOL)value forKey:(NSString *)key;
/**
* Set an float value for a key to be associated with your crash data which will be visible in the Crashlytics UI.
*
* @param value The float value to be set
* @param key The key with which to associate the value
*/
- (void)setFloatValue:(float)value forKey:(NSString *)key;
+ (void)setObjectValue:(nullable id)value forKey:(NSString *)key CLS_DEPRECATED("Please access this method via +sharedInstance");
+ (void)setIntValue:(int)value forKey:(NSString *)key CLS_DEPRECATED("Please access this method via +sharedInstance");
+ (void)setBoolValue:(BOOL)value forKey:(NSString *)key CLS_DEPRECATED("Please access this method via +sharedInstance");
+ (void)setFloatValue:(float)value forKey:(NSString *)key CLS_DEPRECATED("Please access this method via +sharedInstance");
/**
* This method can be used to record a single exception structure in a report. This is particularly useful
* when your code interacts with non-native languages like Lua, C#, or Javascript. This call can be
* expensive and should only be used shortly before process termination. This API is not intended be to used
* to log NSException objects. All safely-reportable NSExceptions are automatically captured by
* Crashlytics.
*
* @param name The name of the custom exception
* @param reason The reason this exception occurred
* @param frameArray An array of CLSStackFrame objects
*/
- (void)recordCustomExceptionName:(NSString *)name reason:(nullable NSString *)reason frameArray:(CLS_GENERIC_NSARRAY(CLSStackFrame *) *)frameArray;
/**
*
* This allows you to record a non-fatal event, described by an NSError object. These events will be grouped and
* displayed similarly to crashes. Keep in mind that this method can be expensive. Also, the total number of
* NSErrors that can be recorded during your app's life-cycle is limited by a fixed-size circular buffer. If the
* buffer is overrun, the oldest data is dropped. Errors are relayed to Crashlytics on a subsequent launch
* of your application.
*
* You can also use the -recordError:withAdditionalUserInfo: to include additional context not represented
* by the NSError instance itself.
*
**/
- (void)recordError:(NSError *)error;
- (void)recordError:(NSError *)error withAdditionalUserInfo:(nullable CLS_GENERIC_NSDICTIONARY(NSString *, id) *)userInfo;
- (void)logEvent:(NSString *)eventName CLS_DEPRECATED("Please refer to Answers +logCustomEventWithName:");
- (void)logEvent:(NSString *)eventName attributes:(nullable NSDictionary *) attributes CLS_DEPRECATED("Please refer to Answers +logCustomEventWithName:");
+ (void)logEvent:(NSString *)eventName CLS_DEPRECATED("Please refer to Answers +logCustomEventWithName:");
+ (void)logEvent:(NSString *)eventName attributes:(nullable NSDictionary *) attributes CLS_DEPRECATED("Please refer to Answers +logCustomEventWithName:");
@end
/**
*
* The CrashlyticsDelegate protocol provides a mechanism for your application to take
* action on events that occur in the Crashlytics crash reporting system. You can make
* use of these calls by assigning an object to the Crashlytics' delegate property directly,
* or through the convenience +startWithAPIKey:delegate: method.
*
*/
@protocol CrashlyticsDelegate <NSObject>
@optional
- (void)crashlyticsDidDetectCrashDuringPreviousExecution:(Crashlytics *)crashlytics CLS_DEPRECATED("Please refer to -crashlyticsDidDetectReportForLastExecution:");
- (void)crashlytics:(Crashlytics *)crashlytics didDetectCrashDuringPreviousExecution:(id <CLSCrashReport>)crash CLS_DEPRECATED("Please refer to -crashlyticsDidDetectReportForLastExecution:");
/**
*
* Called when a Crashlytics instance has determined that the last execution of the
* application resulted in a saved report. This is called synchronously on Crashlytics
* initialization. Your delegate must invoke the completionHandler, but does not need to do so
* synchronously, or even on the main thread. Invoking completionHandler with NO will cause the
* detected report to be deleted and not submitted to Crashlytics. This is useful for
* implementing permission prompts, or other more-complex forms of logic around submitting crashes.
*
* Instead of using this method, you should try to make use of -crashlyticsDidDetectReportForLastExecution:
* if you can.
*
* @warning Failure to invoke the completionHandler will prevent submissions from being reported. Watch out.
*
* @warning Just implementing this delegate method will disable all forms of synchronous report submission. This can
* impact the reliability of reporting crashes very early in application launch.
*
* @param report The CLSReport object representing the last detected report
* @param completionHandler The completion handler to call when your logic has completed.
*
*/
- (void)crashlyticsDidDetectReportForLastExecution:(CLSReport *)report completionHandler:(void (^)(BOOL submit))completionHandler;
/**
*
* Called when a Crashlytics instance has determined that the last execution of the
* application resulted in a saved report. This method differs from
* -crashlyticsDidDetectReportForLastExecution:completionHandler: in three important ways:
*
* - it is not called synchronously during initialization
* - it does not give you the ability to prevent the report from being submitted
* - the report object itself is immutable
*
* Thanks to these limitations, making use of this method does not impact reporting
* reliabilty in any way.
*
* @param report The read-only CLSReport object representing the last detected report
*
*/
- (void)crashlyticsDidDetectReportForLastExecution:(CLSReport *)report;
/**
* If your app is running on an OS that supports it (OS X 10.9+, iOS 7.0+), Crashlytics will submit
* most reports using out-of-process background networking operations. This results in a significant
* improvement in reliability of reporting, as well as power and performance wins for your users.
* If you don't want this functionality, you can disable by returning NO from this method.
*
* @warning Background submission is not supported for extensions on iOS or OS X.
*
* @param crashlytics The Crashlytics singleton instance
*
* @return Return NO if you don't want out-of-process background network operations.
*
*/
- (BOOL)crashlyticsCanUseBackgroundSessions:(Crashlytics *)crashlytics;
@end
/**
* `CrashlyticsKit` can be used as a parameter to `[Fabric with:@[CrashlyticsKit]];` in Objective-C. In Swift, use Crashlytics.sharedInstance()
*/
#define CrashlyticsKit [Crashlytics sharedInstance]
NS_ASSUME_NONNULL_END

Binary file not shown.

View File

@ -1,14 +0,0 @@
framework module Crashlytics {
header "Crashlytics.h"
header "Answers.h"
header "ANSCompatibility.h"
header "CLSLogging.h"
header "CLSReport.h"
header "CLSStackFrame.h"
header "CLSAttributes.h"
export *
link "z"
link "c++"
}

View File

@ -1,73 +0,0 @@
#!/bin/sh
# run
#
# Copyright (c) 2015 Crashlytics. All rights reserved.
#
#
# This script is meant to be run as a Run Script in the "Build Phases" section
# of your Xcode project. It sends debug symbols to symbolicate stacktraces,
# sends build events to track versions, and onboard apps for Crashlytics.
#
# This script calls upload-symbols twice:
#
# 1) First it calls upload-symbols synchronously in "validation" mode. If the
# script finds issues with the build environment, it will report errors to Xcode.
# In validation mode it exits before doing any time consuming work.
#
# 2) Then it calls upload-symbols in the background to actually send the build
# event and upload symbols. It does this in the background so that it doesn't
# slow down your builds. If an error happens here, you won't see it in Xcode.
#
# You can find the output for the background execution in Console.app, by
# searching for "upload-symbols".
#
# If you want verbose output, you can pass the --debug flag to this script
#
# Figure out where we're being called from
DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
# If the first argument is specified without a dash, treat it as the Fabric API
# Key and add it as an argument
if [ -z "$1" ] || [[ $1 == -* ]]; then
API_KEY_ARG=""
else
API_KEY_ARG="-a $1"; shift
fi
# If a second argument is specified without a dash, treat it as the Build Secret
# and add it as an argument
if [ -z "$1" ] || [[ $1 == -* ]]; then
BUILD_SECRET_ARG=""
else
BUILD_SECRET_ARG="-bs $1"; shift
fi
# Build up the arguments list, passing through any flags added after the
# API Key and Build Secret
ARGUMENTS="$API_KEY_ARG $BUILD_SECRET_ARG $@"
VALIDATE_ARGUMENTS="$ARGUMENTS --build-phase --validate"
UPLOAD_ARGUMENTS="$ARGUMENTS --build-phase"
# Quote the path to handle folders with special characters
COMMAND_PATH="\"$DIR/upload-symbols\" "
# Ensure params are as expected, run in sync mode to validate,
# and cause a build error if validation fails
eval $COMMAND_PATH$VALIDATE_ARGUMENTS
return_code=$?
if [[ $return_code != 0 ]]; then
exit $return_code
fi
# Verification passed, convert and upload cSYMs in the background to prevent
# build delays
#
# Note: Validation is performed again at this step before upload
#
# Note: Output can still be found in Console.app, by searching for
# "upload-symbols"
#
eval $COMMAND_PATH$UPLOAD_ARGUMENTS > /dev/null 2>&1 &

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1 +0,0 @@
We've now combined all our supported platforms into a single podspec. As a result, we moved our run script to a new location for Cocoapods projects: ${PODS_ROOT}/Fabric/run. To avoid breaking builds that reference the old location of the run script, we've placed this dummy script that calls to the correct location, while providing a helpful warning in Xcode if it is invoked. This bridge for backwards compatibility will be removed in a future release, so please heed the warning!

View File

@ -1,6 +0,0 @@
if [[ -z $PODS_ROOT ]]; then
echo "error: The run binary delivered by cocoapods is in a new location, under '$"{"PODS_ROOT"}"/Fabric/run'. This script was put in place for backwards compatibility, but it relies on PODS_ROOT, which does not have a value in your current setup. Please update the path to the run binary to fix this issue."
else
echo "warning: The run script is now located at '$"{"PODS_ROOT"}"/Fabric/run'. To remove this warning, update your Run Script Build Phase to point to this new location."
sh "${PODS_ROOT}/Fabric/run" "$@"
fi

View File

@ -1,27 +0,0 @@
# Fabric
## Overview
[Fabric](https://get.fabric.io) provides developers with the tools they need to build the best apps. Developed and maintained by Google and the team that built Crashlytics.
For a full list of SDKs provided through Fabric visit [https://fabric.io/kits](https://fabric.io/kits).
To follow the migration to Firebase, check out the [Fabric Roadmap](https://get.fabric.io/roadmap).
## Setup
Fabric is a dependency for the Crashlytics SDK. To start using Crashlytics, there are two options:
1) The recommended way is to go to the [Firebase Crashlytics Docs](https://firebase.google.com/docs/crashlytics/get-started?platform=ios) and follow the directions there.
2) If you aren't using Firebase yet, go to [Fabric Kits](https://fabric.io/kits), and follow the directions for Crashlytics.
## Resources
* [Documentation](https://docs.fabric.io/)
* [Forums](https://stackoverflow.com/questions/tagged/google-fabric)
* [Website](https://get.fabric.io)
* Follow us on Twitter: [@fabric](https://twitter.com/fabric)

Binary file not shown.

View File

@ -1,51 +0,0 @@
//
// FABAttributes.h
// Fabric
//
// Copyright (C) 2015 Twitter, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#pragma once
#define FAB_UNAVAILABLE(x) __attribute__((unavailable(x)))
#if !__has_feature(nullability)
#define nonnull
#define nullable
#define _Nullable
#define _Nonnull
#endif
#ifndef NS_ASSUME_NONNULL_BEGIN
#define NS_ASSUME_NONNULL_BEGIN
#endif
#ifndef NS_ASSUME_NONNULL_END
#define NS_ASSUME_NONNULL_END
#endif
/**
* The following macros are defined here to provide
* backwards compatability. If you are still using
* them you should migrate to the native nullability
* macros.
*/
#define fab_nullable nullable
#define fab_nonnull nonnull
#define FAB_NONNULL __fab_nonnull
#define FAB_NULLABLE __fab_nullable
#define FAB_START_NONNULL NS_ASSUME_NONNULL_BEGIN
#define FAB_END_NONNULL NS_ASSUME_NONNULL_END

View File

@ -1,82 +0,0 @@
//
// Fabric.h
// Fabric
//
// Copyright (C) 2015 Twitter, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#import <Foundation/Foundation.h>
#import "FABAttributes.h"
NS_ASSUME_NONNULL_BEGIN
#if TARGET_OS_IPHONE
#if __IPHONE_OS_VERSION_MIN_REQUIRED < 60000
#error "Fabric's minimum iOS version is 6.0"
#endif
#else
#if __MAC_OS_X_VERSION_MIN_REQUIRED < 1070
#error "Fabric's minimum OS X version is 10.7"
#endif
#endif
/**
* Fabric Base. Coordinates configuration and starts all provided kits.
*/
@interface Fabric : NSObject
/**
* Initialize Fabric and all provided kits. Call this method within your App Delegate's `application:didFinishLaunchingWithOptions:` and provide the kits you wish to use.
*
* For example, in Objective-C:
*
* `[Fabric with:@[[Crashlytics class], [Twitter class], [Digits class], [MoPub class]]];`
*
* Swift:
*
* `Fabric.with([Crashlytics.self(), Twitter.self(), Digits.self(), MoPub.self()])`
*
* Only the first call to this method is honored. Subsequent calls are no-ops.
*
* @param kitClasses An array of kit Class objects
*
* @return Returns the shared Fabric instance. In most cases this can be ignored.
*/
+ (instancetype)with:(NSArray *)kitClasses;
/**
* Returns the Fabric singleton object.
*/
+ (instancetype)sharedSDK;
/**
* This BOOL enables or disables debug logging, such as kit version information. The default value is NO.
*/
@property (nonatomic, assign) BOOL debug;
/**
* Unavailable. Use `+sharedSDK` to retrieve the shared Fabric instance.
*/
- (id)init FAB_UNAVAILABLE("Use +sharedSDK to retrieve the shared Fabric instance.");
/**
* Unavailable. Use `+sharedSDK` to retrieve the shared Fabric instance.
*/
+ (instancetype)new FAB_UNAVAILABLE("Use +sharedSDK to retrieve the shared Fabric instance.");
@end
NS_ASSUME_NONNULL_END

Binary file not shown.

View File

@ -1,6 +0,0 @@
framework module Fabric {
umbrella header "Fabric.h"
export *
module * { export * }
}

View File

@ -1,73 +0,0 @@
#!/bin/sh
# run
#
# Copyright (c) 2015 Crashlytics. All rights reserved.
#
#
# This script is meant to be run as a Run Script in the "Build Phases" section
# of your Xcode project. It sends debug symbols to symbolicate stacktraces,
# sends build events to track versions, and onboard apps for Crashlytics.
#
# This script calls upload-symbols twice:
#
# 1) First it calls upload-symbols synchronously in "validation" mode. If the
# script finds issues with the build environment, it will report errors to Xcode.
# In validation mode it exits before doing any time consuming work.
#
# 2) Then it calls upload-symbols in the background to actually send the build
# event and upload symbols. It does this in the background so that it doesn't
# slow down your builds. If an error happens here, you won't see it in Xcode.
#
# You can find the output for the background execution in Console.app, by
# searching for "upload-symbols".
#
# If you want verbose output, you can pass the --debug flag to this script
#
# Figure out where we're being called from
DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
# If the first argument is specified without a dash, treat it as the Fabric API
# Key and add it as an argument
if [ -z "$1" ] || [[ $1 == -* ]]; then
API_KEY_ARG=""
else
API_KEY_ARG="-a $1"; shift
fi
# If a second argument is specified without a dash, treat it as the Build Secret
# and add it as an argument
if [ -z "$1" ] || [[ $1 == -* ]]; then
BUILD_SECRET_ARG=""
else
BUILD_SECRET_ARG="-bs $1"; shift
fi
# Build up the arguments list, passing through any flags added after the
# API Key and Build Secret
ARGUMENTS="$API_KEY_ARG $BUILD_SECRET_ARG $@"
VALIDATE_ARGUMENTS="$ARGUMENTS --build-phase --validate"
UPLOAD_ARGUMENTS="$ARGUMENTS --build-phase"
# Quote the path to handle folders with special characters
COMMAND_PATH="\"$DIR/upload-symbols\" "
# Ensure params are as expected, run in sync mode to validate,
# and cause a build error if validation fails
eval $COMMAND_PATH$VALIDATE_ARGUMENTS
return_code=$?
if [[ $return_code != 0 ]]; then
exit $return_code
fi
# Verification passed, convert and upload cSYMs in the background to prevent
# build delays
#
# Note: Validation is performed again at this step before upload
#
# Note: Output can still be found in Console.app, by searching for
# "upload-symbols"
#
eval $COMMAND_PATH$UPLOAD_ARGUMENTS > /dev/null 2>&1 &

Binary file not shown.

73
ios/Pods/Fabric/run generated
View File

@ -1,73 +0,0 @@
#!/bin/sh
# run
#
# Copyright (c) 2015 Crashlytics. All rights reserved.
#
#
# This script is meant to be run as a Run Script in the "Build Phases" section
# of your Xcode project. It sends debug symbols to symbolicate stacktraces,
# sends build events to track versions, and onboard apps for Crashlytics.
#
# This script calls upload-symbols twice:
#
# 1) First it calls upload-symbols synchronously in "validation" mode. If the
# script finds issues with the build environment, it will report errors to Xcode.
# In validation mode it exits before doing any time consuming work.
#
# 2) Then it calls upload-symbols in the background to actually send the build
# event and upload symbols. It does this in the background so that it doesn't
# slow down your builds. If an error happens here, you won't see it in Xcode.
#
# You can find the output for the background execution in Console.app, by
# searching for "upload-symbols".
#
# If you want verbose output, you can pass the --debug flag to this script
#
# Figure out where we're being called from
DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
# If the first argument is specified without a dash, treat it as the Fabric API
# Key and add it as an argument
if [ -z "$1" ] || [[ $1 == -* ]]; then
API_KEY_ARG=""
else
API_KEY_ARG="-a $1"; shift
fi
# If a second argument is specified without a dash, treat it as the Build Secret
# and add it as an argument
if [ -z "$1" ] || [[ $1 == -* ]]; then
BUILD_SECRET_ARG=""
else
BUILD_SECRET_ARG="-bs $1"; shift
fi
# Build up the arguments list, passing through any flags added after the
# API Key and Build Secret
ARGUMENTS="$API_KEY_ARG $BUILD_SECRET_ARG $@"
VALIDATE_ARGUMENTS="$ARGUMENTS --build-phase --validate"
UPLOAD_ARGUMENTS="$ARGUMENTS --build-phase"
# Quote the path to handle folders with special characters
COMMAND_PATH="\"$DIR/upload-symbols\" "
# Ensure params are as expected, run in sync mode to validate,
# and cause a build error if validation fails
eval $COMMAND_PATH$VALIDATE_ARGUMENTS
return_code=$?
if [[ $return_code != 0 ]]; then
exit $return_code
fi
# Verification passed, convert and upload cSYMs in the background to prevent
# build delays
#
# Note: Validation is performed again at this step before upload
#
# Note: Output can still be found in Console.app, by searching for
# "upload-symbols"
#
eval $COMMAND_PATH$UPLOAD_ARGUMENTS > /dev/null 2>&1 &

Binary file not shown.

View File

@ -2,14 +2,20 @@
[![License](https://img.shields.io/cocoapods/l/Firebase.svg?style=flat)](https://cocoapods.org/pods/Firebase) [![License](https://img.shields.io/cocoapods/l/Firebase.svg?style=flat)](https://cocoapods.org/pods/Firebase)
[![Platform](https://img.shields.io/cocoapods/p/Firebase.svg?style=flat)](https://cocoapods.org/pods/Firebase) [![Platform](https://img.shields.io/cocoapods/p/Firebase.svg?style=flat)](https://cocoapods.org/pods/Firebase)
[![Actions Status][gh-abtesting-badge]][gh-actions]
[![Actions Status][gh-auth-badge]][gh-actions] [![Actions Status][gh-auth-badge]][gh-actions]
[![Actions Status][gh-core-badge]][gh-actions] [![Actions Status][gh-core-badge]][gh-actions]
[![Actions Status][gh-crashlytics-badge]][gh-actions]
[![Actions Status][gh-database-badge]][gh-actions]
[![Actions Status][gh-datatransport-badge]][gh-actions] [![Actions Status][gh-datatransport-badge]][gh-actions]
[![Actions Status][gh-dynamiclinks-badge]][gh-actions] [![Actions Status][gh-dynamiclinks-badge]][gh-actions]
[![Actions Status][gh-firebasepod-badge]][gh-actions] [![Actions Status][gh-firebasepod-badge]][gh-actions]
[![Actions Status][gh-firestore-badge]][gh-actions] [![Actions Status][gh-firestore-badge]][gh-actions]
[![Actions Status][gh-functions-badge]][gh-actions]
[![Actions Status][gh-inappmessaging-badge]][gh-actions]
[![Actions Status][gh-interop-badge]][gh-actions] [![Actions Status][gh-interop-badge]][gh-actions]
[![Actions Status][gh-messaging-badge]][gh-actions] [![Actions Status][gh-messaging-badge]][gh-actions]
[![Actions Status][gh-remoteconfig-badge]][gh-actions]
[![Actions Status][gh-storage-badge]][gh-actions] [![Actions Status][gh-storage-badge]][gh-actions]
[![Actions Status][gh-symbolcollision-badge]][gh-actions] [![Actions Status][gh-symbolcollision-badge]][gh-actions]
[![Actions Status][gh-zip-badge]][gh-actions] [![Actions Status][gh-zip-badge]][gh-actions]
@ -92,7 +98,7 @@ Instructions for installing binary frameworks via
To develop Firebase software in this repository, ensure that you have at least To develop Firebase software in this repository, ensure that you have at least
the following software: the following software:
* Xcode 10.1 (or later) * Xcode 10.3 (or later)
* CocoaPods 1.7.2 (or later) * CocoaPods 1.7.2 (or later)
* [CocoaPods generate](https://github.com/square/cocoapods-generate) * [CocoaPods generate](https://github.com/square/cocoapods-generate)
@ -123,6 +129,10 @@ Firestore has a self contained Xcode project. See
See [AddNewPod.md](AddNewPod.md). See [AddNewPod.md](AddNewPod.md).
### Managing Headers and Imports
See [HeadersImports.md](HeadersImports.md).
### Code Formatting ### Code Formatting
To ensure that the code is formatted consistently, run the script To ensure that the code is formatted consistently, run the script
@ -185,8 +195,16 @@ building and running the FirebaseAuth pod along with various samples and tests.
### Firebase Database ### Firebase Database
To run the Database Integration tests, make your database authentication rules The Firebase Database Integration tests can be run against a locally running Database Emulator
[public](https://firebase.google.com/docs/database/security/quickstart). or against a production instance.
To run against a local emulator instance, invoke `./scripts/run_database_emulator.sh start` before
running the integration test.
To run against a production instance, provide a valid GoogleServices-Info.plist and copy it to
`Example/Database/App/GoogleService-Info.plist`. Your Security Rule must be set to
[public](https://firebase.google.com/docs/database/security/quickstart) while your tests are
running.
### Firebase Storage ### Firebase Storage
@ -274,14 +292,20 @@ Your use of Firebase is governed by the
[Terms of Service for Firebase Services](https://firebase.google.com/terms/). [Terms of Service for Firebase Services](https://firebase.google.com/terms/).
[gh-actions]: https://github.com/firebase/firebase-ios-sdk/actions [gh-actions]: https://github.com/firebase/firebase-ios-sdk/actions
[gh-abtesting-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/abtesting/badge.svg
[gh-auth-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/auth/badge.svg [gh-auth-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/auth/badge.svg
[gh-core-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/core/badge.svg [gh-core-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/core/badge.svg
[gh-crashlytics-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/crashlytics/badge.svg
[gh-database-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/database/badge.svg
[gh-datatransport-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/datatransport/badge.svg [gh-datatransport-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/datatransport/badge.svg
[gh-dynamiclinks-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/dynamiclinks/badge.svg [gh-dynamiclinks-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/dynamiclinks/badge.svg
[gh-firebasepod-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/firebasepod/badge.svg [gh-firebasepod-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/firebasepod/badge.svg
[gh-firestore-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/firestore/badge.svg [gh-firestore-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/firestore/badge.svg
[gh-functions-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/functions/badge.svg
[gh-inappmessaging-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/inappmessaging/badge.svg
[gh-interop-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/interop/badge.svg [gh-interop-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/interop/badge.svg
[gh-messaging-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/messaging/badge.svg [gh-messaging-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/messaging/badge.svg
[gh-remoteconfig-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/remoteconfig/badge.svg
[gh-storage-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/storage/badge.svg [gh-storage-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/storage/badge.svg
[gh-symbolcollision-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/symbolcollision/badge.svg [gh-symbolcollision-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/symbolcollision/badge.svg
[gh-zip-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/zip/badge.svg [gh-zip-badge]: https://github.com/firebase/firebase-ios-sdk/workflows/zip/badge.svg

Some files were not shown because too many files have changed in this diff Show More