Parcourir la source

#13305 Develop notification page

vbea il y a 2 ans
Parent
commit
324fdba829

+ 1 - 1
Strides-APP/android/app/build.gradle

@@ -3,7 +3,7 @@ apply plugin: 'com.google.gms.google-services'
 
 import com.android.build.OutputFile
 
-def myVersionName = "2.4.6" //★★★★★版本号★★★★★
+def myVersionName = "2.5.0" //★★★★★版本号★★★★★
 /**
  * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
  * and bundleReleaseJsAndAssets).

+ 3 - 3
Strides-APP/app.json

@@ -1,8 +1,8 @@
 {
   "name": "JuicePlus",
   "displayName": "ChargEco",
-  "versionCode": 280,
-  "versionName": "V2.4.6",
+  "versionCode": 290,
+  "versionName": "V2.5.0",
   "product": false,
   "debug": true,
   "isWhitelabel": true,
@@ -11,7 +11,7 @@
     "bookmarks": true,
     "codePush": false,
     "nationally": false,
-    "notifications": false,
+    "notifications": true,
     "support": {
       "phone": "006597285916",
       "whatsapp": false

+ 23 - 0
Strides-APP/app/api/apiNotification.js

@@ -0,0 +1,23 @@
+import { del, get, post } from "./http";
+
+const prefix = 'devicesApi/notification/';
+
+export default {
+  getUnreadTotal() {
+    return get(prefix + "tobe-read-count")
+  },
+  /**
+   * 查询通知列表
+   * @param {String} notificationId 最后一个列表对象的Id(用于分页)
+   * @returns 
+   */
+  getNotificationList(notificationId) {
+    return get(prefix + "alerts", {notificationId})
+  },
+  readMessage(notificationId) {
+    return get(prefix + "read-alert", {notificationId})
+  },
+  deleteMessage(notificationId) {
+    return del(prefix + "alerts/" + notificationId)
+  }
+}

+ 26 - 3
Strides-APP/app/api/http.js

@@ -10,12 +10,11 @@ export const host = hostUrl;
 
 const DEBUG = app.debug && !app.product;
 
