From cd4409bee6fd488607a9dfa660940751cb909cc7 Mon Sep 17 00:00:00 2001 From: nathansenn Date: Wed, 17 May 2023 03:54:45 +0800 Subject: [PATCH] feat/poa-mvp (#40) * Adding PoA to 3SpeakApp * chore: pinning debug, fix CumulativeSize * tweak: add date to * partial: pinning progress * PoA access fixes Refactoring and optimizing * Fix for pin page issue * Update proofofaccess releases path * partial: dag import ipfs * partial: wire up pinning progress indicator * Keep PoA running when changing pages --------- Co-authored-by: vaultec <47548474+vaultec81@users.noreply.github.com> --- package.json | 8 +- src/components/CustomToggle.tsx | 18 + src/components/DHTProviders.tsx | 38 ++ src/main/AutoUpdaterPoA.ts | 106 ++++ src/main/core/components/DistillerDB.ts | 1 - src/main/core/components/Pins.ts | 126 +++- src/main/core/components/ProgramRunner.ts | 65 ++ src/renderer/App.tsx | 19 +- src/renderer/components/SideNavbar.tsx | 3 + src/renderer/components/hooks/Feeds.ts | 2 + src/renderer/components/video/Player.tsx | 9 +- src/renderer/services/account.service.ts | 588 ++---------------- .../services/accountServices/convertLight.ts | 10 + .../services/accountServices/createPost.ts | 30 + .../services/accountServices/followHandler.ts | 46 ++ .../services/accountServices/getAccount.ts | 6 + .../accountServices/getAccountBalances.ts | 25 + .../accountServices/getAccountMetadata.ts | 14 + .../services/accountServices/getAccounts.ts | 7 + .../accountServices/getFollowerCount.ts | 20 + .../services/accountServices/getFollowing.ts | 32 + .../accountServices/getProfileAbout.ts | 31 + .../getProfileBackgroundImageUrl.ts | 33 + .../accountServices/getProfilePictureURL.ts | 22 + .../services/accountServices/login.ts | 50 ++ .../services/accountServices/logout.ts | 10 + .../accountServices/permalinkToPostInfo.ts | 13 + .../accountServices/permalinkToVideoInfo.ts | 122 ++++ .../services/accountServices/postComment.ts | 93 +++ .../accountServices/postCustomJson.ts | 31 + .../services/accountServices/updateMeta.ts | 31 + .../services/accountServices/voteHandler.ts | 27 + src/renderer/services/peer.service.ts | 107 ++++ src/renderer/services/video.service.ts | 7 +- .../singletons/hive-client.singleton.ts | 2 - .../views/LoginView/loginViewContent.tsx | 173 ++++++ src/renderer/views/PinsView.tsx | 442 ++----------- src/renderer/views/PinsView/PinCids.tsx | 56 ++ src/renderer/views/PinsView/PinRows.tsx | 50 ++ .../views/PinsView/PinsViewComponent.tsx | 159 +++++ src/renderer/views/PinsView/pinningUtils.tsx | 208 +++++++ src/renderer/views/PoAView.tsx | 53 ++ .../views/PoAView/PoAProgramRunnerContext.tsx | 36 ++ .../views/PoAView/PoAStateContext.tsx | 32 + src/renderer/views/PoAView/PoAViewContent.tsx | 123 ++++ src/renderer/views/PoAView/useEnablePoA.ts | 74 +++ src/renderer/views/PoAView/usePoAInstaller.ts | 40 ++ .../views/PoAView/usePoAProgramRunner.ts | 58 ++ src/renderer/views/UploaderView.tsx | 586 ++--------------- .../views/UploaderView/calculatePercentage.ts | 3 + .../views/UploaderView/compileVideoCid.ts | 13 + .../views/UploaderView/normalizeSize.ts | 7 + src/renderer/views/UploaderView/publish.ts | 75 +++ .../views/UploaderView/startEncode.ts | 93 +++ .../views/UploaderView/thumbnailSelect.ts | 34 + .../UploaderView/uploaderViewContent.tsx | 322 ++++++++++ .../views/UploaderView/videoSelect.ts | 12 + src/renderer/views/UserView.tsx | 199 +----- .../views/UserView/UserViewContent.tsx | 97 +++ src/renderer/views/UserView/userQueries.ts | 59 ++ src/renderer/views/UserView/userUtils.ts | 33 + src/renderer/views/WatchView.tsx | 532 +++------------- .../views/WatchView/WatchViewContent.tsx | 195 ++++++ .../WatchView/watchViewHelpers/PinLocally.ts | 34 + .../WatchView/watchViewHelpers/gearSelect.ts | 17 + .../watchViewHelpers/generalFetch.ts | 32 + .../WatchView/watchViewHelpers/mountPlayer.ts | 15 + .../WatchView/watchViewHelpers/recordView.ts | 3 + .../watchViewHelpers/retrieveRecommended.ts | 71 +++ .../WatchView/watchViewHelpers/showDebug.tsx | 34 + 70 files changed, 3586 insertions(+), 2136 deletions(-) create mode 100644 src/components/CustomToggle.tsx create mode 100644 src/components/DHTProviders.tsx create mode 100644 src/main/AutoUpdaterPoA.ts create mode 100644 src/main/core/components/ProgramRunner.ts create mode 100644 src/renderer/services/accountServices/convertLight.ts create mode 100644 src/renderer/services/accountServices/createPost.ts create mode 100644 src/renderer/services/accountServices/followHandler.ts create mode 100644 src/renderer/services/accountServices/getAccount.ts create mode 100644 src/renderer/services/accountServices/getAccountBalances.ts create mode 100644 src/renderer/services/accountServices/getAccountMetadata.ts create mode 100644 src/renderer/services/accountServices/getAccounts.ts create mode 100644 src/renderer/services/accountServices/getFollowerCount.ts create mode 100644 src/renderer/services/accountServices/getFollowing.ts create mode 100644 src/renderer/services/accountServices/getProfileAbout.ts create mode 100644 src/renderer/services/accountServices/getProfileBackgroundImageUrl.ts create mode 100644 src/renderer/services/accountServices/getProfilePictureURL.ts create mode 100644 src/renderer/services/accountServices/login.ts create mode 100644 src/renderer/services/accountServices/logout.ts create mode 100644 src/renderer/services/accountServices/permalinkToPostInfo.ts create mode 100644 src/renderer/services/accountServices/permalinkToVideoInfo.ts create mode 100644 src/renderer/services/accountServices/postComment.ts create mode 100644 src/renderer/services/accountServices/postCustomJson.ts create mode 100644 src/renderer/services/accountServices/updateMeta.ts create mode 100644 src/renderer/services/accountServices/voteHandler.ts create mode 100644 src/renderer/services/peer.service.ts create mode 100644 src/renderer/views/LoginView/loginViewContent.tsx create mode 100644 src/renderer/views/PinsView/PinCids.tsx create mode 100644 src/renderer/views/PinsView/PinRows.tsx create mode 100644 src/renderer/views/PinsView/PinsViewComponent.tsx create mode 100644 src/renderer/views/PinsView/pinningUtils.tsx create mode 100644 src/renderer/views/PoAView.tsx create mode 100644 src/renderer/views/PoAView/PoAProgramRunnerContext.tsx create mode 100644 src/renderer/views/PoAView/PoAStateContext.tsx create mode 100644 src/renderer/views/PoAView/PoAViewContent.tsx create mode 100644 src/renderer/views/PoAView/useEnablePoA.ts create mode 100644 src/renderer/views/PoAView/usePoAInstaller.ts create mode 100644 src/renderer/views/PoAView/usePoAProgramRunner.ts create mode 100644 src/renderer/views/UploaderView/calculatePercentage.ts create mode 100644 src/renderer/views/UploaderView/compileVideoCid.ts create mode 100644 src/renderer/views/UploaderView/normalizeSize.ts create mode 100644 src/renderer/views/UploaderView/publish.ts create mode 100644 src/renderer/views/UploaderView/startEncode.ts create mode 100644 src/renderer/views/UploaderView/thumbnailSelect.ts create mode 100644 src/renderer/views/UploaderView/uploaderViewContent.tsx create mode 100644 src/renderer/views/UploaderView/videoSelect.ts create mode 100644 src/renderer/views/UserView/UserViewContent.tsx create mode 100644 src/renderer/views/UserView/userQueries.ts create mode 100644 src/renderer/views/UserView/userUtils.ts create mode 100644 src/renderer/views/WatchView/WatchViewContent.tsx create mode 100644 src/renderer/views/WatchView/watchViewHelpers/PinLocally.ts create mode 100644 src/renderer/views/WatchView/watchViewHelpers/gearSelect.ts create mode 100644 src/renderer/views/WatchView/watchViewHelpers/generalFetch.ts create mode 100644 src/renderer/views/WatchView/watchViewHelpers/mountPlayer.ts create mode 100644 src/renderer/views/WatchView/watchViewHelpers/recordView.ts create mode 100644 src/renderer/views/WatchView/watchViewHelpers/retrieveRecommended.ts create mode 100644 src/renderer/views/WatchView/watchViewHelpers/showDebug.tsx diff --git a/package.json b/package.json index 519c6d2..881b6ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "3speak-app", "version": "0.1.20", + "PoA": "0", "description": "3Speak decentralized desktop app", "main": "./dist/main.prod.js", "scripts": { @@ -48,7 +49,6 @@ "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", "file-loader": "^3.0.1", - "go-ipfs": "^0.10.0", "html-webpack-plugin": "^3.2.0", "less": "^3.13.1", "less-loader": "^4.1.0", @@ -93,9 +93,11 @@ "date-and-time": "^0.14.2", "dlv": "^1.1.3", "dompurify": "^2.2.6", + "electron-better-ipc": "^2.0.1", "electron-promise-ipc": "^2.2.4", "execa": "^5.0.0", "fluent-ffmpeg": "^2.1.2", + "go-ipfs": "^0.16.0", "graphql": "^16.5.0", "i18next": "^19.8.4", "ipfs-core": "^0.12.0", @@ -132,7 +134,9 @@ "tedious": "^14.0.0", "wa-go-ipfs": "git+https://github.com/vaultec81/wa-go-ipfs.git", "winston": "^3.3.3", - "winston-daily-rotate-file": "^4.5.5" + "winston-daily-rotate-file": "^4.5.5", + "xterm": "^5.1.0", + "xterm-addon-web-links": "^0.8.0" }, "build": { "productName": "3Speak-app", diff --git a/src/components/CustomToggle.tsx b/src/components/CustomToggle.tsx new file mode 100644 index 0000000..d1cec33 --- /dev/null +++ b/src/components/CustomToggle.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { FaCogs } from 'react-icons/fa'; +export const CustomToggle = React.forwardRef(({ children, onClick }, ref) => ( + +)); diff --git a/src/components/DHTProviders.tsx b/src/components/DHTProviders.tsx new file mode 100644 index 0000000..5f07309 --- /dev/null +++ b/src/components/DHTProviders.tsx @@ -0,0 +1,38 @@ +import React, { useState, useEffect } from 'react'; +import { FaSitemap } from 'react-icons/fa'; +import * as IPFSHTTPClient from 'ipfs-http-client'; +import { IPFS_HOST } from '../common/constants'; + +let ipfsClient; +try { + ipfsClient = IPFSHTTPClient.create({ host: IPFS_HOST }); +} catch (error) { + console.error(`Error creating IPFS client in watch.tsx: `, error); + throw error; +} + +export 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 ( +
+ DHT Providers {peers} +
+ ); +} diff --git a/src/main/AutoUpdaterPoA.ts b/src/main/AutoUpdaterPoA.ts new file mode 100644 index 0000000..4ca166d --- /dev/null +++ b/src/main/AutoUpdaterPoA.ts @@ -0,0 +1,106 @@ +import os from 'os'; +import Path from 'path'; +import fs from 'fs'; +import axios from 'axios'; +import compareVersions from 'compare-versions'; +import { EventEmitter } from 'events'; + +const isWin = process.platform === 'win32'; + +class PoAInstaller extends EventEmitter { + async main() { + try { + await this.install(); + } catch (error) { + console.error(error); + this.emit('error', error); + process.exit(1); + } + } + async getCurrentVersion(installDir) { + const versionFilePath = Path.join(installDir, 'version.txt'); + try { + const currentVersion = await fs.promises.readFile(versionFilePath, 'utf-8'); + return currentVersion.trim(); + } catch (error) { + // If the file doesn't exist or there is an error reading it, return a default version + return '0.0.0'; + } + } + async getDefaultPath() { + let defaultPath; + switch (process.platform) { + case 'win32': + defaultPath = Path.join('AppData/Roaming/Microsoft/Windows/Start Menu/Programs/PoA'); + return defaultPath; + case 'darwin': + defaultPath = Path.join(os.homedir(), 'Applications/PoA/poa'); + break; + case 'linux': + defaultPath = Path.join(os.homedir(), 'bin/PoA/poa'); + break; + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } + + // Check if the default path exists + try { + await fs.promises.access(defaultPath, fs.constants.F_OK); + return defaultPath; + } catch (error) { + // Default path does not exist, return null + return null; + } + } + async install() { + const installDir = Path.join(os.homedir(), (await this.getDefaultPath()) || ''); + console.log(`Installing PoA to ${installDir}`); + if (!fs.existsSync(installDir)) { + fs.mkdirSync(installDir, { recursive: true }); + } + + console.log('Installing PoA...'); + + const { data } = await axios.get('https://api.github.com/repos/spknetwork/proofofaccess/releases/latest'); + const { tag_name, assets } = data; + + console.log(tag_name); + const currentVersion = await this.getCurrentVersion(installDir); + if (compareVersions.compare(tag_name, currentVersion, '>')) { + console.log('Update available'); + this.emit('update-available', tag_name); + const asset = assets.find((a) => a.name.includes('win-main') && a.name.includes('exe') && isWin); + + if (!asset) { + console.error('Could not find PoA asset for this platform'); + return; + } + + console.log(`Downloading PoA for ${process.platform}...`); + + const response = await axios({ + method: 'get', + url: asset.browser_download_url, + responseType: 'arraybuffer', + }); + + const installPath = Path.join(installDir, 'PoA.exe'); + + fs.writeFileSync(installPath, Buffer.from(response.data)); + + console.log(`PoA installed at: ${installPath}`); + this.emit('installed', installPath); + + // Update version.txt file + const versionFilePath = Path.join(installDir, 'version.txt'); + fs.writeFileSync(versionFilePath, tag_name); + console.log(`Version ${tag_name} saved to ${versionFilePath}`); + this.emit('version-updated', tag_name); + } else { + console.log('PoA is already up-to-date'); + this.emit('up-to-date'); + } + } +} + +export default PoAInstaller; \ No newline at end of file diff --git a/src/main/core/components/DistillerDB.ts b/src/main/core/components/DistillerDB.ts index e79cd9f..df8bbdc 100644 --- a/src/main/core/components/DistillerDB.ts +++ b/src/main/core/components/DistillerDB.ts @@ -10,7 +10,6 @@ PouchDB.plugin(require('pouchdb-find')) PouchDB.plugin(require('pouchdb-upsert')) const hiveClient = new HiveClient([ - 'https://deathwing.me', 'https://api.openhive.network', 'https://hived.privex.io', 'https://anyx.io', diff --git a/src/main/core/components/Pins.ts b/src/main/core/components/Pins.ts index 2cb3887..6430155 100644 --- a/src/main/core/components/Pins.ts +++ b/src/main/core/components/Pins.ts @@ -1,12 +1,99 @@ +import { CID, globSource, IPFSHTTPClient } from 'ipfs-http-client' import PouchDB from 'pouchdb' +import Axios from 'axios' +import fs from 'fs' +import fsPromises from 'fs/promises' import { CoreService } from '..' import RefLink from '../../RefLink' import { IpfsHandler } from './ipfsHandler' +import tmp from 'tmp' +import execa from 'execa' +import waIpfs from 'wa-go-ipfs' const Path = require('path') const debug = require('debug')('3speak:pins') const Schedule = require('node-schedule') PouchDB.plugin(require('pouchdb-find')) PouchDB.plugin(require('pouchdb-upsert')) + +async function progressPin(ipfs: IPFSHTTPClient, pin: string, callback: Function) { + + // ipfs.dag.exp + + const tmpPath = tmp.dirSync(); + console.log(tmpPath) + const writer = fs.createWriteStream(Path.join(tmpPath.name, 'download.car')); + + const {data} = await Axios.get(`https://ipfs-3speak.b-cdn.net/api/v0/object/stat?arg=${pin}`) + const CumulativeSize = data.CumulativeSize + + let totalSizeSoFar = 0; + await Axios.get(`https://ipfs-3speak.b-cdn.net/api/v0/dag/export?arg=${pin}`, { + responseType: 'stream', + }).then(response => { + + + response.data.on('data', (chunk) => { + totalSizeSoFar = totalSizeSoFar + chunk.length + const pct = Math.round((totalSizeSoFar / CumulativeSize) * 100) + callback(pct) + // console.log(`${pct}%`) + }) + return new Promise((resolve, reject) => { + response.data.pipe(writer); + let error = null; + writer.on('error', err => { + error = err; + writer.close(); + reject(err); + }); + writer.on('close', () => { + if (!error) { + resolve(true); + } + }); + }); + }) + + // console.log('got here') + // for await(let importBlock of ipfs.dag.import(fs.createReadStream(Path.join(tmpPath.name, 'download.car')))) { + // console.log(importBlock) + // } + + const ipfsInfo = await IpfsHandler.getIpfs() + const ipfsPath = await waIpfs.getPath( + waIpfs.getDefaultPath({ dev: process.env.NODE_ENV === 'development' }), + ) + + + const output = await execa(ipfsPath, [ + 'dag', + 'import', + Path.join(tmpPath.name, 'download.car') + ], { + env: { + IPFS_PATH: ipfsInfo.ipfsPath + } + }) + console.log(output) + + await fsPromises.rmdir(tmpPath.name, { + recursive: true, + force: true + } as any) + + // const refs = ipfs.refs(pin, { + // recursive: true + // // maxDepth: 1 + // }) + // const TotalSize = (await ipfs.object.stat(CID.parse(pin))).CumulativeSize + // let counter = 0 + // for await(let result of refs) { + // // const CumulativeSize = (await ipfs.object.stat(CID.parse(result.ref))).CumulativeSize + // const dag = await ipfs.dag.get(CID.parse(result.ref)) + // counter = counter + dag.value.Data.length; + // callback(Number((counter / TotalSize).toFixed(3))) + // } +} class Pins { self: CoreService db: any @@ -43,7 +130,7 @@ class Pins { try { await ipfs.swarm.connect(bt) } catch (ex) { - console.error(ex) + // console.error(ex) } }) doc.cids = doc.cids.filter(function (item, pos, self) { @@ -51,12 +138,47 @@ class Pins { }) doc.size = 0 this.inProgressPins[doc._id] = doc + + let totalSize = 0 + let totalPercent; + + console.log(doc) + + const progressTick = setInterval(async() => { + + try { + const bDoc = await this.db.get("hive:cowboyzlegend27:qauvdrmx") + console.log(bDoc) + } catch { + + } + try { + const cDoc = await this.db.get(doc._id) + console.log(cDoc) + } catch { + + } + await this.db.upsert(doc._id, (oldDoc) => { + console.log('change status', oldDoc, totalPercent) + oldDoc.percent = totalPercent; + doc.percent = totalPercent + return oldDoc + }) + }, 1000) + for (const cid of doc.cids) { + console.log('Pinning CID', cid, new Date(), doc._id) + await progressPin(this.self.ipfs, cid, (percent) => { + totalPercent = percent + }) + console.log('Done pinning, attempting full pin') await this.self.ipfs.pin.add(cid) + console.log('Getting Cumulative size of', cid, new Date(), doc._id) const objectInfo = await this.self.ipfs.object.stat(cid) - totalSize = +objectInfo.CumulativeSize + totalSize = totalSize + objectInfo.CumulativeSize } + clearInterval(progressTick) doc.size = totalSize //Prevet old and new docs from stepping on eachother. await this.db.upsert(doc._id, (oldDoc) => { diff --git a/src/main/core/components/ProgramRunner.ts b/src/main/core/components/ProgramRunner.ts new file mode 100644 index 0000000..82891de --- /dev/null +++ b/src/main/core/components/ProgramRunner.ts @@ -0,0 +1,65 @@ +// ProgramRunner.ts +import { exec, ChildProcess } from 'child_process'; +import treeKill from 'tree-kill'; + +class ProgramRunner { + private command: string; + private process: ChildProcess | null = null; + private outputHandler: (data: string) => void; + + constructor(command: string, outputHandler: (data: string) => void) { + this.command = command; + this.outputHandler = outputHandler; + } + + public setupProgram(onExit: () => void): void { + console.log(`Setting up command: ${this.command}`); + + this.process = exec(this.command, (error, stdout, stderr) => { + if (error && this.process && !this.process.killed) { + console.error(`Error running program: ${error.message}`); + console.error('Error details:', error); + return; + } + + console.log(`stdout: ${stdout}`); + console.error(`stderr: ${stderr}`); + }); + + this.process.stdout.on('data', (data) => { + this.outputHandler(data); + }); + + this.process.stderr.on('data', (data) => { + this.outputHandler(data); + }); + + this.process.on('exit', () => { + onExit(); + this.process = null; + }); + } + + public isRunning(): boolean { + return this.process && !this.process.killed && this.process.exitCode === null; + } + public stopProgram(): void { + if (this.process) { + try { + treeKill(this.process.pid, 'SIGINT'); + } catch (error) { + if (error.code !== 'ESRCH') { + console.error('Error stopping program:', error); + } + } + this.process = null; + } + } + + public cleanup(): void { + this.stopProgram(); + // Remove event listeners here + } +} + +export default ProgramRunner; diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 0705810..6d82c44 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,4 +1,8 @@ -import React from 'react' +// file src\renderer\App.tsx: +import React, { useState } from 'react'; +import { usePoAProgramRunner } from './views/PoAView/usePoAProgramRunner'; +import { PoAProgramRunnerProvider } from './views/PoAView/PoAProgramRunnerContext'; +import { PoAStateProvider } from './views/PoAView/PoAStateContext'; // Import PoAStateProvider import 'bootstrap/dist/css/bootstrap.min.css' import { HashRouter, Switch, Route } from 'react-router-dom' import './css/App.css' @@ -15,6 +19,7 @@ import { WatchView } from './views/WatchView' import { LeaderboardView } from './views/LeaderboardView' import { CommunityView } from './views/CommunityView' import { IpfsConsoleView } from './views/IpfsConsoleView' +import { PoAView } from './views/PoAView' import { GridFeedView } from './views/GridFeedView' import { NotFoundView } from './views/NotFoundView' import { PinsView } from './views/PinsView' @@ -27,13 +32,18 @@ import { CreatorStudioView } from './views/CreatorStudioView' import { useQuery, gql, ApolloClient, InMemoryCache } from '@apollo/client' export const IndexerClient = new ApolloClient({ - uri: 'https://spk-union.us-west.web3telekom.xyz/api/v2/graphql', + uri: 'https://union.us-02.infra.3speak.tv/api/v2/graphql', cache: new InMemoryCache(), }) export function App() { + const terminalRef = React.useRef(); + const { terminal, setTerminal, isPoARunning, runPoA, contextValue } = usePoAProgramRunner(); // Add this line + return ( -
+ + +
+ @@ -93,5 +104,7 @@ export function App() {
+
+
) } diff --git a/src/renderer/components/SideNavbar.tsx b/src/renderer/components/SideNavbar.tsx index 01441c1..5e59da2 100644 --- a/src/renderer/components/SideNavbar.tsx +++ b/src/renderer/components/SideNavbar.tsx @@ -189,6 +189,9 @@ export function SideNavbar(props: any) { Ipfs Console + + Proof of Access + diff --git a/src/renderer/components/hooks/Feeds.ts b/src/renderer/components/hooks/Feeds.ts index ea614c8..0c78a51 100644 --- a/src/renderer/components/hooks/Feeds.ts +++ b/src/renderer/components/hooks/Feeds.ts @@ -1,3 +1,5 @@ +// filename: Feeds.ts + import { gql, useQuery } from '@apollo/client' import { useEffect, useMemo } from 'react' import { IndexerClient } from '../../App' diff --git a/src/renderer/components/video/Player.tsx b/src/renderer/components/video/Player.tsx index 8150e6f..fb60af0 100644 --- a/src/renderer/components/video/Player.tsx +++ b/src/renderer/components/video/Player.tsx @@ -27,7 +27,6 @@ export function Player(props: PlayerProps) { async function load() { let reflink - if (props.reflink) { reflink = props.reflink } else { @@ -46,6 +45,8 @@ export function Player(props: PlayerProps) { } let vidInfo + console.log('videoInfo', props.videoInfo) + console.log('reflink', reflink) if (props.videoInfo) { vidInfo = props.videoInfo } else if (reflink) { @@ -53,8 +54,12 @@ export function Player(props: PlayerProps) { } else { throw new Error('Cannot set video info!') } + console.log('vidInfo', vidInfo) const vidurl = await VideoService.getVideoSourceURL(vidInfo) - setThumbnail(await VideoService.getNewThumbnailURL(vidInfo.author, vidInfo.permlink)) + console.log('vidurl', vidurl) + const thumbnailUrl = await VideoService.getThumbnailURL(vidInfo) + console.log('thumbnailUrl', thumbnailUrl) + setThumbnail(thumbnailUrl) setVideoUrl(vidurl) setVideoInfo(vidInfo) } diff --git a/src/renderer/services/account.service.ts b/src/renderer/services/account.service.ts index f688416..b56710c 100644 --- a/src/renderer/services/account.service.ts +++ b/src/renderer/services/account.service.ts @@ -1,553 +1,41 @@ -import hive from '@hiveio/hive-js' -import axios from 'axios' -import PromiseIPC from 'electron-promise-ipc' -import ArraySearch from 'arraysearch' -import { CommentOp } from '../../common/models/comments.model' -import { HiveInfo } from '../../common/models/hive.model' -import { IpfsService } from './ipfs.service' -import { hiveClient } from '../singletons/hive-client.singleton' -import { VideoInfo, VideoSource } from '../../common/models/video.model' -import RefLink from '../../main/RefLink' - -const Finder = ArraySearch.Finder +import { convertLight } from './accountServices/convertLight' +import { createPost } from './accountServices/createPost' +import { followHandler } from './accountServices/followHandler' +import { getAccount } from './accountServices/getAccount' +import { getAccountBalances } from './accountServices/getAccountBalances' +import { getAccountMetadata } from './accountServices/getAccountMetadata' +import { getAccounts } from './accountServices/getAccounts' +import { getFollowerCount } from './accountServices/getFollowerCount' +import { getFollowing } from './accountServices/getFollowing' +import { getProfileAbout } from './accountServices/getProfileAbout' +import { getProfileBackgroundImageUrl } from './accountServices/getProfileBackgroundImageUrl' +import { getProfilePictureURL } from './accountServices/getProfilePictureURL' +import { login } from './accountServices/login' +import { logout } from './accountServices/logout' +import { permalinkToPostInfo } from './accountServices/permalinkToPostInfo' +import { permalinkToVideoInfo } from './accountServices/permalinkToVideoInfo' +import { postComment } from './accountServices/postComment' +import { updateMeta } from './accountServices/updateMeta' +import { voteHandler } from './accountServices/voteHandler' export class AccountService { - static async login(data) { - switch (data.accountType) { - case 'hive': { - const userAccounts = await hive.api.getAccountsAsync([data.username]) - console.log(`got hive account for username ${data.username}`, userAccounts) - const pubWif = userAccounts[0].posting.key_auths[0][0] - - const Valid = hive.auth.wifIsValid(data.key, pubWif) - - if (Valid) { - const profile = { - _id: userAccounts[0].id.toString(), - nickname: data.profile, - keyring: [ - { - type: 'hive', - username: data.username, - public: { - pubWif, - }, - encrypted: data.encrypted, - privateKeys: { - posting_key: data.key, - }, - }, - ], - } - const profileID = profile._id - const check_profile = await PromiseIPC.send('accounts.has', profileID) - if (check_profile) { - throw new Error('Account exists already') - } else { - // 2nd argument doesn't match function signature - marking with any - await PromiseIPC.send('accounts.createProfile', profile as any) - const get_profile = await PromiseIPC.send('accounts.get', profileID) - localStorage.setItem('SNProfileID', profileID) - return get_profile - } - } else { - throw new Error('Invalid posting key') - } - } - } - } - - static async getAccounts() { - const getAccounts = await PromiseIPC.send('accounts.ls', {} as any) - - return getAccounts - } - - static async getAccount(profileID) { - const getAccount = await PromiseIPC.send('accounts.get', profileID) - return getAccount - } - static async logout(profileID) { - await PromiseIPC.send('accounts.deleteProfile', profileID) - return - } - static async voteHandler(voteOp) { - switch (voteOp.accountType) { - case 'hive': { - const weight = voteOp.weight * 100 - const theWif = voteOp.wif - hive.broadcast.vote( - theWif, - voteOp.voter, - voteOp.author, - voteOp.permlink, - weight, - function (error, succeed) { - if (error) { - console.error(error) - console.error('Error encountered') - } - - if (succeed) { - console.log('vote broadcast successfully') - } - }, - ) - } - } - } - static async followHandler(profileID, followOp) { - switch (followOp.accountType) { - case 'hive': { - //const profile = await acctOps.getAccount(profileID); - const profile = (await this.getAccount(profileID)) as any - - const username = Finder.one.in(profile.keyring).with({ type: 'hive' }).username - const theWifObj = Finder.one.in(profile.keyring).with({ - type: 'hive', - }) - const wif = theWifObj.privateKeys.posting_key // posting key - const jsonObj = [ - 'follow', - { - follower: username, - following: followOp.author, - what: followOp.what ? [followOp.what] : [], - }, - ] - - // console.log( - hive.broadcast.customJson( - wif, - [], - [username], - 'follow', - JSON.stringify(jsonObj), - async (error, succeed) => { - if (error) { - console.error(error) - console.error('Error encountered broadcsting custom json') - } - - if (succeed) { - console.log(succeed) - console.log('success broadcasting custom json') - } - }, - ) - // ) - } - } - } - static async postComment(commentOp: CommentOp) { - switch (commentOp.accountType) { - case 'hive': { - const profileID = window.localStorage.getItem('SNProfileID') as any - const getAccount = (await PromiseIPC.send('accounts.get', profileID)) as any - const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }) as HiveInfo - - console.log(`posting comment with profile ID`, profileID) - console.log(`account`, getAccount) - console.log(`hiveInfo`, hiveInfo) - console.log(`comment op`, commentOp) - - if (!commentOp.json_metadata) { - commentOp.json_metadata = {} - } - - let json_metadata - if (typeof commentOp.json_metadata === 'object') { - //Note: this is for peakd/hive.blog to display a video preview - if (!commentOp.parent_author) { - commentOp.json_metadata.video.info = { - author: hiveInfo.username, - permlink: commentOp.permlink, - } - } - json_metadata = JSON.stringify(commentOp.json_metadata) - } else { - throw new Error('commentOp.json_metadata must be an object') - } - - let body: string - - if (!commentOp.parent_author) { - let header: string - if (commentOp.json_metadata.sourceMap) { - const thumbnailSource = Finder.one.in(commentOp.json_metadata.sourceMap).with({ - type: 'thumbnail', - }) - console.log(`thumbnail source`, thumbnailSource) - try { - const cid = IpfsService.urlToCID(thumbnailSource.url) - const gateway = await IpfsService.getGateway(cid, true) - const imgSrc = gateway + IpfsService.urlToIpfsPath(thumbnailSource.url) - header = `[![](${imgSrc})](https://3speak.tv/watch?v=${hiveInfo.username}/${commentOp.permlink})
` - } catch (ex) { - console.error(`Error getting IPFS info`, ex) - throw ex - } - } - if (header) { - body = `${header} ${commentOp.body}
[▶️Watch on 3Speak Dapp](https://3speak.tv/openDapp?uri=hive:${hiveInfo.username}:${commentOp.permlink})` - } else { - body = `${commentOp.body}
[▶️Watch on 3Speak Dapp](https://3speak.tv/openDapp?uri=hive:${hiveInfo.username}:${commentOp.permlink})` - } - } else { - body = commentOp.body - } - - if (!json_metadata) { - throw new Error(`Cannot publish comment to hive with no metadata!`) - } - console.log(`POSTING TO HIVE WITH JSON METADATA`, json_metadata) - - try { - const out = await hive.broadcast.comment( - hiveInfo.privateKeys.posting_key, - commentOp.parent_author || '', - commentOp.parent_permlink || commentOp.tags[0] || 'threespeak', //parentPermlink - hiveInfo.username, - commentOp.permlink, - commentOp.title, - body, - json_metadata, - ) - - console.log(`comment broadcasted to hive! return:`) - console.log(out) - - return [`hive:${hiveInfo.username}:${commentOp.permlink}`, out] - } catch (err) { - console.error(`Error broadcasting comment to hive! ${err.message}`) - throw err - } - } - } - } - static async createPost(postOp) { - switch (postOp.accountType) { - case 'hive': { - const theWif = postOp.wif - - hive.broadcast.comment( - theWif, - '', - postOp.parentPermlink, - postOp.author, - postOp.permlink, - postOp.title, - postOp.body, - postOp.jsonMetadata, - function (error, succeed) { - console.log('Bout to check') - if (error) { - console.error(error) - console.error('Error encountered') - } - - if (succeed) { - console.log('succeed') - } - }, - ) - } - } - } - static async getFollowing() { - const profileID = window.localStorage.getItem('SNProfileID') - // String does not match 2nd argument for send function signature - const getAccount = (await PromiseIPC.send('accounts.get', profileID as any)) as any - const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }) - - const out = [] - const done = false - let nextFollow = '' - const limit = 100 - while (done === false) { - // const followingChunk = await hiveClient.call('follow_api', 'get_following', [ - const followingChunk = await hiveClient.call('condenser_api', 'get_following', [ - hiveInfo.username, - nextFollow, - 'blog', - limit, - ]) - - out.push(...followingChunk) - if (followingChunk.length !== limit) { - break - } - nextFollow = followingChunk[followingChunk.length - 1].following - } - return out - } - - static async convertLight(val) { - if (typeof val.json_metadata === 'object') { - val.json_metadata = JSON.parse(val.json_metadata) - } - if (!val.json_metadata.video) { - val.json_metadata.video = { - info: {}, - } - } - } - /** - * Retrieves post information from reflink. - * @param {String|RefLink} reflink - */ - static async permalinkToPostInfo(reflink) { - if (!(reflink instanceof RefLink)) { - reflink = RefLink.parse(reflink) - } - const post_content = ( - (await PromiseIPC.send('distiller.getContent', reflink.toString())) as any - ).json_content - return post_content - } - /** - * Retrieves post information as videoInfo from reflink. - * @param {String|RefLink} reflink - */ - static async permalinkToVideoInfo(reflink, options: any = {}): Promise { - if (!reflink) return undefined - - if (!(reflink instanceof RefLink)) { - reflink = RefLink.parse(reflink) - } - if (!options.type) { - options.type == '*' - } - - const postContent = await PromiseIPC.send('distiller.getContent', reflink.toString()) - const post_content = postContent.json_content - - if (!post_content) { - throw new Error('Invalid post content. Empty record') - } - switch (reflink.source.value) { - case 'hive': { - const json_metadata = post_content.json_metadata - if (!(json_metadata.app && options.type === 'video')) { - if (json_metadata.type) { - if (!json_metadata.type.includes('3speak/video')) { - throw new Error('Invalid post content. Not a video') - } - } - //throw new Error("Invalid post content. Not a video"); - } - const sources: VideoSource[] = [] - let title - let description - let duration - if (json_metadata.video.info.sourceMap) { - sources.push(...json_metadata.video.info.sourceMap) - return { - sources, - creation: new Date(post_content.created + 'Z').toISOString(), - title: post_content.title, - description: post_content.body, - tags: json_metadata.tags, - refs: [`hive:${post_content.author}:${post_content.permlink}`], //Reserved for future use when multi account system support is added. - meta: { - duration: json_metadata.duration, - }, //Reserved for future use. - reflink: `hive:${post_content.author}:${post_content.permlink}`, - } - } - try { - const video_info = json_metadata.video.info - const video_content = json_metadata.video.content - description = video_content.description - title = video_info.title - duration = video_info.duration - - - console.log("video_info", video_info) - const urls = [] - if (video_info.ipfs != null && video_info.ipfs) { - urls.push(`ipfs://${video_info.ipfs}`) - } - if (video_info.ipfsThumbnail != null && video_info.ipfsThumbnail) { - sources.push({ - type: 'thumbnail', - url: `ipfs://${video_info.ipfsThumbnail}`, - }) - } - urls.push(`https://threespeakvideo.b-cdn.net/${reflink.permlink}/default.m3u8`) - if (video_info.file) { - urls.push(`https://threespeakvideo.b-cdn.net/${reflink.permlink}/${video_info.file}`) - } - - for (const url of urls) { - sources.push({ - type: 'video', - url, - /** - * Reserved if a different player must be used on per format basis. - * - * If multi-resolution support is added in the future continue to use the url/format scheme. - * url should link to neccessary metadata. - */ - format: url.split('.').slice(-1)[0], - }) - } - - sources.push({ - type: 'thumbnail', - url: `https://threespeakvideo.b-cdn.net/${reflink.permlink}/thumbnails/default.png`, - }) - } catch (ex) { - title = post_content.title - description = post_content.body - } - return { - sources, - creation: new Date(post_content.created + 'Z').toISOString(), - title, - description, - tags: json_metadata.tags, - refs: [`hive:${post_content.author}:${post_content.permlink}`], //Reserved for future use when multi account system support is added. - meta: { duration }, //Reserved for future use. - reflink: `hive:${post_content.author}:${post_content.permlink}`, - } - } - default: { - throw new Error('Unknown account provider') - } - } - } - static async getProfileBackgroundImageUrl(reflink): Promise { - if (!(reflink instanceof RefLink)) { - reflink = RefLink.parse(reflink) - } - switch ('hive') { - case 'hive': { - const jsonContent = ( - (await PromiseIPC.send('distiller.getAccount', reflink.toString())) as any - ).json_content - - if (!jsonContent) { - throw new Error('Invalid account data content. Empty record') - } - const metadata = jsonContent.posting_json_metadata as string - if (!metadata) { - return '' - } - - const parsed = JSON.parse(metadata) - jsonContent.posting_json_metadata = JSON.parse(jsonContent.posting_json_metadata as string) - - const image = parsed.profile.cover_image - - return jsonContent.posting_json_metadata.profile.cover_image - } - default: { - throw new Error('Unknown account provider') - } - } - } - /** - * Retrieves Account profile picture URL. - * @todo Future item: Pull image from URL, then store locally for later use. - * @param {String|RefLink} reflink - */ - static async getProfilePictureURL(reflink) { - if (!(reflink instanceof RefLink)) { - reflink = RefLink.parse(reflink) - } - switch ('hive') { - case 'hive': { - const avatar_url = `https://images.hive.blog/u/${reflink.root}/avatar` - try { - await axios.head(avatar_url) - return avatar_url - } catch { - throw new Error('Failed to retrieve profile picture information') - } - } - default: { - throw new Error(`Unknown account provider ${'hive'}`) - } - } - } - /** - * Retrieves Follower count. - * @param {String|RefLink} reflink - */ - static async getFollowerCount(reflink) { - if (!(reflink instanceof RefLink)) { - reflink = RefLink.parse(reflink) - } - switch (reflink.source.value) { - case 'hive': { - const followerCount = await PromiseIPC.send( - 'distiller.getFollowerCount', - reflink.toString(), - ) - return followerCount - } - default: { - throw new Error(`Unknown account provider ${reflink.source.value}`) - } - } - } - /** - * Retrieves "about" text for user profiles - * @param {String|RefLink} reflink - */ - static async getProfileAbout(reflink) { - if (!(reflink instanceof RefLink)) { - reflink = RefLink.parse(reflink) - } - switch (reflink.source.value) { - case 'hive': { - // type error: 2nd argument string does not match function signature - // const userAboutText = JSON.parse( - // ((await PromiseIPC.send('distiller.getAccount', `hive:${reflink.root}` as any)) as any) - // .json_content.posting_json_metadata, - // ).profile.about - const res = (await PromiseIPC.send( - 'distiller.getAccount', - `hive:${reflink.root}` as any, - )) as any - - if (!res?.json_content?.posting_json_metadata) { - return '' - } else { - const metadata = JSON.parse(res.json_content.posting_json_metadata) - return metadata.profile.about - } - } - default: { - throw new Error(`Unknown account provider ${reflink.source.value}`) - } - } - } - /** - * Retrieves balances for user - * @param {String|RefLink} reflink - */ - static async getAccountBalances(reflink) { - if (!(reflink instanceof RefLink)) { - reflink = RefLink.parse(reflink) - } - switch (reflink.source.value) { - case 'hive': { - let accountBalances = - // type error: 2nd argument (string) does not match function signature - ((await PromiseIPC.send('distiller.getAccount', `hive:${reflink.root}` as any)) as any) - .json_content - - accountBalances = { - hive: accountBalances.balance, - hbd: accountBalances.sbd_balance, - } - return accountBalances - } - default: { - throw new Error(`Unknown account provider ${reflink.source.value}`) - } - } - } + static convertLight = convertLight + static createPost = createPost + static followHandler = followHandler + static getAccount = getAccount + static getAccountBalances = getAccountBalances + static getAccountMetadata = getAccountMetadata + static getAccounts = getAccounts + static getFollowerCount = getFollowerCount + static getFollowing = getFollowing + static getProfileAbout = getProfileAbout + static getProfileBackgroundImageUrl = getProfileBackgroundImageUrl + static getProfilePictureURL = getProfilePictureURL + static login = login + static logout = logout + static permalinkToPostInfo = permalinkToPostInfo + static permalinkToVideoInfo = permalinkToVideoInfo + static postComment = postComment + static updateMeta = updateMeta + static voteHandler = voteHandler } diff --git a/src/renderer/services/accountServices/convertLight.ts b/src/renderer/services/accountServices/convertLight.ts new file mode 100644 index 0000000..f347ee3 --- /dev/null +++ b/src/renderer/services/accountServices/convertLight.ts @@ -0,0 +1,10 @@ +export async function convertLight(val) { + if (typeof val.json_metadata === 'object') { + val.json_metadata = JSON.parse(val.json_metadata) + } + if (!val.json_metadata.video) { + val.json_metadata.video = { + info: {}, + } + } +} \ No newline at end of file diff --git a/src/renderer/services/accountServices/createPost.ts b/src/renderer/services/accountServices/createPost.ts new file mode 100644 index 0000000..43aca88 --- /dev/null +++ b/src/renderer/services/accountServices/createPost.ts @@ -0,0 +1,30 @@ +import { hive } from '@hiveio/hive-js' +export async function createPost(postOp) { + switch (postOp.accountType) { + case 'hive': { + const theWif = postOp.wif + + hive.broadcast.comment( + theWif, + '', + postOp.parentPermlink, + postOp.author, + postOp.permlink, + postOp.title, + postOp.body, + postOp.jsonMetadata, + function (error, succeed) { + console.log('Bout to check') + if (error) { + console.error(error) + console.error('Error encountered') + } + + if (succeed) { + console.log('succeed') + } + }, + ) + } + } +} \ No newline at end of file diff --git a/src/renderer/services/accountServices/followHandler.ts b/src/renderer/services/accountServices/followHandler.ts new file mode 100644 index 0000000..9d5bfd0 --- /dev/null +++ b/src/renderer/services/accountServices/followHandler.ts @@ -0,0 +1,46 @@ +import { hive } from '@hiveio/hive-js' +import ArraySearch from 'arraysearch' +export async function followHandler(profileID, followOp) { + const Finder = ArraySearch.Finder + switch (followOp.accountType) { + case 'hive': { + //const profile = await acctOps.getAccount(profileID); + const profile = (await this.getAccount(profileID)) as any + + const username = Finder.one.in(profile.keyring).with({ type: 'hive' }).username + const theWifObj = Finder.one.in(profile.keyring).with({ + type: 'hive', + }) + const wif = theWifObj.privateKeys.posting_key // posting key + const jsonObj = [ + 'follow', + { + follower: username, + following: followOp.author, + what: followOp.what ? [followOp.what] : [], + }, + ] + + // console.log( + hive.broadcast.customJson( + wif, + [], + [username], + 'follow', + JSON.stringify(jsonObj), + async (error, succeed) => { + if (error) { + console.error(error) + console.error('Error encountered broadcsting custom json') + } + + if (succeed) { + console.log(succeed) + console.log('success broadcasting custom json') + } + }, + ) + // ) + } + } +} \ No newline at end of file diff --git a/src/renderer/services/accountServices/getAccount.ts b/src/renderer/services/accountServices/getAccount.ts new file mode 100644 index 0000000..7514f70 --- /dev/null +++ b/src/renderer/services/accountServices/getAccount.ts @@ -0,0 +1,6 @@ +import PromiseIPC from 'electron-promise-ipc' + +export async function getAccount(profileID) { + const getAccount = await PromiseIPC.send('accounts.get', profileID) + return getAccount +} diff --git a/src/renderer/services/accountServices/getAccountBalances.ts b/src/renderer/services/accountServices/getAccountBalances.ts new file mode 100644 index 0000000..fb21f12 --- /dev/null +++ b/src/renderer/services/accountServices/getAccountBalances.ts @@ -0,0 +1,25 @@ +import RefLink from '../../../main/RefLink' +import PromiseIPC from 'electron-promise-ipc' + +export async function getAccountBalances(reflink) { + if (!(reflink instanceof RefLink)) { + reflink = RefLink.parse(reflink) + } + switch (reflink.source.value) { + case 'hive': { + let accountBalances = + // type error: 2nd argument (string) does not match function signature + ((await PromiseIPC.send('distiller.getAccount', `hive:${reflink.root}` as any)) as any) + .json_content + + accountBalances = { + hive: accountBalances.balance, + hbd: accountBalances.sbd_balance, + } + return accountBalances + } + default: { + throw new Error(`Unknown account provider ${reflink.source.value}`) + } + } +} \ No newline at end of file diff --git a/src/renderer/services/accountServices/getAccountMetadata.ts b/src/renderer/services/accountServices/getAccountMetadata.ts new file mode 100644 index 0000000..22c8095 --- /dev/null +++ b/src/renderer/services/accountServices/getAccountMetadata.ts @@ -0,0 +1,14 @@ +import PromiseIPC from 'electron-promise-ipc' +import { hiveClient } from '../../singletons/hive-client.singleton' +import ArraySearch from 'arraysearch' +//Get account posting_json_metadata +export async function getAccountMetadata() { + const Finder = ArraySearch.Finder + const profileID = window.localStorage.getItem('SNProfileID') + const getAccount = (await PromiseIPC.send('accounts.get', profileID as any)) as any + const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }) + + const account = await hiveClient.call('condenser_api', 'get_accounts', [[hiveInfo.username]]) + const metadata = account[0].posting_json_metadata + return metadata +} \ No newline at end of file diff --git a/src/renderer/services/accountServices/getAccounts.ts b/src/renderer/services/accountServices/getAccounts.ts new file mode 100644 index 0000000..ff7c2b1 --- /dev/null +++ b/src/renderer/services/accountServices/getAccounts.ts @@ -0,0 +1,7 @@ +import PromiseIPC from 'electron-promise-ipc' + +export async function getAccounts() { + const getAccounts = await PromiseIPC.send('accounts.ls', {} as any) + + return getAccounts +} diff --git a/src/renderer/services/accountServices/getFollowerCount.ts b/src/renderer/services/accountServices/getFollowerCount.ts new file mode 100644 index 0000000..46e5b77 --- /dev/null +++ b/src/renderer/services/accountServices/getFollowerCount.ts @@ -0,0 +1,20 @@ +import RefLink from '../../../main/RefLink' +import PromiseIPC from 'electron-promise-ipc' + +export async function getFollowerCount(reflink) { + if (!(reflink instanceof RefLink)) { + reflink = RefLink.parse(reflink) + } + switch (reflink.source.value) { + case 'hive': { + const followerCount = await PromiseIPC.send( + 'distiller.getFollowerCount', + reflink.toString(), + ) + return followerCount + } + default: { + throw new Error(`Unknown account provider ${reflink.source.value}`) + } + } +} \ No newline at end of file diff --git a/src/renderer/services/accountServices/getFollowing.ts b/src/renderer/services/accountServices/getFollowing.ts new file mode 100644 index 0000000..a7255eb --- /dev/null +++ b/src/renderer/services/accountServices/getFollowing.ts @@ -0,0 +1,32 @@ +import PromiseIPC from 'electron-promise-ipc' +import { hiveClient } from '../../singletons/hive-client.singleton' +import ArraySearch from 'arraysearch' + +export async function getFollowing() { + const Finder = ArraySearch.Finder + const profileID = window.localStorage.getItem('SNProfileID') + // String does not match 2nd argument for send function signature + const getAccount = (await PromiseIPC.send('accounts.get', profileID as any)) as any + const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }) + + const out = [] + const done = false + let nextFollow = '' + const limit = 100 + while (done === false) { + // const followingChunk = await hiveClient.call('follow_api', 'get_following', [ + const followingChunk = await hiveClient.call('condenser_api', 'get_following', [ + hiveInfo.username, + nextFollow, + 'blog', + limit, + ]) + + out.push(...followingChunk) + if (followingChunk.length !== limit) { + break + } + nextFollow = followingChunk[followingChunk.length - 1].following + } + return out +} \ No newline at end of file diff --git a/src/renderer/services/accountServices/getProfileAbout.ts b/src/renderer/services/accountServices/getProfileAbout.ts new file mode 100644 index 0000000..87f2ee3 --- /dev/null +++ b/src/renderer/services/accountServices/getProfileAbout.ts @@ -0,0 +1,31 @@ +import RefLink from '../../../main/RefLink' +import PromiseIPC from 'electron-promise-ipc' + +export async function getProfileAbout(reflink) { + if (!(reflink instanceof RefLink)) { + reflink = RefLink.parse(reflink) + } + switch (reflink.source.value) { + case 'hive': { + // type error: 2nd argument string does not match function signature + // const userAboutText = JSON.parse( + // ((await PromiseIPC.send('distiller.getAccount', `hive:${reflink.root}` as any)) as any) + // .json_content.posting_json_metadata, + // ).profile.about + const res = (await PromiseIPC.send( + 'distiller.getAccount', + `hive:${reflink.root}` as any, + )) as any + + if (!res?.json_content?.posting_json_metadata) { + return '' + } else { + const metadata = JSON.parse(res.json_content.posting_json_metadata) + return metadata.profile.about + } + } + default: { + throw new Error(`Unknown account provider ${reflink.source.value}`) + } + } +} \ No newline at end of file diff --git a/src/renderer/services/accountServices/getProfileBackgroundImageUrl.ts b/src/renderer/services/accountServices/getProfileBackgroundImageUrl.ts new file mode 100644 index 0000000..5b424b2 --- /dev/null +++ b/src/renderer/services/accountServices/getProfileBackgroundImageUrl.ts @@ -0,0 +1,33 @@ +import RefLink from '../../../main/RefLink' +import PromiseIPC from 'electron-promise-ipc' + +export async function getProfileBackgroundImageUrl(reflink): Promise { + if (!(reflink instanceof RefLink)) { + reflink = RefLink.parse(reflink) +} +switch ('hive') { + case 'hive': { + const jsonContent = ( + (await PromiseIPC.send('distiller.getAccount', reflink.toString())) as any + ).json_content + + if (!jsonContent) { + throw new Error('Invalid account data content. Empty record') + } + const metadata = jsonContent.posting_json_metadata as string + if (!metadata) { + return '' + } + + const parsed = JSON.parse(metadata) + jsonContent.posting_json_metadata = JSON.parse(jsonContent.posting_json_metadata as string) + + const image = parsed.profile.cover_image + + return jsonContent.posting_json_metadata.profile.cover_image + } + default: { + throw new Error('Unknown account provider') + } +} +} \ No newline at end of file diff --git a/src/renderer/services/accountServices/getProfilePictureURL.ts b/src/renderer/services/accountServices/getProfilePictureURL.ts new file mode 100644 index 0000000..c37f074 --- /dev/null +++ b/src/renderer/services/accountServices/getProfilePictureURL.ts @@ -0,0 +1,22 @@ +import RefLink from '../../../main/RefLink' +import axios from 'axios' + +export async function getProfilePictureURL(reflink) { + if (!(reflink instanceof RefLink)) { + reflink = RefLink.parse(reflink) + } + switch ('hive') { + case 'hive': { + const avatar_url = `https://images.hive.blog/u/${reflink.root}/avatar` + try { + await axios.head(avatar_url) + return avatar_url + } catch { + throw new Error('Failed to retrieve profile picture information') + } + } + default: { + throw new Error(`Unknown account provider ${'hive'}`) + } + } +} \ No newline at end of file diff --git a/src/renderer/services/accountServices/login.ts b/src/renderer/services/accountServices/login.ts new file mode 100644 index 0000000..c9f80a1 --- /dev/null +++ b/src/renderer/services/accountServices/login.ts @@ -0,0 +1,50 @@ +import { hive } from '@hiveio/hive-js' +import PromiseIPC from 'electron-promise-ipc' +import ArraySearch from 'arraysearch' + +const Finder = ArraySearch.Finder + +export async function login(data) { + switch (data.accountType) { + case 'hive': { + const userAccounts = await hive.api.getAccountsAsync([data.username]) + console.log(`got hive account for username ${data.username}`, userAccounts) + const pubWif = userAccounts[0].posting.key_auths[0][0] + + const Valid = hive.auth.wifIsValid(data.key, pubWif) + + if (Valid) { + const profile = { + _id: userAccounts[0].id.toString(), + nickname: data.profile, + keyring: [ + { + type: 'hive', + username: data.username, + public: { + pubWif, + }, + encrypted: data.encrypted, + privateKeys: { + posting_key: data.key, + }, + }, + ], + } + const profileID = profile._id + const check_profile = await PromiseIPC.send('accounts.has', profileID) + if (check_profile) { + throw new Error('Account exists already') + } else { + // 2nd argument doesn't match function signature - marking with any + await PromiseIPC.send('accounts.createProfile', profile as any) + const get_profile = await PromiseIPC.send('accounts.get', profileID) + localStorage.setItem('SNProfileID', profileID) + return get_profile + } + } else { + throw new Error('Invalid posting key') + } + } + } +} \ No newline at end of file diff --git a/src/renderer/services/accountServices/logout.ts b/src/renderer/services/accountServices/logout.ts new file mode 100644 index 0000000..55253ff --- /dev/null +++ b/src/renderer/services/accountServices/logout.ts @@ -0,0 +1,10 @@ +import { hive } from '@hiveio/hive-js' +import PromiseIPC from 'electron-promise-ipc' +import ArraySearch from 'arraysearch' + +const Finder = ArraySearch.Finder + +export async function logout(profileID) { + await PromiseIPC.send('accounts.deleteProfile', profileID) + return +} \ No newline at end of file diff --git a/src/renderer/services/accountServices/permalinkToPostInfo.ts b/src/renderer/services/accountServices/permalinkToPostInfo.ts new file mode 100644 index 0000000..7574228 --- /dev/null +++ b/src/renderer/services/accountServices/permalinkToPostInfo.ts @@ -0,0 +1,13 @@ +import { VideoInfo, VideoSource } from '../../../common/models/video.model' +import RefLink from '../../../main/RefLink' +import PromiseIPC from 'electron-promise-ipc' + +export async function permalinkToPostInfo(reflink) { + if (!(reflink instanceof RefLink)) { + reflink = RefLink.parse(reflink) + } + const post_content = ( + (await PromiseIPC.send('distiller.getContent', reflink.toString())) as any + ).json_content + return post_content +} \ No newline at end of file diff --git a/src/renderer/services/accountServices/permalinkToVideoInfo.ts b/src/renderer/services/accountServices/permalinkToVideoInfo.ts new file mode 100644 index 0000000..5ea7938 --- /dev/null +++ b/src/renderer/services/accountServices/permalinkToVideoInfo.ts @@ -0,0 +1,122 @@ +import { VideoInfo, VideoSource } from '../../../common/models/video.model' +import RefLink from '../../../main/RefLink' +import PromiseIPC from 'electron-promise-ipc' + +export async function permalinkToVideoInfo(reflink, options: any = {}): Promise { + if (!reflink) return undefined + +if (!(reflink instanceof RefLink)) { + reflink = RefLink.parse(reflink) +} +if (!options.type) { + options.type == '*' +} + +const postContent = await PromiseIPC.send('distiller.getContent', reflink.toString()) +console.log('postContent', postContent) +const post_content = postContent.json_content +console.log('post_content', post_content) +if (!post_content) { + throw new Error('Invalid post content. Empty record') +} +switch (reflink.source.value) { + case 'hive': { + const json_metadata = post_content.json_metadata + console.log('json_metadata', json_metadata) + if (!(json_metadata.app && options.type === 'video')) { + if (json_metadata.type) { + if (!json_metadata.type.includes('3speak/video')) { + throw new Error('Invalid post content. Not a video') + } + } + //throw new Error("Invalid post content. Not a video"); + } + const sources: VideoSource[] = [] + let title + let description + let duration + if (json_metadata.video.info.sourceMap[1]) { + console.log('json_metadata.video.info.sourceMap', json_metadata.video.info.sourceMap) + console.log('sources', sources) + console.log('json_metadata', json_metadata) + sources.push(...json_metadata.video.info.sourceMap) + return { + sources, + creation: new Date(post_content.created + 'Z').toISOString(), + title: post_content.title, + description: post_content.body, + tags: json_metadata.tags, + refs: [`hive:${post_content.author}:${post_content.permlink}`], //Reserved for future use when multi account system support is added. + meta: { + duration: json_metadata.duration, + }, //Reserved for future use. + reflink: `hive:${post_content.author}:${post_content.permlink}`, + } + } + try { + const video_info = json_metadata.video.info + const video_content = json_metadata.video.content + description = video_content.description + title = video_info.title + duration = video_info.duration + console.log("video_info", video_info) + console.log("video_info.file", video_info.file) + console.log("video_info.ipfs", video_info.ipfs) + console.log("video_info.ipfsThumbnail", video_info.ipfsThumbnail) + const urls = [] + if (video_info.ipfs != null && video_info.ipfs) { + urls.push(`ipfs://${video_info.ipfs}`) + } + if (video_info.ipfsThumbnail != null && video_info.ipfsThumbnail) { + sources.push({ + type: 'thumbnail', + url: `ipfs://${video_info.ipfsThumbnail}`, + }) + }else if (video_info.sourceMap[0].url) { + console.log("video_info.sourceMap[0].url", video_info.sourceMap[0].url) + sources.push({ + type: 'thumbnail', + url: `${video_info.sourceMap[0].url}`, + }) + } + if (video_info.file) { + urls.push(`https://threespeakvideo.b-cdn.net/${reflink.permlink}/${video_info.file}`) + }else { + urls.push(`https://threespeakvideo.b-cdn.net/${reflink.permlink}/default.m3u8`) + } + + for (const url of urls) { + sources.push({ + type: 'video', + url, + /** + * Reserved if a different player must be used on per format basis. + * + * If multi-resolution support is added in the future continue to use the url/format scheme. + * url should link to neccessary metadata. + */ + format: url.split('.').slice(-1)[0], + }) + } + + } catch (ex) { + title = post_content.title + description = post_content.body + } + console.log('sources', sources) + return { + sources, + creation: new Date(post_content.created + 'Z').toISOString(), + title, + description, + tags: json_metadata.tags, + refs: [`hive:${post_content.author}:${post_content.permlink}`], //Reserved for future use when multi account system support is added. + meta: { duration }, //Reserved for future use. + reflink: `hive:${post_content.author}:${post_content.permlink}`, + } + } + default: { + throw new Error('Unknown account provider') + } +} +} \ No newline at end of file diff --git a/src/renderer/services/accountServices/postComment.ts b/src/renderer/services/accountServices/postComment.ts new file mode 100644 index 0000000..24b8fa9 --- /dev/null +++ b/src/renderer/services/accountServices/postComment.ts @@ -0,0 +1,93 @@ +import { hive } from '@hiveio/hive-js' +import { CommentOp } from '../../../common/models/comments.model' +import PromiseIPC from 'electron-promise-ipc' +import { HiveInfo } from '../../../common/models/hive.model' +import { IpfsService } from '../ipfs.service' +import ArraySearch from 'arraysearch' +export async function postComment(commentOp: CommentOp) { + const Finder = ArraySearch.Finder + switch (commentOp.accountType) { + case 'hive': { + const profileID = window.localStorage.getItem('SNProfileID') as any + const getAccount = (await PromiseIPC.send('accounts.get', profileID)) as any + const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }) as HiveInfo + + console.log(`posting comment with profile ID`, profileID) + console.log(`account`, getAccount) + console.log(`hiveInfo`, hiveInfo) + console.log(`comment op`, commentOp) + + if (!commentOp.json_metadata) { + commentOp.json_metadata = {} + } + + let json_metadata + if (typeof commentOp.json_metadata === 'object') { + //Note: this is for peakd/hive.blog to display a video preview + if (!commentOp.parent_author) { + commentOp.json_metadata.video.info = { + author: hiveInfo.username, + permlink: commentOp.permlink, + } + } + json_metadata = JSON.stringify(commentOp.json_metadata) + } else { + throw new Error('commentOp.json_metadata must be an object') + } + + let body: string + + if (!commentOp.parent_author) { + let header: string + if (commentOp.json_metadata.sourceMap) { + const thumbnailSource = Finder.one.in(commentOp.json_metadata.sourceMap).with({ + type: 'thumbnail', + }) + console.log(`thumbnail source`, thumbnailSource) + try { + const cid = IpfsService.urlToCID(thumbnailSource.url) + const gateway = await IpfsService.getGateway(cid, true) + const imgSrc = gateway + IpfsService.urlToIpfsPath(thumbnailSource.url) + header = `[![](${imgSrc})](https://3speak.tv/watch?v=${hiveInfo.username}/${commentOp.permlink})
` + } catch (ex) { + console.error(`Error getting IPFS info`, ex) + throw ex + } + } + if (header) { + body = `${header} ${commentOp.body}
[▶️Watch on 3Speak Dapp](https://3speak.tv/openDapp?uri=hive:${hiveInfo.username}:${commentOp.permlink})` + } else { + body = `${commentOp.body}
[▶️Watch on 3Speak Dapp](https://3speak.tv/openDapp?uri=hive:${hiveInfo.username}:${commentOp.permlink})` + } + } else { + body = commentOp.body + } + + if (!json_metadata) { + throw new Error(`Cannot publish comment to hive with no metadata!`) + } + console.log(`POSTING TO HIVE WITH JSON METADATA`, json_metadata) + + try { + const out = await hive.broadcast.comment( + hiveInfo.privateKeys.posting_key, + commentOp.parent_author || '', + commentOp.parent_permlink || commentOp.tags[0] || 'threespeak', //parentPermlink + hiveInfo.username, + commentOp.permlink, + commentOp.title, + body, + json_metadata, + ) + + console.log(`comment broadcasted to hive! return:`) + console.log(out) + + return [`hive:${hiveInfo.username}:${commentOp.permlink}`, out] + } catch (err) { + console.error(`Error broadcasting comment to hive! ${err.message}`) + throw err + } + } + } +} \ No newline at end of file diff --git a/src/renderer/services/accountServices/postCustomJson.ts b/src/renderer/services/accountServices/postCustomJson.ts new file mode 100644 index 0000000..8593337 --- /dev/null +++ b/src/renderer/services/accountServices/postCustomJson.ts @@ -0,0 +1,31 @@ +//Post Custom Json +import { api } from '@hiveio/hive-js' +import PromiseIPC from 'electron-promise-ipc' +import ArraySearch from 'arraysearch' +export async function updateMeta(username, metadata) { + const Finder = ArraySearch.Finder + console.log('Updating metadata') + const profileID = window.localStorage.getItem('SNProfileID') + const getAccount = (await PromiseIPC.send('accounts.get', profileID as any)) as any + const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }) + const wif = hiveInfo.privateKeys.posting_key + console.log('WIF', wif) + console.log('JSON METADATA', metadata) + api.broadcast.account_update2( + wif, + [], + [username], + JSON.stringify(metadata), + async (error, succeed) => { + if (error) { + console.error(error) + console.error('Error encountered broadcsting custom json') + } + + if (succeed) { + console.log(succeed) + console.log('success broadcasting custom json') + } + }, + ) +} \ No newline at end of file diff --git a/src/renderer/services/accountServices/updateMeta.ts b/src/renderer/services/accountServices/updateMeta.ts new file mode 100644 index 0000000..3f3db69 --- /dev/null +++ b/src/renderer/services/accountServices/updateMeta.ts @@ -0,0 +1,31 @@ +//Update posting_json_metadata +import { api } from '@hiveio/hive-js' +import PromiseIPC from 'electron-promise-ipc' +import ArraySearch from 'arraysearch' +export async function updateMeta(username, metadata) { + const Finder = ArraySearch.Finder + console.log('Updating metadata') + const profileID = window.localStorage.getItem('SNProfileID') + const getAccount = (await PromiseIPC.send('accounts.get', profileID as any)) as any + const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }) + const wif = hiveInfo.privateKeys.posting_key + console.log('WIF', wif) + console.log('JSON METADATA', metadata) + api.broadcast.account_update2( + wif, + [], + [username], + JSON.stringify(metadata), + async (error, succeed) => { + if (error) { + console.error(error) + console.error('Error encountered broadcsting custom json') + } + + if (succeed) { + console.log(succeed) + console.log('success broadcasting custom json') + } + }, + ) +} \ No newline at end of file diff --git a/src/renderer/services/accountServices/voteHandler.ts b/src/renderer/services/accountServices/voteHandler.ts new file mode 100644 index 0000000..de12881 --- /dev/null +++ b/src/renderer/services/accountServices/voteHandler.ts @@ -0,0 +1,27 @@ +import { hive } from '@hiveio/hive-js' + +export async function voteHandler(voteOp) { + switch (voteOp.accountType) { + case 'hive': { + const weight = voteOp.weight * 100 + const theWif = voteOp.wif + hive.broadcast.vote( + theWif, + voteOp.voter, + voteOp.author, + voteOp.permlink, + weight, + function (error, succeed) { + if (error) { + console.error(error) + console.error('Error encountered') + } + + if (succeed) { + console.log('vote broadcast successfully') + } + }, + ) + } + } +} \ No newline at end of file diff --git a/src/renderer/services/peer.service.ts b/src/renderer/services/peer.service.ts new file mode 100644 index 0000000..a3887f3 --- /dev/null +++ b/src/renderer/services/peer.service.ts @@ -0,0 +1,107 @@ +import { api, broadcast } from "@hiveio/hive-js" +import PromiseIPC from 'electron-promise-ipc' +import ArraySearch from 'arraysearch' + +const Finder = ArraySearch.Finder + +type UpdateAccountOperation = [[ + 'account_update2', + { + account: string; + json_metadata: string; + posting_json_metadata: string; + } +]] + +interface Profile { + posting_json_metadata: string; + json_metadata: string; +} + +const fetchSingleProfile = async (account: string): Promise => { + const params = [[account]] + try { + const data = await api.callAsync('condenser_api.get_accounts', params) + return data[0] + } catch (err) { + console.error(err.message) + throw err + } +} + +const generateUpdateAccountOperation = (account: string, posting_json_metadata: string, json_metadata = ''): Promise => { + return new Promise((resolve) => { + const op_comment: UpdateAccountOperation = [[ + 'account_update2', + { + account, + json_metadata, + posting_json_metadata, + }, + ]] + resolve(op_comment) + }) +} + +export const handleUpdatePostingData = async (peerId: string): Promise => { + const profileID = window.localStorage.getItem('SNProfileID') + const getAccount = (await PromiseIPC.send('accounts.get', profileID as any)) as any + const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }) + const wif = hiveInfo.privateKeys.posting_key + const postingData = JSON.parse((await fetchSingleProfile(getAccount.nickname)).posting_json_metadata) + postingData['peerId'] = peerId + const stringifiedPostingData = JSON.stringify(postingData) + + const operations = await generateUpdateAccountOperation(getAccount.nickname, stringifiedPostingData) + + console.log('type of peerId', typeof peerId) + console.log(peerId) + console.log(operations[0]) + + broadcast.send( + { operations: [operations[0]] }, + { posting: wif }, + (err: Error, result: any) => { + if (err) { + console.error('Error broadcasting operation:', err.message) + } else { + console.log('Operation broadcast successfully:', result) + } + } + ) +} + +export async function postCustomJson(username, metadata, customJsonId) { + const profileID = window.localStorage.getItem('SNProfileID') + const getAccount = (await PromiseIPC.send('accounts.get', profileID as any)) as any + const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }) + const wif = hiveInfo.privateKeys.posting_key + + const customJsonPayload = { + required_auths: [], + required_posting_auths: [username], + id: customJsonId, + json: JSON.stringify(metadata), + } + + const customJsonOperation = [ + 'custom_json', + customJsonPayload + ]; + + broadcast.send( + { operations: [customJsonOperation] }, + { posting: wif }, + (error, result) => { + if (error) { + console.error(error) + console.error('Error encountered broadcasting custom json') + } + + if (result) { + console.log(result) + console.log('Success broadcasting custom json') + } + } + ); +} \ No newline at end of file diff --git a/src/renderer/services/video.service.ts b/src/renderer/services/video.service.ts index 31db546..4473660 100644 --- a/src/renderer/services/video.service.ts +++ b/src/renderer/services/video.service.ts @@ -13,16 +13,20 @@ const Finder = ArraySearch.Finder export class VideoService { static async getVideoSourceURL(permalink) { let post_content + console.log('permalink', permalink) if (typeof permalink === 'object') { post_content = permalink + console.log('post_content1', post_content) } else { post_content = await AccountService.permalinkToVideoInfo(permalink) + console.log('post_content2', post_content) } - + console.log('post_content', post_content.sources) const videoSource = Finder.one.in(post_content.sources).with({ type: 'video', }) if (videoSource) { + console.log('videoSource', videoSource) const url = videoSource.url as string if (typeof url === 'string' && !url.startsWith('ipfs')) { return url @@ -44,6 +48,7 @@ export class VideoService { return videoSource.url } } else { + console.error('Invalid post metadata') throw new Error('Invalid post metadata') } } diff --git a/src/renderer/singletons/hive-client.singleton.ts b/src/renderer/singletons/hive-client.singleton.ts index 7c9fb20..856ed91 100644 --- a/src/renderer/singletons/hive-client.singleton.ts +++ b/src/renderer/singletons/hive-client.singleton.ts @@ -6,8 +6,6 @@ import { promisify } from 'util' hive.broadcast.comment = promisify(hive.broadcast.comment) export const hiveClient = new Client([ - 'https://deathwing.me', - 'https://api.hivekings.com', 'https://anyx.io', 'https://api.openhive.network', ]) diff --git a/src/renderer/views/LoginView/loginViewContent.tsx b/src/renderer/views/LoginView/loginViewContent.tsx new file mode 100644 index 0000000..0648e2c --- /dev/null +++ b/src/renderer/views/LoginView/loginViewContent.tsx @@ -0,0 +1,173 @@ +import { Button, Dropdown, Form, OverlayTrigger, Tooltip } from 'react-bootstrap' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faSpinner } from '@fortawesome/free-solid-svg-icons' +import React from 'react' +const LoginViewContent = ({ + handleSubmit, + accountType, + setAccountType, + profile, + username, + onProfileChange, + onUsernameChange, + submitting, + encryption, + onEncryptionChange, + symKey, + onSymKeyChange, + key, + onKeyChange, + submitRef, + }) => { +return ( +<> +
{ + void handleSubmit(event) +}} +style={{ + maxWidth: '600px', + width: '100%', + padding: '20px', + alignItems: 'center', +}} +> +
+Account type + + {accountType} + + + { + setAccountType('hive') + }} + > + Hive + + { + setAccountType('IDX') + }} + > + IDX + + { + setAccountType('other') + }} + > + Other + + + +{accountType !== 'hive' ? ( + Disabled (Coming Soon!)} + > +
+ + Profile name + + + + Username + + +
+
+) : ( + <> + + Profile name + + + + Username + + + +)} + +{accountType === 'hive' && ( + + Hive Private Posting Key + + +)} +Disabled (Coming Soon!)} +> +
+ + +
+
+ +{encryption && ( + + Symmetric Key + + +)} +
+ + + +
+
+ +) +} +export default LoginViewContent; \ No newline at end of file diff --git a/src/renderer/views/PinsView.tsx b/src/renderer/views/PinsView.tsx index d160c8c..7e72659 100644 --- a/src/renderer/views/PinsView.tsx +++ b/src/renderer/views/PinsView.tsx @@ -1,417 +1,45 @@ -import 'brace/mode/json' -import 'brace/theme/github' -import 'jsoneditor-react/es/editor.min.css' +// PinsView.tsx +import 'brace/mode/json'; +import 'brace/theme/github'; +import 'jsoneditor-react/es/editor.min.css'; -import ace from 'brace' -import CID from 'cids' -import DateTime from 'date-and-time' -import Debug from 'debug' -import PromiseIpc from 'electron-promise-ipc' -import { JsonEditor as Editor } from 'jsoneditor-react' -import React, { useEffect, useMemo, useRef, useState } from 'react' -import { Button, Col, Dropdown, Form, FormControl, Row, Table } from 'react-bootstrap' -import { NotificationManager } from 'react-notifications' -import Popup from 'react-popup' +import ace from 'brace'; +import React, { useMemo, useRef, useState } from 'react'; +import Popup from 'react-popup'; +import { JsonEditor as Editor } from 'jsoneditor-react'; -import { IpfsHandler } from '../../main/core/components/ipfsHandler' -import RefLink from '../../main/RefLink' -import { FormUtils } from '../renderer_utils' -import { AccountService } from '../services/account.service' -import { CustomPinsViewMenu } from './PinsView/CustomMenu' -import { CustomPinsViewToggle } from './PinsView/CustomToggle' -import { bytesAsString, millisecondsAsString } from '../../common/utils/unit-conversion.functions' - -const debug = Debug('3speak:pins') +import { PinsViewComponent } from './PinsView/PinsViewComponent'; +import { pinRows } from './PinsView/PinRows'; +import { usePinningUtils } from './PinsView/pinningUtils'; export function PinsView() { - const [pinList, setPinList] = useState([]) - const [newVideos, setNewVideos] = useState([]) - const [trendingVideos, setTrendingVideos] = useState([]) - const [showExplorer, setShowExplorer] = useState(false) - - const pid = useRef() - - const updateSearchTables = (community = null, creator = null) => { - const ids = pinList.map((x) => { - return x._id - }) - console.log(ids) - const params = '?limit=10&ipfsOnly=true' - let newUrl = `https://3speak.tv/apiv2/feeds/new${params}` - let trendingUrl = `https://3speak.tv/apiv2/feeds/trending${params}` - if (community) { - newUrl = `https://3speak.tv/apiv2/feeds/community/${community}/new${params}` - trendingUrl = `https://3speak.tv/apiv2/feeds/community/${community}/trending${params}` - } else if (creator && creator.length > 2) { - newUrl = `https://3speak.tv/apiv2/feeds/@${creator}` - trendingUrl = null - } - - fetch(newUrl) - .then((r) => r.json()) - .then((r) => { - for (const video of r) { - const id = `hive:${video.author}:${video.permlink}` - video.isPinned = ids.includes(id) - video.id = id - } - console.log(r) - setNewVideos(r) - }) - - if (!trendingUrl) { - setTrendingVideos([]) - } else { - fetch(trendingUrl) - .then((r) => r.json()) - .then((r) => { - for (const video of r) { - const id = `hive:${video.author}:${video.permlink}` - video.isPinned = ids.includes(id) - video.id = id - } - setTrendingVideos(r) - }) - } - } - - const generate = async () => { - // type error - 2 arguments expected - setPinList(await PromiseIpc.send('pins.ls', undefined as any)) - } - - const PinLocally = async (cids, title, _id) => { - debug(`CIDs to store ${JSON.stringify(cids)}`) - if (cids.length !== 0) { - NotificationManager.info('Pinning in progress') + const { + newVideos, + trendingVideos, + pinList, + updateSearchTables, + PinLocally, + actionSelect, + removePin, + } = usePinningUtils(); - await PromiseIpc.send('pins.add', { - _id, - source: 'Pins page', - cids, - expire: null, - meta: { - title, - }, - } as any) + const [showExplorer, setShowExplorer] = useState(false); + const pid = useRef(); - NotificationManager.success( - `Video with title of ${title} has been successfully pinned! Thank you for contributing!`, - 'Pin Successful', - ) - } else { - NotificationManager.warning('This video is not available on IPFS') - } - await generate() - } - const actionSelect = async (key) => { - console.log(key) - switch (key) { - case '1': { - const func = () => - new Promise(async (resolve, reject) => { - const ref = React.createRef() as any - Popup.create({ - content: ( -
-
- Reflink - -
-
- ), - buttons: { - left: [ - { - text: 'Cancel', - className: 'secondary', - action: function () { - Popup.close() - }, - }, - ], - right: [ - { - text: 'Done', - className: 'success', - action: function () { - resolve(FormUtils.formToObj(new FormData(ref.current))) - Popup.close() - }, - }, - ], - }, - }) - }) - const ret = (await func()) as any - const video_info = await AccountService.permalinkToVideoInfo(ret.reflink) - const cids = [] - for (const source of video_info.sources) { - const url = new (require('url').URL)(source.url) - try { - new CID(url.host) - cids.push(url.host) - } catch (ex) { - console.error(ex) - } - } - if (cids.length !== 0) { - NotificationManager.info('Pinning in progress') - await PromiseIpc.send('pins.add', { - _id: ret.reflink, - source: 'Manual Add', - cids, - expire: null, - meta: { - title: video_info.title, - }, - } as any) - NotificationManager.success( - `Video with reflink of ${ret.reflink} has been successfully pinned! Thank you for contributing!`, - 'Pin Successful', - ) - } else { - NotificationManager.warning('This video is not available on IPFS') - } - break - } - case '2': { - NotificationManager.info('GC has started') - const { ipfs } = await IpfsHandler.getIpfs() - ipfs.repo.gc() - break - } - default: { - } - } - } - - const removePin = async (reflink) => { - try { - await PromiseIpc.send('pins.rm', reflink) - NotificationManager.success('IPFS pin removal complete') - await generate() - } catch (ex) { - NotificationManager.error('IPFS pin removal resulted in error') - console.error(ex) - } - } - - useEffect(() => { - document.title = '3Speak - Tokenised video communities' - void generate() - pid.current = setInterval(generate, 1500) - updateSearchTables() - - return () => { - clearInterval(pid.current) - } - }, []) - - const pinRows = useMemo(() => { - const rows = [] - for (const pin of pinList) { - const sizeBest = bytesAsString(pin.size) - - rows.push( - - - {pin._id} -
({RefLink.parse(pin._id).root}) - - - {pin.meta ? pin.meta.title : null} - - - {pin.cids.length > 1 ? ( - { - Popup.create({ - title: 'CIDs', - content: ( -
- -
- ), - buttons: { - left: [], - right: [ - { - text: 'close', - key: '⌘+s', - className: 'success', - action: function () { - Popup.close() - }, - }, - ], - }, - }) - }} - > - View ({pin.cids.length}) -
- ) : ( - pin.cids - )} - - {pin.source} - - {pin.expire - ? (() => { - console.log(pin.expire) - return 'In ' + millisecondsAsString((pin.expire = new Date().getTime())) - })() - : 'Permanent'} - - - {pin.meta.pin_date - ? (() => { - console.log(pin.meta.pin_date) - return new Date(pin.meta.pin_date).toLocaleString() - })() - : null} - - {pin.size === 0 ? Pinning In Progress : sizeBest} - - - - , - ) - } - return rows - }, [pinList]) + const rows = useMemo(() => pinRows(pinList, removePin), [pinList, removePin, ace]); return ( -
- - - - - - - - - Manual Pin - Manual GC - - - - - - - - - - - - - - - - - - {pinRows} -
ReflinkTitleCID(s)SourceExpirationPin DateSize/StatusRemove?
- - {showExplorer && ( - <> -
Select to pin and help secure the network by backing up videos
- { - if (event.target.value.match(/\bhive-\d{6}\b/g)) { - updateSearchTables(event.target.value, null) - } - }} - /> - { - updateSearchTables(null, event.target.value) - }} - /> - - {['new', 'trending'].map((type: 'new' | 'trending') => ( - - - - - - - - - - - - {/* {this.state[`${type}Videos`].map((video) => ( */} - {(type === 'new' ? newVideos : trendingVideos).map((video) => ( - - - - - - - ))} - -
{type} videosTitleCreatorpinned
-
-
- {(() => { - const pattern = DateTime.compile('mm:ss') - return DateTime.format(new Date(video.duration * 1000), pattern) - })()} -
- - - -
-
{video.title}{video.author} - {video.isPinned ? ( - - ) : ( - - )} -
- - ))} -
- - )} -
- ) + + ); } diff --git a/src/renderer/views/PinsView/PinCids.tsx b/src/renderer/views/PinsView/PinCids.tsx new file mode 100644 index 0000000..7a4d515 --- /dev/null +++ b/src/renderer/views/PinsView/PinCids.tsx @@ -0,0 +1,56 @@ +// PinCids.tsx +import React, { useState } from 'react'; +import ace from 'brace'; +import Popup from 'react-popup' +import 'jsoneditor-react/es/editor.min.css' +import { JsonEditor as Editor } from 'jsoneditor-react' + +const PinCids = ({ pin }) => { + console.log('PinCids', pin); + const [isOpen, setIsOpen] = useState(false); + + const handleClose = () => { + setIsOpen(false); + }; + + const handleOpen = () => { + console.log('handleOpen'); + setIsOpen(true); + }; + + if (pin.cids.length > 1) { + return ( + { + Popup.create({ + title: 'CIDs', + content: ( +
+ +
+ ), + buttons: { + left: [], + right: [ + { + text: 'close', + key: '⌘+s', + className: 'success', + action: function () { + Popup.close() + }, + }, + ], + }, + }) + }} + > + View ({pin.cids.length}) +
+ ); + } else { + return
; + } +}; + +export default PinCids; diff --git a/src/renderer/views/PinsView/PinRows.tsx b/src/renderer/views/PinsView/PinRows.tsx new file mode 100644 index 0000000..3c2c44a --- /dev/null +++ b/src/renderer/views/PinsView/PinRows.tsx @@ -0,0 +1,50 @@ +// PinRows.tsx +import React, { useMemo } from 'react'; +import { Button } from 'react-bootstrap'; +import { bytesAsString, millisecondsAsString } from '../../../common/utils/unit-conversion.functions'; +import PinCids from './PinCids'; +import RefLink from '../../../main/RefLink'; + +export const pinRows = (pinList: any[], removePin: any) => { + return ( + <> + {pinList.map((pin) => { + if(pin.meta) { + console.log('pinRows.tsx pin: ', pin); + const sizeBest = bytesAsString(pin.size); + const expireText = pin.expire + ? `In ${millisecondsAsString((pin.expire = new Date().getTime()))}` + : 'Permanent'; + const pinDateText = pin.meta.pin_date ? new Date(pin.meta.pin_date).toLocaleString() : null; + console.log('pinRows.tsx pinDateText: ', pinDateText); + return ( + + + {pin._id} +
({RefLink.parse(pin._id).root}) + + + {pin.meta ? pin.meta.title : null} + + + + + {pin.source} + {expireText} + {pinDateText} + {pin.size === 0 ? Pinning In Progress {pin.percent}% : sizeBest} + + + + + ); + + }else { + console.log('pinRows.tsx pin is null'); + } + })} + + ); +}; diff --git a/src/renderer/views/PinsView/PinsViewComponent.tsx b/src/renderer/views/PinsView/PinsViewComponent.tsx new file mode 100644 index 0000000..668a549 --- /dev/null +++ b/src/renderer/views/PinsView/PinsViewComponent.tsx @@ -0,0 +1,159 @@ +import React from 'react'; +import { Button, Col, Dropdown, Row, Table } from 'react-bootstrap' +import { CustomPinsViewToggle } from './CustomToggle' +import { CustomPinsViewMenu } from './CustomMenu' +import DateTime from 'date-and-time' +export interface PinsViewProps { + pinRows: React.ReactNode; + showExplorer: boolean; + newVideos: any[]; + trendingVideos: any[]; + actionSelect: (key: string) => Promise; + removePin: (reflink: string) => Promise; + PinLocally: (cids: string[], title: string, _id: string) => Promise; + setShowExplorer: (show: boolean) => void; + updateSearchTables: (community?: string, creator?: string) => Promise; +} + +export const PinsViewComponent: React.FC = + ({ + pinRows, + showExplorer, + newVideos, + trendingVideos, + actionSelect, + removePin, + PinLocally, + setShowExplorer, + updateSearchTables, + }) => { + return ( +
+
+ + + + + + + + + Manual Pin + Manual GC + + + + + + + + + + + + + + + + + + {pinRows} +
ReflinkTitleCID(s)SourceExpirationPin DateSize/StatusRemove?
+ + {showExplorer && ( + <> +
Select to pin and help secure the network by backing up videos
+ { + if (event.target.value.match(/\bhive-\d{6}\b/g)) { + updateSearchTables(event.target.value, null) + } + }} + /> + { + updateSearchTables(null, event.target.value) + }} + /> + + {['new', 'trending'].map((type: 'new' | 'trending') => ( + + + + + + + + + + + + {/* {this.state[`${type}Videos`].map((video) => ( */} + {(type === 'new' ? newVideos : trendingVideos).map((video) => ( + + + + + + + ))} + +
{type} videosTitleCreatorpinned
+
+
+ {(() => { + const pattern = DateTime.compile('mm:ss') + return DateTime.format(new Date(video.duration * 1000), pattern) + })()} +
+ + + +
+
{video.title}{video.author} + {video.isPinned ? ( + + ) : ( + + )} +
+ + ))} +
+ + )} +
+
+ ); +}; + diff --git a/src/renderer/views/PinsView/pinningUtils.tsx b/src/renderer/views/PinsView/pinningUtils.tsx new file mode 100644 index 0000000..ca3ddd0 --- /dev/null +++ b/src/renderer/views/PinsView/pinningUtils.tsx @@ -0,0 +1,208 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Button, Form, FormControl } from 'react-bootstrap'; +import { IpfsHandler } from '../../../main/core/components/ipfsHandler'; +import { AccountService } from '../../services/account.service'; +import { FormUtils } from '../../renderer_utils'; +import { NotificationManager } from 'react-notifications'; +import Popup from 'react-popup'; +import CID from 'cids'; +import PromiseIpc from 'electron-promise-ipc'; + +export const usePinningUtils = () => { + const [newVideos, setNewVideos] = useState([]); + const [trendingVideos, setTrendingVideos] = useState([]); + const [pinList, setPinList] = useState([]); + const pid = useRef(null); + + const updateSearchTables = async (community = null, creator = null) => { + const ids = pinList.map((x) => x._id); + console.log('ids', ids); + const params = '?limit=10&ipfsOnly=true'; + let newUrl = `https://3speak.tv/apiv2/feeds/new${params}`; + let trendingUrl = `https://3speak.tv/apiv2/feeds/trending${params}`; + + if (community) { + newUrl = `https://3speak.tv/apiv2/feeds/community/${community}/new${params}`; + trendingUrl = `https://3speak.tv/apiv2/feeds/community/${community}/trending${params}`; + } else if (creator && creator.length > 2) { + newUrl = `https://3speak.tv/apiv2/feeds/@${creator}`; + trendingUrl = null; + } + + try { + const newResponse = await fetch(newUrl); + const newVideos = await newResponse.json(); + newVideos.forEach((video) => { + const id = `hive:${video.author}:${video.permlink}`; + video.isPinned = ids.includes(id); + video.id = id; + }); + setNewVideos(newVideos); + + if (trendingUrl) { + const trendingResponse = await fetch(trendingUrl); + const trendingVideos = await trendingResponse.json(); + trendingVideos.forEach((video) => { + const id = `hive:${video.author}:${video.permlink}`; + video.isPinned = ids.includes(id); + video.id = id; + }); + setTrendingVideos(trendingVideos); + } else { + setTrendingVideos([]); + } + } catch (error) { + console.error('Error fetching data:', error); + } + }; + const generate = async () => { + // type error - 2 arguments expected + setPinList(await PromiseIpc.send('pins.ls', undefined as any)) + } + const PinLocally = async (cids, title, _id) => { + if (cids.length !== 0) { + NotificationManager.info('Pinning in progress'); + + try { + await PromiseIpc.send('pins.add', { + _id, + source: 'Pins page', + cids, + expire: null, + meta: { + title, + }, + }); + + NotificationManager.success( + `Video with title of ${title} has been successfully pinned! Thank you for contributing!`, + 'Pin Successful', + ); + } catch (error) { + console.error('Error pinning video:', error); + NotificationManager.error('Error pinning video', 'Pin Failed'); + } + } else { + NotificationManager.warning('This video is not available on IPFS'); + } + await generate(); + }; + const getReflinkFromPopup = () => new Promise(async (resolve) => { + const ref = React.createRef() as any + Popup.create({ + content: ( +
+
+ Reflink + +
+
+ ), + buttons: { + left: [ + { + text: 'Cancel', + className: 'secondary', + action: () => Popup.close(), + }, + ], + right: [ + { + text: 'Done', + className: 'success', + action: () => { + resolve(FormUtils.formToObj(new FormData(ref.current))); + Popup.close(); + }, + }, + ], + }, + }); + }); + const handleCase1 = async () => { + const ret = (await getReflinkFromPopup()) as any; + const video_info = await AccountService.permalinkToVideoInfo(ret.reflink); + const cids = video_info.sources + .map((source) => new (require('url').URL)(source.url)) + .filter((url) => { + try { + new CID(url.host); + return true; + } catch (ex) { + console.error(ex); + return false; + } + }) + .map((url) => url.host); + + if (cids.length !== 0) { + NotificationManager.info('Pinning in progress'); + await PromiseIpc.send('pins.add', { + _id: ret.reflink, + source: 'Manual Add', + cids, + expire: null, + meta: { + title: video_info.title, + }, + } as any); + NotificationManager.success( + `Video with reflink of ${ret.reflink} has been successfully pinned! Thank you for contributing!`, + 'Pin Successful', + ); + } else { + NotificationManager.warning('This video is not available on IPFS'); + } + }; + const handleCase2 = async () => { + NotificationManager.info('GC has started'); + const { ipfs } = await IpfsHandler.getIpfs(); + ipfs.repo.gc(); + }; + + const actionSelect = async (key) => { + switch (key) { + case '1': + await handleCase1(); + break; + case '2': + await handleCase2(); + break; + default: + } + }; + const removePin = async (reflink) => { + try { + await PromiseIpc.send('pins.rm', reflink) + NotificationManager.success('IPFS pin removal complete') + await generate() + } catch (ex) { + NotificationManager.error('IPFS pin removal resulted in error') + console.error(ex) + } + } + + useEffect(() => { + document.title = '3Speak - Tokenised video communities'; + generate(); + pid.current = setInterval(generate, 1500); + updateSearchTables(); + + return () => { + clearInterval(pid.current); + }; + }, []); + return { + newVideos, + trendingVideos, + pinList, + updateSearchTables, + PinLocally, + actionSelect, + removePin, + }; +}; + diff --git a/src/renderer/views/PoAView.tsx b/src/renderer/views/PoAView.tsx new file mode 100644 index 0000000..4e260fa --- /dev/null +++ b/src/renderer/views/PoAView.tsx @@ -0,0 +1,53 @@ +import 'brace/mode/json'; +import 'brace/theme/github'; +import 'brace/theme/monokai'; +import 'brace/theme/solarized_dark'; +import 'jsoneditor-react/es/editor.min.css'; +import React, { useEffect } from 'react'; +import { Terminal } from 'xterm'; +import { WebLinksAddon } from 'xterm-addon-web-links'; +import 'xterm/css/xterm.css'; + +import { PoAViewContent } from './PoAView/PoAViewContent'; +import { usePoAInstaller } from './PoAView/usePoAInstaller'; +import { usePoAProgramRunner } from './PoAView/usePoAProgramRunner'; +import { useEnablePoA } from './PoAView/useEnablePoA'; +import { usePoAProgramRunnerContext } from './PoAView/PoAProgramRunnerContext'; + +export function PoAView() { + const { programRunner, setProgramRunner, terminalRef } = usePoAProgramRunnerContext(); + + const { + alreadyEnabled, + enablePoA, + loadAlreadyEnabled, + } = useEnablePoA(); + const updater = usePoAInstaller(); + const { terminal, setTerminal, isPoARunning, runPoA, contextValue } = usePoAProgramRunner(); + + useEffect(() => { + if (terminalRef.current && !terminal) { + const term = new Terminal(); + term.open(terminalRef.current); + term.loadAddon(new WebLinksAddon()); + setTerminal(term); + } + + return () => { + if (terminal) { + terminal.dispose(); + } + }; + }, [terminal, terminalRef]); + + return ( + + ); +} diff --git a/src/renderer/views/PoAView/PoAProgramRunnerContext.tsx b/src/renderer/views/PoAView/PoAProgramRunnerContext.tsx new file mode 100644 index 0000000..c967b3b --- /dev/null +++ b/src/renderer/views/PoAView/PoAProgramRunnerContext.tsx @@ -0,0 +1,36 @@ +// file: PoAProgramRunnerContext.tsx +import React, { useState, useRef, useContext } from 'react'; +import ProgramRunner from '../../../main/core/components/ProgramRunner'; + +interface PoAProgramRunnerContextProps { + programRunner: ProgramRunner | null; + setProgramRunner: (runner: ProgramRunner | null) => void; + terminalRef: React.RefObject; +} + +const PoAProgramRunnerContext = React.createContext(undefined); + +interface PoAProgramRunnerProviderProps { + children: React.ReactNode; +} + +export const PoAProgramRunnerProvider: React.FC = ({ children }) => { + const [programRunner, setProgramRunner] = useState(null); + const terminalRef = useRef(null); + + return ( + + {children} + + ); +}; + +export const usePoAProgramRunnerContext = (): PoAProgramRunnerContextProps => { + const context = useContext(PoAProgramRunnerContext); + if (!context) { + throw new Error('usePoAProgramRunnerContext must be used within a PoAProgramRunnerProvider'); + } + return context; +}; + +export default PoAProgramRunnerContext; diff --git a/src/renderer/views/PoAView/PoAStateContext.tsx b/src/renderer/views/PoAView/PoAStateContext.tsx new file mode 100644 index 0000000..4673181 --- /dev/null +++ b/src/renderer/views/PoAView/PoAStateContext.tsx @@ -0,0 +1,32 @@ +// filename: PoAStateContext.tsx +import React, { createContext, useState, useContext, useEffect, useRef } from 'react'; + +interface PoAStateContextType { + logs: string[]; + setLogs: React.Dispatch>; +} + +const PoAStateContext = createContext({ + logs: [], + setLogs: () => {}, +}); + +export const PoAStateProvider: React.FC = ({ children }) => { + const [logs, setLogs] = useState([]); + const terminalRef = useRef(null); + + useEffect(() => { + if (terminalRef.current) { + terminalRef.current.innerHTML = ''; + logs.forEach(log => terminalRef.current.innerHTML += log + '
'); + } + }, [logs, terminalRef]); + + return ( + + {children} + + ); +}; + +export const usePoAState = () => useContext(PoAStateContext); diff --git a/src/renderer/views/PoAView/PoAViewContent.tsx b/src/renderer/views/PoAView/PoAViewContent.tsx new file mode 100644 index 0000000..42b57cb --- /dev/null +++ b/src/renderer/views/PoAView/PoAViewContent.tsx @@ -0,0 +1,123 @@ +//PoAViewContent.tsx +import React, { useEffect, RefObject } from 'react'; +import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'; +import { usePoAState } from './PoAStateContext'; + +interface PoAViewContentProps { + alreadyEnabled: boolean; + isPoARunning: boolean; + enablePoA: () => void; + updatePoA: () => void; + runPoA: () => void; + terminalRef: RefObject; +} + +export const PoAViewContent: React.FC = ({ + alreadyEnabled, + isPoARunning, + enablePoA, + updatePoA, + runPoA, + terminalRef, + }) => { + const { logs } = usePoAState(); + + useEffect(() => { + if (terminalRef.current) { + terminalRef.current.innerHTML = ''; + logs.forEach(log => { + const newLogElement = document.createElement('div'); + newLogElement.innerHTML = log; + terminalRef.current.appendChild(newLogElement); + }); + // Auto scroll to bottom + terminalRef.current.lastElementChild?.scrollIntoView({ behavior: "smooth" }); + } + }, [logs, terminalRef]); + + + return ( +
+

Proof of Access.

+

+ Enable the Proof of Access feature to earn rewards for storing data on your computer. +

+

+ + By enabling proof of access your ipfs peer ID will be published to your hive profile metadata + +

+

+

+ {alreadyEnabled ? ( + Enable or update Proof of Access feature} + > + + + ) : ( + Enable or update Proof of Access feature} + > + + + )} +
+

+

+

+ Update Proof of Access software} + > + + +
+

+

+

+ {isPoARunning ? 'Shutdown' : 'Start'} Proof of Access software} + > + + +
+

