Merge pull request #1717 from ecency/feature/community

Feature/community
This commit is contained in:
Feruz M 2020-07-17 11:14:10 +03:00 committed by GitHub
commit 5f65339ac3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1413 additions and 95 deletions

View File

@ -20,6 +20,7 @@ import WalletUnclaimedPlaceHolder from './view/placeHolder/walletUnclaimedPlaceH
import ListPlaceHolder from './view/placeHolder/listPlaceHolderView';
import BoostPlaceHolder from './view/placeHolder/boostPlaceHolderView';
import CommentPlaceHolder from './view/placeHolder/commentPlaceHolderView';
import CommunitiesPlaceHolder from './view/placeHolder/communitiesPlaceHolder';
export {
Card,
@ -42,4 +43,5 @@ export {
WalletDetailsPlaceHolder,
WalletLineItem,
WalletUnclaimedPlaceHolder,
CommunitiesPlaceHolder,
};

View File

@ -0,0 +1,34 @@
import React from 'react';
import { View } from 'react-native';
import Placeholder from 'rn-placeholder';
import { ThemeContainer } from '../../../../containers';
import styles from './postCardPlaceHolderStyles';
// TODO: make container for place holder wrapper after alpha
const PostCardPlaceHolder = () => {
return (
<ThemeContainer>
{({ isDarkTheme }) => {
const color = isDarkTheme ? '#2e3d51' : '#f5f5f5';
return (
<View style={styles.container}>
<View style={styles.paragraphWrapper}>
<Placeholder.Paragraph
lineNumber={4}
color={color}
textSize={16}
lineSpacing={5}
width="100%"
lastLineWidth="70%"
firstLineWidth="20%"
animate="fade"
/>
</View>
</View>
);
}}
</ThemeContainer>
);
};
export default PostCardPlaceHolder;

View File

@ -0,0 +1,15 @@
import EStyleSheet from 'react-native-extended-stylesheet';
export default EStyleSheet.create({
container: {
backgroundColor: '$primaryBackgroundColor',
padding: 20,
borderStyle: 'solid',
borderWidth: 1,
borderTopWidth: 1,
borderColor: '$primaryLightBackground',
marginRight: 0,
marginLeft: 0,
marginTop: 0,
},
});

View File

@ -48,7 +48,7 @@ class TagContainer extends PureComponent {
onPress();
} else {
navigation.navigate({
routeName: ROUTES.SCREENS.SEARCH_RESULT,
routeName: ROUTES.SCREENS.TAG_RESULT,
params: {
tag: value,
},
@ -57,7 +57,7 @@ class TagContainer extends PureComponent {
};
render() {
const { isPin, value, isPostCardTag, isFilter } = this.props;
const { isPin, value, isPostCardTag, isFilter, style, textStyle } = this.props;
const { label } = this.state;
return (
@ -68,6 +68,8 @@ class TagContainer extends PureComponent {
isPostCardTag={isPostCardTag}
onPress={this._handleOnTagPress}
isFilter={isFilter}
style={style}
textStyle={textStyle}
/>
);
}

View File

@ -2,14 +2,18 @@ import React from 'react';
import { Text, View, TouchableOpacity } from 'react-native';
import styles from './tagStyles';
const Tag = ({ onPress, isPin, value, label, isPostCardTag, isFilter }) => (
<TouchableOpacity onPress={() => onPress && onPress(value)}>
const Tag = ({ onPress, isPin, value, label, isPostCardTag, isFilter, style, textStyle }) => (
<TouchableOpacity
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
onPress={() => onPress && onPress(value)}
>
<View
style={[
styles.textWrapper,
isFilter && styles.isFilter,
isPin && styles.isPin,
isPostCardTag && styles.isPostCardTag,
style,
]}
>
<Text
@ -17,6 +21,7 @@ const Tag = ({ onPress, isPin, value, label, isPostCardTag, isFilter }) => (
styles.text,
!isPin && isFilter && styles.isFilterTextUnPin,
isPin && isFilter && styles.isFilterTextPin,
textStyle,
]}
>
{isPostCardTag ? label : value}

View File

@ -37,7 +37,7 @@ const TextWithIcon = ({
name={iconName}
iconType={iconType}
/>
<Text style={styles.text}>{text}</Text>
<Text style={[styles.text, textStyle]}>{text}</Text>
</View>
)}
</View>

View File

@ -8,7 +8,7 @@ import HeaderView from '../view/headerView';
import { AccountContainer, ThemeContainer } from '../../../containers';
const HeaderContainer = ({ selectedUser, isReverse, navigation, handleOnBackPress }) => {
const HeaderContainer = ({ selectedUser, isReverse, navigation, handleOnBackPress, hideUser }) => {
const _handleOpenDrawer = () => {
if (has(navigation, 'openDrawer') && typeof get(navigation, 'openDrawer') === 'function') {
navigation.openDrawer();
@ -41,6 +41,7 @@ const HeaderContainer = ({ selectedUser, isReverse, navigation, handleOnBackPres
isReverse={isReverse}
reputation={get(_user, 'reputation')}
username={get(_user, 'name')}
hideUser={hideUser}
/>
);
}}

View File

@ -2,12 +2,16 @@ import React, { useState } from 'react';
import { View, Text, SafeAreaView, TouchableOpacity } from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import { useIntl } from 'react-intl';
import { withNavigation } from 'react-navigation';
// Components
import { SearchModal } from '../../searchModal';
import { IconButton } from '../../iconButton';
import { UserAvatar } from '../../userAvatar';
// Constants
import ROUTES from '../../../constants/routeNames';
// Styles
import styles from './headerStyles';
@ -21,6 +25,8 @@ const HeaderView = ({
isReverse,
reputation,
username,
navigation,
hideUser,
}) => {
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
const intl = useIntl();
@ -32,8 +38,16 @@ const HeaderView = ({
gradientColor = isDarkTheme ? ['#081c36', '#43638e'] : ['#2d5aa0', '#357ce6'];
}
const _onPressSearchButton = () => {
navigation.navigate({
routeName: ROUTES.SCREENS.SEARCH_RESULT,
});
};
return (
<SafeAreaView style={[styles.container, isReverse && styles.containerReverse]}>
{!hideUser && (
<>
<SearchModal
placeholder={intl.formatMessage({
id: 'header.search',
@ -81,6 +95,8 @@ const HeaderView = ({
)}
</View>
)}
</>
)}
{isReverse ? (
<View style={styles.reverseBackButtonWrapper}>
@ -93,15 +109,11 @@ const HeaderView = ({
</View>
) : (
<View style={styles.backButtonWrapper}>
<IconButton
iconStyle={styles.backIcon}
name="md-search"
onPress={() => setIsSearchModalOpen(true)}
/>
<IconButton iconStyle={styles.backIcon} name="md-search" onPress={_onPressSearchButton} />
</View>
)}
</SafeAreaView>
);
};
export default HeaderView;
export default withNavigation(HeaderView);

View File

@ -180,7 +180,7 @@ const CommentBody = ({
const __handleTagPress = (tag) => {
if (tag) {
navigate({
routeName: ROUTES.SCREENS.SEARCH_RESULT,
routeName: ROUTES.SCREENS.TAG_RESULT,
params: {
tag,
},

View File

@ -166,7 +166,7 @@ const PostBody = ({
const _handleTagPress = (tag) => {
if (tag) {
navigation.navigate({
routeName: ROUTES.SCREENS.SEARCH_RESULT,
routeName: ROUTES.SCREENS.TAG_RESULT,
params: {
tag,
},

View File

@ -65,9 +65,6 @@ const PostsView = ({
fetchPromotePost();
_loadPosts();
}
return () => {
//unmounting
};
}, [
_getPromotePosts,
_loadPosts,

View File

@ -503,5 +503,33 @@
"reveal_comment": "Reveal comment",
"read_more": "Read more comments",
"more_replies": "more replies"
},
"search_result": {
"others": "Others",
"communities": {
"title": "Communities",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"subscribers": "Subscribers",
"posters": "Posters",
"posts": "Posts"
},
"communities_filter": {
"rank": "Rank",
"subs": "Subscribers",
"new": "New"
},
"post_result_filter": {
"popularity": "Popularity",
"newest": "Newest",
"relevance": "Relevance"
},
"other_result_filter": {
"user": "User",
"tag": "Tag"
}
},
"community": {
"new_post": "New Post"
}
}

View File

@ -21,12 +21,14 @@ export default {
REDEEM: `Redeem${SCREEN_SUFFIX}`,
REGISTER: `Register${SCREEN_SUFFIX}`,
SEARCH_RESULT: `SearchResult${SCREEN_SUFFIX}`,
TAG_RESULT: `TagResult${SCREEN_SUFFIX}`,
SETTINGS: `Settings${SCREEN_SUFFIX}`,
STEEM_CONNECT: `SteemConnect${SCREEN_SUFFIX}`,
TRANSFER: `Transfer${SCREEN_SUFFIX}`,
VOTERS: `Voters${SCREEN_SUFFIX}`,
COMMENTS: `Comments${SCREEN_SUFFIX}`,
ACCOUNT_BOOST: `AccountBoost${SCREEN_SUFFIX}`,
COMMUNITY: `Community${SCREEN_SUFFIX}`,
},
DRAWER: {
MAIN: `Main${DRAWER_SUFFIX}`,

View File

@ -35,6 +35,8 @@ import {
Voters,
Wallet,
AccountBoost,
TagResult,
Community,
} from '../screens';
const bottomTabNavigator = createBottomTabNavigator(
@ -124,12 +126,14 @@ const stackNavigator = createStackNavigator(
[ROUTES.SCREENS.DRAFTS]: { screen: Drafts },
[ROUTES.SCREENS.BOOKMARKS]: { screen: Bookmarks },
[ROUTES.SCREENS.SEARCH_RESULT]: { screen: SearchResult },
[ROUTES.SCREENS.TAG_RESULT]: { screen: TagResult },
[ROUTES.SCREENS.TRANSFER]: { screen: Transfer },
[ROUTES.SCREENS.BOOST]: { screen: Boost },
[ROUTES.SCREENS.REDEEM]: { screen: Redeem },
[ROUTES.SCREENS.REBLOGS]: { screen: Reblogs },
[ROUTES.SCREENS.SPIN_GAME]: { screen: SpinGame },
[ROUTES.SCREENS.ACCOUNT_BOOST]: { screen: AccountBoost },
[ROUTES.SCREENS.COMMUNITY]: { screen: Community },
},
{
headerMode: 'none',

View File

@ -6,8 +6,6 @@ import { Client, PrivateKey, cryptoUtils } from '@esteemapp/dhive';
import hivesigner from 'hivesigner';
import Config from 'react-native-config';
import { get, has } from 'lodash';
import axios from 'axios';
import { getInputRangeFromIndexes } from 'react-native-snap-carousel';
import { getServer } from '../../realm/realm';
import { getUnreadActivityCount } from '../esteem/esteem';
import { userActivity } from '../esteem/ePoint';
@ -1484,7 +1482,7 @@ export const profileUpdate = async (params, pin, currentAccount) => {
.then((resp) => resp.result)
.catch((error) => console.log(error));
}
console.log('priv key', key);
if (key) {
const opArray = [
[
@ -1521,6 +1519,34 @@ export const profileUpdate = async (params, pin, currentAccount) => {
);
};
export const subscribeCommunity = (currentAccount, pinCode, data) => {
const pin = getDigitPinCode(pinCode);
const key = getActiveKey(get(currentAccount, 'local'), pin);
const username = get(currentAccount, 'name');
const json = JSON.stringify([
data.isSubscribed ? 'unsubscribe' : 'subscribe',
{ community: data.communityId },
]);
if (key) {
const privateKey = PrivateKey.fromString(key);
const op = {
id: 'community',
json,
required_auths: [username],
required_posting_auths: [],
};
return client.broadcast.json(op, privateKey);
}
return Promise.reject(
new Error('Check private key permission! Required private active key or above.'),
);
};
export const getBtcAddress = (pin, currentAccount) => {
/*const digitPinCode = getDigitPinCode(pin);
const key = getActiveKey(get(currentAccount, 'local'), digitPinCode);

View File

@ -0,0 +1,38 @@
import axios from 'axios';
const DEFAULT_SERVER = [
'https://rpc.esteem.app',
'https://anyx.io',
'https://api.pharesim.me',
'https://api.hive.blog',
'https://api.hivekings.com',
];
const pickAServer = () => DEFAULT_SERVER.sort(() => 0.5 - Math.random())[0];
const bridgeApiCall = (endpoint, params) =>
axios
.post(pickAServer(), {
jsonrpc: '2.0',
method: endpoint,
params: params,
id: 1,
})
.then((resp) => {
return resp.data.result || null;
});
export const getCommunity = (name, observer = '') =>
bridgeApiCall('bridge.get_community', { name, observer });
export const getCommunities = (last = '', limit = 100, query = '', sort = 'rank', observer = '') =>
bridgeApiCall('bridge.list_communities', {
last,
limit,
query,
sort,
observer,
});
export const getSubscriptions = (account = '') =>
bridgeApiCall('bridge.list_all_subscriptions', { account });

View File

@ -0,0 +1,67 @@
import { useState, useEffect } from 'react';
import { withNavigation } from 'react-navigation';
import get from 'lodash/get';
import { connect } from 'react-redux';
import { getCommunity, getSubscriptions } from '../../../providers/steem/steem';
import { subscribeCommunity } from '../../../providers/steem/dsteem';
import ROUTES from '../../../constants/routeNames';
const CommunityContainer = ({ children, navigation, currentAccount, pinCode }) => {
const [data, setData] = useState(null);
const [isSubscribed, setIsSubscribed] = useState(false);
const tag = get(navigation, 'state.params.tag');
useEffect(() => {
getCommunity(tag).then((res) => {
setData(res);
});
}, [tag]);
useEffect(() => {
if (data) {
getSubscriptions(currentAccount.username).then((result) => {
const _isSubscribed = result.some((item) => item[0] === data.name);
setIsSubscribed(_isSubscribed);
});
}
}, [data]);
const _handleSubscribeButtonPress = () => {
const _data = {
isSubscribed: !isSubscribed,
communityId: data.name,
};
subscribeCommunity(currentAccount, pinCode, _data).then((result) => {
setIsSubscribed(!isSubscribed);
});
};
const _handleNewPostButtonPress = () => {
navigation.navigate({
routeName: ROUTES.SCREENS.EDITOR,
params: {
tags: [tag],
},
});
};
return (
children &&
children({
data,
handleSubscribeButtonPress: _handleSubscribeButtonPress,
handleNewPostButtonPress: _handleNewPostButtonPress,
isSubscribed: isSubscribed,
})
);
};
const mapStateToProps = (state) => ({
currentAccount: state.account.currentAccount,
pinCode: state.application.pin,
});
export default connect(mapStateToProps)(withNavigation(CommunityContainer));

View File

@ -0,0 +1,4 @@
import Community from './screen/communityScreen';
export { Community };
export default Community;

View File

@ -0,0 +1,99 @@
import React from 'react';
import { View, Text } from 'react-native';
import { useIntl } from 'react-intl';
// Components
import { Posts, CollapsibleCard, Header } from '../../../components';
import { Tag, ProfileSummaryPlaceHolder } from '../../../components/basicUIElements';
import CommunityContainer from '../container/communityContainer';
// Styles
import styles from './communityStyles';
import { GLOBAL_POST_FILTERS, GLOBAL_POST_FILTERS_VALUE } from '../../../constants/options/filters';
const TagResultScreen = ({ navigation }) => {
const tag = navigation.getParam('tag', '');
const filter = navigation.getParam('filter', '');
const intl = useIntl();
const _getSelectedIndex = () => {
if (filter) {
const selectedIndex = GLOBAL_POST_FILTERS_VALUE.indexOf(filter);
if (selectedIndex > 0) {
return selectedIndex;
}
}
return 0;
};
return (
<CommunityContainer>
{({ data, handleSubscribeButtonPress, handleNewPostButtonPress, isSubscribed }) => (
<View style={styles.container}>
<Header isReverse hideUser />
{data ? (
<CollapsibleCard title={data.title} isTitleCenter defaultTitle="">
<View style={styles.collapsibleCard}>
<Text style={styles.description}>{data.description}</Text>
<View style={styles.separator} />
<Text style={styles.stats}>
{`${data.subscribers} ${intl.formatMessage({
id: 'search_result.communities.subscribers',
})} ${data.num_authors} ${intl.formatMessage({
id: 'search_result.communities.posters',
})} ${data.num_pending} ${intl.formatMessage({
id: 'search_result.communities.posts',
})}`}
</Text>
<View style={styles.separator} />
<View style={{ flexDirection: 'row' }}>
<Tag
style={styles.subscribeButton}
textStyle={!isSubscribed && styles.subscribeButtonText}
value={
isSubscribed
? intl.formatMessage({
id: 'search_result.communities.subscribe',
})
: intl.formatMessage({
id: 'search_result.communities.unsubscribe',
})
}
isPin={isSubscribed}
isFilter
onPress={handleSubscribeButtonPress}
/>
<Tag
style={styles.subscribeButton}
value={intl.formatMessage({
id: 'community.new_post',
})}
isFilter
isPin
onPress={handleNewPostButtonPress}
/>
</View>
</View>
</CollapsibleCard>
) : (
<ProfileSummaryPlaceHolder />
)}
<View tabLabel={intl.formatMessage({ id: 'search.posts' })} style={styles.tabbarItem}>
<Posts
key={tag}
filterOptions={GLOBAL_POST_FILTERS}
filterOptionsValue={GLOBAL_POST_FILTERS_VALUE}
selectedOptionIndex={_getSelectedIndex()}
tag={tag}
/>
</View>
</View>
)}
</CommunityContainer>
);
};
export default TagResultScreen;

View File

@ -0,0 +1,62 @@
import EStyleSheet from 'react-native-extended-stylesheet';
export default EStyleSheet.create({
container: {
flex: 1,
backgroundColor: '$primaryLightBackground',
},
buttonContainer: {
width: '50%',
alignItems: 'center',
},
tabbar: {
alignSelf: 'center',
height: 40,
backgroundColor: '$primaryBackgroundColor',
shadowOpacity: 0.2,
shadowColor: '$shadowColor',
shadowOffset: { height: 4 },
zIndex: 99,
borderBottomColor: '$shadowColor',
borderBottomWidth: 0.1,
marginTop: 8,
},
tabbarItem: {
flex: 1,
backgroundColor: '$primaryBackgroundColor',
minWidth: '$deviceWidth',
},
tabs: {
flex: 1,
},
tabView: {
backgroundColor: '$primaryGrayBackground',
},
description: {
fontSize: 14,
fontFamily: '$primaryFont',
marginTop: 5,
color: '$primaryBlack',
textAlign: 'center',
},
separator: {
width: 100,
alignSelf: 'center',
backgroundColor: '$primaryDarkGray',
height: 0.5,
marginVertical: 10,
},
stats: {
fontSize: 14,
fontFamily: '$primaryFont',
color: '$primaryDarkGray',
},
subscribeButton: {
borderWidth: 1,
borderColor: '$primaryBlue',
},
collapsibleCard: { alignItems: 'center', marginBottom: 20 },
subscribeButtonText: {
color: '$primaryBlue',
},
});

View File

@ -716,7 +716,7 @@ class EditorContainer extends Component {
}
render() {
const { isLoggedIn, isDarkTheme } = this.props;
const { isLoggedIn, isDarkTheme, navigation } = this.props;
const {
autoFocusText,
draftPost,
@ -731,6 +731,8 @@ class EditorContainer extends Component {
uploadedImage,
} = this.state;
const tags = navigation.state.params.tags;
return (
<EditorScreen
autoFocusText={autoFocusText}
@ -754,6 +756,7 @@ class EditorContainer extends Component {
saveCurrentDraft={this._saveCurrentDraft}
saveDraftToDB={this._saveDraftToDB}
uploadedImage={uploadedImage}
tags={tags}
/>
);
}

View File

@ -36,7 +36,7 @@ class EditorScreen extends Component {
fields: {
title: (props.draftPost && props.draftPost.title) || '',
body: (props.draftPost && props.draftPost.body) || '',
tags: (props.draftPost && props.draftPost.tags) || [],
tags: (props.draftPost && props.draftPost.tags) || props.tags || [],
isValid: false,
},
};
@ -50,6 +50,7 @@ class EditorScreen extends Component {
fields: {
...prevState.fields,
...nextProps.draftPost,
tags: prevState.fields.tags,
},
}));
}

View File

@ -22,6 +22,8 @@ import Transfer from './transfer';
import Voters from './voters';
import AccountBoost from './accountBoost/screen/accountBoostScreen';
import Register from './register/registerScreen';
import TagResult from './tagResult';
import { Community } from './community';
export {
Bookmarks,
@ -48,4 +50,6 @@ export {
Transfer,
Voters,
Wallet,
TagResult,
Community,
};

View File

@ -0,0 +1,76 @@
import { useState, useEffect } from 'react';
import { withNavigation } from 'react-navigation';
import { connect } from 'react-redux';
import ROUTES from '../../../constants/routeNames';
import { getCommunities, getSubscriptions } from '../../../providers/steem/steem';
import { subscribeCommunity } from '../../../providers/steem/dsteem';
const CommunitiesContainer = ({ children, navigation, searchValue, currentAccount, pinCode }) => {
const [data, setData] = useState();
const [filterIndex, setFilterIndex] = useState(0);
const [query, setQuery] = useState('');
const [sort, setSort] = useState('rank');
const [allSubscriptions, setAllSubscriptions] = useState([]);
useEffect(() => {
setData([]);
getCommunities('', 100, query, sort).then((res) => {
if (res) {
setData(res);
}
});
}, [query, sort]);
useEffect(() => {
setData([]);
setQuery(searchValue);
}, [searchValue]);
useEffect(() => {
if (data) {
getSubscriptions(currentAccount.username).then((result) => {
setAllSubscriptions(result);
});
}
}, [data]);
// Component Functions
const _handleOnVotersDropdownSelect = (index, value) => {
setFilterIndex(index);
setSort(value);
};
const _handleOnPress = (name) => {
navigation.navigate({
routeName: ROUTES.SCREENS.COMMUNITY,
params: {
tag: name,
},
});
};
const _handleSubscribeButtonPress = (_data) => {
return subscribeCommunity(currentAccount, pinCode, _data);
};
return (
children &&
children({
data,
filterIndex,
allSubscriptions,
handleOnVotersDropdownSelect: _handleOnVotersDropdownSelect,
handleOnPress: _handleOnPress,
handleSubscribeButtonPress: _handleSubscribeButtonPress,
})
);
};
const mapStateToProps = (state) => ({
currentAccount: state.account.currentAccount,
pinCode: state.application.pin,
});
export default connect(mapStateToProps)(withNavigation(CommunitiesContainer));

View File

@ -0,0 +1,83 @@
import { useState, useEffect } from 'react';
import get from 'lodash/get';
import { withNavigation } from 'react-navigation';
import { connect } from 'react-redux';
import ROUTES from '../../../constants/routeNames';
import { lookupAccounts, getTrendingTags } from '../../../providers/steem/dsteem';
import { getLeaderboard } from '../../../providers/esteem/esteem';
const OtherResultContainer = (props) => {
const [users, setUsers] = useState([]);
const [tags, setTags] = useState([]);
const [filterIndex, setFilterIndex] = useState(0);
const { children, navigation, searchValue, username } = props;
useEffect(() => {
setUsers([]);
setTags([]);
if (searchValue) {
lookupAccounts(searchValue).then((res) => {
setUsers(res);
});
getTrendingTags(searchValue).then((res) => {
setTags(res);
});
} else {
getLeaderboard().then((result) => {
setUsers(result.map((item) => item._id));
});
}
}, [searchValue]);
// Component Functions
const _handleOnPress = (item) => {
switch (filterIndex) {
case 0:
navigation.navigate({
routeName: item === username ? ROUTES.TABBAR.PROFILE : ROUTES.SCREENS.PROFILE,
params: {
username: item,
},
key: item.text,
});
break;
case 1:
navigation.navigate({
routeName: ROUTES.SCREENS.TAG_RESULT,
params: {
tag: get(item, 'name', ''),
},
});
break;
default:
break;
}
};
const _handleFilterChanged = (index, value) => {
setFilterIndex(index);
};
return (
children &&
children({
users,
tags,
filterIndex,
handleOnPress: _handleOnPress,
handleFilterChanged: _handleFilterChanged,
})
);
};
const mapStateToProps = (state) => ({
username: state.account.currentAccount.name,
});
export default connect(mapStateToProps)(withNavigation(OtherResultContainer));

View File

@ -0,0 +1,90 @@
import { useState, useEffect } from 'react';
import get from 'lodash/get';
import { withNavigation } from 'react-navigation';
import { connect } from 'react-redux';
import ROUTES from '../../../constants/routeNames';
import { search, getPromotePosts } from '../../../providers/esteem/esteem';
import { getPost } from '../../../providers/steem/dsteem';
const PostResultContainer = ({ children, navigation, searchValue, currentAccountUsername }) => {
const [data, setData] = useState([]);
const [filterIndex, setFilterIndex] = useState(0);
const [sort, setSort] = useState('relevance');
const [scrollId, setScrollId] = useState('');
useEffect(() => {
setData([]);
if (searchValue) {
search({ q: searchValue, sort }).then((res) => {
setScrollId(res.scroll_id);
setData(res.results);
});
} else {
getPromotePosts()
.then((result) => {
return Promise.all(
result.map((item) =>
getPost(
get(item, 'author'),
get(item, 'permlink'),
currentAccountUsername,
true,
).then((post) => {
post.author_rep = post.author_reputation;
return post;
}),
),
);
})
.then((result) => {
setData(result);
});
}
}, [searchValue, sort]);
// Component Functions
const _handleOnPress = (item) => {
navigation.navigate({
routeName: ROUTES.SCREENS.POST,
params: {
author: get(item, 'author'),
permlink: get(item, 'permlink'),
},
key: get(item, 'permlink'),
});
};
const _handleFilterChanged = (index, value) => {
setFilterIndex(index);
setSort(value);
};
const _loadMore = (index, value) => {
if (scrollId) {
search({ q: searchValue, sort, scroll_id: scrollId }).then((res) => {
setData([...data, ...res.results]);
});
}
};
return (
children &&
children({
data,
filterIndex,
handleOnPress: _handleOnPress,
handleFilterChanged: _handleFilterChanged,
loadMore: _loadMore,
})
);
};
const mapStateToProps = (state) => ({
currentAccountUsername: state.account.currentAccount.username,
});
export default connect(mapStateToProps)(withNavigation(PostResultContainer));

View File

@ -0,0 +1,77 @@
import React, { useState } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { useIntl } from 'react-intl';
import styles from './communitiesListItemStyles';
import { Tag } from '../../../components/basicUIElements';
const UserListItem = ({
index,
handleOnPress,
handleOnLongPress,
title,
about,
admins,
id,
authors,
posts,
subscribers,
isNsfw,
name,
handleSubscribeButtonPress,
isSubscribed,
}) => {
const [subscribed, setSubscribed] = useState(isSubscribed);
const intl = useIntl();
const _handleSubscribeButtonPress = () => {
handleSubscribeButtonPress({ subscribed: !subscribed, communityId: name }).then(() => {
setSubscribed(!subscribed);
});
};
return (
<TouchableOpacity
onLongPress={() => handleOnLongPress && handleOnLongPress()}
onPress={() => handleOnPress && handleOnPress(name)}
>
<View style={[styles.itemWrapper, index % 2 !== 0 && styles.itemWrapperGray]}>
<View style={styles.content}>
<View style={styles.header}>
<Text style={styles.title}>{title}</Text>
<Tag
style={styles.subscribeButton}
textStyle={!subscribed && styles.subscribeButtonText}
value={
subscribed
? intl.formatMessage({
id: 'search_result.communities.subscribe',
})
: intl.formatMessage({
id: 'search_result.communities.unsubscribe',
})
}
isPin={subscribed}
isFilter
onPress={_handleSubscribeButtonPress}
/>
</View>
{!!about && <Text style={styles.about}>{about}</Text>}
<View style={styles.separator} />
<Text style={styles.stats}>
{`${subscribers.toString()} ${intl.formatMessage({
id: 'search_result.communities.subscribers',
})} ${authors.toString()} ${intl.formatMessage({
id: 'search_result.communities.posters',
})} ${posts} ${intl.formatMessage({
id: 'search_result.communities.posts',
})}`}
</Text>
</View>
</View>
</TouchableOpacity>
);
};
export default UserListItem;

View File

@ -0,0 +1,54 @@
import React from 'react';
import { useIntl } from 'react-intl';
import get from 'lodash/get';
// Components
import { FilterBar } from '../../../components';
import CommunitiesList from './communitiesList';
import CommunitiesContainer from '../container/communitiesContainer';
const filterOptions = ['rank', 'subs', 'new'];
const CommunitiesScreen = ({ navigation, searchValue }) => {
const intl = useIntl();
const activeVotes = get(navigation, 'state.params.activeVotes');
return (
<CommunitiesContainer data={activeVotes} searchValue={searchValue}>
{({
data,
filterIndex,
allSubscriptions,
handleOnVotersDropdownSelect,
handleOnPress,
handleSubscribeButtonPress,
}) => (
<>
<FilterBar
dropdownIconName="arrow-drop-down"
options={filterOptions.map((item) =>
intl.formatMessage({
id: `search_result.communities_filter.${item}`,
}),
)}
defaultText={intl.formatMessage({
id: `search_result.communities_filter.${filterOptions[filterIndex]}`,
})}
selectedOptionIndex={filterIndex}
onDropdownSelect={(index) => handleOnVotersDropdownSelect(index, filterOptions[index])}
/>
<CommunitiesList
votes={data}
allSubscriptions={allSubscriptions}
handleOnPress={handleOnPress}
handleSubscribeButtonPress={handleSubscribeButtonPress}
/>
</>
)}
</CommunitiesContainer>
);
};
export default CommunitiesScreen;

View File

@ -0,0 +1,65 @@
import React from 'react';
import { SafeAreaView, FlatList } from 'react-native';
// Components
import CommunitiesListItem from './CommunitiesListItem';
import { CommunitiesPlaceHolder } from '../../../components/basicUIElements';
// Styles
import styles from './communitiesListStyles';
const VotersDisplayView = ({
votes,
handleOnPress,
handleSubscribeButtonPress,
allSubscriptions,
}) => {
const _renderItem = (item, index) => {
const isSubscribed = allSubscriptions.some((sub) => sub[0] === item.name);
return (
<CommunitiesListItem
index={index}
title={item.title}
about={item.about}
admins={item.admins}
id={item.id}
authors={item.num_authors}
posts={item.num_pending}
subscribers={item.subscribers}
isNsfw={item.is_nsfw}
name={item.name}
handleOnPress={handleOnPress}
handleSubscribeButtonPress={handleSubscribeButtonPress}
isSubscribed={isSubscribed}
/>
);
};
const _renderEmptyContent = () => {
return (
<>
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
</>
);
};
return (
<SafeAreaView style={styles.container}>
<FlatList
data={votes}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item, index }) => _renderItem(item, index)}
ListEmptyComponent={_renderEmptyContent}
/>
</SafeAreaView>
);
};
export default VotersDisplayView;

View File

@ -0,0 +1,58 @@
import EStyleSheet from 'react-native-extended-stylesheet';
export default EStyleSheet.create({
container: {
padding: 8,
flexDirection: 'row',
},
content: {
flexDirection: 'column',
marginLeft: 8,
width: '100%',
},
itemWrapper: {
alignItems: 'center',
padding: 16,
borderRadius: 8,
flexDirection: 'row',
backgroundColor: '$primaryBackgroundColor',
},
itemWrapperGray: {
backgroundColor: '$primaryLightBackground',
},
title: {
color: '$primaryBlue',
fontSize: 17,
fontWeight: 'bold',
fontFamily: '$primaryFont',
},
about: {
fontSize: 14,
fontFamily: '$primaryFont',
marginTop: 5,
color: '$primaryBlack',
},
separator: {
width: 100,
alignSelf: 'center',
backgroundColor: '$primaryDarkGray',
height: 0.5,
marginVertical: 5,
},
stats: {
fontSize: 14,
fontFamily: '$primaryFont',
color: '$primaryDarkGray',
},
subscribeButton: {
borderWidth: 1,
borderColor: '$primaryBlue',
},
subscribeButtonText: {
color: '$primaryBlue',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
},
});

View File

@ -0,0 +1,16 @@
import EStyleSheet from 'react-native-extended-stylesheet';
export default EStyleSheet.create({
container: {
flex: 1,
padding: 8,
marginBottom: 40,
flexDirection: 'row',
backgroundColor: '$primaryBackgroundColor',
},
text: {
color: '$iconColor',
fontSize: 12,
fontFamily: '$primaryFont',
},
});

View File

@ -0,0 +1,104 @@
import React from 'react';
import { SafeAreaView, FlatList, View, Text, TouchableOpacity } from 'react-native';
import { useIntl } from 'react-intl';
// Components
import { FilterBar, UserAvatar } from '../../../components';
import { CommunitiesPlaceHolder } from '../../../components/basicUIElements';
import OtherResultContainer from '../container/otherResultContainer';
import styles from './otherResultsStyles';
import DEFAULT_IMAGE from '../../../assets/no_image.png';
const filterOptions = ['user', 'tag'];
const OtherResult = ({ navigation, searchValue }) => {
const intl = useIntl();
const _renderUserItem = (item, index) => (
<View style={[styles.itemWrapper, index % 2 !== 0 && styles.itemWrapperGray]}>
<UserAvatar username={item} defaultSource={DEFAULT_IMAGE} noAction />
<Text style={styles.username}>{item}</Text>
</View>
);
const _renderTagItem = (item, index) => (
<View style={[styles.itemWrapper, index % 2 !== 0 && styles.itemWrapperGray]}>
<Text style={styles.username}>{`#${item.name}`}</Text>
</View>
);
const _renderEmptyContent = () => {
return (
<>
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
</>
);
};
const _renderList = (users, tags, filterIndex, handleOnPress) => {
switch (filterIndex) {
case 0:
return (
<FlatList
data={users}
keyExtractor={(item) => item.id}
renderItem={({ item, index }) => (
<TouchableOpacity onPress={() => handleOnPress(item)}>
{_renderUserItem(item, index)}
</TouchableOpacity>
)}
ListEmptyComponent={_renderEmptyContent}
/>
);
case 1:
return (
<FlatList
data={tags}
keyExtractor={(item) => item.id}
renderItem={({ item, index }) => (
<TouchableOpacity onPress={() => handleOnPress(item)}>
{_renderTagItem(item, index)}
</TouchableOpacity>
)}
ListEmptyComponent={_renderEmptyContent}
/>
);
default:
break;
}
};
return (
<OtherResultContainer searchValue={searchValue}>
{({ users, tags, filterIndex, handleFilterChanged, handleOnPress, loadMore }) => (
<SafeAreaView style={styles.container}>
<FilterBar
dropdownIconName="arrow-drop-down"
options={filterOptions.map((item) =>
intl.formatMessage({
id: `search_result.other_result_filter.${item}`,
}),
)}
defaultText={intl.formatMessage({
id: `search_result.other_result_filter.${filterOptions[filterIndex]}`,
})}
selectedOptionIndex={filterIndex}
onDropdownSelect={(index) => handleFilterChanged(index, filterOptions[index])}
/>
{_renderList(users, tags, filterIndex, handleOnPress)}
</SafeAreaView>
)}
</OtherResultContainer>
);
};
export default OtherResult;

View File

@ -0,0 +1,24 @@
import EStyleSheet from 'react-native-extended-stylesheet';
export default EStyleSheet.create({
container: {
flex: 1,
backgroundColor: '$primaryBackgroundColor',
},
itemWrapper: {
paddingHorizontal: 16,
paddingTop: 16,
paddingBottom: 8,
borderRadius: 8,
backgroundColor: '$primaryBackgroundColor',
flexDirection: 'row',
alignItems: 'center',
},
itemWrapperGray: {
backgroundColor: '$primaryLightBackground',
},
username: {
marginLeft: 10,
color: '$primaryBlack',
},
});

View File

@ -0,0 +1,110 @@
import React from 'react';
import { SafeAreaView, FlatList, View, Text, TouchableOpacity } from 'react-native';
import get from 'lodash/get';
import FastImage from 'react-native-fast-image';
import { useIntl } from 'react-intl';
// Components
import { PostHeaderDescription, FilterBar } from '../../../components';
import { TextWithIcon, CommunitiesPlaceHolder } from '../../../components/basicUIElements';
import PostResultContainer from '../container/postResultContainer';
import { getTimeFromNow } from '../../../utils/time';
import styles from './postResultStyles';
import DEFAULT_IMAGE from '../../../assets/no_image.png';
const filterOptions = ['relevance', 'popularity', 'newest'];
const PostResult = ({ navigation, searchValue }) => {
const intl = useIntl();
const _renderItem = (item, index) => {
return (
<View style={[styles.itemWrapper, index % 2 !== 0 && styles.itemWrapperGray]}>
<PostHeaderDescription
date={getTimeFromNow(get(item, 'created_at'))}
name={get(item, 'author')}
reputation={Math.floor(get(item, 'author_rep'))}
size={36}
tag={item.category}
/>
<FastImage source={item.img_url} style={styles.thumbnail} defaultSource={DEFAULT_IMAGE} />
<View style={[styles.postDescription]}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.summary} numberOfLines={2}>
{item.body}
</Text>
</View>
<View style={styles.stats}>
{item.payout && <Text style={styles.postIconText}>{`$ ${item.payout}`}</Text>}
<TextWithIcon
iconName="heart-outline"
textStyle={styles.postIconText}
iconStyle={styles.postIcon}
iconType="MaterialCommunityIcons"
text={get(item, 'up_votes', 0)}
/>
<TextWithIcon
iconName="comment-outline"
iconStyle={styles.postIcon}
iconType="MaterialCommunityIcons"
text={get(item, 'children', 0)}
textStyle={styles.postIconText}
/>
</View>
</View>
);
};
const _renderEmptyContent = () => {
return (
<>
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
<CommunitiesPlaceHolder />
</>
);
};
return (
<PostResultContainer searchValue={searchValue}>
{({ data, filterIndex, handleFilterChanged, handleOnPress, loadMore }) => (
<SafeAreaView style={styles.container}>
<FilterBar
dropdownIconName="arrow-drop-down"
options={filterOptions.map((item) =>
intl.formatMessage({
id: `search_result.post_result_filter.${item}`,
}),
)}
defaultText={intl.formatMessage({
id: `search_result.post_result_filter.${filterOptions[filterIndex]}`,
})}
selectedOptionIndex={filterIndex}
onDropdownSelect={(index) => handleFilterChanged(index, filterOptions[index])}
/>
<FlatList
data={data}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item, index }) => (
<TouchableOpacity onPress={() => handleOnPress(item)}>
{_renderItem(item, index)}
</TouchableOpacity>
)}
onEndReached={loadMore}
ListEmptyComponent={_renderEmptyContent}
ListFooterComponent={<CommunitiesPlaceHolder />}
/>
</SafeAreaView>
)}
</PostResultContainer>
);
};
export default PostResult;

View File

@ -0,0 +1,44 @@
import EStyleSheet from 'react-native-extended-stylesheet';
export default EStyleSheet.create({
container: {
flex: 1,
backgroundColor: '$primaryBackgroundColor',
},
title: {
fontSize: 16,
fontWeight: 'bold',
marginVertical: 5,
color: '$primaryBlack',
},
summary: {
fontSize: 13,
color: '$primaryDarkGray',
},
itemWrapper: {
paddingHorizontal: 16,
paddingTop: 16,
paddingBottom: 8,
borderRadius: 8,
backgroundColor: '$primaryBackgroundColor',
},
itemWrapperGray: {
backgroundColor: '$primaryLightBackground',
},
stats: {
flexDirection: 'row',
},
postIcon: {
alignSelf: 'flex-start',
fontSize: 20,
color: '$iconColor',
margin: 0,
width: 20,
marginLeft: 25,
},
postIconText: {
color: '$primaryDarkGray',
fontSize: 13,
alignSelf: 'center',
},
});

View File

@ -1,23 +1,31 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { View, SafeAreaView } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view';
import { useIntl } from 'react-intl';
// Components
import { SearchInput, Posts, TabBar } from '../../../components';
import { SearchInput, TabBar } from '../../../components';
import Communities from './communities';
import PostResult from './postResult';
import OtherResult from './otherResults';
// Styles
import styles from './searchResultStyles';
import globalStyles from '../../../globalStyles';
import { GLOBAL_POST_FILTERS, GLOBAL_POST_FILTERS_VALUE } from '../../../constants/options/filters';
const SearchResultScreen = ({ navigation }) => {
const tag = navigation.getParam('tag', '');
const filter = navigation.getParam('filter', '');
const [searchValue, setSearchValue] = useState('');
const [text, setText] = useState('');
const intl = useIntl();
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
setSearchValue(text);
}, 100);
return () => clearTimeout(delayDebounceFn);
}, [text]);
const _navigationGoBack = () => {
navigation.goBack();
};
@ -31,34 +39,34 @@ const SearchResultScreen = ({ navigation }) => {
/>
);
const _getSelectedIndex = () => {
if (filter) {
const selectedIndex = GLOBAL_POST_FILTERS_VALUE.indexOf(filter);
if (selectedIndex > 0) {
return selectedIndex;
}
}
return 0;
};
return (
<View style={styles.container}>
<SafeAreaView>
<SearchInput
handleOnModalClose={_navigationGoBack}
placeholder={`#${tag}`}
editable={false}
placeholder={intl.formatMessage({ id: 'header.search' })}
onChangeText={setText}
/>
</SafeAreaView>
<ScrollableTabView style={globalStyles.tabView} renderTabBar={_renderTabbar}>
<ScrollableTabView
style={globalStyles.tabView}
renderTabBar={_renderTabbar}
prerenderingSiblingsNumber={Infinity}
>
<View
tabLabel={intl.formatMessage({ id: 'search_result.communities.title' })}
style={styles.tabbarItem}
>
<Communities searchValue={searchValue} />
</View>
<View tabLabel={intl.formatMessage({ id: 'search.posts' })} style={styles.tabbarItem}>
<Posts
key={tag}
filterOptions={GLOBAL_POST_FILTERS}
filterOptionsValue={GLOBAL_POST_FILTERS_VALUE}
selectedOptionIndex={_getSelectedIndex()}
tag={tag}
/>
<PostResult searchValue={searchValue} />
</View>
<View
tabLabel={intl.formatMessage({ id: 'search_result.others' })}
style={styles.tabbarItem}
>
<OtherResult searchValue={searchValue} />
</View>
</ScrollableTabView>
</View>

View File

@ -0,0 +1,4 @@
import SearchResult from './screen/tagResultScreen';
export { SearchResult };
export default SearchResult;

View File

@ -0,0 +1,68 @@
import React from 'react';
import { View, SafeAreaView } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view';
import { useIntl } from 'react-intl';
// Components
import { SearchInput, Posts, TabBar } from '../../../components';
// Styles
import styles from './tagResultStyles';
import globalStyles from '../../../globalStyles';
import { GLOBAL_POST_FILTERS, GLOBAL_POST_FILTERS_VALUE } from '../../../constants/options/filters';
const TagResultScreen = ({ navigation }) => {
const tag = navigation.getParam('tag', '');
const filter = navigation.getParam('filter', '');
const intl = useIntl();
const _navigationGoBack = () => {
navigation.goBack();
};
const _renderTabbar = () => (
<TabBar
style={styles.tabbar}
tabUnderlineDefaultWidth={80}
tabUnderlineScaleX={2}
tabBarPosition="overlayTop"
/>
);
const _getSelectedIndex = () => {
if (filter) {
const selectedIndex = GLOBAL_POST_FILTERS_VALUE.indexOf(filter);
if (selectedIndex > 0) {
return selectedIndex;
}
}
return 0;
};
return (
<View style={styles.container}>
<SafeAreaView>
<SearchInput
handleOnModalClose={_navigationGoBack}
placeholder={`#${tag}`}
editable={false}
/>
</SafeAreaView>
<ScrollableTabView style={globalStyles.tabView} renderTabBar={_renderTabbar}>
<View tabLabel={intl.formatMessage({ id: 'search.posts' })} style={styles.tabbarItem}>
<Posts
key={tag}
filterOptions={GLOBAL_POST_FILTERS}
filterOptionsValue={GLOBAL_POST_FILTERS_VALUE}
selectedOptionIndex={_getSelectedIndex()}
tag={tag}
/>
</View>
</ScrollableTabView>
</View>
);
};
export default TagResultScreen;

View File

@ -0,0 +1,31 @@
import EStyleSheet from 'react-native-extended-stylesheet';
export default EStyleSheet.create({
container: {
flex: 1,
backgroundColor: '$primaryBackgroundColor',
},
buttonContainer: {
width: '50%',
alignItems: 'center',
},
tabbar: {
alignSelf: 'center',
height: 40,
backgroundColor: '$primaryBackgroundColor',
shadowOpacity: 0.2,
shadowColor: '$shadowColor',
shadowOffset: { height: 4 },
zIndex: 99,
borderBottomColor: '$shadowColor',
borderBottomWidth: 0.1,
},
tabbarItem: {
flex: 1,
backgroundColor: '$primaryBackgroundColor',
minWidth: '$deviceWidth',
},
tabs: {
flex: 1,
},
});

View File

@ -24,7 +24,7 @@ const VotersScreen = ({ navigation }) => {
return (
<AccountListContainer data={activeVotes}>
{({ data, filterResult, filterIndex, handleOnVotersDropdownSelect, handleSearch }) => (
<SafeAreaView style={globalStyles.container}>
<>
<BasicHeader
title={`${headerTitle} (${data && data.length})`}
isHasSearch
@ -44,7 +44,7 @@ const VotersScreen = ({ navigation }) => {
onDropdownSelect={handleOnVotersDropdownSelect}
/>
<VotersDisplay votes={filterResult || data} />
</SafeAreaView>
</>
)}
</AccountListContainer>
);