-Axios.defaults.timeout = 10000;
+Axios.defaults.timeout = 30000;
 Axios.interceptors.response.use((response) => {
   if (DEBUG) {
     console.log('-------', response.config.method, response.config.url);
-    console.log('-------', JSON.stringify(response.data));
-    console.log('-------', response.status);
+    console.log('-------', response.status, JSON.stringify(response.data));
   }
   if (response.data.code == '401' || response.data.code == '402') {
     setAccessToken('');
@@ -107,6 +106,30 @@ export const upload = (path, params, header) => {
   })
 }
 
+export const del = (path) => {
+  return new Promise((resolve, reject) => {
+    Axios.delete(host + service + path, {
+      method: 'DELETE',
+      headers: {
+        'Accept': 'application/json',
+        'lang': global.currentLocale,
+        'accessToken': global.accessToken ?? ''
+      }
+    }).then(res => {
+      if (res.success) {
+        resolve(res);
+      } else if (res.msg) {
+        reject({err: res.msg, ...res});
+      } else {
+        reject('Request Failed');
+      }
+    }).catch(error => {
+      console.info('HTTP-ERROR', error);
+      reject(error);
+    });
+  });
+}
+
 export const GET = (url, params) => {
   var request = host + service + url;
   if (params) {

+ 9 - 0
Strides-APP/app/i18n/locales/en.js

@@ -57,6 +57,7 @@ export default {
     forgotPassword: "Forgot Password",
     makePayment: "Make Payment",
     myVehicles: "My Vehicles",
+    notifications: "Notifications",
     notificationTest: "Notification Test",
     paymentMethod: "Payment Method",
     paynow: "PAYNOW",
@@ -466,5 +467,13 @@ export default {
     timeWeekDay: "Monday to Friday - 24 Hours",
     btnCallSupport: "Call Support Hotline",
     btnWhatsapp: "Whatsapp Us"
+  },
+  notification: {
+    tabAlerts: "Alerts",
+    tabPromotions: "Promotions",
+    viewMessage: "View Message",
+    deleteMessage: "Delete",
+    confirmDelete: "Are you sure you want to delete this message?",
+    empty: "Empty"
   }
 }

+ 9 - 0
Strides-APP/app/i18n/locales/zh-TW.js

@@ -57,6 +57,7 @@
     forgotPassword: "忘記密碼",
     makePayment: "收銀台",
     myVehicles: "我的車輛",
+    notifications: "通知",
     notificationTest: "Firebase通知測試",
     paymentMethod: "付款方式",
     paynow: "PAYNOW",
@@ -466,5 +467,13 @@
     timeWeekDay: "工作日 - 24小時",
     btnCallSupport: "電話咨詢",
     btnWhatsapp: "Whatsapp"
+  },
+  notification: {
+    tabAlerts: "資訊",
+    tabPromotions: "活動",
+    viewMessage: "消息資訊",
+    deleteMessage: "刪除資訊",
+    confirmDelete: "您確認要刪除這條資訊嗎?",
+    empty: "空空如也"
   }
 }

+ 9 - 0
Strides-APP/app/i18n/locales/zh.js

@@ -57,6 +57,7 @@ export default {
     forgotPassword: "忘记密码",
     makePayment: "收银台",
     myVehicles: "我的车辆",
+    notifications: "通知",
     notificationTest: "Firebase通知测试",
     paymentMethod: "支付方式",
     paynow: "PAYNOW",
@@ -466,5 +467,13 @@ export default {
     timeWeekDay: "工作日 - 24小时",
     btnCallSupport: "拨打电话",
     btnWhatsapp: "Whatsapp"
+  },
+  notification: {
+    tabAlerts: "通知",
+    tabPromotions: "活动",
+    viewMessage: "消息通知",
+    deleteMessage: "删除通知",
+    confirmDelete: "您确定要删除此通知吗?",
+    empty: "空空如也"
   }
 }

+ 18 - 0
Strides-APP/app/pages/Router.js

@@ -56,6 +56,9 @@ import Bookmarks from './bookmark/Bookmarks';
 import MembersList from './member/MembersList';
 import ApplyMember from './member/ApplyMember';
 import Contact from './about/Contact';
+import Notification from './alert/Notification';
+import Message from './alert/Message';
+import Campaign from './alert/Campaign';
 
 export var PageList = {
   'splash': {
@@ -251,6 +254,21 @@ export var PageList = {
     titleScope: 'route.payPerUse',
     component: PayPerUse
   },
+  'notification': {
+    title: 'Notification',
+    titleScope: 'route.notifications',
+    component: Notification
+  },
+  'viewMessage': {
+    title: 'View Message',
+    titleScope: 'notification.viewMessage',
+    component: Message
+  },
+  'viewPromotion': {
+    title: 'View Message',
+    titleScope: 'notification.viewMessage',
+    component: Campaign
+  },
   'notify': {
     title: 'Notification Test',
     titleScope: 'route.notificationTest',

+ 124 - 0
Strides-APP/app/pages/alert/Alerts.js

@@ -0,0 +1,124 @@
+/**
+ * 通知信息列表
+ * @邠心vbe on 2023/08/17
+ */
+import React, { Component } from 'react';
+import { Text, FlatList, StyleSheet, RefreshControl } from 'react-native';
+import apiNotification from '../../api/apiNotification';
+import Dialog from '../../components/Dialog';
+import { MyRefreshProps } from '../../components/ThemesConfig';
+import { PageList } from '../Router';
+import ItemView from './ItemView';
+
+export default class Alerts extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      dataList: [],
+      refreshing: false
+    };
+  }
+
+  componentDidMount() {
+    //this.getMessageList();
+    this.props.navigation.addListener('focus', () => {
+      this.getMessageList();
+    });
+  }
+
+  toDetail(item={}) {
+    if (item.notificationId) {
+      startPage(PageList.viewMessage, {id: item.notificationId});
+    }
+  }
+
+  toDelete(item={}) {
+    if (item.notificationId) {
+      Dialog.showDialog({
+        title: $t("notification.deleteMessage"),
+        message: $t("notification.confirmDelete"),
+        ok: $t("nav.confirm"),
+        callback: (btn) => {
+          if (btn == Dialog.BUTTON_OK) {
+            this.deleteMessage(item.notificationId);
+          }
+        }
+      })
+    }
+  }
+
+  deleteMessage(id) {
+    this.setState({
+      refreshing: true
+    })
+    apiNotification.deleteMessage(id).then(res => {
+      toastShort($t("common.deleteSuccess"))
+      this.getMessageList();
+    }).catch(err => {
+      toastShort(err)
+      this.setState({
+        refreshing: false
+      })
+    })
+  }
+
+  getMessageList() {
+    this.setState({
+      refreshing: true
+    })
+    apiNotification.getNotificationList().then(res => {
+      if (res.data) {
+        this.setState({
+          dataList: res.data
+        })
+      }
+    }).catch(err => {
+      toastShort(err)
+    }).finally(() => {
+      this.setState({
+        refreshing: false
+      })
+    })
+  }
+
+  listItem = (props) => {
+    return (
+      <ItemView
+        {...props}
+        onPress={() => this.toDetail(props.item)}
+        onLongPress={() => this.toDelete(props.item)}
+      />
+    )
+  }
+
+  render() {
+    return (
+      <FlatList
+        style={styles.listView}
+        data={this.state.dataList}
+        renderItem={this.listItem}
+        keyExtractor={item => item.notificationId}
+        refreshControl={
+          <RefreshControl
+            {...MyRefreshProps()}
+            refreshing={this.state.refreshing}
+            onRefresh={() => this.getMessageList()}
+          />
+        }
+        ListEmptyComponent={<Text style={styles.noData}>{$t('notification.empty')}</Text>}
+      />
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  listView: {
+    flex: 1
+  },
+  noData: {
+    color: textPlacehoder,
+    fontSize: 14,
+    padding: 20,
+    textAlign: 'center'
+  }
+})

+ 119 - 0
Strides-APP/app/pages/alert/Campaign.js

@@ -0,0 +1,119 @@
+/**
+ * 活动信息详情
+ * @邠心vbe on 2023/08/17
+ */
+import React, { Component } from 'react';
+import { View, Text, StyleSheet, ScrollView, Image } from 'react-native';
+import apiNotification from '../../api/apiNotification';
+import Button from '../../components/Button';
+import Dialog from '../../components/Dialog';
+import { PageList } from '../Router';
+
+export default class Campaign extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      id: "",
+      messageInfo: {
+        createTime: "",
+        notificationText: "",
+        notificationTitle: "",
+        notificationType: ""
+      }
+    };
+  }
+  
+  componentDidMount() {
+    if (this.props.route?.params?.id) {
+      this.setState({
+        id: this.props.route?.params?.id
+      }, () => {
+        this.readMessage();
+      })
+    }
+  }
+
+  readMessage() {
+    Dialog.showProgressDialog();
+    apiNotification.readMessage(this.state.id).then(res => {
+      if (res.data) {
+        this.setState({
+          messageInfo: res.data
+        });
+      }
+    }).catch(err => {
+      toastShort(err);
+    }).finally(() => {
+      Dialog.dismissLoading();
+    })
+  }
+
+  submitFeedback() {
+    startPage(PageList.feedback);
+  }
+
+  render() {
+    return (
+      <View style={styles.container}>
+        <ScrollView
+          style={ui.flex1}
+          contentContainerStyle={styles.content}
+          stickyHeaderIndices={[1]}>
+          <Image
+            style={styles.topImage}
+            resizeMode="cover"
+          />
+          <View style={styles.header}>
+            <Text
+              style={styles.textTitle}>
+              {this.state.messageInfo.notificationTitle}
+            </Text>
+            <Text
+              style={styles.textDate}
+              numberOfLines={1}>
+              {this.state.messageInfo.createTime}
+            </Text>
+          </View>
+          <Text
+            style={styles.textMessage}>
+            {this.state.messageInfo.notificationText}
+          </Text>
+        </ScrollView>
+      </View>
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: pageBackground
+  },
+  textTitle: {
+    color: textPrimary,
+    fontSize: 18,
+    fontWeight: 'bold'
+  },
+  textDate: {
+    color: textSecondary,
+    fontSize: 10,
+    paddingTop: 1
+  },
+  topImage: {
+    width: $vw(100),
+    height: $vw(80),
+    backgroundColor: "#F0F0F0"
+  },
+  header: {
+    padding: 16,
+    backgroundColor: pageBackground
+  },
+  content: {
+    backgroundColor: pageBackground
+  },
+  textMessage: {
+    color: textPrimary,
+    fontSize: 14,
+    padding: 16,
+  }
+})