+
+
+ ); +}; diff --git a/src/renderer/views/PoAView/useEnablePoA.ts b/src/renderer/views/PoAView/useEnablePoA.ts new file mode 100644 index 0000000..1875343 --- /dev/null +++ b/src/renderer/views/PoAView/useEnablePoA.ts @@ -0,0 +1,74 @@ +import { useState, useEffect } from 'react'; +import { AccountService } from '../../services/account.service'; +import { handleUpdatePostingData } from '../../services/peer.service'; +import { create } from 'ipfs-http-client'; +import ArraySearch from 'arraysearch'; +import { NotificationManager } from 'react-notifications'; + +const Finder = ArraySearch.Finder; + +export function useEnablePoA() { + const [ipfsPeerID, setIpfsPeerID] = useState(''); + const [alreadyEnabled, setAlreadyEnabled] = useState(false); + const getIpfsConfig = async () => { + try { + const ipfs = create({ url: 'http://localhost:5001' }); + const { id } = await ipfs.id(); + console.log('peerId', id); + + setIpfsPeerID(id); + } catch (error) { + console.error('Error getting IPFS peer ID:', error); + } + }; + + const loadAlreadyEnabled = async () => { + const out = await AccountService.getAccountMetadata(); + const parsedOut = JSON.parse(out); + if (parsedOut.peerId) { + setAlreadyEnabled(true); + } else { + console.log(out); + console.log('Proof of access is not enabled'); + setAlreadyEnabled(false); + } + }; + + const enablePoA = async () => { + const profileID = localStorage.getItem('SNProfileID'); + if (profileID) { + try { + void await getIpfsConfig(); + let out = await AccountService.getAccountMetadata(); + const parsedOut = JSON.parse(out); + console.log('parsedOut', parsedOut); + console.log('ipfsPeerID', ipfsPeerID); + if (parsedOut.peerId == "12D3KooWN9tVM5Fk3vsS5emRFA8xfGkj7fztQ8PAPahtaxDjjZzA") { + console.log('Proof of access is already enabled'); + NotificationManager.error('Proof of access is already enabled'); + return; + } + + console.log('peerID: ', ipfsPeerID); + await handleUpdatePostingData("12D3KooWN9tVM5Fk3vsS5emRFA8xfGkj7fztQ8PAPahtaxDjjZzA"); + NotificationManager.success('Proof of access enabled'); + } catch (error) { + console.error(error); + NotificationManager.error('There was an error completing this operation'); + } + } else { + NotificationManager.error('You need to be logged in to perform this operation'); + } + }; + + useEffect(() => { + void getIpfsConfig(); + }, []); + + return { + ipfsPeerID, + alreadyEnabled, + enablePoA, + loadAlreadyEnabled, + }; +} diff --git a/src/renderer/views/PoAView/usePoAInstaller.ts b/src/renderer/views/PoAView/usePoAInstaller.ts new file mode 100644 index 0000000..c65291b --- /dev/null +++ b/src/renderer/views/PoAView/usePoAInstaller.ts @@ -0,0 +1,40 @@ +import { useState, useEffect } from 'react'; +import PoAInstaller from '../../../main/AutoUpdaterPoA'; +import { Terminal } from 'xterm'; +import { WebLinksAddon } from 'xterm-addon-web-links'; +import 'xterm/css/xterm.css'; + +export function usePoAInstaller() { + const [terminal, setTerminal] = useState(null); + const updater = new PoAInstaller(); + + const updatePoA = async () => { + try { + await updater.main(); + } catch (error) { + console.error('Error updating Proof of Access:', error); + } + }; + + const initTerminal = (terminalRef: React.RefObject) => { + if (terminalRef.current && !terminal) { + const term = new Terminal(); + term.open(terminalRef.current); + term.loadAddon(new WebLinksAddon()); + setTerminal(term); + } + }; + + useEffect(() => { + if (terminal) { + updater.on('data', (data: string) => { + terminal.write(data.replace(/\n/g, '\r\n')); + }); + } + }, [terminal]); + + return { + updatePoA, + initTerminal, + }; +} diff --git a/src/renderer/views/PoAView/usePoAProgramRunner.ts b/src/renderer/views/PoAView/usePoAProgramRunner.ts new file mode 100644 index 0000000..630eaa4 --- /dev/null +++ b/src/renderer/views/PoAView/usePoAProgramRunner.ts @@ -0,0 +1,58 @@ +// Type: file name: src\renderer\views\PoAView\usePoAProgramRunner.ts +import { usePoAState } from './PoAStateContext'; +import React, { useState, useRef, useEffect, useCallback } from 'react'; +import ProgramRunner from '../../../main/core/components/ProgramRunner'; +import PoAInstaller from '../../../main/AutoUpdaterPoA'; +import { Terminal } from 'xterm'; +import Path from 'path'; +import os from 'os'; +import ArraySearch from 'arraysearch'; +import PromiseIPC from 'electron-promise-ipc'; + +export const usePoAProgramRunner = () => { + const [terminal, setTerminal] = useState(null); + const [isPoARunning, setIsPoARunning] = useState(false); + const runner = useRef(null); + const poaInstaller = useRef(new PoAInstaller()); + const Finder = ArraySearch.Finder; + const { logs, setLogs } = usePoAState(); + const isMountedRef = useRef(true); + + const runPoA = useCallback(async () => { + let command = ''; // Define command here + if (!isPoARunning) { + const profileID = localStorage.getItem('SNProfileID'); + const getAccount = (await PromiseIPC.send('accounts.get', profileID as any)) as any; + const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }); + const installDir = Path.join(os.homedir(), (await poaInstaller.current.getDefaultPath()) || ''); + const executablePath = Path.join(installDir, 'PoA.exe'); + command = `"${executablePath}" -node=2 -username=${hiveInfo.username}`; // Assign command here + if (!runner.current) { + runner.current = new ProgramRunner(command, (data: string) => { + if (!isMountedRef.current) return; + const logData = data.replace(/\n/g, '\r\n'); + terminal?.write(logData); + setLogs(prevLogs => [...prevLogs, logData]); + }); + } + + runner.current.setupProgram(() => { + if (!isMountedRef.current) return; + setIsPoARunning(false); + }); + setIsPoARunning(true); + } else { + runner.current.stopProgram(); + setIsPoARunning(false); + } + }, [terminal, isPoARunning]); + + const contextValue = { + isPoARunning, + setIsPoARunning, + runPoA, + stopPoA: () => runner.current?.stopProgram(), + }; + + return { terminal, setTerminal, isPoARunning, runPoA, contextValue }; +} diff --git a/src/renderer/views/UploaderView.tsx b/src/renderer/views/UploaderView.tsx index 56676b4..9309a17 100644 --- a/src/renderer/views/UploaderView.tsx +++ b/src/renderer/views/UploaderView.tsx @@ -1,25 +1,17 @@ import './Uploader.css' - -import DateTime from 'date-and-time' -import PromiseIpc from 'electron-promise-ipc' -import Fs from 'fs' import * as IPFSHTTPClient from 'ipfs-http-client' -import randomstring from 'randomstring' import React, { useEffect, useRef, useState } from 'react' -import { Button, Card, Col, Form, ProgressBar, Row, Tab, Tabs } from 'react-bootstrap' -import { NotificationManager } from 'react-notifications' + import { IPFS_HOST } from '../../common/constants' -import { - bytesAsString, - millisecondsAsString, - secondsAsString, -} from '../../common/utils/unit-conversion.functions' -import DefaultThumbnail from '../assets/img/default-thumbnail.jpg' import LoadingMessage from '../components/LoadingMessage' -import { FormUtils } from '../renderer_utils' -import { AccountService } from '../services/account.service' - +import UploaderViewContent from './UploaderView/uploaderViewContent' +import { calculatePercentage } from './UploaderView/calculatePercentage'; +import { normalizeSize } from './UploaderView/normalizeSize'; +import { publish } from './UploaderView/publish'; +import { videoSelect } from './UploaderView/videoSelect'; +import { thumbnailSelect } from './UploaderView/thumbnailSelect' +import { startEncode } from './UploaderView/startEncode'; export function UploaderView() { const videoUpload = useRef() const thumbnailUpload = useRef() @@ -61,241 +53,32 @@ export function UploaderView() { ipfs.current = IPFSHTTPClient.create({ host: IPFS_HOST }) }, []) - const caluclatePercentage = () => { - return progress.percent / statusInfo.nstages + statusInfo.stage * (100 / statusInfo.nstages) - } - - const normalizeSize = () => { - const size = videoInfo.size + thumbnailInfo.size - return bytesAsString(size) - } - - const compileVideoCid = async () => { - const videoCid = videoInfo.cid - if (thumbnailInfo.cid) { - const obj = await ipfs.current.object.stat(thumbnailInfo.cid) - console.log(obj) - console.log(thumbnailInfo) - const output = await ipfs.current.object.patch.addLink(videoCid, { - name: thumbnailInfo.path, - size: thumbnailInfo.size, - cid: thumbnailInfo.cid, - }) - console.log(output) - return output.toString() - } - return videoCid - } - - /** - * Note: example metadata https://hiveblocks.com/hive-181335/@taskmaster4450/tqxwimhy - */ - const publish = async () => { - const videoCid = await compileVideoCid() - // const formData = FormUtils.formToObj(new FormData(publishForm)) - - let tags: string[] = [] - if (publishFormTags) { - tags = publishFormTags.replace(/\s/g, '').split(',') - } + const handlePublish = async () => { + await publish({ + videoInfo, thumbnailInfo, publishFormTitle, publishFormDescription, publishFormTags, setBlockedGlobalMessage, ipfs, + }); + }; - console.log(`thumbnail info`, thumbnailInfo) - - const sourceMap = [] - if (thumbnailInfo.path) { - sourceMap.push({ - type: 'thumbnail', - url: `ipfs://${videoCid}/${thumbnailInfo.path}`, - }) - } - - if (videoInfo) { - sourceMap.push({ - type: 'video', - url: `ipfs://${videoCid}/${videoInfo.path}`, - format: 'm3u8', - }) - } - const permlink = `speak-${randomstring - .generate({ - length: 8, - charset: 'alphabetic', - }) - .toLowerCase()}` - // console.log(permlink) - console.log(`source map`) - console.log(sourceMap) - - setBlockedGlobalMessage('Publishing') - - const filesize = videoInfo.size + thumbnailInfo.size - - try { - const [reflink] = await AccountService.postComment({ - accountType: 'hive', - title: publishFormTitle || 'Untitled video', - body: publishFormDescription || '', - permlink, - tags, - json_metadata: { - title: publishFormTitle || 'Untitled video', - description: publishFormDescription || '', - tags, - sourceMap, - filesize, - created: new Date(), - lang: videoInfo.language, - video: { - duration: videoInfo.duration, - }, - app: '3speak/app-beta', - type: '3speak/video', - }, - }) - - setTimeout(() => { - location.hash = `#/watch/${reflink}` - setBlockedGlobalMessage('done') - }, 15000) - } catch (error) { - console.error(`Error in postComment operation ${error.message}`) - throw error - } - } - - const handleVideoSelect = async (e) => { - let file - if (e.target && e.target.files) { - file = e.target.files[0] - } else if (e.dataTransfer && e.dataTransfer.files) { - file = e.dataTransfer.files[0] - } - if (file) { - setVideoSourceFile(file.path) - setLogData([...logData, `Selected: ${videoInfo.path}`]) - } - } + const handleVideoSelect = (e) => { + videoSelect( + e, setVideoSourceFile, setLogData, logData, videoInfo + ); + }; const handleThumbnailSelect = async (e) => { - console.log(`handling thumbnail selectr`) + await thumbnailSelect({ + e, thumbnailPreview, setThumbnailInfo, setVideoSourceFile, setLogData, ipfs, logData, videoInfo + }); + }; + + const handleStartEncode = async () => { + await startEncode({ + event, videoSourceFile, hwaccelOption, setEncodingInProgress, setStartTime, setEndTime, setProgress, setStatusInfo, setEstimatedTimeRemaining, setVideoInfo, setPublishReady, progress, statusInfo + }); + }; - let file - if (e.target && e.target.files) { - file = e.target.files[0] - } else if (e.dataTransfer && e.dataTransfer.files) { - file = e.dataTransfer.files[0] - } - if (file) { - const imgblob = URL.createObjectURL(file) - const size = file.size - console.log(`uploading file with size ${size}`) - thumbnailPreview.current = imgblob - - const fileDetails = { - path: file.name, - content: file, - } - - const ipfsOut = await ipfs.current.add(fileDetails, { pin: false }) - console.log(`setting thumbnail info to cid`, ipfsOut.cid.toString()) - - setThumbnailInfo({ - size, - path: `thumbnail.${file.type.split('/')[1]}`, - cid: ipfsOut.cid.toString(), - }) - } - } - - const handleStartEncode = async (event) => { - event.preventDefault() - if (videoSourceFile === null) { - NotificationManager.error('No video source file selected') - return - } - if (!Fs.existsSync(videoSourceFile)) { - NotificationManager.error('Source file does not exist') - return - } - setEncodingInProgress(true) - const _startingTime = new Date().getTime() - setStartTime(_startingTime) - setEndTime(null) - - const jobInfo = (await PromiseIpc.send('encoder.createJob', { - sourceUrl: videoSourceFile, - profiles: [ - { - name: '1080p', - size: '1920x1080', - }, - { - name: '720p', - size: '1080x720', - }, - { - name: '480p', - size: '720x480', - }, - ], - options: { - hwaccel: - hwaccelOption && - hwaccelOption.length > 0 && - hwaccelOption !== '' && - hwaccelOption && - hwaccelOption !== 'none' - ? hwaccelOption - : undefined, - }, - } as any)) as any - NotificationManager.success('Encoding Started.') - - let savePct = 0 - const progressTrack = async () => { - const _timeNow = new Date().getTime() - const status = (await PromiseIpc.send('encoder.status', jobInfo.id)) as any - - console.log(`Encoder status: `, status) - - setProgress(status.progress || {}) - setStatusInfo(status) - - const val = status.progress.percent - // const diffPct = val - savePct - // savePct = val - // const pctPerSec = diffPct / 3 - // const totalTimeRemaining = (100 - val) / pctPerSec - const totalTimeRemaining = (100 * (_timeNow - _startingTime)) / val - setEstimatedTimeRemaining(millisecondsAsString(totalTimeRemaining)) - setEndTime(_timeNow) - } - - const pid = setInterval(progressTrack, 3000) - void progressTrack() - - const encodeOutput = (await PromiseIpc.send('encoder.getjoboutput', jobInfo.id)) as any - console.log(`got encode output`) - console.log(encodeOutput) - - setVideoInfo({ - size: encodeOutput.size, - cid: encodeOutput.ipfsHash, - path: encodeOutput.path, - duration: Number(DateTime.parse(encodeOutput.duration, 'hh:mm:ss.SS', true)) / 1000, - }) - - clearInterval(pid) - - setEncodingInProgress(false) - setEstimatedTimeRemaining(null) - setEndTime(new Date().getTime()) - setPublishReady(true) - - NotificationManager.success('Encoding complete.') - } if (blockedGlobalMessage) { return ( @@ -307,288 +90,35 @@ export function UploaderView() { } return ( -
- -
-
videoUpload.current.click()} - style={{ - width: '4000px', - textAlign: 'center', - height: '150px', - fontSize: '16px', - fontWeight: 'bold', - cursor: 'pointer', - }} - onDragOver={(e) => e.preventDefault()} - onDrop={handleVideoSelect} - > - Drop a file or click to start the upload
-

