Staging #1066

Merged
jessopb merged 55 commits from staging into release 2020-05-16 17:08:51 +02:00
49 changed files with 2656 additions and 1410 deletions
Showing only changes of commit 328ab96700 - Show all commits

5
.prettierrc.json Normal file
View file

@ -0,0 +1,5 @@
{
"trailingComma": "es5",
"printWidth": 100,
"singleQuote": true
}

View file

@ -85,6 +85,8 @@ $ npm run configure
#### Build & start the app #### Build & start the app
``` ```
$ npm run build
$ npm run start $ npm run start
``` ```
@ -114,7 +116,7 @@ curl -F 'name=MyPictureName' -F 'file=@/path/to/myPicture.jpeg' https://spee.ch/
``` ```
Parameters: Parameters:
* `name` (required) * `name` (required, must be unique across the instance)
* `file` (required) (must be type .mp4, .jpeg, .jpg, .gif, or .png) * `file` (required) (must be type .mp4, .jpeg, .jpg, .gif, or .png)
* `nsfw` (optional) * `nsfw` (optional)
* `license` (optional) * `license` (optional)
@ -262,4 +264,4 @@ We take security seriously. Please contact security@lbry.io regarding any securi
## Contact ## Contact
The primary contact for this project is [@skhameneh](mailto:shawn@lbry.io). The primary contact for this project is [@jessopb](mailto:jessop@lbry.io).

View file

@ -0,0 +1,7 @@
.asset-blocked__image {
width: 100%;
}
.asset-blocked__text {
width: 100%;
}

View file

