Chore: Start Typescript migration (#3279)

This commit is contained in:
Alex Junior 2021-09-13 17:41:05 -03:00 committed by GitHub
parent 9c526b753e
commit 69a67ea998
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
600 changed files with 37240 additions and 27134 deletions

View File

@ -340,7 +340,7 @@ jobs:
<<: *defaults <<: *defaults
docker: docker:
- image: circleci/node:15 - image: circleci/node:15
resource_class: large
environment: environment:
CODECOV_TOKEN: caa771ab-3d45-4756-8e2a-e1f25996fef6 CODECOV_TOKEN: caa771ab-3d45-4756-8e2a-e1f25996fef6
@ -376,6 +376,7 @@ jobs:
environment: environment:
<<: *android-env <<: *android-env
<<: *bash-env <<: *bash-env
resource_class: large
steps: steps:
- android-build - android-build
@ -386,6 +387,7 @@ jobs:
environment: environment:
<<: *android-env <<: *android-env
<<: *bash-env <<: *bash-env
resource_class: large
steps: steps:
- android-build - android-build

View File

@ -1,161 +1,156 @@
module.exports = { module.exports = {
"settings": { settings: {
"import/resolver": { 'import/resolver': {
"node": { node: {
"extensions": [".js", ".ios.js", ".android.js", ".native.js", ".tsx"] extensions: ['.js', '.ios.js', '.android.js', '.native.js', '.ts', '.tsx']
} }
}
},
"parser": "@babel/eslint-parser",
"extends": "airbnb",
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2017,
"ecmaFeatures": {
"experimentalObjectRestSpread" : true,
"jsx": true,
"legacyDecorators": true
} }
}, },
"plugins": [ parser: '@babel/eslint-parser',
"react", extends: ['@rocket.chat/eslint-config', 'prettier'],
"jsx-a11y", parserOptions: {
"import", sourceType: 'module',
"react-native", ecmaVersion: 2017,
"@babel" ecmaFeatures: {
], experimentalObjectRestSpread: true,
"env": { jsx: true,
"browser": true, legacyDecorators: true
"commonjs": true, }
"es6": true,
"node": true,
"jquery": true,
"mocha": true
}, },
"rules": { plugins: ['react', 'jsx-a11y', 'import', 'react-native', '@babel'],
"react/jsx-filename-extension": [1, { env: {
"extensions": [".js", ".jsx"] browser: true,
}], commonjs: true,
"react/require-default-props": [0], es6: true,
"react/no-unused-prop-types": [2, { node: true,
"skipShapeProps": true jquery: true,
}], mocha: true
"react/no-did-mount-set-state": 0,
"react/no-multi-comp": [0],
"react/jsx-indent": [2, "tab"],
"react/jsx-indent-props": [2, "tab"],
"react/forbid-prop-types": 0,
"jsx-quotes": [2, "prefer-single"],
"jsx-a11y/href-no-hash": 0,
"jsx-a11y/aria-role": 0,
"import/prefer-default-export": 0,
"import/no-cycle": 0,
"camelcase": 0,
"no-underscore-dangle": 0,
"no-return-assign": 0,
"no-param-reassign": 0,
"no-tabs": 0,
"no-multi-spaces": 2,
"no-eval": 2,
"no-extend-native": 2,
"no-multi-str": 2,
"no-use-before-define": 2,
"no-const-assign": 2,
"no-cond-assign": 2,
"no-constant-condition": 2,
"no-control-regex": 2,
"no-debugger": 2,
"no-delete-var": 2,
"no-dupe-keys": 2,
"no-dupe-args": 2,
"no-dupe-class-members": 2,
"no-duplicate-case": 2,
"no-else-return": [0, {allowElseIf: true}],
"no-empty": 2,
"no-empty-character-class": 2,
"no-ex-assign": 2,
"no-extra-boolean-cast": 2,
"no-extra-semi": 2,
"no-fallthrough": 2,
"no-func-assign": 2,
"no-inner-declarations": [2, "functions"],
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-mixed-spaces-and-tabs": 2,
"no-sparse-arrays": 2,
"no-negated-in-lhs": 2,
"no-obj-calls": 2,
"no-octal": 2,
"no-redeclare": 2,
"no-regex-spaces": 2,
"no-undef": 2,
"no-unreachable": 2,
"no-unused-expressions": 0,
"no-unused-vars": [2, {
"vars": "all",
"args": "after-used"
}],
"max-len": 0,
"react/jsx-uses-vars": 2,
"no-void": 2,
"no-var": 2,
"one-var": [2, "never"],
"no-lonely-if": 2,
"no-trailing-spaces": 2,
"complexity": [1, 31],
"space-in-parens": [2, "never"],
"space-before-function-paren": [2, "never"],
"space-before-blocks": [2, "always"],
"indent": [2, "tab", {"SwitchCase": 1}],
"eol-last": [2, "always"],
"comma-dangle": [2, "never"],
"keyword-spacing": 2,
"block-spacing": 2,
"brace-style": [2, "1tbs", { "allowSingleLine": true }],
"computed-property-spacing": 2,
"comma-spacing": 2,
"comma-style": 2,
"guard-for-in": 2,
"wrap-iife": 2,
"block-scoped-var": 2,
"curly": [2, "all"],
"eqeqeq": [2, "allow-null"],
"new-cap": [2],
"use-isnan": 2,
"valid-typeof": 2,
"linebreak-style": 0,
"prefer-template": 2,
"template-curly-spacing": [2, "always"],
"quotes": [2, "single"],
"semi": [2, "always"],
"prefer-const": 2,
"object-shorthand": 2,
"consistent-return": 0,
"global-require": "off",
"react-native/no-unused-styles": 2,
"react/jsx-one-expression-per-line": 0,
"require-await": 2,
"func-names": 0,
"react/sort-comp": ["error", {
"order": [
"static-variables",
"static-methods",
"lifecycle",
"everything-else",
"render"
]
}],
"react/static-property-placement": [0],
"arrow-parens": ["error", "as-needed", { requireForBlockBody: true }],
"react/jsx-props-no-spreading": [1],
"react/jsx-curly-newline": [0],
"react/state-in-constructor": [0],
"no-async-promise-executor": [0],
"max-classes-per-file": [0],
"no-multiple-empty-lines": [0]
}, },
"globals": { rules: {
"__DEV__": true 'import/extensions': [
'error',
'ignorePackages',
{
js: 'warning',
jsx: 'warning',
ts: 'warning',
tsx: 'warning'
}
],
'react/jsx-filename-extension': [
1,
{
extensions: ['.js', '.jsx', '.ts', '.tsx']
}
],
'react/require-default-props': [0],
'ordered-imports': [0],
'react/no-did-mount-set-state': 0,
'react/no-multi-comp': [0],
'react/jsx-indent-props': [2, 'tab'],
'jsx-quotes': [2, 'prefer-single'],
'jsx-a11y/href-no-hash': 0,
'jsx-a11y/aria-role': 0,
'import/prefer-default-export': 0,
'import/no-cycle': 0,
'import/order': [
'error',
{
'newlines-between': 'ignore'
}
],
camelcase: 0,
'no-underscore-dangle': 0,
'no-return-assign': 0,
'no-param-reassign': 0,
'no-tabs': 0,
'no-multi-spaces': 2,
'no-eval': 2,
'no-extend-native': 2,
'no-multi-str': 2,
'no-use-before-define': 2,
'no-const-assign': 2,
'no-cond-assign': 2,
'no-constant-condition': 2,
'no-control-regex': 2,
'no-debugger': 2,
'no-delete-var': 2,
'no-dupe-keys': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
'no-duplicate-case': 2,
'no-else-return': [0, { allowElseIf: true }],
'no-empty': 2,
'no-empty-character-class': 2,
'no-ex-assign': 2,
'no-extra-boolean-cast': 2,
'no-extra-semi': 2,
'no-fallthrough': 2,
'no-func-assign': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-mixed-spaces-and-tabs': 1,
'no-sparse-arrays': 2,
'no-negated-in-lhs': 2,
'no-obj-calls': 2,
'no-octal': 2,
'no-redeclare': 2,
'no-regex-spaces': 2,
'no-undef': 2,
'no-unreachable': 2,
'no-unused-expressions': 0,
'no-unused-vars': 'off',
'max-len': 0,
'react/jsx-uses-vars': 2,
'no-void': 2,
'no-var': 2,
'one-var': [2, 'never'],
'no-lonely-if': 2,
'no-trailing-spaces': 2,
complexity: [1, 31],
'space-in-parens': [2, 'never'],
'space-before-blocks': [2, 'always'],
indent: 'off',
'eol-last': [2, 'always'],
'comma-dangle': [2, 'never'],
'keyword-spacing': 2,
'block-spacing': 2,
'brace-style': [2, '1tbs', { allowSingleLine: true }],
'computed-property-spacing': 2,
'comma-spacing': 2,
'comma-style': 2,
'guard-for-in': 2,
'wrap-iife': 2,
'block-scoped-var': 2,
curly: [2, 'all'],
eqeqeq: [2, 'allow-null'],
'new-cap': 'off',
'use-isnan': 2,
'valid-typeof': 2,
'linebreak-style': 0,
'prefer-template': 2,
quotes: [1, 'single'],
semi: [2, 'always'],
'prefer-const': 2,
'object-shorthand': 2,
'consistent-return': 0,
'global-require': 'off',
'react-native/no-unused-styles': 2,
'react/jsx-one-expression-per-line': 0,
'require-await': 2,
'func-names': 0,
'react/static-property-placement': [0],
'arrow-parens': ['warn', 'as-needed', { requireForBlockBody: true }],
'react/jsx-curly-newline': [0],
'react/state-in-constructor': [0],
'no-async-promise-executor': [0],
'max-classes-per-file': [0],
'no-multiple-empty-lines': [0],
'no-sequences': 'off'
},
globals: {
__DEV__: true
}, },
overrides: [ overrides: [
{ {
@ -173,6 +168,85 @@ module.exports = {
'no-await-in-loop': 0, 'no-await-in-loop': 0,
'no-restricted-syntax': 0 'no-restricted-syntax': 0
} }
},
{
files: ['**/*.ts', '**/*.tsx'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'@rocket.chat/eslint-config',
'prettier'
],
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
ecmaVersion: 2018,
warnOnUnsupportedTypeScriptVersion: false,
ecmaFeatures: {
experimentalObjectRestSpread: true,
legacyDecorators: true
}
},
plugins: ['react', '@typescript-eslint'],
rules: {
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': [0],
'@typescript-eslint/ban-types': [0],
'func-call-spacing': 'off',
'jsx-quotes': ['error', 'prefer-single'],
indent: 'off',
'comma-dangle': [2, 'never'],
'no-return-assign': 0,
'no-dupe-class-members': 'off',
'no-extra-parens': 'off',
'no-spaced-func': 'off',
'no-unused-vars': 'off',
'no-useless-constructor': 'off',
'no-use-before-define': 'off',
'react/jsx-uses-react': 'error',
'react/jsx-uses-vars': 'error',
'react/jsx-no-undef': 'error',
'react/jsx-fragments': ['error', 'syntax'],
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/indent': [
'warn',
'tab',
{
SwitchCase: 1
}
],
'@typescript-eslint/no-extra-parens': [
'warn',
'all',
{
conditionalAssign: true,
nestedBinaryExpressions: false,
returnAssign: true,
ignoreJSX: 'all',
enforceForArrowConditionals: false
}
],
'@typescript-eslint/no-dupe-class-members': 'error',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
ignoreRestSiblings: true
}
],
'new-cap': 'off'
},
globals: {
JSX: true
},
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.ts', '.tsx']
}
}
}
} }
] ]
}; };

25
.prettierignore Normal file
View File

@ -0,0 +1,25 @@
.circleci/
.github/
.husky
build/
node_modules/
coverage/
e2e/docker/
artifacts/
android/
ios/
patches/
scripts/
.bettercodehub.yml
.buckconfig
.gitattributes
.gitignore
.snyk
.watchmanconfig
CONTRIBUTING.md
README.md
SECURITY.md
npm-debug.log
yarn-error.log

10
.prettierrc.js Normal file
View File

@ -0,0 +1,10 @@
module.exports = {
bracketSpacing: true,
jsxBracketSameLine: true,
singleQuote: true,
jsxSingleQuote: true,
trailingComma: 'none',
printWidth: 130,
useTabs: true,
arrowParens: 'avoid'
};

View File

@ -54,6 +54,18 @@ To check for lint issues on your code, run this on your terminal:
yarn lint yarn lint
``` ```
## Code formatting
We use [Prettier](https://prettier.io) to format the code style in our project. We have a pre-commit hook enforcing commits to follow our style guides.
To fix your code formatting issues, run this on your terminal:
```sh
yarn prettier
```
[Check this link](https://prettier.io/docs/en/editors.html) to see how to integrate Prettier with your preferred code editor, and run Prettier when save your file for example.
## Tests ## Tests
It's always important to ensure everything is working properly and that's why tests are great. We have unit and e2e tests on this project. It's always important to ensure everything is working properly and that's why tests are great. We have unit and e2e tests on this project.

View File

@ -899,27 +899,22 @@ exports[`Storyshots BackgroundContainer black theme - loading 1`] = `
</View> </View>
<ActivityIndicator <ActivityIndicator
animating={true} animating={true}
color="#999999" color="#f9f9f9"
hidesWhenStopped={true} hidesWhenStopped={true}
size="small" size="small"
style={ style={
Array [ Object {
Object { "backgroundColor": "transparent",
"backgroundColor": "transparent", "fontFamily": "System",
"fontFamily": "System", "fontSize": 16,
"fontSize": 16, "fontWeight": "400",
"fontWeight": "400", "left": 0,
"left": 0, "paddingHorizontal": 24,
"paddingHorizontal": 24, "position": "absolute",
"position": "absolute", "right": 0,
"right": 0, "textAlign": "center",
"textAlign": "center", "top": 60,
"top": 60, }
},
Object {
"color": "#f9f9f9",
},
]
} }
/> />
</View> </View>
@ -1037,27 +1032,22 @@ exports[`Storyshots BackgroundContainer dark theme - loading 1`] = `
</View> </View>
<ActivityIndicator <ActivityIndicator
animating={true} animating={true}
color="#999999" color="#f9f9f9"
hidesWhenStopped={true} hidesWhenStopped={true}
size="small" size="small"
style={ style={
Array [ Object {
Object { "backgroundColor": "transparent",
"backgroundColor": "transparent", "fontFamily": "System",
"fontFamily": "System", "fontSize": 16,
"fontSize": 16, "fontWeight": "400",
"fontWeight": "400", "left": 0,
"left": 0, "paddingHorizontal": 24,
"paddingHorizontal": 24, "position": "absolute",
"position": "absolute", "right": 0,
"right": 0, "textAlign": "center",
"textAlign": "center", "top": 60,
"top": 60, }
},
Object {
"color": "#f9f9f9",
},
]
} }
/> />
</View> </View>
@ -1175,27 +1165,22 @@ exports[`Storyshots BackgroundContainer loading 1`] = `
</View> </View>
<ActivityIndicator <ActivityIndicator
animating={true} animating={true}
color="#999999" color="#6C727A"
hidesWhenStopped={true} hidesWhenStopped={true}
size="small" size="small"
style={ style={
Array [ Object {
Object { "backgroundColor": "transparent",
"backgroundColor": "transparent", "fontFamily": "System",
"fontFamily": "System", "fontSize": 16,
"fontSize": 16, "fontWeight": "400",
"fontWeight": "400", "left": 0,
"left": 0, "paddingHorizontal": 24,
"paddingHorizontal": 24, "position": "absolute",
"position": "absolute", "right": 0,
"right": 0, "textAlign": "center",
"textAlign": "center", "top": 60,
"top": 60, }
},
Object {
"color": "#6C727A",
},
]
} }
/> />
</View> </View>
@ -4693,7 +4678,7 @@ exports[`Storyshots List pressable 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 92, "height": 92,
}, },
@ -6211,7 +6196,7 @@ exports[`Storyshots List with bigger font 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 69, "height": 69,
}, },
@ -6625,7 +6610,7 @@ exports[`Storyshots List with bigger font 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 69, "height": 69,
}, },
@ -7080,7 +7065,7 @@ exports[`Storyshots List with black theme 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 92, "height": 92,
}, },
@ -7494,7 +7479,7 @@ exports[`Storyshots List with black theme 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 92, "height": 92,
}, },
@ -7972,7 +7957,7 @@ exports[`Storyshots List with custom colors 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 92, "height": 92,
}, },
@ -8129,7 +8114,7 @@ exports[`Storyshots List with dark theme 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 92, "height": 92,
}, },
@ -8543,7 +8528,7 @@ exports[`Storyshots List with dark theme 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 92, "height": 92,
}, },
@ -10410,7 +10395,7 @@ exports[`Storyshots List with small font 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 36.800000000000004, "height": 36.800000000000004,
}, },
@ -10824,7 +10809,7 @@ exports[`Storyshots List with small font 1`] = `
"justifyContent": "center", "justifyContent": "center",
"paddingHorizontal": 12, "paddingHorizontal": 12,
}, },
false, undefined,
Object { Object {
"height": 36.800000000000004, "height": 36.800000000000004,
}, },

View File

@ -25,7 +25,7 @@ import com.android.build.OutputFile
* bundleAssetName: "index.android.bundle", * bundleAssetName: "index.android.bundle",
* *
* // the entry file for bundle generation. If none specified and * // the entry file for bundle generation. If none specified and
* // "index.android.js" exists, it will be used. Otherwise "index.js" is * // "index.android.js" exists, it will be used. Otherwise "index.tsx" is
* // default. Can be overridden with ENTRY_FILE environment variable. * // default. Can be overridden with ENTRY_FILE environment variable.
* entryFile: "index.android.js", * entryFile: "index.android.js",
* *

View File

@ -1,5 +1,5 @@
{ {
"name": "RocketChatRN", "name": "RocketChatRN",
"share": "ShareRocketChatRN", "share": "ShareRocketChatRN",
"displayName": "RocketChatRN" "displayName": "RocketChatRN"
} }

View File

@ -1,21 +1,15 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { NavigationContainer } from '@react-navigation/native'; 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 Navigation from './lib/Navigation'; import Navigation from './lib/Navigation';
import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation'; import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation';
import { import { ROOT_INSIDE, ROOT_LOADING, ROOT_NEW_SERVER, ROOT_OUTSIDE, ROOT_SET_USERNAME } from './actions/app';
ROOT_LOADING, ROOT_OUTSIDE, ROOT_NEW_SERVER, ROOT_INSIDE, ROOT_SET_USERNAME
} from './actions/app';
// Stacks // Stacks
import AuthLoadingView from './views/AuthLoadingView'; import AuthLoadingView from './views/AuthLoadingView';
// SetUsername Stack // SetUsername Stack
import SetUsernameView from './views/SetUsernameView'; import SetUsernameView from './views/SetUsernameView';
import OutsideStack from './stacks/OutsideStack'; import OutsideStack from './stacks/OutsideStack';
import InsideStack from './stacks/InsideStack'; import InsideStack from './stacks/InsideStack';
import MasterDetailStack from './stacks/MasterDetailStack'; import MasterDetailStack from './stacks/MasterDetailStack';
@ -26,16 +20,13 @@ import { setCurrentScreen } from './utils/log';
const SetUsername = createStackNavigator(); const SetUsername = createStackNavigator();
const SetUsernameStack = () => ( const SetUsernameStack = () => (
<SetUsername.Navigator screenOptions={defaultHeader}> <SetUsername.Navigator screenOptions={defaultHeader}>
<SetUsername.Screen <SetUsername.Screen name='SetUsernameView' component={SetUsernameView} />
name='SetUsernameView'
component={SetUsernameView}
/>
</SetUsername.Navigator> </SetUsername.Navigator>
); );
// App // App
const Stack = createStackNavigator(); const Stack = createStackNavigator();
const App = React.memo(({ root, isMasterDetail }) => { const App = React.memo(({ root, isMasterDetail }: { root: string; isMasterDetail: boolean }) => {
if (!root) { if (!root) {
return null; return null;
} }
@ -54,61 +45,34 @@ const App = React.memo(({ root, isMasterDetail }) => {
<NavigationContainer <NavigationContainer
theme={navTheme} theme={navTheme}
ref={Navigation.navigationRef} ref={Navigation.navigationRef}
onStateChange={(state) => { onStateChange={state => {
const previousRouteName = Navigation.routeNameRef.current; const previousRouteName = Navigation.routeNameRef.current;
const currentRouteName = getActiveRouteName(state); const currentRouteName = getActiveRouteName(state);
if (previousRouteName !== currentRouteName) { if (previousRouteName !== currentRouteName) {
setCurrentScreen(currentRouteName); setCurrentScreen(currentRouteName);
} }
Navigation.routeNameRef.current = currentRouteName; Navigation.routeNameRef.current = currentRouteName;
}} }}>
>
<Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}> <Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}>
<> <>
{root === ROOT_LOADING ? ( {root === ROOT_LOADING ? <Stack.Screen name='AuthLoading' component={AuthLoadingView} /> : null}
<Stack.Screen
name='AuthLoading'
component={AuthLoadingView}
/>
) : null}
{root === ROOT_OUTSIDE || root === ROOT_NEW_SERVER ? ( {root === ROOT_OUTSIDE || root === ROOT_NEW_SERVER ? (
<Stack.Screen <Stack.Screen name='OutsideStack' component={OutsideStack} />
name='OutsideStack'
component={OutsideStack}
/>
) : null} ) : null}
{root === ROOT_INSIDE && isMasterDetail ? ( {root === ROOT_INSIDE && isMasterDetail ? (
<Stack.Screen <Stack.Screen name='MasterDetailStack' component={MasterDetailStack} />
name='MasterDetailStack'
component={MasterDetailStack}
/>
) : null}
{root === ROOT_INSIDE && !isMasterDetail ? (
<Stack.Screen
name='InsideStack'
component={InsideStack}
/>
) : null}
{root === ROOT_SET_USERNAME ? (
<Stack.Screen
name='SetUsernameStack'
component={SetUsernameStack}
/>
) : null} ) : null}
{root === ROOT_INSIDE && !isMasterDetail ? <Stack.Screen name='InsideStack' component={InsideStack} /> : null}
{root === ROOT_SET_USERNAME ? <Stack.Screen name='SetUsernameStack' component={SetUsernameStack} /> : null}
</> </>
</Stack.Navigator> </Stack.Navigator>
</NavigationContainer> </NavigationContainer>
); );
}); });
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
root: state.app.root, root: state.app.root,
isMasterDetail: state.app.isMasterDetail isMasterDetail: state.app.isMasterDetail
}); });
App.propTypes = {
root: PropTypes.string,
isMasterDetail: PropTypes.bool
};
const AppContainer = connect(mapStateToProps)(App); const AppContainer = connect(mapStateToProps)(App);
export default AppContainer; export default AppContainer;

View File

@ -2,21 +2,16 @@
import { NativeModules } from 'react-native'; import { NativeModules } from 'react-native';
import Reactotron from 'reactotron-react-native'; import Reactotron from 'reactotron-react-native';
import { reactotronRedux } from 'reactotron-redux'; import { reactotronRedux } from 'reactotron-redux';
import sagaPlugin from 'reactotron-redux-saga' import sagaPlugin from 'reactotron-redux-saga';
if (__DEV__) { if (__DEV__) {
const scriptURL = NativeModules.SourceCode.scriptURL; const scriptURL = NativeModules.SourceCode.scriptURL;
const scriptHostname = scriptURL.split('://')[1].split(':')[0]; const scriptHostname = scriptURL.split('://')[1].split(':')[0];
Reactotron Reactotron.configure({ host: scriptHostname }).useReactNative().use(reactotronRedux()).use(sagaPlugin()).connect();
.configure({ host: scriptHostname }) // Running on android device
.useReactNative() // $ adb reverse tcp:9090 tcp:9090
.use(reactotronRedux()) Reactotron.clear();
.use(sagaPlugin()) console.warn = Reactotron.log;
.connect(); console.log = Reactotron.log;
// Running on android device console.disableYellowBox = true;
// $ adb reverse tcp:9090 tcp:9090
Reactotron.clear();
console.warn = Reactotron.log;
console.log = Reactotron.log;
console.disableYellowBox = true;
} }

View File

@ -4,23 +4,13 @@ const FAILURE = 'FAILURE';
const defaultTypes = [REQUEST, SUCCESS, FAILURE]; const defaultTypes = [REQUEST, SUCCESS, FAILURE];
function createRequestTypes(base, types = defaultTypes) { function createRequestTypes(base, types = defaultTypes) {
const res = {}; const res = {};
types.forEach(type => (res[type] = `${ base }_${ type }`)); types.forEach(type => (res[type] = `${base}_${type}`));
return res; return res;
} }
// Login events // Login events
export const LOGIN = createRequestTypes('LOGIN', [ export const LOGIN = createRequestTypes('LOGIN', [...defaultTypes, 'SET_SERVICES', 'SET_PREFERENCE', 'SET_LOCAL_AUTHENTICATED']);
...defaultTypes, export const SHARE = createRequestTypes('SHARE', ['SELECT_SERVER', 'SET_USER', 'SET_SETTINGS', 'SET_SERVER_INFO']);
'SET_SERVICES',
'SET_PREFERENCE',
'SET_LOCAL_AUTHENTICATED'
]);
export const SHARE = createRequestTypes('SHARE', [
'SELECT_SERVER',
'SET_USER',
'SET_SETTINGS',
'SET_SERVER_INFO'
]);
export const USER = createRequestTypes('USER', ['SET']); export const USER = createRequestTypes('USER', ['SET']);
export const ROOMS = createRequestTypes('ROOMS', [ export const ROOMS = createRequestTypes('ROOMS', [
...defaultTypes, ...defaultTypes,
@ -33,8 +23,24 @@ export const ROOMS = createRequestTypes('ROOMS', [
'OPEN_SEARCH_HEADER', 'OPEN_SEARCH_HEADER',
'CLOSE_SEARCH_HEADER' 'CLOSE_SEARCH_HEADER'
]); ]);
export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'CLOSE', 'FORWARD', 'USER_TYPING']); export const ROOM = createRequestTypes('ROOM', [
export const INQUIRY = createRequestTypes('INQUIRY', [...defaultTypes, 'SET_ENABLED', 'RESET', 'QUEUE_ADD', 'QUEUE_UPDATE', 'QUEUE_REMOVE']); 'SUBSCRIBE',
'UNSUBSCRIBE',
'LEAVE',
'DELETE',
'REMOVED',
'CLOSE',
'FORWARD',
'USER_TYPING'
]);
export const INQUIRY = createRequestTypes('INQUIRY', [
...defaultTypes,
'SET_ENABLED',
'RESET',
'QUEUE_ADD',
'QUEUE_UPDATE',
'QUEUE_REMOVE'
]);
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS', 'SET_MASTER_DETAIL']); export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS', 'SET_MASTER_DETAIL']);
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']); export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]); export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);

View File

@ -32,7 +32,6 @@ export function inviteLinksClear() {
}; };
} }
export function inviteLinksCreate(rid) { export function inviteLinksCreate(rid) {
return { return {
type: types.INVITE_LINKS.CREATE, type: types.INVITE_LINKS.CREATE,

View File

@ -1,6 +1,5 @@
import * as types from './actionsTypes'; import * as types from './actionsTypes';
export function roomsRequest(params = { allData: false }) { export function roomsRequest(params = { allData: false }) {
return { return {
type: types.ROOMS.REQUEST, type: types.ROOMS.REQUEST,

View File

@ -125,15 +125,15 @@ const keyCommands = [
discoverabilityTitle: I18n.t('Add_server') discoverabilityTitle: I18n.t('Add_server')
}, },
// Refers to select rooms on list // Refers to select rooms on list
...([1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({ ...[1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({
input: `${ value }`, input: `${value}`,
modifierFlags: constants.keyModifierCommand modifierFlags: constants.keyModifierCommand
}))), })),
// Refers to select servers on list // Refers to select servers on list
...([1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({ ...[1, 2, 3, 4, 5, 6, 7, 8, 9].map(value => ({
input: `${ value }`, input: `${value}`,
modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate modifierFlags: constants.keyModifierCommand | constants.keyModifierAlternate
}))) }))
]; ];
export const setKeyCommands = () => KeyCommands.setKeyCommands(keyCommands); export const setKeyCommands = () => KeyCommands.setKeyCommands(keyCommands);
@ -161,7 +161,8 @@ export const handleCommandSubmit = event => commandHandle(event, KEY_SEND_MESSAG
export const handleCommandShowUpload = event => commandHandle(event, KEY_UPLOAD, ['command']); export const handleCommandShowUpload = event => commandHandle(event, KEY_UPLOAD, ['command']);
export const handleCommandScroll = event => commandHandle(event, [constants.keyInputUpArrow, constants.keyInputDownArrow], ['alternate']); export const handleCommandScroll = event =>
commandHandle(event, [constants.keyInputUpArrow, constants.keyInputDownArrow], ['alternate']);
export const handleCommandRoomActions = event => commandHandle(event, KEY_ROOM_ACTIONS, ['command']); export const handleCommandRoomActions = event => commandHandle(event, KEY_ROOM_ACTIONS, ['command']);

View File

@ -1,4 +1,4 @@
export const STATUS_COLORS = { export const STATUS_COLORS: any = {
online: '#2de0a5', online: '#2de0a5',
busy: '#f5455c', busy: '#f5455c',
away: '#ffd21f', away: '#ffd21f',
@ -19,7 +19,7 @@ const mentions = {
mentionOtherColor: '#F3BE08' mentionOtherColor: '#F3BE08'
}; };
export const themes = { export const themes: any = {
light: { light: {
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
focusedBackground: '#ffffff', focusedBackground: '#ffffff',

View File

@ -2,8 +2,10 @@ import { getBundleId, isIOS } from '../utils/deviceInfo';
const APP_STORE_ID = '1148741252'; const APP_STORE_ID = '1148741252';
export const PLAY_MARKET_LINK = `https://play.google.com/store/apps/details?id=${ getBundleId }`; export const PLAY_MARKET_LINK = `https://play.google.com/store/apps/details?id=${getBundleId}`;
export const FDROID_MARKET_LINK = 'https://f-droid.org/en/packages/chat.rocket.android'; export const FDROID_MARKET_LINK = 'https://f-droid.org/en/packages/chat.rocket.android';
export const APP_STORE_LINK = `https://itunes.apple.com/app/id${ APP_STORE_ID }`; export const APP_STORE_LINK = `https://itunes.apple.com/app/id${APP_STORE_ID}`;
export const LICENSE_LINK = 'https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/LICENSE'; export const LICENSE_LINK = 'https://github.com/RocketChat/Rocket.Chat.ReactNative/blob/develop/LICENSE';
export const STORE_REVIEW_LINK = isIOS ? `itms-apps://itunes.apple.com/app/id${ APP_STORE_ID }?action=write-review` : `market://details?id=${ getBundleId }`; export const STORE_REVIEW_LINK = isIOS
? `itms-apps://itunes.apple.com/app/id${APP_STORE_ID}?action=write-review`
: `market://details?id=${getBundleId}`;

