diff --git a/js/locale/en.json b/js/locale/en.json
index 6c554516..f11ee713 100644
--- a/js/locale/en.json
+++ b/js/locale/en.json
@@ -92,6 +92,27 @@
"sites": "Sites",
"find_new_communities": "Find new communities to join",
"no_hot_topics": "No hot topics found for this site.",
+ "discover_hot_topics": "Hot Topics",
+ "discover_communities": "Communities",
+ "discover_tag_ai": "ai",
+ "discover_tag_finance": "finance",
+ "discover_tag_apple": "apple",
+ "discover_tag_automation": "automation",
+ "discover_tag_media": "media",
+ "discover_tag_research": "research",
+ "discover_tag_smart_home": "smart-home",
+ "discover_tag_linux": "linux",
+ "discover_tag_open_source": "open-source",
+ "discover_tag_webdev": "webdev",
+ "discover_tag_health": "health",
+ "discover_tag_gaming": "gaming",
+ "discover_tag_audio": "audio",
+ "discover_tag_programming_language": "programming",
+ "discover_tag_devops": "devops",
+ "discover_tag_crypto": "crypto",
+ "discover_tag_mapping": "mapping",
+ "discover_pick_tag": "What are you interested in?",
+ "discover_explore_more": "Explore another topic",
"oops": "Oops, something went wrong.",
"still_loading": "Still loading..."
}
diff --git a/js/screens/DiscoverScreen.js b/js/screens/DiscoverScreen.js
index 5806f95e..ece8371d 100644
--- a/js/screens/DiscoverScreen.js
+++ b/js/screens/DiscoverScreen.js
@@ -18,6 +18,7 @@ import {
} from 'react-native';
import DiscoverComponents from './DiscoverScreenComponents';
+import Common from './CommonComponents';
import { ThemeContext } from '../ThemeContext';
import Site from '../site';
import i18n from 'i18n-js';
@@ -71,14 +72,45 @@ class DiscoverScreen extends React.Component {
hasMoreResults: false,
progress: 0.5,
largerUI: props.screenProps.largerUI,
+ showHotTopics: false,
+ selectedViewIndex: 0,
+ hotTopics: [],
+ hotTopicsLoading: true,
+ hotTopicsSelectedTag: 'ai',
+ hotTopicsPage: 1,
+ hotTopicsHasMore: false,
+ hotTopicsTagChosen: false,
};
this._siteManager = this.props.screenProps.siteManager;
- this.baseUrl = `${Site.discoverUrl()}/search.json?q=`;
+ this.baseUrl = `${Site.discoverUrl()}search.json?q=`;
this.maxPageNumber = 10;
this.debouncedSearch = debounce(this.doSearch, 750);
+ this.hotTopicTags = [
+ { label: i18n.t('discover_tag_ai'), value: 'ai' },
+ { label: i18n.t('discover_tag_finance'), value: 'finance' },
+ { label: i18n.t('discover_tag_apple'), value: 'apple' },
+ { label: i18n.t('discover_tag_automation'), value: 'automation' },
+ { label: i18n.t('discover_tag_media'), value: 'media' },
+ { label: i18n.t('discover_tag_research'), value: 'research' },
+ { label: i18n.t('discover_tag_smart_home'), value: 'smart-home' },
+ { label: i18n.t('discover_tag_linux'), value: 'linux' },
+ { label: i18n.t('discover_tag_open_source'), value: 'open-source' },
+ { label: i18n.t('discover_tag_webdev'), value: 'webdev' },
+ { label: i18n.t('discover_tag_health'), value: 'health' },
+ { label: i18n.t('discover_tag_gaming'), value: 'gaming' },
+ { label: i18n.t('discover_tag_audio'), value: 'audio' },
+ {
+ label: i18n.t('discover_tag_programming_language'),
+ value: 'programming-language',
+ },
+ { label: i18n.t('discover_tag_devops'), value: 'devops' },
+ { label: i18n.t('discover_tag_crypto'), value: 'crypto' },
+ { label: i18n.t('discover_tag_mapping'), value: 'mapping' },
+ ];
+
this.doSearch('');
}
@@ -140,6 +172,64 @@ class DiscoverScreen extends React.Component {
});
}
+ componentDidMount() {
+ this._unsubscribeTabPress = this.props.navigation.addListener(
+ 'tabPress',
+ () => {
+ if (this.state.showHotTopics && this.state.hotTopicsTagChosen) {
+ this.setState({ hotTopicsTagChosen: false, hotTopics: [] });
+ }
+ },
+ );
+ }
+
+ componentWillUnmount() {
+ if (this._unsubscribeTabPress) {
+ this._unsubscribeTabPress();
+ }
+ }
+
+ _onSelectTag(tagValue) {
+ this.setState({
+ hotTopicsTagChosen: true,
+ hotTopicsSelectedTag: tagValue,
+ hotTopicsLoading: true,
+ hotTopicsPage: 1,
+ });
+ this.fetchHotTopics(tagValue);
+ }
+
+ fetchHotTopics(tag, opts = {}) {
+ const page = opts.pageNumber || 1;
+ const url = `${Site.discoverUrl()}discover/hot-topics.json?tag=${encodeURIComponent(
+ tag,
+ )}&page=${page}`;
+
+ fetch(url)
+ .then(res => res.json())
+ .then(json => {
+ if (json.hot_topics) {
+ this.setState(prevState => ({
+ hotTopics: opts.append
+ ? prevState.hotTopics.concat(json.hot_topics)
+ : json.hot_topics,
+ hotTopicsLoading: false,
+ hotTopicsHasMore: Boolean(json.more_topics_url),
+ }));
+ } else {
+ this.setState(prevState => ({
+ hotTopics: opts.append ? prevState.hotTopics : [],
+ hotTopicsLoading: false,
+ hotTopicsHasMore: false,
+ }));
+ }
+ })
+ .catch(e => {
+ console.log(e);
+ this.setState({ hotTopicsLoading: false });
+ });
+ }
+
addSite(term, opts = {}) {
if (term.length === 0) {
return new Promise((resolve, reject) => reject());
@@ -204,80 +294,15 @@ class DiscoverScreen extends React.Component {
render() {
const theme = this.context;
- const resultCount = this.state.results.length;
- const messageText =
- this.state.term.length === 1
- ? i18n.t('discover_no_results_one_character')
- : i18n.t('discover_no_results');
-
- const emptyResult = (
-
- {this.state.term === '' ? (
-
- ) : (
-
-
- {messageText}
-
- {
- this.setState({
- selectedTag: '',
- term: '',
- page: 1,
- });
- this.doSearch('');
- }}
- >
-
- {i18n.t('discover_reset')}
-
-
-
- )}
-
- );
return (
{tabBarHeight => (
- {this._renderSearchBox()}
- {resultCount > 0 ? this._renderTags() : null}
-
- (this.discoverList = ref)}
- contentContainerStyle={{ paddingBottom: tabBarHeight }}
- data={this.state.results}
- refreshing={this.state.loading}
- refreshControl={
- {
- // ensures we don't refresh for keyword searches
- if (this.state.selectedTag !== false) {
- this.debouncedSearch(this.state.selectedTag);
- }
- }}
- />
- }
- renderItem={({ item }) => this._renderItem({ item })}
- onEndReached={() => {
- this._fetchNextPage();
- }}
- extraData={this.state.selectionCount}
- maxToRenderPerBatch={20}
- />
-
+ {this._renderHeader()}
+ {this._renderViewToggle()}
+ {this._renderSubHeader()}
+ {this._renderContent(tabBarHeight)}
)}
@@ -285,19 +310,10 @@ class DiscoverScreen extends React.Component {
);
}
- _fetchNextPage() {
- if (this.state.hasMoreResults) {
- const newPageNumber = this.state.page + 1;
-
- this.setState({ page: newPageNumber, loading: true });
- this.doSearch(this.state.selectedTag || '', {
- append: true,
- pageNumber: newPageNumber,
- });
+ _renderHeader() {
+ if (this.state.showHotTopics) {
+ return ;
}
- }
-
- _renderSearchBox() {
return (
+ {
+ const showHotTopics = Boolean(index);
+ if (showHotTopics && this.state.showHotTopics) {
+ this.setState({ hotTopicsTagChosen: false });
+ return;
+ }
+ this.setState({
+ showHotTopics,
+ selectedViewIndex: index,
+ });
+ }}
+ />
+
+ );
+ }
+
+ _renderSubHeader() {
+ if (this.state.showHotTopics) {
+ return this.state.hotTopicsTagChosen
+ ? this._renderHotTopicsHeader()
+ : null;
+ }
+ return this.state.results.length > 0 ? this._renderTags() : null;
+ }
+
_renderTags() {
const tagOptions = [
'',
@@ -349,34 +405,23 @@ class DiscoverScreen extends React.Component {
);
}
- _renderTag(label, searchString) {
+ _renderTagPill(key, label, isActive, onPress) {
const theme = this.context;
- const isCurrentTerm = searchString === this.state.selectedTag;
return (
{
- this.setState({
- selectedTag: searchString,
- loading: true,
- page: 1,
- });
- if (this.discoverList) {
- this.discoverList.scrollToIndex({
- index: 0,
- animated: true,
- });
- }
- this.doSearch(searchString);
- }}
+ onPress={onPress}
>
{
+ this.setState({
+ selectedTag: searchString,
+ loading: true,
+ page: 1,
+ });
+ if (this.discoverList) {
+ this.discoverList.scrollToIndex({
+ index: 0,
+ animated: true,
+ });
+ }
+ this.doSearch(searchString);
+ });
+ }
+
+ _renderContent(tabBarHeight) {
+ if (this.state.showHotTopics) {
+ return this._renderHotTopicsContent(tabBarHeight);
+ }
+ return this._renderDiscoverList(tabBarHeight);
+ }
+
+ _renderHotTopicsContent(tabBarHeight) {
+ if (!this.state.hotTopicsTagChosen) {
+ return (
+ this._onSelectTag(tag)}
+ />
+ );
+ }
+
+ return (
+
+ this.props.screenProps.openUrl(url)}
+ largerUI={this.state.largerUI}
+ contentContainerStyle={{ paddingBottom: tabBarHeight + 20 }}
+ listRef={ref => (this.hotTopicsList = ref)}
+ onEndReached={() => this._fetchNextHotTopicsPage()}
+ onRefresh={() => {
+ this.setState({
+ hotTopicsLoading: true,
+ hotTopicsPage: 1,
+ });
+ this.fetchHotTopics(this.state.hotTopicsSelectedTag);
+ }}
+ onExploreMore={() => this.setState({ hotTopicsTagChosen: false })}
+ />
+
+ );
+ }
+
+ _renderDiscoverList(tabBarHeight) {
+ return (
+
+ String(item.id || item.featured_link)}
+ ListEmptyComponent={() => this._renderEmptyResult()}
+ ref={ref => (this.discoverList = ref)}
+ contentContainerStyle={{ paddingBottom: tabBarHeight }}
+ data={this.state.results}
+ refreshing={this.state.loading}
+ refreshControl={
+ {
+ // ensures we don't refresh for keyword searches
+ if (this.state.selectedTag !== false) {
+ this.debouncedSearch(this.state.selectedTag);
+ }
+ }}
+ />
+ }
+ renderItem={({ item }) => this._renderItem({ item })}
+ onEndReached={() => {
+ this._fetchNextPage();
+ }}
+ extraData={this.state.selectionCount}
+ maxToRenderPerBatch={20}
+ />
+
+ );
+ }
+
_renderItem({ item }) {
return (
);
}
+
+ _renderEmptyResult() {
+ const theme = this.context;
+
+ if (this.state.term === '') {
+ return (
+
+
+
+ );
+ }
+
+ const messageText =
+ this.state.term.length === 1
+ ? i18n.t('discover_no_results_one_character')
+ : i18n.t('discover_no_results');
+
+ return (
+
+
+
+ {messageText}
+
+ {
+ this.setState({
+ selectedTag: '',
+ term: '',
+ page: 1,
+ });
+ this.doSearch('');
+ }}
+ >
+
+ {i18n.t('discover_reset')}
+
+
+
+
+ );
+ }
+
+ _fetchNextPage() {
+ if (this.state.hasMoreResults) {
+ const newPageNumber = this.state.page + 1;
+
+ this.setState({ page: newPageNumber, loading: true });
+ this.doSearch(this.state.selectedTag || '', {
+ append: true,
+ pageNumber: newPageNumber,
+ });
+ }
+ }
+
+ _fetchNextHotTopicsPage() {
+ if (this.state.hotTopicsHasMore) {
+ const newPage = this.state.hotTopicsPage + 1;
+ this.setState({ hotTopicsPage: newPage, hotTopicsLoading: true });
+ this.fetchHotTopics(this.state.hotTopicsSelectedTag, {
+ append: true,
+ pageNumber: newPage,
+ });
+ }
+ }
+
+ _renderHotTopicsHeader() {
+ return (
+
+ {this.hotTopicTags.map(tag => {
+ const isCurrentTag = tag.value === this.state.hotTopicsSelectedTag;
+
+ return this._renderTagPill(tag.value, tag.label, isCurrentTag, () => {
+ if (tag.value === this.state.hotTopicsSelectedTag) {
+ this.setState({ hotTopicsTagChosen: false });
+ } else {
+ this._onSelectTag(tag.value);
+ }
+ });
+ })}
+
+
+ );
+ }
}
DiscoverScreen.contextType = ThemeContext;
diff --git a/js/screens/DiscoverScreenComponents/DiscoverTopicList.js b/js/screens/DiscoverScreenComponents/DiscoverTopicList.js
new file mode 100644
index 00000000..eb4e8dad
--- /dev/null
+++ b/js/screens/DiscoverScreenComponents/DiscoverTopicList.js
@@ -0,0 +1,271 @@
+/* @flow */
+'use strict';
+
+import React, { useContext } from 'react';
+import {
+ FlatList,
+ Image,
+ StyleSheet,
+ Text,
+ TouchableHighlight,
+ View,
+} from 'react-native';
+import FontAwesome5 from '@react-native-vector-icons/fontawesome5';
+import { ThemeContext } from '../../ThemeContext';
+import i18n from 'i18n-js';
+
+const DiscoverTopicList = props => {
+ const theme = useContext(ThemeContext);
+
+ function _renderTopic({ item }) {
+ return (
+ props.onClickTopic(item.url)}
+ underlayColor={theme.background}
+ activeOpacity={0.6}
+ style={{
+ ...styles.topicRow,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: theme.grayBorder,
+ }}
+ >
+
+
+
+ {item.title}
+
+
+ {item.excerpt ? (
+
+
+ {item.excerpt}
+
+
+ ) : null}
+
+ {_renderCommunity(item)}
+
+
+
+ {item.reply_count}
+
+
+
+ {item.like_count}
+
+
+
+
+
+ );
+ }
+
+ function _renderCommunity(item) {
+ if (!item.community_name) {
+ return ;
+ }
+
+ return (
+
+ {item.community_logo_url ? (
+
+ ) : null}
+
+ {item.community_name}
+
+
+ );
+ }
+
+ function _renderPlaceholder() {
+ return (
+
+ {[0, 1, 2, 3].map(i => (
+
+
+
+
+ ))}
+
+ );
+ }
+
+ function _renderEmpty() {
+ return (
+
+
+ {i18n.t('no_hot_topics')}
+
+
+ );
+ }
+
+ function _renderFooter() {
+ if (props.loading || props.topics.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {i18n.t('discover_explore_more')}
+
+
+
+ );
+ }
+
+ if (props.loading && props.topics.length === 0) {
+ return _renderPlaceholder();
+ }
+
+ return (
+
+ String(item.id)}
+ renderItem={_renderTopic}
+ ListHeaderComponent={props.ListHeaderComponent}
+ ListEmptyComponent={_renderEmpty}
+ ListFooterComponent={_renderFooter}
+ onEndReached={props.onEndReached}
+ onEndReachedThreshold={0.5}
+ contentContainerStyle={props.contentContainerStyle}
+ refreshing={props.loading}
+ onRefresh={props.onRefresh}
+ onScroll={props.onScroll}
+ scrollEventThrottle={16}
+ keyboardDismissMode="on-drag"
+ />
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ placeholder: {
+ minHeight: 560,
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ flex: 1,
+ },
+ placeholderHeading: {
+ height: 40,
+ opacity: 0.3,
+ marginBottom: 20,
+ },
+ placeholderMetadata: {
+ height: 16,
+ opacity: 0.2,
+ marginBottom: 20,
+ },
+ topicTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ },
+ topicExcerpt: {
+ fontSize: 14,
+ paddingTop: 6,
+ paddingBottom: 6,
+ },
+ topicRow: {
+ paddingVertical: 15,
+ paddingHorizontal: 16,
+ },
+ metadataFirstRow: {
+ flexDirection: 'row',
+ paddingTop: 6,
+ },
+ topicCounts: {
+ flexDirection: 'row',
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ paddingLeft: 10,
+ },
+ topicCountsNum: {
+ fontSize: 14,
+ paddingRight: 8,
+ paddingLeft: 4,
+ },
+ communityBadge: {
+ flexDirection: 'row',
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ opacity: 0.8,
+ flexShrink: 1,
+ },
+ communityLogo: {
+ height: 16,
+ width: 16,
+ marginRight: 6,
+ borderRadius: 3,
+ },
+ communityName: {
+ fontSize: 13,
+ },
+ emptyText: {
+ padding: 24,
+ fontSize: 16,
+ textAlign: 'center',
+ },
+ footer: {
+ paddingVertical: 24,
+ paddingHorizontal: 16,
+ alignItems: 'center',
+ },
+ footerContent: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ footerText: {
+ fontSize: 16,
+ fontWeight: '600',
+ },
+});
+export default DiscoverTopicList;
diff --git a/js/screens/DiscoverScreenComponents/NavigationBar.js b/js/screens/DiscoverScreenComponents/NavigationBar.js
new file mode 100644
index 00000000..af3414f5
--- /dev/null
+++ b/js/screens/DiscoverScreenComponents/NavigationBar.js
@@ -0,0 +1,54 @@
+/* @flow */
+'use strict';
+
+import React, { useContext } from 'react';
+import {
+ Platform,
+ StyleSheet,
+ View,
+} from 'react-native';
+import FontAwesome5 from '@react-native-vector-icons/fontawesome5';
+import { ThemeContext } from '../../ThemeContext';
+
+const NavigationBar = () => {
+ const theme = useContext(ThemeContext);
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ height: Platform.OS === 'ios' ? 48 : 60,
+ marginBottom: 14,
+ },
+ titleContainer: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center',
+ padding: 0,
+ },
+ separator: {
+ bottom: 0,
+ height: 1,
+ left: 0,
+ position: 'absolute',
+ right: 0,
+ },
+});
+
+export default NavigationBar;
diff --git a/js/screens/DiscoverScreenComponents/TagSplash.js b/js/screens/DiscoverScreenComponents/TagSplash.js
new file mode 100644
index 00000000..db3b87cf
--- /dev/null
+++ b/js/screens/DiscoverScreenComponents/TagSplash.js
@@ -0,0 +1,86 @@
+/* @flow */
+'use strict';
+
+import React, { useContext } from 'react';
+import {
+ ScrollView,
+ StyleSheet,
+ Text,
+ TouchableHighlight,
+ View,
+} from 'react-native';
+import { ThemeContext } from '../../ThemeContext';
+import i18n from 'i18n-js';
+
+const TagSplash = props => {
+ const theme = useContext(ThemeContext);
+
+ return (
+
+
+
+ {i18n.t('discover_pick_tag')}
+
+
+
+ {props.tags.map(tag => (
+ props.onSelectTag(tag.value)}
+ >
+
+ {tag.label}
+
+
+ ))}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ paddingTop: 40,
+ paddingBottom: 40,
+ paddingHorizontal: 20,
+ },
+ header: {
+ alignItems: 'center',
+ marginBottom: 30,
+ },
+ prompt: {
+ fontSize: 20,
+ fontWeight: '600',
+ marginTop: 16,
+ },
+ tagGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'center',
+ },
+ tagButton: {
+ paddingVertical: 12,
+ paddingHorizontal: 18,
+ borderRadius: 10,
+ borderWidth: StyleSheet.hairlineWidth,
+ margin: 6,
+ },
+ tagLabel: {
+ fontSize: 16,
+ fontWeight: '500',
+ },
+});
+
+export default TagSplash;
diff --git a/js/screens/DiscoverScreenComponents/index.js b/js/screens/DiscoverScreenComponents/index.js
index ea6475fd..07dfef91 100644
--- a/js/screens/DiscoverScreenComponents/index.js
+++ b/js/screens/DiscoverScreenComponents/index.js
@@ -3,8 +3,14 @@
import SiteRow from './SiteRow';
import TermBar from './TermBar';
+import DiscoverTopicList from './DiscoverTopicList';
+import TagSplash from './TagSplash';
+import NavigationBar from './NavigationBar';
module.exports = {
SiteRow: SiteRow,
TermBar: TermBar,
+ DiscoverTopicList: DiscoverTopicList,
+ TagSplash: TagSplash,
+ NavigationBar: NavigationBar,
};