mirror of
https://github.com/wassname/talk.git
synced 2026-08-11 11:27:10 +08:00
[CORL-1150] Discussions Tab (#3050)
* feat: Added Site.topStories edge * feat: Added User.ongoingDiscussions edge * connect discussions tab to relay * style discussions tab * add message if no ongoing discussions * style site name in story row * fix: make line-heights relative * add custom class hooks * feat: condition display based on feature flag * add target attribute to story links * fix: expose some featureFlags * fix: fixed sorting * fix: copy fixes Co-authored-by: tessalt <tessathornton@gmail.com> Co-authored-by: Chi Vinh Le <vinh@vinh.tech>
This commit is contained in:
co-authored by
tessalt
Chi Vinh Le
parent
9a9c92d360
commit
dfdea2b9d2
@@ -11,12 +11,13 @@ import {
|
||||
|
||||
import Comments from "../tabs/Comments";
|
||||
import Configure from "../tabs/Configure";
|
||||
import Discussions from "../tabs/Discussions";
|
||||
import Profile from "../tabs/Profile";
|
||||
import TabBarQuery from "./TabBarQuery";
|
||||
|
||||
import styles from "./App.css";
|
||||
|
||||
type TabValue = "COMMENTS" | "PROFILE" | "%future added value";
|
||||
type TabValue = "COMMENTS" | "PROFILE" | "DISCUSSIONS" | "%future added value";
|
||||
|
||||
export interface AppProps {
|
||||
activeTab: TabValue;
|
||||
@@ -38,6 +39,13 @@ const App: FunctionComponent<AppProps> = (props) => {
|
||||
>
|
||||
<Comments />
|
||||
</TabPane>
|
||||
<TabPane
|
||||
className={CLASSES.discussionsTabPane.$root}
|
||||
tabID="DISCUSSIONS"
|
||||
data-testid="current-tab-pane"
|
||||
>
|
||||
<Discussions />
|
||||
</TabPane>
|
||||
<TabPane
|
||||
className={CLASSES.myProfileTabPane.$root}
|
||||
tabID="PROFILE"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createMutationContainer, LOCAL_ID } from "coral-framework/lib/relay";
|
||||
import { SetMainTabEvent } from "coral-stream/events";
|
||||
|
||||
export interface SetActiveTabInput {
|
||||
tab: "COMMENTS" | "PROFILE" | "%future added value";
|
||||
tab: "COMMENTS" | "PROFILE" | "DISCUSSIONS" | "%future added value";
|
||||
}
|
||||
|
||||
export type SetActiveTabMutation = (input: SetActiveTabInput) => Promise<void>;
|
||||
|
||||
@@ -8,12 +8,13 @@ import { Icon, MatchMedia, Tab, TabBar } from "coral-ui/components/v2";
|
||||
|
||||
import styles from "./TabBar.css";
|
||||
|
||||
type TabValue = "COMMENTS" | "PROFILE" | "%future added value";
|
||||
type TabValue = "COMMENTS" | "PROFILE" | "DISCUSSIONS" | "%future added value";
|
||||
|
||||
export interface Props {
|
||||
activeTab: TabValue;
|
||||
onTabClick: (tab: TabValue) => void;
|
||||
showProfileTab: boolean;
|
||||
showDiscussionsTab: boolean;
|
||||
showConfigureTab: boolean;
|
||||
mode: "%future added value" | "COMMENTS" | "QA" | null;
|
||||
}
|
||||
@@ -69,6 +70,31 @@ const AppTabBar: FunctionComponent<Props> = (props) => {
|
||||
)}
|
||||
</Tab>
|
||||
|
||||
{props.showDiscussionsTab && (
|
||||
<Tab
|
||||
className={cn(CLASSES.tabBar.discussions, {
|
||||
[CLASSES.tabBar.activeTab]: props.activeTab === "DISCUSSIONS",
|
||||
[styles.smallTab]: !matches,
|
||||
})}
|
||||
tabID="DISCUSSIONS"
|
||||
variant="streamPrimary"
|
||||
localizationId="general-tabBar-aria-discussions"
|
||||
>
|
||||
{matches ? (
|
||||
<Localized id="general-tabBar-discussionsTab">
|
||||
<span>Discussions</span>
|
||||
</Localized>
|
||||
) : (
|
||||
<div>
|
||||
<Icon size="lg">list_alt</Icon>
|
||||
<Localized id="general-tabBar-discussionsTab">
|
||||
<div className={styles.smallText}>Discussions</div>
|
||||
</Localized>
|
||||
</div>
|
||||
)}
|
||||
</Tab>
|
||||
)}
|
||||
|
||||
{props.showProfileTab && (
|
||||
<Tab
|
||||
className={cn(CLASSES.tabBar.myProfile, {
|
||||
@@ -93,6 +119,7 @@ const AppTabBar: FunctionComponent<Props> = (props) => {
|
||||
)}
|
||||
</Tab>
|
||||
)}
|
||||
|
||||
{props.showConfigureTab && (
|
||||
<Tab
|
||||
className={cn(CLASSES.tabBar.configure, {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { FunctionComponent, useCallback, useMemo } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import {
|
||||
withFragmentContainer,
|
||||
withLocalStateContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLSTORY_MODE } from "coral-framework/schema";
|
||||
import { GQLFEATURE_FLAG, GQLSTORY_MODE } from "coral-framework/schema";
|
||||
import { Ability, can } from "coral-stream/permissions";
|
||||
|
||||
import { TabBarContainer_settings } from "coral-stream/__generated__/TabBarContainer_settings.graphql";
|
||||
import { TabBarContainer_story } from "coral-stream/__generated__/TabBarContainer_story.graphql";
|
||||
import { TabBarContainer_viewer as ViewerData } from "coral-stream/__generated__/TabBarContainer_viewer.graphql";
|
||||
import { TabBarContainerLocal as Local } from "coral-stream/__generated__/TabBarContainerLocal.graphql";
|
||||
import { TabBarContainer_viewer } from "coral-stream/__generated__/TabBarContainer_viewer.graphql";
|
||||
import { TabBarContainerLocal } from "coral-stream/__generated__/TabBarContainerLocal.graphql";
|
||||
|
||||
import {
|
||||
SetActiveTabInput,
|
||||
@@ -21,43 +22,54 @@ import TabBar from "./TabBar";
|
||||
|
||||
interface Props {
|
||||
story: TabBarContainer_story | null;
|
||||
viewer: ViewerData | null;
|
||||
local: Local;
|
||||
settings: TabBarContainer_settings | null;
|
||||
viewer: TabBarContainer_viewer | null;
|
||||
local: TabBarContainerLocal;
|
||||
setActiveTab: SetActiveTabMutation;
|
||||
}
|
||||
|
||||
export class TabBarContainer extends Component<Props> {
|
||||
private handleSetActiveTab = (tab: SetActiveTabInput["tab"]) => {
|
||||
void this.props.setActiveTab({ tab });
|
||||
};
|
||||
export const TabBarContainer: FunctionComponent<Props> = ({
|
||||
local: { activeTab },
|
||||
viewer,
|
||||
story,
|
||||
settings,
|
||||
setActiveTab,
|
||||
}) => {
|
||||
const handleSetActiveTab = useCallback(
|
||||
(tab: SetActiveTabInput["tab"]) => {
|
||||
void setActiveTab({ tab });
|
||||
},
|
||||
[setActiveTab]
|
||||
);
|
||||
|
||||
public render() {
|
||||
const {
|
||||
local: { activeTab },
|
||||
viewer,
|
||||
story,
|
||||
} = this.props;
|
||||
const showDiscussionsTab = useMemo(
|
||||
() =>
|
||||
!!viewer &&
|
||||
!!settings &&
|
||||
settings.featureFlags.includes(GQLFEATURE_FLAG.DISCUSSIONS),
|
||||
[viewer, settings]
|
||||
);
|
||||
|
||||
return (
|
||||
<TabBar
|
||||
mode={
|
||||
this.props.story
|
||||
? this.props.story.settings.mode
|
||||
: GQLSTORY_MODE.COMMENTS
|
||||
}
|
||||
activeTab={activeTab}
|
||||
showProfileTab={Boolean(viewer)}
|
||||
showConfigureTab={
|
||||
!!viewer &&
|
||||
!!story &&
|
||||
story.canModerate &&
|
||||
can(viewer, Ability.CHANGE_STORY_CONFIGURATION)
|
||||
}
|
||||
onTabClick={this.handleSetActiveTab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
const showConfigureTab = useMemo(
|
||||
() =>
|
||||
!!viewer &&
|
||||
!!story &&
|
||||
story.canModerate &&
|
||||
can(viewer, Ability.CHANGE_STORY_CONFIGURATION),
|
||||
[viewer, story]
|
||||
);
|
||||
|
||||
return (
|
||||
<TabBar
|
||||
mode={story ? story.settings.mode : GQLSTORY_MODE.COMMENTS}
|
||||
activeTab={activeTab}
|
||||
showProfileTab={!!viewer}
|
||||
showDiscussionsTab={showDiscussionsTab}
|
||||
showConfigureTab={showConfigureTab}
|
||||
onTabClick={handleSetActiveTab}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withSetActiveTabMutation(
|
||||
withLocalStateContainer(
|
||||
@@ -81,6 +93,11 @@ const enhanced = withSetActiveTabMutation(
|
||||
}
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment TabBarContainer_settings on Settings {
|
||||
featureFlags
|
||||
}
|
||||
`,
|
||||
})(TabBarContainer)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -28,6 +28,9 @@ class TabBarQuery extends Component<Props> {
|
||||
story(id: $storyID, url: $storyURL) {
|
||||
...TabBarContainer_story
|
||||
}
|
||||
settings {
|
||||
...TabBarContainer_settings
|
||||
}
|
||||
}
|
||||
`}
|
||||
variables={{
|
||||
@@ -41,6 +44,7 @@ class TabBarQuery extends Component<Props> {
|
||||
|
||||
return (
|
||||
<TabBarContainer
|
||||
settings={(props && props.settings) || null}
|
||||
story={(props && props.story) || null}
|
||||
viewer={(props && props.viewer) || null}
|
||||
/>
|
||||
|
||||
@@ -24,6 +24,13 @@ exports[`renders correctly 1`] = `
|
||||
>
|
||||
<CommentsPane />
|
||||
</TabPane>
|
||||
<TabPane
|
||||
className="coral coral-discussions"
|
||||
data-testid="current-tab-pane"
|
||||
tabID="DISCUSSIONS"
|
||||
>
|
||||
<withContext(withLocalStateContainer(DiscussionsQuery)) />
|
||||
</TabPane>
|
||||
<TabPane
|
||||
className="coral coral-myProfile"
|
||||
data-testid="current-tab-pane"
|
||||
|
||||
@@ -60,6 +60,8 @@ const CLASSES = {
|
||||
*/
|
||||
myProfile: "coral coral-tabBar-tab coral-tabBar-myProfile",
|
||||
|
||||
discussions: "coral coral-tabBar-tab coral-tabBar-discussions",
|
||||
|
||||
/**
|
||||
* configure is the button for the "Configure" tab.
|
||||
*/
|
||||
@@ -714,6 +716,10 @@ const CLASSES = {
|
||||
$root: "coral coral-myProfile",
|
||||
},
|
||||
|
||||
discussionsTabPane: {
|
||||
$root: "coral coral-discussions",
|
||||
},
|
||||
|
||||
/**
|
||||
* myUsername is the username part of my profile.
|
||||
*/
|
||||
@@ -981,6 +987,24 @@ const CLASSES = {
|
||||
},
|
||||
|
||||
moderateStream: "coral coral-general-moderateStreamLink",
|
||||
|
||||
discussions: {
|
||||
$root: "coral coral-discussions",
|
||||
mostActiveDiscussions: "coral coral-mostActiveDiscussions",
|
||||
myOngoingDiscussions: "coral coral-myOngoingDiscussions",
|
||||
header: "coral coral-discussions-header",
|
||||
subHeader: "coral coral-discussions-subHeader",
|
||||
discussionsList: "coral coral-discussions-list",
|
||||
story: {
|
||||
$root: "coral coral-discussions-story",
|
||||
header: "coral coral-discussions-story-header",
|
||||
commentsCount: "coral coral-discussions-story-commentsCount",
|
||||
commentsCountIcon: "coral coral-discussions-story-commentsCountIcon",
|
||||
date: "coral coral-discussions-story-date",
|
||||
siteName: "coral coral-discussions-story-siteName",
|
||||
},
|
||||
viewHistoryButton: "coral coral-discussions-viewHistoryButton",
|
||||
},
|
||||
};
|
||||
|
||||
export default CLASSES;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import {
|
||||
createMutation,
|
||||
useMutation,
|
||||
withFragmentContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import UserBoxContainer from "coral-stream/common/UserBox";
|
||||
import { Button, HorizontalGutter } from "coral-ui/components/v2";
|
||||
|
||||
import { DiscussionsContainer_settings } from "coral-stream/__generated__/DiscussionsContainer_settings.graphql";
|
||||
import { DiscussionsContainer_story } from "coral-stream/__generated__/DiscussionsContainer_story.graphql";
|
||||
import { DiscussionsContainer_viewer } from "coral-stream/__generated__/DiscussionsContainer_viewer.graphql";
|
||||
|
||||
import { commit } from "../../App/SetActiveTabMutation";
|
||||
import MostActiveDiscussionsContainer from "./MostActiveDiscussionsContainer";
|
||||
import MyOngoingDiscussionsContainer from "./MyOngoingDiscussionsContainer";
|
||||
|
||||
interface Props {
|
||||
viewer: DiscussionsContainer_viewer;
|
||||
settings: DiscussionsContainer_settings;
|
||||
story: DiscussionsContainer_story;
|
||||
}
|
||||
|
||||
const SetActiveTabMutation = createMutation("setActiveTab", commit);
|
||||
|
||||
const DiscussionsContainer: FunctionComponent<Props> = (props) => {
|
||||
const setActiveTab = useMutation(SetActiveTabMutation);
|
||||
const onFullHistoryClick = useCallback(
|
||||
async () => await setActiveTab({ tab: "PROFILE" }),
|
||||
[]
|
||||
);
|
||||
return (
|
||||
<HorizontalGutter spacing={3} className={CLASSES.discussions.$root}>
|
||||
<UserBoxContainer settings={props.settings} viewer={props.viewer} />
|
||||
<MostActiveDiscussionsContainer site={props.story.site} />
|
||||
<MyOngoingDiscussionsContainer
|
||||
viewer={props.viewer}
|
||||
currentSiteID={props.story.site.id}
|
||||
settings={props.settings}
|
||||
/>
|
||||
<Localized id="discussions-viewFullHistory">
|
||||
<Button
|
||||
variant="outline"
|
||||
color="stream"
|
||||
onClick={onFullHistoryClick}
|
||||
className={CLASSES.discussions.viewHistoryButton}
|
||||
>
|
||||
View full comment history
|
||||
</Button>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
story: graphql`
|
||||
fragment DiscussionsContainer_story on Story {
|
||||
site {
|
||||
id
|
||||
...MostActiveDiscussionsContainer_site
|
||||
}
|
||||
}
|
||||
`,
|
||||
viewer: graphql`
|
||||
fragment DiscussionsContainer_viewer on User {
|
||||
...MyOngoingDiscussionsContainer_viewer
|
||||
...UserBoxContainer_viewer
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment DiscussionsContainer_settings on Settings {
|
||||
...MyOngoingDiscussionsContainer_settings
|
||||
...UserBoxContainer_settings
|
||||
organization {
|
||||
name
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(DiscussionsContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,19 @@
|
||||
.root {
|
||||
border-bottom: 2px solid var(--palette-grey-200);
|
||||
padding-bottom: var(--spacing-2);
|
||||
font-family: var(--font-family-primary);
|
||||
}
|
||||
|
||||
.header {
|
||||
font-weight: var(--font-weight-primary-bold);
|
||||
font-size: var(--font-size-4);
|
||||
line-height: 1;
|
||||
color: var(--palette-text-900);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subHeader {
|
||||
color: var(--palette-text-100);
|
||||
font-size: var(--font-size-2);
|
||||
line-height: 1.285;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import cn from "classnames";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import { Flex, HorizontalGutter, Icon } from "coral-ui/components/v2";
|
||||
|
||||
import styles from "./DiscussionsHeader.css";
|
||||
|
||||
interface Props {
|
||||
header: React.ReactNode;
|
||||
subHeader: React.ReactNode;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const DiscussionsHeader: FunctionComponent<Props> = ({
|
||||
header,
|
||||
subHeader,
|
||||
icon,
|
||||
}) => {
|
||||
return (
|
||||
<HorizontalGutter spacing={1} className={styles.root}>
|
||||
<Flex spacing={1} alignItems="center">
|
||||
<Icon size="md">{icon}</Icon>
|
||||
<h2 className={cn(styles.header, CLASSES.discussions.header)}>
|
||||
{header}
|
||||
</h2>
|
||||
</Flex>
|
||||
|
||||
<div className={cn(styles.subHeader, CLASSES.discussions.subHeader)}>
|
||||
{subHeader}
|
||||
</div>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscussionsHeader;
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import { once } from "lodash";
|
||||
import React, { FunctionComponent, Suspense } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { polyfillCSSVarsForIE11 } from "coral-framework/helpers";
|
||||
import {
|
||||
QueryRenderData,
|
||||
QueryRenderer,
|
||||
withLocalStateContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import useHandleIncompleteAccount from "coral-stream/common/useHandleIncompleteAccount";
|
||||
import { CallOut, Delay, Spinner } from "coral-ui/components/v2";
|
||||
|
||||
import { DiscussionsQuery as QueryTypes } from "coral-stream/__generated__/DiscussionsQuery.graphql";
|
||||
import { DiscussionsQueryLocal as Local } from "coral-stream/__generated__/DiscussionsQueryLocal.graphql";
|
||||
|
||||
const loadDiscussionsContainer = () =>
|
||||
import("./DiscussionsContainer" /* webpackChunkName: "profile" */).then(
|
||||
(x) => {
|
||||
// New css is loaded, take care of polyfilling those css vars for IE11.
|
||||
void polyfillCSSVarsForIE11();
|
||||
return x;
|
||||
}
|
||||
);
|
||||
// (cvle) For some reason without `setTimeout` this request will block other requests.
|
||||
const preloadDiscussionsContainer = once(() =>
|
||||
setTimeout(loadDiscussionsContainer)
|
||||
);
|
||||
|
||||
const LazyDiscussionsContainer = React.lazy(loadDiscussionsContainer);
|
||||
|
||||
interface Props {
|
||||
local: Local;
|
||||
}
|
||||
|
||||
export const render = ({ error, props }: QueryRenderData<QueryTypes>) => {
|
||||
if (error) {
|
||||
return (
|
||||
<CallOut color="error" fullWidth>
|
||||
{error.message}
|
||||
</CallOut>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: use official React API once it has one :-)
|
||||
preloadDiscussionsContainer();
|
||||
|
||||
if (props) {
|
||||
if (!props.viewer) {
|
||||
return (
|
||||
<Localized id="discussions-discussionsQuery-errorLoadingProfile">
|
||||
<CallOut color="error" fullWidth>
|
||||
Error loading profile
|
||||
</CallOut>
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
if (!props.story) {
|
||||
return (
|
||||
<Localized id="discussions-discussionsQuery-storyNotFound">
|
||||
<CallOut>Story not found</CallOut>
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<LazyDiscussionsContainer
|
||||
viewer={props.viewer}
|
||||
story={props.story}
|
||||
settings={props.settings}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Delay>
|
||||
<Spinner />
|
||||
</Delay>
|
||||
);
|
||||
};
|
||||
|
||||
const DiscussionsQuery: FunctionComponent<Props> = ({
|
||||
local: { storyID, storyURL },
|
||||
}) => {
|
||||
const handleIncompleteAccount = useHandleIncompleteAccount();
|
||||
return (
|
||||
<QueryRenderer<QueryTypes>
|
||||
query={graphql`
|
||||
query DiscussionsQuery($storyID: ID, $storyURL: String) {
|
||||
story: stream(id: $storyID, url: $storyURL) {
|
||||
...DiscussionsContainer_story
|
||||
}
|
||||
viewer {
|
||||
...DiscussionsContainer_viewer
|
||||
}
|
||||
settings {
|
||||
...DiscussionsContainer_settings
|
||||
}
|
||||
}
|
||||
`}
|
||||
variables={{
|
||||
storyID,
|
||||
storyURL,
|
||||
}}
|
||||
render={(data) => {
|
||||
if (handleIncompleteAccount(data)) {
|
||||
return null;
|
||||
}
|
||||
return render(data);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withLocalStateContainer(
|
||||
graphql`
|
||||
fragment DiscussionsQueryLocal on Local {
|
||||
storyID
|
||||
storyURL
|
||||
}
|
||||
`
|
||||
)(DiscussionsQuery);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,25 @@
|
||||
.root {
|
||||
font-family: var(--font-family-primary);
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
counter-reset: activeDiscussionsCounter;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
counter-increment: activeDiscussionsCounter;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.listItem:before {
|
||||
content: counter(activeDiscussionsCounter);
|
||||
color: var(--palette-text-100);
|
||||
font-size: var(--font-size-3);
|
||||
font-weight: var(--font-weight-primary-bold);
|
||||
line-height: 1.125;
|
||||
padding-right: var(--spacing-3);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import cn from "classnames";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
|
||||
import { MostActiveDiscussionsContainer_site } from "coral-stream/__generated__/MostActiveDiscussionsContainer_site.graphql";
|
||||
|
||||
import DiscussionsHeader from "./DiscussionsHeader";
|
||||
import StoryRowContainer from "./StoryRowContainer";
|
||||
|
||||
import styles from "./MostActiveDiscussionsContainer.css";
|
||||
|
||||
interface Props {
|
||||
site: MostActiveDiscussionsContainer_site;
|
||||
}
|
||||
|
||||
const MostActiveDiscussionsContainer: FunctionComponent<Props> = ({ site }) => {
|
||||
return (
|
||||
<div className={cn(styles.root, CLASSES.discussions.mostActiveDiscussions)}>
|
||||
<DiscussionsHeader
|
||||
header={
|
||||
<Localized id="discussions-mostActiveDiscussions">
|
||||
Most active discussions
|
||||
</Localized>
|
||||
}
|
||||
subHeader={
|
||||
<Localized
|
||||
id="discussions-mostActiveDiscussions-subhead"
|
||||
$siteName={site.name}
|
||||
>
|
||||
<>
|
||||
Ranked by the most comments received over the last 24 hours on{" "}
|
||||
{site.name}
|
||||
</>
|
||||
</Localized>
|
||||
}
|
||||
icon="show_chart"
|
||||
/>
|
||||
<ol className={cn(styles.list, CLASSES.discussions.discussionsList)}>
|
||||
{site.topStories.map((story) => (
|
||||
<li
|
||||
className={cn(styles.listItem, CLASSES.discussions.story.$root)}
|
||||
key={story.id}
|
||||
>
|
||||
<StoryRowContainer story={story} currentSiteID={site.id} />
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
site: graphql`
|
||||
fragment MostActiveDiscussionsContainer_site on Site {
|
||||
id
|
||||
name
|
||||
topStories {
|
||||
id
|
||||
...StoryRowContainer_story
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(MostActiveDiscussionsContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,14 @@
|
||||
.root {
|
||||
font-family: var(--font-family-primary);
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.emptyList {
|
||||
margin: var(--spacing-3) 0 0 0;
|
||||
color: var(--palette-text-100);
|
||||
font-weight: var(--font-weight-primary-semi-bold);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import cn from "classnames";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
|
||||
import { MyOngoingDiscussionsContainer_settings } from "coral-stream/__generated__/MyOngoingDiscussionsContainer_settings.graphql";
|
||||
import { MyOngoingDiscussionsContainer_viewer } from "coral-stream/__generated__/MyOngoingDiscussionsContainer_viewer.graphql";
|
||||
|
||||
import DiscussionsHeader from "./DiscussionsHeader";
|
||||
import StoryRowContainer from "./StoryRowContainer";
|
||||
|
||||
import styles from "./MyOngoingDiscussionsContainer.css";
|
||||
|
||||
interface Props {
|
||||
viewer: MyOngoingDiscussionsContainer_viewer;
|
||||
settings: MyOngoingDiscussionsContainer_settings;
|
||||
currentSiteID: string;
|
||||
}
|
||||
|
||||
const MyOngoingDiscussionsContainer: FunctionComponent<Props> = ({
|
||||
viewer,
|
||||
settings,
|
||||
currentSiteID,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn(styles.root, CLASSES.discussions.myOngoingDiscussions)}>
|
||||
<DiscussionsHeader
|
||||
header={
|
||||
<Localized id="discussions-myOngoingDiscussions">
|
||||
My ongoing discussions
|
||||
</Localized>
|
||||
}
|
||||
subHeader={
|
||||
<Localized
|
||||
id="discussions-myOngoingDiscussions-subhead"
|
||||
$orgName={settings.organization.name}
|
||||
>
|
||||
<>Where you’ve commented across {settings.organization.name}</>
|
||||
</Localized>
|
||||
}
|
||||
icon="history"
|
||||
/>
|
||||
{viewer.ongoingDiscussions.length === 0 && (
|
||||
<Localized id="discussions-mostActiveDiscussions-empty">
|
||||
<p className={styles.emptyList}>
|
||||
You haven’t participated in any discussions
|
||||
</p>
|
||||
</Localized>
|
||||
)}
|
||||
<ul className={cn(styles.list, CLASSES.discussions.discussionsList)}>
|
||||
{viewer.ongoingDiscussions.map((story) => (
|
||||
<li key={cn(story.id, CLASSES.discussions.story.$root)}>
|
||||
<StoryRowContainer story={story} currentSiteID={currentSiteID} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
viewer: graphql`
|
||||
fragment MyOngoingDiscussionsContainer_viewer on User {
|
||||
ongoingDiscussions {
|
||||
id
|
||||
...StoryRowContainer_story
|
||||
}
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment MyOngoingDiscussionsContainer_settings on Settings {
|
||||
organization {
|
||||
name
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(MyOngoingDiscussionsContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,37 @@
|
||||
.root {
|
||||
text-decoration: none;
|
||||
font-family: var(--font-family-primary);
|
||||
color: var(--palette-text-100);
|
||||
margin-bottom: var(--spacing-4);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.siteName {
|
||||
text-transform: uppercase;
|
||||
font-size: var(--font-size-1);
|
||||
margin-bottom: 0;
|
||||
font-weight: var(--font-weight-primary-bold);
|
||||
}
|
||||
|
||||
.storyTitle {
|
||||
font-weight: var(--font-weight-primary-semi-bold);
|
||||
color: var(--palette-text-500);
|
||||
font-size: var(--font-size-3);
|
||||
line-height: 1.125;
|
||||
}
|
||||
|
||||
.commentsCount {
|
||||
color: var(--palette-primary-500);
|
||||
font-weight: var(--font-weight-primary-bold);
|
||||
font-size: var(--font-size-1);
|
||||
}
|
||||
|
||||
.commentsCountIcon {
|
||||
color: var(--palette-primary-400);
|
||||
font-size: var(--font-size-1);
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: var(--font-size-1);
|
||||
font-weight: var(--font-weight-primary-semi-bold);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import cn from "classnames";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import {
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
Icon,
|
||||
RelativeTime,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { StoryRowContainer_story } from "coral-stream/__generated__/StoryRowContainer_story.graphql";
|
||||
|
||||
import styles from "./StoryRowContainer.css";
|
||||
|
||||
interface Props {
|
||||
story: StoryRowContainer_story;
|
||||
currentSiteID: string;
|
||||
}
|
||||
|
||||
const StoryRowContainer: FunctionComponent<Props> = ({
|
||||
story,
|
||||
currentSiteID,
|
||||
}) => {
|
||||
return (
|
||||
<a href={story.url} className={styles.root} target="_parent">
|
||||
<HorizontalGutter spacing={1}>
|
||||
{currentSiteID !== story.site.id && (
|
||||
<p
|
||||
className={cn(styles.siteName, CLASSES.discussions.story.siteName)}
|
||||
>
|
||||
{story.site.name}
|
||||
</p>
|
||||
)}
|
||||
{story.metadata && story.metadata.title && (
|
||||
<h3
|
||||
className={cn(styles.storyTitle, CLASSES.discussions.story.header)}
|
||||
>
|
||||
{story.metadata.title}
|
||||
</h3>
|
||||
)}
|
||||
<Flex spacing={3}>
|
||||
{story.metadata && story.metadata.publishedAt && (
|
||||
<RelativeTime
|
||||
date={story.metadata.publishedAt}
|
||||
className={cn(styles.time, CLASSES.discussions.story.date)}
|
||||
/>
|
||||
)}
|
||||
<Flex spacing={1} alignItems="center">
|
||||
<Icon
|
||||
className={cn(
|
||||
styles.commentsCountIcon,
|
||||
CLASSES.discussions.story.commentsCountIcon
|
||||
)}
|
||||
>
|
||||
mode_comment
|
||||
</Icon>
|
||||
<span
|
||||
className={cn(
|
||||
styles.commentsCount,
|
||||
CLASSES.discussions.story.commentsCount
|
||||
)}
|
||||
>
|
||||
{story.commentCounts.totalPublished}
|
||||
</span>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
story: graphql`
|
||||
fragment StoryRowContainer_story on Story {
|
||||
site {
|
||||
id
|
||||
name
|
||||
}
|
||||
id
|
||||
url
|
||||
metadata {
|
||||
title
|
||||
publishedAt
|
||||
}
|
||||
commentCounts {
|
||||
totalPublished
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(StoryRowContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./DiscussionsQuery";
|
||||
Reference in New Issue
Block a user