View File

@ -1,208 +0,0 @@
import React, {
useRef,
useState,
useEffect,
forwardRef,
useImperativeHandle,
useCallback,
isValidElement
} from 'react';
import PropTypes from 'prop-types';
import { Keyboard, Text } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { TapGestureHandler, State } from 'react-native-gesture-handler';
import ScrollBottomSheet from 'react-native-scroll-bottom-sheet';
import Animated, {
Extrapolate,
interpolate,
Value,
Easing
} from 'react-native-reanimated';
import * as Haptics from 'expo-haptics';
import { useBackHandler } from '@react-native-community/hooks';
import { Item } from './Item';
import { Handle } from './Handle';
import { Button } from './Button';
import { themes } from '../../constants/colors';
import styles, { ITEM_HEIGHT } from './styles';
import { isTablet, isIOS } from '../../utils/deviceInfo';
import * as List from '../List';
import I18n from '../../i18n';
import { useOrientation, useDimensions } from '../../dimensions';
const getItemLayout = (data, index) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index });
const HANDLE_HEIGHT = isIOS ? 40 : 56;
const MAX_SNAP_HEIGHT = 16;
const CANCEL_HEIGHT = 64;
const ANIMATION_DURATION = 250;
const ANIMATION_CONFIG = {
duration: ANIMATION_DURATION,
// https://easings.net/#easeInOutCubic
easing: Easing.bezier(0.645, 0.045, 0.355, 1.0)
};
const ActionSheet = React.memo(forwardRef(({ children, theme }, ref) => {
const bottomSheetRef = useRef();
const [data, setData] = useState({});
const [isVisible, setVisible] = useState(false);
const { height } = useDimensions();
const { isLandscape } = useOrientation();
const insets = useSafeAreaInsets();
const maxSnap = Math.max(
(
height
// Items height
- (ITEM_HEIGHT * (data?.options?.length || 0))
// Handle height
- HANDLE_HEIGHT
// Custom header height
- (data?.headerHeight || 0)
// Insets bottom height (Notch devices)
- insets.bottom
// Cancel button height
- (data?.hasCancel ? CANCEL_HEIGHT : 0)
),
MAX_SNAP_HEIGHT
);
/*
* if the action sheet cover more
* than 60% of the whole screen
* and it's not at the landscape mode
* we'll provide more one snap
* that point 50% of the whole screen
*/
const snaps = (height - maxSnap > height * 0.6) && !isLandscape ? [maxSnap, height * 0.5, height] : [maxSnap, height];
const openedSnapIndex = snaps.length > 2 ? 1 : 0;
const closedSnapIndex = snaps.length - 1;
const toggleVisible = () => setVisible(!isVisible);
const hide = () => {
bottomSheetRef.current?.snapTo(closedSnapIndex);
};
const show = (options) => {
setData(options);
toggleVisible();
};
const onBackdropPressed = ({ nativeEvent }) => {
if (nativeEvent.oldState === State.ACTIVE) {
hide();
}
};
useBackHandler(() => {
if (isVisible) {
hide();
}
return isVisible;
});
useEffect(() => {
if (isVisible) {
Keyboard.dismiss();
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
bottomSheetRef.current?.snapTo(openedSnapIndex);
}
}, [isVisible]);
// Hides action sheet when orientation changes
useEffect(() => {
setVisible(false);
}, [isLandscape]);
useImperativeHandle(ref, () => ({
showActionSheet: show,
hideActionSheet: hide
}));
const renderHandle = useCallback(() => (
<>
<Handle theme={theme} />
{isValidElement(data?.customHeader) ? data.customHeader : null}
</>
));
const renderFooter = useCallback(() => (data?.hasCancel ? (
<Button
onPress={hide}
style={[styles.button, { backgroundColor: themes[theme].auxiliaryBackground }]}
theme={theme}
>
<Text style={[styles.text, { color: themes[theme].bodyText }]}>
{I18n.t('Cancel')}
</Text>
</Button>
) : null));
const renderItem = useCallback(({ item }) => <Item item={item} hide={hide} theme={theme} />);
const animatedPosition = React.useRef(new Value(0));
const opacity = interpolate(animatedPosition.current, {
inputRange: [0, 1],
outputRange: [0, themes[theme].backdropOpacity],
extrapolate: Extrapolate.CLAMP
});
return (
<>
{children}
{isVisible && (
<>
<TapGestureHandler onHandlerStateChange={onBackdropPressed}>
<Animated.View
testID='action-sheet-backdrop'
style={[
styles.backdrop,
{
backgroundColor: themes[theme].backdropColor,
opacity
}
]}
/>
</TapGestureHandler>
<ScrollBottomSheet
testID='action-sheet'
ref={bottomSheetRef}
componentType='FlatList'
snapPoints={snaps}
initialSnapIndex={closedSnapIndex}
renderHandle={renderHandle}
onSettle={index => (index === closedSnapIndex) && toggleVisible()}
animatedPosition={animatedPosition.current}
containerStyle={[
styles.container,
{ backgroundColor: themes[theme].focusedBackground },
(isLandscape || isTablet) && styles.bottomSheet
]}
animationConfig={ANIMATION_CONFIG}
// FlatList props
data={data?.options}
renderItem={renderItem}
keyExtractor={item => item.title}
style={{ backgroundColor: themes[theme].focusedBackground }}
contentContainerStyle={styles.content}
ItemSeparatorComponent={List.Separator}
ListHeaderComponent={List.Separator}
ListFooterComponent={renderFooter}
getItemLayout={getItemLayout}
removeClippedSubviews={isIOS}
/>
</>
)}
</>
);
}));
ActionSheet.propTypes = {
children: PropTypes.node,
theme: PropTypes.string
};
export default ActionSheet;

View File

@ -0,0 +1,194 @@
import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { Keyboard, Text } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { State, TapGestureHandler } from 'react-native-gesture-handler';
import ScrollBottomSheet from 'react-native-scroll-bottom-sheet';
import Animated, { Easing, Extrapolate, Value, interpolate } from 'react-native-reanimated';
import * as Haptics from 'expo-haptics';
import { useBackHandler } from '@react-native-community/hooks';
import { Item } from './Item';
import { Handle } from './Handle';
import { Button } from './Button';
import { themes } from '../../constants/colors';
import styles, { ITEM_HEIGHT } from './styles';
import { isIOS, isTablet } from '../../utils/deviceInfo';
import * as List from '../List';
import I18n from '../../i18n';
import { IDimensionsContextProps, useDimensions, useOrientation } from '../../dimensions';
interface IActionSheetData {
options: any;
headerHeight?: number;
hasCancel?: boolean;
customHeader: any;
}
const getItemLayout = (data: any, index: number) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index });
const HANDLE_HEIGHT = isIOS ? 40 : 56;
const MAX_SNAP_HEIGHT = 16;
const CANCEL_HEIGHT = 64;
const ANIMATION_DURATION = 250;
const ANIMATION_CONFIG = {
duration: ANIMATION_DURATION,
// https://easings.net/#easeInOutCubic
easing: Easing.bezier(0.645, 0.045, 0.355, 1.0)
};
const ActionSheet = React.memo(
forwardRef(({ children, theme }: { children: JSX.Element; theme: string }, ref) => {
const bottomSheetRef: any = useRef();
const [data, setData] = useState<IActionSheetData>({} as IActionSheetData);
const [isVisible, setVisible] = useState(false);
const { height }: Partial<IDimensionsContextProps> = useDimensions();
const { isLandscape } = useOrientation();
const insets = useSafeAreaInsets();
const maxSnap = Math.max(
height! -
// Items height
ITEM_HEIGHT * (data?.options?.length || 0) -
// Handle height
HANDLE_HEIGHT -
// Custom header height
(data?.headerHeight || 0) -
// Insets bottom height (Notch devices)
insets.bottom -
// Cancel button height
(data?.hasCancel ? CANCEL_HEIGHT : 0),
MAX_SNAP_HEIGHT
);
/*
* if the action sheet cover more
* than 60% of the whole screen
* and it's not at the landscape mode
* we'll provide more one snap
* that point 50% of the whole screen
*/
const snaps: any = height! - maxSnap > height! * 0.6 && !isLandscape ? [maxSnap, height! * 0.5, height] : [maxSnap, height];
const openedSnapIndex = snaps.length > 2 ? 1 : 0;
const closedSnapIndex = snaps.length - 1;
const toggleVisible = () => setVisible(!isVisible);
const hide = () => {
bottomSheetRef.current?.snapTo(closedSnapIndex);
};
const show = (options: any) => {
setData(options);
toggleVisible();
};
const onBackdropPressed = ({ nativeEvent }: any) => {
if (nativeEvent.oldState === State.ACTIVE) {
hide();
}
};
useBackHandler(() => {
if (isVisible) {
hide();
}
return isVisible;
});
useEffect(() => {
if (isVisible) {
Keyboard.dismiss();
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
bottomSheetRef.current?.snapTo(openedSnapIndex);
}
}, [isVisible]);
// Hides action sheet when orientation changes
useEffect(() => {
setVisible(false);
}, [isLandscape]);
useImperativeHandle(ref, () => ({
showActionSheet: show,
hideActionSheet: hide
}));
const renderHandle = () => (
<>
<Handle theme={theme} />
{isValidElement(data?.customHeader) ? data.customHeader : null}
</>
);
const renderFooter = () =>
data?.hasCancel ? (
<Button onPress={hide} style={[styles.button, { backgroundColor: themes[theme].auxiliaryBackground }]} theme={theme}>
<Text style={[styles.text, { color: themes[theme].bodyText }]}>{I18n.t('Cancel')}</Text>
</Button>
) : null;
const renderItem = ({ item }: any) => <Item item={item} hide={hide} theme={theme} />;
const animatedPosition = React.useRef(new Value(0));
const opacity = interpolate(animatedPosition.current, {
inputRange: [0, 1],
outputRange: [0, themes[theme].backdropOpacity],
extrapolate: Extrapolate.CLAMP
});
return (
<>
{children}
{isVisible && (
<>
<TapGestureHandler onHandlerStateChange={onBackdropPressed}>
<Animated.View
testID='action-sheet-backdrop'
style={[
styles.backdrop,
{
backgroundColor: themes[theme].backdropColor,
opacity
}
]}
/>
</TapGestureHandler>
<ScrollBottomSheet
testID='action-sheet'
ref={bottomSheetRef}
componentType='FlatList'
snapPoints={snaps}
initialSnapIndex={closedSnapIndex}
renderHandle={renderHandle}
onSettle={index => index === closedSnapIndex && toggleVisible()}
animatedPosition={animatedPosition.current}
containerStyle={
[
styles.container,
{ backgroundColor: themes[theme].focusedBackground },
(isLandscape || isTablet) && styles.bottomSheet
] as any
}
animationConfig={ANIMATION_CONFIG}
// FlatList props
data={data?.options}
renderItem={renderItem}
keyExtractor={(item: any) => item.title}
style={{ backgroundColor: themes[theme].focusedBackground }}
contentContainerStyle={styles.content}
ItemSeparatorComponent={List.Separator}
ListHeaderComponent={List.Separator}
ListFooterComponent={renderFooter}
getItemLayout={getItemLayout}
removeClippedSubviews={isIOS}
/>
</>
)}
</>
);
})
);
export default ActionSheet;

View File

@ -1,15 +1,11 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native'; import { View } from 'react-native';
import styles from './styles'; import styles from './styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
export const Handle = React.memo(({ theme }) => ( export const Handle = React.memo(({ theme }: { theme: string }) => (
<View style={[styles.handle, { backgroundColor: themes[theme].focusedBackground }]} testID='action-sheet-handle'> <View style={[styles.handle, { backgroundColor: themes[theme].focusedBackground }]} testID='action-sheet-handle'>
<View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} /> <View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} />
</View> </View>
)); ));
Handle.propTypes = {
theme: PropTypes.string
};

View File

@ -1,13 +1,25 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Text, View } from 'react-native'; import { Text, View } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
import { Button } from './Button'; import { Button } from './Button';
import styles from './styles';
export const Item = React.memo(({ item, hide, theme }) => { interface IActionSheetItem {
item: {
title: string;
icon: string;
danger: boolean;
testID: string;
onPress(): void;
right: Function;
};
theme: string;
hide(): void;
}
export const Item = React.memo(({ item, hide, theme }: IActionSheetItem) => {
const onPress = () => { const onPress = () => {
hide(); hide();
item?.onPress(); item?.onPress();
@ -18,34 +30,16 @@ export const Item = React.memo(({ item, hide, theme }) => {
onPress={onPress} onPress={onPress}
style={[styles.item, { backgroundColor: themes[theme].focusedBackground }]} style={[styles.item, { backgroundColor: themes[theme].focusedBackground }]}
theme={theme} theme={theme}
testID={item.testID} testID={item.testID}>
>
<CustomIcon name={item.icon} size={20} color={item.danger ? themes[theme].dangerColor : themes[theme].bodyText} /> <CustomIcon name={item.icon} size={20} color={item.danger ? themes[theme].dangerColor : themes[theme].bodyText} />
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<Text <Text
numberOfLines={1} numberOfLines={1}
style={[styles.title, { color: item.danger ? themes[theme].dangerColor : themes[theme].bodyText }]} style={[styles.title, { color: item.danger ? themes[theme].dangerColor : themes[theme].bodyText }]}>
>
{item.title} {item.title}
</Text> </Text>
</View> </View>
{ item.right ? ( {item.right ? <View style={styles.rightContainer}>{item.right ? item.right() : null}</View> : null}
<View style={styles.rightContainer}>
{item.right ? item.right() : null}
</View>
) : null }
</Button> </Button>
); );
}); });
Item.propTypes = {
item: PropTypes.shape({
title: PropTypes.string,
icon: PropTypes.string,
danger: PropTypes.bool,
onPress: PropTypes.func,
right: PropTypes.func,
testID: PropTypes.string
}),
hide: PropTypes.func,
theme: PropTypes.string
};

View File

@ -1,45 +0,0 @@
import React, { useRef, useContext, forwardRef } from 'react';
import PropTypes from 'prop-types';
import ActionSheet from './ActionSheet';
import { useTheme } from '../../theme';
const context = React.createContext({
showActionSheet: () => {},
hideActionSheet: () => {}
});
export const useActionSheet = () => useContext(context);
const { Provider, Consumer } = context;
export const withActionSheet = Component => forwardRef((props, ref) => (
<Consumer>
{contexts => <Component {...props} {...contexts} ref={ref} />}
</Consumer>
));
export const ActionSheetProvider = React.memo(({ children }) => {
const ref = useRef();
const { theme } = useTheme();
const getContext = () => ({
showActionSheet: (options) => {
ref.current?.showActionSheet(options);
},
hideActionSheet: () => {
ref.current?.hideActionSheet();
}
});
return (
<Provider value={getContext()}>
<ActionSheet ref={ref} theme={theme}>
{children}
</ActionSheet>
</Provider>
);
});
ActionSheetProvider.propTypes = {
children: PropTypes.node
};

View File

@ -0,0 +1,45 @@
import React, { ForwardedRef, forwardRef, useContext, useRef } from 'react';
import ActionSheet from './ActionSheet';
import { useTheme } from '../../theme';
interface IActionSheetProvider {
Provider: any;
Consumer: any;
}
const context: IActionSheetProvider = React.createContext({
showActionSheet: () => {},
hideActionSheet: () => {}
});
export const useActionSheet = () => useContext(context);
const { Provider, Consumer } = context;
export const withActionSheet = (Component: React.FC) =>
forwardRef((props: any, ref: ForwardedRef<any>) => (
<Consumer>{(contexts: any) => <Component {...props} {...contexts} ref={ref} />}</Consumer>
));
export const ActionSheetProvider = React.memo(({ children }: { children: JSX.Element | JSX.Element[] }) => {
const ref: ForwardedRef<any> = useRef();
const { theme }: any = useTheme();
const getContext = () => ({
showActionSheet: (options: any) => {
ref.current?.showActionSheet(options);
},
hideActionSheet: () => {
ref.current?.hideActionSheet();
}
});
return (
<Provider value={getContext()}>
<ActionSheet ref={ref} theme={theme}>
<>{children}</>
</ActionSheet>
</Provider>
);
});

View File

@ -1,40 +0,0 @@
import React from 'react';
import { ActivityIndicator, StyleSheet } from 'react-native';
import { PropTypes } from 'prop-types';
import { themes } from '../constants/colors';
const styles = StyleSheet.create({
indicator: {
padding: 16,
flex: 1
},
absolute: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center'
}
});
const RCActivityIndicator = ({ theme, absolute, ...props }) => (
<ActivityIndicator
style={[styles.indicator, absolute && styles.absolute]}
color={themes[theme].auxiliaryText}
{...props}
/>
);
RCActivityIndicator.propTypes = {
theme: PropTypes.string,
absolute: PropTypes.bool,
props: PropTypes.object
};
RCActivityIndicator.defaultProps = {
theme: 'light'
};
export default RCActivityIndicator;

View File

@ -0,0 +1,34 @@
import React from 'react';
import { ActivityIndicator, ActivityIndicatorProps, StyleSheet } from 'react-native';
import { themes } from '../constants/colors';
type TTheme = 'light' | 'dark' | 'black' | string;
interface IActivityIndicator extends ActivityIndicatorProps {
theme?: TTheme;
absolute?: boolean;
props?: object;
}
const styles = StyleSheet.create({
indicator: {
padding: 16,
flex: 1
},
absolute: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center'
}
});
const RCActivityIndicator = ({ theme = 'light', absolute, ...props }: IActivityIndicator) => (
<ActivityIndicator style={[styles.indicator, absolute && styles.absolute]} color={themes[theme].auxiliaryText} {...props} />
);
export default RCActivityIndicator;

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { StyleSheet, View, Text } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import PropTypes from 'prop-types';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
@ -21,14 +20,13 @@ const styles = StyleSheet.create({
} }
}); });
const AppVersion = React.memo(({ theme }) => ( const AppVersion = React.memo(({ theme }: { theme: string }) => (
<View style={styles.container}> <View style={styles.container}>
<Text style={[styles.text, { color: themes[theme].auxiliaryText }]}>{I18n.t('Version_no', { version: '' })}<Text style={styles.bold}>{getReadableVersion}</Text></Text> <Text style={[styles.text, { color: themes[theme].auxiliaryText }]}>
{I18n.t('Version_no', { version: '' })}
<Text style={styles.bold}>{getReadableVersion}</Text>
</Text>
</View> </View>
)); ));
AppVersion.propTypes = {
theme: PropTypes.string
};
export default AppVersion; export default AppVersion;

