diff --git a/app/src/component/fileList/index.js b/app/src/component/fileList/index.js new file mode 100644 index 0000000..033747c --- /dev/null +++ b/app/src/component/fileList/index.js @@ -0,0 +1,11 @@ +import { connect } from 'react-redux'; +import FileList from './view'; +import { selectClaimsById } from 'lbry-redux'; + +const select = state => ({ + claimsById: selectClaimsById(state), +}); + +const perform = dispatch => ({}); + +export default connect(select, perform)(FileList); diff --git a/app/src/component/fileList/view.js b/app/src/component/fileList/view.js new file mode 100644 index 0000000..aefa98a --- /dev/null +++ b/app/src/component/fileList/view.js @@ -0,0 +1,184 @@ +// @flow +import * as React from 'react'; +import { buildURI } from 'lbry-redux'; +import { FlatList } from 'react-native'; +import FileItem from '../fileItem'; +import discoverStyle from '../../styles/discover'; + +// In the future, all Flow types need to be specified in a common source (lbry-redux, perhaps?) +type FileInfo = { + name: string, + channelName: ?string, + pending?: boolean, + channel_claim_id: string, + value?: { + publisherSignature: { + certificateId: string, + }, + }, + metadata: { + publisherSignature: { + certificateId: string, + }, + }, +}; + +type Props = { + hideFilter: boolean, + sortByHeight?: boolean, + claimsById: Array<{}>, + fileInfos: Array, + checkPending?: boolean, +}; + +type State = { + sortBy: string, +}; + +class FileList extends React.PureComponent { + static defaultProps = { + hideFilter: false, + }; + + constructor(props: Props) { + super(props); + + this.state = { + sortBy: 'dateNew', + }; + + (this: any).handleSortChanged = this.handleSortChanged.bind(this); + + this.sortFunctions = { + dateNew: fileInfos => + this.props.sortByHeight + ? fileInfos.slice().sort((fileInfo1, fileInfo2) => { + if (fileInfo1.pending) { + return -1; + } + const height1 = this.props.claimsById[fileInfo1.claim_id] + ? this.props.claimsById[fileInfo1.claim_id].height + : 0; + const height2 = this.props.claimsById[fileInfo2.claim_id] + ? this.props.claimsById[fileInfo2.claim_id].height + : 0; + if (height1 > height2) { + return -1; + } else if (height1 < height2) { + return 1; + } + return 0; + }) + : [...fileInfos].reverse(), + dateOld: fileInfos => + this.props.sortByHeight + ? fileInfos.slice().sort((fileInfo1, fileInfo2) => { + const height1 = this.props.claimsById[fileInfo1.claim_id] + ? this.props.claimsById[fileInfo1.claim_id].height + : 999999; + const height2 = this.props.claimsById[fileInfo2.claim_id] + ? this.props.claimsById[fileInfo2.claim_id].height + : 999999; + if (height1 < height2) { + return -1; + } else if (height1 > height2) { + return 1; + } + return 0; + }) + : fileInfos, + title: fileInfos => + fileInfos.slice().sort((fileInfo1, fileInfo2) => { + const getFileTitle = fileInfo => { + const { value, metadata, name, claim_name: claimName } = fileInfo; + if (metadata) { + // downloaded claim + return metadata.title || claimName; + } else if (value) { + // published claim + const { title } = value.stream.metadata; + return title || name; + } + // Invalid claim + return ''; + }; + const title1 = getFileTitle(fileInfo1).toLowerCase(); + const title2 = getFileTitle(fileInfo2).toLowerCase(); + if (title1 < title2) { + return -1; + } else if (title1 > title2) { + return 1; + } + return 0; + }), + filename: fileInfos => + fileInfos.slice().sort(({ file_name: fileName1 }, { file_name: fileName2 }) => { + const fileName1Lower = fileName1.toLowerCase(); + const fileName2Lower = fileName2.toLowerCase(); + if (fileName1Lower < fileName2Lower) { + return -1; + } else if (fileName2Lower > fileName1Lower) { + return 1; + } + return 0; + }), + }; + } + + getChannelSignature = (fileInfo: FileInfo) => { + if (fileInfo.pending) { + return undefined; + } + + if (fileInfo.value) { + return fileInfo.value.publisherSignature.certificateId; + } + return fileInfo.channel_claim_id; + }; + + handleSortChanged(event: SyntheticInputEvent<*>) { + this.setState({ + sortBy: event.target.value, + }); + } + + sortFunctions: {}; + + render() { + const { fileInfos, hideFilter, checkPending, navigation, style } = this.props; + const { sortBy } = this.state; + const items = []; + + if (!fileInfos) { + return null; + } + + this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => { + const { name: claimName, claim_name: claimNameDownloaded, claim_id: claimId } = fileInfo; + const uriParams = {}; + + // This is unfortunate + // https://github.com/lbryio/lbry/issues/1159 + const name = claimName || claimNameDownloaded; + uriParams.contentName = name; + uriParams.claimId = claimId; + const uri = buildURI(uriParams); + + items.push(uri); + }); + + return ( + item} + renderItem={({item}) => ( + + )} /> + ); + } +} + +export default FileList; diff --git a/app/src/page/channel/index.js b/app/src/page/channel/index.js index 983b9a3..383bb99 100644 --- a/app/src/page/channel/index.js +++ b/app/src/page/channel/index.js @@ -1,19 +1,22 @@ import { connect } from 'react-redux'; import { + doFetchClaimsByChannel, + doFetchClaimCountByChannel, makeSelectClaimForUri, - makeSelectClaimsInChannelForCurrentPage, + makeSelectClaimsInChannelForPage, makeSelectFetchingChannelClaims, } from 'lbry-redux'; import ChannelPage from './view'; const select = (state, props) => ({ claim: makeSelectClaimForUri(props.uri)(state), - claimsInChannel: makeSelectClaimsInChannelForCurrentPage(props.uri)(state), + claimsInChannel: makeSelectClaimsInChannelForPage(props.uri, props.page || 1)(state), fetching: makeSelectFetchingChannelClaims(props.uri)(state), }); const perform = dispatch => ({ - + fetchClaims: (uri, page) => dispatch(doFetchClaimsByChannel(uri, page)), + fetchClaimCount: uri => dispatch(doFetchClaimCountByChannel(uri)), }); export default connect(select, perform)(ChannelPage); diff --git a/app/src/page/channel/view.js b/app/src/page/channel/view.js index cd59718..0060c5c 100644 --- a/app/src/page/channel/view.js +++ b/app/src/page/channel/view.js @@ -1,20 +1,54 @@ // @flow import React from 'react'; -import { ScrollView, Text, View } from 'react-native'; +import { ActivityIndicator, Text, View } from 'react-native'; +import Colors from '../../styles/colors'; +import FileList from '../../component/fileList'; +import PageHeader from '../../component/pageHeader'; import UriBar from '../../component/uriBar'; import channelPageStyle from '../../styles/channelPage'; class ChannelPage extends React.PureComponent { + componentDidMount() { + const { uri, page, claimsInChannel, fetchClaims, fetchClaimCount } = this.props; + + if (!claimsInChannel || !claimsInChannel.length) { + fetchClaims(uri, page || 1); + fetchClaimCount(uri); + } + } + render() { - const { claim, navigation, uri } = this.props; - const { name } = claim; + const { fetching, claimsInChannel, claim, navigation, uri } = this.props; + const { name, permanent_url: permanentUrl } = claim; + + let contentList; + if (fetching) { + contentList = ( + + + Fetching content... + + ); + } else { + contentList = + claimsInChannel && claimsInChannel.length ? ( + + ) : ( + + No content found. + + ); + } + return ( - {name} - - - + { this.props.navigation.goBack(); }} /> + {contentList} ) diff --git a/app/src/styles/channelPage.js b/app/src/styles/channelPage.js index 6177d2b..8f9e465 100644 --- a/app/src/styles/channelPage.js +++ b/app/src/styles/channelPage.js @@ -9,11 +9,27 @@ const channelPageStyle = StyleSheet.create({ content: { flex: 1 }, + fileList: { + paddingTop: 30, + flex: 1 + }, title: { color: Colors.LbryGreen, fontFamily: 'Metropolis-SemiBold', fontSize: 30, margin: 16 + }, + busyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + flexDirection: 'row' + }, + infoText: { + fontFamily: 'Metropolis-Regular', + fontSize: 20, + textAlign: 'center', + marginLeft: 10 } });