Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow only images in modal image uploader. #7672

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions electron/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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();
Expand Down
9 changes: 9 additions & 0 deletions flow-typed/file-with-path.js
Original file line number Diff line number Diff line change
@@ -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,
}
6 changes: 0 additions & 6 deletions flow-typed/web-file.js

This file was deleted.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 0 additions & 1 deletion ui/component/channelForm/view.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,6 @@ function ChannelForm(props: Props) {
uri={uri}
thumbnailPreview={thumbnailPreview}
allowGifs
showDelayedMessage={isUpload.thumbnail}
setThumbUploadError={setThumbError}
thumbUploadError={thumbError}
/>
Expand Down
14 changes: 7 additions & 7 deletions ui/component/common/file-list.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import React from 'react';
import { useRadioState, Radio, RadioGroup } from 'reakit/Radio';

type Props = {
files: Array<WebFile>,
onChange: (WebFile | void) => void,
files: Array<File>,
onChange: (File | void) => void,
};

type RadioProps = {
Expand All @@ -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);
Expand All @@ -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);
}
Expand Down
34 changes: 25 additions & 9 deletions ui/component/common/file-selector.jsx
Original file line number Diff line number Diff line change
@@ -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<Props> {
Expand Down Expand Up @@ -41,7 +43,7 @@ class FileSelector extends React.PureComponent<Props> {
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
};
Expand All @@ -64,13 +66,27 @@ class FileSelector extends React.PureComponent<Props> {
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 = () => {
Expand Down
39 changes: 22 additions & 17 deletions ui/component/fileDrop/view.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<WebFile> }) => void,
openModal: (id: string, { files: Array<File> }) => void,
// React router
history: {
entities: {}[],
Expand All @@ -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<?WebFile>(null);
const [target, setTarget] = React.useState<?File>(null);
const hideTimer = React.useRef(null);
const targetTimer = React.useRef(null);
const navigationTimer = React.useRef(null);
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion ui/component/postEditor/view.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type Props = {
uri: ?string,
label: ?string,
disabled: ?boolean,
filePath: string | WebFile,
filePath: File,
fileText: ?string,
fileMimeType: ?string,
streamingUrl: ?string,
Expand Down
33 changes: 9 additions & 24 deletions ui/component/publishFile/view.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Props = {
mode: ?string,
name: ?string,
title: ?string,
filePath: string | WebFile,
filePath: ?string,
fileMimeType: ?string,
isStillEditing: boolean,
balance: number,
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -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('.'))) || '';
Expand All @@ -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);
}

Expand Down
Loading