View File

@ -1,130 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import FastImage from '@rocket.chat/react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import { avatarURL } from '../../utils/avatar';
import Emoji from '../markdown/Emoji';
const Avatar = React.memo(({
text,
size,
server,
borderRadius,
style,
avatar,
type,
children,
user,
onPress,
emoji,
theme,
getCustomEmoji,
avatarETag,
isStatic,
rid,
blockUnauthenticatedAccess,
serverVersion
}) => {
if ((!text && !avatar && !emoji && !rid) || !server) {
return null;
}
const avatarStyle = {
width: size,
height: size,
borderRadius
};
let image;
if (emoji) {
image = (
<Emoji
theme={theme}
baseUrl={server}
getCustomEmoji={getCustomEmoji}
isMessageContainsOnlyEmoji
literal={emoji}
style={avatarStyle}
/>
);
} else {
let uri = avatar;
if (!isStatic) {
uri = avatarURL({
type,
text,
size,
user,
avatar,
server,
avatarETag,
serverVersion,
rid,
blockUnauthenticatedAccess
});
}
image = (
<FastImage
style={avatarStyle}
source={{
uri,
headers: RocketChatSettings.customHeaders,
priority: FastImage.priority.high
}}
/>
);
}
if (onPress) {
image = (
<Touchable onPress={onPress}>
{image}
</Touchable>
);
}
return (
<View style={[avatarStyle, style]}>
{image}
{children}
</View>
);
});
Avatar.propTypes = {
server: PropTypes.string,
style: PropTypes.any,
text: PropTypes.string,
avatar: PropTypes.string,
emoji: PropTypes.string,
size: PropTypes.number,
borderRadius: PropTypes.number,
type: PropTypes.string,
children: PropTypes.object,
user: PropTypes.shape({
id: PropTypes.string,
token: PropTypes.string
}),
theme: PropTypes.string,
onPress: PropTypes.func,
getCustomEmoji: PropTypes.func,
avatarETag: PropTypes.string,
isStatic: PropTypes.bool,
rid: PropTypes.string,
blockUnauthenticatedAccess: PropTypes.bool,
serverVersion: PropTypes.string
};
Avatar.defaultProps = {
text: '',
size: 25,
type: 'd',
borderRadius: 4
};
export default Avatar;

View File

@ -0,0 +1,96 @@
import React from 'react';
import { View } from 'react-native';
import FastImage from '@rocket.chat/react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import { avatarURL } from '../../utils/avatar';
import Emoji from '../markdown/Emoji';
import { IAvatar } from './interfaces';
const Avatar = React.memo(
({
server,
style,
avatar,
children,
user,
onPress,
emoji,
theme,
getCustomEmoji,
avatarETag,
isStatic,
rid,
blockUnauthenticatedAccess,
serverVersion,
text,
size = 25,
borderRadius = 4,
type = 'd'
}: Partial<IAvatar>) => {
if ((!text && !avatar && !emoji && !rid) || !server) {
return null;
}
const avatarStyle = {
width: size,
height: size,
borderRadius
};
let image;
if (emoji) {
image = (
<Emoji
theme={theme}
baseUrl={server}
getCustomEmoji={getCustomEmoji}
isMessageContainsOnlyEmoji
literal={emoji}
style={avatarStyle}
/>
);
} else {
let uri = avatar;
if (!isStatic) {
uri = avatarURL({
type,
text,
size,
user,
avatar,
server,
avatarETag,
serverVersion,
rid,
blockUnauthenticatedAccess
});
}
image = (
<FastImage
style={avatarStyle}
source={{
uri,
headers: RocketChatSettings.customHeaders,
priority: FastImage.priority.high
}}
/>
);
}
if (onPress) {
image = <Touchable onPress={onPress}>{image}</Touchable>;
}
return (
<View style={[avatarStyle, style]}>
{image}
{children}
</View>
);
}
);
export default Avatar;

View File

@ -1,27 +1,23 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import database from '../../lib/database'; import database from '../../lib/database';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import Avatar from './Avatar'; import Avatar from './Avatar';
import { IAvatar } from './interfaces';
class AvatarContainer extends React.Component { class AvatarContainer extends React.Component<Partial<IAvatar>, any> {
static propTypes = { private mounted: boolean;
rid: PropTypes.string,
text: PropTypes.string, private subscription!: any;
type: PropTypes.string,
blockUnauthenticatedAccess: PropTypes.bool,
serverVersion: PropTypes.string
};
static defaultProps = { static defaultProps = {
text: '', text: '',
type: 'd' type: 'd'
}; };
constructor(props) { constructor(props: Partial<IAvatar>) {
super(props); super(props);
this.mounted = false; this.mounted = false;
this.state = { avatarETag: '' }; this.state = { avatarETag: '' };
@ -32,7 +28,7 @@ class AvatarContainer extends React.Component {
this.mounted = true; this.mounted = true;
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps: any) {
const { text, type } = this.props; const { text, type } = this.props;
if (prevProps.text !== text || prevProps.type !== type) { if (prevProps.text !== text || prevProps.type !== type) {
this.init(); this.init();
@ -50,7 +46,7 @@ class AvatarContainer extends React.Component {
return type === 'd'; return type === 'd';
} }
init = async() => { init = async () => {
const db = database.active; const db = database.active;
const usersCollection = db.get('users'); const usersCollection = db.get('users');
const subsCollection = db.get('subscriptions'); const subsCollection = db.get('subscriptions');
@ -59,7 +55,7 @@ class AvatarContainer extends React.Component {
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;
@ -71,37 +67,32 @@ class AvatarContainer extends React.Component {
if (record) { if (record) {
const observable = record.observe(); const observable = record.observe();
this.subscription = observable.subscribe((r) => { this.subscription = observable.subscribe((r: any) => {
const { avatarETag } = r; const { avatarETag } = r;
if (this.mounted) { if (this.mounted) {
this.setState({ avatarETag }); this.setState({ avatarETag });
} else { } else {
// @ts-ignore
this.state.avatarETag = avatarETag; this.state.avatarETag = avatarETag;
} }
}); });
} }
} };
render() { render() {
const { avatarETag } = this.state; const { avatarETag } = this.state;
const { serverVersion } = this.props; const { serverVersion } = this.props;
return ( return <Avatar avatarETag={avatarETag} serverVersion={serverVersion} {...this.props} />;
<Avatar
avatarETag={avatarETag}
serverVersion={serverVersion}
{...this.props}
/>
);
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
user: getUserSelector(state), user: getUserSelector(state),
server: state.share.server.server || state.server.server, server: state.share.server.server || state.server.server,
serverVersion: state.share.server.version || state.server.version, serverVersion: state.share.server.version || state.server.version,
blockUnauthenticatedAccess: blockUnauthenticatedAccess:
state.share.settings?.Accounts_AvatarBlockUnauthenticatedAccess state.share.settings?.Accounts_AvatarBlockUnauthenticatedAccess ??
?? state.settings.Accounts_AvatarBlockUnauthenticatedAccess state.settings.Accounts_AvatarBlockUnauthenticatedAccess ??
?? true true
}); });
export default connect(mapStateToProps)(AvatarContainer); export default connect(mapStateToProps)(AvatarContainer);

View File

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

View File

@ -2,48 +2,30 @@
import React from 'react'; import React from 'react';
import { storiesOf } from '@storybook/react-native'; import { storiesOf } from '@storybook/react-native';
import BackgroundContainer from '.';
import { ThemeContext } from '../../theme'; import { ThemeContext } from '../../theme';
import { longText } from '../../../storybook/utils'; import { longText } from '../../../storybook/utils';
import BackgroundContainer from '.';
const stories = storiesOf('BackgroundContainer', module); const stories = storiesOf('BackgroundContainer', module);
stories.add('basic', () => ( stories.add('basic', () => <BackgroundContainer />);
<BackgroundContainer />
));
stories.add('loading', () => ( stories.add('loading', () => <BackgroundContainer loading />);
<BackgroundContainer loading />
));
stories.add('text', () => ( stories.add('text', () => <BackgroundContainer text='Text here' />);
<BackgroundContainer text='Text here' />
));
stories.add('long text', () => ( stories.add('long text', () => <BackgroundContainer text={longText} />);
<BackgroundContainer text={longText} />
));
const ThemeStory = ({ theme, ...props }) => ( const ThemeStory = ({ theme, ...props }) => (
<ThemeContext.Provider <ThemeContext.Provider value={{ theme }}>
value={{ theme }}
>
<BackgroundContainer {...props} /> <BackgroundContainer {...props} />
</ThemeContext.Provider> </ThemeContext.Provider>
); );
stories.add('dark theme - loading', () => ( stories.add('dark theme - loading', () => <ThemeStory theme='dark' loading />);
<ThemeStory theme='dark' loading />
));
stories.add('dark theme - text', () => ( stories.add('dark theme - text', () => <ThemeStory theme='dark' text={longText} />);
<ThemeStory theme='dark' text={longText} />
));
stories.add('black theme - loading', () => ( stories.add('black theme - loading', () => <ThemeStory theme='black' loading />);
<ThemeStory theme='black' loading />
));
stories.add('black theme - text', () => ( stories.add('black theme - text', () => <ThemeStory theme='black' text={longText} />);
<ThemeStory theme='black' text={longText} />
));

View File

@ -1,13 +1,16 @@
import React from 'react'; import React from 'react';
import { import { ActivityIndicator, ImageBackground, StyleSheet, Text, View } from 'react-native';
ImageBackground, StyleSheet, Text, View, ActivityIndicator
} from 'react-native';
import PropTypes from 'prop-types';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
interface IBackgroundContainer {
text: string;
theme: string;
loading: boolean;
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1 flex: 1
@ -29,17 +32,12 @@ const styles = StyleSheet.create({
} }
}); });
const BackgroundContainer = ({ theme, text, loading }) => ( const BackgroundContainer = ({ theme, text, loading }: IBackgroundContainer) => (
<View style={styles.container}> <View style={styles.container}>
<ImageBackground source={{ uri: `message_empty_${ theme }` }} style={styles.image} /> <ImageBackground source={{ uri: `message_empty_${theme}` }} style={styles.image} />
{text ? <Text style={[styles.text, { color: themes[theme].auxiliaryTintColor }]}>{text}</Text> : null} {text ? <Text style={[styles.text, { color: themes[theme].auxiliaryTintColor }]}>{text}</Text> : null}
{loading ? <ActivityIndicator style={[styles.text, { color: themes[theme].auxiliaryTintColor }]} /> : null} {loading ? <ActivityIndicator style={styles.text} color={themes[theme].auxiliaryTintColor} /> : null}
</View> </View>
); );
BackgroundContainer.propTypes = {
text: PropTypes.string,
theme: PropTypes.string,
loading: PropTypes.bool
};
export default withTheme(BackgroundContainer); export default withTheme(BackgroundContainer);

View File

@ -1,94 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StyleSheet, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import ActivityIndicator from '../ActivityIndicator';
const styles = StyleSheet.create({
container: {
paddingHorizontal: 14,
justifyContent: 'center',
height: 48,
borderRadius: 2,
marginBottom: 12
},
text: {
fontSize: 16,
...sharedStyles.textMedium,
...sharedStyles.textAlignCenter
},
disabled: {
opacity: 0.3
}
});
export default class Button extends React.PureComponent {
static propTypes = {
title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
type: PropTypes.string,
onPress: PropTypes.func,
disabled: PropTypes.bool,
backgroundColor: PropTypes.string,
loading: PropTypes.bool,
theme: PropTypes.string,
color: PropTypes.string,
fontSize: PropTypes.string,
style: PropTypes.any
}
static defaultProps = {
title: 'Press me!',
type: 'primary',
onPress: () => alert('It works!'),
disabled: false,
loading: false
}
render() {
const {
title, type, onPress, disabled, backgroundColor, color, loading, style, theme, fontSize, ...otherProps
} = this.props;
const isPrimary = type === 'primary';
let textColor = isPrimary ? themes[theme].buttonText : themes[theme].bodyText;
if (color) {
textColor = color;
}
return (
<Touchable
onPress={onPress}
disabled={disabled || loading}
style={[
styles.container,
backgroundColor
? { backgroundColor }
: { backgroundColor: isPrimary ? themes[theme].actionTintColor : themes[theme].backgroundColor },
disabled && styles.disabled,
style
]}
{...otherProps}
>
{
loading
? <ActivityIndicator color={textColor} />
: (
<Text
style={[
styles.text,
{ color: textColor },
fontSize && { fontSize }
]}
accessibilityLabel={title}
>
{title}
</Text>
)
}
</Touchable>
);
}
}

View File

@ -0,0 +1,82 @@
import React from 'react';
import { StyleSheet, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import ActivityIndicator from '../ActivityIndicator';
interface IButtonProps {
title: string;
type: string;
onPress(): void;
disabled: boolean;
backgroundColor: string;
loading: boolean;
theme: string;
color: string;
fontSize: any;
style: any;
testID: string;
}
const styles = StyleSheet.create({
container: {
paddingHorizontal: 14,
justifyContent: 'center',
height: 48,
borderRadius: 2,
marginBottom: 12
},
text: {
fontSize: 16,
...sharedStyles.textMedium,
...sharedStyles.textAlignCenter
},
disabled: {
opacity: 0.3
}
});
export default class Button extends React.PureComponent<Partial<IButtonProps>, any> {
static defaultProps = {
title: 'Press me!',
type: 'primary',
onPress: () => alert('It works!'),
disabled: false,
loading: false
};
render() {
const { title, type, onPress, disabled, backgroundColor, color, loading, style, theme, fontSize, ...otherProps } = this.props;
const isPrimary = type === 'primary';
let textColor = isPrimary ? themes[theme!].buttonText : themes[theme!].bodyText;
if (color) {
textColor = color;
}
return (
<Touchable
onPress={onPress}
disabled={disabled || loading}
style={[
styles.container,
backgroundColor
? { backgroundColor }
: { backgroundColor: isPrimary ? themes[theme!].actionTintColor : themes[theme!].backgroundColor },
disabled && styles.disabled,
style
]}
{...otherProps}>
{loading ? (
<ActivityIndicator color={textColor} />
) : (
<Text style={[styles.text, { color: textColor }, fontSize && { fontSize }]} accessibilityLabel={title}>
{title}
</Text>
)}
</Touchable>
);
}
}

View File

@ -1,10 +1,13 @@
import React from 'react'; import React from 'react';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
interface ICheck {
style?: object;
theme: string;
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
icon: { icon: {
width: 22, width: 22,
@ -13,11 +16,8 @@ const styles = StyleSheet.create({
} }
}); });
const Check = React.memo(({ theme, style }) => <CustomIcon style={[styles.icon, style]} color={themes[theme].tintColor} size={22} name='check' />); const Check = React.memo(({ theme, style }: ICheck) => (
<CustomIcon style={[styles.icon, style]} color={themes[theme].tintColor} size={22} name='check' />
Check.propTypes = { ));
style: PropTypes.object,
theme: PropTypes.string
};
export default Check; export default Check;

View File

@ -1,26 +0,0 @@
import React from 'react';
import FastImage from '@rocket.chat/react-native-fast-image';
import PropTypes from 'prop-types';
const CustomEmoji = React.memo(({ baseUrl, emoji, style }) => (
<FastImage
style={style}
source={{
uri: `${ baseUrl }/emoji-custom/${ encodeURIComponent(emoji.content || emoji.name) }.${ emoji.extension }`,
priority: FastImage.priority.high
}}
resizeMode={FastImage.resizeMode.contain}
/>
), (prevProps, nextProps) => {
const prevEmoji = prevProps.emoji.content || prevProps.emoji.name;
const nextEmoji = nextProps.emoji.content || nextProps.emoji.name;
return prevEmoji === nextEmoji;
});
CustomEmoji.propTypes = {
baseUrl: PropTypes.string.isRequired,
emoji: PropTypes.object.isRequired,
style: PropTypes.any
};
export default CustomEmoji;

View File

@ -0,0 +1,24 @@
import React from 'react';
import FastImage from '@rocket.chat/react-native-fast-image';
import { ICustomEmoji } from './interfaces';
const CustomEmoji = React.memo(
({ baseUrl, emoji, style }: ICustomEmoji) => (
<FastImage
style={style}
source={{
uri: `${baseUrl}/emoji-custom/${encodeURIComponent(emoji.content || emoji.name)}.${emoji.extension}`,
priority: FastImage.priority.high
}}
resizeMode={FastImage.resizeMode.contain}
/>
),
(prevProps, nextProps) => {
const prevEmoji = prevProps.emoji.content || prevProps.emoji.name;
const nextEmoji = nextProps.emoji.content || nextProps.emoji.name;
return prevEmoji === nextEmoji;
}
);
export default CustomEmoji;

View File

@ -1,44 +1,41 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { FlatList, Text, TouchableOpacity } from 'react-native';
import { Text, TouchableOpacity, FlatList } from 'react-native';
import shortnameToUnicode from '../../utils/shortnameToUnicode'; import shortnameToUnicode from '../../utils/shortnameToUnicode';
import styles from './styles'; import styles from './styles';
import CustomEmoji from './CustomEmoji'; import CustomEmoji from './CustomEmoji';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
import { IEmoji, IEmojiCategory } from './interfaces';
const EMOJI_SIZE = 50; const EMOJI_SIZE = 50;
const renderEmoji = (emoji, size, baseUrl) => { const renderEmoji = (emoji: IEmoji, size: number, baseUrl: string) => {
if (emoji && emoji.isCustom) { if (emoji && emoji.isCustom) {
return <CustomEmoji style={[styles.customCategoryEmoji, { height: size - 16, width: size - 16 }]} emoji={emoji} baseUrl={baseUrl} />; return (
<CustomEmoji
style={[styles.customCategoryEmoji, { height: size - 16, width: size - 16 }]}
emoji={emoji}
baseUrl={baseUrl}
/>
);
} }
return ( return (
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}> <Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}>
{shortnameToUnicode(`:${ emoji }:`)} {shortnameToUnicode(`:${emoji}:`)}
</Text> </Text>
); );
}; };
class EmojiCategory extends React.Component { class EmojiCategory extends React.Component<Partial<IEmojiCategory>> {
static propTypes = { renderItem(emoji: any) {
baseUrl: PropTypes.string.isRequired,
emojis: PropTypes.any,
onEmojiSelected: PropTypes.func,
emojisPerRow: PropTypes.number,
width: PropTypes.number
}
renderItem(emoji) {
const { baseUrl, onEmojiSelected } = this.props; const { baseUrl, onEmojiSelected } = this.props;
return ( return (
<TouchableOpacity <TouchableOpacity
activeOpacity={0.7} activeOpacity={0.7}
key={emoji && emoji.isCustom ? emoji.content : emoji} key={emoji && emoji.isCustom ? emoji.content : emoji}
onPress={() => onEmojiSelected(emoji)} onPress={() => onEmojiSelected!(emoji)}
testID={`reaction-picker-${ emoji && emoji.isCustom ? emoji.content : emoji }`} testID={`reaction-picker-${emoji && emoji.isCustom ? emoji.content : emoji}`}>
> {renderEmoji(emoji, EMOJI_SIZE, baseUrl!)}
{renderEmoji(emoji, EMOJI_SIZE, baseUrl)}
</TouchableOpacity> </TouchableOpacity>
); );
} }
@ -51,13 +48,14 @@ class EmojiCategory extends React.Component {
} }
const numColumns = Math.trunc(width / EMOJI_SIZE); const numColumns = Math.trunc(width / EMOJI_SIZE);
const marginHorizontal = (width - (numColumns * EMOJI_SIZE)) / 2; const marginHorizontal = (width - numColumns * EMOJI_SIZE) / 2;
return ( return (
// @ts-ignore
<FlatList <FlatList
contentContainerStyle={{ marginHorizontal }} contentContainerStyle={{ marginHorizontal }}
// rerender FlatList in case of width changes // rerender FlatList in case of width changes
key={`emoji-category-${ width }`} key={`emoji-category-${width}`}
keyExtractor={item => (item && item.isCustom && item.content) || item} keyExtractor={item => (item && item.isCustom && item.content) || item}
data={emojis} data={emojis}
extraData={this.props} extraData={this.props}

View File

@ -1,49 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, TouchableOpacity, Text } from 'react-native';
import styles from './styles';
import { themes } from '../../constants/colors';
export default class TabBar extends React.Component {
static propTypes = {
goToPage: PropTypes.func,
activeTab: PropTypes.number,
tabs: PropTypes.array,
tabEmojiStyle: PropTypes.object,
theme: PropTypes.string
}
shouldComponentUpdate(nextProps) {
const { activeTab, theme } = this.props;
if (nextProps.activeTab !== activeTab) {
return true;
}
if (nextProps.theme !== theme) {
return true;
}
return false;
}
render() {
const {
tabs, goToPage, tabEmojiStyle, activeTab, theme
} = this.props;
return (
<View style={styles.tabsContainer}>
{tabs.map((tab, i) => (
<TouchableOpacity
activeOpacity={0.7}
key={tab}
onPress={() => goToPage(i)}
style={styles.tab}
testID={`reaction-picker-${ tab }`}
>
<Text style={[styles.tabEmoji, tabEmojiStyle]}>{tab}</Text>
{activeTab === i ? <View style={[styles.activeTabLine, { backgroundColor: themes[theme].tintColor }]} /> : <View style={styles.tabLine} />}
</TouchableOpacity>
))}
</View>
);
}
}

View File

@ -0,0 +1,50 @@
import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import styles from './styles';
import { themes } from '../../constants/colors';
interface ITabBarProps {
goToPage: Function;
activeTab: number;
tabs: [];
tabEmojiStyle: object;
theme: string;
}
export default class TabBar extends React.Component<Partial<ITabBarProps>> {
shouldComponentUpdate(nextProps: any) {
const { activeTab, theme } = this.props;
if (nextProps.activeTab !== activeTab) {
return true;
}
if (nextProps.theme !== theme) {
return true;
}
return false;
}
render() {
const { tabs, goToPage, tabEmojiStyle, activeTab, theme } = this.props;
return (
<View style={styles.tabsContainer}>
{tabs!.map((tab, i) => (
<TouchableOpacity
activeOpacity={0.7}
key={tab}
onPress={() => goToPage!(i)}
style={styles.tab}
testID={`reaction-picker-${tab}`}>
<Text style={[styles.tabEmoji, tabEmojiStyle]}>{tab}</Text>
{activeTab === i ? (
<View style={[styles.activeTabLine, { backgroundColor: themes[theme!].tintColor }]} />
) : (
<View style={styles.tabLine} />
)}
</TouchableOpacity>
))}
</View>
);
}
}

View File

@ -1,6 +1,5 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import PropTypes from 'prop-types';
import ScrollableTabView from 'react-native-scrollable-tab-view'; import ScrollableTabView from 'react-native-scrollable-tab-view';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -18,22 +17,33 @@ import shortnameToUnicode from '../../utils/shortnameToUnicode';
import log from '../../utils/log'; import log from '../../utils/log';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { IEmoji } from './interfaces';
const scrollProps = { const scrollProps = {
keyboardShouldPersistTaps: 'always', keyboardShouldPersistTaps: 'always',
keyboardDismissMode: 'none' keyboardDismissMode: 'none'
}; };
class EmojiPicker extends Component { interface IEmojiPickerProps {
static propTypes = { isMessageContainsOnlyEmoji: boolean;
baseUrl: PropTypes.string.isRequired, getCustomEmoji?: Function;
customEmojis: PropTypes.object, baseUrl: string;
onEmojiSelected: PropTypes.func, customEmojis?: any;
tabEmojiStyle: PropTypes.object, style: object;
theme: PropTypes.string theme?: string;
}; onEmojiSelected?: Function;
tabEmojiStyle?: object;
}
constructor(props) { interface IEmojiPickerState {
frequentlyUsed: [];
customEmojis: any;
show: boolean;
width: number | null;
}
class EmojiPicker extends Component<IEmojiPickerProps, IEmojiPickerState> {
constructor(props: IEmojiPickerProps) {
super(props); super(props);
const customEmojis = Object.keys(props.customEmojis) const customEmojis = Object.keys(props.customEmojis)
.filter(item => item === props.customEmojis[item].name) .filter(item => item === props.customEmojis[item].name)
@ -55,7 +65,7 @@ class EmojiPicker extends Component {
this.setState({ show: true }); this.setState({ show: true });
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps: any, nextState: any) {
const { frequentlyUsed, show, width } = this.state; const { frequentlyUsed, show, width } = this.state;
const { theme } = this.props; const { theme } = this.props;
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
@ -73,67 +83,72 @@ class EmojiPicker extends Component {
return false; return false;
} }
onEmojiSelected = (emoji) => { onEmojiSelected = (emoji: IEmoji) => {
try { try {
const { onEmojiSelected } = this.props; const { onEmojiSelected } = this.props;
if (emoji.isCustom) { if (emoji.isCustom) {
this._addFrequentlyUsed({ this._addFrequentlyUsed({
content: emoji.content, extension: emoji.extension, isCustom: true content: emoji.content,
extension: emoji.extension,
isCustom: true
}); });
onEmojiSelected(`:${ emoji.content }:`); onEmojiSelected!(`:${emoji.content}:`);
} else { } else {
const content = emoji; const content = emoji;
this._addFrequentlyUsed({ content, isCustom: false }); this._addFrequentlyUsed({ content, isCustom: false });
const shortname = `:${ emoji }:`; const shortname = `:${emoji}:`;
onEmojiSelected(shortnameToUnicode(shortname), shortname); onEmojiSelected!(shortnameToUnicode(shortname), shortname);
} }
} catch (e) { } catch (e) {
log(e); log(e);
} }
} };
// eslint-disable-next-line react/sort-comp _addFrequentlyUsed = protectedFunction(async (emoji: IEmoji) => {
_addFrequentlyUsed = protectedFunction(async(emoji) => {
const db = database.active; const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis'); const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojiRecord; let freqEmojiRecord: any;
try { try {
freqEmojiRecord = await freqEmojiCollection.find(emoji.content); freqEmojiRecord = await freqEmojiCollection.find(emoji.content);
} catch (error) { } catch (error) {
// Do nothing // Do nothing
} }
await db.action(async() => { await db.action(async () => {
if (freqEmojiRecord) { if (freqEmojiRecord) {
await freqEmojiRecord.update((f) => { await freqEmojiRecord.update((f: any) => {
f.count += 1; f.count += 1;
}); });
} else { } else {
await freqEmojiCollection.create((f) => { await freqEmojiCollection.create((f: any) => {
f._raw = sanitizedRaw({ id: emoji.content }, freqEmojiCollection.schema); f._raw = sanitizedRaw({ id: emoji.content }, freqEmojiCollection.schema);
Object.assign(f, emoji); Object.assign(f, emoji);
f.count = 1; f.count = 1;
}); });
} }
}); });
}) });
updateFrequentlyUsed = async() => { updateFrequentlyUsed = async () => {
const db = database.active; const db = database.active;
const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch(); const frequentlyUsedRecords = await db.get('frequently_used_emojis').query().fetch();
let frequentlyUsed = orderBy(frequentlyUsedRecords, ['count'], ['desc']); let frequentlyUsed: any = orderBy(frequentlyUsedRecords, ['count'], ['desc']);
frequentlyUsed = frequentlyUsed.map((item) => { frequentlyUsed = frequentlyUsed.map((item: IEmoji) => {
if (item.isCustom) { if (item.isCustom) {
return { content: item.content, extension: item.extension, isCustom: item.isCustom }; return { content: item.content, extension: item.extension, isCustom: item.isCustom };
} }
return shortnameToUnicode(`${ item.content }`); return shortnameToUnicode(`${item.content}`);
}); });
this.setState({ frequentlyUsed }); this.setState({ frequentlyUsed });
} };
onLayout = ({ nativeEvent: { layout: { width } } }) => this.setState({ width }); onLayout = ({
nativeEvent: {
layout: { width }
}
}: any) => this.setState({ width });
renderCategory(category, i, label) { renderCategory(category: any, i: number, label: string) {
const { frequentlyUsed, customEmojis, width } = this.state; const { frequentlyUsed, customEmojis, width } = this.state;
const { baseUrl } = this.props; const { baseUrl } = this.props;
@ -148,9 +163,9 @@ class EmojiPicker extends Component {
return ( return (
<EmojiCategory <EmojiCategory
emojis={emojis} emojis={emojis}
onEmojiSelected={emoji => this.onEmojiSelected(emoji)} onEmojiSelected={(emoji: IEmoji) => this.onEmojiSelected(emoji)}
style={styles.categoryContainer} style={styles.categoryContainer}
width={width} width={width!}
baseUrl={baseUrl} baseUrl={baseUrl}
tabLabel={label} tabLabel={label}
/> />
@ -168,23 +183,21 @@ class EmojiPicker extends Component {
<View onLayout={this.onLayout} style={{ flex: 1 }}> <View onLayout={this.onLayout} style={{ flex: 1 }}>
<ScrollableTabView <ScrollableTabView
renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} theme={theme} />} renderTabBar={() => <TabBar tabEmojiStyle={tabEmojiStyle} theme={theme} />}
/* @ts-ignore*/
contentProps={scrollProps} contentProps={scrollProps}
style={{ backgroundColor: themes[theme].focusedBackground }} style={{ backgroundColor: themes[theme!].focusedBackground }}>
> {categories.tabs.map((tab, i) =>
{ i === 0 && frequentlyUsed.length === 0
categories.tabs.map((tab, i) => ( ? null // when no frequentlyUsed don't show the tab
(i === 0 && frequentlyUsed.length === 0) ? null // when no frequentlyUsed don't show the tab : this.renderCategory(tab.category, i, tab.tabLabel)
: ( )}
this.renderCategory(tab.category, i, tab.tabLabel)
)))
}
</ScrollableTabView> </ScrollableTabView>
</View> </View>
); );
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
customEmojis: state.customEmojis customEmojis: state.customEmojis
}); });

View File

@ -0,0 +1,22 @@
export interface IEmoji {
content: any;
name: string;
extension: any;
isCustom: boolean;
}
export interface ICustomEmoji {
baseUrl: string;
emoji: IEmoji;
style: any;
}
export interface IEmojiCategory {
baseUrl: string;
emojis: IEmoji[];
onEmojiSelected: Function;
emojisPerRow: number;
width: number;
style: any;
tabLabel: string;
}

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { ScrollView, StyleSheet, View } from 'react-native'; import { ScrollView, StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
@ -11,33 +10,35 @@ import AppVersion from './AppVersion';
import { isTablet } from '../utils/deviceInfo'; import { isTablet } from '../utils/deviceInfo';
import SafeAreaView from './SafeAreaView'; import SafeAreaView from './SafeAreaView';
interface IFormContainer {
theme: string;
testID: string;
children: JSX.Element;
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
scrollView: { scrollView: {
minHeight: '100%' minHeight: '100%'
} }
}); });
export const FormContainerInner = ({ children }) => ( export const FormContainerInner = ({ children }: { children: JSX.Element }) => (
<View style={[sharedStyles.container, isTablet && sharedStyles.tabletScreenContent]}> <View style={[sharedStyles.container, isTablet && sharedStyles.tabletScreenContent]}>{children}</View>
{children}
</View>
); );
const FormContainer = ({ const FormContainer = ({ children, theme, testID, ...props }: IFormContainer) => (
children, theme, testID, ...props // @ts-ignore
}) => (
<KeyboardView <KeyboardView
style={{ backgroundColor: themes[theme].backgroundColor }} style={{ backgroundColor: themes[theme].backgroundColor }}
contentContainerStyle={sharedStyles.container} contentContainerStyle={sharedStyles.container}
keyboardVerticalOffset={128} keyboardVerticalOffset={128}>
>
<StatusBar /> <StatusBar />
{/* @ts-ignore*/}
<ScrollView <ScrollView
style={sharedStyles.container} style={sharedStyles.container}
contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]} contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}
{...scrollPersistTaps} {...scrollPersistTaps}
{...props} {...props}>
>
<SafeAreaView testID={testID} style={{ backgroundColor: themes[theme].backgroundColor }}> <SafeAreaView testID={testID} style={{ backgroundColor: themes[theme].backgroundColor }}>
{children} {children}
<AppVersion theme={theme} /> <AppVersion theme={theme} />
@ -46,14 +47,4 @@ const FormContainer = ({
</KeyboardView> </KeyboardView>
); );
FormContainer.propTypes = {
theme: PropTypes.string,
testID: PropTypes.string,
children: PropTypes.element
};
FormContainerInner.propTypes = {
children: PropTypes.element
};
export default FormContainer; export default FormContainer;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { View, StyleSheet } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { themedHeader } from '../../utils/navigation'; import { themedHeader } from '../../utils/navigation';
import { isIOS, isTablet } from '../../utils/deviceInfo'; import { isIOS, isTablet } from '../../utils/deviceInfo';
@ -10,18 +10,25 @@ import { withTheme } from '../../theme';
// Get from https://github.com/react-navigation/react-navigation/blob/master/packages/stack/src/views/Header/HeaderSegment.tsx#L69 // Get from https://github.com/react-navigation/react-navigation/blob/master/packages/stack/src/views/Header/HeaderSegment.tsx#L69
export const headerHeight = isIOS ? 44 : 56; export const headerHeight = isIOS ? 44 : 56;
export const getHeaderHeight = (isLandscape) => { export const getHeaderHeight = (isLandscape: boolean) => {
if (isIOS) { if (isIOS) {
if (isLandscape && !isTablet) { if (isLandscape && !isTablet) {
return 32; return 32;
} else {
return 44;
} }
return 44;
} }
return 56; return 56;
}; };
export const getHeaderTitlePosition = ({ insets, numIconsRight }) => ({ interface IHeaderTitlePosition {
insets: {
left: number;
right: number;
};
numIconsRight: number;
}
export const getHeaderTitlePosition = ({ insets, numIconsRight }: IHeaderTitlePosition) => ({
left: insets.left + 60, left: insets.left + 60,
right: insets.right + Math.max(45 * numIconsRight, 15) right: insets.right + Math.max(45 * numIconsRight, 15)
}); });
@ -35,9 +42,14 @@ const styles = StyleSheet.create({
} }
}); });
const Header = ({ interface IHeader {
theme, headerLeft, headerTitle, headerRight theme: string;
}) => ( headerLeft(): void;
headerTitle(): void;
headerRight(): void;
}
const Header = ({ theme, headerLeft, headerTitle, headerRight }: IHeader) => (
<SafeAreaView style={{ backgroundColor: themes[theme].headerBackground }} edges={['top', 'left', 'right']}> <SafeAreaView style={{ backgroundColor: themes[theme].headerBackground }} edges={['top', 'left', 'right']}>
<View style={[styles.container, { ...themedHeader(theme).headerStyle }]}> <View style={[styles.container, { ...themedHeader(theme).headerStyle }]}>
{headerLeft ? headerLeft() : null} {headerLeft ? headerLeft() : null}
@ -47,11 +59,4 @@ const Header = ({
</SafeAreaView> </SafeAreaView>
); );
Header.propTypes = {
theme: PropTypes.string,
headerLeft: PropTypes.element,
headerTitle: PropTypes.element,
headerRight: PropTypes.element
};
export default withTheme(Header); export default withTheme(Header);

View File

@ -1,84 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { isIOS } from '../../utils/deviceInfo';
import I18n from '../../i18n';
import Container from './HeaderButtonContainer';
import Item from './HeaderButtonItem';
// Left
export const Drawer = React.memo(({ navigation, testID, ...props }) => (
<Container left>
<Item iconName='hamburguer' onPress={() => navigation.toggleDrawer()} testID={testID} {...props} />
</Container>
));
export const CloseModal = React.memo(({
navigation, testID, onPress = () => navigation.pop(), ...props
}) => (
<Container left>
<Item iconName='close' onPress={onPress} testID={testID} {...props} />
</Container>
));
export const CancelModal = React.memo(({ onPress, testID }) => (
<Container left>
{isIOS
? <Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} />
: <Item iconName='close' onPress={onPress} testID={testID} />
}
</Container>
));
// Right
export const More = React.memo(({ onPress, testID }) => (
<Container>
<Item iconName='kebab' onPress={onPress} testID={testID} />
</Container>
));
export const Download = React.memo(({ onPress, testID, ...props }) => (
<Container>
<Item iconName='download' onPress={onPress} testID={testID} {...props} />
</Container>
));
export const Preferences = React.memo(({ onPress, testID, ...props }) => (
<Container>
<Item iconName='settings' onPress={onPress} testID={testID} {...props} />
</Container>
));
export const Legal = React.memo(({ navigation, testID }) => (
<More onPress={() => navigation.navigate('LegalView')} testID={testID} />
));
Drawer.propTypes = {
navigation: PropTypes.object.isRequired,
testID: PropTypes.string.isRequired
};
CloseModal.propTypes = {
navigation: PropTypes.object.isRequired,
testID: PropTypes.string.isRequired,
onPress: PropTypes.func
};
CancelModal.propTypes = {
onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired
};
More.propTypes = {
onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired
};
Download.propTypes = {
onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired
};
Preferences.propTypes = {
onPress: PropTypes.func.isRequired,
testID: PropTypes.string.isRequired
};
Legal.propTypes = {
navigation: PropTypes.object.isRequired,
testID: PropTypes.string.isRequired
};

View File

@ -0,0 +1,60 @@
import React from 'react';
import { isIOS } from '../../utils/deviceInfo';
import I18n from '../../i18n';
import Container from './HeaderButtonContainer';
import Item from './HeaderButtonItem';
interface IHeaderButtonCommon {
navigation: any;
onPress(): void;
testID: string;
}
// Left
export const Drawer = React.memo(({ navigation, testID, ...props }: Partial<IHeaderButtonCommon>) => (
<Container left>
<Item iconName='hamburguer' onPress={() => navigation.toggleDrawer()} testID={testID} {...props} />
</Container>
));
export const CloseModal = React.memo(
({ navigation, testID, onPress = () => navigation.pop(), ...props }: IHeaderButtonCommon) => (
<Container left>
<Item iconName='close' onPress={onPress} testID={testID} {...props} />
</Container>
)
);
export const CancelModal = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => (
<Container left>
{isIOS ? (
<Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} />
) : (
<Item iconName='close' onPress={onPress} testID={testID} />
)}
</Container>
));
// Right
export const More = React.memo(({ onPress, testID }: Partial<IHeaderButtonCommon>) => (
<Container>
<Item iconName='kebab' onPress={onPress} testID={testID} />
</Container>
));
export const Download = React.memo(({ onPress, testID, ...props }: Partial<IHeaderButtonCommon>) => (
<Container>
<Item iconName='download' onPress={onPress} testID={testID} {...props} />
</Container>
));
export const Preferences = React.memo(({ onPress, testID, ...props }: Partial<IHeaderButtonCommon>) => (
<Container>
<Item iconName='settings' onPress={onPress} testID={testID} {...props} />
</Container>
));
export const Legal = React.memo(({ navigation, testID }: Partial<IHeaderButtonCommon>) => (
<More onPress={() => navigation.navigate('LegalView')} testID={testID} />
));

