diff --git a/src/player/sagas.js b/src/player/sagas.js deleted file mode 100644 index e69de29..0000000 diff --git a/src/reducers/index.js b/src/reducers/index.js index a131560..937460c 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -5,13 +5,14 @@ import modal from "../modal/reducer" import client from "../client/reducer" import snackbar from "../snackbar/reducer" import player from "../player/reducer" - +import upload from "../upload/reducer" export default combineReducers({ routing: routerReducer, client, modal, signup, snackbar, - player + player, + upload }) diff --git a/src/sagas/index.js b/src/sagas/index.js index a0b36bc..ef32e6e 100644 --- a/src/sagas/index.js +++ b/src/sagas/index.js @@ -2,12 +2,13 @@ import { all } from "redux-saga/effects" import ModalSaga from "../modal/sagas" import SignupSaga from "../signup/sagas" import SnackbarSaga from "../snackbar/sagas" +import UploaderSaga from "../upload/sagas" export default function* rootSagas() { - yield all([ ModalSaga(), SignupSaga(), SnackbarSaga(), + UploaderSaga() ]) } diff --git a/src/upload/actions.js b/src/upload/actions.js new file mode 100644 index 0000000..ee42206 --- /dev/null +++ b/src/upload/actions.js @@ -0,0 +1,14 @@ +import { UPLOAD_REQUEST, UPLOAD_RESET } from "./constants" + +export const uploadRequest = ({file}) => { + return { + type: UPLOAD_REQUEST, + file + } +} + +export const uploadReset = () => { + return { + type: UPLOAD_RESET, + } +} diff --git a/src/upload/constants.js b/src/upload/constants.js new file mode 100644 index 0000000..66174cd --- /dev/null +++ b/src/upload/constants.js @@ -0,0 +1,5 @@ +export const UPLOAD_REQUEST = "UPLOAD_REQUEST" +export const UPLOAD_PROGRESS = "UPLOAD_PROGRESS" +export const UPLOAD_SUCCESS = "UPLOAD_SUCCESS" +export const UPLOAD_ERROR = "UPLOAD_ERROR" +export const UPLOAD_RESET = "UPLOAD_RESET" diff --git a/src/upload/createFileUploadChannel.js b/src/upload/createFileUploadChannel.js new file mode 100644 index 0000000..4f5c606 --- /dev/null +++ b/src/upload/createFileUploadChannel.js @@ -0,0 +1,47 @@ +/* refs +https://decembersoft.com/posts/file-upload-progress-with-redux-saga/ + */ + +import { buffers, eventChannel, END } from "redux-saga" + +const createUploadFileChannel = (endpoint, file) => { + return eventChannel(emitter => { + const xhr = new XMLHttpRequest() + const onProgress = (e) => { + if (e.lengthComputable) { + const progress = e.loaded / e.total + emitter({progress}) + } + } + const onFailure = (e) => { + emitter({err: new Error("Upload failed")}) + emitter(END) + } + xhr.upload.addEventListener("progress", onProgress) + xhr.upload.addEventListener("error", onFailure) + xhr.upload.addEventListener("abort", onFailure) + xhr.onreadystatechange = () => { + const {readyState, status} = xhr + if (readyState === 4) { + if (status === 200) { + emitter({success: true}) + emitter(END) + } + else { + onFailure(null) + } + } + } + xhr.open("POST", endpoint, true) + xhr.send(file) + return () => { + xhr.upload.removeEventListener("progress", onProgress) + xhr.upload.removeEventListener("error", onFailure) + xhr.upload.removeEventListener("abort", onFailure) + xhr.onreadystatechange = null + xhr.abort() + } + }, buffers.sliding(2)) +} + +export default createUploadFileChannel diff --git a/src/client/sagas.js b/src/upload/index.js similarity index 100% rename from src/client/sagas.js rename to src/upload/index.js diff --git a/src/upload/reducer.js b/src/upload/reducer.js new file mode 100644 index 0000000..1d67f42 --- /dev/null +++ b/src/upload/reducer.js @@ -0,0 +1,73 @@ +import { UPLOAD_RESET, UPLOAD_REQUEST, UPLOAD_SUCCESS, UPLOAD_ERROR, UPLOAD_PROGRESS } from "./constants" + +const initialState = { + file: null, + requesting: false, + successful: false, + progress: 0, + messages: [], + errors: [] +} + +const reducer = (state = initialState, action) => { + switch (action.type) { + case UPLOAD_RESET: + return { + file: null, + requesting: false, + successful: false, + progress: 0, + messages: [], + errors: [] + } + case UPLOAD_REQUEST: + return { + file: null, + requesting: true, + successful: false, + progress: 0, + messages: [{body: "Uploading...", time: new Date()}], + errors: [] + } + + case UPLOAD_SUCCESS: + return { + file: action.file, + requesting: false, + successful: true, + progress: 100, + errors: [], + messages: [{ + body: `Successfully save file.`, + time: new Date() + }] + } + + case UPLOAD_ERROR: + return { + file: state.file, + requesting: false, + successful: false, + progress: null, + errors: state.errors.concat([{ + body: action.error.toString(), + time: new Date() + }]), + messages: [] + } + + case UPLOAD_PROGRESS: + return { + file: action.file, + requesting: false, + successful: false, + progress: action.progress, + errors: [], + messages: [] + } + default: + return state + } +} + +export default reducer diff --git a/src/upload/sagas.js b/src/upload/sagas.js new file mode 100644 index 0000000..9fd959a --- /dev/null +++ b/src/upload/sagas.js @@ -0,0 +1,37 @@ +import { call, put, take, takeEvery } from "redux-saga/effects" +import { UPLOAD_REQUEST, UPLOAD_RESET, UPLOAD_PROGRESS, UPLOAD_SUCCESS, UPLOAD_ERROR} from "./constants" +import {snackbarRequest } from "../snackbar/actions" +import { PREVIEW_SUCCESS} from "../snackbar/constants" +import createFileUploadChannel from "./createFileUploadChannel" + +// Upload the specified file +function* uploadFileFlow(file) { + const channel = yield call(createFileUploadChannel, "/some/path", file) + while (true) { + const {progress = 0, error, success} = yield take(channel) + if (error) { + yield put({type: UPLOAD_ERROR, error}) + return + } + if (success) { + yield put({type: UPLOAD_SUCCESS, file}) + return + } + + // show snackbar notification + yield put(snackbarRequest({snackbarType: PREVIEW_SUCCESS, snackbarOpen: true})) + yield put({type: UPLOAD_PROGRESS, file, progress}) + } +} + +// Watch for an upload request and then +// defer to another saga to perform the actual upload + +function* uploadFileWatcher() { + yield takeEvery(UPLOAD_REQUEST, function* (action) { + const file = action.file + yield call(uploadFileFlow, file) + }) +} + +export default uploadFileWatcher