+ 167 - 0
Strides-APP/app/pages/alert/ItemView.js

@@ -0,0 +1,167 @@
+/**
+ * 通知消息渲染项目组件
+ * @邠心vbe on 2023/08/17
+ */
+import React from 'react';
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+
+const IconType = ({type, style, size=32, color=colorAccent}) => {
+  switch (type) {
+    case "Applications":
+      return (
+        <MaterialIcons
+          name="admin-panel-settings"
+          size={size}
+          style={style}
+          color={color}
+        />
+      )
+    case "Admin":
+      return (
+        <MaterialCommunityIcons
+          name="message-draw"
+          size={size}
+          style={style}
+          color={color}
+        />
+      )
+    case "Charging":
+      return (
+        <MaterialIcons
+          name="offline-bolt"
+          size={size}
+          style={style}
+          color={color}
+        />
+      )
+    case "Wallet":
+      return (
+        <MaterialCommunityIcons
+          name="cash-multiple"
+          size={size}
+          style={style}
+          color={color}
+        />
+      )
+    case "Announcement":
+      return (
+        <MaterialCommunityIcons
+          name="message-alert"
+          size={size}
+          style={style}
+          color={color}
+        />
+      )
+    case "Review":
+      return (
+        <MaterialCommunityIcons
+          name="message-draw"
+          size={size}
+          style={style}
+          color={color}
+        />
+      )
+    case "Promotion":
+      return (
+        <MaterialCommunityIcons
+          name="bullhorn-variant"
+          size={size}
+          style={style}
+          color={color}
+        />
+      )
+    default:
+      return (
+        <MaterialCommunityIcons
+          name="message-alert"
+          size={size}
+          style={style}
+          color={color}
+        />
+      )
+  }
+}
+
+export default ItemView = ({item, index, separators, onPress, onLongPress}) => {
+  const getDotColor = () => {
+    if (item.readStatus) {
+      if (item?.notificationTitle.indexOf("Low") >= 0/* || item.notificationType == "Announcement"*/) {
+        return "#F09327";
+      } else {
+        return colorAccent;
+      }
+    } else {
+      return "#FF3B30";
+    }
+  }
+  return (
+    <Pressable
+      style={styles.notyItemView}
+      onPress={onPress}
+      android_ripple={ripple}
+      onLongPress={onLongPress}>
+      <IconType
+        style={styles.iconType}
+        type={item.notificationType}
+        color={getDotColor()}
+      />
+      <View style={styles.itemContent}>
+        <View style={ui.flexc}>
+          <Text
+            style={[styles.textTitle, (!item.readStatus && styles.unread)]}
+            numberOfLines={1}
+            ellipsizeMode="tail">
+            {item.notificationTitle}
+          </Text>
+          <Text
+            style={[styles.textDate, (!item.readStatus && styles.unread)]}
+            numberOfLines={1}>
+            {item.createTime}
+          </Text>
+        </View>
+        <Text
+          style={styles.textMessage}
+          numberOfLines={2}>
+          {item.notificationText}
+        </Text>
+      </View>
+    </Pressable>
+  )
+}
+
+const styles = StyleSheet.create({
+  notyItemView: {
+    padding: 16,
+    alignItems: 'center',
+    flexDirection: 'row'
+  },
+  iconType: {
+    marginRight: 12
+  },
+  readIcon: {
+    top: 2,
+    left: -5,
+    width: 7,
+    height: 6,
+    position: 'absolute'
+  },
+  itemContent: {
+    flex: 1
+  },
+  textTitle: {
+    flex: 1,
+    color: textPrimary,
+    fontSize: 15
+  },
+  textDate: {
+    color: textPrimary,
+    fontSize: 12
+  },
+  unread: {
+    fontWeight: 'bold'
+  },
+  textMessage: {
+    color: textPrimary,
+    fontSize: 12
+  }
+})

