Browse Source

Added planex group search and selection with native UI

Arnaud Vergnet 4 years ago
parent
commit
e693636464

+ 14
- 1
src/components/Custom/AnimatedBottomBar.js View File

@@ -2,12 +2,14 @@
2 2
 
3 3
 import * as React from 'react';
4 4
 import {StyleSheet, View} from "react-native";
5
-import {IconButton, Surface, withTheme} from "react-native-paper";
5
+import {Button, IconButton, Surface, withTheme} from "react-native-paper";
6 6
 import AutoHideComponent from "./AutoHideComponent";
7 7
 
8 8
 type Props = {
9
+    navigation: Object,
9 10
     theme: Object,
10 11
     onPress: Function,
12
+    currentGroup: string,
11 13
 }
12 14
 
13 15
 type State = {
@@ -39,6 +41,10 @@ class AnimatedBottomBar extends React.Component<Props, State> {
39 41
         this.displayModeIcons[DISPLAY_MODES.MONTH] = "calendar-range";
40 42
     }
41 43
 
44
+    shouldComponentUpdate(nextProps: Props) {
45
+        return (nextProps.currentGroup !== this.props.currentGroup);
46
+    }
47
+
42 48
     onScroll = (event: Object) => {
43 49
         this.ref.current.onScroll(event);
44 50
     };
@@ -79,6 +85,13 @@ class AnimatedBottomBar extends React.Component<Props, State> {
79 85
                             style={{marginLeft: 5}}
80 86
                             onPress={() => this.props.onPress('today', undefined)}/>
81 87
                     </View>
88
+                        <Button
89
+                            icon="book-variant"
90
+                            onPress={() => this.props.navigation.navigate('group-select')}
91
+                            style={{maxWidth: '40%'}}
92
+                        >
93
+                            {this.props.currentGroup.replace(/_/g, " ")}
94
+                        </Button>
82 95
                     <View style={{flexDirection: 'row'}}>
83 96
                         <IconButton
84 97
                             icon="chevron-left"

+ 94
- 0
src/components/Lists/GroupListAccordion.js View File

@@ -0,0 +1,94 @@
1
+// @flow
2
+
3
+import * as React from 'react';
4
+import {List} from 'react-native-paper';
5
+import {FlatList} from "react-native";
6
+import {stringMatchQuery} from "../../utils/Search";
7
+
8
+type Props = {
9
+    item: Object,
10
+    onGroupPress: Function,
11
+    currentSearchString: string,
12
+    height: number,
13
+}
14
+
15
+type State = {
16
+    expanded: boolean,
17
+}
18
+
19
+const LIST_ITEM_HEIGHT = 64;
20
+
21
+const REPLACE_REGEX = /_/g;
22
+
23
+export default class GroupListAccordion extends React.Component<Props, State> {
24
+
25
+    state = {
26
+        expanded: false,
27
+    }
28
+
29
+    shouldComponentUpdate(nextProps: Props, nextSate: State) {
30
+        if (nextProps.currentSearchString !== this.props.currentSearchString)
31
+            this.state.expanded = nextProps.currentSearchString.length > 0;
32
+
33
+        return (nextProps.currentSearchString !== this.props.currentSearchString)
34
+            || (nextSate.expanded !== this.state.expanded);
35
+    }
36
+
37
+    onPress = () => this.setState({expanded: !this.state.expanded});
38
+
39
+    keyExtractor = (item: Object) => item.id.toString();
40
+
41
+    renderItem = ({item}: Object) => {
42
+        if (stringMatchQuery(item.name, this.props.currentSearchString)) {
43
+            const onPress = () => this.props.onGroupPress(item);
44
+            return (
45
+                <List.Item
46
+                    title={item.name.replace(REPLACE_REGEX, " ")}
47
+                    onPress={onPress}
48
+                    left={props =>
49
+                        <List.Icon
50
+                            {...props}
51
+                            icon={"chevron-right"}/>}
52
+                    right={props =>
53
+                        <List.Icon
54
+                            {...props}
55
+                            icon={"star"}/>}
56
+                    style={{
57
+                        height: LIST_ITEM_HEIGHT,
58
+                        justifyContent: 'center',
59
+                    }}
60
+                />
61
+            );
62
+        } else
63
+            return null;
64
+    }
65
+
66
+    itemLayout = (data: Object, index: number) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
67
+
68
+    render() {
69
+        const item = this.props.item;
70
+        return (
71
+            <List.Accordion
72
+                title={item.name}
73
+                expanded={this.state.expanded}
74
+                onPress={this.onPress}
75
+                style={{
76
+                    height: this.props.height,
77
+                    justifyContent: 'center',
78
+                }}
79
+            >
80
+                {/*$FlowFixMe*/}
81
+                <FlatList
82
+                    data={item.content}
83
+                    extraData={this.props.currentSearchString}
84
+                    renderItem={this.renderItem}
85
+                    keyExtractor={this.keyExtractor}
86
+                    listKey={item.id}
87
+                    // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
88
+                    getItemLayout={this.itemLayout}
89
+                    removeClippedSubviews={true}
90
+                />
91
+            </List.Accordion>
92
+        );
93
+    }
94
+}

+ 5
- 0
src/managers/AsyncStorageManager.js View File

@@ -79,6 +79,11 @@ export default class AsyncStorageManager {
79 79
             default: '1',
80 80
             current: '',
81 81
         },
82
+        planexCurrentGroup: {
83
+            key: 'planexCurrentGroup',
84
+            default: '',
85
+            current: '',
86
+        },
82 87
     };
83 88
 
84 89
     /**

+ 17
- 0
src/navigation/MainTabNavigator.js View File

@@ -20,6 +20,7 @@ import ScannerScreen from "../screens/ScannerScreen";
20 20
 import MaterialHeaderButtons, {Item} from "../components/Custom/HeaderButton";
21 21
 import FeedItemScreen from "../screens/FeedItemScreen";
22 22
 import {createCollapsibleStack} from "react-navigation-collapsible";
23
+import GroupSelectionScreen from "../screens/GroupSelectionScreen";
23 24
 
24 25
 
25 26
 const TAB_ICONS = {
@@ -283,6 +284,22 @@ function PlanexStackComponent() {
283 284
                     useNativeDriver: false, // native driver does not work with webview
284 285
                 }
285 286
             )}
287
+            {createCollapsibleStack(
288
+                <PlanexStack.Screen
289
+                    name="group-select"
290
+                    component={GroupSelectionScreen}
291
+                    options={({navigation}) => {
292
+                        return {
293
+                            title: 'GroupSelectionScreen',
294
+                            ...TransitionPresets.ModalSlideFromBottomIOS,
295
+                        };
296
+                    }}
297
+                />,
298
+                {
299
+                    collapsedColor: 'transparent',
300
+                    useNativeDriver: true,
301
+                }
302
+            )}
286 303
         </PlanexStack.Navigator>
287 304
     );
288 305
 }

+ 170
- 0
src/screens/GroupSelectionScreen.js View File

@@ -0,0 +1,170 @@
1
+// @flow
2
+
3
+import * as React from 'react';
4
+import {Platform, View} from "react-native";
5
+import i18n from "i18n-js";
6
+import {Searchbar, withTheme} from "react-native-paper";
7
+import {stringMatchQuery} from "../utils/Search";
8
+import WebSectionList from "../components/Lists/WebSectionList";
9
+import GroupListAccordion from "../components/Lists/GroupListAccordion";
10
+
11
+const LIST_ITEM_HEIGHT = 70;
12
+
13
+type Props = {
14
+    navigation: Object,
15
+    route: Object,
16
+    theme: Object,
17
+    collapsibleStack: Object,
18
+}
19
+
20
+type State = {
21
+    currentSearchString: string,
22
+};
23
+
24
+function sortName(a, b) {
25
+    if (a.name.toLowerCase() < b.name.toLowerCase())
26
+        return -1;
27
+    if (a.name.toLowerCase() > b.name.toLowerCase())
28
+        return 1;
29
+    return 0;
30
+}
31
+
32
+const GROUPS_URL = 'http://planex.insa-toulouse.fr/wsAdeGrp.php?projectId=1';
33
+
34
+/**
35
+ * Class defining proximo's article list of a certain category.
36
+ */
37
+class GroupSelectionScreen extends React.Component<Props, State> {
38
+
39
+    constructor(props) {
40
+        super(props);
41
+        this.state = {
42
+            currentSearchString: '',
43
+        };
44
+    }
45
+
46
+    /**
47
+     * Creates the header content
48
+     */
49
+    componentDidMount() {
50
+        this.props.navigation.setOptions({
51
+            headerTitle: this.getSearchBar,
52
+            headerBackTitleVisible: false,
53
+            headerTitleContainerStyle: Platform.OS === 'ios' ?
54
+                {marginHorizontal: 0, width: '70%'} :
55
+                {marginHorizontal: 0, right: 50, left: 50},
56
+        });
57
+    }
58
+
59
+    /**
60
+     * Gets the header search bar
61
+     *
62
+     * @return {*}
63
+     */
64
+    getSearchBar = () => {
65
+        return (
66
+            <Searchbar
67
+                placeholder={i18n.t('proximoScreen.search')}
68
+                onChangeText={this.onSearchStringChange}
69
+            />
70
+        );
71
+    };
72
+
73
+    /**
74
+     * Callback used when the search changes
75
+     *
76
+     * @param str The new search string
77
+     */
78
+    onSearchStringChange = (str: string) => {
79
+        this.setState({currentSearchString: str})
80
+    };
81
+
82
+    /**
83
+     * Callback used when clicking an article in the list.
84
+     * It opens the modal to show detailed information about the article
85
+     *
86
+     * @param item The article pressed
87
+     */
88
+    onListItemPress = (item: Object) => {
89
+        this.props.navigation.navigate("planex", {
90
+            screen: "index",
91
+            params: {group: item}
92
+        });
93
+    };
94
+
95
+    shouldDisplayAccordion(item: Object) {
96
+        let shouldDisplay = false;
97
+        for (let i = 0; i < item.content.length; i++) {
98
+            if (stringMatchQuery(item.content[i].name, this.state.currentSearchString)) {
99
+                shouldDisplay = true;
100
+                break;
101
+            }
102
+        }
103
+        return shouldDisplay;
104
+    }
105
+
106
+    /**
107
+     * Gets a render item for the given article
108
+     *
109
+     * @param item The article to render
110
+     * @return {*}
111
+     */
112
+    renderItem = ({item}: Object) => {
113
+        if (this.shouldDisplayAccordion(item)) {
114
+            return (
115
+                <GroupListAccordion
116
+                    item={item}
117
+                    onGroupPress={this.onListItemPress}
118
+                    currentSearchString={this.state.currentSearchString}
119
+                    height={LIST_ITEM_HEIGHT}
120
+                />
121
+            );
122
+        } else
123
+            return null;
124
+    };
125
+
126
+    generateData(fetchedData: Object) {
127
+        let data = [];
128
+        for (let key in fetchedData) {
129
+            data.push(fetchedData[key]);
130
+        }
131
+        data.sort(sortName);
132
+        return data;
133
+    }
134
+
135
+    /**
136
+     * Creates the dataset to be used in the FlatList
137
+     *
138
+     * @param fetchedData
139
+     * @return {*}
140
+     * */
141
+    createDataset = (fetchedData: Object) => {
142
+        return [
143
+            {
144
+                title: '',
145
+                data: this.generateData(fetchedData)
146
+            }
147
+        ];
148
+    }
149
+
150
+    render() {
151
+        return (
152
+            <View style={{
153
+                height: '100%'
154
+            }}>
155
+                <WebSectionList
156
+                    {...this.props}
157
+                    createDataset={this.createDataset}
158
+                    autoRefreshTime={0}
159
+                    refreshOnFocus={false}
160
+                    fetchUrl={GROUPS_URL}
161
+                    renderItem={this.renderItem}
162
+                    updateData={this.state.currentSearchString}
163
+                    itemHeight={LIST_ITEM_HEIGHT}
164
+                />
165
+            </View>
166
+        );
167
+    }
168
+}
169
+
170
+export default withTheme(GroupSelectionScreen);

+ 64
- 23
src/screens/Websites/PlanexScreen.js View File

@@ -12,9 +12,11 @@ import {withCollapsible} from "../../utils/withCollapsible";
12 12
 import {dateToString, getTimeOnlyString} from "../../utils/Planning";
13 13
 import DateManager from "../../managers/DateManager";
14 14
 import AnimatedBottomBar from "../../components/Custom/AnimatedBottomBar";
15
+import {CommonActions} from "@react-navigation/native";
15 16
 
16 17
 type Props = {
17 18
     navigation: Object,
19
+    route: Object,
18 20
     theme: Object,
19 21
     collapsibleStack: Object,
20 22
 }
@@ -24,6 +26,7 @@ type State = {
24 26
     dialogVisible: boolean,
25 27
     dialogTitle: string,
26 28
     dialogMessage: string,
29
+    currentGroup: Object,
27 30
 }
28 31
 
29 32
 
@@ -105,10 +108,13 @@ const LISTEN_TO_MESSAGES = `
105 108
 document.addEventListener("message", function(event) {
106 109
     //alert(event.data);
107 110
     var data = JSON.parse(event.data);
108
-    $('#calendar').fullCalendar(data.action, data.data);
111
+    if (data.action === "setGroup")
112
+        displayAde(data.data);
113
+    else
114
+        $('#calendar').fullCalendar(data.action, data.data);
109 115
 }, false);`
110 116
 
111
-const CUSTOM_CSS = "body>.container{padding-top:20px; padding-bottom: 50px}header{display:none}.fc-toolbar .fc-center{width:100%}.fc-toolbar .fc-center>*{float:none;width:100%;margin:0}#entite{margin-bottom:5px!important}#entite,#groupe{width:calc(100% - 20px);margin:0 10px}#calendar .fc-left,#calendar .fc-right{display:none}#groupe_visibility{width:100%}#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}";
117
+const CUSTOM_CSS = "body>.container{padding-top:20px; padding-bottom: 50px}header,#entite,#groupe_visibility,#calendar .fc-left,#calendar .fc-right{display:none}.fc-toolbar .fc-center{width:100%}.fc-toolbar .fc-center>*{float:none;width:100%;margin:0}#entite{margin-bottom:5px!important}#entite,#groupe{width:calc(100% - 20px);margin:0 10px}#groupe_visibility{width:100%}#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 118
 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}";
