diff --git a/.eslintrc.js b/.eslintrc.js index 085f3a89d..952621fbf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,14 +17,15 @@ module.exports = { legacyDecorators: true } }, - plugins: ['react', 'jsx-a11y', 'import', 'react-native', '@babel'], + plugins: ['react', 'jsx-a11y', 'import', 'react-native', '@babel', 'jest'], env: { browser: true, commonjs: true, es6: true, node: true, jquery: true, - mocha: true + mocha: true, + 'jest/globals': true }, rules: { 'import/extensions': [ diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.ts similarity index 96% rename from app/actions/actionsTypes.js rename to app/actions/actionsTypes.ts index 852ce83ea..ad2d1718d 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.ts @@ -2,8 +2,8 @@ const REQUEST = 'REQUEST'; const SUCCESS = 'SUCCESS'; const FAILURE = 'FAILURE'; const defaultTypes = [REQUEST, SUCCESS, FAILURE]; -function createRequestTypes(base, types = defaultTypes) { - const res = {}; +function createRequestTypes(base = {}, types = defaultTypes): Record { + const res: Record = {}; types.forEach(type => (res[type] = `${base}_${type}`)); return res; } diff --git a/app/actions/activeUsers.js b/app/actions/activeUsers.js deleted file mode 100644 index fc359602c..000000000 --- a/app/actions/activeUsers.js +++ /dev/null @@ -1,8 +0,0 @@ -import { SET_ACTIVE_USERS } from './actionsTypes'; - -export function setActiveUsers(activeUsers) { - return { - type: SET_ACTIVE_USERS, - activeUsers - }; -} diff --git a/app/actions/activeUsers.ts b/app/actions/activeUsers.ts new file mode 100644 index 000000000..737ae86b3 --- /dev/null +++ b/app/actions/activeUsers.ts @@ -0,0 +1,15 @@ +import { Action } from 'redux'; + +import { IActiveUsers } from '../reducers/activeUsers'; +import { SET_ACTIVE_USERS } from './actionsTypes'; + +export interface ISetActiveUsers extends Action { + activeUsers: IActiveUsers; +} + +export type TActionActiveUsers = ISetActiveUsers; + +export const setActiveUsers = (activeUsers: IActiveUsers): ISetActiveUsers => ({ + type: SET_ACTIVE_USERS, + activeUsers +}); diff --git a/app/actions/selectedUsers.js b/app/actions/selectedUsers.js deleted file mode 100644 index 65fbb0015..000000000 --- a/app/actions/selectedUsers.js +++ /dev/null @@ -1,28 +0,0 @@ -import * as types from './actionsTypes'; - -export function addUser(user) { - return { - type: types.SELECTED_USERS.ADD_USER, - user - }; -} - -export function removeUser(user) { - return { - type: types.SELECTED_USERS.REMOVE_USER, - user - }; -} - -export function reset() { - return { - type: types.SELECTED_USERS.RESET - }; -} - -export function setLoading(loading) { - return { - type: types.SELECTED_USERS.SET_LOADING, - loading - }; -} diff --git a/app/actions/selectedUsers.ts b/app/actions/selectedUsers.ts new file mode 100644 index 000000000..6924a5696 --- /dev/null +++ b/app/actions/selectedUsers.ts @@ -0,0 +1,43 @@ +import { Action } from 'redux'; + +import { ISelectedUser } from '../reducers/selectedUsers'; +import * as types from './actionsTypes'; + +type TUser = { + user: ISelectedUser; +}; + +type TAction = Action & TUser; + +interface ISetLoading extends Action { + loading: boolean; +} + +export type TActionSelectedUsers = TAction & ISetLoading; + +export function addUser(user: ISelectedUser): TAction { + return { + type: types.SELECTED_USERS.ADD_USER, + user + }; +} + +export function removeUser(user: ISelectedUser): TAction { + return { + type: types.SELECTED_USERS.REMOVE_USER, + user + }; +} + +export function reset(): Action { + return { + type: types.SELECTED_USERS.RESET + }; +} + +export function setLoading(loading: boolean): ISetLoading { + return { + type: types.SELECTED_USERS.SET_LOADING, + loading + }; +} diff --git a/app/definitions/index.ts b/app/definitions/index.ts new file mode 100644 index 000000000..f9ca20d4a --- /dev/null +++ b/app/definitions/index.ts @@ -0,0 +1,12 @@ +import { RouteProp } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { Dispatch } from 'redux'; + +export interface IBaseScreen, S extends string> { + navigation: StackNavigationProp; + route: RouteProp; + dispatch: Dispatch; + theme: string; +} + +export * from './redux'; diff --git a/app/definitions/redux/index.ts b/app/definitions/redux/index.ts new file mode 100644 index 000000000..e95763e29 --- /dev/null +++ b/app/definitions/redux/index.ts @@ -0,0 +1,31 @@ +import { TActionSelectedUsers } from '../../actions/selectedUsers'; +import { TActionActiveUsers } from '../../actions/activeUsers'; +// REDUCERS +import { IActiveUsers } from '../../reducers/activeUsers'; +import { ISelectedUsers } from '../../reducers/selectedUsers'; + +export interface IApplicationState { + settings: any; + login: any; + meteor: any; + server: any; + selectedUsers: ISelectedUsers; + createChannel: any; + app: any; + room: any; + rooms: any; + sortPreferences: any; + share: any; + customEmojis: any; + activeUsers: IActiveUsers; + usersTyping: any; + inviteLinks: any; + createDiscussion: any; + inquiry: any; + enterpriseModules: any; + encryption: any; + permissions: any; + roles: any; +} + +export type TApplicationActions = TActionActiveUsers & TActionSelectedUsers; diff --git a/app/reducers/activeUsers.js b/app/reducers/activeUsers.js deleted file mode 100644 index 8f6c5b38a..000000000 --- a/app/reducers/activeUsers.js +++ /dev/null @@ -1,15 +0,0 @@ -import { SET_ACTIVE_USERS } from '../actions/actionsTypes'; - -const initialState = {}; - -export default function activeUsers(state = initialState, action) { - switch (action.type) { - case SET_ACTIVE_USERS: - return { - ...state, - ...action.activeUsers - }; - default: - return state; - } -} diff --git a/app/reducers/activeUsers.test.ts b/app/reducers/activeUsers.test.ts new file mode 100644 index 000000000..fbe35207a --- /dev/null +++ b/app/reducers/activeUsers.test.ts @@ -0,0 +1,16 @@ +import { setActiveUsers } from '../actions/activeUsers'; +import { IActiveUsers, initialState } from './activeUsers'; +import { mockedStore } from './mockedStore'; + +describe('test reducer', () => { + it('should return initial state', () => { + const state = mockedStore.getState().activeUsers; + expect(state).toEqual(initialState); + }); + it('should return modified store after action', () => { + const activeUsers: IActiveUsers = { any: { status: 'online', statusText: 'any' } }; + mockedStore.dispatch(setActiveUsers(activeUsers)); + const state = mockedStore.getState().activeUsers; + expect(state).toEqual({ ...activeUsers }); + }); +}); diff --git a/app/reducers/activeUsers.ts b/app/reducers/activeUsers.ts new file mode 100644 index 000000000..9877a5ceb --- /dev/null +++ b/app/reducers/activeUsers.ts @@ -0,0 +1,26 @@ +import { TApplicationActions } from '../definitions'; +import { SET_ACTIVE_USERS } from '../actions/actionsTypes'; + +type TUserStatus = 'online' | 'offline'; +export interface IActiveUser { + status: TUserStatus; + statusText?: string; +} + +export interface IActiveUsers { + [key: string]: IActiveUser; +} + +export const initialState: IActiveUsers = {}; + +export default function activeUsers(state = initialState, action: TApplicationActions): IActiveUsers { + switch (action.type) { + case SET_ACTIVE_USERS: + return { + ...state, + ...action.activeUsers + }; + default: + return state; + } +} diff --git a/app/reducers/mockedStore.ts b/app/reducers/mockedStore.ts new file mode 100644 index 000000000..5a03297f2 --- /dev/null +++ b/app/reducers/mockedStore.ts @@ -0,0 +1,7 @@ +import { applyMiddleware, compose, createStore } from 'redux'; +import createSagaMiddleware from 'redux-saga'; + +import reducers from '.'; + +const enhancers = compose(applyMiddleware(createSagaMiddleware())); +export const mockedStore = createStore(reducers, enhancers); diff --git a/app/reducers/selectedUsers.test.ts b/app/reducers/selectedUsers.test.ts new file mode 100644 index 000000000..329be4f91 --- /dev/null +++ b/app/reducers/selectedUsers.test.ts @@ -0,0 +1,36 @@ +import { addUser, reset, setLoading, removeUser } from '../actions/selectedUsers'; +import { mockedStore } from './mockedStore'; +import { initialState } from './selectedUsers'; + +describe('test selectedUsers reducer', () => { + it('should return initial state', () => { + const state = mockedStore.getState().selectedUsers; + expect(state).toEqual(initialState); + }); + + it('should return modified store after addUser', () => { + const user = { _id: 'xxx', name: 'xxx', fname: 'xxx' }; + mockedStore.dispatch(addUser(user)); + const state = mockedStore.getState().selectedUsers.users; + expect(state).toEqual([user]); + }); + + it('should return empty store after remove user', () => { + const user = { _id: 'xxx', name: 'xxx', fname: 'xxx' }; + mockedStore.dispatch(removeUser(user)); + const state = mockedStore.getState().selectedUsers.users; + expect(state).toEqual([]); + }); + + it('should return initial state after reset', () => { + mockedStore.dispatch(reset()); + const state = mockedStore.getState().selectedUsers; + expect(state).toEqual(initialState); + }); + + it('should return loading after call action', () => { + mockedStore.dispatch(setLoading(true)); + const state = mockedStore.getState().selectedUsers.loading; + expect(state).toEqual(true); + }); +}); diff --git a/app/reducers/selectedUsers.js b/app/reducers/selectedUsers.ts similarity index 55% rename from app/reducers/selectedUsers.js rename to app/reducers/selectedUsers.ts index 42d7982c1..f6573ac9e 100644 --- a/app/reducers/selectedUsers.js +++ b/app/reducers/selectedUsers.ts @@ -1,11 +1,26 @@ +import { TApplicationActions } from '../definitions'; import { SELECTED_USERS } from '../actions/actionsTypes'; -const initialState = { +export interface ISelectedUser { + _id: string; + name: string; + fname: string; + search?: boolean; + // username is used when is from searching + username?: string; +} + +export interface ISelectedUsers { + users: ISelectedUser[]; + loading: boolean; +} + +export const initialState: ISelectedUsers = { users: [], loading: false }; -export default function (state = initialState, action) { +export default function (state = initialState, action: TApplicationActions): ISelectedUsers { switch (action.type) { case SELECTED_USERS.ADD_USER: return { diff --git a/app/views/SelectedUsersView.tsx b/app/views/SelectedUsersView.tsx index 8d4a19fc4..85311154e 100644 --- a/app/views/SelectedUsersView.tsx +++ b/app/views/SelectedUsersView.tsx @@ -1,56 +1,43 @@ +import { Q } from '@nozbe/watermelondb'; +import orderBy from 'lodash/orderBy'; import React from 'react'; -import { StackNavigationProp } from '@react-navigation/stack'; -import { RouteProp } from '@react-navigation/native'; import { FlatList, View } from 'react-native'; import { connect } from 'react-redux'; -import orderBy from 'lodash/orderBy'; -import { Q } from '@nozbe/watermelondb'; import { Subscription } from 'rxjs'; +import { addUser, removeUser, reset } from '../actions/selectedUsers'; +import { themes } from '../constants/colors'; +import * as HeaderButton from '../containers/HeaderButton'; import * as List from '../containers/List'; +import Loading from '../containers/Loading'; +import SafeAreaView from '../containers/SafeAreaView'; +import SearchBox from '../containers/SearchBox'; +import StatusBar from '../containers/StatusBar'; +import { IApplicationState, IBaseScreen } from '../definitions'; +import I18n from '../i18n'; import database from '../lib/database'; import RocketChat from '../lib/rocketchat'; import UserItem from '../presentation/UserItem'; -import Loading from '../containers/Loading'; -import I18n from '../i18n'; -import log, { events, logEvent } from '../utils/log'; -import SearchBox from '../containers/SearchBox'; -import * as HeaderButton from '../containers/HeaderButton'; -import StatusBar from '../containers/StatusBar'; -import { themes } from '../constants/colors'; -import { withTheme } from '../theme'; +import { ISelectedUser } from '../reducers/selectedUsers'; import { getUserSelector } from '../selectors/login'; -import { addUser as addUserAction, removeUser as removeUserAction, reset as resetAction } from '../actions/selectedUsers'; -import { showErrorAlert } from '../utils/info'; -import SafeAreaView from '../containers/SafeAreaView'; -import sharedStyles from './Styles'; import { ChatsStackParamList } from '../stacks/types'; +import { withTheme } from '../theme'; +import { showErrorAlert } from '../utils/info'; +import log, { events, logEvent } from '../utils/log'; +import sharedStyles from './Styles'; const ITEM_WIDTH = 250; const getItemLayout = (_: any, index: number) => ({ length: ITEM_WIDTH, offset: ITEM_WIDTH * index, index }); -interface IUser { - _id: string; - name: string; - fname: string; - search?: boolean; - // username is used when is from searching - username?: string; -} interface ISelectedUsersViewState { maxUsers?: number; - search: IUser[]; - chats: IUser[]; + search: ISelectedUser[]; + chats: ISelectedUser[]; } -interface ISelectedUsersViewProps { - navigation: StackNavigationProp; - route: RouteProp; - baseUrl: string; - addUser(user: IUser): void; - removeUser(user: IUser): void; - reset(): void; - users: IUser[]; +interface ISelectedUsersViewProps extends IBaseScreen { + // REDUX STATE + users: ISelectedUser[]; loading: boolean; user: { id: string; @@ -58,7 +45,7 @@ interface ISelectedUsersViewProps { username: string; name: string; }; - theme: string; + baseUrl: string; } class SelectedUsersView extends React.Component { @@ -75,9 +62,9 @@ class SelectedUsersView extends React.Component el.name === username) !== -1; }; - toggleUser = (user: IUser) => { + toggleUser = (user: ISelectedUser) => { const { maxUsers } = this.state; const { - addUser, - removeUser, + dispatch, users, user: { username } } = this.props; @@ -177,14 +163,14 @@ class SelectedUsersView extends React.Component { + _onPressItem = (id: string, item = {} as ISelectedUser) => { if (item.search) { this.toggleUser({ _id: item._id, name: item.username!, fname: item.name }); } else { @@ -192,7 +178,7 @@ class SelectedUsersView extends React.Component this.toggleUser(item); + _onPressSelectedItem = (item: ISelectedUser) => this.toggleUser(item); renderHeader = () => { const { theme } = this.props; @@ -231,7 +217,7 @@ class SelectedUsersView extends React.Component { + renderSelectedItem = ({ item }: { item: ISelectedUser }) => { const { theme } = this.props; return ( { + renderItem = ({ item, index }: { item: ISelectedUser; index: number }) => { const { search, chats } = this.state; const { theme } = this.props; @@ -308,17 +294,11 @@ class SelectedUsersView extends React.Component ({ +const mapStateToProps = (state: IApplicationState) => ({ baseUrl: state.server.server, users: state.selectedUsers.users, loading: state.selectedUsers.loading, user: getUserSelector(state) }); -const mapDispatchToProps = (dispatch: any) => ({ - addUser: (user: any) => dispatch(addUserAction(user)), - removeUser: (user: any) => dispatch(removeUserAction(user)), - reset: () => dispatch(resetAction()) -}); - -export default connect(mapStateToProps, mapDispatchToProps)(withTheme(SelectedUsersView)); +export default connect(mapStateToProps)(withTheme(SelectedUsersView)); diff --git a/package.json b/package.json index f807f6166..a0fd6d506 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,7 @@ "eslint": "^7.31.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-import": "2.22.0", + "eslint-plugin-jest": "24.7.0", "eslint-plugin-jsx-a11y": "6.3.1", "eslint-plugin-react": "7.20.3", "eslint-plugin-react-native": "3.8.1", diff --git a/yarn.lock b/yarn.lock index b4ca2beb5..05a0d4385 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4593,6 +4593,18 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" +"@typescript-eslint/experimental-utils@^4.0.1": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz#6f2a786a4209fa2222989e9380b5331b2810f7fd" + integrity sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q== + dependencies: + "@types/json-schema" "^7.0.7" + "@typescript-eslint/scope-manager" "4.33.0" + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/typescript-estree" "4.33.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + "@typescript-eslint/parser@^4.28.5": version "4.31.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.31.0.tgz#87b7cd16b24b9170c77595d8b1363f8047121e05" @@ -4611,11 +4623,24 @@ "@typescript-eslint/types" "4.31.0" "@typescript-eslint/visitor-keys" "4.31.0" +"@typescript-eslint/scope-manager@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz#d38e49280d983e8772e29121cf8c6e9221f280a3" + integrity sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ== + dependencies: + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/visitor-keys" "4.33.0" + "@typescript-eslint/types@4.31.0": version "4.31.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.31.0.tgz#9a7c86fcc1620189567dc4e46cad7efa07ee8dce" integrity sha512-9XR5q9mk7DCXgXLS7REIVs+BaAswfdHhx91XqlJklmqWpTALGjygWVIb/UnLh4NWhfwhR5wNe1yTyCInxVhLqQ== +"@typescript-eslint/types@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72" + integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ== + "@typescript-eslint/typescript-estree@4.31.0": version "4.31.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.31.0.tgz#4da4cb6274a7ef3b21d53f9e7147cc76f278a078" @@ -4629,6 +4654,19 @@ semver "^7.3.5" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz#0dfb51c2908f68c5c08d82aefeaf166a17c24609" + integrity sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA== + dependencies: + "@typescript-eslint/types" "4.33.0" + "@typescript-eslint/visitor-keys" "4.33.0" + debug "^4.3.1" + globby "^11.0.3" + is-glob "^4.0.1" + semver "^7.3.5" + tsutils "^3.21.0" + "@typescript-eslint/visitor-keys@4.31.0": version "4.31.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.31.0.tgz#4e87b7761cb4e0e627dc2047021aa693fc76ea2b" @@ -4637,6 +4675,14 @@ "@typescript-eslint/types" "4.31.0" eslint-visitor-keys "^2.0.0" +"@typescript-eslint/visitor-keys@4.33.0": + version "4.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz#2a22f77a41604289b7a186586e9ec48ca92ef1dd" + integrity sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg== + dependencies: + "@typescript-eslint/types" "4.33.0" + eslint-visitor-keys "^2.0.0" + "@ungap/promise-all-settled@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" @@ -8145,6 +8191,13 @@ eslint-plugin-import@^2.17.2: resolve "^1.20.0" tsconfig-paths "^3.11.0" +eslint-plugin-jest@24.7.0: + version "24.7.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-24.7.0.tgz#206ac0833841e59e375170b15f8d0955219c4889" + integrity sha512-wUxdF2bAZiYSKBclsUMrYHH6WxiBreNjyDxbRv345TIvPeoCEgPNEn3Sa+ZrSqsf1Dl9SqqSREXMHExlMMu1DA== + dependencies: + "@typescript-eslint/experimental-utils" "^4.0.1" + eslint-plugin-jsx-a11y@6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.3.1.tgz#99ef7e97f567cc6a5b8dd5ab95a94a67058a2660"