+ 113 - 0
Strides-APP/app/pages/alert/Message.js

@@ -0,0 +1,113 @@
+/**
+ * 通知信息详情
+ * @邠心vbe on 2023/08/17
+ */
+import React, { Component } from 'react';
+import { View, Text, StyleSheet, ScrollView } from 'react-native';
+import apiNotification from '../../api/apiNotification';
+import Button from '../../components/Button';
+import Dialog from '../../components/Dialog';
+import { PageList } from '../Router';
+
+export default class Message extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      id: "",
+      messageInfo: {
+        createTime: "",
+        notificationText: "",
+        notificationTitle: "",
+        notificationType: ""
+      }
+    };
+  }
+
+  componentDidMount() {
+    if (this.props.route?.params?.id) {
+      this.setState({
+        id: this.props.route?.params?.id
+      }, () => {
+        this.readMessage();
+      })
+    }
+  }
+
+  readMessage() {
+    Dialog.showProgressDialog();
+    apiNotification.readMessage(this.state.id).then(res => {
+      if (res.data) {
+        this.setState({
+          messageInfo: res.data
+        });
+      }
+    }).catch(err => {
+      toastShort(err);
+    }).finally(() => {
+      Dialog.dismissLoading();
+    })
+  }
+
+  submitFeedback() {
+    startPage(PageList.feedback);
+  }
+
+  render() {
+    return (
+      <View style={styles.container}>
+        <View style={styles.header}>
+          <Text
+            style={styles.textTitle}>
+            {this.state.messageInfo.notificationTitle}
+          </Text>
+          <Text
+            style={styles.textDate}
+            numberOfLines={1}>
+            {this.state.messageInfo.createTime}
+          </Text>
+        </View>
+        <ScrollView
+          style={ui.flex1}>
+          <Text
+            style={styles.textMessage}>
+            {this.state.messageInfo.notificationText}
+          </Text>
+        </ScrollView>
+        { this.state.messageInfo.notificationType == "Review" &&
+          <Button
+            elevation={1}
+            style={$margin(8, 16, 20)}
+            text={$t("feedback.submitFeedback")}
+            onClick={() => this.submitFeedback()}
+          />
+        }
+      </View>
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: pageBackground
+  },
+  textTitle: {
+    color: textPrimary,
+    fontSize: 18,
+    fontWeight: 'bold'
+  },
+  textDate: {
+    color: textSecondary,
+    fontSize: 10,
+    paddingTop: 1
+  },
+  header: {
+    padding: 16,
+    backgroundColor: pageBackground
+  },
+  textMessage: {
+    color: textPrimary,
+    fontSize: 14,
+    padding: 16
+  }
+})