View File

@ -1,6 +1,10 @@
import React from 'react'; import React from 'react';
import { View, StyleSheet } from 'react-native'; import { StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
interface IHeaderButtonContainer {
children: JSX.Element;
left?: boolean;
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -16,21 +20,10 @@ const styles = StyleSheet.create({
} }
}); });
const Container = ({ children, left }) => ( const Container = ({ children, left = false }: IHeaderButtonContainer) => (
<View style={[styles.container, left ? styles.left : styles.right]}> <View style={[styles.container, left ? styles.left : styles.right]}>{children}</View>
{children}
</View>
); );
Container.propTypes = {
children: PropTypes.arrayOf(PropTypes.element),
left: PropTypes.bool
};
Container.defaultProps = {
left: false
};
Container.displayName = 'HeaderButton.Container'; Container.displayName = 'HeaderButton.Container';
export default Container; export default Container;

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { Text, StyleSheet, Platform } from 'react-native'; import { Platform, StyleSheet, Text } from 'react-native';
import PropTypes from 'prop-types';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
@ -8,8 +7,20 @@ import { withTheme } from '../../theme';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
interface IHeaderButtonItem {
title: string;
iconName: string;
onPress(): void;
testID: string;
theme: string;
badge(): void;
}
export const BUTTON_HIT_SLOP = { export const BUTTON_HIT_SLOP = {
top: 5, right: 5, bottom: 5, left: 5 top: 5,
right: 5,
bottom: 5,
left: 5
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -29,30 +40,19 @@ const styles = StyleSheet.create({
} }
}); });
const Item = ({ const Item = ({ title, iconName, onPress, testID, theme, badge }: IHeaderButtonItem) => (
title, iconName, onPress, testID, theme, badge
}) => (
<Touchable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} style={styles.container}> <Touchable onPress={onPress} testID={testID} hitSlop={BUTTON_HIT_SLOP} style={styles.container}>
<> <>
{ {iconName ? (
iconName <CustomIcon name={iconName} size={24} color={themes[theme].headerTintColor} />
? <CustomIcon name={iconName} size={24} color={themes[theme].headerTintColor} /> ) : (
: <Text style={[styles.title, { color: themes[theme].headerTintColor }]}>{title}</Text> <Text style={[styles.title, { color: themes[theme].headerTintColor }]}>{title}</Text>
} )}
{badge ? badge() : null} {badge ? badge() : null}
</> </>
</Touchable> </Touchable>
); );
Item.propTypes = {
onPress: PropTypes.func.isRequired,
title: PropTypes.string,
iconName: PropTypes.string,
testID: PropTypes.string,
theme: PropTypes.string,
badge: PropTypes.func
};
Item.displayName = 'HeaderButton.Item'; Item.displayName = 'HeaderButton.Item';
export default withTheme(Item); export default withTheme(Item);

View File

@ -15,12 +15,6 @@ const styles = StyleSheet.create({
} }
}); });
export const Badge = ({ ...props }) => ( export const Badge = ({ ...props }) => <UnreadBadge {...props} style={styles.badgeContainer} small />;
<UnreadBadge
{...props}
style={styles.badgeContainer}
small
/>
);
export default Badge; export default Badge;

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { StyleSheet, View, Text } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import PropTypes from 'prop-types';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Notifier } from 'react-native-notifier'; import { Notifier } from 'react-native-notifier';
@ -16,10 +15,13 @@ import { goRoom } from '../../utils/goRoom';
import Navigation from '../../lib/Navigation'; import Navigation from '../../lib/Navigation';
import { useOrientation } from '../../dimensions'; import { useOrientation } from '../../dimensions';
interface INotifierComponent {
notification: object;
isMasterDetail: boolean;
}
const AVATAR_SIZE = 48; const AVATAR_SIZE = 48;
const BUTTON_HIT_SLOP = { const BUTTON_HIT_SLOP = { top: 12, right: 12, bottom: 12, left: 12 };
top: 12, right: 12, bottom: 12, left: 12
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -64,16 +66,16 @@ const styles = StyleSheet.create({
const hideNotification = () => Notifier.hideNotification(); const hideNotification = () => Notifier.hideNotification();
const NotifierComponent = React.memo(({ notification, isMasterDetail }) => { const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifierComponent) => {
const { theme } = useTheme(); const { theme }: any = useTheme();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { isLandscape } = useOrientation(); const { isLandscape } = useOrientation();
const { text, payload } = notification; const { text, payload }: any = notification;
const { type, rid } = payload; const { type, rid } = payload;
const name = type === 'd' ? payload.sender.username : payload.name; const name = type === 'd' ? payload.sender.username : payload.name;
// if sub is not on local database, title and avatar will be null, so we use payload from notification // if sub is not on local database, title and avatar will be null, so we use payload from notification
const { title = name, avatar = name } = notification; const { title = name, avatar = name }: any = notification;
const onPress = () => { const onPress = () => {
const { prid, _id } = payload; const { prid, _id } = payload;
@ -81,7 +83,10 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }) => {
return; return;
} }
const item = { const item = {
rid, name: title, t: type, prid rid,
name: title,
t: type,
prid
}; };
if (isMasterDetail) { if (isMasterDetail) {
@ -94,47 +99,41 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }) => {
}; };
return ( return (
<View style={[ <View
styles.container, style={[
(isMasterDetail || isLandscape) && styles.small, styles.container,
{ (isMasterDetail || isLandscape) && styles.small,
backgroundColor: themes[theme].focusedBackground, {
borderColor: themes[theme].separatorColor, backgroundColor: themes[theme].focusedBackground,
marginTop: insets.top borderColor: themes[theme].separatorColor,
} marginTop: insets.top
]} }
> ]}>
<Touchable <Touchable
style={styles.content} style={styles.content}
onPress={onPress} onPress={onPress}
hitSlop={BUTTON_HIT_SLOP} hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()} background={Touchable.SelectableBackgroundBorderless()}>
>
<> <>
<Avatar text={avatar} size={AVATAR_SIZE} type={type} rid={rid} style={styles.avatar} /> <Avatar text={avatar} size={AVATAR_SIZE} type={type} rid={rid} style={styles.avatar} />
<View style={styles.inner}> <View style={styles.inner}>
<Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>{title}</Text> <Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>
<Text style={[styles.message, { color: themes[theme].titleText }]} numberOfLines={1}>{text}</Text> {title}
</Text>
<Text style={[styles.message, { color: themes[theme].titleText }]} numberOfLines={1}>
{text}
</Text>
</View> </View>
</> </>
</Touchable> </Touchable>
<Touchable <Touchable onPress={hideNotification} hitSlop={BUTTON_HIT_SLOP} background={Touchable.SelectableBackgroundBorderless()}>
onPress={hideNotification}
hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()}
>
<CustomIcon name='close' style={[styles.close, { color: themes[theme].titleText }]} size={20} /> <CustomIcon name='close' style={[styles.close, { color: themes[theme].titleText }]} size={20} />
</Touchable> </Touchable>
</View> </View>
); );
}); });
NotifierComponent.propTypes = { const mapStateToProps = (state: any) => ({
notification: PropTypes.object,
isMasterDetail: PropTypes.bool
};
const mapStateToProps = state => ({
isMasterDetail: state.app.isMasterDetail isMasterDetail: state.app.isMasterDetail
}); });

