import ace from 'brace' import 'brace/mode/json' import 'brace/theme/github' import 'jsoneditor-react/es/editor.min.css' import ArraySearch from 'arraysearch' import CID from 'cids' import DateTime from 'date-and-time' import Debug from 'debug' import DOMPurify from 'dompurify' import PromiseIpc from 'electron-promise-ipc' import * as IPFSHTTPClient from 'ipfs-http-client' import { JsonEditor as Editor } from 'jsoneditor-react' import React, { useEffect, useMemo, useRef, useState } from 'react' import { Col, Container, Dropdown, Row, Tab, Tabs } from 'react-bootstrap' import { BsInfoSquare } from 'react-icons/bs' import { FaCogs, FaDownload, FaSitemap } from 'react-icons/fa' import { LoopCircleLoading } from 'react-loadingg' import ReactMarkdown from 'react-markdown' import { NotificationManager } from 'react-notifications' import Popup from 'react-popup' import RefLink from '../../main/RefLink' import EmptyProfile from '../assets/img/EmptyProfile.png' import { Player } from '../components/video/Player' import { IPFS_HOST } from '../../common/constants' import { AccountService } from '../services/account.service' import { VideoService } from '../services/video.service' import { knex } from '../singletons/knex.singleton' import { URL } from 'url' import { CollapsibleText } from '../components/CollapsibleText' import { FollowWidget } from '../components/widgets/FollowWidget' import { VoteWidget } from '../components/video/VoteWidget' import { CommentSection } from '../components/video/CommentSection' import { VideoTeaser } from '../components/video/VideoTeaser' const debug = Debug('3speak:watch') const Finder = ArraySearch.Finder let ipfsClient try { ipfsClient = IPFSHTTPClient.create({ host: IPFS_HOST }) } catch (error) { console.error(`Error creating IPFS cliuent in watch.tsx: `, error) throw error } function DHTProviders(props) { const [peers, setPeers] = useState(0) useEffect(() => { void load() async function load() { if (!props.rootCid) { return } let out = 0 for await (const pov of ipfsClient.dht.findProvs(props.rootCid)) { out = out + 1 setPeers(out) } setPeers(out) } }, []) return ( <div> <FaSitemap /> DHT Providers <strong>{peers}</strong> </div> ) } const CustomToggle = React.forwardRef<any, any>(({ children, onClick }, ref) => ( <button style={{ float: 'right' }} ref={ref} onClick={(e) => { e.preventDefault() onClick(e) }} className="btn btn-sm dropdown-toggle btn-secondary" id="videoOptions" type="button" data-toggle="dropdown" > <FaCogs /> </button> )) export function WatchView(props: any) { const player = useRef<any>() const [videoInfo, setVideoInfo] = useState<any>({}) const [postInfo, setPostInfo] = useState<any>({}) const [profilePictureURL, setProfilePictureUrl] = useState(EmptyProfile) const [commentGraph, setCommentGraph] = useState() const [videoLink, setVideoLink] = useState('') const [recommendedVideos, setRecommendedVideos] = useState([]) const [loaded, setLoaded] = useState(false) const [loadingMessage, setLoadingMessage] = useState('') const [rootCid, setRootCid] = useState() const reflink = useMemo(() => { return props.match.params.reflink }, []) const reflinkParsed = useMemo(() => { return RefLink.parse(reflink) as any }, [reflink]) const generalFetch = async () => { const info = await AccountService.permalinkToVideoInfo(reflink, { type: 'video' }) setVideoInfo(info) setPostInfo(await AccountService.permalinkToPostInfo(reflink)) try { //Leave profileURL default if error is thrown when attempting to retrieve profile picture setProfilePictureUrl(await AccountService.getProfilePictureURL(reflink)) } catch (ex) { console.error(ex) throw ex } document.title = `3Speak - ${info.title}` const cids = [] for (const source of info.sources) { const url = new URL(source.url) try { new CID(url.host) cids.push(url.host) } catch {} } setRootCid(cids[0]) } const mountPlayer = async () => { try { const playerType = 'standard' switch (playerType) { case 'standard': { setVideoLink(await VideoService.getVideoSourceURL(reflink)) } } recordView() } catch (ex) { console.error(ex) } } const recordView = async () => { return /*let cids = []; for(const source of videoInfo.sources) { const url = new (require('url').URL)(source.url) try { new CID(url.host) cids.push(url.host) } catch { } } console.log(`CIDs to cache ${JSON.stringify(cids)}`) if(cids.length !== 0) { await PromiseIpc.send("pins.add", { _id: reflink, source: "Watch Page", cids, expire: (new Date().getTime()) + convert("1").from("d").to("ms"), meta: { title: videoInfo.title } }) }*/ } const gearSelect = async (eventKey) => { switch (eventKey) { case 'mute_post': { await PromiseIpc.send('blocklist.add', reflinkParsed.toString()) break } case 'mute_user': { await PromiseIpc.send( 'blocklist.add', `${reflinkParsed.source.value}:${reflinkParsed.root}` as any, ) break } } } const retrieveRecommended = async () => { const query = knex.raw( `SELECT TOP 25 x.* FROM DBHive.dbo.Comments x WHERE CONTAINS(json_metadata , '3speak/video') AND category LIKE '${postInfo.category}' ORDER BY NEWID()`, ) const blob = [] query.stream().on('data', async (val) => { if (await PromiseIpc.send('blocklist.has', `hive:${val.author}:${val.permlink}` as any)) { console.log(`${val.author} is blocked`) return } val.json_metadata = JSON.parse(val.json_metadata) //console.log(val) if (!val.json_metadata.video) { val.json_metadata.video = { info: {}, } } let thumbnail if (val.json_metadata.sourceMap) { thumbnail = Finder.one.in(val.json_metadata.sourceMap).with({ type: 'thumbnail' }).url console.log(thumbnail) } blob.push({ reflink: `hive:${val.author}:${val.permlink}`, created: val.created, author: val.author, permlink: val.permlink, tags: val.json_metadata.tags, title: val.title, duration: val.json_metadata.video.info.duration || val.json_metadata.video.duration, isIpfs: val.json_metadata.video.info.ipfs || thumbnail ? true : false, ipfs: val.json_metadata.video.info.ipfs, images: { ipfs_thumbnail: thumbnail ? `/ipfs/${thumbnail.slice(7)}` : `/ipfs/${val.json_metadata.video.info.ipfsThumbnail}`, thumbnail: `https://threespeakvideo.b-cdn.net/${val.permlink}/thumbnails/default.png`, poster: `https://threespeakvideo.b-cdn.net/${val.permlink}/poster.png`, post: `https://threespeakvideo.b-cdn.net/${val.permlink}/post.png`, }, views: val.total_vote_weight ? Math.log(val.total_vote_weight / 1000).toFixed(2) : 0, }) setRecommendedVideos(blob) }) query.on('query-response', (ret, det, aet) => { console.log(ret, det, aet) }) query.on('end', (err) => { console.log(err) }) /* let ref = RefLink.parse(reflink) let data = (await axios.get(`https://3speak.tv/apiv2/recommended?v=${ref.root}/${ref.permlink}`)).data data.forEach((value => { let link = value.link.split("=")[1].split("/") value.reflink = `hive:${link[0]}:${link[1]}` }))*/ } const PinLocally = async () => { const cids = [] for (const source of videoInfo.sources) { const url = new URL(source.url) try { new CID(url.host) cids.push(url.host) } catch {} } debug(`CIDs to store ${JSON.stringify(cids)}`) if (cids.length !== 0) { NotificationManager.info('Pinning in progress') await PromiseIpc.send('pins.add', { _id: reflink, source: 'Watch Page', cids, expire: null, meta: { title: videoInfo.title, }, } as any) NotificationManager.success( `Video with reflink of ${reflink} has been successfully pinned! Thank you for contributing!`, 'Pin Successful', ) } else { NotificationManager.warning('This video is not available on IPFS') } } const showDebug = () => { const metadata = videoInfo Popup.registerPlugin('watch_debug', async function () { this.create({ content: ( <div> <Tabs defaultActiveKey="meta" id="uncontrolled-tab-example"> <Tab eventKey="meta" title="Metadata"> <Editor value={metadata} ace={ace} theme="ace/theme/github"></Editor> </Tab> </Tabs> </div> ), buttons: { right: [ { text: 'Close', className: 'success', action: function () { Popup.close() }, }, ], }, }) }) Popup.plugins().watch_debug() } useEffect(() => { const load = async () => { try { await generalFetch() setLoadingMessage('Loading: Mounting player...') await mountPlayer() } catch (ex) { setLoadingMessage('Loading resulted in error') throw ex } setLoaded(true) await retrieveRecommended() } void load() }, []) useEffect(() => { window.scrollTo(0, 0) const update = async () => { await generalFetch() await mountPlayer() await retrieveRecommended() player.current?.ExecUpdate() } void update() }, [reflink]) return ( <div> {loaded ? ( <Container fluid> {/* <Container fluid pb={0}> */} {/* <Row fluid="md"> */} <Row> <Col md={8}> <div> <Player reflink={reflink}></Player> </div> <div className="single-video-title box mb-3 clearfix"> <div className="float-left"> <h2 style={{ fontSize: '18px' }}> <a>{videoInfo.title}</a> </h2> <DHTProviders rootCid={rootCid} /> </div> <div className="float-right" style={ { textAlign: 'right !important', float: 'right !important', display: 'inline-block !important', } as any } > <span> <VoteWidget reflink={reflink} /> </span> <Dropdown onSelect={gearSelect} style={{ paddingTop: '10px' }}> <Dropdown.Toggle as={CustomToggle} id="dropdown-custom-components" ></Dropdown.Toggle> <Dropdown.Menu> <Dropdown.Item eventKey="mute_post"> <p style={{ color: 'red' }}>Mute Post</p> </Dropdown.Item> <Dropdown.Item eventKey="mute_user"> <p style={{ color: 'red' }}>Mute User</p> </Dropdown.Item> </Dropdown.Menu> </Dropdown> </div> </div> <div className="single-video-author box mb-3"> <div className="float-right"> <Row> <FollowWidget reflink={reflink} /> <a target="_blank" style={{ marginRight: '5px', marginLeft: '5px' }} className="btn btn-light btn-sm" onClick={PinLocally} > <FaDownload /> Download to IPFS node </a> <a target="_blank" style={{ marginRight: '5px' }} className="btn btn-light btn-sm" href={(() => { const videoSource = Finder.one.in(videoInfo.sources).with({ format: 'mp4', }) if (videoSource) { return videoSource.url } })()} > <FaDownload /> Download </a> </Row> </div> <img className="img-fluid" src={profilePictureURL} alt="" /> <p> <a href={`#/user/${reflinkParsed.source.value}:${reflinkParsed.root}`}> <strong>{postInfo.author}</strong> </a> </p> <small> Published on{' '} {(() => { const pattern = DateTime.compile('MMMM D, YYYY') return DateTime.format(new Date(videoInfo.creation), pattern) })()} </small> </div> <div className="single-video-info-content box mb-3"> <h6>About :</h6> <CollapsibleText> <ReactMarkdown escapeHtml={false} source={DOMPurify.sanitize(videoInfo.description)} ></ReactMarkdown> <hr /> <Container style={{ marginBottom: '10px', textAlign: 'center' }}> <a target="_blank" style={{ marginRight: '5px' }} className="btn btn-light btn-sm" onClick={() => showDebug()} > <BsInfoSquare /> Debug Info </a> </Container> </CollapsibleText> <h6>Tags: </h6> <p className="tags mb-0"> {(() => { const out = [] if (videoInfo.tags) { for (const tag of videoInfo.tags) { out.push( <span style={{ paddingLeft: '3px' }} key={tag}> <a>{tag}</a> </span>, ) } } return out })()} </p> </div> <CommentSection reflink={reflink.toString()} /> </Col> <Col md={4}> <Row> <Col md={12}> {recommendedVideos.map((value) => ( <VideoTeaser key={value.reflink} reflink={value.reflink} /> ))} </Col> </Row> </Col> </Row> </Container> ) : ( <div> <LoopCircleLoading /> <div style={{ textAlign: 'center', margin: 'auto', position: 'absolute', left: '0px', right: '0px', top: '60%', bottom: '0px', }} > <h1 style={{ top: '60%', fontSize: '20px' }}>{loadingMessage}</h1> </div> </div> )} </div> ) }