Explorar o código

add app/pages/alert/ViewAlerts.js

wudebin hai 5 meses
pai
achega
59e0c7a69a

+ 177 - 0
Strides-SPAPP/app/pages/alert/ViewAlerts.js

@@ -0,0 +1,177 @@
+/**
+ * 通知信息详情
+ * @邠心vbe on 2023/08/17
+ */
+import React, { Component } from 'react';
+import { View, StyleSheet, ScrollView } from 'react-native';
+import apiNotification from '../../api/apiNotification';
+import Button, { ElevationObject } from '../../components/Button';
+import HeaderTitle from '../../components/HeaderTitle';
+import TextView from '../../components/TextView';
+import VbeSkeleton from '../../components/VbeSkeleton';
+import { PageList } from '../Router';
+
+export default class ViewAlerts extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      id: "",
+      loading: true,
+      messageInfo: {
+        createTime: "",
+        notificationText: "",
+        notificationTitle: "",
+        notificationType: ""
+      }
+    };
+  }
+
+  componentDidMount() {
+    if (this.props.route?.params?.id) {
+      this.setState({
+        id: this.props.route?.params?.id
+      }, () => {
+        this.readMessage();
+      })
+    }
+  }
+
+  readMessage() {
+    apiNotification.readMessage(this.state.id).then(res => {
+      if (res.data) {
+        this.setState({
+          messageInfo: res.data
+        }, () => {
+          this.setPageTitle();
+        });
+      }
+    }).catch(err => {
+      toastShort(err);
+    });
+  }
+
+  setPageTitle() {
+    if (this.state.messageInfo.notificationTitle) {
+      this.props.navigation.setOptions({
+        headerTitle: () => (<HeaderTitle title={this.state.messageInfo.notificationTitle}/>)
+      });
+      setTimeout(() => {
+        this.setState({
+          loading: false
+        });
+      }, 300);
+    }
+  }
+
+  submitFeedback() {
+    startPage(PageList.feedback);
+  }
+
+  render() {
+    if (this.state.loading) {
+      return (
+        <VbeSkeleton
+          style={styles.loadingView}
+          layout={[
+            {width: '90%', height: 20},
+            {width: '50%', height: 12, marginTop: 8},
+            {width: '100%', height: 15, marginTop: 24},
+            {width: '100%', height: 15, marginTop: 8},
+            {width: '100%', height: 15, marginTop: 8},
+            {width: '100%', height: 15, marginTop: 8},
+            {width: '30%', height: 15, marginTop: 8},
+            {width: '100%', height: 15, marginTop: 24},
+            {width: '100%', height: 15, marginTop: 8},
+            {width: '100%', height: 15, marginTop: 8},
+            {width: '100%', height: 15, marginTop: 8},
+            {width: '30%', height: 15, marginTop: 8},
+            {width: '100%', height: 15, marginTop: 24},
+            {width: '100%', height: 15, marginTop: 8},
+            {width: '100%', height: 15, marginTop: 8},
+            {width: '100%', height: 15, marginTop: 8},
+            {width: '30%', height: 15, marginTop: 8}
+          ]}
+        />
+      );
+    }
+    return (
+      <View
+        style={styles.container}>
+        <View style={styles.header}>
+          <TextView
+            style={styles.textTitle}>
+            {this.state.messageInfo.notificationTitle}
+          </TextView>
+          <View style={ui.flexc}>
+            <MaterialCommunityIcons
+              name="clock-time-four-outline"
+              color={textSecondary}
+              size={12}/>
+            <TextView
+              style={styles.textDate}
+              numberOfLines={1}>
+              {this.state.messageInfo.createTime}
+            </TextView>
+          </View>
+        </View>
+        <View style={styles.divideView}></View>
+        <ScrollView
+          style={ui.flex1}>
+          <TextView
+            style={styles.textMessage}
+            selectable={true}>
+            {this.state.messageInfo.notificationText}
+          </TextView>
+        </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
+  },
+  loadingView: {
+    flex: 1,
+    padding: 16,
+    justifyContent: 'flex-start',
+    backgroundColor: pageBackground
+  },
+  textTitle: {
+    color: textPrimary,
+    fontSize: 14,
+    fontWeight: 'bold',
+    paddingBottom: 2
+  },
+  textDate: {
+    color: textSecondary,
+    fontSize: 12,
+    paddingLeft: 2
+  },
+  header: {
+    padding: 16,
+    //...ElevationObject(1),
+    backgroundColor: pageBackground
+  },
+  divideView: {
+    height: 1,
+    marginLeft: 16,
+    marginRight: 16,
+    backgroundColor: colorPrimary
+  },
+  textMessage: {
+    color: textPrimary,
+    fontSize: 12,
+    padding: 16
+  }
+})