@ -1,29 +1,37 @@
.asset-main { .asset-main {
height: 75vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
.asset-document {
$asset-info-width: 1000px;
max-width: $asset-info-width;
width: 100%;
padding: $thin-padding;
height: fit-content;
@media (max-width: $break-point-tablet) {
margin: $primary-padding $secondary-padding;
}
@media (max-width: $break-point-mobile) {
margin: $primary-padding 0;
}
}
.asset-display { .asset-display {
display: flex; height: fit-content;
min-height: 50vh width: fit-content;
} }
.asset-title { .asset-title {
padding-bottom: $thin-padding; padding-bottom: $thin-padding;
text-align: center; text-align: center;
@media (min-width: $break-point-mobile) {
padding-top: $secondary-padding;
}
} }
.asset-image, .asset-video { .asset-image, .asset-video {
max-height: 100%; max-height: 95vh;
max-width: 100%; max-width: 95vw;
margin-left: auto;
margin-right: auto;
object-fit: contain; object-fit: contain;
object-position: center; object-position: center;
} }
@ -110,7 +118,7 @@
$asset-info-width: 1000px; $asset-info-width: 1000px;
max-width: $asset-info-width; max-width: $asset-info-width;
margin: $primary-padding; margin: $primary-padding;
max-width: 100%; width: 100%;
@media (max-width: $break-point-tablet) { @media (max-width: $break-point-tablet) {
margin: $primary-padding $secondary-padding; margin: $primary-padding $secondary-padding;
@ -122,7 +130,7 @@
} }
.asset-footer { .asset-footer {
border-top: 1px solid $grey-border; border-top: $subtle-border;
padding-top: $primary-padding; padding-top: $primary-padding;
margin-top: $primary-padding; margin-top: $primary-padding;
color: $grey; color: $grey;

View file

@ -1,28 +1,48 @@
.asset-preview { .asset-preview {
position: relative; position: relative;
background: $card-color;
padding: $thin-padding;
color: $text-color;
width: 240px;
border: $subtle-border;
height: 280px;
&:hover {
border: 1px solid $highlight-border-color;
color: #000000;
}
}
.asset-preview__label {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 7.3em;
}
.asset-preview__label-text {
height: 4.5em;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
} }
.asset-preview__blocked { .asset-preview__blocked {
box-sizing: border-box; box-sizing: border-box;
background: black; background: black;
color: white; color: white;
height: 80%; height: 64%;
padding: 5px; padding: $thin-padding;
//remove margin-bottom after mystery 5px on wrapper is gone. margin-bottom: $thin-padding;
margin-bottom: 5px;
} }
.asset-preview__image { .asset-preview__image {
width : 100%; width : 240px;
height : 180px;
overflow: hidden;
object-fit: cover;
padding: 0; padding: 0;
margin : 0; margin : 0;
} box-sizing: border-box;
.asset-preview__video {
cursor: pointer;
background-color: #ffffff;
width: 100%;
position: relative;
} }
h3.asset-preview__title { h3.asset-preview__title {
@ -30,34 +50,6 @@ h3.asset-preview__title {
text-overflow: ellipsis; text-overflow: ellipsis;
word-wrap: break-word; word-wrap: break-word;
overflow: hidden; overflow: hidden;
line-height: 1em; max-height: 4em;
max-height: 2em; font-size: $text-large;
}
.asset-preview__play-wrapper {
border: 0px;
padding: 0px;
margin: auto;
position: relative;
}
.asset-preview__play-overlay {
padding: 0;
border: 0;
position: absolute;
opacity: 0.80;
height: 25%;
top: 37.5%;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
margin: 0 auto;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E %3Cg stroke='black' stroke-width='2' fill='black' fill-rule='evenodd' stroke-linejoin='round'%3E %3Ccircle cx='30' cy='30' r='28'/%3E%3C/g%3E %3Cg stroke='white' stroke-width='1' fill='white' fill-rule='evenodd' stroke-linejoin='round'%3E %3Cpolygon points='25 19 42 30 25 41'/%3E %3C/g%3E %3C/svg%3E");
background-repeat: no-repeat;
background-position: center;
}
.asset-preview__play-wrapper:hover .asset-preview__play-overlay {
opacity: 0.2;
} }

View file

@ -1,23 +1,24 @@
.channel-claims-display { .channel-claims-display {
width: 100%; width: 100%;
display: grid; display: grid;
grid-gap: 16px; grid-gap: $thin-padding;
} align-content: space-around;
@media (min-width: $break-point-x-large) {
@media (min-width: 1040px) { grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
.channel-claims-display { }
grid-template-columns: 1fr 1fr 1fr 1fr; @media (min-width: $break-point-large) and (max-width: $break-point-x-large){
grid-template-columns: 1fr 1fr 1fr 1fr;
} }
}
@media (min-width: 768px) and (max-width: 1039px) { @media (min-width: $break-point-tablet) and (max-width: $break-point-large) {
.channel-claims-display { grid-template-columns: 1fr 1fr 1fr;
}
@media (min-width: $break-point-mobile) and (max-width: $break-point-tablet) {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
}
@media (max-width: 767px) { @media (max-width: $break-point-mobile) {
.channel-claims-display {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }

View file

@ -4,7 +4,7 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
border: 1px solid $grey-border; border: 1px solid $subtle-border-color;
border-radius: 6px; border-radius: 6px;
.click-to-copy { .click-to-copy {
border: none; border: none;
@ -17,7 +17,7 @@
line-height: 20px; line-height: 20px;
letter-spacing: 0; letter-spacing: 0;
font-family: monospace; font-family: monospace;
border-right: 1px solid $grey-border; border-right: 1px solid $subtle-border-color;
} }
.icon-wrap { .icon-wrap {
width: 30px; width: 30px;

View file

@ -5,15 +5,18 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
width: 100%;
box-sizing: border-box;
padding: 0px;
} }
.dropzone { .dropzone {
border: 2px dashed #9b9b9b; border: 2px dashed $drop-zone-border-color;
// fill the parent flex container // fill the parent flex container
flex: 1 0 auto; flex: 1 0 auto;
// be a flex container for children // be a flex container for children
display: flex; display: flex;
padding: 1em; padding: $thin-padding;
-webkit-flex-direction: column; -webkit-flex-direction: column;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
@ -22,7 +25,7 @@
} }
.dropzone:hover, .dropzone--active { .dropzone:hover, .dropzone--active {
border: 2px dashed #4156C5; border: 2px dashed $drop-zone-border-hover;
cursor: pointer; cursor: pointer;
} }

View file

@ -1,55 +1,55 @@
.horizontal-split { .horizontal-split {
max-width: $width-content-constrained; max-width: $width-content-constrained;
width: 100%; width: 100%;
margin-left: auto;
margin-right: auto;
display : flex; display : flex;
flex-direction : row; flex-direction : row;
justify-content: space-between; justify-content: center;
box-sizing: border-box;
&.horizontal-split--mobile-collapse { &.horizontal-split--mobile-collapse {
@media (max-width: $break-point-mobile) { @media (max-width: $break-point-tablet) {
flex-direction: column; flex-direction: column;
.horizontal-split__column { .horizontal-split__column {
width: 100%;
} }
.horizontal-split__column--left {
padding-top: $thin-padding;
}
.horizontal-split__column--right { .horizontal-split__column--right {
padding-left: 0; padding-top: $thin-padding;
padding-top: $secondary-padding;
} }
} }
} }
}; };
.horizontal-split__column { .horizontal-split__column {
width: 50%; display : flex;
flex: 1 0 auto; flex: 1 1 auto;
box-sizing: border-box; box-sizing: border-box;
width: 100%;
} }
.horizontal-split__column--left { .horizontal-split__column--left {
padding-right: $primary-padding; padding: $tertiary-padding;
@media (max-width: $break-point-mobile) { @media (max-width: $break-point-tablet) {
padding-right: $thin-padding; padding-left: 0px;
padding-right: 0px;
} }
} }
.horizontal-split__column--right { .horizontal-split__column--right {
padding-left: $primary-padding;
@media (max-width: $break-point-mobile) { padding: $tertiary-padding;
padding-left: $thin-padding;
@media (max-width: $break-point-tablet) {
padding-left: 0px;
padding-right: 0px;
} }
} }
@media (max-width: $break-point-tablet) { @media (max-width: $break-point-tablet) {
.horizontal-split__column { .horizontal-split__column {
display : flex;
flex-direction : column;
justify-content: space-between; justify-content: space-between;
}; };

115
client/scss/_markdown.scss Normal file
View file

@ -0,0 +1,115 @@
.markdown-preview {
// Headers
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: 600;
margin-bottom: var(--spacing-vertical-medium);
padding-top: var(--spacing-vertical-medium);
}
// Paragraphs
p {
font-size: 1.15rem;
margin-bottom: var(--spacing-vertical-medium);
white-space: pre-line;
svg {
width: 1rem;
height: 1rem;
margin-left: 0.2rem;
position: relative;
top: 1px;
}
}
// Strikethrough text
del {
}
// Tables
table {
margin-bottom: 1.2rem;
padding: var(--spacing-vertical-medium);
background-color: $base-color;
tr {
td,
th,
td:first-of-type,
th:first-of-type,
td:last-of-type,
th:last-of-type {
padding: var(--spacing-vertical-medium);
}
}
}
// Image
img {
margin-bottom: var(--spacing-vertical-medium);
padding-top: var(--spacing-vertical-medium);
}
// Horizontal Rule
hr {
width: 100%;
height: 1px;
background-color: $base-color;
margin-bottom: 2rem;
position: relative;
top: 1rem;
html[data-theme='dark'] & {
background-color: rgba($base-color, 0.2);
}
}
// Code
pre {
white-space: normal;
}
code {
margin-bottom: var(--spacing-vertical-medium);
padding: var(--spacing-vertical-medium);
background-color: $subtle-border-color;
color: $text-color;
display: block;
font-family: Consolas, 'Lucida Console', 'Source Sans', monospace;
}
a {
color: $primary-color;
display: inline-block;
}
// Lists
ul,
ol {
margin-bottom: var(--spacing-vertical-medium);
> li {
list-style-position: outside;
}
}
ul {
list-style: initial;
}
li {
margin-left: var(--spacing-vertical-large);
p {
display: inline-block;
}
}
}

View file

@ -1,7 +1,10 @@
.nav-bar { .nav-bar {
margin-top: $thin-padding; box-sizing: border-box;
margin-left: $primary-padding; padding: $thin-padding $primary-padding;
margin-right: $primary-padding; background: $base-color;
flex: 0 1 auto;
width: 100%;
border-bottom: $subtle-border;
@media (max-width: $break-point-mobile) { @media (max-width: $break-point-mobile) {
margin-left: 15px; margin-left: 15px;

View file

@ -2,26 +2,24 @@
flex: 1 0 auto; flex: 1 0 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center;
max-width: 100%;
.content { .content {
flex: 1 0 auto; flex: 1 0 auto;
display: flex; display: flex;
-webkit-flex-direction: column; -webkit-flex-direction: column;
flex-direction: column; flex-direction: column;
margin: $secondary-padding; width: 100%;
align-items: center;
box-sizing: border-box;
background: $base-color;
@media (min-width: $break-point-tablet) {
padding: $primary-padding;
}
@media (max-width: $break-point-tablet) {
padding: $tertiary-padding;
}
} }
} }
@media (max-width: $break-point-tablet) {
.page-layout .content { margin: $tertiary-padding; }
}
@media (max-width: $break-point-mobile) {
max-width: calc(100% - 30px);
}
//below should take some styles from _text.scss and probably elsewhere and become "markdown" or "rich" styles
.page-layout {
p {
margin-bottom: $tertiary-padding;
}
}

View file

@ -1,4 +1,6 @@
select { select {
margin: 0; margin: 0;
display: inline-block; display: inline-block;
background: $base-color;
border: 0;
} }

View file

@ -7,7 +7,11 @@ h1, h2, h3, h4, p {
body { body {
color: $text-color; color: $text-color;
font-family: 'Circular', serif; font-family: 'Circular', serif;
font-size: 16px; font-size: 14px;
}
body a {
color: $primary-color;
} }
h1 { h1 {

View file

@ -1,40 +1,60 @@
$base-color: white; //backgrounds
$primary-color: #005da0; $base-color: white; //default white
$card-color: white; //default white
$chrome-color: lightgray; //default white (navbar)
$background-color: $base-color;
//text colors
$primary-color: #005da0; //link default light blue #005da0
$secondary-color: $primary-color; $secondary-color: $primary-color;
$text-color: #333;
$success-color: green; $success-color: green;
$failure-color: red; $failure-color: red;
$grey: #9095A5; $grey: #9095A5;
$help-color: $grey;
$grey-border: #DDDFE4;
$shadow-color: rgba(169, 173, 186, 0.2);
//borders and highlights
$grey: #9095A5;
$help-color: $grey;
$subtle-border-color: #DDD;
$highlight-border-color: #333;
$shadow-color: rgba(169, 173, 186, 0.2);
$subtle-border: 1px dashed $subtle-border-color;
$grey-border: $subtle-border-color; //factor this out for all customers
$drop-zone-border-color: #9b9b9b; //default #9b9b9b
$drop-zone-border-hover: #4156C5; //default #4156C5
//padding
$primary-padding: 3em; $primary-padding: 3em;
$secondary-padding: 2em; $secondary-padding: 2em;
$tertiary-padding: 1em; $tertiary-padding: 1em;
$thin-padding: 0.3em; $thin-padding: 0.3em;
$full-width-thin-padding: calc(100% - 0.6em); $full-width-thin-padding: calc(100% - 0.6em);
$input-padding: 0.3em;
$width-content-constrained: 1000px; $width-content-constrained: 1000px;
$background-color: $base-color;
$text-color: #333;
$button-border-width: 1px; $button-border-width: 1px;
$button-border-strength: solid; $button-border-strength: solid;
$button-full-width: calc(100% - 2px); $button-full-width: calc(100% - 2px);
$input-padding: 0.3em;
$input-full-width: calc(100% - 0.6em); $input-full-width: calc(100% - 0.6em);
//text sizes
$base-font-size: 14px;
$text-xx-large: 2.5em; $text-xx-large: 2.5em;
$text-x-large: 2.0em; $text-x-large: 2.0em;
$text-large: 1.5em; $text-large: 1.5em;
$text-medium: 1.0em; $text-medium: 1.2em;
$text-small: 0.9em; $text-small: 0.9em;
$text-x-small: 0.8em; $text-x-small: 0.8em;
//@media sizes
$break-point-xx-large: 1400px; $break-point-xx-large: 1400px;
$break-point-x-large: 1290px; $break-point-x-large: 1290px;
$break-point-large: 1000px; $break-point-large: 1024px;
$break-point-tablet: 800px; $break-point-tablet: 800px;
$break-point-mobile: 500px; $break-point-mobile: 500px;
$break-point-phone: 300px;
$break-point-phone: 300px;

View file

@ -5,6 +5,7 @@
@import '~scss/_body'; @import '~scss/_body';
@import '~scss/_react-app'; @import '~scss/_react-app';
@import '~scss/_text'; @import '~scss/_text';
@import '~scss/_markdown';
@import '~scss/_link'; @import '~scss/_link';
@import '~scss/_input'; @import '~scss/_input';
@ -15,6 +16,7 @@
@import '~scss/_asset-display'; @import '~scss/_asset-display';
@import '~scss/_asset-preview'; @import '~scss/_asset-preview';
@import '~scss/_asset-blocked';
@import '~scss/_button'; @import '~scss/_button';
@import '~scss/_button-primary'; @import '~scss/_button-primary';
@import '~scss/_button-secondary'; @import '~scss/_button-secondary';

View file

@ -1,12 +1,20 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import createCanonicalLink from '../../../../utils/createCanonicalLink'; import createCanonicalLink from '@globalutils/createCanonicalLink';
import * as Icon from 'react-feather';
const AssetPreview = ({ defaultThumbnail, claimData }) => { const AssetPreview = ({ defaultThumbnail, claimData }) => {
const {name, fileExt, contentType, thumbnail, title, blocked} = claimData; const {name, fileExt, contentType, thumbnail, title, blocked} = claimData;
const showUrl = createCanonicalLink({asset: {...claimData}}); const showUrl = createCanonicalLink({asset: {...claimData}});
const embedUrl = `${showUrl}.${fileExt}`; const embedUrl = `${showUrl}.${fileExt}`;
/*
we'll be assigning media icon based on supported type / mime types
*/
const media = contentType.split('/')[0];
/*
make sure thumb has the right url
*/
const thumb = media === 'image' ? embedUrl : thumbnail;
/* /*
This blocked section shouldn't be necessary after pagination is reworked, This blocked section shouldn't be necessary after pagination is reworked,
though it might be useful for channel_mine situations. though it might be useful for channel_mine situations.
@ -16,45 +24,39 @@ const AssetPreview = ({ defaultThumbnail, claimData }) => {
return ( return (
<div className='asset-preview'> <div className='asset-preview'>
<div className='asset-preview__blocked'> <div className='asset-preview__blocked'>
<h3>Error 451</h3> <p>Error 451</p>
<p>This content is blocked for legal reasons.</p> <p>This content is blocked for legal reasons.</p>
</div> </div>
<h3 className='asset-preview__title'>Blocked Content</h3> <div className={'asset-preview__label'}>
<div className={'asset-preview__label-text'}>
<p className='asset-preview__title text--medium'>Blocked Content</p>
</div>
</div>
</div> </div>
); );
} else { } else {
switch (contentType) { return (
case 'image/jpeg': <Link to={showUrl} className='asset-preview'>
case 'image/jpg': <img
case 'image/png': className={'asset-preview__image'}
case 'image/gif': src={thumb || defaultThumbnail}
return ( alt={name}
<Link to={showUrl} className='asset-preview'> />
<img <div className={'asset-preview__label'}>
className={'asset-preview__image'} <div className={'asset-preview__label-text'}>
src={embedUrl} <p className='asset-preview__title text--medium'>{title}</p>
alt={name} </div>
/> <div className={'asset-preview__label-info'}>
<h3 className='asset-preview__title'>{title}</h3> <div className={'text--medium'}>
</Link> { media === 'image' && <Icon.Image />}
); { media === 'text' && <Icon.FileText />}
case 'video/mp4': { media === 'video' && contentType === 'video/mp4' && <Icon.Video />}
return ( { media !== 'image' && media !== 'text' && contentType !== 'video/mp4' && <Icon.File />}
<Link to={showUrl} className='asset-preview'>
<div className='asset-preview__play-wrapper'>
<img
className={'asset-preview__video'}
src={thumbnail || defaultThumbnail}
alt={name}
/>
<div className='asset-preview__play-overlay' />
</div> </div>
<h3 className='asset-preview__title'>{title}</h3> </div>
</Link> </div>
); </Link>
default: );
return null;
}
} }
}; };

View file

@ -1,11 +1,9 @@
import React from 'react'; import React from 'react';
// TODO: factor out longId OR implement tooltip display
const ChannelInfoDisplay = ({name, longId, shortId}) => { const ChannelInfoDisplay = ({name, longId, shortId}) => {
return ( return (
<div> <div>
<h2>channel name: {name}</h2> <h2>{name}:{shortId}</h2>
<p className={'text--secondary'}>full channel id: {longId}</p>
<p className={'text--secondary'}>short channel id: {shortId}</p>
</div> </div>
); );
}; };

View file

@ -0,0 +1,52 @@
import React from 'react';
import ReactMarkdown from 'react-markdown';
class FileViewer extends React.Component {
constructor (props) {
super(props);
/*
Prevent memory leak by closing fetch before unmount
*/
this.abortController = new AbortController();
this.abortSignal = this.abortController.signal;
this.state = {
fileLoaded: false,
fileText : '',
};
}
componentDidMount () {
const {sourceUrl} = this.props;
const signal = this.abortSignal;
fetch(sourceUrl, { signal })
.then(response => response.text())
.then((text) => {
this.setState({fileText: text});
this.setState({fileLoaded: true});
return true;
})
.catch(e => { console.log('fetch aborted on unmount ', e) });
}
componentWillUnmount () {
this.abortController.abort();
}
render () {
return (
<div className={'markdown'}>
{
this.state.fileLoaded &&
<ReactMarkdown source={this.state.fileText}/>
}
{
!this.state.fileLoaded &&
<p>Loading your file...</p>
}
</div>
);
}
}
export default FileViewer;

View file

@ -1,16 +1,13 @@
import React from 'react'; import React from 'react';
import createCanonicalLink from '../../../../utils/createCanonicalLink'; import createCanonicalLink from '@globalutils/createCanonicalLink';
import HorizontalSplit from '@components/HorizontalSplit'; import HorizontalSplit from '@components/HorizontalSplit';
/*
This component shouldn't be necessary after pagination is reworked,
though it might be useful for channel_mine situations.
*/
class BlockedLeft extends React.PureComponent { class BlockedLeft extends React.PureComponent {
render () { render () {
return ( return (
<React.Fragment> <div>
<img className='asset-image' src={'https://upload.wikimedia.org/wikipedia/commons/archive/a/af/20120315000030%21OR_451.svg'} alt={'451 image'} /> <img className={'asset-blocked__image'} src={'/assets/img/451sign.svg'} alt={'451 image'} />
</React.Fragment> </div>
); );
} }
} }
@ -18,10 +15,10 @@ class BlockedLeft extends React.PureComponent {
class BlockedRight extends React.PureComponent { class BlockedRight extends React.PureComponent {
render () { render () {
return ( return (
<React.Fragment> <div className={'asset-blocked__text'} >
<p>In response to a complaint we received under the US Digital Millennium Copyright Act, we have blocked access to this content from our applications.</p> <p>In response to a complaint we received under the US Digital Millennium Copyright Act, we have blocked access to this content from our applications.</p>
<p><a href={'https://lbry.io/faq/dmca'} >Click here</a> for more information.</p> <p><a href={'https://lbry.io/faq/dmca'} >Click here</a> for more information.</p>
</React.Fragment> </div>
); );
} }
} }
@ -53,6 +50,6 @@ class AssetBlocked extends React.Component {
</div> </div>
); );
} }
}; }
export default AssetBlocked; export default AssetBlocked;

View file

@ -2,7 +2,8 @@ import React from 'react';
import Row from '@components/Row'; import Row from '@components/Row';
import ProgressBar from '@components/ProgressBar'; import ProgressBar from '@components/ProgressBar';
import { LOCAL_CHECK, UNAVAILABLE, ERROR, AVAILABLE } from '../../constants/asset_display_states'; import { LOCAL_CHECK, UNAVAILABLE, ERROR, AVAILABLE } from '../../constants/asset_display_states';
import createCanonicalLink from '../../../../utils/createCanonicalLink'; import createCanonicalLink from '@globalutils/createCanonicalLink';
import FileViewer from '@components/FileViewer';
class AvailableContent extends React.Component { class AvailableContent extends React.Component {
render () { render () {
@ -12,6 +13,7 @@ class AvailableContent extends React.Component {
case 'image/jpg': case 'image/jpg':
case 'image/png': case 'image/png':
case 'image/gif': case 'image/gif':
case 'image/svg+xml':
return ( return (
<img <img
className='asset-image' className='asset-image'
@ -31,9 +33,18 @@ class AvailableContent extends React.Component {
<p>Your browser does not support the <code>video</code> element.</p> <p>Your browser does not support the <code>video</code> element.</p>
</video> </video>
); );
case 'text/markdown':
return (
<div className={'asset-document'}><FileViewer sourceUrl={sourceUrl}/></div>
);
default: default:
return ( return (
<p>Unsupported content type</p> <img
className='asset-image'
src={thumbnail}
alt={name}
/>
); );
} }
} }
@ -95,6 +106,6 @@ class AssetDisplay extends React.Component {
</div> </div>
); );
} }
}; }
export default AssetDisplay; export default AssetDisplay;

View file

@ -5,11 +5,11 @@ import RowLabeled from '@components/RowLabeled';
import SpaceBetween from '@components/SpaceBetween'; import SpaceBetween from '@components/SpaceBetween';
import AssetShareButtons from '@components/AssetShareButtons'; import AssetShareButtons from '@components/AssetShareButtons';
import ClickToCopy from '@components/ClickToCopy'; import ClickToCopy from '@components/ClickToCopy';
import HorizontalSplit from '@components/HorizontalSplit';
import siteConfig from '@config/siteConfig.json'; import siteConfig from '@config/siteConfig.json';
import createCanonicalLink from '../../../../utils/createCanonicalLink'; import createCanonicalLink from '@globalutils/createCanonicalLink';
import AssetInfoFooter from '../../components/AssetInfoFooter/index'; import AssetInfoFooter from '../../components/AssetInfoFooter/index';
import { createPermanentURI } from '@clientutils/createPermanentURI'; import { createPermanentURI } from '@clientutils/createPermanentURI';
import ReactMarkdown from 'react-markdown';
const { details: { host } } = siteConfig; const { details: { host } } = siteConfig;
@ -21,6 +21,8 @@ class AssetInfo extends React.Component {
const canonicalUrl = createCanonicalLink({ asset: { ...claimData, shortId: asset.shortId }}); const canonicalUrl = createCanonicalLink({ asset: { ...claimData, shortId: asset.shortId }});
const assetCanonicalUrl = `${host}${canonicalUrl}`; const assetCanonicalUrl = `${host}${canonicalUrl}`;
// Todo Issue #882 centralize all this media type detection
const embedable = contentType.split('/')[0] === 'image' || contentType === 'video/mp4';
let channelCanonicalUrl; let channelCanonicalUrl;
if (channelName) { if (channelName) {
@ -32,133 +34,130 @@ class AssetInfo extends React.Component {
} }
return ( return (
<div className='asset-info'> <div className='asset-info'>
<HorizontalSplit { description && (
leftSide={ <RowLabeled
description && ( label={<Label value={'Description'} />}
<p className='asset-info__description'>{description}</p> content={<div className='asset-info__description'><ReactMarkdown source={description}/></div>}
) />
)}
{editable && (
<RowLabeled
label={<Label value={'Edit'} />}
content={<Link to={`/edit${canonicalUrl}`}>{name}</Link>}
/>
)}
{channelName && (
<RowLabeled
label={
<Label value={'Channel'} />
}
content={
<span className='text'>
<Link className='link--primary' to={channelCanonicalUrl}>{channelName}</Link>
</span>
}
/>
)}
{claimViews ? (
<RowLabeled
label={
<Label value={'Views'} />
}
content={
<span className='text'>
{claimViews}
</span>
}
/>
) : null}
<RowLabeled
label={
<Label value={'Share'} />
} }
rightSide={ content={
<div> <AssetShareButtons
{editable && ( name={name}
<RowLabeled assetUrl={assetCanonicalUrl}
label={<Label value={'Edit:'} />} />
content={<Link to={`/edit${canonicalUrl}`}>{name}</Link>} }
/> />
)}
{channelName && (
<RowLabeled
label={
<Label value={'Channel'} />
}
content={
<span className='text'>
<Link className='link--primary' to={channelCanonicalUrl}>{channelName}</Link>
</span>
}
/>
)}
{claimViews ? (
<RowLabeled
label={
<Label value={'Views'} />
}
content={
<span className='text'>
{claimViews}
</span>
}
/>
) : null}
<RowLabeled <RowLabeled
label={ label={
<Label value={'Share'} /> <Label value={'Link'} />
} }
content={ content={
<AssetShareButtons <ClickToCopy
name={name} id={'short-link'}
assetUrl={assetCanonicalUrl} value={assetCanonicalUrl}
/> />
} }
/> />
{embedable && (
<RowLabeled <RowLabeled
label={ label={
<Label value={'Link'} /> <Label value={'Embed'} />
} }
content={ content={
<div>
{(contentType === 'video/mp4') ? (
<ClickToCopy <ClickToCopy
id={'short-link'} id={'embed-text-video'}
value={assetCanonicalUrl} value={`<iframe src="${host}/video-embed${canonicalUrl}" allowfullscreen="true" style="border:0"></iframe>`}
/> />
} ) : (
/>
<RowLabeled
label={
<Label value={'Embed'} />
}
content={
<div>
{(contentType === 'video/mp4') ? (
<ClickToCopy
id={'embed-text-video'}
value={`<iframe src="${host}/video-embed${canonicalUrl}" allowfullscreen="true" style="border:0"></iframe>`}
/>
) : (
<ClickToCopy
id={'embed-text-image'}
value={`<img src="${assetCanonicalUrl}.${fileExt}"/>`}
/>
)}
</div>
}
/>
<RowLabeled
label={
<Label value={'LBRY URI'} />
}
content={
<ClickToCopy <ClickToCopy
id={'lbry-permanent-url'} id={'embed-text-image'}
value={`${createPermanentURI(asset)}`} value={`<img alt="${name}" src="${assetCanonicalUrl}.${fileExt}" />`}
/> />
} )}
/> </div>
}
/>
)}
<RowLabeled
label={
<Label value={'LBRY URI'} />
}
content={
<ClickToCopy
id={'lbry-permanent-url'}
value={`${createPermanentURI(asset)}`}
/>
}
/>
<SpaceBetween> <SpaceBetween>
<a <a
className='link--primary' className='link--primary'
href={`${assetCanonicalUrl}.${fileExt}`} href={`${assetCanonicalUrl}.${fileExt}`}
> >
Direct Link Direct Link
</a> </a>
<a <a
className={'link--primary'} className={'link--primary'}
href={`${assetCanonicalUrl}.${fileExt}`} href={`${assetCanonicalUrl}.${fileExt}`}
download={name} download={name}
> >
Download Download
</a> </a>
<a <a
className={'link--primary'} className={'link--primary'}
href={`https://open.lbry.io/${createPermanentURI(asset)}`} href={`https://open.lbry.io/${createPermanentURI(asset)}`}
download={name} download={name}
> >
LBRY URL LBRY URL
</a> </a>
<a <a
className={'link--primary'} className={'link--primary'}
target='_blank' target='_blank'
href='https://lbry.io/dmca' href='https://lbry.io/dmca'
> >
Report Report
</a> </a>
</SpaceBetween> </SpaceBetween>
</div>
} />
<AssetInfoFooter /> <AssetInfoFooter />
</div> </div>
); );

View file

@ -2,7 +2,7 @@ import React from 'react';
const AssetTitle = ({ title }) => { const AssetTitle = ({ title }) => {
return ( return (
<h1 className='asset-title'>{title}</h1> <h2 className='asset-title'>{title}</h2>
); );
}; };

View file

@ -3,11 +3,14 @@ import { selectFile, updateError, clearFile } from '../../actions/publish';
import { selectAsset } from '../../selectors/show'; import { selectAsset } from '../../selectors/show';
import View from './view'; import View from './view';
import siteConfig from '@config/siteConfig.json'; import siteConfig from '@config/siteConfig.json';
import createCanonicalLink from '../../../../utils/createCanonicalLink'; import createCanonicalLink from '@globalutils/createCanonicalLink';
const { assetDefaults: { thumbnail: defaultThumbnail } } = siteConfig; const {
assetDefaults: { thumbnail: defaultThumbnail },
} = siteConfig;
const mapStateToProps = ({ show, publish: { file, thumbnail, fileError, isUpdate } }) => { const mapStateToProps = ({ show, publish: { file, thumbnail, error, isUpdate } }) => {
const fileError = error.file;
const obj = { file, thumbnail, fileError, isUpdate }; const obj = { file, thumbnail, fileError, isUpdate };
let asset, name, claimId, fileExt, outpoint, sourceUrl; let asset, name, claimId, fileExt, outpoint, sourceUrl;
if (isUpdate) { if (isUpdate) {
@ -18,7 +21,7 @@ const mapStateToProps = ({ show, publish: { file, thumbnail, fileError, isUpdate
if (obj.fileExt === 'mp4') { if (obj.fileExt === 'mp4') {
obj.sourceUrl = claimData.thumbnail ? claimData.thumbnail : defaultThumbnail; obj.sourceUrl = claimData.thumbnail ? claimData.thumbnail : defaultThumbnail;
} else { } else {
({fileExt, outpoint} = claimData); ({ fileExt, outpoint } = claimData);
obj.sourceUrl = `${createCanonicalLink({ asset: claimData })}.${fileExt}?${outpoint}`; obj.sourceUrl = `${createCanonicalLink({ asset: claimData })}.${fileExt}?${outpoint}`;
} }
} }
@ -28,14 +31,17 @@ const mapStateToProps = ({ show, publish: { file, thumbnail, fileError, isUpdate
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
selectFile: (file) => { selectFile: file => {
dispatch(selectFile(file)); dispatch(selectFile(file));
}, },
setFileError: (value) => { setFileError: value => {
dispatch(clearFile()); dispatch(clearFile());
dispatch(updateError('file', value)); dispatch(updateError('file', value));
}, },
}; };
}; };
export default connect(mapStateToProps, mapDispatchToProps)(View); export default connect(
mapStateToProps,
mapDispatchToProps
)(View);

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { NavLink, withRouter } from 'react-router-dom'; import { NavLink, withRouter } from 'react-router-dom';
import NavBarChannelOptionsDropdown from '@components/NavBarChannelOptionsDropdown'; import NavBarChannelOptionsDropdown from '@components/NavBarChannelOptionsDropdown';
import createCanonicalLink from '../../../../utils/createCanonicalLink'; import createCanonicalLink from '@globalutils/createCanonicalLink';
const VIEW = 'VIEW'; const VIEW = 'VIEW';
const LOGOUT = 'LOGOUT'; const LOGOUT = 'LOGOUT';

View file

@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import createPageTitle from '../../utils/createPageTitle'; import createPageTitle from '../../utils/createPageTitle';
import createMetaTags from '../../utils/createMetaTags'; import createMetaTags from '../../utils/createMetaTags';
import oEmbed from '../../utils/oEmbed.js'; import oEmbed from '../../utils/oEmbed.js';
import createCanonicalLink from '../../../../utils/createCanonicalLink'; import createCanonicalLink from '@globalutils/createCanonicalLink';
import siteConfig from '@config/siteConfig.json'; import siteConfig from '@config/siteConfig.json';
const { details: { host } } = siteConfig; const { details: { host } } = siteConfig;

View file

@ -10,34 +10,36 @@ class FaqPage extends React.Component {
pageUri={'tos'} pageUri={'tos'}
> >
<Row> <Row>
<h1>Frequently Asked Questions</h1> <Row>
</Row> <h1>Frequently Asked Questions</h1>
<Row> </Row>
<h3>What is spee.ch?</h3> <Row>
<p>Spee.ch is a media-hosting site that reads from and publishes content to the <a href='http://lbry.io/'>LBRY blockchain</a>.</p> <h3>What is spee.ch?</h3>
</Row> <p>Spee.ch is a media-hosting site that reads from and publishes content to the <a href='http://lbry.io/'>LBRY blockchain</a>.</p>
<Row> </Row>
<h3>OK But Why Should I Care?</h3> <Row>
<p>Spee.ch is a fast and easy way to host your images, videos, and other content. What makes this different from other similar sites is that Spee.ch is hosted on the LBRY blockchain. That means it is impossible for your content to be censored via digital means. Even if we took down Spee.ch today, all content would remain immutably stored on the LBRY blockchain.</p> <h3>OK But Why Should I Care?</h3>
<p>Blockchain technology doesnt solve <a href='https://xkcd.com/538/'>the 5 dollar wrench attack</a>, but it solves just about every other problem in media hosting and distribution.</p> <p>Spee.ch is a fast and easy way to host your images, videos, and other content. What makes this different from other similar sites is that Spee.ch is hosted on the LBRY blockchain. That means it is impossible for your content to be censored via digital means. Even if we took down Spee.ch today, all content would remain immutably stored on the LBRY blockchain.</p>
<p>Even better - you can host your own clone of Spee.ch to get even more control over your content. <a href='https://github.com/lbryio/spee.ch/blob/master/README.md'>CLICK HERE FOR INFO</a>.</p> <p>Blockchain technology doesnt solve <a href='https://xkcd.com/538/'>the 5 dollar wrench attack</a>, but it solves just about every other problem in media hosting and distribution.</p>
<p>Spee.ch is just the beginning of what will soon be a vibrant ecosystem of LBRY-powered apps. Use LBRY and youre one step closer to true freedom.</p> <p>Even better - you can host your own clone of Spee.ch to get even more control over your content. <a href='https://github.com/lbryio/spee.ch/blob/master/README.md'>CLICK HERE FOR INFO</a>.</p>
</Row> <p>Spee.ch is just the beginning of what will soon be a vibrant ecosystem of LBRY-powered apps. Use LBRY and youre one step closer to true freedom.</p>
<Row> </Row>
<h3>How to Use spee.ch</h3> <Row>
<p>Its easy. Drag the image or video file of your choice into the center of the spee.ch homepage.</p> <h3>How to Use spee.ch</h3>
<p>Spee.ch is currently best suited for web optimized MP4 video and standard image filetypes (JPEG, PNG, GIF).</p> <p>Its easy. Drag the image or video file of your choice into the center of the spee.ch homepage.</p>
<p>If you want to refer to a piece of content repeatedly, or to build a collection of related content, you could create a channel. Channels work both for private collections and for public repositories. Theres more info about how to do this <a href='https://spee.ch/login'>on the channel page</a>.</p> <p>Spee.ch is currently best suited for web optimized MP4 video and standard image filetypes (JPEG, PNG, GIF).</p>
<p>Published files will be wiewable and embeddable with any web browser and accesible in the LBRY app. You can also use spee.ch to view free and non-NSFW content published on LBRY network from LBRY app. You just need to replace "lbry://" with "http://spee.ch/" in the URL.</p> <p>If you want to refer to a piece of content repeatedly, or to build a collection of related content, you could create a channel. Channels work both for private collections and for public repositories. Theres more info about how to do this <a href='https://spee.ch/login'>on the channel page</a>.</p>
</Row> <p>Published files will be wiewable and embeddable with any web browser and accesible in the LBRY app. You can also use spee.ch to view free and non-NSFW content published on LBRY network from LBRY app. You just need to replace "lbry://" with "http://spee.ch/" in the URL.</p>
<Row> </Row>
<h3>How Long Does Content Stay on Spee.ch?</h3> <Row>
<p>All content uploaded on spee.ch is guaranteed to stay up for at least 10 years with no maintenance. Future updates will likely extend that time horizon further as blockchain technology improves.</p> <h3>How Long Does Content Stay on Spee.ch?</h3>
</Row> <p>All content uploaded on spee.ch is guaranteed to stay up for at least 10 years with no maintenance. Future updates will likely extend that time horizon further as blockchain technology improves.</p>
<Row> </Row>
<h3>Contribute</h3> <Row>
<p>If you have an idea for your own spee.ch-like site on top of LBRY, fork our <a href='https://github.com/lbryio/spee.ch'>github repo</a> and go to town!</p> <h3>Contribute</h3>
<p>If you want to improve spee.ch, join <a href='https://chat.lbry.io/'>our discord channel</a> or solve one of our <a href='https://github.com/lbryio/spee.ch/issues'>github issues</a>.</p> <p>If you have an idea for your own spee.ch-like site on top of LBRY, fork our <a href='https://github.com/lbryio/spee.ch'>github repo</a> and go to town!</p>
<p>If you want to improve spee.ch, join <a href='https://chat.lbry.io/'>our discord channel</a> or solve one of our <a href='https://github.com/lbryio/spee.ch/issues'>github issues</a>.</p>
</Row>
</Row> </Row>
</PageLayout> </PageLayout>
); );

View file

@ -38,13 +38,16 @@ class ShowAssetDetails extends React.Component {
asset={asset} asset={asset}
> >
<div className="asset-main"> <div className="asset-main">
<AssetDisplay />
<AssetTitle /> <AssetTitle />
<button className='collapse-button' onClick={this.collapse}> <AssetDisplay />
{this.state.closed ? <Icon.PlusCircle className='plus-icon' /> : <Icon.MinusCircle />} <div>
</button> <button className='collapse-button' onClick={this.collapse}>
{this.state.closed ? <Icon.PlusCircle className='plus-icon' /> : <Icon.MinusCircle />}
</button>
</div>
</div> </div>
{!this.state.closed && <AssetInfo />} {!this.state.closed && <AssetInfo />}
</PageLayout> </PageLayout>
); );
} else { } else {

View file

@ -10,54 +10,56 @@ class TosPage extends React.Component {
pageUri={'tos'} pageUri={'tos'}
> >
<Row> <Row>
<h1>Terms of Service</h1> <Row>
<p>Last updated: September 25, 2018</p> <h1>Terms of Service</h1>
<p>Please read these Terms of Service ("Terms", "Terms of Service") carefully before using the <a className='link--primary' href='https://spee.ch'>https://spee.ch</a> website (the "Service") operated by <a className='link--primary' href='https://lbry.io'>LBRY INC</a> ("us", "we", or "our").</p> <p>Last updated: September 25, 2018</p>
<p>Your access to and use of the Service is conditioned upon your acceptance of and compliance with these Terms. These Terms apply to all visitors, users and others who wish to access or use the Service.</p> <p>Please read these Terms of Service ("Terms", "Terms of Service") carefully before using the <a className='link--primary' href='https://spee.ch'>https://spee.ch</a> website (the "Service") operated by <a className='link--primary' href='https://lbry.io'>LBRY INC</a> ("us", "we", or "our").</p>
<p>By accessing or using the Service you agree to be bound by these Terms. If you disagree with any part of the terms then you do not have permission to access the Service.</p> <p>Your access to and use of the Service is conditioned upon your acceptance of and compliance with these Terms. These Terms apply to all visitors, users and others who wish to access or use the Service.</p>
</Row> <p>By accessing or using the Service you agree to be bound by these Terms. If you disagree with any part of the terms then you do not have permission to access the Service.</p>
<Row> </Row>
<h3>Links To Other Web Sites</h3> <Row>
<p>Our Service may contain links to third party web sites or services that are not owned or controlled by <a className='link--primary' href='https://lbry.io'>LBRY INC</a></p> <h3>Links To Other Web Sites</h3>
<p><a className='link--primary' href='https://lbry.io'>LBRY INC</a> has no control over, and assumes no responsibility for the content, privacy policies, or practices of any third party web sites or services. We do not warrant the offerings of any of these entities/individuals or their websites.</p> <p>Our Service may contain links to third party web sites or services that are not owned or controlled by <a className='link--primary' href='https://lbry.io'>LBRY INC</a></p>
<p>You acknowledge and agree that <a className='link--primary' href='https://lbry.io'>LBRY INC</a> shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such third party web sites or services.</p> <p><a className='link--primary' href='https://lbry.io'>LBRY INC</a> has no control over, and assumes no responsibility for the content, privacy policies, or practices of any third party web sites or services. We do not warrant the offerings of any of these entities/individuals or their websites.</p>
<p>We strongly advise you to read the terms and conditions and privacy policies of any third party web sites or services that you visit.</p> <p>You acknowledge and agree that <a className='link--primary' href='https://lbry.io'>LBRY INC</a> shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such third party web sites or services.</p>
</Row> <p>We strongly advise you to read the terms and conditions and privacy policies of any third party web sites or services that you visit.</p>
<Row> </Row>
<h3>Termination</h3> <Row>
<p>We may terminate or suspend your access to the Service immediately, without prior notice or liability, under our sole discretion, for any reason whatsoever and without limitation, including but not limited to a breach of the Terms.</p> <h3>Termination</h3>
<p>All provisions of the Terms which by their nature should survive termination shall survive termination, including, without limitation, ownership provisions, warranty disclaimers, indemnity and limitations of liability.</p> <p>We may terminate or suspend your access to the Service immediately, without prior notice or liability, under our sole discretion, for any reason whatsoever and without limitation, including but not limited to a breach of the Terms.</p>
</Row> <p>All provisions of the Terms which by their nature should survive termination shall survive termination, including, without limitation, ownership provisions, warranty disclaimers, indemnity and limitations of liability.</p>
<Row> </Row>
<h3>Indemnification</h3> <Row>
<p>You agree to defend, indemnify and hold harmless LBRY INC and its licensee and licensors, and their employees, contractors, agents, officers and directors, from and against any and all claims, damages, obligations, losses, liabilities, costs or debt, and expenses (including but not limited to attorney's fees), resulting from or arising out of a) your use and access of the Service, or b) a breach of these Terms.</p> <h3>Indemnification</h3>
</Row> <p>You agree to defend, indemnify and hold harmless LBRY INC and its licensee and licensors, and their employees, contractors, agents, officers and directors, from and against any and all claims, damages, obligations, losses, liabilities, costs or debt, and expenses (including but not limited to attorney's fees), resulting from or arising out of a) your use and access of the Service, or b) a breach of these Terms.</p>
<Row> </Row>
<h3>Limitation Of Liability</h3> <Row>
<p>In no event shall <a className='link--primary' href='https://lbry.io'>LBRY INC</a>, nor its directors, employees, partners, agents, suppliers, or affiliates, be liable for any indirect, incidental, special, consequential or punitive damages, including without limitation, loss of profits, data, use, goodwill, or other intangible losses, resulting from (i) your access to or use of or inability to access or use the Service; (ii) any conduct or content of any third party on the Service; (iii) any content obtained from the Service; and (iv) unauthorized access, use or alteration of your transmissions or content, whether based on warranty, contract, tort (including negligence) or any other legal theory, whether or not we have been informed of the possibility of such damage, and even if a remedy set forth herein is found to have failed of its essential purpose.</p> <h3>Limitation Of Liability</h3>
</Row> <p>In no event shall <a className='link--primary' href='https://lbry.io'>LBRY INC</a>, nor its directors, employees, partners, agents, suppliers, or affiliates, be liable for any indirect, incidental, special, consequential or punitive damages, including without limitation, loss of profits, data, use, goodwill, or other intangible losses, resulting from (i) your access to or use of or inability to access or use the Service; (ii) any conduct or content of any third party on the Service; (iii) any content obtained from the Service; and (iv) unauthorized access, use or alteration of your transmissions or content, whether based on warranty, contract, tort (including negligence) or any other legal theory, whether or not we have been informed of the possibility of such damage, and even if a remedy set forth herein is found to have failed of its essential purpose.</p>
<Row> </Row>
<h3>Disclaimer</h3> <Row>
<p>Your use of the Service is at your sole risk. The Service is provided on an "AS IS" and "AS AVAILABLE" basis. The Service is provided without warranties of any kind, whether express or implied, including, but not limited to, implied warranties of merchantability, fitness for a particular purpose, non-infringement or course of performance.</p> <h3>Disclaimer</h3>
<p><a className='link--primary' href='https://lbry.io'>LBRY INC</a> its subsidiaries, affiliates, and its licensors do not warrant that a) the Service will function uninterrupted, secure or available at any particular time or location; b) any errors or defects will be corrected; c) the Service is free of viruses or other harmful components; or d) the results of using the Service will meet your requirements.</p> <p>Your use of the Service is at your sole risk. The Service is provided on an "AS IS" and "AS AVAILABLE" basis. The Service is provided without warranties of any kind, whether express or implied, including, but not limited to, implied warranties of merchantability, fitness for a particular purpose, non-infringement or course of performance.</p>
</Row> <p><a className='link--primary' href='https://lbry.io'>LBRY INC</a> its subsidiaries, affiliates, and its licensors do not warrant that a) the Service will function uninterrupted, secure or available at any particular time or location; b) any errors or defects will be corrected; c) the Service is free of viruses or other harmful components; or d) the results of using the Service will meet your requirements.</p>
<Row> </Row>
<h3>Exclusions</h3> <Row>
<p>Some jurisdictions do not allow the exclusion of certain warranties or the exclusion or limitation of liability for consequential or incidental damages, so the limitations above may not apply to you.</p> <h3>Exclusions</h3>
</Row> <p>Some jurisdictions do not allow the exclusion of certain warranties or the exclusion or limitation of liability for consequential or incidental damages, so the limitations above may not apply to you.</p>
<Row> </Row>
<h3>Governing Law</h3> <Row>
<p>These Terms shall be governed and construed in accordance with the laws of Delaware, United States, without regard to its conflict of law provisions.</p> <h3>Governing Law</h3>
<p>Our failure to enforce any right or provision of these Terms will not be considered a waiver of those rights. If any provision of these Terms is held to be invalid or unenforceable by a court, the remaining provisions of these Terms will remain in effect. These Terms constitute the entire agreement between us regarding our Service, and supersede and replace any prior agreements we might have had between us regarding the Service.</p> <p>These Terms shall be governed and construed in accordance with the laws of Delaware, United States, without regard to its conflict of law provisions.</p>
</Row> <p>Our failure to enforce any right or provision of these Terms will not be considered a waiver of those rights. If any provision of these Terms is held to be invalid or unenforceable by a court, the remaining provisions of these Terms will remain in effect. These Terms constitute the entire agreement between us regarding our Service, and supersede and replace any prior agreements we might have had between us regarding the Service.</p>
<Row> </Row>
<h3>Changes</h3> <Row>
<p>We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a revision is material we will provide at least 30 days notice prior to any new terms taking effect. What constitutes a material change will be determined at our sole discretion.</p> <h3>Changes</h3>
<p>By continuing to access or use our Service after any revisions become effective, you agree to be bound by the revised terms. If you do not agree to the new terms, you are no longer authorized to use the Service.</p> <p>We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a revision is material we will provide at least 30 days notice prior to any new terms taking effect. What constitutes a material change will be determined at our sole discretion.</p>
</Row> <p>By continuing to access or use our Service after any revisions become effective, you agree to be bound by the revised terms. If you do not agree to the new terms, you are no longer authorized to use the Service.</p>
<Row> </Row>
<h3>Contact Us</h3> <Row>
<p>If you have any questions about these Terms, please <a className='link--primary' href='mailto:hello@lbry.io'>contact us</a>.</p> <h3>Contact Us</h3>
<p>If you have any questions about these Terms, please <a className='link--primary' href='mailto:hello@lbry.io'>contact us</a>.</p>
</Row>
</Row> </Row>
</PageLayout> </PageLayout>
); );

View file

@ -1,44 +1,44 @@
import siteConfig from '@config/siteConfig.json'; import siteConfig from '@config/siteConfig.json';
import determineContentTypeFromExtension from './determineContentTypeFromExtension'; import determineContentTypeFromExtension from './determineContentTypeFromExtension';
import createMetaTagsArray from './createMetaTagsArray'; import createMetaTagsArray from './createMetaTagsArray';
import createCanonicalLink from '../../../utils/createCanonicalLink'; import createCanonicalLink from '@globalutils/createCanonicalLink';
const { const {
details: { details: { host, title: siteTitle, twitter },
host, assetDefaults: { description: defaultDescription, thumbnail: defaultThumbnail },
title: siteTitle,
twitter,
},
assetDefaults: {
description: defaultDescription,
thumbnail: defaultThumbnail,
},
} = siteConfig; } = siteConfig;
const VIDEO = 'VIDEO'; const VIDEO = 'VIDEO';
const IMAGE = 'IMAGE'; const IMAGE = 'IMAGE';
const GIF = 'GIF'; const GIF = 'GIF';
const TEXT = 'TEXT';
const determineMediaType = (contentType) => { const determineMediaType = contentType => {
switch (contentType) { switch (contentType) {
case 'image/jpg': case 'image/jpg':
case 'image/jpeg': case 'image/jpeg':
case 'image/png': case 'image/png':
case 'image/svg+xml':
return IMAGE; return IMAGE;
case 'image/gif': case 'image/gif':
return GIF; return GIF;
case 'video/mp4': case 'video/mp4':
case 'video/webm': case 'video/webm':
return VIDEO; return VIDEO;
case 'text/markdown':
case 'text/plain':
return TEXT;
default: default:
return ''; return '';
} }
}; };
const createAssetMetaTags = (asset) => { const createAssetMetaTags = asset => {
const { claimData } = asset; const { claimData } = asset;
const { contentType } = claimData; const { contentType } = claimData;
const canonicalLink = createCanonicalLink({ asset: { ...asset.claimData, shortId: asset.shortId }}); const canonicalLink = createCanonicalLink({
asset: { ...asset.claimData, shortId: asset.shortId },
});
const showUrl = `${host}${canonicalLink}`; const showUrl = `${host}${canonicalLink}`;
const serveUrl = `${showUrl}.${claimData.fileExt}`; const serveUrl = `${showUrl}.${claimData.fileExt}`;
@ -46,16 +46,19 @@ const createAssetMetaTags = (asset) => {
const ogDescription = claimData.description || defaultDescription; const ogDescription = claimData.description || defaultDescription;
const ogThumbnailContentType = determineContentTypeFromExtension(claimData.thumbnail); const ogThumbnailContentType = determineContentTypeFromExtension(claimData.thumbnail);
const ogThumbnail = claimData.thumbnail || defaultThumbnail; const ogThumbnail = claimData.thumbnail || defaultThumbnail;
console.log('asset.claimData', asset.claimData);
// {property: 'og:title'] = ogTitle}, // {property: 'og:title'] = ogTitle},
const metaTags = { const metaTags = {
'og:title' : ogTitle, 'og:title': ogTitle,
'twitter:title' : ogTitle, 'twitter:title': ogTitle,
'og:description' : ogDescription, 'og:description': ogDescription,
'twitter:description': ogDescription, 'twitter:description': ogDescription,
'og:url' : showUrl, 'og:url': showUrl,
'og:site_name' : siteTitle, 'og:site_name': siteTitle,
'twitter:site' : twitter, 'twitter:site': twitter,
'fb:app_id' : '1371961932852223', 'fb:app_id': '1371961932852223',
}; };
if (determineMediaType(contentType) === VIDEO) { if (determineMediaType(contentType) === VIDEO) {
const videoEmbedUrl = `${host}/video-embed${canonicalLink}`; const videoEmbedUrl = `${host}/video-embed${canonicalLink}`;
@ -85,8 +88,6 @@ const createAssetMetaTags = (asset) => {
// image tags // image tags
metaTags['og:image'] = serveUrl; metaTags['og:image'] = serveUrl;
metaTags['og:image'] = serveUrl; metaTags['og:image'] = serveUrl;
metaTags['og:image:width'] = 600;
metaTags['og:image:height'] = 315;
metaTags['og:image:type'] = contentType; metaTags['og:image:type'] = contentType;
metaTags['twitter:image'] = serveUrl; metaTags['twitter:image'] = serveUrl;
} }

View file

@ -1,7 +1,7 @@
import siteConfig from '@config/siteConfig.json'; import siteConfig from '@config/siteConfig.json';
import determineContentTypeFromExtension from './determineContentTypeFromExtension'; import determineContentTypeFromExtension from './determineContentTypeFromExtension';
import createMetaTagsArray from './createMetaTagsArray'; import createMetaTagsArray from './createMetaTagsArray';
import createCanonicalLink from '../../../utils/createCanonicalLink'; import createCanonicalLink from '@globalutils/createCanonicalLink';
const { const {
details: { details: {

View file

@ -1,4 +1,4 @@
const determineContentTypeFromExtension = (thumbnail) => { const determineContentTypeFromExtension = thumbnail => {
if (thumbnail) { if (thumbnail) {
const fileExt = thumbnail.substring(thumbnail.lastIndexOf('.')); const fileExt = thumbnail.substring(thumbnail.lastIndexOf('.'));
switch (fileExt) { switch (fileExt) {
@ -11,6 +11,11 @@ const determineContentTypeFromExtension = (thumbnail) => {
return 'image/gif'; return 'image/gif';
case 'mp4': case 'mp4':
return 'video/mp4'; return 'video/mp4';
case 'svg':
return 'image/svg+xml';
case 'md':
case 'markdown':
return 'text/markdown';
default: default:
return ''; return '';
} }

View file

@ -1,15 +1,12 @@
import siteConfig from '@config/siteConfig.json'; import siteConfig from '@config/siteConfig.json';
const { const {
publishing: { publishing: { maxSizeImage = 10000000, maxSizeGif = 50000000, maxSizeVideo = 50000000 },
maxSizeImage = 10000000,
maxSizeGif = 50000000,
maxSizeVideo = 50000000,
}
} = siteConfig; } = siteConfig;
// TODO: central constants location
const SIZE_MB = 1000000;
export function validateFile(file) {
export function validateFile (file) {
if (!file) { if (!file) {
throw new Error('no file provided'); throw new Error('no file provided');
} }
@ -21,6 +18,7 @@ export function validateFile (file) {
case 'image/jpeg': case 'image/jpeg':
case 'image/jpg': case 'image/jpg':
case 'image/png': case 'image/png':
case 'image/svg+xml':
if (file.size > maxSizeImage) { if (file.size > maxSizeImage) {
throw new Error(`Sorry, images are limited to ${maxSizeImage / SIZE_MB} megabytes.`); throw new Error(`Sorry, images are limited to ${maxSizeImage / SIZE_MB} megabytes.`);
} }
@ -36,6 +34,9 @@ export function validateFile (file) {
} }
break; break;
default: default:
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.'); throw new Error(
file.type +
' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.'
);
} }
} }

6
lintstagedrc.json Normal file
View file

@ -0,0 +1,6 @@
{
"linters": {
"src/**/*.{js,jsx,scss,json}": ["prettier --write", "git add"],
"src/**/*.{js,jsx}": ["eslint --fix", "flow focus-check --color always", "git add"]
}
}

1997
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -63,6 +63,7 @@
"react-feather": "^1.1.4", "react-feather": "^1.1.4",
"react-ga": "^2.5.3", "react-ga": "^2.5.3",
"react-helmet": "^5.2.0", "react-helmet": "^5.2.0",
"react-markdown": "^4.0.6",
"react-redux": "^5.1.1", "react-redux": "^5.1.1",
"react-router-dom": "^4.3.1", "react-router-dom": "^4.3.1",
"react-select": "^2.1.1", "react-select": "^2.1.1",
@ -103,13 +104,15 @@
"extract-css-chunks-webpack-plugin": "^3.2.1", "extract-css-chunks-webpack-plugin": "^3.2.1",
"file-loader": "^2.0.0", "file-loader": "^2.0.0",
"har-validator": "^5.1.3", "har-validator": "^5.1.3",
"husky": "^1.1.3", "husky": "^1.3.1",
"lint-staged": "^8.1.0",
"md5-file": "^4.0.0", "md5-file": "^4.0.0",
"mini-css-extract-plugin": "^0.5.0", "mini-css-extract-plugin": "^0.5.0",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"ndb": "^1.0.26", "ndb": "^1.0.42",
"node-sass": "^4.11.0", "node-sass": "^4.11.0",
"nodemon": "^1.18.6", "nodemon": "^1.18.6",
"prettier": "1.15.3",
"react-color": "^2.14.1", "react-color": "^2.14.1",
"react-hot-loader": "^4.6.0", "react-hot-loader": "^4.6.0",
"redux-devtools": "^3.4.1", "redux-devtools": "^3.4.1",
@ -128,7 +131,18 @@
}, },
"husky": { "husky": {
"hooks": { "hooks": {
"pre-commit": "eslint ." "pre-commit": "lint-staged"
} }
},
"lint-staged": {
"*.js": [
"eslint --fix",
"prettier --write",
"git add"
],
"*.{json,css,md}": [
"prettier --write",
"git add"
]
} }
} }

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.0"
width="750"
height="600"
id="svg2450">
<defs
id="defs2453" />
<path
d="M 49.593496,600 C 12.021823,606.01223 0.374478,584.2484 0,550.4065 L 0,50 C 0.26436,7.306444 22.977953,0.531662 49.593496,0 L 700,0 C 737.99534,1.025643 751.96677,19.66882 750,50 L 750,550.4065 C 750.61508,591.42014 731.34035,604.25656 700,600 L 49.593496,600 z"
id="path2477"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
d="M 376,580 C 120.0594,566.76664 10.515234,350.74039 13,201 C 25.291003,120.21718 21.978524,25.547435 376,19 C 654.93316,28.316194 722.07201,66.834005 735,201 C 734.63343,336.58827 649.04448,559.11396 376,580 z"
id="path2475"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
d="M 266.04757,331.75287 L 266.04757,401.89337 L 224.48919,401.89337 L 224.48919,331.75287 L 104.10864,331.75287 L 104.10864,302.29861 L 224.48919,111.77756 L 266.04757,111.77756 L 266.04757,292.77124 L 283.37127,292.77124 L 283.37127,331.75287 L 266.04757,331.75287 z M 224.48919,184.09839 L 155.20761,292.77124 L 224.48919,292.77124 L 224.48919,184.09839 z M 489.08856,300.14471 C 489.08838,315.43793 486.77811,329.57922 482.15776,342.56862 C 477.53705,355.55814 471.18548,366.81655 463.10301,376.34389 C 455.02023,385.87128 445.49507,393.37689 434.52749,398.86073 C 423.55966,404.34459 411.57888,407.08651 398.58511,407.08652 C 385.59997,407.08651 373.11705,404.9216 361.13632,400.59178 C 349.15548,396.26196 337.8266,389.91259 327.14962,381.54364 L 354.42355,352.1026 C 361.6384,357.00948 368.27849,360.61693 374.34382,362.92496 C 380.40903,365.23306 387.04911,366.38709 394.26409,366.38705 C 402.05152,366.38709 409.26642,364.72652 415.90881,361.40533 C 422.55099,358.08423 428.25287,353.39323 433.01448,347.33231 C 437.77583,341.27149 441.52864,334.0566 444.2729,325.68759 C 447.01689,317.31874 448.38896,308.08208 448.38909,297.9776 C 448.38896,282.09436 444.7793,269.67971 437.56013,260.73363 C 430.3407,251.78782 420.67018,247.31485 408.54855,247.3147 C 395.8453,247.31485 384.30058,252.07633 373.91436,261.59915 L 334.07382,256.40601 L 341.43408,111.77756 L 477.83013,111.77756 L 477.83013,152.49023 L 379.96643,152.49023 L 376.94039,213.53943 C 384.15522,210.94084 390.00026,209.28027 394.47552,208.55771 C 398.95061,207.83553 403.93232,207.47435 409.42068,207.47415 C 421.2516,207.47435 432.07395,209.71193 441.88775,214.18692 C 451.70129,218.66228 460.07242,225.01386 467.00117,233.24167 C 473.9296,241.46982 479.34298,251.21301 483.24131,262.47128 C 487.1393,273.72983 489.08838,286.28763 489.08856,300.14471 L 489.08856,300.14471 z M 557.52004,401.89337 L 557.52004,163.74866 L 539.33743,163.74866 L 539.33743,132.1405 C 547.70633,131.55935 554.41689,129.5376 559.46913,126.07523 C 564.52127,122.61341 568.63306,117.84752 571.8045,111.77756 L 599.5277,111.77756 L 599.5277,401.89337 L 557.52004,401.89337 z"
id="text3960"
style="font-size:433px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:100%;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Arial;-inkscape-font-specification:Arial" />
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -2,7 +2,7 @@ const logger = require('winston');
const { const {
assetDefaults: { thumbnail: defaultThumbnail }, assetDefaults: { thumbnail: defaultThumbnail },
details: { host } details: { host },
} = require('@config/siteConfig'); } = require('@config/siteConfig');
const getterMethods = { const getterMethods = {
@ -15,8 +15,12 @@ const getterMethods = {
return 'png'; return 'png';
case 'image/gif': case 'image/gif':
return 'gif'; return 'gif';
case 'image/svg+xml':
return 'svg';
case 'video/mp4': case 'video/mp4':
return 'mp4'; return 'mp4';
case 'text/markdown':
return 'md';
default: default:
logger.debug('setting unknown file type as file extension jpg'); logger.debug('setting unknown file type as file extension jpg');
return 'jpg'; return 'jpg';
@ -31,141 +35,134 @@ const getterMethods = {
generated_channel() { generated_channel() {
console.log(this); console.log(this);
// //
}
}
export default (sequelize, {
BOOLEAN,
DATE,
DECIMAL,
ENUM,
INTEGER,
STRING,
TEXT,
}) => sequelize.define(
'claim',
{
id: {
primaryKey: true,
type: INTEGER,
set() { },
},
transaction_hash_id: {
type: STRING,
set() { },
},
vout: {
type: INTEGER,
set() { },
},
name: {
type: STRING,
set() { },
},
claim_id: {
type: STRING,
set() { },
},
claim_type: {
type: INTEGER,
set() { },
},
publisher_id: {
type: STRING,
set() { },
},
publisher_sig: {
type: STRING,
set() { },
},
certificate: {
type: STRING,
set() { },
},
sd_hash: {
type: STRING,
set() { },
},
transaction_time: {
type: INTEGER,
set() { },
},
version: {
type: STRING,
set() { },
},
valid_at_height: {
type: INTEGER,
set() { },
},
height: {
type: INTEGER,
set() { },
},
effective_amount: {
type: INTEGER,
set() { },
},
author: {
type: STRING,
set() { },
},
description: {
type: STRING,
set() { },
},
content_type: {
type: STRING,
set() { },
},
is_nsfw: {
type: BOOLEAN,
set() { },
},
language: {
type: STRING,
set() { },
},
thumbnail_url: {
type: STRING,
set() { },
},
title: {
type: STRING,
set() { },
},
fee: {
type: DECIMAL(58, 8),
set() { },
},
fee_currency: {
type: STRING,
set() { },
},
bid_state: {
type: ENUM('Active', 'Expired', 'Controlling', 'Spent', 'Accepted'),
set() { },
},
created_at: {
type: DATE(6),
set() { },
},
modified_at: {
type: DATE(6),
set() { },
},
fee_address: {
type: STRING,
set() { },
},
claim_address: {
type: STRING,
set() { },
},
}, },
{ };
freezeTableName: true,
getterMethods, export default (sequelize, { BOOLEAN, DATE, DECIMAL, ENUM, INTEGER, STRING, TEXT }) =>
timestamps: false, // don't use default timestamps columns sequelize.define(
} 'claim',
); {
id: {
primaryKey: true,
type: INTEGER,
set() {},
},
transaction_hash_id: {
type: STRING,
set() {},
},
vout: {
type: INTEGER,
set() {},
},
name: {
type: STRING,
set() {},
},
claim_id: {
type: STRING,
set() {},
},
claim_type: {
type: INTEGER,
set() {},
},
publisher_id: {
type: STRING,
set() {},
},
publisher_sig: {
type: STRING,
set() {},
},
certificate: {
type: STRING,
set() {},
},
sd_hash: {
type: STRING,
set() {},
},
transaction_time: {
type: INTEGER,
set() {},
},
version: {
type: STRING,
set() {},
},
valid_at_height: {
type: INTEGER,
set() {},
},
height: {
type: INTEGER,
set() {},
},
effective_amount: {
type: INTEGER,
set() {},
},
author: {
type: STRING,
set() {},
},
description: {
type: STRING,
set() {},
},
content_type: {
type: STRING,
set() {},
},
is_nsfw: {
type: BOOLEAN,
set() {},
},
language: {
type: STRING,
set() {},
},
thumbnail_url: {
type: STRING,
set() {},
},
title: {
type: STRING,
set() {},
},
fee: {
type: DECIMAL(58, 8),
set() {},
},
fee_currency: {
type: STRING,
set() {},
},
bid_state: {
type: ENUM('Active', 'Expired', 'Controlling', 'Spent', 'Accepted'),
set() {},
},
created_at: {
type: DATE(6),
set() {},
},
modified_at: {
type: DATE(6),
set() {},
},
fee_address: {
type: STRING,
set() {},
},
claim_address: {
type: STRING,
set() {},
},
},
{
freezeTableName: true,
getterMethods,
timestamps: false, // don't use default timestamps columns
}
);

View file

@ -18,92 +18,77 @@ const returnShortId = (claimsArray, longId) => {
shortIdLength += 1; shortIdLength += 1;
shortId = longId.substring(0, shortIdLength); shortId = longId.substring(0, shortIdLength);
possibleMatches = possibleMatches.filter(element => { possibleMatches = possibleMatches.filter(element => {
return (element.claim_id && (element.claim_id.substring(0, shortIdLength) === shortId)); return element.claim_id && element.claim_id.substring(0, shortIdLength) === shortId;
}); });
} }
return shortId; return shortId;
}; };
const isLongClaimId = (claimId) => { const isLongClaimId = claimId => {
return (claimId && (claimId.length === 40)); return claimId && claimId.length === 40;
} };
const isShortClaimId = (claimId) => { const isShortClaimId = claimId => {
return (claimId && (claimId.length < 40)); return claimId && claimId.length < 40;
} };
export default (db, table, sequelize) => ({ export default (db, table, sequelize) => ({
getClaimChannelName: async publisher_id => {
return await table
.findAll({
where: { claim_id: publisher_id },
attributes: ['name'],
})
.then(result => {
if (result.length === 0) {
throw new Error(`no record found for ${claimId}`);
} else if (result.length !== 1) {
logger.warn(`more than one record matches ${claimId} in db.Claim`);
}
getClaimChannelName: async (publisher_id) => { return result[0].name;
return await table.findAll({ });
where : { claim_id: publisher_id },
attributes: ['name'],
}).then(result => {
if(result.length === 0) {
throw new Error(`no record found for ${claimId}`);
} else if(result.length !== 1) {
logger.warn(`more than one record matches ${claimId} in db.Claim`);
}
return result[0].name;
});
}, },
getShortClaimIdFromLongClaimId: async (claimId, claimName, pendingClaim) => { getShortClaimIdFromLongClaimId: async (claimId, claimName, pendingClaim) => {
logger.debug(`claim.getShortClaimIdFromLongClaimId for ${claimName}#${claimId}`); logger.debug(`claim.getShortClaimIdFromLongClaimId for ${claimName}#${claimId}`);
return await table.findAll({ return await table
where: { name: claimName }, .findAll({
order: [['height', 'ASC']], where: { name: claimName },
}).then(result => { order: [['height', 'ASC']],
if(result.length === 0) { })
throw new Error('No claim(s) found with that claim name'); .then(result => {
} if (result.length === 0) {
throw new Error('No claim(s) found with that claim name');
}
let list = result.map(claim => claim.dataValues); let list = result.map(claim => claim.dataValues);
if (pendingClaim) { if (pendingClaim) {
list = list.concat(pendingClaim); list = list.concat(pendingClaim);
} }
return returnShortId(list, claimId); return returnShortId(list, claimId);
}); });
}, },
getAllChannelClaims: async (channelClaimId, params) => { getAllChannelClaims: async (channelClaimId, bidState) => {
logger.debug(`claim.getAllChannelClaims for ${channelClaimId}`); logger.debug(`claim.getAllChannelClaims for ${channelClaimId}`);
const whereClause = bidState || {
const defaultWhereClauses = { [sequelize.Op.or]: [
bid_state: { [sequelize.Op.or]: ['Controlling', 'Active', 'Accepted'] } { bid_state: 'Controlling' },
{ bid_state: 'Active' },
{ bid_state: 'Accepted' },
],
}; };
const addWhereClauses = (whereClauses, params) => {
/*
input params = { col: ['Val', 'Val']}
output = { col: { Op.or : [ { Op.eq: 'Value'},...]}, col2:...}
*/
const cols = Object.keys(params)
for (let colKey in cols){
let col = Object.keys(params)[colKey]
whereClauses[col] = {}
whereClauses[col][sequelize.Op.or] = []
for (let itemKey in params[col] ){
let itemsArr = params[col]
whereClauses[col][sequelize.Op.or].push({ [sequelize.Op.eq]: itemsArr[itemKey] })
}
}
return whereClauses;
}
const whereClause = addWhereClauses(defaultWhereClauses, params);
const selectWhere = { const selectWhere = {
...whereClause, ...whereClause,
publisher_id: channelClaimId, publisher_id: channelClaimId,
}; };
return await table.findAll({ return await table
where: selectWhere, .findAll({
order: [['height', 'DESC'],['claim_id', 'ASC']], where: selectWhere,
}) order: [['height', 'DESC'], ['claim_id', 'ASC']],
})
.then(channelClaimsArray => { .then(channelClaimsArray => {
if (channelClaimsArray.length === 0) { if (channelClaimsArray.length === 0) {
return null; return null;
@ -114,67 +99,80 @@ export default (db, table, sequelize) => ({
getClaimIdByLongChannelId: async (channelClaimId, claimName) => { getClaimIdByLongChannelId: async (channelClaimId, claimName) => {
logger.debug(`finding claim id for claim ${claimName} from channel ${channelClaimId}`); logger.debug(`finding claim id for claim ${claimName} from channel ${channelClaimId}`);
return await table.findAll({ return await table
where: { name: claimName, publisher_id: channelClaimId, bid_state: { [sequelize.Op.or]: ['Controlling', 'Active', 'Accepted'] } }, .findAll({
order: [['id', 'ASC']], where: {
}) name: claimName,
.then(result => { publisher_id: channelClaimId,
switch (result.length) { bid_state: { [sequelize.Op.or]: ['Controlling', 'Active', 'Accepted'] },
case 0: },
return null; order: [['id', 'ASC']],
case 1: })
return result[0].claim_id; .then(result => {
default: switch (result.length) {
// Does this actually happen??? (from converted code) case 0:
logger.warn(`${result.length} records found for "${claimName}" in channel "${channelClaimId}"`); return null;
return result[0].claim_id; case 1:
} return result[0].claim_id;
}); default:
// Does this actually happen??? (from converted code)
logger.warn(
`${result.length} records found for "${claimName}" in channel "${channelClaimId}"`
);
return result[0].claim_id;
}
});
}, },
validateLongClaimId: async (name, claimId) => { validateLongClaimId: async (name, claimId) => {
return await table.findOne({ return await table
where: { .findOne({
name, where: {
claim_id: claimId, name,
}, claim_id: claimId,
}).then(result => { },
if (!result) { })
return false; .then(result => {
} if (!result) {
return claimId; return false;
}); }
return claimId;
});
}, },
getLongClaimIdFromShortClaimId: async (name, shortId) => { getLongClaimIdFromShortClaimId: async (name, shortId) => {
return await table.findAll({ return await table
where: { .findAll({
name, where: {
claim_id: { name,
[sequelize.Op.like]: `${shortId}%`, claim_id: {
}}, [sequelize.Op.like]: `${shortId}%`,
order: [['height', 'ASC']], },
}) },
.then(result => { order: [['height', 'ASC']],
if(result.length === 0) { })
return null; .then(result => {
} if (result.length === 0) {
return null;
}
return result[0].claim_id; return result[0].claim_id;
}); });
}, },
getTopFreeClaimIdByClaimName: async (name) => { getTopFreeClaimIdByClaimName: async name => {
return await table.findAll({ return await table
// TODO: Limit 1 .findAll({
where: { name, bid_state: { [sequelize.Op.or]: ['Controlling', 'Active', 'Accepted'] } }, // TODO: Limit 1
order: [['effective_amount', 'DESC'], ['height', 'ASC']], where: { name, bid_state: { [sequelize.Op.or]: ['Controlling', 'Active', 'Accepted'] } },
}).then(result => { order: [['effective_amount', 'DESC'], ['height', 'ASC']],
if(result.length === 0) { })
return null; .then(result => {
} if (result.length === 0) {
return result[0].claim_id; return null;
}) }
return result[0].claim_id;
});
}, },
getLongClaimId: async (claimName, claimId) => { getLongClaimId: async (claimName, claimId) => {
@ -191,60 +189,63 @@ export default (db, table, sequelize) => ({
resolveClaim: async (name, claimId) => { resolveClaim: async (name, claimId) => {
logger.debug(`Claim.resolveClaim: ${name} ${claimId}`); logger.debug(`Claim.resolveClaim: ${name} ${claimId}`);
return table.findAll({ return table
where: { name, claim_id: claimId }, .findAll({
}).then(claimArray => { where: { name, claim_id: claimId },
if(claimArray.length === 0) { })
return null; .then(claimArray => {
} else if(claimArray.length !== 1) { if (claimArray.length === 0) {
logger.warn(`more than one record matches ${name}#${claimId} in db.Claim`); return null;
} } else if (claimArray.length !== 1) {
logger.warn(`more than one record matches ${name}#${claimId} in db.Claim`);
}
return claimArray[0]; return claimArray[0];
}); });
}, },
resolveClaimInChannel: async (claimName, channelId) => { resolveClaimInChannel: async (claimName, channelId) => {
logger.debug(`Claim.resolveClaimByNames: ${claimName} in ${channelId}`); logger.debug(`Claim.resolveClaimByNames: ${claimName} in ${channelId}`);
return table.findAll({ return table
where: { .findAll({
name: claimName, where: {
publisher_id: channelId, name: claimName,
}, publisher_id: channelId,
}).then(claimArray => { },
if (claimArray.length === 0) { })
return null; .then(claimArray => {
} else if (claimArray.length !== 1) { if (claimArray.length === 0) {
logger.warn(`more than one record matches ${claimName} in ${channelId}`); return null;
} } else if (claimArray.length !== 1) {
logger.warn(`more than one record matches ${claimName} in ${channelId}`);
}
return claimArray[0]; return claimArray[0];
}); });
}, },
getOutpoint: async (name, claimId) => { getOutpoint: async (name, claimId) => {
logger.debug(`finding outpoint for ${name}#${claimId}`); logger.debug(`finding outpoint for ${name}#${claimId}`);
return await table.findAll({ return await table
where : { name, claim_id: claimId }, .findAll({
attributes: ['transaction_hash_id'], where: { name, claim_id: claimId },
}).then(result => { attributes: ['transaction_hash_id'],
if(result.length === 0) { })
throw new Error(`no record found for ${name}#${claimId}`); .then(result => {
} else if(result.length !== 1) { if (result.length === 0) {
logger.warn(`more than one record matches ${name}#${claimId} in db.Claim`); throw new Error(`no record found for ${name}#${claimId}`);
} } else if (result.length !== 1) {
logger.warn(`more than one record matches ${name}#${claimId} in db.Claim`);
}
return result[0].transaction_hash_id; return result[0].transaction_hash_id;
}); });
}, },
getCurrentHeight: async () => { getCurrentHeight: async () => {
return await table return await table.max('height').then(result => {
.max('height') return result || 100000;
.then(result => {
return (result || 100000);
}); });
}, },
});
})

View file

@ -1,32 +1,34 @@
const db = require('../../../../models');
const chainquery = require('chainquery').default; const chainquery = require('chainquery').default;
const getClaimData = require('server/utils/getClaimData'); const getClaimData = require('server/utils/getClaimData');
const { returnPaginatedChannelClaims } = require('./channelPagination.js'); const { returnPaginatedChannelClaims } = require('./channelPagination.js');
const getChannelClaims = async (channelName, channelShortId, page) => { const getChannelClaims = async (channelName, channelLongId, page) => {
const channelId = await chainquery.claim.queries.getLongClaimId(channelName, channelShortId); let channelShortId = await chainquery.claim.queries.getShortClaimIdFromLongClaimId(
const params = { content_type: [ channelLongId,
'image/jpeg', channelName
'image/jpg', );
'image/png',
'image/gif',
'video/mp4',
] };
let channelClaims; let channelClaims;
if (channelId) { if (channelLongId) {
channelClaims = await chainquery.claim.queries.getAllChannelClaims(channelId, params); channelClaims = await chainquery.claim.queries.getAllChannelClaims(channelLongId);
} }
/*
Put mempool unconfirmed claims at the beginning
*/
const split = channelClaims.reduce( const split = channelClaims.reduce(
(acc, val) => val.dataValues.height === 0 ? { ...acc, zero: acc.zero.concat(val) } : { ...acc, nonzero: acc.nonzero.concat(val) }, (acc, val) =>
val.dataValues.height === 0
? { ...acc, zero: acc.zero.concat(val) }
: { ...acc, nonzero: acc.nonzero.concat(val) },
{ zero: [], nonzero: [] } { zero: [], nonzero: [] }
); );
channelClaims = split.zero.concat(split.nonzero); channelClaims = split.zero.concat(split.nonzero);
const processingChannelClaims = channelClaims ? channelClaims.map((claim) => getClaimData(claim, channelName, channelShortId)) : []; const processingChannelClaims = channelClaims
? channelClaims.map(claim => getClaimData(claim, channelName, channelShortId))
: [];
const processedChannelClaims = await Promise.all(processingChannelClaims); const processedChannelClaims = await Promise.all(processingChannelClaims);
return returnPaginatedChannelClaims(channelName, channelId, processedChannelClaims, page); return returnPaginatedChannelClaims(channelName, channelShortId, processedChannelClaims, page);
}; };
module.exports = getChannelClaims; module.exports = getChannelClaims;

View file

@ -3,7 +3,7 @@ const logger = require('winston');
const { details: { host }, publishing: { disabled, disabledMessage } } = require('@config/siteConfig'); const { details: { host }, publishing: { disabled, disabledMessage } } = require('@config/siteConfig');
const { sendGATimingEvent } = require('../../../../utils/googleAnalytics.js'); const { sendGATimingEvent } = require('../../../../utils/googleAnalytics.js');
const isApprovedChannel = require('../../../../../utils/isApprovedChannel'); const isApprovedChannel = require('@globalutils/isApprovedChannel');
const { publishing: { publishOnlyApproved, approvedChannels } } = require('@config/siteConfig'); const { publishing: { publishOnlyApproved, approvedChannels } } = require('@config/siteConfig');
const { handleErrorResponse } = require('../../../utils/errorHandlers.js'); const { handleErrorResponse } = require('../../../utils/errorHandlers.js');
@ -18,7 +18,7 @@ const parsePublishApiRequestFiles = require('./parsePublishApiRequestFiles.js');
const authenticateUser = require('./authentication.js'); const authenticateUser = require('./authentication.js');
const chainquery = require('chainquery').default; const chainquery = require('chainquery').default;
const createCanonicalLink = require('../../../../../utils/createCanonicalLink'); const createCanonicalLink = require('@globalutils/createCanonicalLink');
const CLAIM_TAKEN = 'CLAIM_TAKEN'; const CLAIM_TAKEN = 'CLAIM_TAKEN';
const UNAPPROVED_CHANNEL = 'UNAPPROVED_CHANNEL'; const UNAPPROVED_CHANNEL = 'UNAPPROVED_CHANNEL';
@ -115,7 +115,7 @@ const claimPublish = ({ body, files, headers, ip, originalUrl, user, tor }, res)
if (channelName) { if (channelName) {
return chainquery.claim.queries.getShortClaimIdFromLongClaimId(claimData.certificateId, channelName); return chainquery.claim.queries.getShortClaimIdFromLongClaimId(claimData.certificateId, channelName);
} else { } else {
return chainquery.claim.queries.getShortClaimIdFromLongClaimId(claimId, name, claimData).catch(error => { return chainquery.claim.queries.getShortClaimIdFromLongClaimId(claimId, name, claimData).catch(() => {
return claimId.slice(0, 1); return claimId.slice(0, 1);
}); });
} }

View file

@ -1,7 +1,7 @@
const path = require('path'); const path = require('path');
const validateFileTypeAndSize = require('./validateFileTypeAndSize.js'); const validateFileTypeAndSize = require('./validateFileTypeAndSize.js');
const parsePublishApiRequestFiles = ({file, thumbnail}, isUpdate) => { const parsePublishApiRequestFiles = ({ file, thumbnail }, isUpdate) => {
// make sure a file was provided // make sure a file was provided
if (!file) { if (!file) {
if (isUpdate) { if (isUpdate) {
@ -14,39 +14,39 @@ const parsePublishApiRequestFiles = ({file, thumbnail}, isUpdate) => {
} }
return {}; return {};
} }
throw new Error('no file with key of [file] found in request'); throw new Error('No file with key of [file] found in request');
} }
if (!file.path) { if (!file.path) {
throw new Error('no file path found'); throw new Error('No file path found');
} }
if (!file.type) { if (!file.type) {
throw new Error('no file type found'); throw new Error('No file type found');
} }
if (!file.size) { if (!file.size) {
throw new Error('no file size found'); throw new Error('No file size found');
} }
// validate the file name // validate the file name
if (!file.name) { if (!file.name) {
throw new Error('no file name found'); throw new Error('No file name found');
} }
if (file.name.indexOf('.') < 0) { if (file.name.indexOf('.') < 0) {
throw new Error('no file extension found in file name'); throw new Error('No file extension found in file name');
} }
if (file.name.indexOf('.') === 0) { if (file.name.indexOf('.') === 0) {
throw new Error('file name cannot start with "."'); throw new Error('File name cannot start with "."');
} }
if (/'/.test(file.name)) { if (/'/.test(file.name)) {
throw new Error('apostrophes are not allowed in the file name'); throw new Error('Apostrophes are not allowed in the file name');
} }
// validate the file // validate the file
if (file) validateFileTypeAndSize(file); if (file) validateFileTypeAndSize(file);
// return results // return results
const obj = { const obj = {
fileName : file.name, fileName: file.name,
filePath : file.path, filePath: file.path,
fileExtension: path.extname(file.path), fileExtension: path.extname(file.path),
fileType : file.type, fileType: file.type,
}; };
if (thumbnail) { if (thumbnail) {

View file

@ -1,21 +1,18 @@
const logger = require('winston'); const logger = require('winston');
const { const {
publishing: { publishing: { maxSizeImage = 10000000, maxSizeGif = 50000000, maxSizeVideo = 50000000 },
maxSizeImage = 10000000,
maxSizeGif = 50000000,
maxSizeVideo = 50000000,
}
} = require('@config/siteConfig'); } = require('@config/siteConfig');
const SIZE_MB = 1000000; const SIZE_MB = 1000000;
const validateFileTypeAndSize = (file) => { const validateFileTypeAndSize = file => {
// check file type and size // check file type and size
switch (file.type) { switch (file.type) {
case 'image/jpeg': case 'image/jpeg':
case 'image/jpg': case 'image/jpg':
case 'image/png': case 'image/png':
case 'image/svg+xml':
if (file.size > maxSizeImage) { if (file.size > maxSizeImage) {
logger.debug('publish > file validation > .jpeg/.jpg/.png was too big'); logger.debug('publish > file validation > .jpeg/.jpg/.png was too big');
throw new Error(`Sorry, images are limited to ${maxSizeImage / SIZE_MB} megabytes.`); throw new Error(`Sorry, images are limited to ${maxSizeImage / SIZE_MB} megabytes.`);
@ -35,7 +32,11 @@ const validateFileTypeAndSize = (file) => {
break; break;
default: default:
logger.debug('publish > file validation > unrecognized file type'); logger.debug('publish > file validation > unrecognized file type');
throw new Error('The ' + file.type + ' content type is not supported. Only, image/jpg, image/png, image/gif, and video/mp4 content types are currently supported.'); throw new Error(
'The ' +
file.type +
' content type is not supported. Only, image/jpg, image/png, image/gif, and video/mp4 content types are currently supported.'
);
} }
return file; return file;
}; };

View file

@ -10,7 +10,7 @@ const parsePublishApiRequestFiles = require('../publish/parsePublishApiRequestFi
const authenticateUser = require('../publish/authentication.js'); const authenticateUser = require('../publish/authentication.js');
const createThumbnailPublishParams = require('../publish/createThumbnailPublishParams.js'); const createThumbnailPublishParams = require('../publish/createThumbnailPublishParams.js');
const chainquery = require('chainquery').default; const chainquery = require('chainquery').default;
const createCanonicalLink = require('../../../../../utils/createCanonicalLink'); const createCanonicalLink = require('@globalutils/createCanonicalLink');
/* /*
route to update a claim through the daemon route to update a claim through the daemon
@ -153,7 +153,7 @@ const claimUpdate = ({ body, files, headers, ip, originalUrl, user, tor }, res)
if (channelName) { if (channelName) {
return chainquery.claim.queries.getShortClaimIdFromLongClaimId(result.certificateId, channelName); return chainquery.claim.queries.getShortClaimIdFromLongClaimId(result.certificateId, channelName);
} else { } else {
return chainquery.claim.queries.getShortClaimIdFromLongClaimId(result.claimId, name, result).catch(error => { return chainquery.claim.queries.getShortClaimIdFromLongClaimId(result.claimId, name, result).catch(() => {
return result.claimId.slice(0, 1); return result.claimId.slice(0, 1);
}); });
} }

View file

@ -3,19 +3,16 @@ const db = require('../../../models');
const getClaimId = require('../../utils/getClaimId'); const getClaimId = require('../../utils/getClaimId');
const { const {
details: { details: { host, title: siteTitle },
host,
title: siteTitle,
},
} = require('@config/siteConfig'); } = require('@config/siteConfig');
const getOEmbedDataForAsset = (channelName, channelClaimId, claimName, claimId) => { const getOEmbedDataForAsset = (channelName, channelClaimId, claimName, claimId) => {
let fileData, claimData; let fileData, claimData;
let data = { let data = {
version : '1.0', version: '1.0',
provider_name: siteTitle, provider_name: siteTitle,
provider_url : host, provider_url: host,
cache_age : 86400, // one day in seconds cache_age: 86400, // one day in seconds
}; };
return getClaimId(channelName, channelClaimId, claimName, claimId) return getClaimId(channelName, channelClaimId, claimName, claimId)
@ -23,7 +20,7 @@ const getOEmbedDataForAsset = (channelName, channelClaimId, claimName, claimId)
claimId = fullClaimId; claimId = fullClaimId;
return db.Claim.findOne({ return db.Claim.findOne({
where: { where: {
name : claimName, name: claimName,
claimId: fullClaimId, claimId: fullClaimId,
}, },
}); });
@ -43,19 +40,23 @@ const getOEmbedDataForAsset = (channelName, channelClaimId, claimName, claimId)
.then(fileRecord => { .then(fileRecord => {
fileData = fileRecord.dataValues; fileData = fileRecord.dataValues;
logger.debug('file data:', fileData); logger.debug('file data:', fileData);
const serveUrl = `${host}/${fileData.claimId}/${fileData.name}.${fileData.fileType.substring(fileData.fileType.indexOf('/') + 1)}`; const serveUrl = `${host}/${fileData.claimId}/${fileData.name}.${fileData.fileType.substring(
fileData.fileType.indexOf('/') + 1
)}`;
// set the resource type // set the resource type
if (fileData.fileType === 'video/mp4') { if (fileData.fileType === 'video/mp4') {
data['type'] = 'video'; data['type'] = 'video';
data['html'] = `<video width="100%" controls poster="${claimData.thumbnail}" src="${serveUrl}"/></video>`; data['html'] = `<video width="100%" controls poster="${
claimData.thumbnail
}" src="${serveUrl}"/></video>`;
} else { } else {
data['type'] = 'picture'; data['type'] = 'picture';
data['url'] = serveUrl; data['url'] = serveUrl;
} }
// get the data // get the data
data['title'] = claimData.title; data['title'] = claimData.title;
data['width'] = fileData.width || 600; data['width'] = fileData.fileWidth || 600;
data['height'] = fileData.height || 400; data['height'] = fileData.fileHeight || 400;
data['author_name'] = siteTitle; data['author_name'] = siteTitle;
data['author_url'] = host; data['author_url'] = host;
}) })

View file

@ -36,7 +36,7 @@ const {
const { sessionKey } = require('@private/authConfig.json'); const { sessionKey } = require('@private/authConfig.json');
// configure.js doesn't handle new keys in config.json files yet. Make sure it doens't break. // configure.js doesn't handle new keys in config.json files yet. Make sure it doens't break.
let bLE; let finalBlockListEndpoint;
function Server () { function Server () {
this.initialize = () => { this.initialize = () => {
@ -176,30 +176,29 @@ function Server () {
return; return;
} }
if (blockListEndpoint) { if (blockListEndpoint) {
bLE = blockListEndpoint; finalBlockListEndpoint = blockListEndpoint;
} else if (!blockListEndpoint) { } else if (!blockListEndpoint) {
if (typeof (blockListEndpoint) !== 'string') { if (typeof (blockListEndpoint) !== 'string') {
logger.warn('blockListEndpoint is null due to outdated siteConfig file. \n' + logger.warn('blockListEndpoint is null due to outdated siteConfig file. \n' +
'Continuing with default LBRY blocklist api endpoint. \n ' + 'Continuing with default LBRY blocklist api endpoint. \n ' +
'(Specify /"blockListEndpoint" : ""/ to disable.') '(Specify /"blockListEndpoint" : ""/ to disable.');
bLE = 'https://api.lbry.io/file/list_blocked'; finalBlockListEndpoint = 'https://api.lbry.io/file/list_blocked';
} }
} }
logger.info(`Peforming updates...`); logger.info(`Peforming updates...`);
if (!bLE) { if (!finalBlockListEndpoint) {
logger.info('Configured for no Block List') logger.info('Configured for no Block List');
db.Tor.refreshTable().then( (updatedTorList) => { db.Tor.refreshTable().then((updatedTorList) => {
logger.info('Tor list updated, length:', updatedTorList.length); logger.info('Tor list updated, length:', updatedTorList.length);
}); });
} else { } else {
return Promise.all([ return Promise.all([
db.Blocked.refreshTable(bLE), db.Blocked.refreshTable(finalBlockListEndpoint),
db.Tor.refreshTable()]) db.Tor.refreshTable()])
.then(([updatedBlockedList, updatedTorList]) => { .then(([updatedBlockedList, updatedTorList]) => {
logger.info('Blocked list updated, length:', updatedBlockedList.length); logger.info('Blocked list updated, length:', updatedBlockedList.length);
logger.info('Tor list updated, length:', updatedTorList.length); logger.info('Tor list updated, length:', updatedTorList.length);
}) });
} }
}; };
this.start = () => { this.start = () => {

View file

@ -1,12 +1,17 @@
const logger = require('winston'); const logger = require('winston');
const returnShortId = require('./utils/returnShortId.js'); const returnShortId = require('./utils/returnShortId.js');
const isApprovedChannel = require('../../utils/isApprovedChannel'); const isApprovedChannel = require('../../utils/isApprovedChannel');
const { assetDefaults: { thumbnail: defaultThumbnail }, details: { host } } = require('@config/siteConfig'); const {
const { publishing: { serveOnlyApproved, approvedChannels } } = require('@config/siteConfig'); assetDefaults: { thumbnail: defaultThumbnail },
details: { host },
} = require('@config/siteConfig');
const {
publishing: { serveOnlyApproved, approvedChannels },
} = require('@config/siteConfig');
const NO_CLAIM = 'NO_CLAIM'; const NO_CLAIM = 'NO_CLAIM';
function determineFileExtensionFromContentType (contentType) { function determineFileExtensionFromContentType(contentType) {
switch (contentType) { switch (contentType) {
case 'image/jpeg': case 'image/jpeg':
case 'image/jpg': case 'image/jpg':
@ -17,20 +22,22 @@ function determineFileExtensionFromContentType (contentType) {
return 'gif'; return 'gif';
case 'video/mp4': case 'video/mp4':
return 'mp4'; return 'mp4';
case 'image/svg+xml':
return 'svg';
default: default:
logger.debug('setting unknown file type as file extension jpg'); logger.debug('setting unknown file type as file extension jpg');
return 'jpg'; return 'jpg';
} }
} }
function determineThumbnail (storedThumbnail, defaultThumbnail) { function determineThumbnail(storedThumbnail, defaultThumbnail) {
if (storedThumbnail === '') { if (storedThumbnail === '') {
return defaultThumbnail; return defaultThumbnail;
} }
return storedThumbnail; return storedThumbnail;
} }
function prepareClaimData (claim) { function prepareClaimData(claim) {
// logger.debug('preparing claim data based on resolved data:', claim); // logger.debug('preparing claim data based on resolved data:', claim);
claim['thumbnail'] = determineThumbnail(claim.thumbnail, defaultThumbnail); claim['thumbnail'] = determineThumbnail(claim.thumbnail, defaultThumbnail);
claim['fileExt'] = determineFileExtensionFromContentType(claim.contentType); claim['fileExt'] = determineFileExtensionFromContentType(claim.contentType);
@ -38,12 +45,12 @@ function prepareClaimData (claim) {
return claim; return claim;
} }
function isLongClaimId (claimId) { function isLongClaimId(claimId) {
return (claimId && (claimId.length === 40)); return claimId && claimId.length === 40;
} }
function isShortClaimId (claimId) { function isShortClaimId(claimId) {
return (claimId && (claimId.length < 40)); return claimId && claimId.length < 40;
} }
module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
@ -51,141 +58,141 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
'Claim', 'Claim',
{ {
address: { address: {
type : STRING, type: STRING,
default: null, default: null,
}, },
amount: { amount: {
type : DECIMAL(19, 8), type: DECIMAL(19, 8),
default: null, default: null,
}, },
claimId: { claimId: {
type : STRING, type: STRING,
default: null, default: null,
}, },
claimSequence: { claimSequence: {
type : INTEGER, type: INTEGER,
default: null, default: null,
}, },
decodedClaim: { decodedClaim: {
type : BOOLEAN, type: BOOLEAN,
default: null, default: null,
}, },
depth: { depth: {
type : INTEGER, type: INTEGER,
default: null, default: null,
}, },
effectiveAmount: { effectiveAmount: {
type : DECIMAL(19, 8), type: DECIMAL(19, 8),
default: null, default: null,
}, },
hasSignature: { hasSignature: {
type : BOOLEAN, type: BOOLEAN,
default: null, default: null,
}, },
height: { height: {
type : INTEGER, type: INTEGER,
default: null, default: null,
}, },
hex: { hex: {
type : TEXT('long'), type: TEXT('long'),
default: null, default: null,
}, },
name: { name: {
type : STRING, type: STRING,
default: null, default: null,
}, },
nout: { nout: {
type : INTEGER, type: INTEGER,
default: null, default: null,
}, },
txid: { txid: {
type : STRING, type: STRING,
default: null, default: null,
}, },
validAtHeight: { validAtHeight: {
type : INTEGER, type: INTEGER,
default: null, default: null,
}, },
outpoint: { outpoint: {
type : STRING, type: STRING,
default: null, default: null,
}, },
claimType: { claimType: {
type : STRING, type: STRING,
default: null, default: null,
}, },
certificateId: { certificateId: {
type : STRING, type: STRING,
default: null, default: null,
}, },
author: { author: {
type : STRING, type: STRING,
default: null, default: null,
}, },
description: { description: {
type : TEXT('long'), type: TEXT('long'),
default: null, default: null,
}, },
language: { language: {
type : STRING, type: STRING,
default: null, default: null,
}, },
license: { license: {
type : STRING, type: STRING,
default: null, default: null,
}, },
licenseUrl: { licenseUrl: {
type : STRING, type: STRING,
default: null, default: null,
}, },
nsfw: { nsfw: {
type : BOOLEAN, type: BOOLEAN,
default: null, default: null,
}, },
preview: { preview: {
type : STRING, type: STRING,
default: null, default: null,
}, },
thumbnail: { thumbnail: {
type : STRING, type: STRING,
default: null, default: null,
}, },
title: { title: {
type : STRING, type: STRING,
default: null, default: null,
}, },
metadataVersion: { metadataVersion: {
type : STRING, type: STRING,
default: null, default: null,
}, },
contentType: { contentType: {
type : STRING, type: STRING,
default: null, default: null,
}, },
source: { source: {
type : STRING, type: STRING,
default: null, default: null,
}, },
sourceType: { sourceType: {
type : STRING, type: STRING,
default: null, default: null,
}, },
sourceVersion: { sourceVersion: {
type : STRING, type: STRING,
default: null, default: null,
}, },
streamVersion: { streamVersion: {
type : STRING, type: STRING,
default: null, default: null,
}, },
valueVersion: { valueVersion: {
type : STRING, type: STRING,
default: null, default: null,
}, },
channelName: { channelName: {
type : STRING, type: STRING,
allowNull: true, allowNull: true,
default : null, default: null,
}, },
}, },
{ {
@ -201,14 +208,13 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
}); });
}; };
Claim.getShortClaimIdFromLongClaimId = function (claimId, claimName) { Claim.getShortClaimIdFromLongClaimId = function(claimId, claimName) {
logger.debug(`Claim.getShortClaimIdFromLongClaimId for ${claimName}#${claimId}`); logger.debug(`Claim.getShortClaimIdFromLongClaimId for ${claimName}#${claimId}`);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this this.findAll({
.findAll({ where: { name: claimName },
where: { name: claimName }, order: [['height', 'ASC']],
order: [['height', 'ASC']], })
})
.then(result => { .then(result => {
switch (result.length) { switch (result.length) {
case 0: case 0:
@ -223,15 +229,14 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
}); });
}; };
Claim.getAllChannelClaims = function (channelClaimId) { Claim.getAllChannelClaims = function(channelClaimId) {
logger.debug(`Claim.getAllChannelClaims for ${channelClaimId}`); logger.debug(`Claim.getAllChannelClaims for ${channelClaimId}`);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this this.findAll({
.findAll({ where: { certificateId: channelClaimId },
where: { certificateId: channelClaimId }, order: [['height', 'DESC']],
order: [['height', 'DESC']], raw: true, // returns an array of only data, not an array of instances
raw : true, // returns an array of only data, not an array of instances })
})
.then(channelClaimsArray => { .then(channelClaimsArray => {
switch (channelClaimsArray.length) { switch (channelClaimsArray.length) {
case 0: case 0:
@ -251,14 +256,13 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
}); });
}; };
Claim.getClaimIdByLongChannelId = function (channelClaimId, claimName) { Claim.getClaimIdByLongChannelId = function(channelClaimId, claimName) {
logger.debug(`finding claim id for claim ${claimName} from channel ${channelClaimId}`); logger.debug(`finding claim id for claim ${claimName} from channel ${channelClaimId}`);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this this.findAll({
.findAll({ where: { name: claimName, certificateId: channelClaimId },
where: { name: claimName, certificateId: channelClaimId }, order: [['id', 'ASC']],
order: [['id', 'ASC']], })
})
.then(result => { .then(result => {
switch (result.length) { switch (result.length) {
case 0: case 0:
@ -266,7 +270,9 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
case 1: case 1:
return resolve(result[0].claimId); return resolve(result[0].claimId);
default: default:
logger.warn(`${result.length} records found for "${claimName}" in channel "${channelClaimId}"`); logger.warn(
`${result.length} records found for "${claimName}" in channel "${channelClaimId}"`
);
return resolve(result[0].claimId); return resolve(result[0].claimId);
} }
}) })
@ -276,7 +282,7 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
}); });
}; };
Claim.validateLongClaimId = function (name, claimId) { Claim.validateLongClaimId = function(name, claimId) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.findOne({ this.findOne({
where: { where: {
@ -297,17 +303,17 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
}); });
}; };
Claim.getLongClaimIdFromShortClaimId = function (name, shortId) { Claim.getLongClaimIdFromShortClaimId = function(name, shortId) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this this.findAll({
.findAll({ where: {
where: { name,
name, claimId: {
claimId: { [sequelize.Op.like]: `${shortId}%`,
[sequelize.Op.like]: `${shortId}%`, },
}}, },
order: [['height', 'ASC']], order: [['height', 'ASC']],
}) })
.then(result => { .then(result => {
switch (result.length) { switch (result.length) {
case 0: case 0:
@ -323,13 +329,12 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
}); });
}; };
Claim.getTopFreeClaimIdByClaimName = function (name) { Claim.getTopFreeClaimIdByClaimName = function(name) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this this.findAll({
.findAll({ where: { name },
where: { name }, order: [['effectiveAmount', 'DESC'], ['height', 'ASC']],
order: [['effectiveAmount', 'DESC'], ['height', 'ASC']], })
})
.then(result => { .then(result => {
switch (result.length) { switch (result.length) {
case 0: case 0:
@ -345,7 +350,7 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
}); });
}; };
Claim.getLongClaimId = function (claimName, claimId) { Claim.getLongClaimId = function(claimName, claimId) {
logger.debug(`getLongClaimId(${claimName}, ${claimId})`); logger.debug(`getLongClaimId(${claimName}, ${claimId})`);
if (isLongClaimId(claimId)) { if (isLongClaimId(claimId)) {
return this.validateLongClaimId(claimName, claimId); return this.validateLongClaimId(claimName, claimId);
@ -356,13 +361,12 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
} }
}; };
Claim.fetchClaim = function (name, claimId) { Claim.fetchClaim = function(name, claimId) {
logger.debug(`Claim.resolveClaim: ${name} ${claimId}`); logger.debug(`Claim.resolveClaim: ${name} ${claimId}`);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this this.findAll({
.findAll({ where: { name, claimId },
where: { name, claimId }, })
})
.then(claimArray => { .then(claimArray => {
switch (claimArray.length) { switch (claimArray.length) {
case 0: case 0:
@ -380,13 +384,15 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
}); });
}; };
Claim.resolveClaim = function (name, claimId) { Claim.resolveClaim = function(name, claimId) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this this.fetchClaim(name, claimId)
.fetchClaim(name, claimId)
.then(claim => { .then(claim => {
logger.info('resolveClaim claims:', claim); logger.info('resolveClaim claims:', claim);
if (serveOnlyApproved && !isApprovedChannel({ longId: claim.certificateId }, approvedChannels)) { if (
serveOnlyApproved &&
!isApprovedChannel({ longId: claim.certificateId }, approvedChannels)
) {
throw new Error('This content is unavailable'); throw new Error('This content is unavailable');
} }
return resolve(claim); return resolve(claim);
@ -397,13 +403,12 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
}); });
}; };
Claim.getOutpoint = function (name, claimId) { Claim.getOutpoint = function(name, claimId) {
logger.debug(`finding outpoint for ${name}#${claimId}`); logger.debug(`finding outpoint for ${name}#${claimId}`);
return this return this.findAll({
.findAll({ where: { name, claimId },
where : { name, claimId }, attributes: ['outpoint'],
attributes: ['outpoint'], })
})
.then(result => { .then(result => {
logger.debug('outpoint result'); logger.debug('outpoint result');
switch (result.length) { switch (result.length) {
@ -421,10 +426,9 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
}); });
}; };
Claim.getCurrentHeight = function () { Claim.getCurrentHeight = function() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
return this return this.max('height')
.max('height')
.then(result => { .then(result => {
if (result) { if (result) {
return resolve(result); return resolve(result);

View file

@ -12,10 +12,8 @@ import * as httpContext from 'express-http-context';
import Reducers from '@reducers'; import Reducers from '@reducers';
import GAListener from '@components/GAListener'; import GAListener from '@components/GAListener';
import App from '@app'; import App from '@app';
import Sagas from '@sagas';
import Actions from '@actions';
const createCanonicalLink = require('../../utils/createCanonicalLink'); const createCanonicalLink = require('@globalutils/createCanonicalLink');
const getCanonicalUrlFromShow = show => { const getCanonicalUrlFromShow = show => {
const requestId = show.requestList[show.request.id]; const requestId = show.requestList[show.request.id];
@ -49,19 +47,18 @@ export default (req, res) => {
action = false, action = false,
saga = false, saga = false,
} = httpContext.get('routeData'); } = httpContext.get('routeData');
if (action === 'fallback') { if (action === 'fallback') {
res.status(404); res.status(404);
} }
const runSaga = (action !== false && saga !== false); const runSaga = (action !== false && saga !== false);
const renderPage = (store) => { const renderPage = (store) => {
// Workaround, remove when a solution for async httpContext exists // Workaround, remove when a solution for async httpContext exists
const showState = store.getState().show; const showState = store.getState().show;
const assetKeys = Object.keys(showState.assetList); const assetKeys = Object.keys(showState.assetList);
if(assetKeys.length !== 0) { if (assetKeys.length !== 0) {
res.claimId = showState.assetList[assetKeys[0]].claimId; res.claimId = showState.assetList[assetKeys[0]].claimId;
} else { } else {
const channelKeys = Object.keys(showState.channelList); const channelKeys = Object.keys(showState.channelList);
@ -118,17 +115,17 @@ export default (req, res) => {
.then(() => { .then(() => {
// redirect if request does not use canonical url // redirect if request does not use canonical url
const canonicalUrl = getCanonicalUrlFromShow(store.getState().show); const canonicalUrl = getCanonicalUrlFromShow(store.getState().show);
if (!canonicalUrl) { if (!canonicalUrl) {
res.status(404); res.status(404);
} }
if (canonicalUrl && canonicalUrl !== req.originalUrl) { if (canonicalUrl && canonicalUrl !== req.originalUrl) {
console.log(`redirecting ${req.originalUrl} to ${canonicalUrl}`); console.log(`redirecting ${req.originalUrl} to ${canonicalUrl}`);
res.redirect(canonicalUrl); res.redirect(canonicalUrl);
} }
return renderPage(store) return renderPage(store);
}); });
} else { } else {
const store = createStore(Reducers); const store = createStore(Reducers);

View file

@ -18,7 +18,7 @@ async function getMediaDimensions (fileType, filePath) {
[ height, width ] = await getVideoHeightAndWidth(filePath); [ height, width ] = await getVideoHeightAndWidth(filePath);
break; break;
default: default:
logger.error('unable to create File data for unspported file type:', fileType); logger.error('unable to create File dimension data for unspported file type:', fileType);
break; break;
} }
return { return {