Merge branch 'master' into master

This commit is contained in:
Thomas Zarebczan 2018-07-12 07:53:05 -07:00 committed by GitHub
commit 455188dff3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 640 additions and 192 deletions

View file

@ -5,6 +5,42 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
## [Unreleased]
### Fixed
### Added
### Changed
## [0.22.2] - 2018-07-09
### Fixed
* Fixed 'Get Credits' screen so the app doesn't break when LBC is unavailable on ShapeShift ([#1739](https://github.com/lbryio/lbry-app/pull/1739))
## [0.22.1] - 2018-07-05
### Added
### Fixed
* Take previous bid amount into account when determining how much users have available to deposit ([#1725](https://github.com/lbryio/lbry-app/pull/1725))
* Sidebar sizing on larger screens ([#1709](https://github.com/lbryio/lbry-app/pull/1709))
* Publishing scenario while editing and changing URI ([#1716](https://github.com/lbryio/lbry-app/pull/1716))
* Fix can't right click > paste into description on publish ([#1664](https://github.com/lbryio/lbry-app/issues/1664))
* Mac/Linux error when starting app up too quickly after shutdown ([#1727](https://github.com/lbryio/lbry-app/pull/1727))
* Console errors when multiple downloads for same claim exist ([#1724](https://github.com/lbryio/lbry-app/pull/1724))
* App version in dev mode ([#1722](https://github.com/lbryio/lbry-app/pull/1722))
* Long URI name displays in transaction list/Help ([#1694](https://github.com/lbryio/lbry-app/pull/1694))/([#1692](https://github.com/lbryio/lbry-app/pull/1692))
* Edit option missing from certain published claims ([#175](https://github.com/lbryio/lbry-desktop/issues/1756))
### Changed
* Show claim name, instead of URI, when loading a channel([#1711](https://github.com/lbryio/lbry-app/pull/1711))
* Updated LBRY daemon to 0.20.3 which contains some availability improvements ([v0.20.3](https://github.com/lbryio/lbry/releases/tag/v0.20.3))
## [0.22.0] - 2018-06-26
### Added
* Ability to upload thumbnails through spee.ch while publishing ([#1248](https://github.com/lbryio/lbry-app/pull/1248))
* QR code for wallet address to Send and Receive page ([#1582](https://github.com/lbryio/lbry-app/pull/1582))

View file

@ -3,7 +3,7 @@
[![Build Status](https://travis-ci.org/lbryio/lbry-app.svg?branch=master)](https://travis-ci.org/lbryio/lbry-app)
[![Dependencies](https://david-dm.org/lbryio/lbry-app/status.svg)](https://david-dm.org/lbryio/lbry-app)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/78b627d4f5524792adc48719835e1523)](https://www.codacy.com/app/LBRY/lbry-app?utm_source=github.com&utm_medium=referral&utm_content=lbryio/lbry-app&utm_campaign=Badge_Grade)
[![chat on Discord](https://img.shields.io/discord/362322208485277697.svg?logo=discord)](https://discord.gg/U5aRyN6)
[![chat on Discord](https://img.shields.io/discord/362322208485277697.svg?logo=discord)](https://chat.lbry.io)
The LBRY app is a graphical browser for the decentralized content marketplace provided by the
[LBRY](https://lbry.io) protocol. It is essentially the
@ -22,17 +22,18 @@ We provide installers for Windows, macOS (v10.9 or greater), and Debian-based Li
| Latest Pre-release | [Download](https://lbry.io/get/lbry.pre.exe) | [Download](https://lbry.io/get/lbry.pre.dmg) | [Download](https://lbry.io/get/lbry.pre.deb)
Our [releases page](https://github.com/lbryio/lbry-app/releases) also contains the latest
release, pre-releases, and past builds.
release, pre-releases, and past builds.
*Note: If the deb fails to install using the Ubuntu Software Center, install manually via `sudo dpkg -i <path to deb>`. You'll need to run `sudo apt-get install -f` if this is the first time installing it to install dependencies*
To install from source or make changes to the application, continue to the next section below.
**Community maintained** builds for Arch Linux and Flatpak are available, see below. These installs will need to be updated manually as the in-app update process only supports deb installs at this time.
*Note: If coming from a deb install, the directory structure is different and you'll need to [migrate data](https://lbry.io/faq/backup-data).*
| | Flatpak | Arch
| | Flatpak | Arch
| --------------------- | ------------------------------------------| --------------------------------------------
| Latest Release | [FlatHub Page](https://flathub.org/apps/details/io.lbry.lbry-app) | [AUR Package](https://aur.archlinux.org/packages/lbry-app-bin/)
| Maintainers | [@choofee](https://github.com/choffee)/[@iuyte](https://github.com/iuyte) | [@kcseb]()/[@TimurKiyivinski](https://github.com/TimurKiyivinski)
| Maintainers | [@choofee](https://github.com/choffee)/[@iuyte](https://github.com/iuyte) | [@kcseb]()/[@TimurKiyivinski](https://github.com/TimurKiyivinski)
## Usage
Double click the installed application to browse with the LBRY network.
@ -48,7 +49,7 @@ Double click the installed application to browse with the LBRY network.
#### Steps
1. Clone this repository: `git clone https://github.com/lbryio/lbry-app`
1. Clone (or [fork](https://help.github.com/articles/fork-a-repo/)) this repository: `git clone https://github.com/lbryio/lbry-app`
2. Change directories into the downloaded folder: `cd lbry-app`
3. Install the dependencies: `yarn`
4. Run the app: `yarn dev`

View file

@ -1,6 +1,6 @@
{
"name": "LBRY",
"version": "0.22.0",
"version": "0.22.2",
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
"keywords": [
"lbry"
@ -30,8 +30,8 @@
"flow-defs": "flow-typed install",
"release": "yarn compile && electron-builder build",
"precommit": "lint-staged",
"postinstall": "electron-builder install-app-deps & node build/downloadDaemon.js",
"clean": "rm -r node_modules && yarn cache clean lbry-redux && yarn"
"preinstall": "yarn cache clean lbry-redux",
"postinstall": "electron-builder install-app-deps & node build/downloadDaemon.js"
},
"dependencies": {
"bluebird": "^3.5.1",
@ -48,8 +48,9 @@
"formik": "^0.10.4",
"hast-util-sanitize": "^1.1.2",
"keytar": "^4.2.1",
"lbry-redux": "lbryio/lbry-redux#201d78b68a329065ee5d2a03bfb1607ea0666588",
"lbry-redux": "lbryio/lbry-redux#a0d2d1ac532ade639d39c92f79678ac26e904dfd",
"localforage": "^1.7.1",
"mime": "^2.3.1",
"mixpanel-browser": "^2.17.1",
"moment": "^2.22.0",
"qrcode.react": "^0.8.0",
@ -60,7 +61,7 @@
"react-modal": "^3.1.7",
"react-paginate": "^5.2.1",
"react-redux": "^5.0.3",
"react-simplemde-editor": "^3.6.15",
"react-simplemde-editor": "^3.6.16",
"react-toggle": "^4.0.2",
"react-transition-group": "1.x",
"redux": "^3.6.0",
@ -76,6 +77,7 @@
"semver": "^5.3.0",
"shapeshift.io": "^1.3.1",
"source-map-support": "^0.5.4",
"stream-to-blob-url": "^2.1.1",
"tree-kill": "^1.1.0",
"y18n": "^4.0.0"
},
@ -127,7 +129,7 @@
"yarn": "^1.3"
},
"lbrySettings": {
"lbrynetDaemonVersion": "0.20.2",
"lbrynetDaemonVersion": "0.20.3",
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-daemon-vDAEMONVER-OSNAME.zip",
"lbrynetDaemonDir": "static/daemon",
"lbrynetDaemonFileName": "lbrynet-daemon"

View file

@ -14,7 +14,7 @@ export default appState => {
defaultHeight: height,
});
let windowConfiguration = {
const windowConfiguration = {
backgroundColor: '#44b098',
minWidth: 950,
minHeight: 600,
@ -26,17 +26,13 @@ export default appState => {
// If state is undefined, create window as maximized.
width: windowState.width === undefined ? width : windowState.width,
height: windowState.height === undefined ? height : windowState.height,
};
// Disable renderer process's webSecurity on development to enable CORS.
windowConfiguration = isDev
? {
...windowConfiguration,
webPreferences: {
webSecurity: false,
},
}
: windowConfiguration;
webPreferences: {
// Disable renderer process's webSecurity on development to enable CORS.
webSecurity: !isDev,
plugins: true,
},
};
const rendererURL = isDev
? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`

View file

@ -11,6 +11,7 @@ import isDev from 'electron-is-dev';
import Daemon from './Daemon';
import createTray from './createTray';
import createWindow from './createWindow';
import pjson from '../../package.json';
autoUpdater.autoDownload = true;
@ -58,16 +59,18 @@ app.on('ready', async () => {
if (!isDaemonRunning) {
daemon = new Daemon();
daemon.on('exit', () => {
daemon = null;
if (!appState.isQuitting) {
dialog.showErrorBox(
'Daemon has Exited',
'The daemon may have encountered an unexpected error, or another daemon instance is already running. \n\n' +
'For more information please visit: \n' +
'https://lbry.io/faq/startup-troubleshooting'
);
if (!isDev) {
daemon = null;
if (!appState.isQuitting) {
dialog.showErrorBox(
'Daemon has Exited',
'The daemon may have encountered an unexpected error, or another daemon instance is already running. \n\n' +
'For more information please visit: \n' +
'https://lbry.io/faq/startup-troubleshooting'
);
}
app.quit();
}
app.quit();
});
daemon.launch();
}
@ -82,7 +85,9 @@ app.on('ready', async () => {
});
app.on('activate', () => {
rendererWindow.show();
if (rendererWindow) {
rendererWindow.show();
}
});
app.on('will-quit', event => {
@ -119,6 +124,10 @@ app.on('will-quit', event => {
daemon.quit();
event.preventDefault();
}
if (rendererWindow) {
rendererWindow = null;
}
});
// https://electronjs.org/docs/api/app#event-will-finish-launching
@ -171,7 +180,7 @@ ipcMain.on('version-info-requested', () => {
return ver.replace(/([^-])rc/, '$1-rc');
}
const localVersion = app.getVersion();
const localVersion = pjson.version;
const latestReleaseAPIURL = 'https://api.github.com/repos/lbryio/lbry-app/releases/latest';
const opts = {
headers: {

View file

@ -23,6 +23,7 @@ type Props = {
noPadding: ?boolean, // to remove padding and allow circular buttons
uppercase: ?boolean,
iconColor: ?string,
tourniquet: ?boolean, // to shorten the button and ellipsis, only use for links
};
class Button extends React.PureComponent<Props> {
@ -50,6 +51,7 @@ class Button extends React.PureComponent<Props> {
noPadding,
uppercase,
iconColor,
tourniquet,
...otherProps
} = this.props;
@ -69,6 +71,7 @@ class Button extends React.PureComponent<Props> {
'btn--link': button === 'link',
'btn--external-link': button === 'link' && href,
'btn--uppercase': uppercase,
'btn--tourniquet': tourniquet,
}
: 'btn--no-style',
className

View file

@ -5,10 +5,20 @@ type Props = {
message: ?string,
};
const BusyIndicator = (props: Props) => (
<span className="busy-indicator">
{props.message} <span className="busy-indicator__loader" />
</span>
);
class BusyIndicator extends React.PureComponent<Props> {
static defaultProps = {
message: '',
};
render() {
const { message } = this.props;
return (
<span className="busy-indicator">
{message} <span className="busy-indicator__loader" />
</span>
);
}
}
export default BusyIndicator;

View file

@ -6,6 +6,7 @@ import MarkdownPreview from 'component/common/markdown-preview';
import SimpleMDE from 'react-simplemde-editor';
import 'simplemde/dist/simplemde.min.css';
import Toggle from 'react-toggle';
import { openEditorMenu } from 'util/contextMenu';
type Props = {
name: string,
@ -54,11 +55,21 @@ export class FormField extends React.PureComponent<Props> {
</select>
);
} else if (type === 'markdown') {
const stopContextMenu = event => {
event.preventDefault();
event.stopPropagation();
};
const handleEvents = {
contextmenu(codeMirror, event) {
openEditorMenu(event, codeMirror);
},
};
input = (
<div className="form-field--SimpleMDE">
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
<SimpleMDE
{...inputProps}
type="textarea"
events={handleEvents}
options={{
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
previewRender(plainText) {

View file

@ -3,8 +3,8 @@ import React from 'react';
import Spinner from 'component/spinner';
type Props = {
spinner: boolean,
status: string,
spinner: boolean,
};
class LoadingScreen extends React.PureComponent<Props> {
@ -17,8 +17,7 @@ class LoadingScreen extends React.PureComponent<Props> {
return (
<div className="content__loading">
{spinner && <Spinner light />}
<span className="content__loading-text">{status}</span>
{status && <span className="content__loading-text">{status}</span>}
</div>
);
}

View file

@ -152,7 +152,12 @@ class FileList extends React.PureComponent<Props, State> {
}
this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
const { name: claimName, claim_name: claimNameDownloaded, claim_id: claimId } = fileInfo;
const {
name: claimName,
claim_name: claimNameDownloaded,
claim_id: claimId,
outpoint,
} = fileInfo;
const uriParams = {};
// This is unfortunate
@ -162,7 +167,8 @@ class FileList extends React.PureComponent<Props, State> {
uriParams.claimId = claimId;
const uri = buildURI(uriParams);
content.push(<FileCard key={uri} uri={uri} checkPending={checkPending} />);
// See https://github.com/lbryio/lbry-app/issues/1327 for discussion around using outpoint as the key
content.push(<FileCard key={outpoint} uri={uri} checkPending={checkPending} />);
});
return (

View file

@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import { THEME } from 'constants/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import FileRender from './view';
const select = (state, props) => ({
currentTheme: makeSelectClientSetting(THEME)(state),
});
const perform = dispatch => ({});
export default connect(
select,
perform
)(FileRender);

View file

@ -0,0 +1,47 @@
// @flow
import React from 'react';
import LoadingScreen from 'component/common/loading-screen';
import PdfViewer from 'component/viewers/pdfViewer';
type Props = {
mediaType: string,
source: {
filePath: string,
fileType: string,
downloadPath: string,
},
currentTheme: string,
};
class FileRender extends React.PureComponent<Props> {
renderViewer() {
const { source, mediaType, currentTheme } = this.props;
const viewerProps = { source, theme: currentTheme };
// Supported mediaTypes
const mediaTypes = {
// '3D-file': <ThreeViewer {...viewerProps}/>,
// Add routes to viewer...
};
// Supported fileType
const fileTypes = {
pdf: <PdfViewer {...viewerProps} />,
// Add routes to viewer...
};
const { fileType } = source;
const viewer = mediaType && source && (mediaTypes[mediaType] || fileTypes[fileType]);
const unsupportedMessage = __("Sorry, looks like we can't preview this file.");
const unsupported = <LoadingScreen status={unsupportedMessage} spinner={false} />;
// Return viewer
return viewer || unsupported;
}
render() {
return <div className="file-render">{this.renderViewer()}</div>;
}
}
export default FileRender;

View file

@ -17,7 +17,7 @@ import {
import { makeSelectClientSetting, selectShowNsfw } from 'redux/selectors/settings';
import { selectMediaPaused, makeSelectMediaPositionForUri } from 'redux/selectors/media';
import { selectPlayingUri } from 'redux/selectors/content';
import Video from './view';
import FileViewer from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
@ -45,4 +45,7 @@ const perform = dispatch => ({
savePosition: (claimId, position) => dispatch(savePosition(claimId, position)),
});
export default connect(select, perform)(Video);
export default connect(
select,
perform
)(FileViewer);

View file

@ -1,6 +1,7 @@
// @flow
import React from 'react';
import Button from 'component/button';
import * as icons from 'constants/icons';
type Props = {
play: () => void,
@ -14,8 +15,8 @@ class VideoPlayButton extends React.PureComponent<Props> {
const { fileInfo, mediaType, isLoading, play } = this.props;
const disabled = isLoading || fileInfo === undefined;
const doesPlayback = ['audio', 'video'].indexOf(mediaType) !== -1;
const icon = doesPlayback ? 'Play' : 'Folder';
const label = doesPlayback ? 'Play' : 'View';
const icon = doesPlayback ? icons.PLAY : icons.EYE;
const label = doesPlayback ? __('Play') : __('View');
return <Button button="primary" disabled={disabled} label={label} icon={icon} onClick={play} />;
}

View file

@ -1,13 +1,17 @@
/* eslint-disable */
import React from 'react';
import { remote } from 'electron';
import Thumbnail from 'component/common/thumbnail';
import player from 'render-media';
import fs from 'fs';
import LoadingScreen from './loading-screen';
import path from 'path';
import player from 'render-media';
import toBlobURL from 'stream-to-blob-url';
import FileRender from 'component/fileRender';
import Thumbnail from 'component/common/thumbnail';
import LoadingScreen from 'component/common/loading-screen';
class VideoPlayer extends React.PureComponent {
static MP3_CONTENT_TYPES = ['audio/mpeg3', 'audio/mpeg'];
static FILE_MEDIA_TYPES = ['e-book', 'comic-book', 'document', '3D-file'];
constructor(props) {
super(props);
@ -16,6 +20,7 @@ class VideoPlayer extends React.PureComponent {
hasMetadata: false,
startedPlaying: false,
unplayable: false,
fileSource: null,
};
this.togglePlayListener = this.togglePlay.bind(this);
@ -29,7 +34,7 @@ class VideoPlayer extends React.PureComponent {
componentDidMount() {
const container = this.media;
const { contentType, changeVolume, volume, position, claim } = this.props;
const { downloadCompleted, contentType, changeVolume, volume, position, claim } = this.props;
const loadedMetadata = () => {
this.setState({ hasMetadata: true, startedPlaying: true });
@ -51,7 +56,13 @@ class VideoPlayer extends React.PureComponent {
// use renderAudio override for mp3
if (VideoPlayer.MP3_CONTENT_TYPES.indexOf(contentType) > -1) {
this.renderAudio(container, null, false);
} else {
}
// Render custom viewer: FileRender
else if (this.fileType()) {
downloadCompleted && this.renderFile();
}
// Render default viewer: render-media (video, audio, img, iframe)
else {
player.append(
this.file(),
container,
@ -89,7 +100,7 @@ class VideoPlayer extends React.PureComponent {
componentDidUpdate() {
const { contentType, downloadCompleted } = this.props;
const { startedPlaying } = this.state;
const { startedPlaying, fileSource } = this.state;
if (this.playableType() && !startedPlaying && downloadCompleted) {
const container = this.media.children[0];
@ -102,6 +113,8 @@ class VideoPlayer extends React.PureComponent {
controls: true,
});
}
} else if (this.fileType() && !fileSource && downloadCompleted) {
this.renderFile();
}
}
@ -159,6 +172,40 @@ class VideoPlayer extends React.PureComponent {
return ['audio', 'video'].indexOf(mediaType) !== -1;
}
supportedType() {
// Files supported by render-media
const { contentType, mediaType } = this.props;
return Object.values(player.mime).indexOf(contentType) !== -1;
}
fileType() {
// This files are supported using a custom viewer
const { mediaType } = this.props;
return VideoPlayer.FILE_MEDIA_TYPES.indexOf(mediaType) > -1;
}
renderFile() {
// This is what render-media does with unplayable files
const { filename, downloadPath, contentType, mediaType } = this.props;
toBlobURL(fs.createReadStream(downloadPath), contentType, (err, url) => {
if (err) {
this.setState({ unsupported: true });
return false;
}
// File to render
const fileSource = {
downloadPath,
filePath: url,
fileType: path.extname(filename).substring(1),
};
// Update state
this.setState({ fileSource });
});
}
renderAudio(container, autoplay) {
if (container.firstChild) {
container.firstChild.remove();
@ -173,25 +220,61 @@ class VideoPlayer extends React.PureComponent {
container.appendChild(audio);
}
showLoadingScreen(isFileType, isPlayableType) {
const { mediaType } = this.props;
const { hasMetadata, unplayable, unsupported, fileSource } = this.state;
const loader = {
isLoading: false,
loadingStatus: null,
};
// Loading message
const noFileMessage = __('Waiting for blob.');
const noMetadataMessage = __('Waiting for metadata.');
// Error message
const unplayableMessage = __("Sorry, looks like we can't play this file.");
const unsupportedMessage = __("Sorry, looks like we can't preview this file.");
// Files
const isLoadingFile = !fileSource && isFileType;
const isUnsupported =
(mediaType === 'application' || !this.supportedType()) && !isFileType && !isPlayableType;
// Media (audio, video)
const isUnplayable = isPlayableType && unplayable;
const isLoadingMetadata = isPlayableType && (!hasMetadata && !unplayable);
// Show loading message
if (isLoadingFile || isLoadingMetadata) {
loader.loadingStatus = isFileType ? noFileMessage : noMetadataMessage;
loader.isLoading = true;
// Show unsupported error message
} else if (isUnsupported || isUnplayable) {
loader.loadingStatus = isUnsupported ? unsupportedMessage : unplayableMessage;
}
return loader;
}
render() {
const { mediaType, poster } = this.props;
const { hasMetadata, unplayable } = this.state;
const noMetadataMessage = 'Waiting for metadata.';
const unplayableMessage = "Sorry, looks like we can't play this file.";
const hideMedia = this.playableType() && !hasMetadata && !unplayable;
const { mediaType } = this.props;
const { fileSource } = this.state;
const isFileType = this.fileType();
const isFileReady = fileSource && isFileType;
const isPlayableType = this.playableType();
const { isLoading, loadingStatus } = this.showLoadingScreen(isFileType, isPlayableType);
return (
<React.Fragment>
{['audio', 'application'].indexOf(mediaType) !== -1 &&
(!this.playableType() || hasMetadata) &&
!unplayable && <Thumbnail src={poster} />}
{this.playableType() &&
!hasMetadata &&
!unplayable && <LoadingScreen status={noMetadataMessage} />}
{unplayable && <LoadingScreen status={unplayableMessage} spinner={false} />}
{loadingStatus && <LoadingScreen status={loadingStatus} spinner={isLoading} />}
{isFileReady && <FileRender source={fileSource} mediaType={mediaType} />}
<div
className={'content__view--container'}
style={{ opacity: hideMedia ? 0 : 1 }}
style={{ opacity: isLoading ? 0 : 1 }}
ref={container => {
this.media = container;
}}

View file

@ -1,11 +1,10 @@
// @flow
import React from 'react';
import { Lbry } from 'lbry-redux';
import classnames from 'classnames';
import type { Claim } from 'types/claim';
import VideoPlayer from './internal/player';
import VideoPlayButton from './internal/play-button';
import LoadingScreen from './internal/loading-screen';
import LoadingScreen from 'component/common/loading-screen';
import Player from './internal/player';
import PlayButton from './internal/play-button';
const SPACE_BAR_KEYCODE = 32;
@ -40,9 +39,10 @@ type Props = {
obscureNsfw: boolean,
play: string => void,
searchBarFocused: boolean,
mediaType: string,
};
class Video extends React.PureComponent<Props> {
class FileViewer extends React.PureComponent<Props> {
constructor() {
super();
@ -123,12 +123,12 @@ class Video extends React.PureComponent<Props> {
mediaPosition,
className,
obscureNsfw,
mediaType,
} = this.props;
const isPlaying = playingUri === uri;
const isReadyToPlay = fileInfo && fileInfo.written_bytes > 0;
const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw;
const mediaType = Lbry.getMediaType(contentType, fileInfo && fileInfo.file_name);
let loadStatusMessage = '';
@ -156,7 +156,7 @@ class Video extends React.PureComponent<Props> {
<LoadingScreen status={loadStatusMessage} />
</div>
) : (
<VideoPlayer
<Player
filename={fileInfo.file_name}
poster={poster}
downloadPath={fileInfo.download_path}
@ -183,7 +183,7 @@ class Video extends React.PureComponent<Props> {
className={layoverClass}
style={layoverStyle}
>
<VideoPlayButton
<PlayButton
play={e => {
e.stopPropagation();
this.playContent();
@ -200,4 +200,4 @@ class Video extends React.PureComponent<Props> {
}
}
export default Video;
export default FileViewer;

View file

@ -25,11 +25,13 @@ class BidHelpText extends React.PureComponent<Props> {
} = this.props;
if (!uri) {
return __('Create a URL for this content');
return __('Create a URL for this content.');
}
if (isStillEditing) {
return __('You are currently editing this claim');
return __(
'You are currently editing this claim. If you change the URL, you will need reselect a file.'
);
}
if (isResolvingUri) {
@ -61,10 +63,10 @@ class BidHelpText extends React.PureComponent<Props> {
<React.Fragment>
{__('A deposit greater than')} {winningBidForClaimUri} {__('is needed to win')}
{` ${uri}. `}
{__('However, you can still get this URL for any amount')}
{__('However, you can still get this URL for any amount.')}
</React.Fragment>
) : (
__('Any amount will give you the winning bid')
__('Any amount will give you the winning bid.')
);
}
}

View file

@ -152,14 +152,21 @@ class PublishForm extends React.PureComponent<Props> {
}
handleBidChange(bid: number) {
const { balance, updatePublishForm } = this.props;
const { balance, updatePublishForm, myClaimForUri } = this.props;
let previousBidAmount = 0;
if (myClaimForUri) {
previousBidAmount = myClaimForUri.amount;
}
const totalAvailableBidAmount = previousBidAmount + balance;
let bidError;
if (bid === 0) {
bidError = __('Deposit cannot be 0');
} else if (balance === bid) {
} else if (totalAvailableBidAmount === bid) {
bidError = __('Please decrease your deposit to account for transaction fees');
} else if (balance < bid) {
} else if (totalAvailableBidAmount < bid) {
bidError = __('Deposit cannot be higher than your balance');
} else if (bid <= MINIMUM_PUBLISH_BID) {
bidError = __('Your deposit must be higher');
@ -237,32 +244,54 @@ class PublishForm extends React.PureComponent<Props> {
}
checkIsFormValid() {
const { name, nameError, title, bid, bidError, tosAccepted } = this.props;
return name && !nameError && title && bid && !bidError && tosAccepted;
const {
name,
nameError,
title,
bid,
bidError,
tosAccepted,
editingURI,
isStillEditing,
filePath,
} = this.props;
// If they are editing, they don't need a new file chosen
const formValidLessFile = name && !nameError && title && bid && !bidError && tosAccepted;
return editingURI && !filePath ? isStillEditing && formValidLessFile : formValidLessFile;
}
renderFormErrors() {
const { name, nameError, title, bid, bidError, tosAccepted } = this.props;
const {
name,
nameError,
title,
bid,
bidError,
tosAccepted,
editingURI,
filePath,
isStillEditing,
} = this.props;
if (nameError || bidError) {
// There will be inline errors if either of these exist
// These are just extra help at the bottom of the screen
// There could be multiple bid errors, so just duplicate it at the bottom
return (
<div className="card__subtitle form-field__error">
{nameError && <div>{__('The URL you created is not valid.')}</div>}
{bidError && <div>{bidError}</div>}
</div>
);
}
const isFormValid = this.checkIsFormValid();
// These are extra help
// If there is an error it will be presented as an inline error as well
return (
<div className="card__content card__subtitle card__subtitle--block form-field__error">
{!title && <div>{__('A title is required')}</div>}
{!name && <div>{__('A URL is required')}</div>}
{!bid && <div>{__('A bid amount is required')}</div>}
{!tosAccepted && <div>{__('You must agree to the terms of service')}</div>}
</div>
!isFormValid && (
<div className="card__content card__subtitle form-field__error">
{!title && <div>{__('A title is required')}</div>}
{!name && <div>{__('A URL is required')}</div>}
{name && nameError && <div>{__('The URL you created is not valid')}</div>}
{!bid && <div>{__('A bid amount is required')}</div>}
{!!bid && bidError && <div>{bidError}</div>}
{!tosAccepted && <div>{__('You must agree to the terms of service')}</div>}
{!!editingURI &&
!isStillEditing &&
!filePath && <div>{__('You need to reselect a file after changing the LBRY URL')}</div>}
</div>
)
);
}
@ -312,10 +341,10 @@ class PublishForm extends React.PureComponent<Props> {
return (
<Form onSubmit={this.handlePublish}>
<section className={classnames('card card--section')}>
<section className={classnames('card card--section', { 'card--disabled': publishing })}>
<div className="card__title">{__('Content')}</div>
<div className="card__subtitle">
{editingURI ? __('Editing a claim') : __('What are you publishing?')}
{isStillEditing ? __('Editing a claim') : __('What are you publishing?')}
</div>
{(filePath || !!editingURI) && (
<div className="card-media__internal-links">
@ -328,7 +357,7 @@ class PublishForm extends React.PureComponent<Props> {
</div>
)}
<FileSelector currentPath={filePath} onFileChosen={this.handleFileChange} />
{!!editingURI && (
{!!isStillEditing && (
<p className="card__content card__subtitle">
{__("If you don't choose a file, the file from your existing claim")}
{` "${name}" `}
@ -368,11 +397,11 @@ class PublishForm extends React.PureComponent<Props> {
<div className="card__title">{__('Thumbnail')}</div>
<div className="card__subtitle">
{uploadThumbnailStatus === THUMBNAIL_STATUSES.API_DOWN ? (
__('Enter a url for your thumbnail.')
__('Enter a URL for your thumbnail.')
) : (
<React.Fragment>
{__(
'Upload your thumbnail to spee.ch, or enter the url manually. Learn more about spee.ch '
'Upload your thumbnail (.png/.jpg/.jpeg/.gif) to spee.ch, or enter the URL manually. Learn more about spee.ch '
)}
<Button button="link" label={__('here')} href="https://spee.ch/about" />.
</React.Fragment>

View file

@ -35,7 +35,7 @@ class SelectThumbnail extends React.PureComponent<Props> {
stretch
type="text"
name="content_thumbnail"
label={__('Url')}
label={__('URL')}
placeholder="http://spee.ch/mylogo"
value={thumbnail}
disabled={formDisabled}

View file

@ -56,7 +56,7 @@ class ActiveShapeShift extends React.PureComponent<Props> {
}
}
continousFetch: ?number;
continousFetch: ?IntervalID;
render() {
const {
@ -135,8 +135,8 @@ class ActiveShapeShift extends React.PureComponent<Props> {
{shiftState === statuses.NO_DEPOSITS &&
shiftReturnAddress && (
<div className="help">
If the transaction doesn't go through, ShapeShift will return your {shiftCoinType}{' '}
back to {shiftReturnAddress}
{__("If the transaction doesn't go through, ShapeShift will return your")}{' '}
{shiftCoinType} {__('back to')} {shiftReturnAddress}
</div>
)}
</div>

View file

@ -72,6 +72,7 @@ class TransactionListItem extends React.PureComponent<Props> {
{name &&
claimId && (
<Button
tourniquet
button="link"
navigate="/show"
navigateParams={{ uri: buildURI({ claimName: name, claimId }) }}

View file

@ -0,0 +1,37 @@
// @flow
import React from 'react';
type Props = {
source: {
fileType: string,
filePath: string,
downloadPath: string,
},
};
class PdfViewer extends React.PureComponent<Props> {
constructor(props) {
super(props);
this.viewer = React.createRef();
}
// TODO: Enable context-menu
stopContextMenu = event => {
event.preventDefault();
event.stopPropagation();
};
render() {
const { source } = this.props;
return (
<div className="file-render__viewer" onContextMenu={this.stopContextMenu}>
<webview
ref={this.viewer}
src={`chrome://pdf-viewer/index.html?src=file://${source.downloadPath}`}
/>
</div>
);
}
}
export default PdfViewer;

View file

@ -28,3 +28,5 @@ export const CHECK_SIMPLE = 'Check';
export const GLOBE = 'Globe';
export const EXTERNAL_LINK = 'ExternalLink';
export const GIFT = 'Gift';
export const EYE = 'Eye';
export const PLAY = 'Play';

View file

@ -1,3 +1,5 @@
export const NO_DEPOSITS = 'no_deposits';
export const RECEIVED = 'received';
export const COMPLETE = 'complete';
export const AVAILABLE = 'available';
export const UNAVAILABLE = 'unavailable';

View file

@ -48,7 +48,7 @@ class ChannelPage extends React.PureComponent<Props> {
this.props.navigate('/show', newParams);
}
paginate(e, totalPages: number) {
paginate(e: SyntheticKeyboardEvent<*>, totalPages: number) {
// Change page if enter was pressed, and the given page is between
// the first and the last.
const pageFromInput = Number(e.target.value);
@ -67,22 +67,21 @@ class ChannelPage extends React.PureComponent<Props> {
const { fetching, claimsInChannel, claim, page, totalPages } = this.props;
const { name, permanent_url: permanentUrl, claim_id: claimId } = claim;
const currentPage = parseInt((page || 1) - 1, 10);
let contentList;
if (fetching) {
contentList = <BusyIndicator message={__('Fetching content')} />;
} else {
contentList =
claimsInChannel && claimsInChannel.length ? (
<FileList sortByHeight hideFilter fileInfos={claimsInChannel} />
) : (
<span className="empty">{__('No content found.')}</span>
);
}
const contentList =
claimsInChannel && claimsInChannel.length ? (
<FileList sortByHeight hideFilter fileInfos={claimsInChannel} />
) : (
!fetching && <span className="empty">{__('No content found.')}</span>
);
return (
<Page notContained>
<section className="card__channel-info card__channel-info--large">
<h1>{name}</h1>
<h1>
{name}
{fetching && <BusyIndicator />}
</h1>
<div className="card__actions card__actions--no-margin">
<SubscribeButton uri={permanentUrl} channelName={name} />
<ViewOnWebButton claimId={claimId} claimName={name} />

View file

@ -1,7 +1,7 @@
// @flow
import * as React from 'react';
import { Lbry, buildURI, normalizeURI, MODALS } from 'lbry-redux';
import Video from 'component/video';
import { buildURI, normalizeURI, MODALS } from 'lbry-redux';
import FileViewer from 'component/fileViewer';
import Thumbnail from 'component/common/thumbnail';
import FilePrice from 'component/filePrice';
import FileDetails from 'component/fileDetails';
@ -14,7 +14,6 @@ import Button from 'component/button';
import SubscribeButton from 'component/subscribeButton';
import ViewOnWebButton from 'component/viewOnWebButton';
import Page from 'component/page';
import player from 'render-media';
import * as settings from 'constants/settings';
import type { Claim } from 'types/claim';
import type { Subscription } from 'types/subscription';
@ -22,6 +21,7 @@ import FileDownloadLink from 'component/fileDownloadLink';
import classnames from 'classnames';
import { FormField, FormRow } from 'component/common/form';
import ToolTip from 'component/common/tooltip';
import getMediaType from 'util/getMediaType';
type Props = {
claim: Claim,
@ -29,6 +29,7 @@ type Props = {
metadata: {
title: string,
thumbnail: string,
file_name: string,
nsfw: boolean,
},
contentType: string,
@ -49,6 +50,18 @@ type Props = {
};
class FilePage extends React.Component<Props> {
static PLAYABLE_MEDIA_TYPES = ['audio', 'video'];
static PREVIEW_MEDIA_TYPES = [
'text',
'model',
'image',
'3D-file',
'document',
// Bypass unplayable files
// TODO: Find a better way to detect supported types
'application',
];
constructor(props: Props) {
super(props);
@ -108,15 +121,19 @@ class FilePage extends React.Component<Props> {
navigate,
autoplay,
costInfo,
fileInfo,
} = this.props;
// File info
const { title, thumbnail } = metadata;
const { height, channel_name: channelName, value } = claim;
const { PLAYABLE_MEDIA_TYPES, PREVIEW_MEDIA_TYPES } = FilePage;
const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
const shouldObscureThumbnail = obscureNsfw && metadata.nsfw;
const { height, channel_name: channelName, value } = claim;
const mediaType = Lbry.getMediaType(contentType);
const isPlayable = Object.values(player.mime).includes(contentType) || mediaType === 'audio';
const fileName = fileInfo ? fileInfo.file_name : null;
const mediaType = getMediaType(contentType, fileName);
const showFile =
PLAYABLE_MEDIA_TYPES.includes(mediaType) || PREVIEW_MEDIA_TYPES.includes(mediaType);
const channelClaimId =
value && value.publisherSignature && value.publisherSignature.certificateId;
let subscriptionUri;
@ -150,8 +167,10 @@ class FilePage extends React.Component<Props> {
</section>
) : (
<section className="card">
{isPlayable && <Video className="content__embedded" uri={uri} />}
{!isPlayable &&
{showFile && (
<FileViewer className="content__embedded" uri={uri} mediaType={mediaType} />
)}
{!showFile &&
(thumbnail ? (
<Thumbnail shouldObscure={shouldObscureThumbnail} src={thumbnail} />
) : (
@ -160,7 +179,9 @@ class FilePage extends React.Component<Props> {
'content__empty--nsfw': shouldObscureThumbnail,
})}
>
<div className="card__media-text">{__('This content is not playable.')}</div>
<div className="card__media-text">
{__("Sorry, looks like we can't preview this file.")}
</div>
</div>
))}
<div className="card__content">
@ -182,34 +203,32 @@ class FilePage extends React.Component<Props> {
<UriIndicator uri={uri} link />
</div>
<div className="card__actions card__actions--no-margin card__actions--between">
{(claimIsMine || subscriptionUri || speechSharable) && (
<div className="card__actions">
{claimIsMine ? (
<Button
button="primary"
icon={icons.EDIT}
label={__('Edit')}
onClick={() => {
prepareEdit(claim, editUri);
navigate('/publish');
}}
/>
) : (
<SubscribeButton uri={subscriptionUri} channelName={channelName} />
)}
{!claimIsMine && (
<Button
button="alt"
icon={icons.GIFT}
label={__('Enjoy this? Send a tip')}
onClick={() => openModal({ id: MODALS.SEND_TIP }, { uri })}
/>
)}
{speechSharable && (
<ViewOnWebButton claimId={claim.claim_id} claimName={claim.name} />
)}
</div>
)}
<div className="card__actions">
{claimIsMine ? (
<Button
button="primary"
icon={icons.EDIT}
label={__('Edit')}
onClick={() => {
prepareEdit(claim, editUri);
navigate('/publish');
}}
/>
) : (
<SubscribeButton uri={subscriptionUri} channelName={channelName} />
)}
{!claimIsMine && (
<Button
button="alt"
icon={icons.GIFT}
label={__('Enjoy this? Send a tip')}
onClick={() => openModal({ id: MODALS.SEND_TIP }, { uri })}
/>
)}
{speechSharable && (
<ViewOnWebButton claimId={claim.claim_id} claimName={claim.name} />
)}
</div>
<div className="card__actions">
<FileDownloadLink uri={uri} />

View file

@ -1,6 +1,5 @@
import React from 'react';
import Button from 'component/button';
import { FileTile } from 'component/fileTile';
import FileList from 'component/fileList';
import Page from 'component/page';

View file

@ -1,5 +1,6 @@
// @flow
import React from 'react';
import { parseURI } from 'lbry-redux';
import BusyIndicator from 'component/common/busy-indicator';
import ChannelPage from 'page/channel';
import FilePage from 'page/file';
@ -39,10 +40,11 @@ class ShowPage extends React.PureComponent<Props> {
let innerContent = '';
if ((isResolvingUri && !claim) || !claim) {
const { claimName } = parseURI(uri);
innerContent = (
<Page>
<section className="card">
<h1>{uri}</h1>
<h1>{claimName}</h1>
<div className="card__content">
{isResolvingUri && <BusyIndicator message={__('Loading decentralized data...')} />}
{claim === null &&

View file

@ -253,17 +253,19 @@ export function doCheckUpgradeSubscribe() {
export function doCheckDaemonVersion() {
return dispatch => {
Lbry.version().then(({ lbrynet_version: lbrynetVersion }) => {
if (config.lbrynetDaemonVersion === lbrynetVersion) {
dispatch({
// Avoid the incompatible daemon modal if running in dev mode
// Lets you run a different daemon than the one specified in package.json
if (isDev || config.lbrynetDaemonVersion === lbrynetVersion) {
return dispatch({
type: ACTIONS.DAEMON_VERSION_MATCH,
});
return;
}
dispatch({
type: ACTIONS.DAEMON_VERSION_MISMATCH,
});
dispatch(
return dispatch(
doNotify({
id: MODALS.INCOMPATIBLE_DAEMON,
})

View file

@ -74,6 +74,7 @@ export const doResetThumbnailStatus = () => (dispatch: Dispatch): PromiseAction
export const doUploadThumbnail = (filePath: string, nsfw: boolean) => (dispatch: Dispatch) => {
const thumbnail = fs.readFileSync(filePath);
const fileExt = path.extname(filePath);
const fileName = path.basename(filePath);
const makeid = () => {
let text = '';
@ -100,9 +101,9 @@ export const doUploadThumbnail = (filePath: string, nsfw: boolean) => (dispatch:
const data = new FormData();
const name = makeid();
const blob = new Blob([thumbnail], { type: `image/${fileExt.slice(1)}` });
const file = new File([thumbnail], fileName, { type: `image/${fileExt.slice(1)}` });
data.append('name', name);
data.append('file', blob);
data.append('file', file);
data.append('nsfw', nsfw.toString());
return fetch('https://spee.ch/api/claim/publish', {
method: 'POST',

View file

@ -1,5 +1,6 @@
// @flow
import Promise from 'bluebird';
import * as SHAPESHIFT_STATUSES from 'constants/shape_shift';
import * as ACTIONS from 'constants/action_types';
import { coinRegexPatterns } from 'util/shape_shift';
import type {
@ -65,9 +66,15 @@ export const shapeShiftInit = () => (dispatch: Dispatch): ThunkAction => {
return shapeShift
.coinsAsync()
.then(coinData => {
if (coinData.LBC.status === SHAPESHIFT_STATUSES.UNAVAILABLE) {
return dispatch({
type: ACTIONS.GET_SUPPORTED_COINS_FAIL,
});
}
let supportedCoins = [];
Object.keys(coinData).forEach(symbol => {
if (coinData[symbol].status === 'available') {
if (coinData[symbol].status === SHAPESHIFT_STATUSES.UNAVAILABLE) {
supportedCoins.push(coinData[symbol]);
}
});
@ -81,7 +88,7 @@ export const shapeShiftInit = () => (dispatch: Dispatch): ThunkAction => {
type: ACTIONS.GET_SUPPORTED_COINS_SUCCESS,
data: supportedCoins,
});
dispatch(getCoinStats(supportedCoins[0]));
return dispatch(getCoinStats(supportedCoins[0]));
})
.catch(err => dispatch({ type: ACTIONS.GET_SUPPORTED_COINS_FAIL, data: err }));
};

View file

@ -117,7 +117,7 @@ export default handleActions(
[ACTIONS.GET_SUPPORTED_COINS_FAIL]: (state: ShapeShiftState): ShapeShiftState => ({
...state,
loading: false,
error: 'Error getting available coins',
error: __('There was an error. Please try again later.'),
}),
[ACTIONS.GET_COIN_STATS_START]: (

View file

@ -23,5 +23,6 @@
@import 'component/_spinner.scss';
@import 'component/_nav.scss';
@import 'component/_file-list.scss';
@import 'component/_file-render.scss';
@import 'component/_search.scss';
@import 'component/_toggle.scss';

View file

@ -62,7 +62,7 @@ button:disabled {
font-size: 1em;
color: var(--btn-color-inverse);
border-radius: 0;
display: inline-block;
display: inline;
min-width: 0;
box-shadow: none;
text-align: left;
@ -76,6 +76,13 @@ button:disabled {
background-color: var(--btn-bg-secondary);
}
.btn.btn--tourniquet {
max-width: 20vw;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.btn.btn--no-style {
font-size: inherit;
font-weight: inherit;

View file

@ -150,10 +150,6 @@
padding-top: $spacing-vertical * 1/3;
}
.card__subtitle--block {
display: block;
}
.card__meta {
color: var(--color-help);
font-size: 14px;

View file

@ -96,6 +96,25 @@
}
}
.file-render {
width: 100%;
height: 100%;
margin: auto;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
overflow: hidden;
.file-render__viewer {
width: 100%;
height: 100%;
background: black;
}
}
img {
max-height: 100%;
max-width: 100%;

View file

@ -0,0 +1,25 @@
.file-render {
width: 100%;
height: 100%;
margin: auto;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
overflow: hidden;
.file-render__viewer {
margin: 0;
width: 100%;
height: 100%;
background: black;
iframe,
webview {
width: 100%;
height: 100%;
}
}
}

View file

@ -10,6 +10,10 @@
border: solid 1px var(--color-divider);
margin: $spacing-vertical $spacing-vertical * 2/3;
}
@media (min-width: $large-breakpoint) {
width: calc(var(--side-nav-width) * 1.1);
}
}
// Sidebar links

View file

@ -80,6 +80,14 @@ table.table--help {
font-family: 'metropolis-semibold';
min-width: 130px;
}
td:nth-of-type(2) {
/*Tourniquets text over 20VW*/
max-width: 20vw;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--color-help);
}
}
table.table--transactions {

View file

@ -21,11 +21,11 @@ function injectDevelopmentTemplate(event, templates) {
return templates;
}
export function openContextMenu(event, templates = []) {
const isSomethingSelected = window.getSelection().toString().length > 0;
const { type } = event.target;
export function openContextMenu(event, templates = [], canEdit = false, selection = '') {
const { type, value } = event.target;
const isSomethingSelected = selection.length > 0 || window.getSelection().toString().length > 0;
const isInput = event.target.matches('input') && (type === 'text' || type === 'number');
const { value } = event.target;
const isTextField = canEdit || isInput || event.target.matches('textarea');
templates.push({
label: 'Copy',
@ -36,12 +36,12 @@ export function openContextMenu(event, templates = []) {
// If context menu is opened on Input and there is text on the input and something is selected.
const { selectionStart, selectionEnd } = event.target;
if (!!value && isInput && selectionStart !== selectionEnd) {
if (!!value && isTextField && selectionStart !== selectionEnd) {
templates.push({ label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' });
}
// If context menu is opened on Input and text is present on clipboard
if (clipboard.readText().length > 0 && isInput) {
if (clipboard.readText().length > 0 && isTextField) {
templates.push({
label: 'Paste',
accelerator: 'CmdOrCtrl+V',
@ -50,7 +50,7 @@ export function openContextMenu(event, templates = []) {
}
// If context menu is opened on Input
if (isInput && value) {
if (isTextField && value) {
templates.push({
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
@ -61,6 +61,31 @@ export function openContextMenu(event, templates = []) {
injectDevelopmentTemplate(event, templates);
remote.Menu.buildFromTemplate(templates).popup();
}
// This function is used for the markdown description on the publish page
export function openEditorMenu(event, codeMirror) {
const value = codeMirror.doc.getValue();
const selection = codeMirror.doc.getSelection();
const templates = [
{
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
role: 'selectall',
click: () => {
codeMirror.execCommand('selectAll');
},
enabled: value.length > 0,
},
{
label: 'Cut',
accelerator: 'CmdOrCtrl+X',
role: 'cut',
enabled: selection.length > 0,
},
];
openContextMenu(event, templates, true, selection);
}
export function openCopyLinkMenu(text, event) {
const templates = [
{
@ -70,5 +95,5 @@ export function openCopyLinkMenu(text, event) {
},
},
];
openContextMenu(event, templates, false);
openContextMenu(event, templates);
}

View file

@ -13,7 +13,7 @@ export const validateSendTx = (formValues: DraftTxValues) => {
// All we need to check is if the address is valid
// If values are missing, users wont' be able to submit the form
if (address && !regexAddress.test(address)) {
if (!process.env.NO_ADDRESS_VALIDATION && !regexAddress.test(address)) {
errors.address = __('Not a valid LBRY address');
}

View file

@ -0,0 +1,32 @@
import mime from 'mime';
const formats = [
[/\.(mp4|m4v|webm|flv|f4v|ogv)$/i, 'video'],
[/\.(mp3|m4a|aac|wav|flac|ogg|opus)$/i, 'audio'],
[/\.(html|htm|xml|pdf|odf|doc|docx|md|markdown|txt|epub|org)$/i, 'document'],
[/\.(stl|obj|fbx|gcode)$/i, '3D-file'],
];
export default function getMediaType(contentType, fileName) {
const extName = mime.getExtension(contentType);
const fileExt = extName ? `.${extName}` : null;
const testString = fileName || fileExt;
// Get mediaType from file extension
if (testString) {
const res = formats.reduce((ret, testpair) => {
const [regex, mediaType] = testpair;
return regex.test(ret) ? mediaType : ret;
}, testString);
if (res !== testString) return res;
}
// Get mediaType from contentType
if (contentType) {
return /^[^/]+/.exec(contentType)[0];
}
return 'unknown';
}

View file

@ -5564,9 +5564,9 @@ lazy-val@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.3.tgz#bb97b200ef00801d94c317e29dc6ed39e31c5edc"
lbry-redux@lbryio/lbry-redux#201d78b68a329065ee5d2a03bfb1607ea0666588:
lbry-redux@lbryio/lbry-redux#a0d2d1ac532ade639d39c92f79678ac26e904dfd:
version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/201d78b68a329065ee5d2a03bfb1607ea0666588"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/a0d2d1ac532ade639d39c92f79678ac26e904dfd"
dependencies:
proxy-polyfill "0.1.6"
reselect "^3.0.0"
@ -7626,9 +7626,9 @@ react-redux@^5.0.3:
loose-envify "^1.1.0"
prop-types "^15.6.0"
react-simplemde-editor@^3.6.15:
version "3.6.15"
resolved "https://registry.yarnpkg.com/react-simplemde-editor/-/react-simplemde-editor-3.6.15.tgz#b4991304c7e1cac79258bb225579d008c13b5991"
react-simplemde-editor@^3.6.16:
version "3.6.16"
resolved "https://registry.yarnpkg.com/react-simplemde-editor/-/react-simplemde-editor-3.6.16.tgz#33633259478d3395f2c7b70deb56a1a40e863bea"
dependencies:
simplemde "^1.11.2"
@ -8726,6 +8726,12 @@ stream-to-blob-url@^2.0.0:
dependencies:
stream-to-blob "^1.0.0"
stream-to-blob-url@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/stream-to-blob-url/-/stream-to-blob-url-2.1.1.tgz#e1ac97f86ca8e9f512329a48e7830ce9a50beef2"
dependencies:
stream-to-blob "^1.0.0"
stream-to-blob@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/stream-to-blob/-/stream-to-blob-1.0.0.tgz#9f7a1ada39e16ea282ebb7e4cda307edabde658d"