Merge branch 'develop' into new.add-discusions-roomactionsview

This commit is contained in:
Gerzon Z 2022-01-17 10:12:19 -04:00
commit e010d804d5
280 changed files with 4281 additions and 2405 deletions

View File

@ -17,14 +17,15 @@ module.exports = {
legacyDecorators: true legacyDecorators: true
} }
}, },
plugins: ['react', 'jsx-a11y', 'import', 'react-native', '@babel'], plugins: ['react', 'jsx-a11y', 'import', 'react-native', '@babel', 'jest'],
env: { env: {
browser: true, browser: true,
commonjs: true, commonjs: true,
es6: true, es6: true,
node: true, node: true,
jquery: true, jquery: true,
mocha: true mocha: true,
'jest/globals': true
}, },
rules: { rules: {
'import/extensions': [ 'import/extensions': [

View File

@ -1419,6 +1419,7 @@ Array [
</Text> </Text>
</View> </View>
<View <View
accessibilityLabel="Use"
accessible={true} accessible={true}
focusable={true} focusable={true}
onClick={[Function]} onClick={[Function]}
@ -1581,6 +1582,7 @@ Array [
</Text> </Text>
</View> </View>
<View <View
accessibilityLabel="Use"
accessible={true} accessible={true}
focusable={true} focusable={true}
onClick={[Function]} onClick={[Function]}
@ -41244,6 +41246,7 @@ exports[`Storyshots Message Show a button as attachment 1`] = `
Test Button Test Button
</Text> </Text>
<View <View
accessibilityLabel="Text button"
accessible={true} accessible={true}
focusable={true} focusable={true}
onClick={[Function]} onClick={[Function]}

View File

@ -144,13 +144,15 @@ android {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode VERSIONCODE as Integer versionCode VERSIONCODE as Integer
versionName "4.22.0" versionName "4.24.0"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
if (!isFoss) { if (!isFoss) {
manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String]
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60" // See note below! missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60" // See note below!
} }
resValue "string", "rn_config_reader_custom_package", "chat.rocket.reactnative" resValue "string", "rn_config_reader_custom_package", "chat.rocket.reactnative"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
} }
signingConfigs { signingConfigs {
@ -203,6 +205,10 @@ android {
dimension = "app" dimension = "app"
buildConfigField "boolean", "IS_OFFICIAL", "false" buildConfigField "boolean", "IS_OFFICIAL", "false"
} }
e2e {
dimension = "app"
buildConfigField "boolean", "IS_OFFICIAL", "false"
}
foss { foss {
dimension = "type" dimension = "type"
buildConfigField "boolean", "FDROID_BUILD", "true" buildConfigField "boolean", "FDROID_BUILD", "true"
@ -230,6 +236,16 @@ android {
java.srcDirs = ['src/main/java', 'src/play/java'] java.srcDirs = ['src/main/java', 'src/play/java']
manifest.srcFile 'src/play/AndroidManifest.xml' manifest.srcFile 'src/play/AndroidManifest.xml'
} }
e2ePlayDebug {
java.srcDirs = ['src/main/java', 'src/play/java']
res.srcDirs = ['src/experimental/res']
manifest.srcFile 'src/play/AndroidManifest.xml'
}
e2ePlayRelease {
java.srcDirs = ['src/main/java', 'src/play/java']
res.srcDirs = ['src/experimental/res']
manifest.srcFile 'src/play/AndroidManifest.xml'
}
} }
applicationVariants.all { variant -> applicationVariants.all { variant ->
@ -294,6 +310,8 @@ dependencies {
implementation "com.tencent:mmkv-static:1.2.1" implementation "com.tencent:mmkv-static:1.2.1"
implementation 'com.squareup.okhttp3:okhttp:4.9.0' implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.9.0" implementation "com.squareup.okhttp3:okhttp-urlconnection:4.9.0"
androidTestImplementation('com.wix:detox:+') { transitive = true }
androidTestImplementation 'junit:junit:4.12'
} }
// Run this once to be able to run the application with BUCK // Run this once to be able to run the application with BUCK

View File

@ -0,0 +1,32 @@
// Replace "com.example" here and below with your app's package name from the top of MainActivity.java
package chat.rocket.reactnative;
import com.wix.detox.Detox;
import com.wix.detox.config.DetoxConfig;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class DetoxTest {
@Rule
// Replace 'MainActivity' with the value of android:name entry in
// <activity> in AndroidManifest.xml
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
@Test
public void runDetoxTests() {
DetoxConfig detoxConfig = new DetoxConfig();
detoxConfig.idlePolicyConfig.masterTimeoutSec = 90;
detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60;
detoxConfig.rnContextLoadTimeoutSec = (chat.rocket.reactnative.BuildConfig.DEBUG ? 180 : 60);
Detox.runTests(mActivityRule, detoxConfig);
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:tools="http://schemas.android.com/tools">
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user"
tools:ignore="AcceptsUserCertificates" />
</trust-anchors>
</base-config>
</network-security-config>

View File

@ -11,9 +11,12 @@ import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.Promise;
import java.net.Socket; import java.net.Socket;
import java.security.KeyStore;
import java.security.Principal; import java.security.Principal;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedKeyManager; import javax.net.ssl.X509ExtendedKeyManager;
import java.security.PrivateKey; import java.security.PrivateKey;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
@ -21,11 +24,12 @@ import javax.net.ssl.X509TrustManager;
import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManager;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import java.lang.InterruptedException;
import android.app.Activity; import android.app.Activity;
import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManager;
import android.security.KeyChain; import android.security.KeyChain;
import android.security.KeyChainAliasCallback; import android.security.KeyChainAliasCallback;
import java.util.Arrays;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import com.RNFetchBlob.RNFetchBlob; import com.RNFetchBlob.RNFetchBlob;
@ -52,8 +56,9 @@ public class SSLPinningModule extends ReactContextBaseJavaModule implements KeyC
public void apply(OkHttpClient.Builder builder) { public void apply(OkHttpClient.Builder builder) {
if (alias != null) { if (alias != null) {
SSLSocketFactory sslSocketFactory = getSSLFactory(alias); SSLSocketFactory sslSocketFactory = getSSLFactory(alias);
X509TrustManager trustManager = getTrustManagerFactory();
if (sslSocketFactory != null) { if (sslSocketFactory != null) {
builder.sslSocketFactory(sslSocketFactory); builder.sslSocketFactory(sslSocketFactory, trustManager);
} }
} }
} }
@ -68,8 +73,9 @@ public class SSLPinningModule extends ReactContextBaseJavaModule implements KeyC
if (alias != null) { if (alias != null) {
SSLSocketFactory sslSocketFactory = getSSLFactory(alias); SSLSocketFactory sslSocketFactory = getSSLFactory(alias);
X509TrustManager trustManager = getTrustManagerFactory();
if (sslSocketFactory != null) { if (sslSocketFactory != null) {
builder.sslSocketFactory(sslSocketFactory); builder.sslSocketFactory(sslSocketFactory, trustManager);
} }
} }
@ -162,25 +168,9 @@ public class SSLPinningModule extends ReactContextBaseJavaModule implements KeyC
} }
}; };
final TrustManager[] trustAllCerts = new TrustManager[] { final X509TrustManager trustManager = getTrustManagerFactory();
new X509TrustManager() {
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return certChain;
}
}
};
final SSLContext sslContext = SSLContext.getInstance("TLS"); final SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(new KeyManager[]{keyManager}, trustAllCerts, new java.security.SecureRandom()); sslContext.init(new KeyManager[]{keyManager}, new TrustManager[]{trustManager}, new java.security.SecureRandom());
SSLContext.setDefault(sslContext); SSLContext.setDefault(sslContext);
final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
@ -190,4 +180,19 @@ public class SSLPinningModule extends ReactContextBaseJavaModule implements KeyC
return null; return null;
} }
} }
public static X509TrustManager getTrustManagerFactory() {
try {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
}
final X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
return trustManager;
} catch (Exception e) {
return null;
}
}
} }

View File

@ -53,6 +53,10 @@ allprojects {
url("$rootDir/../node_modules/jsc-android/dist") url("$rootDir/../node_modules/jsc-android/dist")
} }
maven {
url "$rootDir/../node_modules/detox/Detox-android"
}
maven { maven {
url jitsi_url url jitsi_url
} }

View File

@ -3,6 +3,7 @@ import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack'; import { createStackNavigator } from '@react-navigation/stack';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SetUsernameStackParamList, StackParamList } from './navigationTypes';
import Navigation from './lib/Navigation'; import Navigation from './lib/Navigation';
import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation'; import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation';
import { ROOT_INSIDE, ROOT_LOADING, ROOT_OUTSIDE, ROOT_SET_USERNAME } from './actions/app'; import { ROOT_INSIDE, ROOT_LOADING, ROOT_OUTSIDE, ROOT_SET_USERNAME } from './actions/app';
@ -17,7 +18,7 @@ import { ThemeContext } from './theme';
import { setCurrentScreen } from './utils/log'; import { setCurrentScreen } from './utils/log';
// SetUsernameStack // SetUsernameStack
const SetUsername = createStackNavigator(); const SetUsername = createStackNavigator<SetUsernameStackParamList>();
const SetUsernameStack = () => ( const SetUsernameStack = () => (
<SetUsername.Navigator screenOptions={defaultHeader}> <SetUsername.Navigator screenOptions={defaultHeader}>
<SetUsername.Screen name='SetUsernameView' component={SetUsernameView} /> <SetUsername.Screen name='SetUsernameView' component={SetUsernameView} />
@ -25,7 +26,7 @@ const SetUsernameStack = () => (
); );
// App // App
const Stack = createStackNavigator(); const Stack = createStackNavigator<StackParamList>();
const App = React.memo(({ root, isMasterDetail }: { root: string; isMasterDetail: boolean }) => { const App = React.memo(({ root, isMasterDetail }: { root: string; isMasterDetail: boolean }) => {
if (!root) { if (!root) {
return null; return null;

View File

@ -2,8 +2,8 @@ const REQUEST = 'REQUEST';
const SUCCESS = 'SUCCESS'; const SUCCESS = 'SUCCESS';
const FAILURE = 'FAILURE'; const FAILURE = 'FAILURE';
const defaultTypes = [REQUEST, SUCCESS, FAILURE]; const defaultTypes = [REQUEST, SUCCESS, FAILURE];
function createRequestTypes(base, types = defaultTypes) { function createRequestTypes(base = {}, types = defaultTypes): Record<any, any> {
const res = {}; const res: Record<any, any> = {};
types.forEach(type => (res[type] = `${base}_${type}`)); types.forEach(type => (res[type] = `${base}_${type}`));
return res; return res;
} }

View File

@ -1,8 +0,0 @@
import { SET_ACTIVE_USERS } from './actionsTypes';
export function setActiveUsers(activeUsers) {
return {
type: SET_ACTIVE_USERS,
activeUsers
};
}

View File

@ -0,0 +1,15 @@
import { Action } from 'redux';
import { IActiveUsers } from '../reducers/activeUsers';
import { SET_ACTIVE_USERS } from './actionsTypes';
export interface ISetActiveUsers extends Action {
activeUsers: IActiveUsers;
}
export type TActionActiveUsers = ISetActiveUsers;
export const setActiveUsers = (activeUsers: IActiveUsers): ISetActiveUsers => ({
type: SET_ACTIVE_USERS,
activeUsers
});

View File

@ -1,28 +0,0 @@
import * as types from './actionsTypes';
export function addUser(user) {
return {
type: types.SELECTED_USERS.ADD_USER,
user
};
}
export function removeUser(user) {
return {
type: types.SELECTED_USERS.REMOVE_USER,
user
};
}
export function reset() {
return {
type: types.SELECTED_USERS.RESET
};
}
export function setLoading(loading) {
return {
type: types.SELECTED_USERS.SET_LOADING,
loading
};
}

View File

@ -0,0 +1,43 @@
import { Action } from 'redux';
import { ISelectedUser } from '../reducers/selectedUsers';
import * as types from './actionsTypes';
type TUser = {
user: ISelectedUser;
};
type TAction = Action & TUser;
interface ISetLoading extends Action {
loading: boolean;
}
export type TActionSelectedUsers = TAction & ISetLoading;
export function addUser(user: ISelectedUser): TAction {
return {
type: types.SELECTED_USERS.ADD_USER,
user
};
}
export function removeUser(user: ISelectedUser): TAction {
return {
type: types.SELECTED_USERS.REMOVE_USER,
user
};
}
export function reset(): Action {
return {
type: types.SELECTED_USERS.RESET
};
}
export function setLoading(loading: boolean): ISetLoading {
return {
type: types.SELECTED_USERS.SET_LOADING,
loading
};
}

View File

@ -1,2 +0,0 @@
export const DISPLAY_MODE_CONDENSED = 'condensed';
export const DISPLAY_MODE_EXPANDED = 'expanded';

View File

@ -0,0 +1,9 @@
export enum DisplayMode {
Condensed = 'condensed',
Expanded = 'expanded'
}
export enum SortBy {
Alphabetical = 'alphabetical',
Activity = 'activity'
}

View File

@ -124,7 +124,11 @@ const ActionSheet = React.memo(
const renderFooter = () => const renderFooter = () =>
data?.hasCancel ? ( data?.hasCancel ? (
<Button onPress={hide} style={[styles.button, { backgroundColor: themes[theme].auxiliaryBackground }]} theme={theme}> <Button
onPress={hide}
style={[styles.button, { backgroundColor: themes[theme].auxiliaryBackground }]}
theme={theme}
accessibilityLabel={I18n.t('Cancel')}>
<Text style={[styles.text, { color: themes[theme].bodyText }]}>{I18n.t('Cancel')}</Text> <Text style={[styles.text, { color: themes[theme].bodyText }]}>{I18n.t('Cancel')}</Text>
</Button> </Button>
) : null; ) : null;

View File

@ -1,7 +1,8 @@
import React from 'react';
import { TouchableOpacity } from 'react-native'; import { TouchableOpacity } from 'react-native';
import { isAndroid } from '../../utils/deviceInfo'; import { isAndroid } from '../../utils/deviceInfo';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
// Taken from https://github.com/rgommezz/react-native-scroll-bottom-sheet#touchables // Taken from https://github.com/rgommezz/react-native-scroll-bottom-sheet#touchables
export const Button = isAndroid ? Touch : TouchableOpacity; export const Button: typeof React.Component = isAndroid ? Touch : TouchableOpacity;

View File

@ -17,7 +17,7 @@ export const useActionSheet = () => useContext(context);
const { Provider, Consumer } = context; const { Provider, Consumer } = context;
export const withActionSheet = <P extends object>(Component: React.ComponentType<P>) => export const withActionSheet = (Component: any): any =>
forwardRef((props: any, ref: ForwardedRef<any>) => ( forwardRef((props: any, ref: ForwardedRef<any>) => (
<Consumer>{(contexts: any) => <Component {...props} {...contexts} ref={ref} />}</Consumer> <Consumer>{(contexts: any) => <Component {...props} {...contexts} ref={ref} />}</Consumer>
)); ));

View File

@ -5,6 +5,7 @@ import Touchable from 'react-native-platform-touchable';
import { settings as RocketChatSettings } from '@rocket.chat/sdk'; import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import { avatarURL } from '../../utils/avatar'; import { avatarURL } from '../../utils/avatar';
import { SubscriptionType } from '../../definitions/ISubscription';
import Emoji from '../markdown/Emoji'; import Emoji from '../markdown/Emoji';
import { IAvatar } from './interfaces'; import { IAvatar } from './interfaces';
@ -27,8 +28,8 @@ const Avatar = React.memo(
text, text,
size = 25, size = 25,
borderRadius = 4, borderRadius = 4,
type = 'd' type = SubscriptionType.DIRECT
}: Partial<IAvatar>) => { }: IAvatar) => {
if ((!text && !avatar && !emoji && !rid) || !server) { if ((!text && !avatar && !emoji && !rid) || !server) {
return null; return null;
} }

View File

@ -7,17 +7,17 @@ import { getUserSelector } from '../../selectors/login';
import Avatar from './Avatar'; import Avatar from './Avatar';
import { IAvatar } from './interfaces'; import { IAvatar } from './interfaces';
class AvatarContainer extends React.Component<Partial<IAvatar>, any> { class AvatarContainer extends React.Component<IAvatar, any> {
private mounted: boolean; private mounted: boolean;
private subscription!: any; private subscription: any;
static defaultProps = { static defaultProps = {
text: '', text: '',
type: 'd' type: 'd'
}; };
constructor(props: Partial<IAvatar>) { constructor(props: IAvatar) {
super(props); super(props);
this.mounted = false; this.mounted = false;
this.state = { avatarETag: '' }; this.state = { avatarETag: '' };
@ -55,7 +55,7 @@ class AvatarContainer extends React.Component<Partial<IAvatar>, any> {
try { try {
if (this.isDirect) { if (this.isDirect) {
const { text } = this.props; const { text } = this.props;
const [user] = await usersCollection.query(Q.where('username', text!)).fetch(); const [user] = await usersCollection.query(Q.where('username', text)).fetch();
record = user; record = user;
} else { } else {
const { rid } = this.props; const { rid } = this.props;
@ -82,7 +82,7 @@ class AvatarContainer extends React.Component<Partial<IAvatar>, any> {
render() { render() {
const { avatarETag } = this.state; const { avatarETag } = this.state;
const { serverVersion } = this.props; const { serverVersion } = this.props;
return <Avatar avatarETag={avatarETag} serverVersion={serverVersion} {...this.props} />; return <Avatar {...this.props} avatarETag={avatarETag} serverVersion={serverVersion} />;
} }
} }

View File

@ -1,23 +1,23 @@
export interface IAvatar { export interface IAvatar {
server: string; server?: string;
style: any; style?: any;
text: string; text: string;
avatar: string; avatar?: string;
emoji: string; emoji?: string;
size: number; size?: number;
borderRadius: number; borderRadius?: number;
type: string; type?: string;
children: JSX.Element; children?: JSX.Element;
user: { user?: {
id: string; id?: string;
token: string; token?: string;
}; };
theme: string; theme?: string;
onPress(): void; onPress?: () => void;
getCustomEmoji(): any; getCustomEmoji?: () => any;
avatarETag: string; avatarETag?: string;
isStatic: boolean | string; isStatic?: boolean | string;
rid: string; rid?: string;
blockUnauthenticatedAccess: boolean; blockUnauthenticatedAccess?: boolean;
serverVersion: string; serverVersion?: string;
} }

View File

@ -70,6 +70,7 @@ export default class Button extends React.PureComponent<Partial<IButtonProps>, a
disabled && styles.disabled, disabled && styles.disabled,
style style
]} ]}
accessibilityLabel={title}
{...otherProps}> {...otherProps}>
{loading ? ( {loading ? (
<ActivityIndicator color={textColor} /> <ActivityIndicator color={textColor} />

View File

@ -31,7 +31,7 @@ interface IEmojiPickerProps {
customEmojis?: any; customEmojis?: any;
style: object; style: object;
theme?: string; theme?: string;
onEmojiSelected?: Function; onEmojiSelected?: ((emoji: any) => void) | ((keyboardId: any, params?: any) => void);
tabEmojiStyle?: object; tabEmojiStyle?: object;
} }
@ -201,4 +201,5 @@ const mapStateToProps = (state: any) => ({
customEmojis: state.customEmojis customEmojis: state.customEmojis
}); });
export default connect(mapStateToProps)(withTheme(EmojiPicker)); // TODO - remove this as any, at the new PR to fix the HOC erros
export default connect(mapStateToProps)(withTheme(EmojiPicker)) as any;

View File

@ -2,7 +2,7 @@ import React from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
interface IHeaderButtonContainer { interface IHeaderButtonContainer {
children: JSX.Element; children: React.ReactNode;
left?: boolean; left?: boolean;
} }

View File

@ -423,4 +423,4 @@ const mapStateToProps = (state: any) => ({
services: state.login.services services: state.login.services
}); });
export default connect(mapStateToProps)(withTheme(LoginServices)); export default connect(mapStateToProps)(withTheme(LoginServices)) as any;

View File

@ -305,8 +305,6 @@ const MessageActions = React.memo(
}; };
const handleDelete = (message: any) => { const handleDelete = (message: any) => {
// TODO - migrate this function for ts when fix the lint erros
// @ts-ignore
showConfirmationAlert({ showConfirmationAlert({
message: I18n.t('You_will_not_be_able_to_recover_this_message'), message: I18n.t('You_will_not_be_able_to_recover_this_message'),
confirmationText: I18n.t('Delete'), confirmationText: I18n.t('Delete'),

View File

@ -13,7 +13,7 @@ interface IMessageBoxEmojiKeyboard {
} }
export default class EmojiKeyboard extends React.PureComponent<IMessageBoxEmojiKeyboard, any> { export default class EmojiKeyboard extends React.PureComponent<IMessageBoxEmojiKeyboard, any> {
private readonly baseUrl: any; private readonly baseUrl: string;
constructor(props: IMessageBoxEmojiKeyboard) { constructor(props: IMessageBoxEmojiKeyboard) {
super(props); super(props);

View File

@ -13,6 +13,7 @@ import { events, logEvent } from '../../utils/log';
interface IMessageBoxRecordAudioProps { interface IMessageBoxRecordAudioProps {
theme: string; theme: string;
permissionToUpload: boolean;
recordingCallback: Function; recordingCallback: Function;
onFinish: Function; onFinish: Function;
} }
@ -192,9 +193,11 @@ export default class RecordAudio extends React.PureComponent<IMessageBoxRecordAu
}; };
render() { render() {
const { theme } = this.props; const { theme, permissionToUpload } = this.props;
const { isRecording, isRecorderActive } = this.state; const { isRecording, isRecorderActive } = this.state;
if (!permissionToUpload) {
return null;
}
if (!isRecording && !isRecorderActive) { if (!isRecording && !isRecorderActive) {
return ( return (
<BorderlessButton <BorderlessButton

View File

@ -109,6 +109,8 @@ interface IMessageBoxProps {
sharing: boolean; sharing: boolean;
isActionsEnabled: boolean; isActionsEnabled: boolean;
usedCannedResponse: string; usedCannedResponse: string;
uploadFilePermission: string[];
serverVersion: string;
} }
interface IMessageBoxState { interface IMessageBoxState {
@ -124,6 +126,7 @@ interface IMessageBoxState {
}; };
tshow: boolean; tshow: boolean;
mentionLoading: boolean; mentionLoading: boolean;
permissionToUpload: boolean;
} }
class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> { class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
@ -179,41 +182,13 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
showCommandPreview: false, showCommandPreview: false,
command: {}, command: {},
tshow: false, tshow: false,
mentionLoading: false mentionLoading: false,
permissionToUpload: true
}; };
this.text = ''; this.text = '';
this.selection = { start: 0, end: 0 }; this.selection = { start: 0, end: 0 };
this.focused = false; this.focused = false;
// MessageBox Actions
this.options = [
{
title: I18n.t('Take_a_photo'),
icon: 'camera-photo',
onPress: this.takePhoto
},
{
title: I18n.t('Take_a_video'),
icon: 'camera',
onPress: this.takeVideo
},
{
title: I18n.t('Choose_from_library'),
icon: 'image',
onPress: this.chooseFromLibrary
},
{
title: I18n.t('Choose_file'),
icon: 'attach',
onPress: this.chooseFile
},
{
title: I18n.t('Create_Discussion'),
icon: 'discussions',
onPress: this.createDiscussion
}
];
const libPickerLabels = { const libPickerLabels = {
cropperChooseText: I18n.t('Choose'), cropperChooseText: I18n.t('Choose'),
cropperCancelText: I18n.t('Cancel'), cropperCancelText: I18n.t('Cancel'),
@ -277,6 +252,8 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
this.onChangeText(usedCannedResponse); this.onChangeText(usedCannedResponse);
} }
this.setOptions();
this.unsubscribeFocus = navigation.addListener('focus', () => { this.unsubscribeFocus = navigation.addListener('focus', () => {
// didFocus // didFocus
// We should wait pushed views be dismissed // We should wait pushed views be dismissed
@ -321,10 +298,20 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
} }
} }
shouldComponentUpdate(nextProps: any, nextState: any) { shouldComponentUpdate(nextProps: IMessageBoxProps, nextState: IMessageBoxState) {
const { showEmojiKeyboard, showSend, recording, mentions, commandPreview, tshow, mentionLoading, trackingType } = this.state; const {
showEmojiKeyboard,
showSend,
recording,
mentions,
commandPreview,
tshow,
mentionLoading,
trackingType,
permissionToUpload
} = this.state;
const { roomType, replying, editing, isFocused, message, theme, usedCannedResponse } = this.props; const { roomType, replying, editing, isFocused, message, theme, usedCannedResponse, uploadFilePermission } = this.props;
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
return true; return true;
} }
@ -358,6 +345,9 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (nextState.tshow !== tshow) { if (nextState.tshow !== tshow) {
return true; return true;
} }
if (nextState.permissionToUpload !== permissionToUpload) {
return true;
}
if (!dequal(nextState.mentions, mentions)) { if (!dequal(nextState.mentions, mentions)) {
return true; return true;
} }
@ -367,12 +357,22 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (!dequal(nextProps.message?.id, message?.id)) { if (!dequal(nextProps.message?.id, message?.id)) {
return true; return true;
} }
if (!dequal(nextProps.uploadFilePermission, uploadFilePermission)) {
return true;
}
if (nextProps.usedCannedResponse !== usedCannedResponse) { if (nextProps.usedCannedResponse !== usedCannedResponse) {
return true; return true;
} }
return false; return false;
} }
componentDidUpdate(prevProps: IMessageBoxProps) {
const { uploadFilePermission } = this.props;
if (!dequal(prevProps.uploadFilePermission, uploadFilePermission)) {
this.setOptions();
}
}
componentWillUnmount() { componentWillUnmount() {
console.countReset(`${this.constructor.name}.render calls`); console.countReset(`${this.constructor.name}.render calls`);
if (this.onChangeText && this.onChangeText.stop) { if (this.onChangeText && this.onChangeText.stop) {
@ -404,6 +404,19 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
} }
} }
setOptions = async () => {
const { uploadFilePermission, rid } = this.props;
// Servers older than 4.2
if (!uploadFilePermission) {
this.setState({ permissionToUpload: true });
return;
}
const permissionToUpload = await RocketChat.hasPermission([uploadFilePermission], rid);
this.setState({ permissionToUpload: permissionToUpload[0] });
};
onChangeText: any = (text: string): void => { onChangeText: any = (text: string): void => {
const isTextEmpty = text.length === 0; const isTextEmpty = text.length === 0;
this.setShowSend(!isTextEmpty); this.setShowSend(!isTextEmpty);
@ -666,8 +679,9 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}; };
canUploadFile = (file: any) => { canUploadFile = (file: any) => {
const { permissionToUpload } = this.state;
const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = this.props; const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = this.props;
const result = canUploadFile(file, FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize); const result = canUploadFile(file, FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize, permissionToUpload);
if (result.success) { if (result.success) {
return true; return true;
} }
@ -766,8 +780,41 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
showMessageBoxActions = () => { showMessageBoxActions = () => {
logEvent(events.ROOM_SHOW_BOX_ACTIONS); logEvent(events.ROOM_SHOW_BOX_ACTIONS);
const { permissionToUpload } = this.state;
const { showActionSheet } = this.props; const { showActionSheet } = this.props;
showActionSheet({ options: this.options });
const options = [];
if (permissionToUpload) {
options.push(
{
title: I18n.t('Take_a_photo'),
icon: 'camera-photo',
onPress: this.takePhoto
},
{
title: I18n.t('Take_a_video'),
icon: 'camera',
onPress: this.takeVideo
},
{
title: I18n.t('Choose_from_library'),
icon: 'image',
onPress: this.chooseFromLibrary
},
{
title: I18n.t('Choose_file'),
icon: 'attach',
onPress: this.chooseFile
}
);
}
options.push({
title: I18n.t('Create_Discussion'),
icon: 'discussions',
onPress: this.createDiscussion
});
showActionSheet({ options });
}; };
editCancel = () => { editCancel = () => {
@ -968,8 +1015,17 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
}; };
renderContent = () => { renderContent = () => {
const { recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview, mentionLoading } = const {
this.state; recording,
showEmojiKeyboard,
showSend,
mentions,
trackingType,
commandPreview,
showCommandPreview,
mentionLoading,
permissionToUpload
} = this.state;
const { const {
editing, editing,
message, message,
@ -995,7 +1051,12 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const recordAudio = const recordAudio =
showSend || !Message_AudioRecorderEnabled ? null : ( showSend || !Message_AudioRecorderEnabled ? null : (
<RecordAudio theme={theme} recordingCallback={this.recordingCallback} onFinish={this.finishAudioMessage} /> <RecordAudio
theme={theme}
recordingCallback={this.recordingCallback}
onFinish={this.finishAudioMessage}
permissionToUpload={permissionToUpload}
/>
); );
const commandsPreviewAndMentions = !recording ? ( const commandsPreviewAndMentions = !recording ? (
@ -1117,11 +1178,12 @@ const mapStateToProps = (state: any) => ({
user: getUserSelector(state), user: getUserSelector(state),
FileUpload_MediaTypeWhiteList: state.settings.FileUpload_MediaTypeWhiteList, FileUpload_MediaTypeWhiteList: state.settings.FileUpload_MediaTypeWhiteList,
FileUpload_MaxFileSize: state.settings.FileUpload_MaxFileSize, FileUpload_MaxFileSize: state.settings.FileUpload_MaxFileSize,
Message_AudioRecorderEnabled: state.settings.Message_AudioRecorderEnabled Message_AudioRecorderEnabled: state.settings.Message_AudioRecorderEnabled,
uploadFilePermission: state.permissions['mobile-upload-file']
}); });
const dispatchToProps = { const dispatchToProps = {
typing: (rid: any, status: any) => userTypingAction(rid, status) typing: (rid: any, status: any) => userTypingAction(rid, status)
}; };
// @ts-ignore // @ts-ignore
export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(withActionSheet(MessageBox)); export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(withActionSheet(MessageBox)) as any;

View File

@ -7,28 +7,28 @@ import Touch from '../../../utils/touch';
import { CustomIcon } from '../../../lib/Icons'; import { CustomIcon } from '../../../lib/Icons';
interface IPasscodeButton { interface IPasscodeButton {
text: string; text?: string;
icon: string; icon?: string;
theme: string; theme: string;
disabled: boolean; disabled?: boolean;
onPress: Function; onPress?: Function;
} }
const Button = React.memo(({ text, disabled, theme, onPress, icon }: Partial<IPasscodeButton>) => { const Button = React.memo(({ text, disabled, theme, onPress, icon }: IPasscodeButton) => {
const press = () => onPress && onPress(text!); const press = () => onPress && onPress(text);
return ( return (
<Touch <Touch
style={[styles.buttonView, { backgroundColor: 'transparent' }]} style={[styles.buttonView, { backgroundColor: 'transparent' }]}
underlayColor={themes[theme!].passcodeButtonActive} underlayColor={themes[theme].passcodeButtonActive}
rippleColor={themes[theme!].passcodeButtonActive} rippleColor={themes[theme].passcodeButtonActive}
enabled={!disabled} enabled={!disabled}
theme={theme} theme={theme}
onPress={press}> onPress={press}>
{icon ? ( {icon ? (
<CustomIcon name={icon} size={36} color={themes[theme!].passcodePrimary} /> <CustomIcon name={icon} size={36} color={themes[theme].passcodePrimary} />
) : ( ) : (
<Text style={[styles.buttonText, { color: themes[theme!].passcodePrimary }]}>{text}</Text> <Text style={[styles.buttonText, { color: themes[theme].passcodePrimary }]}>{text}</Text>
)} )}
</Touch> </Touch>
); );

View File

@ -20,7 +20,7 @@ interface IPasscodeBase {
previousPasscode?: string; previousPasscode?: string;
title: string; title: string;
subtitle?: string; subtitle?: string;
showBiometry?: string; showBiometry?: boolean;
onEndProcess: Function; onEndProcess: Function;
onError?: Function; onError?: Function;
onBiometryPress?(): void; onBiometryPress?(): void;

View File

@ -15,7 +15,7 @@ import I18n from '../../i18n';
interface IPasscodePasscodeEnter { interface IPasscodePasscodeEnter {
theme: string; theme: string;
hasBiometry: string; hasBiometry: boolean;
finishProcess: Function; finishProcess: Function;
} }

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, TextInputProps, View } from 'react-native';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import TextInput from '../presentation/TextInput'; import TextInput from '../presentation/TextInput';
@ -45,7 +45,7 @@ const styles = StyleSheet.create({
}); });
interface ISearchBox { interface ISearchBox {
onChangeText: () => void; onChangeText: TextInputProps['onChangeText'];
onSubmitEditing: () => void; onSubmitEditing: () => void;
hasCancel: boolean; hasCancel: boolean;
onCancelPress: Function; onCancelPress: Function;

View File

@ -8,6 +8,7 @@ interface IStatus {
status: string; status: string;
size: number; size: number;
style?: StyleProp<TextStyle>; style?: StyleProp<TextStyle>;
testID?: string;
} }
const Status = React.memo(({ style, status = 'offline', size = 32, ...props }: IStatus) => { const Status = React.memo(({ style, status = 'offline', size = 32, ...props }: IStatus) => {

View File

@ -43,7 +43,11 @@ const Content = React.memo(
content = <Text style={[styles.text, { color: themes[props.theme].bodyText }]}>{I18n.t('Sent_an_attachment')}</Text>; content = <Text style={[styles.text, { color: themes[props.theme].bodyText }]}>{I18n.t('Sent_an_attachment')}</Text>;
} else if (props.isEncrypted) { } else if (props.isEncrypted) {
content = ( content = (
<Text style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}>{I18n.t('Encrypted_message')}</Text> <Text
style={[styles.textInfo, { color: themes[props.theme].auxiliaryText }]}
accessibilityLabel={I18n.t('Encrypted_message')}>
{I18n.t('Encrypted_message')}
</Text>
); );
} else { } else {
const { baseUrl, user, onLinkPress } = useContext(MessageContext); const { baseUrl, user, onLinkPress } = useContext(MessageContext);

View File

@ -13,6 +13,7 @@ import { themes } from '../../constants/colors';
import MessageContext from './Context'; import MessageContext from './Context';
import { fileDownloadAndPreview } from '../../utils/fileDownload'; import { fileDownloadAndPreview } from '../../utils/fileDownload';
import { formatAttachmentUrl } from '../../lib/utils'; import { formatAttachmentUrl } from '../../lib/utils';
import { IAttachment } from '../../definitions/IAttachment';
import RCActivityIndicator from '../ActivityIndicator'; import RCActivityIndicator from '../ActivityIndicator';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -90,43 +91,26 @@ const styles = StyleSheet.create({
} }
}); });
interface IMessageReplyAttachment {
author_name: string;
message_link: string;
ts: string;
text: string;
title: string;
short: boolean;
value: string;
title_link: string;
author_link: string;
type: string;
color: string;
description: string;
fields: IMessageReplyAttachment[];
thumb_url: string;
}
interface IMessageTitle { interface IMessageTitle {
attachment: Partial<IMessageReplyAttachment>; attachment: IAttachment;
timeFormat: string; timeFormat: string;
theme: string; theme: string;
} }
interface IMessageDescription { interface IMessageDescription {
attachment: Partial<IMessageReplyAttachment>; attachment: IAttachment;
getCustomEmoji: Function; getCustomEmoji: Function;
theme: string; theme: string;
} }
interface IMessageFields { interface IMessageFields {
attachment: Partial<IMessageReplyAttachment>; attachment: IAttachment;
theme: string; theme: string;
getCustomEmoji: Function; getCustomEmoji: Function;
} }
interface IMessageReply { interface IMessageReply {
attachment: IMessageReplyAttachment; attachment: IAttachment;
timeFormat: string; timeFormat: string;
index: number; index: number;
theme: string; theme: string;
@ -198,7 +182,7 @@ const Fields = React.memo(
<Text style={[styles.fieldTitle, { color: themes[theme].bodyText }]}>{field.title}</Text> <Text style={[styles.fieldTitle, { color: themes[theme].bodyText }]}>{field.title}</Text>
{/* @ts-ignore*/} {/* @ts-ignore*/}
<Markdown <Markdown
msg={field.value} msg={field.value!}
baseUrl={baseUrl} baseUrl={baseUrl}
username={user.username} username={user.username}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}

View File

@ -13,6 +13,7 @@ import { fileDownload } from '../../utils/fileDownload';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import { LISTENER } from '../Toast'; import { LISTENER } from '../Toast';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { IAttachment } from '../../definitions/IAttachment';
import RCActivityIndicator from '../ActivityIndicator'; import RCActivityIndicator from '../ActivityIndicator';
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])]; const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])];
@ -30,14 +31,7 @@ const styles = StyleSheet.create({
}); });
interface IMessageVideo { interface IMessageVideo {
file: { file: IAttachment;
title: string;
title_link: string;
type: string;
video_type: string;
video_url: string;
description: string;
};
showAttachment: Function; showAttachment: Function;
getCustomEmoji: Function; getCustomEmoji: Function;
theme: string; theme: string;

View File

@ -84,7 +84,7 @@ export interface IMessageContent {
export interface IMessageDiscussion { export interface IMessageDiscussion {
msg: string; msg: string;
dcount: number; dcount: number;
dlm: string; dlm: Date;
theme: string; theme: string;
} }

View File

@ -0,0 +1,25 @@
export interface IAttachment {
ts: Date;
title: string;
type: string;
description: string;
title_link?: string;
image_url?: string;
image_type?: string;
video_url?: string;
video_type?: string;
title_link_download?: boolean;
fields?: IAttachment[];
image_dimensions?: { width?: number; height?: number };
image_preview?: string;
image_size?: number;
author_name?: string;
author_icon?: string;
message_link?: string;
text?: string;
short?: boolean;
value?: string;
author_link?: string;
color?: string;
thumb_url?: string;
}

View File

@ -0,0 +1,6 @@
export interface ICommand {
event: {
input: string;
modifierFlags: number;
};
}

View File

@ -0,0 +1,10 @@
import Model from '@nozbe/watermelondb/Model';
export interface ICustomEmoji {
name?: string;
aliases?: string;
extension: string;
_updatedAt: Date;
}
export type TCustomEmojiModel = ICustomEmoji & Model;

View File

@ -0,0 +1,10 @@
import Model from '@nozbe/watermelondb/Model';
export interface IFrequentlyUsedEmoji {
content?: string;
extension?: string;
isCustom: boolean;
count: number;
}
export type TFrequentlyUsedEmoji = IFrequentlyUsedEmoji & Model;

View File

@ -0,0 +1,18 @@
import Model from '@nozbe/watermelondb/Model';
export interface ILoggedUser {
id: string;
token: string;
username: string;
name: string;
language?: string;
status: string;
statusText?: string;
roles: string[];
avatarETag?: string;
showMessageInMainThread: boolean;
isFromWebView: boolean;
enableMessageParserEarlyAdoption?: boolean;
}
export type TLoggedUser = ILoggedUser & Model;

View File

@ -0,0 +1,6 @@
export interface IMention {
_id: string;
name: string;
username: string;
type: string;
}

View File

@ -0,0 +1,95 @@
import Model from '@nozbe/watermelondb/Model';
import { MarkdownAST } from '@rocket.chat/message-parser';
import { IAttachment } from './IAttachment';
import { IReaction } from './IReaction';
import { SubscriptionType } from './ISubscription';
export interface IUserMessage {
_id: string;
username?: string;
name?: string;
}
export interface IUserMention extends IUserMessage {
type: string;
}
export interface IUserChannel {
[index: number]: string | number;
name: string;
_id: string;
}
export interface IEditedBy {
_id: string;
username: string;
}
export type TOnLinkPress = (link: string) => void;
export interface ITranslations {
_id: string;
language: string;
value: string;
}
export interface ILastMessage {
_id: string;
rid: string;
tshow: boolean;
tmid: string;
msg: string;
ts: Date;
u: IUserMessage;
_updatedAt: Date;
urls: string[];
mentions: IUserMention[];
channels: IUserChannel[];
md: MarkdownAST;
attachments: IAttachment[];
reactions: IReaction[];
unread: boolean;
status: boolean;
}
export interface IMessage {
msg?: string;
t?: SubscriptionType;
ts: Date;
u: IUserMessage;
alias: string;
parseUrls: boolean;
groupable?: boolean;
avatar?: string;
emoji?: string;
attachments?: IAttachment[];
urls?: string[];
_updatedAt: Date;
status?: number;
pinned?: boolean;
starred?: boolean;
editedBy?: IEditedBy;
reactions?: IReaction[];
role?: string;
drid?: string;
dcount?: number;
dlm?: Date;
tmid?: string;
tcount?: number;
tlm?: Date;
replies?: string[];
mentions?: IUserMention[];
channels?: IUserChannel[];
unread?: boolean;
autoTranslate?: boolean;
translations?: ITranslations[];
tmsg?: string;
blocks?: any;
e2e?: string;
tshow?: boolean;
md?: MarkdownAST;
subscription: { id: string };
}
export type TMessageModel = IMessage & Model;

View File

@ -0,0 +1,12 @@
export interface INotification {
message: string;
style: string;
ejson: string;
collapse_key: string;
notId: string;
msgcnt: string;
title: string;
from: string;
image: string;
soundname: string;
}

View File

@ -0,0 +1,9 @@
import Model from '@nozbe/watermelondb/Model';
export interface IPermission {
id: string;
roles: string[];
_updatedAt: Date;
}
export type TPermissionModel = IPermission & Model;

View File

@ -0,0 +1,5 @@
export interface IReaction {
_id: string;
emoji: string;
usernames: string[];
}

8
app/definitions/IRole.ts Normal file
View File

@ -0,0 +1,8 @@
import Model from '@nozbe/watermelondb/Model';
export interface IRole {
id: string;
description?: string;
}
export type TRoleModel = IRole & Model;

20
app/definitions/IRoom.ts Normal file
View File

@ -0,0 +1,20 @@
import Model from '@nozbe/watermelondb/Model';
import { IServedBy } from './IServedBy';
export interface IRoom {
id: string;
customFields: string[];
broadcast: boolean;
encrypted: boolean;
ro: boolean;
v?: string[];
servedBy?: IServedBy;
departmentId?: string;
livechatData?: any;
tags?: string[];
e2eKeyId?: string;
avatarETag?: string;
}
export type TRoomModel = IRoom & Model;

View File

@ -0,0 +1,5 @@
export interface IServedBy {
_id: string;
username: string;
ts: Date;
}

View File

@ -0,0 +1,20 @@
import Model from '@nozbe/watermelondb/Model';
export interface IServer {
name: string;
iconURL: string;
useRealName: boolean;
FileUpload_MediaTypeWhiteList: string;
FileUpload_MaxFileSize: number;
roomsUpdatedAt: Date;
version: string;
lastLocalAuthenticatedSession: Date;
autoLock: boolean;
autoLockTime?: number;
biometry?: boolean;
uniqueID: string;
enterpriseModules: string;
E2E_Enable: boolean;
}
export type TServerModel = IServer & Model;

View File

@ -0,0 +1,10 @@
import Model from '@nozbe/watermelondb/Model';
export interface IServerHistory {
id: string;
url: string;
username: string;
updatedAt: Date;
}
export type TServerHistory = IServerHistory & Model;

View File

@ -0,0 +1,12 @@
import Model from '@nozbe/watermelondb/Model';
export interface ISettings {
id: string;
valueAsString?: string;
valueAsBoolean?: boolean;
valueAsNumber?: number;
valueAsArray?: string[];
_updatedAt?: Date;
}
export type TSettingsModel = ISettings & Model;

View File

@ -0,0 +1,12 @@
import Model from '@nozbe/watermelondb/Model';
export interface ISlashCommand {
id: string;
params?: string;
description?: string;
clientOnly?: boolean;
providesPreview?: boolean;
appId?: string;
}
export type TSlashCommandModel = ISlashCommand & Model;

View File

@ -0,0 +1,91 @@
import Model from '@nozbe/watermelondb/Model';
import Relation from '@nozbe/watermelondb/Relation';
import { ILastMessage, TMessageModel } from './IMessage';
import { IServedBy } from './IServedBy';
import { TThreadModel } from './IThread';
import { TThreadMessageModel } from './IThreadMessage';
import { TUploadModel } from './IUpload';
export enum SubscriptionType {
GROUP = 'p',
DIRECT = 'd',
CHANNEL = 'c',
OMNICHANNEL = 'l',
THREAD = 'thread'
}
export interface IVisitor {
_id: string;
username: string;
token: string;
status: string;
lastMessageTs: Date;
}
export interface ISubscription {
_id: string; // _id belongs watermelonDB
id: string; // id from server
f: boolean;
t: SubscriptionType;
ts: Date;
ls: Date;
name: string;
fname?: string;
rid: string; // the same as id
open: boolean;
alert: boolean;
roles?: string[];
unread: number;
userMentions: number;
groupMentions: number;
tunread?: string[];
tunreadUser?: string[];
tunreadGroup?: string[];
roomUpdatedAt: Date;
ro: boolean;
lastOpen?: Date;
description?: string;
announcement?: string;
bannerClosed?: boolean;
topic?: string;
blocked?: boolean;
blocker?: boolean;
reactWhenReadOnly?: boolean;
archived: boolean;
joinCodeRequired?: boolean;
muted?: string[];
ignored?: string[];
broadcast?: boolean;
prid?: string;
draftMessage?: string;
lastThreadSync?: Date;
jitsiTimeout?: number;
autoTranslate?: boolean;
autoTranslateLanguage: string;
lastMessage?: ILastMessage;
hideUnreadStatus?: boolean;
sysMes?: string[] | boolean;
uids?: string[];
usernames?: string[];
visitor?: IVisitor;
departmentId?: string;
servedBy?: IServedBy;
livechatData?: any;
tags?: string[];
E2EKey?: string;
encrypted?: boolean;
e2eKeyId?: string;
avatarETag?: string;
teamId?: string;
teamMain?: boolean;
search?: boolean;
username?: string;
// https://nozbe.github.io/WatermelonDB/Relation.html#relation-api
messages: Relation<TMessageModel>;
threads: Relation<TThreadModel>;
threadMessages: Relation<TThreadMessageModel>;
uploads: Relation<TUploadModel>;
}
export type TSubscriptionModel = ISubscription & Model;

View File

@ -1,5 +1,5 @@
// https://github.com/RocketChat/Rocket.Chat/blob/develop/definition/ITeam.ts // https://github.com/RocketChat/Rocket.Chat/blob/develop/definition/ITeam.ts
export const TEAM_TYPE = { exports.TEAM_TYPE = {
PUBLIC: 0, PUBLIC: 0,
PRIVATE: 1 PRIVATE: 1
}; };

View File

@ -0,0 +1,8 @@
export type TThemeMode = 'automatic' | 'light' | 'dark';
export type TDarkLevel = 'black' | 'dark';
export interface IThemePreference {
currentTheme: TThemeMode;
darkLevel: TDarkLevel;
}

View File

@ -0,0 +1,78 @@
import Model from '@nozbe/watermelondb/Model';
import { MarkdownAST } from '@rocket.chat/message-parser';
import { IAttachment } from './IAttachment';
import { IEditedBy, IUserChannel, IUserMention, IUserMessage } from './IMessage';
import { IReaction } from './IReaction';
import { SubscriptionType } from './ISubscription';
export interface IUrl {
title: string;
description: string;
image: string;
url: string;
}
interface IFileThread {
_id: string;
name: string;
type: string;
}
export interface IThreadResult {
_id: string;
rid: string;
ts: string;
msg: string;
file?: IFileThread;
files?: IFileThread[];
groupable?: boolean;
attachments?: IAttachment[];
md?: MarkdownAST;
u: IUserMessage;
_updatedAt: string;
urls: IUrl[];
mentions: IUserMention[];
channels: IUserChannel[];
replies: string[];
tcount: number;
tlm: string;
}
export interface IThread {
id: string;
msg?: string;
t?: SubscriptionType;
rid: string;
_updatedAt: Date;
ts: Date;
u: IUserMessage;
alias?: string;
parseUrls?: boolean;
groupable?: boolean;
avatar?: string;
emoji?: string;
attachments?: IAttachment[];
urls?: IUrl[];
status?: number;
pinned?: boolean;
starred?: boolean;
editedBy?: IEditedBy;
reactions?: IReaction[];
role?: string;
drid?: string;
dcount?: number;
dlm?: number;
tmid?: string;
tcount?: number;
tlm?: Date;
replies?: string[];
mentions?: IUserMention[];
channels?: IUserChannel[];
unread?: boolean;
autoTranslate?: boolean;
translations?: any;
e2e?: string;
}
export type TThreadModel = IThread & Model;

View File

@ -0,0 +1,44 @@
import Model from '@nozbe/watermelondb/Model';
import { IAttachment } from './IAttachment';
import { IEditedBy, ITranslations, IUserChannel, IUserMention, IUserMessage } from './IMessage';
import { IReaction } from './IReaction';
import { SubscriptionType } from './ISubscription';
export interface IThreadMessage {
msg?: string;
t?: SubscriptionType;
rid: string;
ts: Date;
u: IUserMessage;
alias?: string;
parseUrls?: boolean;
groupable?: boolean;
avatar?: string;
emoji?: string;
attachments?: IAttachment[];
urls?: string[];
_updatedAt?: Date;
status?: number;
pinned?: boolean;
starred?: boolean;
editedBy?: IEditedBy;
reactions?: IReaction[];
role?: string;
drid?: string;
dcount?: number;
dlm?: Date;
tmid?: string;
tcount?: number;
tlm?: Date;
replies?: string[];
mentions?: IUserMention[];
channels?: IUserChannel[];
unread?: boolean;
autoTranslate?: boolean;
translations?: ITranslations[];
e2e?: string;
subscription?: { id: string };
}
export type TThreadMessageModel = IThreadMessage & Model;

View File

@ -0,0 +1,16 @@
import Model from '@nozbe/watermelondb/Model';
export interface IUpload {
id: string;
path?: string;
name?: string;
description?: string;
size: number;
type?: string;
store?: string;
progress: number;
error: boolean;
subscription: { id: string };
}
export type TUploadModel = IUpload & Model;

6
app/definitions/IUrl.ts Normal file
View File

@ -0,0 +1,6 @@
export interface IUrl {
title: string;
description: string;
image: string;
url: string;
}

10
app/definitions/IUser.ts Normal file
View File

@ -0,0 +1,10 @@
import Model from '@nozbe/watermelondb/Model';
export interface IUser {
_id: string;
name?: string;
username: string;
avatarETag?: string;
}
export type TUserModel = IUser & Model;

19
app/definitions/index.ts Normal file
View File

@ -0,0 +1,19 @@
import { RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { Dispatch } from 'redux';
export * from './IAttachment';
export * from './IMessage';
export * from './INotification';
export * from './IRoom';
export * from './IServer';
export * from './ISubscription';
export interface IBaseScreen<T extends Record<string, object | undefined>, S extends string> {
navigation: StackNavigationProp<T, S>;
route: RouteProp<T, S>;
dispatch: Dispatch;
theme: string;
}
export * from './redux';

View File

@ -0,0 +1,31 @@
import { TActionSelectedUsers } from '../../actions/selectedUsers';
import { TActionActiveUsers } from '../../actions/activeUsers';
// REDUCERS
import { IActiveUsers } from '../../reducers/activeUsers';
import { ISelectedUsers } from '../../reducers/selectedUsers';
export interface IApplicationState {
settings: any;
login: any;
meteor: any;
server: any;
selectedUsers: ISelectedUsers;
createChannel: any;
app: any;
room: any;
rooms: any;
sortPreferences: any;
share: any;
customEmojis: any;
activeUsers: IActiveUsers;
usersTyping: any;
inviteLinks: any;
createDiscussion: any;
inquiry: any;
enterpriseModules: any;
encryption: any;
permissions: any;
roles: any;
}
export type TApplicationActions = TActionActiveUsers & TActionSelectedUsers;

View File

@ -22,7 +22,7 @@ export interface IDimensionsContextProps {
export const DimensionsContext = React.createContext<Partial<IDimensionsContextProps>>(Dimensions.get('window')); export const DimensionsContext = React.createContext<Partial<IDimensionsContextProps>>(Dimensions.get('window'));
export function withDimensions(Component: any) { export function withDimensions(Component: any): any {
const DimensionsComponent = (props: any) => ( const DimensionsComponent = (props: any) => (
<DimensionsContext.Consumer>{contexts => <Component {...props} {...contexts} />}</DimensionsContext.Consumer> <DimensionsContext.Consumer>{contexts => <Component {...props} {...contexts} />}</DimensionsContext.Consumer>
); );

View File

@ -6,7 +6,6 @@ import { inquiryQueueAdd, inquiryQueueRemove, inquiryQueueUpdate, inquiryRequest
const removeListener = listener => listener.stop(); const removeListener = listener => listener.stop();
let connectedListener; let connectedListener;
let disconnectedListener;
let queueListener; let queueListener;
const streamTopic = 'stream-livechat-inquiry-queue-observer'; const streamTopic = 'stream-livechat-inquiry-queue-observer';
@ -48,10 +47,6 @@ export default function subscribeInquiry() {
connectedListener.then(removeListener); connectedListener.then(removeListener);
connectedListener = false; connectedListener = false;
} }
if (disconnectedListener) {
disconnectedListener.then(removeListener);
disconnectedListener = false;
}
if (queueListener) { if (queueListener) {
queueListener.then(removeListener); queueListener.then(removeListener);
queueListener = false; queueListener = false;
@ -59,7 +54,6 @@ export default function subscribeInquiry() {
}; };
connectedListener = RocketChat.onStreamData('connected', handleConnection); connectedListener = RocketChat.onStreamData('connected', handleConnection);
disconnectedListener = RocketChat.onStreamData('close', handleConnection);
queueListener = RocketChat.onStreamData(streamTopic, handleQueueMessageReceived); queueListener = RocketChat.onStreamData(streamTopic, handleQueueMessageReceived);
try { try {

View File

@ -161,4 +161,5 @@ const mapStateToProps = state => ({
showAvatar: state.sortPreferences.showAvatar, showAvatar: state.sortPreferences.showAvatar,
displayMode: state.sortPreferences.displayMode displayMode: state.sortPreferences.displayMode
}); });
export default connect(mapStateToProps)(withDimensions(withTheme(QueueListView))); export default connect(mapStateToProps)(withDimensions(withTheme(QueueListView)));

View File

@ -13,3 +13,4 @@ declare module 'react-native-mime-types';
declare module 'react-native-restart'; declare module 'react-native-restart';
declare module 'react-native-prompt-android'; declare module 'react-native-prompt-android';
declare module 'react-native-jitsi-meet'; declare module 'react-native-jitsi-meet';
declare module 'rn-root-view';

View File

@ -328,7 +328,6 @@
"N_users": "{{n}} مستخدمين", "N_users": "{{n}} مستخدمين",
"N_channels": "{{n}} القنوات", "N_channels": "{{n}} القنوات",
"Name": "اسم", "Name": "اسم",
"Navigation_history": "تاريخ التصفح",
"Never": "أبداً", "Never": "أبداً",
"New_Message": "رسالة جديدة", "New_Message": "رسالة جديدة",
"New_Password": "كلمة مرور جديدة", "New_Password": "كلمة مرور جديدة",

View File

@ -330,7 +330,6 @@
"N_users": "{{n}} Benutzer", "N_users": "{{n}} Benutzer",
"N_channels": "{{n}} Kanäle", "N_channels": "{{n}} Kanäle",
"Name": "Name", "Name": "Name",
"Navigation_history": "Navigations-Verlauf",
"Never": "Niemals", "Never": "Niemals",
"New_Message": "Neue Nachricht", "New_Message": "Neue Nachricht",
"New_Password": "Neues Kennwort", "New_Password": "Neues Kennwort",

View File

@ -21,6 +21,7 @@
"error-save-video": "Error while saving video", "error-save-video": "Error while saving video",
"error-field-unavailable": "{{field}} is already in use :(", "error-field-unavailable": "{{field}} is already in use :(",
"error-file-too-large": "File is too large", "error-file-too-large": "File is too large",
"error-not-permission-to-upload-file": "You don't have permission to upload files",
"error-importer-not-defined": "The importer was not defined correctly, it is missing the Import class.", "error-importer-not-defined": "The importer was not defined correctly, it is missing the Import class.",
"error-input-is-not-a-valid-field": "{{input}} is not a valid {{field}}", "error-input-is-not-a-valid-field": "{{input}} is not a valid {{field}}",
"error-invalid-actionlink": "Invalid action link", "error-invalid-actionlink": "Invalid action link",
@ -330,7 +331,6 @@
"N_users": "{{n}} users", "N_users": "{{n}} users",
"N_channels": "{{n}} channels", "N_channels": "{{n}} channels",
"Name": "Name", "Name": "Name",
"Navigation_history": "Navigation history",
"Never": "Never", "Never": "Never",
"New_Message": "New Message", "New_Message": "New Message",
"New_Password": "New Password", "New_Password": "New Password",
@ -787,4 +787,4 @@
"Unsupported_format": "Unsupported format", "Unsupported_format": "Unsupported format",
"Downloaded_file": "Downloaded file", "Downloaded_file": "Downloaded file",
"Error_Download_file": "Error while downloading file" "Error_Download_file": "Error while downloading file"
} }

View File

@ -330,7 +330,6 @@
"N_users": "{{n}} utilisateurs", "N_users": "{{n}} utilisateurs",
"N_channels": "{{n}} canaux", "N_channels": "{{n}} canaux",
"Name": "Nom", "Name": "Nom",
"Navigation_history": "Historique de navigation",
"Never": "Jamais", "Never": "Jamais",
"New_Message": "Nouveau message", "New_Message": "Nouveau message",
"New_Password": "Nouveau mot de passe", "New_Password": "Nouveau mot de passe",
@ -782,5 +781,8 @@
"No_canned_responses": "Pas de réponses standardisées", "No_canned_responses": "Pas de réponses standardisées",
"Send_email_confirmation": "Envoyer un e-mail de confirmation", "Send_email_confirmation": "Envoyer un e-mail de confirmation",
"sending_email_confirmation": "envoi d'e-mail de confirmation", "sending_email_confirmation": "envoi d'e-mail de confirmation",
"Enable_Message_Parser": "Activer le parseur de messages" "Enable_Message_Parser": "Activer le parseur de messages",
"Unsupported_format": "Format non supporté",
"Downloaded_file": "Fichier téléchargé",
"Error_Download_file": "Erreur lors du téléchargement du fichier"
} }

View File

@ -322,7 +322,6 @@
"N_people_reacted": "{{n}} persone hanno reagito", "N_people_reacted": "{{n}} persone hanno reagito",
"N_users": "{{n}} utenti", "N_users": "{{n}} utenti",
"Name": "Nome", "Name": "Nome",
"Navigation_history": "Cronologia di navigazione",
"Never": "Mai", "Never": "Mai",
"New_Message": "Nuovo messaggio", "New_Message": "Nuovo messaggio",
"New_Password": "Nuova password", "New_Password": "Nuova password",

View File

@ -330,7 +330,6 @@
"N_users": "{{n}} gebruikers", "N_users": "{{n}} gebruikers",
"N_channels": "{{n}} kanalen", "N_channels": "{{n}} kanalen",
"Name": "Naam", "Name": "Naam",
"Navigation_history": "Navigatie geschiedenis",
"Never": "Nooit", "Never": "Nooit",
"New_Message": "Nieuw bericht", "New_Message": "Nieuw bericht",
"New_Password": "Nieuw wachtwoord", "New_Password": "Nieuw wachtwoord",
@ -782,5 +781,8 @@
"No_canned_responses": "Geen standaardantwoorden", "No_canned_responses": "Geen standaardantwoorden",
"Send_email_confirmation": "Stuur e-mailbevestiging", "Send_email_confirmation": "Stuur e-mailbevestiging",
"sending_email_confirmation": "e-mailbevestiging aan het verzenden", "sending_email_confirmation": "e-mailbevestiging aan het verzenden",
"Enable_Message_Parser": "Berichtparser inschakelen" "Enable_Message_Parser": "Berichtparser inschakelen",
"Unsupported_format": "Niet ondersteund formaat",
"Downloaded_file": "Gedownload bestand",
"Error_Download_file": "Fout tijdens het downloaden van bestand"
} }

View File

@ -309,7 +309,6 @@
"N_users": "{{n}} usuários", "N_users": "{{n}} usuários",
"N_channels": "{{n}} canais", "N_channels": "{{n}} canais",
"Name": "Nome", "Name": "Nome",
"Navigation_history": "Histórico de navegação",
"Never": "Nunca", "Never": "Nunca",
"New_Message": "Nova Mensagem", "New_Message": "Nova Mensagem",
"New_Password": "Nova Senha", "New_Password": "Nova Senha",
@ -737,4 +736,4 @@
"Unsupported_format": "Formato não suportado", "Unsupported_format": "Formato não suportado",
"Downloaded_file": "Arquivo baixado", "Downloaded_file": "Arquivo baixado",
"Error_Download_file": "Erro ao baixar o arquivo" "Error_Download_file": "Erro ao baixar o arquivo"
} }

View File

@ -329,7 +329,6 @@
"N_users": "{{n}} utilizadores", "N_users": "{{n}} utilizadores",
"N_channels": "{{n}} canais", "N_channels": "{{n}} canais",
"Name": "Nome", "Name": "Nome",
"Navigation_history": "Histórico de navegação",
"Never": "Nunca", "Never": "Nunca",
"New_Message": "Nova Mensagem", "New_Message": "Nova Mensagem",
"New_Password": "Nova Palavra-passe", "New_Password": "Nova Palavra-passe",

View File

@ -330,7 +330,6 @@
"N_users": "{{n}} пользователи", "N_users": "{{n}} пользователи",
"N_channels": "{{n}} каналов", "N_channels": "{{n}} каналов",
"Name": "Имя", "Name": "Имя",
"Navigation_history": "История навигации",
"Never": "Никогда", "Never": "Никогда",
"New_Message": "Новое сообщение", "New_Message": "Новое сообщение",
"New_Password": "Новый пароль", "New_Password": "Новый пароль",
@ -782,5 +781,8 @@
"No_canned_responses": "Нет заготовленных ответов", "No_canned_responses": "Нет заготовленных ответов",
"Send_email_confirmation": "Отправить электронное письмо с подтверждением", "Send_email_confirmation": "Отправить электронное письмо с подтверждением",
"sending_email_confirmation": "отправка подтверждения по электронной почте", "sending_email_confirmation": "отправка подтверждения по электронной почте",
"Enable_Message_Parser": "Включить парсер сообщений" "Enable_Message_Parser": "Включить парсер сообщений",
"Unsupported_format": "Неподдерживаемый формат",
"Downloaded_file": "Скачанный файл",
"Error_Download_file": "Ошибка при скачивании файла"
} }

View File

@ -323,7 +323,6 @@
"N_people_reacted": "{{n}} kişi tepki verdi", "N_people_reacted": "{{n}} kişi tepki verdi",
"N_users": "{{n}} kullanıcı", "N_users": "{{n}} kullanıcı",
"Name": "İsim", "Name": "İsim",
"Navigation_history": "Gezinti geçmişi",
"Never": "Asla", "Never": "Asla",
"New_Message": "Yeni İleti", "New_Message": "Yeni İleti",
"New_Password": "Yeni Şifre", "New_Password": "Yeni Şifre",

View File

@ -320,7 +320,6 @@
"N_people_reacted": "{{n}} 人回复", "N_people_reacted": "{{n}} 人回复",
"N_users": "{{n}} 位用户", "N_users": "{{n}} 位用户",
"Name": "名称", "Name": "名称",
"Navigation_history": "浏览历史记录",
"Never": "从不", "Never": "从不",
"New_Message": "新信息", "New_Message": "新信息",
"New_Password": "新密码", "New_Password": "新密码",

View File

@ -321,7 +321,6 @@
"N_people_reacted": "{{n}} 人回复", "N_people_reacted": "{{n}} 人回复",
"N_users": "{{n}} 位使用者", "N_users": "{{n}} 位使用者",
"Name": "名稱", "Name": "名稱",
"Navigation_history": "瀏覽歷史記錄",
"Never": "從不", "Never": "從不",
"New_Message": "新訊息", "New_Message": "新訊息",
"New_Password": "新密碼", "New_Password": "新密碼",

View File

@ -30,6 +30,8 @@ import InAppNotification from './containers/InAppNotification';
import { ActionSheetProvider } from './containers/ActionSheet'; import { ActionSheetProvider } from './containers/ActionSheet';
import debounce from './utils/debounce'; import debounce from './utils/debounce';
import { isFDroidBuild } from './constants/environment'; import { isFDroidBuild } from './constants/environment';
import { IThemePreference } from './definitions/ITheme';
import { ICommand } from './definitions/ICommand';
RNScreens.enableScreens(); RNScreens.enableScreens();
@ -42,10 +44,7 @@ interface IDimensions {
interface IState { interface IState {
theme: string; theme: string;
themePreferences: { themePreferences: IThemePreference;
currentTheme: 'automatic' | 'light';
darkLevel: string;
};
width: number; width: number;
height: number; height: number;
scale: number; scale: number;
@ -175,7 +174,7 @@ export default class Root extends React.Component<{}, IState> {
setTheme = (newTheme = {}) => { setTheme = (newTheme = {}) => {
// change theme state // change theme state
this.setState( this.setState(
prevState => newThemeState(prevState, newTheme), prevState => newThemeState(prevState, newTheme as IThemePreference),
() => { () => {
const { themePreferences } = this.state; const { themePreferences } = this.state;
// subscribe to Appearance changes // subscribe to Appearance changes
@ -191,7 +190,7 @@ export default class Root extends React.Component<{}, IState> {
initTablet = () => { initTablet = () => {
const { width } = this.state; const { width } = this.state;
this.setMasterDetail(width); this.setMasterDetail(width);
this.onKeyCommands = KeyCommandsEmitter.addListener('onKeyCommand', (command: unknown) => { this.onKeyCommands = KeyCommandsEmitter.addListener('onKeyCommand', (command: ICommand) => {
EventEmitter.emit(KEY_COMMAND, { event: command }); EventEmitter.emit(KEY_COMMAND, { event: command });
}); });
}; };

View File

@ -55,7 +55,8 @@ const PERMISSIONS = [
'convert-team', 'convert-team',
'edit-omnichannel-contact', 'edit-omnichannel-contact',
'edit-livechat-room-customfields', 'edit-livechat-room-customfields',
'view-canned-responses' 'view-canned-responses',
'mobile-upload-file'
]; ];
export async function setPermissions() { export async function setPermissions() {

View File

@ -8,7 +8,6 @@ import messagesStatus from '../../../constants/messagesStatus';
import log from '../../../utils/log'; import log from '../../../utils/log';
import random from '../../../utils/random'; import random from '../../../utils/random';
import store from '../../createStore'; import store from '../../createStore';
import { roomsRequest } from '../../../actions/rooms';
import { handlePayloadUserInteraction } from '../actions'; import { handlePayloadUserInteraction } from '../actions';
import buildMessage from '../helpers/buildMessage'; import buildMessage from '../helpers/buildMessage';
import RocketChat from '../../rocketchat'; import RocketChat from '../../rocketchat';
@ -21,8 +20,6 @@ import { E2E_MESSAGE_TYPE } from '../../encryption/constants';
const removeListener = listener => listener.stop(); const removeListener = listener => listener.stop();
let connectedListener;
let disconnectedListener;
let streamListener; let streamListener;
let subServer; let subServer;
let queue = {}; let queue = {};
@ -255,10 +252,6 @@ const debouncedUpdate = subscription => {
}; };
export default function subscribeRooms() { export default function subscribeRooms() {
const handleConnection = () => {
store.dispatch(roomsRequest());
};
const handleStreamMessageReceived = protectedFunction(async ddpMessage => { const handleStreamMessageReceived = protectedFunction(async ddpMessage => {
const db = database.active; const db = database.active;
@ -388,14 +381,6 @@ export default function subscribeRooms() {
}); });
const stop = () => { const stop = () => {
if (connectedListener) {
connectedListener.then(removeListener);
connectedListener = false;
}
if (disconnectedListener) {
disconnectedListener.then(removeListener);
disconnectedListener = false;
}
if (streamListener) { if (streamListener) {
streamListener.then(removeListener); streamListener.then(removeListener);
streamListener = false; streamListener = false;
@ -407,8 +392,6 @@ export default function subscribeRooms() {
} }
}; };
connectedListener = this.sdk.onStreamData('connected', handleConnection);
// disconnectedListener = this.sdk.onStreamData('close', handleConnection);
streamListener = this.sdk.onStreamData('stream-notify-user', handleStreamMessageReceived); streamListener = this.sdk.onStreamData('stream-notify-user', handleStreamMessageReceived);
try { try {

View File

@ -24,7 +24,7 @@ import { selectServerFailure } from '../actions/server';
import { useSsl } from '../utils/url'; import { useSsl } from '../utils/url';
import EventEmitter from '../utils/events'; import EventEmitter from '../utils/events';
import { updatePermission } from '../actions/permissions'; import { updatePermission } from '../actions/permissions';
import { TEAM_TYPE } from '../definition/ITeam'; import { TEAM_TYPE } from '../definitions/ITeam';
import { updateSettings } from '../actions/settings'; import { updateSettings } from '../actions/settings';
import { compareServerVersion, methods } from './utils'; import { compareServerVersion, methods } from './utils';
import reduxStore from './createStore'; import reduxStore from './createStore';
@ -239,37 +239,34 @@ const RocketChat = {
this.code = null; this.code = null;
} }
this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) }); // The app can't reconnect if reopen interval is 5s while in development
this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server), reopen: __DEV__ ? 20000 : 5000 });
this.getSettings(); this.getSettings();
const sdkConnect = () => this.sdk
this.sdk .connect()
.connect() .then(() => {
.then(() => { console.log('connected');
const { server: currentServer } = reduxStore.getState().server; })
if (user && user.token && server === currentServer) { .catch(err => {
reduxStore.dispatch(loginRequest({ resume: user.token }, logoutOnError)); console.log('connect error', err);
} });
})
.catch(err => {
console.log('connect error', err);
// when `connect` raises an error, we try again in 10 seconds
this.connectTimeout = setTimeout(() => {
if (this.sdk?.client?.host === server) {
sdkConnect();
}
}, 10000);
});
sdkConnect();
this.connectingListener = this.sdk.onStreamData('connecting', () => { this.connectingListener = this.sdk.onStreamData('connecting', () => {
reduxStore.dispatch(connectRequest()); reduxStore.dispatch(connectRequest());
}); });
this.connectedListener = this.sdk.onStreamData('connected', () => { this.connectedListener = this.sdk.onStreamData('connected', () => {
const { connected } = reduxStore.getState().meteor;
if (connected) {
return;
}
reduxStore.dispatch(connectSuccess()); reduxStore.dispatch(connectSuccess());
const { server: currentServer } = reduxStore.getState().server;
const { user } = reduxStore.getState().login;
if (user?.token && server === currentServer) {
reduxStore.dispatch(loginRequest({ resume: user.token }, logoutOnError));
}
}); });
this.closeListener = this.sdk.onStreamData('close', () => { this.closeListener = this.sdk.onStreamData('close', () => {
@ -848,17 +845,21 @@ const RocketChat = {
// RC 3.13.0 // RC 3.13.0
return this.post('teams.removeRoom', { roomId, teamId }); return this.post('teams.removeRoom', { roomId, teamId });
}, },
leaveTeam({ teamName, rooms }) { leaveTeam({ teamId, rooms }) {
// RC 3.13.0 // RC 3.13.0
return this.post('teams.leave', { teamName, rooms }); return this.post('teams.leave', {
teamId,
// RC 4.2.0
...(rooms?.length && { rooms })
});
}, },
removeTeamMember({ teamId, teamName, userId, rooms }) { removeTeamMember({ teamId, userId, rooms }) {
// RC 3.13.0 // RC 3.13.0
return this.post('teams.removeMember', { return this.post('teams.removeMember', {
teamId, teamId,
teamName,
userId, userId,
rooms // RC 4.2.0
...(rooms?.length && { rooms })
}); });
}, },
updateTeamRoom({ roomId, isDefault }) { updateTeamRoom({ roomId, isDefault }) {
@ -1151,10 +1152,6 @@ const RocketChat = {
// RC 0.36.0 // RC 0.36.0
return this.methodCallWrapper('livechat:transfer', transferData); return this.methodCallWrapper('livechat:transfer', transferData);
}, },
getPagesLivechat(rid, offset) {
// RC 2.3.0
return this.sdk.get(`livechat/visitors.pagesVisited/${rid}?count=50&offset=${offset}`);
},
getDepartmentInfo(departmentId) { getDepartmentInfo(departmentId) {
// RC 2.2.0 // RC 2.2.0
return this.sdk.get(`livechat/department/${departmentId}?includeAgents=false`); return this.sdk.get(`livechat/department/${departmentId}?includeAgents=false`);
@ -1533,16 +1530,7 @@ const RocketChat = {
return this.sdk.get(`${this.roomTypeToApiType(type)}.files`, { return this.sdk.get(`${this.roomTypeToApiType(type)}.files`, {
roomId, roomId,
offset, offset,
sort: { uploadedAt: -1 }, sort: { uploadedAt: -1 }
fields: {
name: 1,
description: 1,
size: 1,
type: 1,
uploadedAt: 1,
url: 1,
userId: 1
}
}); });
}, },
getMessages(roomId, type, query, offset) { getMessages(roomId, type, query, offset) {

View File

@ -7,11 +7,12 @@ const MMKV = new MMKVStorage.Loader()
.initialize(); .initialize();
class UserPreferences { class UserPreferences {
private mmkv: MMKVStorage.API;
constructor() { constructor() {
this.mmkv = MMKV; this.mmkv = MMKV;
} }
async getStringAsync(key) { async getStringAsync(key: string) {
try { try {
const value = await this.mmkv.getStringAsync(key); const value = await this.mmkv.getStringAsync(key);
return value; return value;
@ -20,11 +21,11 @@ class UserPreferences {
} }
} }
setStringAsync(key, value) { setStringAsync(key: string, value: string) {
return this.mmkv.setStringAsync(key, value); return this.mmkv.setStringAsync(key, value);
} }
async getBoolAsync(key) { async getBoolAsync(key: string) {
try { try {
const value = await this.mmkv.getBoolAsync(key); const value = await this.mmkv.getBoolAsync(key);
return value; return value;
@ -33,11 +34,11 @@ class UserPreferences {
} }
} }
setBoolAsync(key, value) { setBoolAsync(key: string, value: boolean) {
return this.mmkv.setBoolAsync(key, value); return this.mmkv.setBoolAsync(key, value);
} }
async getMapAsync(key) { async getMapAsync(key: string) {
try { try {
const value = await this.mmkv.getMapAsync(key); const value = await this.mmkv.getMapAsync(key);
return value; return value;
@ -46,11 +47,11 @@ class UserPreferences {
} }
} }
setMapAsync(key, value) { setMapAsync(key: string, value: object) {
return this.mmkv.setMapAsync(key, value); return this.mmkv.setMapAsync(key, value);
} }
removeItem(key) { removeItem(key: string) {
return this.mmkv.removeItem(key); return this.mmkv.removeItem(key);
} }
} }

45
app/navigationTypes.ts Normal file
View File

@ -0,0 +1,45 @@
import { NavigatorScreenParams } from '@react-navigation/core';
import { ISubscription } from './definitions/ISubscription';
import { IServer } from './definitions/IServer';
import { IAttachment } from './definitions/IAttachment';
import { MasterDetailInsideStackParamList } from './stacks/MasterDetailStack/types';
import { OutsideParamList, InsideStackParamList } from './stacks/types';
export type SetUsernameStackParamList = {
SetUsernameView: {
title: string;
};
};
export type StackParamList = {
AuthLoading: undefined;
OutsideStack: NavigatorScreenParams<OutsideParamList>;
InsideStack: NavigatorScreenParams<InsideStackParamList>;
MasterDetailStack: NavigatorScreenParams<MasterDetailInsideStackParamList>;
SetUsernameStack: NavigatorScreenParams<SetUsernameStackParamList>;
};
export type ShareInsideStackParamList = {
ShareListView: undefined;
ShareView: {
attachments: IAttachment[];
isShareView?: boolean;
isShareExtension: boolean;
serverInfo: IServer;
text: string;
room: ISubscription;
thread: any; // TODO: Change
};
SelectServerView: undefined;
};
export type ShareOutsideStackParamList = {
WithoutServersView: undefined;
};
export type ShareAppStackParamList = {
AuthLoading?: undefined;
OutsideStack?: NavigatorScreenParams<ShareOutsideStackParamList>;
InsideStack?: NavigatorScreenParams<ShareInsideStackParamList>;
};

View File

@ -1,50 +0,0 @@
import EJSON from 'ejson';
import store from '../../lib/createStore';
import { deepLinkingOpen } from '../../actions/deepLinking';
import { isFDroidBuild } from '../../constants/environment';
import PushNotification from './push';
export const onNotification = notification => {
if (notification) {
const data = notification.getData();
if (data) {
try {
const { rid, name, sender, type, host, messageType, messageId } = EJSON.parse(data.ejson);
const types = {
c: 'channel',
d: 'direct',
p: 'group',
l: 'channels'
};
let roomName = type === 'd' ? sender.username : name;
if (type === 'l') {
roomName = sender.name;
}
const params = {
host,
rid,
messageId,
path: `${types[type]}/${roomName}`,
isCall: messageType === 'jitsi_call_started'
};
store.dispatch(deepLinkingOpen(params));
} catch (e) {
console.warn(e);
}
}
}
};
export const getDeviceToken = () => PushNotification.getDeviceToken();
export const setBadgeCount = count => PushNotification.setBadgeCount(count);
export const initializePushNotifications = () => {
if (!isFDroidBuild) {
setBadgeCount();
return PushNotification.configure({
onNotification
});
}
};

View File

@ -0,0 +1,57 @@
import EJSON from 'ejson';
import store from '../../lib/createStore';
import { deepLinkingOpen } from '../../actions/deepLinking';
import { isFDroidBuild } from '../../constants/environment';
import PushNotification from './push';
import { INotification, SubscriptionType } from '../../definitions';
interface IEjson {
rid: string;
name: string;
sender: { username: string; name: string };
type: string;
host: string;
messageType: string;
messageId: string;
}
export const onNotification = (notification: INotification): void => {
if (notification) {
try {
const { rid, name, sender, type, host, messageType, messageId }: IEjson = EJSON.parse(notification.ejson);
const types: Record<string, string> = {
c: 'channel',
d: 'direct',
p: 'group',
l: 'channels'
};
let roomName = type === SubscriptionType.DIRECT ? sender.username : name;
if (type === SubscriptionType.OMNICHANNEL) {
roomName = sender.name;
}
const params = {
host,
rid,
messageId,
path: `${types[type]}/${roomName}`,
isCall: messageType === 'jitsi_call_started'
};
// TODO REDUX MIGRATION TO TS
store.dispatch(deepLinkingOpen(params));
} catch (e) {
console.warn(e);
}
}
};
export const getDeviceToken = (): string => PushNotification.getDeviceToken();
export const setBadgeCount = (count?: number): void => PushNotification.setBadgeCount(count);
export const initializePushNotifications = (): Promise<INotification> | undefined => {
if (!isFDroidBuild) {
setBadgeCount();
return PushNotification.configure(onNotification);
}
};

View File

@ -1,32 +0,0 @@
import { NotificationsAndroid, PendingNotifications } from 'react-native-notifications';
class PushNotification {
constructor() {
this.onRegister = null;
this.onNotification = null;
this.deviceToken = null;
NotificationsAndroid.setRegistrationTokenUpdateListener(deviceToken => {
this.deviceToken = deviceToken;
});
NotificationsAndroid.setNotificationOpenedListener(notification => {
this.onNotification(notification);
});
}
getDeviceToken() {
return this.deviceToken;
}
setBadgeCount = () => {};
configure(params) {
this.onRegister = params.onRegister;
this.onNotification = params.onNotification;
NotificationsAndroid.refreshToken();
return PendingNotifications.getInitialNotification();
}
}
export default new PushNotification();

View File

@ -1,61 +0,0 @@
import NotificationsIOS, { NotificationAction, NotificationCategory } from 'react-native-notifications';
import reduxStore from '../../lib/createStore';
import I18n from '../../i18n';
const replyAction = new NotificationAction({
activationMode: 'background',
title: I18n.t('Reply'),
textInput: {
buttonTitle: I18n.t('Reply'),
placeholder: I18n.t('Type_message')
},
identifier: 'REPLY_ACTION'
});
class PushNotification {
constructor() {
this.onRegister = null;
this.onNotification = null;
this.deviceToken = null;
NotificationsIOS.addEventListener('remoteNotificationsRegistered', deviceToken => {
this.deviceToken = deviceToken;
});
NotificationsIOS.addEventListener('notificationOpened', (notification, completion) => {
const { background } = reduxStore.getState().app;
if (background) {
this.onNotification(notification);
}
completion();
});
const actions = [];
actions.push(
new NotificationCategory({
identifier: 'MESSAGE',
actions: [replyAction]
})
);
NotificationsIOS.requestPermissions(actions);
}
getDeviceToken() {
return this.deviceToken;
}
setBadgeCount = (count = 0) => {
NotificationsIOS.setBadgesCount(count);
};
async configure(params) {
this.onRegister = params.onRegister;
this.onNotification = params.onNotification;
const initial = await NotificationsIOS.getInitialNotification();
// NotificationsIOS.consumeBackgroundQueue();
return Promise.resolve(initial);
}
}
export default new PushNotification();

View File

@ -0,0 +1,63 @@
// @ts-ignore
// TODO BUMP LIB VERSION
import NotificationsIOS, { NotificationAction, NotificationCategory, Notification } from 'react-native-notifications';
import reduxStore from '../../lib/createStore';
import I18n from '../../i18n';
import { INotification } from '../../definitions/INotification';
class PushNotification {
onNotification: (notification: Notification) => void;
deviceToken: string;
constructor() {
this.onNotification = () => {};
this.deviceToken = '';
NotificationsIOS.addEventListener('remoteNotificationsRegistered', (deviceToken: string) => {
this.deviceToken = deviceToken;
});
NotificationsIOS.addEventListener('notificationOpened', (notification: Notification, completion: () => void) => {
// TODO REDUX MIGRATION TO TS
const { background } = reduxStore.getState().app;
if (background) {
this.onNotification(notification?.getData());
}
completion();
});
const actions = [
new NotificationCategory({
identifier: 'MESSAGE',
actions: [
new NotificationAction({
activationMode: 'background',
title: I18n.t('Reply'),
textInput: {
buttonTitle: I18n.t('Reply'),
placeholder: I18n.t('Type_message')
},
identifier: 'REPLY_ACTION'
})
]
})
];
NotificationsIOS.requestPermissions(actions);
}
getDeviceToken() {
return this.deviceToken;
}
setBadgeCount = (count = 0) => {
NotificationsIOS.setBadgesCount(count);
};
async configure(onNotification: (notification: INotification) => void) {
this.onNotification = onNotification;
const initial = await NotificationsIOS.getInitialNotification();
return Promise.resolve(initial);
}
}
export default new PushNotification();

View File

@ -0,0 +1,36 @@
// @ts-ignore
// TODO BUMP LIB VERSION
import { NotificationsAndroid, PendingNotifications, Notification } from 'react-native-notifications';
import { INotification } from '../../definitions/INotification';
class PushNotification {
onNotification: (notification: Notification) => void;
deviceToken: string;
constructor() {
this.onNotification = () => {};
this.deviceToken = '';
NotificationsAndroid.setRegistrationTokenUpdateListener((deviceToken: string) => {
this.deviceToken = deviceToken;
});
NotificationsAndroid.setNotificationOpenedListener((notification: Notification) => {
this.onNotification(notification?.getData());
});
}
getDeviceToken() {
return this.deviceToken;
}
setBadgeCount = (_?: number) => {};
configure(onNotification: (notification: INotification) => void) {
this.onNotification = onNotification;
NotificationsAndroid.refreshToken();
return PendingNotifications.getInitialNotification();
}
}
export default new PushNotification();

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Text, View } from 'react-native'; import { Text, View, ViewStyle } from 'react-native';
import Touch from '../../utils/touch'; import Touch from '../../utils/touch';
import Avatar from '../../containers/Avatar'; import Avatar from '../../containers/Avatar';
@ -10,7 +10,7 @@ import { themes } from '../../constants/colors';
export { ROW_HEIGHT }; export { ROW_HEIGHT };
interface IDirectoryItemLabel { interface IDirectoryItemLabel {
text: string; text?: string;
theme: string; theme: string;
} }
@ -21,9 +21,9 @@ interface IDirectoryItem {
type: string; type: string;
onPress(): void; onPress(): void;
testID: string; testID: string;
style: any; style?: ViewStyle;
rightLabel: string; rightLabel?: string;
rid: string; rid?: string;
theme: string; theme: string;
teamMain?: boolean; teamMain?: boolean;
} }
@ -32,7 +32,7 @@ const DirectoryItemLabel = React.memo(({ text, theme }: IDirectoryItemLabel) =>
if (!text) { if (!text) {
return null; return null;
} }
return <Text style={[styles.directoryItemLabel, { color: themes[theme!].auxiliaryText }]}>{text}</Text>; return <Text style={[styles.directoryItemLabel, { color: themes[theme].auxiliaryText }]}>{text}</Text>;
}); });
const DirectoryItem = ({ const DirectoryItem = ({

View File

@ -4,7 +4,7 @@ import { KeyboardAwareScrollView, KeyboardAwareScrollViewProps } from '@codler/r
import scrollPersistTaps from '../utils/scrollPersistTaps'; import scrollPersistTaps from '../utils/scrollPersistTaps';
interface IKeyboardViewProps extends KeyboardAwareScrollViewProps { interface IKeyboardViewProps extends KeyboardAwareScrollViewProps {
keyboardVerticalOffset: number; keyboardVerticalOffset?: number;
scrollEnabled?: boolean; scrollEnabled?: boolean;
children: React.ReactNode; children: React.ReactNode;
} }

View File

@ -5,7 +5,7 @@ import { RectButton } from 'react-native-gesture-handler';
import { isRTL } from '../../i18n'; import { isRTL } from '../../i18n';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { DISPLAY_MODE_CONDENSED } from '../../constants/constantDisplayMode'; import { DisplayMode } from '../../constants/constantDisplayMode';
import styles, { ACTION_WIDTH, LONG_SWIPE, ROW_HEIGHT_CONDENSED } from './styles'; import styles, { ACTION_WIDTH, LONG_SWIPE, ROW_HEIGHT_CONDENSED } from './styles';
interface ILeftActions { interface ILeftActions {
@ -40,7 +40,7 @@ export const LeftActions = React.memo(({ theme, transX, isRead, width, onToggleR
reverse reverse
); );
const isCondensed = displayMode === DISPLAY_MODE_CONDENSED; const isCondensed = displayMode === DisplayMode.Condensed;
const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null; const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null;
return ( return (
@ -87,7 +87,7 @@ export const RightActions = React.memo(
reverse reverse
); );
const isCondensed = displayMode === DISPLAY_MODE_CONDENSED; const isCondensed = displayMode === DisplayMode.Condensed;
const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null; const viewHeight = isCondensed ? { height: ROW_HEIGHT_CONDENSED } : null;
return ( return (

View File

@ -3,7 +3,7 @@ import { View } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Avatar from '../../containers/Avatar'; import Avatar from '../../containers/Avatar';
import { DISPLAY_MODE_CONDENSED, DISPLAY_MODE_EXPANDED } from '../../constants/constantDisplayMode'; import { DisplayMode } from '../../constants/constantDisplayMode';
import TypeIcon from './TypeIcon'; import TypeIcon from './TypeIcon';
import styles from './styles'; import styles from './styles';
@ -22,11 +22,11 @@ const IconOrAvatar = ({
}) => { }) => {
if (showAvatar) { if (showAvatar) {
return ( return (
<Avatar text={avatar} size={displayMode === DISPLAY_MODE_CONDENSED ? 36 : 48} type={type} style={styles.avatar} rid={rid} /> <Avatar text={avatar} size={displayMode === DisplayMode.Condensed ? 36 : 48} type={type} style={styles.avatar} rid={rid} />
); );
} }
if (displayMode === DISPLAY_MODE_EXPANDED && showLastMessage) { if (displayMode === DisplayMode.Expanded && showLastMessage) {
return ( return (
<View style={styles.typeIcon}> <View style={styles.typeIcon}>
<TypeIcon <TypeIcon

View File

@ -11,7 +11,7 @@ import UpdatedAt from './UpdatedAt';
import Touchable from './Touchable'; import Touchable from './Touchable';
import Tag from './Tag'; import Tag from './Tag';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { DISPLAY_MODE_EXPANDED } from '../../constants/constantDisplayMode'; import { DisplayMode } from '../../constants/constantDisplayMode';
interface IRoomItem { interface IRoomItem {
rid: string; rid: string;
@ -132,7 +132,7 @@ const RoomItem = ({
displayMode={displayMode} displayMode={displayMode}
showAvatar={showAvatar} showAvatar={showAvatar}
showLastMessage={showLastMessage}> showLastMessage={showLastMessage}>
{showLastMessage && displayMode === DISPLAY_MODE_EXPANDED ? ( {showLastMessage && displayMode === DisplayMode.Expanded ? (
<> <>
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
{showAvatar ? ( {showAvatar ? (

View File

@ -2,7 +2,7 @@ import React from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { DISPLAY_MODE_CONDENSED } from '../../constants/constantDisplayMode'; import { DisplayMode } from '../../constants/constantDisplayMode';
import IconOrAvatar from './IconOrAvatar'; import IconOrAvatar from './IconOrAvatar';
import styles from './styles'; import styles from './styles';
@ -25,7 +25,7 @@ interface IWrapper {
const Wrapper = ({ accessibilityLabel, theme, children, displayMode, ...props }: IWrapper) => ( const Wrapper = ({ accessibilityLabel, theme, children, displayMode, ...props }: IWrapper) => (
<View <View
style={[styles.container, displayMode === DISPLAY_MODE_CONDENSED && styles.containerCondensed]} style={[styles.container, displayMode === DisplayMode.Condensed && styles.containerCondensed]}
accessibilityLabel={accessibilityLabel}> accessibilityLabel={accessibilityLabel}>
<IconOrAvatar theme={theme} displayMode={displayMode} {...props} /> <IconOrAvatar theme={theme} displayMode={displayMode} {...props} />
<View <View
@ -34,7 +34,7 @@ const Wrapper = ({ accessibilityLabel, theme, children, displayMode, ...props }:
{ {
borderColor: themes[theme].separatorColor borderColor: themes[theme].separatorColor
}, },
displayMode === DISPLAY_MODE_CONDENSED && styles.condensedPaddingVertical displayMode === DisplayMode.Condensed && styles.condensedPaddingVertical
]}> ]}>
{children} {children}
</View> </View>

View File

@ -46,7 +46,7 @@ interface IUserItem {
testID: string; testID: string;
onLongPress?: () => void; onLongPress?: () => void;
style?: StyleProp<ViewStyle>; style?: StyleProp<ViewStyle>;
icon: string; icon?: string | null;
theme: string; theme: string;
} }

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