- Selected: {videoSourceFile} -

- -
-
-
- - -
-
- - Title - setPublishFormTitle(e.target.value)} - value={publishFormTitle} - type="text" - name="title" - > - - - Description - - - - Tags - setPublishFormTags(e.target.value)} - value={publishFormTags} - type="text" - name="tags" - > - - Separate multiple tags with a ,{' '} - - - - Language - - - - - - - - Thumbnail -
- thumbnailUpload.current.click()} - onDragOver={(e) => e.preventDefault()} - onDrop={handleThumbnailSelect} - /> - -

Click the thumbnail to change it

-

Recommended 5MB. Ideally 1280px×720px.

-
- - -
-
- - - - Encoder status - - This area will show live encoding statistics - {' '} -
- - -
- Time Remaining:{' '} - {estimatedTimeRemaining !== 'NaNns' ? estimatedTimeRemaining : 'Calculating'} -
-
- Total Time (so far):{' '} - {endTime !== 0 ? millisecondsAsString(endTime - startTime) : 'Calculating'} -
-
-
-
-
-
Control Panel
-
- - - - - Format - - - - - - Hardware Accel - - - Use hardware acceleration to speed up video encode. Not available on all - systems, results may vary. - - - - - - - Video IpfsPath - - - - Thumbnail IpfsPath - - - - Total Size - - - - {/* - - - - - - - - - - - - - - - -
EnabledIDUsername
Hivevaultec
- -
*/} - - - - -
-
- -
-
+ ) } diff --git a/src/renderer/views/UploaderView/calculatePercentage.ts b/src/renderer/views/UploaderView/calculatePercentage.ts new file mode 100644 index 0000000..2cf93a2 --- /dev/null +++ b/src/renderer/views/UploaderView/calculatePercentage.ts @@ -0,0 +1,3 @@ +export const calculatePercentage = (progress, statusInfo) => { + return progress.percent / statusInfo.nstages + statusInfo.stage * (100 / statusInfo.nstages); +}; \ No newline at end of file diff --git a/src/renderer/views/UploaderView/compileVideoCid.ts b/src/renderer/views/UploaderView/compileVideoCid.ts new file mode 100644 index 0000000..9b02b0d --- /dev/null +++ b/src/renderer/views/UploaderView/compileVideoCid.ts @@ -0,0 +1,13 @@ +export const compileVideoCid = async (videoInfo, thumbnailInfo, ipfs) => { + const videoCid = videoInfo.cid; + if (thumbnailInfo.cid) { + const obj = await ipfs.current.object.stat(thumbnailInfo.cid); + const output = await ipfs.current.object.patch.addLink(videoCid, { + name: thumbnailInfo.path, + size: thumbnailInfo.size, + cid: thumbnailInfo.cid, + }); + return output.toString(); + } + return videoCid; +}; diff --git a/src/renderer/views/UploaderView/normalizeSize.ts b/src/renderer/views/UploaderView/normalizeSize.ts new file mode 100644 index 0000000..a3e5418 --- /dev/null +++ b/src/renderer/views/UploaderView/normalizeSize.ts @@ -0,0 +1,7 @@ +import { + bytesAsString, +} from '../../../common/utils/unit-conversion.functions' +export const normalizeSize = (videoInfo, thumbnailInfo) => { + const size = videoInfo.size + thumbnailInfo.size; + return bytesAsString(size); +}; diff --git a/src/renderer/views/UploaderView/publish.ts b/src/renderer/views/UploaderView/publish.ts new file mode 100644 index 0000000..2f4a963 --- /dev/null +++ b/src/renderer/views/UploaderView/publish.ts @@ -0,0 +1,75 @@ +import { AccountService } from '../../services/account.service' +import randomstring from 'randomstring' +import { compileVideoCid } from './compileVideoCid' +export const publish = async ({videoInfo, thumbnailInfo, publishFormTitle, publishFormDescription, publishFormTags, setBlockedGlobalMessage, ipfs}) => { + const videoCid = await compileVideoCid(videoInfo, thumbnailInfo, ipfs) + // const formData = FormUtils.formToObj(new FormData(publishForm)) + + let tags: string[] = [] + if (publishFormTags) { + tags = publishFormTags.replace(/\s/g, '').split(',') + } + + console.log(`thumbnail info`, thumbnailInfo) + + const sourceMap = [] + if (thumbnailInfo.path) { + sourceMap.push({ + type: 'thumbnail', + url: `ipfs://${videoCid}/${thumbnailInfo.path}`, + }) + } + + if (videoInfo) { + sourceMap.push({ + type: 'video', + url: `ipfs://${videoCid}/${videoInfo.path}`, + format: 'm3u8', + }) + } + const permlink = `speak-${randomstring + .generate({ + length: 8, + charset: 'alphabetic', + }) + .toLowerCase()}` + // console.log(permlink) + console.log(`source map`) + console.log(sourceMap) + + setBlockedGlobalMessage('Publishing') + + const filesize = videoInfo.size + thumbnailInfo.size + + try { + const [reflink] = await AccountService.postComment({ + accountType: 'hive', + title: publishFormTitle || 'Untitled video', + body: publishFormDescription || '', + permlink, + tags, + json_metadata: { + title: publishFormTitle || 'Untitled video', + description: publishFormDescription || '', + tags, + sourceMap, + filesize, + created: new Date(), + lang: videoInfo.language, + video: { + duration: videoInfo.duration, + }, + app: '3speak/app-beta', + type: '3speak/video', + }, + }) + + setTimeout(() => { + location.hash = `#/watch/${reflink}` + setBlockedGlobalMessage('done') + }, 15000) + } catch (error) { + console.error(`Error in postComment operation ${error.message}`) + throw error + } +}; diff --git a/src/renderer/views/UploaderView/startEncode.ts b/src/renderer/views/UploaderView/startEncode.ts new file mode 100644 index 0000000..7015e32 --- /dev/null +++ b/src/renderer/views/UploaderView/startEncode.ts @@ -0,0 +1,93 @@ +import Fs from 'fs' +import PromiseIpc from 'electron-promise-ipc' +import { millisecondsAsString, secondsAsString } from '../../../common/utils/unit-conversion.functions' +import { NotificationManager } from 'react-notifications' +import { calculatePercentage } from './calculatePercentage' +import DateTime from 'date-and-time' +export const startEncode = async ({event, videoSourceFile, hwaccelOption, setEncodingInProgress, setStartTime, setEndTime, setProgress, setStatusInfo, setEstimatedTimeRemaining, setVideoInfo, setPublishReady, progress, statusInfo}) => { + event.preventDefault() + if (videoSourceFile === null) { + NotificationManager.error('No video source file selected') + return + } + if (!Fs.existsSync(videoSourceFile)) { + NotificationManager.error('Source file does not exist') + return + } + setEncodingInProgress(true) + const _startingTime = new Date().getTime() + setStartTime(_startingTime) + setEndTime(null) + + const jobInfo = (await PromiseIpc.send('encoder.createJob', { + sourceUrl: videoSourceFile, + profiles: [ + { + name: '1080p', + size: '1920x1080', + }, + { + name: '720p', + size: '1080x720', + }, + { + name: '480p', + size: '720x480', + }, + ], + options: { + hwaccel: + hwaccelOption && + hwaccelOption.length > 0 && + hwaccelOption !== '' && + hwaccelOption && + hwaccelOption !== 'none' + ? hwaccelOption + : undefined, + }, + } as any)) as any + NotificationManager.success('Encoding Started.') + + let savePct = 0 + const progressTrack = async () => { + const _timeNow = new Date().getTime() + const status = (await PromiseIpc.send('encoder.status', jobInfo.id)) as any + + console.log(`Encoder status: `, status) + + setProgress(status.progress || {}) + setStatusInfo(status) + + const val = status.progress.percent + // const diffPct = val - savePct + // savePct = val + // const pctPerSec = diffPct / 3 + // const totalTimeRemaining = (100 - val) / pctPerSec + const totalTimeRemaining = (100 * (_timeNow - _startingTime)) / val + setEstimatedTimeRemaining(millisecondsAsString(totalTimeRemaining)) + setEndTime(_timeNow) + } + + const pid = setInterval(progressTrack, 3000) + void progressTrack() + + const encodeOutput = (await PromiseIpc.send('encoder.getjoboutput', jobInfo.id)) as any + console.log(`got encode output`) + console.log(encodeOutput) + + setVideoInfo({ + size: encodeOutput.size, + cid: encodeOutput.ipfsHash, + path: encodeOutput.path, + duration: Number(DateTime.parse(encodeOutput.duration, 'hh:mm:ss.SS', true)) / 1000, + }) + + clearInterval(pid) + + setEncodingInProgress(false) + setEstimatedTimeRemaining(null) + setEndTime(new Date().getTime()) + setPublishReady(true) + + NotificationManager.success('Encoding complete.') +}; diff --git a/src/renderer/views/UploaderView/thumbnailSelect.ts b/src/renderer/views/UploaderView/thumbnailSelect.ts new file mode 100644 index 0000000..34bf995 --- /dev/null +++ b/src/renderer/views/UploaderView/thumbnailSelect.ts @@ -0,0 +1,34 @@ +export const thumbnailSelect = async ({e, thumbnailPreview, setThumbnailInfo, setVideoSourceFile, setLogData, ipfs, logData, videoInfo}) => { + console.log(`handling thumbnail selectr`) + + let file + if (e.target && e.target.files) { + file = e.target.files[0] + } else if (e.dataTransfer && e.dataTransfer.files) { + file = e.dataTransfer.files[0] + } + if (file) { + setVideoSourceFile(file.path) + setLogData([...logData, `Selected: ${videoInfo.path}`]) + } + const imgblob = URL.createObjectURL(file) + const size = file.size + + console.log(`uploading file with size ${size}`) + + thumbnailPreview.current = imgblob + + const fileDetails = { + path: e.target.files[0].name, + content: e.target.files[0], + } + + const ipfsOut = await ipfs.current.add(fileDetails, { pin: false }) + console.log(`setting thumbnail info to cid`, ipfsOut.cid.toString()) + + setThumbnailInfo({ + size, + path: `thumbnail.${file.type.split('/')[1]}`, + cid: ipfsOut.cid.toString(), + }) +}; \ No newline at end of file diff --git a/src/renderer/views/UploaderView/uploaderViewContent.tsx b/src/renderer/views/UploaderView/uploaderViewContent.tsx new file mode 100644 index 0000000..b011b88 --- /dev/null +++ b/src/renderer/views/UploaderView/uploaderViewContent.tsx @@ -0,0 +1,322 @@ +// uploaderViewContent.tsx +import React from 'react'; +import { Card, Col, Row, Button, Form, ProgressBar, Tabs, Tab } from 'react-bootstrap'; +import DefaultThumbnail from '../../assets/img/default-thumbnail.jpg' +import { millisecondsAsString } from '../../../common/utils/unit-conversion.functions' +const UploaderViewContent = ({ + videoSourceFile, + videoUpload, + handleVideoSelect, + setPublishFormTitle, + publishFormTitle, + setPublishFormDescription, + publishFormDescription, + setPublishFormTags, + publishFormTags, + thumbnailPreview, + thumbnailUpload, + progress, + handleThumbnailSelect, + handleStartEncode, + publish, + encodingInProgress, + publishReady, + normalizeSize, + calculatePercentage, + estimatedTimeRemaining, + endTime, + startTime, + hwaccelOption, + setHwaccelOption, + videoInfo, + thumbnailInfo, + logData, + statusInfo, + }) => { + return ( +
+ +
+
videoUpload.current.click()} + style={{ + width: '4000px', + textAlign: 'center', + height: '150px', + fontSize: '16px', + fontWeight: 'bold', + cursor: 'pointer', + }} + onDragOver={(e) => e.preventDefault()} + onDrop={handleVideoSelect} + > + Drop a file or click to start the upload
+