+ 98 - 0
Strides-APP/app/pages/alert/Notification.js

@@ -0,0 +1,98 @@
+/**
+ * 通知功能页面适配器
+ * @邠心vbe on 2023/08/17
+ */
+import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
+import React, { Component } from 'react';
+import { StyleSheet, BackHandler } from 'react-native';
+import { PageList } from '../Router';
+import Alerts from './Alerts';
+import Promotions from './Promotions';
+
+export default class Notification extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      refreshing: false
+    };
+    this.pageAdapter = [{
+      title: $t('notification.tabAlerts'),
+      name: "Alerts",
+      component: Alerts
+    }, {
+      title: $t('notification.tabPromotions'),
+      name: "Promotions",
+      component: Promotions
+    }]
+    this.tabBarStyle = {
+      style: styles.tabStyle,
+      pressColor: rippleColor,
+      scrollEnabled: false,
+      indicatorStyle: styles.indicator,
+      activeTintColor: colorLight,
+      inactiveTintColor: "#E0E0E0", 
+    }
+    this.isHide = false;
+  }
+
+  componentDidMount() {
+    this.props.navigation.addListener('focus', () => {
+      this.isHide = false;
+    });
+    this.props.navigation.addListener('blur', () => {
+      this.isHide = true;
+    });
+    BackHandler.addEventListener('hardwareBackPress', this.backPage)
+  }
+
+  componentWillUnmount() {
+    BackHandler.removeEventListener("hardwareBackPress", this.backPage)
+  }
+
+  backPage = () => {
+    //const params = this.props.route.params;
+    if (!this.isHide) {
+      startPage(PageList.home);
+      return true;
+    }
+  }
+
+  onPullRefresh() {
+
+  }
+
+  render() {
+    const Tab = createMaterialTopTabNavigator();
+    return (
+      <Tab.Navigator
+        style={styles.container}
+        tabBarOptions={this.tabBarStyle}
+        lazy={false}
+        lazyPreloadDistance={1}>
+        { this.pageAdapter.map((item, index) => 
+          <Tab.Screen
+            key={index}
+            name={item.name}
+            component={item.component}
+            options={{
+              title: item.title
+            }}
+          />
+        )}
+      </Tab.Navigator>
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: colorLight
+  },
+  tabStyle: {
+    backgroundColor: colorPrimary
+  },
+  indicator: {
+    backgroundColor: colorLight
+  }
+})

