├── media ├── .ignore └── media1.PNG ├── .npmrc ├── src ├── module-declarations.d.ts ├── renderer │ ├── assets │ │ ├── locales │ │ │ ├── index.js │ │ │ └── en.js │ │ └── img │ │ │ ├── 404.png │ │ │ ├── app.icns │ │ │ ├── app.ico │ │ │ ├── app.png │ │ │ ├── blog.png │ │ │ ├── cover.png │ │ │ ├── nsfw.png │ │ │ ├── EmptyProfile.png │ │ │ ├── default-thumbnail.jpg │ │ │ ├── index.d.ts │ │ │ ├── play.svg │ │ │ ├── icon_leaderboard.svg │ │ │ ├── icon_new_content.svg │ │ │ ├── icon_home.svg │ │ │ ├── icon_newcomer.svg │ │ │ ├── hive.svg │ │ │ ├── icon_trend.svg │ │ │ ├── icon_livestream.svg │ │ │ ├── ipfs-logo-vector-ice.svg │ │ │ └── 3S_logo.svg │ ├── views │ │ ├── WatchView │ │ │ └── watchViewHelpers │ │ │ │ ├── recordView.ts │ │ │ │ ├── mountPlayer.ts │ │ │ │ ├── gearSelect.ts │ │ │ │ ├── showDebug.tsx │ │ │ │ ├── PinLocally.ts │ │ │ │ ├── generalFetch.ts │ │ │ │ └── retrieveRecommended.ts │ │ ├── UploaderView │ │ │ ├── calculatePercentage.ts │ │ │ ├── normalizeSize.ts │ │ │ ├── videoSelect.ts │ │ │ ├── compileVideoCid.ts │ │ │ ├── thumbnailSelect.ts │ │ │ ├── publish.ts │ │ │ └── startEncode.ts │ │ ├── PinsView │ │ │ ├── CustomToggle.tsx │ │ │ ├── CustomMenu.tsx │ │ │ ├── PinCids.tsx │ │ │ └── PinRows.tsx │ │ ├── Uploader.css │ │ ├── CreatorStudioView.tsx │ │ ├── NotFoundView.tsx │ │ ├── TagView.tsx │ │ ├── PoAView │ │ │ ├── usePoAInstaller.ts │ │ │ ├── PoAStateContext.tsx │ │ │ ├── PoAProgramRunnerContext.tsx │ │ │ └── usePoAProgramRunner.ts │ │ ├── UserView │ │ │ ├── userQueries.ts │ │ │ ├── userUtils.ts │ │ │ └── UserViewContent.tsx │ │ ├── CommunitiesView.tsx │ │ ├── PinsView.tsx │ │ ├── GridFeed │ │ │ └── grid-feed-query.service.ts │ │ ├── BlocklistView.tsx │ │ ├── PoAView.tsx │ │ ├── UserView.tsx │ │ ├── IpfsConsoleView │ │ │ └── IpfsStatsView.tsx │ │ ├── LeaderboardView.tsx │ │ ├── IpfsConsoleView.tsx │ │ ├── Accounts.tsx │ │ ├── WatchView.tsx │ │ └── UploaderView.tsx │ ├── services │ │ ├── accountServices │ │ │ ├── getAccount.ts │ │ │ ├── getAccounts.ts │ │ │ ├── convertLight.ts │ │ │ ├── logout.ts │ │ │ ├── permalinkToPostInfo.ts │ │ │ ├── getFollowerCount.ts │ │ │ ├── getProfilePictureURL.ts │ │ │ ├── getAccountMetadata.ts │ │ │ ├── voteHandler.ts │ │ │ ├── createPost.ts │ │ │ ├── getAccountBalances.ts │ │ │ ├── getProfileBackgroundImageUrl.ts │ │ │ ├── postCustomJson.ts │ │ │ ├── updateMeta.ts │ │ │ ├── getProfileAbout.ts │ │ │ ├── getFollowing.ts │ │ │ ├── followHandler.ts │ │ │ ├── login.ts │ │ │ └── postComment.ts │ │ ├── ipfs.service.ts │ │ ├── account.service.ts │ │ └── peer.service.ts │ ├── components │ │ ├── video │ │ │ ├── CommentSection │ │ │ │ ├── NoComments.tsx │ │ │ │ ├── ConnectAccountNotice.tsx │ │ │ │ ├── CommentForm.tsx │ │ │ │ └── CommentSection.css │ │ │ ├── PostComment │ │ │ │ └── CommentForm.tsx │ │ │ ├── VideoTeaser.tsx │ │ │ ├── CommentSection.tsx │ │ │ ├── Player.tsx │ │ │ └── VideoWidget.tsx │ │ ├── LoadingMessage.tsx │ │ ├── CollapsibleText.tsx │ │ ├── widgets │ │ │ ├── CommunityTile.tsx │ │ │ ├── LeaderTile.tsx │ │ │ └── FollowWidget.tsx │ │ └── Navbar.css │ ├── css │ │ ├── Startup.css │ │ ├── User.css │ │ └── App.css │ ├── singletons │ │ ├── hive-client.singleton.ts │ │ └── knex.singleton.ts │ ├── renderer_utils.ts │ ├── index.html │ ├── i18n.ts │ ├── index.tsx │ ├── StartUp.tsx │ └── App.tsx ├── common │ ├── constants.ts │ ├── utils │ │ ├── delay.function.ts │ │ ├── ipfs-utils.ts │ │ └── unit-conversion.functions.ts │ └── models │ │ ├── hive.model.ts │ │ ├── comments.model.ts │ │ └── video.model.ts ├── consts.ts ├── main │ ├── core │ │ ├── utils.ts │ │ ├── components │ │ │ ├── index.ts │ │ │ ├── Blocklist.ts │ │ │ ├── ProgramRunner.ts │ │ │ ├── Logger.ts │ │ │ ├── AccountSystem.ts │ │ │ └── Config.ts │ │ └── ffmpeg_helper.ts │ ├── RefLink.ts │ ├── AutoUpdater.ts │ ├── AutoUpdaterPoA.ts │ ├── index.ts │ └── ipcAdapter.ts └── components │ ├── CustomToggle.tsx │ └── DHTProviders.tsx ├── .prettierrc ├── tsconfig.release.json ├── tsconfig.json ├── .gitignore ├── .babelrc ├── scripts └── prepare_ci.js ├── webpack ├── renderer.prod.config.js ├── main.config.js ├── renderer.dev.config.js └── renderer.base.config.js ├── .github └── workflows │ ├── nodejs_ci.yml │ └── build.yml ├── README.MD ├── .eslintrc.js └── test └── main.core.ts /media/.ignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /src/module-declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'electron-promise-ipc' 2 | -------------------------------------------------------------------------------- /media/media1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spknetwork/3Speak-app/HEAD/media/media1.PNG -------------------------------------------------------------------------------- /src/renderer/assets/locales/index.js: -------------------------------------------------------------------------------- 1 | import en from './en' 2 | export default { 3 | en 4 | } -------------------------------------------------------------------------------- /src/renderer/views/WatchView/watchViewHelpers/recordView.ts: -------------------------------------------------------------------------------- 1 | export async function recordView() { 2 | return 3 | } -------------------------------------------------------------------------------- /src/renderer/assets/img/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spknetwork/3Speak-app/HEAD/src/renderer/assets/img/404.png -------------------------------------------------------------------------------- /src/renderer/assets/img/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spknetwork/3Speak-app/HEAD/src/renderer/assets/img/app.icns -------------------------------------------------------------------------------- /src/renderer/assets/img/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spknetwork/3Speak-app/HEAD/src/renderer/assets/img/app.ico -------------------------------------------------------------------------------- /src/renderer/assets/img/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spknetwork/3Speak-app/HEAD/src/renderer/assets/img/app.png -------------------------------------------------------------------------------- /src/renderer/assets/img/blog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spknetwork/3Speak-app/HEAD/src/renderer/assets/img/blog.png -------------------------------------------------------------------------------- /src/renderer/assets/img/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spknetwork/3Speak-app/HEAD/src/renderer/assets/img/cover.png -------------------------------------------------------------------------------- /src/renderer/assets/img/nsfw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spknetwork/3Speak-app/HEAD/src/renderer/assets/img/nsfw.png -------------------------------------------------------------------------------- /src/renderer/assets/locales/en.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * Insert text tag -> translated text here 4 | */ 5 | } -------------------------------------------------------------------------------- /src/renderer/assets/img/EmptyProfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spknetwork/3Speak-app/HEAD/src/renderer/assets/img/EmptyProfile.png -------------------------------------------------------------------------------- /src/common/constants.ts: -------------------------------------------------------------------------------- 1 | export const IPFS_SELF_MULTIADDR = '/ip4/127.0.0.1/tcp/5004' 2 | export const IPFS_HOST = 'http://127.0.0.1:5004' 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/assets/img/default-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spknetwork/3Speak-app/HEAD/src/renderer/assets/img/default-thumbnail.jpg -------------------------------------------------------------------------------- /tsconfig.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["node_modules", "**/*.spec.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /src/common/utils/delay.function.ts: -------------------------------------------------------------------------------- 1 | export async function delay(ms: number): Promise { 2 | return new Promise((resolve) => setTimeout(resolve, ms)) 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "exclude": ["node_modules", "**/*.spec.ts", "scripts"], 4 | "include": ["src/**/*", "test/**/*"] 5 | } 6 | -------------------------------------------------------------------------------- /src/consts.ts: -------------------------------------------------------------------------------- 1 | export const HIVESQL_USERNAME = process.env.HIVESQL_USERNAME || 'HIVESQL_USERNAME_FILLIN' 2 | export const HIVESQL_PASSWORD = process.env.HIVESQL_PASSWORD || 'HIVESQL_PASSWORD_FILLIN' 3 | -------------------------------------------------------------------------------- /src/renderer/views/UploaderView/calculatePercentage.ts: -------------------------------------------------------------------------------- 1 | export const calculatePercentage = (progress, statusInfo) => { 2 | return progress.percent / statusInfo.nstages + statusInfo.stage * (100 / statusInfo.nstages); 3 | }; -------------------------------------------------------------------------------- /src/common/models/hive.model.ts: -------------------------------------------------------------------------------- 1 | export interface HiveInfo { 2 | privateKeys: { 3 | posting_key: string 4 | } 5 | public: { 6 | pubwif: string 7 | } 8 | type: string 9 | username: string 10 | } 11 | -------------------------------------------------------------------------------- /src/renderer/services/accountServices/getAccount.ts: -------------------------------------------------------------------------------- 1 | import PromiseIPC from 'electron-promise-ipc' 2 | 3 | export async function getAccount(profileID) { 4 | const getAccount = await PromiseIPC.send('accounts.get', profileID) 5 | return getAccount 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/services/accountServices/getAccounts.ts: -------------------------------------------------------------------------------- 1 | import PromiseIPC from 'electron-promise-ipc' 2 | 3 | export async function getAccounts() { 4 | const getAccounts = await PromiseIPC.send('accounts.ls', {} as any) 5 | 6 | return getAccounts 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Visiual Studio code 2 | .vscode/ 3 | 4 | # Webstorm 5 | .idea/ 6 | 7 | # OSX 8 | .DS_Store 9 | 10 | # Dependency directory 11 | node_modules/ 12 | 13 | # build artifacts 14 | dist/ 15 | 16 | build/ 17 | 18 | .env 19 | 20 | temp 21 | .aider* 22 | -------------------------------------------------------------------------------- /src/renderer/assets/img/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const value: any 3 | export = value 4 | } 5 | 6 | declare module '*.png' { 7 | const value: any 8 | export = value 9 | } 10 | 11 | declare module '*.jpg' { 12 | const value: any 13 | export = value 14 | } 15 | -------------------------------------------------------------------------------- /src/common/models/comments.model.ts: -------------------------------------------------------------------------------- 1 | export interface CommentOp { 2 | accountType: string 3 | body: any 4 | parent_author?: string 5 | parent_permlink?: string 6 | username?: string 7 | permlink: string 8 | title: string 9 | json_metadata: any 10 | tags?: string[] 11 | } 12 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | ["@babel/plugin-transform-runtime", 9 | { 10 | "regenerator":true 11 | } 12 | ] 13 | ] 14 | } -------------------------------------------------------------------------------- /src/renderer/views/UploaderView/normalizeSize.ts: -------------------------------------------------------------------------------- 1 | import { 2 | bytesAsString, 3 | } from '../../../common/utils/unit-conversion.functions' 4 | export const normalizeSize = (videoInfo, thumbnailInfo) => { 5 | const size = videoInfo.size + thumbnailInfo.size; 6 | return bytesAsString(size); 7 | }; 8 | -------------------------------------------------------------------------------- /src/renderer/services/accountServices/convertLight.ts: -------------------------------------------------------------------------------- 1 | export async function convertLight(val) { 2 | if (typeof val.json_metadata === 'object') { 3 | val.json_metadata = JSON.parse(val.json_metadata) 4 | } 5 | if (!val.json_metadata.video) { 6 | val.json_metadata.video = { 7 | info: {}, 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/renderer/components/video/CommentSection/NoComments.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export function NoComments() { 4 | return ( 5 |
6 | This video has no comments yet. To write a comment login and click the "Reply" button below 7 | the video player. 8 |
9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/renderer/services/accountServices/logout.ts: -------------------------------------------------------------------------------- 1 | import hive from '@hiveio/hive-js' 2 | import PromiseIPC from 'electron-promise-ipc' 3 | import ArraySearch from 'arraysearch' 4 | 5 | const Finder = ArraySearch.Finder 6 | 7 | export async function logout(profileID) { 8 | await PromiseIPC.send('accounts.deleteProfile', profileID) 9 | return 10 | } -------------------------------------------------------------------------------- /src/renderer/css/Startup.css: -------------------------------------------------------------------------------- 1 | .modal-open.modal { 2 | overflow-x: hidden; 3 | overflow-y: auto; 4 | } 5 | .start-backdrop.show { 6 | opacity: .98; 7 | } 8 | .start-backdrop { 9 | position: fixed; 10 | top: 0; 11 | left: 0; 12 | z-index: 1040; 13 | width: 100vw; 14 | height: 100vh; 15 | background-color: white; 16 | } -------------------------------------------------------------------------------- /src/renderer/singletons/hive-client.singleton.ts: -------------------------------------------------------------------------------- 1 | import { Client } from '@hiveio/dhive' 2 | 3 | import hive from '@hiveio/hive-js' 4 | import { promisify } from 'util' 5 | 6 | hive.broadcast.comment = promisify(hive.broadcast.comment) 7 | 8 | export const hiveClient = new Client([ 9 | 'https://anyx.io', 10 | 'https://api.openhive.network', 11 | ]) 12 | -------------------------------------------------------------------------------- /src/main/core/utils.ts: -------------------------------------------------------------------------------- 1 | const Path = require('path') 2 | const os = require('os') 3 | 4 | function getRepoPath() { 5 | let appPath 6 | if (process.env.speak_path) { 7 | appPath = process.env.speak_path 8 | } else { 9 | appPath = Path.join(os.homedir(), '.blasio') 10 | } 11 | return appPath 12 | } 13 | module.exports = { 14 | getRepoPath, 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/assets/img/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/renderer/assets/img/icon_leaderboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/renderer/assets/img/icon_new_content.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /scripts/prepare_ci.js: -------------------------------------------------------------------------------- 1 | // Do NOT execute this file in dev mode 2 | const fs = require('fs') 3 | const workPath = 'src/consts.ts' 4 | 5 | let file = fs.readFileSync(workPath).toString() 6 | file = file.replace('HIVESQL_USERNAME_FILLIN', `${process.env.hivesql_username}`) 7 | file = file.replace('HIVESQL_PASSWORD_FILLIN', `${process.env.hivesql_password}`) 8 | fs.writeFileSync(workPath, file) 9 | -------------------------------------------------------------------------------- /src/renderer/assets/img/icon_home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/renderer/renderer_utils.ts: -------------------------------------------------------------------------------- 1 | import hive from '@hiveio/hive-js' 2 | import { promisify } from 'util' 3 | 4 | hive.broadcast.comment = promisify(hive.broadcast.comment) 5 | 6 | export class FormUtils { 7 | static formToObj(formData: any): any { 8 | const out = {} 9 | for (const key of formData.keys()) { 10 | out[key] = formData.get(key) 11 | } 12 | return out 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/core/components/index.ts: -------------------------------------------------------------------------------- 1 | import Blocklist from './Blocklist' 2 | import Config from './Config' 3 | import Pins from './Pins' 4 | import { MakeLogger } from './Logger' 5 | import { EncoderService } from './EncoderService' 6 | import AccountSystem from './AccountSystem' 7 | export default { 8 | Blocklist, 9 | AccountSystem, 10 | Config, 11 | Pins, 12 | MakeLogger, 13 | EncoderService, 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/singletons/knex.singleton.ts: -------------------------------------------------------------------------------- 1 | import Knex from 'knex' 2 | import { HIVESQL_PASSWORD, HIVESQL_USERNAME } from '../../consts' 3 | 4 | export const knex = Knex({ 5 | client: 'mssql', 6 | connection: { 7 | host: 'vip.hivesql.io', 8 | user: HIVESQL_USERNAME, 9 | password: HIVESQL_PASSWORD, 10 | database: 'DBHive', 11 | }, 12 | pool: { 13 | max: 7, 14 | min: 3, 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/renderer/components/video/CommentSection/ConnectAccountNotice.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export function ConnectAccountNotice() { 4 | return ( 5 |

6 | To comment on this video please connect a HIVE account to your profile:   7 | 8 | Connect HIVE Account 9 | 10 |

11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 3Speak - Tokenised video communities 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/renderer/views/PinsView/CustomToggle.tsx: -------------------------------------------------------------------------------- 1 | import 'brace/mode/json' 2 | import 'brace/theme/github' 3 | import 'jsoneditor-react/es/editor.min.css' 4 | 5 | import React from 'react' 6 | 7 | export const CustomPinsViewToggle = React.forwardRef(({ children, onClick }: any, ref: any) => ( 8 | { 12 | e.preventDefault() 13 | onClick(e) 14 | }} 15 | > 16 | {children} 17 | 18 | )) 19 | -------------------------------------------------------------------------------- /src/renderer/views/Uploader.css: -------------------------------------------------------------------------------- 1 | .card { 2 | position: relative; 3 | display: -webkit-box; 4 | display: -ms-flexbox; 5 | display: flex; 6 | -webkit-box-orient: vertical; 7 | -webkit-box-direction: normal; 8 | -ms-flex-direction: column; 9 | flex-direction: column; 10 | min-width: 0; 11 | word-wrap: break-word; 12 | background-color: #fff; 13 | background-clip: border-box; 14 | border: 1px solid #e3e6f0; 15 | border-radius: 0.35rem; 16 | } -------------------------------------------------------------------------------- /src/renderer/views/UploaderView/videoSelect.ts: -------------------------------------------------------------------------------- 1 | export const videoSelect = (e, setVideoSourceFile, setLogData, logData, videoInfo) => { 2 | let file 3 | if (e.target && e.target.files) { 4 | file = e.target.files[0] 5 | } else if (e.dataTransfer && e.dataTransfer.files) { 6 | file = e.dataTransfer.files[0] 7 | } 8 | if (file) { 9 | setVideoSourceFile(file.path) 10 | setLogData([...logData, `Selected: ${videoInfo.path}`]) 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /webpack/renderer.prod.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | const baseConfig = require('./renderer.base.config'); 4 | 5 | module.exports = merge(baseConfig, { 6 | mode: 'production', 7 | entry: path.join(__dirname, '../src/renderer/index.tsx'), 8 | resolve: { 9 | extensions: ['.ts', '.tsx', '.js', '.json'], 10 | }, 11 | output: { 12 | path: path.join(__dirname, '../dist'), 13 | filename: 'renderer.prod.js' 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /src/renderer/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next' 2 | import { initReactI18next } from 'react-i18next' 3 | import locals from './assets/locales' 4 | 5 | i18n 6 | .use(initReactI18next) // passes i18n down to react-i18next 7 | .init({ 8 | resources: locals, 9 | lng: 'en', 10 | 11 | keySeparator: false, // we do not use keys in form messages.welcome 12 | 13 | interpolation: { 14 | escapeValue: false, // react already safes from xss 15 | }, 16 | }) 17 | 18 | export default i18n 19 | -------------------------------------------------------------------------------- /src/renderer/views/UploaderView/compileVideoCid.ts: -------------------------------------------------------------------------------- 1 | export const compileVideoCid = async (videoInfo, thumbnailInfo, ipfs) => { 2 | const videoCid = videoInfo.cid; 3 | if (thumbnailInfo.cid) { 4 | const obj = await ipfs.current.object.stat(thumbnailInfo.cid); 5 | const output = await ipfs.current.object.patch.addLink(videoCid, { 6 | name: thumbnailInfo.path, 7 | size: thumbnailInfo.size, 8 | cid: thumbnailInfo.cid, 9 | }); 10 | return output.toString(); 11 | } 12 | return videoCid; 13 | }; 14 | -------------------------------------------------------------------------------- /src/renderer/views/WatchView/watchViewHelpers/mountPlayer.ts: -------------------------------------------------------------------------------- 1 | import { VideoService } from '../../../services/video.service'; 2 | 3 | export async function mountPlayer(reflink: string, setVideoLink: (link: string) => void, recordView: () => void) { 4 | try { 5 | const playerType = 'standard'; 6 | switch (playerType) { 7 | case 'standard': { 8 | setVideoLink(await VideoService.getVideoSourceURL(reflink)); 9 | } 10 | } 11 | recordView(); 12 | } catch (ex) { 13 | console.error(ex); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/CustomToggle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FaCogs } from 'react-icons/fa'; 3 | export const CustomToggle = React.forwardRef(({ children, onClick }, ref) => ( 4 | 18 | )); 19 | -------------------------------------------------------------------------------- /src/renderer/services/accountServices/permalinkToPostInfo.ts: -------------------------------------------------------------------------------- 1 | import { VideoInfo, VideoSource } from '../../../common/models/video.model' 2 | import RefLink from '../../../main/RefLink' 3 | import PromiseIPC from 'electron-promise-ipc' 4 | 5 | export async function permalinkToPostInfo(reflink) { 6 | if (!(reflink instanceof RefLink)) { 7 | reflink = RefLink.parse(reflink) 8 | } 9 | const post_content = ( 10 | (await PromiseIPC.send('distiller.getContent', reflink.toString())) as any 11 | ).json_content 12 | return post_content 13 | } -------------------------------------------------------------------------------- /src/renderer/assets/img/icon_newcomer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/renderer/views/CreatorStudioView.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { Container, Row } from 'react-bootstrap' 3 | 4 | export function CreatorStudioView() { 5 | useEffect(() => { 6 | document.title = '3Speak - Tokenised video communities' 7 | }, []) 8 | return ( 9 | 10 | 11 | 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/views/WatchView/watchViewHelpers/gearSelect.ts: -------------------------------------------------------------------------------- 1 | import PromiseIpc from 'electron-promise-ipc'; 2 | 3 | export async function gearSelect(eventKey: string, reflinkParsed: any) { 4 | switch (eventKey) { 5 | case 'mute_post': { 6 | await PromiseIpc.send('blocklist.add', reflinkParsed.toString()); 7 | break; 8 | } 9 | case 'mute_user': { 10 | await PromiseIpc.send( 11 | 'blocklist.add', 12 | `${reflinkParsed.source.value}:${reflinkParsed.root}` as any, 13 | ); 14 | break; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/common/utils/ipfs-utils.ts: -------------------------------------------------------------------------------- 1 | import { CID } from 'multiformats' 2 | 3 | /** 4 | * @param {Uint8Array|CID|string} path 5 | */ 6 | export function normalizeCidPath(path: string | Uint8Array | string) { 7 | if (path instanceof Uint8Array) { 8 | return CID.decode(path).toString() 9 | } 10 | 11 | path = path.toString() 12 | 13 | if (path.indexOf('/ipfs/') === 0) { 14 | path = path.substring('/ipfs/'.length) 15 | } 16 | 17 | if (path.charAt(path.length - 1) === '/') { 18 | path = path.substring(0, path.length - 1) 19 | } 20 | 21 | return path 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | 4 | import { App } from './App' 5 | import './i18n' 6 | 7 | // This code adds 2 new items to the context menu to zoom in the window (in and out) 8 | // Read other steps for more information 9 | ;(window as any).$ = (window as any).jQuery = require('jquery') 10 | const shell = require('electron').shell 11 | //open links externally by default 12 | $(document).on('click', 'a[href^="http"]', function (event) { 13 | event.preventDefault() 14 | shell.openExternal(this.href) 15 | }) 16 | render(, document.getElementById('app')) 17 | -------------------------------------------------------------------------------- /src/renderer/views/NotFoundView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import NotFoundIMG from '../../renderer/assets/img/404.png' 4 | 5 | export function NotFoundView() { 6 | return ( 7 |
8 |
9 | 10 |

SORRY! PAGE NOT FOUND.

11 | Unfortunately the page you are looking for has been moved or deleted. 12 |
13 |
14 | 15 | {' '} 16 | GO TO HOME PAGE 17 | 18 |
19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/services/accountServices/getFollowerCount.ts: -------------------------------------------------------------------------------- 1 | import RefLink from '../../../main/RefLink' 2 | import PromiseIPC from 'electron-promise-ipc' 3 | 4 | export async function getFollowerCount(reflink) { 5 | if (!(reflink instanceof RefLink)) { 6 | reflink = RefLink.parse(reflink) 7 | } 8 | switch (reflink.source.value) { 9 | case 'hive': { 10 | const followerCount = await PromiseIPC.send( 11 | 'distiller.getFollowerCount', 12 | reflink.toString(), 13 | ) 14 | return followerCount 15 | } 16 | default: { 17 | throw new Error(`Unknown account provider ${reflink.source.value}`) 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/renderer/services/accountServices/getProfilePictureURL.ts: -------------------------------------------------------------------------------- 1 | import RefLink from '../../../main/RefLink' 2 | import axios from 'axios' 3 | 4 | export async function getProfilePictureURL(reflink) { 5 | if (!(reflink instanceof RefLink)) { 6 | reflink = RefLink.parse(reflink) 7 | } 8 | switch ('hive') { 9 | case 'hive': { 10 | const avatar_url = `https://images.hive.blog/u/${reflink.root}/avatar` 11 | try { 12 | await axios.head(avatar_url) 13 | return avatar_url 14 | } catch { 15 | throw new Error('Failed to retrieve profile picture information') 16 | } 17 | } 18 | default: { 19 | throw new Error(`Unknown account provider ${'hive'}`) 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/renderer/services/accountServices/getAccountMetadata.ts: -------------------------------------------------------------------------------- 1 | import PromiseIPC from 'electron-promise-ipc' 2 | import { hiveClient } from '../../singletons/hive-client.singleton' 3 | import ArraySearch from 'arraysearch' 4 | //Get account posting_json_metadata 5 | export async function getAccountMetadata() { 6 | const Finder = ArraySearch.Finder 7 | const profileID = window.localStorage.getItem('SNProfileID') 8 | const getAccount = (await PromiseIPC.send('accounts.get', profileID as any)) as any 9 | const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }) 10 | 11 | const account = await hiveClient.call('condenser_api', 'get_accounts', [[hiveInfo.username]]) 12 | const metadata = account[0].posting_json_metadata 13 | return metadata 14 | } -------------------------------------------------------------------------------- /src/renderer/services/accountServices/voteHandler.ts: -------------------------------------------------------------------------------- 1 | import hive from '@hiveio/hive-js' 2 | 3 | export async function voteHandler(voteOp) { 4 | switch (voteOp.accountType) { 5 | case 'hive': { 6 | const weight = voteOp.weight * 100 7 | const theWif = voteOp.wif 8 | hive.broadcast.vote( 9 | theWif, 10 | voteOp.voter, 11 | voteOp.author, 12 | voteOp.permlink, 13 | weight, 14 | function (error, succeed) { 15 | if (error) { 16 | console.error(error) 17 | console.error('Error encountered') 18 | } 19 | 20 | if (succeed) { 21 | console.log('vote broadcast successfully') 22 | } 23 | }, 24 | ) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/renderer/views/TagView.tsx: -------------------------------------------------------------------------------- 1 | import { GridFeedView } from './GridFeedView' 2 | import React, { useEffect, useMemo, useState } from 'react' 3 | import RefLink from '../../main/RefLink' 4 | import { useNewTagFeed } from '../components/hooks/Feeds' 5 | import { useParams } from 'react-router-dom' 6 | 7 | export function TagView(props: any) { 8 | // let { reflink } = useParams() 9 | const reflink = useMemo(() => { 10 | return RefLink.parse(props.match.params.reflink) 11 | }, [props.match]) 12 | const newVideos = useNewTagFeed(reflink.root) 13 | 14 | return ( 15 | <> 16 | {newVideos !== null ? ( 17 | 18 | ) : null} 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/assets/img/hive.svg: -------------------------------------------------------------------------------- 1 | hive-hive-logo -------------------------------------------------------------------------------- /src/renderer/services/accountServices/createPost.ts: -------------------------------------------------------------------------------- 1 | import hive from '@hiveio/hive-js' 2 | export async function createPost(postOp) { 3 | switch (postOp.accountType) { 4 | case 'hive': { 5 | const theWif = postOp.wif 6 | 7 | hive.broadcast.comment( 8 | theWif, 9 | '', 10 | postOp.parentPermlink, 11 | postOp.author, 12 | postOp.permlink, 13 | postOp.title, 14 | postOp.body, 15 | postOp.jsonMetadata, 16 | function (error, succeed) { 17 | console.log('Bout to check') 18 | if (error) { 19 | console.error(error) 20 | console.error('Error encountered') 21 | } 22 | 23 | if (succeed) { 24 | console.log('succeed') 25 | } 26 | }, 27 | ) 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/renderer/components/LoadingMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LoopCircleLoading } from 'react-loadingg' 3 | 4 | export default function LoadingMessage(props) { 5 | return ( 6 |
12 |
21 | 22 |
23 |
24 |

{props.loadingMessage}

25 |

{props.subtitle}

26 |
27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/services/accountServices/getAccountBalances.ts: -------------------------------------------------------------------------------- 1 | import RefLink from '../../../main/RefLink' 2 | import PromiseIPC from 'electron-promise-ipc' 3 | 4 | export async function getAccountBalances(reflink) { 5 | if (!(reflink instanceof RefLink)) { 6 | reflink = RefLink.parse(reflink) 7 | } 8 | switch (reflink.source.value) { 9 | case 'hive': { 10 | let accountBalances = 11 | // type error: 2nd argument (string) does not match function signature 12 | ((await PromiseIPC.send('distiller.getAccount', `hive:${reflink.root}` as any)) as any) 13 | .json_content 14 | 15 | accountBalances = { 16 | hive: accountBalances.balance, 17 | hbd: accountBalances.sbd_balance, 18 | } 19 | return accountBalances 20 | } 21 | default: { 22 | throw new Error(`Unknown account provider ${reflink.source.value}`) 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /webpack/main.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | module.exports = { 5 | mode: 'production', 6 | target: 'electron-main', 7 | entry: path.join(__dirname, '../src/main/index.ts'), 8 | resolve: { 9 | extensions: ['.ts', '.tsx', '.js', '.json'], 10 | }, 11 | output: { 12 | path: path.join(__dirname, '../dist'), 13 | filename: 'main.prod.js', 14 | }, 15 | module: { 16 | rules: [{ test: /\.ts?$/, use: 'ts-loader', exclude: /node_modules/ }], 17 | }, 18 | node: { 19 | __dirname: false, 20 | __filename: false, 21 | }, 22 | plugins: [ 23 | new webpack.ExternalsPlugin('commonjs', [ 24 | 'leveldown', 25 | 'ky-universal', 26 | 'ipfs-http-client', 27 | 'pouchdb', 28 | 'pouchdb-find', 29 | ]), 30 | new webpack.DefinePlugin({ 31 | 'process.env.FLUENTFFMPEG_COV': false, 32 | }), 33 | ], 34 | } 35 | -------------------------------------------------------------------------------- /src/renderer/components/CollapsibleText.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { FaChevronUp, FaChevronDown } from 'react-icons/fa' 3 | 4 | export function CollapsibleText(props: any) { 5 | const [collapsed, setCollapsed] = useState(true) 6 | 7 | const handleClick = (e) => { 8 | setCollapsed(!collapsed) 9 | } 10 | 11 | return ( 12 | <> 13 |
19 | {props.children} 20 |
21 |
27 | {collapsed ? : } 28 | {collapsed ? 'Show more' : 'Show less'} 29 |
30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/css/User.css: -------------------------------------------------------------------------------- 1 | .channel-profile-img { 2 | background: #fff none repeat scroll 0 0; 3 | border: 2px solid #fff; 4 | border-radius: 50px; 5 | height: 90px; 6 | width: 90px; 7 | } 8 | .single-channel-image { 9 | position: relative; 10 | } 11 | .channel-profile { 12 | bottom: 0; 13 | left: 0; 14 | padding: 1rem 30px; 15 | /*position: absolute;*/ 16 | right: 0; 17 | padding-top: 4em; 18 | } 19 | .bg-steem { 20 | background-color: #1FBF8F !important; 21 | } 22 | .bg-sbd { 23 | background: #161fc8 !important; 24 | } 25 | .status .card-title { 26 | font-family: 'Oswald', sans-serif; 27 | font-size: 48px; 28 | font-weight: bold; 29 | color: #fff; 30 | line-height: 45px; 31 | padding-top: 20px; 32 | letter-spacing: -0.8px; 33 | } 34 | .channel-brand { 35 | color: rgb(0, 0, 0); 36 | font-size: 16px; 37 | font-weight: bold; 38 | padding-left: 10px; 39 | padding-right: 10px; 40 | } -------------------------------------------------------------------------------- /src/common/utils/unit-conversion.functions.ts: -------------------------------------------------------------------------------- 1 | import convert from 'convert-units' 2 | 3 | export function millisecondsAsString(ms: number) { 4 | if (typeof ms !== 'number') { 5 | throw new Error(`${ms} is not a number...cannot convert to bytes string`) 6 | } 7 | 8 | const conv = convert(ms).from('ms').toBest() 9 | return `${Math.round(conv.val)} ${conv.unit}` 10 | } 11 | 12 | export function secondsAsString(seconds: number) { 13 | if (typeof seconds !== 'number') { 14 | throw new Error(`${seconds} is not a number...cannot convert to bytes string`) 15 | } 16 | 17 | const conv = convert(seconds).from('s').toBest() 18 | return `${Math.round(conv.val)} ${conv.unit}` 19 | } 20 | 21 | export function bytesAsString(numBytes: number) { 22 | if (typeof numBytes !== 'number') { 23 | throw new Error(`${numBytes} is not a number...cannot convert to bytes string`) 24 | } 25 | 26 | const conv = convert(numBytes).from('B').toBest() 27 | return `${Math.round(conv.val)} ${conv.unit}` 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/views/PinsView/CustomMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { FormControl } from 'react-bootstrap' 3 | 4 | // forwardRef again here! 5 | // Dropdown needs access to the DOM of the Menu to measure it 6 | export const CustomPinsViewMenu = React.forwardRef( 7 | ({ children, style, className, 'aria-labelledby': labeledBy }: any, ref: any) => { 8 | const [value, setValue] = useState('') 9 | 10 | return ( 11 |
12 | setValue(e.target.value)} 17 | value={value} 18 | /> 19 |
    20 | {React.Children.toArray(children).filter( 21 | (child: any) => !value || child.props.children.toLowerCase().startsWith(value), 22 | )} 23 |
24 |
25 | ) 26 | }, 27 | ) 28 | -------------------------------------------------------------------------------- /src/components/DHTProviders.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { FaSitemap } from 'react-icons/fa'; 3 | import * as IPFSHTTPClient from 'ipfs-http-client'; 4 | import { IPFS_HOST } from '../common/constants'; 5 | 6 | let ipfsClient; 7 | try { 8 | ipfsClient = IPFSHTTPClient.create({ url: IPFS_HOST }); 9 | } catch (error) { 10 | console.error(`Error creating IPFS client in watch.tsx: `, error); 11 | throw error; 12 | } 13 | 14 | export function DHTProviders(props) { 15 | const [peers, setPeers] = useState('N/A'); 16 | 17 | useEffect(() => { 18 | void load(); 19 | 20 | async function load() { 21 | if (!props.rootCid) { 22 | return; 23 | } 24 | let out = 0; 25 | for await (const pov of ipfsClient.dht.findProvs(props.rootCid)) { 26 | out = out + 1; 27 | setPeers(out); 28 | } 29 | setPeers(out); 30 | } 31 | }, []); 32 | 33 | return ( 34 |
35 | Storage Servers: {peers} 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/nodejs_ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | env: 13 | NODE_OPTIONS: "--max-old-space-size=8096" 14 | 15 | jobs: 16 | unit_tests: 17 | name: Running Unit Tests 18 | runs-on: ${{ matrix.os }} 19 | 20 | strategy: 21 | matrix: 22 | node-version: [16.x] 23 | os: [ubuntu-latest, macOS-latest, windows-latest] 24 | 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Use Node.js ${{ matrix.node-version }} 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | - name: Install 32 | run: npm install 33 | - name: Test 34 | run: npm test 35 | - name: Build 36 | run: npm run build -------------------------------------------------------------------------------- /src/common/models/video.model.ts: -------------------------------------------------------------------------------- 1 | export interface VideoSource { 2 | video?: { 3 | format: 'mp4' | 'hls' | 'webm' 4 | url: string 5 | } 6 | thumbnail?: string //Full URL can be IPFS URL or http 7 | type?: 'thumbnail' | 'video' 8 | url?: string 9 | format?: string 10 | } 11 | 12 | export interface VideoInfo { 13 | sources: VideoSource[] 14 | title: string 15 | description: string 16 | duration?: number 17 | creation: string 18 | tags: string[] 19 | refs: string[] 20 | meta: any 21 | reflink: string 22 | size?: number 23 | } 24 | 25 | export interface VideoResolutionProfile { 26 | name: string 27 | size: string 28 | } 29 | 30 | export interface VideoEncodingOptions { 31 | hwaccel: any 32 | } 33 | 34 | export interface VideoEncodingJob { 35 | id: any 36 | sourceUrl: string 37 | profiles: VideoResolutionProfile[] 38 | options: VideoEncodingOptions 39 | } 40 | 41 | export interface EncodingOutput { 42 | ipfsHash: string 43 | size: number 44 | playUrl: string 45 | folderPath: string 46 | duration: number 47 | path: 'manifest.m3u8' 48 | } 49 | -------------------------------------------------------------------------------- /src/renderer/views/WatchView/watchViewHelpers/showDebug.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tab, Tabs } from 'react-bootstrap'; 3 | import ace from 'brace'; 4 | import Popup from 'react-popup'; 5 | import { JsonEditor as Editor } from 'jsoneditor-react'; 6 | 7 | export function showDebug(videoInfo: any) { 8 | const metadata = videoInfo; 9 | Popup.registerPlugin('watch_debug', async function () { 10 | this.create({ 11 | content: ( 12 |
13 | 14 | 15 | 16 | 17 | 18 |
19 | ), 20 | buttons: { 21 | right: [ 22 | { 23 | text: 'Close', 24 | className: 'success', 25 | action: function () { 26 | Popup.close(); 27 | }, 28 | }, 29 | ], 30 | }, 31 | }); 32 | }); 33 | Popup.plugins().watch_debug(); 34 | } 35 | -------------------------------------------------------------------------------- /webpack/renderer.dev.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const merge = require('webpack-merge') 3 | const { spawn } = require('child_process') 4 | const baseConfig = require('./renderer.base.config') 5 | 6 | module.exports = merge(baseConfig, { 7 | mode: 'development', 8 | devtool: 'cheap-module-eval-source-map', 9 | entry: path.join(__dirname, '../src/renderer/index.tsx'), 10 | resolve: { 11 | extensions: ['.ts', '.tsx', '.js', '.json'], 12 | }, 13 | output: { 14 | path: path.join(__dirname, '../dist'), 15 | filename: 'renderer.dev.js', 16 | }, 17 | optimization: { 18 | minimize: false, 19 | }, 20 | devServer: { 21 | hot: true, 22 | compress: true, 23 | port: 6789, 24 | contentBase: path.join(__dirname, '../src/renderer'), 25 | after() { 26 | spawn('npm', ['run', 'start-main'], { 27 | shell: true, 28 | env: process.env, 29 | stdio: 'inherit', 30 | }) 31 | .on('close', (code) => process.exit(code)) 32 | .on('error', (spawnError) => console.error(spawnError)) 33 | }, 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /src/renderer/views/WatchView/watchViewHelpers/PinLocally.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url'; 2 | import PromiseIpc from 'electron-promise-ipc'; 3 | import CID from 'cids'; 4 | import { NotificationManager } from 'react-notifications'; 5 | 6 | export async function PinLocally(videoInfo: any, reflink: string) { 7 | const cids = [] 8 | for (const source of videoInfo.sources) { 9 | const url = new URL(source.url) 10 | try { 11 | new CID(url.host) 12 | cids.push(url.host) 13 | } catch {} 14 | } 15 | 16 | if (cids.length !== 0) { 17 | NotificationManager.info('Pinning in progress') 18 | await PromiseIpc.send('pins.add', { 19 | _id: reflink, 20 | source: 'Watch Page', 21 | cids, 22 | expire: null, 23 | meta: { 24 | title: videoInfo.title, 25 | }, 26 | } as any) 27 | NotificationManager.success( 28 | `Video with reflink of ${reflink} has been successfully pinned! Thank you for contributing!`, 29 | 'Pin Successful', 30 | ) 31 | } else { 32 | NotificationManager.warning('This video is not available on IPFS') 33 | } 34 | } -------------------------------------------------------------------------------- /src/renderer/services/accountServices/getProfileBackgroundImageUrl.ts: -------------------------------------------------------------------------------- 1 | import RefLink from '../../../main/RefLink' 2 | import PromiseIPC from 'electron-promise-ipc' 3 | 4 | export async function getProfileBackgroundImageUrl(reflink): Promise { 5 | if (!(reflink instanceof RefLink)) { 6 | reflink = RefLink.parse(reflink) 7 | } 8 | switch ('hive') { 9 | case 'hive': { 10 | const jsonContent = ( 11 | (await PromiseIPC.send('distiller.getAccount', reflink.toString())) as any 12 | ).json_content 13 | 14 | if (!jsonContent) { 15 | throw new Error('Invalid account data content. Empty record') 16 | } 17 | const metadata = jsonContent.posting_json_metadata as string 18 | if (!metadata) { 19 | return '' 20 | } 21 | 22 | const parsed = JSON.parse(metadata) 23 | jsonContent.posting_json_metadata = JSON.parse(jsonContent.posting_json_metadata as string) 24 | 25 | const image = parsed.profile.cover_image 26 | 27 | return jsonContent.posting_json_metadata.profile.cover_image 28 | } 29 | default: { 30 | throw new Error('Unknown account provider') 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/renderer/services/accountServices/postCustomJson.ts: -------------------------------------------------------------------------------- 1 | //Post Custom Json 2 | import { api } from '@hiveio/hive-js' 3 | import PromiseIPC from 'electron-promise-ipc' 4 | import ArraySearch from 'arraysearch' 5 | export async function updateMeta(username, metadata) { 6 | const Finder = ArraySearch.Finder 7 | console.log('Updating metadata') 8 | const profileID = window.localStorage.getItem('SNProfileID') 9 | const getAccount = (await PromiseIPC.send('accounts.get', profileID as any)) as any 10 | const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }) 11 | const wif = hiveInfo.privateKeys.posting_key 12 | console.log('WIF', wif) 13 | console.log('JSON METADATA', metadata) 14 | api.broadcast.account_update2( 15 | wif, 16 | [], 17 | [username], 18 | JSON.stringify(metadata), 19 | async (error, succeed) => { 20 | if (error) { 21 | console.error(error) 22 | console.error('Error encountered broadcsting custom json') 23 | } 24 | 25 | if (succeed) { 26 | console.log(succeed) 27 | console.log('success broadcasting custom json') 28 | } 29 | }, 30 | ) 31 | } -------------------------------------------------------------------------------- /src/renderer/services/accountServices/updateMeta.ts: -------------------------------------------------------------------------------- 1 | //Update posting_json_metadata 2 | import { api } from '@hiveio/hive-js' 3 | import PromiseIPC from 'electron-promise-ipc' 4 | import ArraySearch from 'arraysearch' 5 | export async function updateMeta(username, metadata) { 6 | const Finder = ArraySearch.Finder 7 | console.log('Updating metadata') 8 | const profileID = window.localStorage.getItem('SNProfileID') 9 | const getAccount = (await PromiseIPC.send('accounts.get', profileID as any)) as any 10 | const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }) 11 | const wif = hiveInfo.privateKeys.posting_key 12 | console.log('WIF', wif) 13 | console.log('JSON METADATA', metadata) 14 | api.broadcast.account_update2( 15 | wif, 16 | [], 17 | [username], 18 | JSON.stringify(metadata), 19 | async (error, succeed) => { 20 | if (error) { 21 | console.error(error) 22 | console.error('Error encountered broadcsting custom json') 23 | } 24 | 25 | if (succeed) { 26 | console.log(succeed) 27 | console.log('success broadcasting custom json') 28 | } 29 | }, 30 | ) 31 | } -------------------------------------------------------------------------------- /src/renderer/views/WatchView/watchViewHelpers/generalFetch.ts: -------------------------------------------------------------------------------- 1 | import { AccountService } from '../../../services/account.service'; 2 | import CID from 'cids' 3 | import { URL } from 'url' 4 | 5 | export async function generalFetch( 6 | reflink: string, 7 | setVideoInfo: (info: any) => void, 8 | setPostInfo: (info: any) => void, 9 | setProfilePictureUrl: (url: string) => void, 10 | setRootCid: (cid: string) => void 11 | ) { 12 | const info = await AccountService.permalinkToVideoInfo(reflink, { type: 'video' }) 13 | setVideoInfo(info) 14 | setPostInfo(await AccountService.permalinkToPostInfo(reflink)) 15 | try { 16 | //Leave profileURL default if error is thrown when attempting to retrieve profile picture 17 | setProfilePictureUrl(await AccountService.getProfilePictureURL(reflink)) 18 | } catch (ex) { 19 | console.error(ex) 20 | throw ex 21 | } 22 | document.title = `3Speak - ${info.title}` 23 | const cids = [] 24 | for (const source of info.sources) { 25 | const url = new URL(source.url) 26 | try { 27 | new CID(url.host) 28 | cids.push(url.host) 29 | } catch {} 30 | } 31 | setRootCid(cids[0]) 32 | } -------------------------------------------------------------------------------- /src/renderer/views/UploaderView/thumbnailSelect.ts: -------------------------------------------------------------------------------- 1 | export const thumbnailSelect = async ({e, thumbnailPreview, setThumbnailInfo, setVideoSourceFile, setLogData, ipfs, logData, videoInfo}) => { 2 | console.log(`handling thumbnail selectr`) 3 | 4 | let file 5 | if (e.target && e.target.files) { 6 | file = e.target.files[0] 7 | } else if (e.dataTransfer && e.dataTransfer.files) { 8 | file = e.dataTransfer.files[0] 9 | } 10 | if (file) { 11 | setVideoSourceFile(file.path) 12 | setLogData([...logData, `Selected: ${videoInfo.path}`]) 13 | } 14 | const imgblob = URL.createObjectURL(file) 15 | const size = file.size 16 | 17 | console.log(`uploading file with size ${size}`) 18 | 19 | thumbnailPreview.current = imgblob 20 | 21 | const fileDetails = { 22 | path: e.target.files[0].name, 23 | content: e.target.files[0], 24 | } 25 | 26 | const ipfsOut = await ipfs.current.add(fileDetails, { pin: false }) 27 | console.log(`setting thumbnail info to cid`, ipfsOut.cid.toString()) 28 | 29 | setThumbnailInfo({ 30 | size, 31 | path: `thumbnail.${file.type.split('/')[1]}`, 32 | cid: ipfsOut.cid.toString(), 33 | }) 34 | }; -------------------------------------------------------------------------------- /src/renderer/services/accountServices/getProfileAbout.ts: -------------------------------------------------------------------------------- 1 | import RefLink from '../../../main/RefLink' 2 | import PromiseIPC from 'electron-promise-ipc' 3 | 4 | export async function getProfileAbout(reflink) { 5 | if (!(reflink instanceof RefLink)) { 6 | reflink = RefLink.parse(reflink) 7 | } 8 | switch (reflink.source.value) { 9 | case 'hive': { 10 | // type error: 2nd argument string does not match function signature 11 | // const userAboutText = JSON.parse( 12 | // ((await PromiseIPC.send('distiller.getAccount', `hive:${reflink.root}` as any)) as any) 13 | // .json_content.posting_json_metadata, 14 | // ).profile.about 15 | const res = (await PromiseIPC.send( 16 | 'distiller.getAccount', 17 | `hive:${reflink.root}` as any, 18 | )) as any 19 | 20 | if (!res?.json_content?.posting_json_metadata) { 21 | return '' 22 | } else { 23 | const metadata = JSON.parse(res.json_content.posting_json_metadata) 24 | return metadata.profile.about 25 | } 26 | } 27 | default: { 28 | throw new Error(`Unknown account provider ${reflink.source.value}`) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/renderer/assets/img/icon_trend.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/renderer/services/accountServices/getFollowing.ts: -------------------------------------------------------------------------------- 1 | import PromiseIPC from 'electron-promise-ipc' 2 | import { hiveClient } from '../../singletons/hive-client.singleton' 3 | import ArraySearch from 'arraysearch' 4 | 5 | export async function getFollowing() { 6 | const Finder = ArraySearch.Finder 7 | const profileID = window.localStorage.getItem('SNProfileID') 8 | // String does not match 2nd argument for send function signature 9 | const getAccount = (await PromiseIPC.send('accounts.get', profileID as any)) as any 10 | const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }) 11 | 12 | const out = [] 13 | const done = false 14 | let nextFollow = '' 15 | const limit = 100 16 | while (done === false) { 17 | // const followingChunk = await hiveClient.call('follow_api', 'get_following', [ 18 | const followingChunk = await hiveClient.call('condenser_api', 'get_following', [ 19 | hiveInfo.username, 20 | nextFollow, 21 | 'blog', 22 | limit, 23 | ]) 24 | 25 | out.push(...followingChunk) 26 | if (followingChunk.length !== limit) { 27 | break 28 | } 29 | nextFollow = followingChunk[followingChunk.length - 1].following 30 | } 31 | return out 32 | } -------------------------------------------------------------------------------- /src/renderer/views/PoAView/usePoAInstaller.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import PoAInstaller from '../../../main/AutoUpdaterPoA'; 3 | import { Terminal } from 'xterm'; 4 | import { WebLinksAddon } from 'xterm-addon-web-links'; 5 | import 'xterm/css/xterm.css'; 6 | 7 | export function usePoAInstaller() { 8 | const [terminal, setTerminal] = useState(null); 9 | const updater = new PoAInstaller(); 10 | 11 | const updatePoA = async () => { 12 | try { 13 | await updater.main(); 14 | } catch (error) { 15 | console.error('Error updating Proof of Access:', error); 16 | } 17 | return 18 | }; 19 | 20 | const initTerminal = (terminalRef: React.RefObject) => { 21 | if (terminalRef.current && !terminal) { 22 | const term = new Terminal(); 23 | term.open(terminalRef.current); 24 | term.loadAddon(new WebLinksAddon()); 25 | setTerminal(term); 26 | } 27 | }; 28 | 29 | useEffect(() => { 30 | if (terminal) { 31 | updater.on('data', (data: string) => { 32 | terminal.write(data.replace(/\n/g, '\r\n')); 33 | }); 34 | } 35 | }, [terminal]); 36 | 37 | return { 38 | updatePoA, 39 | initTerminal, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/renderer/views/UserView/userQueries.ts: -------------------------------------------------------------------------------- 1 | // userQueries.ts 2 | import { gql } from '@apollo/client'; 3 | 4 | export const QUERY = gql` 5 | 6 | query Query($author: String) { 7 | 8 | latestFeed(author:$author, limit: 15) { 9 | items { 10 | ... on CeramicPost { 11 | stream_id 12 | version_id 13 | parent_id 14 | title 15 | body 16 | json_metadata 17 | app_metadata 18 | } 19 | ... on HivePost { 20 | created_at 21 | updated_at 22 | parent_author 23 | parent_permlink 24 | permlink 25 | author 26 | title 27 | body 28 | lang 29 | post_type 30 | app 31 | tags 32 | json_metadata 33 | app_metadata 34 | community_ref 35 | 36 | three_video 37 | 38 | children { 39 | parent_author 40 | parent_permlink 41 | permlink 42 | title 43 | body 44 | title 45 | lang 46 | post_type 47 | app 48 | json_metadata 49 | app_metadata 50 | community_ref 51 | } 52 | } 53 | __typename 54 | } 55 | } 56 | } 57 | 58 | ` 59 | ; -------------------------------------------------------------------------------- /webpack/renderer.base.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | target: 'electron-renderer', 6 | devtool: 'cheap-module-eval-source-map', 7 | externals: { 8 | mssql: 'commonjs mssql' 9 | }, 10 | module: { 11 | rules: [ 12 | { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ }, 13 | { 14 | test: /\.css$/, 15 | loader: 'style-loader!css-loader' 16 | }, 17 | { 18 | test: /\.less$/, 19 | loader: 'style-loader!css-loader!less-loader' 20 | }, 21 | { 22 | test: /\.(sass|scss)$/, 23 | loader: 'style-loader!css-loader!sass-loader' 24 | }, 25 | { 26 | test: /\.(eot|woff|woff2|ttf|ico|gif|png|jpg|jpeg|webp|svg)$/, 27 | loader: 'file-loader', 28 | options: { 29 | limit: 1024, 30 | name: 'assets/[folder]/[name].[ext]', 31 | } 32 | } 33 | ] 34 | }, 35 | plugins: [ 36 | new HtmlWebpackPlugin({ 37 | filename: 'index.html', 38 | template: path.join(__dirname, '../src/renderer/index.html') 39 | }) 40 | ], 41 | node: { 42 | __dirname: false, 43 | __filename: false 44 | } 45 | }; -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # 3Speak-App 2 | [![Node.js CI](https://github.com/3speaknetwork/3Speak-app/actions/workflows/nodejs_ci.yml/badge.svg)](https://github.com/3speaknetwork/3Speak-app/actions/workflows/nodejs_ci.yml) 3 | 4 | The 3Speak decentralized desktop app. 5 | 6 | ![3Speak Preview](https://raw.githubusercontent.com/3speaknetwork/3Speak-app/master/media/media1.PNG) 7 | 8 | ## Usage guide 9 | 10 | For normal users you will want to go to [Releases](https://github.com/3speaknetwork/3Speak-app/releases) where you can find the latest release for your operating system. 11 | 12 | ## Developer Setup 13 | 14 | First, you should clone this repo to your computer via git like this: 15 | 16 | ```bash 17 | git clone https://github.com/3speaknetwork/3Speak-app 18 | ``` 19 | 20 | Then, install npm dependancies: 21 | 22 | ```bash 23 | cd 3Speak-app 24 | npm install --legacy-peer-deps --python=python2.7 25 | ``` 26 | 27 | After installing dependancies, you can run the following command to start app in `dev` mode: 28 | 29 | ```bash 30 | npm run dev 31 | ``` 32 | 33 | If you want to build a production binary: 34 | 35 | ```bash 36 | npm run package 37 | ``` 38 | 39 | If you want to run in production mode: 40 | 41 | ```bash 42 | npm run prod 43 | ``` 44 | 45 | # License 46 | GPLv3 47 | -------------------------------------------------------------------------------- /src/renderer/views/PoAView/PoAStateContext.tsx: -------------------------------------------------------------------------------- 1 | // filename: PoAStateContext.tsx 2 | import React, { createContext, useState, useContext, useEffect, useRef } from 'react'; 3 | 4 | interface PoAStateContextType { 5 | logs: string[]; 6 | setLogs: React.Dispatch>; 7 | validatorOnline: boolean; 8 | setValidatorOnline: React.Dispatch>; 9 | } 10 | 11 | const PoAStateContext = createContext({ 12 | logs: [], 13 | setLogs: () => {}, 14 | validatorOnline: false, 15 | setValidatorOnline: () => {}, 16 | }); 17 | 18 | export const PoAStateProvider: React.FC = ({ children }) => { 19 | const [logs, setLogs] = useState([]); 20 | const [validatorOnline, setValidatorOnline] = useState(false); 21 | const terminalRef = useRef(null); 22 | 23 | useEffect(() => { 24 | if (terminalRef.current) { 25 | terminalRef.current.innerHTML = ''; 26 | logs.forEach(log => terminalRef.current.innerHTML += log + '
'); 27 | } 28 | }, [logs, terminalRef]); 29 | 30 | return ( 31 | 32 | {children} 33 | 34 | ); 35 | }; 36 | 37 | export const usePoAState = () => useContext(PoAStateContext); 38 | -------------------------------------------------------------------------------- /src/renderer/views/CommunitiesView.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { Button, Container, Row } from 'react-bootstrap' 3 | import { CommunityTile } from '../components/widgets/CommunityTile' 4 | 5 | const { Client: HiveClient } = require('@hiveio/dhive') 6 | const client = new HiveClient('https://api.openhive.network') 7 | 8 | export function CommunitiesView() { 9 | const [data, setData] = useState([]) 10 | 11 | const generate = async () => { 12 | const res = await client.call('bridge', 'list_communities', { 13 | last: '', 14 | limit: 100, 15 | }) 16 | setData(res) 17 | } 18 | useEffect(() => { 19 | document.title = '3Speak - Tokenised video communities' 20 | generate() 21 | }, []) 22 | return ( 23 | 24 | 25 |
26 |

Communities

27 | 28 | 31 | 32 |
33 |
34 | 35 | {data.map((value) => ( 36 | 37 | ))} 38 | 39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/renderer/views/PinsView.tsx: -------------------------------------------------------------------------------- 1 | // PinsView.tsx 2 | import 'brace/mode/json'; 3 | import 'brace/theme/github'; 4 | import 'jsoneditor-react/es/editor.min.css'; 5 | 6 | import ace from 'brace'; 7 | import React, { useMemo, useRef, useState } from 'react'; 8 | import Popup from 'react-popup'; 9 | import { JsonEditor as Editor } from 'jsoneditor-react'; 10 | 11 | import { PinsViewComponent } from './PinsView/PinsViewComponent'; 12 | import { pinRows } from './PinsView/PinRows'; 13 | import { usePinningUtils } from './PinsView/pinningUtils'; 14 | 15 | export function PinsView() { 16 | const { 17 | newVideos, 18 | trendingVideos, 19 | pinList, 20 | updateSearchTables, 21 | PinLocally, 22 | actionSelect, 23 | removePin, 24 | } = usePinningUtils(); 25 | 26 | const [showExplorer, setShowExplorer] = useState(false); 27 | const pid = useRef(); 28 | 29 | 30 | const rows = useMemo(() => pinRows(pinList, removePin), [pinList, removePin, ace]); 31 | 32 | return ( 33 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/renderer/views/PoAView/PoAProgramRunnerContext.tsx: -------------------------------------------------------------------------------- 1 | // file: PoAProgramRunnerContext.tsx 2 | import React, { useState, useRef, useContext } from 'react'; 3 | import ProgramRunner from '../../../main/core/components/ProgramRunner'; 4 | 5 | interface PoAProgramRunnerContextProps { 6 | programRunner: ProgramRunner | null; 7 | setProgramRunner: (runner: ProgramRunner | null) => void; 8 | terminalRef: React.RefObject; 9 | } 10 | 11 | const PoAProgramRunnerContext = React.createContext(undefined); 12 | 13 | interface PoAProgramRunnerProviderProps { 14 | children: React.ReactNode; 15 | } 16 | 17 | export const PoAProgramRunnerProvider: React.FC = ({ children }) => { 18 | const [programRunner, setProgramRunner] = useState(null); 19 | const terminalRef = useRef(null); 20 | 21 | return ( 22 | 23 | {children} 24 | 25 | ); 26 | }; 27 | 28 | export const usePoAProgramRunnerContext = (): PoAProgramRunnerContextProps => { 29 | const context = useContext(PoAProgramRunnerContext); 30 | if (!context) { 31 | throw new Error('usePoAProgramRunnerContext must be used within a PoAProgramRunnerProvider'); 32 | } 33 | return context; 34 | }; 35 | 36 | export default PoAProgramRunnerContext; 37 | -------------------------------------------------------------------------------- /src/renderer/views/UserView/userUtils.ts: -------------------------------------------------------------------------------- 1 | // userUtils.ts 2 | export function transformGraphqlToNormal(data: any) { 3 | 4 | let blob = [] 5 | for(let video of data) { 6 | console.log(video) 7 | blob.push({ 8 | created: new Date(video.created_at), 9 | author: video.author, 10 | permlink: video.permlink, 11 | tags: video.tags, 12 | title: video.title, 13 | duration: video.json_metadata.video.info.duration || video.json_metadata.video.duration, 14 | //isIpfs: val.json_metadata.video.info.ipfs || thumbnail ? true : false, 15 | //ipfs: val.json_metadata.video.info.ipfs, 16 | isIpfs: true, 17 | images: { 18 | thumbnail: video.three_video.thumbnail_url.replace('img.3speakcontent.co', 'media.3speak.tv'), 19 | poster: video.three_video.thumbnail, 20 | post: video.three_video.thumbnail, 21 | ipfs_thumbnail: video.three_video.thumbnail 22 | /*ipfs_thumbnail: thumbnail 23 | ? `/ipfs/${thumbnail.slice(7)}` 24 | : `/ipfs/${val.json_metadata.video.info.ipfsThumbnail}`, 25 | thumbnail: `https://threespeakvideo.b-cdn.net/${val.permlink}/thumbnails/default.png`, 26 | poster: `https://threespeakvideo.b-cdn.net/${val.permlink}/poster.png`, 27 | post: `https://threespeakvideo.b-cdn.net/${val.permlink}/post.png`,*/ 28 | }, 29 | }) 30 | } 31 | return blob; 32 | } 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build/Release 2 | 3 | on: push 4 | 5 | jobs: 6 | release: 7 | name: Build Release 8 | runs-on: ${{ matrix.os }} 9 | 10 | strategy: 11 | matrix: 12 | os: [macos-latest, ubuntu-latest, windows-latest] 13 | 14 | steps: 15 | - name: Check out Git repository 16 | uses: actions/checkout@v1 17 | 18 | - name: Setup Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: '3.x' # or the specific version you need 22 | 23 | - name: Install Node.js, NPM and Yarn 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: '16' 27 | 28 | - name: Setup environment 29 | run: node ./scripts/prepare_ci 30 | env: 31 | HIVESQL_USERNAME: ${{ secrets.HIVESQL_USERNAME }} 32 | HIVESQL_PASSWORD: ${{ secrets.HIVESQL_PASSWORD }} 33 | 34 | - name: Build/Release Electron app 35 | env: 36 | NODE_OPTIONS: --max_old_space_size=8096 37 | 38 | uses: samuelmeuli/action-electron-builder@v1.6.0 39 | with: 40 | # GitHub token, automatically provided to the action 41 | # (No need to define this secret in the repo settings) 42 | github_token: ${{ secrets.github_token }} 43 | 44 | # If the commit is tagged with a version (e.g. "v1.0.0"), 45 | # release the app after building 46 | release: ${{ startsWith(github.ref, 'refs/tags/v') }} 47 | -------------------------------------------------------------------------------- /src/renderer/views/GridFeed/grid-feed-query.service.ts: -------------------------------------------------------------------------------- 1 | export class GridFeedQueryService { 2 | static getFeedSql(feedType: string, offset = 0): string { 3 | if (feedType === 'new') { 4 | return `SELECT x.* FROM DBHive.dbo.Comments x WHERE CONTAINS(json_metadata , '3speak/video') AND created >= DATEADD(day,-10,GETDATE()) ORDER BY ID DESC OFFSET ${offset} ROWS FETCH NEXT 25 ROWS ONLY` 5 | } else if (feedType === 'trending') { 6 | return `SELECT x.* FROM DBHive.dbo.Comments x WHERE CONTAINS(json_metadata , '3speak/video') AND created >= DATEADD(day,-10,GETDATE()) ORDER BY total_vote_weight DESC OFFSET ${offset} ROWS FETCH NEXT 25 ROWS ONLY` 7 | } else if (feedType[0] === '@') { 8 | const author = feedType.substring(1) 9 | return `SELECT x.* FROM DBHive.dbo.Comments x WHERE CONTAINS(json_metadata , '3speak/video') AND author LIKE '${author}' ORDER BY ID DESC OFFSET ${offset} ROWS FETCH NEXT 25 ROWS ONLY` 10 | } else if (feedType[0] === '#') { 11 | const catString = feedType.substring(1) 12 | const category = catString.split('/') 13 | return `SELECT x.* FROM DBHive.dbo.Comments x WHERE CONTAINS(json_metadata , '3speak/video') AND category LIKE '${ 14 | category[0] 15 | }' ORDER BY ${ 16 | category[1] === 'trending' ? 'total_vote_weight' : 'ID' 17 | } DESC OFFSET 0 ROWS FETCH NEXT 25 ROWS ONLY` 18 | } else { 19 | throw new Error(`Could not get sql for unrecognized feed type ${feedType}`) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/services/accountServices/followHandler.ts: -------------------------------------------------------------------------------- 1 | import hive from '@hiveio/hive-js' 2 | import ArraySearch from 'arraysearch' 3 | export async function followHandler(profileID, followOp) { 4 | const Finder = ArraySearch.Finder 5 | switch (followOp.accountType) { 6 | case 'hive': { 7 | //const profile = await acctOps.getAccount(profileID); 8 | const profile = (await this.getAccount(profileID)) as any 9 | 10 | const username = Finder.one.in(profile.keyring).with({ type: 'hive' }).username 11 | const theWifObj = Finder.one.in(profile.keyring).with({ 12 | type: 'hive', 13 | }) 14 | const wif = theWifObj.privateKeys.posting_key // posting key 15 | const jsonObj = [ 16 | 'follow', 17 | { 18 | follower: username, 19 | following: followOp.author, 20 | what: followOp.what ? [followOp.what] : [], 21 | }, 22 | ] 23 | 24 | // console.log( 25 | hive.broadcast.customJson( 26 | wif, 27 | [], 28 | [username], 29 | 'follow', 30 | JSON.stringify(jsonObj), 31 | async (error, succeed) => { 32 | if (error) { 33 | console.error(error) 34 | console.error('Error encountered broadcsting custom json') 35 | } 36 | 37 | if (succeed) { 38 | console.log(succeed) 39 | console.log('success broadcasting custom json') 40 | } 41 | }, 42 | ) 43 | // ) 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/renderer/views/PinsView/PinCids.tsx: -------------------------------------------------------------------------------- 1 | // PinCids.tsx 2 | import React, { useState } from 'react'; 3 | import ace from 'brace'; 4 | import Popup from 'react-popup' 5 | import 'jsoneditor-react/es/editor.min.css' 6 | import { JsonEditor as Editor } from 'jsoneditor-react' 7 | 8 | const PinCids = ({ pin }) => { 9 | console.log('PinCids', pin); 10 | const [isOpen, setIsOpen] = useState(false); 11 | 12 | const handleClose = () => { 13 | setIsOpen(false); 14 | }; 15 | 16 | const handleOpen = () => { 17 | console.log('handleOpen'); 18 | setIsOpen(true); 19 | }; 20 | 21 | if (pin.cids.length > 1) { 22 | return ( 23 | { 25 | Popup.create({ 26 | title: 'CIDs', 27 | content: ( 28 |
29 | 30 |
31 | ), 32 | buttons: { 33 | left: [], 34 | right: [ 35 | { 36 | text: 'close', 37 | key: '⌘+s', 38 | className: 'success', 39 | action: function () { 40 | Popup.close() 41 | }, 42 | }, 43 | ], 44 | }, 45 | }) 46 | }} 47 | > 48 | View ({pin.cids.length}) 49 |
50 | ); 51 | } else { 52 | return
; 53 | } 54 | }; 55 | 56 | export default PinCids; 57 | -------------------------------------------------------------------------------- /src/renderer/views/BlocklistView.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { Table, Button } from 'react-bootstrap' 3 | import PromiseIpc from 'electron-promise-ipc' 4 | import { NotificationManager } from 'react-notifications' 5 | 6 | export function BlocklistView(props: any) { 7 | const [list, setList] = useState([]) 8 | 9 | const generate = () => { 10 | PromiseIpc.send('blocklist.ls', {} as any).then((value: any) => { 11 | setList(value) 12 | }) 13 | } 14 | 15 | const handleRemove = async (reflink) => { 16 | await PromiseIpc.send('blocklist.rm', reflink) 17 | NotificationManager.success(`Unblocked ${reflink}`) 18 | generate() 19 | } 20 | 21 | useEffect(() => { 22 | document.title = '3Speak - Tokenised video communities' 23 | generate() 24 | }, []) 25 | 26 | return ( 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {list.map((value) => ( 38 | 39 | 40 | 41 | 46 | 47 | ))} 48 | 49 |
ReflinkReasonRemove?
{value._id}{value.reason} 42 | 45 |
50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/renderer/services/ipfs.service.ts: -------------------------------------------------------------------------------- 1 | import { normalizeCidPath } from '../../common/utils/ipfs-utils' 2 | import { IpfsHandler } from '../../main/core/components/ipfsHandler' 3 | import CID from 'cids' 4 | 5 | export class IpfsService { 6 | static gateway = 'https://ipfs.3speak.tv/ipfs/' 7 | static async getGateway(cid: string, bypass) { 8 | if (bypass === true) { 9 | return this.gateway 10 | } 11 | const { ipfs: ipfsInstance } = await IpfsHandler.getIpfs() 12 | let has = false 13 | try { 14 | for await (const pin of ipfsInstance.pin.ls({ 15 | path: cid, 16 | type: 'recursive', 17 | })) { 18 | if (pin.cid.equals(new CID(cid))) { 19 | has = true 20 | break 21 | } 22 | } 23 | } catch (ex) { 24 | console.error(ex) 25 | } 26 | if (has) { 27 | return 'http://localhost:8080/ipfs/' 28 | } else { 29 | return ipfsInstance.gateway 30 | } 31 | } 32 | static urlToIpfsPath(urlString: string) { 33 | const url = new URL(urlString) 34 | if (url.protocol === 'ipfs:' && url.pathname !== '') { 35 | return url.pathname 36 | } else { 37 | return normalizeCidPath(url.href) 38 | } 39 | } 40 | static urlToCID(urlString: string) { 41 | const url = new URL(urlString) 42 | 43 | if (url.protocol === 'ipfs:' && url.pathname !== '') { 44 | return url.hostname 45 | } else { 46 | const ipfsPath = normalizeCidPath(url.href).split('/') 47 | return ipfsPath[0] 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/renderer/components/video/PostComment/CommentForm.tsx: -------------------------------------------------------------------------------- 1 | import randomstring from 'randomstring' 2 | import React, { useCallback, useRef } from 'react' 3 | import RefLink from '../../../../main/RefLink' 4 | import { AccountService } from '../../../services/account.service' 5 | 6 | export function CommentForm(props) { 7 | const commentBodyRef = useRef() as any 8 | const { parent_reflink } = props 9 | const postComment = useCallback(async () => { 10 | const [networkId, parent_author, parent_permlink] = (RefLink.parse(parent_reflink) as any).link 11 | const [reflink, finishOpt] = await AccountService.postComment({ 12 | accountType: 'hive', 13 | body: commentBodyRef.current.value, 14 | parent_author, 15 | parent_permlink, 16 | username: 'sisy', 17 | permlink: `re-${parent_permlink}-${randomstring 18 | .generate({ 19 | length: 8, 20 | charset: 'alphabetic', 21 | }) 22 | .toLowerCase()}`, 23 | title: '', 24 | json_metadata: {}, 25 | }) 26 | if (typeof props.onCommentPost === 'function') { 27 | props.onCommentPost() 28 | } 29 | }, [parent_reflink]) 30 | return ( 31 | <> 32 | 39 | 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/main/RefLink.ts: -------------------------------------------------------------------------------- 1 | export interface RefLinkSource { 2 | value: string 3 | type: string 4 | } 5 | 6 | export default class RefLink { 7 | link: any 8 | source: RefLinkSource 9 | constructor(link) { 10 | this.link = link 11 | 12 | if (this.link[0]) { 13 | const mid = this.link[0] 14 | const source = {} as any 15 | switch (mid[0]) { 16 | case '$': { 17 | source.value = mid.slice(1) 18 | source.type = 'state' 19 | break 20 | } 21 | case '#': { 22 | source.value = mid.slice(1) 23 | source.type = 'tag' 24 | break 25 | } 26 | default: { 27 | source.value = mid 28 | source.type = 'source' 29 | } 30 | } 31 | this.source = source 32 | } 33 | } 34 | get type() { 35 | switch (this.link.length) { 36 | case 3: { 37 | return 'permlink' 38 | } 39 | case 2: { 40 | return 'root' 41 | } 42 | case 1: { 43 | return 'source' 44 | } 45 | } 46 | } 47 | get permlink() { 48 | return this.link[2] 49 | } 50 | get root() { 51 | return this.link[1] 52 | } 53 | toString() { 54 | return this.link.join(':') 55 | } 56 | static isValid(link) { 57 | try { 58 | RefLink.parse(link) 59 | return true 60 | } catch { 61 | return false 62 | } 63 | } 64 | static parse(link) { 65 | if (link instanceof RefLink) { 66 | return link 67 | } 68 | if (typeof link !== 'string') { 69 | throw new Error('Invalid reflink') 70 | } 71 | link = link.split(':') 72 | return new RefLink(link) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/renderer/StartUp.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { Modal } from 'react-bootstrap' 3 | import PromiseIpc from 'electron-promise-ipc' 4 | import './css/Startup.css' 5 | 6 | export function StartUp(props: any) { 7 | const [show, setShow] = useState(false) 8 | const [message, setMessage] = useState('') 9 | const [downloadProgress, setDownloadProgress] = useState(null) 10 | 11 | useEffect(() => { 12 | const load = async () => { 13 | const backendStatus = (await PromiseIpc.send('core.status', undefined as any)) as any 14 | if (backendStatus.ready === false) { 15 | setShow(true) 16 | const pid = setInterval(async () => { 17 | const status = (await PromiseIpc.send('core.status', undefined as any)) as any 18 | setMessage(status.start_progress.message) 19 | setDownloadProgress(status.start_progress.ipfsDownloadPct) 20 | }, 25) 21 | PromiseIpc.send('core.ready', undefined as any).then((eda) => { 22 | setShow(false) 23 | clearInterval(pid) 24 | }) 25 | } 26 | } 27 | 28 | void load() 29 | }, []) 30 | 31 | return ( 32 |
33 | 34 | 35 | App Starting Up 36 | 37 | 38 |
39 |

Loading

40 |
41 |

{message}

42 | {downloadProgress ? `${downloadProgress}%` : null} 43 |
44 |
45 |
46 |
47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/renderer/services/accountServices/login.ts: -------------------------------------------------------------------------------- 1 | import hive from '@hiveio/hive-js' 2 | import PromiseIPC from 'electron-promise-ipc' 3 | import ArraySearch from 'arraysearch' 4 | 5 | const Finder = ArraySearch.Finder 6 | 7 | export async function login(data) { 8 | switch (data.accountType) { 9 | case 'hive': { 10 | const userAccounts = await hive.api.getAccountsAsync([data.username]) 11 | console.log(`got hive account for username ${data.username}`, userAccounts) 12 | const pubWif = userAccounts[0].posting.key_auths[0][0] 13 | 14 | const Valid = hive.auth.wifIsValid(data.key, pubWif) 15 | 16 | if (Valid) { 17 | const profile = { 18 | _id: userAccounts[0].id.toString(), 19 | nickname: data.username, 20 | keyring: [ 21 | { 22 | type: 'hive', 23 | username: data.username, 24 | public: { 25 | pubWif, 26 | }, 27 | encrypted: data.encrypted, 28 | privateKeys: { 29 | posting_key: data.key, 30 | }, 31 | }, 32 | ], 33 | } 34 | const profileID = profile._id 35 | const check_profile = await PromiseIPC.send('accounts.has', profileID) 36 | if (check_profile) { 37 | throw new Error('Account exists already') 38 | } else { 39 | // 2nd argument doesn't match function signature - marking with any 40 | await PromiseIPC.send('accounts.createProfile', profile as any) 41 | const get_profile = await PromiseIPC.send('accounts.get', profileID) 42 | localStorage.setItem('SNProfileID', profileID) 43 | return get_profile 44 | } 45 | } else { 46 | throw new Error('Invalid posting key') 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/renderer/views/PoAView.tsx: -------------------------------------------------------------------------------- 1 | // Filename: PoAView.tsx 2 | import 'brace/mode/json'; 3 | import 'brace/theme/github'; 4 | import 'brace/theme/monokai'; 5 | import 'brace/theme/solarized_dark'; 6 | import 'jsoneditor-react/es/editor.min.css'; 7 | import React, { useEffect } from 'react'; 8 | import { Terminal } from 'xterm'; 9 | import { WebLinksAddon } from 'xterm-addon-web-links'; 10 | import 'xterm/css/xterm.css'; 11 | 12 | import { PoAViewContent } from './PoAView/PoAViewContent'; 13 | import { usePoAInstaller } from './PoAView/usePoAInstaller'; 14 | import { usePoAProgramRunner } from './PoAView/usePoAProgramRunner'; 15 | import { usePoAProgramRunnerContext } from './PoAView/PoAProgramRunnerContext'; 16 | 17 | export function PoAView() { 18 | const { programRunner, setProgramRunner, terminalRef } = usePoAProgramRunnerContext(); 19 | 20 | const updater = usePoAInstaller(); 21 | const { terminal, setTerminal, isPoARunning, runPoA, contextValue, storageSize, autoPin, setAutoPin, setStorageSize } = usePoAProgramRunner(); 22 | const { stopPoA } = contextValue; 23 | useEffect(() => { 24 | if (terminalRef.current && !terminal) { 25 | const term = new Terminal(); 26 | term.open(terminalRef.current); 27 | term.loadAddon(new WebLinksAddon()); 28 | setTerminal(term); 29 | } 30 | 31 | return () => { 32 | if (terminal) { 33 | terminal.dispose(); 34 | } 35 | }; 36 | }, [terminal, terminalRef]); 37 | 38 | return ( 39 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/renderer/assets/img/icon_livestream.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/renderer/components/widgets/CommunityTile.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from 'react' 2 | import { Col } from 'react-bootstrap' 3 | import { FaChevronCircleRight } from 'react-icons/fa' 4 | 5 | import RefLink from '../../../main/RefLink' 6 | import { AccountService } from '../../services/account.service' 7 | 8 | export function CommunityTile(props: any) { 9 | const reflink = useMemo(() => { 10 | return RefLink.parse(props.reflink) 11 | }, [props.reflink]) 12 | 13 | const [communityPicture, setCommunityPicture] = useState('') 14 | 15 | useEffect(() => { 16 | const load = async () => { 17 | setCommunityPicture(await AccountService.getProfilePictureURL(props.reflink)) 18 | } 19 | 20 | void load() 21 | }, []) 22 | 23 | return ( 24 | 25 | 26 |
27 |
28 | 37 | {props.info.title} 38 |
39 |
47 |
48 | 49 | 50 |
51 |
52 |
53 |
54 | 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /src/main/core/components/Blocklist.ts: -------------------------------------------------------------------------------- 1 | import PouchDB from 'pouchdb' 2 | import RefLink from '../../RefLink' 3 | const Path = require('path') 4 | PouchDB.plugin(require('pouchdb-find')) 5 | PouchDB.plugin(require('pouchdb-upsert')) 6 | 7 | /** 8 | * A simple interface to hide/block content from a certain author, a particular permalink or metadata tag. 9 | * (Wrapper datastore-level) 10 | */ 11 | class Blocklist { 12 | pouch: any 13 | self: any 14 | constructor(self) { 15 | this.self = self 16 | 17 | this.pouch = new PouchDB(Path.join(this.self._options.path, 'blocklist.db')) 18 | } 19 | async add(reflink, options = {} as any) { 20 | try { 21 | await this.pouch.put({ 22 | _id: reflink.toString(), 23 | reason: options.reason, 24 | ts: new Date().getTime(), 25 | source: 'manual', 26 | }) 27 | } catch (ex) { 28 | console.error(ex) 29 | throw new Error('Reflink already in blocklist') 30 | } 31 | } 32 | async rm(reflink) { 33 | await this.pouch.upsert(reflink.toString(), (doc) => { 34 | doc._deleted = true 35 | return doc 36 | }) 37 | } 38 | /** 39 | * 40 | * @param {String|RefLink} reflink 41 | */ 42 | async has(reflink) { 43 | if (!(reflink instanceof RefLink)) { 44 | reflink = RefLink.parse(reflink) 45 | } 46 | if (!this.self.config.get('blocklist.enabled')) { 47 | return false 48 | } 49 | try { 50 | await this.pouch.get(reflink.toString()) 51 | return true 52 | } catch {} 53 | try { 54 | await this.pouch.get(`${reflink.source.value}:${reflink.root}`) 55 | return true 56 | } catch {} 57 | return false 58 | } 59 | async ls(query = {} as any) { 60 | query._deleted = { 61 | $exists: false, 62 | } 63 | return ( 64 | await this.pouch.find({ 65 | selector: query, 66 | }) 67 | ).docs 68 | } 69 | async start() {} 70 | } 71 | export default Blocklist 72 | -------------------------------------------------------------------------------- /src/renderer/assets/img/ipfs-logo-vector-ice.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/core/components/ProgramRunner.ts: -------------------------------------------------------------------------------- 1 | // ProgramRunner.ts 2 | import { spawn, SpawnOptions, ChildProcess } from 'child_process'; 3 | import treeKill from 'tree-kill'; 4 | 5 | class ProgramRunner { 6 | private command: string; 7 | private process: ChildProcess | null = null; 8 | private outputHandler: (data: string) => void; 9 | 10 | constructor(command: string, outputHandler: (data: string) => void) { 11 | this.command = command; 12 | this.outputHandler = outputHandler; 13 | } 14 | 15 | public setupProgram(onExit: () => void): void { 16 | console.log(`Setting up command: ${this.command}`); 17 | 18 | const commandParts = this.command.split(' '); 19 | const cmd = commandParts[0]; 20 | const args = commandParts.slice(1); 21 | 22 | const options: SpawnOptions = { 23 | stdio: 'pipe', 24 | detached: true, // This might help in some scenarios to open the program in a new process group. 25 | shell: true // Running in a shell can sometimes help with GUI apps. 26 | }; 27 | 28 | this.process = spawn(cmd, args, options); 29 | 30 | this.process.stdout.on('data', (data) => { 31 | this.outputHandler(data.toString()); 32 | }); 33 | 34 | this.process.stderr.on('data', (data) => { 35 | this.outputHandler(data.toString()); 36 | }); 37 | 38 | this.process.on('exit', () => { 39 | onExit(); 40 | this.process = null; 41 | }); 42 | } 43 | 44 | public isRunning(): boolean { 45 | return this.process && !this.process.killed && this.process.exitCode === null; 46 | } 47 | public stopProgram(): void { 48 | if (this.process) { 49 | try { 50 | treeKill(this.process.pid, 'SIGINT'); 51 | } catch (error) { 52 | if (error.code !== 'ESRCH') { 53 | console.error('Error stopping program:', error); 54 | } 55 | } 56 | this.process = null; 57 | } 58 | } 59 | 60 | public cleanup(): void { 61 | this.stopProgram(); 62 | // Remove event listeners here 63 | } 64 | } 65 | 66 | export default ProgramRunner; 67 | -------------------------------------------------------------------------------- /src/renderer/components/video/CommentSection/CommentForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useRef, useState } from 'react' 2 | import RefLink from '../../../../main/RefLink' 3 | import randomstring from 'randomstring' 4 | import { faSpinner } from '@fortawesome/free-solid-svg-icons' 5 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 6 | import { AccountService } from '../../../services/account.service' 7 | 8 | export function CommentForm(props) { 9 | const [postingStatus, setPostingStatus] = useState(false) 10 | const commentBodyRef = useRef() as any 11 | const { parent_reflink } = props 12 | 13 | const postComment = useCallback(async () => { 14 | setPostingStatus(true) 15 | const [networkId, parent_author, parent_permlink] = (RefLink.parse(parent_reflink) as any).link 16 | const [reflink, finishOpt] = await AccountService.postComment({ 17 | accountType: 'hive', 18 | body: commentBodyRef.current.value, 19 | parent_author, 20 | parent_permlink, 21 | username: 'sisy', 22 | permlink: `re-${parent_permlink}-${randomstring 23 | .generate({ 24 | length: 8, 25 | charset: 'alphabetic', 26 | }) 27 | .toLowerCase()}`, 28 | title: '', 29 | json_metadata: {}, 30 | }) 31 | if (typeof props.onCommentPost === 'function') { 32 | props.onCommentPost() 33 | } 34 | commentBodyRef.current.value = '' 35 | setPostingStatus(false) 36 | }, [parent_reflink]) 37 | return ( 38 | <> 39 | 46 | 54 | 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /src/renderer/services/account.service.ts: -------------------------------------------------------------------------------- 1 | import { convertLight } from './accountServices/convertLight' 2 | import { createPost } from './accountServices/createPost' 3 | import { followHandler } from './accountServices/followHandler' 4 | import { getAccount } from './accountServices/getAccount' 5 | import { getAccountBalances } from './accountServices/getAccountBalances' 6 | import { getAccountMetadata } from './accountServices/getAccountMetadata' 7 | import { getAccounts } from './accountServices/getAccounts' 8 | import { getFollowerCount } from './accountServices/getFollowerCount' 9 | import { getFollowing } from './accountServices/getFollowing' 10 | import { getProfileAbout } from './accountServices/getProfileAbout' 11 | import { getProfileBackgroundImageUrl } from './accountServices/getProfileBackgroundImageUrl' 12 | import { getProfilePictureURL } from './accountServices/getProfilePictureURL' 13 | import { login } from './accountServices/login' 14 | import { logout } from './accountServices/logout' 15 | import { permalinkToPostInfo } from './accountServices/permalinkToPostInfo' 16 | import { permalinkToVideoInfo } from './accountServices/permalinkToVideoInfo' 17 | import { postComment } from './accountServices/postComment' 18 | import { updateMeta } from './accountServices/updateMeta' 19 | import { voteHandler } from './accountServices/voteHandler' 20 | 21 | export class AccountService { 22 | static convertLight = convertLight 23 | static createPost = createPost 24 | static followHandler = followHandler 25 | static getAccount = getAccount 26 | static getAccountBalances = getAccountBalances 27 | static getAccountMetadata = getAccountMetadata 28 | static getAccounts = getAccounts 29 | static getFollowerCount = getFollowerCount 30 | static getFollowing = getFollowing 31 | static getProfileAbout = getProfileAbout 32 | static getProfileBackgroundImageUrl = getProfileBackgroundImageUrl 33 | static getProfilePictureURL = getProfilePictureURL 34 | static login = login 35 | static logout = logout 36 | static permalinkToPostInfo = permalinkToPostInfo 37 | static permalinkToVideoInfo = permalinkToVideoInfo 38 | static postComment = postComment 39 | static updateMeta = updateMeta 40 | static voteHandler = voteHandler 41 | } 42 | -------------------------------------------------------------------------------- /src/renderer/views/UserView.tsx: -------------------------------------------------------------------------------- 1 | // filename: UserView.tsx 2 | import React, { useEffect, useMemo, useState } from 'react'; 3 | import RefLink from '../../main/RefLink'; 4 | import '../css/User.css'; 5 | import { AccountService } from '../services/account.service'; 6 | import { IndexerClient } from '../App'; 7 | import { useQuery } from '@apollo/client'; 8 | import UserViewContent from './UserView/UserViewContent'; 9 | import { QUERY } from './UserView/userQueries'; 10 | import { transformGraphqlToNormal } from './UserView/userUtils'; 11 | /** 12 | * User about page with all the public information a casual and power user would need to see about another user. 13 | */ 14 | export function UserView(props: any) { 15 | const [profileAbout, setProfileAbout] = useState('') 16 | const [hiveBalance, setHiveBalance] = useState() 17 | const [hbdBalance, setHbdBalance] = useState() 18 | const [coverUrl, setCoverUrl] = useState('') 19 | const [profileUrl, setProfileUrl] = useState('') 20 | 21 | const reflink = useMemo(() => { 22 | return RefLink.parse(props.match.params.reflink) 23 | }, [props.match]) 24 | 25 | const username = useMemo(() => { 26 | return reflink.root 27 | }, [reflink]) 28 | 29 | const { data, loading } = useQuery(QUERY, { 30 | variables: { 31 | author: username 32 | }, 33 | client: IndexerClient, 34 | }) 35 | 36 | console.log(data) 37 | const videos = data?.latestFeed?.items || []; 38 | 39 | useEffect(() => { 40 | const load = async () => { 41 | const accountBalances = await AccountService.getAccountBalances(reflink) 42 | setProfileUrl(await AccountService.getProfilePictureURL(reflink)) 43 | setProfileAbout(await AccountService.getProfileAbout(reflink)) 44 | setHiveBalance(accountBalances.hive) 45 | setHbdBalance(accountBalances.hbd) 46 | setCoverUrl(await AccountService.getProfileBackgroundImageUrl(reflink)) 47 | } 48 | 49 | void load() 50 | }, [reflink]) 51 | 52 | return ( 53 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/renderer/components/video/CommentSection/CommentSection.css: -------------------------------------------------------------------------------- 1 | .alert-info { 2 | background: #9C27B0 -webkit-gradient(linear, left top, left bottom, from(#ab47bc), to(#9C27B0)) repeat-x; 3 | background: #9C27B0 linear-gradient( 4 | 180deg 5 | , #ab47bc, #9C27B0) repeat-x; 6 | } 7 | .alert, .alert h1, .alert h2, .alert h3, .alert h4, .alert h5, .alert h6 { 8 | color: #fff; 9 | } 10 | .alert a:not(.btn), .alert .alert-link { 11 | color: #fff; 12 | font-weight: bold; 13 | } 14 | .text-muted { 15 | color: #666 !important; 16 | } 17 | .form-control { 18 | display: block; 19 | width: 100%; 20 | height: calc(2.81875rem + 0rem); 21 | /*padding: 1rem 0;*/ 22 | font-size: 0.8125rem; 23 | font-weight: 400; 24 | line-height: 1.5; 25 | color: #666; 26 | background-color: transparent; 27 | background-clip: padding-box; 28 | border: 0rem solid transparent; 29 | border-radius: 0; 30 | -webkit-transition: border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; 31 | transition: border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; 32 | transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 33 | transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; 34 | } 35 | .w-100 { 36 | width: 100% !important; 37 | } 38 | textarea, textarea.form-control, input.form-control, input[type=text], input[type=password], input[type=email], input[type=number], [type=text].form-control, [type=password].form-control, [type=email].form-control, [type=tel].form-control, [contenteditable].form-control { 39 | -webkit-box-shadow: inset 0 -1px 0 #ddd; 40 | box-shadow: inset 0 -1px 0 #ddd; 41 | -webkit-transition: all 0.2s; 42 | transition: all 0.2s; 43 | } 44 | .form-control { 45 | background: #eceff0 none repeat scroll 0 0; 46 | border-color: #dcdfdf; 47 | border-radius: 2px; 48 | font-size: 13px; 49 | } 50 | textarea { 51 | overflow: auto; 52 | resize: vertical; 53 | } 54 | textarea.form-control { 55 | height: auto; 56 | } 57 | 58 | 59 | .list-inline-item { 60 | display: inline-block; 61 | } 62 | .list-inline { 63 | padding-left: 0; 64 | list-style: none; 65 | } 66 | .list-inline-item:not(:last-child) { 67 | margin-right: 0.5rem; 68 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md 2 | // TODO - we want react hooks / exhaustive deps eslint rule...figure out why I have to import to get those 3 | module.exports = { 4 | root: true, 5 | parser: '@typescript-eslint/parser', 6 | parserOptions: { 7 | tsconfigRootDir: __dirname, 8 | project: ['./tsconfig.base.json'], 9 | }, 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:prettier/recommended', 13 | 'plugin:@typescript-eslint/eslint-recommended', 14 | 'plugin:@typescript-eslint/recommended', 15 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 16 | ], 17 | plugins: ['@typescript-eslint', 'prettier'], 18 | rules: { 19 | 'prettier/prettier': 'warn', 20 | '@typescript-eslint/camelcase': 'off', 21 | '@typescript-eslint/no-unused-vars': 'off', 22 | '@typescript-eslint/explicit-function-return-type': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | '@typescript-eslint/restrict-plus-operands': 'off', 25 | '@typescript-eslint/no-unsafe-member-access': 'off', 26 | '@typescript-eslint/no-unsafe-call': 'off', 27 | '@typescript-eslint/no-unsafe-assignment': 'off', 28 | '@typescript-eslint/no-unsafe-return': 'off', 29 | '@typescript-eslint/explicit-module-boundary-types': 'off', 30 | '@typescript-eslint/require-await': 'off', 31 | '@typescript-eslint/no-empty-function': 'off', 32 | '@typescript-eslint/ban-ts-comment': 'off', 33 | '@typescript-eslint/no-unnecessary-type-assertion': 'off', 34 | '@typescript-eslint/restrict-template-expressions': 'off', 35 | '@typescript-eslint/no-var-requires': 'warn', 36 | '@typescript-eslint/no-floating-promises': 'warn', 37 | '@typescript-eslint/prefer-regexp-exec': 'off', 38 | '@typescript-eslint/no-misused-promises': 'warn', 39 | '@typescript-eslint/ban-types': 'warn', 40 | '@typescript-eslint/no-non-null-assertion': 'warn', 41 | '@typescript-eslint/unbound-method': 'off', 42 | '@typescript-eslint/no-unsafe-argument': 'warn', 43 | 'prefer-const': 'warn', 44 | '@typescript-eslint/no-this-alias': 'warn', 45 | 'no-async-promise-executor': 'warn', 46 | 'no-empty': 'warn', 47 | 'no-prototype-builtins': 'off', 48 | 'no-constant-condition': 'warn', 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /src/renderer/components/widgets/LeaderTile.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from 'react' 2 | import RefLink from '../../../main/RefLink' 3 | import { AccountService } from '../../services/account.service' 4 | 5 | export interface LeaderTileProps { 6 | info: { 7 | score: number 8 | rank: number 9 | } 10 | reflink: string 11 | } 12 | 13 | export function LeaderTile(props: LeaderTileProps) { 14 | const reflink = useMemo(() => { 15 | return RefLink.parse(props.reflink) 16 | }, [props.reflink]) 17 | 18 | const [profilePicture, setProfilePicture] = useState('') 19 | const [borderLeftCode, setBorderLeftCode] = useState('') 20 | 21 | useEffect(() => { 22 | const load = async () => { 23 | console.log(`displaying leader tile`, props) 24 | let color: string 25 | switch (props.info.rank) { 26 | case 1: { 27 | color = '#d4af37 solid 6px' 28 | break 29 | } 30 | case 2: { 31 | color = '#bec2cb solid 6px' 32 | break 33 | } 34 | case 3: { 35 | color = '#b08d57 solid 6px' 36 | break 37 | } 38 | } 39 | 40 | setProfilePicture(await AccountService.getProfilePictureURL(props.reflink)) 41 | setBorderLeftCode(color) 42 | } 43 | 44 | void load() 45 | }, []) 46 | 47 | return ( 48 |
49 |
50 | 51 | 52 | 53 | 58 |
59 |
60 |
61 | {reflink.root} 62 |
63 |
64 | 65 |
66 |
67 |
Rank: {props.info.rank}
68 |
69 | Score: {Math.round(props.info.score)} 70 |
71 |
72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /src/renderer/css/App.css: -------------------------------------------------------------------------------- 1 | .box { 2 | padding: 15px; 3 | background: #fff none repeat scroll 0 0; 4 | border-radius: 2px; 5 | box-shadow: 0 0 11px #ececec; 6 | transition-duration: 0.4s; 7 | } 8 | .float-left { 9 | float: left !important; 10 | } 11 | .float-right { 12 | float: right !important; 13 | } 14 | .clearfix::after { 15 | display: block; 16 | clear: both; 17 | content: ""; 18 | } 19 | .single-video-author img { 20 | border-radius: 50px; 21 | float: left; 22 | height: 38px; 23 | margin: 0 13px 0 0; 24 | width: 38px; 25 | } 26 | .single-video-author p { 27 | margin: 0; 28 | padding: 0; 29 | } 30 | .btn { 31 | text-transform: uppercase; 32 | border: none; 33 | -webkit-box-shadow: 0 1px 4px rgba(0,0,0,0.4); 34 | box-shadow: 0 1px 4px rgba(0,0,0,0.4); 35 | -webkit-transition: all 0.4s; 36 | transition: all 0.4s; 37 | } 38 | .btn-sm, .btn-group-sm>.btn { 39 | padding: 0.25rem 0.5rem; 40 | font-size: 0.7109375rem; 41 | line-height: 1.5; 42 | border-radius: 0.2rem; 43 | } 44 | .btn-light { 45 | position: relative; 46 | } 47 | .btn-light { 48 | color: #212121; 49 | background: #fff -webkit-gradient(linear, left top, left bottom, from(white), to(#fff)) repeat-x; 50 | background: #fff linear-gradient(180deg, white, #fff) repeat-x; 51 | border-color: #fff; 52 | } 53 | .video-block .mb-3 { 54 | margin-bottom: 30px !important; 55 | } 56 | a { 57 | cursor: pointer; 58 | color: black; 59 | } 60 | .ml-2, .mx-2 { 61 | margin-left: 0.5rem !important; 62 | } 63 | #videoAbout { 64 | max-height: 200px; 65 | overflow: hidden; 66 | } 67 | .tags span a { 68 | background: #ccc none repeat scroll 0 0; 69 | border-radius: 2px; 70 | color: #fff; 71 | display: inline-block; 72 | padding: 4px 9px; 73 | } 74 | body { 75 | font-size: 13px; 76 | font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 77 | } 78 | h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { 79 | margin-bottom: 0.5rem; 80 | font-family: inherit; 81 | font-weight: 500; 82 | line-height: 1.2; 83 | color: inherit; 84 | } 85 | h1 { 86 | font-size: 39px; 87 | text-transform: uppercase; 88 | line-height: 38px; 89 | font-weight: 200; 90 | } 91 | .videowidget-padding { 92 | padding: 1px !important 93 | } -------------------------------------------------------------------------------- /src/renderer/views/UploaderView/publish.ts: -------------------------------------------------------------------------------- 1 | import { AccountService } from '../../services/account.service' 2 | import randomstring from 'randomstring' 3 | import { compileVideoCid } from './compileVideoCid' 4 | export const publish = async ({videoInfo, thumbnailInfo, publishFormTitle, publishFormDescription, publishFormTags, setBlockedGlobalMessage, ipfs}) => { 5 | const videoCid = await compileVideoCid(videoInfo, thumbnailInfo, ipfs) 6 | // const formData = FormUtils.formToObj(new FormData(publishForm)) 7 | 8 | let tags: string[] = [] 9 | if (publishFormTags) { 10 | tags = publishFormTags.replace(/\s/g, '').split(',') 11 | } 12 | 13 | console.log(`thumbnail info`, thumbnailInfo) 14 | 15 | const sourceMap = [] 16 | if (thumbnailInfo.path) { 17 | sourceMap.push({ 18 | type: 'thumbnail', 19 | url: `ipfs://${videoCid}/${thumbnailInfo.path}`, 20 | }) 21 | } 22 | 23 | if (videoInfo) { 24 | sourceMap.push({ 25 | type: 'video', 26 | url: `ipfs://${videoCid}/${videoInfo.path}`, 27 | format: 'm3u8', 28 | }) 29 | } 30 | const permlink = `speak-${randomstring 31 | .generate({ 32 | length: 8, 33 | charset: 'alphabetic', 34 | }) 35 | .toLowerCase()}` 36 | // console.log(permlink) 37 | console.log(`source map`) 38 | console.log(sourceMap) 39 | 40 | setBlockedGlobalMessage('Publishing') 41 | 42 | const filesize = videoInfo.size + thumbnailInfo.size 43 | 44 | try { 45 | const [reflink] = await AccountService.postComment({ 46 | accountType: 'hive', 47 | title: publishFormTitle || 'Untitled video', 48 | body: publishFormDescription || '', 49 | permlink, 50 | tags, 51 | json_metadata: { 52 | title: publishFormTitle || 'Untitled video', 53 | description: publishFormDescription || '', 54 | tags, 55 | sourceMap, 56 | filesize, 57 | created: new Date(), 58 | lang: videoInfo.language, 59 | video: { 60 | duration: videoInfo.duration, 61 | }, 62 | app: '3speak/app-beta', 63 | type: '3speak/video', 64 | }, 65 | }) 66 | 67 | setTimeout(() => { 68 | location.hash = `#/watch/${reflink}` 69 | setBlockedGlobalMessage('done') 70 | }, 15000) 71 | } catch (error) { 72 | console.error(`Error in postComment operation ${error.message}`) 73 | throw error 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /src/renderer/views/WatchView/watchViewHelpers/retrieveRecommended.ts: -------------------------------------------------------------------------------- 1 | import { knex } from '../../../singletons/knex.singleton'; 2 | import PromiseIpc from 'electron-promise-ipc'; 3 | import ArraySearch from 'arraysearch'; 4 | const Finder = ArraySearch.Finder; 5 | 6 | export async function retrieveRecommended( 7 | postInfo: any, 8 | setRecommendedVideos: (videos: any[]) => void 9 | ) { 10 | const query = knex.raw( 11 | `SELECT TOP 25 x.* FROM DBHive.dbo.Comments x WHERE CONTAINS(json_metadata , '3speak/video') AND category LIKE '${postInfo.category}' ORDER BY NEWID()`, 12 | ); 13 | const blob = []; 14 | query.stream().on('data', async (val) => { 15 | if ( 16 | await PromiseIpc.send( 17 | 'blocklist.has', 18 | `hive:${val.author}:${val.permlink}` as any 19 | ) 20 | ) { 21 | console.log(`${val.author} is blocked`); 22 | return; 23 | } 24 | val.json_metadata = JSON.parse(val.json_metadata); 25 | //console.log(val) 26 | if (!val.json_metadata.video) { 27 | val.json_metadata.video = { 28 | info: {}, 29 | }; 30 | } 31 | let thumbnail; 32 | if (val.json_metadata.sourceMap) { 33 | thumbnail = Finder.one 34 | .in(val.json_metadata.sourceMap) 35 | .with({ type: 'thumbnail' }).url; 36 | console.log(thumbnail); 37 | } 38 | blob.push({ 39 | reflink: `hive:${val.author}:${val.permlink}`, 40 | created: val.created, 41 | author: val.author, 42 | permlink: val.permlink, 43 | tags: val.json_metadata.tags, 44 | title: val.title, 45 | duration: 46 | val.json_metadata.video.info.duration || 47 | val.json_metadata.video.duration, 48 | isIpfs: val.json_metadata.video.info.ipfs || thumbnail ? true : false, 49 | ipfs: val.json_metadata.video.info.ipfs, 50 | images: { 51 | ipfs_thumbnail: thumbnail 52 | ? `/ipfs/${thumbnail.slice(7)}` 53 | : `/ipfs/${val.json_metadata.video.info.ipfsThumbnail}`, 54 | thumbnail: `https://threespeakvideo.b-cdn.net/${val.permlink}/thumbnails/default.png`, 55 | poster: `https://threespeakvideo.b-cdn.net/${val.permlink}/poster.png`, 56 | post: `https://threespeakvideo.b-cdn.net/${val.permlink}/post.png`, 57 | }, 58 | views: val.total_vote_weight 59 | ? Math.log(val.total_vote_weight / 1000).toFixed(2) 60 | : 0, 61 | }); 62 | 63 | setRecommendedVideos(blob); 64 | }); 65 | query.on('query-response', (ret, det, aet) => { 66 | console.log(ret, det, aet); 67 | }); 68 | query.on('end', (err) => { 69 | console.log(err); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /src/main/core/ffmpeg_helper.ts: -------------------------------------------------------------------------------- 1 | // let os = require('os'); 2 | import os from 'os' 3 | const fs = require('fs') 4 | const path = require('path') 5 | const appRoot = require('app-root-path') 6 | function verifyFile(file) { 7 | try { 8 | const stats = fs.statSync(file) 9 | return stats.isFile() 10 | } catch (ignored) { 11 | return false 12 | } 13 | } 14 | export function GetFfmpegPath() { 15 | const platform = os.platform() + '-' + os.arch() 16 | 17 | const packageName = '@ffmpeg-installer/' + platform 18 | /*if (!require('@ffmpeg-installer/ffmpeg/package.json').optionalDependencies[packageName]) { 19 | throw 'Unsupported platform/architecture: ' + platform 20 | }*/ 21 | 22 | const appRootPath = appRoot.path //.split(require('path').sep).join(path.posix.sep); 23 | 24 | const binary = os.platform() === 'win32' ? 'ffmpeg.exe' : 'ffmpeg' 25 | 26 | const topLevelPath = path.resolve( 27 | appRootPath.substr(0, appRootPath.indexOf('node_modules')), 28 | 'node_modules', 29 | '@ffmpeg-installer', 30 | platform, 31 | ) 32 | const npm3Path = path.resolve(appRootPath, '..', platform) 33 | const npm2Path = path.resolve(appRootPath, 'node_modules', '@ffmpeg-installer', platform) 34 | const npm4Path = path.resolve(appRootPath, '..', 'node_modules', '@ffmpeg-installer', platform) 35 | 36 | const topLevelBinary = path.join(topLevelPath, binary) 37 | const npm3Binary = path.join(npm3Path, binary) 38 | const npm2Binary = path.join(npm2Path, binary) 39 | const npm4Binary = path.join(npm4Path, binary) 40 | 41 | let ffmpegPath, packageJson 42 | 43 | if (verifyFile(npm3Binary)) { 44 | ffmpegPath = npm3Binary 45 | const topLevelPackage = `${npm3Path}/package.json` 46 | packageJson = JSON.parse(fs.readFileSync(topLevelPackage)) 47 | } else if (verifyFile(npm2Binary)) { 48 | ffmpegPath = npm2Binary 49 | const topLevelPackage = `${npm2Path}/package.json` 50 | packageJson = JSON.parse(fs.readFileSync(topLevelPackage)) 51 | } else if (verifyFile(topLevelBinary)) { 52 | ffmpegPath = topLevelBinary 53 | const topLevelPackage = `${topLevelPath}/package.json` 54 | packageJson = JSON.parse(fs.readFileSync(topLevelPackage)) 55 | } else if (verifyFile(npm4Binary)) { 56 | ffmpegPath = npm4Binary 57 | const topLevelPackage = `${npm4Path}/package.json` 58 | packageJson = JSON.parse(fs.readFileSync(topLevelPackage)) //Fix for webpack in production 59 | } else { 60 | throw ( 61 | 'Could not find ffmpeg executable, tried "' + 62 | npm4Binary + 63 | '", "' + 64 | npm3Binary + 65 | '", "' + 66 | npm2Binary + 67 | '" and "' + 68 | topLevelBinary + 69 | '"' 70 | ) 71 | } 72 | return ffmpegPath 73 | } 74 | -------------------------------------------------------------------------------- /src/renderer/views/IpfsConsoleView/IpfsStatsView.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react' 2 | import { Button, Table } from 'react-bootstrap' 3 | 4 | import { bytesAsString } from '../../../common/utils/unit-conversion.functions' 5 | import { IpfsHandler } from '../../../main/core/components/ipfsHandler' 6 | 7 | // import byteSize from 'byte-size' 8 | const { shell } = require('electron') 9 | 10 | export function IpfsStatsView() { 11 | const [stats, setStats] = useState({} as any) 12 | const [repoSize, setRepoSize] = useState('') 13 | const [repoPath, setRepoPath] = useState('') 14 | const loopPid = useRef() 15 | 16 | const update = async () => { 17 | const { ipfs } = await IpfsHandler.getIpfs() 18 | const out = {} 19 | for await (const theStats of ipfs.stats.bw()) { 20 | for (const key of Object.keys(theStats)) { 21 | const stat = Number(theStats[key]) 22 | out[key] = bytesAsString(stat) 23 | } 24 | } 25 | const repoRes = await ipfs.stats.repo() 26 | setRepoPath(repoRes.path) 27 | setRepoSize(bytesAsString(Number(repoRes.repoSize))) 28 | setStats(out) 29 | } 30 | 31 | useEffect(() => { 32 | loopPid.current = setInterval(update, 500) 33 | 34 | return () => { 35 | clearInterval(loopPid.current) 36 | } 37 | }, []) 38 | 39 | return ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 59 | 62 | 65 | 66 | , 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 91 | 92 | 93 |
#InOutTotal InTotal Out
54 | {stats.rateIn} /s 55 | 57 | {stats.rateOut} /s 58 | 60 | {stats.totalIn} 61 | 63 | {stats.totalOut} 64 |
#Repo SizeRepo Path
{repoSize} 80 | {repoPath} 81 | 90 |
94 | ) 95 | } 96 | -------------------------------------------------------------------------------- /src/main/core/components/Logger.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston' 2 | import fs from 'fs' 3 | import Path from 'path' 4 | import DailyRotateFile from 'winston-daily-rotate-file' 5 | 6 | export function MakeLogger(workingDirectory: string): winston.Logger { 7 | if (!fs.existsSync(workingDirectory)) { 8 | fs.mkdirSync(workingDirectory) 9 | } 10 | const Logger = winston.createLogger({ 11 | level: 'info', 12 | format: winston.format.combine( 13 | winston.format.timestamp({ format: 'YYYY/MM/DD HH:mm:ss' }), 14 | winston.format.printf((info) => { 15 | return `${info.timestamp} [${info.level}]: ${info.message}` 16 | }), 17 | ), 18 | defaultMeta: {}, 19 | transports: [ 20 | // - Write all logs with level `error` and below to `error.log` 21 | // - Write all logs with level `info` and below to `combined.log` 22 | new DailyRotateFile({ 23 | filename: Path.join(workingDirectory, 'logs', 'error'), 24 | datePattern: 'YYYY-MM-DD', 25 | extension: '.log', 26 | level: 'error', 27 | zippedArchive: true, 28 | maxSize: '20m', 29 | maxFiles: '31d', 30 | format: winston.format.combine( 31 | winston.format.timestamp({ format: 'YYYY/MM/DD HH:mm:ss' }), 32 | winston.format.printf((info) => { 33 | if (info.jse_info) { 34 | info.jse_info = Object.values(info.jse_info).join('') 35 | } 36 | return `${info.timestamp} [${info.level}]: ${JSON.stringify(info)}` 37 | }), 38 | ), 39 | }), 40 | new winston.transports.DailyRotateFile({ 41 | filename: Path.join(workingDirectory, 'logs', 'info'), 42 | datePattern: 'YYYY-MM-DD', 43 | extension: '.log', 44 | level: 'info', 45 | zippedArchive: true, 46 | maxSize: '20m', 47 | maxFiles: '31d', 48 | }), 49 | new winston.transports.DailyRotateFile({ 50 | filename: Path.join(workingDirectory, 'logs', 'verbose'), 51 | datePattern: 'YYYY-MM-DD', 52 | extension: '.log', 53 | level: 'verbose', 54 | zippedArchive: true, 55 | maxSize: '20m', 56 | maxFiles: '31d', 57 | }), 58 | ], 59 | }) 60 | 61 | // If we're not in production then log to the `console` with the format: 62 | // `${info.level}: ${info.message} JSON.stringify({ ...rest }) ` 63 | if (process.env.NODE_ENV !== 'production') { 64 | Logger.add( 65 | new winston.transports.Console({ 66 | format: winston.format.combine( 67 | winston.format.timestamp(), 68 | winston.format.colorize(), 69 | winston.format.simple(), 70 | ), 71 | }), 72 | ) 73 | } 74 | Logger.info('Logger Starting Up') 75 | return Logger 76 | } 77 | -------------------------------------------------------------------------------- /src/renderer/components/Navbar.css: -------------------------------------------------------------------------------- 1 | .navbar-light .navbar-nav .nav-link { 2 | color: rgba(0,0,0,0.5); 3 | } 4 | .navbar-nav .nav-link { 5 | padding-top: 0.9rem; 6 | padding-bottom: 0.9rem; 7 | } 8 | .navbar-nav .nav-link { 9 | padding-right: 0; 10 | padding-left: 0; 11 | } 12 | .nav-link { 13 | display: block; 14 | padding: 0.5rem 1rem; 15 | } 16 | .navbar-nav { 17 | display: -webkit-box; 18 | display: -ms-flexbox; 19 | display: flex; 20 | -webkit-box-orient: vertical; 21 | -webkit-box-direction: normal; 22 | -ms-flex-direction: column; 23 | flex-direction: column; 24 | padding-left: 0; 25 | margin-bottom: 0; 26 | list-style: none; 27 | } 28 | .nav_dist { 29 | margin-top: 20px; 30 | } 31 | .nav_icons { 32 | width: 30px; 33 | margin-right: 15px; 34 | text-align: center; 35 | display: inline-block; 36 | } 37 | 38 | .display-mobile { 39 | display: none; 40 | } 41 | @media (max-width: 768px) { 42 | .display-wide { 43 | display: none; 44 | } 45 | .display-mobile { 46 | display: block; 47 | } 48 | } 49 | 50 | /* 51 | Navbar fixed left 52 | */ 53 | .navbar.fixed-left { 54 | position: fixed; 55 | top: 0; 56 | left: 0; 57 | right: 0; 58 | z-index: 1030; 59 | } 60 | 61 | @media (min-width: 768px) { 62 | .navbar.fixed-left { 63 | bottom: 0; 64 | width: 232px; 65 | flex-flow: column nowrap; 66 | align-items: flex-start; 67 | } 68 | .navbar.fixed-left .navbar-collapse { 69 | flex-grow: 0; 70 | flex-direction: column; 71 | width: 100%; 72 | } 73 | .navbar-expand-md .navbar-collapse { 74 | display: -webkit-box !important; 75 | display: -ms-flexbox !important; 76 | display: flex !important; 77 | -ms-flex-preferred-size: auto; 78 | flex-basis: auto; 79 | } 80 | .navbar.fixed-left .navbar-collapse .navbar-nav { 81 | flex-direction: column; 82 | width: 100%; 83 | } 84 | } 85 | 86 | .dropdown-toggle::after { 87 | display: none; 88 | } 89 | .navbar-brand img { 90 | width: 100%; 91 | height: 240px; 92 | margin: -90px 0 0 -10px; 93 | } 94 | .navbar-brand { 95 | width: 180px; 96 | height: 60px; 97 | overflow: none; 98 | } 99 | body { 100 | padding-top: 80px; 101 | } 102 | @media (min-width: 768px) { 103 | .navbar.fixed-left { 104 | bottom: 0; 105 | width: 232px; 106 | flex-flow: column nowrap; 107 | align-items: flex-start; 108 | } 109 | .navbar.fixed-left { 110 | right: auto; 111 | } 112 | body { 113 | padding-top: 0; 114 | margin-left: 232px; 115 | } 116 | } -------------------------------------------------------------------------------- /src/renderer/components/video/VideoTeaser.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from 'react' 2 | import { FaCalendarAlt, FaEye } from 'react-icons/fa' 3 | import Reflink from '../../../main/RefLink' 4 | import convert from 'convert-units' 5 | import DateTime from 'date-and-time' 6 | import { AccountService } from '../../services/account.service' 7 | import { VideoService } from '../../services/video.service' 8 | 9 | export function VideoTeaser(props: any) { 10 | const [video_info, setVideoInfo] = useState({}) 11 | const [thumbnail, setThumbnail] = useState('') 12 | const reflink = useMemo(() => { 13 | return Reflink.parse(props.reflink) 14 | }, []) 15 | 16 | useEffect(() => { 17 | const load = async () => { 18 | setVideoInfo(await AccountService.permalinkToVideoInfo(props.reflink)) 19 | // setThumbnail(await VideoService.getNewThumbnailURL(...(props.reflink as any).split(':').slice(1))) 20 | } 21 | 22 | void load() 23 | }, []) 24 | 25 | return ( 26 |
27 |
28 |
29 | {(() => { 30 | const pattern = DateTime.compile('mm:ss') 31 | return DateTime.format(new Date(video_info.meta.duration * 1000), pattern) 32 | })()} 33 |
34 | 35 | 36 | 37 |
38 | 39 | 47 |
48 | {reflink.root} 49 |
50 |
51 | Unknown views 52 | 53 | 54 | {(() => { 55 | if (video_info.creation) { 56 | const dateBest = convert( 57 | (new Date(new Date().toUTCString()) as any) / 1 - 58 | Date.parse(video_info.creation) / 1, 59 | ) 60 | .from('ms') 61 | .toBest() 62 | if (Math.round(dateBest.val) >= 2) { 63 | return `${Math.round(dateBest.val)} ${dateBest.plural} ago` 64 | } else { 65 | return `${Math.round(dateBest.val)} ${dateBest.singular} ago` 66 | } 67 | } 68 | })()} 69 | 70 |
71 |
72 |
73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /src/renderer/views/LeaderboardView.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { Row, Col, Container } from 'react-bootstrap' 3 | import axios from 'axios' 4 | import { LeaderTile } from '../components/widgets/LeaderTile' 5 | 6 | export function LeaderboardView() { 7 | const [first, setFirst] = useState() 8 | const [second, setSecond] = useState() 9 | const [third, setThird] = useState() 10 | const [bronze, setBronze] = useState([]) 11 | 12 | useEffect(() => { 13 | document.title = '3Speak - Tokenised video communities' 14 | void load() 15 | 16 | async function load() { 17 | const data = (await axios.get('https://3speak.tv/apiv2/leaderboard')).data 18 | let step = 1 19 | let bronzeToSet = []; 20 | for (const ex of data) { 21 | if (step >= 30) { 22 | break 23 | } 24 | if (step === 1) { 25 | setFirst(ex) 26 | } else if (step === 2) { 27 | setSecond(ex) 28 | } else if (step === 3) { 29 | setThird(ex) 30 | } else { 31 | bronzeToSet.push(ex); 32 | } 33 | step++ 34 | } 35 | setBronze(bronzeToSet); 36 | } 37 | }, []) 38 | 39 | useEffect(() => { 40 | console.log(`first is now `, first) 41 | }, [first]) 42 | 43 | return ( 44 |
45 |
46 | 47 |
48 |
49 |

Content Creator Leaderboard

50 |
51 |
52 |
53 |
54 |
55 | 56 | 57 |
58 | {first && } 59 |
60 |
61 | 62 |
63 | {second ? : null} 64 |
65 |
66 | {third ? : null} 67 |
68 | 69 | {bronze.map((value) => ( 70 |
71 | 72 |
73 | ))} 74 |
75 |
76 |
77 |
78 |
79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /test/main.core.ts: -------------------------------------------------------------------------------- 1 | import { CoreService } from '../src/main/core' 2 | const tmp = require('tmp') 3 | const assert = require('assert') 4 | /** 5 | * @type {CoreService} 6 | */ 7 | let client 8 | describe('Main', function () { 9 | this.timeout(300000) 10 | this.beforeAll(async () => { 11 | // 12 | const testDir = tmp.dirSync().name 13 | client = new CoreService({ 14 | path: testDir, 15 | }) 16 | await client.start() 17 | //populate 18 | await client.blocklist.add('hive:username:blockedpost1') 19 | await client.blocklist.add('hive:blockeduser') 20 | }) 21 | describe('DistillerDB', function () { 22 | afterEach(async () => { 23 | await client.distillerDB.pouch.compact() 24 | }) 25 | it('Fetch content', async () => { 26 | //console.log(client.distillerDB) 27 | await client.distillerDB.getContent( 28 | 'hive:evagavilan:una-que-otra-bicicleta-una-foto-cada-dia-183-366', 29 | ) 30 | }) 31 | it('Fetch content cached', async () => { 32 | //console.log(client.distillerDB) 33 | await client.distillerDB.getContent( 34 | 'hive:evagavilan:una-que-otra-bicicleta-una-foto-cada-dia-183-366', 35 | ) 36 | }) 37 | it('Fetch tags', async () => { 38 | //console.log(client.distillerDB) 39 | await client.distillerDB.getTag('3speak') 40 | }) 41 | it('Fetch tags cached', async () => { 42 | //console.log(client.distillerDB) 43 | await client.distillerDB.getTag('3speak') 44 | }) 45 | it('Fetch children initial', async () => { 46 | await client.distillerDB.getChildren('hive:evagavilan:hnrhelpe') 47 | }) 48 | it('Fetch children cached', async () => { 49 | await client.distillerDB.getChildren('hive:evagavilan:hnrhelpe') 50 | }) 51 | it('Fetch account', async () => { 52 | await client.distillerDB.getAccount('hive:evagavilan') 53 | }) 54 | it('Fetch account cached', async () => { 55 | await client.distillerDB.getAccount('hive:evagavilan') 56 | }) 57 | it('Fetch posts', async () => { 58 | await client.distillerDB.getPosts('hive:evagavilan') 59 | }) 60 | it('Fetch posts cached', async () => { 61 | await client.distillerDB.getPosts('hive:evagavilan') 62 | }) 63 | }) 64 | describe('Blocklist', function () { 65 | it('Check blocked post', async () => { 66 | assert.strictEqual(await client.blocklist.has('hive:username:blockedpost1'), true) 67 | }) 68 | it('Checked unblocked post', async () => { 69 | assert.strictEqual(await client.blocklist.has('hive:username:unblockedpost1'), false) 70 | }) 71 | it('Checked blocked author', async () => { 72 | assert.strictEqual(await client.blocklist.has('hive:blockeduser:post1'), true) 73 | }) 74 | it('Checked unblocked author', async () => { 75 | assert.strictEqual(await client.blocklist.has('hive:username:post1'), false) 76 | }) 77 | }) 78 | this.afterAll(async () => { 79 | await client.stop() 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /src/renderer/views/PoAView/usePoAProgramRunner.ts: -------------------------------------------------------------------------------- 1 | // Type: file name: src\renderer\views\PoAView\usePoAProgramRunner.ts 2 | import { usePoAState } from './PoAStateContext'; 3 | import React, { useState, useRef, useEffect, useCallback } from 'react'; 4 | import ProgramRunner from '../../../main/core/components/ProgramRunner'; 5 | import PoAInstaller from '../../../main/AutoUpdaterPoA'; 6 | import { Terminal } from 'xterm'; 7 | import Path from 'path'; 8 | import os from 'os'; 9 | import ArraySearch from 'arraysearch'; 10 | import PromiseIPC from 'electron-promise-ipc'; 11 | 12 | const isWin = process.platform === 'win32'; 13 | export const usePoAProgramRunner = () => { 14 | const [terminal, setTerminal] = useState(null); 15 | const [isPoARunning, setIsPoARunning] = useState(false); 16 | const runner = useRef(null); 17 | const poaInstaller = useRef(new PoAInstaller()); 18 | const Finder = ArraySearch.Finder; 19 | const { logs, setLogs } = usePoAState(); 20 | const isMountedRef = useRef(true); 21 | const [autoPin, setAutoPin] = useState(false); 22 | const [storageSize, setStorageSize] = useState(0); 23 | 24 | const runPoA = useCallback(async () => { 25 | let command = ''; // Define command here 26 | if (!isPoARunning) { 27 | const profileID = localStorage.getItem('SNProfileID'); 28 | const getAccount = (await PromiseIPC.send('accounts.get', profileID as any)) as any; 29 | const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }); 30 | const installDir = Path.join(os.homedir(), (await poaInstaller.current.getDefaultPath()) || ''); 31 | const executablePath = isWin ? Path.join(installDir, 'PoA.exe') : Path.join(installDir, 'PoA'); 32 | command = `"${executablePath}" -node=2 -username=${hiveInfo.username} -useWS=true -IPFS_PORT=5004`; 33 | console.log(command); 34 | console.log(autoPin); 35 | console.log(storageSize); 36 | if (autoPin) { 37 | command += " -getVids=true -pinVideos=true"; 38 | if (storageSize > 0) { 39 | command += ` -storageLimit=${storageSize}`; 40 | } 41 | } 42 | console.log(command); 43 | if (!runner.current) { 44 | runner.current = new ProgramRunner(command, (data: string) => { 45 | if (!isMountedRef.current) return; 46 | const logData = data.replace(/\n/g, '\r\n'); 47 | terminal?.write(logData); 48 | setLogs(prevLogs => [...prevLogs, logData]); 49 | }); 50 | } 51 | 52 | runner.current.setupProgram(() => { 53 | if (!isMountedRef.current) return; 54 | setIsPoARunning(false); 55 | }); 56 | setIsPoARunning(true); 57 | } else { 58 | runner.current.stopProgram(); 59 | setIsPoARunning(false); 60 | } 61 | }, [terminal, isPoARunning, autoPin, storageSize]); 62 | 63 | const contextValue = { 64 | isPoARunning, 65 | setIsPoARunning, 66 | runPoA, 67 | stopPoA: () => runner.current?.stopProgram(), 68 | }; 69 | 70 | return { terminal, setTerminal, isPoARunning, runPoA, contextValue, setAutoPin, setStorageSize, storageSize, autoPin }; 71 | } 72 | -------------------------------------------------------------------------------- /src/renderer/components/video/CommentSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from 'react' 2 | import PromiseIpc from 'electron-promise-ipc' 3 | import './CommentSection/CommentSection.css' 4 | import { ConnectAccountNotice } from './CommentSection/ConnectAccountNotice' 5 | import { CommentForm } from './CommentSection/CommentForm' 6 | import { NoComments } from './CommentSection/NoComments' 7 | import { AccountService } from '../../services/account.service' 8 | import { PostComment } from './PostComment' 9 | 10 | export function CommentSection(props: any) { 11 | const [commentGraph, setCommentGraph] = useState([]) 12 | const [totalComments, setTotalComments] = useState(0) 13 | 14 | const generateComments = async (reflink) => { 15 | let count = 0 16 | const comments = [] 17 | for await (const rootComment of generateCommentGraph(reflink, () => { 18 | count++ 19 | })) { 20 | comments.push(rootComment) 21 | } 22 | setCommentGraph(comments) 23 | setTotalComments(count) 24 | } 25 | useEffect(() => { 26 | void generateComments(props.reflink) 27 | }, [props.reflink]) 28 | 29 | /** 30 | * Generates a html graph of the comments 31 | * @param {String} startRoot 32 | * @param {Function} progressHandler 33 | */ 34 | async function* generateCommentGraph(startRoot, progressHandler) { 35 | const children = (await PromiseIpc.send('distiller.getChildren', startRoot, { 36 | asPost: true, 37 | })) as any[] 38 | for (const child of children) { 39 | const childComments = [] 40 | for await (const childComment of generateCommentGraph(child._id, progressHandler)) { 41 | childComments.push(childComment) 42 | } 43 | 44 | try { 45 | const childVideoInfo = await AccountService.permalinkToVideoInfo(child._id) 46 | const proc = ( 47 |
48 | { 52 | const childVideoInfo = await AccountService.permalinkToVideoInfo(commentData) 53 | }} 54 | > 55 | {childComments} 56 |
57 | ) 58 | yield proc 59 | if (progressHandler) { 60 | progressHandler() 61 | } 62 | } catch (ex) { 63 | console.error(ex) 64 | continue 65 | } 66 | } 67 | } 68 | 69 | const isLogged = useMemo(() => { 70 | return !!localStorage.getItem('SNProfileID') 71 | }, []) 72 | 73 | return ( 74 | <> 75 |
Comments: ({totalComments})
76 |
77 |
Reply:
78 | {isLogged ? ( 79 | { 82 | console.log(commentData) 83 | }} 84 | /> 85 | ) : ( 86 | 87 | )} 88 |
89 | {totalComments === 0 ? : commentGraph} 90 | 91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /src/renderer/views/UploaderView/startEncode.ts: -------------------------------------------------------------------------------- 1 | import Fs from 'fs' 2 | import PromiseIpc from 'electron-promise-ipc' 3 | import { millisecondsAsString, secondsAsString } from '../../../common/utils/unit-conversion.functions' 4 | import { NotificationManager } from 'react-notifications' 5 | import { calculatePercentage } from './calculatePercentage' 6 | import DateTime from 'date-and-time' 7 | export const startEncode = async ({event, videoSourceFile, hwaccelOption, setEncodingInProgress, setStartTime, setEndTime, setProgress, setStatusInfo, setEstimatedTimeRemaining, setVideoInfo, setPublishReady, progress, statusInfo}) => { 8 | event.preventDefault() 9 | if (videoSourceFile === null) { 10 | NotificationManager.error('No video source file selected') 11 | return 12 | } 13 | if (!Fs.existsSync(videoSourceFile)) { 14 | NotificationManager.error('Source file does not exist') 15 | return 16 | } 17 | setEncodingInProgress(true) 18 | const _startingTime = new Date().getTime() 19 | setStartTime(_startingTime) 20 | setEndTime(null) 21 | 22 | const jobInfo = (await PromiseIpc.send('encoder.createJob', { 23 | sourceUrl: videoSourceFile, 24 | profiles: [ 25 | { 26 | name: '1080p', 27 | size: '1920x1080', 28 | }, 29 | { 30 | name: '720p', 31 | size: '1080x720', 32 | }, 33 | { 34 | name: '480p', 35 | size: '720x480', 36 | }, 37 | ], 38 | options: { 39 | hwaccel: 40 | hwaccelOption && 41 | hwaccelOption.length > 0 && 42 | hwaccelOption !== '' && 43 | hwaccelOption && 44 | hwaccelOption !== 'none' 45 | ? hwaccelOption 46 | : undefined, 47 | }, 48 | } as any)) as any 49 | NotificationManager.success('Encoding Started.') 50 | 51 | let savePct = 0 52 | const progressTrack = async () => { 53 | const _timeNow = new Date().getTime() 54 | const status = (await PromiseIpc.send('encoder.status', jobInfo.id)) as any 55 | 56 | console.log(`Encoder status: `, status) 57 | 58 | setProgress(status.progress || {}) 59 | setStatusInfo(status) 60 | 61 | const val = status.progress.percent 62 | // const diffPct = val - savePct 63 | // savePct = val 64 | // const pctPerSec = diffPct / 3 65 | // const totalTimeRemaining = (100 - val) / pctPerSec 66 | const totalTimeRemaining = (100 * (_timeNow - _startingTime)) / val 67 | setEstimatedTimeRemaining(millisecondsAsString(totalTimeRemaining)) 68 | setEndTime(_timeNow) 69 | } 70 | 71 | const pid = setInterval(progressTrack, 3000) 72 | void progressTrack() 73 | 74 | const encodeOutput = (await PromiseIpc.send('encoder.getjoboutput', jobInfo.id)) as any 75 | console.log(`got encode output`) 76 | console.log(encodeOutput) 77 | 78 | setVideoInfo({ 79 | size: encodeOutput.size, 80 | cid: encodeOutput.ipfsHash, 81 | path: encodeOutput.path, 82 | duration: Number(DateTime.parse(encodeOutput.duration, 'hh:mm:ss.SS', true)) / 1000, 83 | }) 84 | 85 | clearInterval(pid) 86 | 87 | setEncodingInProgress(false) 88 | setEstimatedTimeRemaining(null) 89 | setEndTime(new Date().getTime()) 90 | setPublishReady(true) 91 | 92 | NotificationManager.success('Encoding complete.') 93 | }; 94 | -------------------------------------------------------------------------------- /src/main/core/components/AccountSystem.ts: -------------------------------------------------------------------------------- 1 | import PouchDB from 'pouchdb' 2 | const { Finder } = require('arraysearch') 3 | const Path = require('path') 4 | PouchDB.plugin(require('pouchdb-find')) 5 | PouchDB.plugin(require('pouchdb-upsert')) 6 | 7 | const profile = { 8 | profileID: '12345', 9 | nickname: 'vaultec', 10 | keyring: [ 11 | { 12 | type: 'hive', 13 | username: 'vaultec', 14 | public: {}, 15 | encrypted: true, 16 | private: 'Encrypted blob', //Or object 17 | }, 18 | ], 19 | } 20 | class AccountSystem { 21 | pouch: any 22 | self: any 23 | symmetricKeyCache: {} 24 | constructor(self) { 25 | this.self = self 26 | 27 | this.symmetricKeyCache = {} 28 | } 29 | async get(profileID, symmetricKey) { 30 | const profileInfo = await this.pouch.get(profileID) 31 | for (const key of profileInfo.keyring) { 32 | if (key.encrypted === true) { 33 | //Do decryption here... 34 | //throw new Error("Decryption is currently unavailable.") 35 | } 36 | } 37 | return profileInfo 38 | } 39 | async has(profileID) { 40 | try { 41 | await this.pouch.get(profileID) 42 | return true 43 | } catch (ex) { 44 | console.error(ex) 45 | return false 46 | } 47 | } 48 | async createProfile(profile) { 49 | if (!profile.keyring) { 50 | profile.keyring = [] 51 | } 52 | return await this.pouch.post(profile) 53 | } 54 | async ls(options = {} as any) { 55 | if (!options.showPrivate) { 56 | options.showPrivate = false 57 | } 58 | if (!options.selector) { 59 | options.selector = {} 60 | } 61 | const accounts = (await this.pouch.find({ selector: options.selector })).docs 62 | if (options.showPrivate === false) { 63 | for (const account of accounts) { 64 | for (const key of account.keyring) { 65 | delete key.private 66 | } 67 | } 68 | } 69 | console.log(accounts) 70 | return accounts 71 | } 72 | async addProfileKey(profileID, key) { 73 | if (key.id) { 74 | throw new Error('key ID is required.') 75 | } 76 | await this.pouch.upsert(profileID, (doc) => { 77 | //if(Finder.one.in(doc.keyring).with({id:key.id})) { 78 | //} 79 | 80 | doc.keyring.push(key) 81 | return doc 82 | }) 83 | } 84 | async deleteProfileKey(profileID, keyID) { 85 | this.pouch.upsert(profileID, (doc) => { 86 | // type error: secret not declared? 87 | // doc.keyring.push(secret); 88 | doc.keyring.push(undefined) 89 | return doc 90 | }) 91 | } 92 | async deleteProfile(profileID) { 93 | await this.pouch.upsert(profileID, (doc) => { 94 | doc._deleted = true 95 | return doc 96 | }) 97 | } 98 | async setProfileInfo(profileID, opt) { 99 | await this.pouch.upsert(profileID, (doc) => { 100 | doc._deleted = true 101 | return doc 102 | }) 103 | } 104 | async export(options = {}) { 105 | //Placeholder 106 | } 107 | async import(blob, options = {}) { 108 | //Placeholder 109 | } 110 | async start() { 111 | this.pouch = new PouchDB(Path.join(this.self._options.path, 'accountdb')) 112 | } 113 | } 114 | export default AccountSystem 115 | -------------------------------------------------------------------------------- /src/renderer/views/PinsView/PinRows.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { bytesAsString, millisecondsAsString } from '../../../common/utils/unit-conversion.functions' 3 | import RefLink from '../../../main/RefLink' 4 | import { IoIosRadioButtonOn } from 'react-icons/io' 5 | import PinCids from './PinCids' 6 | import { Button } from 'react-bootstrap' 7 | 8 | export const pinRows = (pinList: any[], removePin: any) => { 9 | const [cidStatus, setCidStatus] = React.useState({}); 10 | console.log('pinRows.tsx cidStatus: ', cidStatus); 11 | const fetchCIDStatus = async (cid) => { 12 | let firstCid; 13 | if (Array.isArray(cid)) { 14 | firstCid = cid[0]; 15 | } else { 16 | firstCid = cid.split(',')[0]; 17 | } 18 | console.log('pinRows.tsx fetchCIDStatus cid: ', firstCid); 19 | const response = await fetch(`http://spk.tv/CID?key=${firstCid}`); 20 | console.log('pinRows.tsx fetchCIDStatus response: ', response); 21 | const data = await response.json(); 22 | return data; 23 | }; 24 | 25 | React.useEffect(() => { 26 | const intervalId = setInterval(async () => { 27 | const newCidStatus = {}; 28 | for (let pin of pinList) { 29 | if (pin.meta) { 30 | const status = await fetchCIDStatus(pin.cids); 31 | console.log('pinRows.tsx status: ', status); 32 | newCidStatus[pin.cids] = status; 33 | } 34 | } 35 | setCidStatus(newCidStatus); 36 | }, 3000); // Every 30 seconds 37 | 38 | return () => clearInterval(intervalId); // Clear interval on unmount 39 | }, [pinList]); 40 | 41 | return ( 42 | <> 43 | {pinList.map((pin) => { 44 | if(pin.meta) { 45 | const sizeBest = bytesAsString(pin.size); 46 | const expireText = pin.expire 47 | ? `In ${millisecondsAsString((pin.expire = new Date().getTime()))}` 48 | : 'Permanent'; 49 | const pinDateText = pin.meta.pin_date ? new Date(pin.meta.pin_date).toLocaleString() : null; 50 | const currentStatus = cidStatus[pin.cids]; 51 | return ( 52 | 53 | 54 | {pin._id} 55 |
({RefLink.parse(pin._id).root}) 56 | 57 | 58 | {pin.meta ? pin.meta.title : null} 59 | 60 | 61 | 62 | 63 | {pin.source} 64 | {expireText} 65 | {pinDateText} 66 | {pin.size === 0 ? Pinning In Progress {pin.percent}% : sizeBest} 67 | 68 | 71 | 72 | 73 | 74 | {currentStatus && `${currentStatus.percentage}%`} 75 | 76 | 77 | ); 78 | 79 | }else { 80 | console.log('pinRows.tsx pin is null'); 81 | } 82 | })} 83 | 84 | ); 85 | }; 86 | -------------------------------------------------------------------------------- /src/main/AutoUpdater.ts: -------------------------------------------------------------------------------- 1 | import { app, dialog, shell } from 'electron' 2 | import Path from 'path' 3 | import fs from 'fs' 4 | import axios from 'axios' 5 | import compareVersions from 'compare-versions' 6 | import tmp from 'tmp' 7 | import { spawn } from 'child_process' 8 | const isWin = process.platform === 'win32' 9 | import { version } from '../../package.json' 10 | 11 | class AutoUpdater { 12 | async run() { 13 | try { 14 | const data = ( 15 | await axios.get('https://api.github.com/repos/3Speaknetwork/3Speak-app/releases/latest') 16 | ).data 17 | if (data.id) { 18 | const tag_name = data['tag_name'] 19 | if (compareVersions.compare(tag_name, version, '>')) { 20 | //Update available 21 | for (const asset of data.assets) { 22 | if (asset.name.includes('Setup') && asset.name.includes('exe') && isWin) { 23 | const tmpDir = tmp.dirSync() 24 | const filePath = Path.join(tmpDir.name, asset.name) 25 | const file = fs.createWriteStream(filePath) 26 | const response = await axios({ 27 | method: 'get', 28 | url: asset.browser_download_url, 29 | responseType: 'stream', 30 | }) 31 | await new Promise((resolve, reject) => { 32 | response.data.pipe(file) 33 | let error = null 34 | file.on('error', (err) => { 35 | error = err 36 | // type error: writer not declared? 37 | // writer.close(); 38 | reject(err) 39 | }) 40 | file.on('close', () => { 41 | if (!error) { 42 | resolve(true) 43 | } 44 | }) 45 | }) 46 | 47 | let dialogResponse = dialog.showMessageBoxSync({ 48 | type: 'question', 49 | buttons: ['Remind me later', 'View release log', 'Update'], 50 | title: 'Update available', 51 | message: `New update available version ${tag_name.slice( 52 | 1, 53 | )}\nWould you like to update your local 3Speak installation?`, 54 | }) 55 | 56 | if (dialogResponse === 2) { 57 | spawn(filePath, [], { detached: true }) 58 | app.exit(1) 59 | break 60 | } else if (dialogResponse === 1) { 61 | void shell.openExternal(data.html_url) 62 | } else { 63 | break 64 | } 65 | 66 | dialogResponse = dialog.showMessageBoxSync({ 67 | type: 'question', 68 | buttons: ['Remind me later', 'Update'], 69 | title: 'Update available', 70 | message: `New update available version ${tag_name.slice( 71 | 1, 72 | )}\nWould you like to update your local 3Speak installation?`, 73 | }) 74 | if (dialogResponse === 1) { 75 | spawn(filePath, [], { detached: true }) 76 | app.exit(1) 77 | } 78 | break 79 | } 80 | } 81 | } 82 | } 83 | } catch (ex) { 84 | //Shouldn't be important if request fails... But still should be logged. 85 | console.error(`Error in autoupdater`, ex.message) 86 | } 87 | } 88 | } 89 | export default AutoUpdater 90 | -------------------------------------------------------------------------------- /src/renderer/services/peer.service.ts: -------------------------------------------------------------------------------- 1 | import { api, broadcast } from "@hiveio/hive-js" 2 | import PromiseIPC from 'electron-promise-ipc' 3 | import ArraySearch from 'arraysearch' 4 | 5 | const Finder = ArraySearch.Finder 6 | 7 | type UpdateAccountOperation = [[ 8 | 'account_update2', 9 | { 10 | account: string; 11 | json_metadata: string; 12 | posting_json_metadata: string; 13 | } 14 | ]] 15 | 16 | interface Profile { 17 | posting_json_metadata: string; 18 | json_metadata: string; 19 | } 20 | 21 | const fetchSingleProfile = async (account: string): Promise => { 22 | const params = [[account]] 23 | try { 24 | const data = await api.callAsync('condenser_api.get_accounts', params) 25 | return data[0] 26 | } catch (err) { 27 | console.error(err.message) 28 | throw err 29 | } 30 | } 31 | 32 | const generateUpdateAccountOperation = (account: string, posting_json_metadata: string, json_metadata = ''): Promise => { 33 | return new Promise((resolve) => { 34 | const op_comment: UpdateAccountOperation = [[ 35 | 'account_update2', 36 | { 37 | account, 38 | json_metadata, 39 | posting_json_metadata, 40 | }, 41 | ]] 42 | resolve(op_comment) 43 | }) 44 | } 45 | 46 | export const handleUpdatePostingData = async (peerId: string): Promise => { 47 | const profileID = window.localStorage.getItem('SNProfileID') 48 | const getAccount = (await PromiseIPC.send('accounts.get', profileID as any)) as any 49 | const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }) 50 | const wif = hiveInfo.privateKeys.posting_key 51 | const postingData = JSON.parse((await fetchSingleProfile(getAccount.nickname)).posting_json_metadata) 52 | postingData['peerId'] = peerId 53 | const stringifiedPostingData = JSON.stringify(postingData) 54 | 55 | const operations = await generateUpdateAccountOperation(getAccount.nickname, stringifiedPostingData) 56 | 57 | console.log('type of peerId', typeof peerId) 58 | console.log(peerId) 59 | console.log(operations[0]) 60 | 61 | broadcast.send( 62 | { operations: [operations[0]] }, 63 | { posting: wif }, 64 | (err: Error, result: any) => { 65 | if (err) { 66 | console.error('Error broadcasting operation:', err.message) 67 | } else { 68 | console.log('Operation broadcast successfully:', result) 69 | } 70 | } 71 | ) 72 | } 73 | 74 | export async function postCustomJson(username, metadata, customJsonId) { 75 | const profileID = window.localStorage.getItem('SNProfileID') 76 | const getAccount = (await PromiseIPC.send('accounts.get', profileID as any)) as any 77 | const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }) 78 | const wif = hiveInfo.privateKeys.posting_key 79 | 80 | const customJsonPayload = { 81 | required_auths: [], 82 | required_posting_auths: [username], 83 | id: customJsonId, 84 | json: JSON.stringify(metadata), 85 | } 86 | 87 | const customJsonOperation = [ 88 | 'custom_json', 89 | customJsonPayload 90 | ]; 91 | 92 | broadcast.send( 93 | { operations: [customJsonOperation] }, 94 | { posting: wif }, 95 | (error, result) => { 96 | if (error) { 97 | console.error(error) 98 | console.error('Error encountered broadcasting custom json') 99 | } 100 | 101 | if (result) { 102 | console.log(result) 103 | console.log('Success broadcasting custom json') 104 | } 105 | } 106 | ); 107 | } -------------------------------------------------------------------------------- /src/renderer/components/widgets/FollowWidget.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from 'react' 2 | import { Button } from 'react-bootstrap' 3 | import { NotificationManager } from 'react-notifications' 4 | 5 | import RefLink from '../../../main/RefLink' 6 | import { AccountService } from '../../services/account.service' 7 | 8 | export function FollowWidget(props: any) { 9 | const [followers, setFollowers] = useState(0) 10 | const reflink = useMemo(() => { 11 | return RefLink.parse(props.reflink) 12 | }, [props.reflink]) 13 | 14 | const [alreadyFollowing, setAlreadyFollowing] = useState(false) 15 | 16 | const loadAlreadyFollowing = async () => { 17 | const out = await AccountService.getFollowing() 18 | const whoFollow = reflink.root 19 | 20 | for (const ln of Object.values(out)) { 21 | if (whoFollow === ln.following) { 22 | setAlreadyFollowing(true) 23 | break 24 | } 25 | } 26 | } 27 | 28 | const loadFollowers = async () => { 29 | setFollowers(await AccountService.getFollowerCount(props.reflink)) 30 | } 31 | 32 | useEffect(() => { 33 | void loadFollowers() 34 | void loadAlreadyFollowing() 35 | }, []) 36 | 37 | const handleFollow = async () => { 38 | const profileID = localStorage.getItem('SNProfileID') 39 | 40 | if (profileID) { 41 | try { 42 | // const profile = await AccountService.getAccount(profileID) 43 | const accountType = 'hive' 44 | const author = RefLink.parse(props.reflink).root // person to follow 45 | const what = 'blog' 46 | const followOp = { author, accountType, what } 47 | await AccountService.followHandler(profileID, followOp) 48 | NotificationManager.success('User followed') 49 | 50 | setAlreadyFollowing(true) 51 | } catch (error) { 52 | console.error(error) 53 | NotificationManager.error('There was an error completing this operation') 54 | } 55 | } else { 56 | NotificationManager.error('You need to be logged in to perform this operation') 57 | } 58 | } 59 | 60 | const handleUnfollow = async () => { 61 | const profileID = localStorage.getItem('SNProfileID') 62 | 63 | if (profileID) { 64 | try { 65 | const accountType = 'hive' 66 | const author = RefLink.parse(props.reflink).root // person to follow 67 | 68 | const what = null 69 | const followOp = { author, accountType, what } 70 | await AccountService.followHandler(profileID, followOp) 71 | NotificationManager.success('User unfollowed') 72 | } catch (error) { 73 | console.error(error) 74 | NotificationManager.error('There was an error completing this operation') 75 | } 76 | setAlreadyFollowing(false) 77 | } else { 78 | NotificationManager.error('You need to be logged in to perform this operation') 79 | } 80 | } 81 | 82 | return ( 83 |
84 | {alreadyFollowing ? ( 85 | 94 | ) : ( 95 | 113 | )} 114 |
115 | ) 116 | } 117 | -------------------------------------------------------------------------------- /src/renderer/services/accountServices/postComment.ts: -------------------------------------------------------------------------------- 1 | import hive from '@hiveio/hive-js' 2 | import { CommentOp } from '../../../common/models/comments.model' 3 | import PromiseIPC from 'electron-promise-ipc' 4 | import { HiveInfo } from '../../../common/models/hive.model' 5 | import { IpfsService } from '../ipfs.service' 6 | import ArraySearch from 'arraysearch' 7 | export async function postComment(commentOp: CommentOp) { 8 | const Finder = ArraySearch.Finder 9 | switch (commentOp.accountType) { 10 | case 'hive': { 11 | const profileID = window.localStorage.getItem('SNProfileID') as any 12 | const getAccount = (await PromiseIPC.send('accounts.get', profileID)) as any 13 | const hiveInfo = Finder.one.in(getAccount.keyring).with({ type: 'hive' }) as HiveInfo 14 | 15 | console.log(`posting comment with profile ID`, profileID) 16 | console.log(`account`, getAccount) 17 | console.log(`hiveInfo`, hiveInfo) 18 | console.log(`comment op`, commentOp) 19 | 20 | if (!commentOp.json_metadata) { 21 | commentOp.json_metadata = {} 22 | } 23 | 24 | let json_metadata 25 | if (typeof commentOp.json_metadata === 'object') { 26 | //Note: this is for peakd/hive.blog to display a video preview 27 | if (!commentOp.parent_author) { 28 | commentOp.json_metadata.video.info = { 29 | author: hiveInfo.username, 30 | permlink: commentOp.permlink, 31 | } 32 | } 33 | json_metadata = JSON.stringify(commentOp.json_metadata) 34 | } else { 35 | throw new Error('commentOp.json_metadata must be an object') 36 | } 37 | 38 | let body: string 39 | 40 | if (!commentOp.parent_author) { 41 | let header: string 42 | if (commentOp.json_metadata.sourceMap) { 43 | const thumbnailSource = Finder.one.in(commentOp.json_metadata.sourceMap).with({ 44 | type: 'thumbnail', 45 | }) 46 | console.log(`thumbnail source`, thumbnailSource) 47 | try { 48 | const cid = IpfsService.urlToCID(thumbnailSource.url) 49 | const gateway = await IpfsService.getGateway(cid, true) 50 | const imgSrc = gateway + IpfsService.urlToIpfsPath(thumbnailSource.url) 51 | header = `[![](${imgSrc})](https://3speak.tv/watch?v=${hiveInfo.username}/${commentOp.permlink})
` 52 | } catch (ex) { 53 | console.error(`Error getting IPFS info`, ex) 54 | throw ex 55 | } 56 | } 57 | if (header) { 58 | body = `${header} ${commentOp.body}
[▶️Watch on 3Speak Dapp](https://3speak.tv/openDapp?uri=hive:${hiveInfo.username}:${commentOp.permlink})` 59 | } else { 60 | body = `${commentOp.body}
[▶️Watch on 3Speak Dapp](https://3speak.tv/openDapp?uri=hive:${hiveInfo.username}:${commentOp.permlink})` 61 | } 62 | } else { 63 | body = commentOp.body 64 | } 65 | 66 | if (!json_metadata) { 67 | throw new Error(`Cannot publish comment to hive with no metadata!`) 68 | } 69 | console.log(`POSTING TO HIVE WITH JSON METADATA`, json_metadata) 70 | 71 | try { 72 | const out = await hive.broadcast.comment( 73 | hiveInfo.privateKeys.posting_key, 74 | commentOp.parent_author || '', 75 | commentOp.parent_permlink || commentOp.tags[0] || 'threespeak', //parentPermlink 76 | hiveInfo.username, 77 | commentOp.permlink, 78 | commentOp.title, 79 | body, 80 | json_metadata, 81 | ) 82 | 83 | console.log(`comment broadcasted to hive! return:`) 84 | console.log(out) 85 | 86 | return [`hive:${hiveInfo.username}:${commentOp.permlink}`, out] 87 | } catch (err) { 88 | console.error(`Error broadcasting comment to hive! ${err.message}`) 89 | throw err 90 | } 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/renderer/views/UserView/UserViewContent.tsx: -------------------------------------------------------------------------------- 1 | // UserViewContent.tsx 2 | import React from 'react'; 3 | import { Navbar, Nav, Card, Col, Row, Button } from 'react-bootstrap'; 4 | import { Switch, Route } from 'react-router-dom'; 5 | import ReactMarkdown from 'react-markdown'; 6 | import { GridFeedView } from './../GridFeedView'; 7 | import { FollowWidget } from '../../components/widgets/FollowWidget'; 8 | 9 | const UserViewContent = ({ 10 | coverUrl, 11 | profileUrl, 12 | username, 13 | reflink, 14 | hiveBalance, 15 | hbdBalance, 16 | profileAbout, 17 | }) => { 18 | return ( 19 |
20 |
21 | 31 |
32 | 33 |
34 |
35 |
36 | 37 | {username} 38 | 45 | 46 | 53 |
54 | 55 |
56 |
57 |
58 |
59 | 60 | 61 |
62 | 63 |
64 |
65 | 66 | 67 | 68 | 69 | 70 | {hiveBalance} 71 | 72 | 73 | Available HIVE Balance 74 | 75 | 76 | 77 | 78 | 79 | 80 | {hbdBalance} 81 | 82 | 83 | Available HBD Balance 84 | 85 | 86 | 87 | 88 | 89 | 90 | {profileAbout} 91 | 92 |
93 |
94 | ); 95 | }; 96 | 97 | export default UserViewContent; 98 | -------------------------------------------------------------------------------- /src/main/core/components/Config.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import _get from 'dlv' 3 | import mergeOptions from 'merge-options' 4 | import { Key } from 'interface-datastore' 5 | import DatastoreFs from 'datastore-fs' 6 | 7 | function obj_set(obj, props, value) { 8 | if (typeof props == 'string') { 9 | props = props.split('.') 10 | } 11 | if (typeof props == 'symbol') { 12 | props = [props] 13 | } 14 | const lastProp = props.pop() 15 | if (!lastProp) { 16 | return false 17 | } 18 | let thisProp 19 | while ((thisProp = props.shift())) { 20 | if (typeof obj[thisProp] == 'undefined') { 21 | obj[thisProp] = {} 22 | } 23 | obj = obj[thisProp] 24 | if (!obj || typeof obj != 'object') { 25 | return false 26 | } 27 | } 28 | obj[lastProp] = value 29 | return true 30 | } 31 | class ChildConfig { 32 | parentConfig: any 33 | rootKey: any 34 | constructor(parentConfig, rootKey) { 35 | this.parentConfig = parentConfig 36 | this.rootKey = rootKey 37 | } 38 | /** 39 | * 40 | * @param {String} key 41 | */ 42 | get(key) { 43 | return this.parentConfig.get(`${this.rootKey}.${key}`) 44 | } 45 | /** 46 | * 47 | * @param {String} key 48 | * @param {*} value 49 | */ 50 | set(key, value) { 51 | return this.parentConfig.set(`${this.rootKey}.${key}`, value) 52 | } 53 | } 54 | class Config { 55 | config: any 56 | datastore: any 57 | modules: {} 58 | obj_set: (obj: any, props: any, value: any) => boolean 59 | constructor(datastore) { 60 | if (typeof datastore === 'string') { 61 | this.datastore = new DatastoreFs(datastore, { 62 | extension: '', 63 | }) 64 | } else { 65 | this.datastore = datastore 66 | } 67 | 68 | this.modules = {} 69 | this.obj_set = obj_set 70 | this.get = this.get.bind(this) 71 | this.set = this.set.bind(this) 72 | this.open = this.open.bind(this) 73 | this.init = this.init.bind(this) 74 | } 75 | 76 | // path method not implemented, methods not being used - commenting out 77 | // reload() { 78 | // let buf = fs.readFileSync(this.path).toString(); 79 | // let obj = JSON.parse(buf); 80 | // //patch 81 | // this.config = mergeOptions(this.config, obj); 82 | // } 83 | // path(path: any) { 84 | // throw new Error("Method not implemented."); 85 | // } 86 | 87 | async save() { 88 | const buf = Buffer.from(JSON.stringify(this.config, null, 2)) 89 | await this.datastore.put(new Key('config'), buf) 90 | } 91 | /** 92 | * 93 | * @param {String} key 94 | */ 95 | get(key) { 96 | if (typeof key === 'undefined') { 97 | return this.config 98 | } 99 | if (typeof key !== 'string') { 100 | return new Error('Key ' + key + ' must be a string.') 101 | } 102 | return _get(this.config, key) 103 | } 104 | /** 105 | * 106 | * @param {String} key 107 | * @param {*} value 108 | */ 109 | set(key, value) { 110 | obj_set(this.config, key, value) 111 | this.save() 112 | } 113 | async open() { 114 | if (!(await this.datastore.has(new Key('config')))) { 115 | // type error: expected config as argument 116 | await this.init(undefined as any) 117 | } 118 | const buf = await this.datastore.get(new Key('config')) 119 | this.config = JSON.parse(buf.toString()) 120 | } 121 | child(key) { 122 | return new ChildConfig(this, key) 123 | } 124 | /** 125 | * Creates config with default settings 126 | * @param {Object} config custom config object 127 | */ 128 | async init(config) { 129 | const defaultConfig = { 130 | blocklist: { 131 | enabled: true, 132 | provider: '', 133 | }, 134 | } 135 | 136 | /*for (let mod of this.modules) { 137 | defaultConfig[mod.key()] = mod.init(); 138 | }*/ 139 | 140 | this.config = config || defaultConfig 141 | await this.save() 142 | } 143 | } 144 | export default Config 145 | -------------------------------------------------------------------------------- /src/renderer/components/video/Player.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' 2 | import ReactJWPlayer from 'react-jw-player' 3 | import mergeOptions from 'merge-options' 4 | import PromiseIpc from 'electron-promise-ipc' 5 | import CID from 'cids' 6 | import convert from 'convert-units' 7 | import { VideoInfo } from '../../../common/models/video.model' 8 | import { AccountService } from '../../services/account.service' 9 | import { VideoService } from '../../services/video.service' 10 | 11 | interface PlayerProps { 12 | videoInfo?: VideoInfo 13 | reflink: any 14 | match?: any 15 | options?: any 16 | } 17 | 18 | export function Player(props: PlayerProps) { 19 | const [playerId] = useState(Math.random().toString()) 20 | const [videoInfo, setVideoInfo] = useState(undefined as VideoInfo) 21 | const [videoUrl, setVideoUrl] = useState('') 22 | const [thumbnail, setThumbnail] = useState() 23 | const playerRef = useRef() 24 | 25 | useEffect(() => { 26 | void load() 27 | 28 | async function load() { 29 | let reflink 30 | if (props.reflink) { 31 | reflink = props.reflink 32 | } else { 33 | reflink = props.match.params.reflink 34 | } 35 | 36 | const defaultOptions = { 37 | ipfsGateway: 'https://ipfs.3speak.tv', 38 | } 39 | 40 | let options 41 | if (props.options) { 42 | options = mergeOptions(defaultOptions, props.options) 43 | } else { 44 | options = defaultOptions 45 | } 46 | 47 | let vidInfo 48 | console.log('videoInfo', props.videoInfo) 49 | console.log('reflink', reflink) 50 | if (props.videoInfo) { 51 | vidInfo = props.videoInfo 52 | } else if (reflink) { 53 | vidInfo = await AccountService.permalinkToVideoInfo(reflink) 54 | } else { 55 | throw new Error('Cannot set video info!') 56 | } 57 | console.log('vidInfo', vidInfo) 58 | const vidurl = await VideoService.getVideoSourceURL(vidInfo) 59 | console.log('vidurl', vidurl) 60 | const thumbnailUrl = await VideoService.getThumbnailURL(vidInfo) 61 | console.log('thumbnailUrl', thumbnailUrl) 62 | setThumbnail(thumbnailUrl) 63 | setVideoUrl(vidurl) 64 | setVideoInfo(vidInfo) 65 | } 66 | }, []) 67 | 68 | const onPlay = useCallback(async () => { 69 | console.log(`ONPLAY video`) 70 | if (!videoInfo) { 71 | throw new Error(`Cannot play video - videoInfo is not defined`) 72 | } 73 | 74 | const cids = [] 75 | console.log(videoInfo.sources) 76 | 77 | for (const source of videoInfo.sources) { 78 | try { 79 | const url = new URL(source.url) 80 | new CID(url.host) 81 | cids.push(url.host) 82 | } catch (err) { 83 | console.error(`Error creating CID for video URL ${source.url}`) 84 | } 85 | } 86 | console.log(`CIDs to cache ${JSON.stringify(cids)}`) 87 | 88 | if (cids.length !== 0) { 89 | await PromiseIpc.send('pins.add', { 90 | _id: props.reflink, 91 | source: 'Watch Page', 92 | cids, 93 | expire: new Date().getTime() + convert('10').from('d').to('ms'), 94 | meta: { 95 | title: videoInfo.title, 96 | }, 97 | } as any) 98 | } 99 | }, [videoInfo]) 100 | 101 | return ( 102 | <> 103 | {videoUrl ? ( 104 | 115 | ) : ( 116 |
[Player] videoInfo not specified [Player]
117 | )} 118 | 119 | ) 120 | } 121 | -------------------------------------------------------------------------------- /src/main/AutoUpdaterPoA.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import Path from 'path'; 3 | import fs from 'fs'; 4 | import axios from 'axios'; 5 | import compareVersions from 'compare-versions'; 6 | import { EventEmitter } from 'events'; 7 | 8 | const isWin = process.platform === 'win32'; 9 | 10 | class PoAInstaller extends EventEmitter { 11 | async main() { 12 | try { 13 | await this.install(); 14 | } catch (error) { 15 | console.error(error); 16 | this.emit('error', error); 17 | process.exit(1); 18 | } 19 | } 20 | 21 | async getCurrentVersion(installDir) { 22 | const versionFilePath = Path.join(installDir, 'version.txt'); 23 | try { 24 | const currentVersion = await fs.promises.readFile(versionFilePath, 'utf-8'); 25 | return currentVersion.trim(); 26 | } catch (error) { 27 | return '0.0.0'; 28 | } 29 | } 30 | 31 | async getDefaultPath() { 32 | let defaultPath; 33 | switch (process.platform) { 34 | case 'win32': 35 | defaultPath = Path.join('AppData/Roaming/Microsoft/Windows/Start Menu/Programs/PoA'); 36 | return defaultPath; 37 | case 'darwin': 38 | defaultPath = Path.join(os.homedir(), 'Applications/PoA/poa'); 39 | break; 40 | case 'linux': 41 | defaultPath = Path.join(os.homedir(), 'bin/PoA/poa'); 42 | break; 43 | default: 44 | throw new Error(`Unsupported platform: ${process.platform}`); 45 | } 46 | 47 | try { 48 | await fs.promises.access(defaultPath, fs.constants.F_OK); 49 | return defaultPath; 50 | } catch (error) { 51 | return null; 52 | } 53 | } 54 | 55 | async install() { 56 | const installDir = Path.join(os.homedir(), (await this.getDefaultPath()) || ''); 57 | console.log(`Installing PoA to ${installDir}`); 58 | 59 | // Create the data/badger directory if it doesn't exist 60 | const dataDir = Path.join('.', 'data', 'badger'); 61 | if (!fs.existsSync(dataDir)) { 62 | fs.mkdirSync(dataDir, { recursive: true }); 63 | console.log(`Directory ${dataDir} created.`); 64 | } 65 | 66 | // ... (rest of your existing code) 67 | const { data } = await axios.get('https://api.github.com/repos/spknetwork/proofofaccess/releases/latest'); 68 | const { tag_name, assets } = data; 69 | 70 | const currentVersion = await this.getCurrentVersion(installDir); 71 | if (compareVersions.compare(tag_name, currentVersion, '>')) { 72 | this.emit('update-available', tag_name); 73 | 74 | let asset; 75 | if (isWin) { 76 | asset = assets.find((a) => a.name.includes('win-main') && a.name.includes('exe')); 77 | } else if (process.platform === 'linux') { 78 | asset = assets.find((a) => a.name.includes('linux-main')); 79 | } else if (process.platform === 'darwin') { 80 | asset = assets.find((a) => a.name.includes('mac-main')); 81 | } 82 | 83 | if (!asset) { 84 | console.error('Could not find PoA asset for this platform'); 85 | return; 86 | } 87 | 88 | const response = await axios({ 89 | method: 'get', 90 | url: asset.browser_download_url, 91 | responseType: 'arraybuffer', 92 | }); 93 | 94 | const installPath = isWin ? Path.join(installDir, 'PoA.exe') : Path.join(installDir, 'PoA'); 95 | fs.writeFileSync(installPath, Buffer.from(response.data)); 96 | 97 | // Make the file executable (only for non-Windows) 98 | if (!isWin) { 99 | fs.chmodSync(installPath, 0o755); 100 | } 101 | 102 | console.log(`PoA installed at: ${installPath}`); 103 | this.emit('installed', installPath); 104 | 105 | const versionFilePath = Path.join(installDir, 'version.txt'); 106 | fs.writeFileSync(versionFilePath, tag_name); 107 | console.log(`Version ${tag_name} saved to ${versionFilePath}`); 108 | this.emit('version-updated', tag_name); 109 | } else { 110 | console.log('PoA is already up-to-date'); 111 | this.emit('up-to-date'); 112 | } 113 | } 114 | } 115 | 116 | export default PoAInstaller; 117 | -------------------------------------------------------------------------------- /src/renderer/views/IpfsConsoleView.tsx: -------------------------------------------------------------------------------- 1 | import 'brace/mode/json' 2 | import 'brace/theme/github' 3 | import 'brace/theme/monokai' 4 | import 'brace/theme/solarized_dark' 5 | import 'jsoneditor-react/es/editor.min.css' 6 | 7 | import ace from 'brace' 8 | import { JsonEditor as Editor } from 'jsoneditor-react' 9 | import React, { useEffect, useRef, useState } from 'react' 10 | import { Button, ButtonGroup, Col, Row } from 'react-bootstrap' 11 | import { NotificationManager } from 'react-notifications' 12 | 13 | import { IpfsHandler } from '../../main/core/components/ipfsHandler' 14 | import { IpfsStatsView } from './IpfsConsoleView/IpfsStatsView' 15 | 16 | //JSON editor specific 17 | export function IpfsConsoleView() { 18 | const [ipfsConfig, setIpfsConfig] = useState({} as any) 19 | const [ipfsInfo, setIpfsInfo] = useState({} as any) 20 | const [configError, setConfigError] = useState(false) 21 | const editor = useRef() 22 | const loopPid = useRef() 23 | 24 | const getIpfsConfig = async () => { 25 | const info = await IpfsHandler.getIpfs() 26 | setIpfsInfo(info) 27 | 28 | let jsonContent 29 | const { ipfs } = info 30 | 31 | if (editor.current) { 32 | editor.current?.createEditor({ 33 | value: await ipfs.config.getAll(), 34 | ace: ace, 35 | mode: 'code', 36 | theme: 'ace/theme/solarized_dark', 37 | ref: editor, 38 | htmlElementProps: { 39 | style: { 40 | height: '500px', 41 | }, 42 | }, 43 | onChange: (json) => { 44 | jsonContent = json 45 | }, 46 | }) 47 | } else { 48 | throw new Error(`editor ref is not defined! Cannot create editor.`) 49 | } 50 | } 51 | 52 | const update = async () => { 53 | console.log(`UPDATING`) 54 | const annotations = editor.current.jsonEditor.aceEditor.getSession().getAnnotations() 55 | setConfigError(annotations.length === 0 ? false : true) 56 | } 57 | 58 | useEffect(() => { 59 | void getIpfsConfig() 60 | loopPid.current = setInterval(update, 150) 61 | 62 | return () => { 63 | clearInterval(loopPid.current) 64 | } 65 | }, []) 66 | 67 | return ( 68 |
69 |

70 | This is the IPFS Debug Console. This is for advanced users only, if you don't know what you 71 | are doing stay out of this area. 72 |

73 |
74 | 75 | 76 | { 88 | console.log(json) 89 | }} 90 | /> 91 | 92 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 |
118 |
119 | ) 120 | } 121 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { app, BrowserWindow, dialog } from 'electron' 3 | import debug from 'debug' 4 | import { CoreService } from './core' 5 | import IpcAdapter from './ipcAdapter' 6 | import AutoUpdater from './AutoUpdater' 7 | import dotenv from 'dotenv' 8 | dotenv.config() 9 | 10 | const entryUrl = 11 | process.env.NODE_ENV === 'development' 12 | ? 'http://localhost:6789/index.html' 13 | : `file://${path.join(__dirname, 'index.html')}` 14 | 15 | if (process.env.NODE_ENV === 'development') { 16 | debug.enable('3speak:*') 17 | } 18 | 19 | let window = null 20 | const core = new CoreService(undefined as any) 21 | 22 | const gotTheLock = app.requestSingleInstanceLock() 23 | 24 | app.removeAsDefaultProtocolClient('speak') 25 | app.setAsDefaultProtocolClient('speak', process.execPath) 26 | function devToolsLog(s) { 27 | if (window && window.webContents) { 28 | window.webContents.executeJavaScript(`console.log("${s}")`) 29 | } 30 | } 31 | app.on('open-url', (event, url) => { 32 | // handle the data 33 | devToolsLog('process args ' + url) 34 | if (url.includes('speak://')) { 35 | const UrlWoo = new URL(url) 36 | window.loadURL(entryUrl + UrlWoo.hash) 37 | } 38 | }) 39 | 40 | if (!gotTheLock) { 41 | app.quit() 42 | } else { 43 | app.on('second-instance', (event, argv, workingDirectory) => { 44 | if (window) { 45 | if (window.isMinimized()) window.restore() 46 | window.focus() 47 | const theUrl = argv[argv.length - 1] 48 | if (theUrl.includes('speak://')) { 49 | const UrlWoo = new URL(theUrl) 50 | window.loadURL(entryUrl + UrlWoo.hash) 51 | } else { 52 | window.loadURL(entryUrl) 53 | } 54 | } 55 | }) 56 | 57 | app.on('ready', () => { 58 | window = new BrowserWindow({ 59 | width: 800, 60 | height: 600, 61 | icon: path.resolve(__dirname, '../renderer/assets/img/app.png'), 62 | webPreferences: { 63 | nodeIntegration: true, 64 | webSecurity: false, 65 | webviewTag: true, 66 | contextIsolation: false, 67 | }, 68 | }) 69 | const theUrl = process.argv[process.argv.length - 1] 70 | if (theUrl.includes('speak://')) { 71 | const UrlWoo = new URL(theUrl) 72 | window.loadURL(entryUrl + UrlWoo.hash) 73 | } else { 74 | window.loadURL(entryUrl) 75 | } 76 | window.on('close', (event) => { 77 | if (window) { 78 | const choice = dialog.showMessageBoxSync(window, { 79 | type: 'question', 80 | buttons: ['Yes', 'No'], 81 | title: 'Stop Proof of Access', 82 | message: 83 | 'Closing the 3Speak app will result in the interruption of the Proof of Access. Are you ok with stopping it?', 84 | }) 85 | 86 | if (choice === 1) { 87 | event.preventDefault() 88 | } 89 | } 90 | }) 91 | window.on('closed', () => (window = null)) 92 | }) 93 | } 94 | 95 | app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors') 96 | app.on('window-all-closed', async () => { 97 | if (process.platform !== 'darwin') { 98 | await core.stop() 99 | app.quit() 100 | } 101 | }) 102 | 103 | async function startup(): Promise { 104 | const updater = new AutoUpdater() 105 | void updater.run() 106 | 107 | try { 108 | new IpcAdapter(core).start() 109 | await core.start() 110 | } catch (ex) { 111 | console.error(ex) 112 | app.quit() 113 | } 114 | } 115 | 116 | void startup() 117 | 118 | process.on('unhandledRejection', (err: Error) => { 119 | console.log(err) 120 | core.logger.error(`Unhandled rejection!`) 121 | core.logger.error(err.message) 122 | core.logger.error(err.stack) 123 | core.logger.error(`Halting process with error code 1.`) 124 | process.exit(1) 125 | }) 126 | 127 | process.on('uncaughtException', (err: Error) => { 128 | console.log('err', err) 129 | core.logger.error(`Uncaught exception!`) 130 | core.logger.error(err.message) 131 | core.logger.error(err.stack) 132 | core.logger.error(`Halting process with error code 1.`) 133 | process.exit(1) 134 | }) 135 | -------------------------------------------------------------------------------- /src/renderer/views/Accounts.tsx: -------------------------------------------------------------------------------- 1 | import ArraySearch from 'arraysearch' 2 | import React, { useEffect, useState } from 'react' 3 | import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap' 4 | import { Link } from 'react-router-dom' 5 | 6 | import hive from '../assets/img/hive.svg' 7 | import { AccountService } from '../services/account.service' 8 | 9 | const Finder = ArraySearch.Finder 10 | 11 | export function AccountsView() { 12 | const [accounts, setAccounts] = useState([]) 13 | const [login, setLogin] = useState('') 14 | const [accountAdded, setAccountAdded] = useState(true) 15 | 16 | useEffect(() => { 17 | const load = async () => { 18 | const accounts = [] 19 | const accountsInit = (await AccountService.getAccounts()) as any[] 20 | accountsInit.forEach((obj) => { 21 | accounts.push(obj) 22 | }) 23 | if (accounts.length === 0) { 24 | setAccountAdded(false) 25 | } 26 | const login = localStorage.getItem('SNProfileID') 27 | 28 | if (login) { 29 | const user = (await AccountService.getAccount(login)) as any 30 | setLogin(user.nickname) 31 | } 32 | 33 | setAccounts(accounts) 34 | } 35 | 36 | void load() 37 | 38 | //TODO: get accounts 39 | }, []) 40 | 41 | const handleAccountChange = async (profileID: string) => { 42 | const theAcc = (await AccountService.getAccount(profileID)) as any 43 | 44 | localStorage.setItem('SNProfileID', profileID) 45 | setLogin(theAcc.nickname) 46 | window.location.reload() 47 | } 48 | 49 | const logOut = async (profileID: string) => { 50 | await AccountService.logout(profileID) 51 | const accountsInit = (await AccountService.getAccounts()) as any[] 52 | 53 | if (accountsInit.length > 0) { 54 | localStorage.setItem('SNProfileID', accountsInit[0]._id) 55 | } else { 56 | localStorage.removeItem('SNProfileID') 57 | } 58 | window.location.reload() 59 | } 60 | 61 | return ( 62 |
63 |

Your accounts

64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | {accounts.map(({ keyring, nickname, _id }) => ( 75 | 76 | 90 | 93 | 106 | 116 | 117 | ))} 118 | 119 |
AccountEncryptedActiveRemove
77 | @{nickname} 78 | {keyring.map((baseInfo) => ( 79 | {baseInfo.username} 84 | } 85 | > 86 | 87 | 88 | ))} 89 | 91 | 92 | 94 | {nickname === login &&
Currently active
} 95 | {!(nickname === login) && ( 96 | 104 | )} 105 |
107 | 115 |
120 | {!accountAdded &&

You need to add an account first

} 121 | Add new account 122 |
123 | ) 124 | } 125 | -------------------------------------------------------------------------------- /src/renderer/views/WatchView.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useRef, useState } from 'react'; 2 | import { Dropdown } from 'react-bootstrap'; 3 | import RefLink from '../../main/RefLink'; 4 | import EmptyProfile from '../assets/img/EmptyProfile.png'; 5 | import { IPFS_HOST } from '../../common/constants'; 6 | import { CollapsibleText } from '../components/CollapsibleText'; 7 | import { FollowWidget } from '../components/widgets/FollowWidget'; 8 | import { VoteWidget } from '../components/video/VoteWidget'; 9 | import { CommentSection } from '../components/video/CommentSection'; 10 | import { VideoTeaser } from '../components/video/VideoTeaser'; 11 | import { WatchViewContent } from './WatchView/WatchViewContent'; 12 | import { DHTProviders } from '../../components/DHTProviders'; 13 | import { CustomToggle } from '../../components/CustomToggle'; 14 | import ArraySearch from 'arraysearch'; 15 | import * as IPFSHTTPClient from 'ipfs-http-client'; 16 | import { generalFetch } from './WatchView/watchViewHelpers/generalFetch'; 17 | import { mountPlayer } from './WatchView/watchViewHelpers/mountPlayer'; 18 | import { recordView } from './WatchView/watchViewHelpers/recordView'; 19 | import { gearSelect } from './WatchView/watchViewHelpers/gearSelect'; 20 | import { retrieveRecommended } from './WatchView/watchViewHelpers/retrieveRecommended'; 21 | import { PinLocally } from './WatchView/watchViewHelpers/PinLocally'; 22 | import { showDebug } from './WatchView/watchViewHelpers/showDebug'; 23 | const Finder = ArraySearch.Finder 24 | 25 | let ipfsClient = IPFSHTTPClient.create({ url: IPFS_HOST }) 26 | 27 | 28 | export function WatchView(props: any) { 29 | const player = useRef() 30 | const [videoInfo, setVideoInfo] = useState({}) 31 | const [postInfo, setPostInfo] = useState({}) 32 | const [profilePictureURL, setProfilePictureUrl] = useState(EmptyProfile) 33 | const [videoLink, setVideoLink] = useState('') 34 | const [recommendedVideos, setRecommendedVideos] = useState([]) 35 | const [loaded, setLoaded] = useState(false) 36 | const [loadingMessage, setLoadingMessage] = useState('') 37 | const [rootCid, setRootCid] = useState(''); 38 | 39 | const reflink = useMemo(() => { 40 | return props.match.params.reflink 41 | }, []) 42 | 43 | const reflinkParsed = useMemo(() => { 44 | return RefLink.parse(reflink) as any 45 | }, [reflink]) 46 | useEffect(() => { 47 | const load = async () => { 48 | try { 49 | await generalFetch( 50 | reflink, 51 | setVideoInfo, 52 | setPostInfo, 53 | setProfilePictureUrl, 54 | setRootCid 55 | ); 56 | setLoadingMessage('Loading: Mounting player...'); 57 | await mountPlayer(reflink, setVideoLink, recordView); 58 | } catch (ex) { 59 | console.log(ex); 60 | setLoadingMessage('Loading resulted in error'); 61 | throw ex; 62 | } 63 | setLoaded(true); 64 | await retrieveRecommended(postInfo, setRecommendedVideos); 65 | }; 66 | void load(); 67 | }, []); 68 | 69 | useEffect(() => { 70 | window.scrollTo(0, 0); 71 | const update = async () => { 72 | await generalFetch(reflink, setVideoInfo, setPostInfo, setProfilePictureUrl, setRootCid); 73 | await mountPlayer(reflink, setVideoLink, recordView); 74 | await retrieveRecommended(postInfo, setRecommendedVideos); 75 | player.current?.ExecUpdate(); 76 | }; 77 | console.log('Updating...'); 78 | void update(); 79 | console.log('Updated'); 80 | }, [reflink]); 81 | 82 | return ( 83 | PinLocally(videoInfo, reflink)} 95 | showDebug={showDebug} 96 | DHTProviders={DHTProviders} 97 | VoteWidget={VoteWidget} 98 | FollowWidget={FollowWidget} 99 | CollapsibleText={CollapsibleText} 100 | CommentSection={CommentSection} 101 | VideoTeaser={VideoTeaser} 102 | CustomToggle={CustomToggle} 103 | Dropdown={Dropdown} 104 | gearSelect={gearSelect} 105 | /> 106 | ) 107 | } 108 | -------------------------------------------------------------------------------- /src/renderer/assets/img/3S_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 15 | 16 | 17 | 18 | 26 | 27 | 28 | 31 | 33 | 34 | 38 | 45 | 48 | 49 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/renderer/components/video/VideoWidget.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from 'react' 2 | import Reflink from '../../../main/RefLink' 3 | import DateTime from 'date-and-time' 4 | import PlaySVG from '../../assets/img/play.svg' 5 | import { FaUser } from 'react-icons/fa' 6 | import convert from 'convert-units' 7 | import { binary_to_base58 } from 'base58-js' 8 | import { HashRouter } from 'react-router-dom' 9 | import nsfwWarning from '../../assets/img/nsfw.png' 10 | import IpfsLogo from '../../assets/img/ipfs-logo-vector-ice.svg' 11 | import { OverlayTrigger, Tooltip } from 'react-bootstrap' 12 | import { VideoService } from '../../services/video.service' 13 | 14 | export function VideoWidget(props: any) { 15 | const video_info = useMemo(() => { 16 | return props 17 | }, []) 18 | 19 | const reflink = useMemo(() => { 20 | return Reflink.parse(props.reflink) 21 | }, [props.reflink]) 22 | 23 | const [thumbnailUrl, setThumbnailUrl] = useState('') 24 | 25 | useEffect(() => { 26 | const load = async () => { 27 | let thumbnail: string 28 | if (props.isNsfw === true) { 29 | thumbnail = nsfwWarning 30 | } else { 31 | const [, author, permlink] = props.reflink.split(':') 32 | // thumbnail = await VideoService.getNewThumbnailURL(author, permlink) 33 | console.log(props) 34 | // thumbnail = props.three_video.thumbnail_url 35 | } 36 | 37 | setThumbnailUrl(thumbnail) 38 | } 39 | 40 | void load() 41 | }, []) 42 | 43 | return ( 44 | 45 |
46 |
47 | {/*
48 | 49 | {props.views} 50 |
*/} 51 |
52 | {(() => { 53 | const pattern = DateTime.compile('mm:ss') 54 | return DateTime.format(new Date(video_info.duration * 1000), pattern) 55 | })()} 56 |
57 | 58 | 69 | 70 |
71 | 72 | 79 | {video_info.title} 80 | 81 | 82 |
83 | 84 | 85 | 86 | {' '} 87 | {reflink.root} 88 | 89 | 90 | 91 |
92 | 93 | {(() => { 94 | const dateBest = convert( 95 | new Date().getTime() - (new Date(video_info.created) as any) / 1, 96 | ) 97 | .from('ms') 98 | .toBest() 99 | if (Math.round(dateBest.val) >= 2) { 100 | return `${Math.round(dateBest.val)} ${dateBest.plural} ago` 101 | } else { 102 | return `${Math.round(dateBest.val)} ${dateBest.singular} ago` 103 | } 104 | })()} 105 | 106 | {props.isIpfs ? ( 107 |
108 | Video available on IPFS} 110 | > 111 | 112 | 113 |
114 | ) : null} 115 |
116 |
117 |
118 | ) 119 | } 120 | -------------------------------------------------------------------------------- /src/main/ipcAdapter.ts: -------------------------------------------------------------------------------- 1 | import { CoreService } from './core' 2 | 3 | const PromiseIPC = require('electron-promise-ipc') 4 | /** 5 | * Section of code to translate IPC promise calls to and from renderer and main prcoess/core bundle. 6 | */ 7 | class IpcAdapter { 8 | core: CoreService 9 | constructor(core: CoreService) { 10 | this.core = core 11 | } 12 | start() { 13 | //distillerDb 14 | PromiseIPC.on('distiller.getTag', async (tag, options) => { 15 | return await this.core.distillerDB.getTag(tag, options) 16 | }) 17 | PromiseIPC.on('distiller.getContent', async (reflink, options) => { 18 | return await this.core.distillerDB.getContent(reflink, options) 19 | }) 20 | PromiseIPC.on('distiller.getPosts', async (reflink, options) => { 21 | return await this.core.distillerDB.getPosts(reflink, options) 22 | }) 23 | PromiseIPC.on('distiller.getChildren', async (reflink, options) => { 24 | return await this.core.distillerDB.getChildren(reflink, options) 25 | }) 26 | PromiseIPC.on('distiller.getAccount', async (reflink, options) => { 27 | return await this.core.distillerDB.getAccount(reflink, options) 28 | }) 29 | PromiseIPC.on('distiller.getState', async (stateKey) => { 30 | return await this.core.distillerDB.getState(stateKey) 31 | }) 32 | PromiseIPC.on('distiller.getFollowerCount', async (reflink) => { 33 | return await this.core.distillerDB.getFollowerCount(reflink) 34 | }) 35 | //Blocklist 36 | PromiseIPC.on('blocklist.add', async (reflink, options) => { 37 | return await this.core.blocklist.add(reflink, options) 38 | }) 39 | PromiseIPC.on('blocklist.has', async (reflink) => { 40 | return await this.core.blocklist.has(reflink) 41 | }) 42 | PromiseIPC.on('blocklist.rm', async (reflink) => { 43 | return await this.core.blocklist.rm(reflink) 44 | }) 45 | PromiseIPC.on('blocklist.ls', async (query) => { 46 | return await this.core.blocklist.ls(query) 47 | }) 48 | //Core 49 | PromiseIPC.on('core.install', async () => { 50 | return await this.core.install() 51 | }) 52 | PromiseIPC.on('core.status', async () => { 53 | return await this.core.status() 54 | }) 55 | PromiseIPC.on('core.ready', async () => { 56 | return await this.core.ready() 57 | }) 58 | //Encoder 59 | PromiseIPC.on('encoder.createJob', async (req_obj) => { 60 | return await this.core.encoder.createJob(req_obj) 61 | }) 62 | PromiseIPC.on('encoder.status', async (id) => { 63 | return await this.core.encoder.status(id) 64 | }) 65 | PromiseIPC.on('encoder.getjoboutput', async (id) => { 66 | return await this.core.encoder.getJobOutput(id) 67 | }) 68 | PromiseIPC.on('encoder.ready', async () => { 69 | return await this.core.encoder.ready() 70 | }) 71 | //Pins 72 | PromiseIPC.on('pins.add', async (doc) => { 73 | return await this.core.pins.add(doc) 74 | }) 75 | PromiseIPC.on('pins.rm', async (ref) => { 76 | return await this.core.pins.rm(ref) 77 | }) 78 | PromiseIPC.on('pins.ls', async () => { 79 | return await this.core.pins.ls() 80 | }) 81 | PromiseIPC.on('pins.mv', async (ref) => { 82 | return await this.core.pins.mv(ref.sender) 83 | }) 84 | //Accounts 85 | PromiseIPC.on('accounts.createProfile', async (doc) => { 86 | return await this.core.accounts.createProfile(doc) 87 | }) 88 | PromiseIPC.on('accounts.deleteProfile', async (profileID) => { 89 | return await this.core.accounts.deleteProfile(profileID) 90 | }) 91 | PromiseIPC.on('accounts.get', async (ref) => { 92 | return await this.core.accounts.get(ref) 93 | }) 94 | PromiseIPC.on('accounts.has', async (ref) => { 95 | return await this.core.accounts.has(ref) 96 | }) 97 | PromiseIPC.on('accounts.ls', async (obj) => { 98 | return await this.core.accounts.ls(obj) 99 | }) 100 | PromiseIPC.on('accounts.addProfileKey', async (ref) => { 101 | return await this.core.accounts.addProfileKey(ref) 102 | }) 103 | PromiseIPC.on('accounts.getProfileKey', async (ref) => { 104 | return await this.core.accounts.getProfileKey(ref) 105 | }) 106 | PromiseIPC.on('accounts.deleteProfileKey', async (ref) => { 107 | return await this.core.accounts.deleteProfileKey(ref) 108 | }) 109 | PromiseIPC.on('accounts.deleteProfile', async (ref) => { 110 | return await this.core.accounts.deleteProfile(ref) 111 | }) 112 | } 113 | } 114 | export default IpcAdapter 115 | -------------------------------------------------------------------------------- /src/renderer/views/UploaderView.tsx: -------------------------------------------------------------------------------- 1 | import './Uploader.css' 2 | import * as IPFSHTTPClient from 'ipfs-http-client' 3 | import React, { useEffect, useRef, useState } from 'react' 4 | 5 | 6 | import { IPFS_HOST } from '../../common/constants' 7 | import LoadingMessage from '../components/LoadingMessage' 8 | import UploaderViewContent from './UploaderView/uploaderViewContent' 9 | import { calculatePercentage } from './UploaderView/calculatePercentage'; 10 | import { normalizeSize } from './UploaderView/normalizeSize'; 11 | import { publish } from './UploaderView/publish'; 12 | import { videoSelect } from './UploaderView/videoSelect'; 13 | import { thumbnailSelect } from './UploaderView/thumbnailSelect' 14 | import { startEncode } from './UploaderView/startEncode'; 15 | export function UploaderView() { 16 | const videoUpload = useRef() 17 | const thumbnailUpload = useRef() 18 | const thumbnailPreview = useRef('') 19 | // const publishForm = useRef() 20 | const [publishFormTitle, setPublishFormTitle] = useState('') 21 | const [publishFormDescription, setPublishFormDescription] = useState('') 22 | const [publishFormTags, setPublishFormTags] = useState('') 23 | 24 | // const hwaccelOption = useRef() 25 | const ipfs = useRef() 26 | 27 | const [logData, setLogData] = useState([]) 28 | const [hwaccelOption, setHwaccelOption] = useState('') 29 | 30 | const [videoSourceFile, setVideoSourceFile] = useState() 31 | const [encodingInProgress, setEncodingInProgress] = useState(false) 32 | const [progress, setProgress] = useState({}) 33 | const [statusInfo, setStatusInfo] = useState({ progress: {} }) 34 | const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState('') 35 | const [videoInfo, setVideoInfo] = useState({ 36 | path: null, 37 | size: 0, 38 | cid: null, 39 | language: '', 40 | duration: null, 41 | }) 42 | const [thumbnailInfo, setThumbnailInfo] = useState({ 43 | path: null, 44 | size: 0, 45 | cid: null, 46 | }) 47 | const [startTime, setStartTime] = useState() 48 | const [endTime, setEndTime] = useState(0) 49 | const [publishReady, setPublishReady] = useState(false) 50 | const [blockedGlobalMessage, setBlockedGlobalMessage] = useState('') 51 | 52 | useEffect(() => { 53 | ipfs.current = IPFSHTTPClient.create({ url: IPFS_HOST }) 54 | }, []) 55 | 56 | const handlePublish = async () => { 57 | await publish({ 58 | videoInfo, thumbnailInfo, publishFormTitle, publishFormDescription, publishFormTags, setBlockedGlobalMessage, ipfs, 59 | }); 60 | }; 61 | 62 | const handleVideoSelect = (e) => { 63 | videoSelect( 64 | e, setVideoSourceFile, setLogData, logData, videoInfo 65 | ); 66 | }; 67 | 68 | const handleThumbnailSelect = async (e) => { 69 | await thumbnailSelect({ 70 | e, thumbnailPreview, setThumbnailInfo, setVideoSourceFile, setLogData, ipfs, logData, videoInfo 71 | }); 72 | }; 73 | 74 | const handleStartEncode = async () => { 75 | await startEncode({ 76 | event, videoSourceFile, hwaccelOption, setEncodingInProgress, setStartTime, setEndTime, setProgress, setStatusInfo, setEstimatedTimeRemaining, setVideoInfo, setPublishReady, progress, statusInfo 77 | }); 78 | }; 79 | 80 | 81 | 82 | 83 | if (blockedGlobalMessage) { 84 | return ( 85 | 89 | ) 90 | } 91 | 92 | return ( 93 | 123 | ) 124 | } 125 | -------------------------------------------------------------------------------- /src/renderer/App.tsx: -------------------------------------------------------------------------------- 1 | // file src\renderer\App.tsx: 2 | import React, { useState } from 'react' 3 | import { PoAProgramRunnerProvider } from './views/PoAView/PoAProgramRunnerContext' 4 | import { PoAStateProvider } from './views/PoAView/PoAStateContext' // Import PoAStateProvider 5 | import 'bootstrap/dist/css/bootstrap.min.css' 6 | import { HashRouter, Switch, Route } from 'react-router-dom' 7 | import './css/App.css' 8 | import './css/main.css' 9 | import 'react-notifications/lib/notifications.css' 10 | import { NotificationContainer } from 'react-notifications' 11 | import Popup from 'react-popup' 12 | import './css/Popup.css' 13 | import { AccountsView } from './views/Accounts' 14 | import { BlocklistView } from './views/BlocklistView' 15 | import { LoginView } from './views/LoginView' 16 | import { CommunitiesView } from './views/CommunitiesView' 17 | import { WatchView } from './views/WatchView' 18 | import { LeaderboardView } from './views/LeaderboardView' 19 | import { CommunityView } from './views/CommunityView' 20 | import { IpfsConsoleView } from './views/IpfsConsoleView' 21 | import { PoAView } from './views/PoAView' 22 | import { GridFeedView } from './views/GridFeedView' 23 | import { NotFoundView } from './views/NotFoundView' 24 | import { PinsView } from './views/PinsView' 25 | import { UploaderView } from './views/UploaderView' 26 | import { UserView } from './views/UserView' 27 | import { TopNavbar } from './components/TopNavbar' 28 | import { SideNavbar } from './components/SideNavbar' 29 | import { StartUp } from './StartUp' 30 | import { CreatorStudioView } from './views/CreatorStudioView' 31 | import { useQuery, gql, ApolloClient, InMemoryCache } from '@apollo/client' 32 | import { TagView } from './views/TagView' 33 | 34 | export const IndexerClient = new ApolloClient({ 35 | uri: 'https://union.us-02.infra.3speak.tv/api/v2/graphql', 36 | cache: new InMemoryCache(), 37 | }) 38 | 39 | export function App() { 40 | return ( 41 | 42 | 43 |
44 | 45 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | 75 | 81 | 82 | 83 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 |
109 |
110 |
111 | ) 112 | } 113 | --------------------------------------------------------------------------------