Browse Source

Improve planex components to match linter

Arnaud Vergnet 3 years ago
parent
commit
ab86c1c85c

+ 101
- 82
src/components/Lists/PlanexGroups/GroupListAccordion.js View File

@@ -2,96 +2,115 @@
2 2
 
3 3
 import * as React from 'react';
4 4
 import {List, withTheme} from 'react-native-paper';
5
-import {FlatList, View} from "react-native";
6
-import {stringMatchQuery} from "../../../utils/Search";
7
-import GroupListItem from "./GroupListItem";
8
-import AnimatedAccordion from "../../Animations/AnimatedAccordion";
9
-import type {group, groupCategory} from "../../../screens/Planex/GroupSelectionScreen";
10
-import type {CustomTheme} from "../../../managers/ThemeManager";
5
+import {FlatList, View} from 'react-native';
6
+import {stringMatchQuery} from '../../../utils/Search';
7
+import GroupListItem from './GroupListItem';
8
+import AnimatedAccordion from '../../Animations/AnimatedAccordion';
9
+import type {
10
+  PlanexGroupType,
11
+  PlanexGroupCategoryType,
12
+} from '../../../screens/Planex/GroupSelectionScreen';
13
+import type {CustomTheme} from '../../../managers/ThemeManager';
11 14
 
12
-type Props = {
13
-    item: groupCategory,
14
-    onGroupPress: (group) => void,
15
-    onFavoritePress: (group) => void,
16
-    currentSearchString: string,
17
-    favoriteNumber: number,
18
-    height: number,
19
-    theme: CustomTheme,
20
-}
15
+type PropsType = {
16
+  item: PlanexGroupCategoryType,
17
+  onGroupPress: (PlanexGroupType) => void,
18
+  onFavoritePress: (PlanexGroupType) => void,
19
+  currentSearchString: string,
20
+  favoriteNumber: number,
21
+  height: number,
22
+  theme: CustomTheme,
23
+};
21 24
 
22 25
 const LIST_ITEM_HEIGHT = 64;
23 26
 
