react-native#SectionList TypeScript Examples

The following examples show how to use react-native#SectionList. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: HabitList.tsx    From nyxo-app with GNU General Public License v3.0 6 votes vote down vote up
List = styled(SectionList as new () => SectionList<Habit>).attrs(
  ({ theme }) => ({
    contentContainerStyle: {
      backgroundColor: theme.PRIMARY_BACKGROUND_COLOR
    }
  })
)`
  background-color: ${colors.darkBlue};
`
Example #2
Source File: index.tsx    From react-native-scroll-bottom-sheet with MIT License 6 votes vote down vote up
private getScrollComponent = () => {
    switch (this.props.componentType) {
      case 'FlatList':
        return FlatList;
      case 'ScrollView':
        return ScrollView;
      case 'SectionList':
        return SectionList;
      default:
        throw new Error(
          'Component type not supported: it should be one of `FlatList`, `ScrollView` or `SectionList`'
        );
    }
  };
Example #3
Source File: HabitView.tsx    From nyxo-app with GNU General Public License v3.0 5 votes vote down vote up
HabitView: FC = () => {
  const dispatch = useAppDispatch()

  const renderItem = ({ item }: { item: Habit }) => {
    return <HabitCard key={item.id} habit={item} />
  }

  const toggleModal = () => {
    dispatch(toggleNewHabitModal(true))
  }

  const sections = [
    {
      title: 'HABIT.ACTIVE',
      subtitle: 'HABIT.ACTIVE_SUBTITLE',
      data: []
    },
    {
      title: 'HABIT.ARCHIVED',
      subtitle: 'HABIT.ARCHIVED_SUBTITLE',
      data: []
    }
  ]

  const habitKeyExtractor = (item: Habit) => {
    return item.id
  }

  return (
    <SafeAreaView>
      <SectionList
        ListHeaderComponent={() => (
          <>
            <TitleRow>
              <PageTitle adjustsFontSizeToFit>HABIT.HABIT_TITLE</PageTitle>
              <NewHabitButton onPress={toggleModal}>
                <IconBold
                  width={20}
                  height={20}
                  name="circleAdd"
                  fill={colors.darkBlue}
                />
              </NewHabitButton>
            </TitleRow>

            <Container>
              <P>HABIT.EXPLANATION_1</P>
              <P>HABIT.EXPLANATION_2</P>
            </Container>
          </>
        )}
        renderSectionHeader={({ section: { title, data, subtitle } }) => (
          <CoachingSectionHeader
            data={data}
            title={title}
            subtitle={subtitle}
          />
        )}
        sections={sections}
        ListEmptyComponent={<EmptyState />}
        renderItem={renderItem}
        keyExtractor={habitKeyExtractor}
      />

      <EditHabitModal />
      <NewHabitModal />
    </SafeAreaView>
  )
}
Example #4
Source File: Lessons.tsx    From nyxo-app with GNU General Public License v3.0 5 votes vote down vote up
AnimatedSectionList = Animated.createAnimatedComponent(SectionList)
Example #5
Source File: Mobility.tsx    From wuhan2020-frontend-react-native-app with MIT License 5 votes vote down vote up
function MobilityScreen() {
  const { data, loading, refresh } = useContext(MobilityDataContext);
  const [refreshing, setRefreshing] = useState(false);

  function keyExtractor(item: EntryType) {
    return String(item.id);
  }

  function renderItem({ item }: { item: EntryType }) {
    return <Mobility item={item} />;
  }

  const onRefresh = useCallback(() => {
    setRefreshing(true);

    refresh();
    wait(2000).then(() => setRefreshing(false));
  }, [refreshing, refresh]);

  return (
    <StatusBarSafeLayout>
      <View>
        <SectionList
          keyExtractor={keyExtractor}
          refreshing={loading}
          refreshControl={
            <RefreshControl
              tintColor="pink"
              refreshing={refreshing}
              onRefresh={onRefresh}
            />
          }
          renderItem={renderItem}
          sections={toSection(data)}
          renderSectionHeader={({ section: { title } }) => (
            <View style={{ backgroundColor: '#eee' }}>
              <Text style={styles.subheader}>{title}</Text>
            </View>
          )}
          ListEmptyComponent={<ActivityIndicator size="large" color="red" />}
          ListHeaderComponent={
            <View style={styles.header}>
              <H1 title="确诊患者相同行程查询" />
            </View>
          }
        />
      </View>
    </StatusBarSafeLayout>
  );
}
Example #6
Source File: index.tsx    From react-native-section-alphabet-list with MIT License 4 votes vote down vote up
AlphabetList: React.FC<AlphabetListProps> = (props) => {
  const {
    data,
    index = DEFAULT_CHAR_INDEX,
    style,
    indexContainerStyle,
    indexLetterStyle,
    indexLetterContainerStyle,
    letterListContainerStyle,
    getItemHeight: onGetItemHeight = () => sizes.itemHeight,
    sectionHeaderHeight = sizes.itemHeight,
    listHeaderHeight = sizes.listHeaderHeight,
    uncategorizedAtTop = false,
    renderCustomSectionHeader,
    renderCustomItem,
    renderCustomListHeader,
    renderCustomIndexLetter,
    ...sectionListProps
  } = props

  const sectionListRef = useRef(null);
  const [sectionData, setSectionData] = useState<ISectionData[]>([])

  useEffect(() => {
    setSectionData(getSectionData(data, index, uncategorizedAtTop))
  }, [data])

  const onScrollToSection = (sectionIndex: number) => {
    const sectionList = sectionListRef.current! as SectionList;
    if (!sectionList) return

    sectionList.scrollToLocation({
      sectionIndex,
      itemIndex: 0,
    });
  }


  const onGetItemLayout: any = sectionListGetItemLayout({
    getItemHeight: (_rowData, sectionIndex: number, rowIndex: number) => {
      return onGetItemHeight(sectionIndex, rowIndex)
    },
    getSectionHeaderHeight: () => sectionHeaderHeight,
    getSectionFooterHeight: () => 0,
    listHeaderHeight,
  });

  const onRenderSectionHeader = ({ section }: { section: SectionListData<IData> }) => {
    if (renderCustomSectionHeader) return renderCustomSectionHeader(section);

    return (
      <View testID="header" style={styles.sectionHeaderContainer}>
        <Text testID="header__label" style={styles.sectionHeaderLabel}>{section.title}</Text>
      </View>
    );
  };

  const onRenderItem = ({ item }: { item: IData }) => {
    if (renderCustomItem) return renderCustomItem(item);

    return (
      <View testID="cell" style={styles.listItemContainer}>
        <Text testID="cell__label" style={styles.listItemLabel}>{item.value}</Text>
      </View>
    );
  };


  return (
    <View style={[styles.container, style]}>
      <SectionList
        {...sectionListProps}
        testID="sectionList"
        ref={sectionListRef}
        sections={sectionData}
        keyExtractor={(item: IData) => item.key}
        renderItem={onRenderItem}
        renderSectionHeader={onRenderSectionHeader}
        ListHeaderComponent={renderCustomListHeader}
        getItemLayout={onGetItemLayout}
      />

      <ListLetterIndex
        sectionData={sectionData}
        onPressLetter={onScrollToSection}
        indexContainerStyle={indexContainerStyle}
        indexLetterStyle={indexLetterStyle}
        indexLetterContainerStyle={indexLetterContainerStyle}
        letterListContainerStyle={letterListContainerStyle}
        renderCustomIndexLetter={renderCustomIndexLetter}
      />
    </View>
  );
}
Example #7
Source File: bankPayment.tsx    From THUInfo with MIT License 4 votes vote down vote up
BankPaymentScreen = () => {
	const [data, setData] = useState<BankPaymentByMonth[]>([]);
	const [refreshing, setRefreshing] = useState(true);

	const themeName = useColorScheme();
	const theme = themes(themeName);

	const fetchData = () => {
		setRefreshing(true);
		helper
			.getBankPayment()
			.then((r) => {
				setData(r);
				setRefreshing(false);
			})
			.catch(() => {
				Snackbar.show({
					text: getStr("networkRetry"),
					duration: Snackbar.LENGTH_SHORT,
				});
				setRefreshing(false);
			});
	};

	useEffect(fetchData, []);

	return (
		<>
			<SectionList
				sections={data.map(({month, payment}) => ({
					month,
					data: payment,
				}))}
				renderSectionHeader={({section}) => (
					<View style={{marginTop: 12, paddingTop: 12}}>
						<View
							style={{
								padding: 9,
								paddingBottom: 0,
								flexDirection: "row",
								justifyContent: "space-between",
								alignItems: "center",
							}}>
							<Text
								numberOfLines={1}
								style={{
									fontSize: 20,
									fontWeight: "bold",
									flex: 1,
									color: theme.colors.text,
								}}>
								{section.month}
							</Text>
							<Text
								numberOfLines={1}
								style={{
									fontSize: 20,
									fontWeight: "bold",
									flex: 0,
									color: theme.colors.text,
								}}>
								{section.data
									.reduce((acc, payment) => acc + Number(payment.total), 0)
									.toFixed(2)}
							</Text>
						</View>
					</View>
				)}
				renderItem={({item}) => <PaymentItem payment={item} />}
				refreshControl={
					<RefreshControl
						refreshing={refreshing}
						onRefresh={fetchData}
						colors={[theme.colors.accent]}
					/>
				}
				keyExtractor={(item) => `${item.time}`}
			/>
		</>
	);
}
Example #8
Source File: report.tsx    From THUInfo with MIT License 4 votes vote down vote up
ReportScreen = () => {
	const [report, setReport] = useState<Course[]>();
	const [refreshing, setRefreshing] = useState(true);

	const [flag, setFlag] = useState<1 | 2 | 3>(1);
	const [bx, setBx] = useState(false);

	const themeName = useColorScheme();
	const theme = themes(themeName);

	const fetchData = () => {
		setRefreshing(true);
		helper
			.getReport(bx && flag === 1, true, flag)
			.then((res) => {
				setReport(res);
				setRefreshing(false);
			})
			.catch(() => {
				Snackbar.show({
					text: getStr("networkRetry"),
					duration: Snackbar.LENGTH_SHORT,
				});
				setRefreshing(false);
			});
	};

	useEffect(fetchData, [bx, flag]);

	const {gpa, sections, allCredits, totalCredits, totalPoints} = prepareData(
		report || [],
	);

	return (
		<View style={{marginHorizontal: 20, flex: 1}}>
			<View style={{flexDirection: "row", margin: 5}}>
				<TouchableOpacity
					style={{padding: 6, flex: 1}}
					onPress={() => setFlag(1)}>
					<Text
						style={{
							color: flag === 1 ? "blue" : theme.colors.text,
							textAlign: "center",
						}}>
						{getStr("reportFlag1")}
					</Text>
				</TouchableOpacity>
				<TouchableOpacity
					style={{padding: 6, flex: 1}}
					onPress={() => setFlag(2)}>
					<Text
						style={{
							color: flag === 2 ? "blue" : theme.colors.text,
							textAlign: "center",
						}}>
						{getStr("reportFlag2")}
					</Text>
				</TouchableOpacity>
				<TouchableOpacity
					style={{padding: 6, flex: 1}}
					onPress={() => setFlag(3)}>
					<Text
						style={{
							color: flag === 3 ? "blue" : theme.colors.text,
							textAlign: "center",
						}}>
						{getStr("reportFlag3")}
					</Text>
				</TouchableOpacity>
				<TouchableOpacity
					style={{padding: 6, flex: 1}}
					onPress={() => setBx((o) => !o)}>
					<Text style={{color: "blue", textAlign: "center"}}>
						{getStr(bx ? "bx" : "bxr")}
					</Text>
				</TouchableOpacity>
			</View>
			<SectionList
				sections={sections}
				stickySectionHeadersEnabled={false}
				renderSectionHeader={({section}) => (
					<ReportHeader
						semester={section.semester}
						gpa={section.gpa}
						totalCredits={section.totalCredits}
						allCredits={section.allCredits}
						totalPoints={section.totalPoints}
					/>
				)}
				renderItem={({item}) => (
					<ReportItem
						name={item.name}
						credit={item.credit}
						grade={item.grade}
						point={item.point}
					/>
				)}
				ListHeaderComponent={
					<ReportSummary
						gpa={gpa}
						totalCredits={totalCredits}
						allCredits={allCredits}
						totalPoints={totalPoints}
					/>
				}
				refreshControl={
					<RefreshControl
						refreshing={refreshing}
						onRefresh={fetchData}
						colors={[theme.colors.accent]}
					/>
				}
				keyExtractor={(item, index) => `${item.semester}${index}`}
			/>
		</View>
	);
}
Example #9
Source File: settings.tsx    From nyxo-app with GNU General Public License v3.0 4 votes vote down vote up
SettingsScreen: FC<Props> = ({ navigation }) => {
  const theme = useAppSelector((state) => state.theme.theme)

  const rateApp = () => {
    // FIXME
    Rate.rate(options, () => undefined)
  }

  const displayTheme = (t: string) => (t === 'dark' ? 'Light' : 'Dark')

  const settings = [
    {
      text: 'Select Tracking Source',
      icon: 'smartWatchCircleGraph',
      action: () => navigation.navigate('Sources')
    },
    {
      text: 'Coaching settings',
      icon: 'schoolPhysicalBold',
      action: () => navigation.navigate('Coaching')
    },

    {
      text: 'Manage Nyxo Subscription',
      icon: 'receipt',
      action: () => navigation.navigate('Subscription')
    },
    {
      text: 'Sync to backend',
      icon: 'syncCloud',
      action: () => navigation.navigate('Cloud', { code: '' })
    },
    {
      text: 'Switch mode',
      icon: 'astronomyMoon',
      theme: displayTheme(theme),
      action: () => navigation.push('Theme')
    },
    {
      text: 'RATE_APP',
      icon: 'star',
      action: rateApp
    },
    {
      text: 'ONBOARDING.TITLE',
      icon: 'compass',
      action: () => navigation.push('Onboarding')
    }
  ]

  const socialActions = [
    {
      text: 'Send feedback',
      icon: 'envelope',
      action: () => Linking.openURL('mailto:[email protected]')
    },
    {
      text: 'Visit site',
      icon: 'browserDotCom',
      action: () => Linking.openURL('https://nyxo.app/')
    },
    {
      text: 'Follow us on Facebook',
      icon: 'facebook',
      action: () =>
        Linking.openURL('https://www.facebook.com/Nyxo-467927177117033/')
    },

    {
      text: 'Follow us on Twitter',
      icon: 'twitter',
      action: () => Linking.openURL('https://twitter.com/hellonyxo')
    },

    {
      text: 'Follow us on Instagram',
      icon: 'instagram',
      action: () => Linking.openURL('https://www.instagram.com/hellonyxo/')
    },
    {
      text: 'Terms of Service',
      icon: 'handshake',
      action: () => Linking.openURL(CONFIG.TERMS_LINK)
    },
    {
      text: 'Privacy Policy',
      icon: 'lockCircle',
      action: () => Linking.openURL(CONFIG.PRIVACY_LINK)
    }
  ]

  const renderItem: ListRenderItem<SettingItem> = ({ item }) => {
    if (!item) return null
    return (
      <SettingRow onPress={item.action} badge={item.badge} icon={item.icon}>
        <Title>{`${item.text}`}</Title>
      </SettingRow>
    )
  }

  const renderSectionHeader = ({
    section
  }: {
    section: SectionListData<SettingItem, { title: string }>
  }) => {
    if (section.title === 'Settings') return null
    return <SectionHeader>{section.title}</SectionHeader>
  }

  const sections = [
    {
      title: 'Settings',
      data: settings
    },
    {
      title: 'Support',
      data: socialActions
    }
  ]

  return (
    <SafeAreaView>
      <SectionList
        ListHeaderComponent={<PageTitle>Settings</PageTitle>}
        sections={sections}
        renderSectionHeader={renderSectionHeader}
        keyExtractor={keyExtractor}
        renderItem={renderItem}
        ListFooterComponent={<VersionInformation />}
      />
    </SafeAreaView>
  )
}
Example #10
Source File: Main.tsx    From DoobooIAP with MIT License 4 votes vote down vote up
function Intro(): React.ReactElement {
  const [sections, setSections] = useState<Section[]>([]);
  const [loading, setLoading] = useState<boolean>(true);
  const [totalPaidAmount, setTotalPaidAmount] = useState<number>(10000);
  const { theme } = useThemeContext();

  const getProducts = useCallback(async (): Promise<void> => {
    RNIap.clearProductsIOS();

    try {
      const result = await RNIap.initConnection();
      await RNIap.consumeAllItemsAndroid();
      console.log('result', result);
    } catch (err) {
      console.warn(err.code, err.message);
    }

    purchaseUpdateSubscription = purchaseUpdatedListener(
      async (purchase: InAppPurchase | SubscriptionPurchase) => {
        const receipt = purchase.transactionReceipt;
        if (receipt) {
          try {
            const ackResult = await finishTransaction(purchase);
            console.log('ackResult', ackResult);
          } catch (ackErr) {
            console.warn('ackErr', ackErr);
          }

          Alert.alert('purchase error', JSON.stringify(receipt));
        }
      },
    );

    purchaseErrorSubscription = purchaseErrorListener(
      (error: PurchaseError) => {
        console.log('purchaseErrorListener', error);
        Alert.alert('purchase error', JSON.stringify(error));
      },
    );

    const products = await RNIap.getProducts(itemSkus);
    products.forEach((product) => {
      product.type = 'inapp';
    });
    // console.log('products', JSON.stringify(products));
    const subscriptions = await RNIap.getSubscriptions(itemSubs);
    subscriptions.forEach((subscription) => {
      subscription.type = 'subs';
    });
    console.log('subscriptions', JSON.stringify(subscriptions));
    const list = [
      {
        title: getString('ONE_TIME_PURCHASE'),
        data: products,
      },
      {
        title: getString('SUBSCRIPTION_PURCHASE'),
        data: subscriptions,
      },
    ];
    setSections(list);
    setLoading(false);
  }, [sections]);

  useEffect(() => {
    getProducts();

    return (): void => {
      if (purchaseUpdateSubscription) {
        purchaseUpdateSubscription.remove();
      }
      if (purchaseErrorSubscription) {
        purchaseErrorSubscription.remove();
      }
    };
  }, []);

  const purchase = (item: Product | Subscription): void => {
    if (getSkuType(item) === ITEM_TYPE.PRODUCT) {
      RNIap.requestPurchase(item.productId);
    } else {
      RNIap.requestSubscription(item.productId);
    }
  };

  const renderHeader = (): ReactElement => (
    <Header>
      <Text
        style={{
          fontSize: 28,
          color: 'white',
        }}
      >
        {totalPaidAmount.toLocaleString()}
      </Text>
      <Text
        style={{
          marginTop: 8,
          fontSize: 16,
          color: 'white',
        }}
      >
        {getString('TOTAL_PURCHASE')}
      </Text>
    </Header>
  );

  const renderSectionHeader = (title: string): ReactElement => (
    <View
      style={{
        height: 40,
        flexDirection: 'row',
        paddingHorizontal: 16,
        backgroundColor: theme.placeholder,
      }}
    >
      <Text
        style={{
          fontSize: 13,
          color: theme.background,
          marginTop: 12,
        }}
      >
        {title}
      </Text>
    </View>
  );

  const renderItem = (item: Product | Subscription): ReactElement => (
    <View
      style={{
        padding: 16,
        flexDirection: 'row',
        backgroundColor: theme.background,
        borderBottomWidth: 1,
        borderBottomColor: theme.placeholder,
      }}
    >
      <Image
        source={IC_COIN}
        style={{
          height: 80,
          width: 80,
        }}
      />
      <View
        style={{
          flexDirection: 'column',
          marginLeft: 32,
          marginRight: 20,
        }}
      >
        <Text
          style={{
            fontSize: 16,
            color: theme.brandLight,
            marginBottom: 4,
          }}
        >
          {item.title}
        </Text>
        <Text
          style={{
            fontSize: 14,
            color: theme.font,
          }}
        >
          {item.productId}
        </Text>
        <Text
          style={{
            fontSize: 14,
            fontWeight: 'bold',
            color: theme.warning,
          }}
        >
          {getSkuType(item) === ITEM_TYPE.SUBSCRIPTION &&
            getTrialPeriod(item as Subscription) !== 0 &&
            `FREE TRIAL DAYS: ${getTrialPeriod(item as Subscription)}`}
        </Text>
        <Button
          containerStyle={{
            width: 120,
            marginTop: 16,
          }}
          style={{
            backgroundColor: theme.background,
            height: 40,
            width: 120,
            borderWidth: 1,
            borderColor: theme.focused,
            borderRadius: 8,
            paddingHorizontal: 10,
          }}
          textStyle={{
            color: theme.font,
          }}
          onPress={(): void => purchase(item)}
          text={item.localizedPrice}
        />
      </View>
    </View>
  );

  return (
    <Container>
      <SectionList
        style={{ width: '100%' }}
        ListHeaderComponent={renderHeader}
        refreshing={loading}
        onRefresh={getProducts}
        // @ts-ignore
        sections={sections}
        keyExtractor={(item, index): string => index.toString()}
        renderItem={({ item }): ReactElement => renderItem(item)}
        renderSectionHeader={({ section: { title } }): ReactElement =>
          renderSectionHeader(title)
        }
      />
    </Container>
  );
}
Example #11
Source File: Albums.tsx    From jellyfin-audio-player with MIT License 4 votes vote down vote up
Albums: React.FC = () => {
    // Retrieve data from store
    const { entities: albums } = useTypedSelector((state) => state.music.albums);
    const isLoading = useTypedSelector((state) => state.music.albums.isLoading);
    const lastRefreshed = useTypedSelector((state) => state.music.albums.lastRefreshed);
    const sections = useTypedSelector(selectAlbumsByAlphabet);
    
    // Initialise helpers
    const dispatch = useAppDispatch();
    const navigation = useNavigation<MusicNavigationProp>();
    const getImage = useGetImage();
    const listRef = useRef<SectionList<EntityId>>(null);

    const getItemLayout = useCallback((data: SectionedId[] | null, index: number): { offset: number, length: number, index: number } => {
        // We must wait for the ref to become available before we can use the
        // native item retriever in VirtualizedSectionList
        if (!listRef.current) {
            return { offset: 0, length: 0, index };
        }

        // Retrieve the right item info
        // @ts-ignore
        const wrapperListRef = (listRef.current?._wrapperListRef) as VirtualizedSectionList;
        const info: VirtualizedItemInfo = wrapperListRef._subExtractor(index);
        const { index: itemIndex, header, key } = info;
        const sectionIndex = parseInt(key.split(':')[0]);

        // We can then determine the "length" (=height) of this item. Header items
        // end up with an itemIndex of -1, thus are easy to identify.
        const length = header ? 50 : (itemIndex % 2 === 0 ? AlbumHeight : 0);
    
        // We'll also need to account for any unevenly-ended lists up until the
        // current item.
        const previousRows = data?.filter((row, i) => i < sectionIndex)
            .reduce((sum, row) => sum + Math.ceil(row.data.length / 2), 0) || 0;

        // We must also calcuate the offset, total distance from the top of the
        // screen. First off, we'll account for each sectionIndex that is shown up
        // until now. This only includes the heading for the current section if the
        // item is not the section header
        const headingOffset = HeadingHeight * (header ? sectionIndex : sectionIndex + 1);
        const currentRows = itemIndex > 1 ? Math.ceil((itemIndex + 1) / 2) : 0;
        const itemOffset = AlbumHeight * (previousRows + currentRows);
        const offset = headingOffset + itemOffset;
    
        return { index, length, offset };
    }, [listRef]);

    // Set callbacks
    const retrieveData = useCallback(() => dispatch(fetchAllAlbums()), [dispatch]);
    const selectAlbum = useCallback((id: string) => navigation.navigate('Album', { id, album: albums[id] as Album }), [navigation, albums]);
    const selectLetter = useCallback((sectionIndex: number) => { 
        listRef.current?.scrollToLocation({ sectionIndex, itemIndex: 0, animated: false, }); 
    }, [listRef]);
    const generateItem = useCallback(({ item, index, section }: { item: EntityId, index: number, section: SectionedId }) => {
        if (index % 2 === 1) {
            return <View key={item} />;
        }
        const nextItem = section.data[index + 1];

        return (
            <View style={{ flexDirection: 'row', marginLeft: 10, marginRight: 10 }} key={item}>
                <GeneratedAlbumItem
                    id={item}
                    imageUrl={getImage(item as string)}
                    name={albums[item]?.Name || ''}
                    artist={albums[item]?.AlbumArtist || ''}
                    onPress={selectAlbum}
                />
                {albums[nextItem] && 
                    <GeneratedAlbumItem
                        id={nextItem}
                        imageUrl={getImage(nextItem as string)}
                        name={albums[nextItem]?.Name || ''}
                        artist={albums[nextItem]?.AlbumArtist || ''}
                        onPress={selectAlbum}
                    />
                }
            </View>
        );
    }, [albums, getImage, selectAlbum]);

    // Retrieve data on mount
    useEffect(() => { 
        // GUARD: Only refresh this API call every set amounts of days
        if (!lastRefreshed || differenceInDays(lastRefreshed, new Date()) > ALBUM_CACHE_AMOUNT_OF_DAYS) {
            retrieveData(); 
        }
    });
    
    return (
        <SafeAreaView>
            <AlphabetScroller onSelect={selectLetter} />
            <SectionList
                sections={sections} 
                refreshing={isLoading}
                onRefresh={retrieveData}
                getItemLayout={getItemLayout}
                ref={listRef}
                keyExtractor={(item) => item as string}
                renderSectionHeader={generateSection}
                renderItem={generateItem}
            />
        </SafeAreaView>
    );
}
Example #12
Source File: index.tsx    From krmanga with MIT License 4 votes vote down vote up
function Home({ dispatch, commendList, refreshing, navigation, loading, hasMore }: IProps) {

    const headerHeight = useHeaderHeight();
    const scrollY: Animated.Value = useRef(new Animated.Value(0)).current;
    const [endReached, setEndReached] = useState<boolean>(false);

    useEffect(() => {
        SplashScreen.hide();//关闭启动屏
        dispatch({
            type: "home/setState",
            payload: {
                headerHeight
            }
        });
        syncImmediate();
        loadCarouselList();
        loadCommendList(true);
    }, []);

    const syncImmediate = () => {
        if (Platform.OS === "android") {
            codePush.checkForUpdate().then((update) => {
                if (update) {
                    navigation.navigate("AppUpdate");
                }
            });
        }
    };

    const loadCarouselList = () => {
        dispatch({
            type: "home/fetchCarouselList"
        });
    };

    const loadCommendList = (refreshing: boolean, callback?: () => void) => {
        dispatch({
            type: "home/fetchCommendList",
            payload: {
                refreshing
            },
            callback
        });
    };

    const renderSectionHeader = ({ section: { title } }: any) => {
        return (
            <View style={styles.sectionHeader}>
                <View style={styles.cell} />
                <Text style={styles.classifyName}>{title}</Text>
            </View>
        );
    };

    const onRefresh = () => {
        loadCarouselList();
        loadCommendList(true);
    };

    const onEndReached = () => {
        if (!hasMore || loading) {
            return;
        }
        setEndReached(true);
        loadCommendList(false, () => {
            setEndReached(false);
        });
    };

    const renderFooter = () => {
        if (endReached) {
            return <More />;
        }
        if (!hasMore) {
            return <End />;
        }

        return null;
    };

    const goBrief = useCallback((data: IBook) => {
        navigation.navigate("Brief", {
            id: data.id
        });
    }, []);

    const renderItem = ({ item }: SectionListRenderItemInfo<IBook[]>) => {
        return (
            <View style={styles.contentContainer}>
                {item.map(data => {
                    return (
                        <BookCover data={data} goBrief={goBrief} key={data.id} />
                    );
                })}
            </View>
        );
    };

    const getTopBarColor = useCallback(() => {
        return scrollY.interpolate({
            inputRange: [0, maxScroll],
            outputRange: [0, 1],
            extrapolate: "clamp"
        });
    }, []);

    const TopBarColor = getTopBarColor();

    return (
        (loading && refreshing) ? <HomePlaceholder /> :
            <View style={{ flex: 1 }}>
                <CarouselBlurBackground />
                <TopBarWrapper navigation={navigation} topBarColor={TopBarColor} />
                <SectionList
                    keyExtractor={(item, index) => `item-${item["id"]}-key-${index}`}
                    ListHeaderComponent={() => <Carousel />}
                    renderSectionHeader={renderSectionHeader}
                    onRefresh={onRefresh}
                    refreshing={refreshing}
                    sections={commendList}
                    stickySectionHeadersEnabled={true}
                    scrollEventThrottle={1}
                    onScroll={Animated.event(
                        [{
                            nativeEvent: { contentOffset: { y: scrollY } }
                        }],
                        {
                            useNativeDriver: false
                        }
                    )}
                    onEndReached={onEndReached}
                    onEndReachedThreshold={0.1}
                    renderItem={renderItem}
                    extraData={endReached}
                    ListFooterComponent={renderFooter}
                />
            </View>
    );
}
Example #13
Source File: index.tsx    From krmanga with MIT License 4 votes vote down vote up
function History({ dispatch, navigation, isLogin, isEdit, historyList, ids, loading, refreshing, hasMore, pages }: IProps) {

    const translateX: Animated.Value = useRef(new Animated.Value(0)).current;
    const [endReached, setEndReached] = useState<boolean>(false);

    useFocusEffect(
        React.useCallback(() => {
            loadData(true);
            return () => {
                dispatch({
                    type: "history/setState",
                    payload: {
                        isEdit: false,
                        ids: []
                    }
                });
            };
        }, [isLogin])
    );

    const loadData = (refreshing: boolean, callback?: () => void) => {
        if (isLogin) {
            dispatch({
                type: "history/fetchHistoryList",
                payload: {
                    refreshing: refreshing
                },
                callback
            });
        }
    };

    const renderSectionHeader = ({ section: { title } }: any) => {
        return (
            <View style={styles.headerView}>
                <Text style={styles.headerTitle}>{title}</Text>
            </View>
        );
    };

    const getBeforeX = () => {
        Animated.timing(translateX,
            {
                useNativeDriver: false,
                toValue: wp(5),
                duration: 150
            }
        ).start();
    };

    const getAfterX = () => {
        Animated.timing(translateX,
            {
                useNativeDriver: false,
                toValue: 0,
                duration: 150
            }
        ).start();
    };

    const onClickItem = useCallback((item: IHistory[]) => {
        if (isEdit) {
            let i = ids.indexOf(item["book_id"]);
            if (i > -1) {
                ids.splice(i, 1);
                dispatch({
                    type: "history/setState",
                    payload: {
                        ids: [...ids]
                    }
                });
            } else {
                dispatch({
                    type: "history/setState",
                    payload: {
                        ids: [...ids, item["book_id"]]
                    }
                });
            }
        } else {
            navigation.navigate("Brief", {
                id: item["book_id"]
            });
        }
    }, [isEdit, ids]);

    const goMangaView = useCallback((item: IHistory[]) => {
        navigation.navigate("Brief", {
            id: item["book_id"]
        });
        navigation.navigate("MangaView", {
            chapter_num: item["chapter_num"],
            markRoast: item["roast"],
            book_id: item["book_id"]
        });
    }, []);

    const renderItem = ({ item }: SectionListRenderItemInfo<IHistory[]>) => {
        const selected = ids.indexOf(item["book_id"]) > -1;
        return (
            <Touchable
                key={item["id"]}
                onPress={() => onClickItem(item)}
            >
                <Animated.View
                    style={{ transform: [{ translateX: translateX }] }}
                >
                    <Item
                        data={item}
                        isEdit={isEdit}
                        selected={selected}
                        goMangaView={goMangaView}
                    />
                </Animated.View>
            </Touchable>
        );
    };

    const onRefresh = () => {
        dispatch({
            type: "history/setScreenReload"
        });
        loadData(true);
    };

    const onEndReached = () => {
        if (!hasMore || loading) {
            return;
        }
        setEndReached(true);
        loadData(false, () => {
            setEndReached(false);
        });
    };

    const renderFooter = () => {
        if (endReached) {
            return <More />;
        }
        if (!hasMore) {
            return <End />;
        }

        return null;
    };

    const cancel = () => {
        let newData: string[] = [];
        historyList.forEach(items => {
                items.data.forEach(item => {
                    newData = newData.concat(item["book_id"]);
                });
            }
        );
        if (newData.length === ids.length) {
            dispatch({
                type: "history/setState",
                payload: {
                    ids: []
                }
            });
        } else {
            dispatch({
                type: "history/setState",
                payload: {
                    ids: newData
                }
            });
        }
    };

    const destroy = () => {
        dispatch({
            type: "history/delUserHistory",
            payload: {
                ids
            }
        });
    };

    if (isEdit) {
        getBeforeX();
    } else {
        getAfterX();
    }

    return (
        !isLogin ? null :
            (loading && refreshing) ? <ListPlaceholder /> :
                <View style={styles.container}>
                    <SectionList
                        keyExtractor={(item, index) => `section-item-${index}`}
                        renderSectionHeader={renderSectionHeader}
                        onRefresh={onRefresh}
                        refreshing={refreshing}
                        sections={historyList}
                        style={styles.container}
                        stickySectionHeadersEnabled={true}
                        onEndReached={onEndReached}
                        onEndReachedThreshold={0.1}
                        renderItem={renderItem}
                        extraData={endReached}
                        ListFooterComponent={renderFooter}
                    />
                    <EditView
                        data_length={pages.current_page * pages.page_size < pages.total ?
                            pages.current_page * pages.page_size : pages.total}
                        isEdit={isEdit}
                        ids={ids}
                        cancel={cancel}
                        destroy={destroy}
                    />
                </View>
    );
}
Example #14
Source File: index.tsx    From sharingan-rn-modal-dropdown with MIT License 4 votes vote down vote up
GroupDropdown: React.FC<IGroupDropdownProps> = props => {
  const {
    error,
    value,
    label,
    required,
    disabled,
    data,
    onChange,
    floating,
    enableSearch,
    primaryColor,
    elevation,
    borderRadius,
    activityIndicatorColor,
    searchPlaceholder,
    helperText,
    errorColor,
    itemTextStyle,
    itemContainerStyle,
    showLoader,
    animationIn = 'fadeIn',
    animationOut = 'fadeOut',
    supportedOrientations = ['portrait', 'landscape'],
    animationInTiming,
    animationOutTiming,
    headerTextStyle,
    headerContainerStyle,
    stickySectionHeadersEnabled,
    parentDDContainerStyle,
    rippleColor,
    emptyListText,
    enableAvatar,
    avatarSize,
    onBlur,
    paperTheme,
    textInputStyle,
    mainContainerStyle,
    underlineColor,
    disableSelectionTick,
    selectedItemTextStyle,
    selectedItemViewStyle,
    removeLabel,
    mode = 'flat',
    disabledItemTextStyle,
    disabledItemViewStyle,
    dropdownIcon = 'menu-down',
    dropdownIconSize = 30,
    itemSelectIcon,
    itemSelectIconSize,
    multiline = false,
    searchInputTheme,
  } = props;
  const { colors } = useTheme();
  const [selected, setSelected] = useState<string | number>();
  const [labelv, setlabelV] = useState<string>('');
  const [isVisible, setIsVisible] = useState<boolean>(false);
  const [iconColor, setIconColor] = useState<string | undefined>('grey');
  const [options, setOptions] = useState<IGroupDropdownData[]>([]);
  const [hasError, setError] = useState<boolean>(false);
  const [singluarData, setSingularData] = useState<IDropdownData[]>([]);

  const [contMeasure, setConMeasure] = useState({
    vx: 0,
    vy: 0,
    vWidth: 0,
    vHeight: 0,
  });
  const [dimension, setDimension] = useState({
    dw: deviceWidth,
    dh: deviceHeight,
  });
  const [searchQuery, setSearchQuery] = useState('');

  const pViewRef = useRef<View | any>();
  const listRef = useRef<SectionList | any>();

  useEffect(() => {
    Dimensions.addEventListener('change', () => {
      const { width, height } = Dimensions.get('window');
      setDimension({ dw: width, dh: height });
      setIsVisible(false);
      setIconColor('grey');
    });
    return () => {
      Dimensions.removeEventListener('change', () => {});
    };
  }, []);

  useEffect(() => {
    const destructuredData: IDropdownData[] = [];
    Lo.forEach(data, d => {
      Lo.forEach(d.data, dv => destructuredData.push(dv));
    });
    setSingularData(destructuredData);
    setOptions(data);
  }, [data]);

  useEffect(() => {
    if (!Lo.isEmpty(singluarData) && value) {
      const lFilter = Lo.filter(singluarData, { value: value })[0];
      if (!Lo.isEmpty(lFilter)) setlabelV(lFilter.label);
      setSelected(value);
    }
  }, [value, singluarData]);

  useEffect(() => {
    if (isVisible && listRef) {
      listRef.current.flashScrollIndicators();
    }
  }, [isVisible]);

  useEffect(() => {
    if (isVisible && selected) {
      let secionIndex = 0;
      let itemIndex = 0;
      if (!Lo.isEmpty(options)) {
        options.forEach((e, secIndex) => {
          const itIndex = Lo.findIndex(e.data, { value: selected });
          if (itIndex >= 0 && listRef) {
            itemIndex = itIndex;
            secionIndex = secIndex;
            setTimeout(() => {
              listRef.current.scrollToLocation({
                animated: false,
                sectionIndex: secionIndex,
                itemIndex: itemIndex,
                viewPosition: Platform.OS === 'android' ? 0 : 0.5,
              });
            }, 100);
          }
        });
      }
    }
  }, [selected, options, isVisible]);

  useEffect(() => {
    if (disabled) {
      setIconColor('lightgrey');
    }
  }, [disabled]);

  useEffect(() => {
    if (required && error) {
      setError(true);
      setIconColor(errorColor);
    } else {
      setError(false);
      setIconColor('grey');
    }
  }, [required, error, errorColor]);

  const onTextInputFocus = () => {
    if (hasError) {
      setIconColor('red');
    } else {
      setIconColor(primaryColor);
    }
    // if (Platform.OS === 'ios') {
    pViewRef.current.measureInWindow(
      (vx: number, vy: number, vWidth: number, vHeight: number) => {
        const ddTop = vy + vHeight;
        const bottomMetric = dimension.dh - vy;
        if (bottomMetric < 300) {
          setConMeasure({ vx, vy: ddTop - 217, vWidth, vHeight });
        } else {
          setConMeasure({ vx, vy: ddTop, vWidth, vHeight });
        }
      }
    );
    // }
    setIsVisible(true);
  };

  const androidOnLayout = () => {
    if (Platform.OS === 'android') {
      pViewRef.current.measureInWindow(
        (vx: number, vy: number, vWidth: number, vHeight: number) => {
          const ddTop = vy + vHeight;
          const bottomMetric = dimension.dh - vy;
          // setPx(bottomMetric);
          if (bottomMetric < 300) {
            setConMeasure({ vx, vy: ddTop - 217, vWidth, vHeight });
          } else {
            setConMeasure({ vx, vy: ddTop, vWidth, vHeight });
          }
        }
      );
    }
  };

  const onModalBlur = () => {
    setIsVisible(false);
    if (hasError) {
      setIconColor('red');
    } else {
      setIconColor('grey');
    }
    if (onBlur && typeof onBlur === 'function') onBlur();
  };

  const handleOptionSelect = (v: string | number) => {
    const lFilter = Lo.filter(singluarData, { value: v })[0];
    if (!Lo.isEmpty(lFilter)) setlabelV(lFilter.label);
    setSelected(v);
    if (onChange && typeof onChange === 'function') {
      onChange(v);
      setIsVisible(false);
    }
    if (hasError) {
      setIconColor('red');
    } else {
      setIconColor('grey');
    }
    setSearchQuery('');

    setOptions(data);
  };

  const onChangeSearch = (query: string) => {
    setSearchQuery(query);
    if (!Lo.isEmpty(data) && query) {
      let matches: IGroupDropdownData[] = [];
      data.forEach(e => {
        const sF = e.data.filter(c =>
          c.label
            .toString()
            .toLowerCase()
            .trim()
            .includes(query.toString().toLowerCase())
        );
        if (!Lo.isEmpty(sF))
          matches = matches.concat([{ title: e.title, data: sF }]);
      });
      if (matches.length === 0) setOptions([]);
      else setOptions(matches);
    } else if (!Lo.isEmpty(data) && !query) {
      setOptions(data);
    }
  };

  const getEmptyComponent = () => {
    if (typeof emptyListText === 'string')
      return <EmptyList emptyItemMessage={emptyListText} />;
    else return <>{emptyListText}</>;
  };

  const labelAction = () => {
    if (removeLabel) {
      return '';
    } else {
      return required ? `${label}*` : label;
    }
  };

  return (
    <PaperProvider theme={paperTheme}>
      <View>
        <View>
          <PressableTouch
            onPress={onTextInputFocus}
            disabled={disabled}
            rippleColor={rippleColor}
          >
            <View
              style={[styles.fullWidth, mainContainerStyle]}
              ref={pViewRef}
              onLayout={androidOnLayout}
              pointerEvents="none"
            >
              <TextInput
                label={labelAction()}
                value={labelv}
                style={[styles.textInput, textInputStyle]}
                underlineColor={underlineColor}
                underlineColorAndroid={underlineColor}
                editable={false}
                error={hasError}
                disabled={disabled}
                multiline={multiline}
                theme={{
                  ...searchInputTheme,
                  colors: { primary: primaryColor, error: errorColor },
                }}
                right={
                  <TextInput.Icon
                    name={dropdownIcon}
                    size={dropdownIconSize}
                    color={iconColor}
                  />
                }
                mode={mode}
              />
            </View>
            {required && hasError ? (
              <HelperText
                type="error"
                theme={{ colors: { error: errorColor } }}
                visible={hasError}
              >
                {helperText ? helperText : `${label} is required`}
              </HelperText>
            ) : null}
          </PressableTouch>
        </View>
        <View>
          <Modal
            isVisible={isVisible}
            onBackdropPress={onModalBlur}
            backdropColor={floating ? 'rgba(0,0,0,0.1)' : 'transparent'}
            style={styles.modalStyle}
            animationIn={animationIn}
            animationOut={animationOut}
            animationInTiming={animationInTiming}
            animationOutTiming={animationOutTiming}
            supportedOrientations={supportedOrientations}
          >
            <View
              style={{
                backgroundColor: 'transparent',
                width: !floating ? contMeasure.vWidth : 'auto',
                left: !floating ? contMeasure.vx : 0,
                top: !floating ? contMeasure.vy : 100,
                right: 0,
                position: 'absolute',
                padding: floating ? 20 : 0,
              }}
            >
              <Surface
                style={[
                  styles.surface,
                  parentDDContainerStyle,
                  { elevation, borderRadius },
                  floating ? { maxHeight: dimension.dh / 2 } : null,
                ]}
              >
                {showLoader ? (
                  <View style={[styles.loader, { borderRadius }]}>
                    <ActivityIndicator
                      size="small"
                      color={activityIndicatorColor}
                    />
                  </View>
                ) : null}
                <SectionList
                  ref={listRef}
                  sections={options}
                  legacyImplementation
                  initialNumToRender={25}
                  maxToRenderPerBatch={25}
                  ListHeaderComponent={
                    enableSearch ? (
                      <View>
                        <Searchbar
                          placeholder={searchPlaceholder}
                          onChangeText={onChangeSearch}
                          value={searchQuery}
                          theme={{ colors: { primary: primaryColor } }}
                          style={{
                            elevation: 0,
                            backgroundColor: showLoader
                              ? 'transparent'
                              : colors.background,
                            height: ITEMLAYOUT,
                          }}
                        />
                        <Divider style={styles.divider} />
                      </View>
                    ) : null
                  }
                  stickyHeaderIndices={enableSearch ? [0] : undefined}
                  renderItem={({ item }) => (
                    <Item
                      item={item}
                      onSelect={handleOptionSelect}
                      selected={value}
                      selectedColor={primaryColor}
                      itemTextStyle={itemTextStyle}
                      itemContainerStyle={itemContainerStyle}
                      rippleColor={rippleColor}
                      disabled={showLoader || item?.disabled}
                      enableAvatar={enableAvatar}
                      avatarSize={avatarSize}
                      disableSelectionTick={disableSelectionTick}
                      selectedItemTextStyle={selectedItemTextStyle}
                      selectedItemViewStyle={selectedItemViewStyle}
                      disabledItemTextStyle={disabledItemTextStyle}
                      disabledItemViewStyle={disabledItemViewStyle}
                      itemSelectIcon={itemSelectIcon}
                      itemSelectIconSize={itemSelectIconSize}
                    />
                  )}
                  renderSectionHeader={({ section: { title } }) => (
                    <View
                      style={{
                        backgroundColor: showLoader
                          ? 'transparent'
                          : colors.background,
                        borderRadius: 3,
                      }}
                    >
                      <Divider style={styles.divider} />
                      <View style={[styles.headerView, headerContainerStyle]}>
                        <Text style={[styles.headerText, headerTextStyle]}>
                          {title.trim()}
                        </Text>
                      </View>
                      <Divider style={styles.divider} />
                    </View>
                  )}
                  keyExtractor={() => Math.random().toString()}
                  ItemSeparatorComponent={() => (
                    <Divider style={styles.divider} />
                  )}
                  getItemLayout={(_d, index) => ({
                    length: ITEMLAYOUT,
                    offset: ITEMLAYOUT * index,
                    index,
                  })}
                  stickySectionHeadersEnabled={stickySectionHeadersEnabled}
                  ListEmptyComponent={getEmptyComponent()}
                />
              </Surface>
            </View>
          </Modal>
        </View>
      </View>
    </PaperProvider>
  );
}
Example #15
Source File: invite-screen.tsx    From beancount-mobile with MIT License 4 votes vote down vote up
export function InviteScreen(props: Props) {
  React.useEffect(() => {
    async function init() {
      await analytics.track("page_view_invite", {});
    }
    init();
  }, []);

  const theme = useTheme().colorTheme;
  const styles = getStyles(theme);

  const contacts = useContacts();

  const [selectedContacts, setSelectedContacts] = React.useState<RowItem[]>([]);
  const [keyword, setKeyword] = React.useState("");

  const sections = React.useMemo(() => {
    // @ts-ignore
    return Object.entries(
      groupBy(
        // Create one contact per phone number and email.
        contacts.data.reduce((res, cur) => {
          if (cur.phoneNumbers != null) {
            for (const p of cur.phoneNumbers) {
              res.push({
                id: cur.id + p.number,
                name: cur.name || "",
                phoneNumber: p.number,
              });
            }
          }
          if (cur.emails != null) {
            for (const e of cur.emails) {
              res.push({
                id: cur.id + e.email,
                name: cur.name || "",
                email: e.email,
              });
            }
          }
          return res;
        }, [] as Array<RowItem>),
        (c: RowItem) => {
          const firstChar = (c.name.charAt(0) || "#").toLowerCase();
          return firstChar.match(/[a-z]/) ? firstChar : "#";
        }
      )
    )
      .map(([key, value]: [string, RowItem[]]) => ({
        key,
        data: value.sort((a, b) =>
          (a.name || a.name || "") < (b.name || b.name || "") ? -1 : 1
        ),
      }))
      .sort((a: { key: string }, b: { key: string }) =>
        a.key < b.key ? -1 : 1
      );
  }, [contacts.data]);

  const filteredSection = React.useMemo(() => {
    if (keyword.length > 0) {
      const filteredSections = new Array<SectionItem>();
      for (const s of sections) {
        const filteredData = s.data.filter(
          (d) =>
            d.name.indexOf(keyword) >= 0 ||
            (d.email && d.email.indexOf(keyword) >= 0) ||
            (d.phoneNumber && d.phoneNumber.indexOf(keyword) >= 0)
        );
        if (filteredData.length > 0) {
          filteredSections.push({ key: s.key, data: filteredData });
        }
      }
      return filteredSections;
    }
    return sections;
  }, [sections, keyword]);

  const onInvitePress = async () => {
    const { shareLink } = props.route.params;
    let didShare = false;
    const message = `${i18n.t("recommend")} ${shareLink}`;
    const emails = selectedContacts
      .filter((c) => c.email != null)
      .map((c) => c.email) as string[];
    const phoneNumbers = selectedContacts
      .filter((c) => c.phoneNumber != null)
      .map((c) => c.phoneNumber) as string[];
    if (emails.length > 0) {
      try {
        const result = await composeAsync({
          recipients: emails,
          subject: "beancount.io",
          body: message,
          isHtml: false,
        });
        didShare = didShare || result.status === "sent";
      } catch (ex) {
        Toast.fail(ex.message);
      }
    }
    if (phoneNumbers.length > 0 && (await isAvailableAsync())) {
      try {
        const result = await sendSMSAsync(phoneNumbers, message);
        didShare = didShare || result.result === "sent";
      } catch (ex) {
        Toast.fail(ex.message);
      }
    }

    if (didShare) {
      Toast.show(i18n.t("thanksShare"));
      await analytics.track("tap_invite", { shareLink, selectedContacts });
    }
  };

  const renderBody = () => {
    if (contacts.loading) {
      return (
        <View style={styles.bodyContainer}>
          <ActivityIndicator size="large" color={theme.primary} />
          <CommonMargin />
          <Text style={styles.loadingOrErrText}>{i18n.t("loading")}</Text>
        </View>
      );
    }
    if (contacts.error != null) {
      const errMsg =
        String(contacts.error.message).indexOf("permission") >= 0
          ? i18n.t("noContactPermission")
          : String(contacts.error.message);
      return (
        <View style={styles.bodyContainer}>
          <MaterialIcons name="error" size={48} color={theme.primary} />
          <CommonMargin />
          <Text style={styles.loadingOrErrText}>{errMsg}</Text>
        </View>
      );
    }

    return (
      <View style={styles.flex1}>
        <SearchBar
          styles={{
            wrapper: {
              backgroundColor: theme.white,
            },
          }}
          style={styles.searchInput}
          placeholder={i18n.t("inputKeyword")}
          value={keyword}
          onCancel={() => {
            setKeyword("");
          }}
          onChange={(val) => {
            setKeyword(val);
          }}
        />
        <SectionList
          showsVerticalScrollIndicator={false}
          bounces={false}
          sections={filteredSection}
          renderSectionHeader={({ section }) => (
            <View style={styles.sectionHeaderContainer}>
              <Text style={styles.sectionHeaderText}>
                {section.key!.toUpperCase()}
              </Text>
            </View>
          )}
          renderItem={({ item }: { item: RowItem }) => {
            const selectedIndex = selectedContacts.findIndex(
              (i) => i.id === item.id
            );
            const onPress = () => {
              const newContacts = [...selectedContacts];
              if (selectedIndex >= 0) {
                newContacts.splice(selectedIndex, 1);
              } else {
                newContacts.push(item);
              }
              setSelectedContacts(newContacts);
            };
            return (
              <ContactRow
                name={item.name}
                emailOrNumber={(item.email || item.phoneNumber)!}
                selected={selectedIndex >= 0}
                onPress={onPress}
              />
            );
          }}
          extraData={selectedContacts}
          contentContainerStyle={styles.contentContainerStyle}
        />
        <SafeAreaView style={styles.bottomButtonContainer}>
          <Button
            style={styles.button}
            onPress={onInvitePress}
            disabled={selectedContacts.length === 0}
          >
            <Text style={styles.buttonText}>{`${i18n.t("invite")} (${
              selectedContacts.length
            })`}</Text>
          </Button>
        </SafeAreaView>
      </View>
    );
  };

  return (
    <View style={styles.container}>
      <NavigationBar
        title={i18n.t("inviteFriends")}
        showBack
        navigation={props.navigation}
      />

      {renderBody()}
    </View>
  );
}