View File

@ -1,57 +0,0 @@
import React, { memo, useEffect } from 'react';
import PropTypes from 'prop-types';
import { NotifierRoot, Notifier, Easing } from 'react-native-notifier';
import { connect } from 'react-redux';
import { dequal } from 'dequal';
import NotifierComponent from './NotifierComponent';
import EventEmitter from '../../utils/events';
import Navigation from '../../lib/Navigation';
import { getActiveRoute } from '../../utils/navigation';
export const INAPP_NOTIFICATION_EMITTER = 'NotificationInApp';
const InAppNotification = memo(({ rooms, appState }) => {
const show = (notification) => {
if (appState !== 'foreground') {
return;
}
const { payload } = notification;
const state = Navigation.navigationRef.current?.getRootState();
const route = getActiveRoute(state);
if (payload.rid) {
if (rooms.includes(payload.rid) || route?.name === 'JitsiMeetView') {
return;
}
Notifier.showNotification({
showEasing: Easing.inOut(Easing.quad),
Component: NotifierComponent,
componentProps: {
notification
}
});
}
};
useEffect(() => {
const listener = EventEmitter.addEventListener(INAPP_NOTIFICATION_EMITTER, show);
return () => {
EventEmitter.removeListener(INAPP_NOTIFICATION_EMITTER, listener);
};
}, [rooms]);
return <NotifierRoot />;
}, (prevProps, nextProps) => dequal(prevProps.rooms, nextProps.rooms));
const mapStateToProps = state => ({
rooms: state.room.rooms,
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background'
});
InAppNotification.propTypes = {
rooms: PropTypes.array,
appState: PropTypes.string
};
export default connect(mapStateToProps)(InAppNotification);

View File

@ -0,0 +1,54 @@
import React, { memo, useEffect } from 'react';
import { Easing, Notifier, NotifierRoot } from 'react-native-notifier';
import { connect } from 'react-redux';
import { dequal } from 'dequal';
import NotifierComponent from './NotifierComponent';
import EventEmitter from '../../utils/events';
import Navigation from '../../lib/Navigation';
import { getActiveRoute } from '../../utils/navigation';
export const INAPP_NOTIFICATION_EMITTER = 'NotificationInApp';
const InAppNotification = memo(
({ rooms, appState }: { rooms: any; appState: string }) => {
const show = (notification: any) => {
if (appState !== 'foreground') {
return;
}
const { payload } = notification;
const state = Navigation.navigationRef.current?.getRootState();
const route = getActiveRoute(state);
if (payload.rid) {
if (rooms.includes(payload.rid) || route?.name === 'JitsiMeetView') {
return;
}
Notifier.showNotification({
showEasing: Easing.inOut(Easing.quad),
Component: NotifierComponent,
componentProps: {
notification
}
});
}
};
useEffect(() => {
const listener = EventEmitter.addEventListener(INAPP_NOTIFICATION_EMITTER, show);
return () => {
EventEmitter.removeListener(INAPP_NOTIFICATION_EMITTER, listener);
};
}, [rooms]);
return <NotifierRoot />;
},
(prevProps, nextProps) => dequal(prevProps.rooms, nextProps.rooms)
);
const mapStateToProps = (state: any) => ({
rooms: state.room.rooms,
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background'
});
export default connect(mapStateToProps)(InAppNotification);

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { ScrollView, StyleSheet } from 'react-native'; import { ScrollView, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
@ -10,21 +10,20 @@ const styles = StyleSheet.create({
} }
}); });
const ListContainer = React.memo(({ children, ...props }) => ( interface IListContainer {
children: JSX.Element;
}
const ListContainer = React.memo(({ children, ...props }: IListContainer) => (
// @ts-ignore
<ScrollView <ScrollView
contentContainerStyle={styles.container} contentContainerStyle={styles.container}
scrollIndicatorInsets={{ right: 1 }} // https://github.com/facebook/react-native/issues/26610#issuecomment-539843444 scrollIndicatorInsets={{ right: 1 }} // https://github.com/facebook/react-native/issues/26610#issuecomment-539843444
{...scrollPersistTaps} {...scrollPersistTaps}
{...props} {...props}>
>
{children} {children}
</ScrollView> </ScrollView>
)); ));
ListContainer.propTypes = {
children: PropTypes.array.isRequired
};
ListContainer.displayName = 'List.Container'; ListContainer.displayName = 'List.Container';
export default withTheme(ListContainer); export default withTheme(ListContainer);

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { View, Text, StyleSheet } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import PropTypes from 'prop-types';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
@ -19,22 +18,20 @@ const styles = StyleSheet.create({
} }
}); });
const ListHeader = React.memo(({ title, theme, translateTitle }) => ( interface IListHeader {
title: string;
theme: string;
translateTitle: boolean;
}
const ListHeader = React.memo(({ title, theme, translateTitle = true }: IListHeader) => (
<View style={styles.container}> <View style={styles.container}>
<Text style={[styles.title, { color: themes[theme].infoText }]} numberOfLines={1}>{translateTitle ? I18n.t(title) : title}</Text> <Text style={[styles.title, { color: themes[theme].infoText }]} numberOfLines={1}>
{translateTitle ? I18n.t(title) : title}
</Text>
</View> </View>
)); ));
ListHeader.propTypes = {
title: PropTypes.string,
theme: PropTypes.string,
translateTitle: PropTypes.bool
};
ListHeader.defaultProps = {
translateTitle: true
};
ListHeader.displayName = 'List.Header'; ListHeader.displayName = 'List.Header';
export default withTheme(ListHeader); export default withTheme(ListHeader);

View File

@ -1,44 +0,0 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons';
import { withTheme } from '../../theme';
import { ICON_SIZE } from './constants';
const styles = StyleSheet.create({
icon: {
alignItems: 'center',
justifyContent: 'center'
}
});
const ListIcon = React.memo(({
theme,
name,
color,
style,
testID
}) => (
<View style={[styles.icon, style]}>
<CustomIcon
name={name}
color={color ?? themes[theme].auxiliaryText}
size={ICON_SIZE}
testID={testID}
/>
</View>
));
ListIcon.propTypes = {
theme: PropTypes.string,
name: PropTypes.string,
color: PropTypes.string,
style: PropTypes.object,
testID: PropTypes.string
};
ListIcon.displayName = 'List.Icon';
export default withTheme(ListIcon);

View File

@ -0,0 +1,32 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons';
import { withTheme } from '../../theme';
import { ICON_SIZE } from './constants';
interface IListIcon {
theme: string;
name: string;
color: string;
style: object;
testID: string;
}
const styles = StyleSheet.create({
icon: {
alignItems: 'center',
justifyContent: 'center'
}
});
const ListIcon = React.memo(({ theme, name, color, style, testID }: IListIcon) => (
<View style={[styles.icon, style]}>
<CustomIcon name={name} color={color ?? themes[theme].auxiliaryText} size={ICON_SIZE} testID={testID} />
</View>
));
ListIcon.displayName = 'List.Icon';
export default withTheme(ListIcon);

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { View, Text, StyleSheet } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import PropTypes from 'prop-types';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
@ -19,22 +18,18 @@ const styles = StyleSheet.create({
} }
}); });
const ListInfo = React.memo(({ info, translateInfo, theme }) => ( interface IListHeader {
info: string;
theme: string;
translateInfo: boolean;
}
const ListInfo = React.memo(({ info, theme, translateInfo = true }: IListHeader) => (
<View style={styles.container}> <View style={styles.container}>
<Text style={[styles.text, { color: themes[theme].infoText }]}>{translateInfo ? I18n.t(info) : info}</Text> <Text style={[styles.text, { color: themes[theme].infoText }]}>{translateInfo ? I18n.t(info) : info}</Text>
</View> </View>
)); ));
ListInfo.propTypes = {
info: PropTypes.string,
theme: PropTypes.string,
translateInfo: PropTypes.bool
};
ListInfo.defaultProps = {
translateInfo: true
};
ListInfo.displayName = 'List.Info'; ListInfo.displayName = 'List.Info';
export default withTheme(ListInfo); export default withTheme(ListInfo);

View File

@ -1,163 +0,0 @@
import React from 'react';
import {
View, Text, StyleSheet, I18nManager
} from 'react-native';
import PropTypes from 'prop-types';
import Touch from '../../utils/touch';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import { withTheme } from '../../theme';
import I18n from '../../i18n';
import { Icon } from '.';
import { BASE_HEIGHT, ICON_SIZE, PADDING_HORIZONTAL } from './constants';
import { withDimensions } from '../../dimensions';
import { CustomIcon } from '../../lib/Icons';
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: PADDING_HORIZONTAL
},
leftContainer: {
paddingRight: PADDING_HORIZONTAL
},
rightContainer: {
paddingLeft: PADDING_HORIZONTAL
},
disabled: {
opacity: 0.3
},
textContainer: {
flex: 1,
justifyContent: 'center'
},
textAlertContainer: {
flexDirection: 'row',
alignItems: 'center'
},
alertIcon: {
paddingLeft: 4
},
title: {
flexShrink: 1,
fontSize: 16,
...sharedStyles.textRegular
},
subtitle: {
fontSize: 14,
...sharedStyles.textRegular
},
actionIndicator: {
...I18nManager.isRTL
? { transform: [{ rotate: '180deg' }] }
: {}
}
});
const Content = React.memo(({
title, subtitle, disabled, testID, left, right, color, theme, translateTitle, translateSubtitle, showActionIndicator, fontScale, alert
}) => (
<View style={[styles.container, disabled && styles.disabled, { height: BASE_HEIGHT * fontScale }]} testID={testID}>
{left
? (
<View style={styles.leftContainer}>
{left()}
</View>
)
: null}
<View style={styles.textContainer}>
<View style={styles.textAlertContainer}>
<Text style={[styles.title, { color: color || themes[theme].titleText }]} numberOfLines={1}>{translateTitle ? I18n.t(title) : title}</Text>
{alert ? (
<CustomIcon style={[styles.alertIcon, { color: themes[theme].dangerColor }]} size={ICON_SIZE} name='info' />
) : null}
</View>
{subtitle
? <Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>{translateSubtitle ? I18n.t(subtitle) : subtitle}</Text>
: null
}
</View>
{right || showActionIndicator
? (
<View style={styles.rightContainer}>
{right ? right() : null}
{showActionIndicator ? <Icon name='chevron-right' style={styles.actionIndicator} /> : null}
</View>
)
: null}
</View>
));
const Button = React.memo(({
onPress, backgroundColor, underlayColor, ...props
}) => (
<Touch
onPress={() => onPress(props.title)}
style={{ backgroundColor: backgroundColor || themes[props.theme].backgroundColor }}
underlayColor={underlayColor}
enabled={!props.disabled}
theme={props.theme}
>
<Content {...props} />
</Touch>
));
const ListItem = React.memo(({ ...props }) => {
if (props.onPress) {
return <Button {...props} />;
}
return (
<View style={{ backgroundColor: props.backgroundColor || themes[props.theme].backgroundColor }}>
<Content {...props} />
</View>
);
});
ListItem.propTypes = {
onPress: PropTypes.func,
theme: PropTypes.string,
backgroundColor: PropTypes.string
};
ListItem.displayName = 'List.Item';
Content.propTypes = {
title: PropTypes.string.isRequired,
subtitle: PropTypes.string,
left: PropTypes.func,
right: PropTypes.func,
disabled: PropTypes.bool,
testID: PropTypes.string,
theme: PropTypes.string,
color: PropTypes.string,
translateTitle: PropTypes.bool,
translateSubtitle: PropTypes.bool,
showActionIndicator: PropTypes.bool,
fontScale: PropTypes.number,
alert: PropTypes.bool
};
Content.defaultProps = {
translateTitle: true,
translateSubtitle: true,
showActionIndicator: false
};
Button.propTypes = {
title: PropTypes.string,
onPress: PropTypes.func,
disabled: PropTypes.bool,
theme: PropTypes.string,
backgroundColor: PropTypes.string,
underlayColor: PropTypes.string
};
Button.defaultProps = {
disabled: false
};
export default withTheme(withDimensions(ListItem));

View File

@ -0,0 +1,154 @@
import React from 'react';
import { I18nManager, StyleSheet, Text, View } from 'react-native';
import Touch from '../../utils/touch';
import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import { withTheme } from '../../theme';
import I18n from '../../i18n';
import { Icon } from '.';
import { BASE_HEIGHT, ICON_SIZE, PADDING_HORIZONTAL } from './constants';
import { withDimensions } from '../../dimensions';
import { CustomIcon } from '../../lib/Icons';
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: PADDING_HORIZONTAL
},
leftContainer: {
paddingRight: PADDING_HORIZONTAL
},
rightContainer: {
paddingLeft: PADDING_HORIZONTAL
},
disabled: {
opacity: 0.3
},
textContainer: {
flex: 1,
justifyContent: 'center'
},
textAlertContainer: {
flexDirection: 'row',
alignItems: 'center'
},
alertIcon: {
paddingLeft: 4
},
title: {
flexShrink: 1,
fontSize: 16,
...sharedStyles.textRegular
},
subtitle: {
fontSize: 14,
...sharedStyles.textRegular
},
actionIndicator: {
...(I18nManager.isRTL ? { transform: [{ rotate: '180deg' }] } : {})
}
});
interface IListItemContent {
title?: string;
subtitle?: string;
left?: Function;
right?: Function;
disabled?: boolean;
testID?: string;
theme: string;
color?: string;
translateTitle?: boolean;
translateSubtitle?: boolean;
showActionIndicator?: boolean;
fontScale?: number;
alert?: boolean;
}
const Content = React.memo(
({
title,
subtitle,
disabled,
testID,
left,
right,
color,
theme,
fontScale,
alert,
translateTitle = true,
translateSubtitle = true,
showActionIndicator = false
}: IListItemContent) => (
<View style={[styles.container, disabled && styles.disabled, { height: BASE_HEIGHT * fontScale! }]} testID={testID}>
{left ? <View style={styles.leftContainer}>{left()}</View> : null}
<View style={styles.textContainer}>
<View style={styles.textAlertContainer}>
<Text style={[styles.title, { color: color || themes[theme].titleText }]} numberOfLines={1}>
{translateTitle ? I18n.t(title) : title}
</Text>
{alert ? (
<CustomIcon style={[styles.alertIcon, { color: themes[theme].dangerColor }]} size={ICON_SIZE} name='info' />
) : null}
</View>
{subtitle ? (
<Text style={[styles.subtitle, { color: themes[theme].auxiliaryText }]} numberOfLines={1}>
{translateSubtitle ? I18n.t(subtitle) : subtitle}
</Text>
) : null}
</View>
{right || showActionIndicator ? (
<View style={styles.rightContainer}>
{right ? right() : null}
{showActionIndicator ? <Icon name='chevron-right' style={styles.actionIndicator} /> : null}
</View>
) : null}
</View>
)
);
interface IListItemButton {
title?: string;
onPress: Function;
disabled?: boolean;
theme: string;
backgroundColor: string;
underlayColor?: string;
}
const Button = React.memo(({ onPress, backgroundColor, underlayColor, ...props }: IListItemButton) => (
<Touch
onPress={() => onPress(props.title)}
style={{ backgroundColor: backgroundColor || themes[props.theme].backgroundColor }}
underlayColor={underlayColor}
enabled={!props.disabled}
theme={props.theme}>
<Content {...props} />
</Touch>
));
interface IListItem {
onPress: Function;
theme: string;
backgroundColor: string;
}
const ListItem = React.memo(({ ...props }: IListItem) => {
if (props.onPress) {
return <Button {...props} />;
}
return (
<View style={{ backgroundColor: props.backgroundColor || themes[props.theme].backgroundColor }}>
<Content {...props} />
</View>
);
});
ListItem.displayName = 'List.Item';
export default withTheme(withDimensions(ListItem));

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { View, StyleSheet } from 'react-native'; import { StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { Header } from '.'; import { Header } from '.';
@ -10,19 +10,19 @@ const styles = StyleSheet.create({
} }
}); });
const ListSection = React.memo(({ children, title, translateTitle }) => ( interface IListSection {
children: JSX.Element;
title: string;
translateTitle: boolean;
}
const ListSection = React.memo(({ children, title, translateTitle }: IListSection) => (
<View style={styles.container}> <View style={styles.container}>
{title ? <Header {...{ title, translateTitle }} /> : null} {title ? <Header {...{ title, translateTitle }} /> : null}
{children} {children}
</View> </View>
)); ));
ListSection.propTypes = {
children: PropTypes.array.isRequired,
title: PropTypes.string,
translateTitle: PropTypes.bool
};
ListSection.displayName = 'List.Section'; ListSection.displayName = 'List.Section';
export default withTheme(ListSection); export default withTheme(ListSection);

View File

@ -1,32 +0,0 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
const styles = StyleSheet.create({
separator: {
height: StyleSheet.hairlineWidth
}
});
const ListSeparator = React.memo(({ style, theme }) => (
<View
style={[
styles.separator,
style,
{ backgroundColor: themes[theme].separatorColor }
]}
/>
));
ListSeparator.propTypes = {
style: PropTypes.object,
theme: PropTypes.string
};
ListSeparator.displayName = 'List.Separator';
export default withTheme(ListSeparator);

View File

@ -0,0 +1,24 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
const styles = StyleSheet.create({
separator: {
height: StyleSheet.hairlineWidth
}
});
interface IListSeparator {
style: object;
theme: string;
}
const ListSeparator = React.memo(({ style, theme }: IListSeparator) => (
<View style={[styles.separator, style, { backgroundColor: themes[theme].separatorColor }]} />
));
ListSeparator.displayName = 'List.Separator';
export default withTheme(ListSeparator);

View File

