diff --git a/src/core/client/framework/lib/relay/withPaginationContainer.ts b/src/core/client/framework/lib/relay/withPaginationContainer.ts index 5f768c0a7..9c669123b 100644 --- a/src/core/client/framework/lib/relay/withPaginationContainer.ts +++ b/src/core/client/framework/lib/relay/withPaginationContainer.ts @@ -1,10 +1,23 @@ import { - ConnectionConfig, + ConnectionConfig as OrigConnectionConfig, createPaginationContainer, GraphQLTaggedNode, + PageInfo, RelayPaginationProp, } from "react-relay"; import { InferableComponentEnhancerWithProps } from "recompose"; +import { Overwrite } from "talk-framework/types"; + +// TODO: (cvle) Fix this type definition upstream. +interface ConnectionData { + edges?: Readonly; + pageInfo?: PageInfo; +} + +type ConnectionConfig = Overwrite< + OrigConnectionConfig, + { getConnectionFromProps?(props: T): ConnectionData | undefined | null } +>; /** * withPaginationContainer is a curried version of `createPaginationContainers` diff --git a/src/core/client/stream/components/App.tsx b/src/core/client/stream/components/App.tsx index dde8e386c..3e2dc2692 100644 --- a/src/core/client/stream/components/App.tsx +++ b/src/core/client/stream/components/App.tsx @@ -4,17 +4,11 @@ import { StatelessComponent } from "react"; import { Center } from "talk-ui/components"; import AssetListContainer from "../containers/AssetListContainer"; -import PostCommentFormContainer from "../containers/PostCommentFormContainer"; import StreamContainer from "../containers/StreamContainer"; -import Logo from "./Logo"; export interface AppProps { assets?: any | null; - asset?: { - id: string; - isClosed: boolean; - comments: any | null; - } | null; + asset?: {} | null; } const App: StatelessComponent = props => { @@ -24,9 +18,7 @@ const App: StatelessComponent = props => { if (props.asset) { return (
- - - +
); } diff --git a/src/core/client/stream/components/PostCommentForm.css b/src/core/client/stream/components/PostCommentForm.css index a536dcc8b..c776ca7c8 100644 --- a/src/core/client/stream/components/PostCommentForm.css +++ b/src/core/client/stream/components/PostCommentForm.css @@ -7,6 +7,7 @@ margin-bottom: calc(2px * $spacing-unit); } -.postButton { - float: right; -} +.postButtonContainer { + display: flex; + justify-content: flex-end; +} \ No newline at end of file diff --git a/src/core/client/stream/components/PostCommentForm.tsx b/src/core/client/stream/components/PostCommentForm.tsx index 2187994be..018cc71dd 100644 --- a/src/core/client/stream/components/PostCommentForm.tsx +++ b/src/core/client/stream/components/PostCommentForm.tsx @@ -39,11 +39,13 @@ const PostCommentForm: StatelessComponent = props => ( )} - - - +
+ + + +
)} diff --git a/src/core/client/stream/components/Stream.tsx b/src/core/client/stream/components/Stream.tsx index c42bb21b3..ccda84eea 100644 --- a/src/core/client/stream/components/Stream.tsx +++ b/src/core/client/stream/components/Stream.tsx @@ -1,18 +1,36 @@ import * as React from "react"; import { StatelessComponent } from "react"; -import CommentContainer from "../containers/CommentContainer"; +import Logo from "talk-stream/components/Logo"; +import CommentContainer from "talk-stream/containers/CommentContainer"; +import PostCommentFormContainer from "talk-stream/containers/PostCommentFormContainer"; +import { Button } from "talk-ui/components"; export interface StreamProps { - comments: ReadonlyArray<{ id: string }>; + id: string; + isClosed: boolean; + comments: ReadonlyArray<{ id: string }> | null; + onLoadMore: () => void; + hasMore: boolean; } const Stream: StatelessComponent = props => { + if (props.comments === null) { + // TODO: (@cvle) What's the reason for this being null? + return
Comments unavailable
; + } return (
+ + {props.comments.map(comment => ( ))} + {props.hasMore && ( + + )}
); }; diff --git a/src/core/client/stream/containers/AppContainer.tsx b/src/core/client/stream/containers/AppContainer.tsx index 02abbf314..256d860b4 100644 --- a/src/core/client/stream/containers/AppContainer.tsx +++ b/src/core/client/stream/containers/AppContainer.tsx @@ -26,11 +26,7 @@ const enhanced = withFragmentContainer<{ data: Data }>( ...AssetListContainer_assets } asset(id: $assetID) @skip(if: $showAssetList) { - id - isClosed - comments { - ...StreamContainer_comments - } + ...StreamContainer_asset } } ` diff --git a/src/core/client/stream/containers/PostCommentFormContainer.tsx b/src/core/client/stream/containers/PostCommentFormContainer.tsx index d2da26f64..bf4ab8cf9 100644 --- a/src/core/client/stream/containers/PostCommentFormContainer.tsx +++ b/src/core/client/stream/containers/PostCommentFormContainer.tsx @@ -25,6 +25,8 @@ class PostCommentFormContainer extends Component { if (error instanceof BadUserInputError) { return error.invalidArgsLocalized; } + // tslint:disable-next-line:no-console + console.error(error); } return undefined; }; diff --git a/src/core/client/stream/containers/StreamContainer.tsx b/src/core/client/stream/containers/StreamContainer.tsx index 771c25efa..8660dc7bb 100644 --- a/src/core/client/stream/containers/StreamContainer.tsx +++ b/src/core/client/stream/containers/StreamContainer.tsx @@ -1,32 +1,108 @@ -import React, { StatelessComponent } from "react"; -import { graphql } from "react-relay"; +import React from "react"; +import { graphql, RelayPaginationProp } from "react-relay"; -import { withFragmentContainer } from "talk-framework/lib/relay"; +import { withPaginationContainer } from "talk-framework/lib/relay"; import { PropTypesOf } from "talk-framework/types"; -import { StreamContainer_comments as Data } from "talk-stream/__generated__/StreamContainer_comments.graphql"; +import { StreamContainer_asset as Data } from "talk-stream/__generated__/StreamContainer_asset.graphql"; import Stream from "../components/Stream"; interface InnerProps { - comments: Data; + asset: Data; + relay: RelayPaginationProp; } -const StreamContainer: StatelessComponent = props => { - const comments = props.comments.edges.map(edge => edge.node); - return ; -}; +class StreamContainer extends React.Component { + public render() { + const comments = this.props.asset.comments + ? this.props.asset.comments.edges.map(edge => edge.node) + : null; + return ( + + ); + } -const enhanced = withFragmentContainer<{ comments: Data }>( + private loadMore = () => { + if (!this.props.relay.hasMore() || this.props.relay.isLoading()) { + return; + } + + this.props.relay.loadMore( + 10, // Fetch the next 10 feed items + error => { + if (error) { + // tslint:disable-next-line:no-console + console.error(error); + } + } + ); + }; +} + +const enhanced = withPaginationContainer<{ asset: Data }, InnerProps>( graphql` - fragment StreamContainer_comments on CommentsConnection { - edges { - node { - id - ...CommentContainer + fragment StreamContainer_asset on Asset + @argumentDefinitions( + count: { type: "Int", defaultValue: 5 } + cursor: { type: "Cursor" } + orderBy: { type: "COMMENT_SORT", defaultValue: CREATED_AT_DESC } + ) { + id + isClosed + comments(first: $count, after: $cursor, orderBy: $orderBy) + @connection(key: "Stream_comments") { + edges { + node { + id + ...CommentContainer + } } } } - ` + `, + { + direction: "forward", + getConnectionFromProps(props) { + return props.asset && props.asset.comments; + }, + // This is also the default implementation of `getFragmentVariables` if it isn't provided. + getFragmentVariables(prevVars, totalCount) { + return { + ...prevVars, + count: totalCount, + }; + }, + getVariables(props, { count, cursor }, fragmentVariables) { + return { + count, + cursor, + orderBy: fragmentVariables.orderBy, + // assetID isn't specified as an @argument for the fragment, but it should be a + // variable available for the fragment under the query root. + assetID: props.asset.id, + }; + }, + query: graphql` + # Pagination query to be fetched upon calling 'loadMore'. + # Notice that we re-use our fragment, and the shape of this query matches our fragment spec. + query StreamContainerPaginationQuery( + $count: Int! + $cursor: Cursor + $orderBy: COMMENT_SORT! + $assetID: ID! + ) { + asset(id: $assetID) { + ...StreamContainer_asset + @arguments(count: $count, cursor: $cursor, orderBy: $orderBy) + } + } + `, + } )(StreamContainer); export type StreamContainerProps = PropTypesOf; diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index eb060b6a4..b3bfb94eb 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -232,7 +232,10 @@ type PageInfo { """ Indicates that there are more nodes after this subset. """ - hasNextPage: Boolean! + hasNextPage: Boolean + hasPreviousPage: Boolean + startCursor: Cursor + endCursor: Cursor } """ diff --git a/src/core/server/models/comment.ts b/src/core/server/models/comment.ts index 81d046159..ca20647a5 100644 --- a/src/core/server/models/comment.ts +++ b/src/core/server/models/comment.ts @@ -280,6 +280,7 @@ async function retrieveConnection( edges, pageInfo: { hasNextPage, + endCursor: (nodes.length && nodes[nodes.length - 1].created_at) || null, }, }; diff --git a/src/core/server/models/connection.ts b/src/core/server/models/connection.ts index 9ed8cac2b..cc69d1f2c 100644 --- a/src/core/server/models/connection.ts +++ b/src/core/server/models/connection.ts @@ -7,6 +7,7 @@ export interface Edge { export interface PageInfo { hasNextPage: boolean; + endCursor: Cursor; } export interface Connection {