+ Selected: {videoSourceFile} +

+ +
+
+
+ + +
+
+ + Title + setPublishFormTitle(e.target.value)} + value={publishFormTitle} + type="text" + name="title" + > + + + Description + + + + Tags + setPublishFormTags(e.target.value)} + value={publishFormTags} + type="text" + name="tags" + > + + Separate multiple tags with a ,{' '} + + + + Language + + + + + + + + Thumbnail +
+ thumbnailUpload.current.click()} + onDragOver={(e) => e.preventDefault()} + onDrop={handleThumbnailSelect} + /> + +

Click the thumbnail to change it

+

Recommended 5MB. Ideally 1280px×720px.

+
+ + +
+
+ + + + Encoder status + + This area will show live encoding statistics + {' '} +
+ + +
+ Time Remaining:{' '} + {estimatedTimeRemaining !== 'NaNns' ? estimatedTimeRemaining : 'Calculating'} +
+
+ Total Time (so far):{' '} + {endTime !== 0 ? millisecondsAsString(endTime - startTime) : 'Calculating'} +
+
+
+
+
+
Control Panel
+
+ + + + + Format + + + + + + Hardware Accel + + + Use hardware acceleration to speed up video encode. Not available on all + systems, results may vary. + + + + + + + Video IpfsPath + + + + Thumbnail IpfsPath + + + + Total Size + + + + {/* + + + + + + + + + + + + + + + +
EnabledIDUsername
Hivevaultec
+ +
*/} + + + + +
+
+ +
+
+ ) +} +export default UploaderViewContent; diff --git a/src/renderer/views/UploaderView/videoSelect.ts b/src/renderer/views/UploaderView/videoSelect.ts new file mode 100644 index 0000000..9e36a7d --- /dev/null +++ b/src/renderer/views/UploaderView/videoSelect.ts @@ -0,0 +1,12 @@ +export const videoSelect = (e, setVideoSourceFile, setLogData, logData, videoInfo) => { + let file + if (e.target && e.target.files) { + file = e.target.files[0] + } else if (e.dataTransfer && e.dataTransfer.files) { + file = e.dataTransfer.files[0] + } + if (file) { + setVideoSourceFile(file.path) + setLogData([...logData, `Selected: ${videoInfo.path}`]) + } +}; diff --git a/src/renderer/views/UserView.tsx b/src/renderer/views/UserView.tsx index 38796c9..a095600 100644 --- a/src/renderer/views/UserView.tsx +++ b/src/renderer/views/UserView.tsx @@ -1,102 +1,13 @@ -import React, { useEffect, useMemo, useState } from 'react' -import { Navbar, Nav, Card, Col, Row, Button } from 'react-bootstrap' -import RefLink from '../../main/RefLink' -import { Switch, Route } from 'react-router-dom' -import '../css/User.css' -import ReactMarkdown from 'react-markdown' -import { AccountService } from '../services/account.service' -import { GridFeedView } from './GridFeedView' -import { FollowWidget } from '../components/widgets/FollowWidget' -import { IndexerClient } from '../App' -import { gql, useQuery } from '@apollo/client' - -const QUERY = gql` - -query Query($author: String) { - -latestFeed(author:$author, limit: 15) { - items { - ... on CeramicPost { - stream_id - version_id - parent_id - title - body - json_metadata - app_metadata - } - ... on HivePost { - created_at - updated_at - parent_author - parent_permlink - permlink - author - title - body - lang - post_type - app - tags - json_metadata - app_metadata - community_ref - - three_video - - children { - parent_author - parent_permlink - permlink - title - body - title - lang - post_type - app - json_metadata - app_metadata - community_ref - } - } - __typename - } - } -} - -` - -function transformGraphqlToNormal(data) { - - let blob = [] - for(let video of data) { - console.log(video) - blob.push({ - created: new Date(video.created_at), - author: video.author, - permlink: video.permlink, - tags: video.tags, - title: video.title, - duration: video.json_metadata.video.info.duration || video.json_metadata.video.duration, - //isIpfs: val.json_metadata.video.info.ipfs || thumbnail ? true : false, - //ipfs: val.json_metadata.video.info.ipfs, - isIpfs: true, - images: { - thumbnail: video.three_video.thumbnail_url.replace('img.3speakcontent.co', 'media.3speak.tv'), - poster: video.three_video.thumbnail, - post: video.three_video.thumbnail, - ipfs_thumbnail: video.three_video.thumbnail - /*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`,*/ - }, - }) - } - return blob; -} +// filename: UserView.tsx +import React, { useEffect, useMemo, useState } from 'react'; +import RefLink from '../../main/RefLink'; +import '../css/User.css'; +import { AccountService } from '../services/account.service'; +import { IndexerClient } from '../App'; +import { useQuery } from '@apollo/client'; +import UserViewContent from './UserView/UserViewContent'; +import { QUERY } from './UserView/userQueries'; +import { transformGraphqlToNormal } from './UserView/userUtils'; /** * User about page with all the public information a casual and power user would need to see about another user. */ @@ -107,7 +18,6 @@ export function UserView(props: any) { const [coverUrl, setCoverUrl] = useState('') const [profileUrl, setProfileUrl] = useState('') - const reflink = useMemo(() => { return RefLink.parse(props.match.params.reflink) }, [props.match]) @@ -126,12 +36,9 @@ export function UserView(props: any) { console.log(data) const videos = data?.latestFeed?.items || []; - - useEffect(() => { const load = async () => { const accountBalances = await AccountService.getAccountBalances(reflink) - setProfileUrl(await AccountService.getProfilePictureURL(reflink)) setProfileAbout(await AccountService.getProfileAbout(reflink)) setHiveBalance(accountBalances.hive) @@ -143,80 +50,14 @@ export function UserView(props: any) { }, [reflink]) return ( -
-
- -
- -
-
-
- - {username} - - - -
- -
-
-
-
- - -
- -
-
- - - - - - {hiveBalance} - - - Available HIVE Balance - - - - - - - {hbdBalance} - - - Available HBD Balance - - - - - - - {profileAbout} - -
-
- ) + + ); } diff --git a/src/renderer/views/UserView/UserViewContent.tsx b/src/renderer/views/UserView/UserViewContent.tsx new file mode 100644 index 0000000..cd47137 --- /dev/null +++ b/src/renderer/views/UserView/UserViewContent.tsx @@ -0,0 +1,97 @@ +// UserViewContent.tsx +import React from 'react'; +import { Navbar, Nav, Card, Col, Row, Button } from 'react-bootstrap'; +import { Switch, Route } from 'react-router-dom'; +import ReactMarkdown from 'react-markdown'; +import { GridFeedView } from './../GridFeedView'; +import { FollowWidget } from '../../components/widgets/FollowWidget'; + +const UserViewContent = ({ + coverUrl, + profileUrl, + username, + reflink, + hiveBalance, + hbdBalance, + profileAbout, + }) => { + return ( +
+
+ +
+ +
+
+
+ + {username} + + + +
+ +
+
+
+
+ + +
+ +
+
+ + + + + + {hiveBalance} + + + Available HIVE Balance + + + + + + + {hbdBalance} + + + Available HBD Balance + + + + + + + {profileAbout} + +
+
+ ); +}; + +export default UserViewContent; diff --git a/src/renderer/views/UserView/userQueries.ts b/src/renderer/views/UserView/userQueries.ts new file mode 100644 index 0000000..99a364f --- /dev/null +++ b/src/renderer/views/UserView/userQueries.ts @@ -0,0 +1,59 @@ +// userQueries.ts +import { gql } from '@apollo/client'; + +export const QUERY = gql` + +query Query($author: String) { + +latestFeed(author:$author, limit: 15) { + items { + ... on CeramicPost { + stream_id + version_id + parent_id + title + body + json_metadata + app_metadata + } + ... on HivePost { + created_at + updated_at + parent_author + parent_permlink + permlink + author + title + body + lang + post_type + app + tags + json_metadata + app_metadata + community_ref + + three_video + + children { + parent_author + parent_permlink + permlink + title + body + title + lang + post_type + app + json_metadata + app_metadata + community_ref + } + } + __typename + } + } +} + +` +; \ No newline at end of file diff --git a/src/renderer/views/UserView/userUtils.ts b/src/renderer/views/UserView/userUtils.ts new file mode 100644 index 0000000..4a5c5f7 --- /dev/null +++ b/src/renderer/views/UserView/userUtils.ts @@ -0,0 +1,33 @@ +// userUtils.ts +export function transformGraphqlToNormal(data: any) { + + let blob = [] + for(let video of data) { + console.log(video) + blob.push({ + created: new Date(video.created_at), + author: video.author, + permlink: video.permlink, + tags: video.tags, + title: video.title, + duration: video.json_metadata.video.info.duration || video.json_metadata.video.duration, + //isIpfs: val.json_metadata.video.info.ipfs || thumbnail ? true : false, + //ipfs: val.json_metadata.video.info.ipfs, + isIpfs: true, + images: { + thumbnail: video.three_video.thumbnail_url.replace('img.3speakcontent.co', 'media.3speak.tv'), + poster: video.three_video.thumbnail, + post: video.three_video.thumbnail, + ipfs_thumbnail: video.three_video.thumbnail + /*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`,*/ + }, + }) + } + return blob; + } + diff --git a/src/renderer/views/WatchView.tsx b/src/renderer/views/WatchView.tsx index fba06a0..f44c864 100644 --- a/src/renderer/views/WatchView.tsx +++ b/src/renderer/views/WatchView.tsx @@ -1,100 +1,44 @@ -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') +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { Dropdown } from 'react-bootstrap'; +import RefLink from '../../main/RefLink'; +import EmptyProfile from '../assets/img/EmptyProfile.png'; +import { IPFS_HOST } from '../../common/constants'; +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'; +import { WatchViewContent } from './WatchView/WatchViewContent'; +import { DHTProviders } from '../../components/DHTProviders'; +import { CustomToggle } from '../../components/CustomToggle'; +import ArraySearch from 'arraysearch'; +import * as IPFSHTTPClient from 'ipfs-http-client'; +import { generalFetch } from './WatchView/watchViewHelpers/generalFetch'; +import { mountPlayer } from './WatchView/watchViewHelpers/mountPlayer'; +import { recordView } from './WatchView/watchViewHelpers/recordView'; +import { gearSelect } from './WatchView/watchViewHelpers/gearSelect'; +import { retrieveRecommended } from './WatchView/watchViewHelpers/retrieveRecommended'; +import { PinLocally } from './WatchView/watchViewHelpers/PinLocally'; +import { showDebug } from './WatchView/watchViewHelpers/showDebug'; 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 ( -
- DHT Providers {peers} -
- ) -} - -const CustomToggle = React.forwardRef(({ children, onClick }, ref) => ( - -)) export function WatchView(props: any) { const player = useRef() const [videoInfo, setVideoInfo] = useState({}) const [postInfo, setPostInfo] = useState({}) 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 [rootCid, setRootCid] = useState(''); const reflink = useMemo(() => { return props.match.params.reflink @@ -103,395 +47,65 @@ export function WatchView(props: any) { 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 {} - } - // console.log('video_info', info) - // console.log(cids) - 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: ( -
- - - - - -
- ), - 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() + await generalFetch( + reflink, + setVideoInfo, + setPostInfo, + setProfilePictureUrl, + setRootCid + ); + setLoadingMessage('Loading: Mounting player...'); + await mountPlayer(reflink, setVideoLink, recordView); } catch (ex) { - console.log(ex) - setLoadingMessage('Loading resulted in error') - throw ex + console.log(ex); + setLoadingMessage('Loading resulted in error'); + throw ex; } - setLoaded(true) - await retrieveRecommended() - } - - void load() - }, []) + setLoaded(true); + await retrieveRecommended(postInfo, setRecommendedVideos); + }; + void load(); + }, []); useEffect(() => { - window.scrollTo(0, 0) - + window.scrollTo(0, 0); const update = async () => { - await generalFetch() - await mountPlayer() - await retrieveRecommended() - player.current?.ExecUpdate() - } - - void update() - }, [reflink]) + await generalFetch(reflink, setVideoInfo, setPostInfo, setProfilePictureUrl, setRootCid); + await mountPlayer(reflink, setVideoLink, recordView); + await retrieveRecommended(postInfo, setRecommendedVideos); + player.current?.ExecUpdate(); + }; + console.log('Updating...'); + void update(); + console.log('Updated'); + }, [reflink]); return ( -
- {loaded ? ( - - {/* */} - {/* */} - - -
- -
-
-
-

- {videoInfo.title} -

- -
-
- - - - - - - -

Mute Post

-
- -

Mute User

-
-
-
-
-
-
- - -

- - {postInfo.author} - -

- - Published on{' '} - {(() => { - const pattern = DateTime.compile('MMMM D, YYYY') - return DateTime.format(new Date(videoInfo.creation), pattern) - })()} - -
-
-
About :
- - {DOMPurify.sanitize(videoInfo.description)} -
- - showDebug()} - > - Debug Info - - -
-
Tags:
-

- {(() => { - const out = [] - if (videoInfo.tags) { - for (const tag of videoInfo.tags) { - out.push( - - {tag} - , - ) - } - } - return out - })()} -

-
- - - - - - {recommendedVideos.map((value) => ( - - ))} - - - -
-
- ) : ( -
- -
-

{loadingMessage}

-
-
- )} -
+ PinLocally(videoInfo, reflink)} + showDebug={showDebug} + DHTProviders={DHTProviders} + VoteWidget={VoteWidget} + FollowWidget={FollowWidget} + CollapsibleText={CollapsibleText} + CommentSection={CommentSection} + VideoTeaser={VideoTeaser} + CustomToggle={CustomToggle} + Dropdown={Dropdown} + gearSelect={gearSelect} + /> ) } diff --git a/src/renderer/views/WatchView/WatchViewContent.tsx b/src/renderer/views/WatchView/WatchViewContent.tsx new file mode 100644 index 0000000..3414e20 --- /dev/null +++ b/src/renderer/views/WatchView/WatchViewContent.tsx @@ -0,0 +1,195 @@ +import React from 'react'; +import { Container, Col, Row } from 'react-bootstrap'; +import { FollowWidget } from '../../components/widgets/FollowWidget' +import { FaDownload } from 'react-icons/fa' +import { CollapsibleText } from '../../components/CollapsibleText' +import ReactMarkdown from 'react-markdown' +import { BsInfoSquare } from 'react-icons/bs' +import { CommentSection } from '../../components/video/CommentSection' +import { VideoTeaser } from '../../components/video/VideoTeaser' +import { Player } from '../../components/video/Player'; +import { LoopCircleLoading } from 'react-loadingg' +import DateTime from 'date-and-time' +import DOMPurify from 'dompurify' +export const WatchViewContent = (props: any) => { + const { + loaded, + videoInfo, + postInfo, + profilePictureURL, + rootCid, + reflinkParsed, + recommendedVideos, + PinLocally, + showDebug, + DHTProviders, + VoteWidget, + FollowWidget, + CollapsibleText, + CommentSection, + VideoTeaser, + CustomToggle, + Dropdown, + gearSelect, + loadingMessage, + reflink, + Finder, + } = props; + + return ( +
+ {loaded ? ( + + + +
+ +
+
+
+

+ {videoInfo.title} +

+ +
+
+ + + + + + + +

Mute Post

+
+ +

Mute User

+
+
+
+
+
+
+ + +

+ + {postInfo.author} + +

+ + Published on{' '} + {(() => { + const pattern = DateTime.compile('MMMM D, YYYY') + return DateTime.format(new Date(videoInfo.creation), pattern) + })()} + +
+
+
About :
+ + {DOMPurify.sanitize(videoInfo.description)} +
+ + showDebug()} + > + Debug Info + + +
+
Tags:
+

+ {(() => { + const out = [] + if (videoInfo.tags) { + for (const tag of videoInfo.tags) { + out.push( + + {tag} + , + ) + } + } + return out + })()} +

+
+ + + + + + {recommendedVideos.map((value) => ( + + ))} + + + +
+
+ ) : ( +
+ +
+

{loadingMessage}

+
+
+ )} +
+ ); +}; diff --git a/src/renderer/views/WatchView/watchViewHelpers/PinLocally.ts b/src/renderer/views/WatchView/watchViewHelpers/PinLocally.ts new file mode 100644 index 0000000..a8558f1 --- /dev/null +++ b/src/renderer/views/WatchView/watchViewHelpers/PinLocally.ts @@ -0,0 +1,34 @@ +import { URL } from 'url'; +import PromiseIpc from 'electron-promise-ipc'; +import CID from 'cids'; +import { NotificationManager } from 'react-notifications'; + +export async function PinLocally(videoInfo: any, reflink: string) { + const cids = [] + for (const source of videoInfo.sources) { + const url = new URL(source.url) + try { + new CID(url.host) + cids.push(url.host) + } catch {} + } + + 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') + } +} \ No newline at end of file diff --git a/src/renderer/views/WatchView/watchViewHelpers/gearSelect.ts b/src/renderer/views/WatchView/watchViewHelpers/gearSelect.ts new file mode 100644 index 0000000..23b2f8a --- /dev/null +++ b/src/renderer/views/WatchView/watchViewHelpers/gearSelect.ts @@ -0,0 +1,17 @@ +import PromiseIpc from 'electron-promise-ipc'; + +export async function gearSelect(eventKey: string, reflinkParsed: any) { + 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; + } + } +} diff --git a/src/renderer/views/WatchView/watchViewHelpers/generalFetch.ts b/src/renderer/views/WatchView/watchViewHelpers/generalFetch.ts new file mode 100644 index 0000000..ab571a3 --- /dev/null +++ b/src/renderer/views/WatchView/watchViewHelpers/generalFetch.ts @@ -0,0 +1,32 @@ +import { AccountService } from '../../../services/account.service'; +import CID from 'cids' +import { URL } from 'url' + +export async function generalFetch( + reflink: string, + setVideoInfo: (info: any) => void, + setPostInfo: (info: any) => void, + setProfilePictureUrl: (url: string) => void, + setRootCid: (cid: string) => void +) { + 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]) +} \ No newline at end of file diff --git a/src/renderer/views/WatchView/watchViewHelpers/mountPlayer.ts b/src/renderer/views/WatchView/watchViewHelpers/mountPlayer.ts new file mode 100644 index 0000000..4bc1033 --- /dev/null +++ b/src/renderer/views/WatchView/watchViewHelpers/mountPlayer.ts @@ -0,0 +1,15 @@ +import { VideoService } from '../../../services/video.service'; + +export async function mountPlayer(reflink: string, setVideoLink: (link: string) => void, recordView: () => void) { + try { + const playerType = 'standard'; + switch (playerType) { + case 'standard': { + setVideoLink(await VideoService.getVideoSourceURL(reflink)); + } + } + recordView(); + } catch (ex) { + console.error(ex); + } +} diff --git a/src/renderer/views/WatchView/watchViewHelpers/recordView.ts b/src/renderer/views/WatchView/watchViewHelpers/recordView.ts new file mode 100644 index 0000000..8e254ac --- /dev/null +++ b/src/renderer/views/WatchView/watchViewHelpers/recordView.ts @@ -0,0 +1,3 @@ +export async function recordView() { + return +} \ No newline at end of file diff --git a/src/renderer/views/WatchView/watchViewHelpers/retrieveRecommended.ts b/src/renderer/views/WatchView/watchViewHelpers/retrieveRecommended.ts new file mode 100644 index 0000000..8225d9a --- /dev/null +++ b/src/renderer/views/WatchView/watchViewHelpers/retrieveRecommended.ts @@ -0,0 +1,71 @@ +import { knex } from '../../../singletons/knex.singleton'; +import PromiseIpc from 'electron-promise-ipc'; +import ArraySearch from 'arraysearch'; +const Finder = ArraySearch.Finder; + +export async function retrieveRecommended( + postInfo: any, + setRecommendedVideos: (videos: any[]) => void +) { + 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); + }); +} diff --git a/src/renderer/views/WatchView/watchViewHelpers/showDebug.tsx b/src/renderer/views/WatchView/watchViewHelpers/showDebug.tsx new file mode 100644 index 0000000..07e185c --- /dev/null +++ b/src/renderer/views/WatchView/watchViewHelpers/showDebug.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Tab, Tabs } from 'react-bootstrap'; +import ace from 'brace'; +import Popup from 'react-popup'; +import { JsonEditor as Editor } from 'jsoneditor-react'; + +export function showDebug(videoInfo: any) { + const metadata = videoInfo; + Popup.registerPlugin('watch_debug', async function () { + this.create({ + content: ( +
+ + + + + +
+ ), + buttons: { + right: [ + { + text: 'Close', + className: 'success', + action: function () { + Popup.close(); + }, + }, + ], + }, + }); + }); + Popup.plugins().watch_debug(); +}