Skip to content

Commit

Permalink
feat: Statement draft (#1302)
Browse files Browse the repository at this point in the history
  • Loading branch information
Betree committed Sep 15, 2024
1 parent 646de2c commit 3399813
Show file tree
Hide file tree
Showing 13 changed files with 165 additions and 43 deletions.
1 change: 1 addition & 0 deletions app/components/Statements/Statement.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default class Statement extends React.PureComponent {
<div>
<StatementHeader
statementTime={statement.time + offset}
isDraft={statement.is_draft}
speaker={speaker}
handleEdit={handleEdit}
handleDelete={handleDelete}
Expand Down
82 changes: 69 additions & 13 deletions app/components/Statements/StatementContainer.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { Check, X } from '@styled-icons/feather'
import classNames from 'classnames'
import React from 'react'
import { withNamespaces } from 'react-i18next'
import { connect } from 'react-redux'

import { MIN_REPUTATION_REMOVE_STATEMENT, MIN_REPUTATION_UPDATE_STATEMENT } from '../../constants'
import { handleFormEffectResponse } from '../../lib/handle_effect_response'
import { deleteStatement, updateStatement } from '../../state/video_debate/statements/effects'
import * as statementSelectors from '../../state/video_debate/statements/selectors'
import CommentForm from '../Comments/CommentForm'
import { withLoggedInUser } from '../LoggedInUser/UserProvider'
import ModalConfirmDelete from '../Modal/ModalConfirmDelete'
import UnstyledButton from '../StyledUtils/UnstyledButton'
import ReputationGuardTooltip from '../Utils/ReputationGuardTooltip'
import Statement from './Statement'
import StatementComments from './StatementComments'
import { StatementForm } from './StatementForm'
Expand All @@ -27,7 +31,7 @@ import { StatementForm } from './StatementForm'
@withLoggedInUser
@withNamespaces('videoDebate')
export default class StatementContainer extends React.PureComponent {
state = { isDeleting: false, isEditing: false, replyTo: null }
state = { isDeleting: false, isEditing: false, replyTo: null, editDraftAction: null }

componentDidUpdate(prevProps) {
if (this.shouldScroll(this.props, prevProps)) {
Expand All @@ -40,7 +44,7 @@ export default class StatementContainer extends React.PureComponent {
}

render() {
const { isDeleting, replyTo } = this.state
const { isDeleting, isEditing, replyTo } = this.state
const { statement, isFocused, speaker, isAuthenticated, loggedInUser, t } = this.props

return (
Expand All @@ -50,17 +54,69 @@ export default class StatementContainer extends React.PureComponent {
>
<div className="card statement">
{this.renderStatementOrEditForm(speaker, statement)}
<StatementComments
statement={statement}
speaker={speaker}
setReplyToComment={this.setReplyToComment}
/>
<CommentForm
statementID={statement.id}
replyTo={replyTo}
setReplyToComment={this.setReplyToComment}
user={isAuthenticated ? loggedInUser : null}
/>
{statement.is_draft && !isEditing ? (
<footer className="card-footer">
<ReputationGuardTooltip requiredRep={MIN_REPUTATION_UPDATE_STATEMENT} asChild>
{({ hasReputation }) => (
<UnstyledButton
className={classNames('card-footer-item', 'submit-button', {
'is-loading': this.props.submitting,
})}
p=".75rem"
disabled={Boolean(!hasReputation || this.state.editDraftAction)}
onClick={async () => {
this.setState({ editDraftAction: 'save' })
try {
await this.props.updateStatement(statement.set('is_draft', false))
} finally {
this.setState({ editDraftAction: null })
}
}}
>
<Check size={16} className="mr-1" />
{t('statement.publish')}
</UnstyledButton>
)}
</ReputationGuardTooltip>
<ReputationGuardTooltip requiredRep={MIN_REPUTATION_REMOVE_STATEMENT} asChild>
{({ hasReputation }) => (
<UnstyledButton
type="button"
p=".75rem"
className="card-footer-item"
disabled={Boolean(!hasReputation || this.state.editDraftAction)}
onClick={async () => {
this.setState({ editDraftAction: 'discard' })
try {
await this.props.deleteStatement({ id: statement.id })
} finally {
this.setState({ editDraftAction: null })
}
}}
>
<X size={16} className="mr-1" />
{t('statement.discard')}
</UnstyledButton>
)}
</ReputationGuardTooltip>
</footer>
) : (
<React.Fragment>
<StatementComments
statement={statement}
speaker={speaker}
setReplyToComment={this.setReplyToComment}
/>
{!statement.is_draft && (
<CommentForm
statementID={statement.id}
replyTo={replyTo}
setReplyToComment={this.setReplyToComment}
user={isAuthenticated ? loggedInUser : null}
/>
)}
</React.Fragment>
)}
{isDeleting && (
<ModalConfirmDelete
title={t('statement.remove')}
Expand Down
79 changes: 53 additions & 26 deletions app/components/Statements/StatementHeader.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { Box } from '@rebass/grid'
import React from 'react'
import { withNamespaces } from 'react-i18next'
import Popup from 'reactjs-popup'
import { InfoCircle } from 'styled-icons/fa-solid'

import { MIN_REPUTATION_REMOVE_STATEMENT, MIN_REPUTATION_UPDATE_STATEMENT } from '../../constants'
import ClickableIcon from '../Utils/ClickableIcon'
import Message from '../Utils/Message'
import ReputationGuardTooltip from '../Utils/ReputationGuardTooltip'
import Tag from '../Utils/Tag'
import TimeDisplay from '../Utils/TimeDisplay'

export default withNamespaces('videoDebate')(
({
t,
statementTime,
isDraft,
speaker,
handleTimeClick,
handleShowHistory,
Expand All @@ -25,6 +30,22 @@ export default withNamespaces('videoDebate')(
<Box mr={2}>
<TimeDisplay time={statementTime} handleClick={handleTimeClick} />
</Box>
{isDraft && (
<Popup
contentStyle={{ zIndex: 999, maxWidth: 350 }}
on="hover"
trigger={
<div className="mr-2">
<Tag type="warning" size="small">
<span className="mr-1"> {t('statement.draft')}</span>
<InfoCircle size={12} />
</Tag>
</div>
}
>
<Message type="dark">{t('statement.draftDetails')}</Message>
</Popup>
)}
{speaker && speaker.picture && (
<img className="speaker-mini" src={speaker.picture} alt="" />
)}
Expand All @@ -34,20 +55,22 @@ export default withNamespaces('videoDebate')(
<div className="card-header-icon">
{!withoutActions && (
<React.Fragment>
<ReputationGuardTooltip
requiredRep={MIN_REPUTATION_REMOVE_STATEMENT}
tooltipPosition="left center"
>
{({ hasReputation }) => (
<ClickableIcon
name="times"
size="action-size"
title={t('main:actions.remove')}
onClick={handleDelete}
disabled={!hasReputation}
/>
)}
</ReputationGuardTooltip>
{!isDraft && (
<ReputationGuardTooltip
requiredRep={MIN_REPUTATION_REMOVE_STATEMENT}
tooltipPosition="left center"
>
{({ hasReputation }) => (
<ClickableIcon
name="times"
size="action-size"
title={t('main:actions.remove')}
onClick={handleDelete}
disabled={!hasReputation}
/>
)}
</ReputationGuardTooltip>
)}
<ReputationGuardTooltip
requiredRep={MIN_REPUTATION_UPDATE_STATEMENT}
tooltipPosition="left center"
Expand All @@ -62,18 +85,22 @@ export default withNamespaces('videoDebate')(
/>
)}
</ReputationGuardTooltip>
<ClickableIcon
name="history"
size="action-size"
title={t('history')}
onClick={handleShowHistory}
/>
<ClickableIcon
name="share-alt"
size="action-size"
title={t('main:actions.share')}
onClick={handleShare}
/>
{!isDraft && (
<React.Fragment>
<ClickableIcon
name="history"
size="action-size"
title={t('history')}
onClick={handleShowHistory}
/>
<ClickableIcon
name="share-alt"
size="action-size"
title={t('main:actions.share')}
onClick={handleShare}
/>
</React.Fragment>
)}
</React.Fragment>
)}

Expand Down
13 changes: 12 additions & 1 deletion app/components/Statements/StatementsList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { FULLHD_WIDTH_THRESHOLD } from '../../constants'
import { postStatement } from '../../state/video_debate/statements/effects'
import { closeStatementForm, setScrollTo } from '../../state/video_debate/statements/reducer'
import { statementFormValueSelector } from '../../state/video_debate/statements/selectors'
import { withLoggedInUser } from '../LoggedInUser/UserProvider'
import StatementContainer from './StatementContainer'
import { StatementForm } from './StatementForm'

Expand All @@ -25,6 +26,7 @@ import { StatementForm } from './StatementForm'
)
@withNamespaces('videoDebate')
@withRouter
@withLoggedInUser
export default class StatementsList extends React.PureComponent {
componentDidMount() {
const searchParams = new URLSearchParams(this.props.location.search)
Expand Down Expand Up @@ -54,10 +56,19 @@ export default class StatementsList extends React.PureComponent {
time,
}))

filterStatements = memoizeOne((statements, isLoggedIn) => {
if (!isLoggedIn) {
return statements.filter((s) => !s.is_draft)
} else {
return statements
}
})

render() {
const { speakers, statementFormSpeakerId, statements, offset } = this.props
const { speakers, statementFormSpeakerId, offset } = this.props
const speakerId =
speakers.size === 1 && !statementFormSpeakerId ? speakers.get(0).id : statementFormSpeakerId
const statements = this.filterStatements(this.props.statements, this.props.isAuthenticated)
return (
<div className="statements-list">
{statementFormSpeakerId !== undefined && (
Expand Down
3 changes: 2 additions & 1 deletion app/components/UsersActions/ActionDiff.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ class ActionDiff extends PureComponent {
render() {
const allActions = this.props.allActions || new List([this.props.action])
const diff = this.generateDiff(allActions, this.props.action)

if (diff.size === 0) {
return null
}
Expand Down Expand Up @@ -63,6 +62,8 @@ class ActionDiff extends PureComponent {
formatChangeValue(value, key) {
if (key === 'speaker_id' && value) {
return <Link to={speakerURL(value)}>#{value}</Link>
} else if (key === 'is_draft' && !value) {
return 'No'
}
return value
}
Expand Down
1 change: 1 addition & 0 deletions app/components/UsersActions/ActionIcon.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const ACTIONS_ICONS = {
revert_vote_up: 'chevron-down',
revert_vote_down: 'chevron-up',
revert_self_vote: 'chevron-down',
start_automatic_statements_extraction: 'tasks',
}

const getIconName = (type) => ACTIONS_ICONS[type] || 'question'
Expand Down
3 changes: 2 additions & 1 deletion app/i18n/en/history.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"action_banned_not_constructive": "Moderated ($t(moderation:reason.4))",
"confirmed_flag": "Confirmed a flag",
"abused_flag": "Refuted a flag",
"social_network_linked": "Linked a social network account"
"social_network_linked": "Linked a social network account",
"start_automatic_statements_extraction": "Started automatic statements extraction"
},
"actionTarget": {
"vote_up": "Received a positive vote",
Expand Down
4 changes: 4 additions & 0 deletions app/i18n/en/videoDebate.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@
},
"statement": {
"remove": "Remove statement",
"draft": "Draft",
"draftDetails": "This statement was automatically extracted from the video, it is not yet verified and may contain errors. Only registered users can see it.",
"discard": "Discard",
"publish": "Publish",
"confirmRemove": "Do you really want to remove this statement?",
"textPlaceholder": "Type a raw transcript of what the speaker says",
"noSpeakerTextPlaceholder": "Describe what you see or select a speaker",
Expand Down
3 changes: 2 additions & 1 deletion app/i18n/fr/history.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"action_banned_not_constructive": "Modéré ($t(moderation:reason.4))",
"confirmed_flag": "A confirmé un signalement",
"abused_flag": "A réfuté d'un signalement",
"social_network_linked": "Compte lié à un réseau social"
"social_network_linked": "Compte lié à un réseau social",
"start_automatic_statements_extraction": "Démarré l'extraction automatique des citations"
},
"actionTarget": {
"vote_up": "Reçu un vote positif",
Expand Down
4 changes: 4 additions & 0 deletions app/i18n/fr/videoDebate.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@
},
"statement": {
"remove": "Retirer la citation",
"draft": "Brouillon",
"draftDetails": "Cette citation a été extraite automatiquement. Elle doit être vérifiée et complétée avant d'être publiée. Seuls les utilisateurs inscrits peuvent voir les brouillons.",
"discard": "Supprimer",
"publish": "Publier",
"confirmRemove": "Êtes-vous sûr·e ?",
"textPlaceholder": "Retranscrivez ici les propos de l'intervenant(e)",
"noSpeakerTextPlaceholder": "Décrivez ce qui apparait à l'image ou ajoutez un intervenant",
Expand Down
1 change: 1 addition & 0 deletions app/state/video_debate/statements/record.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ const Statement = new Record({
text: '',
time: 0,
speaker_id: 0,
is_draft: false,
})
export default Statement
5 changes: 5 additions & 0 deletions app/styles/_components/statements.sass
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ $statement-text-color: #e0e7f1
.card-footer
border-top: none
border-bottom: 1px solid #dbdbdb
.help-tooltip-trigger
display: flex
flex: 1
&:not(:last-child)
border-right: 1px solid #dbdbdb
&.comments
display: block
.expend-comment-form
Expand Down
9 changes: 9 additions & 0 deletions app/styles/_global/global.sass
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ h1
&:hover
color: darken($white, 10)

.is-justify-content-flex-end
justify-content: flex-end

.is-justify-content-center
justify-content: center

/** Spacing helpers **/

@for $i from 0 through 10
Expand Down Expand Up @@ -109,3 +115,6 @@ h1
.py-#{$i}
padding-top: #{$value}rem
padding-bottom: #{$value}rem

.gap-#{$i}
gap: #{$value}rem

0 comments on commit 3399813

Please sign in to comment.