+ 323 - 0
Strides-SPAPP/app/pages/alert/ViewArticle.js

@@ -0,0 +1,323 @@
+/**
+ * 文章详情
+ * @邠心vbe on 2023/10/24
+ */
+import React, { Component } from 'react';
+import { View,  StyleSheet, Image, ScrollView, Linking, Animated, Easing } from 'react-native';
+import Swiper from 'react-native-swiper';
+import apiArticle from '../../api/apiArticle';
+import TextView from '../../components/TextView';
+import VbeSkeleton from '../../components/VbeSkeleton';
+import utils from '../../utils/utils';
+import { PagerView } from './ViewUtil';
+import MyStatusBar from '../../components/MyStatusBar';
+import Toolbar, { BackButton } from '../../components/Toolbar';
+
+export default class ViewArticle extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      id: "",
+      loading: true,
+      showTitleBar: false,
+      messageInfo: {
+        articleTypeName: "",
+        articleTitle: "",
+        articleContent: ""
+      },
+      opacity: new Animated.Value(0)
+    };
+  }
+
+  componentDidMount() {
+    if (this.props.route?.params?.id) {
+      this.setState({
+        id: this.props.route?.params?.id
+      }, () => {
+        this.readMessage();
+      })
+    }
+    MyStatusBar.setStatusBarTheme(MyStatusBar.LIGHT_STYLE);
+    this.props.navigation.addListener('beforeRemove', (e) => {
+      MyStatusBar.setStatusBarTheme(MyStatusBar.DEFAULT_STYLE);
+    });
+  }
+
+  readMessage() {
+    apiArticle.readMessage(this.state.id).then(res => {
+      if (res.data) {
+        this.setState({
+          loading: false,
+          messageInfo: res.data
+        });
+        //this.setPageTitle();
+      }
+    }).catch(err => {
+      toastShort(err);
+    });
+  }
+
+  setPageTitle() {
+    if (this.state.messageInfo.articleTitle) {
+      /*this.props.navigation.setOptions({
+        headerTitle: () => (<HeaderTitle title={this.state.messageInfo.articleTitle}/>)
+      })*/
+      setTimeout(() => {
+        this.setState({
+          loading: false
+        });
+      }, 300);
+    }
+  }
+
+  accessLink(url) {
+    Linking.openURL(utils.getImageUrl(url))
+  }
+
+  onScrollView(e) {
+    if (e.nativeEvent.contentOffset) {
+      const isR = e.nativeEvent.contentOffset.y >= $vw(95);
+      if (isR != this.state.showTitleBar) {
+        this.setState({
+          showTitleBar: isR
+        });
+        if (isR) {
+          this.startTitleAnimate();
+        } else {
+          this.hideTitleAnimate();
+        }
+        MyStatusBar.setStatusBarTheme(isR ? MyStatusBar.DEFAULT_STYLE : MyStatusBar.LIGHT_STYLE);
+      }
+    }
+  }
+
+  startTitleAnimate() {
+    Animated.timing(this.state.opacity, {
+      toValue: 1,
+      duration: 250,
+      easing: Easing.linear,
+      useNativeDriver: true
+    }).start(() => {
+      
+    });
+  }
+
+  hideTitleAnimate() {
+    Animated.timing(this.state.opacity, {
+      toValue: 0,
+      duration: 250,
+      easing: Easing.linear,
+      useNativeDriver: true
+    }).start(() => {
+      
+    });
+  }
+
+  render() {
+    if (this.state.loading) {
+      return (
+        <View style={styles.container}>
+          <VbeSkeleton
+            style={{width: $width, height: $width}}
+            layout={[
+              {width: $width, height: $width}
+            ]}
+            animationDirection={"horizontalRight"}/>
+          <VbeSkeleton
+            style={styles.loadingView}
+            layout={[
+              {width: '90%', height: 20, marginTop: 8},
+              {width: '50%', height: 12, marginTop: 8},
+              {width: '100%', height: 15, marginTop: 24},
+              {width: '100%', height: 15, marginTop: 8},
+              {width: '100%', height: 15, marginTop: 8},
+              {width: '100%', height: 15, marginTop: 8},
+              {width: '30%', height: 15, marginTop: 8},
+              {width: '100%', height: 15, marginTop: 24},
+              {width: '100%', height: 15, marginTop: 8},
+              {width: '100%', height: 15, marginTop: 8},
+              {width: '100%', height: 15, marginTop: 8},
+              {width: '30%', height: 15, marginTop: 8}
+            ]}
+            animationDirection={"horizontalRight"}/>
+        </View>
+      )
+    } else {
+      return (
+        <>
+        <ScrollView
+          style={styles.container}
+          onScroll={e => this.onScrollView(e)}
+          scrollEventThrottle={16}
+          contentContainerStyle={$padding(0,0,32)}>
+          { utils.isNotEmpty(this.state.messageInfo.articleImages) &&
+            <Swiper
+              style={{height: $width}}
+              autoplay={true}
+              autoplayTimeout={5}
+              renderPagination={(index,total) => <PagerView index={index+1} total={total}/> }
+              removeClippedSubviews={false}>
+              { this.state.messageInfo.articleImages.map((item, index) => {
+                return (
+                  <Image
+                    key={index}
+                    style={{width: $width, height: $width}}
+                    source={{uri: utils.getImageUrl(item.articleImagePath)}}/>
+                );
+              })}
+            </Swiper>
+          }
+          <View style={styles.header}>
+            <TextView
+              style={styles.textTitle}>
+              {this.state.messageInfo.articleTitle}
+            </TextView>
+            <View style={ui.flexc}>
+              <MaterialCommunityIcons
+                name="clock-time-four-outline"
+                color={textSecondary}
+                size={12}/>
+              <TextView
+                style={styles.textDate}
+                numberOfLines={1}>
+                {this.state.messageInfo.createTime}
+              </TextView>
+              <View style={ui.flexc}>
+                <MaterialCommunityIcons
+                  name="eye-check-outline"
+                  size={12}
+                  color={textPrimary}/>
+                <TextView
+                  style={styles.textView}
+                  numberOfLines={1}>
+                  {this.state.messageInfo.articleViews}
+                </TextView>
+              </View>
+            </View>
+            <View style={ui.flex}>
+              <TextView 
+                style={styles.labelTypeText}
+                numberOfLines={1}>
+                {this.state.messageInfo.articleTypeName}
+              </TextView>
+            </View>
+          </View>
+          <TextView
+            style={styles.textMessage}
+            selectable={true}>
+            {this.state.messageInfo.articleContent}
+          </TextView>
+          { utils.isNotEmpty(this.state.messageInfo.articleLinks) &&
+            <>
+              <TextView style={styles.textLinkTitle}>{$t("notification.labelLinks")}</TextView>
+              { this.state.messageInfo.articleLinks.map((item, index) =>
+                <View style={styles.itemLink} key={index}>
+                  <TextView style={styles.linkIndex}>{index + 1}.</TextView>
+                  <TextView
+                    style={styles.linkHyper}
+                    onPress={() => this.accessLink(item.articleLink)}>{item.articleLinkName}</TextView>
+                </View>
+              )}
+            </>
+          }
+          <EndView/>
+        </ScrollView>
+        { !this.state.showTitleBar &&
+          <View style={[styles.toolbar, {top: statusHeight}]}>
+            <BackButton style={styles.backIcon} color={"#F0F0F0"}/>
+          </View>
+        }
+        <Animated.View style={[styles.toolbar, {opacity: this.state.opacity}]}>
+          <Toolbar title={this.state.messageInfo.articleTitle}/>
+        </Animated.View>
+        </>
+      );
+    }
+  }
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: pageBackground
+  },
+  toolbar: {
+    top: 0,
+    left: 0,
+    right: 0,
+    zIndex: 2,
+    position: 'absolute'
+  },
+  backIcon: {
+    width: 48,
+    height: toolbarSize,
+    zIndex: 2,
+    marginLeft: isIOS ? 0 : 2,
+    alignItems: 'center',
+    justifyContent: 'center'
+  },
+  loadingView: {
+    flex: 1,
+    padding: 16,
+    justifyContent: 'flex-start',
+    backgroundColor: pageBackground
+  },
+  textTitle: {
+    color: textPrimary,
+    fontSize: 18,
+    fontWeight: 'bold',
+    paddingBottom: 2
+  },
+  textDate: {
+    flex: 1,
+    color: textSecondary,
+    fontSize: 10,
+    paddingLeft: 2
+  },
+  textView: {
+    color: textSecondary,
+    fontSize: 10,
+    padding: 4
+  },
+  header: {
+    padding: 16,
+    backgroundColor: pageBackground
+  },
+  textMessage: {
+    color: textPrimary,
+    fontSize: 12,
+    paddingLeft: 16,
+    paddingRight: 16
+  },
+  labelTypeText: {
+    fontSize: 12,
+    fontWeight: 'bold',
+    borderWidth: 1,
+    borderRadius: 4,
+    borderColor: colorPrimary,
+    marginTop: 8,
+    ...$padding(2, 6)
+  },
+  textLinkTitle: {
+    color: textPrimary,
+    fontSize: 14,
+    fontWeight: 'bold',
+    padding: 16
+  },
+  itemLink: {
+    ...$padding(0, 16, 8),
+    flexDirection: 'row'
+  },
+  linkIndex: {
+    fontSize: 12,
+    paddingRight: 2
+  },
+  linkHyper: {
+    ...ui.link,
+    fontSize: 12,
+    textDecorationLine: 'underline'
+  },
+  linkActive: {
+    color: "#FF3B30"
+  }
+})