24
-class GroupListAccordion extends React.Component<Props> {
25
-
26
-    shouldComponentUpdate(nextProps: Props) {
27
-        return (nextProps.currentSearchString !== this.props.currentSearchString)
28
-            || (nextProps.favoriteNumber !== this.props.favoriteNumber)
29
-            || (nextProps.item.content.length !== this.props.item.content.length);
30
-    }
31
-
32
-    keyExtractor = (item: group) => item.id.toString();
27
+class GroupListAccordion extends React.Component<PropsType> {
28
+  shouldComponentUpdate(nextProps: PropsType): boolean {
29
+    const {props} = this;
30
+    return (
31
+      nextProps.currentSearchString !== props.currentSearchString ||
32
+      nextProps.favoriteNumber !== props.favoriteNumber ||
33
+      nextProps.item.content.length !== props.item.content.length
34
+    );
35
+  }
33 36
 
34
-    renderItem = ({item}: { item: group }) => {
35
-        const onPress = () => this.props.onGroupPress(item);
36
-        const onStarPress = () => this.props.onFavoritePress(item);
37
-        return (
38
-            <GroupListItem
39
-                height={LIST_ITEM_HEIGHT}
40
-                item={item}
41
-                onPress={onPress}
42
-                onStarPress={onStarPress}/>
43
-        );
44
-    }
37
+  getRenderItem = ({item}: {item: PlanexGroupType}): React.Node => {
38
+    const {props} = this;
39
+    const onPress = () => {
40
+      props.onGroupPress(item);
41
+    };
42
+    const onStarPress = () => {
43
+      props.onFavoritePress(item);
44
+    };
45
+    return (
46
+      <GroupListItem
47
+        height={LIST_ITEM_HEIGHT}
48
+        item={item}
49
+        onPress={onPress}
50
+        onStarPress={onStarPress}
51
+      />
52
+    );
53
+  };
45 54
 
46
-    getData() {
47
-        const originalData = this.props.item.content;
48
-        let displayData = [];
49
-        for (let i = 0; i < originalData.length; i++) {
50
-            if (stringMatchQuery(originalData[i].name, this.props.currentSearchString))
51
-                displayData.push(originalData[i]);
52
-        }
53
-        return displayData;
54
-    }
55
+  getData(): Array<PlanexGroupType> {
56
+    const {props} = this;
57
+    const originalData = props.item.content;
58
+    const displayData = [];
59
+    originalData.forEach((data: PlanexGroupType) => {
60
+      if (stringMatchQuery(data.name, props.currentSearchString))
61
+        displayData.push(data);
62
+    });
63
+    return displayData;
64
+  }
55 65
 
56
-    itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
66
+  itemLayout = (
67
+    data: ?Array<PlanexGroupType>,
68
+    index: number,
69
+  ): {length: number, offset: number, index: number} => ({
70
+    length: LIST_ITEM_HEIGHT,
71
+    offset: LIST_ITEM_HEIGHT * index,
72
+    index,
73
+  });
57 74
 
75
+  keyExtractor = (item: PlanexGroupType): string => item.id.toString();
58 76
 
59
-    render() {
60
-        const item = this.props.item;
61
-        return (
62
-            <View>
63
-                <AnimatedAccordion
64
-                    title={item.name}
65
-                    style={{
66
-                        height: this.props.height,
67
-                        justifyContent: 'center',
68
-                    }}
69
-                    left={props =>
70
-                        item.id === 0
71
-                            ? <List.Icon
72
-                                {...props}
73
-                                icon={"star"}
74
-                                color={this.props.theme.colors.tetrisScore}
75
-                            />
76
-                            : null}
77
-                    unmountWhenCollapsed={true}// Only render list if expanded for increased performance
78
-                    opened={this.props.item.id === 0 || this.props.currentSearchString.length > 0}
79
-                >
80
-                    {/*$FlowFixMe*/}
81
-                    <FlatList
82
-                        data={this.getData()}
83
-                        extraData={this.props.currentSearchString}
84
-                        renderItem={this.renderItem}
85
-                        keyExtractor={this.keyExtractor}
86
-                        listKey={item.id.toString()}
87
-                        // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
88
-                        getItemLayout={this.itemLayout}
89
-                        removeClippedSubviews={true}
90
-                    />
91
-                </AnimatedAccordion>
92
-            </View>
93
-        );
94
-    }
77
+  render(): React.Node {
78
+    const {props} = this;
79
+    const {item} = this.props;
80
+    return (
81
+      <View>
82
+        <AnimatedAccordion
83
+          title={item.name}
84
+          style={{
85
+            height: props.height,
86
+            justifyContent: 'center',
87
+          }}
88
+          left={({size}: {size: number}): React.Node =>
89
+            item.id === 0 ? (
90
+              <List.Icon
91
+                size={size}
92
+                icon="star"
93
+                color={props.theme.colors.tetrisScore}
94
+              />
95
+            ) : null
96
+          }
97
+          unmountWhenCollapsed // Only render list if expanded for increased performance
98
+          opened={props.item.id === 0 || props.currentSearchString.length > 0}>
99
+          {/* $FlowFixMe */}
100
+          <FlatList
101
+            data={this.getData()}
102
+            extraData={props.currentSearchString}
103
+            renderItem={this.getRenderItem}
104
+            keyExtractor={this.keyExtractor}
105
+            listKey={item.id.toString()}
106
+            // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
107
+            getItemLayout={this.itemLayout}
108
+            removeClippedSubviews
109
+          />
110
+        </AnimatedAccordion>
111
+      </View>
112
+    );
113
+  }
95 114
 }
96 115
 
97
-export default withTheme(GroupListAccordion)
116
+export default withTheme(GroupListAccordion);

+ 55
- 53
src/components/Lists/PlanexGroups/GroupListItem.js View File

@@ -2,65 +2,67 @@
2 2
 
3 3
 import * as React from 'react';
4 4
 import {IconButton, List, withTheme} from 'react-native-paper';
5
-import type {CustomTheme} from "../../../managers/ThemeManager";
6
-import type {group} from "../../../screens/Planex/GroupSelectionScreen";
5
+import type {CustomTheme} from '../../../managers/ThemeManager';
6
+import type {PlanexGroupType} from '../../../screens/Planex/GroupSelectionScreen';
7 7
 
8
-type Props = {
9
-    theme: CustomTheme,
10
-    onPress: () => void,
11
-    onStarPress: () => void,
12
-    item: group,
13
-    height: number,
14
-}
15
-
16
-type State = {
17
-    isFav: boolean,
18
-}
8
+type PropsType = {
9
+  theme: CustomTheme,
10
+  onPress: () => void,
11
+  onStarPress: () => void,
12
+  item: PlanexGroupType,
13
+  height: number,
14
+};
19 15
 
20
-class GroupListItem extends React.Component<Props, State> {
16
+type StateType = {
17
+  isFav: boolean,
18
+};
21 19
 
22
-    constructor(props) {
23
-        super(props);
24
-        this.state = {
25
-            isFav: (props.item.isFav !== undefined && props.item.isFav),
26
-        }
27
-    }
20
+class GroupListItem extends React.Component<PropsType, StateType> {
21
+  constructor(props: PropsType) {
22
+    super(props);
23
+    this.state = {
24
+      isFav: props.item.isFav !== undefined && props.item.isFav,
25
+    };
26
+  }
28 27
 
29
-    shouldComponentUpdate(prevProps: Props, prevState: State) {
30
-        return (prevState.isFav !== this.state.isFav);
31
-    }
28
+  shouldComponentUpdate(prevProps: PropsType, prevState: StateType): boolean {
29
+    const {isFav} = this.state;
30
+    return prevState.isFav !== isFav;
31
+  }
32 32
 
33
-    onStarPress = () => {
34
-        this.setState({isFav: !this.state.isFav});
35
-        this.props.onStarPress();
36
-    }
33
+  onStarPress = () => {
34
+    const {props} = this;
35
+    this.setState((prevState: StateType): StateType => ({
36
+      isFav: !prevState.isFav,
37
+    }));
38
+    props.onStarPress();
39
+  };
37 40
 
38
-    render() {
39
-        const colors = this.props.theme.colors;
40
-        return (
41
-            <List.Item
42
-                title={this.props.item.name}
43
-                onPress={this.props.onPress}
44
-                left={props =>
45
-                    <List.Icon
46
-                        {...props}
47
-                        icon={"chevron-right"}/>}
48
-                right={props =>
49
-                    <IconButton
50
-                        {...props}
51
-                        icon={"star"}
52
-                        onPress={this.onStarPress}
53
-                        color={this.state.isFav
54
-                            ? colors.tetrisScore
55
-                            : props.color}
56
-                    />}
57
-                style={{
58
-                    height: this.props.height,
59
-                    justifyContent: 'center',
60
-                }}
61
-            />
62
-        );
63
-    }
41
+  render(): React.Node {
42
+    const {props, state} = this;
43
+    const {colors} = props.theme;
44
+    return (
45
+      <List.Item
46
+        title={props.item.name}
47
+        onPress={props.onPress}
48
+        left={({size}: {size: number}): React.Node => (
49
+          <List.Icon size={size} icon="chevron-right" />
50
+        )}
51
+        right={({size, color}: {size: number, color: string}): React.Node => (
52
+          <IconButton
53
+            size={size}
54
+            icon="star"
55
+            onPress={this.onStarPress}
56
+            color={state.isFav ? colors.tetrisScore : color}
57
+          />
58
+        )}
59
+        style={{
60
+          height: props.height,
61
+          justifyContent: 'center',
62
+        }}
63
+      />
64
+    );
65
+  }
64 66
 }
65 67
 
66 68
 export default withTheme(GroupListItem);

+ 268
- 236
src/screens/Planex/GroupSelectionScreen.js View File

@@ -1,44 +1,45 @@
1 1
 // @flow
2 2
 
3 3
 import * as React from 'react';
4
-import {Platform} from "react-native";
5
-import i18n from "i18n-js";
6
-import {Searchbar} from "react-native-paper";
7
-import {stringMatchQuery} from "../../utils/Search";
8
-import WebSectionList from "../../components/Screens/WebSectionList";
9
-import GroupListAccordion from "../../components/Lists/PlanexGroups/GroupListAccordion";
10
-import AsyncStorageManager from "../../managers/AsyncStorageManager";
11
-import {StackNavigationProp} from "@react-navigation/stack";
4
+import {Platform} from 'react-native';
5
+import i18n from 'i18n-js';
6
+import {Searchbar} from 'react-native-paper';
7
+import {StackNavigationProp} from '@react-navigation/stack';
8
+import {stringMatchQuery} from '../../utils/Search';
9
+import WebSectionList from '../../components/Screens/WebSectionList';
10
+import GroupListAccordion from '../../components/Lists/PlanexGroups/GroupListAccordion';
11
+import AsyncStorageManager from '../../managers/AsyncStorageManager';
12 12
 
13 13
 const LIST_ITEM_HEIGHT = 70;
14 14
 
15
-export type group = {
16
-    name: string,
17
-    id: number,
18
-    isFav: boolean,
15
+export type PlanexGroupType = {
16
+  name: string,
17
+  id: number,
18
+  isFav: boolean,
19 19
 };
20 20
 
21
-export type groupCategory = {
22
-    name: string,
23
-    id: number,
24
-    content: Array<group>,
21
+export type PlanexGroupCategoryType = {
22
+  name: string,
23
+  id: number,
24
+  content: Array<PlanexGroupType>,
25 25
 };
26 26
 
27
-type Props = {
28
-    navigation: StackNavigationProp,
29
-}
27
+type PropsType = {
28
+  navigation: StackNavigationProp,
29
+};
30 30
 
31
-type State = {
32
-    currentSearchString: string,
33
-    favoriteGroups: Array<group>,
31
+type StateType = {
32
+  currentSearchString: string,
33
+  favoriteGroups: Array<PlanexGroupType>,
34 34
 };
35 35
 
36
-function sortName(a: group | groupCategory, b: group | groupCategory) {
37
-    if (a.name.toLowerCase() < b.name.toLowerCase())
38
-        return -1;
39
-    if (a.name.toLowerCase() > b.name.toLowerCase())
40
-        return 1;
41
-    return 0;
36
+function sortName(
37
+  a: PlanexGroupType | PlanexGroupCategoryType,
38
+  b: PlanexGroupType | PlanexGroupCategoryType,
39
+): number {
40
+  if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
41
+  if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
42
+  return 0;
42 43
 }
43 44
 
44 45
 const GROUPS_URL = 'http://planex.insa-toulouse.fr/wsAdeGrp.php?projectId=1';
@@ -47,232 +48,263 @@ const REPLACE_REGEX = /_/g;
47 48
 /**
48 49
  * Class defining planex group selection screen.
49 50
  */
50
-class GroupSelectionScreen extends React.Component<Props, State> {
51
-
52
-    constructor(props: Props) {
53
-        super(props);
54
-        this.state = {
55
-            currentSearchString: '',
56
-            favoriteGroups: AsyncStorageManager.getObject(AsyncStorageManager.PREFERENCES.planexFavoriteGroups.key),
57
-        };
51
+class GroupSelectionScreen extends React.Component<PropsType, StateType> {
52
+  /**
53
+   * Removes the given group from the given array
54
+   *
55
+   * @param favorites The array containing favorites groups
56
+   * @param group The group to remove from the array
57
+   */
58
+  static removeGroupFromFavorites(
59
+    favorites: Array<PlanexGroupType>,
60
+    group: PlanexGroupType,
61
+  ) {
62
+    for (let i = 0; i < favorites.length; i += 1) {
63
+      if (group.id === favorites[i].id) {
64
+        favorites.splice(i, 1);
65
+        break;
66
+      }
58 67
     }
68
+  }
59 69
 
60
-    /**
61
-     * Creates the header content
62
-     */
63
-    componentDidMount() {
64
-        this.props.navigation.setOptions({
65
-            headerTitle: this.getSearchBar,
66
-            headerBackTitleVisible: false,
67
-            headerTitleContainerStyle: Platform.OS === 'ios' ?
68
-                {marginHorizontal: 0, width: '70%'} :
69
-                {marginHorizontal: 0, right: 50, left: 50},
70
-        });
71
-    }
70
+  /**
71
+   * Adds the given group to the given array
72
+   *
73
+   * @param favorites The array containing favorites groups
74
+   * @param group The group to add to the array
75
+   */
76
+  static addGroupToFavorites(
77
+    favorites: Array<PlanexGroupType>,
78
+    group: PlanexGroupType,
79
+  ) {
80
+    const favGroup = {...group};
81
+    favGroup.isFav = true;
82
+    favorites.push(favGroup);
83
+    favorites.sort(sortName);
84
+  }
72 85
 
73
-    /**
74
-     * Gets the header search bar
75
-     *
76
-     * @return {*}
77
-     */
78
-    getSearchBar = () => {
79
-        return (
80
-            <Searchbar
81
-                placeholder={i18n.t('screens.proximo.search')}
82
-                onChangeText={this.onSearchStringChange}
83
-            />
84
-        );
86
+  constructor(props: PropsType) {
87
+    super(props);
88
+    this.state = {
89
+      currentSearchString: '',
90
+      favoriteGroups: AsyncStorageManager.getObject(
91
+        AsyncStorageManager.PREFERENCES.planexFavoriteGroups.key,
92
+      ),
85 93
     };
94
+  }
86 95
 
87
-    /**
88
-     * Callback used when the search changes
89
-     *
90
-     * @param str The new search string
91
-     */
92
-    onSearchStringChange = (str: string) => {
93
-        this.setState({currentSearchString: str})
94
-    };
96
+  /**
97
+   * Creates the header content
98
+   */
99
+  componentDidMount() {
100
+    const [navigation] = this.props;
101
+    navigation.setOptions({
102
+      headerTitle: this.getSearchBar,
103
+      headerBackTitleVisible: false,
104
+      headerTitleContainerStyle:
105
+        Platform.OS === 'ios'
106
+          ? {marginHorizontal: 0, width: '70%'}
107
+          : {marginHorizontal: 0, right: 50, left: 50},
108
+    });
109
+  }
95 110
 
96
-    /**
97
-     * Callback used when clicking an article in the list.
98
-     * It opens the modal to show detailed information about the article
99
-     *
100
-     * @param item The article pressed
101
-     */
102
-    onListItemPress = (item: group) => {
103
-        this.props.navigation.navigate("planex", {
104
-            screen: "index",
105
-            params: {group: item}
106
-        });
107
-    };
108
-
109
-    /**
110
-     * Callback used when the user clicks on the favorite button
111
-     *
112
-     * @param item The item to add/remove from favorites
113
-     */
114
-    onListFavoritePress = (item: group) => {
115
-        this.updateGroupFavorites(item);
116
-    };
111
+  /**
112
+   * Gets the header search bar
113
+   *
114
+   * @return {*}
115
+   */
116
+  getSearchBar = (): React.Node => {
117
+    return (
118
+      <Searchbar
119
+        placeholder={i18n.t('screens.proximo.search')}
120
+        onChangeText={this.onSearchStringChange}
121
+      />
122
+    );
123
+  };
117 124
 
118
-    /**
119
-     * Checks if the given group is in the favorites list
120
-     *
121
-     * @param group The group to check
122
-     * @returns {boolean}
123
-     */
124
-    isGroupInFavorites(group: group) {
125
-        let isFav = false;
126
-        for (let i = 0; i < this.state.favoriteGroups.length; i++) {
127
-            if (group.id === this.state.favoriteGroups[i].id) {
128
-                isFav = true;
129
-                break;
130
-            }
131
-        }
132
-        return isFav;
125
+  /**
126
+   * Gets a render item for the given article
127
+   *
128
+   * @param item The article to render
129
+   * @return {*}
130
+   */
131
+  getRenderItem = ({item}: {item: PlanexGroupCategoryType}): React.Node => {
132
+    const {currentSearchString, favoriteGroups} = this.state;
133
+    if (this.shouldDisplayAccordion(item)) {
134
+      return (
135
+        <GroupListAccordion
136
+          item={item}
137
+          onGroupPress={this.onListItemPress}
138
+          onFavoritePress={this.onListFavoritePress}
139
+          currentSearchString={currentSearchString}
140
+          favoriteNumber={favoriteGroups.length}
141
+          height={LIST_ITEM_HEIGHT}
142
+        />
143
+      );
133 144
     }
145
+    return null;
146
+  };
134 147
 
135
-    /**
136
-     * Removes the given group from the given array
137
-     *
138
-     * @param favorites The array containing favorites groups
139
-     * @param group The group to remove from the array
140
-     */
141
-    removeGroupFromFavorites(favorites: Array<group>, group: group) {
142
-        for (let i = 0; i < favorites.length; i++) {
143
-            if (group.id === favorites[i].id) {
144
-                favorites.splice(i, 1);
145
-                break;
146
-            }
147
-        }
148
-    }
148
+  /**
149
+   * Replaces underscore by spaces and sets the favorite state of every group in the given category
150
+   *
151
+   * @param groups The groups to format
152
+   * @return {Array<PlanexGroupType>}
153
+   */
154
+  getFormattedGroups(groups: Array<PlanexGroupType>): Array<PlanexGroupType> {
155
+    return groups.map((group: PlanexGroupType): PlanexGroupType => {
156
+      const newGroup = {...group};
157
+      newGroup.name = group.name.replace(REPLACE_REGEX, ' ');
158
+      newGroup.isFav = this.isGroupInFavorites(group);
159
+      return newGroup;
160
+    });
161
+  }
149 162
 
150
-    /**
151
-     * Adds the given group to the given array
152
-     *
153
-     * @param favorites The array containing favorites groups
154
-     * @param group The group to add to the array
155
-     */
156
-    addGroupToFavorites(favorites: Array<group>, group: group) {
157
-        group.isFav = true;
158
-        favorites.push(group);
159
-        favorites.sort(sortName);
160
-    }
163
+  /**
164
+   * Creates the dataset to be used in the FlatList
165
+   *
166
+   * @param fetchedData
167
+   * @return {*}
168
+   * */
169
+  createDataset = (fetchedData: {
170
+    [key: string]: PlanexGroupCategoryType,
171
+  }): Array<{title: string, data: Array<PlanexGroupCategoryType>}> => {
172
+    return [
173
+      {
174
+        title: '',
175
+        data: this.generateData(fetchedData),
176
+      },
177
+    ];
178
+  };
161 179
 
162
-    /**
163
-     * Adds or removes the given group to the favorites list, depending on whether it is already in it or not.
164
-     * Favorites are then saved in user preferences
165
-     *
166
-     * @param group The group to add/remove to favorites
167
-     */
168
-    updateGroupFavorites(group: group) {
169
-        let newFavorites = [...this.state.favoriteGroups]
170
-        if (this.isGroupInFavorites(group))
171
-            this.removeGroupFromFavorites(newFavorites, group);
172
-        else
173
-            this.addGroupToFavorites(newFavorites, group);
174
-        this.setState({favoriteGroups: newFavorites})
175
-        AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.planexFavoriteGroups.key, newFavorites);
176
-    }
180
+  /**
181
+   * Callback used when the search changes
182
+   *
183
+   * @param str The new search string
184
+   */
185
+  onSearchStringChange = (str: string) => {
186
+    this.setState({currentSearchString: str});
187
+  };
177 188
 
178
-    /**
179
-     * Checks whether to display the given group category, depending on user search query
180
-     *
181
-     * @param item The group category
182
-     * @returns {boolean}
183
-     */
184
-    shouldDisplayAccordion(item: groupCategory) {
185
-        let shouldDisplay = false;
186
-        for (let i = 0; i < item.content.length; i++) {
187
-            if (stringMatchQuery(item.content[i].name, this.state.currentSearchString)) {
188
-                shouldDisplay = true;
189
-                break;
190
-            }
191
-        }
192
-        return shouldDisplay;
193
-    }
189
+  /**
190
+   * Callback used when clicking an article in the list.
191
+   * It opens the modal to show detailed information about the article
192
+   *
193
+   * @param item The article pressed
194
+   */
195
+  onListItemPress = (item: PlanexGroupType) => {
196
+    const {navigation} = this.props;
197
+    navigation.navigate('planex', {
198
+      screen: 'index',
199
+      params: {group: item},
200
+    });
201
+  };
194 202
 
195
-    /**
196
-     * Gets a render item for the given article
197
-     *
198
-     * @param item The article to render
199
-     * @return {*}
200
-     */
201
-    renderItem = ({item}: { item: groupCategory }) => {
202
-        if (this.shouldDisplayAccordion(item)) {
203
-            return (
204
-                <GroupListAccordion
205
-                    item={item}
206
-                    onGroupPress={this.onListItemPress}
207
-                    onFavoritePress={this.onListFavoritePress}
208
-                    currentSearchString={this.state.currentSearchString}
209
-                    favoriteNumber={this.state.favoriteGroups.length}
210
-                    height={LIST_ITEM_HEIGHT}
211
-                />
212
-            );
213
-        } else
214
-            return null;
215
-    };
203
+  /**
204
+   * Callback used when the user clicks on the favorite button
205
+   *
206
+   * @param item The item to add/remove from favorites
207
+   */
208
+  onListFavoritePress = (item: PlanexGroupType) => {
209
+    this.updateGroupFavorites(item);
210
+  };
216 211
 
217
-    /**
218
-     * Generates the dataset to be used in the FlatList.
219
-     * This improves formatting of group names, sorts alphabetically the categories, and adds favorites at the top.
220
-     *
221
-     * @param fetchedData The raw data fetched from the server
222
-     * @returns {[]}
223
-     */
224
-    generateData(fetchedData: { [key: string]: groupCategory }) {
225
-        let data = [];
226
-        for (let key in fetchedData) {
227
-            this.formatGroups(fetchedData[key]);
228
-            data.push(fetchedData[key]);
229
-        }
230
-        data.sort(sortName);
231
-        data.unshift({name: i18n.t("screens.planex.favorites"), id: 0, content: this.state.favoriteGroups});
232
-        return data;
233
-    }
212
+  /**
213
+   * Checks if the given group is in the favorites list
214
+   *
215
+   * @param group The group to check
216
+   * @returns {boolean}
217
+   */
218
+  isGroupInFavorites(group: PlanexGroupType): boolean {
219
+    let isFav = false;
220
+    const {favoriteGroups} = this.state;
221
+    favoriteGroups.forEach((favGroup: PlanexGroupType) => {
222
+      if (group.id === favGroup.id) isFav = true;
223
+    });
224
+    return isFav;
225
+  }
234 226
 
235
-    /**
236
-     * Replaces underscore by spaces and sets the favorite state of every group in the given category
237
-     *
238
-     * @param item The category containing groups to format
239
-     */
240
-    formatGroups(item: groupCategory) {
241
-        for (let i = 0; i < item.content.length; i++) {
242
-            item.content[i].name = item.content[i].name.replace(REPLACE_REGEX, " ")
243
-            item.content[i].isFav = this.isGroupInFavorites(item.content[i]);
244
-        }
245
-    }
227
+  /**
228
+   * Adds or removes the given group to the favorites list, depending on whether it is already in it or not.
229
+   * Favorites are then saved in user preferences
230
+   *
231
+   * @param group The group to add/remove to favorites
232
+   */
233
+  updateGroupFavorites(group: PlanexGroupType) {
234
+    const {favoriteGroups} = this.state;
235
+    const newFavorites = [...favoriteGroups];
236
+    if (this.isGroupInFavorites(group))
237
+      GroupSelectionScreen.removeGroupFromFavorites(newFavorites, group);
238
+    else GroupSelectionScreen.addGroupToFavorites(newFavorites, group);
239
+    this.setState({favoriteGroups: newFavorites});
240
+    AsyncStorageManager.set(
241
+      AsyncStorageManager.PREFERENCES.planexFavoriteGroups.key,
242
+      newFavorites,
243
+    );
244
+  }
246 245
 
247
-    /**
248
-     * Creates the dataset to be used in the FlatList
249
-     *
250
-     * @param fetchedData
251
-     * @return {*}
252
-     * */
253
-    createDataset = (fetchedData: { [key: string]: groupCategory }) => {
254
-        return [
255
-            {
256
-                title: '',
257
-                data: this.generateData(fetchedData)
258
-            }
259
-        ];
246
+  /**
247
+   * Checks whether to display the given group category, depending on user search query
248
+   *
249
+   * @param item The group category
250
+   * @returns {boolean}
251
+   */
252
+  shouldDisplayAccordion(item: PlanexGroupCategoryType): boolean {
253
+    const {currentSearchString} = this.state;
254
+    let shouldDisplay = false;
255
+    for (let i = 0; i < item.content.length; i += 1) {
256
+      if (stringMatchQuery(item.content[i].name, currentSearchString)) {
257
+        shouldDisplay = true;
258
+        break;
259
+      }
260 260
     }
261
+    return shouldDisplay;
262
+  }
261 263
 
262
-    render() {
263
-        return (
264
-            <WebSectionList
265
-                {...this.props}
266
-                createDataset={this.createDataset}
267
-                autoRefreshTime={0}
268
-                refreshOnFocus={false}
269
-                fetchUrl={GROUPS_URL}
270
-                renderItem={this.renderItem}
271
-                updateData={this.state.currentSearchString + this.state.favoriteGroups.length}
272
-                itemHeight={LIST_ITEM_HEIGHT}
273
-            />
274
-        );
275
-    }
264
+  /**
265
+   * Generates the dataset to be used in the FlatList.
266
+   * This improves formatting of group names, sorts alphabetically the categories, and adds favorites at the top.
267
+   *
268
+   * @param fetchedData The raw data fetched from the server
269
+   * @returns {[]}
270
+   */
271
+  generateData(fetchedData: {
272
+    [key: string]: PlanexGroupCategoryType,
273
+  }): Array<PlanexGroupCategoryType> {
274
+    const {favoriteGroups} = this.state;
275
+    const data = [];
276
+    // eslint-disable-next-line flowtype/no-weak-types
277
+    (Object.values(fetchedData): Array<any>).forEach(
278
+      (category: PlanexGroupCategoryType) => {
279
+        const newCat = {...category};
280
+        newCat.content = this.getFormattedGroups(category.content);
281
+        data.push(newCat);
282
+      },
283
+    );
284
+    data.sort(sortName);
285
+    data.unshift({
286
+      name: i18n.t('screens.planex.favorites'),
287
+      id: 0,
288
+      content: favoriteGroups,
289
+    });
290
+    return data;
291
+  }
292
+
293
+  render(): React.Node {
294
+    const {props, state} = this;
295
+    return (
296
+      <WebSectionList
297
+        navigation={props.navigation}
298
+        createDataset={this.createDataset}
299
+        autoRefreshTime={0}
300
+        refreshOnFocus={false}
301
+        fetchUrl={GROUPS_URL}
302
+        renderItem={this.getRenderItem}
303
+        updateData={state.currentSearchString + state.favoriteGroups.length}
304
+        itemHeight={LIST_ITEM_HEIGHT}
305
+      />
306
+    );
307
+  }
276 308
 }
277 309
 
278 310
 export default GroupSelectionScreen;

+ 327
- 306
src/screens/Planex/PlanexScreen.js View File

@@ -1,37 +1,36 @@
1 1
 // @flow
2 2
 
3 3
 import * as React from 'react';
4
-import type {CustomTheme} from "../../managers/ThemeManager";
5
-import ThemeManager from "../../managers/ThemeManager";
6
-import WebViewScreen from "../../components/Screens/WebViewScreen";
7
-import {withTheme} from "react-native-paper";
8
-import i18n from "i18n-js";
9
-import {View} from "react-native";
10
-import AsyncStorageManager from "../../managers/AsyncStorageManager";
11
-import AlertDialog from "../../components/Dialogs/AlertDialog";
12
-import {dateToString, getTimeOnlyString} from "../../utils/Planning";
13
-import DateManager from "../../managers/DateManager";
14
-import AnimatedBottomBar from "../../components/Animations/AnimatedBottomBar";
15
-import {CommonActions} from "@react-navigation/native";
16
-import ErrorView from "../../components/Screens/ErrorView";
17
-import {StackNavigationProp} from "@react-navigation/stack";
18
-import type {group} from "./GroupSelectionScreen";
19
-import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
20
-import MascotPopup from "../../components/Mascot/MascotPopup";
21
-
22
-type Props = {
23
-    navigation: StackNavigationProp,
24
-    route: { params: { group: group } },
25
-    theme: CustomTheme,
26
-}
27
-
28
-type State = {
29
-    dialogVisible: boolean,
30
-    dialogTitle: string,
31
-    dialogMessage: string,
32
-    currentGroup: group,
33
-}
34
-
4
+import {withTheme} from 'react-native-paper';
5
+import i18n from 'i18n-js';
6
+import {View} from 'react-native';
7
+import {CommonActions} from '@react-navigation/native';
8
+import {StackNavigationProp} from '@react-navigation/stack';
9
+import type {CustomTheme} from '../../managers/ThemeManager';
10
+import ThemeManager from '../../managers/ThemeManager';
11
+import WebViewScreen from '../../components/Screens/WebViewScreen';
12
+import AsyncStorageManager from '../../managers/AsyncStorageManager';
13
+import AlertDialog from '../../components/Dialogs/AlertDialog';
14
+import {dateToString, getTimeOnlyString} from '../../utils/Planning';
15
+import DateManager from '../../managers/DateManager';
16
+import AnimatedBottomBar from '../../components/Animations/AnimatedBottomBar';
17
+import ErrorView from '../../components/Screens/ErrorView';
18
+import type {PlanexGroupType} from './GroupSelectionScreen';
19
+import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
20
+import MascotPopup from '../../components/Mascot/MascotPopup';
21
+
22
+type PropsType = {
23
+  navigation: StackNavigationProp,
24
+  route: {params: {group: PlanexGroupType}},
25
+  theme: CustomTheme,
26
+};
27
+
28
+type StateType = {
29
+  dialogVisible: boolean,
30
+  dialogTitle: string,
31
+  dialogMessage: string,
32
+  currentGroup: PlanexGroupType,
33
+};
35 34
 
36 35
 const PLANEX_URL = 'http://planex.insa-toulouse.fr/';
37 36
 
@@ -66,32 +65,32 @@ const PLANEX_URL = 'http://planex.insa-toulouse.fr/';
66 65
 
67 66
 // Watch for changes in the calendar and call the remove alpha function to prevent invisible events
68 67
 const OBSERVE_MUTATIONS_INJECTED =
69
-    'function removeAlpha(node) {\n' +
70
-    '    let bg = node.css("background-color");\n' +
71
-    '    if (bg.match("^rgba")) {\n' +
72
-    '        let a = bg.slice(5).split(\',\');\n' +
73
-    '        // Fix for tooltips with broken background\n' +
74
-    '        if (parseInt(a[0]) === parseInt(a[1]) && parseInt(a[1]) === parseInt(a[2]) && parseInt(a[2]) === 0) {\n' +
75
-    '            a[0] = a[1] = a[2] = \'255\';\n' +
76
-    '        }\n' +
77
-    '        let newBg =\'rgb(\' + a[0] + \',\' + a[1] + \',\' + a[2] + \')\';\n' +
78
-    '        node.css("background-color", newBg);\n' +
79
-    '    }\n' +
80
-    '}\n' +
81
-    '// Observe for planning DOM changes\n' +
82
-    'let observer = new MutationObserver(function(mutations) {\n' +
83
-    '    for (let i = 0; i < mutations.length; i++) {\n' +
84
-    '        if (mutations[i][\'addedNodes\'].length > 0 &&\n' +
85
-    '            ($(mutations[i][\'addedNodes\'][0]).hasClass("fc-event") || $(mutations[i][\'addedNodes\'][0]).hasClass("tooltiptopicevent")))\n' +
86
-    '            removeAlpha($(mutations[i][\'addedNodes\'][0]))\n' +
87
-    '    }\n' +
88
-    '});\n' +
89
-    '// observer.observe(document.querySelector(".fc-body"), {attributes: false, childList: true, characterData: false, subtree:true});\n' +
90
-    'observer.observe(document.querySelector("body"), {attributes: false, childList: true, characterData: false, subtree:true});\n' +
91
-    '// Run remove alpha a first time on whole planning. Useful when code injected after planning fully loaded.\n' +
92
-    '$(".fc-event-container .fc-event").each(function(index) {\n' +
93
-    '    removeAlpha($(this));\n' +
94
-    '});';
68
+  'function removeAlpha(node) {\n' +
69
+  '    let bg = node.css("background-color");\n' +
70
+  '    if (bg.match("^rgba")) {\n' +
71
+  "        let a = bg.slice(5).split(',');\n" +
72
+  '        // Fix for tooltips with broken background\n' +
73
+  '        if (parseInt(a[0]) === parseInt(a[1]) && parseInt(a[1]) === parseInt(a[2]) && parseInt(a[2]) === 0) {\n' +
74
+  "            a[0] = a[1] = a[2] = '255';\n" +
75
+  '        }\n' +
76
+  "        let newBg ='rgb(' + a[0] + ',' + a[1] + ',' + a[2] + ')';\n" +
77
+  '        node.css("background-color", newBg);\n' +
78
+  '    }\n' +
79
+  '}\n' +
80
+  '// Observe for planning DOM changes\n' +
81
+  'let observer = new MutationObserver(function(mutations) {\n' +
82
+  '    for (let i = 0; i < mutations.length; i++) {\n' +
83
+  "        if (mutations[i]['addedNodes'].length > 0 &&\n" +
84
+  '            ($(mutations[i][\'addedNodes\'][0]).hasClass("fc-event") || $(mutations[i][\'addedNodes\'][0]).hasClass("tooltiptopicevent")))\n' +
85
+  "            removeAlpha($(mutations[i]['addedNodes'][0]))\n" +
86
+  '    }\n' +
87
+  '});\n' +
88
+  '// observer.observe(document.querySelector(".fc-body"), {attributes: false, childList: true, characterData: false, subtree:true});\n' +
89
+  'observer.observe(document.querySelector("body"), {attributes: false, childList: true, characterData: false, subtree:true});\n' +
90
+  '// Run remove alpha a first time on whole planning. Useful when code injected after planning fully loaded.\n' +
91
+  '$(".fc-event-container .fc-event").each(function(index) {\n' +
92
+  '    removeAlpha($(this));\n' +
93
+  '});';
95 94
 
96 95
 // Overrides default settings to send a message to the webview when clicking on an event
97 96
 const FULL_CALENDAR_SETTINGS = `
@@ -108,272 +107,294 @@ calendar.option({
108 107
   }
109 108
 });`;
110 109
 
111
-const CUSTOM_CSS = "body>.container{padding-top:20px; padding-bottom: 50px}header,#entite,#groupe_visibility,#calendar .fc-left,#calendar .fc-right{display:none}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-time{font-size:.5rem}#calendar .fc-month-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-month-view .fc-content-skeleton .fc-time{font-size:.7rem}.fc-axis{font-size:.8rem;width:15px!important}.fc-day-header{font-size:.8rem}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}";
112
-const CUSTOM_CSS_DARK = "body{background-color:#121212}.fc-unthemed .fc-content,.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-list-view,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#222}.fc-toolbar .fc-center>*,h2,table{color:#fff}.fc-event-container{color:#121212}.fc-event-container .fc-bg{opacity:0.2;background-color:#000}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}";
110
+const CUSTOM_CSS =
111
+  'body>.container{padding-top:20px; padding-bottom: 50px}header,#entite,#groupe_visibility,#calendar .fc-left,#calendar .fc-right{display:none}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-time{font-size:.5rem}#calendar .fc-month-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-month-view .fc-content-skeleton .fc-time{font-size:.7rem}.fc-axis{font-size:.8rem;width:15px!important}.fc-day-header{font-size:.8rem}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}';
112
+const CUSTOM_CSS_DARK =
113
+  'body{background-color:#121212}.fc-unthemed .fc-content,.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-list-view,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#222}.fc-toolbar .fc-center>*,h2,table{color:#fff}.fc-event-container{color:#121212}.fc-event-container .fc-bg{opacity:0.2;background-color:#000}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}';
113 114
 
114 115
 const INJECT_STYLE = `
115
-$('head').append('<style>` + CUSTOM_CSS + `</style>');
116
+$('head').append('<style>${CUSTOM_CSS}</style>');
116 117
 `;
117 118
 
118 119
 /**
119 120
  * Class defining the app's Planex screen.
120 121
  * This screen uses a webview to render the page
121 122
  */
122
-class PlanexScreen extends React.Component<Props, State> {
123
-
124
-    webScreenRef: { current: null | WebViewScreen };
125
-    barRef: { current: null | AnimatedBottomBar };
126
-
127
-    customInjectedJS: string;
128
-
129
-    /**
130
-     * Defines custom injected JavaScript to improve the page display on mobile
131
-     */
132
-    constructor(props) {
133
-        super(props);
134
-        this.webScreenRef = React.createRef();
135
-        this.barRef = React.createRef();
136
-
137
-        let currentGroup = AsyncStorageManager.getString(AsyncStorageManager.PREFERENCES.planexCurrentGroup.key);
138
-        if (currentGroup === '')
139
-            currentGroup = {name: "SELECT GROUP", id: -1, isFav: false};
140
-        else {
141
-            currentGroup = JSON.parse(currentGroup);
142
-            props.navigation.setOptions({title: currentGroup.name})
143
-        }
144
-        this.state = {
145
-            dialogVisible: false,
146
-            dialogTitle: "",
147
-            dialogMessage: "",
148
-            currentGroup: currentGroup,
149
-        };
150
-        this.generateInjectedJS(currentGroup.id);
123
+class PlanexScreen extends React.Component<PropsType, StateType> {
124
+  webScreenRef: {current: null | WebViewScreen};
125
+
126
+  barRef: {current: null | AnimatedBottomBar};
127
+
128
+  customInjectedJS: string;
129
+
130
+  /**
131
+   * Defines custom injected JavaScript to improve the page display on mobile
132
+   */
133
+  constructor(props: PropsType) {
134
+    super(props);
135
+    this.webScreenRef = React.createRef();
136
+    this.barRef = React.createRef();
137
+
138
+    let currentGroup = AsyncStorageManager.getString(
139
+      AsyncStorageManager.PREFERENCES.planexCurrentGroup.key,
140
+    );
141
+    if (currentGroup === '')
142
+      currentGroup = {name: 'SELECT GROUP', id: -1, isFav: false};
143
+    else {
144
+      currentGroup = JSON.parse(currentGroup);
145
+      props.navigation.setOptions({title: currentGroup.name});
151 146
     }
152
-
153
-    /**
154
-     * Register for events and show the banner after 2 seconds
155
-     */
156
-    componentDidMount() {
157
-        this.props.navigation.addListener('focus', this.onScreenFocus);
158
-    }
159
-
160
-    /**
161
-     * Callback used when the user clicks on the navigate to settings button.
162
-     * This will hide the banner and open the SettingsScreen
163
-     */
164
-    onGoToSettings = () => this.props.navigation.navigate('settings');
165
-
166
-    onScreenFocus = () => {
167
-        this.handleNavigationParams();
147
+    this.state = {
148
+      dialogVisible: false,
149
+      dialogTitle: '',
150
+      dialogMessage: '',
151
+      currentGroup,
168 152
     };
153
+    this.generateInjectedJS(currentGroup.id);
154
+  }
169 155
 
170
-    /**
171
-     * If navigations parameters contain a group, set it as selected
172
-     */
173
-    handleNavigationParams = () => {
174
-        if (this.props.route.params != null) {
175
-            if (this.props.route.params.group !== undefined && this.props.route.params.group !== null) {
176
-                // reset params to prevent infinite loop
177
-                this.selectNewGroup(this.props.route.params.group);
178
-                this.props.navigation.dispatch(CommonActions.setParams({group: null}));
179
-            }
180
-        }
181
-    };
156
+  /**
157
+   * Register for events and show the banner after 2 seconds
158
+   */
159
+  componentDidMount() {
160
+    const {navigation} = this.props;
161
+    navigation.addListener('focus', this.onScreenFocus);
162
+  }
182 163
 
183
-    /**
184
-     * Sends the webpage a message with the new group to select and save it to preferences
185
-     *
186
-     * @param group The group object selected
187
-     */
188
-    selectNewGroup(group: group) {
189
-        this.sendMessage('setGroup', group.id);
190
-        this.setState({currentGroup: group});
191
-        AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.planexCurrentGroup.key, group);
192
-        this.props.navigation.setOptions({title: group.name});
193
-        this.generateInjectedJS(group.id);
194
-    }
164
+  /**
165
+   * Only update the screen if the dark theme changed
166
+   *
167
+   * @param nextProps
168
+   * @returns {boolean}
169
+   */
170
+  shouldComponentUpdate(nextProps: PropsType): boolean {
171
+    const {props, state} = this;
172
+    if (nextProps.theme.dark !== props.theme.dark)
173
+      this.generateInjectedJS(state.currentGroup.id);
174
+    return true;
175
+  }
195 176
 
196
-    /**
197
-     * Generates custom JavaScript to be injected into the webpage
198
-     *
199
-     * @param groupID The current group selected
200
-     */
201
-    generateInjectedJS(groupID: number) {
202
-        this.customInjectedJS = "$(document).ready(function() {"
203
-            + OBSERVE_MUTATIONS_INJECTED
204
-            + FULL_CALENDAR_SETTINGS
205
-            + "displayAde(" + groupID + ");" // Reset Ade
206
-            + (DateManager.isWeekend(new Date()) ? "calendar.next()" : "")
207
-            + INJECT_STYLE;
208
-
209
-        if (ThemeManager.getNightMode())
210
-            this.customInjectedJS += "$('head').append('<style>" + CUSTOM_CSS_DARK + "</style>');";
211
-
212
-        this.customInjectedJS += 'removeAlpha();});true;'; // Prevents crash on ios
213
-    }
177
+  /**
178
+   * Gets the Webview, with an error view on top if no group is selected.
179
+   *
180
+   * @returns {*}
181
+   */
182
+  getWebView(): React.Node {
183
+    const {props, state} = this;
184
+    const showWebview = state.currentGroup.id !== -1;
185
+
186
+    return (
187
+      <View style={{height: '100%'}}>
188
+        {!showWebview ? (
189
+          <ErrorView
190
+            navigation={props.navigation}
191
+            icon="account-clock"
192
+            message={i18n.t('screens.planex.noGroupSelected')}
193
+            showRetryButton={false}
194
+          />
195
+        ) : null}
196
+        <WebViewScreen
197
+          ref={this.webScreenRef}
198
+          navigation={props.navigation}
199
+          url={PLANEX_URL}
200
+          customJS={this.customInjectedJS}
201
+          onMessage={this.onMessage}
202
+          onScroll={this.onScroll}
203
+          showAdvancedControls={false}
204
+        />
205
+      </View>
206
+    );
207
+  }
214 208
 
215
-    /**
216
-     * Only update the screen if the dark theme changed
217
-     *
218
-     * @param nextProps
219
-     * @returns {boolean}
220
-     */
221
-    shouldComponentUpdate(nextProps: Props): boolean {
222
-        if (nextProps.theme.dark !== this.props.theme.dark)
223
-            this.generateInjectedJS(this.state.currentGroup.id);
224
-        return true;
209
+  /**
210
+   * Callback used when the user clicks on the navigate to settings button.
211
+   * This will hide the banner and open the SettingsScreen
212
+   */
213
+  onGoToSettings = () => {
214
+    const {navigation} = this.props;
215
+    navigation.navigate('settings');
216
+  };
217
+
218
+  onScreenFocus = () => {
219
+    this.handleNavigationParams();
220
+  };
221
+
222
+  /**
223
+   * Sends a FullCalendar action to the web page inside the webview.
224
+   *
225
+   * @param action The action to perform, as described in the FullCalendar doc https://fullcalendar.io/docs/v3.
226
+   * Or "setGroup" with the group id as data to set the selected group
227
+   * @param data Data to pass to the action
228
+   */
229
+  sendMessage = (action: string, data: string) => {
230
+    let command;
231
+    if (action === 'setGroup') command = `displayAde(${data})`;
232
+    else command = `$('#calendar').fullCalendar('${action}', '${data}')`;
233
+    if (this.webScreenRef.current != null)
234
+      this.webScreenRef.current.injectJavaScript(`${command};true;`); // Injected javascript must end with true
235
+  };
236
+
237
+  /**
238
+   * Shows a dialog when the user clicks on an event.
239
+   *
240
+   * @param event
241
+   */
242
+  onMessage = (event: {nativeEvent: {data: string}}) => {
243
+    const data: {
244
+      start: string,
245
+      end: string,
246
+      title: string,
247
+      color: string,
248
+    } = JSON.parse(event.nativeEvent.data);
249
+    const startDate = dateToString(new Date(data.start), true);
250
+    const endDate = dateToString(new Date(data.end), true);
251
+    const startString = getTimeOnlyString(startDate);
252
+    const endString = getTimeOnlyString(endDate);
253
+
254
+    let msg = `${DateManager.getInstance().getTranslatedDate(startDate)}\n`;
255
+    if (startString != null && endString != null)
256
+      msg += `${startString} - ${endString}`;
257
+    this.showDialog(data.title, msg);
258
+  };
259
+
260
+  /**
261
+   * Shows a simple dialog to the user.
262
+   *
263
+   * @param title The dialog's title
264
+   * @param message The message to show
265
+   */
266
+  showDialog = (title: string, message: string) => {
267
+    this.setState({
268
+      dialogVisible: true,
269
+      dialogTitle: title,
270
+      dialogMessage: message,
271
+    });
272
+  };
273
+
274
+  /**
275
+   * Hides the dialog
276
+   */
277
+  hideDialog = () => {
278
+    this.setState({
279
+      dialogVisible: false,
280
+    });
281
+  };
282
+
283
+  /**
284
+   * Binds the onScroll event to the control bar for automatic hiding based on scroll direction and speed
285
+   *
286
+   * @param event
287
+   */
288
+  onScroll = (event: SyntheticEvent<EventTarget>) => {
289
+    if (this.barRef.current != null) this.barRef.current.onScroll(event);
290
+  };
291
+
292
+  /**
293
+   * If navigations parameters contain a group, set it as selected
294
+   */
295
+  handleNavigationParams = () => {
296
+    const {props} = this;
297
+    if (props.route.params != null) {
298
+      if (
299
+        props.route.params.group !== undefined &&
300
+        props.route.params.group !== null
301
+      ) {
302
+        // reset params to prevent infinite loop
303
+        this.selectNewGroup(props.route.params.group);
304
+        props.navigation.dispatch(CommonActions.setParams({group: null}));
305
+      }
225 306
     }
307
+  };
308
+
309
+  /**
310
+   * Sends the webpage a message with the new group to select and save it to preferences
311
+   *
312
+   * @param group The group object selected
313
+   */
314
+  selectNewGroup(group: PlanexGroupType) {
315
+    const {navigation} = this.props;
316
+    this.sendMessage('setGroup', group.id.toString());
317
+    this.setState({currentGroup: group});
318
+    AsyncStorageManager.set(
319
+      AsyncStorageManager.PREFERENCES.planexCurrentGroup.key,
320
+      group,
321
+    );
322
+    navigation.setOptions({title: group.name});
323
+    this.generateInjectedJS(group.id);
324
+  }
226 325
 
326
+  /**
327
+   * Generates custom JavaScript to be injected into the webpage
328
+   *
329
+   * @param groupID The current group selected
330
+   */
331
+  generateInjectedJS(groupID: number) {
332
+    this.customInjectedJS = `$(document).ready(function() {${OBSERVE_MUTATIONS_INJECTED}${FULL_CALENDAR_SETTINGS}displayAde(${groupID});${
333
+      // Reset Ade
334
+      DateManager.isWeekend(new Date()) ? 'calendar.next()' : ''
335
+    }${INJECT_STYLE}`;
336
+
337
+    if (ThemeManager.getNightMode())
338
+      this.customInjectedJS += `$('head').append('<style>${CUSTOM_CSS_DARK}</style>');`;
339
+
340
+    this.customInjectedJS += 'removeAlpha();});true;'; // Prevents crash on ios
341
+  }
227 342
 
228
-    /**
229
-     * Sends a FullCalendar action to the web page inside the webview.
230
-     *
231
-     * @param action The action to perform, as described in the FullCalendar doc https://fullcalendar.io/docs/v3.
232
-     * Or "setGroup" with the group id as data to set the selected group
233
-     * @param data Data to pass to the action
234
-     */
235
-    sendMessage = (action: string, data: any) => {
236
-        let command;
237
-        if (action === "setGroup")
238
-            command = "displayAde(" + data + ")";
239
-        else
240
-            command = "$('#calendar').fullCalendar('" + action + "', '" + data + "')";
241
-        if (this.webScreenRef.current != null)
242
-            this.webScreenRef.current.injectJavaScript(command + ';true;'); // Injected javascript must end with true
243
-    };
244
-
245
-    /**
246
-     * Shows a dialog when the user clicks on an event.
247
-     *
248
-     * @param event
249
-     */
250
-    onMessage = (event: { nativeEvent: { data: string } }) => {
251
-        const data: { start: string, end: string, title: string, color: string } = JSON.parse(event.nativeEvent.data);
252
-        const startDate = dateToString(new Date(data.start), true);
253
-        const endDate = dateToString(new Date(data.end), true);
254
-        const startString = getTimeOnlyString(startDate);
255
-        const endString = getTimeOnlyString(endDate);
256
-
257
-        let msg = DateManager.getInstance().getTranslatedDate(startDate) + "\n";
258
-        if (startString != null && endString != null)
259
-            msg += startString + ' - ' + endString;
260
-        this.showDialog(data.title, msg)
261
-    };
262
-
263
-    /**
264
-     * Shows a simple dialog to the user.
265
-     *
266
-     * @param title The dialog's title
267
-     * @param message The message to show
268
-     */
269
-    showDialog = (title: string, message: string) => {
270
-        this.setState({
271
-            dialogVisible: true,
272
-            dialogTitle: title,
273
-            dialogMessage: message,
274
-        });
275
-    };
276
-
277
-    /**
278
-     * Hides the dialog
279
-     */
280
-    hideDialog = () => {
281
-        this.setState({
282
-            dialogVisible: false,
283
-        });
284
-    };
285
-
286
-    /**
287
-     * Binds the onScroll event to the control bar for automatic hiding based on scroll direction and speed
288
-     *
289
-     * @param event
290
-     */
291
-    onScroll = (event: SyntheticEvent<EventTarget>) => {
292
-        if (this.barRef.current != null)
293
-            this.barRef.current.onScroll(event);
294
-    };
295
-
296
-    /**
297
-     * Gets the Webview, with an error view on top if no group is selected.
298
-     *
299
-     * @returns {*}
300
-     */
301
-    getWebView() {
302
-        const showWebview = this.state.currentGroup.id !== -1;
303
-
304
-        return (
305
-            <View style={{height: '100%'}}>
306
-                {!showWebview
307
-                    ? <ErrorView
308
-                        {...this.props}
309
-                        icon={'account-clock'}
310
-                        message={i18n.t("screens.planex.noGroupSelected")}
311
-                        showRetryButton={false}
312
-                    />
313
-                    : null}
314
-                <WebViewScreen
315
-                    ref={this.webScreenRef}
316
-                    navigation={this.props.navigation}
317
-                    url={PLANEX_URL}
318
-                    customJS={this.customInjectedJS}
319
-                    onMessage={this.onMessage}
320
-                    onScroll={this.onScroll}
321
-                    showAdvancedControls={false}
322
-                />
323
-            </View>
324
-        );
325
-    }
326
-
327
-    render() {
328
-        return (
329
-            <View
330
-                style={{flex: 1}}
331
-            >
332
-                {/*Allow to draw webview bellow banner*/}
333
-                <View style={{
334
-                    position: 'absolute',
335
-                    height: '100%',
336
-                    width: '100%',
337
-                }}>
338
-                    {this.props.theme.dark // Force component theme update by recreating it on theme change
339
-                        ? this.getWebView()
340
-                        : <View style={{height: '100%'}}>{this.getWebView()}</View>}
341
-                </View>
342
-                {AsyncStorageManager.getString(AsyncStorageManager.PREFERENCES.defaultStartScreen.key)
343
-                    .toLowerCase() !== 'planex'
344
-                    ? <MascotPopup
345
-                    prefKey={AsyncStorageManager.PREFERENCES.planexShowBanner.key}
346
-                    title={i18n.t("screens.planex.mascotDialog.title")}
347
-                    message={i18n.t("screens.planex.mascotDialog.message")}
348
-                    icon={"emoticon-kiss"}
349
-                    buttons={{
350
-                        action: {
351
-                            message: i18n.t("screens.planex.mascotDialog.ok"),
352
-                            icon: "cog",
353
-                            onPress: this.onGoToSettings,
354
-                        },
355
-                        cancel: {
356
-                            message: i18n.t("screens.planex.mascotDialog.cancel"),
357
-                            icon: "close",
358
-                            color: this.props.theme.colors.warning,
359
-                        }
360
-                    }}
361
-                    emotion={MASCOT_STYLE.INTELLO}
362
-                /> : null }
363
-                <AlertDialog
364
-                    visible={this.state.dialogVisible}
365
-                    onDismiss={this.hideDialog}
366
-                    title={this.state.dialogTitle}
367
-                    message={this.state.dialogMessage}/>
368
-                <AnimatedBottomBar
369
-                    {...this.props}
370
-                    ref={this.barRef}
371
-                    onPress={this.sendMessage}
372
-                    seekAttention={this.state.currentGroup.id === -1}
373
-                />
374
-            </View>
375
-        );
376
-    }
343
+  render(): React.Node {
344
+    const {props, state} = this;
345
+    return (
346
+      <View style={{flex: 1}}>
347
+        {/* Allow to draw webview bellow banner */}
348
+        <View
349
+          style={{
350
+            position: 'absolute',
351
+            height: '100%',
352
+            width: '100%',
353
+          }}>
354
+          {props.theme.dark ? ( // Force component theme update by recreating it on theme change
355
+            this.getWebView()
356
+          ) : (
357
+            <View style={{height: '100%'}}>{this.getWebView()}</View>
358
+          )}
359
+        </View>
360
+        {AsyncStorageManager.getString(
361
+          AsyncStorageManager.PREFERENCES.defaultStartScreen.key,
362
+        ).toLowerCase() !== 'planex' ? (
363
+          <MascotPopup
364
+            prefKey={AsyncStorageManager.PREFERENCES.planexShowBanner.key}
365
+            title={i18n.t('screens.planex.mascotDialog.title')}
366
+            message={i18n.t('screens.planex.mascotDialog.message')}
367
+            icon="emoticon-kiss"
368
+            buttons={{
369
+              action: {
370
+                message: i18n.t('screens.planex.mascotDialog.ok'),
371
+                icon: 'cog',
372
+                onPress: this.onGoToSettings,
373
+              },
374
+              cancel: {
375
+                message: i18n.t('screens.planex.mascotDialog.cancel'),
376
+                icon: 'close',
377
+                color: props.theme.colors.warning,
378
+              },
379
+            }}
380
+            emotion={MASCOT_STYLE.INTELLO}
381
+          />
382
+        ) : null}
383
+        <AlertDialog
384
+          visible={state.dialogVisible}
385
+          onDismiss={this.hideDialog}
386
+          title={state.dialogTitle}
387
+          message={state.dialogMessage}
388
+        />
389
+        <AnimatedBottomBar
390
+          navigation={props.navigation}
391
+          ref={this.barRef}
392
+          onPress={this.sendMessage}
393
+          seekAttention={state.currentGroup.id === -1}
394
+        />
395
+      </View>
396
+    );
397
+  }
377 398
 }
378 399
 
379 400
 export default withTheme(PlanexScreen);

Loading…
Cancel
Save