+ 135 - 0
Strides-APP/app/pages/alert/Promotions.js

@@ -0,0 +1,135 @@
+/**
+ * 活动信息列表
+ * @邠心vbe on 2023/08/17
+ */
+import React, { Component } from 'react';
+import { View, Text, FlatList, StyleSheet, RefreshControl } from 'react-native';
+import apiNotification from '../../api/apiNotification';
+import Dialog from '../../components/Dialog';
+import { MyRefreshProps } from '../../components/ThemesConfig';
+import { PageList } from '../Router';
+import ItemView from './ItemView';
+
+export default class Promotions extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      dataList: [],
+      refreshing: false
+    };
+    this.open = false;
+  }
+
+  componentDidMount() {
+    //this.getMessageList();
+    if (this.open) {
+      this.props.navigation.addListener('focus', () => {
+        this.getMessageList();
+      });
+    }
+  }
+
+  toDetail(item={}) {
+    if (item.notificationId) {
+      startPage(PageList.viewPromotion, {id: item.notificationId});
+    }
+  }
+
+  toDelete(item={}) {
+    if (item.notificationId) {
+      Dialog.showDialog({
+        title: $t("notification.deleteMessage"),
+        message: $t("notification.confirmDelete"),
+        ok: $t("nav.confirm"),
+        callback: (btn) => {
+          if (btn == Dialog.BUTTON_OK) {
+            this.deleteMessage(item.notificationId);
+          }
+        }
+      })
+    }
+  }
+
+  deleteMessage(id) {
+    this.setState({
+      refreshing: true
+    })
+    apiNotification.deleteMessage(id).then(res => {
+      toastShort($t("common.deleteSuccess"))
+      this.getMessageList();
+    }).catch(err => {
+      toastShort(err)
+      this.setState({
+        refreshing: false
+      })
+    })
+  }
+
+  getMessageList() {
+    this.setState({
+      refreshing: true
+    })
+    apiNotification.getNotificationList().then(res => {
+      if (res.data) {
+        this.setState({
+          dataList: res.data
+        })
+      }
+    }).catch(err => {
+      toastShort(err)
+    }).finally(() => {
+      this.setState({
+        refreshing: false
+      })
+    })
+  }
+
+  listItem = (props) => {
+    return (
+      <ItemView
+        {...props}
+        onPress={() => this.toDetail(props.item)}
+        onLongPress={() => this.toDelete(props.item)}
+      />
+    )
+  }
+
+  render() {
+    if (this.open) {
+      return (
+        <FlatList
+          style={styles.listView}
+          data={this.state.dataList}
+          renderItem={this.listItem}
+          keyExtractor={item => item.notificationId}
+          refreshControl={
+            <RefreshControl
+              {...MyRefreshProps()}
+              refreshing={this.state.refreshing}
+              onRefresh={() => this.getMessageList()}
+            />
+          }
+          ListEmptyComponent={<Text style={styles.noData}>{$t('notification.empty')}</Text>}
+        />
+      );
+    } else {
+      return (
+        <View>
+          <Text style={ui.noData}> Coming soon </Text>
+        </View>
+      );
+    }
+  }
+}
+
+const styles = StyleSheet.create({
+  listView: {
+    flex: 1
+  },
+  noData: {
+    color: textPlacehoder,
+    fontSize: 14,
+    padding: 20,
+    textAlign: 'center'
+  }
+})