+ 363 - 0
Strides-SPAPP/app/pages/alert/ViewCampaign.js

@@ -0,0 +1,363 @@
+/**
+ * 活动详情
+ * @邠心vbe on 2023/08/17
+ */
+import React, { Component } from 'react';
+import { View,  StyleSheet, Image, ScrollView, Linking, Animated, Easing } from 'react-native';
+import Swiper from 'react-native-swiper';
+import apiArticle from '../../api/apiArticle';
+import TextView from '../../components/TextView';
+import VbeSkeleton from '../../components/VbeSkeleton';
+import utils from '../../utils/utils';
+import { PagerView } from './ViewUtil';
+import MyStatusBar from '../../components/MyStatusBar';
+import Toolbar, { BackButton } from '../../components/Toolbar';
+
+export default class ViewCampaign extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      id: "",
+      loading: true,
+      showTitleBar: false,
+      messageInfo: {
+        articleTypeName: "",
+        articleTitle: "",
+        articleContent: ""
+      },
+      opacity: new Animated.Value(0)
+    };
+  }
+
+  componentDidMount() {
+    if (this.props.route?.params?.id) {
+      this.setState({
+        id: this.props.route?.params?.id
+      }, () => {
+        this.readMessage();
+      })
+    }
+    MyStatusBar.setStatusBarTheme(MyStatusBar.LIGHT_STYLE);
+    this.props.navigation.addListener('beforeRemove', (e) => {
+      MyStatusBar.setStatusBarTheme(MyStatusBar.DEFAULT_STYLE);
+    });
+  }
+
+  readMessage() {
+    apiArticle.readMessage(this.state.id).then(res => {
+      if (res.data) {
+        this.setState({
+          loading: false,
+          messageInfo: res.data
+        });
+        //this.setPageTitle();
+      }
+    }).catch(err => {
+      toastShort(err);
+    });
+  }
+
+  setPageTitle() {
+    if (this.state.messageInfo.articleTitle) {
+      /*this.props.navigation.setOptions({
+        headerTitle: () => (<HeaderTitle title={this.state.messageInfo.articleTitle}/>)
+      })*/
+      setTimeout(() => {
+        this.setState({
+          loading: false
+        });
+      }, 300);
+    }
+  }
+
+  accessLink(url) {
+    Linking.openURL(utils.getImageUrl(url))
+  }
+
+  getColorByType(type) {
+    switch (type) {
+      case "Ended":
+        return {
+          backgroundColor: "#E11919"
+        }
+      case "Upcoming":
+        return {
+          backgroundColor: "#FFAA2C"
+        }
+    }
+  }
+
+  onScrollView(e) {
+    if (e.nativeEvent.contentOffset) {
+      const isR = e.nativeEvent.contentOffset.y >= $vw(95);
+      if (isR != this.state.showTitleBar) {
+        this.setState({
+          showTitleBar: isR
+        });
+        if (isR) {
+          this.startTitleAnimate();
+        } else {
+          this.hideTitleAnimate();
+        }
+        MyStatusBar.setStatusBarTheme(isR ? MyStatusBar.DEFAULT_STYLE : MyStatusBar.LIGHT_STYLE);
+      }
+    }
+  }
+
+  startTitleAnimate() {
+    Animated.timing(this.state.opacity, {
+      toValue: 1,
+      duration: 250,
+      easing: Easing.linear,
+      useNativeDriver: true
+    }).start(() => {
+      
+    });
+  }
+
+  hideTitleAnimate() {
+    Animated.timing(this.state.opacity, {
+      toValue: 0,
+      duration: 250,
+      easing: Easing.linear,
+      useNativeDriver: true
+    }).start(() => {
+      
+    });
+  }
+
+  render() {
+    if (this.state.loading) {
+      return (
+        <View style={styles.container}>
+          <VbeSkeleton
+            style={{width: $width, height: $width}}
+            layout={[
+              {width: $width, height: $width}
+            ]}
+            animationDirection={"horizontalRight"}/>
+          <VbeSkeleton
+            style={styles.loadingView}
+            layout={[
+              {width: '90%', height: 20, marginTop: 8},
+              {width: '50%', height: 12, marginTop: 8},
+              {width: '100%', height: 15, marginTop: 24},
+              {width: '100%', height: 15, marginTop: 8},
+              {width: '100%', height: 15, marginTop: 8},
+              {width: '100%', height: 15, marginTop: 8},
+              {width: '30%', height: 15, marginTop: 8},
+              {width: '100%', height: 15, marginTop: 24},
+              {width: '100%', height: 15, marginTop: 8},
+              {width: '100%', height: 15, marginTop: 8},
+              {width: '100%', height: 15, marginTop: 8},
+              {width: '30%', height: 15, marginTop: 8}
+            ]}
+            animationDirection={"horizontalRight"}/>
+        </View>
+      )
+    }
+    return (
+      <>
+      <ScrollView
+        style={styles.container}
+        onScroll={e => this.onScrollView(e)}
+        scrollEventThrottle={16}
+        contentContainerStyle={$padding(0,0,32)}>
+        { utils.isNotEmpty(this.state.messageInfo.articleImages) &&
+          <Swiper
+            style={{height: $width}}
+            autoplay={true}
+            autoplayTimeout={5}
+            renderPagination={(index,total) => <PagerView index={index+1} total={total}/> }
+            removeClippedSubviews={false}>
+            { this.state.messageInfo.articleImages.map((item, index) => {
+              return (
+                <Image
+                  key={index}
+                  style={{width: $width, height: $width}}
+                  source={{uri: utils.getImageUrl(item.articleImagePath)}}/>
+              );
+            })}
+          </Swiper>
+        }
+        <View style={styles.header}>
+          <View>
+            <TextView 
+              style={[styles.labelTypeText, this.getColorByType(this.state.messageInfo.campaignMark)]}
+              numberOfLines={1}>
+              {this.state.messageInfo.campaignMark}
+            </TextView>
+            <TextView
+              style={styles.textTitle}
+              numberOfLines={2}
+              ellipsizeMode="tail">
+              <TextView
+                style={styles.hideText}
+                numberOfLines={1}>
+                {this.state.messageInfo.campaignMark}
+              </TextView>
+              {this.state.messageInfo.articleTitle}
+            </TextView>
+          </View>
+          <View style={ui.flexc}>
+            <MaterialCommunityIcons
+              name="clock-time-four-outline"
+              color={textSecondary}
+              size={12}/>
+            <TextView
+              style={styles.textDate}
+              numberOfLines={1}>
+              {this.state.messageInfo.createTime}
+            </TextView>
+            <MaterialCommunityIcons
+              name="eye-check-outline"
+              size={12}
+              color={textPrimary}/>
+            <TextView
+              style={styles.textView}
+              numberOfLines={1}>
+              {this.state.messageInfo.articleViews}
+            </TextView>
+          </View>
+        </View>
+        <TextView style={styles.textLinkTitle}>{$t("notification.labelSummary")}</TextView>
+        <TextView
+          style={styles.textSummary}>
+          {$t("notification.startTime")}
+          {this.state.messageInfo.startTime}
+        </TextView>
+        <TextView
+          style={styles.textSummary}>
+          {$t("notification.endTime")}
+          {this.state.messageInfo.endTime}
+        </TextView>
+        <TextView style={styles.textLinkTitle}>{$t("notification.labelDetails")}</TextView>
+        <TextView
+          style={styles.textMessage}
+          selectable={true}>
+          {this.state.messageInfo.articleContent}
+        </TextView>
+        { utils.isNotEmpty(this.state.messageInfo.articleLinks) &&
+          <>
+            <TextView style={styles.textLinkTitle}>{$t("notification.labelLinks")}</TextView>
+            { this.state.messageInfo.articleLinks.map((item, index) =>
+              <View style={styles.itemLink} key={index}>
+                <TextView style={styles.linkIndex}>{index + 1}.</TextView>
+                <TextView 
+                  style={styles.linkHyper}
+                  onPress={() => this.accessLink(item.articleLink)}>{item.articleLinkName}</TextView>
+              </View>
+            )}
+          </>
+        }
+        <EndView/>
+      </ScrollView>
+      { !this.state.showTitleBar &&
+        <View style={[styles.toolbar, {top: statusHeight}]}>
+          <BackButton style={styles.backIcon} color={"#F0F0F0"}/>
+        </View>
+      }
+      <Animated.View style={[styles.toolbar, {opacity: this.state.opacity}]}>
+        <Toolbar title={this.state.messageInfo.articleTitle}/>
+      </Animated.View>
+      </>
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: pageBackground
+  },
+  toolbar: {
+    top: 0,
+    left: 0,
+    right: 0,
+    zIndex: 2,
+    position: 'absolute'
+  },
+  backIcon: {
+    width: 48,
+    height: toolbarSize,
+    zIndex: 2,
+    marginLeft: isIOS ? 0 : 2,
+    alignItems: 'center',
+    justifyContent: 'center'
+  },
+  loadingView: {
+    flex: 1,
+    padding: 16,
+    justifyContent: 'flex-start',
+    backgroundColor: pageBackground
+  },
+  textTitle: {
+    color: textPrimary,
+    fontSize: 18,
+    fontWeight: 'bold',
+    paddingBottom: 2
+  },
+  textDate: {
+    flex: 1,
+    color: textSecondary,
+    fontSize: 10,
+    paddingLeft: 2
+  },
+  textView: {
+    color: textSecondary,
+    fontSize: 10,
+    paddingLeft: 4
+  },
+  header: {
+    padding: 16,
+    backgroundColor: pageBackground
+  },
+  textSummary: {
+    color: textPrimary,
+    fontSize: 14,
+    ...$padding(0, 16, 8)
+  },
+  textMessage: {
+    color: textPrimary,
+    fontSize: 14,
+    ...$padding(0, 16, 16)
+  },
+  labelTypeText: {
+    color: textLight,
+    height: 16,
+    fontSize: 10,
+    borderRadius: 4,
+    marginTop: 3,
+    ...$padding(0, 6),
+    position: 'absolute',
+    backgroundColor: "#1ABD00"
+  },
+  hideText: {
+    height: 16,
+    opacity: 0,
+    fontSize: 10,
+    ...$padding(2, 8),
+  },
+  textLinkTitle: {
+    color: textPrimary,
+    fontSize: 16,
+    fontWeight: 'bold',
+    padding: 16
+  },
+  itemLink: {
+    ...$padding(0, 16, 8),
+    flexDirection: 'row'
+  },
+  linkIndex: {
+    fontSize: 14,
+    paddingRight: 2
+  },
+  linkHyper: {
+    ...ui.link,
+    fontSize: 14,
+    textDecorationLine: 'underline'
+  },
+  linkActive: {
+    color: "#FF3B30"
+  }
+})