@ -1,8 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { Animated, Modal, StyleSheet, View } from 'react-native';
import {
StyleSheet, Modal, Animated, View
} from 'react-native';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
@ -19,54 +17,51 @@ const styles = StyleSheet.create({
} }
}); });
class Loading extends React.PureComponent { interface ILoadingProps {
static propTypes = { visible: boolean;
visible: PropTypes.bool, theme: string;
theme: PropTypes.string }
}
class Loading extends React.PureComponent<ILoadingProps, any> {
state = { state = {
scale: new Animated.Value(1), scale: new Animated.Value(1),
opacity: new Animated.Value(0) opacity: new Animated.Value(0)
} };
private opacityAnimation: any;
private scaleAnimation: any;
componentDidMount() { componentDidMount() {
const { opacity, scale } = this.state; const { opacity, scale } = this.state;
const { visible } = this.props; const { visible } = this.props;
this.opacityAnimation = Animated.timing( this.opacityAnimation = Animated.timing(opacity, {
opacity, toValue: 1,
{ duration: 200,
toValue: 1, useNativeDriver: true
duration: 200, });
useNativeDriver: true this.scaleAnimation = Animated.loop(
} Animated.sequence([
); Animated.timing(scale, {
this.scaleAnimation = Animated.loop(Animated.sequence([
Animated.timing(
scale,
{
toValue: 0, toValue: 0,
duration: 1000, duration: 1000,
useNativeDriver: true useNativeDriver: true
} }),
), Animated.timing(scale, {
Animated.timing(
scale,
{
toValue: 1, toValue: 1,
duration: 1000, duration: 1000,
useNativeDriver: true useNativeDriver: true
} })
) ])
])); );
if (visible) { if (visible) {
this.startAnimations(); this.startAnimations();
} }
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps: any) {
const { visible } = this.props; const { visible } = this.props;
if (visible && visible !== prevProps.visible) { if (visible && visible !== prevProps.visible) {
this.startAnimations(); this.startAnimations();
@ -107,29 +102,30 @@ class Loading extends React.PureComponent {
}); });
return ( return (
<Modal <Modal visible={visible} transparent onRequestClose={() => {}}>
visible={visible} <View style={styles.container} testID='loading'>
transparent
onRequestClose={() => {}}
>
<View
style={styles.container}
testID='loading'
>
<Animated.View <Animated.View
style={[{ style={[
...StyleSheet.absoluteFill, {
backgroundColor: themes[theme].backdropColor, // @ts-ignore
opacity: opacityAnimation ...StyleSheet.absoluteFill,
}]} backgroundColor: themes[theme].backdropColor,
opacity: opacityAnimation
}
]}
/> />
<Animated.Image <Animated.Image
source={require('../static/images/logo.png')} source={require('../static/images/logo.png')}
style={[styles.image, { style={[
transform: [{ styles.image,
scale: scaleAnimation {
}] transform: [
}]} {
scale: scaleAnimation
}
]
}
]}
/> />
</View> </View>
</Modal> </Modal>
@ -137,4 +133,4 @@ class Loading extends React.PureComponent {
} }
} }
export default (withTheme(Loading)); export default withTheme(Loading);

View File

@ -1,8 +1,5 @@
import React from 'react'; import React from 'react';
import { import { Animated, Easing, Linking, StyleSheet, Text, View } from 'react-native';
View, StyleSheet, Text, Animated, Easing, Linking
} from 'react-native';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import * as AppleAuthentication from 'expo-apple-authentication'; import * as AppleAuthentication from 'expo-apple-authentication';
@ -15,7 +12,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 { events, logEvent } from '../utils/log';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
@ -60,153 +57,175 @@ const styles = StyleSheet.create({
} }
}); });
class LoginServices extends React.PureComponent { interface IOpenOAuth {
static propTypes = { url?: string;
navigation: PropTypes.object, ssoToken?: string;
server: PropTypes.string, authType?: string;
services: PropTypes.object, }
Gitlab_URL: PropTypes.string,
CAS_enabled: PropTypes.bool, interface IService {
CAS_login_url: PropTypes.string, name: string;
separator: PropTypes.bool, service: string;
theme: PropTypes.string authType: string;
} buttonColor: string;
buttonLabelColor: string;
}
interface ILoginServicesProps {
navigation: any;
server: string;
services: {
facebook: { clientId: string };
github: { clientId: string };
gitlab: { clientId: string };
google: { clientId: string };
linkedin: { clientId: string };
'meteor-developer': { clientId: string };
wordpress: { clientId: string; serverURL: string };
};
Gitlab_URL: string;
CAS_enabled: boolean;
CAS_login_url: string;
separator: boolean;
theme: string;
}
class LoginServices extends React.PureComponent<ILoginServicesProps, any> {
private _animation: any;
static defaultProps = { static defaultProps = {
separator: true separator: true
} };
state = { state = {
collapsed: true, collapsed: true,
servicesHeight: new Animated.Value(SERVICES_COLLAPSED_HEIGHT) servicesHeight: new Animated.Value(SERVICES_COLLAPSED_HEIGHT)
} };
onPressFacebook = () => { onPressFacebook = () => {
logEvent(events.ENTER_WITH_FACEBOOK); logEvent(events.ENTER_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';
const redirect_uri = `${ server }/_oauth/facebook?close`; const redirect_uri = `${server}/_oauth/facebook?close`;
const scope = 'email'; const scope = 'email';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&display=touch`; const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&display=touch`;
this.openOAuth({ url: `${ endpoint }${ params }` }); this.openOAuth({ url: `${endpoint}${params}` });
} };
onPressGithub = () => { onPressGithub = () => {
logEvent(events.ENTER_WITH_GITHUB); logEvent(events.ENTER_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')}`;
const redirect_uri = `${ server }/_oauth/github?close`; const redirect_uri = `${server}/_oauth/github?close`;
const scope = 'user:email'; const scope = 'user:email';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }`; const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}`;
this.openOAuth({ url: `${ endpoint }${ encodeURIComponent(params) }` }); this.openOAuth({ url: `${endpoint}${encodeURIComponent(params)}` });
} };
onPressGitlab = () => { onPressGitlab = () => {
logEvent(events.ENTER_WITH_GITLAB); logEvent(events.ENTER_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';
const endpoint = `${ baseURL }/oauth/authorize`; const endpoint = `${baseURL}/oauth/authorize`;
const redirect_uri = `${ server }/_oauth/gitlab?close`; const redirect_uri = `${server}/_oauth/gitlab?close`;
const scope = 'read_user'; const scope = 'read_user';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
this.openOAuth({ url: `${ endpoint }${ params }` }); this.openOAuth({ url: `${endpoint}${params}` });
} };
onPressGoogle = () => { onPressGoogle = () => {
logEvent(events.ENTER_WITH_GOOGLE); logEvent(events.ENTER_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';
const redirect_uri = `${ server }/_oauth/google?close`; const redirect_uri = `${server}/_oauth/google?close`;
const scope = 'email'; const scope = 'email';
const state = this.getOAuthState(LOGIN_STYPE_REDIRECT); const state = this.getOAuthState(LOGIN_STYPE_REDIRECT);
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
Linking.openURL(`${ endpoint }${ params }`); Linking.openURL(`${endpoint}${params}`);
} };
onPressLinkedin = () => { onPressLinkedin = () => {
logEvent(events.ENTER_WITH_LINKEDIN); logEvent(events.ENTER_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';
const redirect_uri = `${ server }/_oauth/linkedin?close`; const redirect_uri = `${server}/_oauth/linkedin?close`;
const scope = 'r_liteprofile,r_emailaddress'; const scope = 'r_liteprofile,r_emailaddress';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
this.openOAuth({ url: `${ endpoint }${ params }` }); this.openOAuth({ url: `${endpoint}${params}` });
} };
onPressMeteor = () => { onPressMeteor = () => {
logEvent(events.ENTER_WITH_METEOR); logEvent(events.ENTER_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';
const redirect_uri = `${ server }/_oauth/meteor-developer`; const redirect_uri = `${server}/_oauth/meteor-developer`;
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&state=${ state }&response_type=code`; const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&state=${state}&response_type=code`;
this.openOAuth({ url: `${ endpoint }${ params }` }); this.openOAuth({ url: `${endpoint}${params}` });
} };
onPressTwitter = () => { onPressTwitter = () => {
logEvent(events.ENTER_WITH_TWITTER); logEvent(events.ENTER_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}`;
this.openOAuth({ url }); this.openOAuth({ url });
} };
onPressWordpress = () => { onPressWordpress = () => {
logEvent(events.ENTER_WITH_WORDPRESS); logEvent(events.ENTER_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`;
const redirect_uri = `${ server }/_oauth/wordpress?close`; const redirect_uri = `${server}/_oauth/wordpress?close`;
const scope = 'openid'; const scope = 'openid';
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`; const params = `?client_id=${clientId}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}&response_type=code`;
this.openOAuth({ url: `${ endpoint }${ params }` }); this.openOAuth({ url: `${endpoint}${params}` });
} };
onPressCustomOAuth = (loginService) => { onPressCustomOAuth = (loginService: any) => {
logEvent(events.ENTER_WITH_CUSTOM_OAUTH); logEvent(events.ENTER_WITH_CUSTOM_OAUTH);
const { server } = this.props; const { server } = this.props;
const { const { serverURL, authorizePath, clientId, scope, service } = loginService;
serverURL, authorizePath, clientId, scope, service const redirectUri = `${server}/_oauth/${service}`;
} = loginService;
const redirectUri = `${ server }/_oauth/${ service }`;
const state = this.getOAuthState(); const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirectUri }&response_type=code&state=${ state }&scope=${ scope }`; const params = `?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&state=${state}&scope=${scope}`;
const domain = `${ serverURL }`; const domain = `${serverURL}`;
const absolutePath = `${ authorizePath }${ params }`; const absolutePath = `${authorizePath}${params}`;
const url = absolutePath.includes(domain) ? absolutePath : domain + absolutePath; const url = absolutePath.includes(domain) ? absolutePath : domain + absolutePath;
this.openOAuth({ url }); this.openOAuth({ url });
} };
onPressSaml = (loginService) => { onPressSaml = (loginService: any) => {
logEvent(events.ENTER_WITH_SAML); logEvent(events.ENTER_WITH_SAML);
const { server } = this.props; const { server } = this.props;
const { clientConfig } = loginService; const { clientConfig } = loginService;
const { provider } = clientConfig; const { provider } = clientConfig;
const ssoToken = random(17); const ssoToken = random(17);
const url = `${ server }/_saml/authorize/${ provider }/${ ssoToken }`; const url = `${server}/_saml/authorize/${provider}/${ssoToken}`;
this.openOAuth({ url, ssoToken, authType: 'saml' }); this.openOAuth({ url, ssoToken, authType: 'saml' });
} };
onPressCas = () => { onPressCas = () => {
logEvent(events.ENTER_WITH_CAS); logEvent(events.ENTER_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}`;
this.openOAuth({ url, ssoToken, authType: 'cas' }); this.openOAuth({ url, ssoToken, authType: 'cas' });
} };
onPressAppleLogin = async() => { onPressAppleLogin = async () => {
logEvent(events.ENTER_WITH_APPLE); logEvent(events.ENTER_WITH_APPLE);
try { try {
const { fullName, email, identityToken } = await AppleAuthentication.signInAsync({ const { fullName, email, identityToken } = await AppleAuthentication.signInAsync({
@ -220,11 +239,11 @@ class LoginServices extends React.PureComponent {
} catch { } catch {
logEvent(events.ENTER_WITH_APPLE_F); logEvent(events.ENTER_WITH_APPLE_F);
} }
} };
getOAuthState = (loginStyle = LOGIN_STYPE_POPUP) => { getOAuthState = (loginStyle = LOGIN_STYPE_POPUP) => {
const credentialToken = random(43); const credentialToken = random(43);
let obj = { loginStyle, credentialToken, isCordova: true }; let obj: any = { loginStyle, credentialToken, isCordova: true };
if (loginStyle === LOGIN_STYPE_REDIRECT) { if (loginStyle === LOGIN_STYPE_REDIRECT) {
obj = { obj = {
...obj, ...obj,
@ -232,24 +251,26 @@ class LoginServices extends React.PureComponent {
}; };
} }
return Base64.encodeURI(JSON.stringify(obj)); return Base64.encodeURI(JSON.stringify(obj));
} };
openOAuth = ({ url, ssoToken, authType = 'oauth' }) => { openOAuth = ({ url, ssoToken, authType = 'oauth' }: IOpenOAuth) => {
const { navigation } = this.props; const { navigation } = this.props;
navigation.navigate('AuthenticationWebView', { url, authType, ssoToken }); navigation.navigate('AuthenticationWebView', { url, authType, ssoToken });
} };
transitionServicesTo = (height) => { transitionServicesTo = (height: number) => {
const { servicesHeight } = this.state; const { servicesHeight } = this.state;
if (this._animation) { if (this._animation) {
this._animation.stop(); this._animation.stop();
} }
// @ts-ignore
this._animation = Animated.timing(servicesHeight, { this._animation = Animated.timing(servicesHeight, {
toValue: height, toValue: height,
duration: 300, duration: 300,
// @ts-ignore
easing: Easing.easeOutCubic easing: Easing.easeOutCubic
}).start(); }).start();
} };
toggleServices = () => { toggleServices = () => {
const { collapsed } = this.state; const { collapsed } = this.state;
@ -260,11 +281,11 @@ class LoginServices extends React.PureComponent {
} else { } else {
this.transitionServicesTo(SERVICES_COLLAPSED_HEIGHT); this.transitionServicesTo(SERVICES_COLLAPSED_HEIGHT);
} }
this.setState(prevState => ({ collapsed: !prevState.collapsed })); this.setState((prevState: any) => ({ collapsed: !prevState.collapsed }));
} };
getSocialOauthProvider = (name) => { getSocialOauthProvider = (name: string) => {
const oauthProviders = { const oauthProviders: any = {
facebook: this.onPressFacebook, facebook: this.onPressFacebook,
github: this.onPressGithub, github: this.onPressGithub,
gitlab: this.onPressGitlab, gitlab: this.onPressGitlab,
@ -275,7 +296,7 @@ class LoginServices extends React.PureComponent {
wordpress: this.onPressWordpress wordpress: this.onPressWordpress
}; };
return oauthProviders[name]; return oauthProviders[name];
} };
renderServicesSeparator = () => { renderServicesSeparator = () => {
const { collapsed } = this.state; const { collapsed } = this.state;
@ -301,13 +322,13 @@ class LoginServices extends React.PureComponent {
return <OrSeparator theme={theme} />; return <OrSeparator theme={theme} />;
} }
return null; return null;
} };
renderItem = (service) => { renderItem = (service: IService) => {
const { CAS_enabled, theme } = this.props; const { CAS_enabled, theme } = this.props;
let { name } = service; let { name } = service;
name = name === 'meteor-developer' ? 'meteor' : name; name = name === 'meteor-developer' ? 'meteor' : name;
const icon = `${ name }-monochromatic`; const icon = `${name}-monochromatic`;
const isSaml = service.service === 'saml'; const isSaml = service.service === 'saml';
let onPress = () => {}; let onPress = () => {};
@ -357,15 +378,16 @@ class LoginServices extends React.PureComponent {
style={[styles.serviceButton, { backgroundColor }]} style={[styles.serviceButton, { backgroundColor }]}
theme={theme} theme={theme}
activeOpacity={0.5} activeOpacity={0.5}
underlayColor={themes[theme].buttonText} underlayColor={themes[theme].buttonText}>
>
<View style={styles.serviceButtonContainer}> <View style={styles.serviceButtonContainer}>
{service.authType === 'oauth' || service.authType === 'apple' ? <CustomIcon name={icon} size={24} color={themes[theme].titleText} style={styles.serviceIcon} /> : null} {service.authType === 'oauth' || service.authType === 'apple' ? (
<CustomIcon name={icon} size={24} color={themes[theme].titleText} style={styles.serviceIcon} />
) : null}
<Text style={[styles.serviceText, { color: themes[theme].titleText }]}>{buttonText}</Text> <Text style={[styles.serviceText, { color: themes[theme].titleText }]}>{buttonText}</Text>
</View> </View>
</Touch> </Touch>
); );
} };
render() { render() {
const { servicesHeight } = this.state; const { servicesHeight } = this.state;
@ -379,23 +401,21 @@ class LoginServices extends React.PureComponent {
if (length > 3 && separator) { if (length > 3 && separator) {
return ( return (
<> <>
<Animated.View style={style}> <Animated.View style={style}>{Object.values(services).map((service: any) => this.renderItem(service))}</Animated.View>
{Object.values(services).map(service => this.renderItem(service))}
</Animated.View>
{this.renderServicesSeparator()} {this.renderServicesSeparator()}
</> </>
); );
} }
return ( return (
<> <>
{Object.values(services).map(service => this.renderItem(service))} {Object.values(services).map((service: any) => this.renderItem(service))}
{this.renderServicesSeparator()} {this.renderServicesSeparator()}
</> </>
); );
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
server: state.server.server, server: state.server.server,
Gitlab_URL: state.settings.API_Gitlab_URL, Gitlab_URL: state.settings.API_Gitlab_URL,
CAS_enabled: state.settings.CAS_enabled, CAS_enabled: state.settings.CAS_enabled,

View File

@ -1,8 +1,5 @@
import React, { useEffect, useState, useCallback } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import { FlatList, StyleSheet, Text, View } from 'react-native';
import {
View, Text, FlatList, StyleSheet
} from 'react-native';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
@ -13,6 +10,27 @@ import database from '../../lib/database';
import { Button } from '../ActionSheet'; import { Button } from '../ActionSheet';
import { useDimensions } from '../../dimensions'; import { useDimensions } from '../../dimensions';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { IEmoji } from '../EmojiPicker/interfaces';
interface IHeader {
handleReaction: Function;
server: string;
message: object;
isMasterDetail: boolean;
theme: string;
}
interface THeaderItem {
item: IEmoji;
onReaction: Function;
server: string;
theme: string;
}
interface THeaderFooter {
onReaction: any;
theme: string;
}
export const HEADER_HEIGHT = 36; export const HEADER_HEIGHT = 36;
const ITEM_SIZE = 36; const ITEM_SIZE = 36;
@ -43,65 +61,47 @@ const styles = StyleSheet.create({
} }
}); });
const keyExtractor = item => item?.id || item; const keyExtractor = (item: any) => item?.id || item;
const DEFAULT_EMOJIS = ['clap', '+1', 'heart_eyes', 'grinning', 'thinking_face', 'smiley']; const DEFAULT_EMOJIS = ['clap', '+1', 'heart_eyes', 'grinning', 'thinking_face', 'smiley'];
const HeaderItem = React.memo(({ const HeaderItem = React.memo(({ item, onReaction, server, theme }: THeaderItem) => (
item, onReaction, server, theme
}) => (
<Button <Button
testID={`message-actions-emoji-${ item.content || item }`} testID={`message-actions-emoji-${item.content || item}`}
onPress={() => onReaction({ emoji: `:${ item.content || item }:` })} onPress={() => onReaction({ emoji: `:${item.content || item}:` })}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]} style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
theme={theme} theme={theme}>
>
{item?.isCustom ? ( {item?.isCustom ? (
<CustomEmoji style={styles.customEmoji} emoji={item} baseUrl={server} /> <CustomEmoji style={styles.customEmoji} emoji={item} baseUrl={server} />
) : ( ) : (
<Text style={styles.headerIcon}> <Text style={styles.headerIcon}>{shortnameToUnicode(`:${item.content || item}:`)}</Text>
{shortnameToUnicode(`:${ item.content || item }:`)}
</Text>
)} )}
</Button> </Button>
)); ));
HeaderItem.propTypes = {
item: PropTypes.string,
onReaction: PropTypes.func,
server: PropTypes.string,
theme: PropTypes.string
};
const HeaderFooter = React.memo(({ onReaction, theme }) => ( const HeaderFooter = React.memo(({ onReaction, theme }: THeaderFooter) => (
<Button <Button
testID='add-reaction' testID='add-reaction'
onPress={onReaction} onPress={onReaction}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]} style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
theme={theme} theme={theme}>
>
<CustomIcon name='reaction-add' size={24} color={themes[theme].bodyText} /> <CustomIcon name='reaction-add' size={24} color={themes[theme].bodyText} />
</Button> </Button>
)); ));
HeaderFooter.propTypes = {
onReaction: PropTypes.func,
theme: PropTypes.string
};
const Header = React.memo(({ const Header = React.memo(({ handleReaction, server, message, isMasterDetail, theme }: IHeader) => {
handleReaction, server, message, isMasterDetail, theme
}) => {
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
const { width, height } = useDimensions(); const { width, height }: any = useDimensions();
const setEmojis = async() => { const setEmojis = async () => {
try { try {
const db = database.active; const db = database.active;
const freqEmojiCollection = db.get('frequently_used_emojis'); const freqEmojiCollection = db.get('frequently_used_emojis');
let freqEmojis = await freqEmojiCollection.query().fetch(); let freqEmojis = await freqEmojiCollection.query().fetch();
const isLandscape = width > height; const isLandscape = width > height;
const size = (isLandscape || isMasterDetail ? width / 2 : width) - (CONTAINER_MARGIN * 2); const size = (isLandscape || isMasterDetail ? width / 2 : width) - CONTAINER_MARGIN * 2;
const quantity = (size / (ITEM_SIZE + (ITEM_MARGIN * 2))) - 1; const quantity = size / (ITEM_SIZE + ITEM_MARGIN * 2) - 1;
freqEmojis = freqEmojis.concat(DEFAULT_EMOJIS).slice(0, quantity); freqEmojis = freqEmojis.concat(DEFAULT_EMOJIS).slice(0, quantity);
setItems(freqEmojis); setItems(freqEmojis);
@ -114,11 +114,14 @@ const Header = React.memo(({
setEmojis(); setEmojis();
}, []); }, []);
const onReaction = ({ emoji }) => handleReaction(emoji, message); const onReaction = ({ emoji }: { emoji: IEmoji }) => handleReaction(emoji, message);
const renderItem = useCallback(({ item }) => <HeaderItem item={item} onReaction={onReaction} server={server} theme={theme} />); const renderItem = useCallback(
({ item }) => <HeaderItem item={item} onReaction={onReaction} server={server} theme={theme} />,
[]
);
const renderFooter = useCallback(() => <HeaderFooter onReaction={onReaction} theme={theme} />); const renderFooter = useCallback(() => <HeaderFooter onReaction={onReaction} theme={theme} />, []);
return ( return (
<View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}> <View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}>
@ -135,11 +138,5 @@ const Header = React.memo(({
</View> </View>
); );
}); });
Header.propTypes = {
handleReaction: PropTypes.func,
server: PropTypes.string,
message: PropTypes.object,
isMasterDetail: PropTypes.bool,
theme: PropTypes.string
};
export default withTheme(Header); export default withTheme(Header);

View File

@ -1,472 +0,0 @@
import React, { forwardRef, useImperativeHandle } from 'react';
import PropTypes from 'prop-types';
import { Alert, Clipboard, Share } from 'react-native';
import { connect } from 'react-redux';
import moment from 'moment';
import RocketChat from '../../lib/rocketchat';
import database from '../../lib/database';
import I18n from '../../i18n';
import log, { logEvent } from '../../utils/log';
import Navigation from '../../lib/Navigation';
import { getMessageTranslation } from '../message/utils';
import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events';
import { showConfirmationAlert } from '../../utils/info';
import { useActionSheet } from '../ActionSheet';
import Header, { HEADER_HEIGHT } from './Header';
import events from '../../utils/log/events';
const MessageActions = React.memo(forwardRef(({
room,
tmid,
user,
editInit,
reactionInit,
onReactionPress,
replyInit,
isReadOnly,
server,
Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing,
Message_AllowEditing_BlockEditInMinutes,
Message_AllowPinning,
Message_AllowStarring,
Message_Read_Receipt_Store_Users,
isMasterDetail,
editMessagePermission,
deleteMessagePermission,
forceDeleteMessagePermission,
pinMessagePermission
}, ref) => {
let permissions = {};
const { showActionSheet, hideActionSheet } = useActionSheet();
const getPermissions = async() => {
try {
const permission = [editMessagePermission, deleteMessagePermission, forceDeleteMessagePermission, pinMessagePermission];
const result = await RocketChat.hasPermission(permission, room.rid);
permissions = {
hasEditPermission: result[0],
hasDeletePermission: result[1],
hasForceDeletePermission: result[2],
hasPinPermission: result[3]
};
} catch {
// Do nothing
}
};
const isOwn = message => message.u && message.u._id === user.id;
const allowEdit = (message) => {
if (isReadOnly) {
return false;
}
const editOwn = isOwn(message);
if (!(permissions.hasEditPermission || (Message_AllowEditing && editOwn))) {
return false;
}
const blockEditInMinutes = Message_AllowEditing_BlockEditInMinutes;
if (blockEditInMinutes) {
let msgTs;
if (message.ts != null) {
msgTs = moment(message.ts);
}
let currentTsDiff;
if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes');
}
return currentTsDiff < blockEditInMinutes;
}
return true;
};
const allowDelete = (message) => {
if (isReadOnly) {
return false;
}
// Prevent from deleting thread start message when positioned inside the thread
if (tmid === message.id) {
return false;
}
const deleteOwn = isOwn(message);
if (!(permissions.hasDeletePermission || (Message_AllowDeleting && deleteOwn) || permissions.hasForceDeletePermission)) {
return false;
}
if (permissions.hasForceDeletePermission) {
return true;
}
const blockDeleteInMinutes = Message_AllowDeleting_BlockDeleteInMinutes;
if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) {
let msgTs;
if (message.ts != null) {
msgTs = moment(message.ts);
}
let currentTsDiff;
if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes');
}
return currentTsDiff < blockDeleteInMinutes;
}
return true;
};
const getPermalink = message => RocketChat.getPermalinkMessage(message);
const handleReply = (message) => {
logEvent(events.ROOM_MSG_ACTION_REPLY);
replyInit(message, true);
};
const handleEdit = (message) => {
logEvent(events.ROOM_MSG_ACTION_EDIT);
editInit(message);
};
const handleCreateDiscussion = (message) => {
logEvent(events.ROOM_MSG_ACTION_DISCUSSION);
const params = { message, channel: room, showCloseModal: true };
if (isMasterDetail) {
Navigation.navigate('ModalStackNavigator', { screen: 'CreateDiscussionView', params });
} else {
Navigation.navigate('NewMessageStackNavigator', { screen: 'CreateDiscussionView', params });
}
};
const handleUnread = async(message) => {
logEvent(events.ROOM_MSG_ACTION_UNREAD);
const { id: messageId, ts } = message;
const { rid } = room;
try {
const db = database.active;
const result = await RocketChat.markAsUnread({ messageId });
if (result.success) {
const subCollection = db.get('subscriptions');
const subRecord = await subCollection.find(rid);
await db.action(async() => {
try {
await subRecord.update(sub => sub.lastOpen = ts);
} catch {
// do nothing
}
});
Navigation.navigate('RoomsListView');
}
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_UNREAD_F);
log(e);
}
};
const handlePermalink = async(message) => {
logEvent(events.ROOM_MSG_ACTION_PERMALINK);
try {
const permalink = await getPermalink(message);
Clipboard.setString(permalink);
EventEmitter.emit(LISTENER, { message: I18n.t('Permalink_copied_to_clipboard') });
} catch {
logEvent(events.ROOM_MSG_ACTION_PERMALINK_F);
}
};
const handleCopy = async(message) => {
logEvent(events.ROOM_MSG_ACTION_COPY);
await Clipboard.setString(message?.attachments?.[0]?.description || message.msg);
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
};
const handleShare = async(message) => {
logEvent(events.ROOM_MSG_ACTION_SHARE);
try {
const permalink = await getPermalink(message);
Share.share({ message: permalink });
} catch {
logEvent(events.ROOM_MSG_ACTION_SHARE_F);
}
};
const handleQuote = (message) => {
logEvent(events.ROOM_MSG_ACTION_QUOTE);
replyInit(message, false);
};
const handleStar = async(message) => {
logEvent(message.starred ? events.ROOM_MSG_ACTION_UNSTAR : events.ROOM_MSG_ACTION_STAR);
try {
await RocketChat.toggleStarMessage(message.id, message.starred);
EventEmitter.emit(LISTENER, { message: message.starred ? I18n.t('Message_unstarred') : I18n.t('Message_starred') });
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_STAR_F);
log(e);
}
};
const handlePin = async(message) => {
logEvent(events.ROOM_MSG_ACTION_PIN);
try {
await RocketChat.togglePinMessage(message.id, message.pinned);
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_PIN_F);
log(e);
}
};
const handleReaction = (shortname, message) => {
logEvent(events.ROOM_MSG_ACTION_REACTION);
if (shortname) {
onReactionPress(shortname, message.id);
} else {
reactionInit(message);
}
// close actionSheet when click at header
hideActionSheet();
};
const handleReadReceipt = (message) => {
if (isMasterDetail) {
Navigation.navigate('ModalStackNavigator', { screen: 'ReadReceiptsView', params: { messageId: message.id } });
} else {
Navigation.navigate('ReadReceiptsView', { messageId: message.id });
}
};
const handleToggleTranslation = async(message) => {
try {
const db = database.active;
await db.action(async() => {
await message.update((m) => {
m.autoTranslate = !m.autoTranslate;
m._updatedAt = new Date();
});
});
const translatedMessage = getMessageTranslation(message, room.autoTranslateLanguage);
if (!translatedMessage) {
const m = {
_id: message.id,
rid: message.subscription.id,
u: message.u,
msg: message.msg
};
await RocketChat.translateMessage(m, room.autoTranslateLanguage);
}
} catch (e) {
log(e);
}
};
const handleReport = async(message) => {
logEvent(events.ROOM_MSG_ACTION_REPORT);
try {
await RocketChat.reportMessage(message.id);
Alert.alert(I18n.t('Message_Reported'));
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_REPORT_F);
log(e);
}
};
const handleDelete = (message) => {
showConfirmationAlert({
message: I18n.t('You_will_not_be_able_to_recover_this_message'),
confirmationText: I18n.t('Delete'),
onPress: async() => {
try {
logEvent(events.ROOM_MSG_ACTION_DELETE);
await RocketChat.deleteMessage(message.id, message.subscription.id);
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_DELETE_F);
log(e);
}
}
});
};
const getOptions = (message) => {
let options = [];
// Reply
if (!isReadOnly) {
options = [{
title: I18n.t('Reply_in_Thread'),
icon: 'threads',
onPress: () => handleReply(message)
}];
}
// Quote
if (!isReadOnly) {
options.push({
title: I18n.t('Quote'),
icon: 'quote',
onPress: () => handleQuote(message)
});
}
// Edit
if (allowEdit(message)) {
options.push({
title: I18n.t('Edit'),
icon: 'edit',
onPress: () => handleEdit(message)
});
}
// Permalink
options.push({
title: I18n.t('Permalink'),
icon: 'link',
onPress: () => handlePermalink(message)
});
// Create Discussion
options.push({
title: I18n.t('Start_a_Discussion'),
icon: 'discussions',
onPress: () => handleCreateDiscussion(message)
});
// Mark as unread
if (message.u && message.u._id !== user.id) {
options.push({
title: I18n.t('Mark_unread'),
icon: 'flag',
onPress: () => handleUnread(message)
});
}
// Copy
options.push({
title: I18n.t('Copy'),
icon: 'copy',
onPress: () => handleCopy(message)
});
// Share
options.push({
title: I18n.t('Share'),
icon: 'share',
onPress: () => handleShare(message)
});
// Star
if (Message_AllowStarring) {
options.push({
title: I18n.t(message.starred ? 'Unstar' : 'Star'),
icon: message.starred ? 'star-filled' : 'star',
onPress: () => handleStar(message)
});
}
// Pin
if (Message_AllowPinning && permissions?.hasPinPermission) {
options.push({
title: I18n.t(message.pinned ? 'Unpin' : 'Pin'),
icon: 'pin',
onPress: () => handlePin(message)
});
}
// Read Receipts
if (Message_Read_Receipt_Store_Users) {
options.push({
title: I18n.t('Read_Receipt'),
icon: 'info',
onPress: () => handleReadReceipt(message)
});
}
// Toggle Auto-translate
if (room.autoTranslate && message.u && message.u._id !== user.id) {
options.push({
title: I18n.t(message.autoTranslate ? 'View_Original' : 'Translate'),
icon: 'language',
onPress: () => handleToggleTranslation(message)
});
}
// Report
options.push({
title: I18n.t('Report'),
icon: 'warning',
danger: true,
onPress: () => handleReport(message)
});
// Delete
if (allowDelete(message)) {
options.push({
title: I18n.t('Delete'),
icon: 'delete',
danger: true,
onPress: () => handleDelete(message)
});
}
return options;
};
const showMessageActions = async(message) => {
logEvent(events.ROOM_SHOW_MSG_ACTIONS);
await getPermissions();
showActionSheet({
options: getOptions(message),
headerHeight: HEADER_HEIGHT,
customHeader: (!isReadOnly || room.reactWhenReadOnly ? (
<Header
server={server}
handleReaction={handleReaction}
isMasterDetail={isMasterDetail}
message={message}
/>
) : null)
});
};
useImperativeHandle(ref, () => ({ showMessageActions }));
return null;
}));
MessageActions.propTypes = {
room: PropTypes.object,
tmid: PropTypes.string,
user: PropTypes.object,
editInit: PropTypes.func,
reactionInit: PropTypes.func,
onReactionPress: PropTypes.func,
replyInit: PropTypes.func,
isReadOnly: PropTypes.bool,
Message_AllowDeleting: PropTypes.bool,
Message_AllowDeleting_BlockDeleteInMinutes: PropTypes.number,
Message_AllowEditing: PropTypes.bool,
Message_AllowEditing_BlockEditInMinutes: PropTypes.number,
Message_AllowPinning: PropTypes.bool,
Message_AllowStarring: PropTypes.bool,
Message_Read_Receipt_Store_Users: PropTypes.bool,
server: PropTypes.string,
editMessagePermission: PropTypes.array,
deleteMessagePermission: PropTypes.array,
forceDeleteMessagePermission: PropTypes.array,
pinMessagePermission: PropTypes.array
};
const mapStateToProps = state => ({
server: state.server.server,
Message_AllowDeleting: state.settings.Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing: state.settings.Message_AllowEditing,
Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes,
Message_AllowPinning: state.settings.Message_AllowPinning,
Message_AllowStarring: state.settings.Message_AllowStarring,
Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users,
isMasterDetail: state.app.isMasterDetail,
editMessagePermission: state.permissions['edit-message'],
deleteMessagePermission: state.permissions['delete-message'],
forceDeleteMessagePermission: state.permissions['force-delete-message'],
pinMessagePermission: state.permissions['pin-message']
});
export default connect(mapStateToProps, null, null, { forwardRef: true })(MessageActions);

View File

@ -0,0 +1,487 @@
import React, { forwardRef, useImperativeHandle } from 'react';
import { Alert, Clipboard, Share } from 'react-native';
import { connect } from 'react-redux';
import moment from 'moment';
import RocketChat from '../../lib/rocketchat';
import database from '../../lib/database';
import I18n from '../../i18n';
import log, { logEvent } from '../../utils/log';
import Navigation from '../../lib/Navigation';
import { getMessageTranslation } from '../message/utils';
import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events';
import { showConfirmationAlert } from '../../utils/info';
import { useActionSheet } from '../ActionSheet';
import Header, { HEADER_HEIGHT } from './Header';
import events from '../../utils/log/events';
interface IMessageActions {
room: {
rid: string | number;
autoTranslateLanguage: any;
autoTranslate: any;
reactWhenReadOnly: any;
};
tmid: string;
user: {
id: string | number;
};
editInit: Function;
reactionInit: Function;
onReactionPress: Function;
replyInit: Function;
isMasterDetail: boolean;
isReadOnly: boolean;
Message_AllowDeleting: boolean;
Message_AllowDeleting_BlockDeleteInMinutes: number;
Message_AllowEditing: boolean;
Message_AllowEditing_BlockEditInMinutes: number;
Message_AllowPinning: boolean;
Message_AllowStarring: boolean;
Message_Read_Receipt_Store_Users: boolean;
server: string;
editMessagePermission: [];
deleteMessagePermission: [];
forceDeleteMessagePermission: [];
pinMessagePermission: [];
}
const MessageActions = React.memo(
forwardRef(
(
{
room,
tmid,
user,
editInit,
reactionInit,
onReactionPress,
replyInit,
isReadOnly,
server,
Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing,
Message_AllowEditing_BlockEditInMinutes,
Message_AllowPinning,
Message_AllowStarring,
Message_Read_Receipt_Store_Users,
isMasterDetail,
editMessagePermission,
deleteMessagePermission,
forceDeleteMessagePermission,
pinMessagePermission
}: IMessageActions,
ref
): any => {
let permissions: any = {};
const { showActionSheet, hideActionSheet }: any = useActionSheet();
const getPermissions = async () => {
try {
const permission = [editMessagePermission, deleteMessagePermission, forceDeleteMessagePermission, pinMessagePermission];
const result = await RocketChat.hasPermission(permission, room.rid);
permissions = {
hasEditPermission: result[0],
hasDeletePermission: result[1],
hasForceDeletePermission: result[2],
hasPinPermission: result[3]
};
} catch {
// Do nothing
}
};
const isOwn = (message: any) => message.u && message.u._id === user.id;
const allowEdit = (message: any) => {
if (isReadOnly) {
return false;
}
const editOwn = isOwn(message);
if (!(permissions.hasEditPermission || (Message_AllowEditing && editOwn))) {
return false;
}
const blockEditInMinutes = Message_AllowEditing_BlockEditInMinutes;
if (blockEditInMinutes) {
let msgTs;
if (message.ts != null) {
msgTs = moment(message.ts);
}
let currentTsDiff: any;
if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes');
}
return currentTsDiff < blockEditInMinutes;
}
return true;
};
const allowDelete = (message: any) => {
if (isReadOnly) {
return false;
}
// Prevent from deleting thread start message when positioned inside the thread
if (tmid === message.id) {
return false;
}
const deleteOwn = isOwn(message);
if (!(permissions.hasDeletePermission || (Message_AllowDeleting && deleteOwn) || permissions.hasForceDeletePermission)) {
return false;
}
if (permissions.hasForceDeletePermission) {
return true;
}
const blockDeleteInMinutes = Message_AllowDeleting_BlockDeleteInMinutes;
if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) {
let msgTs;
if (message.ts != null) {
msgTs = moment(message.ts);
}
let currentTsDiff: any;
if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes');
}
return currentTsDiff < blockDeleteInMinutes;
}
return true;
};
const getPermalink = (message: any) => RocketChat.getPermalinkMessage(message);
const handleReply = (message: any) => {
logEvent(events.ROOM_MSG_ACTION_REPLY);
replyInit(message, true);
};
const handleEdit = (message: any) => {
logEvent(events.ROOM_MSG_ACTION_EDIT);
editInit(message);
};
const handleCreateDiscussion = (message: any) => {
logEvent(events.ROOM_MSG_ACTION_DISCUSSION);
const params = { message, channel: room, showCloseModal: true };
if (isMasterDetail) {
Navigation.navigate('ModalStackNavigator', { screen: 'CreateDiscussionView', params });
} else {
Navigation.navigate('NewMessageStackNavigator', { screen: 'CreateDiscussionView', params });
}
};
const handleUnread = async (message: any) => {
logEvent(events.ROOM_MSG_ACTION_UNREAD);
const { id: messageId, ts } = message;
const { rid } = room;
try {
const db = database.active;
const result = await RocketChat.markAsUnread({ messageId });
if (result.success) {
const subCollection = db.get('subscriptions');
const subRecord = await subCollection.find(rid);
await db.action(async () => {
try {
await subRecord.update((sub: any) => (sub.lastOpen = ts));
} catch {
// do nothing
}
});
Navigation.navigate('RoomsListView');
}
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_UNREAD_F);
log(e);
}
};
const handlePermalink = async (message: any) => {
logEvent(events.ROOM_MSG_ACTION_PERMALINK);
try {
const permalink: any = await getPermalink(message);
Clipboard.setString(permalink);
EventEmitter.emit(LISTENER, { message: I18n.t('Permalink_copied_to_clipboard') });
} catch {
logEvent(events.ROOM_MSG_ACTION_PERMALINK_F);
}
};
const handleCopy = async (message: any) => {
logEvent(events.ROOM_MSG_ACTION_COPY);
await Clipboard.setString(message?.attachments?.[0]?.description || message.msg);
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
};
const handleShare = async (message: any) => {
logEvent(events.ROOM_MSG_ACTION_SHARE);
try {
const permalink: any = await getPermalink(message);
Share.share({ message: permalink });
} catch {
logEvent(events.ROOM_MSG_ACTION_SHARE_F);
}
};
const handleQuote = (message: any) => {
logEvent(events.ROOM_MSG_ACTION_QUOTE);
replyInit(message, false);
};
const handleStar = async (message: any) => {
logEvent(message.starred ? events.ROOM_MSG_ACTION_UNSTAR : events.ROOM_MSG_ACTION_STAR);
try {
await RocketChat.toggleStarMessage(message.id, message.starred);
EventEmitter.emit(LISTENER, { message: message.starred ? I18n.t('Message_unstarred') : I18n.t('Message_starred') });
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_STAR_F);
log(e);
}
};
const handlePin = async (message: any) => {
logEvent(events.ROOM_MSG_ACTION_PIN);
try {
await RocketChat.togglePinMessage(message.id, message.pinned);
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_PIN_F);
log(e);
}
};
const handleReaction = (shortname: any, message: any) => {
logEvent(events.ROOM_MSG_ACTION_REACTION);
if (shortname) {
onReactionPress(shortname, message.id);
} else {
reactionInit(message);
}
// close actionSheet when click at header
hideActionSheet();
};
const handleReadReceipt = (message: any) => {
if (isMasterDetail) {
Navigation.navigate('ModalStackNavigator', { screen: 'ReadReceiptsView', params: { messageId: message.id } });
} else {
Navigation.navigate('ReadReceiptsView', { messageId: message.id });
}
};
const handleToggleTranslation = async (message: any) => {
try {
const db = database.active;
await db.action(async () => {
await message.update((m: any) => {
m.autoTranslate = !m.autoTranslate;
m._updatedAt = new Date();
});
});
const translatedMessage = getMessageTranslation(message, room.autoTranslateLanguage);
if (!translatedMessage) {
const m = {
_id: message.id,
rid: message.subscription.id,
u: message.u,
msg: message.msg
};
await RocketChat.translateMessage(m, room.autoTranslateLanguage);
}
} catch (e) {
log(e);
}
};
const handleReport = async (message: any) => {
logEvent(events.ROOM_MSG_ACTION_REPORT);
try {
await RocketChat.reportMessage(message.id);
Alert.alert(I18n.t('Message_Reported'));
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_REPORT_F);
log(e);
}
};
const handleDelete = (message: any) => {
// TODO - migrate this function for ts when fix the lint erros
// @ts-ignore
showConfirmationAlert({
message: I18n.t('You_will_not_be_able_to_recover_this_message'),
confirmationText: I18n.t('Delete'),
onPress: async () => {
try {
logEvent(events.ROOM_MSG_ACTION_DELETE);
await RocketChat.deleteMessage(message.id, message.subscription.id);
} catch (e) {
logEvent(events.ROOM_MSG_ACTION_DELETE_F);
log(e);
}
}
});
};
const getOptions = (message: any) => {
let options: any = [];
// Reply
if (!isReadOnly) {
options = [
{
title: I18n.t('Reply_in_Thread'),
icon: 'threads',
onPress: () => handleReply(message)
}
];
}
// Quote
if (!isReadOnly) {
options.push({
title: I18n.t('Quote'),
icon: 'quote',
onPress: () => handleQuote(message)
});
}
// Edit
if (allowEdit(message)) {
options.push({
title: I18n.t('Edit'),
icon: 'edit',
onPress: () => handleEdit(message)
});
}
// Permalink
options.push({
title: I18n.t('Permalink'),
icon: 'link',
onPress: () => handlePermalink(message)
});
// Create Discussion
options.push({
title: I18n.t('Start_a_Discussion'),
icon: 'discussions',
onPress: () => handleCreateDiscussion(message)
});
// Mark as unread
if (message.u && message.u._id !== user.id) {
options.push({
title: I18n.t('Mark_unread'),
icon: 'flag',
onPress: () => handleUnread(message)
});
}
// Copy
options.push({
title: I18n.t('Copy'),
icon: 'copy',
onPress: () => handleCopy(message)
});
// Share
options.push({
title: I18n.t('Share'),
icon: 'share',
onPress: () => handleShare(message)
});
// Star
if (Message_AllowStarring) {
options.push({
title: I18n.t(message.starred ? 'Unstar' : 'Star'),
icon: message.starred ? 'star-filled' : 'star',
onPress: () => handleStar(message)
});
}
// Pin
if (Message_AllowPinning && permissions?.hasPinPermission) {
options.push({
title: I18n.t(message.pinned ? 'Unpin' : 'Pin'),
icon: 'pin',
onPress: () => handlePin(message)
});
}
// Read Receipts
if (Message_Read_Receipt_Store_Users) {
options.push({
title: I18n.t('Read_Receipt'),
icon: 'info',
onPress: () => handleReadReceipt(message)
});
}
// Toggle Auto-translate
if (room.autoTranslate && message.u && message.u._id !== user.id) {
options.push({
title: I18n.t(message.autoTranslate ? 'View_Original' : 'Translate'),
icon: 'language',
onPress: () => handleToggleTranslation(message)
});
}
// Report
options.push({
title: I18n.t('Report'),
icon: 'warning',
danger: true,
onPress: () => handleReport(message)
});
// Delete
if (allowDelete(message)) {
options.push({
title: I18n.t('Delete'),
icon: 'delete',
danger: true,
onPress: () => handleDelete(message)
});
}
return options;
};
const showMessageActions = async (message: any) => {
logEvent(events.ROOM_SHOW_MSG_ACTIONS);
await getPermissions();
showActionSheet({
options: getOptions(message),
headerHeight: HEADER_HEIGHT,
customHeader:
!isReadOnly || room.reactWhenReadOnly ? (
<Header server={server} handleReaction={handleReaction} isMasterDetail={isMasterDetail} message={message} />
) : null
});
};
useImperativeHandle(ref, () => ({ showMessageActions }));
return null;
}
)
);
const mapStateToProps = (state: any) => ({
server: state.server.server,
Message_AllowDeleting: state.settings.Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing: state.settings.Message_AllowEditing,
Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes,
Message_AllowPinning: state.settings.Message_AllowPinning,
Message_AllowStarring: state.settings.Message_AllowStarring,
Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users,
isMasterDetail: state.app.isMasterDetail,
editMessagePermission: state.permissions['edit-message'],
deleteMessagePermission: state.permissions['delete-message'],
forceDeleteMessagePermission: state.permissions['force-delete-message'],
pinMessagePermission: state.permissions['pin-message']
});
export default connect(mapStateToProps, null, null, { forwardRef: true })(MessageActions);

View File

@ -1,5 +1,4 @@
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { TouchableOpacity } from 'react-native'; import { TouchableOpacity } from 'react-native';
import FastImage from '@rocket.chat/react-native-fast-image'; import FastImage from '@rocket.chat/react-native-fast-image';
@ -9,7 +8,16 @@ import { themes } from '../../../constants/colors';
import MessageboxContext from '../Context'; import MessageboxContext from '../Context';
import ActivityIndicator from '../../ActivityIndicator'; import ActivityIndicator from '../../ActivityIndicator';
const Item = ({ item, theme }) => { interface IMessageBoxCommandsPreviewItem {
item: {
type: string;
id: string;
value: string;
};
theme: string;
}
const Item = ({ item, theme }: IMessageBoxCommandsPreviewItem) => {
const context = useContext(MessageboxContext); const context = useContext(MessageboxContext);
const { onPressCommandPreview } = context; const { onPressCommandPreview } = context;
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -18,29 +26,21 @@ const Item = ({ item, theme }) => {
<TouchableOpacity <TouchableOpacity
style={styles.commandPreview} style={styles.commandPreview}
onPress={() => onPressCommandPreview(item)} onPress={() => onPressCommandPreview(item)}
testID={`command-preview-item${ item.id }`} testID={`command-preview-item${item.id}`}>
> {item.type === 'image' ? (
{item.type === 'image' <FastImage
? ( style={styles.commandPreviewImage}
<FastImage source={{ uri: item.value }}
style={styles.commandPreviewImage} resizeMode={FastImage.resizeMode.cover}
source={{ uri: item.value }} onLoadStart={() => setLoading(true)}
resizeMode={FastImage.resizeMode.cover} onLoad={() => setLoading(false)}>
onLoadStart={() => setLoading(true)} {loading ? <ActivityIndicator theme={theme} /> : null}
onLoad={() => setLoading(false)} </FastImage>
> ) : (
{ loading ? <ActivityIndicator theme={theme} /> : null } <CustomIcon name='attach' size={36} color={themes[theme].actionTintColor} />
</FastImage> )}
)
: <CustomIcon name='attach' size={36} color={themes[theme].actionTintColor} />
}
</TouchableOpacity> </TouchableOpacity>
); );
}; };
Item.propTypes = {
item: PropTypes.object,
theme: PropTypes.string
};
export default Item; export default Item;

View File

@ -1,46 +0,0 @@
import React from 'react';
import { FlatList } from 'react-native';
import PropTypes from 'prop-types';
import { dequal } from 'dequal';
import Item from './Item';
import styles from '../styles';
import { themes } from '../../../constants/colors';
import { withTheme } from '../../../theme';
const CommandsPreview = React.memo(({ theme, commandPreview, showCommandPreview }) => {
if (!showCommandPreview) {
return null;
}
return (
<FlatList
testID='commandbox-container'
style={[styles.mentionList, { backgroundColor: themes[theme].messageboxBackground }]}
data={commandPreview}
renderItem={({ item }) => <Item item={item} theme={theme} />}
keyExtractor={item => item.id}
keyboardShouldPersistTaps='always'
horizontal
showsHorizontalScrollIndicator={false}
/>
);
}, (prevProps, nextProps) => {
if (prevProps.theme !== nextProps.theme) {
return false;
}
if (prevProps.showCommandPreview !== nextProps.showCommandPreview) {
return false;
}
if (!dequal(prevProps.commandPreview, nextProps.commandPreview)) {
return false;
}
return true;
});
CommandsPreview.propTypes = {
commandPreview: PropTypes.array,
showCommandPreview: PropTypes.bool,
theme: PropTypes.string
};
export default withTheme(CommandsPreview);

View File

@ -0,0 +1,48 @@
import React from 'react';
import { FlatList } from 'react-native';
import { dequal } from 'dequal';
import Item from './Item';
import styles from '../styles';
import { themes } from '../../../constants/colors';
import { withTheme } from '../../../theme';
interface IMessageBoxCommandsPreview {
commandPreview: [];
showCommandPreview: boolean;
theme: string;
}
const CommandsPreview = React.memo(
({ theme, commandPreview, showCommandPreview }: IMessageBoxCommandsPreview) => {
if (!showCommandPreview) {
return null;
}
return (
<FlatList
testID='commandbox-container'
style={[styles.mentionList, { backgroundColor: themes[theme].messageboxBackground }]}
data={commandPreview}
renderItem={({ item }) => <Item item={item} theme={theme} />}
keyExtractor={(item: any) => item.id}
keyboardShouldPersistTaps='always'
horizontal
showsHorizontalScrollIndicator={false}
/>
);
},
(prevProps, nextProps) => {
if (prevProps.theme !== nextProps.theme) {
return false;
}
if (prevProps.showCommandPreview !== nextProps.showCommandPreview) {
return false;
}
if (!dequal(prevProps.commandPreview, nextProps.commandPreview)) {
return false;
}
return true;
}
);
export default withTheme(CommandsPreview);

View File

@ -1,4 +0,0 @@
import React from 'react';
const MessageboxContext = React.createContext();
export default MessageboxContext;

View File

@ -0,0 +1,5 @@
import React from 'react';
// @ts-ignore
const MessageboxContext = React.createContext<any>();
export default MessageboxContext;

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { KeyboardRegistry } from 'react-native-ui-lib/keyboard'; import { KeyboardRegistry } from 'react-native-ui-lib/keyboard';
import PropTypes from 'prop-types';
import store from '../../lib/createStore'; import store from '../../lib/createStore';
import EmojiPicker from '../EmojiPicker'; import EmojiPicker from '../EmojiPicker';
@ -9,25 +8,29 @@ import styles from './styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
export default class EmojiKeyboard extends React.PureComponent { interface IMessageBoxEmojiKeyboard {
static propTypes = { theme: string;
theme: PropTypes.string }
};
constructor(props) { export default class EmojiKeyboard extends React.PureComponent<IMessageBoxEmojiKeyboard, any> {
private readonly baseUrl: any;
constructor(props: IMessageBoxEmojiKeyboard) {
super(props); super(props);
const state = store.getState(); const state = store.getState();
this.baseUrl = state.share.server.server || state.server.server; this.baseUrl = state.share.server.server || state.server.server;
} }
onEmojiSelected = (emoji) => { onEmojiSelected = (emoji: any) => {
KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji }); KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji });
} };
render() { render() {
const { theme } = this.props; const { theme } = this.props;
return ( return (
<View style={[styles.emojiKeyboardContainer, { borderTopColor: themes[theme].borderColor }]} testID='messagebox-keyboard-emoji'> <View
style={[styles.emojiKeyboardContainer, { borderTopColor: themes[theme].borderColor }]}
testID='messagebox-keyboard-emoji'>
<EmojiPicker onEmojiSelected={this.onEmojiSelected} baseUrl={this.baseUrl} /> <EmojiPicker onEmojiSelected={this.onEmojiSelected} baseUrl={this.baseUrl} />
</View> </View>
); );

View File

@ -1,31 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CancelEditingButton, ToggleEmojiButton } from './buttons';
const LeftButtons = React.memo(({
theme, showEmojiKeyboard, editing, editCancel, openEmoji, closeEmoji
}) => {
if (editing) {
return <CancelEditingButton onPress={editCancel} theme={theme} />;
}
return (
<ToggleEmojiButton
show={showEmojiKeyboard}
open={openEmoji}
close={closeEmoji}
theme={theme}
/>
);
});
LeftButtons.propTypes = {
theme: PropTypes.string,
showEmojiKeyboard: PropTypes.bool,
openEmoji: PropTypes.func.isRequired,
closeEmoji: PropTypes.func.isRequired,
editing: PropTypes.bool,
editCancel: PropTypes.func.isRequired
};
export default LeftButtons;

View File

@ -0,0 +1,23 @@
import React from 'react';
import { CancelEditingButton, ToggleEmojiButton } from './buttons';
interface IMessageBoxLeftButtons {
theme: string;
showEmojiKeyboard: boolean;
openEmoji(): void;
closeEmoji(): void;
editing: boolean;
editCancel(): void;
}
const LeftButtons = React.memo(
({ theme, showEmojiKeyboard, editing, editCancel, openEmoji, closeEmoji }: IMessageBoxLeftButtons) => {
if (editing) {
return <CancelEditingButton onPress={editCancel} theme={theme} />;
}
return <ToggleEmojiButton show={showEmojiKeyboard} open={openEmoji} close={closeEmoji} theme={theme} />;
}
);
export default LeftButtons;

View File

@ -1,28 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import { CancelEditingButton, ActionsButton } from './buttons';
import styles from './styles';
const LeftButtons = React.memo(({
theme, showMessageBoxActions, editing, editCancel, isActionsEnabled
}) => {
if (editing) {
return <CancelEditingButton onPress={editCancel} theme={theme} />;
}
if (isActionsEnabled) {
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
}
return <View style={styles.buttonsWhitespace} />;
});
LeftButtons.propTypes = {
theme: PropTypes.string,
showMessageBoxActions: PropTypes.func.isRequired,
editing: PropTypes.bool,
editCancel: PropTypes.func.isRequired,
isActionsEnabled: PropTypes.bool
};
export default LeftButtons;

View File

@ -0,0 +1,27 @@
import React from 'react';
import { View } from 'react-native';
import { ActionsButton, CancelEditingButton } from './buttons';
import styles from './styles';
interface IMessageBoxLeftButtons {
theme: string;
showMessageBoxActions(): void;
editing: boolean;
editCancel(): void;
isActionsEnabled: boolean;
}
const LeftButtons = React.memo(
({ theme, showMessageBoxActions, editing, editCancel, isActionsEnabled }: IMessageBoxLeftButtons) => {
if (editing) {
return <CancelEditingButton onPress={editCancel} theme={theme} />;
}
if (isActionsEnabled) {
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
}
return <View style={styles.buttonsWhitespace} />;
}
);
export default LeftButtons;

View File

@ -1,12 +1,19 @@
import React from 'react'; import React from 'react';
import { TouchableOpacity, Text } from 'react-native'; import { Text, TouchableOpacity } from 'react-native';
import PropTypes from 'prop-types';
import styles from '../styles'; import styles from '../styles';
import I18n from '../../../i18n'; import I18n from '../../../i18n';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
const FixedMentionItem = ({ item, onPress, theme }) => ( interface IMessageBoxFixedMentionItem {
item: {
username: string;
};
onPress: Function;
theme: string;
}
const FixedMentionItem = ({ item, onPress, theme }: IMessageBoxFixedMentionItem) => (
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.mentionItem, styles.mentionItem,
@ -15,8 +22,7 @@ const FixedMentionItem = ({ item, onPress, theme }) => (
borderTopColor: themes[theme].separatorColor borderTopColor: themes[theme].separatorColor
} }
]} ]}
onPress={() => onPress(item)} onPress={() => onPress(item)}>
>
<Text style={[styles.fixedMentionAvatar, { color: themes[theme].titleText }]}>{item.username}</Text> <Text style={[styles.fixedMentionAvatar, { color: themes[theme].titleText }]}>{item.username}</Text>
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}> <Text style={[styles.mentionText, { color: themes[theme].titleText }]}>
{item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')} {item.username === 'here' ? I18n.t('Notify_active_in_this_room') : I18n.t('Notify_all_in_this_room')}
@ -24,10 +30,4 @@ const FixedMentionItem = ({ item, onPress, theme }) => (
</TouchableOpacity> </TouchableOpacity>
); );
FixedMentionItem.propTypes = {
item: PropTypes.object,
onPress: PropTypes.func,
theme: PropTypes.string
};
export default FixedMentionItem; export default FixedMentionItem;

View File

@ -6,25 +6,20 @@ import shortnameToUnicode from '../../../utils/shortnameToUnicode';
import styles from '../styles'; import styles from '../styles';
import MessageboxContext from '../Context'; import MessageboxContext from '../Context';
import CustomEmoji from '../../EmojiPicker/CustomEmoji'; import CustomEmoji from '../../EmojiPicker/CustomEmoji';
import { IEmoji } from '../../EmojiPicker/interfaces';
const MentionEmoji = ({ item }) => { interface IMessageBoxMentionEmoji {
item: IEmoji;
}
const MentionEmoji = ({ item }: IMessageBoxMentionEmoji) => {
const context = useContext(MessageboxContext); const context = useContext(MessageboxContext);
const { baseUrl } = context; const { baseUrl } = context;
if (item.name) { if (item.name) {
return ( return <CustomEmoji style={styles.mentionItemCustomEmoji} emoji={item} baseUrl={baseUrl} />;
<CustomEmoji
style={styles.mentionItemCustomEmoji}
emoji={item}
baseUrl={baseUrl}
/>
);
} }
return ( return <Text style={styles.mentionItemEmoji}>{shortnameToUnicode(`:${item}:`)}</Text>;
<Text style={styles.mentionItemEmoji}>
{shortnameToUnicode(`:${ item }:`)}
</Text>
);
}; };
MentionEmoji.propTypes = { MentionEmoji.propTypes = {

View File

@ -1,32 +1,39 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { TouchableOpacity, Text } from 'react-native'; import { Text, TouchableOpacity } from 'react-native';
import PropTypes from 'prop-types';
import styles from '../styles'; import styles from '../styles';
import Avatar from '../../Avatar'; import Avatar from '../../Avatar';
import MessageboxContext from '../Context'; import MessageboxContext from '../Context';
import FixedMentionItem from './FixedMentionItem'; import FixedMentionItem from './FixedMentionItem';
import MentionEmoji from './MentionEmoji'; import MentionEmoji from './MentionEmoji';
import { import { MENTIONS_TRACKING_TYPE_COMMANDS, MENTIONS_TRACKING_TYPE_EMOJIS } from '../constants';
MENTIONS_TRACKING_TYPE_EMOJIS,
MENTIONS_TRACKING_TYPE_COMMANDS
} from '../constants';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { IEmoji } from '../../EmojiPicker/interfaces';
const MentionItem = ({ interface IMessageBoxMentionItem {
item, trackingType, theme item: {
}) => { name: string;
command: string;
username: string;
t: string;
id: string;
} & IEmoji;
trackingType: string;
theme: string;
}
const MentionItem = ({ item, trackingType, theme }: IMessageBoxMentionItem) => {
const context = useContext(MessageboxContext); const context = useContext(MessageboxContext);
const { onPressMention } = context; const { onPressMention } = context;
const defineTestID = (type) => { const defineTestID = (type: string) => {
switch (type) { switch (type) {
case MENTIONS_TRACKING_TYPE_EMOJIS: case MENTIONS_TRACKING_TYPE_EMOJIS:
return `mention-item-${ item.name || item }`; return `mention-item-${item.name || item}`;
case MENTIONS_TRACKING_TYPE_COMMANDS: case MENTIONS_TRACKING_TYPE_COMMANDS:
return `mention-item-${ item.command || item }`; return `mention-item-${item.command || item}`;
default: default:
return `mention-item-${ item.username || item.name || item }`; return `mention-item-${item.username || item.name || item}`;
} }
}; };
@ -38,13 +45,8 @@ const MentionItem = ({
let content = ( let content = (
<> <>
<Avatar <Avatar style={styles.avatar} text={item.username || item.name} size={30} type={item.t} />
style={styles.avatar} <Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{item.username || item.name || item}</Text>
text={item.username || item.name}
size={30}
type={item.t}
/>
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>{ item.username || item.name || item }</Text>
</> </>
); );
@ -52,7 +54,7 @@ const MentionItem = ({
content = ( content = (
<> <>
<MentionEmoji item={item} /> <MentionEmoji item={item} />
<Text style={[styles.mentionText, { color: themes[theme].titleText }]}>:{ item.name || item }:</Text> <Text style={[styles.mentionText, { color: themes[theme].titleText }]}>:{item.name || item}:</Text>
</> </>
); );
} }
@ -76,17 +78,10 @@ const MentionItem = ({
} }
]} ]}
onPress={() => onPressMention(item)} onPress={() => onPressMention(item)}
testID={testID} testID={testID}>
>
{content} {content}
</TouchableOpacity> </TouchableOpacity>
); );
}; };
MentionItem.propTypes = {
item: PropTypes.object,
trackingType: PropTypes.string,
theme: PropTypes.string
};
export default MentionItem; export default MentionItem;

View File

@ -1,45 +0,0 @@
import React from 'react';
import { FlatList, View } from 'react-native';
import PropTypes from 'prop-types';
import { dequal } from 'dequal';
import styles from '../styles';
import MentionItem from './MentionItem';
import { themes } from '../../../constants/colors';
const Mentions = React.memo(({ mentions, trackingType, theme }) => {
if (!trackingType) {
return null;
}
return (
<View testID='messagebox-container'>
<FlatList
style={[styles.mentionList, { backgroundColor: themes[theme].auxiliaryBackground }]}
data={mentions}
extraData={mentions}
renderItem={({ item }) => <MentionItem item={item} trackingType={trackingType} theme={theme} />}
keyExtractor={item => item.rid || item.name || item.command || item}
keyboardShouldPersistTaps='always'
/>
</View>
);
}, (prevProps, nextProps) => {
if (prevProps.theme !== nextProps.theme) {
return false;
}
if (prevProps.trackingType !== nextProps.trackingType) {
return false;
}
if (!dequal(prevProps.mentions, nextProps.mentions)) {
return false;
}
return true;
});
Mentions.propTypes = {
mentions: PropTypes.array,
trackingType: PropTypes.string,
theme: PropTypes.string
};
export default Mentions;

View File

@ -0,0 +1,47 @@
import React from 'react';
import { FlatList, View } from 'react-native';
import { dequal } from 'dequal';
import styles from '../styles';
import MentionItem from './MentionItem';
import { themes } from '../../../constants/colors';
interface IMessageBoxMentions {
mentions: [];
trackingType: string;
theme: string;
}
const Mentions = React.memo(
({ mentions, trackingType, theme }: IMessageBoxMentions) => {
if (!trackingType) {
return null;
}
return (
<View testID='messagebox-container'>
<FlatList
style={[styles.mentionList, { backgroundColor: themes[theme].auxiliaryBackground }]}
data={mentions}
extraData={mentions}
renderItem={({ item }) => <MentionItem item={item} trackingType={trackingType} theme={theme} />}
keyExtractor={(item: any) => item.rid || item.name || item.command || item}
keyboardShouldPersistTaps='always'
/>
</View>
);
},
(prevProps, nextProps) => {
if (prevProps.theme !== nextProps.theme) {
return false;
}
if (prevProps.trackingType !== nextProps.trackingType) {
return false;
}
if (!dequal(prevProps.mentions, nextProps.mentions)) {
return false;
}
return true;
}
);
export default Mentions;

View File

@ -1,16 +1,21 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { Text, View } from 'react-native';
import { View, Text } from 'react-native';
import { Audio } from 'expo-av'; import { Audio } from 'expo-av';
import { BorderlessButton } from 'react-native-gesture-handler'; import { BorderlessButton } from 'react-native-gesture-handler';
import { getInfoAsync } from 'expo-file-system'; import { getInfoAsync } from 'expo-file-system';
import { deactivateKeepAwake, activateKeepAwake } from 'expo-keep-awake'; import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
import styles from './styles'; import styles from './styles';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { logEvent, events } from '../../utils/log'; import { events, logEvent } from '../../utils/log';
interface IMessageBoxRecordAudioProps {
theme: string;
recordingCallback: Function;
onFinish: Function;
}
const RECORDING_EXTENSION = '.aac'; const RECORDING_EXTENSION = '.aac';
const RECORDING_SETTINGS = { const RECORDING_SETTINGS = {
@ -41,22 +46,24 @@ const RECORDING_MODE = {
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX
}; };
const formatTime = function(seconds) { const formatTime = function (seconds: any) {
let minutes = Math.floor(seconds / 60); let minutes: any = Math.floor(seconds / 60);
seconds %= 60; seconds %= 60;
if (minutes < 10) { minutes = `0${ minutes }`; } if (minutes < 10) {
if (seconds < 10) { seconds = `0${ seconds }`; } minutes = `0${minutes}`;
return `${ minutes }:${ seconds }`; }
if (seconds < 10) {
seconds = `0${seconds}`;
}
return `${minutes}:${seconds}`;
}; };
export default class RecordAudio extends React.PureComponent { export default class RecordAudio extends React.PureComponent<IMessageBoxRecordAudioProps, any> {
static propTypes = { private isRecorderBusy: boolean;
theme: PropTypes.string,
recordingCallback: PropTypes.func,
onFinish: PropTypes.func
}
constructor(props) { private recording: any;
constructor(props: IMessageBoxRecordAudioProps) {
super(props); super(props);
this.isRecorderBusy = false; this.isRecorderBusy = false;
this.state = { this.state = {
@ -83,7 +90,7 @@ export default class RecordAudio extends React.PureComponent {
return formatTime(Math.floor(recordingDurationMillis / 1000)); return formatTime(Math.floor(recordingDurationMillis / 1000));
} }
isRecordingPermissionGranted = async() => { isRecordingPermissionGranted = async () => {
try { try {
const permission = await Audio.getPermissionsAsync(); const permission = await Audio.getPermissionsAsync();
if (permission.status === 'granted') { if (permission.status === 'granted') {
@ -94,16 +101,16 @@ export default class RecordAudio extends React.PureComponent {
// Do nothing // Do nothing
} }
return false; return false;
} };
onRecordingStatusUpdate = (status) => { onRecordingStatusUpdate = (status: any) => {
this.setState({ this.setState({
isRecording: status.isRecording, isRecording: status.isRecording,
recordingDurationMillis: status.durationMillis recordingDurationMillis: status.durationMillis
}); });
} };
startRecordingAudio = async() => { startRecordingAudio = async () => {
logEvent(events.ROOM_AUDIO_RECORD); logEvent(events.ROOM_AUDIO_RECORD);
if (!this.isRecorderBusy) { if (!this.isRecorderBusy) {
this.isRecorderBusy = true; this.isRecorderBusy = true;
@ -128,7 +135,7 @@ export default class RecordAudio extends React.PureComponent {
} }
}; };
finishRecordingAudio = async() => { finishRecordingAudio = async () => {
logEvent(events.ROOM_AUDIO_FINISH); logEvent(events.ROOM_AUDIO_FINISH);
if (!this.isRecorderBusy) { if (!this.isRecorderBusy) {
const { onFinish } = this.props; const { onFinish } = this.props;
@ -140,7 +147,7 @@ export default class RecordAudio extends React.PureComponent {
const fileURI = this.recording.getURI(); const fileURI = this.recording.getURI();
const fileData = await getInfoAsync(fileURI); const fileData = await getInfoAsync(fileURI);
const fileInfo = { const fileInfo = {
name: `${ Date.now() }.aac`, name: `${Date.now()}.aac`,
mime: 'audio/aac', mime: 'audio/aac',
type: 'audio/aac', type: 'audio/aac',
store: 'Uploads', store: 'Uploads',
@ -158,7 +165,7 @@ export default class RecordAudio extends React.PureComponent {
} }
}; };
cancelRecordingAudio = async() => { cancelRecordingAudio = async () => {
logEvent(events.ROOM_AUDIO_CANCEL); logEvent(events.ROOM_AUDIO_CANCEL);
if (!this.isRecorderBusy) { if (!this.isRecorderBusy) {
this.isRecorderBusy = true; this.isRecorderBusy = true;
@ -183,9 +190,9 @@ export default class RecordAudio extends React.PureComponent {
onPress={this.startRecordingAudio} onPress={this.startRecordingAudio}
style={styles.actionButton} style={styles.actionButton}
testID='messagebox-send-audio' testID='messagebox-send-audio'
// @ts-ignore
accessibilityLabel={I18n.t('Send_audio_message')} accessibilityLabel={I18n.t('Send_audio_message')}
accessibilityTraits='button' accessibilityTraits='button'>
>
<CustomIcon name='microphone' size={24} color={themes[theme].auxiliaryTintColor} /> <CustomIcon name='microphone' size={24} color={themes[theme].auxiliaryTintColor} />
</BorderlessButton> </BorderlessButton>
); );
@ -196,33 +203,21 @@ export default class RecordAudio extends React.PureComponent {
<View style={styles.textArea}> <View style={styles.textArea}>
<BorderlessButton <BorderlessButton
onPress={this.cancelRecordingAudio} onPress={this.cancelRecordingAudio}
// @ts-ignore
accessibilityLabel={I18n.t('Cancel_recording')} accessibilityLabel={I18n.t('Cancel_recording')}
accessibilityTraits='button' accessibilityTraits='button'
style={styles.actionButton} style={styles.actionButton}>
> <CustomIcon size={24} color={themes[theme].dangerColor} name='close' />
<CustomIcon
size={24}
color={themes[theme].dangerColor}
name='close'
/>
</BorderlessButton> </BorderlessButton>
<Text <Text style={[styles.recordingCancelText, { color: themes[theme].titleText }]}>{this.duration}</Text>
style={[styles.recordingCancelText, { color: themes[theme].titleText }]}
>
{this.duration}
</Text>
</View> </View>
<BorderlessButton <BorderlessButton
onPress={this.finishRecordingAudio} onPress={this.finishRecordingAudio}
// @ts-ignore
accessibilityLabel={I18n.t('Finish_recording')} accessibilityLabel={I18n.t('Finish_recording')}
accessibilityTraits='button' accessibilityTraits='button'
style={styles.actionButton} style={styles.actionButton}>
> <CustomIcon size={24} color={themes[theme].successColor} name='check' />
<CustomIcon
size={24}
color={themes[theme].successColor}
name='check'
/>
</BorderlessButton> </BorderlessButton>
</View> </View>
); );

View File

@ -1,97 +0,0 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import moment from 'moment';
import { connect } from 'react-redux';
import Markdown from '../markdown';
import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
paddingTop: 10
},
messageContainer: {
flex: 1,
marginHorizontal: 10,
paddingHorizontal: 15,
paddingVertical: 10,
borderRadius: 4
},
header: {
flexDirection: 'row',
alignItems: 'center'
},
username: {
fontSize: 16,
...sharedStyles.textMedium
},
time: {
fontSize: 12,
lineHeight: 16,
marginLeft: 6,
...sharedStyles.textRegular,
fontWeight: '300'
},
close: {
marginRight: 10
}
});
const ReplyPreview = React.memo(({
message, Message_TimeFormat, baseUrl, username, replying, getCustomEmoji, close, theme, useRealName
}) => {
if (!replying) {
return null;
}
const time = moment(message.ts).format(Message_TimeFormat);
return (
<View
style={[
styles.container,
{ backgroundColor: themes[theme].messageboxBackground }
]}
>
<View style={[styles.messageContainer, { backgroundColor: themes[theme].chatComponentBackground }]}>
<View style={styles.header}>
<Text style={[styles.username, { color: themes[theme].tintColor }]}>{useRealName ? message.u?.name : message.u?.username}</Text>
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
</View>
<Markdown
msg={message.msg}
baseUrl={baseUrl}
username={username}
getCustomEmoji={getCustomEmoji}
numberOfLines={1}
preview
theme={theme}
/>
</View>
<CustomIcon name='close' color={themes[theme].auxiliaryText} size={20} style={styles.close} onPress={close} />
</View>
);
}, (prevProps, nextProps) => prevProps.replying === nextProps.replying && prevProps.theme === nextProps.theme && prevProps.message.id === nextProps.message.id);
ReplyPreview.propTypes = {
replying: PropTypes.bool,
message: PropTypes.object.isRequired,
Message_TimeFormat: PropTypes.string.isRequired,
close: PropTypes.func.isRequired,
baseUrl: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
getCustomEmoji: PropTypes.func,
theme: PropTypes.string,
useRealName: PropTypes.bool
};
const mapStateToProps = state => ({
Message_TimeFormat: state.settings.Message_TimeFormat,
baseUrl: state.server.server,
useRealName: state.settings.UI_Use_Real_Name
});
export default connect(mapStateToProps)(ReplyPreview);

View File

@ -0,0 +1,112 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import moment from 'moment';
import { connect } from 'react-redux';
import Markdown from '../markdown';
import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
paddingTop: 10
},
messageContainer: {
flex: 1,
marginHorizontal: 10,
paddingHorizontal: 15,
paddingVertical: 10,
borderRadius: 4
},
header: {
flexDirection: 'row',
alignItems: 'center'
},
username: {
fontSize: 16,
...sharedStyles.textMedium
},
time: {
fontSize: 12,
lineHeight: 16,
marginLeft: 6,
...sharedStyles.textRegular,
fontWeight: '300'
},
close: {
marginRight: 10
}
});
interface IMessageBoxReplyPreview {
replying: boolean;
message: {
ts: Date;
msg: string;
u: any;
};
Message_TimeFormat: string;
close(): void;
baseUrl: string;
username: string;
getCustomEmoji: Function;
theme: string;
useRealName: boolean;
}
const ReplyPreview = React.memo(
({
message,
Message_TimeFormat,
baseUrl,
username,
replying,
getCustomEmoji,
close,
theme,
useRealName
}: IMessageBoxReplyPreview) => {
if (!replying) {
return null;
}
const time = moment(message.ts).format(Message_TimeFormat);
return (
<View style={[styles.container, { backgroundColor: themes[theme].messageboxBackground }]}>
<View style={[styles.messageContainer, { backgroundColor: themes[theme].chatComponentBackground }]}>
<View style={styles.header}>
<Text style={[styles.username, { color: themes[theme].tintColor }]}>
{useRealName ? message.u?.name : message.u?.username}
</Text>
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
</View>
{/* @ts-ignore*/}
<Markdown
msg={message.msg}
baseUrl={baseUrl}
username={username}
getCustomEmoji={getCustomEmoji}
numberOfLines={1}
preview
theme={theme}
/>
</View>
<CustomIcon name='close' color={themes[theme].auxiliaryText} size={20} style={styles.close} onPress={close} />
</View>
);
},
(prevProps: any, nextProps: any) =>
prevProps.replying === nextProps.replying &&
prevProps.theme === nextProps.theme &&
prevProps.message.id === nextProps.message.id
);
const mapStateToProps = (state: any) => ({
Message_TimeFormat: state.settings.Message_TimeFormat,
baseUrl: state.server.server,
useRealName: state.settings.UI_Use_Real_Name
});
export default connect(mapStateToProps)(ReplyPreview);

View File

@ -1,29 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import { SendButton, ActionsButton } from './buttons';
import styles from './styles';
const RightButtons = React.memo(({
theme, showSend, submit, showMessageBoxActions, isActionsEnabled
}) => {
if (showSend) {
return <SendButton onPress={submit} theme={theme} />;
}
if (isActionsEnabled) {
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
}
return <View style={styles.buttonsWhitespace} />;
});
RightButtons.propTypes = {
theme: PropTypes.string,
showSend: PropTypes.bool,
submit: PropTypes.func.isRequired,
showMessageBoxActions: PropTypes.func.isRequired,
isActionsEnabled: PropTypes.bool
};
export default RightButtons;

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