113 119
 
114 120
 const INJECT_STYLE = `
@@ -127,15 +133,6 @@ class PlanexScreen extends React.Component<Props, State> {
127 133
 
128 134
     customInjectedJS: string;
129 135
 
130
-    state = {
131
-        bannerVisible:
132
-            AsyncStorageManager.getInstance().preferences.planexShowBanner.current === '1' &&
133
-            AsyncStorageManager.getInstance().preferences.defaultStartScreen.current !== 'Planex',
134
-        dialogVisible: false,
135
-        dialogTitle: "",
136
-        dialogMessage: "",
137
-    };
138
-
139 136
     /**
140 137
      * Defines custom injected JavaScript to improve the page display on mobile
141 138
      */
@@ -143,16 +140,58 @@ class PlanexScreen extends React.Component<Props, State> {
143 140
         super();
144 141
         this.webScreenRef = React.createRef();
145 142
         this.barRef = React.createRef();
146
-        this.generateInjectedCSS();
143
+
144
+        let currentGroup = AsyncStorageManager.getInstance().preferences.planexCurrentGroup.current;
145
+        if (currentGroup === '')
146
+            currentGroup = {name: "SELECT GROUP", id: 0};
147
+        else
148
+            currentGroup = JSON.parse(currentGroup);
149
+        this.state = {
150
+            bannerVisible:
151
+                AsyncStorageManager.getInstance().preferences.planexShowBanner.current === '1' &&
152
+                AsyncStorageManager.getInstance().preferences.defaultStartScreen.current !== 'Planex',
153
+            dialogVisible: false,
154
+            dialogTitle: "",
155
+            dialogMessage: "",
156
+            currentGroup: currentGroup,
157
+        };
158
+        this.generateInjectedJS(currentGroup.id);
159
+    }
160
+
161
+    componentDidMount() {
162
+        this.props.navigation.addListener('focus', this.onScreenFocus);
163
+    }
164
+
165
+    onScreenFocus = () => {
166
+        this.handleNavigationParams();
167
+    };
168
+
169
+    handleNavigationParams = () => {
170
+        if (this.props.route.params !== undefined) {
171
+            if (this.props.route.params.group !== undefined && this.props.route.params.group !== null) {
172
+                // reset params to prevent infinite loop
173
+                this.selectNewGroup(this.props.route.params.group);
174
+                this.props.navigation.dispatch(CommonActions.setParams({group: null}));
175
+            }
176
+        }
177
+    };
178
+
179
+    selectNewGroup(group: Object) {
180
+        this.sendMessage('setGroup', group.id);
181
+        this.setState({currentGroup: group});
182
+        AsyncStorageManager.getInstance().savePref(
183
+            AsyncStorageManager.getInstance().preferences.planexCurrentGroup.key,
184
+            JSON.stringify(group));
185
+        this.generateInjectedJS(group.id);
147 186
     }
148 187
 
149
-    generateInjectedCSS() {
150
-        this.customInjectedJS =
151
-            "$(document).ready(function() {" +
152
-            OBSERVE_MUTATIONS_INJECTED +
153
-            FULL_CALENDAR_SETTINGS +
154
-            LISTEN_TO_MESSAGES +
155
-            INJECT_STYLE;
188
+    generateInjectedJS(groupID: number) {
189
+        this.customInjectedJS = "$(document).ready(function() {"
190
+            + OBSERVE_MUTATIONS_INJECTED
191
+            + FULL_CALENDAR_SETTINGS
192
+            + "displayAde(" + groupID + ");" // Reset Ade
193
+            + LISTEN_TO_MESSAGES
194
+            + INJECT_STYLE;
156 195
 
157 196
         if (ThemeManager.getNightMode())
158 197
             this.customInjectedJS += "$('head').append('<style>" + CUSTOM_CSS_DARK + "</style>');";
@@ -162,10 +201,10 @@ class PlanexScreen extends React.Component<Props, State> {
162 201
             '});true;'; // Prevents crash on ios
163 202
     }
164 203
 
165
-    componentWillUpdate(prevProps: Props) {
166
-        if (prevProps.theme.dark !== this.props.theme.dark)
167
-            this.generateInjectedCSS();
168
-    }
204
+    // componentWillUpdate(prevProps: Props) {
205
+    //     if (prevProps.theme.dark !== this.props.theme.dark)
206
+    //         this.generateInjectedCSS();
207
+    // }
169 208
 
170 209
     /**
171 210
      * Callback used when closing the banner.
@@ -269,8 +308,10 @@ class PlanexScreen extends React.Component<Props, State> {
269 308
                     ? this.getWebView()
270 309
                     : <View style={{height: '100%'}}>{this.getWebView()}</View>}
271 310
                 <AnimatedBottomBar
311
+                    {...this.props}
272 312
                     ref={this.barRef}
273 313
                     onPress={this.sendMessage}
314
+                    currentGroup={this.state.currentGroup.name}
274 315
                 />
275 316
             </View>
276 317
         );

+ 2
- 1
src/utils/Search.js View File

@@ -11,7 +11,8 @@ export function sanitizeString(str: string): string {
11 11
     return str.toLowerCase()
12 12
         .normalize("NFD")
13 13
         .replace(/[\u0300-\u036f]/g, "")
14
-        .replace(" ", "");
14
+        .replace(/ /g, "")
15
+        .replace(/_/g, "");
15 16
 }
16 17
 
17 18
 export function stringMatchQuery(str: string, query: string) {

Loading…
Cancel
Save