-
+
{speaker && speaker.picture && (
)}
diff --git a/app/components/Statements/StatementsList.jsx b/app/components/Statements/StatementsList.jsx
index fc67e2271..a6ae7fecb 100644
--- a/app/components/Statements/StatementsList.jsx
+++ b/app/components/Statements/StatementsList.jsx
@@ -17,7 +17,8 @@ import { FULLHD_WIDTH_THRESHOLD } from '../../constants'
@connect(
state => ({
statements: state.VideoDebate.statements.data,
- statementFormSpeakerId: statementFormValueSelector(state, 'speaker_id')
+ statementFormSpeakerId: statementFormValueSelector(state, 'speaker_id'),
+ offset: state.VideoDebate.video.offset
}),
{ closeStatementForm, postStatement, setScrollTo }
)
@@ -34,11 +35,12 @@ export default class StatementsList extends React.PureComponent {
}
render() {
- const { statementFormSpeakerId, statements } = this.props
+ const { statementFormSpeakerId, statements, offset } = this.props
return (
{statementFormSpeakerId !== undefined && (
{
- if (!action.error) this.props.router.push(`/videos/${action.payload.hash_id}`)
- else if (action.payload === 'unauthorized' && !this.props.isAuthenticated)
+ return this.props.postVideo(video).then(action => {
+ if (!action.error) {
+ this.props.router.push(`/videos/${action.payload.hash_id}`)
+ } else if (action.payload === 'unauthorized' && !this.props.isAuthenticated) {
this.props.router.push('/login')
+ }
})
}
}
diff --git a/app/components/Videos/EditVideoModal.jsx b/app/components/Videos/EditVideoModal.jsx
index 1e80241cd..f07562d45 100644
--- a/app/components/Videos/EditVideoModal.jsx
+++ b/app/components/Videos/EditVideoModal.jsx
@@ -1,58 +1,84 @@
import React from 'react'
import { connect } from 'react-redux'
import { withNamespaces } from 'react-i18next'
-import { Field, reduxForm } from 'redux-form'
+import { Formik } from 'formik'
+import { Flex } from '@rebass/grid'
+import { Edit } from 'styled-icons/fa-regular/Edit'
+
+import { shiftStatements } from '../../state/video_debate/effects'
import Modal from '../Modal/Modal'
import { popModal } from '../../state/modals/reducer'
-import { Icon } from '../Utils/Icon'
import { flashErrorMsg, flashSuccessMsg } from '../../state/flashes/reducer'
import FieldWithButton from '../FormUtils/FieldWithButton'
-import { shiftStatements } from '../../state/video_debate/statements/effects'
+import StyledLabel from '../FormUtils/StyledLabel'
+import { StyledH3 } from '../StyledUtils/Title'
-const TimeShiftForm = reduxForm({
- form: 'shiftStatements',
- initialValues: { offset: 0 }
-})(
- withNamespaces('main')(({ handleSubmit, t }) => (
-
- ))
-)
+class EditVideoModal extends React.PureComponent {
+ renderTitle() {
+ return (
+
+ {this.props.t('video.edit')}
+
+ )
+ }
-@connect(
- null,
- { popModal, flashErrorMsg, flashSuccessMsg, shiftStatements }
-)
-@withNamespaces('videoDebate')
-export default class EditVideoModal extends React.PureComponent {
render() {
+ const { t, popModal, video } = this.props
+
return (
-
- {this.props.t('video.edit')}
-
- }
- >
- {this.props.t('video.shiftStatements')}
-
+
+
+ {t('video.shiftStatements')}
+ {
+ setSubmitting(true)
+ const reply = await this.props.shiftStatements(values)
+ setSubmitting(false)
+ this.props.popModal()
+ return reply
+ }}
+ >
+ {({ handleSubmit, isSubmitting, values, handleChange }) => (
+
+ )}
+
+
)
}
+}
- shiftSubmit = ({ offset }) => {
- if (offset) {
- return this.props.shiftStatements(offset).then(() => this.props.popModal())
- }
- }
+const mapDispatchToProps = {
+ popModal,
+ flashErrorMsg,
+ flashSuccessMsg,
+ shiftStatements
}
+
+export default withNamespaces(['videoDebate', 'main'])(
+ connect(
+ state => ({ video: state.VideoDebate.video.data }),
+ mapDispatchToProps
+ )(EditVideoModal)
+)
diff --git a/app/components/Videos/VideoCard.jsx b/app/components/Videos/VideoCard.jsx
index 15e34ee12..50f52093f 100644
--- a/app/components/Videos/VideoCard.jsx
+++ b/app/components/Videos/VideoCard.jsx
@@ -12,7 +12,7 @@ import { videoURL } from '../../lib/cf_routes'
export class VideoCard extends React.PureComponent {
render() {
const { t, video } = this.props
- const { hash_id, title, provider, provider_id } = video
+ const { hash_id, title } = video
const linkTarget = videoURL(hash_id)
return (
@@ -25,7 +25,7 @@ export class VideoCard extends React.PureComponent {
}
@@ -93,9 +93,9 @@ export class VideoCard extends React.PureComponent {
return {speaker.full_name}
}
- static videoThumb(provider, provider_id) {
- if (provider === 'youtube') {
- return `https://img.youtube.com/vi/${provider_id}/mqdefault.jpg`
+ static videoThumb({youtube_id}) {
+ if (youtube_id) {
+ return `https://img.youtube.com/vi/${youtube_id}/mqdefault.jpg`
}
return ''
}
diff --git a/app/constants.js b/app/constants.js
index 9b501b3f0..9c79212db 100644
--- a/app/constants.js
+++ b/app/constants.js
@@ -92,3 +92,6 @@ export const STATEMENT_LENGTH = [10, 255]
export const ALL_VIDEOS = 'ALL_VIDEOS'
export const ONLY_PARTNERS = 'ONLY_PARTNERS'
export const ONLY_COMMUNITY = 'ONLY_COMMUNITY'
+
+/* ------ Third party providers ------ */
+export const VIDEO_PLAYER_YOUTUBE = 'youtube'
diff --git a/app/i18n/en/errors.js b/app/i18n/en/errors.js
index f56adeaa4..3ae3afa02 100644
--- a/app/i18n/en/errors.js
+++ b/app/i18n/en/errors.js
@@ -15,7 +15,8 @@ export default {
action_already_done: 'This action has already been done',
unauthenticated: 'You need an account to do that',
unauthorized: 'Please (re)connect to continue',
- noInternet: 'Server connection failed, try refreshing the page'
+ noInternet: 'Server connection failed, try refreshing the page',
+ remote_video_404: "Remote video doesn't seems to exist"
},
client: {
joinCrashed: 'Server connection failed',
diff --git a/app/i18n/fr/errors.js b/app/i18n/fr/errors.js
index 764bf9830..1c8282be1 100644
--- a/app/i18n/fr/errors.js
+++ b/app/i18n/fr/errors.js
@@ -15,7 +15,8 @@ export default {
action_already_done: 'Cette action a déjà été effectuée',
unauthenticated: 'Vous devez vous connecter pour effectuer cette action',
unauthorized: 'Merci de vous (re)connecter pour continuer',
- noInternet: 'La connexion au serveur a échoué, essayez de rafraichir la page'
+ noInternet: 'La connexion au serveur a échoué, essayez de rafraichir la page',
+ remote_video_404: "Il semble que cette vidéo n'existe pas"
},
client: {
joinCrashed: 'La cconnexion au serveur a échoué',
diff --git a/app/lib/video_utils.js b/app/lib/video_utils.js
new file mode 100644
index 000000000..20849277b
--- /dev/null
+++ b/app/lib/video_utils.js
@@ -0,0 +1,20 @@
+import { VIDEO_PLAYER_YOUTUBE } from '../constants'
+
+/**
+ * Returns the offset used to shift all video's timecodes.
+ */
+export const getTimecodesOffset = (video, videoPlayer) => {
+ if (videoPlayer === VIDEO_PLAYER_YOUTUBE) {
+ return video.youtube_offset
+ }
+
+ console.warn(`getTimecode: Unknown provider ${videoPlayer}`)
+ return 0
+}
+
+/**
+ * Return the timecode shifted with appropriate offset.
+ */
+export const getTimecode = (videoPlayer, video, baseTimecode) => {
+ return getTimecodesOffset(video, videoPlayer) + baseTimecode
+}
diff --git a/app/state/flashes/reducer.js b/app/state/flashes/reducer.js
index a0e21b7c7..3ad6fe53e 100644
--- a/app/state/flashes/reducer.js
+++ b/app/state/flashes/reducer.js
@@ -12,27 +12,33 @@ export const unPause = createAction('FLASHES/UNPAUSE', () => false)
export const update = createAction('FLASHES/UPDATE')
// Actions helpers (use them like regular actions)
-export const flashError = options =>
- addFlash({
+export const flashError = options => {
+ return addFlash({
flashType: 'danger',
iconName: 'exclamation-circle',
...options
})
+}
+
export const flashErrorMsg = message => flashError({ message })
-export const flashErrorUnauthenticated = () =>
- flashError({
+
+export const flashErrorUnauthenticated = () => {
+ return flashError({
message: 'errors:server.unauthenticated',
infoText: 'main:menu.loginSignup',
infoUrl: '/login'
})
+}
-export const flashSuccessMsg = (message, params = {}) =>
- addFlash({
+export const flashSuccessMsg = (message, params = {}) => {
+ return addFlash({
flashType: 'success',
iconName: 'check-circle',
message,
...params
})
+}
+
export function errorToFlash(msg) {
const errorInfo = getErrorInfo(msg)
let action = null
@@ -47,6 +53,7 @@ export function errorToFlash(msg) {
action.error = true
return action
}
+
// Same as errorToFlash but doesn't show anything if payload is
// not a string (useful for forms)
export function errorMsgToFlash(msg) {
@@ -65,8 +72,8 @@ const FlashesReducer = handleActions(
[addFlash]: (state, { payload }) => {
// Only display one error for connections problems (instead of one per request)
if (
- payload.message === NO_INTERNET_ERROR &&
- state.flashes.find(f => f.message === NO_INTERNET_ERROR)
+ payload.message === NO_INTERNET_ERROR
+ && state.flashes.find(f => f.message === NO_INTERNET_ERROR)
)
return state
return state.update('flashes', l => l.push(payload))
@@ -76,14 +83,12 @@ const FlashesReducer = handleActions(
if (flashIdx !== -1) return state.update('flashes', l => l.delete(flashIdx))
return state
},
- [combineActions(pause, unPause)]: (state, { payload }) =>
- state.set('isPaused', payload),
+ [combineActions(pause, unPause)]: (state, { payload }) => state.set('isPaused', payload),
[update]: (state, { payload }) => {
if (!state.isPaused)
- return state.update('flashes', flashes =>
- flashes
- .map(f => f.set('timeLeft', f.timeLeft - payload))
- .filter(msg => msg.timeLeft > 0)
+ return state.update('flashes', flashes => flashes
+ .map(f => f.set('timeLeft', f.timeLeft - payload))
+ .filter(msg => msg.timeLeft > 0)
)
return state
}
diff --git a/app/state/video_debate/effects.js b/app/state/video_debate/effects.js
index f1bc0bf61..731c6775a 100644
--- a/app/state/video_debate/effects.js
+++ b/app/state/video_debate/effects.js
@@ -15,7 +15,8 @@ export const joinVideoDebateChannel = videoId => dispatch => {
speaker_removed: s => dispatch(videoReducer.removeSpeaker(s)),
speaker_updated: s => dispatch(videoReducer.updateSpeaker(s)),
presence_state: s => dispatch(setPresence(s)),
- presence_diff: s => dispatch(presenceDiff(s))
+ presence_diff: s => dispatch(presenceDiff(s)),
+ video_updated: ({ video }) => dispatch(videoReducer.updateVideo(video))
})
)
)
@@ -25,17 +26,34 @@ export const leaveVideoDebateChannel = () => () => {
return SocketApi.leaveChannel(VIDEO_DEBATE_CHANNEL)
}
-export const addSpeaker = speaker =>
- createEffect(SocketApi.push(VIDEO_DEBATE_CHANNEL, 'new_speaker', speaker), {
+export const addSpeaker = speaker => {
+ return createEffect(SocketApi.push(VIDEO_DEBATE_CHANNEL, 'new_speaker', speaker), {
catch: errorToFlash
})
+}
-export const removeSpeaker = speaker =>
- createEffect(SocketApi.push(VIDEO_DEBATE_CHANNEL, 'remove_speaker', speaker), {
+export const removeSpeaker = speaker => {
+ return createEffect(SocketApi.push(VIDEO_DEBATE_CHANNEL, 'remove_speaker', speaker), {
catch: errorToFlash
})
+}
-export const updateSpeaker = speaker =>
- createEffect(SocketApi.push(VIDEO_DEBATE_CHANNEL, 'update_speaker', speaker), {
+export const updateSpeaker = speaker => {
+ return createEffect(SocketApi.push(VIDEO_DEBATE_CHANNEL, 'update_speaker', speaker), {
catch: [errorMsgToFlash, generateFSAError]
})
+}
+
+/**
+ * Shift all video's statements
+ *
+ * @param {object} offsets a map of offsets
+ *
+ * ## Examples
+ * > shiftStatements({youtube_offset: 42})
+ */
+export const shiftStatements = offsets => {
+ return createEffect(SocketApi.push(VIDEO_DEBATE_CHANNEL, 'shift_statements', offsets), {
+ catch: errorToFlash
+ })
+}
diff --git a/app/state/video_debate/statements/effects.js b/app/state/video_debate/statements/effects.js
index 94c584504..06df83319 100644
--- a/app/state/video_debate/statements/effects.js
+++ b/app/state/video_debate/statements/effects.js
@@ -37,26 +37,18 @@ export const leaveStatementsChannel = () => () => {
return SocketApi.leaveChannel(STATEMENTS_CHANNEL)
}
-export const postStatement = statement =>
- createEffect(SocketApi.push(STATEMENTS_CHANNEL, 'new_statement', statement), {
- before: setSubmitting(true),
- then: [setSubmitting(false), returnSuccess],
- catch: [setSubmitting(false), errorToFlash]
- })
-
-export const updateStatement = statement =>
- createEffect(SocketApi.push(STATEMENTS_CHANNEL, 'update_statement', statement), {
- catch: errorToFlash
- })
-
-export const deleteStatement = statement =>
- createEffect(SocketApi.push(STATEMENTS_CHANNEL, 'remove_statement', statement), {
- catch: errorToFlash
- })
-
-export const shiftStatements = offset =>
- createEffect(SocketApi.push(STATEMENTS_CHANNEL, 'shift_all', offset), {
- catch: errorToFlash
- })
+export const postStatement = statement => createEffect(SocketApi.push(STATEMENTS_CHANNEL, 'new_statement', statement), {
+ before: setSubmitting(true),
+ then: [setSubmitting(false), returnSuccess],
+ catch: [setSubmitting(false), errorToFlash]
+})
+
+export const updateStatement = statement => createEffect(SocketApi.push(STATEMENTS_CHANNEL, 'update_statement', statement), {
+ catch: errorToFlash
+})
+
+export const deleteStatement = statement => createEffect(SocketApi.push(STATEMENTS_CHANNEL, 'remove_statement', statement), {
+ catch: errorToFlash
+})
export const destroyStatementForm = () => destroy('StatementForm')
diff --git a/app/state/video_debate/statements/selectors.js b/app/state/video_debate/statements/selectors.js
index d37d61869..ae22131aa 100644
--- a/app/state/video_debate/statements/selectors.js
+++ b/app/state/video_debate/statements/selectors.js
@@ -16,10 +16,12 @@ export const getStatementSpeaker = createCachedSelector(
export const getFocusedStatementId = createSelector(
state => state.VideoDebate.statements.data,
state => state.VideoDebate.video.playback.position,
- (statements, position) => {
+ state => state.VideoDebate.video.offset,
+ (statements, position, offset) => {
if (!position) return -1
- const statement = statements.findLast(st => position >= st.time)
- return statement && position <= statement.time + STATEMENT_FOCUS_TIME
+ const adjustedPosition = position - offset
+ const statement = statements.findLast(st => adjustedPosition >= st.time)
+ return statement && adjustedPosition <= statement.time + STATEMENT_FOCUS_TIME
? statement.id
: -1
}
@@ -43,5 +45,6 @@ export const isStatementFocused = createSelector(
export const statementFormValueSelector = formValueSelector('StatementForm')
-export const hasStatementForm = state =>
- statementFormValueSelector(state, 'speaker_id') !== undefined
+export const hasStatementForm = state => {
+ return statementFormValueSelector(state, 'speaker_id') !== undefined
+}
diff --git a/app/state/video_debate/video/__tests__/__snapshots__/reducer.spec.js.snap b/app/state/video_debate/video/__tests__/__snapshots__/reducer.spec.js.snap
index acc287167..9fc8d43cd 100644
--- a/app/state/video_debate/video/__tests__/__snapshots__/reducer.spec.js.snap
+++ b/app/state/video_debate/video/__tests__/__snapshots__/reducer.spec.js.snap
@@ -6,8 +6,8 @@ Immutable.Record {
"id": 0,
"hash_id": null,
"posted_at": 0,
- "provider": "",
- "provider_id": 0,
+ "youtube_id": null,
+ "youtube_offset": 0,
"url": "",
"title": "",
"speakers": Immutable.List [],
@@ -16,6 +16,8 @@ Immutable.Record {
},
"errors": null,
"isLoading": false,
+ "player": "youtube",
+ "offset": 0,
"playback": Immutable.Record {
"position": null,
"forcedPosition": Immutable.Record {
@@ -33,8 +35,8 @@ Immutable.Record {
"id": 0,
"hash_id": null,
"posted_at": 0,
- "provider": "",
- "provider_id": 0,
+ "youtube_id": null,
+ "youtube_offset": 0,
"url": "",
"title": "",
"speakers": Immutable.List [
@@ -53,6 +55,8 @@ Immutable.Record {
},
"errors": null,
"isLoading": false,
+ "player": "youtube",
+ "offset": 0,
"playback": Immutable.Record {
"position": null,
"forcedPosition": Immutable.Record {
@@ -70,8 +74,8 @@ Immutable.Record {
"id": 0,
"hash_id": null,
"posted_at": 0,
- "provider": "",
- "provider_id": 0,
+ "youtube_id": null,
+ "youtube_offset": 0,
"url": "",
"title": "",
"speakers": Immutable.List [],
@@ -80,6 +84,8 @@ Immutable.Record {
},
"errors": null,
"isLoading": false,
+ "player": "youtube",
+ "offset": 0,
"playback": Immutable.Record {
"position": null,
"forcedPosition": Immutable.Record {
@@ -97,8 +103,8 @@ Immutable.Record {
"id": 0,
"hash_id": null,
"posted_at": 0,
- "provider": "",
- "provider_id": 0,
+ "youtube_id": null,
+ "youtube_offset": 0,
"url": "",
"title": "",
"speakers": Immutable.List [
@@ -126,6 +132,8 @@ Immutable.Record {
},
"errors": null,
"isLoading": false,
+ "player": "youtube",
+ "offset": 0,
"playback": Immutable.Record {
"position": null,
"forcedPosition": Immutable.Record {
@@ -143,8 +151,8 @@ Immutable.Record {
"id": 0,
"hash_id": null,
"posted_at": 0,
- "provider": "",
- "provider_id": 0,
+ "youtube_id": null,
+ "youtube_offset": 0,
"url": "",
"title": "",
"speakers": Immutable.List [],
@@ -153,6 +161,8 @@ Immutable.Record {
},
"errors": null,
"isLoading": false,
+ "player": "youtube",
+ "offset": 0,
"playback": Immutable.Record {
"position": null,
"forcedPosition": Immutable.Record {
@@ -170,8 +180,8 @@ Immutable.Record {
"id": 0,
"hash_id": null,
"posted_at": 0,
- "provider": "",
- "provider_id": 0,
+ "youtube_id": null,
+ "youtube_offset": 0,
"url": "",
"title": "",
"speakers": Immutable.List [],
@@ -180,6 +190,8 @@ Immutable.Record {
},
"errors": null,
"isLoading": false,
+ "player": "youtube",
+ "offset": 0,
"playback": Immutable.Record {
"position": Object {
"forcedPosition": Object {
@@ -209,8 +221,8 @@ Immutable.Record {
"id": 0,
"hash_id": null,
"posted_at": 0,
- "provider": "",
- "provider_id": 0,
+ "youtube_id": null,
+ "youtube_offset": 0,
"url": "",
"title": "",
"speakers": Immutable.List [],
@@ -219,6 +231,8 @@ Immutable.Record {
},
"errors": null,
"isLoading": false,
+ "player": "youtube",
+ "offset": 0,
"playback": Immutable.Record {
"position": null,
"forcedPosition": Immutable.Record {
@@ -236,8 +250,8 @@ Immutable.Record {
"id": 0,
"hash_id": null,
"posted_at": 0,
- "provider": "",
- "provider_id": 0,
+ "youtube_id": null,
+ "youtube_offset": 0,
"url": "",
"title": "",
"speakers": Immutable.List [],
@@ -246,6 +260,8 @@ Immutable.Record {
},
"errors": null,
"isLoading": false,
+ "player": "youtube",
+ "offset": 0,
"playback": Immutable.Record {
"position": null,
"forcedPosition": Immutable.Record {
@@ -263,8 +279,8 @@ Immutable.Record {
"id": 0,
"hash_id": null,
"posted_at": 0,
- "provider": "",
- "provider_id": 0,
+ "youtube_id": null,
+ "youtube_offset": 0,
"url": "",
"title": "",
"speakers": Immutable.List [],
@@ -273,6 +289,8 @@ Immutable.Record {
},
"errors": null,
"isLoading": false,
+ "player": "youtube",
+ "offset": 0,
"playback": Immutable.Record {
"position": null,
"forcedPosition": Immutable.Record {
@@ -290,8 +308,8 @@ Immutable.Record {
"id": 0,
"hash_id": null,
"posted_at": 0,
- "provider": "",
- "provider_id": 0,
+ "youtube_id": null,
+ "youtube_offset": 0,
"url": "",
"title": "",
"speakers": Immutable.List [],
@@ -300,6 +318,8 @@ Immutable.Record {
},
"errors": null,
"isLoading": false,
+ "player": "youtube",
+ "offset": 0,
"playback": Immutable.Record {
"position": null,
"forcedPosition": Immutable.Record {
@@ -317,8 +337,8 @@ Immutable.Record {
"id": 0,
"hash_id": null,
"posted_at": 0,
- "provider": "",
- "provider_id": 0,
+ "youtube_id": null,
+ "youtube_offset": 0,
"url": "",
"title": "",
"speakers": Immutable.List [],
@@ -327,6 +347,8 @@ Immutable.Record {
},
"errors": null,
"isLoading": true,
+ "player": "youtube",
+ "offset": 0,
"playback": Immutable.Record {
"position": null,
"forcedPosition": Immutable.Record {
@@ -344,8 +366,8 @@ Immutable.Record {
"id": 0,
"hash_id": null,
"posted_at": 0,
- "provider": "",
- "provider_id": 0,
+ "youtube_id": null,
+ "youtube_offset": 0,
"url": "",
"title": "",
"speakers": Immutable.List [],
@@ -354,6 +376,8 @@ Immutable.Record {
},
"errors": null,
"isLoading": false,
+ "player": "youtube",
+ "offset": 0,
"playback": Immutable.Record {
"position": null,
"forcedPosition": Immutable.Record {
@@ -371,8 +395,8 @@ Immutable.Record {
"id": 0,
"hash_id": null,
"posted_at": 0,
- "provider": "",
- "provider_id": 0,
+ "youtube_id": null,
+ "youtube_offset": 0,
"url": "",
"title": "",
"speakers": Immutable.List [],
@@ -381,6 +405,8 @@ Immutable.Record {
},
"errors": null,
"isLoading": false,
+ "player": "youtube",
+ "offset": 0,
"playback": Immutable.Record {
"position": null,
"forcedPosition": Immutable.Record {
@@ -398,8 +424,8 @@ Immutable.Record {
"id": 0,
"hash_id": null,
"posted_at": 0,
- "provider": "",
- "provider_id": 0,
+ "youtube_id": null,
+ "youtube_offset": 0,
"url": "",
"title": "",
"speakers": Immutable.List [],
@@ -408,6 +434,8 @@ Immutable.Record {
},
"errors": null,
"isLoading": false,
+ "player": "youtube",
+ "offset": 0,
"playback": Immutable.Record {
"position": NaN,
"forcedPosition": Immutable.Record {
@@ -425,8 +453,8 @@ Immutable.Record {
"id": 0,
"hash_id": null,
"posted_at": 0,
- "provider": "",
- "provider_id": 0,
+ "youtube_id": null,
+ "youtube_offset": 0,
"url": "",
"title": "",
"speakers": Immutable.List [],
@@ -435,6 +463,8 @@ Immutable.Record {
},
"errors": null,
"isLoading": false,
+ "player": "youtube",
+ "offset": 0,
"playback": Immutable.Record {
"position": null,
"forcedPosition": Immutable.Record {
@@ -452,8 +482,8 @@ Immutable.Record {
"id": 0,
"hash_id": null,
"posted_at": 0,
- "provider": "",
- "provider_id": 0,
+ "youtube_id": null,
+ "youtube_offset": 0,
"url": "",
"title": "",
"speakers": Immutable.List [],
@@ -462,6 +492,8 @@ Immutable.Record {
},
"errors": null,
"isLoading": false,
+ "player": "youtube",
+ "offset": 0,
"playback": Immutable.Record {
"position": null,
"forcedPosition": Immutable.Record {
diff --git a/app/state/video_debate/video/reducer.js b/app/state/video_debate/video/reducer.js
index 6115111aa..49af86747 100644
--- a/app/state/video_debate/video/reducer.js
+++ b/app/state/video_debate/video/reducer.js
@@ -2,6 +2,8 @@ import { handleActions, createAction } from 'redux-actions'
import { Record, List } from 'immutable'
import uuidv1 from 'uuid/v1'
+import { VIDEO_PLAYER_YOUTUBE } from '../../../constants'
+import { getTimecodesOffset } from '../../../lib/video_utils'
import Video from '../../videos/record'
import Speaker from '../../speakers/record'
import { resetVideoDebate } from '../actions'
@@ -13,6 +15,7 @@ export const setLoading = createAction('VIDEO/SET_LOADING')
export const addSpeaker = createAction('VIDEO/ADD_SPEAKER')
export const removeSpeaker = createAction('VIDEO/REMOVE_SPEAKER')
export const updateSpeaker = createAction('VIDEO/UPDATE_SPEAKER')
+export const updateVideo = createAction('VIDEO/UPDATE_VIDEO')
export const setPosition = createAction('PLAYBACK/SET_POSITION')
export const forcePosition = createAction('PLAYBACK/FORCE_POSITION')
@@ -26,6 +29,11 @@ const INITIAL_STATE = new Record({
data: new Video(),
errors: null,
isLoading: false,
+ /** The player type currently displayed */
+ player: VIDEO_PLAYER_YOUTUBE,
+ /** An offset used to shift all video's timecodes */
+ offset: 0,
+ /** Information about the current playback status */
playback: new Record({
position: null,
forcedPosition: FORCED_POSITION_RECORD(),
@@ -43,13 +51,20 @@ function sortSpeakers(speakers) {
const VideoReducer = handleActions(
{
+ [updateVideo]: (state, { payload }) => {
+ return state.merge({
+ data: payload,
+ offset: getTimecodesOffset(payload, state.player)
+ })
+ },
[fetchAll]: {
next: (state, { payload: { speakers, ...data } }) => {
speakers = sortSpeakers(new List(speakers.map(s => new Speaker(s))))
return state.mergeDeep({
isLoading: false,
errors: null,
- data: new Video(data).set('speakers', speakers)
+ data: new Video(data).set('speakers', speakers),
+ offset: getTimecodesOffset(data, state.player)
})
},
throw: (state, { payload }) => {
@@ -81,11 +96,10 @@ const VideoReducer = handleActions(
return state.setIn(['playback', 'position'], Math.trunc(payload))
},
[forcePosition]: (state, { payload }) => {
- return state.update('playback', p =>
- p.mergeDeep({
- position: payload,
- forcedPosition: { time: payload, requestId: uuidv1() }
- })
+ return state.update('playback', p => p.mergeDeep({
+ position: payload,
+ forcedPosition: { time: payload, requestId: uuidv1() }
+ })
)
},
[setPlaying]: (state, { payload }) => {
diff --git a/app/state/videos/effects.js b/app/state/videos/effects.js
index 62a3471ef..255635855 100644
--- a/app/state/videos/effects.js
+++ b/app/state/videos/effects.js
@@ -3,16 +3,18 @@ import { setSubmitting } from './reducer'
import { createEffect, returnSuccess, generateFSAError } from '../utils'
import { errorToFlash } from '../flashes/reducer'
-export const searchVideo = videoUrl =>
- createEffect(HttpApi.post('search/video', { url: videoUrl }), {
+export const searchVideo = videoUrl => {
+ return createEffect(HttpApi.post('search/video', { url: videoUrl }), {
before: setSubmitting(true),
then: [setSubmitting(false), returnSuccess],
catch: [setSubmitting(false), generateFSAError]
})
+}
-export const postVideo = video =>
- createEffect(HttpApi.post('videos', video), {
+export const postVideo = video => {
+ return createEffect(HttpApi.post('videos', video), {
before: setSubmitting(true),
then: [setSubmitting(false), returnSuccess],
catch: [setSubmitting(false), errorToFlash, generateFSAError]
})
+}
diff --git a/app/state/videos/record.js b/app/state/videos/record.js
index 433db6980..6c35d8205 100644
--- a/app/state/videos/record.js
+++ b/app/state/videos/record.js
@@ -4,8 +4,8 @@ const Video = new Record({
id: 0,
hash_id: null,
posted_at: 0,
- provider: '',
- provider_id: 0,
+ youtube_id: null,
+ youtube_offset: 0,
url: '',
title: '',
speakers: new List(),
diff --git a/app/styles/theme.js b/app/styles/theme.js
index aaddf5455..c01af724f 100644
--- a/app/styles/theme.js
+++ b/app/styles/theme.js
@@ -1,5 +1,6 @@
const theme = {
breakpoints: ['768px', '1024px', '1216px', '1408px'],
+ space: ['0px', '4px', '8px', '16px', '32px', '64px', '128px', '256px', '512px'],
fontSizes: ['3rem', '2.5rem', '1.8rem', '1.5rem', '1.25rem', '1rem', '0.75rem'],
colors: {
green: '#39b714',
diff --git a/package-lock.json b/package-lock.json
index 171815ce3..3e7458f3e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "captain-fact-frontend",
- "version": "0.9.0",
+ "version": "0.9.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -4160,9 +4160,9 @@
}
},
"core-js": {
- "version": "2.5.7",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
- "integrity": "sha1-+XJgj/DOrWi4QaFqky0LGDeRgU4="
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.1.tgz",
+ "integrity": "sha512-L72mmmEayPJBejKIWe2pYtGis5r0tQ5NaJekdhyXgeMQTpJoBsH0NL4ElY2LfSoV15xeQWKQ+XTTOZdyero5Xg=="
},
"core-util-is": {
"version": "1.0.2",
@@ -4268,27 +4268,6 @@
"requires": {
"fbjs": "^0.8.0",
"gud": "^1.0.0"
- },
- "dependencies": {
- "core-js": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
- "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
- },
- "fbjs": {
- "version": "0.8.17",
- "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz",
- "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=",
- "requires": {
- "core-js": "^1.0.0",
- "isomorphic-fetch": "^2.1.1",
- "loose-envify": "^1.0.0",
- "object-assign": "^4.1.0",
- "promise": "^7.1.1",
- "setimmediate": "^1.0.5",
- "ua-parser-js": "^0.7.18"
- }
- }
}
},
"cross-spawn": {
@@ -6426,6 +6405,11 @@
}
}
},
+ "fbjs-css-vars": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.1.tgz",
+ "integrity": "sha512-IM+v/C40MNZWqsLErc32e0TyIk/NhkkQZL0QmjBh6zi1eXv0/GeVKmKmueQX7nn9SXQBQbTUcB8zuexIF3/88w=="
+ },
"fclone": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz",
@@ -6689,6 +6673,29 @@
}
}
},
+ "formik": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/formik/-/formik-1.4.1.tgz",
+ "integrity": "sha512-1pjcg65Pn4fuOgQv4cQOn9wDjCQ6f2J1QONDQaP4GfaiRYN/pQx2xtoyGo9ibNr/zR/cmayr1ew7EFaeAPLvsA==",
+ "requires": {
+ "create-react-context": "^0.2.2",
+ "deepmerge": "^2.1.1",
+ "hoist-non-react-statics": "^2.5.5",
+ "lodash": "^4.17.11",
+ "lodash-es": "^4.17.11",
+ "prop-types": "^15.6.1",
+ "react-fast-compare": "^2.0.1",
+ "tslib": "^1.9.3",
+ "warning": "^3.0.0"
+ },
+ "dependencies": {
+ "hoist-non-react-statics": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz",
+ "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw=="
+ }
+ }
+ },
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
@@ -6799,14 +6806,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -6821,20 +6826,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"core-util-is": {
"version": "1.0.2",
@@ -6951,8 +6953,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"ini": {
"version": "1.3.5",
@@ -6964,7 +6965,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -6979,7 +6979,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -7091,8 +7090,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"object-assign": {
"version": "4.1.1",
@@ -7104,7 +7102,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -7226,7 +7223,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -8423,9 +8419,9 @@
}
},
"i18next": {
- "version": "12.0.0",
- "resolved": "https://registry.npmjs.org/i18next/-/i18next-12.0.0.tgz",
- "integrity": "sha512-Zy/nFpmBZxgmi6k9HkHbf+MwvAwiY5BDzNjNfvyLPKyalc2YBwwZtblESDlTKLDO8XSv23qYRY2uZcADDlRSjQ=="
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-13.0.0.tgz",
+ "integrity": "sha512-P8SDA5PN4bI5/ZdLE2AlanZhU+eXh31icrojQvbGcG7jovoDoz79eKY9mSgQ0LAeuDFKAw4Vl4mGf1/EWGFACg=="
},
"iconv-lite": {
"version": "0.4.24",
@@ -11065,11 +11061,15 @@
"integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=",
"dev": true
},
+ "lodash.flowright": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.flowright/-/lodash.flowright-3.5.0.tgz",
+ "integrity": "sha1-K1//OZcW1+fcVyT+k0n2cGUYTWc="
+ },
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
- "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
- "dev": true
+ "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"lodash.kebabcase": {
"version": "4.1.1",
@@ -18592,21 +18592,32 @@
}
},
"react-apollo": {
- "version": "2.1.11",
- "resolved": "https://registry.npmjs.org/react-apollo/-/react-apollo-2.1.11.tgz",
- "integrity": "sha512-WRoSOuZiQYgieXTgep4v3YwtVlxRnPHdcLEJBLX5+zq2atLmonvEkuFLJNjhAAOW/mYM8XhbLrbaC5NPrFrn0w==",
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/react-apollo/-/react-apollo-2.3.3.tgz",
+ "integrity": "sha512-y4CwwmJjp0De/An7vrvEWOJ27lxmS/SXT8z22I8aOEBC2wzdcavDPjKzeLYJKs+hv1MGS3h84PSwFtlU4Em/bA==",
"requires": {
- "fbjs": "^0.8.16",
- "hoist-non-react-statics": "^2.5.0",
+ "fbjs": "^1.0.0",
+ "hoist-non-react-statics": "^3.0.0",
"invariant": "^2.2.2",
- "lodash": "^4.17.10",
+ "lodash.flowright": "^3.5.0",
+ "lodash.isequal": "^4.5.0",
"prop-types": "^15.6.0"
},
"dependencies": {
- "hoist-non-react-statics": {
- "version": "2.5.5",
- "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz",
- "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw=="
+ "fbjs": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-1.0.0.tgz",
+ "integrity": "sha512-MUgcMEJaFhCaF1QtWGnmq9ZDRAzECTCRAF7O6UZIlAlkTs1SasiX9aP0Iw7wfD2mJ7wDTNfg2w7u5fSCwJk1OA==",
+ "requires": {
+ "core-js": "^2.4.1",
+ "fbjs-css-vars": "^1.0.0",
+ "isomorphic-fetch": "^2.1.1",
+ "loose-envify": "^1.0.0",
+ "object-assign": "^4.1.0",
+ "promise": "^7.1.1",
+ "setimmediate": "^1.0.5",
+ "ua-parser-js": "^0.7.18"
+ }
}
}
},
@@ -18621,6 +18632,11 @@
"prop-types": "^15.6.0"
}
},
+ "react-fast-compare": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
+ "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
+ },
"react-flip-move": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/react-flip-move/-/react-flip-move-3.0.2.tgz",
@@ -18660,9 +18676,9 @@
}
},
"react-i18next": {
- "version": "8.0.7",
- "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-8.0.7.tgz",
- "integrity": "sha512-oJDVe5X8QK72NYYH7/VQtopONZPcNiarlWdVuCSkn9PucQCEAOf/yRt0V2LdjC4nBY48Y20/540glwR1nlE9gA==",
+ "version": "8.3.9",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-8.3.9.tgz",
+ "integrity": "sha512-/Une5B2xqkFKuI8uyRKq3BUNx4kFMZDESV+sn7KUNpAgYq4r0Wsd0HhMUEuJh8N7Y+ffeTbF5IaDH+pgPJdBEg==",
"requires": {
"@babel/runtime": "^7.1.2",
"create-react-context": "0.2.3",
@@ -18704,9 +18720,9 @@
}
},
"react-player": {
- "version": "1.6.6",
- "resolved": "https://registry.npmjs.org/react-player/-/react-player-1.6.6.tgz",
- "integrity": "sha512-Y9/ioqGIGomF1ydFNkqlxdkf4TOcjJ3Yw8LJmU/PXcL0glIRNRepmISP3OvVsGYs9i5omZZosy8VnvyRsd/pwg==",
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/react-player/-/react-player-1.8.0.tgz",
+ "integrity": "sha512-DqIJvkL7itGLgs/8u+sgeD3DXq5kc/O6FsB6J9KEuoaiYiSKhluBRymZWIwfCuPNTdpPsA2roZ+NSCyLrHAA9g==",
"requires": {
"deepmerge": "^2.0.1",
"load-script": "^1.0.0",
@@ -21382,9 +21398,9 @@
}
},
"styled-icons": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/styled-icons/-/styled-icons-5.2.2.tgz",
- "integrity": "sha512-9xus9e/rzuccgBB4LLNtKcl3ZYF2lmy3uj9Uz7KUahrfVIWZ8KDyFgH+O3VzsWyfKwVzSONNht//2ZberDnwRA=="
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/styled-icons/-/styled-icons-5.5.0.tgz",
+ "integrity": "sha512-PlKyhEIFHcdhIwMJnPKNJHOA0Bt6X13YneJtqGyjAqXI4rmLrPxtqORVn5BRbAJLccA+dSBOG7gMs+jpxJS94w=="
},
"styled-system": {
"version": "3.1.11",
@@ -22050,8 +22066,7 @@
"tslib": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
- "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
- "dev": true
+ "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ=="
},
"tty-browserify": {
"version": "0.0.0",
diff --git a/package.json b/package.json
index 52a644445..8f751d487 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "captain-fact-frontend",
- "version": "0.9.0",
+ "version": "0.9.3",
"private": true,
"scripts": {
"build": "npx webpack-cli --config webpack.production.config.js",
@@ -13,7 +13,7 @@
"start": "npm run dev",
"watch": "npx jest --watch",
"test": "npx jest",
- "test_update": "npx jest -u",
+ "test:update": "npx jest -u",
"wtest": "npx jest --watch",
"coverage": "npx jest --coverage '--collectCoverageFrom=app/+(API|components|lib|state)/**/!(record).{js,jsx}'",
"lint": "npx eslint app",
@@ -61,16 +61,17 @@
"apollo-boost": "~0.1.12",
"bulma": "~0.7.2",
"classnames": "~2.2.6",
- "core-js": "~2.5.6",
+ "core-js": "~2.6.1",
"date-fns": "~1.28.5",
"debounce": "~1.0.2",
"debounce-promise": "~3.1.0",
"diff": "~3.3.1",
"dotenv-webpack": "~1.5.5",
"faker": "~4.1.0",
+ "formik": "^1.4.1",
"graphql": "~14.0.2",
"graphql-tag": "~2.9.2",
- "i18next": "~12.0.0",
+ "i18next": "~13.0.0",
"immutable": "~4.0.0-rc.9",
"is-promise": "~2.1.0",
"isomorphic-fetch": "~2.2.1",
@@ -78,13 +79,13 @@
"prop-types": "~15.6.2",
"re-reselect": "~2.1.0",
"react": "~16.3.2",
- "react-apollo": "~2.1.9",
+ "react-apollo": "~2.3.3",
"react-dom": "~16.3.2",
"react-flip-move": "~3.0.2",
"react-helmet": "~5.2.0",
- "react-i18next": "~8.0.7",
+ "react-i18next": "~8.3.9",
"react-markdown": "~4.0.3",
- "react-player": "~1.6.6",
+ "react-player": "~1.8.0",
"react-redux": "~5.0.7",
"react-router": "~3.0.1",
"react-select": "~1.2.1",
@@ -97,7 +98,7 @@
"reselect": "~4.0.0",
"smoothscroll-polyfill": "~0.4.0",
"styled-components": "~4.1.2",
- "styled-icons": "~5.2.2",
+ "styled-icons": "~5.5.0",
"styled-system": "~3.1.11",
"tinycon": "~0.6.8",
"uuid": "~3.3.2",