diff --git a/README.md b/README.md index aff80f2e1..342e8df74 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,7 @@ This is a HTML5 application, built with [Brunch](http://brunch.io). - Redux: state management - ReactRouter: routing - phoenix: interaction with phoenix socket -- Bulma: base styles (we're still using an old 0.3.2 version, documentation -[here (Github)](https://github.com/jgthms/bulma/tree/0395dc59d8b147f2f47d057a6ffde2eb2966db49/docs/documentation)) -or [here (archive.org)](https://web.archive.org/web/20170518075321/http://bulma.io/documentation/overview/start/) +- Bulma: base styles ## Conventions @@ -64,7 +62,7 @@ Components files should export two versions : 2. `export default MyComponent` : connected component Non-connected components exports are there for testing them without the need -to be connected to a store. If component is always dump, you can export +to be connected to a store. If component is always dumb, you can export `default` only. You might find some exceptions in old components but all new @@ -82,6 +80,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) ## Linked projects * [Extension](https://github.com/CaptainFact/captain-fact-extension) +* [Overlay injector](https://github.com/CaptainFact/captain-fact-overlay-injector) ## Dependencies diff --git a/app/assets/assets/help/en/contribute/tasks.md b/app/assets/assets/help/en/contribute/tasks.md index 14f24d459..6f1abb45a 100644 --- a/app/assets/assets/help/en/contribute/tasks.md +++ b/app/assets/assets/help/en/contribute/tasks.md @@ -15,7 +15,7 @@ can [contact us](/help/contact) directly. If you have experience with legal structures issues and particularly with non-profit / common good companies your help could be extremely valuable. -# Traductions +# Translation Helping to translate CaptainFact doesn't require any technical skills, it's just a matter of replacing strings in files. @@ -31,4 +31,4 @@ of replacing strings in files. #### If you don't know what the heck git is about -[Contact us](/help/contact) and we'll send you the files along with instructions. \ No newline at end of file +[Contact us](/help/contact) and we'll send you the files along with instructions. diff --git a/app/assets/assets/help/fr/ambassadors.md b/app/assets/assets/help/fr/ambassadors.md index 6c3688205..2968f0c84 100644 --- a/app/assets/assets/help/fr/ambassadors.md +++ b/app/assets/assets/help/fr/ambassadors.md @@ -37,7 +37,7 @@ vous êtes là pour nous rappeler à l'ordre. Que ça soit sur la plateforme ou le [Discord](https://discord.gg/2Qd7hMz), vous pouvez prendre la température des utilisateurs, modérer les échanges et garantir -une utilisation seine de la plateforme. +une utilisation saine de la plateforme. ### 👁‍ Vous êtes en **auto-gestion** ! @@ -47,17 +47,16 @@ nous souhaitons construire une relation de confiance avec vous. ### ⏲‍ Vous êtes **mandatés** ! Afin de garantir un minimum d'investissement et -un renouvellement régulier, votre mandat sera limité à une durée de 6 mois -et renouvelable. - +un renouvellement régulier des ambassadeurs, votre mandat est de 6 mois +et il est renouvelable. ### 😃‍ Vous êtes **réels** ! Nous allons travailler avec vous pour définir votre -statut juridique au sein de notre structure pour garantir +statut juridique au sein de notre structure pour garantir votre indépendance. ### 🔗‍ Vous êtes **un lien** ! -Nous souhaitons casser cette dimension virtuelle de +Nous souhaitons casser la dimension virtuelle de notre communauté et vous avez un rôle à jouer, que ca soit à travers la promotion ou l'organisation d'événements nous serons là pour vous soutenir. @@ -72,4 +71,4 @@ des règles qui vous sembleraient importantes. De quitter vos fonctions d'ambassadeur à tout moment. ### 😃‍ Vous êtes **géniaux** ! -Merci pour votre implication et votre travail. \ No newline at end of file +Merci pour votre implication et votre travail. diff --git a/app/assets/assets/help/fr/bug_report.md b/app/assets/assets/help/fr/bug_report.md index dad333966..87cf5f2fb 100644 --- a/app/assets/assets/help/fr/bug_report.md +++ b/app/assets/assets/help/fr/bug_report.md @@ -14,7 +14,7 @@ pour l'extension. # Pire ! Une faille de sécurité ? -Nous croyons en la divulation responsable et l'encourageons. +Nous croyons en la divulgation responsable et l'encourageons. Si votre signalement est relatif à une faille de sécurité critique, vous pouvez chiffrer votre message avec la clef PGP ci-dessous : diff --git a/app/assets/assets/help/fr/contribute/tasks.md b/app/assets/assets/help/fr/contribute/tasks.md index 9c70e67b6..07f5ff294 100644 --- a/app/assets/assets/help/fr/contribute/tasks.md +++ b/app/assets/assets/help/fr/contribute/tasks.md @@ -6,7 +6,7 @@ Pour toutes les tâches liées au développement, # Design Pour les tâches liées à la ludification (gamification) vous pouvez jeter un -coup d'oeuil [par ici](https://github.com/CaptainFact/captain-fact-frontend/issues/6) +coup d'œuil [par ici](https://github.com/CaptainFact/captain-fact-frontend/issues/6) Pour tout le reste ou si vous n'êtes pas à l'aise avec Github vous pouvez [nous contacter](/help/contact) directement. diff --git a/app/assets/assets/help/fr/moderation.md b/app/assets/assets/help/fr/moderation.md index aa0564bd6..62bd8dd34 100644 --- a/app/assets/assets/help/fr/moderation.md +++ b/app/assets/assets/help/fr/moderation.md @@ -1,6 +1,6 @@ > Si vous croisez un troll, mettez lui un vote négatif et ne le nourissez pas. -Le système de signalement vous permet de signaler du contenu inaproprié. Vous êtes autorisé à effectuer +Le système de signalement vous permet de signaler du contenu inapproprié. Vous êtes autorisé à effectuer un petit nombre de signalements chaque jour et devez les utiliser judicieusement: en abuser peut vous faire perdre beaucoup de réputation. @@ -31,7 +31,7 @@ Pour chaque action signalée, les modérateurs ont 3 choix : ## Règles -Il faut le vote d'au mois 3 modérateurs avant de pouvoir prendre une décision. +Il faut le vote d'au moins 3 modérateurs avant de pouvoir prendre une décision. Un score est ensuite généré entre -1.0 et +1.0: ``` diff --git a/app/assets/assets/help/fr/privacy.md b/app/assets/assets/help/fr/privacy.md index 50ab19d08..3246b1112 100644 --- a/app/assets/assets/help/fr/privacy.md +++ b/app/assets/assets/help/fr/privacy.md @@ -18,7 +18,7 @@ Vous devez par contre considérer toutes vos intéractions (ajout d'un commentai comme **publiques**. Vous êtes libre d'utiliser votre vrai nom ou juste un bon vieux pseudonyme si c'est ce que vous préférez. -Bien que rien ne soit infaible, nous faisons attention à la sécurité et essayons de suivre les +Bien que rien ne soit infaillible, nous faisons attention à la sécurité et essayons de suivre les meilleurs pratiques pour garder vos données aussi protégées que possible. Pour en savoir plus sur la politique de vie privée de l'extension pour navigateur, diff --git a/app/components/App/Sidebar.jsx b/app/components/App/Sidebar.jsx index a77fbb10a..e32b41b88 100644 --- a/app/components/App/Sidebar.jsx +++ b/app/components/App/Sidebar.jsx @@ -8,6 +8,7 @@ import { Icon } from "../Utils" import { MOBILE_WIDTH_THRESHOLD, USER_PICTURE_SMALL } from '../../constants' import { MIN_REPUTATION_MODERATION } from "../../constants" import { LoadingFrame } from '../Utils/LoadingFrame' +import RawIcon from '../Utils/RawIcon' import ReputationGuard from '../Utils/ReputationGuard' import LanguageSelector from './LanguageSelector' import capitalize from 'voca/capitalize' @@ -41,7 +42,7 @@ export default class Sidebar extends React.PureComponent { const classes = classNames(className, {'link-with-icon': !!iconName}) return ( - {iconName && } + {iconName && } {children} ) @@ -61,9 +62,9 @@ export default class Sidebar extends React.PureComponent {
-

+ { username } -

+
@@ -77,10 +78,10 @@ export default class Sidebar extends React.PureComponent { @@ -114,7 +115,7 @@ export default class Sidebar extends React.PureComponent { className={`menu ${className} ${sidebarExpended ? 'expended' : ''}`}>
this.props.toggleSidebar()}> - +
@@ -153,7 +154,7 @@ export default class Sidebar extends React.PureComponent { { capitalize(t('entities.video_plural')) } - + { capitalize(t('entities.speaker_plural')) } @@ -166,6 +167,6 @@ export default class Sidebar extends React.PureComponent { } usernameFontSize() { - return `${1.5 - this.props.CurrentUser.username.length / 38}em` + return `${1.4 - this.props.CurrentUser.username.length / 40}em` } } diff --git a/app/components/App/index.jsx b/app/components/App/index.jsx index aeed21c62..6e7c18b2b 100644 --- a/app/components/App/index.jsx +++ b/app/components/App/index.jsx @@ -1,6 +1,7 @@ import React from "react" import { connect } from "react-redux" import { I18nextProvider } from 'react-i18next' +import { Helmet } from 'react-helmet' import i18n from '../../i18n/i18n' import { FlashMessages } from "../Utils" @@ -20,6 +21,9 @@ export default class App extends React.PureComponent { return (
+ + CaptainFact +
diff --git a/app/components/Comments/CommentDisplay.jsx b/app/components/Comments/CommentDisplay.jsx index 0c7f599cc..56c07042a 100644 --- a/app/components/Comments/CommentDisplay.jsx +++ b/app/components/Comments/CommentDisplay.jsx @@ -22,33 +22,25 @@ import MediaLayout from '../Utils/MediaLayout' import Vote from './Vote' -// TODO Use ReputationGuard to protect actions -// Add the following possibilities to reputationGuard : -// onUnauthorized = "hide" | "showMessage" | "flash" - @connect(({CurrentUser, VideoDebate}, props) => ({ currentUser: CurrentUser.data, myVote: VideoDebate.comments.voted.get(props.comment.id, 0), isVoting: VideoDebate.comments.voting.has(props.comment.id), replies: VideoDebate.comments.replies.get(props.comment.id), - isFlagged: VideoDebate.comments.myFlags.has(props.comment.id) // TODO Selector + isFlagged: VideoDebate.comments.myFlags.has(props.comment.id) }), {addModal, deleteComment, flagComment, commentVote, change, flashErrorUnauthenticated}) -@translate(['main', 'videoDebate']) +@translate('main') export class CommentDisplay extends React.PureComponent { constructor(props) { super(props) this.state = {isBlurred: false, showReplies: props.nesting !== 4} - this.showRepliesToggle = this.showRepliesToggle.bind(this) - - // Authenticated actions - this.actionReply = this.actionAuthenticated(this.actionReply.bind(this)) - this.handleFlag = this.actionAuthenticated(this.handleFlag.bind(this)) } render() { const { user, text, source, score, inserted_at, approve } = this.props.comment const { t, withoutActions, withoutHeader, replyingTo, nesting, replies, myVote, isVoting, hideThread, className, richMedias=true } = this.props const approveClass = approve !== null && (approve ? 'approve' : 'refute') + const showReplies = this.state.showReplies const isOwnComment = this.props.comment.user.id === this.props.currentUser.id return ( @@ -58,7 +50,9 @@ export class CommentDisplay extends React.PureComponent { ContainerType="article" left={!withoutActions && this.props.commentVote({comment: this.props.comment, value})}/> + onVote={value => this.ensureAuthenticated() && + this.props.commentVote({comment: this.props.comment, value}) + }/> } content={
@@ -83,11 +77,12 @@ export class CommentDisplay extends React.PureComponent {
@@ -127,22 +126,26 @@ export class CommentForm extends React.PureComponent { } renderSubmit(valid, sourceUrl, isReply) { - const commonClasses = ['button', {'is-disabled': !valid}] + const disabled = !valid const i18nParams = isReply ? {context: 'reply'} : null - if (!sourceUrl) return ([ - - ]) + ) else return ([ - , - , - diff --git a/app/components/Comments/CommentsContainer.jsx b/app/components/Comments/CommentsContainer.jsx index 5e36bd48c..f5ba358e3 100644 --- a/app/components/Comments/CommentsContainer.jsx +++ b/app/components/Comments/CommentsContainer.jsx @@ -5,7 +5,6 @@ import FlipMove from 'react-flip-move' import { CommentDisplay } from "./CommentDisplay" -// TODO optimize this all thing @translate('videoDebate') export class CommentsContainer extends React.PureComponent { constructor(props) { @@ -13,17 +12,9 @@ export class CommentsContainer extends React.PureComponent { this.state = {nbComments: CommentsContainer.getNbDisplayedRange(props.nesting || 1)} } - static getNbDisplayedRange(nesting) { - if (nesting > 3) - return [3, 5] - return [4 - nesting, 6 - nesting] - } - render() { const {t, comments, className, header, replyingTo, nesting=1} = this.props - // TODO Optimize: This re-update the list each time! - // TODO Optimize: Use a selector ! let numComment = 0 const displayedComments = comments.takeWhile(c => ++numComment <= this.state.nbComments[0] || @@ -55,6 +46,12 @@ export class CommentsContainer extends React.PureComponent {
) } + + static getNbDisplayedRange(nesting) { + if (nesting > 3) + return [3, 5] + return [4 - nesting, 6 - nesting] + } } CommentsContainer.defaultProps = { diff --git a/app/components/Comments/FlagForm.jsx b/app/components/Comments/FlagForm.jsx index be54dc9a8..5b971d9ea 100644 --- a/app/components/Comments/FlagForm.jsx +++ b/app/components/Comments/FlagForm.jsx @@ -26,7 +26,7 @@ export default class FlagForm extends React.PureComponent { render() { const { handleSubmit } = this.props - // TODO see https://stackoverflow.com/help/privileges/flag-posts + return (
diff --git a/app/components/Comments/ModalFlag.jsx b/app/components/Comments/ModalFlag.jsx index 14e683d0b..5478f8dd2 100644 --- a/app/components/Comments/ModalFlag.jsx +++ b/app/components/Comments/ModalFlag.jsx @@ -11,12 +11,9 @@ import HttpApi from '../../API/http_api' const flagFormValueSelector = formValueSelector('flagForm') @connect(state => ({selectedReason: flagFormValueSelector(state, 'reason')})) -@translate(['videoDebate', 'main']) +@translate('videoDebate') export default class ModalFlag extends React.PureComponent { - constructor(props) { - super(props) - this.state = {isLoading: true, flagsAvailable: 0, error: null} - } + state = {isLoading: true, flagsAvailable: 0, error: null} componentDidMount() { HttpApi.get('users/me/available_flags') @@ -29,7 +26,6 @@ export default class ModalFlag extends React.PureComponent { render() { const { isLoading, flagsAvailable } = this.state const { t, handleAbort, selectedReason, comment, ...otherProps } = this.props - const availableStr = t('flagForm.xAvailable', {count: flagsAvailable}) return ( ) } + + renderConfirmText(t, flagsAvailable) { + return ( + + {t('main:actions.flag')} ({t('flagForm.xAvailable', {count: flagsAvailable})}) + + ) + } } diff --git a/app/components/Comments/Source.jsx b/app/components/Comments/Source.jsx index 7f8eef68c..3fe12f35b 100644 --- a/app/components/Comments/Source.jsx +++ b/app/components/Comments/Source.jsx @@ -25,14 +25,20 @@ const isPlayer = url => { return false } +const PLAYER_CONFIG = {youtube: {playerVars: { showinfo: 1 }}} + export const Source = ({ source: { url, title, site_name }, withoutPlayer }) => { if (!withoutPlayer && isPlayer(url)) { - return + return } else { return (
- + {upperCase(site_name) || getDisplayableHostname(url)} diff --git a/app/components/Comments/Vote.jsx b/app/components/Comments/Vote.jsx index 11be7adac..290a7db6e 100644 --- a/app/components/Comments/Vote.jsx +++ b/app/components/Comments/Vote.jsx @@ -1,21 +1,23 @@ import React from 'react' import classNames from 'classnames' -import { Icon } from '../Utils' +import ClickableIcon from '../Utils/ClickableIcon' const Vote = ({isVoting, score, myVote, onVote}) => (
- 0 })} - onClick={() => myVote <= 0 ? onVote(1) : onVote(0)}/> + onClick={() => myVote <= 0 ? onVote(1) : onVote(0)} + />
{isVoting ? : score}
- myVote >= 0 ? onVote(-1) : onVote(0)}/> + onClick={() => myVote >= 0 ? onVote(-1) : onVote(0)} + />
) diff --git a/app/components/FormUtils/index.jsx b/app/components/FormUtils/index.jsx index f2c9b7d2e..0c58155c6 100644 --- a/app/components/FormUtils/index.jsx +++ b/app/components/FormUtils/index.jsx @@ -71,18 +71,20 @@ export const renderFieldWithLabel = (params) => ( export const FieldWithButton = (params) => { const { submitting, invalid } = params.meta || {} - const { buttonClassName, buttonLabel, buttonClickHandler, ...inputProps } = params + const { buttonClassName, buttonLabel, buttonClickHandler, expandInput, ...inputProps } = params return ( -
-

+

+
{ renderInput(inputProps) } +
+
-

+
) } diff --git a/app/components/Help/Help.jsx b/app/components/Help/Help.jsx index d6a2324c6..68b8398c7 100644 --- a/app/components/Help/Help.jsx +++ b/app/components/Help/Help.jsx @@ -7,7 +7,7 @@ import HelpPageContent from './HelpPageContent' import PublicAchievementUnlocker from '../Users/PublicAchievementUnlocker' -@translate(['help', 'main']) +@translate('help') @withRouter export default class Help extends React.PureComponent { render() { diff --git a/app/components/Modal/Modal.jsx b/app/components/Modal/Modal.jsx index 2a1c7ff21..c5afa9411 100644 --- a/app/components/Modal/Modal.jsx +++ b/app/components/Modal/Modal.jsx @@ -21,7 +21,7 @@ const Modal = ({
{helpLink && - + }
- +
) } diff --git a/app/components/Modal/ModalFormContainer.jsx b/app/components/Modal/ModalFormContainer.jsx index e3fc61954..ed63ed076 100644 --- a/app/components/Modal/ModalFormContainer.jsx +++ b/app/components/Modal/ModalFormContainer.jsx @@ -43,21 +43,22 @@ export class ModalFormContainer extends React.PureComponent { const isSubmitting = this.props.isSubmitting || this.state.isSubmitting return ( )} diff --git a/app/components/Moderation/ModerationEntry.jsx b/app/components/Moderation/ModerationEntry.jsx index b6e366ee9..820e3b299 100644 --- a/app/components/Moderation/ModerationEntry.jsx +++ b/app/components/Moderation/ModerationEntry.jsx @@ -17,7 +17,7 @@ import format from 'date-fns/format' import { locales } from '../../i18n/i18n' @connect(state => ({ locale: state.UserPreferences.locale })) -@translate('moderation', 'main') +@translate('moderation') export default class ModerationEntry extends React.PureComponent { render() { const { entry, locale, t, onAction } = this.props diff --git a/app/components/Pages/BrowserExtensionsPage.jsx b/app/components/Pages/BrowserExtensionsPage.jsx index 73ef16baa..3995cb925 100644 --- a/app/components/Pages/BrowserExtensionsPage.jsx +++ b/app/components/Pages/BrowserExtensionsPage.jsx @@ -44,11 +44,11 @@ export const BrowserExtensionsPage = translate('extension')(({t}) => ( const BrowserExtension = ({browser, image, buttonLabel, url, onClick, disabled=false}) => (
- +
{browser}/
- + {buttonLabel} diff --git a/app/components/Pages/Home.jsx b/app/components/Pages/Home.jsx index 4dc48f8d6..a7fe34eb2 100644 --- a/app/components/Pages/Home.jsx +++ b/app/components/Pages/Home.jsx @@ -10,7 +10,7 @@ import Logo from '../App/Logo' @connect(state => ({authenticated: isAuthenticated(state)})) -@translate(['home', 'main']) +@translate('home') export class Home extends React.PureComponent { render() { return ( @@ -19,36 +19,27 @@ export class Home extends React.PureComponent {
-

+

Let's check the Internet

-
-
-

- {this.props.t('presentation1')} -

- CaptainFact {this.props.t('presentation2')} -

-
-

-
-

- {this.props.t('main:entities.video_plural')} - }/> - {this.props.authenticated ? '.' : - {this.props.t('creatingAnAccount')} - }/> - } -
-
-

-
+
+

+ {this.props.t('presentation1')} +

+ CaptainFact {this.props.t('presentation2')} +

+
+

+ {this.props.t('main:entities.video_plural')} + }/> +
+
+

@@ -62,14 +53,12 @@ export class Home extends React.PureComponent {
diff --git a/app/components/Speakers/SpeakerPage.jsx b/app/components/Speakers/SpeakerPage.jsx index da632fb48..9cc06e381 100644 --- a/app/components/Speakers/SpeakerPage.jsx +++ b/app/components/Speakers/SpeakerPage.jsx @@ -1,5 +1,6 @@ import React from "react" import { connect } from "react-redux" +import Helmet from 'react-helmet' import { SpeakerPreview } from './SpeakerPreview' import { fetchSpeaker, fetchWikiDataInfo } from '../../state/speakers/effects' @@ -54,6 +55,9 @@ export class SpeakerPage extends React.PureComponent { return return (
+ + {this.props.speaker.full_name} +
@@ -90,7 +94,7 @@ export class SpeakerPage extends React.PureComponent { return null return ( - {siteName} + {siteName} ) } diff --git a/app/components/Speakers/SpeakerPreview.jsx b/app/components/Speakers/SpeakerPreview.jsx index e705110a5..d5885a184 100644 --- a/app/components/Speakers/SpeakerPreview.jsx +++ b/app/components/Speakers/SpeakerPreview.jsx @@ -11,7 +11,8 @@ import { import { isAuthenticated } from "../../state/users/current_user/selectors" import { staticResource } from "../../API" import { ModalFormContainer } from "../Modal" -import { Icon, LinkWithIcon } from "../Utils" +import Icon from "../Utils/Icon" +import ClickableIcon from '../Utils/ClickableIcon' import ReputationGuard from '../Utils/ReputationGuard' import { EditSpeakerForm } from "./SpeakerForm" import ModalRemoveSpeaker from './ModalRemoveSpeaker' @@ -23,73 +24,12 @@ import {getFocusedStatementSpeakerId} from '../../state/video_debate/statements/ @withRouter -@translate(['videoDebate', 'main']) +@translate('videoDebate') @connect((state, props) => ( {isAuthenticated: isAuthenticated(state), isFocused: getFocusedStatementSpeakerId(state) === props.speaker.id}), {addModal, changeStatementFormSpeaker, removeSpeaker, updateSpeaker} ) export class SpeakerPreview extends React.PureComponent { - handleRemove() { - this.props.addModal({ - Modal: ModalRemoveSpeaker, - props: { - speaker: this.props.speaker, - handleConfirm: () => this.props.removeSpeaker(this.props.speaker) - } - }) - } - - handleEdit() { - this.props.addModal({ - Modal: ModalFormContainer, - props: { - title: `Edit ${this.props.speaker.full_name} information`, - FormComponent: EditSpeakerForm, - handleConfirm: (s) => this.props.updateSpeaker(s), - formProps: {initialValues: this.props.speaker.toJS()} - } - }) - } - - handleAddStatement() { - const historyRegex = new RegExp("/history/?$") - const currentPath = this.props.location.pathname - if (currentPath.match(historyRegex)) - this.props.router.push(currentPath.replace(historyRegex, "")) - this.props.changeStatementFormSpeaker({id: this.props.speaker.id}) - } - - renderSpeakerThumb(speaker) { - if (speaker.picture) - return () - return () - } - - getTitle() { - const { title, is_user_defined, country } = this.props.speaker - // Only translate if title exists and is not user defined - if (!title) - return '...' - else if (is_user_defined) - return title - - let i18nTitle = '' - if (this.props.i18n.language === 'en') // No need to translate title for english - i18nTitle = title - else { - // If unknown title, return raw title - const i18nTitleKey = `speaker.titles.${title}` - i18nTitle = this.props.t(i18nTitleKey) - if (i18nTitle === i18nTitleKey) - return title - } - // Try to return title + nationality, otherwise fallback on translated title - return this.props.t('speaker.titleFormat', { - title: i18nTitle, - context: country - }) - } - render() { const { speaker, isAuthenticated, withoutActions , className} = this.props @@ -100,7 +40,7 @@ export class SpeakerPreview extends React.PureComponent { content={
{this.renderName(speaker)} -

{this.getTitle()}

+

{this.getTitle()}

} right={isAuthenticated && !withoutActions && this.renderActions()} @@ -108,25 +48,34 @@ export class SpeakerPreview extends React.PureComponent { ) } + renderSpeakerThumb(speaker) { + if (speaker.picture) + return + return + } + renderActions() { return (
{this.props.speaker.is_user_defined && - this.handleEdit()}/> + this.handleEdit()}/> } - this.handleRemove()}/> + this.handleRemove()}/> - this.handleAddStatement()}/> + this.handleAddStatement()}/>
) @@ -134,11 +83,66 @@ export class SpeakerPreview extends React.PureComponent { renderName(speaker) { if (speaker.is_user_defined) - return
{speaker.full_name}
+ return
{speaker.full_name}
return ( - + {speaker.full_name} ) } + + getTitle() { + const { title, is_user_defined, country } = this.props.speaker + // Only translate if title exists and is not user defined + if (!title) + return '...' + else if (is_user_defined) + return title + + let i18nTitle = '' + if (this.props.i18n.language === 'en') // No need to translate title for english + i18nTitle = title + else { + // If unknown title, return raw title + const i18nTitleKey = `speaker.titles.${title}` + i18nTitle = this.props.t(i18nTitleKey) + if (i18nTitle === i18nTitleKey) + return title + } + // Try to return title + nationality, otherwise fallback on translated title + return this.props.t('speaker.titleFormat', { + title: i18nTitle, + context: country + }) + } + + handleRemove() { + this.props.addModal({ + Modal: ModalRemoveSpeaker, + props: { + speaker: this.props.speaker, + handleConfirm: () => this.props.removeSpeaker(this.props.speaker) + } + }) + } + + handleEdit() { + this.props.addModal({ + Modal: ModalFormContainer, + props: { + title: `Edit ${this.props.speaker.full_name} information`, + FormComponent: EditSpeakerForm, + handleConfirm: (s) => this.props.updateSpeaker(s), + formProps: {initialValues: this.props.speaker.toJS()} + } + }) + } + + handleAddStatement() { + const historyRegex = new RegExp("/history/?$") + const currentPath = this.props.location.pathname + if (currentPath.match(historyRegex)) + this.props.router.push(currentPath.replace(historyRegex, "")) + this.props.changeStatementFormSpeaker({id: this.props.speaker.id}) + } } diff --git a/app/components/Statements/Statement.jsx b/app/components/Statements/Statement.jsx index 7c552f1e4..bcaec44cc 100644 --- a/app/components/Statements/Statement.jsx +++ b/app/components/Statements/Statement.jsx @@ -3,7 +3,7 @@ import { connect } from "react-redux" import { translate } from 'react-i18next' import { staticResource } from "../../API" -import { LoadingFrame, LinkWithIcon } from "../Utils" +import ClickableIcon from '../Utils/ClickableIcon' import ReputationGuard from '../Utils/ReputationGuard' import TimeDisplay from '../Utils/TimeDisplay' import { StatementForm } from "./StatementForm" @@ -33,7 +33,6 @@ import { setScrollTo } from '../../state/video_debate/statements/reducer' refutingFacts : commentsSelectors.getStatementRefutingFacts(state, props), approveScore: statementSelectors.getStatementApproveScore(state, props), refuteScore: statementSelectors.getStatementRefuteScore(state, props), - commentsLoading: commentsSelectors.areCommentsLoading(state), isFocused: statementSelectors.isStatementFocused(state, props), currentUser: state.CurrentUser.data, scrollTo: state.VideoDebate.statements.scrollTo, @@ -42,11 +41,7 @@ import { setScrollTo } from '../../state/video_debate/statements/reducer' }), {addModal, updateStatement, deleteStatement, forcePosition, setScrollTo}) @translate('videoDebate') export class Statement extends React.PureComponent { - constructor(props) { - super(props) - this.state = { isDeleting: false, isEditing: false } - this.showHistory = this.showHistory.bind(this) - } + state = { isDeleting: false, isEditing: false } componentDidUpdate(prevProps) { if (this.shouldScroll(this.props, prevProps)) @@ -78,16 +73,6 @@ export class Statement extends React.PureComponent { ) } - showHistory() { - this.props.addModal({ - Modal: ModalHistory, - props: { - entity: ENTITY_STATEMENT, - entityId: this.props.statement.id - } - }) - } - renderCardHeaderAndContent(speaker, statement) { const {t, forcePosition, setScrollTo, addModal} = this.props @@ -122,20 +107,28 @@ export class Statement extends React.PureComponent {

- + this.showHistory() }/> - this.setState({isEditing: true})}/> + this.setState({isEditing: true})}/> - addModal({ - Modal: ShareModal, - props: {path: `${location.pathname}?statement=${statement.id}`} - })}/> + addModal({ + Modal: ShareModal, + props: {path: `${location.pathname}?statement=${statement.id}`} + })}/> - this.setState({isDeleting: true})}/> + this.setState({isDeleting: true})}/>
@@ -147,18 +140,21 @@ export class Statement extends React.PureComponent { } renderCommentsContainerHeader(label, tagType, score) { - return {this.props.t(label)} { score } + return ( +
+ {this.props.t(label)} + { score } +
+ ) } renderFactsAndComments() { - if (this.props.commentsLoading) - return () const { statement, comments, approvingFacts, refutingFacts } = this.props return (
{(approvingFacts.size > 0 || refutingFacts.size > 0) && -
+
{refutingFacts.size > 0 && {comments.size > 0 && } - {/* TODO This can be optimized as initialValues will always change upon rendering */} - +
) } + showHistory() { + this.props.addModal({ + Modal: ModalHistory, + props: { + entity: ENTITY_STATEMENT, + entityId: this.props.statement.id + } + }) + } + // ---- Autoscroll ---- shouldScroll = (props, prevProps) => { diff --git a/app/components/Statements/StatementForm.jsx b/app/components/Statements/StatementForm.jsx index 77a14bc0c..be2c5aae2 100644 --- a/app/components/Statements/StatementForm.jsx +++ b/app/components/Statements/StatementForm.jsx @@ -14,13 +14,13 @@ import { decrementFormCount, incrementFormCount, setScrollTo, STATEMENT_FORM_NAM import { handleFormEffectResponse } from '../../lib/handle_effect_response' -@translate(['videoDebate', 'main']) -@reduxForm({form: STATEMENT_FORM_NAME}) @connect(({VideoDebate: {video, statements}}) => ({ position: video.playback.position, speakers: video.data.speakers, submitting: statements.isSubmitting }), {forcePosition, setScrollTo, incrementFormCount, decrementFormCount}) +@reduxForm({form: STATEMENT_FORM_NAME}) +@translate('videoDebate') export class StatementForm extends React.PureComponent { constructor(props) { super(props) @@ -102,16 +102,15 @@ export class StatementForm extends React.PureComponent {
{t('main:actions.save')} {t('main:actions.cancel')} diff --git a/app/components/Statements/StatementsList.jsx b/app/components/Statements/StatementsList.jsx index 219a93e55..b65b1489a 100644 --- a/app/components/Statements/StatementsList.jsx +++ b/app/components/Statements/StatementsList.jsx @@ -2,6 +2,7 @@ import React from 'react' import { connect } from "react-redux" import { translate } from 'react-i18next' import { withRouter } from 'react-router' +import FlipMove from 'react-flip-move' import { StatementForm } from './StatementForm' import { Statement } from './Statement' @@ -38,7 +39,11 @@ export default class StatementsList extends React.PureComponent { e => {if (!e.error) this.props.closeStatementForm(); return e;} )}/> } - {statements.map(statement => )} + + {statements.map(statement => + + )} +
) } diff --git a/app/components/Users/DeleteUserModal.jsx b/app/components/Users/DeleteUserModal.jsx index 25ef5a019..df5a7c73b 100644 --- a/app/components/Users/DeleteUserModal.jsx +++ b/app/components/Users/DeleteUserModal.jsx @@ -16,7 +16,7 @@ class DeleteForm extends React.PureComponent {

- This action is irreversible. + This action is irreversible


Deleting your account will...

@@ -28,7 +28,7 @@ class DeleteForm extends React.PureComponent {
  • Anonymize your actions history

  • -
    Type your username below to confirm the deletion :
    +

    Type your username below to confirm the deletion :

    ) @@ -41,7 +41,7 @@ const valueSelector = formValueSelector(DELETE_FORM) @connect(state => ({ isValid: valueSelector(state, 'usernameConfirm') === state.CurrentUser.data.username })) -@translate(['main', 'user']) +@translate('main') export default class DeleteUserModal extends React.PureComponent { render() { const { t, isValid, ...otherProps } = this.props diff --git a/app/components/Users/EditUserForm.jsx b/app/components/Users/EditUserForm.jsx index 61f986118..3830e2c0f 100644 --- a/app/components/Users/EditUserForm.jsx +++ b/app/components/Users/EditUserForm.jsx @@ -18,7 +18,7 @@ import { handleFormEffectResponse } from '../../lib/handle_effect_response' }), {updateInfo}) @reduxForm({ form: 'editUserForm', validate: validatePasswordRepeat }) @withRouter -@translate(['user', 'main']) +@translate('user') export default class EditUserForm extends React.PureComponent { componentDidUpdate() { // Redirect to user profile when logged in diff --git a/app/components/Users/InvitationRequestForm.jsx b/app/components/Users/InvitationRequestForm.jsx index 42a30bc6c..1e8f76eea 100644 --- a/app/components/Users/InvitationRequestForm.jsx +++ b/app/components/Users/InvitationRequestForm.jsx @@ -20,13 +20,10 @@ const validate = ({email}) => { } @reduxForm({form: 'newsletterSubscribeForm', validate}) -@translate(['home', 'user']) +@translate('home') @connect(null, {addFlash, errorToFlash, requestInvitation}) export default class InvitationRequestForm extends React.PureComponent { - constructor(props) { - super(props) - this.state = {confirmed: false} - } + state = { confirmed: false } submit(user) { return this.props.requestInvitation(user) @@ -41,9 +38,12 @@ export default class InvitationRequestForm extends React.PureComponent { getContent() { if (!this.state.confirmed) - return + buttonLabel={this.props.t('main:actions.send')}/> else return ( diff --git a/app/components/Users/LoginForm.jsx b/app/components/Users/LoginForm.jsx index 31ab532c4..20888d02b 100644 --- a/app/components/Users/LoginForm.jsx +++ b/app/components/Users/LoginForm.jsx @@ -13,7 +13,7 @@ import { tError } from '../../lib/errors' @reduxForm({form: 'loginForm'}) @connect(({CurrentUser: {data, error}}) => ({CurrentUser: data, error}), {login}) @withRouter -@translate(['user', 'main', 'errors']) +@translate('user') export default class LoginForm extends React.PureComponent { componentWillReceiveProps(props) { // Redirect when logged in diff --git a/app/components/Users/ScoreTag.jsx b/app/components/Users/ScoreTag.jsx index 25ebf1670..e49756bf9 100644 --- a/app/components/Users/ScoreTag.jsx +++ b/app/components/Users/ScoreTag.jsx @@ -13,7 +13,7 @@ function getTagType(reputation) { } const ScoreTag = ({reputation, size="small", withIcon=false}) => - + { withIcon && } { reputation } diff --git a/app/components/Users/ThirdPartyAuthList.jsx b/app/components/Users/ThirdPartyAuthList.jsx index ab8a3249e..35b34d938 100644 --- a/app/components/Users/ThirdPartyAuthList.jsx +++ b/app/components/Users/ThirdPartyAuthList.jsx @@ -11,9 +11,9 @@ const ThirdPartyAuthList = ({t, location: {query: {invitation_token}}}) => (

    {t('actionWithThirdParty')}

    - - - + + +
    ) diff --git a/app/components/Users/ThirdPartyServiceButton.jsx b/app/components/Users/ThirdPartyServiceButton.jsx index f43468d14..02536856b 100644 --- a/app/components/Users/ThirdPartyServiceButton.jsx +++ b/app/components/Users/ThirdPartyServiceButton.jsx @@ -2,12 +2,14 @@ import React from "react" import classNames from 'classnames' import { Icon } from "../Utils" +import RawIcon from '../Utils/RawIcon' -const ThirdPartyServiceButton = ({url, icon, className, newTab=false}) => ( +const ThirdPartyServiceButton = ({url, icon, className, newTab=false, ...props}) => ( - + className={classNames("icon is-large third-party-service-button", className)} + {...props}> + ) diff --git a/app/components/Users/User.jsx b/app/components/Users/User.jsx index 751524e35..8b4564c26 100644 --- a/app/components/Users/User.jsx +++ b/app/components/Users/User.jsx @@ -20,7 +20,7 @@ import { resetUser } from '../../state/users/displayed_user/reducer' isLoading, errors, user: data }), {fetchUser, resetUser}) -@translate(['main', 'user']) +@translate('main') export default class User extends React.PureComponent { componentDidMount() { this.props.fetchUser(this.props.params.username) @@ -30,8 +30,7 @@ export default class User extends React.PureComponent { // If user's username was updated if (this.props.user.id === oldProps.user.id && this.props.user.username !== oldProps.user.username) - // Remove old user profile from history - // TODO + // TODO Remove old user profile from history // Redirect this.props.router.replace(`/u/${this.props.user.username}`) @@ -52,7 +51,7 @@ export default class User extends React.PureComponent { return (
  • - + {this.props.t(menuTKey)} diff --git a/app/components/Users/UserFormFields.jsx b/app/components/Users/UserFormFields.jsx index ce579730a..3c4373c02 100644 --- a/app/components/Users/UserFormFields.jsx +++ b/app/components/Users/UserFormFields.jsx @@ -40,9 +40,8 @@ export const passwordRepeatField = (t) => export const submitButton = (text, valid) =>

    -

    diff --git a/app/components/Users/UserProfile.jsx b/app/components/Users/UserProfile.jsx index 005fa02f3..ba8c25fd1 100644 --- a/app/components/Users/UserProfile.jsx +++ b/app/components/Users/UserProfile.jsx @@ -15,14 +15,6 @@ class UserProfile extends PureComponent { const {user: {achievements}, t} = this.props return (
    - {/*
    */} - {/*

    */} - {/* Statistics*/} - {/*

    */} - {/*

    Posted x comments

    */} - {/*

    Posted x sourced comments

    */} - {/*TODO: Top comments*/} - {/*
    */}

    {t('title')} diff --git a/app/components/Users/UserSettings.jsx b/app/components/Users/UserSettings.jsx index 37c345478..7ad80ea90 100644 --- a/app/components/Users/UserSettings.jsx +++ b/app/components/Users/UserSettings.jsx @@ -15,9 +15,15 @@ import { LoadingFrame } from '../Utils/LoadingFrame' class ThirdPartyAccountLinker extends React.PureComponent { render() { return ( -

    - - {this.props.isLinked ? this.renderUnlinkAccount() : this.renderLinkAccount()} +

    +

    +
    + {this.props.title} +
    +
    +
    + {this.props.isLinked ? this.renderUnlinkAccount() : this.renderLinkAccount()} +

    ) } @@ -55,7 +61,9 @@ export default class UserSettings extends React.PureComponent {

    {this.props.t('linkedAccounts')}

    -
    diff --git a/app/components/UsersActions/ActionsTable.jsx b/app/components/UsersActions/ActionsTable.jsx index 5876b79b1..dddb10a62 100644 --- a/app/components/UsersActions/ActionsTable.jsx +++ b/app/components/UsersActions/ActionsTable.jsx @@ -17,7 +17,7 @@ import { ACTION_DELETE, ACTION_REMOVE, MIN_REPUTATION_RESTORE_ENTITY } from '../ import { LoadingFrame } from '../Utils/LoadingFrame' -@translate(['history', 'main']) +@translate('history') @connect( (state, props) => ({ lastActionsIds: state.UsersActions.lastActionsIds, @@ -54,7 +54,7 @@ class ActionsTable extends React.PureComponent { {showEntity && {t('entity')}} {this.renderCompareAllButton(isMostlyComparing)} {canRestore && {t('revert')}} - {t('moderation')} + {/*{t('moderation')}*/} ) } @@ -115,16 +115,16 @@ class ActionsTable extends React.PureComponent { } } - - - - {t('main:actions.approve')} -    - - - {t('main:actions.flag')} - - + {/**/} + {/**/} + {/**/} + {/*{t('main:actions.approve')}*/} + {/*  */} + {/**/} + {/**/} + {/*{t('main:actions.flag')}*/} + {/**/} + {/**/} ) } diff --git a/app/components/UsersActions/Entity.jsx b/app/components/UsersActions/Entity.jsx index 6cb204fc1..1ac069115 100644 --- a/app/components/UsersActions/Entity.jsx +++ b/app/components/UsersActions/Entity.jsx @@ -16,7 +16,7 @@ import { SpeakerPreview } from '../Speakers/SpeakerPreview' reference: state.UsersActions.referenceEntities.get(props.entityKey), speakers: state.VideoDebate.video.data.speakers }), {forcePosition}) -@translate(['main', 'videoDebate']) +@translate('main') export default class Entity extends React.PureComponent { render() { return ( diff --git a/app/components/UsersActions/History.jsx b/app/components/UsersActions/History.jsx index cd8e7cb31..ea0e5adbe 100644 --- a/app/components/UsersActions/History.jsx +++ b/app/components/UsersActions/History.jsx @@ -10,7 +10,7 @@ import { UserAction } from './UserAction' /** * Display a list of `UserAction` record as an history */ -@translate(['main', 'videoDebate']) +@translate('main') export class History extends React.PureComponent { constructor(props) { super(props) diff --git a/app/components/UsersActions/UserAction.jsx b/app/components/UsersActions/UserAction.jsx index 6d047fdbd..05e7b43eb 100644 --- a/app/components/UsersActions/UserAction.jsx +++ b/app/components/UsersActions/UserAction.jsx @@ -9,7 +9,7 @@ import ActionDiff from './ActionDiff' import ActionIcon from './ActionIcon' -@translate(['history', 'main']) +@translate('history') export class UserAction extends React.PureComponent { render() { const { action, className, t } = this.props diff --git a/app/components/Utils/ClickableIcon.jsx b/app/components/Utils/ClickableIcon.jsx new file mode 100644 index 000000000..f2cd3d261 --- /dev/null +++ b/app/components/Utils/ClickableIcon.jsx @@ -0,0 +1,16 @@ +import React from "react" +import classNames from 'classnames' +import RawIcon from './RawIcon' + + +export const ClickableIcon = ({name, size, className, ...otherProps}) => { + const sizeClass = size && `is-${size}` + + return ( + + + + ) +} + +export default ClickableIcon \ No newline at end of file diff --git a/app/components/Utils/ErrorView.jsx b/app/components/Utils/ErrorView.jsx index 59beea419..2efbebb7e 100644 --- a/app/components/Utils/ErrorView.jsx +++ b/app/components/Utils/ErrorView.jsx @@ -11,7 +11,7 @@ const refreshableErrors = ['join_crashed'] @withRouter -@translate(['errors', 'main']) +@translate('errors') export class ErrorView extends React.PureComponent { getMoreInfo() { const errorInfo = getErrorInfo(this.props.error) @@ -37,15 +37,17 @@ export class ErrorView extends React.PureComponent {

    {tError(t, error)}{this.getMoreInfo()}

    {(canGoBack || canReload) &&
    } - {canGoBack && this.props.router.goBack()}> - {t('main:actions.goBack')} - } - {canReload && location.reload()} - style={{float: 'right'}}> - {t('main:actions.reload')} - } + {canGoBack && + this.props.router.goBack()}> + {t('main:actions.goBack')} + } + {canReload && + location.reload()} + style={{float: 'right'}}> + {t('main:actions.reload')} + }

    diff --git a/app/components/Utils/FlashMessages.jsx b/app/components/Utils/FlashMessages.jsx index 210e45588..9b4c9bc03 100644 --- a/app/components/Utils/FlashMessages.jsx +++ b/app/components/Utils/FlashMessages.jsx @@ -65,7 +65,7 @@ export class FlashMessages extends React.PureComponent { } -@translate(['main', 'errors', 'achievements']) +@translate('main') @connect(null, {popModal, removeFlash}) class FlashContent extends React.Component { shouldComponentUpdate(nextProps) { diff --git a/app/components/Utils/Icon.jsx b/app/components/Utils/Icon.jsx index 02cddc7e7..869f20257 100644 --- a/app/components/Utils/Icon.jsx +++ b/app/components/Utils/Icon.jsx @@ -1,19 +1,15 @@ import React from "react" import classNames from 'classnames' +import RawIcon from './RawIcon' -export const Icon = ({name, size, withContainer=true, className, isClickable, ...otherProps}) => { +export const Icon = ({name, size, className, ...otherProps}) => { const sizeClass = size && `is-${size}` - const icon = - if (!withContainer) - return icon - - const Container = isClickable ? 'a' : 'span' return ( - - {icon} - + + + ) } diff --git a/app/components/Utils/LinkWithIcon.jsx b/app/components/Utils/LinkWithIcon.jsx index 1aa0d860e..c49f226f6 100644 --- a/app/components/Utils/LinkWithIcon.jsx +++ b/app/components/Utils/LinkWithIcon.jsx @@ -3,11 +3,12 @@ import { Link } from 'react-router' import classNames from 'classnames' import { Icon } from './Icon' +import RawIcon from './RawIcon' export const LinkWithIcon = ({to, iconName, children, className, ...props}) => ( - + {children} ) diff --git a/app/components/Utils/LoadingFrame.jsx b/app/components/Utils/LoadingFrame.jsx index 3fad62083..03682ba7b 100644 --- a/app/components/Utils/LoadingFrame.jsx +++ b/app/components/Utils/LoadingFrame.jsx @@ -1,23 +1,19 @@ import React from "react" import { translate } from 'react-i18next' -import capitalize from 'voca/capitalize' import classNames from 'classnames' @translate('main') export class LoadingFrame extends React.PureComponent { - defaultTitle() { - // If translation is not available, we'll show "Loading..." - return capitalize(this.props.t('actions.loading').replace('actions.', '')) - } - render() { const {title, size="large"} = this.props return (
    -

    {title || this.defaultTitle()}...

    +
    + {title || this.props.t('actions.loading')}... +
    ) } diff --git a/app/components/Utils/RawIcon.jsx b/app/components/Utils/RawIcon.jsx new file mode 100644 index 000000000..dbcfcc7f9 --- /dev/null +++ b/app/components/Utils/RawIcon.jsx @@ -0,0 +1,7 @@ +import React from 'react' + + +export const RawIcon = ({name}) => + + +export default RawIcon \ No newline at end of file diff --git a/app/components/Utils/ShareModal.jsx b/app/components/Utils/ShareModal.jsx index 095c42455..79b139e10 100644 --- a/app/components/Utils/ShareModal.jsx +++ b/app/components/Utils/ShareModal.jsx @@ -27,12 +27,11 @@ export default class ShareModal extends React.PureComponent { buttonClassName="is-medium" buttonLabel={} buttonClickHandler={this.copyUrlToClipboard.bind(this)} - /> + expandInput/>
    - diff --git a/app/components/Utils/TimeSince.jsx b/app/components/Utils/TimeSince.jsx index b97db2be3..8a14e53c1 100644 --- a/app/components/Utils/TimeSince.jsx +++ b/app/components/Utils/TimeSince.jsx @@ -19,6 +19,21 @@ export class TimeSince extends React.PureComponent { this.timeoutUpdate() } + componentWillUnmount() { + this.clearTimeout() + } + + render() { + const { time, locale, dispatch, addSuffix=true, isDateTime=true, ...props } = this.props + const localeObj = locales[locale] + const dateFormat = isDateTime ? localeObj.defaultDateTimeFormat : localeObj.defaultDateFormat + return ( + + { distanceInWordsToNow(time, {addSuffix: addSuffix, locale: localeObj}) } + + ) + } + timeoutUpdate() { const secondsSince = !this.props.time ? 0 : differenceInSeconds(Date.now(), this.props.time) const minutesSince = Math.trunc(secondsSince / 60) @@ -35,10 +50,6 @@ export class TimeSince extends React.PureComponent { this.timeout = setTimeout(this.timeoutUpdate, (60 - (minutesSince % 60)) * 60 * 1000) } - componentWillUnmount() { - this.clearTimeout() - } - clearTimeout() { if (this.timeout) clearTimeout(this.timeout) @@ -48,15 +59,4 @@ export class TimeSince extends React.PureComponent { static getMinutesSince(time) { return !time ? 0 : Math.trunc(differenceInSeconds(Date.now(), time) / 60) } - - render() { - const { time, locale, dispatch, addSuffix=true, isDateTime=true, ...props } = this.props - const localeObj = locales[locale] - const dateFormat = isDateTime ? localeObj.defaultDateTimeFormat : localeObj.defaultDateFormat - return ( - - { distanceInWordsToNow(time, {addSuffix: addSuffix, locale: localeObj}) } - - ) - } } \ No newline at end of file diff --git a/app/components/Utils/__tests__/Icon.spec.jsx b/app/components/Utils/__tests__/Icon.spec.jsx index 57e6107e8..3c81d448d 100644 --- a/app/components/Utils/__tests__/Icon.spec.jsx +++ b/app/components/Utils/__tests__/Icon.spec.jsx @@ -8,14 +8,6 @@ test("set size", () => { snapshot() }) -test("render without container", () => { - snapshot() -}) - -test("use 'a' if is link", () => { - snapshot() -}) - test("other props get passed to container", () => { snapshot() }) \ No newline at end of file diff --git a/app/components/Utils/__tests__/RawIcon.spec.jsx b/app/components/Utils/__tests__/RawIcon.spec.jsx new file mode 100644 index 000000000..a4816ea75 --- /dev/null +++ b/app/components/Utils/__tests__/RawIcon.spec.jsx @@ -0,0 +1,5 @@ +import RawIcon from '../RawIcon' + +test("render icon", () => { + snapshot() +}) diff --git a/app/components/Utils/__tests__/__snapshots__/Icon.spec.jsx.snap b/app/components/Utils/__tests__/__snapshots__/Icon.spec.jsx.snap index 542c89b21..63203b6b5 100644 --- a/app/components/Utils/__tests__/__snapshots__/Icon.spec.jsx.snap +++ b/app/components/Utils/__tests__/__snapshots__/Icon.spec.jsx.snap @@ -5,8 +5,8 @@ exports[`other props get passed to container 1`] = ` className="icon" title="Add some stuff" > - `; @@ -15,34 +15,18 @@ exports[`render icon 1`] = ` - `; -exports[`render without container 1`] = ` - -`; - exports[`set size 1`] = ` - `; - -exports[`use 'a' if is link 1`] = ` - - - -`; diff --git a/app/components/Utils/__tests__/__snapshots__/RawIcon.spec.jsx.snap b/app/components/Utils/__tests__/__snapshots__/RawIcon.spec.jsx.snap new file mode 100644 index 000000000..61f234c09 --- /dev/null +++ b/app/components/Utils/__tests__/__snapshots__/RawIcon.spec.jsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render icon 1`] = ` + +`; diff --git a/app/components/VideoDebate/ActionBubbleMenu.jsx b/app/components/VideoDebate/ActionBubbleMenu.jsx index 455a2b555..3f4c16076 100644 --- a/app/components/VideoDebate/ActionBubbleMenu.jsx +++ b/app/components/VideoDebate/ActionBubbleMenu.jsx @@ -3,7 +3,6 @@ import classNames from 'classnames' import { connect } from 'react-redux' import { withRouter } from 'react-router' import { translate } from 'react-i18next' -import debounce from 'debounce' import { MIN_REPUTATION_UPDATE_VIDEO } from '../../constants' import { changeStatementFormSpeaker } from '../../state/video_debate/statements/reducer' @@ -26,7 +25,7 @@ import { destroyStatementForm } from '../../state/video_debate/statements/effect }), {changeStatementFormSpeaker, toggleAutoscroll, addModal, destroyStatementForm} ) -@translate(['videoDebate', 'main']) +@translate('videoDebate') @withRouter export default class ActionBubbleMenu extends React.PureComponent { render() { diff --git a/app/components/VideoDebate/ColumnDebate.jsx b/app/components/VideoDebate/ColumnDebate.jsx index 658453fb2..e4a76a8df 100644 --- a/app/components/VideoDebate/ColumnDebate.jsx +++ b/app/components/VideoDebate/ColumnDebate.jsx @@ -1,6 +1,7 @@ import React from "react" import { connect } from "react-redux" import { Trans, translate } from 'react-i18next' +import { isLoadingVideoDebate } from '../../state/video_debate/selectors' import VideoDebateHistory from "./VideoDebateHistory" import ActionBubbleMenu from './ActionBubbleMenu' @@ -12,7 +13,7 @@ import { isAuthenticated } from '../../state/users/current_user/selectors' @connect(state => ({ - isLoading: state.VideoDebate.video.isLoading || state.VideoDebate.statements.isLoading, + isLoading: isLoadingVideoDebate(state), hasStatements: state.VideoDebate.statements.data.size !== 0, hasSpeakers: state.VideoDebate.video.data.speakers.size !== 0, hasStatementForm: hasStatementForm(state), diff --git a/app/components/VideoDebate/ColumnVideo.jsx b/app/components/VideoDebate/ColumnVideo.jsx index e8049836b..581733388 100644 --- a/app/components/VideoDebate/ColumnVideo.jsx +++ b/app/components/VideoDebate/ColumnVideo.jsx @@ -7,9 +7,9 @@ import { Link } from 'react-router' import { MIN_REPUTATION_ADD_SPEAKER } from '../../constants' import { videoDebateOnlineUsersCount, videoDebateOnlineViewersCount } from '../../state/video_debate/presence/selectors' import { AddSpeakerForm, SpeakerPreview } from "../Speakers" -import { VideoPlayer } from "../Videos" import { LoadingFrame, Icon } from "../Utils" import ReputationGuard from '../Utils/ReputationGuard' +import VideoDebatePlayer from './VideoDebatePlayer' import Presence from './Presence' @@ -27,22 +27,24 @@ export class ColumnVideo extends React.PureComponent { const { video, view, t } = this.props const { url, title, speakers } = video + const isDebate = view === "debate" + return (
    - +
    -

    {title}

    +

    {title}

      -
    • +
    • { t('debate') }
    • -
    • +
    • { t('history') } @@ -50,16 +52,20 @@ export class ColumnVideo extends React.PureComponent {
    - -
    - + {isDebate && +
    + +
    + +
    +
    +
    + {speakers.map(speaker => + + )} +
    - -
    - {speakers.map(speaker => - - )} -
    + }
    ) } diff --git a/app/components/VideoDebate/ModalHistory.jsx b/app/components/VideoDebate/ModalHistory.jsx index 71a1e54d5..0d63a6261 100644 --- a/app/components/VideoDebate/ModalHistory.jsx +++ b/app/components/VideoDebate/ModalHistory.jsx @@ -11,11 +11,11 @@ import ActionsTable from '../UsersActions/ActionsTable' import { reset } from '../../state/user_actions/reducer' -@connect((state, props) => ({ +@connect(state => ({ actions: state.UsersActions.actions, isLoading: state.UsersActions.isLoading }), {joinStatementHistoryChannel, leaveStatementHistoryChannel, popModal, reset}) -@translate(['history']) +@translate('history') export class ModalHistory extends React.PureComponent { componentDidMount() { if (this.props.entity === ENTITY_STATEMENT) diff --git a/app/components/VideoDebate/Presence.jsx b/app/components/VideoDebate/Presence.jsx index c0bca5cbf..84fb9cabb 100644 --- a/app/components/VideoDebate/Presence.jsx +++ b/app/components/VideoDebate/Presence.jsx @@ -8,11 +8,11 @@ import Tag from '../Utils/Tag' const Presence = ({t, nbUsers, nbViewers}) =>
    - + {t('presence.user', {count: nbUsers})} - + {t('presence.viewer', {count: nbViewers})} diff --git a/app/components/VideoDebate/VideoDebateHistory.jsx b/app/components/VideoDebate/VideoDebateHistory.jsx index 14c32a0a8..e52be211a 100644 --- a/app/components/VideoDebate/VideoDebateHistory.jsx +++ b/app/components/VideoDebate/VideoDebateHistory.jsx @@ -26,10 +26,8 @@ export default class VideoDebateHistory extends React.PureComponent { return (
    -
    - { error && error } - -
    + { error && error } +
    ) } diff --git a/app/components/VideoDebate/VideoDebatePlayer.jsx b/app/components/VideoDebate/VideoDebatePlayer.jsx new file mode 100644 index 000000000..ac3744172 --- /dev/null +++ b/app/components/VideoDebate/VideoDebatePlayer.jsx @@ -0,0 +1,43 @@ +import React from "react" +import { connect } from "react-redux" +import ReactPlayer from 'react-player' + +import { setPosition } from '../../state/video_debate/video/reducer' + + +/** + * A player connected to VideoDebate state. Update position in it when playing + * and seekTo position when requested in state. + */ +@connect(state => ({ + position: state.VideoDebate.video.playback.position, + forcedPosition: state.VideoDebate.video.playback.forcedPosition +}), {setPosition}) +export default class VideoDebatePlayer extends React.Component { + shouldComponentUpdate(newProps) { + return this.props.url !== newProps.url + } + + componentWillReceiveProps(newProps) { + const { forcedPosition } = newProps + if (forcedPosition.requestId !== null && + forcedPosition.requestId !== this.props.forcedPosition.requestId) { + this.refs.player.seekTo(forcedPosition.time) + } + } + + render() { + const { setPosition, url } = this.props + + return ( + setPosition(playedSeconds)} + width="" + height="" + controls + /> + ) + } +} diff --git a/app/components/VideoDebate/index.jsx b/app/components/VideoDebate/index.jsx index fb44af9c0..c46c26a2f 100644 --- a/app/components/VideoDebate/index.jsx +++ b/app/components/VideoDebate/index.jsx @@ -1,6 +1,7 @@ import React from "react" import { connect } from "react-redux" import { translate } from 'react-i18next' +import { Helmet } from 'react-helmet' import { ErrorView } from "../Utils" import { isAuthenticated } from "../../state/users/current_user/selectors" @@ -14,6 +15,8 @@ import { ColumnDebate } from './ColumnDebate' @connect(state => ({ videoErrors: state.VideoDebate.video.errors, + isLoading: state.VideoDebate.video.isLoading, + videoTitle: state.VideoDebate.video.data.title, authenticated: isAuthenticated(state), }), { joinVideoDebateChannel, joinCommentsChannel, joinStatementsChannel, @@ -44,6 +47,9 @@ export class VideoDebate extends React.PureComponent { return return (
    + + {!this.props.isLoading && {this.props.videoTitle}} +
    diff --git a/app/components/Videos/AddVideoForm.jsx b/app/components/Videos/AddVideoForm.jsx index 26ff04d7e..0fe873547 100644 --- a/app/components/Videos/AddVideoForm.jsx +++ b/app/components/Videos/AddVideoForm.jsx @@ -3,9 +3,9 @@ import { withRouter } from "react-router" import { Field, reduxForm } from 'redux-form' import { connect } from 'react-redux' import trim from 'voca/trim' +import ReactPlayer from 'react-player' import { youtubeRegex } from '../../lib/url_utils' -import { DummyVideoPlayer } from "../Videos" import { FieldWithButton } from "../FormUtils" import { LoadingFrame } from '../Utils/LoadingFrame' import { postVideo, searchVideo } from '../../state/videos/effects' @@ -18,19 +18,6 @@ const validate = ({ url }) => { return {} } -const renderVideoField = (field) => { - const { meta: {error}, input: {value} } = field - const urlInput = FieldWithButton(field) - - return ( -
    - {!error && } - {error &&
    } - {urlInput} -
    - ) -} - @withRouter @connect((state, props) => ({ initialValues: {url: props.params.videoUrl}, @@ -48,24 +35,16 @@ export class AddVideoForm extends React.PureComponent { } } - handleSubmit(video) { - const promise = this.props.postVideo(video) - return promise.then(action => { - if (!action.error) - this.props.router.push(`/videos/${action.payload.id}`) - else if (action.payload === 'unauthorized' && !this.props.isAuthenticated) - this.props.router.push('/login') - }) - } - render() { return (
    - trim(s)} + expandInput />
    @@ -74,4 +53,33 @@ export class AddVideoForm extends React.PureComponent {
    ) } + + renderVideoField = (field) => { + const { meta: {error}, input: {value} } = field + const urlInput = FieldWithButton(field) + + return ( +
    + {!error && + + } + {error &&
    } + {urlInput} +
    + ) + } + + handleSubmit(video) { + const promise = this.props.postVideo(video) + return promise.then(action => { + if (!action.error) + this.props.router.push(`/videos/${action.payload.id}`) + else if (action.payload === 'unauthorized' && !this.props.isAuthenticated) + this.props.router.push('/login') + }) + } } diff --git a/app/components/Videos/DummyVideoPlayer.jsx b/app/components/Videos/DummyVideoPlayer.jsx deleted file mode 100644 index 35b343b94..000000000 --- a/app/components/Videos/DummyVideoPlayer.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from "react" - -import ReactPlayer from 'react-player' - - -export class DummyVideoPlayer extends React.PureComponent { - constructor(props) { - super(props) - this.duration = 0 - } - - handleDuration(duration) { - this.duration = duration - } - - // Override props handleprogress to return time elapsed instead of rate played - handleProgress({played}) { - if (this.props.onProgress) - this.props.onProgress({played: played * this.duration}) - } - - seekTo(time) { - if (this.duration) - this.refs.player.seekTo(time / this.duration) - } - - render() { - const { url } = this.props - const {playback, dispatch, onProgress, position, forcedPosition, ...iframeProps} = this.props - if (!url) - return (
    ) - return ( - - ) - } -} - -DummyVideoPlayer.defaultProps = { - frameBorder: 0, - allowFullScreen: true, - width: '', - height: '', - controls: true -} diff --git a/app/components/Videos/EditVideoModal.jsx b/app/components/Videos/EditVideoModal.jsx index a7ff8b7c3..a240b82a4 100644 --- a/app/components/Videos/EditVideoModal.jsx +++ b/app/components/Videos/EditVideoModal.jsx @@ -13,8 +13,11 @@ import {shiftStatements} from '../../state/video_debate/statements/effects' const TimeShiftForm = reduxForm({form: 'shiftStatements', initialValues: {offset: 0}})(translate('main')( ({handleSubmit, t}) => -
    - + !offset}/> @@ -25,7 +28,8 @@ const TimeShiftForm = reduxForm({form: 'shiftStatements', initialValues: {offset export default class EditVideoModal extends React.PureComponent { render() { return ( - {this.props.t('video.edit')}}>

    {this.props.t('video.shiftStatements')}

    diff --git a/app/components/Videos/PublicVideos.jsx b/app/components/Videos/PublicVideos.jsx index c51189be2..53fdce78a 100644 --- a/app/components/Videos/PublicVideos.jsx +++ b/app/components/Videos/PublicVideos.jsx @@ -23,7 +23,7 @@ import capitalize from 'voca/capitalize' error: state.Videos.error, languageFilter: state.UserPreferences.videosLanguageFilter }), {fetchPublicVideos, reset, changeVideosLanguageFilter}) -@translate(['main', 'errors']) +@translate('main') export class PublicVideos extends React.PureComponent { componentDidMount() { this.props.fetchPublicVideos(this.props.languageFilter && {language: this.props.languageFilter}) @@ -38,8 +38,8 @@ export class PublicVideos extends React.PureComponent {

    - - {capitalize(this.props.t('entities.video_plural'))} + + {capitalize(this.props.t('entities.video_plural'))}

    hasReputation || user.is_publisher}> @@ -49,17 +49,15 @@ export class PublicVideos extends React.PureComponent {
    -
    - {this.renderFilterBar()} - {this.renderContent()} -
    + {this.renderFilterBar()} + {this.renderContent()}
    ) } renderFilterBar() { return ( -