+ 61 - 4
Strides-APP/app/pages/home/Drawer.js

@@ -18,6 +18,8 @@ import apiCharge from '../../api/apiCharge';
 import { TouchableWithoutFeedback } from 'react-native-gesture-handler';
 import { toTopupPage } from '../payment/PaymentConfig';
 import utils from '../../utils/utils';
+import apiNotification from '../../api/apiNotification';
+import TextRadius from '../../components/TextRadius';
 
 const Drawer = createDrawerNavigator();
 
@@ -29,6 +31,7 @@ export default class Home extends Component {
     this.state = {
       isLogin: false,
       userInfo: {},
+      notificationCount: 0
     }
   }
 
@@ -45,6 +48,9 @@ export default class Home extends Component {
           userInfo: info
         });
       }, true);
+      if (app.modules.notifications && this.state.isLogin) {
+        this.getNotificationTotal();
+      }
     });
     /*BackHandler.addEventListener('hardwareBackPress', (e) => {
       if (global.dialogId !== 0) {
@@ -72,6 +78,9 @@ export default class Home extends Component {
             }
           }
         }, true);
+        if (app.modules.notifications) {
+          this.getNotificationTotal();
+        }
       });
     }
   }
@@ -92,6 +101,24 @@ export default class Home extends Component {
     Dialog.dismissLoading();
   }
 
+  getNotificationTotal() {
+    apiNotification.getUnreadTotal().then(res => {
+      if (res.data) {
+        this.setState({
+          notificationCount: res.data?.toBeReadCount ?? 0
+        })
+      } else {
+        this.setState({
+          notificationCount: 0
+        })
+      }
+    }).catch(err => {
+      this.setState({
+        notificationCount: 0
+      })
+    })
+  }
+
   render () {
     return (
       <Drawer.Navigator
@@ -102,6 +129,7 @@ export default class Home extends Component {
             isLogin={this.state.isLogin}
             userInfo={this.state.userInfo}
             onLogout={() => this.requestLogout()}
+            notificationCount={this.state.notificationCount}
           />
         }
         drawerType={
@@ -127,7 +155,7 @@ const CustomerDrawerContent = (props) => {
   );
 }
 
-const DrawerContent = ({isLogin, userInfo, onLogout, navigation}) => {
+const DrawerContent = ({isLogin, userInfo, onLogout, notificationCount=0, navigation}) => {
   const getCharging = () => {
     Dialog.showProgressDialog();
     apiCharge.getUserCharging().then(res => {
@@ -320,14 +348,19 @@ const DrawerContent = ({isLogin, userInfo, onLogout, navigation}) => {
           style={styles.itemButton}
           viewStyle={styles.itemView}
           onClick={() => {
-            startPage(PageList.bookmarks);
+            startPage(PageList.notification);
           }}>
           <MaterialIcons
             style={styles.icon}
-            name="stars"
+            name="notifications"
             color="#222"
             size={26}/>
-          <Text style={styles.label}>{$t('route.bookmarks')}</Text>
+          <Text style={styles.label}>{$t('route.notifications')}</Text>
+          { notificationCount > 0 &&
+            notificationCount < 100
+            ? <TextRadius style={styles.bridgeText}>{notificationCount}</TextRadius>
+            : <TextRadius style={styles.bridgeText2}>99+</TextRadius>
+          }
         </Button>
       }
       { (app.modules.bookmarks && isLogin) &&
@@ -679,5 +712,29 @@ const styles = StyleSheet.create({
     color: textCancel,
     fontSize: 14,
     marginRight: 32
+  },
+  bridgeText: {
+    width: 20,
+    height: 20,
+    color: textLight,
+    fontSize: 12,
+    lineHeight: 19,
+    marginRight: 16,
+    borderRadius: 20,
+    fontWeight: 'bold',
+    textAlign: 'center',
+    backgroundColor: "#FF3B30"
+  },
+  bridgeText2: {
+    width: 22,
+    height: 22,
+    color: textLight,
+    fontSize: 10,
+    lineHeight: 22,
+    marginRight: 16,
+    borderRadius: 22,
+    fontWeight: 'bold',
+    textAlign: 'center',
+    backgroundColor: "#FF3B30"
   }
 });

+ 20 - 3
Strides-APP/app/pages/member/MembersList.js

@@ -6,6 +6,8 @@ import React, { Component } from 'react';
 import { View, Text, StyleSheet, Pressable } from 'react-native';
 import apiMember from '../../api/apiMember';
 import { ElevationObject } from '../../components/Button';
+import TextRadius from '../../components/TextRadius';
+import utils from '../../utils/utils';
 
 export default class MembersList extends Component {
   constructor(props) {
@@ -70,8 +72,13 @@ export default class MembersList extends Component {
               size={56}/>
           </View>
           <View style={styles.memberItem}>
-            <Text style={styles.textLabel2}>{$t('members.membership')}:</Text>
-            <Text style={styles.textValue2}>{item.membership}</Text>
+            { utils.isEmpty(item.groupType)
+            ? <Text style={styles.textLabel2}>{$t('members.membership')}:</Text>
+            : <TextRadius style={styles.textType}>{item.groupType}</TextRadius>
+            }
+            <Text
+              style={styles.textValue2}
+              numberOfLines={1}>{item.membership}</Text>
           </View>
           <View style={styles.memberItem}>
             <Text style={styles.textLabel}>{$t('members.membershipNo')}:</Text>
@@ -100,6 +107,7 @@ const styles = StyleSheet.create({
   memberItem: {
     paddingTop: 1,
     paddingBottom: 1,
+    alignItems: 'center',
     flexDirection: 'row'
   },
   textLabel: {
@@ -124,7 +132,16 @@ const styles = StyleSheet.create({
     flex: 1,
     color: textPrimary,
     fontSize: 16,
-    marginBottom: 2,
+    fontWeight: 'bold',
+    marginBottom: 2
+  },
+  textType: {
+    color: textLight,
+    fontSize: 10,
+    marginRight: 4,
+    borderRadius: 2,
+    ...$padding(1, 4),
+    backgroundColor: colorAccent
   },
   itemBackground: {
     right: 0,

+ 1 - 1
Strides-APP/app/pages/search/ListViewV2.js

@@ -13,7 +13,7 @@ import app from '../../../app.json';
 export default ListViewV2 = ({item, index, separators, onPress, onFavorite}) => {
   if (item.id) {
     return (
-      <Pressable 
+      <Pressable
         style={styles.itemView}
         key={index}
         onPress={onPress}