diff --git a/electron/index.js b/electron/index.js index f49eeb6aa..2c28a5a61 100644 --- a/electron/index.js +++ b/electron/index.js @@ -20,6 +20,7 @@ import path from 'path'; import { diskSpaceLinux, diskSpaceWindows, diskSpaceMac } from '../ui/util/diskspace'; const { download } = require('electron-dl'); +const mime = require('mime'); const remote = require('@electron/remote/main'); const os = require('os'); const sudo = require('sudo-prompt'); @@ -299,6 +300,50 @@ app.on('before-quit', () => { appState.isQuitting = true; }); +// Get the content of a file as a raw buffer of bytes. +// Useful to convert a file path to a File instance. +// Example: +// const result = await ipcMain.invoke('get-file-from-path', 'path/to/file'); +// const file = new File([result.buffer], result.name); +// NOTE: if path points to a folder, an empty +// file will be given. +ipcMain.handle('get-file-from-path', (event, path) => { + return new Promise((resolve, reject) => { + fs.stat(path, (error, stats) => { + if (error) { + reject(error); + return; + } + // Separate folders considering "\" and "/" + // as separators (cross platform) + const folders = path.split(/[\\/]/); + const name = folders[folders.length - 1]; + if (stats.isDirectory()) { + resolve({ + name, + mime: undefined, + path, + buffer: new ArrayBuffer(0), + }); + return; + } + // Encoding null ensures data results in a Buffer. + fs.readFile(path, { encoding: null }, (err, data) => { + if (err) { + reject(err); + return; + } + resolve({ + name, + mime: mime.getType(name) || undefined, + path, + buffer: data, + }); + }); + }); + }); +}); + ipcMain.on('get-disk-space', async (event) => { try { const { data_dir } = await Lbry.settings_get(); diff --git a/flow-typed/file-with-path.js b/flow-typed/file-with-path.js new file mode 100644 index 000000000..48f727ca9 --- /dev/null +++ b/flow-typed/file-with-path.js @@ -0,0 +1,9 @@ +// @flow + +declare type FileWithPath = { + file: File, + // The full path will only be available in + // the application. For browser, the name + // of the file will be used. + path: string, +} diff --git a/flow-typed/web-file.js b/flow-typed/web-file.js deleted file mode 100644 index d1445a90f..000000000 --- a/flow-typed/web-file.js +++ /dev/null @@ -1,6 +0,0 @@ -// @flow - -declare type WebFile = File & { - path?: string, - title?: string, -} diff --git a/package.json b/package.json index b62603452..e0b489d73 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "humanize-duration": "^3.27.0", "if-env": "^1.0.4", "match-sorter": "^6.3.0", + "mime": "^3.0.0", "node-html-parser": "^5.1.0", "parse-duration": "^1.0.0", "proxy-polyfill": "0.1.6", diff --git a/ui/component/channelForm/view.jsx b/ui/component/channelForm/view.jsx index 85eda9693..084fbbe64 100644 --- a/ui/component/channelForm/view.jsx +++ b/ui/component/channelForm/view.jsx @@ -325,7 +325,6 @@ function ChannelForm(props: Props) { uri={uri} thumbnailPreview={thumbnailPreview} allowGifs - showDelayedMessage={isUpload.thumbnail} setThumbUploadError={setThumbError} thumbUploadError={thumbError} /> diff --git a/ui/component/common/file-list.jsx b/ui/component/common/file-list.jsx index 733eca3ad..b4acb993c 100644 --- a/ui/component/common/file-list.jsx +++ b/ui/component/common/file-list.jsx @@ -3,8 +3,8 @@ import React from 'react'; import { useRadioState, Radio, RadioGroup } from 'reakit/Radio'; type Props = { - files: Array, - onChange: (WebFile | void) => void, + files: Array, + onChange: (File | void) => void, }; type RadioProps = { @@ -26,16 +26,16 @@ function FileList(props: Props) { const getFile = (value?: string) => { if (files && files.length) { - return files.find((file: WebFile) => file.name === value); + return files.find((file: File) => file.name === value); } }; React.useEffect(() => { - if (radio.stops.length) { + if (radio.items.length) { if (!radio.currentId) { radio.first(); } else { - const first = radio.stops[0].ref.current; + const first = radio.items[0].ref.current; // First auto-selection if (first && first.id === radio.currentId && !radio.state) { const file = getFile(first.value); @@ -46,12 +46,12 @@ function FileList(props: Props) { if (radio.state) { // Find selected element - const stop = radio.stops.find(item => item.id === radio.currentId); + const stop = radio.items.find((item) => item.id === radio.currentId); const element = stop && stop.ref.current; // Only update state if new item is selected if (element && element.value !== radio.state) { const file = getFile(element.value); - // Sselect new file and update state + // Select new file and update state onChange(file); radio.setState(element.value); } diff --git a/ui/component/common/file-selector.jsx b/ui/component/common/file-selector.jsx index bd1729d31..47d2e651e 100644 --- a/ui/component/common/file-selector.jsx +++ b/ui/component/common/file-selector.jsx @@ -1,19 +1,21 @@ // @flow import * as React from 'react'; import * as remote from '@electron/remote'; +import { ipcRenderer } from 'electron'; import Button from 'component/button'; import { FormField } from 'component/common/form'; type Props = { type: string, currentPath?: ?string, - onFileChosen: (WebFile) => void, + onFileChosen: (FileWithPath) => void, label?: string, placeholder?: string, accept?: string, error?: string, disabled?: boolean, autoFocus?: boolean, + filters?: Array<{ name: string, extension: string[] }>, }; class FileSelector extends React.PureComponent { @@ -41,7 +43,7 @@ class FileSelector extends React.PureComponent { const file = files[0]; if (this.props.onFileChosen) { - this.props.onFileChosen(file); + this.props.onFileChosen({ file, path: file.path || file.name }); } this.fileInput.current.value = null; // clear the file input }; @@ -64,13 +66,27 @@ class FileSelector extends React.PureComponent { properties = ['openDirectory']; } - remote.dialog.showOpenDialog({ properties, defaultPath }).then((result) => { - const path = result && result.filePaths[0]; - if (path) { - // $FlowFixMe - this.props.onFileChosen({ path }); - } - }); + remote.dialog + .showOpenDialog({ + properties, + defaultPath, + filters: this.props.filters, + }) + .then((result) => { + const path = result && result.filePaths[0]; + if (path) { + return ipcRenderer.invoke('get-file-from-path', path); + } + }) + .then((result) => { + if (!result) { + return; + } + const file = new File([result.buffer], result.name, { + type: result.mime, + }); + this.props.onFileChosen({ file, path: result.path }); + }); }; fileInputButton = () => { diff --git a/ui/component/fileDrop/view.jsx b/ui/component/fileDrop/view.jsx index cb42b6a59..bd946aa32 100644 --- a/ui/component/fileDrop/view.jsx +++ b/ui/component/fileDrop/view.jsx @@ -11,10 +11,10 @@ import Icon from 'component/common/icon'; type Props = { modal: { id: string, modalProps: {} }, - filePath: string | WebFile, + filePath: ?string, clearPublish: () => void, updatePublishForm: ({}) => void, - openModal: (id: string, { files: Array }) => void, + openModal: (id: string, { files: Array }) => void, // React router history: { entities: {}[], @@ -37,7 +37,7 @@ function FileDrop(props: Props) { const { drag, dropData } = useDragDrop(); const [files, setFiles] = React.useState([]); const [error, setError] = React.useState(false); - const [target, setTarget] = React.useState(null); + const [target, setTarget] = React.useState(null); const hideTimer = React.useRef(null); const targetTimer = React.useRef(null); const navigationTimer = React.useRef(null); @@ -65,24 +65,29 @@ function FileDrop(props: Props) { } }, [history]); - // Delay hide and navigation for a smooth transition - const hideDropArea = React.useCallback(() => { - hideTimer.current = setTimeout(() => { - setFiles([]); - // Navigate to publish area - navigationTimer.current = setTimeout(() => { - navigateToPublish(); - }, NAVIGATE_TIME_OUT); - }, HIDE_TIME_OUT); - }, [navigateToPublish]); - // Handle file selection const handleFileSelected = React.useCallback( (selectedFile) => { - updatePublishForm({ filePath: selectedFile }); - hideDropArea(); + // Delay hide and navigation for a smooth transition + hideTimer.current = setTimeout(() => { + setFiles([]); + // Navigate to publish area + navigationTimer.current = setTimeout(() => { + // Navigate first, THEN assign filePath, otherwise + // the file selected will get reset (that's how the + // publish file view works, when the user switches to + // publish a file, the pathFile value gets reset to undefined) + navigateToPublish(); + updatePublishForm({ + filePath: selectedFile.path || selectedFile.name, + fileDur: 0, + fileSize: 0, + fileVid: false, + }); + }, NAVIGATE_TIME_OUT); + }, HIDE_TIME_OUT); }, - [updatePublishForm, hideDropArea] + [setFiles, navigateToPublish, updatePublishForm] ); // Clear timers when unmounted diff --git a/ui/component/postEditor/view.jsx b/ui/component/postEditor/view.jsx index 0cada8349..07333e2f1 100644 --- a/ui/component/postEditor/view.jsx +++ b/ui/component/postEditor/view.jsx @@ -6,7 +6,7 @@ type Props = { uri: ?string, label: ?string, disabled: ?boolean, - filePath: string | WebFile, + filePath: File, fileText: ?string, fileMimeType: ?string, streamingUrl: ?string, diff --git a/ui/component/publishFile/view.jsx b/ui/component/publishFile/view.jsx index b53b7720c..3879a9d94 100644 --- a/ui/component/publishFile/view.jsx +++ b/ui/component/publishFile/view.jsx @@ -19,7 +19,7 @@ type Props = { mode: ?string, name: ?string, title: ?string, - filePath: string | WebFile, + filePath: ?string, fileMimeType: ?string, isStillEditing: boolean, balance: number, @@ -77,7 +77,7 @@ function PublishFile(props: Props) { const sizeInMB = Number(size) / 1000000; const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND; const ffmpegAvail = ffmpegStatus.available; - const [currentFile, setCurrentFile] = useState(null); + const currentFile = filePath; const [currentFileType, setCurrentFileType] = useState(null); const [optimizeAvail, setOptimizeAvail] = useState(false); const [userOptimize, setUserOptimize] = usePersistedState('publish-file-user-optimize', false); @@ -91,18 +91,6 @@ function PublishFile(props: Props) { } }, [currentFileType, mode, isStillEditing, updatePublishForm]); - useEffect(() => { - if (!filePath || filePath === '') { - setCurrentFile(''); - updateFileInfo(0, 0, false); - } else if (typeof filePath !== 'string') { - // Update currentFile file - if (filePath.name !== currentFile && filePath.path !== currentFile) { - handleFileChange(filePath); - } - } - }, [filePath, currentFile, handleFileChange, updateFileInfo]); - useEffect(() => { const isOptimizeAvail = currentFile && currentFile !== '' && isVid && ffmpegAvail; const finalOptimizeState = isOptimizeAvail && userOptimize; @@ -209,11 +197,11 @@ function PublishFile(props: Props) { } } - function handleFileChange(file: WebFile, clearName = true) { + function handleFileChange(fileWithPath: FileWithPath, clearName = true) { window.URL = window.URL || window.webkitURL; // select file, start to select a new one, then cancel - if (!file) { + if (!fileWithPath) { if (isStillEditing || !clearName) { updatePublishForm({ filePath: '' }); } else { @@ -222,7 +210,8 @@ function PublishFile(props: Props) { return; } - // if video, extract duration so we can warn about bitrateif (typeof file !== 'string') { + // if video, extract duration so we can warn about bitrate if (typeof file !== 'string') + const file = fileWithPath.file; const contentType = file.type && file.type.split('/'); const isVideo = contentType && contentType[0] === 'video'; const isMp4 = contentType && contentType[1] === 'mp4'; @@ -233,7 +222,7 @@ function PublishFile(props: Props) { isTextPost = contentType[1] === 'plain' || contentType[1] === 'markdown'; setCurrentFileType(contentType); } else if (file.name) { - // If user's machine is missign a valid content type registration + // If user's machine is missing a valid content type registration // for markdown content: text/markdown, file extension will be used instead const extension = file.name.split('.').pop(); isTextPost = MARKDOWN_FILE_EXTENSIONS.includes(extension); @@ -270,10 +259,8 @@ function PublishFile(props: Props) { setPublishMode(PUBLISH_MODES.FILE); } - const publishFormParams: { filePath: string | WebFile, name?: string, optimize?: boolean } = { - // if electron, we'll set filePath to the path string because SDK is handling publishing. - // File.path will be undefined from web due to browser security, so it will default to the File Object. - filePath: file.path || file, + const publishFormParams: { filePath: string, name?: string, optimize?: boolean } = { + filePath: fileWithPath.path, }; // Strip off extention and replace invalid characters let fileName = name || (file.name && file.name.substring(0, file.name.lastIndexOf('.'))) || ''; @@ -282,8 +269,6 @@ function PublishFile(props: Props) { publishFormParams.name = parseName(fileName); } - // File path is not supported on web for security reasons so we use the name instead. - setCurrentFile(file.path || file.name); updatePublishForm(publishFormParams); } diff --git a/ui/component/publishForm/view.jsx b/ui/component/publishForm/view.jsx index 8042975e8..7a180b98c 100644 --- a/ui/component/publishForm/view.jsx +++ b/ui/component/publishForm/view.jsx @@ -35,8 +35,8 @@ import tempy from 'tempy'; type Props = { disabled: boolean, tags: Array, - publish: (source?: string | File, ?boolean) => void, - filePath: string | File, + publish: (source: ?File, ?boolean) => void, + filePath: ?File, fileText: string, bid: ?number, bidError: ?string, @@ -373,9 +373,6 @@ function PublishForm(props: Props) { if (!output || output === '') { // Generate a temporary file: output = tempy.file({ name: 'post.md' }); - } else if (typeof filePath === 'string') { - // Use current file - output = filePath; } // Create a temporary file and save file changes if (output && output !== '') { @@ -447,7 +444,7 @@ function PublishForm(props: Props) { // with other properties such as name, title, etc.) for security reasons. useEffect(() => { if (mode === PUBLISH_MODES.FILE) { - updatePublishForm({ filePath: '', fileDur: 0, fileSize: 0 }); + updatePublishForm({ filePath: undefined, fileDur: 0, fileSize: 0 }); } }, [mode, updatePublishForm]); diff --git a/ui/component/selectAsset/view.jsx b/ui/component/selectAsset/view.jsx index 13fe2c324..72ceb9299 100644 --- a/ui/component/selectAsset/view.jsx +++ b/ui/component/selectAsset/view.jsx @@ -27,6 +27,14 @@ type Props = { // passed to the onUpdate function after the // upload service returns success. buildImagePreview?: boolean, + // File extension filtering. Files can be filtered + // but the "All Files" options always shows up. To + // avoid that, you can use the filters property. + // For example, to only accept images pass the + // following filter: + // { name: 'Images', extensions: ['jpg', 'png', 'gif'] }, + filters?: Array<{ name: string, extension: string[] }>, + type?: string, }; function filePreview(file) { @@ -43,7 +51,8 @@ function filePreview(file) { } function SelectAsset(props: Props) { - const { onUpdate, onDone, assetName, currentValue, recommended, title, inline, buildImagePreview } = props; + const { onUpdate, onDone, assetName, currentValue, recommended, title, inline, buildImagePreview, filters, type } = + props; const [pathSelected, setPathSelected] = React.useState(''); const [fileSelected, setFileSelected] = React.useState(null); const [uploadStatus, setUploadStatus] = React.useState(SPEECH_READY); @@ -121,17 +130,17 @@ function SelectAsset(props: Props) { /> ) : ( { - if (file.name) { - setFileSelected(file); - // what why? why not target=WEB this? - // file.path is undefined in web but available in electron - setPathSelected(file.name || file.path); + onFileChosen={(fileWithPath) => { + if (fileWithPath.file.name) { + setFileSelected(fileWithPath.file); + setPathSelected(fileWithPath.path); } }} accept={accept} diff --git a/ui/component/selectThumbnail/view.jsx b/ui/component/selectThumbnail/view.jsx index 8998272ae..06b685269 100644 --- a/ui/component/selectThumbnail/view.jsx +++ b/ui/component/selectThumbnail/view.jsx @@ -160,9 +160,9 @@ function SelectThumbnail(props: Props) { label={__('Thumbnail')} placeholder={__('Choose an enticing thumbnail')} accept={accept} - onFileChosen={(file) => + onFileChosen={(fileWithPath) => openModal(MODALS.CONFIRM_THUMBNAIL_UPLOAD, { - file, + file: fileWithPath, cb: (url) => updateThumbnailParams && updateThumbnailParams({ thumbnail_url: url }), }) } diff --git a/ui/component/settingSystem/view.jsx b/ui/component/settingSystem/view.jsx index af40546f8..545fe1b50 100644 --- a/ui/component/settingSystem/view.jsx +++ b/ui/component/settingSystem/view.jsx @@ -130,7 +130,7 @@ export default function SettingSystem(props: Props) { { + onFileChosen={(newDirectory: FileWithPath) => { setDaemonSetting('download_dir', newDirectory.path); }} /> @@ -224,7 +224,7 @@ export default function SettingSystem(props: Props) { type="openDirectory" placeholder={__('A Folder containing FFmpeg')} currentPath={ffmpegPath || daemonSettings.ffmpeg_path} - onFileChosen={(newDirectory: WebFile) => { + onFileChosen={(newDirectory: FileWithPath) => { // $FlowFixMe setDaemonSetting('ffmpeg_path', newDirectory.path); findFFmpeg(); diff --git a/ui/modal/modalAutoGenerateThumbnail/view.jsx b/ui/modal/modalAutoGenerateThumbnail/view.jsx index 6eb6ec17d..868a1841b 100644 --- a/ui/modal/modalAutoGenerateThumbnail/view.jsx +++ b/ui/modal/modalAutoGenerateThumbnail/view.jsx @@ -4,7 +4,7 @@ import { Modal } from 'modal/modal'; import { formatFileSystemPath } from 'util/url'; type Props = { - upload: WebFile => void, + upload: (File) => void, filePath: string, closeModal: () => void, showToast: ({}) => void, diff --git a/ui/modal/modalConfirmThumbnailUpload/index.js b/ui/modal/modalConfirmThumbnailUpload/index.js index 3b8256881..5f4fb9e87 100644 --- a/ui/modal/modalConfirmThumbnailUpload/index.js +++ b/ui/modal/modalConfirmThumbnailUpload/index.js @@ -5,7 +5,7 @@ import ModalConfirmThumbnailUpload from './view'; const perform = (dispatch) => ({ closeModal: () => dispatch(doHideModal()), - upload: (file, cb) => dispatch(doUploadThumbnail(null, file, null, null, file.path, cb)), + upload: (fileWithPath, cb) => dispatch(doUploadThumbnail(null, fileWithPath.file, null, null, fileWithPath.path, cb)), updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)), }); diff --git a/ui/modal/modalConfirmThumbnailUpload/view.jsx b/ui/modal/modalConfirmThumbnailUpload/view.jsx index efd5be9b1..f05037a44 100644 --- a/ui/modal/modalConfirmThumbnailUpload/view.jsx +++ b/ui/modal/modalConfirmThumbnailUpload/view.jsx @@ -4,8 +4,8 @@ import { Modal } from 'modal/modal'; import { DOMAIN } from 'config'; type Props = { - file: WebFile, - upload: (WebFile, (string) => void) => void, + file: FileWithPath, + upload: (FileWithPath, (string) => void) => void, cb: (string) => void, closeModal: () => void, updatePublishForm: ({}) => void, @@ -23,7 +23,7 @@ class ModalConfirmThumbnailUpload extends React.PureComponent { render() { const { closeModal, file } = this.props; - const filePath = file && (file.path || file.name); + const filePath = file && file.path; return ( , + files: Array, hideModal: () => void, updatePublishForm: ({}) => void, history: { location: { pathname: string }, - push: string => void, + push: (string) => void, }, }; @@ -43,7 +43,7 @@ const ModalFileSelection = (props: Props) => { navigateToPublish(); } - const handleFileChange = (file?: WebFile) => { + const handleFileChange = (file?: File) => { // $FlowFixMe setSelectedFile(file); }; diff --git a/ui/modal/modalImageUpload/view.jsx b/ui/modal/modalImageUpload/view.jsx index b76fe10db..a992167cb 100644 --- a/ui/modal/modalImageUpload/view.jsx +++ b/ui/modal/modalImageUpload/view.jsx @@ -14,10 +14,13 @@ type Props = { function ModalImageUpload(props: Props) { const { closeModal, currentValue, title, assetName, helpText, onUpdate } = props; + const filters = React.useMemo(() => [{ name: 'Images', extensions: ['jpg', 'jpeg', 'png', 'gif', 'svg'] }]); return ( { // @endif } - function getFilePathName(filePath: string | WebFile) { + function getFilePathName(filePath: ?File) { if (!filePath) { return NO_FILE; } - - if (typeof filePath === 'string') { - return filePath; - } else { - return filePath.name; - } + return filePath.name; } function createRow(label: string, value: any) { @@ -127,7 +122,7 @@ const ModalPublishPreview = (props: Props) => { const txFee = previewResponse ? previewResponse['total_fee'] : null; // $FlowFixMe add outputs[0] etc to PublishResponse type - const isOptimizeAvail = filePath && filePath !== '' && isVid && ffmpegStatus.available; + const isOptimizeAvail = filePath && isVid && ffmpegStatus.available; let modalTitle; if (isStillEditing) { modalTitle = __('Confirm Edit'); diff --git a/ui/redux/actions/publish.js b/ui/redux/actions/publish.js index 33d3f77dc..13134e211 100644 --- a/ui/redux/actions/publish.js +++ b/ui/redux/actions/publish.js @@ -18,7 +18,7 @@ import Lbry from 'lbry'; import { isClaimNsfw } from 'util/claim'; export const NO_FILE = '---'; -export const doPublishDesktop = (filePath: string, preview?: boolean) => (dispatch: Dispatch, getState: () => {}) => { +export const doPublishDesktop = (filePath: ?File, preview?: boolean) => (dispatch: Dispatch, getState: () => {}) => { const publishPreview = (previewResponse) => { dispatch( doOpenModal(MODALS.PUBLISH_PREVIEW, { @@ -138,335 +138,327 @@ export const doUpdatePublishForm = (publishFormValue: UpdatePublishFormData) => data: { ...publishFormValue }, }); -export const doUploadThumbnail = ( - filePath?: string, - thumbnailBlob?: File, - fsAdapter?: any, - fs?: any, - path?: any, - cb?: (string) => void -) => (dispatch: Dispatch) => { - const downMessage = __('Thumbnail upload service may be down, try again later.'); - let thumbnail, fileExt, fileName, fileType; +export const doUploadThumbnail = + (filePath?: string, thumbnailBlob?: File, fsAdapter?: any, fs?: any, path?: any, cb?: (string) => void) => + (dispatch: Dispatch) => { + const downMessage = __('Thumbnail upload service may be down, try again later.'); + let thumbnail, fileExt, fileName, fileType; - const makeid = () => { - let text = ''; - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for (let i = 0; i < 24; i += 1) text += possible.charAt(Math.floor(Math.random() * 62)); - return text; - }; + const makeid = () => { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 24; i += 1) text += possible.charAt(Math.floor(Math.random() * 62)); + return text; + }; - const uploadError = (error = '') => { - dispatch( - batchActions( - { - type: ACTIONS.UPDATE_PUBLISH_FORM, - data: { - uploadThumbnailStatus: THUMBNAIL_STATUSES.READY, - thumbnail: '', - nsfw: false, + const uploadError = (error = '') => { + dispatch( + batchActions( + { + type: ACTIONS.UPDATE_PUBLISH_FORM, + data: { + uploadThumbnailStatus: THUMBNAIL_STATUSES.READY, + thumbnail: '', + nsfw: false, + }, }, - }, - doError(error) - ) - ); - }; + doError(error) + ) + ); + }; - dispatch({ - type: ACTIONS.UPDATE_PUBLISH_FORM, - data: { - thumbnailError: undefined, - }, - }); + dispatch({ + type: ACTIONS.UPDATE_PUBLISH_FORM, + data: { + thumbnailError: undefined, + }, + }); - const doUpload = (data) => { - return fetch(SPEECH_PUBLISH, { - method: 'POST', - body: data, - }) - .then((res) => res.text()) - .then((text) => (text.length ? JSON.parse(text) : {})) - .then((json) => { - if (!json.success) return uploadError(json.message || downMessage); - if (cb) { - cb(json.data.serveUrl); - } - return dispatch({ - type: ACTIONS.UPDATE_PUBLISH_FORM, - data: { - uploadThumbnailStatus: THUMBNAIL_STATUSES.COMPLETE, - thumbnail: json.data.serveUrl, - }, - }); + const doUpload = (data) => { + return fetch(SPEECH_PUBLISH, { + method: 'POST', + body: data, }) - .catch((err) => { - let message = err.message; + .then((res) => res.text()) + .then((text) => (text.length ? JSON.parse(text) : {})) + .then((json) => { + if (!json.success) return uploadError(json.message || downMessage); + if (cb) { + cb(json.data.serveUrl); + } + return dispatch({ + type: ACTIONS.UPDATE_PUBLISH_FORM, + data: { + uploadThumbnailStatus: THUMBNAIL_STATUSES.COMPLETE, + thumbnail: json.data.serveUrl, + }, + }); + }) + .catch((err) => { + let message = err.message; - // This sucks but ¯\_(ツ)_/¯ - if (message === 'Failed to fetch') { - message = downMessage; - } - const userInput = [fileName, fileExt, fileType, thumbnail]; - uploadError(`${message}\nUser input: ${userInput.join(', ')}`); + // This sucks but ¯\_(ツ)_/¯ + if (message === 'Failed to fetch') { + message = downMessage; + } + const userInput = [fileName, fileExt, fileType, thumbnail]; + uploadError(`${message}\nUser input: ${userInput.join(', ')}`); + }); + }; + + dispatch({ + type: ACTIONS.UPDATE_PUBLISH_FORM, + data: { uploadThumbnailStatus: THUMBNAIL_STATUSES.IN_PROGRESS }, + }); + + if (fsAdapter && fsAdapter.readFile && filePath) { + fsAdapter.readFile(filePath, 'base64').then((base64Image) => { + fileExt = 'png'; + fileName = 'thumbnail.png'; + fileType = 'image/png'; + + const data = new FormData(); + const name = makeid(); + data.append('name', name); + // $FlowFixMe + data.append('file', { uri: 'file://' + filePath, type: fileType, name: fileName }); + return doUpload(data); }); - }; - - dispatch({ - type: ACTIONS.UPDATE_PUBLISH_FORM, - data: { uploadThumbnailStatus: THUMBNAIL_STATUSES.IN_PROGRESS }, - }); - - if (fsAdapter && fsAdapter.readFile && filePath) { - fsAdapter.readFile(filePath, 'base64').then((base64Image) => { - fileExt = 'png'; - fileName = 'thumbnail.png'; - fileType = 'image/png'; + } else { + if (filePath && fs && path) { + thumbnail = fs.readFileSync(filePath); + fileExt = path.extname(filePath); + fileName = path.basename(filePath); + fileType = `image/${fileExt.slice(1)}`; + } else if (thumbnailBlob) { + fileExt = `.${thumbnailBlob.type && thumbnailBlob.type.split('/')[1]}`; + fileName = thumbnailBlob.name; + fileType = thumbnailBlob.type; + } else { + return null; + } const data = new FormData(); const name = makeid(); + const file = thumbnailBlob || (thumbnail && new File([thumbnail], fileName, { type: fileType })); data.append('name', name); // $FlowFixMe - data.append('file', { uri: 'file://' + filePath, type: fileType, name: fileName }); + data.append('file', file); return doUpload(data); - }); - } else { - if (filePath && fs && path) { - thumbnail = fs.readFileSync(filePath); - fileExt = path.extname(filePath); - fileName = path.basename(filePath); - fileType = `image/${fileExt.slice(1)}`; - } else if (thumbnailBlob) { - fileExt = `.${thumbnailBlob.type && thumbnailBlob.type.split('/')[1]}`; - fileName = thumbnailBlob.name; - fileType = thumbnailBlob.type; - } else { - return null; } - - const data = new FormData(); - const name = makeid(); - const file = thumbnailBlob || (thumbnail && new File([thumbnail], fileName, { type: fileType })); - data.append('name', name); - // $FlowFixMe - data.append('file', file); - return doUpload(data); - } -}; - -export const doPrepareEdit = (claim: StreamClaim, uri: string, fileInfo: FileListItem, fs: any) => ( - dispatch: Dispatch -) => { - const { name, amount, value = {} } = claim; - const channelName = (claim && claim.signing_channel && claim.signing_channel.name) || null; - const { - author, - description, - // use same values as default state - // fee will be undefined for free content - fee = { - amount: '0', - currency: 'LBC', - }, - languages, - release_time, - license, - license_url: licenseUrl, - thumbnail, - title, - tags, - } = value; - - const publishData: UpdatePublishFormData = { - name, - bid: Number(amount), - contentIsFree: fee.amount === '0', - author, - description, - fee, - languages, - releaseTime: release_time, - releaseTimeEdited: undefined, - thumbnail: thumbnail ? thumbnail.url : null, - title, - uri, - uploadThumbnailStatus: thumbnail ? THUMBNAIL_STATUSES.MANUAL : undefined, - licenseUrl, - nsfw: isClaimNsfw(claim), - tags: tags ? tags.map((tag) => ({ name: tag })) : [], }; - // Make sure custom licenses are mapped properly - // If the license isn't one of the standard licenses, map the custom license and description/url - if (!CC_LICENSES.some(({ value }) => value === license)) { - if (!license || license === NONE || license === PUBLIC_DOMAIN) { +export const doPrepareEdit = + (claim: StreamClaim, uri: string, fileInfo: FileListItem, fs: any) => (dispatch: Dispatch) => { + const { name, amount, value = {} } = claim; + const channelName = (claim && claim.signing_channel && claim.signing_channel.name) || null; + const { + author, + description, + // use same values as default state + // fee will be undefined for free content + fee = { + amount: '0', + currency: 'LBC', + }, + languages, + release_time, + license, + license_url: licenseUrl, + thumbnail, + title, + tags, + } = value; + + const publishData: UpdatePublishFormData = { + name, + bid: Number(amount), + contentIsFree: fee.amount === '0', + author, + description, + fee, + languages, + releaseTime: release_time, + releaseTimeEdited: undefined, + thumbnail: thumbnail ? thumbnail.url : null, + title, + uri, + uploadThumbnailStatus: thumbnail ? THUMBNAIL_STATUSES.MANUAL : undefined, + licenseUrl, + nsfw: isClaimNsfw(claim), + tags: tags ? tags.map((tag) => ({ name: tag })) : [], + }; + + // Make sure custom licenses are mapped properly + // If the license isn't one of the standard licenses, map the custom license and description/url + if (!CC_LICENSES.some(({ value }) => value === license)) { + if (!license || license === NONE || license === PUBLIC_DOMAIN) { + publishData.licenseType = license; + } else if (license && !licenseUrl && license !== NONE) { + publishData.licenseType = COPYRIGHT; + } else { + publishData.licenseType = OTHER; + } + + publishData.otherLicenseDescription = license; + } else { publishData.licenseType = license; - } else if (license && !licenseUrl && license !== NONE) { - publishData.licenseType = COPYRIGHT; - } else { - publishData.licenseType = OTHER; + } + if (channelName) { + publishData['channel'] = channelName; } - publishData.otherLicenseDescription = license; - } else { - publishData.licenseType = license; - } - if (channelName) { - publishData['channel'] = channelName; - } - - dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData }); -}; - -export const doPublish = (success: Function, fail: Function, preview: Function) => ( - dispatch: Dispatch, - getState: () => {} -) => { - if (!preview) { - dispatch({ type: ACTIONS.PUBLISH_START }); - } - - const state = getState(); - const myClaimForUri = selectMyClaimForUri(state); - const myChannels = selectMyChannelClaims(state); - // const myClaims = selectMyClaimsWithoutChannels(state); - // get redux publish form - const publishData = selectPublishFormValues(state); - - // destructure the data values - const { - name, - bid, - filePath, - description, - language, - releaseTimeEdited, - // license, - licenseUrl, - useLBRYUploader, - licenseType, - otherLicenseDescription, - thumbnail, - channel, - title, - contentIsFree, - fee, - tags, - // locations, - optimize, - } = publishData; - - // Handle scenario where we have a claim that has the same name as a channel we are publishing with. - const myClaimForUriEditing = myClaimForUri && myClaimForUri.name === name ? myClaimForUri : null; - - let publishingLicense; - switch (licenseType) { - case COPYRIGHT: - case OTHER: - publishingLicense = otherLicenseDescription; - break; - default: - publishingLicense = licenseType; - } - - // get the claim id from the channel name, we will use that instead - const namedChannelClaim = myChannels ? myChannels.find((myChannel) => myChannel.name === channel) : null; - const channelId = namedChannelClaim ? namedChannelClaim.claim_id : ''; - - const publishPayload: { - name: ?string, - bid: string, - description?: string, - channel_id?: string, - file_path?: string, - license_url?: string, - license?: string, - thumbnail_url?: string, - release_time?: number, - fee_currency?: string, - fee_amount?: string, - languages?: Array, - tags: Array, - locations?: Array, - blocking: boolean, - optimize_file?: boolean, - preview?: boolean, - remote_url?: string, - } = { - name, - title, - description, - locations: [], - bid: creditsToString(bid), - languages: [language], - tags: tags && tags.map((tag) => tag.name), - thumbnail_url: thumbnail, - blocking: true, - preview: false, + dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData }); }; - // Temporary solution to keep the same publish flow with the new tags api - // Eventually we will allow users to enter their own tags on publish - if (publishingLicense) { - publishPayload.license = publishingLicense; - } +export const doPublish = + (success: Function, fail: Function, preview: Function) => (dispatch: Dispatch, getState: () => {}) => { + if (!preview) { + dispatch({ type: ACTIONS.PUBLISH_START }); + } - if (licenseUrl) { - publishPayload.license_url = licenseUrl; - } + const state = getState(); + const myClaimForUri = selectMyClaimForUri(state); + const myChannels = selectMyChannelClaims(state); + // const myClaims = selectMyClaimsWithoutChannels(state); + // get redux publish form + const publishData = selectPublishFormValues(state); - if (thumbnail) { - publishPayload.thumbnail_url = thumbnail; - } + // destructure the data values + const { + name, + bid, + filePath, + description, + language, + releaseTimeEdited, + // license, + licenseUrl, + useLBRYUploader, + licenseType, + otherLicenseDescription, + thumbnail, + channel, + title, + contentIsFree, + fee, + tags, + // locations, + optimize, + } = publishData; - if (useLBRYUploader) { - publishPayload.tags.push('lbry-first'); - } + // Handle scenario where we have a claim that has the same name as a channel we are publishing with. + const myClaimForUriEditing = myClaimForUri && myClaimForUri.name === name ? myClaimForUri : null; - // Set release time to curret date. On edits, keep original release/transaction time as release_time - if (releaseTimeEdited) { - publishPayload.release_time = releaseTimeEdited; - } else if (myClaimForUriEditing && myClaimForUriEditing.value.release_time) { - publishPayload.release_time = Number(myClaimForUri.value.release_time); - } else if (myClaimForUriEditing && myClaimForUriEditing.timestamp) { - publishPayload.release_time = Number(myClaimForUriEditing.timestamp); - } else { - publishPayload.release_time = Number(Math.round(Date.now() / 1000)); - } + let publishingLicense; + switch (licenseType) { + case COPYRIGHT: + case OTHER: + publishingLicense = otherLicenseDescription; + break; + default: + publishingLicense = licenseType; + } - if (channelId) { - publishPayload.channel_id = channelId; - } + // get the claim id from the channel name, we will use that instead + const namedChannelClaim = myChannels ? myChannels.find((myChannel) => myChannel.name === channel) : null; + const channelId = namedChannelClaim ? namedChannelClaim.claim_id : ''; - if (myClaimForUriEditing && myClaimForUriEditing.value && myClaimForUriEditing.value.locations) { - publishPayload.locations = myClaimForUriEditing.value.locations; - } + const publishPayload: { + name: ?string, + bid: string, + description?: string, + channel_id?: string, + file_path?: string, + license_url?: string, + license?: string, + thumbnail_url?: string, + release_time?: number, + fee_currency?: string, + fee_amount?: string, + languages?: Array, + tags: Array, + locations?: Array, + blocking: boolean, + optimize_file?: boolean, + preview?: boolean, + remote_url?: string, + } = { + name, + title, + description, + locations: [], + bid: creditsToString(bid), + languages: [language], + tags: tags && tags.map((tag) => tag.name), + thumbnail_url: thumbnail, + blocking: true, + preview: false, + }; + // Temporary solution to keep the same publish flow with the new tags api + // Eventually we will allow users to enter their own tags on publish - if (!contentIsFree && fee && fee.currency && Number(fee.amount) > 0) { - publishPayload.fee_currency = fee.currency; - publishPayload.fee_amount = creditsToString(fee.amount); - } + if (publishingLicense) { + publishPayload.license = publishingLicense; + } - if (optimize) { - publishPayload.optimize_file = true; - } + if (licenseUrl) { + publishPayload.license_url = licenseUrl; + } - // Only pass file on new uploads, not metadata only edits. - // The sdk will figure it out - if (filePath) publishPayload.file_path = filePath; + if (thumbnail) { + publishPayload.thumbnail_url = thumbnail; + } - if (preview) { - publishPayload.preview = true; - publishPayload.optimize_file = false; + if (useLBRYUploader) { + publishPayload.tags.push('lbry-first'); + } - return Lbry.publish(publishPayload).then((previewResponse: PublishResponse) => { - return preview(previewResponse); + // Set release time to curret date. On edits, keep original release/transaction time as release_time + if (releaseTimeEdited) { + publishPayload.release_time = releaseTimeEdited; + } else if (myClaimForUriEditing && myClaimForUriEditing.value.release_time) { + publishPayload.release_time = Number(myClaimForUri.value.release_time); + } else if (myClaimForUriEditing && myClaimForUriEditing.timestamp) { + publishPayload.release_time = Number(myClaimForUriEditing.timestamp); + } else { + publishPayload.release_time = Number(Math.round(Date.now() / 1000)); + } + + if (channelId) { + publishPayload.channel_id = channelId; + } + + if (myClaimForUriEditing && myClaimForUriEditing.value && myClaimForUriEditing.value.locations) { + publishPayload.locations = myClaimForUriEditing.value.locations; + } + + if (!contentIsFree && fee && fee.currency && Number(fee.amount) > 0) { + publishPayload.fee_currency = fee.currency; + publishPayload.fee_amount = creditsToString(fee.amount); + } + + if (optimize) { + publishPayload.optimize_file = true; + } + + // Only pass file on new uploads, not metadata only edits. + // The sdk will figure it out + if (filePath) publishPayload.file_path = filePath; + + if (preview) { + publishPayload.preview = true; + publishPayload.optimize_file = false; + + return Lbry.publish(publishPayload).then((previewResponse: PublishResponse) => { + return preview(previewResponse); + }, fail); + } + + return Lbry.publish(publishPayload).then((response: PublishResponse) => { + return success(response); }, fail); - } - - return Lbry.publish(publishPayload).then((response: PublishResponse) => { - return success(response); - }, fail); -}; + }; // Calls file_list until any reflecting files are done export const doCheckReflectingFiles = () => (dispatch: Dispatch, getState: GetState) => { diff --git a/yarn.lock b/yarn.lock index 25071062b..4af269233 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11549,6 +11549,7 @@ __metadata: lodash-es: ^4.17.21 mammoth: ^1.4.16 match-sorter: ^6.3.0 + mime: ^3.0.0 moment: ^2.29.2 node-abi: ^2.5.1 node-fetch: ^2.6.7 @@ -12576,6 +12577,15 @@ __metadata: languageName: node linkType: hard +"mime@npm:^3.0.0": + version: 3.0.0 + resolution: "mime@npm:3.0.0" + bin: + mime: cli.js + checksum: f43f9b7bfa64534e6b05bd6062961681aeb406a5b53673b53b683f27fcc4e739989941836a355eef831f4478923651ecc739f4a5f6e20a76487b432bfd4db928 + languageName: node + linkType: hard + "mimic-fn@npm:^1.0.0": version: 1.2.0 resolution: "mimic-fn@npm:1.2.0"