diff --git a/.flowconfig b/.flowconfig index 0ae16f24e..4124fddb6 100644 --- a/.flowconfig +++ b/.flowconfig @@ -7,13 +7,6 @@ [include] [libs] -./flow-typed -node_modules/lbry-redux/flow-typed/ -node_modules/lbryinc/flow-typed/ - -[untyped] -.*/node_modules/lbry-redux -.*/node_modules/lbryinc [lints] diff --git a/electron/Daemon.js b/electron/Daemon.js index 976423b25..2d11e36a9 100644 --- a/electron/Daemon.js +++ b/electron/Daemon.js @@ -1,7 +1,7 @@ import path from 'path'; import fs from 'fs'; import { spawn, execSync } from 'child_process'; -import { Lbry } from 'lbry-redux'; +import Lbry from 'lbry'; export default class Daemon { static lbrynetPath = diff --git a/electron/index.js b/electron/index.js index ec7cc01d1..be4d11801 100644 --- a/electron/index.js +++ b/electron/index.js @@ -6,7 +6,7 @@ import SemVer from 'semver'; import https from 'https'; import { app, dialog, ipcMain, session, shell } from 'electron'; import { autoUpdater } from 'electron-updater'; -import { Lbry } from 'lbry-redux'; +import Lbry from 'lbry'; import LbryFirstInstance from './LbryFirstInstance'; import Daemon from './Daemon'; import isDev from 'electron-is-dev'; diff --git a/electron/startSandbox.js b/electron/startSandbox.js index 4c6d09e97..d2f1077d9 100644 --- a/electron/startSandbox.js +++ b/electron/startSandbox.js @@ -8,7 +8,7 @@ if (typeof global.fetch === 'object') { global.fetch = global.fetch.default; } -const { Lbry } = require('lbry-redux'); +const Lbry = require('lbry'); delete global.window; diff --git a/extras/lbry-first/lbry-first.js b/extras/lbry-first/lbry-first.js new file mode 100644 index 000000000..a6025b753 --- /dev/null +++ b/extras/lbry-first/lbry-first.js @@ -0,0 +1,184 @@ +// @flow +/* + LBRY FIRST does not work due to api changes + */ +import 'proxy-polyfill'; + +const CHECK_LBRYFIRST_STARTED_TRY_NUMBER = 200; +// +// Basic LBRYFIRST connection config +// Offers a proxy to call LBRYFIRST methods + +// +const LbryFirst: LbryFirstTypes = { + isConnected: false, + connectPromise: null, + lbryFirstConnectionString: 'http://localhost:1337/rpc', + apiRequestHeaders: { 'Content-Type': 'application/json' }, + + // Allow overriding lbryFirst connection string (e.g. to `/api/proxy` for lbryweb) + setLbryFirstConnectionString: (value: string) => { + LbryFirst.lbryFirstConnectionString = value; + }, + + setApiHeader: (key: string, value: string) => { + LbryFirst.apiRequestHeaders = Object.assign(LbryFirst.apiRequestHeaders, { [key]: value }); + }, + + unsetApiHeader: key => { + Object.keys(LbryFirst.apiRequestHeaders).includes(key) && + delete LbryFirst.apiRequestHeaders['key']; + }, + // Allow overriding Lbry methods + overrides: {}, + setOverride: (methodName, newMethod) => { + LbryFirst.overrides[methodName] = newMethod; + }, + getApiRequestHeaders: () => LbryFirst.apiRequestHeaders, + + // LbryFirst Methods + status: (params = {}) => lbryFirstCallWithResult('status', params), + stop: () => lbryFirstCallWithResult('stop', {}), + version: () => lbryFirstCallWithResult('version', {}), + + // Upload to youtube + upload: (params: { title: string, description: string, file_path: ?string } = {}) => { + // Only upload when originally publishing for now + if (!params.file_path) { + return Promise.resolve(); + } + + const uploadParams: { + Title: string, + Description: string, + FilePath: string, + Category: string, + Keywords: string, + } = { + Title: params.title, + Description: params.description, + FilePath: params.file_path, + Category: '', + Keywords: '', + }; + + return lbryFirstCallWithResult('youtube.Upload', uploadParams); + }, + + hasYTAuth: (token: string) => { + const hasYTAuthParams = {}; + hasYTAuthParams.AuthToken = token; + return lbryFirstCallWithResult('youtube.HasAuth', hasYTAuthParams); + }, + + ytSignup: () => { + const emptyParams = {}; + return lbryFirstCallWithResult('youtube.Signup', emptyParams); + }, + + remove: () => { + const emptyParams = {}; + return lbryFirstCallWithResult('youtube.Remove', emptyParams); + }, + + // Connect to lbry-first + connect: () => { + if (LbryFirst.connectPromise === null) { + LbryFirst.connectPromise = new Promise((resolve, reject) => { + let tryNum = 0; + // Check every half second to see if the lbryFirst is accepting connections + function checkLbryFirstStarted() { + tryNum += 1; + LbryFirst.status() + .then(resolve) + .catch(() => { + if (tryNum <= CHECK_LBRYFIRST_STARTED_TRY_NUMBER) { + setTimeout(checkLbryFirstStarted, tryNum < 50 ? 400 : 1000); + } else { + reject(new Error('Unable to connect to LBRY')); + } + }); + } + + checkLbryFirstStarted(); + }); + } + + // Flow thinks this could be empty, but it will always return a promise + // $FlowFixMe + return LbryFirst.connectPromise; + }, +}; + +function checkAndParse(response) { + if (response.status >= 200 && response.status < 300) { + return response.json(); + } + return response.json().then(json => { + let error; + if (json.error) { + const errorMessage = typeof json.error === 'object' ? json.error.message : json.error; + error = new Error(errorMessage); + } else { + error = new Error('Protocol error with unknown response signature'); + } + return Promise.reject(error); + }); +} + +export function apiCall(method: string, params: ?{}, resolve: Function, reject: Function) { + const counter = new Date().getTime(); + const paramsArray = [params]; + const options = { + method: 'POST', + headers: LbryFirst.apiRequestHeaders, + body: JSON.stringify({ + jsonrpc: '2.0', + method, + params: paramsArray, + id: counter, + }), + }; + + return fetch(LbryFirst.lbryFirstConnectionString, options) + .then(checkAndParse) + .then(response => { + const error = response.error || (response.result && response.result.error); + + if (error) { + return reject(error); + } + return resolve(response.result); + }) + .catch(reject); +} + +function lbryFirstCallWithResult(name: string, params: ?{} = {}) { + return new Promise((resolve, reject) => { + apiCall( + name, + params, + result => { + resolve(result); + }, + reject + ); + }); +} + +// This is only for a fallback +// If there is a LbryFirst method that is being called by an app, it should be added to /flow-typed/LbryFirst.js +const lbryFirstProxy = new Proxy(LbryFirst, { + get(target: LbryFirstTypes, name: string) { + if (name in target) { + return target[name]; + } + + return (params = {}) => + new Promise((resolve, reject) => { + apiCall(name, params, resolve, reject); + }); + }, +}); + +export default lbryFirstProxy; diff --git a/extras/lbryinc/constants/action_types.js b/extras/lbryinc/constants/action_types.js new file mode 100644 index 000000000..65723007d --- /dev/null +++ b/extras/lbryinc/constants/action_types.js @@ -0,0 +1,97 @@ +// Claims +export const FETCH_FEATURED_CONTENT_STARTED = 'FETCH_FEATURED_CONTENT_STARTED'; +export const FETCH_FEATURED_CONTENT_COMPLETED = 'FETCH_FEATURED_CONTENT_COMPLETED'; +export const FETCH_TRENDING_CONTENT_STARTED = 'FETCH_TRENDING_CONTENT_STARTED'; +export const FETCH_TRENDING_CONTENT_COMPLETED = 'FETCH_TRENDING_CONTENT_COMPLETED'; +export const RESOLVE_URIS_STARTED = 'RESOLVE_URIS_STARTED'; +export const RESOLVE_URIS_COMPLETED = 'RESOLVE_URIS_COMPLETED'; +export const FETCH_CHANNEL_CLAIMS_STARTED = 'FETCH_CHANNEL_CLAIMS_STARTED'; +export const FETCH_CHANNEL_CLAIMS_COMPLETED = 'FETCH_CHANNEL_CLAIMS_COMPLETED'; +export const FETCH_CHANNEL_CLAIM_COUNT_STARTED = 'FETCH_CHANNEL_CLAIM_COUNT_STARTED'; +export const FETCH_CHANNEL_CLAIM_COUNT_COMPLETED = 'FETCH_CHANNEL_CLAIM_COUNT_COMPLETED'; +export const FETCH_CLAIM_LIST_MINE_STARTED = 'FETCH_CLAIM_LIST_MINE_STARTED'; +export const FETCH_CLAIM_LIST_MINE_COMPLETED = 'FETCH_CLAIM_LIST_MINE_COMPLETED'; +export const ABANDON_CLAIM_STARTED = 'ABANDON_CLAIM_STARTED'; +export const ABANDON_CLAIM_SUCCEEDED = 'ABANDON_CLAIM_SUCCEEDED'; +export const FETCH_CHANNEL_LIST_STARTED = 'FETCH_CHANNEL_LIST_STARTED'; +export const FETCH_CHANNEL_LIST_COMPLETED = 'FETCH_CHANNEL_LIST_COMPLETED'; +export const CREATE_CHANNEL_STARTED = 'CREATE_CHANNEL_STARTED'; +export const CREATE_CHANNEL_COMPLETED = 'CREATE_CHANNEL_COMPLETED'; +export const PUBLISH_STARTED = 'PUBLISH_STARTED'; +export const PUBLISH_COMPLETED = 'PUBLISH_COMPLETED'; +export const PUBLISH_FAILED = 'PUBLISH_FAILED'; +export const SET_PLAYING_URI = 'SET_PLAYING_URI'; +export const SET_CONTENT_POSITION = 'SET_CONTENT_POSITION'; +export const SET_CONTENT_LAST_VIEWED = 'SET_CONTENT_LAST_VIEWED'; +export const CLEAR_CONTENT_HISTORY_URI = 'CLEAR_CONTENT_HISTORY_URI'; +export const CLEAR_CONTENT_HISTORY_ALL = 'CLEAR_CONTENT_HISTORY_ALL'; + +// Subscriptions +export const CHANNEL_SUBSCRIBE = 'CHANNEL_SUBSCRIBE'; +export const CHANNEL_UNSUBSCRIBE = 'CHANNEL_UNSUBSCRIBE'; +export const CHANNEL_SUBSCRIPTION_ENABLE_NOTIFICATIONS = + 'CHANNEL_SUBSCRIPTION_ENABLE_NOTIFICATIONS'; +export const CHANNEL_SUBSCRIPTION_DISABLE_NOTIFICATIONS = + 'CHANNEL_SUBSCRIPTION_DISABLE_NOTIFICATIONS'; +export const HAS_FETCHED_SUBSCRIPTIONS = 'HAS_FETCHED_SUBSCRIPTIONS'; +export const SET_SUBSCRIPTION_LATEST = 'SET_SUBSCRIPTION_LATEST'; +export const UPDATE_SUBSCRIPTION_UNREADS = 'UPDATE_SUBSCRIPTION_UNREADS'; +export const REMOVE_SUBSCRIPTION_UNREADS = 'REMOVE_SUBSCRIPTION_UNREADS'; +export const CHECK_SUBSCRIPTION_STARTED = 'CHECK_SUBSCRIPTION_STARTED'; +export const CHECK_SUBSCRIPTION_COMPLETED = 'CHECK_SUBSCRIPTION_COMPLETED'; +export const CHECK_SUBSCRIPTIONS_SUBSCRIBE = 'CHECK_SUBSCRIPTIONS_SUBSCRIBE'; +export const FETCH_SUBSCRIPTIONS_START = 'FETCH_SUBSCRIPTIONS_START'; +export const FETCH_SUBSCRIPTIONS_FAIL = 'FETCH_SUBSCRIPTIONS_FAIL'; +export const FETCH_SUBSCRIPTIONS_SUCCESS = 'FETCH_SUBSCRIPTIONS_SUCCESS'; +export const SET_VIEW_MODE = 'SET_VIEW_MODE'; +export const GET_SUGGESTED_SUBSCRIPTIONS_START = 'GET_SUGGESTED_SUBSCRIPTIONS_START'; +export const GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS = 'GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS'; +export const GET_SUGGESTED_SUBSCRIPTIONS_FAIL = 'GET_SUGGESTED_SUBSCRIPTIONS_FAIL'; +export const SUBSCRIPTION_FIRST_RUN_COMPLETED = 'SUBSCRIPTION_FIRST_RUN_COMPLETED'; +export const VIEW_SUGGESTED_SUBSCRIPTIONS = 'VIEW_SUGGESTED_SUBSCRIPTIONS'; + +// Blacklist +export const FETCH_BLACK_LISTED_CONTENT_STARTED = 'FETCH_BLACK_LISTED_CONTENT_STARTED'; +export const FETCH_BLACK_LISTED_CONTENT_COMPLETED = 'FETCH_BLACK_LISTED_CONTENT_COMPLETED'; +export const FETCH_BLACK_LISTED_CONTENT_FAILED = 'FETCH_BLACK_LISTED_CONTENT_FAILED'; +export const BLACK_LISTED_CONTENT_SUBSCRIBE = 'BLACK_LISTED_CONTENT_SUBSCRIBE'; + +// Filtered list +export const FETCH_FILTERED_CONTENT_STARTED = 'FETCH_FILTERED_CONTENT_STARTED'; +export const FETCH_FILTERED_CONTENT_COMPLETED = 'FETCH_FILTERED_CONTENT_COMPLETED'; +export const FETCH_FILTERED_CONTENT_FAILED = 'FETCH_FILTERED_CONTENT_FAILED'; +export const FILTERED_CONTENT_SUBSCRIBE = 'FILTERED_CONTENT_SUBSCRIBE'; + +// Cost Info +export const FETCH_COST_INFO_STARTED = 'FETCH_COST_INFO_STARTED'; +export const FETCH_COST_INFO_COMPLETED = 'FETCH_COST_INFO_COMPLETED'; + +// Stats +export const FETCH_VIEW_COUNT_STARTED = 'FETCH_VIEW_COUNT_STARTED'; +export const FETCH_VIEW_COUNT_FAILED = 'FETCH_VIEW_COUNT_FAILED'; +export const FETCH_VIEW_COUNT_COMPLETED = 'FETCH_VIEW_COUNT_COMPLETED'; +export const FETCH_SUB_COUNT_STARTED = 'FETCH_SUB_COUNT_STARTED'; +export const FETCH_SUB_COUNT_FAILED = 'FETCH_SUB_COUNT_FAILED'; +export const FETCH_SUB_COUNT_COMPLETED = 'FETCH_SUB_COUNT_COMPLETED'; + +// Cross-device Sync +export const GET_SYNC_STARTED = 'GET_SYNC_STARTED'; +export const GET_SYNC_COMPLETED = 'GET_SYNC_COMPLETED'; +export const GET_SYNC_FAILED = 'GET_SYNC_FAILED'; +export const SET_SYNC_STARTED = 'SET_SYNC_STARTED'; +export const SET_SYNC_FAILED = 'SET_SYNC_FAILED'; +export const SET_SYNC_COMPLETED = 'SET_SYNC_COMPLETED'; +export const SET_DEFAULT_ACCOUNT = 'SET_DEFAULT_ACCOUNT'; +export const SYNC_APPLY_STARTED = 'SYNC_APPLY_STARTED'; +export const SYNC_APPLY_COMPLETED = 'SYNC_APPLY_COMPLETED'; +export const SYNC_APPLY_FAILED = 'SYNC_APPLY_FAILED'; +export const SYNC_APPLY_BAD_PASSWORD = 'SYNC_APPLY_BAD_PASSWORD'; +export const SYNC_RESET = 'SYNC_RESET'; + +// Lbry.tv +export const UPDATE_UPLOAD_PROGRESS = 'UPDATE_UPLOAD_PROGRESS'; + +// User +export const GENERATE_AUTH_TOKEN_FAILURE = 'GENERATE_AUTH_TOKEN_FAILURE'; +export const GENERATE_AUTH_TOKEN_STARTED = 'GENERATE_AUTH_TOKEN_STARTED'; +export const GENERATE_AUTH_TOKEN_SUCCESS = 'GENERATE_AUTH_TOKEN_SUCCESS'; diff --git a/extras/lbryinc/constants/claim.js b/extras/lbryinc/constants/claim.js new file mode 100644 index 000000000..4cf33ce09 --- /dev/null +++ b/extras/lbryinc/constants/claim.js @@ -0,0 +1,5 @@ +export const MINIMUM_PUBLISH_BID = 0.00000001; + +export const CHANNEL_ANONYMOUS = 'anonymous'; +export const CHANNEL_NEW = 'new'; +export const PAGE_SIZE = 20; diff --git a/extras/lbryinc/constants/errors.js b/extras/lbryinc/constants/errors.js new file mode 100644 index 000000000..90444632a --- /dev/null +++ b/extras/lbryinc/constants/errors.js @@ -0,0 +1,4 @@ +export const ALREADY_CLAIMED = + 'once the invite reward has been claimed the referrer cannot be changed'; +export const REFERRER_NOT_FOUND = + 'A lbry.tv account could not be found for the referrer you provided.'; diff --git a/extras/lbryinc/constants/youtube.js b/extras/lbryinc/constants/youtube.js new file mode 100644 index 000000000..f2db86ab1 --- /dev/null +++ b/extras/lbryinc/constants/youtube.js @@ -0,0 +1,11 @@ +export const YOUTUBE_SYNC_NOT_TRANSFERRED = 'not_transferred'; +export const YOUTUBE_SYNC_PENDING = 'pending'; +export const YOUTUBE_SYNC_PENDING_EMAIL = 'pendingemail'; +export const YOUTUBE_SYNC_PENDING_TRANSFER = 'pending_transfer'; +export const YOUTUBE_SYNC_COMPLETED_TRANSFER = 'completed_transfer'; +export const YOUTUBE_SYNC_QUEUED = 'queued'; +export const YOUTUBE_SYNC_SYNCING = 'syncing'; +export const YOUTUBE_SYNC_SYNCED = 'synced'; +export const YOUTUBE_SYNC_FAILED = 'failed'; +export const YOUTUBE_SYNC_PENDINGUPGRADE = 'pendingupgrade'; +export const YOUTUBE_SYNC_ABANDONDED = 'abandoned'; diff --git a/extras/lbryinc/index.js b/extras/lbryinc/index.js new file mode 100644 index 000000000..b68d006e6 --- /dev/null +++ b/extras/lbryinc/index.js @@ -0,0 +1,83 @@ +import * as LBRYINC_ACTIONS from 'constants/action_types'; +import * as YOUTUBE_STATUSES from 'constants/youtube'; +import * as ERRORS from 'constants/errors'; +import Lbryio from './lbryio'; + +export { Lbryio }; + +export function testTheThing() { + console.log('tested'); +} + +// constants +export { LBRYINC_ACTIONS, YOUTUBE_STATUSES, ERRORS }; + +// utils +export { doTransifexUpload } from 'util/transifex-upload'; + +// actions +export { doGenerateAuthToken } from './redux/actions/auth'; +export { doFetchCostInfoForUri } from './redux/actions/cost_info'; +export { doBlackListedOutpointsSubscribe } from './redux/actions/blacklist'; +export { doFilteredOutpointsSubscribe } from './redux/actions/filtered'; +// export { doFetchFeaturedUris, doFetchTrendingUris } from './redux/actions/homepage'; +export { doFetchViewCount, doFetchSubCount } from './redux/actions/stats'; +export { + doCheckSync, + doGetSync, + doSetSync, + doSetDefaultAccount, + doSyncApply, + doResetSync, + doSyncEncryptAndDecrypt, +} from 'redux/actions/sync'; +export { doUpdateUploadProgress } from './redux/actions/web'; + +// reducers +export { authReducer } from './redux/reducers/auth'; +export { costInfoReducer } from './redux/reducers/cost_info'; +export { blacklistReducer } from './redux/reducers/blacklist'; +export { filteredReducer } from './redux/reducers/filtered'; +// export { homepageReducer } from './redux/reducers/homepage'; +export { statsReducer } from './redux/reducers/stats'; +export { syncReducer } from './redux/reducers/sync'; +export { webReducer } from './redux/reducers/web'; + +// selectors +export { selectAuthToken, selectIsAuthenticating } from './redux/selectors/auth'; +export { + makeSelectFetchingCostInfoForUri, + makeSelectCostInfoForUri, + selectAllCostInfoByUri, + selectFetchingCostInfo, +} from './redux/selectors/cost_info'; +export { + selectBlackListedOutpoints, + selectBlacklistedOutpointMap, +} from './redux/selectors/blacklist'; +export { selectFilteredOutpoints, selectFilteredOutpointMap } from './redux/selectors/filtered'; +// export { +// selectFeaturedUris, +// selectFetchingFeaturedUris, +// selectTrendingUris, +// selectFetchingTrendingUris, +// } from './redux/selectors/homepage'; +export { + selectViewCount, + makeSelectViewCountForUri, + makeSelectSubCountForUri, +} from './redux/selectors/stats'; +export { + selectHasSyncedWallet, + selectSyncData, + selectSyncHash, + selectSetSyncErrorMessage, + selectGetSyncErrorMessage, + selectGetSyncIsPending, + selectSetSyncIsPending, + selectSyncApplyIsPending, + selectHashChanged, + selectSyncApplyErrorMessage, + selectSyncApplyPasswordError, +} from './redux/selectors/sync'; +export { selectCurrentUploads, selectUploadCount } from './redux/selectors/web'; diff --git a/extras/lbryinc/lbryio.js b/extras/lbryinc/lbryio.js new file mode 100644 index 000000000..89c0536e9 --- /dev/null +++ b/extras/lbryinc/lbryio.js @@ -0,0 +1,238 @@ +import * as ACTIONS from 'constants/action_types'; +import Lbry from 'lbry'; +import querystring from 'querystring'; + +const Lbryio = { + enabled: true, + authenticationPromise: null, + exchangePromise: null, + exchangeLastFetched: null, + CONNECTION_STRING: 'https://api.lbry.com/', +}; + +const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000; +const INTERNAL_APIS_DOWN = 'internal_apis_down'; + +// We can't use env's because they aren't passed into node_modules +Lbryio.setLocalApi = endpoint => { + Lbryio.CONNECTION_STRING = endpoint.replace(/\/*$/, '/'); // exactly one slash at the end; +}; + +Lbryio.call = (resource, action, params = {}, method = 'get') => { + if (!Lbryio.enabled) { + return Promise.reject(new Error(__('LBRY internal API is disabled'))); + } + + if (!(method === 'get' || method === 'post')) { + return Promise.reject(new Error(__('Invalid method'))); + } + + function checkAndParse(response) { + if (response.status >= 200 && response.status < 300) { + return response.json(); + } + + if (response.status === 500) { + return Promise.reject(INTERNAL_APIS_DOWN); + } + + if (response) + return response.json().then(json => { + let error; + if (json.error) { + error = new Error(json.error); + } else { + error = new Error('Unknown API error signature'); + } + error.response = response; // This is primarily a hack used in actions/user.js + return Promise.reject(error); + }); + } + + function makeRequest(url, options) { + return fetch(url, options).then(checkAndParse); + } + + return Lbryio.getAuthToken().then(token => { + const fullParams = { auth_token: token, ...params }; + Object.keys(fullParams).forEach(key => { + const value = fullParams[key]; + if (typeof value === 'object') { + fullParams[key] = JSON.stringify(value); + } + }); + + const qs = querystring.stringify(fullParams); + let url = `${Lbryio.CONNECTION_STRING}${resource}/${action}?${qs}`; + + let options = { + method: 'GET', + }; + + if (method === 'post') { + options = { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: qs, + }; + url = `${Lbryio.CONNECTION_STRING}${resource}/${action}`; + } + + return makeRequest(url, options).then(response => response.data); + }); +}; + +Lbryio.authToken = null; + +Lbryio.getAuthToken = () => + new Promise(resolve => { + if (Lbryio.authToken) { + resolve(Lbryio.authToken); + } else if (Lbryio.overrides.getAuthToken) { + Lbryio.overrides.getAuthToken().then(token => { + resolve(token); + }); + } else if (typeof window !== 'undefined') { + const { store } = window; + if (store) { + const state = store.getState(); + const token = state.auth ? state.auth.authToken : null; + Lbryio.authToken = token; + resolve(token); + } + + resolve(null); + } else { + resolve(null); + } + }); + +Lbryio.getCurrentUser = () => Lbryio.call('user', 'me'); + +Lbryio.authenticate = (domain, language) => { + if (!Lbryio.enabled) { + const params = { + id: 1, + primary_email: 'disabled@lbry.io', + has_verified_email: true, + is_identity_verified: true, + is_reward_approved: false, + language: language || 'en', + }; + + return new Promise(resolve => { + resolve(params); + }); + } + + if (Lbryio.authenticationPromise === null) { + Lbryio.authenticationPromise = new Promise((resolve, reject) => { + Lbryio.getAuthToken() + .then(token => { + if (!token || token.length > 60) { + return false; + } + + // check that token works + return Lbryio.getCurrentUser() + .then(user => user) + .catch(error => { + if (error === INTERNAL_APIS_DOWN) { + throw new Error('Internal APIS down'); + } + + return false; + }); + }) + .then(user => { + if (user) { + return user; + } + + return Lbry.status() + .then( + status => + new Promise((res, rej) => { + const appId = + domain && domain !== 'lbry.tv' + ? (domain.replace(/[.]/gi, '') + status.installation_id).slice(0, 66) + : status.installation_id; + Lbryio.call( + 'user', + 'new', + { + auth_token: '', + language: language || 'en', + app_id: appId, + }, + 'post' + ) + .then(response => { + if (!response.auth_token) { + throw new Error('auth_token was not set in the response'); + } + + const { store } = window; + if (Lbryio.overrides.setAuthToken) { + Lbryio.overrides.setAuthToken(response.auth_token); + } + + if (store) { + store.dispatch({ + type: ACTIONS.GENERATE_AUTH_TOKEN_SUCCESS, + data: { authToken: response.auth_token }, + }); + } + Lbryio.authToken = response.auth_token; + return res(response); + }) + .catch(error => rej(error)); + }) + ) + .then(newUser => { + if (!newUser) { + return Lbryio.getCurrentUser(); + } + return newUser; + }); + }) + .then(resolve, reject); + }); + } + + return Lbryio.authenticationPromise; +}; + +Lbryio.getStripeToken = () => + Lbryio.CONNECTION_STRING.startsWith('http://localhost:') + ? 'pk_test_NoL1JWL7i1ipfhVId5KfDZgo' + : 'pk_live_e8M4dRNnCCbmpZzduEUZBgJO'; + +Lbryio.getExchangeRates = () => { + if ( + !Lbryio.exchangeLastFetched || + Date.now() - Lbryio.exchangeLastFetched > EXCHANGE_RATE_TIMEOUT + ) { + Lbryio.exchangePromise = new Promise((resolve, reject) => { + Lbryio.call('lbc', 'exchange_rate', {}, 'get', true) + .then(({ lbc_usd: LBC_USD, lbc_btc: LBC_BTC, btc_usd: BTC_USD }) => { + const rates = { LBC_USD, LBC_BTC, BTC_USD }; + resolve(rates); + }) + .catch(reject); + }); + Lbryio.exchangeLastFetched = Date.now(); + } + return Lbryio.exchangePromise; +}; + +// Allow overriding lbryio methods +// The desktop app will need to use it for getAuthToken because we use electron's ipcRenderer +Lbryio.overrides = {}; +Lbryio.setOverride = (methodName, newMethod) => { + Lbryio.overrides[methodName] = newMethod; +}; + +export default Lbryio; diff --git a/extras/lbryinc/redux/actions/auth.js b/extras/lbryinc/redux/actions/auth.js new file mode 100644 index 000000000..743991a35 --- /dev/null +++ b/extras/lbryinc/redux/actions/auth.js @@ -0,0 +1,38 @@ +import * as ACTIONS from 'constants/action_types'; +import { Lbryio } from 'lbryinc'; + +export function doGenerateAuthToken(installationId) { + return dispatch => { + dispatch({ + type: ACTIONS.GENERATE_AUTH_TOKEN_STARTED, + }); + + Lbryio.call( + 'user', + 'new', + { + auth_token: '', + language: 'en', + app_id: installationId, + }, + 'post' + ) + .then(response => { + if (!response.auth_token) { + dispatch({ + type: ACTIONS.GENERATE_AUTH_TOKEN_FAILURE, + }); + } else { + dispatch({ + type: ACTIONS.GENERATE_AUTH_TOKEN_SUCCESS, + data: { authToken: response.auth_token }, + }); + } + }) + .catch(() => { + dispatch({ + type: ACTIONS.GENERATE_AUTH_TOKEN_FAILURE, + }); + }); + }; +} diff --git a/extras/lbryinc/redux/actions/blacklist.js b/extras/lbryinc/redux/actions/blacklist.js new file mode 100644 index 000000000..4e0008691 --- /dev/null +++ b/extras/lbryinc/redux/actions/blacklist.js @@ -0,0 +1,52 @@ +import { Lbryio } from 'lbryinc'; +import * as ACTIONS from 'constants/action_types'; + +const CHECK_BLACK_LISTED_CONTENT_INTERVAL = 60 * 60 * 1000; + +export function doFetchBlackListedOutpoints() { + return dispatch => { + dispatch({ + type: ACTIONS.FETCH_BLACK_LISTED_CONTENT_STARTED, + }); + + const success = ({ outpoints }) => { + const splitOutpoints = []; + if (outpoints) { + outpoints.forEach((outpoint, index) => { + const [txid, nout] = outpoint.split(':'); + + splitOutpoints[index] = { txid, nout: Number.parseInt(nout, 10) }; + }); + } + + dispatch({ + type: ACTIONS.FETCH_BLACK_LISTED_CONTENT_COMPLETED, + data: { + outpoints: splitOutpoints, + success: true, + }, + }); + }; + + const failure = ({ message: error }) => { + dispatch({ + type: ACTIONS.FETCH_BLACK_LISTED_CONTENT_FAILED, + data: { + error, + success: false, + }, + }); + }; + + Lbryio.call('file', 'list_blocked', { + auth_token: '', + }).then(success, failure); + }; +} + +export function doBlackListedOutpointsSubscribe() { + return dispatch => { + dispatch(doFetchBlackListedOutpoints()); + setInterval(() => dispatch(doFetchBlackListedOutpoints()), CHECK_BLACK_LISTED_CONTENT_INTERVAL); + }; +} diff --git a/extras/lbryinc/redux/actions/cost_info.js b/extras/lbryinc/redux/actions/cost_info.js new file mode 100644 index 000000000..ca92a364b --- /dev/null +++ b/extras/lbryinc/redux/actions/cost_info.js @@ -0,0 +1,35 @@ +import * as ACTIONS from 'constants/action_types'; +import { Lbryio } from 'lbryinc'; +import { selectClaimsByUri } from 'redux/selectors/claims'; + +// eslint-disable-next-line import/prefer-default-export +export function doFetchCostInfoForUri(uri) { + return (dispatch, getState) => { + const state = getState(); + const claim = selectClaimsByUri(state)[uri]; + + if (!claim) return; + + function resolve(costInfo) { + dispatch({ + type: ACTIONS.FETCH_COST_INFO_COMPLETED, + data: { + uri, + costInfo, + }, + }); + } + + const fee = claim.value ? claim.value.fee : undefined; + + if (fee === undefined) { + resolve({ cost: 0, includesData: true }); + } else if (fee.currency === 'LBC') { + resolve({ cost: fee.amount, includesData: true }); + } else { + Lbryio.getExchangeRates().then(({ LBC_USD }) => { + resolve({ cost: fee.amount / LBC_USD, includesData: true }); + }); + } + }; +} diff --git a/extras/lbryinc/redux/actions/filtered.js b/extras/lbryinc/redux/actions/filtered.js new file mode 100644 index 000000000..4777c591d --- /dev/null +++ b/extras/lbryinc/redux/actions/filtered.js @@ -0,0 +1,47 @@ +import { Lbryio } from 'lbryinc'; +import * as ACTIONS from 'constants/action_types'; + +const CHECK_FILTERED_CONTENT_INTERVAL = 60 * 60 * 1000; + +export function doFetchFilteredOutpoints() { + return dispatch => { + dispatch({ + type: ACTIONS.FETCH_FILTERED_CONTENT_STARTED, + }); + + const success = ({ outpoints }) => { + let formattedOutpoints = []; + if (outpoints) { + formattedOutpoints = outpoints.map(outpoint => { + const [txid, nout] = outpoint.split(':'); + return { txid, nout: Number.parseInt(nout, 10) }; + }); + } + + dispatch({ + type: ACTIONS.FETCH_FILTERED_CONTENT_COMPLETED, + data: { + outpoints: formattedOutpoints, + }, + }); + }; + + const failure = ({ error }) => { + dispatch({ + type: ACTIONS.FETCH_FILTERED_CONTENT_FAILED, + data: { + error, + }, + }); + }; + + Lbryio.call('file', 'list_filtered', { auth_token: '' }).then(success, failure); + }; +} + +export function doFilteredOutpointsSubscribe() { + return dispatch => { + dispatch(doFetchFilteredOutpoints()); + setInterval(() => dispatch(doFetchFilteredOutpoints()), CHECK_FILTERED_CONTENT_INTERVAL); + }; +} diff --git a/extras/lbryinc/redux/actions/homepage.js b/extras/lbryinc/redux/actions/homepage.js new file mode 100644 index 000000000..41de4a2bd --- /dev/null +++ b/extras/lbryinc/redux/actions/homepage.js @@ -0,0 +1,79 @@ +import { Lbryio } from 'lbryinc'; +import { batchActions } from 'util/batch-actions'; +import { doResolveUris } from 'util/lbryURI'; +import * as ACTIONS from 'constants/action_types'; + +export function doFetchFeaturedUris(offloadResolve = false) { + return dispatch => { + dispatch({ + type: ACTIONS.FETCH_FEATURED_CONTENT_STARTED, + }); + + const success = ({ Uris }) => { + let urisToResolve = []; + Object.keys(Uris).forEach(category => { + urisToResolve = [...urisToResolve, ...Uris[category]]; + }); + + const actions = [ + { + type: ACTIONS.FETCH_FEATURED_CONTENT_COMPLETED, + data: { + uris: Uris, + success: true, + }, + }, + ]; + if (urisToResolve.length && !offloadResolve) { + actions.push(doResolveUris(urisToResolve)); + } + + dispatch(batchActions(...actions)); + }; + + const failure = () => { + dispatch({ + type: ACTIONS.FETCH_FEATURED_CONTENT_COMPLETED, + data: { + uris: {}, + }, + }); + }; + + Lbryio.call('file', 'list_homepage').then(success, failure); + }; +} + +export function doFetchTrendingUris() { + return dispatch => { + dispatch({ + type: ACTIONS.FETCH_TRENDING_CONTENT_STARTED, + }); + + const success = data => { + const urisToResolve = data.map(uri => uri.url); + const actions = [ + doResolveUris(urisToResolve), + { + type: ACTIONS.FETCH_TRENDING_CONTENT_COMPLETED, + data: { + uris: data, + success: true, + }, + }, + ]; + dispatch(batchActions(...actions)); + }; + + const failure = () => { + dispatch({ + type: ACTIONS.FETCH_TRENDING_CONTENT_COMPLETED, + data: { + uris: [], + }, + }); + }; + + Lbryio.call('file', 'list_trending').then(success, failure); + }; +} diff --git a/extras/lbryinc/redux/actions/stats.js b/extras/lbryinc/redux/actions/stats.js new file mode 100644 index 000000000..7e152b55c --- /dev/null +++ b/extras/lbryinc/redux/actions/stats.js @@ -0,0 +1,32 @@ +// @flow +import { Lbryio } from 'lbryinc'; +import * as ACTIONS from 'constants/action_types'; + +export const doFetchViewCount = (claimIdCsv: string) => dispatch => { + dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_STARTED }); + + return Lbryio.call('file', 'view_count', { claim_id: claimIdCsv }) + .then((result: Array) => { + const viewCounts = result; + dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_COMPLETED, data: { claimIdCsv, viewCounts } }); + }) + .catch(error => { + dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_FAILED, data: error }); + }); +}; + +export const doFetchSubCount = (claimId: string) => dispatch => { + dispatch({ type: ACTIONS.FETCH_SUB_COUNT_STARTED }); + + return Lbryio.call('subscription', 'sub_count', { claim_id: claimId }) + .then((result: Array) => { + const subCount = result[0]; + dispatch({ + type: ACTIONS.FETCH_SUB_COUNT_COMPLETED, + data: { claimId, subCount }, + }); + }) + .catch(error => { + dispatch({ type: ACTIONS.FETCH_SUB_COUNT_FAILED, data: error }); + }); +}; diff --git a/extras/lbryinc/redux/actions/sync.js b/extras/lbryinc/redux/actions/sync.js new file mode 100644 index 000000000..9550f3e85 --- /dev/null +++ b/extras/lbryinc/redux/actions/sync.js @@ -0,0 +1,289 @@ +import * as ACTIONS from 'constants/action_types'; +import { Lbryio } from 'lbryinc'; +import Lbry from 'lbry'; +import { doWalletEncrypt, doWalletDecrypt } from 'redux/actions/wallet'; + +const NO_WALLET_ERROR = 'no wallet found for this user'; + +export function doSetDefaultAccount(success, failure) { + return dispatch => { + dispatch({ + type: ACTIONS.SET_DEFAULT_ACCOUNT, + }); + + Lbry.account_list() + .then(accountList => { + const { lbc_mainnet: accounts } = accountList; + let defaultId; + for (let i = 0; i < accounts.length; ++i) { + if (accounts[i].satoshis > 0) { + defaultId = accounts[i].id; + break; + } + } + + // In a case where there's no balance on either account + // assume the second (which is created after sync) as default + if (!defaultId && accounts.length > 1) { + defaultId = accounts[1].id; + } + + // Set the default account + if (defaultId) { + Lbry.account_set({ account_id: defaultId, default: true }) + .then(() => { + if (success) { + success(); + } + }) + .catch(err => { + if (failure) { + failure(err); + } + }); + } else if (failure) { + // no default account to set + failure('Could not set a default account'); // fail + } + }) + .catch(err => { + if (failure) { + failure(err); + } + }); + }; +} + +export function doSetSync(oldHash, newHash, data) { + return dispatch => { + dispatch({ + type: ACTIONS.SET_SYNC_STARTED, + }); + + return Lbryio.call('sync', 'set', { old_hash: oldHash, new_hash: newHash, data }, 'post') + .then(response => { + if (!response.hash) { + throw Error('No hash returned for sync/set.'); + } + + return dispatch({ + type: ACTIONS.SET_SYNC_COMPLETED, + data: { syncHash: response.hash }, + }); + }) + .catch(error => { + dispatch({ + type: ACTIONS.SET_SYNC_FAILED, + data: { error }, + }); + }); + }; +} + +export function doGetSync(passedPassword, callback) { + const password = passedPassword === null || passedPassword === undefined ? '' : passedPassword; + + function handleCallback(error, hasNewData) { + if (callback) { + if (typeof callback !== 'function') { + throw new Error('Second argument passed to "doGetSync" must be a function'); + } + + callback(error, hasNewData); + } + } + + return dispatch => { + dispatch({ + type: ACTIONS.GET_SYNC_STARTED, + }); + + const data = {}; + + Lbry.wallet_status() + .then(status => { + if (status.is_locked) { + return Lbry.wallet_unlock({ password }); + } + + // Wallet is already unlocked + return true; + }) + .then(isUnlocked => { + if (isUnlocked) { + return Lbry.sync_hash(); + } + data.unlockFailed = true; + throw new Error(); + }) + .then(hash => Lbryio.call('sync', 'get', { hash }, 'post')) + .then(response => { + const syncHash = response.hash; + data.syncHash = syncHash; + data.syncData = response.data; + data.changed = response.changed; + data.hasSyncedWallet = true; + + if (response.changed) { + return Lbry.sync_apply({ password, data: response.data, blocking: true }); + } + }) + .then(response => { + if (!response) { + dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data }); + handleCallback(null, data.changed); + return; + } + + const { hash: walletHash, data: walletData } = response; + + if (walletHash !== data.syncHash) { + // different local hash, need to synchronise + dispatch(doSetSync(data.syncHash, walletHash, walletData)); + } + + dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data }); + handleCallback(null, data.changed); + }) + .catch(syncAttemptError => { + if (data.unlockFailed) { + dispatch({ type: ACTIONS.GET_SYNC_FAILED, data: { error: syncAttemptError } }); + + if (password !== '') { + dispatch({ type: ACTIONS.SYNC_APPLY_BAD_PASSWORD }); + } + + handleCallback(syncAttemptError); + } else if (data.hasSyncedWallet) { + const error = + (syncAttemptError && syncAttemptError.message) || 'Error getting synced wallet'; + dispatch({ + type: ACTIONS.GET_SYNC_FAILED, + data: { + error, + }, + }); + + // Temp solution until we have a bad password error code + // Don't fail on blank passwords so we don't show a "password error" message + // before users have ever entered a password + if (password !== '') { + dispatch({ type: ACTIONS.SYNC_APPLY_BAD_PASSWORD }); + } + + handleCallback(error); + } else { + // user doesn't have a synced wallet + dispatch({ + type: ACTIONS.GET_SYNC_COMPLETED, + data: { hasSyncedWallet: false, syncHash: null }, + }); + + // call sync_apply to get data to sync + // first time sync. use any string for old hash + if (syncAttemptError.message === NO_WALLET_ERROR) { + Lbry.sync_apply({ password }) + .then(({ hash: walletHash, data: syncApplyData }) => { + dispatch(doSetSync('', walletHash, syncApplyData, password)); + handleCallback(); + }) + .catch(syncApplyError => { + handleCallback(syncApplyError); + }); + } + } + }); + }; +} + +export function doSyncApply(syncHash, syncData, password) { + return dispatch => { + dispatch({ + type: ACTIONS.SYNC_APPLY_STARTED, + }); + + Lbry.sync_apply({ password, data: syncData }) + .then(({ hash: walletHash, data: walletData }) => { + dispatch({ + type: ACTIONS.SYNC_APPLY_COMPLETED, + }); + + if (walletHash !== syncHash) { + // different local hash, need to synchronise + dispatch(doSetSync(syncHash, walletHash, walletData)); + } + }) + .catch(() => { + dispatch({ + type: ACTIONS.SYNC_APPLY_FAILED, + data: { + error: + 'Invalid password specified. Please enter the password for your previously synchronised wallet.', + }, + }); + }); + }; +} + +export function doCheckSync() { + return dispatch => { + dispatch({ + type: ACTIONS.GET_SYNC_STARTED, + }); + + Lbry.sync_hash().then(hash => { + Lbryio.call('sync', 'get', { hash }, 'post') + .then(response => { + const data = { + hasSyncedWallet: true, + syncHash: response.hash, + syncData: response.data, + hashChanged: response.changed, + }; + dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data }); + }) + .catch(() => { + // user doesn't have a synced wallet + dispatch({ + type: ACTIONS.GET_SYNC_COMPLETED, + data: { hasSyncedWallet: false, syncHash: null }, + }); + }); + }); + }; +} + +export function doResetSync() { + return dispatch => + new Promise(resolve => { + dispatch({ type: ACTIONS.SYNC_RESET }); + resolve(); + }); +} + +export function doSyncEncryptAndDecrypt(oldPassword, newPassword, encrypt) { + return dispatch => { + const data = {}; + return Lbry.sync_hash() + .then(hash => Lbryio.call('sync', 'get', { hash }, 'post')) + .then(syncGetResponse => { + data.oldHash = syncGetResponse.hash; + + return Lbry.sync_apply({ password: oldPassword, data: syncGetResponse.data }); + }) + .then(() => { + if (encrypt) { + dispatch(doWalletEncrypt(newPassword)); + } else { + dispatch(doWalletDecrypt()); + } + }) + .then(() => Lbry.sync_apply({ password: newPassword })) + .then(syncApplyResponse => { + if (syncApplyResponse.hash !== data.oldHash) { + return dispatch(doSetSync(data.oldHash, syncApplyResponse.hash, syncApplyResponse.data)); + } + }) + .catch(console.error); + }; +} diff --git a/extras/lbryinc/redux/actions/web.js b/extras/lbryinc/redux/actions/web.js new file mode 100644 index 000000000..1bcce69a5 --- /dev/null +++ b/extras/lbryinc/redux/actions/web.js @@ -0,0 +1,12 @@ +// @flow +import * as ACTIONS from 'constants/action_types'; + +export const doUpdateUploadProgress = ( + progress: string, + params: { [key: string]: any }, + xhr: any +) => (dispatch: Dispatch) => + dispatch({ + type: ACTIONS.UPDATE_UPLOAD_PROGRESS, + data: { progress, params, xhr }, + }); diff --git a/extras/lbryinc/redux/reducers/auth.js b/extras/lbryinc/redux/reducers/auth.js new file mode 100644 index 000000000..6de9a2c9d --- /dev/null +++ b/extras/lbryinc/redux/reducers/auth.js @@ -0,0 +1,29 @@ +import * as ACTIONS from 'constants/action_types'; + +const reducers = {}; +const defaultState = { + authenticating: false, +}; + +reducers[ACTIONS.GENERATE_AUTH_TOKEN_FAILURE] = state => + Object.assign({}, state, { + authToken: null, + authenticating: false, + }); + +reducers[ACTIONS.GENERATE_AUTH_TOKEN_STARTED] = state => + Object.assign({}, state, { + authenticating: true, + }); + +reducers[ACTIONS.GENERATE_AUTH_TOKEN_SUCCESS] = (state, action) => + Object.assign({}, state, { + authToken: action.data.authToken, + authenticating: false, + }); + +export function authReducer(state = defaultState, action) { + const handler = reducers[action.type]; + if (handler) return handler(state, action); + return state; +} diff --git a/extras/lbryinc/redux/reducers/blacklist.js b/extras/lbryinc/redux/reducers/blacklist.js new file mode 100644 index 000000000..48d74b1ff --- /dev/null +++ b/extras/lbryinc/redux/reducers/blacklist.js @@ -0,0 +1,37 @@ +import * as ACTIONS from 'constants/action_types'; +import { handleActions } from 'util/redux-utils'; + +const defaultState = { + fetchingBlackListedOutpoints: false, + fetchingBlackListedOutpointsSucceed: undefined, + blackListedOutpoints: undefined, +}; + +export const blacklistReducer = handleActions( + { + [ACTIONS.FETCH_BLACK_LISTED_CONTENT_STARTED]: state => ({ + ...state, + fetchingBlackListedOutpoints: true, + }), + [ACTIONS.FETCH_BLACK_LISTED_CONTENT_COMPLETED]: (state, action) => { + const { outpoints, success } = action.data; + return { + ...state, + fetchingBlackListedOutpoints: false, + fetchingBlackListedOutpointsSucceed: success, + blackListedOutpoints: outpoints, + }; + }, + [ACTIONS.FETCH_BLACK_LISTED_CONTENT_FAILED]: (state, action) => { + const { error, success } = action.data; + + return { + ...state, + fetchingBlackListedOutpoints: false, + fetchingBlackListedOutpointsSucceed: success, + fetchingBlackListedOutpointsError: error, + }; + }, + }, + defaultState +); diff --git a/extras/lbryinc/redux/reducers/cost_info.js b/extras/lbryinc/redux/reducers/cost_info.js new file mode 100644 index 000000000..c6dd6b1b2 --- /dev/null +++ b/extras/lbryinc/redux/reducers/cost_info.js @@ -0,0 +1,38 @@ +import { handleActions } from 'util/redux-utils'; +import * as ACTIONS from 'constants/action_types'; + +const defaultState = { + fetching: {}, + byUri: {}, +}; + +export const costInfoReducer = handleActions( + { + [ACTIONS.FETCH_COST_INFO_STARTED]: (state, action) => { + const { uri } = action.data; + const newFetching = Object.assign({}, state.fetching); + newFetching[uri] = true; + + return { + ...state, + fetching: newFetching, + }; + }, + + [ACTIONS.FETCH_COST_INFO_COMPLETED]: (state, action) => { + const { uri, costInfo } = action.data; + const newByUri = Object.assign({}, state.byUri); + const newFetching = Object.assign({}, state.fetching); + + newByUri[uri] = costInfo; + delete newFetching[uri]; + + return { + ...state, + byUri: newByUri, + fetching: newFetching, + }; + }, + }, + defaultState +); diff --git a/extras/lbryinc/redux/reducers/filtered.js b/extras/lbryinc/redux/reducers/filtered.js new file mode 100644 index 000000000..02147156d --- /dev/null +++ b/extras/lbryinc/redux/reducers/filtered.js @@ -0,0 +1,34 @@ +import * as ACTIONS from 'constants/action_types'; +import { handleActions } from 'util/redux-utils'; + +const defaultState = { + loading: false, + filteredOutpoints: undefined, +}; + +export const filteredReducer = handleActions( + { + [ACTIONS.FETCH_FILTERED_CONTENT_STARTED]: state => ({ + ...state, + loading: true, + }), + [ACTIONS.FETCH_FILTERED_CONTENT_COMPLETED]: (state, action) => { + const { outpoints } = action.data; + return { + ...state, + loading: false, + filteredOutpoints: outpoints, + }; + }, + [ACTIONS.FETCH_FILTERED_CONTENT_FAILED]: (state, action) => { + const { error } = action.data; + + return { + ...state, + loading: false, + fetchingFilteredOutpointsError: error, + }; + }, + }, + defaultState +); diff --git a/extras/lbryinc/redux/reducers/homepage.js b/extras/lbryinc/redux/reducers/homepage.js new file mode 100644 index 000000000..912bf4f32 --- /dev/null +++ b/extras/lbryinc/redux/reducers/homepage.js @@ -0,0 +1,48 @@ +import { handleActions } from 'util/redux-utils'; +import * as ACTIONS from 'constants/action_types'; + +const defaultState = { + fetchingFeaturedContent: false, + fetchingFeaturedContentFailed: false, + featuredUris: undefined, + fetchingTrendingContent: false, + fetchingTrendingContentFailed: false, + trendingUris: undefined, +}; + +export const homepageReducer = handleActions( + { + [ACTIONS.FETCH_FEATURED_CONTENT_STARTED]: state => ({ + ...state, + fetchingFeaturedContent: true, + }), + + [ACTIONS.FETCH_FEATURED_CONTENT_COMPLETED]: (state, action) => { + const { uris, success } = action.data; + + return { + ...state, + fetchingFeaturedContent: false, + fetchingFeaturedContentFailed: !success, + featuredUris: uris, + }; + }, + + [ACTIONS.FETCH_TRENDING_CONTENT_STARTED]: state => ({ + ...state, + fetchingTrendingContent: true, + }), + + [ACTIONS.FETCH_TRENDING_CONTENT_COMPLETED]: (state, action) => { + const { uris, success } = action.data; + + return { + ...state, + fetchingTrendingContent: false, + fetchingTrendingContentFailed: !success, + trendingUris: uris, + }; + }, + }, + defaultState +); diff --git a/extras/lbryinc/redux/reducers/stats.js b/extras/lbryinc/redux/reducers/stats.js new file mode 100644 index 000000000..22fc9ce16 --- /dev/null +++ b/extras/lbryinc/redux/reducers/stats.js @@ -0,0 +1,55 @@ +import { handleActions } from 'util/redux-utils'; +import * as ACTIONS from 'constants/action_types'; + +const defaultState = { + fetchingViewCount: false, + viewCountError: undefined, + viewCountById: {}, + fetchingSubCount: false, + subCountError: undefined, + subCountById: {}, +}; + +export const statsReducer = handleActions( + { + [ACTIONS.FETCH_VIEW_COUNT_STARTED]: state => ({ ...state, fetchingViewCount: true }), + [ACTIONS.FETCH_VIEW_COUNT_FAILED]: (state, action) => ({ + ...state, + viewCountError: action.data, + }), + [ACTIONS.FETCH_VIEW_COUNT_COMPLETED]: (state, action) => { + const { claimIdCsv, viewCounts } = action.data; + + const viewCountById = Object.assign({}, state.viewCountById); + const claimIds = claimIdCsv.split(','); + + if (claimIds.length === viewCounts.length) { + claimIds.forEach((claimId, index) => { + viewCountById[claimId] = viewCounts[index]; + }); + } + + return { + ...state, + fetchingViewCount: false, + viewCountById, + }; + }, + [ACTIONS.FETCH_SUB_COUNT_STARTED]: state => ({ ...state, fetchingSubCount: true }), + [ACTIONS.FETCH_SUB_COUNT_FAILED]: (state, action) => ({ + ...state, + subCountError: action.data, + }), + [ACTIONS.FETCH_SUB_COUNT_COMPLETED]: (state, action) => { + const { claimId, subCount } = action.data; + + const subCountById = { ...state.subCountById, [claimId]: subCount }; + return { + ...state, + fetchingSubCount: false, + subCountById, + }; + }, + }, + defaultState +); diff --git a/extras/lbryinc/redux/reducers/sync.js b/extras/lbryinc/redux/reducers/sync.js new file mode 100644 index 000000000..497e58107 --- /dev/null +++ b/extras/lbryinc/redux/reducers/sync.js @@ -0,0 +1,89 @@ +import * as ACTIONS from 'constants/action_types'; + +const reducers = {}; +const defaultState = { + hasSyncedWallet: false, + syncHash: null, + syncData: null, + setSyncErrorMessage: null, + getSyncErrorMessage: null, + syncApplyErrorMessage: '', + syncApplyIsPending: false, + syncApplyPasswordError: false, + getSyncIsPending: false, + setSyncIsPending: false, + hashChanged: false, +}; + +reducers[ACTIONS.GET_SYNC_STARTED] = state => + Object.assign({}, state, { + getSyncIsPending: true, + getSyncErrorMessage: null, + }); + +reducers[ACTIONS.GET_SYNC_COMPLETED] = (state, action) => + Object.assign({}, state, { + syncHash: action.data.syncHash, + syncData: action.data.syncData, + hasSyncedWallet: action.data.hasSyncedWallet, + getSyncIsPending: false, + hashChanged: action.data.hashChanged, + }); + +reducers[ACTIONS.GET_SYNC_FAILED] = (state, action) => + Object.assign({}, state, { + getSyncIsPending: false, + getSyncErrorMessage: action.data.error, + }); + +reducers[ACTIONS.SET_SYNC_STARTED] = state => + Object.assign({}, state, { + setSyncIsPending: true, + setSyncErrorMessage: null, + }); + +reducers[ACTIONS.SET_SYNC_FAILED] = (state, action) => + Object.assign({}, state, { + setSyncIsPending: false, + setSyncErrorMessage: action.data.error, + }); + +reducers[ACTIONS.SET_SYNC_COMPLETED] = (state, action) => + Object.assign({}, state, { + setSyncIsPending: false, + setSyncErrorMessage: null, + hasSyncedWallet: true, // sync was successful, so the user has a synced wallet at this point + syncHash: action.data.syncHash, + }); + +reducers[ACTIONS.SYNC_APPLY_STARTED] = state => + Object.assign({}, state, { + syncApplyPasswordError: false, + syncApplyIsPending: true, + syncApplyErrorMessage: '', + }); + +reducers[ACTIONS.SYNC_APPLY_COMPLETED] = state => + Object.assign({}, state, { + syncApplyIsPending: false, + syncApplyErrorMessage: '', + }); + +reducers[ACTIONS.SYNC_APPLY_FAILED] = (state, action) => + Object.assign({}, state, { + syncApplyIsPending: false, + syncApplyErrorMessage: action.data.error, + }); + +reducers[ACTIONS.SYNC_APPLY_BAD_PASSWORD] = state => + Object.assign({}, state, { + syncApplyPasswordError: true, + }); + +reducers[ACTIONS.SYNC_RESET] = () => defaultState; + +export function syncReducer(state = defaultState, action) { + const handler = reducers[action.type]; + if (handler) return handler(state, action); + return state; +} diff --git a/extras/lbryinc/redux/reducers/web.js b/extras/lbryinc/redux/reducers/web.js new file mode 100644 index 000000000..70e90296e --- /dev/null +++ b/extras/lbryinc/redux/reducers/web.js @@ -0,0 +1,62 @@ +// @flow +import * as ACTIONS from 'constants/action_types'; + +/* +test mock: + currentUploads: { + 'test#upload': { + progress: 50, + params: { + name: 'steve', + thumbnail_url: 'https://dev2.spee.ch/4/KMNtoSZ009fawGz59VG8PrID.jpeg', + }, + }, + }, + */ + +export type Params = { + channel?: string, + name: string, + thumbnail_url: ?string, + title: ?string, +}; + +export type UploadItem = { + progess: string, + params: Params, + xhr?: any, +}; + +export type TvState = { + currentUploads: { [key: string]: UploadItem }, +}; + +const reducers = {}; + +const defaultState: TvState = { + currentUploads: {}, +}; + +reducers[ACTIONS.UPDATE_UPLOAD_PROGRESS] = (state: TvState, action) => { + const { progress, params, xhr } = action.data; + const key = params.channel ? `${params.name}#${params.channel}` : `${params.name}#anonymous`; + let currentUploads; + if (!progress) { + currentUploads = Object.assign({}, state.currentUploads); + Object.keys(currentUploads).forEach(k => { + if (k === key) { + delete currentUploads[key]; + } + }); + } else { + currentUploads = Object.assign({}, state.currentUploads); + currentUploads[key] = { progress, params, xhr }; + } + return { ...state, currentUploads }; +}; + +export function webReducer(state = defaultState, action) { + const handler = reducers[action.type]; + if (handler) return handler(state, action); + return state; +} diff --git a/extras/lbryinc/redux/selectors/auth.js b/extras/lbryinc/redux/selectors/auth.js new file mode 100644 index 000000000..819ee5286 --- /dev/null +++ b/extras/lbryinc/redux/selectors/auth.js @@ -0,0 +1,7 @@ +import { createSelector } from 'reselect'; + +const selectState = state => state.auth || {}; + +export const selectAuthToken = createSelector(selectState, state => state.authToken); + +export const selectIsAuthenticating = createSelector(selectState, state => state.authenticating); diff --git a/extras/lbryinc/redux/selectors/blacklist.js b/extras/lbryinc/redux/selectors/blacklist.js new file mode 100644 index 000000000..ed0d8ccb1 --- /dev/null +++ b/extras/lbryinc/redux/selectors/blacklist.js @@ -0,0 +1,20 @@ +import { createSelector } from 'reselect'; + +export const selectState = state => state.blacklist || {}; + +export const selectBlackListedOutpoints = createSelector( + selectState, + state => state.blackListedOutpoints +); + +export const selectBlacklistedOutpointMap = createSelector( + selectBlackListedOutpoints, + outpoints => + outpoints + ? outpoints.reduce((acc, val) => { + const outpoint = `${val.txid}:${val.nout}`; + acc[outpoint] = 1; + return acc; + }, {}) + : {} +); diff --git a/extras/lbryinc/redux/selectors/cost_info.js b/extras/lbryinc/redux/selectors/cost_info.js new file mode 100644 index 000000000..b510c7c84 --- /dev/null +++ b/extras/lbryinc/redux/selectors/cost_info.js @@ -0,0 +1,13 @@ +import { createSelector } from 'reselect'; + +export const selectState = state => state.costInfo || {}; + +export const selectAllCostInfoByUri = createSelector(selectState, state => state.byUri || {}); + +export const makeSelectCostInfoForUri = uri => + createSelector(selectAllCostInfoByUri, costInfos => costInfos && costInfos[uri]); + +export const selectFetchingCostInfo = createSelector(selectState, state => state.fetching || {}); + +export const makeSelectFetchingCostInfoForUri = uri => + createSelector(selectFetchingCostInfo, fetchingByUri => fetchingByUri && fetchingByUri[uri]); diff --git a/extras/lbryinc/redux/selectors/filtered.js b/extras/lbryinc/redux/selectors/filtered.js new file mode 100644 index 000000000..46ea2b1fc --- /dev/null +++ b/extras/lbryinc/redux/selectors/filtered.js @@ -0,0 +1,20 @@ +import { createSelector } from 'reselect'; + +export const selectState = state => state.filtered || {}; + +export const selectFilteredOutpoints = createSelector( + selectState, + state => state.filteredOutpoints +); + +export const selectFilteredOutpointMap = createSelector( + selectFilteredOutpoints, + outpoints => + outpoints + ? outpoints.reduce((acc, val) => { + const outpoint = `${val.txid}:${val.nout}`; + acc[outpoint] = 1; + return acc; + }, {}) + : {} +); diff --git a/extras/lbryinc/redux/selectors/homepage.js b/extras/lbryinc/redux/selectors/homepage.js new file mode 100644 index 000000000..710058cbc --- /dev/null +++ b/extras/lbryinc/redux/selectors/homepage.js @@ -0,0 +1,17 @@ +import { createSelector } from 'reselect'; + +const selectState = state => state.homepage || {}; + +export const selectFeaturedUris = createSelector(selectState, state => state.featuredUris); + +export const selectFetchingFeaturedUris = createSelector( + selectState, + state => state.fetchingFeaturedContent +); + +export const selectTrendingUris = createSelector(selectState, state => state.trendingUris); + +export const selectFetchingTrendingUris = createSelector( + selectState, + state => state.fetchingTrendingContent +); diff --git a/extras/lbryinc/redux/selectors/stats.js b/extras/lbryinc/redux/selectors/stats.js new file mode 100644 index 000000000..2b2065285 --- /dev/null +++ b/extras/lbryinc/redux/selectors/stats.js @@ -0,0 +1,20 @@ +import { createSelector } from 'reselect'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; + +const selectState = state => state.stats || {}; +export const selectViewCount = createSelector(selectState, state => state.viewCountById); +export const selectSubCount = createSelector(selectState, state => state.subCountById); + +export const makeSelectViewCountForUri = uri => + createSelector( + makeSelectClaimForUri(uri), + selectViewCount, + (claim, viewCountById) => (claim ? viewCountById[claim.claim_id] || 0 : 0) + ); + +export const makeSelectSubCountForUri = uri => + createSelector( + makeSelectClaimForUri(uri), + selectSubCount, + (claim, subCountById) => (claim ? subCountById[claim.claim_id] || 0 : 0) + ); diff --git a/extras/lbryinc/redux/selectors/sync.js b/extras/lbryinc/redux/selectors/sync.js new file mode 100644 index 000000000..4448c6c49 --- /dev/null +++ b/extras/lbryinc/redux/selectors/sync.js @@ -0,0 +1,40 @@ +import { createSelector } from 'reselect'; + +const selectState = state => state.sync || {}; + +export const selectHasSyncedWallet = createSelector(selectState, state => state.hasSyncedWallet); + +export const selectSyncHash = createSelector(selectState, state => state.syncHash); + +export const selectSyncData = createSelector(selectState, state => state.syncData); + +export const selectSetSyncErrorMessage = createSelector( + selectState, + state => state.setSyncErrorMessage +); + +export const selectGetSyncErrorMessage = createSelector( + selectState, + state => state.getSyncErrorMessage +); + +export const selectGetSyncIsPending = createSelector(selectState, state => state.getSyncIsPending); + +export const selectSetSyncIsPending = createSelector(selectState, state => state.setSyncIsPending); + +export const selectHashChanged = createSelector(selectState, state => state.hashChanged); + +export const selectSyncApplyIsPending = createSelector( + selectState, + state => state.syncApplyIsPending +); + +export const selectSyncApplyErrorMessage = createSelector( + selectState, + state => state.syncApplyErrorMessage +); + +export const selectSyncApplyPasswordError = createSelector( + selectState, + state => state.syncApplyPasswordError +); diff --git a/extras/lbryinc/redux/selectors/web.js b/extras/lbryinc/redux/selectors/web.js new file mode 100644 index 000000000..54ac3ec4e --- /dev/null +++ b/extras/lbryinc/redux/selectors/web.js @@ -0,0 +1,10 @@ +import { createSelector } from 'reselect'; + +const selectState = state => state.web || {}; + +export const selectCurrentUploads = createSelector(selectState, state => state.currentUploads); + +export const selectUploadCount = createSelector( + selectCurrentUploads, + currentUploads => currentUploads && Object.keys(currentUploads).length +); diff --git a/extras/lbryinc/util/redux-utils-delete-me.js b/extras/lbryinc/util/redux-utils-delete-me.js new file mode 100644 index 000000000..bf9c17158 --- /dev/null +++ b/extras/lbryinc/util/redux-utils-delete-me.js @@ -0,0 +1,17 @@ +// util for creating reducers +// based off of redux-actions +// https://redux-actions.js.org/docs/api/handleAction.html#handleactions + +// eslint-disable-next-line import/prefer-default-export +export const handleActions = (actionMap, defaultState) => (state = defaultState, action) => { + const handler = actionMap[action.type]; + + if (handler) { + const newState = handler(state, action); + return Object.assign({}, state, newState); + } + + // just return the original state if no handler + // returning a copy here breaks redux-persist + return state; +}; diff --git a/extras/lbryinc/util/swap-json.js b/extras/lbryinc/util/swap-json.js new file mode 100644 index 000000000..6b52a9a64 --- /dev/null +++ b/extras/lbryinc/util/swap-json.js @@ -0,0 +1,10 @@ +export function swapKeyAndValue(dict) { + const ret = {}; + // eslint-disable-next-line no-restricted-syntax + for (const key in dict) { + if (dict.hasOwnProperty(key)) { + ret[dict[key]] = key; + } + } + return ret; +} diff --git a/extras/lbryinc/util/transifex-upload.js b/extras/lbryinc/util/transifex-upload.js new file mode 100644 index 000000000..94a83a866 --- /dev/null +++ b/extras/lbryinc/util/transifex-upload.js @@ -0,0 +1,78 @@ +const apiBaseUrl = 'https://www.transifex.com/api/2/project'; +const resource = 'app-strings'; + +export function doTransifexUpload(contents, project, token, success, fail) { + const url = `${apiBaseUrl}/${project}/resources/`; + const updateUrl = `${apiBaseUrl}/${project}/resource/${resource}/content/`; + const headers = { + Authorization: `Basic ${Buffer.from(`api:${token}`).toString('base64')}`, + 'Content-Type': 'application/json', + }; + + const req = { + accept_translations: true, + i18n_type: 'KEYVALUEJSON', + name: resource, + slug: resource, + content: contents, + }; + + function handleResponse(text) { + let json; + try { + // transifex api returns Python dicts for some reason. + // Any way to get the api to return valid JSON? + json = JSON.parse(text); + } catch (e) { + // ignore + } + + if (success) { + success(json || text); + } + } + + function handleError(err) { + if (fail) { + fail(err.message ? err.message : 'Could not upload strings resource to Transifex'); + } + } + + // check if the resource exists + fetch(updateUrl, { headers }) + .then(response => response.json()) + .then(() => { + // perform an update + fetch(updateUrl, { + method: 'PUT', + headers, + body: JSON.stringify({ content: contents }), + }) + .then(response => { + if (response.status !== 200 && response.status !== 201) { + throw new Error('failed to update transifex'); + } + + return response.text(); + }) + .then(handleResponse) + .catch(handleError); + }) + .catch(() => { + // resource doesn't exist, create a fresh resource + fetch(url, { + method: 'POST', + headers, + body: JSON.stringify(req), + }) + .then(response => { + if (response.status !== 200 && response.status !== 201) { + throw new Error('failed to upload to transifex'); + } + + return response.text(); + }) + .then(handleResponse) + .catch(handleError); + }); +} diff --git a/ui/recsys.js b/extras/recsys/recsys.js similarity index 97% rename from ui/recsys.js rename to extras/recsys/recsys.js index 6a92e2bd2..26ef26e85 100644 --- a/ui/recsys.js +++ b/extras/recsys/recsys.js @@ -1,10 +1,12 @@ import { selectUser } from 'redux/selectors/user'; import { makeSelectRecommendedRecsysIdForClaimId } from 'redux/selectors/search'; import { v4 as Uuidv4 } from 'uuid'; -import { parseURI, SETTINGS, makeSelectClaimForUri } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; +import * as SETTINGS from 'constants/settings'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; import { selectPlayingUri, selectPrimaryUri } from 'redux/selectors/content'; import { makeSelectClientSetting, selectDaemonSettings } from 'redux/selectors/settings'; -import { history } from './store'; +import { history } from 'ui/store'; const recsysEndpoint = 'https://clickstream.odysee.com/log/video/view'; const recsysId = 'lighthouse-v0'; diff --git a/flow-typed/Blocklist.js b/flow-typed/Blocklist.js new file mode 100644 index 000000000..454a7142f --- /dev/null +++ b/flow-typed/Blocklist.js @@ -0,0 +1,10 @@ +declare type BlocklistState = { + blockedChannels: Array +}; + +declare type BlocklistAction = { + type: string, + data: { + uri: string, + }, +}; diff --git a/flow-typed/Claim.js b/flow-typed/Claim.js new file mode 100644 index 000000000..e03eeae81 --- /dev/null +++ b/flow-typed/Claim.js @@ -0,0 +1,214 @@ +// @flow + +declare type Claim = StreamClaim | ChannelClaim | CollectionClaim; + +declare type ChannelClaim = GenericClaim & { + value: ChannelMetadata, +}; + +declare type CollectionClaim = GenericClaim & { + value: CollectionMetadata, +}; + +declare type StreamClaim = GenericClaim & { + value: StreamMetadata, +}; + +declare type GenericClaim = { + address: string, // address associated with tx + amount: string, // bid amount at time of tx + canonical_url: string, // URL with short id, includes channel with short id + claim_id: string, // unique claim identifier + claim_sequence: number, // not being used currently + claim_op: 'create' | 'update', + confirmations: number, + decoded_claim: boolean, // Not available currently https://github.com/lbryio/lbry/issues/2044 + timestamp?: number, // date of last transaction + height: number, // block height the tx was confirmed + is_channel_signature_valid?: boolean, + is_my_output: boolean, + name: string, + normalized_name: string, // `name` normalized via unicode NFD spec, + nout: number, // index number for an output of a tx + permanent_url: string, // name + claim_id + short_url: string, // permanent_url with short id, no channel + txid: string, // unique tx id + type: 'claim' | 'update' | 'support', + value_type: 'stream' | 'channel' | 'collection', + signing_channel?: ChannelClaim, + reposted_claim?: GenericClaim, + repost_channel_url?: string, + repost_url?: string, + repost_bid_amount?: string, + purchase_receipt?: PurchaseReceipt, + meta: { + activation_height: number, + claims_in_channel?: number, + creation_height: number, + creation_timestamp: number, + effective_amount: string, + expiration_height: number, + is_controlling: boolean, + support_amount: string, + reposted: number, + trending_global: number, + trending_group: number, + trending_local: number, + trending_mixed: number, + }, +}; + +declare type GenericMetadata = { + title?: string, + description?: string, + thumbnail?: { + url?: string, + }, + languages?: Array, + tags?: Array, + locations?: Array, +}; + +declare type ChannelMetadata = GenericMetadata & { + public_key: string, + public_key_id: string, + cover_url?: string, + email?: string, + website_url?: string, + featured?: Array, +}; + +declare type CollectionMetadata = GenericMetadata & { + claims: Array, +} + +declare type StreamMetadata = GenericMetadata & { + license?: string, // License "title" ex: Creative Commons, Custom copyright + license_url?: string, // Link to full license + release_time?: number, // linux timestamp + author?: string, + + source: { + sd_hash: string, + media_type?: string, + hash?: string, + name?: string, // file name + size?: number, // size of file in bytes + }, + + // Only exists if a stream has a fee + fee?: Fee, + + stream_type: 'video' | 'audio' | 'image' | 'software', + // Below correspond to `stream_type` + video?: { + duration: number, + height: number, + width: number, + }, + audio?: { + duration: number, + }, + image?: { + height: number, + width: number, + }, + software?: { + os: string, + }, +}; + +declare type Location = { + latitude?: number, + longitude?: number, + country?: string, + state?: string, + city?: string, + code?: string, +}; + +declare type Fee = { + amount: string, + currency: string, + address: string, +}; + +declare type PurchaseReceipt = { + address: string, + amount: string, + claim_id: string, + confirmations: number, + height: number, + nout: number, + timestamp: number, + txid: string, + type: 'purchase', +}; + +declare type ClaimActionResolveInfo = { + [string]: { + stream: ?StreamClaim, + channel: ?ChannelClaim, + claimsInChannel: ?number, + collection: ?CollectionClaim, + }, +} + +declare type ChannelUpdateParams = { + claim_id: string, + bid?: string, + title?: string, + cover_url?: string, + thumbnail_url?: string, + description?: string, + website_url?: string, + email?: string, + tags?: Array, + replace?: boolean, + languages?: Array, + locations?: Array, + blocking?: boolean, +} + +declare type ChannelPublishParams = { + name: string, + bid: string, + blocking?: true, + title?: string, + cover_url?: string, + thumbnail_url?: string, + description?: string, + website_url?: string, + email?: string, + tags?: Array, + languages?: Array, +} + +declare type CollectionUpdateParams = { + claim_id: string, + claim_ids?: Array, + bid?: string, + title?: string, + cover_url?: string, + thumbnail_url?: string, + description?: string, + website_url?: string, + email?: string, + tags?: Array, + replace?: boolean, + languages?: Array, + locations?: Array, + blocking?: boolean, +} + +declare type CollectionPublishParams = { + name: string, + bid: string, + claim_ids: Array, + blocking?: true, + title?: string, + thumbnail_url?: string, + description?: string, + tags?: Array, + languages?: Array, +} diff --git a/flow-typed/CoinSwap.js b/flow-typed/CoinSwap.js new file mode 100644 index 000000000..a41c57196 --- /dev/null +++ b/flow-typed/CoinSwap.js @@ -0,0 +1,29 @@ +declare type CoinSwapInfo = { + chargeCode: string, + coins: Array, + sendAddresses: { [string]: string}, + sendAmounts: { [string]: any }, + lbcAmount: number, + status?: { + status: string, + receiptCurrency: string, + receiptTxid: string, + lbcTxid: string, + }, +} + +declare type CoinSwapState = { + coinSwaps: Array, +}; + +declare type CoinSwapAddAction = { + type: string, + data: CoinSwapInfo, +}; + +declare type CoinSwapRemoveAction = { + type: string, + data: { + chargeCode: string, + }, +}; diff --git a/flow-typed/Collections.js b/flow-typed/Collections.js new file mode 100644 index 000000000..f70825a4f --- /dev/null +++ b/flow-typed/Collections.js @@ -0,0 +1,34 @@ +declare type Collection = { + id: string, + items: Array, + name: string, + type: string, + updatedAt: number, + totalItems?: number, + sourceId?: string, // if copied, claimId of original collection +}; + +declare type CollectionState = { + unpublished: CollectionGroup, + resolved: CollectionGroup, + pending: CollectionGroup, + edited: CollectionGroup, + builtin: CollectionGroup, + saved: Array, + isResolvingCollectionById: { [string]: boolean }, + error?: string | null, +}; + +declare type CollectionGroup = { + [string]: Collection, +} + +declare type CollectionEditParams = { + claims?: Array, + remove?: boolean, + claimIds?: Array, + replace?: boolean, + order?: { from: number, to: number }, + type?: string, + name?: string, +} diff --git a/flow-typed/Comment.js b/flow-typed/Comment.js index 57a666683..c851d297d 100644 --- a/flow-typed/Comment.js +++ b/flow-typed/Comment.js @@ -177,7 +177,7 @@ declare type CommentCreateParams = { claim_id: string, parent_id?: string, signature: string, - signing_ts: number, + signing_ts: string, support_tx_id?: string, }; diff --git a/flow-typed/Lbry.js b/flow-typed/Lbry.js new file mode 100644 index 000000000..f913e15cc --- /dev/null +++ b/flow-typed/Lbry.js @@ -0,0 +1,369 @@ +// @flow +declare type StatusResponse = { + blob_manager: { + finished_blobs: number, + }, + blockchain_headers: { + download_progress: number, + downloading_headers: boolean, + }, + dht: { + node_id: string, + peers_in_routing_table: number, + }, + hash_announcer: { + announce_queue_size: number, + }, + installation_id: string, + is_running: boolean, + skipped_components: Array, + startup_status: { + blob_manager: boolean, + blockchain_headers: boolean, + database: boolean, + dht: boolean, + exchange_rate_manager: boolean, + hash_announcer: boolean, + peer_protocol_server: boolean, + stream_manager: boolean, + upnp: boolean, + wallet: boolean, + }, + stream_manager: { + managed_files: number, + }, + upnp: { + aioupnp_version: string, + dht_redirect_set: boolean, + external_ip: string, + gateway: string, + peer_redirect_set: boolean, + redirects: {}, + }, + wallet: ?{ + connected: string, + best_blockhash: string, + blocks: number, + blocks_behind: number, + is_encrypted: boolean, + is_locked: boolean, + headers_synchronization_progress: number, + available_servers: number, + }, +}; + +declare type VersionResponse = { + build: string, + lbrynet_version: string, + os_release: string, + os_system: string, + platform: string, + processor: string, + python_version: string, +}; + +declare type BalanceResponse = { + available: string, + reserved: string, + reserved_subtotals: ?{ + claims: string, + supports: string, + tips: string, + }, + total: string, +}; + +declare type ResolveResponse = { + // Keys are the url(s) passed to resolve + [string]: { error?: {}, stream?: StreamClaim, channel?: ChannelClaim, collection?: CollectionClaim, claimsInChannel?: number }, +}; + +declare type GetResponse = FileListItem & { error?: string }; + +declare type GenericTxResponse = { + height: number, + hex: string, + inputs: Array<{}>, + outputs: Array<{}>, + total_fee: string, + total_input: string, + total_output: string, + txid: string, +}; + +declare type PublishResponse = GenericTxResponse & { + // Only first value in outputs is a claim + // That's the only value we care about + outputs: Array, +}; + +declare type ClaimSearchResponse = { + items: Array, + page: number, + page_size: number, + total_items: number, + total_pages: number, +}; + +declare type ClaimListResponse = { + items: Array, + page: number, + page_size: number, + total_items: number, + total_pages: number, +}; + +declare type ChannelCreateResponse = GenericTxResponse & { + outputs: Array, +}; + +declare type ChannelUpdateResponse = GenericTxResponse & { + outputs: Array, +}; + +declare type CommentCreateResponse = Comment; +declare type CommentUpdateResponse = Comment; + +declare type MyReactions = { + // Keys are the commentId + [string]: Array, +}; + +declare type OthersReactions = { + // Keys are the commentId + [string]: { + // Keys are the reaction_type, e.g. 'like' + [string]: number, + }, +}; + +declare type CommentReactListResponse = { + my_reactions: Array, + others_reactions: Array, +}; + +declare type CommentHideResponse = { + // keyed by the CommentIds entered + [string]: { hidden: boolean }, +}; + +declare type CommentPinResponse = { + // keyed by the CommentIds entered + items: Comment, +}; + +declare type CommentAbandonResponse = { + // keyed by the CommentId given + abandoned: boolean, +}; + +declare type ChannelListResponse = { + items: Array, + page: number, + page_size: number, + total_items: number, + total_pages: number, +}; + +declare type ChannelSignResponse = { + signature: string, + signing_ts: string, +}; + +declare type CollectionCreateResponse = { + outputs: Array, + page: number, + page_size: number, + total_items: number, + total_pages: number, +} + +declare type CollectionListResponse = { + items: Array, + page: number, + page_size: number, + total_items: number, + total_pages: number, +}; + +declare type CollectionResolveResponse = { + items: Array, + total_items: number, +}; + +declare type CollectionResolveOptions = { + claim_id: string, +}; + +declare type CollectionListOptions = { + page: number, + page_size: number, + resolve?: boolean, +}; + +declare type FileListResponse = { + items: Array, + page: number, + page_size: number, + total_items: number, + total_pages: number, +}; + +declare type TxListResponse = { + items: Array, + page: number, + page_size: number, + total_items: number, + total_pages: number, +}; + +declare type SupportListResponse = { + items: Array, + page: number, + page_size: number, + total_items: number, + total_pages: number, +}; + +declare type BlobListResponse = { items: Array }; + +declare type WalletListResponse = Array<{ + id: string, + name: string, +}>; + +declare type WalletStatusResponse = { + is_encrypted: boolean, + is_locked: boolean, + is_syncing: boolean, +}; + +declare type SyncApplyResponse = { + hash: string, + data: string, +}; + +declare type SupportAbandonResponse = GenericTxResponse; + +declare type StreamListResponse = { + items: Array, + page: number, + page_size: number, + total_items: number, + total_pages: number, +}; + +declare type StreamRepostOptions = { + name: string, + bid: string, + claim_id: string, + channel_id?: string, +}; + +declare type StreamRepostResponse = GenericTxResponse; + +declare type PurchaseListResponse = { + items: Array, + page: number, + page_size: number, + total_items: number, + total_pages: number, +}; + +declare type PurchaseListOptions = { + page: number, + page_size: number, + resolve: boolean, + claim_id?: string, + channel_id?: string, +}; + +// +// Types used in the generic Lbry object that is exported +// +declare type LbryTypes = { + isConnected: boolean, + connectPromise: any, // null | + connect: () => any, // void | Promise ? + daemonConnectionString: string, + alternateConnectionString: string, + methodsUsingAlternateConnectionString: Array, + apiRequestHeaders: { [key: string]: string }, + setDaemonConnectionString: string => void, + setApiHeader: (string, string) => void, + unsetApiHeader: string => void, + overrides: { [string]: ?Function }, + setOverride: (string, Function) => void, + // getMediaType: (?string, ?string) => string, + + // Lbry Methods + stop: () => Promise, + status: () => Promise, + version: () => Promise, + resolve: (params: {}) => Promise, + get: (params: {}) => Promise, + publish: (params: {}) => Promise, + + claim_search: (params: {}) => Promise, + claim_list: (params: {}) => Promise, + channel_create: (params: {}) => Promise, + channel_update: (params: {}) => Promise, + channel_import: (params: {}) => Promise, + channel_list: (params: {}) => Promise, + channel_sign: (params: {}) => Promise, + stream_abandon: (params: {}) => Promise, + stream_list: (params: {}) => Promise, + channel_abandon: (params: {}) => Promise, + support_create: (params: {}) => Promise, + support_list: (params: {}) => Promise, + support_abandon: (params: {}) => Promise, + stream_repost: (params: StreamRepostOptions) => Promise, + purchase_list: (params: PurchaseListOptions) => Promise, + collection_resolve: (params: CollectionResolveOptions) => Promise, + collection_list: (params: CollectionListOptions) => Promise, + collection_create: (params: {}) => Promise, + collection_update: (params: {}) => Promise, + + // File fetching and manipulation + file_list: (params: {}) => Promise, + file_delete: (params: {}) => Promise, + blob_delete: (params: {}) => Promise, + blob_list: (params: {}) => Promise, + file_set_status: (params: {}) => Promise, + file_reflect: (params: {}) => Promise, + + // Preferences + preference_get: (params?: {}) => Promise, + preference_set: (params: {}) => Promise, + + // Commenting + comment_update: (params: {}) => Promise, + comment_hide: (params: {}) => Promise, + comment_abandon: (params: {}) => Promise, + comment_list: (params: {}) => Promise, + comment_create: (params: {}) => Promise, + + // Wallet utilities + wallet_balance: (params: {}) => Promise, + wallet_decrypt: (prams: {}) => Promise, + wallet_encrypt: (params: {}) => Promise, + wallet_unlock: (params: {}) => Promise, + wallet_list: (params: {}) => Promise, + wallet_send: (params: {}) => Promise, + wallet_status: (params?: {}) => Promise, + address_is_mine: (params: {}) => Promise, + address_unused: (params: {}) => Promise, // New address + address_list: (params: {}) => Promise, + transaction_list: (params: {}) => Promise, + txo_list: (params: {}) => Promise, + account_set: (params: {}) => Promise, + account_list: (params?: {}) => Promise, + + // Sync + sync_hash: (params?: {}) => Promise, + sync_apply: (params: {}) => Promise, + // syncGet + + // The app shouldn't need to do this + utxo_release: () => Promise, +}; diff --git a/flow-typed/LbryFirst.js b/flow-typed/LbryFirst.js new file mode 100644 index 000000000..e0954a496 --- /dev/null +++ b/flow-typed/LbryFirst.js @@ -0,0 +1,99 @@ +// @flow +declare type LbryFirstStatusResponse = { + Version: string, + Message: string, + Running: boolean, + Commit: string, +}; + +declare type LbryFirstVersionResponse = { + build: string, + lbrynet_version: string, + os_release: string, + os_system: string, + platform: string, + processor: string, + python_version: string, +}; +/* SAMPLE UPLOAD RESPONSE (FULL) +"Video": { + "etag": "\"Dn5xIderbhAnUk5TAW0qkFFir0M/xlGLrlTox7VFTRcR8F77RbKtaU4\"", + "id": "8InjtdvVmwE", + "kind": "youtube#video", + "snippet": { + "categoryId": "22", + "channelId": "UCXiVsGTU88fJjheB2rqF0rA", + "channelTitle": "Mark Beamer", + "liveBroadcastContent": "none", + "localized": { + "title": "my title" + }, + "publishedAt": "2020-05-05T04:17:53.000Z", + "thumbnails": { + "default": { + "height": 90, + "url": "https://i9.ytimg.com/vi/8InjtdvVmwE/default.jpg?sqp=CMTQw_UF&rs=AOn4CLB6dlhZMSMrazDlWRsitPgCsn8fVw", + "width": 120 + }, + "high": { + "height": 360, + "url": "https://i9.ytimg.com/vi/8InjtdvVmwE/hqdefault.jpg?sqp=CMTQw_UF&rs=AOn4CLB-Je_7l6qvASRAR_bSGWZHaXaJWQ", + "width": 480 + }, + "medium": { + "height": 180, + "url": "https://i9.ytimg.com/vi/8InjtdvVmwE/mqdefault.jpg?sqp=CMTQw_UF&rs=AOn4CLCvSnDLqVznRNMKuvJ_0misY_chPQ", + "width": 320 + } + }, + "title": "my title" + }, + "status": { + "embeddable": true, + "license": "youtube", + "privacyStatus": "private", + "publicStatsViewable": true, + "uploadStatus": "uploaded" + } + } + */ +declare type UploadResponse = { + Video: { + id: string, + snippet: { + channelId: string, + }, + status: { + uploadStatus: string, + }, + }, +}; + +declare type HasYTAuthResponse = { + HashAuth: boolean, +}; + +declare type YTSignupResponse = {}; + +// +// Types used in the generic LbryFirst object that is exported +// +declare type LbryFirstTypes = { + isConnected: boolean, + connectPromise: ?Promise, + connect: () => void, + lbryFirstConnectionString: string, + apiRequestHeaders: { [key: string]: string }, + setApiHeader: (string, string) => void, + unsetApiHeader: string => void, + overrides: { [string]: ?Function }, + setOverride: (string, Function) => void, + + // LbryFirst Methods + stop: () => Promise, + status: () => Promise, + version: () => Promise, + upload: any => Promise, + hasYTAuth: string => Promise, + ytSignup: () => Promise, +}; diff --git a/flow-typed/Notification.js b/flow-typed/Notification.js new file mode 100644 index 000000000..f0f63a1a9 --- /dev/null +++ b/flow-typed/Notification.js @@ -0,0 +1,93 @@ +// @flow +import * as ACTIONS from 'constants/action_types'; + +/* + Toasts: + - First-in, first-out queue + - Simple messages that are shown in response to user interactions + - Never saved + - If they are the result of errors, use the isError flag when creating + - For errors that should interrupt user behavior, use Error +*/ +declare type ToastParams = { + message: string, + title?: string, + linkText?: string, + linkTarget?: string, + isError?: boolean, +}; + +declare type Toast = { + id: string, + params: ToastParams, +}; + +declare type DoToast = { + type: ACTIONS.CREATE_TOAST, + data: Toast, +}; + +/* + Notifications: + - List of notifications based on user interactions/app notifications + - Always saved, but can be manually deleted + - Can happen in the background, or because of user interaction (ex: publish confirmed) +*/ +declare type Notification = { + id: string, // Unique id + dateCreated: number, + isRead: boolean, // Used to display "new" notifications that a user hasn't seen yet + source?: string, // The type/area an notification is from. Used for sorting (ex: publishes, transactions) + // We may want to use priority/isDismissed in the future to specify how urgent a notification is + // and if the user should see it immediately + // isDissmied: boolean, + // priority?: number +}; + +declare type DoNotification = { + type: ACTIONS.CREATE_NOTIFICATION, + data: Notification, +}; + +declare type DoEditNotification = { + type: ACTIONS.EDIT_NOTIFICATION, + data: { + notification: Notification, + }, +}; + +declare type DoDeleteNotification = { + type: ACTIONS.DELETE_NOTIFICATION, + data: { + id: string, // The id to delete + }, +}; + +/* + Errors: + - First-in, first-out queue + - Errors that should interupt user behavior + - For errors that can be shown without interrupting a user, use Toast with the isError flag +*/ +declare type ErrorNotification = { + title: string, + text: string, +}; + +declare type DoError = { + type: ACTIONS.CREATE_ERROR, + data: ErrorNotification, +}; + +declare type DoDismissError = { + type: ACTIONS.DISMISS_ERROR, +}; + +/* + NotificationState +*/ +declare type NotificationState = { + notifications: Array, + errors: Array, + toasts: Array, +}; diff --git a/flow-typed/Publish.js b/flow-typed/Publish.js new file mode 100644 index 000000000..87955ae24 --- /dev/null +++ b/flow-typed/Publish.js @@ -0,0 +1,27 @@ +// @flow + +declare type UpdatePublishFormData = { + filePath?: string, + contentIsFree?: boolean, + fee?: { + amount: string, + currency: string, + }, + title?: string, + thumbnail_url?: string, + uploadThumbnailStatus?: string, + thumbnailPath?: string, + description?: string, + language?: string, + channel?: string, + channelId?: string, + name?: string, + nameError?: string, + bid?: string, + bidError?: string, + otherLicenseDescription?: string, + licenseUrl?: string, + licenseType?: string, + uri?: string, + nsfw: boolean, +}; diff --git a/flow-typed/Redux.js b/flow-typed/Redux.js new file mode 100644 index 000000000..addce023d --- /dev/null +++ b/flow-typed/Redux.js @@ -0,0 +1,6 @@ +// @flow +/* eslint-disable no-use-before-define */ +declare type GetState = () => any; +declare type ThunkAction = (dispatch: Dispatch, getState: GetState) => any; +declare type Dispatch = (action: {} | Promise<*> | Array<{}> | ThunkAction) => any; // Need to refer to ThunkAction +/* eslint-enable */ diff --git a/flow-typed/Reflector.js b/flow-typed/Reflector.js new file mode 100644 index 000000000..fc6be97c1 --- /dev/null +++ b/flow-typed/Reflector.js @@ -0,0 +1,5 @@ +declare type ReflectingUpdate = { + fileListItem: FileListItem, + progress: number | boolean, + stalled: boolean, +}; diff --git a/flow-typed/Tags.js b/flow-typed/Tags.js new file mode 100644 index 000000000..01c58ac10 --- /dev/null +++ b/flow-typed/Tags.js @@ -0,0 +1,21 @@ +declare type TagState = { + followedTags: FollowedTags, + knownTags: KnownTags, +}; + +declare type Tag = { + name: string, +}; + +declare type KnownTags = { + [string]: Tag, +}; + +declare type FollowedTags = Array; + +declare type TagAction = { + type: string, + data: { + name: string, + }, +}; diff --git a/flow-typed/Transaction.js b/flow-typed/Transaction.js new file mode 100644 index 000000000..db9e592fb --- /dev/null +++ b/flow-typed/Transaction.js @@ -0,0 +1,28 @@ +// @flow +declare type Transaction = { + amount: number, + claim_id: string, + claim_name: string, + fee: number, + nout: number, + txid: string, + type: string, + date: Date, +}; + +declare type Support = { + address: string, + amount: string, + claim_id: string, + confirmations: number, + height: string, + is_change: string, + is_mine: string, + name: string, + normalized_name: string, + nout: string, + permanent_url: string, + timestamp: number, + txid: string, + type: string, +}; diff --git a/flow-typed/Txo.js b/flow-typed/Txo.js new file mode 100644 index 000000000..c4ab9b6bc --- /dev/null +++ b/flow-typed/Txo.js @@ -0,0 +1,27 @@ +declare type Txo = { + amount: number, + claim_id: string, + normalized_name: string, + nout: number, + txid: string, + type: string, + value_type: string, + timestamp: number, + is_my_output: boolean, + is_my_input: boolean, + is_spent: boolean, + signing_channel?: { + channel_id: string, + }, +}; + +declare type TxoListParams = { + page: number, + page_size: number, + type: string, + is_my_input?: boolean, + is_my_output?: boolean, + is_not_my_input?: boolean, + is_not_my_output?: boolean, + is_spent?: boolean, +}; diff --git a/flow-typed/i18n.js b/flow-typed/i18n.js new file mode 100644 index 000000000..de97c0c86 --- /dev/null +++ b/flow-typed/i18n.js @@ -0,0 +1,2 @@ +// @flow +declare function __(a: string, b?: {}): string; diff --git a/flow-typed/lbryURI.js b/flow-typed/lbryURI.js new file mode 100644 index 000000000..549c99106 --- /dev/null +++ b/flow-typed/lbryURI.js @@ -0,0 +1,21 @@ +// @flow +declare type LbryUrlObj = { + // Path and channel will always exist when calling parseURI + // But they may not exist when code calls buildURI + isChannel?: boolean, + path?: string, + streamName?: string, + streamClaimId?: string, + channelName?: string, + channelClaimId?: string, + primaryClaimSequence?: number, + secondaryClaimSequence?: number, + primaryBidPosition?: number, + secondaryBidPosition?: number, + startTime?: number, + + // Below are considered deprecated and should not be used due to unreliableness with claim.canonical_url + claimName?: string, + claimId?: string, + contentName?: string, +}; diff --git a/flow-typed/publish.js b/flow-typed/publish.js index ca11a6a5d..f237b12c9 100644 --- a/flow-typed/publish.js +++ b/flow-typed/publish.js @@ -25,7 +25,7 @@ declare type UpdatePublishFormData = { licenseType?: string, uri?: string, nsfw: boolean, - isMarkdownPost: boolean, + isMarkdownPost?: boolean, }; declare type PublishParams = { diff --git a/package.json b/package.json index 6826b3296..adf9ced23 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,8 @@ "postinstall:warning": "echo '\n\nWARNING\n\nNot all node modules were installed because NODE_ENV is set to \"production\".\nThis should only be set after installing dependencies with \"yarn\". The app will not work.\n\n'" }, "dependencies": { + "@ungap/from-entries": "^0.2.1", + "proxy-polyfill": "0.1.6", "auto-launch": "^5.0.5", "electron-dl": "^1.11.0", "electron-log": "^2.2.12", @@ -157,8 +159,6 @@ "imagesloaded": "^4.1.4", "json-loader": "^0.5.4", "lbry-format": "https://github.com/lbryio/lbry-format.git", - "lbry-redux": "lbryio/lbry-redux#32b578707116d45f5b51b7ab523d200e75668676", - "lbryinc": "lbryio/lbryinc#0b4e41ef90d6347819dd3453f2f9398a5c1b4f36", "lint-staged": "^7.0.2", "localforage": "^1.7.1", "lodash-es": "^4.17.14", diff --git a/ui/component/abandonedChannelPreview/view.jsx b/ui/component/abandonedChannelPreview/view.jsx index 1fc67c6bd..f02afb3d1 100644 --- a/ui/component/abandonedChannelPreview/view.jsx +++ b/ui/component/abandonedChannelPreview/view.jsx @@ -2,7 +2,7 @@ import React from 'react'; import classnames from 'classnames'; import ChannelThumbnail from 'component/channelThumbnail'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; import ChannelBlockButton from 'component/channelBlockButton'; import ChannelMuteButton from 'component/channelMuteButton'; import SubscribeButton from 'component/subscribeButton'; diff --git a/ui/component/app/index.js b/ui/component/app/index.js index 1e3da5da0..457b1886f 100644 --- a/ui/component/app/index.js +++ b/ui/component/app/index.js @@ -5,13 +5,9 @@ import { selectGetSyncErrorMessage, selectSyncFatalError } from 'redux/selectors import { doFetchAccessToken, doUserSetReferrer } from 'redux/actions/user'; import { selectUser, selectAccessToken, selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUnclaimedRewards } from 'redux/selectors/rewards'; -import { - doFetchChannelListMine, - doFetchCollectionListMine, - SETTINGS, - selectMyChannelUrls, - doResolveUris, -} from 'lbry-redux'; +import { doFetchChannelListMine, doFetchCollectionListMine, doResolveUris } from 'redux/actions/claims'; +import { selectMyChannelUrls } from 'redux/selectors/claims'; +import * as SETTINGS from 'constants/settings'; import { selectSubscriptions } from 'redux/selectors/subscriptions'; import { makeSelectClientSetting, diff --git a/ui/component/app/view.jsx b/ui/component/app/view.jsx index 00505e48a..cfa3f701e 100644 --- a/ui/component/app/view.jsx +++ b/ui/component/app/view.jsx @@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState, useLayoutEffect } from 'react'; import { lazyImport } from 'util/lazyImport'; import classnames from 'classnames'; import analytics from 'analytics'; -import { buildURI, parseURI } from 'lbry-redux'; +import { buildURI, parseURI } from 'util/lbryURI'; import { SIMPLE_SITE } from 'config'; import Router from 'component/router/index'; import ReactModal from 'react-modal'; diff --git a/ui/component/autoplayCountdown/index.js b/ui/component/autoplayCountdown/index.js index 8793048ea..7cdccf686 100644 --- a/ui/component/autoplayCountdown/index.js +++ b/ui/component/autoplayCountdown/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; import { withRouter } from 'react-router'; import AutoplayCountdown from './view'; import { selectModal } from 'redux/selectors/app'; diff --git a/ui/component/channelAbout/index.js b/ui/component/channelAbout/index.js index 40678c8c7..5770ce7ea 100644 --- a/ui/component/channelAbout/index.js +++ b/ui/component/channelAbout/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectMetadataItemForUri, makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectMetadataItemForUri, makeSelectClaimForUri } from 'redux/selectors/claims'; import ChannelAbout from './view'; const select = (state, props) => ({ diff --git a/ui/component/channelBlockButton/index.js b/ui/component/channelBlockButton/index.js index 50ca09e1e..9852404d7 100644 --- a/ui/component/channelBlockButton/index.js +++ b/ui/component/channelBlockButton/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimIdForUri } from 'lbry-redux'; +import { makeSelectClaimIdForUri } from 'redux/selectors/claims'; import { doCommentModUnBlock, doCommentModBlock, diff --git a/ui/component/channelContent/index.js b/ui/component/channelContent/index.js index 8d3bbc6fb..fc1a349f0 100644 --- a/ui/component/channelContent/index.js +++ b/ui/component/channelContent/index.js @@ -6,9 +6,9 @@ import { makeSelectClaimIsMine, makeSelectTotalPagesInChannelSearch, makeSelectClaimForUri, - doResolveUris, - SETTINGS, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { doResolveUris } from 'redux/actions/claims'; +import * as SETTINGS from 'constants/settings'; import { makeSelectChannelIsMuted } from 'redux/selectors/blocked'; import { withRouter } from 'react-router'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; diff --git a/ui/component/channelDiscussion/index.js b/ui/component/channelDiscussion/index.js index ff198701f..e0f04c8cc 100644 --- a/ui/component/channelDiscussion/index.js +++ b/ui/component/channelDiscussion/index.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import { withRouter } from 'react-router'; import { DISABLE_COMMENTS_TAG } from 'constants/tags'; import ChannelDiscussion from './view'; -import { makeSelectTagInClaimOrChannelForUri } from 'lbry-redux'; +import { makeSelectTagInClaimOrChannelForUri } from 'redux/selectors/claims'; const select = (state, props) => { const { search } = props.location; diff --git a/ui/component/channelEdit/index.js b/ui/component/channelEdit/index.js index 9a195039b..43e054f7e 100644 --- a/ui/component/channelEdit/index.js +++ b/ui/component/channelEdit/index.js @@ -4,17 +4,15 @@ import { makeSelectThumbnailForUri, makeSelectCoverForUri, makeSelectMetadataItemForUri, - doUpdateChannel, - doCreateChannel, makeSelectAmountForUri, makeSelectClaimForUri, selectUpdateChannelError, selectUpdatingChannel, selectCreateChannelError, selectCreatingChannel, - selectBalance, - doClearChannelErrors, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { selectBalance } from 'redux/selectors/wallet'; +import { doUpdateChannel, doCreateChannel, doClearChannelErrors } from 'redux/actions/claims'; import { doOpenModal } from 'redux/actions/app'; import { doUpdateBlockListForPublishedChannel } from 'redux/actions/comments'; import { doClaimInitialRewards } from 'redux/actions/rewards'; diff --git a/ui/component/channelEdit/view.jsx b/ui/component/channelEdit/view.jsx index 3e84e40ff..9148ad33c 100644 --- a/ui/component/channelEdit/view.jsx +++ b/ui/component/channelEdit/view.jsx @@ -9,7 +9,7 @@ import TagsSearch from 'component/tagsSearch'; import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field'; import ErrorText from 'component/common/error-text'; import ChannelThumbnail from 'component/channelThumbnail'; -import { isNameValid, parseURI } from 'lbry-redux'; +import { isNameValid, parseURI } from 'util/lbryURI'; import ClaimAbandonButton from 'component/claimAbandonButton'; import { useHistory } from 'react-router-dom'; import { MINIMUM_PUBLISH_BID, INVALID_NAME_ERROR, ESTIMATED_FEE } from 'constants/claim'; diff --git a/ui/component/channelMentionSuggestion/index.js b/ui/component/channelMentionSuggestion/index.js index 40b1a92ae..afc418ce7 100644 --- a/ui/component/channelMentionSuggestion/index.js +++ b/ui/component/channelMentionSuggestion/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri, makeSelectIsUriResolving } from 'lbry-redux'; +import { makeSelectClaimForUri, makeSelectIsUriResolving } from 'redux/selectors/claims'; import ChannelMentionSuggestion from './view'; const select = (state, props) => ({ diff --git a/ui/component/channelMentionSuggestions/index.js b/ui/component/channelMentionSuggestions/index.js index c5ba9712d..d6b4af95b 100644 --- a/ui/component/channelMentionSuggestions/index.js +++ b/ui/component/channelMentionSuggestions/index.js @@ -2,7 +2,8 @@ import { connect } from 'react-redux'; import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectSubscriptions } from 'redux/selectors/subscriptions'; import { withRouter } from 'react-router'; -import { doResolveUris, makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; +import { doResolveUris } from 'redux/actions/claims'; import { makeSelectTopLevelCommentsForUri } from 'redux/selectors/comments'; import ChannelMentionSuggestions from './view'; diff --git a/ui/component/channelMentionSuggestions/view.jsx b/ui/component/channelMentionSuggestions/view.jsx index 9529ba717..d3fefd1f9 100644 --- a/ui/component/channelMentionSuggestions/view.jsx +++ b/ui/component/channelMentionSuggestions/view.jsx @@ -1,7 +1,7 @@ // @flow import { Combobox, ComboboxInput, ComboboxPopover, ComboboxList } from '@reach/combobox'; import { Form } from 'component/common/form'; -import { parseURI, regexInvalidURI } from 'lbry-redux'; +import { parseURI, regexInvalidURI } from 'util/lbryURI'; import { SEARCH_OPTIONS } from 'constants/search'; import * as KEYCODES from 'constants/keycodes'; import ChannelMentionSuggestion from 'component/channelMentionSuggestion'; @@ -69,8 +69,9 @@ export default function ChannelMentionSuggestions(props: Props) { const allShownCanonical = [canonicalCreator, ...canonicalSubscriptions, ...canonicalCommentors]; const possibleMatches = allShownUris.filter((uri) => { try { + // yuck a try catch in a filter? const { channelName } = parseURI(uri); - return channelName.toLowerCase().includes(termToMatch); + return channelName && channelName.toLowerCase().includes(termToMatch); } catch (e) {} }); diff --git a/ui/component/channelMentionTopSuggestion/index.js b/ui/component/channelMentionTopSuggestion/index.js index d3a1d55b5..54d1f6786 100644 --- a/ui/component/channelMentionTopSuggestion/index.js +++ b/ui/component/channelMentionTopSuggestion/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { makeSelectIsUriResolving, doResolveUri } from 'lbry-redux'; +import { makeSelectIsUriResolving } from 'redux/selectors/claims'; +import { doResolveUri } from 'redux/actions/claims'; import { makeSelectWinningUriForQuery } from 'redux/selectors/search'; import ChannelMentionTopSuggestion from './view'; diff --git a/ui/component/channelSelector/index.js b/ui/component/channelSelector/index.js index f56a21d63..3407bce27 100644 --- a/ui/component/channelSelector/index.js +++ b/ui/component/channelSelector/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { selectMyChannelClaims } from 'lbry-redux'; +import { selectMyChannelClaims } from 'redux/selectors/claims'; import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app'; import { doSetActiveChannel, doSetIncognito } from 'redux/actions/app'; import ChannelSelector from './view'; diff --git a/ui/component/channelStakedIndicator/index.js b/ui/component/channelStakedIndicator/index.js index 987683f17..d217012a4 100644 --- a/ui/component/channelStakedIndicator/index.js +++ b/ui/component/channelStakedIndicator/index.js @@ -3,7 +3,7 @@ import { makeSelectClaimForUri, makeSelectStakedLevelForChannelUri, makeSelectTotalStakedAmountForChannelUri, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; import ChannelStakedIndicator from './view'; const select = (state, props) => ({ diff --git a/ui/component/channelThumbnail/index.js b/ui/component/channelThumbnail/index.js index 8f99d916c..ebedaff1a 100644 --- a/ui/component/channelThumbnail/index.js +++ b/ui/component/channelThumbnail/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { makeSelectThumbnailForUri, doResolveUri, makeSelectClaimForUri, makeSelectIsUriResolving } from 'lbry-redux'; +import { makeSelectThumbnailForUri, makeSelectClaimForUri, makeSelectIsUriResolving } from 'redux/selectors/claims'; +import { doResolveUri } from 'redux/actions/claims'; import ChannelThumbnail from './view'; const select = (state, props) => ({ diff --git a/ui/component/channelThumbnail/view.jsx b/ui/component/channelThumbnail/view.jsx index 572a36028..f1449cc5d 100644 --- a/ui/component/channelThumbnail/view.jsx +++ b/ui/component/channelThumbnail/view.jsx @@ -1,6 +1,6 @@ // @flow import React from 'react'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; import classnames from 'classnames'; import Gerbil from './gerbil.png'; import FreezeframeWrapper from 'component/fileThumbnail/FreezeframeWrapper'; diff --git a/ui/component/channelTitle/index.js b/ui/component/channelTitle/index.js index 502068855..d98e83683 100644 --- a/ui/component/channelTitle/index.js +++ b/ui/component/channelTitle/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri, makeSelectTitleForUri } from 'lbry-redux'; +import { makeSelectClaimForUri, makeSelectTitleForUri } from 'redux/selectors/claims'; import ChannelTitle from './view'; const select = (state, props) => ({ diff --git a/ui/component/claimAbandonButton/index.js b/ui/component/claimAbandonButton/index.js index e32042d7e..8f706b679 100644 --- a/ui/component/claimAbandonButton/index.js +++ b/ui/component/claimAbandonButton/index.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import { doOpenModal } from 'redux/actions/app'; import ClaimAbandonButton from './view'; -import { makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; const select = (state, props) => ({ claim: props.uri && makeSelectClaimForUri(props.uri)(state), diff --git a/ui/component/claimAuthor/index.js b/ui/component/claimAuthor/index.js index 7119e395b..736bff290 100644 --- a/ui/component/claimAuthor/index.js +++ b/ui/component/claimAuthor/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectChannelForClaimUri } from 'lbry-redux'; +import { makeSelectChannelForClaimUri } from 'redux/selectors/claims'; import ClaimAuthor from './view'; const select = (state, props) => ({ diff --git a/ui/component/claimCollectionAdd/index.js b/ui/component/claimCollectionAdd/index.js index 896af365b..e1a43261b 100644 --- a/ui/component/claimCollectionAdd/index.js +++ b/ui/component/claimCollectionAdd/index.js @@ -2,12 +2,12 @@ import { connect } from 'react-redux'; import ClaimCollectionAdd from './view'; import { withRouter } from 'react-router'; import { - makeSelectClaimForUri, - doLocalCollectionCreate, selectBuiltinCollections, selectMyPublishedCollections, selectMyUnpublishedCollections, -} from 'lbry-redux'; +} from 'redux/selectors/collections'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; +import { doLocalCollectionCreate } from 'redux/actions/collections'; const select = (state, props) => ({ claim: makeSelectClaimForUri(props.uri)(state), diff --git a/ui/component/claimCollectionAddButton/index.js b/ui/component/claimCollectionAddButton/index.js index 57e05a763..fabd9421a 100644 --- a/ui/component/claimCollectionAddButton/index.js +++ b/ui/component/claimCollectionAddButton/index.js @@ -1,7 +1,8 @@ import { connect } from 'react-redux'; import { doOpenModal } from 'redux/actions/app'; import CollectionAddButton from './view'; -import { makeSelectClaimForUri, makeSelectClaimUrlInCollection } from 'lbry-redux'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; +import { makeSelectClaimUrlInCollection } from 'redux/selectors/collections'; const select = (state, props) => { const claim = makeSelectClaimForUri(props.uri)(state); diff --git a/ui/component/claimEffectiveAmount/index.js b/ui/component/claimEffectiveAmount/index.js index 7601e0eca..0325d9e17 100644 --- a/ui/component/claimEffectiveAmount/index.js +++ b/ui/component/claimEffectiveAmount/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; import ClaimEffectiveAmount from './view'; const select = (state, props) => ({ diff --git a/ui/component/claimInsufficientCredits/index.js b/ui/component/claimInsufficientCredits/index.js index 1b65245ac..3a0230ea4 100644 --- a/ui/component/claimInsufficientCredits/index.js +++ b/ui/component/claimInsufficientCredits/index.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import { makeSelectInsufficientCreditsForUri } from 'redux/selectors/content'; -import { makeSelectClaimWasPurchased } from 'lbry-redux'; +import { makeSelectClaimWasPurchased } from 'redux/selectors/claims'; import ClaimInsufficientCredits from './view'; const select = (state, props) => ({ diff --git a/ui/component/claimLink/index.js b/ui/component/claimLink/index.js index 6aaed830a..ea4ab51b3 100644 --- a/ui/component/claimLink/index.js +++ b/ui/component/claimLink/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { doResolveUri, makeSelectClaimForUri, makeSelectIsUriResolving } from 'lbry-redux'; +import { makeSelectClaimForUri, makeSelectIsUriResolving } from 'redux/selectors/claims'; +import { doResolveUri } from 'redux/actions/claims'; import { doSetPlayingUri } from 'redux/actions/content'; import { punctuationMarks } from 'util/remark-lbry'; import { selectBlackListedOutpoints } from 'lbryinc'; diff --git a/ui/component/claimList/index.js b/ui/component/claimList/index.js index d9d20509c..086af2607 100644 --- a/ui/component/claimList/index.js +++ b/ui/component/claimList/index.js @@ -1,6 +1,7 @@ import { connect } from 'react-redux'; import ClaimList from './view'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; + import { makeSelectClientSetting } from 'redux/selectors/settings'; const select = (state) => ({ diff --git a/ui/component/claimListDiscover/index.js b/ui/component/claimListDiscover/index.js index f1d93ca49..eac963d15 100644 --- a/ui/component/claimListDiscover/index.js +++ b/ui/component/claimListDiscover/index.js @@ -1,12 +1,12 @@ import { connect } from 'react-redux'; import { - doClaimSearch, selectClaimsByUri, selectClaimSearchByQuery, selectClaimSearchByQueryLastPageReached, selectFetchingClaimSearch, - SETTINGS, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { doClaimSearch } from 'redux/actions/claims'; +import * as SETTINGS from 'constants/settings'; import { selectFollowedTags } from 'redux/selectors/tags'; import { selectMutedChannels } from 'redux/selectors/blocked'; import { doToggleTagFollowDesktop } from 'redux/actions/tags'; diff --git a/ui/component/claimListDiscover/view.jsx b/ui/component/claimListDiscover/view.jsx index e450a2c45..38c59879c 100644 --- a/ui/component/claimListDiscover/view.jsx +++ b/ui/component/claimListDiscover/view.jsx @@ -5,7 +5,9 @@ import * as CS from 'constants/claim_search'; import React from 'react'; import usePersistedState from 'effects/use-persisted-state'; import { withRouter } from 'react-router'; -import { createNormalizedClaimSearchKey, MATURE_TAGS, splitBySeparator } from 'lbry-redux'; +import { MATURE_TAGS } from 'constants/tags'; +import { createNormalizedClaimSearchKey } from 'util/claim'; +import { splitBySeparator } from 'util/lbryURI'; import Button from 'component/button'; import moment from 'moment'; import ClaimList from 'component/claimList'; diff --git a/ui/component/claimListHeader/index.js b/ui/component/claimListHeader/index.js index 994329044..ec9d839ca 100644 --- a/ui/component/claimListHeader/index.js +++ b/ui/component/claimListHeader/index.js @@ -1,9 +1,10 @@ import { connect } from 'react-redux'; -import { selectFetchingClaimSearch, SETTINGS } from 'lbry-redux'; +import { selectFetchingClaimSearch } from 'redux/selectors/claims'; import { selectFollowedTags } from 'redux/selectors/tags'; import { doToggleTagFollowDesktop } from 'redux/actions/tags'; import { makeSelectClientSetting, selectShowMatureContent, selectLanguage } from 'redux/selectors/settings'; import { doSetClientSetting } from 'redux/actions/settings'; +import * as SETTINGS from 'constants/settings'; import ClaimListHeader from './view'; const select = (state) => ({ diff --git a/ui/component/claimListHeader/view.jsx b/ui/component/claimListHeader/view.jsx index 2865211a3..24c3dfadd 100644 --- a/ui/component/claimListHeader/view.jsx +++ b/ui/component/claimListHeader/view.jsx @@ -1,12 +1,12 @@ // @flow import * as CS from 'constants/claim_search'; import * as ICONS from 'constants/icons'; +import * as SETTINGS from 'constants/settings'; import type { Node } from 'react'; import classnames from 'classnames'; import React from 'react'; import usePersistedState from 'effects/use-persisted-state'; import { useHistory } from 'react-router'; -import { SETTINGS } from 'lbry-redux'; import { FormField } from 'component/common/form'; import Button from 'component/button'; import { toCapitalCase } from 'util/string'; diff --git a/ui/component/claimMenuList/index.js b/ui/component/claimMenuList/index.js index 87791afd3..2f00b8766 100644 --- a/ui/component/claimMenuList/index.js +++ b/ui/component/claimMenuList/index.js @@ -1,17 +1,15 @@ import { connect } from 'react-redux'; +import { makeSelectClaimForUri, makeSelectClaimIsMine } from 'redux/selectors/claims'; +import { doCollectionEdit, doFetchItemsInCollection } from 'redux/actions/collections'; +import { doPrepareEdit } from 'redux/actions/publish'; import { - doCollectionEdit, - makeSelectClaimForUri, - makeSelectFileInfoForUri, - doPrepareEdit, makeSelectCollectionForIdHasClaimUrl, makeSelectCollectionIsMine, - COLLECTIONS_CONSTS, makeSelectEditedCollectionForId, - makeSelectClaimIsMine, - doFetchItemsInCollection, makeSelectUrlsForCollectionId, -} from 'lbry-redux'; +} from 'redux/selectors/collections'; +import { makeSelectFileInfoForUri } from 'redux/selectors/file_info'; +import * as COLLECTIONS_CONSTS from 'constants/collections'; import { makeSelectChannelIsMuted } from 'redux/selectors/blocked'; import { doChannelMute, doChannelUnmute } from 'redux/actions/blocked'; import { doSetActiveChannel, doSetIncognito, doOpenModal } from 'redux/actions/app'; diff --git a/ui/component/claimMenuList/view.jsx b/ui/component/claimMenuList/view.jsx index 3d9b95d0b..71fbd9631 100644 --- a/ui/component/claimMenuList/view.jsx +++ b/ui/component/claimMenuList/view.jsx @@ -3,6 +3,7 @@ import { URL, SHARE_DOMAIN_URL } from 'config'; import * as ICONS from 'constants/icons'; import * as PAGES from 'constants/pages'; import * as MODALS from 'constants/modal_types'; +import * as COLLECTIONS_CONSTS from 'constants/collections'; import React from 'react'; import classnames from 'classnames'; import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button'; @@ -15,7 +16,7 @@ import { generateListSearchUrlParams, } from 'util/url'; import { useHistory } from 'react-router'; -import { buildURI, parseURI, COLLECTIONS_CONSTS } from 'lbry-redux'; +import { buildURI, parseURI } from 'util/lbryURI'; const SHARE_DOMAIN = SHARE_DOMAIN_URL || URL; const PAGE_VIEW_QUERY = 'view'; diff --git a/ui/component/claimPreview/index.js b/ui/component/claimPreview/index.js index 2dc96541d..b99cabf4e 100644 --- a/ui/component/claimPreview/index.js +++ b/ui/component/claimPreview/index.js @@ -1,6 +1,5 @@ import { connect } from 'react-redux'; import { - doResolveUri, makeSelectClaimForUri, makeSelectIsUriResolving, makeSelectClaimIsMine, @@ -8,15 +7,20 @@ import { makeSelectClaimIsNsfw, makeSelectReflectingClaimForUri, makeSelectClaimWasPurchased, - makeSelectStreamingUrlForUri, makeSelectClaimIsStreamPlaceholder, - makeSelectCollectionIsMine, - doCollectionEdit, - makeSelectUrlsForCollectionId, - makeSelectIndexForUrlInCollection, makeSelectTitleForUri, makeSelectDateForUri, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { makeSelectStreamingUrlForUri } from 'redux/selectors/file_info'; +import { + makeSelectCollectionIsMine, + makeSelectUrlsForCollectionId, + makeSelectIndexForUrlInCollection, +} from 'redux/selectors/collections'; + +import { doResolveUri } from 'redux/actions/claims'; +import { doCollectionEdit } from 'redux/actions/collections'; +import { doFileGet } from 'redux/actions/file'; import { selectMutedChannels, makeSelectChannelIsMuted } from 'redux/selectors/blocked'; import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc'; import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream'; diff --git a/ui/component/claimPreview/view.jsx b/ui/component/claimPreview/view.jsx index 34e7dc188..89edc23d6 100644 --- a/ui/component/claimPreview/view.jsx +++ b/ui/component/claimPreview/view.jsx @@ -5,7 +5,8 @@ import { NavLink, withRouter } from 'react-router-dom'; import { isEmpty } from 'util/object'; import { lazyImport } from 'util/lazyImport'; import classnames from 'classnames'; -import { parseURI, COLLECTIONS_CONSTS, isURIEqual } from 'lbry-redux'; +import { parseURI, isURIEqual } from 'util/lbryURI'; +import * as COLLECTIONS_CONSTS from 'constants/collections'; import { formatLbryUrlForWeb } from 'util/url'; import { formatClaimPreviewTitle } from 'util/formatAriaLabel'; import FileThumbnail from 'component/fileThumbnail'; diff --git a/ui/component/claimPreviewSubtitle/index.js b/ui/component/claimPreviewSubtitle/index.js index c0166d2b8..cf5f1826d 100644 --- a/ui/component/claimPreviewSubtitle/index.js +++ b/ui/component/claimPreviewSubtitle/index.js @@ -3,10 +3,9 @@ import { connect } from 'react-redux'; import { makeSelectClaimForUri, makeSelectClaimIsPending, - doClearPublish, - doPrepareEdit, makeSelectClaimIsStreamPlaceholder, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { doClearPublish, doPrepareEdit } from 'redux/actions/publish'; import { push } from 'connected-react-router'; import ClaimPreviewSubtitle from './view'; diff --git a/ui/component/claimPreviewSubtitle/view.jsx b/ui/component/claimPreviewSubtitle/view.jsx index f26e08498..6fb8ec833 100644 --- a/ui/component/claimPreviewSubtitle/view.jsx +++ b/ui/component/claimPreviewSubtitle/view.jsx @@ -5,7 +5,7 @@ import UriIndicator from 'component/uriIndicator'; import DateTime from 'component/dateTime'; import Button from 'component/button'; import FileViewCountInline from 'component/fileViewCountInline'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; type Props = { uri: string, diff --git a/ui/component/claimPreviewTile/index.js b/ui/component/claimPreviewTile/index.js index 59d506718..68a6b6362 100644 --- a/ui/component/claimPreviewTile/index.js +++ b/ui/component/claimPreviewTile/index.js @@ -1,6 +1,5 @@ import { connect } from 'react-redux'; import { - doResolveUri, makeSelectClaimForUri, makeSelectIsUriResolving, makeSelectThumbnailForUri, @@ -9,7 +8,9 @@ import { makeSelectClaimIsNsfw, makeSelectClaimIsStreamPlaceholder, makeSelectDateForUri, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { doFileGet } from 'redux/actions/file'; +import { doResolveUri } from 'redux/actions/claims'; import { selectMutedChannels } from 'redux/selectors/blocked'; import { makeSelectViewCountForUri, selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc'; import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream'; diff --git a/ui/component/claimPreviewTile/view.jsx b/ui/component/claimPreviewTile/view.jsx index a5c515ffd..8c562b269 100644 --- a/ui/component/claimPreviewTile/view.jsx +++ b/ui/component/claimPreviewTile/view.jsx @@ -12,7 +12,7 @@ import SubscribeButton from 'component/subscribeButton'; import useGetThumbnail from 'effects/use-get-thumbnail'; import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url'; import { formatClaimPreviewTitle } from 'util/formatAriaLabel'; -import { parseURI, isURIEqual } from 'lbry-redux'; +import { parseURI, isURIEqual } from 'util/lbryURI'; import PreviewOverlayProperties from 'component/previewOverlayProperties'; import FileDownloadLink from 'component/fileDownloadLink'; import FileWatchLaterLink from 'component/fileWatchLaterLink'; @@ -191,9 +191,11 @@ function ClaimPreviewTile(props: Props) {
-
+
); @@ -253,9 +255,11 @@ function ClaimPreviewTile(props: Props) {
-
+
{isChannel ? (
diff --git a/ui/component/claimPreviewTitle/index.js b/ui/component/claimPreviewTitle/index.js index e1d05cf4e..5a36e2b29 100644 --- a/ui/component/claimPreviewTitle/index.js +++ b/ui/component/claimPreviewTitle/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri, makeSelectTitleForUri } from 'lbry-redux'; +import { makeSelectClaimForUri, makeSelectTitleForUri } from 'redux/selectors/claims'; import ClaimPreviewTitle from './view'; const select = (state, props) => ({ diff --git a/ui/component/claimProperties/index.js b/ui/component/claimProperties/index.js index 286a0e4fa..574ceb0e1 100644 --- a/ui/component/claimProperties/index.js +++ b/ui/component/claimProperties/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectClaimIsMine, makeSelectClaimForUri } from 'redux/selectors/claims'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; import ClaimProperties from './view'; diff --git a/ui/component/claimRepostAuthor/index.js b/ui/component/claimRepostAuthor/index.js index 2d5cf2a12..83a8ccd7c 100644 --- a/ui/component/claimRepostAuthor/index.js +++ b/ui/component/claimRepostAuthor/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; import ClaimRepostAuthor from './view'; const select = (state, props) => ({ diff --git a/ui/component/claimSupportButton/index.js b/ui/component/claimSupportButton/index.js index a702c4ef5..918faf199 100644 --- a/ui/component/claimSupportButton/index.js +++ b/ui/component/claimSupportButton/index.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import { doOpenModal } from 'redux/actions/app'; -import { makeSelectTagInClaimOrChannelForUri, makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectTagInClaimOrChannelForUri, makeSelectClaimForUri } from 'redux/selectors/claims'; import ClaimSupportButton from './view'; const DISABLE_SUPPORT_TAG = 'disable-support'; diff --git a/ui/component/claimTags/index.js b/ui/component/claimTags/index.js index 1fbca33d3..a0280f138 100644 --- a/ui/component/claimTags/index.js +++ b/ui/component/claimTags/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectTagsForUri } from 'lbry-redux'; +import { makeSelectTagsForUri } from 'redux/selectors/claims'; import { selectFollowedTags } from 'redux/selectors/tags'; import ClaimTags from './view'; diff --git a/ui/component/claimTilesDiscover/index.js b/ui/component/claimTilesDiscover/index.js index 25c446ca6..218035326 100644 --- a/ui/component/claimTilesDiscover/index.js +++ b/ui/component/claimTilesDiscover/index.js @@ -1,13 +1,9 @@ import { connect } from 'react-redux'; import { withRouter } from 'react-router'; -import { - doClaimSearch, - selectClaimSearchByQuery, - selectFetchingClaimSearchByQuery, - SETTINGS, - selectClaimsByUri, - MATURE_TAGS, -} from 'lbry-redux'; +import { selectClaimSearchByQuery, selectFetchingClaimSearchByQuery, selectClaimsByUri } from 'redux/selectors/claims'; +import { doClaimSearch } from 'redux/actions/claims'; +import * as SETTINGS from 'constants/settings'; +import { MATURE_TAGS } from 'constants/tags'; import { doFetchViewCount } from 'lbryinc'; import { doToggleTagFollowDesktop } from 'redux/actions/tags'; import { makeSelectClientSetting, selectShowMatureContent } from 'redux/selectors/settings'; diff --git a/ui/component/claimTilesDiscover/view.jsx b/ui/component/claimTilesDiscover/view.jsx index fe50cc8e4..351f8cf07 100644 --- a/ui/component/claimTilesDiscover/view.jsx +++ b/ui/component/claimTilesDiscover/view.jsx @@ -1,7 +1,7 @@ // @flow import type { Node } from 'react'; import React from 'react'; -import { createNormalizedClaimSearchKey } from 'lbry-redux'; +import { createNormalizedClaimSearchKey } from 'util/claim'; import ClaimPreviewTile from 'component/claimPreviewTile'; import useFetchViewCount from 'effects/use-fetch-view-count'; import usePrevious from 'effects/use-previous'; diff --git a/ui/component/claimType/index.js b/ui/component/claimType/index.js index b456df711..98034acda 100644 --- a/ui/component/claimType/index.js +++ b/ui/component/claimType/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri, makeSelectClaimIsStreamPlaceholder } from 'lbry-redux'; +import { makeSelectClaimForUri, makeSelectClaimIsStreamPlaceholder } from 'redux/selectors/claims'; import FileType from './view'; const select = (state, props) => ({ diff --git a/ui/component/claimUri/index.js b/ui/component/claimUri/index.js index 622b9048e..976000481 100644 --- a/ui/component/claimUri/index.js +++ b/ui/component/claimUri/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectCanonicalUrlForUri } from 'lbry-redux'; +import { makeSelectCanonicalUrlForUri } from 'redux/selectors/claims'; import { doToast } from 'redux/actions/notifications'; import ClaimUri from './view'; diff --git a/ui/component/collectionActions/index.js b/ui/component/collectionActions/index.js index 3c916441a..9b3d8fb73 100644 --- a/ui/component/collectionActions/index.js +++ b/ui/component/collectionActions/index.js @@ -1,10 +1,6 @@ import { connect } from 'react-redux'; -import { - makeSelectClaimForUri, - makeSelectClaimIsPending, - makeSelectCollectionIsMine, - makeSelectEditedCollectionForId, -} from 'lbry-redux'; +import { makeSelectClaimForUri, makeSelectClaimIsPending } from 'redux/selectors/claims'; +import { makeSelectCollectionIsMine, makeSelectEditedCollectionForId } from 'redux/selectors/collections'; import { doOpenModal } from 'redux/actions/app'; import { selectListShuffle } from 'redux/selectors/content'; import { doToggleShuffleList, doToggleLoopList } from 'redux/actions/content'; diff --git a/ui/component/collectionContentSidebar/index.js b/ui/component/collectionContentSidebar/index.js index 694f112cc..e07304052 100644 --- a/ui/component/collectionContentSidebar/index.js +++ b/ui/component/collectionContentSidebar/index.js @@ -1,12 +1,11 @@ import { connect } from 'react-redux'; import CollectionContent from './view'; +import { makeSelectClaimForUri, makeSelectClaimIsMine } from 'redux/selectors/claims'; import { makeSelectUrlsForCollectionId, makeSelectNameForCollectionId, makeSelectCollectionForId, - makeSelectClaimForUri, - makeSelectClaimIsMine, -} from 'lbry-redux'; +} from 'redux/selectors/collections'; import { selectPlayingUri, selectListLoop, selectListShuffle } from 'redux/selectors/content'; import { doToggleLoopList, doToggleShuffleList } from 'redux/actions/content'; diff --git a/ui/component/collectionContentSidebar/view.jsx b/ui/component/collectionContentSidebar/view.jsx index 786c233c2..e21302803 100644 --- a/ui/component/collectionContentSidebar/view.jsx +++ b/ui/component/collectionContentSidebar/view.jsx @@ -4,9 +4,9 @@ import ClaimList from 'component/claimList'; import Card from 'component/common/card'; import Button from 'component/button'; import * as PAGES from 'constants/pages'; +import * as COLLECTIONS_CONSTS from 'constants/collections'; import Icon from 'component/common/icon'; import * as ICONS from 'constants/icons'; -import { COLLECTIONS_CONSTS } from 'lbry-redux'; type Props = { id: string, diff --git a/ui/component/collectionEdit/index.js b/ui/component/collectionEdit/index.js index 3dd9383ea..54cf51ee3 100644 --- a/ui/component/collectionEdit/index.js +++ b/ui/component/collectionEdit/index.js @@ -3,20 +3,21 @@ import { makeSelectTitleForUri, makeSelectThumbnailForUri, makeSelectMetadataItemForUri, - doCollectionPublish, - doCollectionPublishUpdate, makeSelectAmountForUri, makeSelectClaimForUri, selectUpdateCollectionError, selectUpdatingCollection, selectCreateCollectionError, - selectBalance, selectCreatingCollection, +} from 'redux/selectors/claims'; +import { makeSelectCollectionForId, makeSelectUrlsForCollectionId, makeSelectClaimIdsForCollectionId, - ACTIONS as LBRY_REDUX_ACTIONS, -} from 'lbry-redux'; +} from 'redux/selectors/collections'; +import { doCollectionPublish, doCollectionPublishUpdate } from 'redux/actions/claims'; +import { selectBalance } from 'redux/selectors/wallet'; +import * as ACTIONS from 'constants/action_types'; import CollectionForm from './view'; import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app'; @@ -46,7 +47,7 @@ const select = (state, props) => ({ const perform = (dispatch) => ({ publishCollectionUpdate: (params) => dispatch(doCollectionPublishUpdate(params)), publishCollection: (params, collectionId) => dispatch(doCollectionPublish(params, collectionId)), - clearCollectionErrors: () => dispatch({ type: LBRY_REDUX_ACTIONS.CLEAR_COLLECTION_ERRORS }), + clearCollectionErrors: () => dispatch({ type: ACTIONS.CLEAR_COLLECTION_ERRORS }), setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)), setIncognito: (incognito) => dispatch(doSetIncognito(incognito)), }); diff --git a/ui/component/collectionEdit/view.jsx b/ui/component/collectionEdit/view.jsx index 99ccc6ccf..c835caed2 100644 --- a/ui/component/collectionEdit/view.jsx +++ b/ui/component/collectionEdit/view.jsx @@ -12,7 +12,8 @@ import Card from 'component/common/card'; import LbcSymbol from 'component/common/lbc-symbol'; import SelectThumbnail from 'component/selectThumbnail'; import { useHistory } from 'react-router-dom'; -import { isNameValid, regexInvalidURI, THUMBNAIL_STATUSES } from 'lbry-redux'; +import { isNameValid, regexInvalidURI } from 'util/lbryURI'; +import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses'; import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs'; import { FormField } from 'component/common/form'; import { handleBidChange } from 'util/publish'; diff --git a/ui/component/collectionMenuList/index.js b/ui/component/collectionMenuList/index.js index f8e244bb1..320a22650 100644 --- a/ui/component/collectionMenuList/index.js +++ b/ui/component/collectionMenuList/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectNameForCollectionId } from 'lbry-redux'; +import { makeSelectNameForCollectionId } from 'redux/selectors/collections'; import { doOpenModal } from 'redux/actions/app'; import { selectListShuffle } from 'redux/selectors/content'; import { doToggleLoopList, doToggleShuffleList } from 'redux/actions/content'; diff --git a/ui/component/collectionPreviewOverlay/index.js b/ui/component/collectionPreviewOverlay/index.js index bf88345d3..95885e705 100644 --- a/ui/component/collectionPreviewOverlay/index.js +++ b/ui/component/collectionPreviewOverlay/index.js @@ -1,14 +1,12 @@ import { connect } from 'react-redux'; +import { makeSelectIsUriResolving, makeSelectClaimIdForUri, makeSelectClaimForClaimId } from 'redux/selectors/claims'; import { - makeSelectIsUriResolving, - makeSelectClaimIdForUri, - makeSelectClaimForClaimId, makeSelectUrlsForCollectionId, makeSelectNameForCollectionId, makeSelectPendingCollectionForId, makeSelectCountForCollectionId, - doFetchItemsInCollection, -} from 'lbry-redux'; +} from 'redux/selectors/collections'; +import { doFetchItemsInCollection } from 'redux/actions/collections'; import CollectionPreviewOverlay from './view'; const select = (state, props) => { diff --git a/ui/component/collectionPreviewTile/index.js b/ui/component/collectionPreviewTile/index.js index 4bb4ebc15..06f2b5cf1 100644 --- a/ui/component/collectionPreviewTile/index.js +++ b/ui/component/collectionPreviewTile/index.js @@ -1,6 +1,5 @@ import { connect } from 'react-redux'; import { - doResolveUri, makeSelectIsUriResolving, makeSelectThumbnailForUri, makeSelectTitleForUri, @@ -8,15 +7,17 @@ import { makeSelectClaimIsNsfw, makeSelectClaimIdForUri, makeSelectClaimForClaimId, +} from 'redux/selectors/claims'; +import { makeSelectUrlsForCollectionId, makeSelectNameForCollectionId, - doLocalCollectionDelete, - doFetchItemsInCollection, makeSelectEditedCollectionForId, makeSelectPendingCollectionForId, makeSelectCountForCollectionId, makeSelectIsResolvingCollectionForId, -} from 'lbry-redux'; +} from 'redux/selectors/collections'; +import { doFetchItemsInCollection, doCollectionDelete } from 'redux/actions/collections'; +import { doResolveUri } from 'redux/actions/claims'; import { selectMutedChannels } from 'redux/selectors/blocked'; import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc'; import { selectShowMatureContent } from 'redux/selectors/settings'; @@ -52,7 +53,7 @@ const select = (state, props) => { const perform = (dispatch) => ({ resolveUri: (uri) => dispatch(doResolveUri(uri)), resolveCollectionItems: (options) => doFetchItemsInCollection(options), - deleteCollection: (id) => dispatch(doLocalCollectionDelete(id)), + deleteCollection: (id) => dispatch(doCollectionDelete(id)), }); export default connect(select, perform)(CollectionPreviewTile); diff --git a/ui/component/collectionSelectItem/index.js b/ui/component/collectionSelectItem/index.js index 613ef30ce..2e3c3956c 100644 --- a/ui/component/collectionSelectItem/index.js +++ b/ui/component/collectionSelectItem/index.js @@ -1,10 +1,7 @@ import { connect } from 'react-redux'; -import { - doCollectionEdit, - makeSelectCollectionForId, - makeSelectClaimIsPending, - makeSelectCollectionForIdHasClaimUrl, -} from 'lbry-redux'; +import { makeSelectCollectionForId, makeSelectCollectionForIdHasClaimUrl } from 'redux/selectors/collections'; +import { makeSelectClaimIsPending } from 'redux/selectors/claims'; +import { doCollectionEdit } from 'redux/actions/collections'; import CollectionSelectItem from './view'; const select = (state, props) => { diff --git a/ui/component/collectionSelectItem/view.jsx b/ui/component/collectionSelectItem/view.jsx index 267d0f2f3..732a71150 100644 --- a/ui/component/collectionSelectItem/view.jsx +++ b/ui/component/collectionSelectItem/view.jsx @@ -1,9 +1,9 @@ // @flow import * as ICONS from 'constants/icons'; +import * as COLLECTIONS_CONSTS from 'constants/collections'; import React from 'react'; import { FormField } from 'component/common/form'; import Icon from 'component/common/icon'; -import { COLLECTIONS_CONSTS } from 'lbry-redux'; type Props = { collection: Collection, @@ -25,7 +25,10 @@ function CollectionSelectItem(props: Props) { let icon; switch (category) { case 'builtin': - icon = (id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) || (id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || ICONS.STACK; + icon = + (id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) || + (id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || + ICONS.STACK; break; case 'published': icon = ICONS.STACK; diff --git a/ui/component/collectionsListMine/index.js b/ui/component/collectionsListMine/index.js index 50228630b..b229e56b5 100644 --- a/ui/component/collectionsListMine/index.js +++ b/ui/component/collectionsListMine/index.js @@ -4,8 +4,8 @@ import { selectMyPublishedPlaylistCollections, selectMyUnpublishedCollections, // should probably distinguish types // selectSavedCollections, - selectFetchingMyCollections, -} from 'lbry-redux'; +} from 'redux/selectors/collections'; +import { selectFetchingMyCollections } from 'redux/selectors/claims'; import CollectionsListMine from './view'; const select = (state) => ({ diff --git a/ui/component/collectionsListMine/view.jsx b/ui/component/collectionsListMine/view.jsx index 3cbffa9b1..5b8104c49 100644 --- a/ui/component/collectionsListMine/view.jsx +++ b/ui/component/collectionsListMine/view.jsx @@ -3,7 +3,7 @@ import React from 'react'; import CollectionPreviewTile from 'component/collectionPreviewTile'; import ClaimList from 'component/claimList'; import Button from 'component/button'; -import { COLLECTIONS_CONSTS } from 'lbry-redux'; +import * as COLLECTIONS_CONSTS from 'constants/collections'; import Icon from 'component/common/icon'; import * as ICONS from 'constants/icons'; import * as PAGES from 'constants/pages'; diff --git a/ui/component/comment/index.js b/ui/component/comment/index.js index 901b8cd3b..712b0e34b 100644 --- a/ui/component/comment/index.js +++ b/ui/component/comment/index.js @@ -4,7 +4,7 @@ import { makeSelectClaimForUri, makeSelectThumbnailForUri, selectMyChannelClaims, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; import { doCommentUpdate, doCommentList } from 'redux/actions/comments'; import { makeSelectChannelIsMuted } from 'redux/selectors/blocked'; import { doToast } from 'redux/actions/notifications'; diff --git a/ui/component/comment/view.jsx b/ui/component/comment/view.jsx index b4816303a..4fb2c35a9 100644 --- a/ui/component/comment/view.jsx +++ b/ui/component/comment/view.jsx @@ -7,7 +7,7 @@ import { SORT_BY, COMMENT_PAGE_SIZE_REPLIES } from 'constants/comment'; import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field'; import { SITE_NAME, SIMPLE_SITE, ENABLE_COMMENT_REACTIONS } from 'config'; import React, { useEffect, useState } from 'react'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; import DateTime from 'component/dateTime'; import Button from 'component/button'; import Expandable from 'component/expandable'; diff --git a/ui/component/commentCreate/index.js b/ui/component/commentCreate/index.js index 07ea8e876..21d1627dc 100644 --- a/ui/component/commentCreate/index.js +++ b/ui/component/commentCreate/index.js @@ -4,9 +4,8 @@ import { makeSelectClaimIsMine, selectMyChannelClaims, selectFetchingMyChannels, - doSendTip, - makeSelectTagInClaimOrChannelForUri, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { doSendTip } from 'redux/actions/wallet'; import { doCommentCreate, doFetchCreatorSettings, doCommentById } from 'redux/actions/comments'; import { selectActiveChannelClaim } from 'redux/selectors/app'; import { selectSettingsByChannelId } from 'redux/selectors/comments'; diff --git a/ui/component/commentMenuList/index.js b/ui/component/commentMenuList/index.js index 44e87e54a..62ddf4c8b 100644 --- a/ui/component/commentMenuList/index.js +++ b/ui/component/commentMenuList/index.js @@ -4,7 +4,11 @@ import { doCommentPin, doCommentModAddDelegate } from 'redux/actions/comments'; import { doOpenModal } from 'redux/actions/app'; import { doSetPlayingUri } from 'redux/actions/content'; import { doToast } from 'redux/actions/notifications'; -import { makeSelectChannelPermUrlForClaimUri, makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux'; +import { + makeSelectChannelPermUrlForClaimUri, + makeSelectClaimIsMine, + makeSelectClaimForUri, +} from 'redux/selectors/claims'; import { selectActiveChannelClaim } from 'redux/selectors/app'; import { selectModerationDelegatorsById } from 'redux/selectors/comments'; import { selectPlayingUri } from 'redux/selectors/content'; diff --git a/ui/component/commentMenuList/view.jsx b/ui/component/commentMenuList/view.jsx index b938f1b16..da1b7b3b7 100644 --- a/ui/component/commentMenuList/view.jsx +++ b/ui/component/commentMenuList/view.jsx @@ -1,7 +1,7 @@ // @flow import { getChannelFromClaim } from 'util/claim'; import { MenuList, MenuItem } from '@reach/menu-button'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; import { URL } from 'config'; import { useHistory } from 'react-router'; import * as ICONS from 'constants/icons'; diff --git a/ui/component/commentReactions/index.js b/ui/component/commentReactions/index.js index 0bafedfca..9f640d619 100644 --- a/ui/component/commentReactions/index.js +++ b/ui/component/commentReactions/index.js @@ -1,6 +1,7 @@ import { connect } from 'react-redux'; import Comment from './view'; -import { makeSelectClaimIsMine, makeSelectClaimForUri, doResolveUri } from 'lbry-redux'; +import { makeSelectClaimIsMine, makeSelectClaimForUri } from 'redux/selectors/claims'; +import { doResolveUri } from 'redux/actions/claims'; import { doToast } from 'redux/actions/notifications'; import { makeSelectMyReactionsForComment, makeSelectOthersReactionsForComment } from 'redux/selectors/comments'; import { doCommentReact } from 'redux/actions/comments'; diff --git a/ui/component/commentsList/index.js b/ui/component/commentsList/index.js index c83dae73b..d32dc8476 100644 --- a/ui/component/commentsList/index.js +++ b/ui/component/commentsList/index.js @@ -4,7 +4,7 @@ import { makeSelectClaimIsMine, selectFetchingMyChannels, selectMyChannelClaims, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; import { makeSelectTopLevelCommentsForUri, makeSelectTopLevelTotalPagesForUri, diff --git a/ui/component/commentsReplies/index.js b/ui/component/commentsReplies/index.js index 8008d5535..2b4a38c20 100644 --- a/ui/component/commentsReplies/index.js +++ b/ui/component/commentsReplies/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimIsMine, selectMyChannelClaims } from 'lbry-redux'; +import { makeSelectClaimIsMine, selectMyChannelClaims } from 'redux/selectors/claims'; import { selectIsFetchingCommentsByParentId, makeSelectRepliesForParentId } from 'redux/selectors/comments'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; import CommentsReplies from './view'; diff --git a/ui/component/common/credit-amount.jsx b/ui/component/common/credit-amount.jsx index 92adbba4c..99136699e 100644 --- a/ui/component/common/credit-amount.jsx +++ b/ui/component/common/credit-amount.jsx @@ -2,7 +2,7 @@ import React from 'react'; import classnames from 'classnames'; import LbcSymbol from 'component/common/lbc-symbol'; -import { formatCredits, formatFullPrice } from 'lbry-redux'; +import { formatCredits, formatFullPrice } from 'util/format-credits'; type Props = { amount: number, @@ -80,7 +80,7 @@ class CreditAmount extends React.PureComponent { if (showLBC && !isFiat) { amountText = ; } else if (showLBC && isFiat) { - amountText =

${(Math.round(Number(amountText) * 100) / 100).toFixed(2)}

; + amountText =

${(Math.round(Number(amountText) * 100) / 100).toFixed(2)}

; } if (fee) { diff --git a/ui/component/creatorAnalytics/index.js b/ui/component/creatorAnalytics/index.js index 549fcf4c9..7bd5476ee 100644 --- a/ui/component/creatorAnalytics/index.js +++ b/ui/component/creatorAnalytics/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; import CreatorAnalytics from './view'; const select = (state, props) => ({ diff --git a/ui/component/dateTime/index.js b/ui/component/dateTime/index.js index 0dacd1609..367ac4ab1 100644 --- a/ui/component/dateTime/index.js +++ b/ui/component/dateTime/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { makeSelectDateForUri, SETTINGS } from 'lbry-redux'; +import { makeSelectDateForUri } from 'redux/selectors/claims'; +import * as SETTINGS from 'constants/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings'; import DateTime from './view'; diff --git a/ui/component/emailCollection/index.js b/ui/component/emailCollection/index.js index c3a28a4e5..d0a7f9ac3 100644 --- a/ui/component/emailCollection/index.js +++ b/ui/component/emailCollection/index.js @@ -3,15 +3,15 @@ import { doSetClientSetting } from 'redux/actions/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings'; import { selectEmailToVerify, selectUser } from 'redux/selectors/user'; import FirstRunEmailCollection from './view'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; -const select = state => ({ +const select = (state) => ({ emailCollectionAcknowledged: makeSelectClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED)(state), email: selectEmailToVerify(state), user: selectUser(state), }); -const perform = dispatch => () => ({ +const perform = (dispatch) => () => ({ acknowledgeEmail: () => { dispatch(doSetClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, true)); }, diff --git a/ui/component/embedPlayButton/index.js b/ui/component/embedPlayButton/index.js index f85f42388..70847b149 100644 --- a/ui/component/embedPlayButton/index.js +++ b/ui/component/embedPlayButton/index.js @@ -1,5 +1,7 @@ import { connect } from 'react-redux'; -import { makeSelectThumbnailForUri, doResolveUri, makeSelectClaimForUri, SETTINGS } from 'lbry-redux'; +import { makeSelectThumbnailForUri, makeSelectClaimForUri } from 'redux/selectors/claims'; +import { doResolveUri } from 'redux/actions/claims'; +import * as SETTINGS from 'constants/settings'; import { doFetchCostInfoForUri, makeSelectCostInfoForUri } from 'lbryinc'; import { doPlayUri, doSetPlayingUri } from 'redux/actions/content'; import { doAnaltyicsPurchaseEvent } from 'redux/actions/app'; diff --git a/ui/component/errorBoundary/view.jsx b/ui/component/errorBoundary/view.jsx index 6e10fc5bb..4999ee718 100644 --- a/ui/component/errorBoundary/view.jsx +++ b/ui/component/errorBoundary/view.jsx @@ -7,7 +7,7 @@ import { withRouter } from 'react-router'; import analytics from 'analytics'; import I18nMessage from 'component/i18nMessage'; import Native from 'native'; -import { Lbry } from 'lbry-redux'; +import Lbry from 'lbry'; type Props = { children: Node, diff --git a/ui/component/fileActions/index.js b/ui/component/fileActions/index.js index f6e17a4bc..a2d4b8bcd 100644 --- a/ui/component/fileActions/index.js +++ b/ui/component/fileActions/index.js @@ -1,14 +1,13 @@ import { connect } from 'react-redux'; import { makeSelectClaimIsMine, - makeSelectFileInfoForUri, makeSelectClaimForUri, - doPrepareEdit, selectMyChannelClaims, makeSelectClaimIsStreamPlaceholder, makeSelectTagInClaimOrChannelForUri, - makeSelectStreamingUrlForUri, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { makeSelectStreamingUrlForUri, makeSelectFileInfoForUri } from 'redux/selectors/file_info'; +import { doPrepareEdit } from 'redux/actions/publish'; import { DISABLE_COMMENTS_TAG } from 'constants/tags'; import { makeSelectCostInfoForUri } from 'lbryinc'; import { doSetPlayingUri, doPlayUri } from 'redux/actions/content'; diff --git a/ui/component/fileActions/view.jsx b/ui/component/fileActions/view.jsx index 64f6204af..24b68961d 100644 --- a/ui/component/fileActions/view.jsx +++ b/ui/component/fileActions/view.jsx @@ -6,7 +6,8 @@ import * as ICONS from 'constants/icons'; import React from 'react'; import Button from 'component/button'; import FileDownloadLink from 'component/fileDownloadLink'; -import { buildURI, COLLECTIONS_CONSTS } from 'lbry-redux'; +import { buildURI } from 'util/lbryURI'; +import * as COLLECTIONS_CONSTS from 'constants/collections'; import * as RENDER_MODES from 'constants/file_render_modes'; import { useIsMobile } from 'effects/use-screensize'; import ClaimSupportButton from 'component/claimSupportButton'; diff --git a/ui/component/fileDescription/index.js b/ui/component/fileDescription/index.js index c33a812dc..5004b7220 100644 --- a/ui/component/fileDescription/index.js +++ b/ui/component/fileDescription/index.js @@ -3,9 +3,9 @@ import { makeSelectClaimForUri, makeSelectMetadataForUri, makeSelectTagsForUri, - makeSelectPendingAmountByUri, makeSelectClaimIsMine, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { makeSelectPendingAmountByUri } from 'redux/selectors/wallet'; import { doOpenModal } from 'redux/actions/app'; import { selectUser } from 'redux/selectors/user'; import FileDescription from './view'; diff --git a/ui/component/fileDescription/view.jsx b/ui/component/fileDescription/view.jsx index af7e97a63..4b045822e 100644 --- a/ui/component/fileDescription/view.jsx +++ b/ui/component/fileDescription/view.jsx @@ -3,7 +3,7 @@ import * as ICONS from 'constants/icons'; import * as MODALS from 'constants/modal_types'; import React from 'react'; import classnames from 'classnames'; -import { formatCredits } from 'lbry-redux'; +import { formatCredits } from 'util/format-credits'; import MarkdownPreview from 'component/common/markdown-preview'; import ClaimTags from 'component/claimTags'; import Button from 'component/button'; diff --git a/ui/component/fileDetails/index.js b/ui/component/fileDetails/index.js index c0d15cbc5..8bff1455d 100644 --- a/ui/component/fileDetails/index.js +++ b/ui/component/fileDetails/index.js @@ -1,10 +1,6 @@ import { connect } from 'react-redux'; -import { - makeSelectClaimForUri, - makeSelectContentTypeForUri, - makeSelectMetadataForUri, - makeSelectFileInfoForUri, -} from 'lbry-redux'; +import { makeSelectClaimForUri, makeSelectContentTypeForUri, makeSelectMetadataForUri } from 'redux/selectors/claims'; +import { makeSelectFileInfoForUri } from 'redux/selectors/file_info'; import { selectUser } from 'redux/selectors/user'; import { doOpenFileInFolder } from 'redux/actions/file'; import FileDetails from './view'; @@ -17,8 +13,8 @@ const select = (state, props) => ({ user: selectUser(state), }); -const perform = dispatch => ({ - openFolder: path => dispatch(doOpenFileInFolder(path)), +const perform = (dispatch) => ({ + openFolder: (path) => dispatch(doOpenFileInFolder(path)), }); export default connect(select, perform)(FileDetails); diff --git a/ui/component/fileDownloadLink/index.js b/ui/component/fileDownloadLink/index.js index 7f635b544..41e32d044 100644 --- a/ui/component/fileDownloadLink/index.js +++ b/ui/component/fileDownloadLink/index.js @@ -1,13 +1,11 @@ import { connect } from 'react-redux'; +import { makeSelectClaimIsMine, makeSelectClaimForUri, makeSelectClaimWasPurchased } from 'redux/selectors/claims'; import { makeSelectFileInfoForUri, makeSelectDownloadingForUri, makeSelectLoadingForUri, - makeSelectClaimIsMine, - makeSelectClaimForUri, - makeSelectClaimWasPurchased, makeSelectStreamingUrlForUri, -} from 'lbry-redux'; +} from 'redux/selectors/file_info'; import { makeSelectCostInfoForUri } from 'lbryinc'; import { doOpenModal, doAnalyticsView } from 'redux/actions/app'; import { doSetPlayingUri, doPlayUri } from 'redux/actions/content'; @@ -24,10 +22,10 @@ const select = (state, props) => ({ streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ openModal: (modal, props) => dispatch(doOpenModal(modal, props)), pause: () => dispatch(doSetPlayingUri({ uri: null })), - download: uri => dispatch(doPlayUri(uri, false, true, () => dispatch(doAnalyticsView(uri)))), + download: (uri) => dispatch(doPlayUri(uri, false, true, () => dispatch(doAnalyticsView(uri)))), }); export default connect(select, perform)(FileDownloadLink); diff --git a/ui/component/fileDrop/index.js b/ui/component/fileDrop/index.js index 9a75644b8..97a182ff5 100644 --- a/ui/component/fileDrop/index.js +++ b/ui/component/fileDrop/index.js @@ -1,20 +1,21 @@ import { connect } from 'react-redux'; -import { doUpdatePublishForm, makeSelectPublishFormValue } from 'lbry-redux'; +import { doUpdatePublishForm } from 'redux/actions/publish'; +import { makeSelectPublishFormValue } from 'redux/selectors/publish'; import { selectModal } from 'redux/selectors/app'; import { doOpenModal } from 'redux/actions/app'; import FileDrop from './view'; -const select = state => ({ +const select = (state) => ({ modal: selectModal(state), filePath: makeSelectPublishFormValue('filePath')(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ openModal: (modal, props) => dispatch(doOpenModal(modal, props)), - updatePublishForm: value => dispatch(doUpdatePublishForm(value)), + updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)), }); export default connect(select, perform)(FileDrop); diff --git a/ui/component/filePrice/index.js b/ui/component/filePrice/index.js index 5bfa30df5..d795184c5 100644 --- a/ui/component/filePrice/index.js +++ b/ui/component/filePrice/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri, makeSelectClaimWasPurchased, makeSelectClaimIsMine } from 'lbry-redux'; +import { makeSelectClaimForUri, makeSelectClaimWasPurchased, makeSelectClaimIsMine } from 'redux/selectors/claims'; import { makeSelectCostInfoForUri, doFetchCostInfoForUri, makeSelectFetchingCostInfoForUri } from 'lbryinc'; import FilePrice from './view'; @@ -11,8 +11,8 @@ const select = (state, props) => ({ claimIsMine: makeSelectClaimIsMine(props.uri)(state), }); -const perform = dispatch => ({ - fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)), +const perform = (dispatch) => ({ + fetchCostInfo: (uri) => dispatch(doFetchCostInfoForUri(uri)), }); export default connect(select, perform)(FilePrice); diff --git a/ui/component/fileReactions/index.js b/ui/component/fileReactions/index.js index 58b2bf15c..4d5c6d8c3 100644 --- a/ui/component/fileReactions/index.js +++ b/ui/component/fileReactions/index.js @@ -8,7 +8,7 @@ import { import { doFetchReactions, doReactionLike, doReactionDislike } from 'redux/actions/reactions'; import { selectThemePath } from 'redux/selectors/settings'; import FileViewCount from './view'; -import { makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; const select = (state, props) => ({ claim: makeSelectClaimForUri(props.uri)(state), diff --git a/ui/component/fileRender/index.js b/ui/component/fileRender/index.js index b25f7f9e6..83a29abc0 100644 --- a/ui/component/fileRender/index.js +++ b/ui/component/fileRender/index.js @@ -1,12 +1,7 @@ import { connect } from 'react-redux'; -import { - makeSelectClaimForUri, - makeSelectThumbnailForUri, - makeSelectContentTypeForUri, - makeSelectDownloadPathForUri, - makeSelectStreamingUrlForUri, - SETTINGS, -} from 'lbry-redux'; +import { makeSelectDownloadPathForUri, makeSelectStreamingUrlForUri } from 'redux/selectors/file_info'; +import { makeSelectClaimForUri, makeSelectThumbnailForUri, makeSelectContentTypeForUri } from 'redux/selectors/claims'; +import * as SETTINGS from 'constants/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectFileRenderModeForUri, makeSelectFileExtensionForUri } from 'redux/selectors/content'; import FileRender from './view'; diff --git a/ui/component/fileRenderFloating/index.js b/ui/component/fileRenderFloating/index.js index b1b491a9d..be0e6d4dd 100644 --- a/ui/component/fileRenderFloating/index.js +++ b/ui/component/fileRenderFloating/index.js @@ -1,14 +1,11 @@ import { connect } from 'react-redux'; +import { makeSelectTitleForUri, makeSelectClaimIsNsfw, makeSelectClaimWasPurchased } from 'redux/selectors/claims'; +import { makeSelectFileInfoForUri, makeSelectStreamingUrlForUri } from 'redux/selectors/file_info'; import { - makeSelectFileInfoForUri, - makeSelectTitleForUri, - makeSelectStreamingUrlForUri, - makeSelectClaimIsNsfw, - makeSelectClaimWasPurchased, makeSelectNextUrlForCollectionAndUrl, makeSelectPreviousUrlForCollectionAndUrl, - SETTINGS, -} from 'lbry-redux'; +} from 'redux/selectors/collections'; +import * as SETTINGS from 'constants/settings'; import { makeSelectIsPlayerFloating, selectPrimaryUri, diff --git a/ui/component/fileRenderFloating/view.jsx b/ui/component/fileRenderFloating/view.jsx index 4c94fea70..0ff49a17a 100644 --- a/ui/component/fileRenderFloating/view.jsx +++ b/ui/component/fileRenderFloating/view.jsx @@ -15,7 +15,7 @@ import { generateListSearchUrlParams, formatLbryUrlForWeb } from 'util/url'; import { useIsMobile } from 'effects/use-screensize'; import debounce from 'util/debounce'; import { useHistory } from 'react-router'; -import { isURIEqual } from 'lbry-redux'; +import { isURIEqual } from 'util/lbryURI'; import AutoplayCountdown from 'component/autoplayCountdown'; const IS_DESKTOP_MAC = typeof process === 'object' ? process.platform === 'darwin' : false; diff --git a/ui/component/fileRenderInitiator/index.js b/ui/component/fileRenderInitiator/index.js index 2a9765ce1..178ccb403 100644 --- a/ui/component/fileRenderInitiator/index.js +++ b/ui/component/fileRenderInitiator/index.js @@ -1,13 +1,9 @@ import { connect } from 'react-redux'; import { doPlayUri, doSetPlayingUri, doSetPrimaryUri } from 'redux/actions/content'; -import { - makeSelectFileInfoForUri, - makeSelectThumbnailForUri, - makeSelectClaimForUri, - makeSelectClaimWasPurchased, - SETTINGS, - COLLECTIONS_CONSTS, -} from 'lbry-redux'; +import { makeSelectThumbnailForUri, makeSelectClaimForUri, makeSelectClaimWasPurchased } from 'redux/selectors/claims'; +import { makeSelectFileInfoForUri } from 'redux/selectors/file_info'; +import * as SETTINGS from 'constants/settings'; +import * as COLLECTIONS_CONSTS from 'constants/collections'; import { makeSelectCostInfoForUri } from 'lbryinc'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { makeSelectClientSetting } from 'redux/selectors/settings'; diff --git a/ui/component/fileRenderInline/index.js b/ui/component/fileRenderInline/index.js index 83b1f4b53..f5e7a0db1 100644 --- a/ui/component/fileRenderInline/index.js +++ b/ui/component/fileRenderInline/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { makeSelectFileInfoForUri, makeSelectStreamingUrlForUri, makeSelectClaimWasPurchased } from 'lbry-redux'; +import { makeSelectFileInfoForUri, makeSelectStreamingUrlForUri } from 'redux/selectors/file_info'; +import { makeSelectClaimWasPurchased } from 'redux/selectors/claims'; import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards'; import { makeSelectFileRenderModeForUri, selectPrimaryUri } from 'redux/selectors/content'; import { withRouter } from 'react-router'; diff --git a/ui/component/fileThumbnail/index.js b/ui/component/fileThumbnail/index.js index a5a169060..01dfe7bc2 100644 --- a/ui/component/fileThumbnail/index.js +++ b/ui/component/fileThumbnail/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { doResolveUri, makeSelectClaimForUri } from 'lbry-redux'; +import { doResolveUri } from 'redux/actions/claims'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; import CardMedia from './view'; const select = (state, props) => ({ diff --git a/ui/component/fileTitle/index.js b/ui/component/fileTitle/index.js index d89eea60a..f3644c754 100644 --- a/ui/component/fileTitle/index.js +++ b/ui/component/fileTitle/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectTitleForUri } from 'lbry-redux'; +import { makeSelectTitleForUri } from 'redux/selectors/claims'; import FileTitleSection from './view'; const select = (state, props) => ({ diff --git a/ui/component/fileTitleSection/index.js b/ui/component/fileTitleSection/index.js index 7af998148..fa5be5a0a 100644 --- a/ui/component/fileTitleSection/index.js +++ b/ui/component/fileTitleSection/index.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import { doFetchSubCount, makeSelectSubCountForUri } from 'lbryinc'; -import { makeSelectTitleForUri, makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectTitleForUri, makeSelectClaimForUri } from 'redux/selectors/claims'; import { makeSelectInsufficientCreditsForUri } from 'redux/selectors/content'; import { makeSelectViewersForId } from 'redux/selectors/livestream'; import FileTitleSection from './view'; diff --git a/ui/component/fileTitleSection/view.jsx b/ui/component/fileTitleSection/view.jsx index 3b641a133..a6db551ee 100644 --- a/ui/component/fileTitleSection/view.jsx +++ b/ui/component/fileTitleSection/view.jsx @@ -1,6 +1,6 @@ // @flow import * as React from 'react'; -import { normalizeURI } from 'lbry-redux'; +import { normalizeURI } from 'util/lbryURI'; import FilePrice from 'component/filePrice'; import ClaimInsufficientCredits from 'component/claimInsufficientCredits'; import FileSubtitle from 'component/fileSubtitle'; diff --git a/ui/component/fileType/index.js b/ui/component/fileType/index.js index f67ced53d..67e40411b 100644 --- a/ui/component/fileType/index.js +++ b/ui/component/fileType/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { makeSelectMediaTypeForUri, makeSelectClaimIsStreamPlaceholder } from 'lbry-redux'; +import { makeSelectMediaTypeForUri } from 'redux/selectors/file_info'; +import { makeSelectClaimIsStreamPlaceholder } from 'redux/selectors/claims'; import FileType from './view'; const select = (state, props) => ({ diff --git a/ui/component/fileValues/index.js b/ui/component/fileValues/index.js index 24658d85d..8fe82ba56 100644 --- a/ui/component/fileValues/index.js +++ b/ui/component/fileValues/index.js @@ -3,10 +3,10 @@ import { makeSelectClaimForUri, makeSelectContentTypeForUri, makeSelectMetadataForUri, - makeSelectFileInfoForUri, - makeSelectPendingAmountByUri, makeSelectClaimIsMine, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { makeSelectPendingAmountByUri } from 'redux/selectors/wallet'; +import { makeSelectFileInfoForUri } from 'redux/selectors/file_info'; import { selectUser } from 'redux/selectors/user'; import { doOpenModal } from 'redux/actions/app'; @@ -22,7 +22,7 @@ const select = (state, props) => ({ claimIsMine: makeSelectClaimIsMine(props.uri)(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ openModal: (modal, props) => dispatch(doOpenModal(modal, props)), }); diff --git a/ui/component/fileViewCount/index.js b/ui/component/fileViewCount/index.js index d2c7af7eb..e4c5e64d5 100644 --- a/ui/component/fileViewCount/index.js +++ b/ui/component/fileViewCount/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; import { doFetchViewCount, makeSelectViewCountForUri } from 'lbryinc'; import { doAnalyticsView } from 'redux/actions/app'; import FileViewCount from './view'; diff --git a/ui/component/fileViewCountInline/index.js b/ui/component/fileViewCountInline/index.js index 4481f9eed..0f9f2c1e6 100644 --- a/ui/component/fileViewCountInline/index.js +++ b/ui/component/fileViewCountInline/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; import { makeSelectViewCountForUri } from 'lbryinc'; import { selectLanguage } from 'redux/selectors/settings'; import FileViewCountInline from './view'; diff --git a/ui/component/fileViewerEmbeddedTitle/index.js b/ui/component/fileViewerEmbeddedTitle/index.js index a4b0b43e1..4e7dda364 100644 --- a/ui/component/fileViewerEmbeddedTitle/index.js +++ b/ui/component/fileViewerEmbeddedTitle/index.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import fileViewerEmbeddedTitle from './view'; -import { makeSelectTagInClaimOrChannelForUri, makeSelectTitleForUri } from 'lbry-redux'; +import { makeSelectTagInClaimOrChannelForUri, makeSelectTitleForUri } from 'redux/selectors/claims'; import { PREFERENCE_EMBED } from 'constants/tags'; export default connect((state, props) => { diff --git a/ui/component/fileWatchLaterLink/index.js b/ui/component/fileWatchLaterLink/index.js index 107ac4eaf..b3f5cc064 100644 --- a/ui/component/fileWatchLaterLink/index.js +++ b/ui/component/fileWatchLaterLink/index.js @@ -1,10 +1,8 @@ import { connect } from 'react-redux'; -import { - makeSelectClaimForUri, - COLLECTIONS_CONSTS, - makeSelectCollectionForIdHasClaimUrl, - doCollectionEdit, -} from 'lbry-redux'; +import { doCollectionEdit } from 'redux/actions/collections'; +import { makeSelectCollectionForIdHasClaimUrl } from 'redux/selectors/collections'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; +import * as COLLECTIONS_CONSTS from 'constants/collections'; import FileWatchLaterLink from './view'; import { doToast } from 'redux/actions/notifications'; @@ -18,7 +16,7 @@ const select = (state, props) => { }; }; -const perform = dispatch => ({ +const perform = (dispatch) => ({ doToast: (props) => dispatch(doToast(props)), doCollectionEdit: (collection, props) => dispatch(doCollectionEdit(collection, props)), }); diff --git a/ui/component/fileWatchLaterLink/view.jsx b/ui/component/fileWatchLaterLink/view.jsx index ad6d748dd..3dd838b77 100644 --- a/ui/component/fileWatchLaterLink/view.jsx +++ b/ui/component/fileWatchLaterLink/view.jsx @@ -3,7 +3,7 @@ import * as ICONS from 'constants/icons'; import React, { useRef } from 'react'; import Button from 'component/button'; import useHover from 'effects/use-hover'; -import { COLLECTIONS_CONSTS } from 'lbry-redux'; +import * as COLLECTIONS_CONSTS from 'constants/collections'; type Props = { uri: string, diff --git a/ui/component/header/index.js b/ui/component/header/index.js index 2356298e3..a7c7bf21e 100644 --- a/ui/component/header/index.js +++ b/ui/component/header/index.js @@ -1,6 +1,8 @@ import * as MODALS from 'constants/modal_types'; +import * as SETTINGS from 'constants/settings'; import { connect } from 'react-redux'; -import { selectTotalBalance, selectBalance, formatCredits, SETTINGS } from 'lbry-redux'; +import { selectTotalBalance, selectBalance } from 'redux/selectors/wallet'; +import { formatCredits } from 'util/format-credits'; import { selectGetSyncErrorMessage } from 'redux/selectors/sync'; import { selectUserVerifiedEmail, selectUserEmail, selectEmailToVerify, selectUser } from 'redux/selectors/user'; import { doClearEmailEntry, doClearPasswordEntry } from 'redux/actions/user'; diff --git a/ui/component/header/view.jsx b/ui/component/header/view.jsx index 09a4753c6..a94703bba 100644 --- a/ui/component/header/view.jsx +++ b/ui/component/header/view.jsx @@ -1,7 +1,7 @@ // @flow import { ENABLE_NO_SOURCE_CLAIMS, CHANNEL_STAKED_LEVEL_LIVESTREAM, ENABLE_UI_NOTIFICATIONS } from 'config'; import * as ICONS from 'constants/icons'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; import * as PAGES from 'constants/pages'; import React from 'react'; import { withRouter } from 'react-router'; diff --git a/ui/component/hiddenNsfwClaims/index.js b/ui/component/hiddenNsfwClaims/index.js index 8121bd0a2..f0355848a 100644 --- a/ui/component/hiddenNsfwClaims/index.js +++ b/ui/component/hiddenNsfwClaims/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { makeSelectNsfwCountFromUris, makeSelectOmittedCountForChannel, parseURI } from 'lbry-redux'; +import { makeSelectNsfwCountFromUris, makeSelectOmittedCountForChannel } from 'redux/selectors/claims'; +import { parseURI } from 'util/lbryURI'; import { selectShowMatureContent } from 'redux/selectors/settings'; import HiddenNsfwClaims from './view'; @@ -24,7 +25,4 @@ const select = (state, props) => { const perform = () => ({}); -export default connect( - select, - perform -)(HiddenNsfwClaims); +export default connect(select, perform)(HiddenNsfwClaims); diff --git a/ui/component/inviteNew/index.js b/ui/component/inviteNew/index.js index 274ecbb32..c5b72df8b 100644 --- a/ui/component/inviteNew/index.js +++ b/ui/component/inviteNew/index.js @@ -7,7 +7,8 @@ import { selectUserInviteReferralCode, } from 'redux/selectors/user'; import { doUserInviteNew } from 'redux/actions/user'; -import { selectMyChannelClaims, selectFetchingMyChannels, doFetchChannelListMine } from 'lbry-redux'; +import { selectMyChannelClaims, selectFetchingMyChannels } from 'redux/selectors/claims'; +import { doFetchChannelListMine } from 'redux/actions/claims'; import InviteNew from './view'; const select = (state) => ({ diff --git a/ui/component/invited/view.jsx b/ui/component/invited/view.jsx index 09916455c..c6ff3403f 100644 --- a/ui/component/invited/view.jsx +++ b/ui/component/invited/view.jsx @@ -5,7 +5,7 @@ import React, { useEffect } from 'react'; import Button from 'component/button'; import ClaimPreview from 'component/claimPreview'; import Card from 'component/common/card'; -import { buildURI, parseURI } from 'lbry-redux'; +import { buildURI, parseURI } from 'util/lbryURI'; import { ERRORS } from 'lbryinc'; import REWARDS from 'rewards'; import { formatLbryUrlForWeb } from 'util/url'; @@ -15,11 +15,11 @@ import I18nMessage from 'component/i18nMessage'; type Props = { user: any, claimReward: () => void, - setReferrer: string => void, + setReferrer: (string) => void, referrerSetPending: boolean, referrerSetError: string, channelSubscribe: (sub: Subscription) => void, - history: { push: string => void }, + history: { push: (string) => void }, rewards: Array, referrer: string, fullUri: string, @@ -52,7 +52,7 @@ function Invited(props: Props) { formatLbryUrlForWeb(buildURI({ channelName: referrerChannelName, channelClaimId: referrerChannelClaimId })); const rewardsApproved = user && user.is_reward_approved; const hasVerifiedEmail = user && user.has_verified_email; - const referredRewardAvailable = rewards && rewards.some(reward => reward.reward_type === REWARDS.TYPE_REFEREE); + const referredRewardAvailable = rewards && rewards.some((reward) => reward.reward_type === REWARDS.TYPE_REFEREE); const redirect = channelUri || `/`; // always follow if it's a channel diff --git a/ui/component/livestreamComment/index.js b/ui/component/livestreamComment/index.js index 8f6527467..8c6a97de5 100644 --- a/ui/component/livestreamComment/index.js +++ b/ui/component/livestreamComment/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectStakedLevelForChannelUri, makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectStakedLevelForChannelUri, makeSelectClaimForUri } from 'redux/selectors/claims'; import LivestreamComment from './view'; const select = (state, props) => ({ diff --git a/ui/component/livestreamComment/view.jsx b/ui/component/livestreamComment/view.jsx index cefdfc8ae..49dd26f95 100644 --- a/ui/component/livestreamComment/view.jsx +++ b/ui/component/livestreamComment/view.jsx @@ -1,7 +1,7 @@ // @flow import * as ICONS from 'constants/icons'; import React from 'react'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; import MarkdownPreview from 'component/common/markdown-preview'; import Tooltip from 'component/common/tooltip'; import ChannelThumbnail from 'component/channelThumbnail'; diff --git a/ui/component/livestreamComments/index.js b/ui/component/livestreamComments/index.js index 10ee56c06..f68fc3372 100644 --- a/ui/component/livestreamComments/index.js +++ b/ui/component/livestreamComments/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri, selectMyChannelClaims } from 'lbry-redux'; +import { makeSelectClaimForUri, selectMyChannelClaims } from 'redux/selectors/claims'; import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket'; import { doCommentList, doSuperChatList } from 'redux/actions/comments'; import { diff --git a/ui/component/livestreamLayout/index.js b/ui/component/livestreamLayout/index.js index 30063aa84..03547a34d 100644 --- a/ui/component/livestreamLayout/index.js +++ b/ui/component/livestreamLayout/index.js @@ -1,5 +1,9 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri, makeSelectTagInClaimOrChannelForUri, makeSelectThumbnailForUri } from 'lbry-redux'; +import { + makeSelectClaimForUri, + makeSelectTagInClaimOrChannelForUri, + makeSelectThumbnailForUri, +} from 'redux/selectors/claims'; import LivestreamLayout from './view'; import { DISABLE_COMMENTS_TAG } from 'constants/tags'; diff --git a/ui/component/livestreamLink/index.js b/ui/component/livestreamLink/index.js index 4bc23a4f0..efc44ccaa 100644 --- a/ui/component/livestreamLink/index.js +++ b/ui/component/livestreamLink/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; import LivestreamLink from './view'; const select = (state, props) => ({ diff --git a/ui/component/livestreamLink/view.jsx b/ui/component/livestreamLink/view.jsx index ed411e899..23243cb5a 100644 --- a/ui/component/livestreamLink/view.jsx +++ b/ui/component/livestreamLink/view.jsx @@ -4,7 +4,7 @@ import * as CS from 'constants/claim_search'; import React from 'react'; import Card from 'component/common/card'; import ClaimPreview from 'component/claimPreview'; -import { Lbry } from 'lbry-redux'; +import Lbry from 'lbry'; import { useHistory } from 'react-router'; import { formatLbryUrlForWeb } from 'util/url'; @@ -38,6 +38,7 @@ export default function LivestreamLink(props: Props) { .then((res) => { if (res && res.items && res.items.length > 0) { const claim = res.items[0]; + // $FlowFixMe Too many Claim GenericClaim etc types. setLivestreamClaim(claim); } }) diff --git a/ui/component/logo/index.js b/ui/component/logo/index.js index 6d0f96f4b..327c06886 100644 --- a/ui/component/logo/index.js +++ b/ui/component/logo/index.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import Logo from './view'; import { makeSelectClientSetting } from 'redux/selectors/settings'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; const select = (state, props) => ({ currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state), diff --git a/ui/component/markdownLink/view.jsx b/ui/component/markdownLink/view.jsx index 222af7a1e..d2d7ab6fd 100644 --- a/ui/component/markdownLink/view.jsx +++ b/ui/component/markdownLink/view.jsx @@ -2,7 +2,7 @@ import { KNOWN_APP_DOMAINS } from 'config'; import * as ICONS from 'constants/icons'; import * as React from 'react'; -import { isURIValid } from 'lbry-redux'; +import { isURIValid } from 'util/lbryURI'; import Button from 'component/button'; import ClaimLink from 'component/claimLink'; diff --git a/ui/component/nagContinueFirstRun/index.js b/ui/component/nagContinueFirstRun/index.js index 66f41ab2a..95dda62a3 100644 --- a/ui/component/nagContinueFirstRun/index.js +++ b/ui/component/nagContinueFirstRun/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings'; import NagContinueFirstRun from './view'; diff --git a/ui/component/navigationHistoryItem/index.js b/ui/component/navigationHistoryItem/index.js index a2a00ac58..1e8be0c31 100644 --- a/ui/component/navigationHistoryItem/index.js +++ b/ui/component/navigationHistoryItem/index.js @@ -1,16 +1,14 @@ import { connect } from 'react-redux'; -import { doResolveUri, makeSelectClaimForUri } from 'lbry-redux'; +import { doResolveUri } from 'redux/actions/claims'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; import UserHistoryItem from './view'; const select = (state, props) => ({ claim: makeSelectClaimForUri(props.uri)(state), }); -const perform = dispatch => ({ - resolveUri: uri => dispatch(doResolveUri(uri)), +const perform = (dispatch) => ({ + resolveUri: (uri) => dispatch(doResolveUri(uri)), }); -export default connect( - select, - perform -)(UserHistoryItem); +export default connect(select, perform)(UserHistoryItem); diff --git a/ui/component/notification/view.jsx b/ui/component/notification/view.jsx index 996978442..cab730c3f 100644 --- a/ui/component/notification/view.jsx +++ b/ui/component/notification/view.jsx @@ -10,7 +10,7 @@ import Button from 'component/button'; import ChannelThumbnail from 'component/channelThumbnail'; import { formatLbryUrlForWeb } from 'util/url'; import { useHistory } from 'react-router'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; import { PAGE_VIEW_QUERY, DISCUSSION_PAGE } from 'page/channel/view'; import FileThumbnail from 'component/fileThumbnail'; import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button'; diff --git a/ui/component/notificationContentChannelMenu/view.jsx b/ui/component/notificationContentChannelMenu/view.jsx index 5d7803345..de4484571 100644 --- a/ui/component/notificationContentChannelMenu/view.jsx +++ b/ui/component/notificationContentChannelMenu/view.jsx @@ -2,14 +2,14 @@ import * as ICONS from 'constants/icons'; import React from 'react'; import { MenuItem } from '@reach/menu-button'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; import Icon from 'component/common/icon'; type Props = { uri: string, notificationsDisabled: boolean, doToast: ({ message: string }) => void, - doChannelSubscribe: Subscription => void, + doChannelSubscribe: (Subscription) => void, }; export default function NotificationContentChannelMenu(props: Props) { diff --git a/ui/component/page/index.js b/ui/component/page/index.js index cccdf3123..8e5d6b846 100644 --- a/ui/component/page/index.js +++ b/ui/component/page/index.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import { makeSelectClientSetting } from 'redux/selectors/settings'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; import Page from './view'; const select = (state, props) => ({ diff --git a/ui/component/page/view.jsx b/ui/component/page/view.jsx index 6d288c3bc..4b35789b8 100644 --- a/ui/component/page/view.jsx +++ b/ui/component/page/view.jsx @@ -12,7 +12,7 @@ import StatusBar from 'component/common/status-bar'; import usePersistedState from 'effects/use-persisted-state'; import { useHistory } from 'react-router'; import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; const Footer = lazyImport(() => import('web/component/footer' /* webpackChunkName: "secondary" */)); diff --git a/ui/component/postEditor/index.js b/ui/component/postEditor/index.js index dfb1ea2b1..0d7f64103 100644 --- a/ui/component/postEditor/index.js +++ b/ui/component/postEditor/index.js @@ -1,10 +1,7 @@ import { connect } from 'react-redux'; -import { - selectIsStillEditing, - makeSelectPublishFormValue, - doUpdatePublishForm, - makeSelectStreamingUrlForUri, -} from 'lbry-redux'; +import { doUpdatePublishForm } from 'redux/actions/publish'; +import { selectIsStillEditing, makeSelectPublishFormValue } from 'redux/selectors/publish'; +import { makeSelectStreamingUrlForUri } from 'redux/selectors/file_info'; import { doPlayUri } from 'redux/actions/content'; import PostEditor from './view'; @@ -15,9 +12,9 @@ const select = (state, props) => ({ isStillEditing: selectIsStillEditing(state), }); -const perform = dispatch => ({ - updatePublishForm: value => dispatch(doUpdatePublishForm(value)), - fetchStreamingUrl: uri => dispatch(doPlayUri(uri)), +const perform = (dispatch) => ({ + updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)), + fetchStreamingUrl: (uri) => dispatch(doPlayUri(uri)), }); export default connect(select, perform)(PostEditor); diff --git a/ui/component/postViewer/index.js b/ui/component/postViewer/index.js index fd706066f..1647988da 100644 --- a/ui/component/postViewer/index.js +++ b/ui/component/postViewer/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri, makeSelectClaimIsMine } from 'lbry-redux'; +import { makeSelectClaimForUri, makeSelectClaimIsMine } from 'redux/selectors/claims'; import PostViewer from './view'; import { doOpenModal } from 'redux/actions/app'; diff --git a/ui/component/postViewer/view.jsx b/ui/component/postViewer/view.jsx index 02e280fb3..84eb408b5 100644 --- a/ui/component/postViewer/view.jsx +++ b/ui/component/postViewer/view.jsx @@ -2,7 +2,7 @@ import * as React from 'react'; import * as ICONS from 'constants/icons'; import * as MODALS from 'constants/modal_types'; -import { formatCredits } from 'lbry-redux'; +import { formatCredits } from 'util/format-credits'; import FileDetails from 'component/fileDetails'; import ClaimAuthor from 'component/claimAuthor'; import FileTitle from 'component/fileTitle'; diff --git a/ui/component/previewLink/index.js b/ui/component/previewLink/index.js index b8c4cb8d9..17493a0ed 100644 --- a/ui/component/previewLink/index.js +++ b/ui/component/previewLink/index.js @@ -1,13 +1,13 @@ import { connect } from 'react-redux'; import { - doResolveUri, makeSelectClaimIsMine, makeSelectTitleForUri, makeSelectThumbnailForUri, makeSelectClaimForUri, makeSelectIsUriResolving, makeSelectMetadataItemForUri, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { doResolveUri } from 'redux/actions/claims'; import { selectBlackListedOutpoints } from 'lbryinc'; import PreviewLink from './view'; @@ -24,8 +24,8 @@ const select = (state, props) => { }; }; -const perform = dispatch => ({ - resolveUri: uri => dispatch(doResolveUri(uri)), +const perform = (dispatch) => ({ + resolveUri: (uri) => dispatch(doResolveUri(uri)), }); export default connect(select, perform)(PreviewLink); diff --git a/ui/component/previewLink/view.jsx b/ui/component/previewLink/view.jsx index d2f293b05..78f3faf68 100644 --- a/ui/component/previewLink/view.jsx +++ b/ui/component/previewLink/view.jsx @@ -5,7 +5,7 @@ import TruncatedText from 'component/common/truncated-text'; import MarkdownPreview from 'component/common/markdown-preview'; import { withRouter } from 'react-router-dom'; import { formatLbryUrlForWeb } from 'util/url'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; import classnames from 'classnames'; type Props = { diff --git a/ui/component/previewOverlayProperties/index.js b/ui/component/previewOverlayProperties/index.js index 195602884..622fb642b 100644 --- a/ui/component/previewOverlayProperties/index.js +++ b/ui/component/previewOverlayProperties/index.js @@ -1,10 +1,7 @@ import { connect } from 'react-redux'; -import { - makeSelectFilePartlyDownloaded, - makeSelectClaimIsMine, - makeSelectClaimForUri, - makeSelectEditedCollectionForId, -} from 'lbry-redux'; +import { makeSelectClaimIsMine, makeSelectClaimForUri } from 'redux/selectors/claims'; +import { makeSelectFilePartlyDownloaded } from 'redux/selectors/file_info'; +import { makeSelectEditedCollectionForId } from 'redux/selectors/collections'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; import PreviewOverlayProperties from './view'; diff --git a/ui/component/privacyAgreement/index.js b/ui/component/privacyAgreement/index.js index 5a1032a09..f7b4e3082 100644 --- a/ui/component/privacyAgreement/index.js +++ b/ui/component/privacyAgreement/index.js @@ -2,7 +2,7 @@ import { DOMAIN } from 'config'; import { connect } from 'react-redux'; import { doSetDaemonSetting } from 'redux/actions/settings'; import { doSetWelcomeVersion, doToggle3PAnalytics, doSignOut } from 'redux/actions/app'; -import { DAEMON_SETTINGS } from 'lbry-redux'; +import * as DAEMON_SETTINGS from 'constants/daemon_settings'; import { WELCOME_VERSION } from 'config.js'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { doAuthenticate } from 'redux/actions/user'; @@ -10,14 +10,14 @@ import { version as appVersion } from 'package.json'; import PrivacyAgreement from './view'; -const select = state => ({ +const select = (state) => ({ authenticated: selectUserVerifiedEmail(state), }); -const perform = dispatch => ({ - setWelcomeVersion: version => dispatch(doSetWelcomeVersion(version || WELCOME_VERSION)), - setShareDataInternal: share => dispatch(doSetDaemonSetting(DAEMON_SETTINGS.SHARE_USAGE_DATA, share)), - setShareDataThirdParty: share => dispatch(doToggle3PAnalytics(share)), +const perform = (dispatch) => ({ + setWelcomeVersion: (version) => dispatch(doSetWelcomeVersion(version || WELCOME_VERSION)), + setShareDataInternal: (share) => dispatch(doSetDaemonSetting(DAEMON_SETTINGS.SHARE_USAGE_DATA, share)), + setShareDataThirdParty: (share) => dispatch(doToggle3PAnalytics(share)), signOut: () => dispatch(doSignOut()), authenticateIfSharingData: () => dispatch(doAuthenticate(appVersion, undefined, undefined, true, undefined, undefined, DOMAIN)), diff --git a/ui/component/publishAdditionalOptions/index.js b/ui/component/publishAdditionalOptions/index.js index 8a5a4f826..f0c188e25 100644 --- a/ui/component/publishAdditionalOptions/index.js +++ b/ui/component/publishAdditionalOptions/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { selectPublishFormValues, doUpdatePublishForm } from 'lbry-redux'; +import { selectPublishFormValues } from 'redux/selectors/publish'; +import { doUpdatePublishForm } from 'redux/actions/publish'; import PublishAdditionalOptions from './view'; import { selectUser, selectAccessToken } from 'redux/selectors/user'; import { doFetchAccessToken } from 'redux/actions/user'; diff --git a/ui/component/publishBid/index.js b/ui/component/publishBid/index.js index 17ce7cf2a..bd8ceff9e 100644 --- a/ui/component/publishBid/index.js +++ b/ui/component/publishBid/index.js @@ -1,13 +1,12 @@ import { connect } from 'react-redux'; +import { selectBalance } from 'redux/selectors/wallet'; import { makeSelectPublishFormValue, selectMyClaimForUri, selectIsResolvingPublishUris, selectTakeOverAmount, - doUpdatePublishForm, - doPrepareEdit, - selectBalance, -} from 'lbry-redux'; +} from 'redux/selectors/publish'; +import { doUpdatePublishForm, doPrepareEdit } from 'redux/actions/publish'; import PublishPage from './view'; const select = (state) => ({ diff --git a/ui/component/publishDescription/index.js b/ui/component/publishDescription/index.js index 96da26fd7..ee3d20ac9 100644 --- a/ui/component/publishDescription/index.js +++ b/ui/component/publishDescription/index.js @@ -1,13 +1,14 @@ import { connect } from 'react-redux'; -import { doUpdatePublishForm, makeSelectPublishFormValue } from 'lbry-redux'; +import { makeSelectPublishFormValue } from 'redux/selectors/publish'; +import { doUpdatePublishForm } from 'redux/actions/publish'; import PublishDescription from './view'; -const select = state => ({ +const select = (state) => ({ description: makeSelectPublishFormValue('description')(state), }); -const perform = dispatch => ({ - updatePublishForm: value => dispatch(doUpdatePublishForm(value)), +const perform = (dispatch) => ({ + updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)), }); export default connect(select, perform)(PublishDescription); diff --git a/ui/component/publishFile/index.js b/ui/component/publishFile/index.js index 26a73211f..6d2089a4f 100644 --- a/ui/component/publishFile/index.js +++ b/ui/component/publishFile/index.js @@ -1,12 +1,8 @@ import { connect } from 'react-redux'; -import { - selectBalance, - selectIsStillEditing, - makeSelectPublishFormValue, - doUpdatePublishForm, - doClearPublish, - makeSelectClaimIsStreamPlaceholder, -} from 'lbry-redux'; +import { selectBalance } from 'redux/selectors/wallet'; +import { selectIsStillEditing, makeSelectPublishFormValue } from 'redux/selectors/publish'; +import { doUpdatePublishForm, doClearPublish } from 'redux/actions/publish'; +import { makeSelectClaimIsStreamPlaceholder } from 'redux/selectors/claims'; import { doToast } from 'redux/actions/notifications'; import { selectFfmpegStatus } from 'redux/selectors/settings'; import PublishPage from './view'; diff --git a/ui/component/publishFile/view.jsx b/ui/component/publishFile/view.jsx index 5ff95a9f3..6040f0596 100644 --- a/ui/component/publishFile/view.jsx +++ b/ui/component/publishFile/view.jsx @@ -3,7 +3,7 @@ import { SITE_NAME, WEB_PUBLISH_SIZE_LIMIT_GB, SIMPLE_SITE } from 'config'; import type { Node } from 'react'; import * as ICONS from 'constants/icons'; import React, { useState, useEffect } from 'react'; -import { regexInvalidURI } from 'lbry-redux'; +import { regexInvalidURI } from 'util/lbryURI'; import PostEditor from 'component/postEditor'; import FileSelector from 'component/common/file-selector'; import Button from 'component/button'; diff --git a/ui/component/publishForm/index.js b/ui/component/publishForm/index.js index 58b24bdb5..a78b5da0a 100644 --- a/ui/component/publishForm/index.js +++ b/ui/component/publishForm/index.js @@ -1,23 +1,23 @@ import { connect } from 'react-redux'; import { - doResolveUri, - selectPublishFormValues, - selectIsStillEditing, - selectMyClaimForUri, - selectIsResolvingPublishUris, - selectTakeOverAmount, doResetThumbnailStatus, doClearPublish, doUpdatePublishForm, doPrepareEdit, - doCheckPublishNameAvailability, - SETTINGS, - selectMyChannelClaims, - makeSelectClaimIsStreamPlaceholder, + doPublishDesktop, +} from 'redux/actions/publish'; +import { doResolveUri, doCheckPublishNameAvailability } from 'redux/actions/claims'; +import { + selectTakeOverAmount, + selectPublishFormValues, + selectIsStillEditing, makeSelectPublishFormValue, -} from 'lbry-redux'; + selectIsResolvingPublishUris, + selectMyClaimForUri, +} from 'redux/selectors/publish'; +import { selectMyChannelClaims, makeSelectClaimIsStreamPlaceholder } from 'redux/selectors/claims'; import * as RENDER_MODES from 'constants/file_render_modes'; -import { doPublishDesktop } from 'redux/actions/publish'; +import * as SETTINGS from 'constants/settings'; import { doClaimInitialRewards } from 'redux/actions/rewards'; import { selectUnclaimedRewardValue, diff --git a/ui/component/publishForm/view.jsx b/ui/component/publishForm/view.jsx index f5e0e6e46..7371f4eb1 100644 --- a/ui/component/publishForm/view.jsx +++ b/ui/component/publishForm/view.jsx @@ -10,7 +10,9 @@ import { SITE_NAME, ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE, CHANNEL_STAKED_LEVEL_LIVESTREAM } from 'config'; import React, { useEffect, useState } from 'react'; -import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES, Lbry } from 'lbry-redux'; +import Lbry from 'lbry'; +import { buildURI, isURIValid, isNameValid } from 'util/lbryURI'; +import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses'; import Button from 'component/button'; import ChannelSelect from 'component/channelSelector'; import classnames from 'classnames'; @@ -230,7 +232,7 @@ function PublishForm(props: Props) { // If they are editing, they don't need a new file chosen const formValidLessFile = name && - isNameValid(name, false) && + isNameValid(name) && title && bid && thumbnail && @@ -381,7 +383,7 @@ function PublishForm(props: Props) { } catch (e) {} } - const isValid = isURIValid(uri); + const isValid = uri && isURIValid(uri); if (uri && isValid && checkAvailability && name) { resolveUri(uri); checkAvailability(name); diff --git a/ui/component/publishFormErrors/index.js b/ui/component/publishFormErrors/index.js index 28ca6c690..c69627697 100644 --- a/ui/component/publishFormErrors/index.js +++ b/ui/component/publishFormErrors/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectPublishFormValue, selectIsStillEditing } from 'lbry-redux'; +import { makeSelectPublishFormValue, selectIsStillEditing } from 'redux/selectors/publish'; import PublishPage from './view'; const select = (state) => ({ diff --git a/ui/component/publishFormErrors/view.jsx b/ui/component/publishFormErrors/view.jsx index a01012589..bcefce7d2 100644 --- a/ui/component/publishFormErrors/view.jsx +++ b/ui/component/publishFormErrors/view.jsx @@ -1,6 +1,7 @@ // @flow import React from 'react'; -import { THUMBNAIL_STATUSES, isNameValid } from 'lbry-redux'; +import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses'; +import { isNameValid } from 'util/lbryURI'; import { INVALID_NAME_ERROR } from 'constants/claim'; type Props = { diff --git a/ui/component/publishName/index.js b/ui/component/publishName/index.js index 4159056e3..974309f36 100644 --- a/ui/component/publishName/index.js +++ b/ui/component/publishName/index.js @@ -1,12 +1,11 @@ import { connect } from 'react-redux'; +import { doUpdatePublishForm, doPrepareEdit } from 'redux/actions/publish'; import { makeSelectPublishFormValue, selectIsStillEditing, selectMyClaimForUri, selectTakeOverAmount, - doUpdatePublishForm, - doPrepareEdit, -} from 'lbry-redux'; +} from 'redux/selectors/publish'; import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app'; import { doSetActiveChannel } from 'redux/actions/app'; import PublishPage from './view'; diff --git a/ui/component/publishName/name-help-text.jsx b/ui/component/publishName/name-help-text.jsx index c51a8aeb9..c567e734a 100644 --- a/ui/component/publishName/name-help-text.jsx +++ b/ui/component/publishName/name-help-text.jsx @@ -1,7 +1,7 @@ // @flow import * as React from 'react'; import Button from 'component/button'; -import { buildURI } from 'lbry-redux'; +import { buildURI } from 'util/lbryURI'; import I18nMessage from 'component/i18nMessage'; type Props = { diff --git a/ui/component/publishName/view.jsx b/ui/component/publishName/view.jsx index d79dceede..3c5ff7f85 100644 --- a/ui/component/publishName/view.jsx +++ b/ui/component/publishName/view.jsx @@ -2,7 +2,7 @@ import { DOMAIN } from 'config'; import { INVALID_NAME_ERROR } from 'constants/claim'; import React, { useState, useEffect } from 'react'; -import { isNameValid } from 'lbry-redux'; +import { isNameValid } from 'util/lbryURI'; import { FormField } from 'component/common/form'; import NameHelpText from './name-help-text'; @@ -55,7 +55,7 @@ function PublishName(props: Props) { let nameError; if (!name) { nameError = __('A name is required'); - } else if (!isNameValid(name, false)) { + } else if (!isNameValid(name)) { nameError = INVALID_NAME_ERROR; } diff --git a/ui/component/publishPending/index.js b/ui/component/publishPending/index.js index efa28236e..14d958e7e 100644 --- a/ui/component/publishPending/index.js +++ b/ui/component/publishPending/index.js @@ -1,12 +1,13 @@ import { connect } from 'react-redux'; -import { makeSelectReflectingClaimForUri, doCheckReflectingFiles } from 'lbry-redux'; +import { doCheckReflectingFiles } from 'redux/actions/publish'; +import { makeSelectReflectingClaimForUri } from 'redux/selectors/claims'; import PublishPending from './view'; const select = (state, props) => ({ reflectingInfo: props.uri && makeSelectReflectingClaimForUri(props.uri)(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ checkReflecting: () => dispatch(doCheckReflectingFiles()), }); diff --git a/ui/component/publishPending/view.jsx b/ui/component/publishPending/view.jsx index 2db1a1c85..d0ed5c45d 100644 --- a/ui/component/publishPending/view.jsx +++ b/ui/component/publishPending/view.jsx @@ -1,7 +1,7 @@ // @flow import React from 'react'; -import Lbry from 'lbry-redux'; +import Lbry from 'lbry'; import Button from 'component/button'; import Spinner from 'component/spinner'; diff --git a/ui/component/publishPrice/index.js b/ui/component/publishPrice/index.js index 7313d0f9c..88156b3c8 100644 --- a/ui/component/publishPrice/index.js +++ b/ui/component/publishPrice/index.js @@ -1,17 +1,15 @@ import { connect } from 'react-redux'; -import { makeSelectPublishFormValue, doUpdatePublishForm } from 'lbry-redux'; +import { makeSelectPublishFormValue } from 'redux/selectors/publish'; +import { doUpdatePublishForm } from 'redux/actions/publish'; import PublishPage from './view'; -const select = state => ({ +const select = (state) => ({ contentIsFree: makeSelectPublishFormValue('contentIsFree')(state), fee: makeSelectPublishFormValue('fee')(state), }); -const perform = dispatch => ({ - updatePublishForm: values => dispatch(doUpdatePublishForm(values)), +const perform = (dispatch) => ({ + updatePublishForm: (values) => dispatch(doUpdatePublishForm(values)), }); -export default connect( - select, - perform -)(PublishPage); +export default connect(select, perform)(PublishPage); diff --git a/ui/component/publishReleaseDate/index.js b/ui/component/publishReleaseDate/index.js index 67306008a..962afce19 100644 --- a/ui/component/publishReleaseDate/index.js +++ b/ui/component/publishReleaseDate/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { doUpdatePublishForm, makeSelectPublishFormValue } from 'lbry-redux'; +import { makeSelectPublishFormValue } from 'redux/selectors/publish'; +import { doUpdatePublishForm } from 'redux/actions/publish'; import PublishReleaseDate from './view'; const select = (state) => ({ diff --git a/ui/component/recommendedContent/index.js b/ui/component/recommendedContent/index.js index 4538d3305..2eed9f940 100644 --- a/ui/component/recommendedContent/index.js +++ b/ui/component/recommendedContent/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; import { doFetchRecommendedContent } from 'redux/actions/search'; import { makeSelectRecommendedContentForUri, selectIsSearching } from 'redux/selectors/search'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; diff --git a/ui/component/recommendedContent/view.jsx b/ui/component/recommendedContent/view.jsx index 2de10bd10..88cf76186 100644 --- a/ui/component/recommendedContent/view.jsx +++ b/ui/component/recommendedContent/view.jsx @@ -8,7 +8,7 @@ import Card from 'component/common/card'; import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize'; import Button from 'component/button'; import classnames from 'classnames'; -import RecSys from 'recsys'; +import RecSys from 'extras/recsys/recsys'; const VIEW_ALL_RELATED = 'view_all_related'; const VIEW_MORE_FROM = 'view_more_from'; diff --git a/ui/component/reportContent/index.js b/ui/component/reportContent/index.js index 3851769b0..84a72ebc0 100644 --- a/ui/component/reportContent/index.js +++ b/ui/component/reportContent/index.js @@ -2,7 +2,8 @@ import { connect } from 'react-redux'; import { doReportContent } from 'redux/actions/reportContent'; import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app'; import { selectIsReportingContent, selectReportContentError } from 'redux/selectors/reportContent'; -import { makeSelectClaimForClaimId, doClaimSearch } from 'lbry-redux'; +import { doClaimSearch } from 'redux/actions/claims'; +import { makeSelectClaimForClaimId } from 'redux/selectors/claims'; import { withRouter } from 'react-router'; import ReportContent from './view'; diff --git a/ui/component/repostCreate/index.js b/ui/component/repostCreate/index.js index 07b5c5426..da2d21455 100644 --- a/ui/component/repostCreate/index.js +++ b/ui/component/repostCreate/index.js @@ -3,19 +3,22 @@ import { doHideModal } from 'redux/actions/app'; import { makeSelectClaimForUri, makeSelectTitleForUri, - selectBalance, selectMyChannelClaims, - doRepost, selectRepostError, selectRepostLoading, - doClearRepostError, selectMyClaimsWithoutChannels, - doCheckPublishNameAvailability, - doCheckPendingClaims, makeSelectEffectiveAmountForUri, makeSelectIsUriResolving, selectFetchingMyChannels, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; + +import { selectBalance } from 'redux/selectors/wallet'; +import { + doRepost, + doClearRepostError, + doCheckPublishNameAvailability, + doCheckPendingClaims, +} from 'redux/actions/claims'; import { doToast } from 'redux/actions/notifications'; import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app'; import RepostCreate from './view'; diff --git a/ui/component/repostCreate/view.jsx b/ui/component/repostCreate/view.jsx index c4e47be1c..935418a12 100644 --- a/ui/component/repostCreate/view.jsx +++ b/ui/component/repostCreate/view.jsx @@ -8,7 +8,8 @@ import Card from 'component/common/card'; import Button from 'component/button'; import ChannelSelector from 'component/channelSelector'; import { FormField } from 'component/common/form'; -import { parseURI, isNameValid, creditsToString, isURIValid, normalizeURI } from 'lbry-redux'; +import { parseURI, isNameValid, isURIValid, normalizeURI } from 'util/lbryURI'; +import { creditsToString } from 'util/format-credits'; import analytics from 'analytics'; import LbcSymbol from 'component/common/lbc-symbol'; import ClaimPreview from 'component/claimPreview'; diff --git a/ui/component/router/index.js b/ui/component/router/index.js index 6f4454872..48b648cd2 100644 --- a/ui/component/router/index.js +++ b/ui/component/router/index.js @@ -3,12 +3,13 @@ import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectHasNavigated, selectScrollStartingPosition, selectWelcomeVersion } from 'redux/selectors/app'; import { selectHomepageData } from 'redux/selectors/settings'; import Router from './view'; -import { normalizeURI, makeSelectTitleForUri } from 'lbry-redux'; +import { normalizeURI } from 'util/lbryURI'; +import { makeSelectTitleForUri } from 'redux/selectors/claims'; import { doSetHasNavigated } from 'redux/actions/app'; import { doUserSetReferrer } from 'redux/actions/user'; import { selectHasUnclaimedRefereeReward } from 'redux/selectors/rewards'; -const select = state => { +const select = (state) => { const { pathname, hash } = state.router.location; const urlPath = pathname + hash; // Remove the leading "/" added by the browser @@ -37,9 +38,9 @@ const select = state => { }; }; -const perform = dispatch => ({ +const perform = (dispatch) => ({ setHasNavigated: () => dispatch(doSetHasNavigated()), - setReferrer: referrer => dispatch(doUserSetReferrer(referrer)), + setReferrer: (referrer) => dispatch(doUserSetReferrer(referrer)), }); export default connect(select, perform)(Router); diff --git a/ui/component/router/view.jsx b/ui/component/router/view.jsx index ad5234265..49b64eb8c 100644 --- a/ui/component/router/view.jsx +++ b/ui/component/router/view.jsx @@ -6,7 +6,7 @@ import * as PAGES from 'constants/pages'; import { PAGE_TITLE } from 'constants/pageTitles'; import { lazyImport } from 'util/lazyImport'; import { LINKED_COMMENT_QUERY_PARAM } from 'constants/comment'; -import { parseURI, isURIValid } from 'lbry-redux'; +import { parseURI, isURIValid } from 'util/lbryURI'; import { SITE_TITLE, WELCOME_VERSION, SIMPLE_SITE } from 'config'; import LoadingBarOneOff from 'component/loadingBarOneOff'; import { GetLinksData } from 'util/buildHomepage'; diff --git a/ui/component/searchChannelField/view.jsx b/ui/component/searchChannelField/view.jsx index 89fa419bd..0b2087cae 100644 --- a/ui/component/searchChannelField/view.jsx +++ b/ui/component/searchChannelField/view.jsx @@ -1,6 +1,6 @@ // @flow import React from 'react'; -import { isNameValid, parseURI } from 'lbry-redux'; +import { isNameValid, parseURI } from 'util/lbryURI'; import Button from 'component/button'; import ClaimPreview from 'component/claimPreview'; import { FormField } from 'component/common/form-components/form-field'; diff --git a/ui/component/searchTopClaim/index.js b/ui/component/searchTopClaim/index.js index 65cc16fe9..74f7b9cbf 100644 --- a/ui/component/searchTopClaim/index.js +++ b/ui/component/searchTopClaim/index.js @@ -1,5 +1,7 @@ import { connect } from 'react-redux'; -import { doResolveUris, doClearPublish, doPrepareEdit, selectPendingIds, makeSelectClaimForUri } from 'lbry-redux'; +import { doClearPublish, doPrepareEdit } from 'redux/actions/publish'; +import { doResolveUris } from 'redux/actions/claims'; +import { selectPendingIds, makeSelectClaimForUri } from 'redux/selectors/claims'; import { makeSelectWinningUriForQuery, makeSelectIsResolvingWinningUri } from 'redux/selectors/search'; import SearchTopClaim from './view'; import { push } from 'connected-react-router'; @@ -16,13 +18,13 @@ const select = (state, props) => { }; }; -const perform = dispatch => ({ - beginPublish: name => { +const perform = (dispatch) => ({ + beginPublish: (name) => { dispatch(doClearPublish()); dispatch(doPrepareEdit({ name })); dispatch(push(`/$/${PAGES.UPLOAD}`)); }, - doResolveUris: uris => dispatch(doResolveUris(uris)), + doResolveUris: (uris) => dispatch(doResolveUris(uris)), }); export default connect(select, perform)(SearchTopClaim); diff --git a/ui/component/searchTopClaim/view.jsx b/ui/component/searchTopClaim/view.jsx index 7891b6462..8e5f2c65c 100644 --- a/ui/component/searchTopClaim/view.jsx +++ b/ui/component/searchTopClaim/view.jsx @@ -2,7 +2,7 @@ import * as ICONS from 'constants/icons'; import * as PAGES from 'constants/pages'; import React from 'react'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; import ClaimPreview from 'component/claimPreview'; import Button from 'component/button'; import ClaimEffectiveAmount from 'component/claimEffectiveAmount'; diff --git a/ui/component/selectAsset/view.jsx b/ui/component/selectAsset/view.jsx index 56f4d5e56..469a67e94 100644 --- a/ui/component/selectAsset/view.jsx +++ b/ui/component/selectAsset/view.jsx @@ -1,7 +1,7 @@ // @flow import React from 'react'; import FileSelector from 'component/common/file-selector'; -import { SPEECH_URLS } from 'lbry-redux'; +import * as SPEECH_URLS from 'constants/speech_urls'; import { FormField, Form } from 'component/common/form'; import Button from 'component/button'; import Card from 'component/common/card'; @@ -119,7 +119,7 @@ function SelectAsset(props: Props) { button="primary" type="submit" label={__('Done')} - disabled={!useUrl && ((uploadStatus === SPEECH_UPLOADING) || !pathSelected || !fileSelected)} + disabled={!useUrl && (uploadStatus === SPEECH_UPLOADING || !pathSelected || !fileSelected)} onClick={() => doUploadAsset()} /> )} diff --git a/ui/component/selectChannel/index.js b/ui/component/selectChannel/index.js index dc7b96638..0fcf80476 100644 --- a/ui/component/selectChannel/index.js +++ b/ui/component/selectChannel/index.js @@ -1,17 +1,13 @@ import { connect } from 'react-redux'; import SelectChannel from './view'; -import { - selectBalance, - selectMyChannelClaims, - selectFetchingMyChannels, - doFetchChannelListMine, - doCreateChannel, -} from 'lbry-redux'; +import { selectBalance } from 'redux/selectors/wallet'; +import { selectMyChannelClaims, selectFetchingMyChannels } from 'redux/selectors/claims'; +import { doFetchChannelListMine, doCreateChannel } from 'redux/actions/claims'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectActiveChannelClaim } from 'redux/selectors/app'; import { doSetActiveChannel } from 'redux/actions/app'; -const select = state => ({ +const select = (state) => ({ myChannelClaims: selectMyChannelClaims(state), fetchingChannels: selectFetchingMyChannels(state), balance: selectBalance(state), @@ -19,10 +15,10 @@ const select = state => ({ activeChannelClaim: selectActiveChannelClaim(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)), fetchChannelListMine: () => dispatch(doFetchChannelListMine()), - setActiveChannel: claimId => dispatch(doSetActiveChannel(claimId)), + setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)), }); export default connect(select, perform)(SelectChannel); diff --git a/ui/component/selectThumbnail/index.js b/ui/component/selectThumbnail/index.js index a16b74aec..9170755aa 100644 --- a/ui/component/selectThumbnail/index.js +++ b/ui/component/selectThumbnail/index.js @@ -1,11 +1,7 @@ import { connect } from 'react-redux'; -import { - selectPublishFormValues, - selectMyClaimForUri, - doUpdatePublishForm, - selectFileInfosByOutpoint, - doResetThumbnailStatus, -} from 'lbry-redux'; +import { selectPublishFormValues, selectMyClaimForUri } from 'redux/selectors/publish'; +import { selectFileInfosByOutpoint } from 'redux/selectors/file_info'; +import { doUpdatePublishForm, doResetThumbnailStatus } from 'redux/actions/publish'; import { doOpenModal } from 'redux/actions/app'; import PublishPage from './view'; diff --git a/ui/component/selectThumbnail/view.jsx b/ui/component/selectThumbnail/view.jsx index 3dbd70fea..c8090583b 100644 --- a/ui/component/selectThumbnail/view.jsx +++ b/ui/component/selectThumbnail/view.jsx @@ -1,6 +1,7 @@ // @flow import * as MODALS from 'constants/modal_types'; -import { Lbry, THUMBNAIL_STATUSES } from 'lbry-redux'; +import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses'; +import Lbry from 'lbry'; import { DOMAIN } from 'config'; import * as React from 'react'; import { FormField } from 'component/common/form'; diff --git a/ui/component/settingAccount/index.js b/ui/component/settingAccount/index.js index 0644ceaf6..5cbc5fd70 100644 --- a/ui/component/settingAccount/index.js +++ b/ui/component/settingAccount/index.js @@ -1,5 +1,7 @@ import { connect } from 'react-redux'; -import { doWalletStatus, selectMyChannelClaims, selectWalletIsEncrypted } from 'lbry-redux'; +import { selectMyChannelClaims } from 'redux/selectors/claims'; +import { selectWalletIsEncrypted } from 'redux/selectors/wallet'; +import { doWalletStatus } from 'redux/actions/wallet'; import { selectUser, selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectLanguage } from 'redux/selectors/settings'; diff --git a/ui/component/settingAppearance/index.js b/ui/component/settingAppearance/index.js index 33cea31fc..4d7d81cf1 100644 --- a/ui/component/settingAppearance/index.js +++ b/ui/component/settingAppearance/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; import { doSetClientSetting } from 'redux/actions/settings'; import { selectLanguage, makeSelectClientSetting } from 'redux/selectors/settings'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; diff --git a/ui/component/settingAppearance/view.jsx b/ui/component/settingAppearance/view.jsx index 436e390b5..7fe719433 100644 --- a/ui/component/settingAppearance/view.jsx +++ b/ui/component/settingAppearance/view.jsx @@ -1,7 +1,7 @@ // @flow import { SETTINGS_GRP } from 'constants/settings'; import React from 'react'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; import Card from 'component/common/card'; import { FormField } from 'component/common/form'; import HomepageSelector from 'component/homepageSelector'; diff --git a/ui/component/settingAutoLaunch/index.js b/ui/component/settingAutoLaunch/index.js index d295fcf7f..ef8a0f5a7 100644 --- a/ui/component/settingAutoLaunch/index.js +++ b/ui/component/settingAutoLaunch/index.js @@ -1,18 +1,18 @@ import { connect } from 'react-redux'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; import { doSetAutoLaunch } from 'redux/actions/settings'; import { makeSelectClientSetting, selectLanguage } from 'redux/selectors/settings'; import { doToast } from 'redux/actions/notifications'; import SettingAutoLaunch from './view'; -const select = state => ({ +const select = (state) => ({ autoLaunch: makeSelectClientSetting(SETTINGS.AUTO_LAUNCH)(state), language: selectLanguage(state), }); -const perform = dispatch => ({ - showToast: options => dispatch(doToast(options)), - setAutoLaunch: value => dispatch(doSetAutoLaunch(value)), +const perform = (dispatch) => ({ + showToast: (options) => dispatch(doToast(options)), + setAutoLaunch: (value) => dispatch(doSetAutoLaunch(value)), }); export default connect(select, perform)(SettingAutoLaunch); diff --git a/ui/component/settingClosingBehavior/index.js b/ui/component/settingClosingBehavior/index.js index e90759a99..ca2791833 100644 --- a/ui/component/settingClosingBehavior/index.js +++ b/ui/component/settingClosingBehavior/index.js @@ -1,15 +1,15 @@ import { connect } from 'react-redux'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; import { doSetAppToTrayWhenClosed } from 'redux/actions/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings'; import SettingClosingBehavior from './view'; -const select = state => ({ +const select = (state) => ({ toTrayWhenClosed: makeSelectClientSetting(SETTINGS.TO_TRAY_WHEN_CLOSED)(state), }); -const perform = dispatch => ({ - setToTrayWhenClosed: value => dispatch(doSetAppToTrayWhenClosed(value)), +const perform = (dispatch) => ({ + setToTrayWhenClosed: (value) => dispatch(doSetAppToTrayWhenClosed(value)), }); export default connect(select, perform)(SettingClosingBehavior); diff --git a/ui/component/settingCommentsServer/index.js b/ui/component/settingCommentsServer/index.js index 9ad06707b..deee05b7f 100644 --- a/ui/component/settingCommentsServer/index.js +++ b/ui/component/settingCommentsServer/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; import { doSetClientSetting } from 'redux/actions/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings'; import SettingCommentsServer from './view'; diff --git a/ui/component/settingContent/index.js b/ui/component/settingContent/index.js index 9330df570..071c2cb09 100644 --- a/ui/component/settingContent/index.js +++ b/ui/component/settingContent/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { selectMyChannelUrls, SETTINGS } from 'lbry-redux'; +import { selectMyChannelUrls } from 'redux/selectors/claims'; +import * as SETTINGS from 'constants/settings'; import { doOpenModal } from 'redux/actions/app'; import { doSetPlayingUri } from 'redux/actions/content'; import { doSetClientSetting } from 'redux/actions/settings'; diff --git a/ui/component/settingContent/view.jsx b/ui/component/settingContent/view.jsx index 7f0b5f47a..f86251318 100644 --- a/ui/component/settingContent/view.jsx +++ b/ui/component/settingContent/view.jsx @@ -2,7 +2,7 @@ import * as ICONS from 'constants/icons'; import * as PAGES from 'constants/pages'; import React from 'react'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; import { Lbryio } from 'lbryinc'; import { SIMPLE_SITE } from 'config'; import * as MODALS from 'constants/modal_types'; diff --git a/ui/component/settingSystem/index.js b/ui/component/settingSystem/index.js index 53155e49d..f2b66a1ba 100644 --- a/ui/component/settingSystem/index.js +++ b/ui/component/settingSystem/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { doWalletStatus, selectWalletIsEncrypted } from 'lbry-redux'; +import { doWalletStatus } from 'redux/actions/wallet'; +import { selectWalletIsEncrypted } from 'redux/selectors/wallet'; import { doClearCache, doNotifyDecryptWallet, diff --git a/ui/component/settingUnauthenticated/index.js b/ui/component/settingUnauthenticated/index.js index 8476d4a40..d99adcc73 100644 --- a/ui/component/settingUnauthenticated/index.js +++ b/ui/component/settingUnauthenticated/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; import { doSetClientSetting } from 'redux/actions/settings'; import { selectLanguage, makeSelectClientSetting } from 'redux/selectors/settings'; diff --git a/ui/component/settingWalletServer/index.js b/ui/component/settingWalletServer/index.js index 54917e792..95d4b169b 100644 --- a/ui/component/settingWalletServer/index.js +++ b/ui/component/settingWalletServer/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { DAEMON_SETTINGS, selectIsWalletReconnecting } from 'lbry-redux'; +import { selectIsWalletReconnecting } from 'redux/selectors/wallet'; +import * as DAEMON_SETTINGS from 'constants/daemon_settings'; import { doSetDaemonSetting, doClearDaemonSetting, @@ -9,21 +10,18 @@ import { import { selectSavedWalletServers, selectDaemonStatus, selectHasWalletServerPrefs } from 'redux/selectors/settings'; import SettingWalletServer from './view'; -const select = state => ({ +const select = (state) => ({ daemonStatus: selectDaemonStatus(state), customWalletServers: selectSavedWalletServers(state), hasWalletServerPrefs: selectHasWalletServerPrefs(state), walletReconnecting: selectIsWalletReconnecting(state), }); -const perform = dispatch => ({ - setCustomWalletServers: value => dispatch(doSetDaemonSetting(DAEMON_SETTINGS.LBRYUM_SERVERS, value)), +const perform = (dispatch) => ({ + setCustomWalletServers: (value) => dispatch(doSetDaemonSetting(DAEMON_SETTINGS.LBRYUM_SERVERS, value)), clearWalletServers: () => dispatch(doClearDaemonSetting(DAEMON_SETTINGS.LBRYUM_SERVERS)), getDaemonStatus: () => dispatch(doGetDaemonStatus()), - saveServerConfig: servers => dispatch(doSaveCustomWalletServers(servers)), + saveServerConfig: (servers) => dispatch(doSaveCustomWalletServers(servers)), }); -export default connect( - select, - perform -)(SettingWalletServer); +export default connect(select, perform)(SettingWalletServer); diff --git a/ui/component/sideNavigation/index.js b/ui/component/sideNavigation/index.js index 40a13ccc8..5fbd7ac62 100644 --- a/ui/component/sideNavigation/index.js +++ b/ui/component/sideNavigation/index.js @@ -1,13 +1,12 @@ import { connect } from 'react-redux'; import { selectSubscriptions } from 'redux/selectors/subscriptions'; -import { selectPurchaseUriSuccess } from 'lbry-redux'; +import { doClearPurchasedUriSuccess } from 'redux/actions/file'; import { selectFollowedTags } from 'redux/selectors/tags'; import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user'; import { selectHomepageData, selectLanguage } from 'redux/selectors/settings'; import { doSignOut } from 'redux/actions/app'; -import { doClearPurchasedUriSuccess } from 'redux/actions/file'; - import { selectUnseenNotificationCount } from 'redux/selectors/notifications'; +import { selectPurchaseUriSuccess } from 'redux/selectors/claims'; import SideNavigation from './view'; diff --git a/ui/component/socialShare/index.js b/ui/component/socialShare/index.js index 744008699..24591839a 100644 --- a/ui/component/socialShare/index.js +++ b/ui/component/socialShare/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri, makeSelectTitleForUri } from 'lbry-redux'; +import { makeSelectClaimForUri, makeSelectTitleForUri } from 'redux/selectors/claims'; import SocialShare from './view'; import { selectUserInviteReferralCode, selectUser } from 'redux/selectors/user'; import { makeSelectContentPositionForUri } from 'redux/selectors/content'; diff --git a/ui/component/splash/index.js b/ui/component/splash/index.js index 355096a47..8fde0e9c2 100644 --- a/ui/component/splash/index.js +++ b/ui/component/splash/index.js @@ -3,24 +3,24 @@ import { connect } from 'react-redux'; import { selectDaemonVersionMatched, selectModal, selectSplashAnimationEnabled } from 'redux/selectors/app'; import { doCheckDaemonVersion, doOpenModal, doHideModal, doToggleSplashAnimation } from 'redux/actions/app'; import { doClearDaemonSetting } from 'redux/actions/settings'; -import { DAEMON_SETTINGS } from 'lbry-redux'; +import * as DAEMON_SETTINGS from 'constants/daemon_settings'; import { doToast } from 'redux/actions/notifications'; import SplashScreen from './view'; -const select = state => ({ +const select = (state) => ({ modal: selectModal(state), daemonVersionMatched: selectDaemonVersionMatched(state), animationHidden: selectSplashAnimationEnabled(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ checkDaemonVersion: () => dispatch(doCheckDaemonVersion()), - notifyUnlockWallet: shouldTryWithBlankPassword => + notifyUnlockWallet: (shouldTryWithBlankPassword) => dispatch(doOpenModal(MODALS.WALLET_UNLOCK, { shouldTryWithBlankPassword })), hideModal: () => dispatch(doHideModal()), toggleSplashAnimation: () => dispatch(doToggleSplashAnimation()), clearWalletServers: () => dispatch(doClearDaemonSetting(DAEMON_SETTINGS.LBRYUM_SERVERS)), - doShowSnackBar: message => dispatch(doToast({ isError: true, message })), + doShowSnackBar: (message) => dispatch(doToast({ isError: true, message })), }); export default connect(select, perform)(SplashScreen); diff --git a/ui/component/splash/view.jsx b/ui/component/splash/view.jsx index c48251a27..6d6c5459c 100644 --- a/ui/component/splash/view.jsx +++ b/ui/component/splash/view.jsx @@ -3,7 +3,7 @@ import type { Node } from 'react'; import * as MODALS from 'constants/modal_types'; import * as ICONS from 'constants/icons'; import React from 'react'; -import { Lbry } from 'lbry-redux'; +import Lbry from 'lbry'; import Button from 'component/button'; import ModalWalletUnlock from 'modal/modalWalletUnlock'; import ModalIncompatibleDaemon from 'modal/modalIncompatibleDaemon'; diff --git a/ui/component/subscribeButton/index.js b/ui/component/subscribeButton/index.js index 52ea92e5e..f6331c809 100644 --- a/ui/component/subscribeButton/index.js +++ b/ui/component/subscribeButton/index.js @@ -5,7 +5,7 @@ import { selectFirstRunCompleted, makeSelectNotificationsDisabled, } from 'redux/selectors/subscriptions'; -import { makeSelectPermanentUrlForUri } from 'lbry-redux'; +import { makeSelectPermanentUrlForUri } from 'redux/selectors/claims'; import { selectUser } from 'redux/selectors/user'; import { doToast } from 'redux/actions/notifications'; import SubscribeButton from './view'; diff --git a/ui/component/subscribeButton/view.jsx b/ui/component/subscribeButton/view.jsx index 82b898f73..c11d23c0c 100644 --- a/ui/component/subscribeButton/view.jsx +++ b/ui/component/subscribeButton/view.jsx @@ -1,7 +1,7 @@ // @flow import * as ICONS from 'constants/icons'; import React, { useRef } from 'react'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; import Button from 'component/button'; import useHover from 'effects/use-hover'; import { useIsMobile } from 'effects/use-screensize'; @@ -72,11 +72,14 @@ export default function SubscribeButton(props: Props) { onClick={(e) => { e.stopPropagation(); - subscriptionHandler({ - channelName: '@' + rawChannelName, - uri: uri, - notificationsDisabled: true, - }, true); + subscriptionHandler( + { + channelName: '@' + rawChannelName, + uri: uri, + notificationsDisabled: true, + }, + true + ); }} />
@@ -97,11 +100,14 @@ export default function SubscribeButton(props: Props) { onClick={(e) => { e.stopPropagation(); - subscriptionHandler({ - channelName: claimName, - uri: permanentUrl, - notificationsDisabled: true, - }, true); + subscriptionHandler( + { + channelName: claimName, + uri: permanentUrl, + notificationsDisabled: true, + }, + true + ); }} /> {isSubscribed && uiNotificationsEnabled && ( @@ -112,14 +118,23 @@ export default function SubscribeButton(props: Props) { onClick={() => { const newNotificationsDisabled = !notificationsDisabled; - doChannelSubscribe({ - channelName: claimName, - uri: permanentUrl, - notificationsDisabled: newNotificationsDisabled, - }, false); + doChannelSubscribe( + { + channelName: claimName, + uri: permanentUrl, + notificationsDisabled: newNotificationsDisabled, + }, + false + ); - doToast({ message: __(newNotificationsDisabled ? 'Notifications turned off for %channel%' : 'Notifications turned on for %channel%!', - { channel: claimName }) }); + doToast({ + message: __( + newNotificationsDisabled + ? 'Notifications turned off for %channel%' + : 'Notifications turned on for %channel%!', + { channel: claimName } + ), + }); }} /> )} diff --git a/ui/component/supportsLiquidate/index.js b/ui/component/supportsLiquidate/index.js index e2bf8ec90..c7626b769 100644 --- a/ui/component/supportsLiquidate/index.js +++ b/ui/component/supportsLiquidate/index.js @@ -5,11 +5,11 @@ import { selectClaimsBalance, selectSupportsBalance, selectTipsBalance, - makeSelectMetadataForUri, - makeSelectClaimForUri, - doSupportAbandonForClaim, selectAbandonClaimSupportError, -} from 'lbry-redux'; +} from 'redux/selectors/wallet'; + +import { makeSelectMetadataForUri, makeSelectClaimForUri } from 'redux/selectors/claims'; +import { doSupportAbandonForClaim } from 'redux/actions/wallet'; import SupportsLiquidate from './view'; const select = (state, props) => ({ @@ -23,7 +23,7 @@ const select = (state, props) => ({ abandonClaimError: selectAbandonClaimSupportError(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ abandonSupportForClaim: (claimId, type, keep, preview) => dispatch(doSupportAbandonForClaim(claimId, type, keep, preview)), }); diff --git a/ui/component/syncEnableFlow/index.js b/ui/component/syncEnableFlow/index.js index 7ebfbcf45..3c7f06e16 100644 --- a/ui/component/syncEnableFlow/index.js +++ b/ui/component/syncEnableFlow/index.js @@ -1,4 +1,4 @@ -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; import { connect } from 'react-redux'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { @@ -13,7 +13,7 @@ import { doSetWalletSyncPreference } from 'redux/actions/settings'; import SyncToggle from './view'; import { doGetAndPopulatePreferences } from 'redux/actions/app'; -const select = state => ({ +const select = (state) => ({ syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state), hasSyncedWallet: selectHasSyncedWallet(state), hasSyncChanged: selectHashChanged(state), @@ -23,8 +23,8 @@ const select = state => ({ language: selectLanguage(state), }); -const perform = dispatch => ({ - setSyncEnabled: value => dispatch(doSetWalletSyncPreference(value)), +const perform = (dispatch) => ({ + setSyncEnabled: (value) => dispatch(doSetWalletSyncPreference(value)), checkSync: () => dispatch(doCheckSync()), getSync: (pw, cb) => dispatch(doGetSync(pw, cb)), updatePreferences: () => dispatch(doGetAndPopulatePreferences()), diff --git a/ui/component/syncEnableFlow/view.jsx b/ui/component/syncEnableFlow/view.jsx index 7c4c742d6..1c115722c 100644 --- a/ui/component/syncEnableFlow/view.jsx +++ b/ui/component/syncEnableFlow/view.jsx @@ -5,12 +5,12 @@ import { getSavedPassword } from 'util/saved-passwords'; import Card from 'component/common/card'; import { withRouter } from 'react-router'; import Spinner from 'component/spinner'; -import { Lbry } from 'lbry-redux'; +import Lbry from 'lbry'; import ErrorText from 'component/common/error-text'; import I18nMessage from 'component/i18nMessage'; type Props = { - setSyncEnabled: boolean => void, + setSyncEnabled: (boolean) => void, syncEnabled: boolean, getSyncError: ?string, getSyncPending: boolean, @@ -90,13 +90,13 @@ function SyncEnableFlow(props: Props) { const altTags = altData.tags || []; if (altBlocklist.length) { - altBlocklist.forEach(el => mergedBlockListSet.add(el)); + altBlocklist.forEach((el) => mergedBlockListSet.add(el)); } if (altSubscriptions.length) { - altSubscriptions.forEach(el => mergedSubscriptionsSet.add(el)); + altSubscriptions.forEach((el) => mergedSubscriptionsSet.add(el)); } if (altTags.length) { - altTags.forEach(el => mergedTagsSet.add(el)); + altTags.forEach((el) => mergedTagsSet.add(el)); } baseData.blocked = Array.from(mergedBlockListSet); @@ -110,7 +110,7 @@ function SyncEnableFlow(props: Props) { if (mode) { checkSync(); if (mode === ENABLE_MODE) { - getSavedPassword().then(pw => { + getSavedPassword().then((pw) => { setPassword(pw); setStep(FETCH_FOR_ENABLE); }); @@ -127,7 +127,7 @@ function SyncEnableFlow(props: Props) { setStep(ERROR); setError(e && e.message ? e.message : e); } else { - Lbry.preference_get().then(result => { + Lbry.preference_get().then((result) => { const prefs = {}; if (result[SHARED_KEY]) prefs[SHARED_KEY] = result[SHARED_KEY]; if (result[LOCAL_KEY]) prefs[LOCAL_KEY] = result[LOCAL_KEY]; @@ -138,7 +138,7 @@ function SyncEnableFlow(props: Props) { }); } if (step === FETCH_FOR_DISABLE) { - Lbry.preference_get().then(result => { + Lbry.preference_get().then((result) => { const prefs = {}; if (result[SHARED_KEY]) prefs[SHARED_KEY] = result[SHARED_KEY]; if (result[LOCAL_KEY]) prefs[LOCAL_KEY] = result[LOCAL_KEY]; diff --git a/ui/component/syncToggle/index.js b/ui/component/syncToggle/index.js index 84580a3dd..26c5f6673 100644 --- a/ui/component/syncToggle/index.js +++ b/ui/component/syncToggle/index.js @@ -1,4 +1,4 @@ -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; import { connect } from 'react-redux'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectGetSyncErrorMessage } from 'redux/selectors/sync'; @@ -7,15 +7,15 @@ import { doSetWalletSyncPreference } from 'redux/actions/settings'; import { doOpenModal } from 'redux/actions/app'; import SyncToggle from './view'; -const select = state => ({ +const select = (state) => ({ syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state), verifiedEmail: selectUserVerifiedEmail(state), getSyncError: selectGetSyncErrorMessage(state), language: selectLanguage(state), }); -const perform = dispatch => ({ - setSyncEnabled: value => dispatch(doSetWalletSyncPreference(value)), +const perform = (dispatch) => ({ + setSyncEnabled: (value) => dispatch(doSetWalletSyncPreference(value)), openModal: (id, props) => dispatch(doOpenModal(id, props)), }); diff --git a/ui/component/tag/view.jsx b/ui/component/tag/view.jsx index 2dfb552f7..d8fab2ce5 100644 --- a/ui/component/tag/view.jsx +++ b/ui/component/tag/view.jsx @@ -1,15 +1,16 @@ // @flow import * as ICONS from 'constants/icons'; import * as PAGES from 'constants/pages'; +// import * as MATURE_TAGS from 'constants/ta'; import React from 'react'; import classnames from 'classnames'; -import { MATURE_TAGS } from 'lbry-redux'; +import { MATURE_TAGS } from 'constants/tags'; import Button from 'component/button'; type Props = { name: string, type?: string, - onClick?: any => any, + onClick?: (any) => any, disabled: boolean, }; diff --git a/ui/component/themeSelector/index.js b/ui/component/themeSelector/index.js index 926a41546..a2f483258 100644 --- a/ui/component/themeSelector/index.js +++ b/ui/component/themeSelector/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; import { doSetClientSetting, doSetDarkTime } from 'redux/actions/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings'; import ThemeSelector from './view'; diff --git a/ui/component/themeSelector/view.jsx b/ui/component/themeSelector/view.jsx index 2f7985d16..d2cc95622 100644 --- a/ui/component/themeSelector/view.jsx +++ b/ui/component/themeSelector/view.jsx @@ -1,6 +1,6 @@ // @flow import React from 'react'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; import { FormField } from 'component/common/form'; type SetDaemonSettingArg = boolean | string | number; diff --git a/ui/component/transactionListTable/index.js b/ui/component/transactionListTable/index.js index ff8d36db4..2c07dfade 100644 --- a/ui/component/transactionListTable/index.js +++ b/ui/component/transactionListTable/index.js @@ -1,15 +1,15 @@ import { connect } from 'react-redux'; import { selectClaimedRewardsByTransactionId } from 'redux/selectors/rewards'; import { doOpenModal } from 'redux/actions/app'; -import { selectIsFetchingTxos } from 'lbry-redux'; +import { selectIsFetchingTxos } from 'redux/selectors/wallet'; import TransactionListTable from './view'; -const select = state => ({ +const select = (state) => ({ rewards: selectClaimedRewardsByTransactionId(state), loading: selectIsFetchingTxos(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ openModal: (modal, props) => dispatch(doOpenModal(modal, props)), }); diff --git a/ui/component/transactionListTableItem/index.js b/ui/component/transactionListTableItem/index.js index 8cbc1fb93..11d7e5a9a 100644 --- a/ui/component/transactionListTableItem/index.js +++ b/ui/component/transactionListTableItem/index.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import { doOpenModal } from 'redux/actions/app'; -import { makeSelectClaimForClaimId } from 'lbry-redux'; +import { makeSelectClaimForClaimId } from 'redux/selectors/claims'; import TransactionListTableItem from './view'; const select = (state, props) => { diff --git a/ui/component/transactionListTableItem/view.jsx b/ui/component/transactionListTableItem/view.jsx index 3c25633e9..c222dee36 100644 --- a/ui/component/transactionListTableItem/view.jsx +++ b/ui/component/transactionListTableItem/view.jsx @@ -8,7 +8,9 @@ import DateTime from 'component/dateTime'; import Button from 'component/button'; import Spinner from 'component/spinner'; import { toCapitalCase } from 'util/string'; -import { buildURI, parseURI, TXO_LIST as TXO, ABANDON_STATES } from 'lbry-redux'; +import { buildURI, parseURI } from 'util/lbryURI'; +import * as TXO from 'constants/txo_list'; +import * as ABANDON_STATES from 'constants/abandon_states'; import UriIndicator from 'component/uriIndicator'; type Props = { diff --git a/ui/component/txoList/index.js b/ui/component/txoList/index.js index a3808256c..514f0a2f3 100644 --- a/ui/component/txoList/index.js +++ b/ui/component/txoList/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; import { doOpenModal } from 'redux/actions/app'; +import { doFetchTxoPage, doFetchTransactions, doUpdateTxoPageParams } from 'redux/actions/wallet'; import { selectIsFetchingTxos, selectIsFetchingTransactions, @@ -8,10 +9,7 @@ import { selectTxoPage, selectTxoPageNumber, selectTxoItemCount, - doFetchTxoPage, - doFetchTransactions, - doUpdateTxoPageParams, -} from 'lbry-redux'; +} from 'redux/selectors/wallet'; import { withRouter } from 'react-router'; import TxoList from './view'; diff --git a/ui/component/txoList/view.jsx b/ui/component/txoList/view.jsx index b7ec6a466..1f98abb42 100644 --- a/ui/component/txoList/view.jsx +++ b/ui/component/txoList/view.jsx @@ -2,7 +2,7 @@ import * as ICONS from 'constants/icons'; import React, { useEffect } from 'react'; import { withRouter } from 'react-router'; -import { TXO_LIST as TXO } from 'lbry-redux'; +import * as TXO from 'constants/txo_list'; import TransactionListTable from 'component/transactionListTable'; import Paginate from 'component/common/paginate'; import { FormField } from 'component/common/form-components/form-field'; diff --git a/ui/component/uriIndicator/index.js b/ui/component/uriIndicator/index.js index 3b64b7522..e18fbf192 100644 --- a/ui/component/uriIndicator/index.js +++ b/ui/component/uriIndicator/index.js @@ -1,5 +1,7 @@ import { connect } from 'react-redux'; -import { normalizeURI, doResolveUri, makeSelectIsUriResolving, makeSelectClaimForUri } from 'lbry-redux'; +import { normalizeURI } from 'util/lbryURI'; +import { doResolveUri } from 'redux/actions/claims'; +import { makeSelectIsUriResolving, makeSelectClaimForUri } from 'redux/selectors/claims'; import UriIndicator from './view'; const select = (state, props) => ({ @@ -8,11 +10,8 @@ const select = (state, props) => ({ uri: normalizeURI(props.uri), }); -const perform = dispatch => ({ - resolveUri: uri => dispatch(doResolveUri(uri)), +const perform = (dispatch) => ({ + resolveUri: (uri) => dispatch(doResolveUri(uri)), }); -export default connect( - select, - perform -)(UriIndicator); +export default connect(select, perform)(UriIndicator); diff --git a/ui/component/userChannelFollowIntro/view.jsx b/ui/component/userChannelFollowIntro/view.jsx index 409ddba43..559bc93b5 100644 --- a/ui/component/userChannelFollowIntro/view.jsx +++ b/ui/component/userChannelFollowIntro/view.jsx @@ -3,7 +3,7 @@ import React, { useEffect } from 'react'; import ClaimListDiscover from 'component/claimListDiscover'; import * as CS from 'constants/claim_search'; import Nag from 'component/common/nag'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; import Button from 'component/button'; import Card from 'component/common/card'; import { AUTO_FOLLOW_CHANNELS, CUSTOM_HOMEPAGE, SIMPLE_SITE, SITE_NAME } from 'config'; diff --git a/ui/component/userEmailNew/index.js b/ui/component/userEmailNew/index.js index 9060123cd..067b9ce53 100644 --- a/ui/component/userEmailNew/index.js +++ b/ui/component/userEmailNew/index.js @@ -6,7 +6,8 @@ import { selectEmailAlreadyExists, selectUser, } from 'redux/selectors/user'; -import { DAEMON_SETTINGS, SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; +import * as DAEMON_SETTINGS from 'constants/daemon_settings'; import { doSetWalletSyncPreference, doSetDaemonSetting } from 'redux/actions/settings'; import { selectDaemonSettings, makeSelectClientSetting } from 'redux/selectors/settings'; import UserEmailNew from './view'; diff --git a/ui/component/userFirstChannel/index.js b/ui/component/userFirstChannel/index.js index cc7639d14..f457de2d9 100644 --- a/ui/component/userFirstChannel/index.js +++ b/ui/component/userFirstChannel/index.js @@ -1,9 +1,10 @@ import { connect } from 'react-redux'; import { selectUser, selectEmailToVerify } from 'redux/selectors/user'; -import { doCreateChannel, selectCreatingChannel, selectMyChannelClaims, selectCreateChannelError } from 'lbry-redux'; +import { selectCreatingChannel, selectMyChannelClaims, selectCreateChannelError } from 'redux/selectors/claims'; +import { doCreateChannel } from 'redux/actions/claims'; import UserFirstChannel from './view'; -const select = state => ({ +const select = (state) => ({ email: selectEmailToVerify(state), user: selectUser(state), channels: selectMyChannelClaims(state), @@ -11,7 +12,7 @@ const select = state => ({ createChannelError: selectCreateChannelError(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)), }); diff --git a/ui/component/userFirstChannel/view.jsx b/ui/component/userFirstChannel/view.jsx index 0a431b3ce..4b8201a51 100644 --- a/ui/component/userFirstChannel/view.jsx +++ b/ui/component/userFirstChannel/view.jsx @@ -1,6 +1,6 @@ // @flow import React, { useState } from 'react'; -import { isNameValid } from 'lbry-redux'; +import { isNameValid } from 'util/lbryURI'; import Button from 'component/button'; import { Form, FormField } from 'component/common/form'; import { INVALID_NAME_ERROR } from 'constants/claim'; @@ -34,7 +34,7 @@ function UserFirstChannel(props: Props) { const [nameError, setNameError] = useState(undefined); function handleCreateChannel() { - createChannel(`@${channel}`, DEFAULT_BID_FOR_FIRST_CHANNEL).then(channelClaim => { + createChannel(`@${channel}`, DEFAULT_BID_FOR_FIRST_CHANNEL).then((channelClaim) => { if (channelClaim) { analytics.apiLogPublish(channelClaim); } diff --git a/ui/component/userSignUp/index.js b/ui/component/userSignUp/index.js index 758b5da58..e93eafdd4 100644 --- a/ui/component/userSignUp/index.js +++ b/ui/component/userSignUp/index.js @@ -12,13 +12,9 @@ import { selectUser, selectAccessToken, } from 'redux/selectors/user'; -import { - selectMyChannelClaims, - selectBalance, - selectFetchingMyChannels, - selectCreatingChannel, - SETTINGS, -} from 'lbry-redux'; +import { selectMyChannelClaims, selectFetchingMyChannels, selectCreatingChannel } from 'redux/selectors/claims'; +import { selectBalance } from 'redux/selectors/wallet'; +import * as SETTINGS from 'constants/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings'; import { selectInterestedInYoutubeSync } from 'redux/selectors/app'; import { doToggleInterestedInYoutubeSync } from 'redux/actions/app'; diff --git a/ui/component/userSignUp/view.jsx b/ui/component/userSignUp/view.jsx index 0fca417c4..e7a0c9a21 100644 --- a/ui/component/userSignUp/view.jsx +++ b/ui/component/userSignUp/view.jsx @@ -1,5 +1,6 @@ // @flow import * as PAGES from 'constants/pages'; +import * as SETTINGS from 'constants/settings'; import React from 'react'; import classnames from 'classnames'; import { useHistory } from 'react-router'; @@ -11,7 +12,6 @@ import UserTagFollowIntro from 'component/userTagFollowIntro'; import YoutubeSync from 'page/youtubeSync'; import { DEFAULT_BID_FOR_FIRST_CHANNEL } from 'component/userFirstChannel/view'; import { YOUTUBE_STATUSES } from 'lbryinc'; -import { SETTINGS } from 'lbry-redux'; import REWARDS from 'rewards'; import UserVerify from 'component/userVerify'; import Spinner from 'component/spinner'; diff --git a/ui/component/videoDuration/index.js b/ui/component/videoDuration/index.js index 0dc7bd2b0..e7cb5dfb8 100644 --- a/ui/component/videoDuration/index.js +++ b/ui/component/videoDuration/index.js @@ -1,12 +1,9 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; import VideoDuration from './view'; const select = (state, props) => ({ claim: makeSelectClaimForUri(props.uri)(state), }); -export default connect( - select, - null -)(VideoDuration); +export default connect(select, null)(VideoDuration); diff --git a/ui/component/viewers/appViewer/index.js b/ui/component/viewers/appViewer/index.js index 81a29a47d..2b0c280dc 100644 --- a/ui/component/viewers/appViewer/index.js +++ b/ui/component/viewers/appViewer/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri, makeSelectContentTypeForUri } from 'lbry-redux'; +import { makeSelectClaimForUri, makeSelectContentTypeForUri } from 'redux/selectors/claims'; import AppViewer from './view'; const select = (state, props) => ({ @@ -7,9 +7,6 @@ const select = (state, props) => ({ contentType: makeSelectContentTypeForUri(props.uri)(state), }); -const perform = dispatch => ({}); +const perform = (dispatch) => ({}); -export default connect( - select, - perform -)(AppViewer); +export default connect(select, perform)(AppViewer); diff --git a/ui/component/viewers/videoViewer/index.js b/ui/component/viewers/videoViewer/index.js index d9b6ddce1..5cccfc102 100644 --- a/ui/component/viewers/videoViewer/index.js +++ b/ui/component/viewers/videoViewer/index.js @@ -1,12 +1,11 @@ import { connect } from 'react-redux'; +import { makeSelectClaimForUri, makeSelectThumbnailForUri } from 'redux/selectors/claims'; import { - makeSelectClaimForUri, - makeSelectThumbnailForUri, - SETTINGS, - COLLECTIONS_CONSTS, makeSelectNextUrlForCollectionAndUrl, makeSelectPreviousUrlForCollectionAndUrl, -} from 'lbry-redux'; +} from 'redux/selectors/collections'; +import * as SETTINGS from 'constants/settings'; +import * as COLLECTIONS_CONSTS from 'constants/collections'; import { doChangeVolume, doChangeMute, diff --git a/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js b/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js index dd69f41b6..7b655c559 100644 --- a/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js +++ b/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js @@ -1,7 +1,7 @@ // Created by xander on 6/21/2021 import videojs from 'video.js'; -import RecSys from 'recsys'; +import RecSys from 'extras/recsys/recsys'; const VERSION = '0.0.1'; /* RecSys */ diff --git a/ui/component/walletAddress/index.js b/ui/component/walletAddress/index.js index 13cdafd47..8be3d6baa 100644 --- a/ui/component/walletAddress/index.js +++ b/ui/component/walletAddress/index.js @@ -1,21 +1,17 @@ import { connect } from 'react-redux'; -import { doCheckAddressIsMine, doGetNewAddress, selectReceiveAddress, selectGettingNewAddress } from 'lbry-redux'; +import { selectReceiveAddress, selectGettingNewAddress } from 'redux/selectors/wallet'; +import { doCheckAddressIsMine, doGetNewAddress } from 'redux/actions/wallet'; import WalletAddress from './view'; import { withRouter } from 'react-router'; -const select = state => ({ +const select = (state) => ({ receiveAddress: selectReceiveAddress(state), gettingNewAddress: selectGettingNewAddress(state), }); -const perform = dispatch => ({ - checkAddressIsMine: address => dispatch(doCheckAddressIsMine(address)), +const perform = (dispatch) => ({ + checkAddressIsMine: (address) => dispatch(doCheckAddressIsMine(address)), getNewAddress: () => dispatch(doGetNewAddress()), }); -export default withRouter( - connect( - select, - perform - )(WalletAddress) -); +export default withRouter(connect(select, perform)(WalletAddress)); diff --git a/ui/component/walletBalance/index.js b/ui/component/walletBalance/index.js index 83b0a30cf..06422bcf1 100644 --- a/ui/component/walletBalance/index.js +++ b/ui/component/walletBalance/index.js @@ -6,19 +6,18 @@ import { selectTipsBalance, selectIsFetchingUtxoCounts, selectUtxoCounts, - doFetchUtxoCounts, - doUtxoConsolidate, selectIsConsolidatingUtxos, selectIsMassClaimingTips, selectPendingConsolidateTxid, selectPendingMassClaimTxid, -} from 'lbry-redux'; +} from 'redux/selectors/wallet'; +import { doFetchUtxoCounts, doUtxoConsolidate } from 'redux/actions/wallet'; import { doOpenModal } from 'redux/actions/app'; import { selectSyncHash } from 'redux/selectors/sync'; import { selectClaimedRewards } from 'redux/selectors/rewards'; import WalletBalance from './view'; -const select = state => ({ +const select = (state) => ({ balance: selectBalance(state), claimsBalance: selectClaimsBalance(state) || 0, supportsBalance: selectSupportsBalance(state) || 0, diff --git a/ui/component/walletSend/index.js b/ui/component/walletSend/index.js index 3f13b15f2..7b83eb34f 100644 --- a/ui/component/walletSend/index.js +++ b/ui/component/walletSend/index.js @@ -1,11 +1,12 @@ import { connect } from 'react-redux'; -import { selectBalance, selectMyChannelClaims, makeSelectClaimForUri } from 'lbry-redux'; +import { selectBalance } from 'redux/selectors/wallet'; +import { selectMyChannelClaims, makeSelectClaimForUri } from 'redux/selectors/claims'; import { doOpenModal } from 'redux/actions/app'; import WalletSend from './view'; import { withRouter } from 'react-router'; import { selectToast } from 'redux/selectors/notifications'; -const perform = dispatch => ({ +const perform = (dispatch) => ({ openModal: (modal, props) => dispatch(doOpenModal(modal, props)), }); diff --git a/ui/component/walletSendTip/index.js b/ui/component/walletSendTip/index.js index b09425fe4..5cf567057 100644 --- a/ui/component/walletSendTip/index.js +++ b/ui/component/walletSendTip/index.js @@ -1,14 +1,13 @@ import { connect } from 'react-redux'; import { - doSendTip, makeSelectTitleForUri, makeSelectClaimForUri, - selectIsSendingSupport, - selectBalance, - SETTINGS, makeSelectClaimIsMine, selectFetchingMyChannels, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { selectBalance, selectIsSendingSupport } from 'redux/selectors/wallet'; +import { doSendTip } from 'redux/actions/wallet'; +import * as SETTINGS from 'constants/settings'; import WalletSendTip from './view'; import { doOpenModal, doHideModal } from 'redux/actions/app'; import { withRouter } from 'react-router'; @@ -31,7 +30,7 @@ const select = (state, props) => ({ isAuthenticated: Boolean(selectUserVerifiedEmail(state)), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ openModal: (modal, props) => dispatch(doOpenModal(modal, props)), closeModal: () => dispatch(doHideModal()), sendSupport: (params, isSupport) => dispatch(doSendTip(params, isSupport)), diff --git a/ui/component/walletSendTip/view.jsx b/ui/component/walletSendTip/view.jsx index d7ad01e9a..2f843c9d9 100644 --- a/ui/component/walletSendTip/view.jsx +++ b/ui/component/walletSendTip/view.jsx @@ -12,7 +12,7 @@ import Card from 'component/common/card'; import classnames from 'classnames'; import ChannelSelector from 'component/channelSelector'; import LbcSymbol from 'component/common/lbc-symbol'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; import usePersistedState from 'effects/use-persisted-state'; import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp'; import { getStripeEnvironment } from 'util/stripe'; @@ -36,7 +36,7 @@ type Props = { claim: StreamClaim, isPending: boolean, isSupport: boolean, - sendSupport: (SupportParams, boolean) => void, // function that comes from lbry-redux + sendSupport: (SupportParams, boolean) => void, closeModal: () => void, balance: number, fetchingChannels: boolean, diff --git a/ui/component/walletSpendableBalanceHelp/index.js b/ui/component/walletSpendableBalanceHelp/index.js index b68d99ab5..7fdb72f33 100644 --- a/ui/component/walletSpendableBalanceHelp/index.js +++ b/ui/component/walletSpendableBalanceHelp/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { selectBalance } from 'lbry-redux'; +import { selectBalance } from 'redux/selectors/wallet'; import WalletSpendableBalanceHelp from './view'; const select = (state) => ({ diff --git a/ui/component/walletSwap/index.js b/ui/component/walletSwap/index.js index e9f520d73..5fbb8cc15 100644 --- a/ui/component/walletSwap/index.js +++ b/ui/component/walletSwap/index.js @@ -6,7 +6,8 @@ import { doAddCoinSwap, doQueryCoinSwapStatus } from 'redux/actions/coinSwap'; import { doToast } from 'redux/actions/notifications'; import { selectCoinSwaps } from 'redux/selectors/coinSwap'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; -import { selectReceiveAddress, doGetNewAddress, doCheckAddressIsMine } from 'lbry-redux'; +import { doGetNewAddress, doCheckAddressIsMine } from 'redux/actions/wallet'; +import { selectReceiveAddress } from 'redux/selectors/wallet'; const select = (state, props) => ({ receiveAddress: selectReceiveAddress(state), diff --git a/ui/component/walletTipAmountSelector/index.js b/ui/component/walletTipAmountSelector/index.js index f811d9502..4e6fd9ac0 100644 --- a/ui/component/walletTipAmountSelector/index.js +++ b/ui/component/walletTipAmountSelector/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { selectBalance } from 'lbry-redux'; +import { selectBalance } from 'redux/selectors/wallet'; import WalletTipAmountSelector from './view'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; diff --git a/ui/component/wunderbarSuggestion/index.js b/ui/component/wunderbarSuggestion/index.js index 10da10419..6b5b1fd19 100644 --- a/ui/component/wunderbarSuggestion/index.js +++ b/ui/component/wunderbarSuggestion/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri, makeSelectIsUriResolving } from 'lbry-redux'; +import { makeSelectClaimForUri, makeSelectIsUriResolving } from 'redux/selectors/claims'; import WunderbarSuggestion from './view'; const select = (state, props) => ({ diff --git a/ui/component/wunderbarSuggestions/index.js b/ui/component/wunderbarSuggestions/index.js index e849fd5a1..7964b9ec7 100644 --- a/ui/component/wunderbarSuggestions/index.js +++ b/ui/component/wunderbarSuggestions/index.js @@ -4,7 +4,7 @@ import { selectLanguage, selectShowMatureContent } from 'redux/selectors/setting import { doToast } from 'redux/actions/notifications'; import { doOpenModal, doHideModal } from 'redux/actions/app'; import { withRouter } from 'react-router'; -import { doResolveUris } from 'lbry-redux'; +import { doResolveUris } from 'redux/actions/claims'; import analytics from 'analytics'; import Wunderbar from './view'; diff --git a/ui/component/wunderbarSuggestions/view.jsx b/ui/component/wunderbarSuggestions/view.jsx index 79510c3fa..cdb0bbd61 100644 --- a/ui/component/wunderbarSuggestions/view.jsx +++ b/ui/component/wunderbarSuggestions/view.jsx @@ -6,7 +6,7 @@ import * as ICONS from 'constants/icons'; import React from 'react'; import classnames from 'classnames'; import Icon from 'component/common/icon'; -import { isURIValid, normalizeURI, parseURI } from 'lbry-redux'; +import { isURIValid, normalizeURI, parseURI } from 'util/lbryURI'; import { Combobox, ComboboxInput, ComboboxPopover, ComboboxList, ComboboxOption } from '@reach/combobox'; // import '@reach/combobox/styles.css'; --> 'scss/third-party.scss' import useLighthouse from 'effects/use-lighthouse'; diff --git a/ui/component/wunderbarTopSuggestion/index.js b/ui/component/wunderbarTopSuggestion/index.js index 7c8ec3348..8dd77e369 100644 --- a/ui/component/wunderbarTopSuggestion/index.js +++ b/ui/component/wunderbarTopSuggestion/index.js @@ -1,11 +1,11 @@ import { connect } from 'react-redux'; import { - doResolveUris, makeSelectClaimForUri, makeSelectIsUriResolving, makeSelectTagInClaimOrChannelForUri, - parseURI, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { doResolveUris } from 'redux/actions/claims'; +import { parseURI } from 'util/lbryURI'; import { makeSelectWinningUriForQuery } from 'redux/selectors/search'; import WunderbarTopSuggestion from './view'; import { PREFERENCE_EMBED } from 'constants/tags'; diff --git a/ui/component/youtubeTransferStatus/view.jsx b/ui/component/youtubeTransferStatus/view.jsx index e882cb841..069df853c 100644 --- a/ui/component/youtubeTransferStatus/view.jsx +++ b/ui/component/youtubeTransferStatus/view.jsx @@ -7,7 +7,7 @@ import Button from 'component/button'; import ClaimPreview from 'component/claimPreview'; import Card from 'component/common/card'; import { YOUTUBE_STATUSES } from 'lbryinc'; -import { buildURI } from 'lbry-redux'; +import { buildURI } from 'util/lbryURI'; import Spinner from 'component/spinner'; import Icon from 'component/common/icon'; import I18nMessage from 'component/i18nMessage'; diff --git a/ui/component/yrblWalletEmpty/index.js b/ui/component/yrblWalletEmpty/index.js index cf94adba5..a71e4800d 100644 --- a/ui/component/yrblWalletEmpty/index.js +++ b/ui/component/yrblWalletEmpty/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { selectBalance } from 'lbry-redux'; +import { selectBalance } from 'redux/selectors/wallet'; import Wallet from './view'; const select = (state) => ({ diff --git a/ui/constants/abandon_states.js b/ui/constants/abandon_states.js new file mode 100644 index 000000000..3761c9e58 --- /dev/null +++ b/ui/constants/abandon_states.js @@ -0,0 +1,4 @@ +export const PENDING = 'pending'; +export const DONE = 'done'; +export const READY = 'ready'; +export const ERROR = 'error'; diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js index 4460028ed..9fbeeecd1 100644 --- a/ui/constants/action_types.js +++ b/ui/constants/action_types.js @@ -71,6 +71,51 @@ export const SEND_TRANSACTION_FAILED = 'SEND_TRANSACTION_FAILED'; export const SUPPORT_TRANSACTION_STARTED = 'SUPPORT_TRANSACTION_STARTED'; export const SUPPORT_TRANSACTION_COMPLETED = 'SUPPORT_TRANSACTION_COMPLETED'; export const SUPPORT_TRANSACTION_FAILED = 'SUPPORT_TRANSACTION_FAILED'; +export const FETCH_TXO_PAGE_STARTED = 'FETCH_TXO_PAGE_STARTED'; +export const FETCH_TXO_PAGE_COMPLETED = 'FETCH_TXO_PAGE_COMPLETED'; +export const FETCH_TXO_PAGE_FAILED = 'FETCH_TXO_PAGE_FAILED'; +export const UPDATE_TXO_FETCH_PARAMS = 'UPDATE_TXO_FETCH_PARAMS'; +export const FETCH_SUPPORTS_STARTED = 'FETCH_SUPPORTS_STARTED'; +export const FETCH_SUPPORTS_COMPLETED = 'FETCH_SUPPORTS_COMPLETED'; +export const ABANDON_SUPPORT_STARTED = 'ABANDON_SUPPORT_STARTED'; +export const ABANDON_SUPPORT_COMPLETED = 'ABANDON_SUPPORT_COMPLETED'; +export const ABANDON_CLAIM_SUPPORT_STARTED = 'ABANDON_CLAIM_SUPPORT_STARTED'; +export const ABANDON_CLAIM_SUPPORT_COMPLETED = 'ABANDON_CLAIM_SUPPORT_COMPLETED'; +export const ABANDON_CLAIM_SUPPORT_FAILED = 'ABANDON_CLAIM_SUPPORT_FAILED'; +export const ABANDON_CLAIM_SUPPORT_PREVIEW = 'ABANDON_CLAIM_SUPPORT_PREVIEW'; +export const PENDING_SUPPORTS_UPDATED = 'PENDING_SUPPORTS_UPDATED'; +export const UPDATE_TOTAL_BALANCE = 'UPDATE_TOTAL_BALANCE'; +export const CLEAR_SUPPORT_TRANSACTION = 'CLEAR_SUPPORT_TRANSACTION'; +export const WALLET_ENCRYPT_START = 'WALLET_ENCRYPT_START'; +export const WALLET_ENCRYPT_COMPLETED = 'WALLET_ENCRYPT_COMPLETED'; +export const WALLET_ENCRYPT_FAILED = 'WALLET_ENCRYPT_FAILED'; +export const WALLET_UNLOCK_START = 'WALLET_UNLOCK_START'; +export const WALLET_UNLOCK_COMPLETED = 'WALLET_UNLOCK_COMPLETED'; +export const WALLET_UNLOCK_FAILED = 'WALLET_UNLOCK_FAILED'; +export const WALLET_DECRYPT_START = 'WALLET_DECRYPT_START'; +export const WALLET_DECRYPT_COMPLETED = 'WALLET_DECRYPT_COMPLETED'; +export const WALLET_DECRYPT_FAILED = 'WALLET_DECRYPT_FAILED'; +export const WALLET_LOCK_START = 'WALLET_LOCK_START'; +export const WALLET_LOCK_COMPLETED = 'WALLET_LOCK_COMPLETED'; +export const WALLET_LOCK_FAILED = 'WALLET_LOCK_FAILED'; +export const WALLET_STATUS_START = 'WALLET_STATUS_START'; +export const WALLET_STATUS_COMPLETED = 'WALLET_STATUS_COMPLETED'; +export const WALLET_RESTART = 'WALLET_RESTART'; +export const WALLET_RESTART_COMPLETED = 'WALLET_RESTART_COMPLETED'; +export const SET_TRANSACTION_LIST_FILTER = 'SET_TRANSACTION_LIST_FILTER'; +export const UPDATE_CURRENT_HEIGHT = 'UPDATE_CURRENT_HEIGHT'; +export const SET_DRAFT_TRANSACTION_AMOUNT = 'SET_DRAFT_TRANSACTION_AMOUNT'; +export const SET_DRAFT_TRANSACTION_ADDRESS = 'SET_DRAFT_TRANSACTION_ADDRESS'; +export const FETCH_UTXO_COUNT_STARTED = 'FETCH_UTXO_COUNT_STARTED'; +export const FETCH_UTXO_COUNT_COMPLETED = 'FETCH_UTXO_COUNT_COMPLETED'; +export const FETCH_UTXO_COUNT_FAILED = 'FETCH_UTXO_COUNT_FAILED'; +export const TIP_CLAIM_MASS_STARTED = 'TIP_CLAIM_MASS_STARTED'; +export const TIP_CLAIM_MASS_COMPLETED = 'TIP_CLAIM_MASS_COMPLETED'; +export const TIP_CLAIM_MASS_FAILED = 'TIP_CLAIM_MASS_FAILED'; +export const DO_UTXO_CONSOLIDATE_STARTED = 'DO_UTXO_CONSOLIDATE_STARTED'; +export const DO_UTXO_CONSOLIDATE_COMPLETED = 'DO_UTXO_CONSOLIDATE_COMPLETED'; +export const DO_UTXO_CONSOLIDATE_FAILED = 'DO_UTXO_CONSOLIDATE_FAILED'; +export const PENDING_CONSOLIDATED_TXOS_UPDATED = 'PENDING_CONSOLIDATED_TXOS_UPDATED'; // Claims export const FETCH_FEATURED_CONTENT_STARTED = 'FETCH_FEATURED_CONTENT_STARTED'; @@ -102,6 +147,59 @@ export const RECOMMENDATION_UPDATED = 'RECOMMENDATION_UPDATED'; export const RECOMMENDATION_CLICKED = 'RECOMMENDATION_CLICKED'; export const TOGGLE_LOOP_LIST = 'TOGGLE_LOOP_LIST'; export const TOGGLE_SHUFFLE_LIST = 'TOGGLE_SHUFFLE_LIST'; +export const FETCH_CHANNEL_LIST_FAILED = 'FETCH_CHANNEL_LIST_FAILED'; +export const FETCH_COLLECTION_LIST_STARTED = 'FETCH_COLLECTION_LIST_STARTED'; +export const FETCH_COLLECTION_LIST_COMPLETED = 'FETCH_COLLECTION_LIST_COMPLETED'; +export const FETCH_COLLECTION_LIST_FAILED = 'FETCH_COLLECTION_LIST_FAILED'; +export const CREATE_CHANNEL_FAILED = 'CREATE_CHANNEL_FAILED'; +export const UPDATE_CHANNEL_STARTED = 'UPDATE_CHANNEL_STARTED'; +export const UPDATE_CHANNEL_COMPLETED = 'UPDATE_CHANNEL_COMPLETED'; +export const UPDATE_CHANNEL_FAILED = 'UPDATE_CHANNEL_FAILED'; +export const IMPORT_CHANNEL_STARTED = 'IMPORT_CHANNEL_STARTED'; +export const IMPORT_CHANNEL_COMPLETED = 'IMPORT_CHANNEL_COMPLETED'; +export const IMPORT_CHANNEL_FAILED = 'IMPORT_CHANNEL_FAILED'; +export const CLEAR_CHANNEL_ERRORS = 'CLEAR_CHANNEL_ERRORS'; +export const CLAIM_SEARCH_STARTED = 'CLAIM_SEARCH_STARTED'; +export const CLAIM_SEARCH_COMPLETED = 'CLAIM_SEARCH_COMPLETED'; +export const CLAIM_SEARCH_FAILED = 'CLAIM_SEARCH_FAILED'; +export const CLAIM_SEARCH_BY_TAGS_STARTED = 'CLAIM_SEARCH_BY_TAGS_STARTED'; +export const CLAIM_SEARCH_BY_TAGS_COMPLETED = 'CLAIM_SEARCH_BY_TAGS_COMPLETED'; +export const CLAIM_SEARCH_BY_TAGS_FAILED = 'CLAIM_SEARCH_BY_TAGS_FAILED'; +export const CLAIM_REPOST_STARTED = 'CLAIM_REPOST_STARTED'; +export const CLAIM_REPOST_COMPLETED = 'CLAIM_REPOST_COMPLETED'; +export const CLAIM_REPOST_FAILED = 'CLAIM_REPOST_FAILED'; +export const CLEAR_REPOST_ERROR = 'CLEAR_REPOST_ERROR'; +export const CHECK_PUBLISH_NAME_STARTED = 'CHECK_PUBLISH_NAME_STARTED'; +export const CHECK_PUBLISH_NAME_COMPLETED = 'CHECK_PUBLISH_NAME_COMPLETED'; +export const UPDATE_PENDING_CLAIMS = 'UPDATE_PENDING_CLAIMS'; +export const UPDATE_CONFIRMED_CLAIMS = 'UPDATE_CONFIRMED_CLAIMS'; +export const ADD_FILES_REFLECTING = 'ADD_FILES_REFLECTING'; +export const UPDATE_FILES_REFLECTING = 'UPDATE_FILES_REFLECTING'; +export const TOGGLE_CHECKING_REFLECTING = 'TOGGLE_CHECKING_REFLECTING'; +export const TOGGLE_CHECKING_PENDING = 'TOGGLE_CHECKING_PENDING'; +export const PURCHASE_LIST_STARTED = 'PURCHASE_LIST_STARTED'; +export const PURCHASE_LIST_COMPLETED = 'PURCHASE_LIST_COMPLETED'; +export const PURCHASE_LIST_FAILED = 'PURCHASE_LIST_FAILED'; +export const COLLECTION_PUBLISH_STARTED = 'COLLECTION_PUBLISH_STARTED'; +export const COLLECTION_PUBLISH_COMPLETED = 'COLLECTION_PUBLISH_COMPLETED'; +export const COLLECTION_PUBLISH_FAILED = 'COLLECTION_PUBLISH_FAILED'; +export const COLLECTION_PUBLISH_UPDATE_STARTED = 'COLLECTION_PUBLISH_UPDATE_STARTED'; +export const COLLECTION_PUBLISH_UPDATE_COMPLETED = 'COLLECTION_PUBLISH_UPDATE_COMPLETED'; +export const COLLECTION_PUBLISH_UPDATE_FAILED = 'COLLECTION_PUBLISH_UPDATE_FAILED'; +export const COLLECTION_PUBLISH_ABANDON_STARTED = 'COLLECTION_PUBLISH_ABANDON_STARTED'; +export const COLLECTION_PUBLISH_ABANDON_COMPLETED = 'COLLECTION_PUBLISH_ABANDON_COMPLETED'; +export const COLLECTION_PUBLISH_ABANDON_FAILED = 'COLLECTION_PUBLISH_ABANDON_FAILED'; +export const CLEAR_COLLECTION_ERRORS = 'CLEAR_COLLECTION_ERRORS'; +export const COLLECTION_ITEMS_RESOLVE_STARTED = 'COLLECTION_ITEMS_RESOLVE_STARTED'; +export const COLLECTION_ITEMS_RESOLVE_COMPLETED = 'COLLECTION_ITEMS_RESOLVE_COMPLETED'; +export const COLLECTION_ITEMS_RESOLVE_FAILED = 'COLLECTION_ITEMS_RESOLVE_FAILED'; +export const COLLECTION_NEW = 'COLLECTION_NEW'; +export const COLLECTION_DELETE = 'COLLECTION_DELETE'; +export const COLLECTION_PENDING = 'COLLECTION_PENDING'; +export const COLLECTION_EDIT = 'COLLECTION_EDIT'; +export const COLLECTION_COPY = 'COLLECTION_COPY'; +export const COLLECTION_SAVE = 'COLLECTION_SAVE'; +export const COLLECTION_ERROR = 'COLLECTION_ERROR'; // Files export const FILE_LIST_STARTED = 'FILE_LIST_STARTED'; @@ -142,6 +240,9 @@ export const UPDATE_IS_NIGHT = 'UPDATE_IS_NIGHT'; export const FINDING_FFMPEG_STARTED = 'FINDING_FFMPEG_STARTED'; export const FINDING_FFMPEG_COMPLETED = 'FINDING_FFMPEG_COMPLETED'; export const SYNC_CLIENT_SETTINGS = 'SYNC_CLIENT_SETTINGS'; +export const DAEMON_STATUS_RECEIVED = 'DAEMON_STATUS_RECEIVED'; +export const SHARED_PREFERENCE_SET = 'SHARED_PREFERENCE_SET'; +export const SAVE_CUSTOM_WALLET_SERVERS = 'SAVE_CUSTOM_WALLET_SERVERS'; // User export const AUTHENTICATION_STARTED = 'AUTHENTICATION_STARTED'; @@ -196,6 +297,7 @@ export const USER_SET_REFERRER_STARTED = 'USER_SET_REFERRER_STARTED'; export const USER_SET_REFERRER_SUCCESS = 'USER_SET_REFERRER_SUCCESS'; export const USER_SET_REFERRER_FAILURE = 'USER_SET_REFERRER_FAILURE'; export const USER_SET_REFERRER_RESET = 'USER_SET_REFERRER_RESET'; +export const USER_EMAIL_VERIFY_RETRY = 'USER_EMAIL_VERIFY_RETRY'; // Rewards export const FETCH_REWARDS_STARTED = 'FETCH_REWARDS_STARTED'; @@ -254,6 +356,11 @@ export const CREATE_TOAST = 'CREATE_TOAST'; export const DISMISS_TOAST = 'DISMISS_TOAST'; export const CREATE_ERROR = 'CREATE_ERROR'; export const DISMISS_ERROR = 'DISMISS_ERROR'; +export const CREATE_NOTIFICATION = 'CREATE_NOTIFICATION'; +export const EDIT_NOTIFICATION = 'EDIT_NOTIFICATION'; +export const DELETE_NOTIFICATION = 'DELETE_NOTIFICATION'; +export const DISMISS_NOTIFICATION = 'DISMISS_NOTIFICATION'; +export const FETCH_DATE = 'FETCH_DATE'; // Comments export const COMMENT_LIST_STARTED = 'COMMENT_LIST_STARTED'; @@ -341,6 +448,7 @@ export const SYNC_APPLY_FAILED = 'SYNC_APPLY_FAILED'; export const SYNC_APPLY_BAD_PASSWORD = 'SYNC_APPLY_BAD_PASSWORD'; export const SYNC_RESET = 'SYNC_RESET'; export const SYNC_FATAL_ERROR = 'SYNC_FATAL_ERROR'; +export const USER_STATE_POPULATE = 'USER_STATE_POPULATE'; export const REACTIONS_LIST_STARTED = 'REACTIONS_LIST_STARTED'; export const REACTIONS_LIST_FAILED = 'REACTIONS_LIST_FAILED'; @@ -364,3 +472,31 @@ export const FETCH_ACTIVE_LIVESTREAMS_STARTED = 'FETCH_ACTIVE_LIVESTREAMS_STARTE export const FETCH_ACTIVE_LIVESTREAMS_FAILED = 'FETCH_ACTIVE_LIVESTREAMS_FAILED'; export const FETCH_ACTIVE_LIVESTREAMS_SKIPPED = 'FETCH_ACTIVE_LIVESTREAMS_SKIPPED'; export const FETCH_ACTIVE_LIVESTREAMS_COMPLETED = 'FETCH_ACTIVE_LIVESTREAMS_COMPLETED'; + +// Blacklist +export const FETCH_BLACK_LISTED_CONTENT_STARTED = 'FETCH_BLACK_LISTED_CONTENT_STARTED'; +export const FETCH_BLACK_LISTED_CONTENT_COMPLETED = 'FETCH_BLACK_LISTED_CONTENT_COMPLETED'; +export const FETCH_BLACK_LISTED_CONTENT_FAILED = 'FETCH_BLACK_LISTED_CONTENT_FAILED'; +export const BLACK_LISTED_CONTENT_SUBSCRIBE = 'BLACK_LISTED_CONTENT_SUBSCRIBE'; + +// Filtered list +export const FETCH_FILTERED_CONTENT_STARTED = 'FETCH_FILTERED_CONTENT_STARTED'; +export const FETCH_FILTERED_CONTENT_COMPLETED = 'FETCH_FILTERED_CONTENT_COMPLETED'; +export const FETCH_FILTERED_CONTENT_FAILED = 'FETCH_FILTERED_CONTENT_FAILED'; +export const FILTERED_CONTENT_SUBSCRIBE = 'FILTERED_CONTENT_SUBSCRIBE'; + +// Stats +export const FETCH_VIEW_COUNT_STARTED = 'FETCH_VIEW_COUNT_STARTED'; +export const FETCH_VIEW_COUNT_FAILED = 'FETCH_VIEW_COUNT_FAILED'; +export const FETCH_VIEW_COUNT_COMPLETED = 'FETCH_VIEW_COUNT_COMPLETED'; +export const FETCH_SUB_COUNT_STARTED = 'FETCH_SUB_COUNT_STARTED'; +export const FETCH_SUB_COUNT_FAILED = 'FETCH_SUB_COUNT_FAILED'; +export const FETCH_SUB_COUNT_COMPLETED = 'FETCH_SUB_COUNT_COMPLETED'; + +// Lbry.tv +export const UPDATE_UPLOAD_PROGRESS = 'UPDATE_UPLOAD_PROGRESS'; + +// User +export const GENERATE_AUTH_TOKEN_FAILURE = 'GENERATE_AUTH_TOKEN_FAILURE'; +export const GENERATE_AUTH_TOKEN_STARTED = 'GENERATE_AUTH_TOKEN_STARTED'; +export const GENERATE_AUTH_TOKEN_SUCCESS = 'GENERATE_AUTH_TOKEN_SUCCESS'; diff --git a/ui/constants/claim.js b/ui/constants/claim.js index 61214a568..11a812953 100644 --- a/ui/constants/claim.js +++ b/ui/constants/claim.js @@ -1,13 +1,20 @@ +// @flow +import { MATURE_TAGS } from 'constants/tags'; export const MINIMUM_PUBLISH_BID = 0.0001; export const ESTIMATED_FEE = 0.048; // .001 + .001 | .048 + .048 = .1 -export const CHANNEL_ANONYMOUS = 'anonymous'; -export const CHANNEL_NEW = 'new'; -export const PAGE_SIZE = 20; export const MY_CLAIMS_PAGE_SIZE = 10; export const PAGE_PARAM = 'page'; export const PAGE_SIZE_PARAM = 'page_size'; +export const CHANNEL_ANONYMOUS = 'anonymous'; +export const CHANNEL_NEW = 'new'; +export const PAGE_SIZE = 20; +export const LEVEL_1_STAKED_AMOUNT = 0; +export const LEVEL_2_STAKED_AMOUNT = 1; +export const LEVEL_3_STAKED_AMOUNT = 50; +export const LEVEL_4_STAKED_AMOUNT = 250; +export const LEVEL_5_STAKED_AMOUNT = 1000; export const INVALID_NAME_ERROR = __('LBRY names cannot contain spaces or reserved symbols') + ' ' + '(?$#@;:/\\="<>%{}|^~[]`)'; @@ -32,3 +39,68 @@ export const FORCE_CONTENT_TYPE_COMIC = [ 'application/x-cbz', 'application/x-cb7', ]; + +const matureTagMap = MATURE_TAGS.reduce((acc, tag) => ({ ...acc, [tag]: true }), {}); + +export const isClaimNsfw = (claim: Claim): boolean => { + if (!claim) { + throw new Error('No claim passed to isClaimNsfw()'); + } + + if (!claim.value) { + return false; + } + + const tags = claim.value.tags || []; + for (let i = 0; i < tags.length; i += 1) { + const tag = tags[i].toLowerCase(); + if (matureTagMap[tag]) { + return true; + } + } + + return false; +}; + +export function createNormalizedClaimSearchKey(options: { page: number, release_time?: string }) { + // Ignore page because we don't care what the last page searched was, we want everything + // Ignore release_time because that will change depending on when you call claim_search ex: release_time: ">12344567" + const { page: optionToIgnoreForQuery, release_time: anotherToIgnore, ...rest } = options; + const query = JSON.stringify(rest); + return query; +} + +export function concatClaims(claimList: Array = [], concatClaimList: Array = []): Array { + if (!claimList || claimList.length === 0) { + if (!concatClaimList) { + return []; + } + return concatClaimList.slice(); + } + + const claims = claimList.slice(); + concatClaimList.forEach((claim) => { + if (!claims.some((item) => item.claim_id === claim.claim_id)) { + claims.push(claim); + } + }); + + return claims; +} + +export function filterClaims(claims: Array, query: ?string): Array { + if (query) { + const queryMatchRegExp = new RegExp(query, 'i'); + return claims.filter((claim) => { + const { value } = claim; + + return ( + (value.title && value.title.match(queryMatchRegExp)) || + (claim.signing_channel && claim.signing_channel.name.match(queryMatchRegExp)) || + (claim.name && claim.name.match(queryMatchRegExp)) + ); + }); + } + + return claims; +} diff --git a/ui/constants/collections.js b/ui/constants/collections.js index 66d919ba2..41a57702c 100644 --- a/ui/constants/collections.js +++ b/ui/constants/collections.js @@ -1,2 +1,19 @@ -// in repo constants for collections ui +// ui export const ICON_SIZE = 12; + +// see which of these are used +export const COLLECTION_ID = 'lid'; +export const COLLECTION_INDEX = 'linx'; + +export const COL_TYPE_PLAYLIST = 'playlist'; +export const COL_TYPE_CHANNELS = 'channelList'; + +export const WATCH_LATER_ID = 'watchlater'; +export const FAVORITES_ID = 'favorites'; +export const FAVORITE_CHANNELS_ID = 'favoriteChannels'; +export const BUILTIN_LISTS = [WATCH_LATER_ID, FAVORITES_ID, FAVORITE_CHANNELS_ID]; + +export const COL_KEY_EDITED = 'edited'; +export const COL_KEY_UNPUBLISHED = 'unpublished'; +export const COL_KEY_PENDING = 'pending'; +export const COL_KEY_SAVED = 'saved'; diff --git a/ui/constants/daemon_settings.js b/ui/constants/daemon_settings.js new file mode 100644 index 000000000..04e4fd297 --- /dev/null +++ b/ui/constants/daemon_settings.js @@ -0,0 +1,39 @@ +export const ANNOUNCE_HEAD_AND_SD_ONLY = 'announce_head_and_sd_only'; +export const API = 'api'; +export const BLOB_DOWNLOAD_TIMEOUT = 'blob_download_timeout'; +export const BLOB_LRU_CACHE_SIZE = 'blob_lru_cache_size'; +export const BLOCKCHAIN_NAME = 'blockchain_name'; +export const CACHE_TIME = 'cache_time'; +export const COIN_SELECTION_STRATEGY = 'coin_selection_strategy'; +export const COMMENT_SERVER = 'comment_server'; +export const COMPONENTS_TO_SKIP = 'components_to_skip'; +export const CONCURRENT_BLOB_ANNOUNCERS = 'concurrent_blob_announcers'; +export const CONCURRENT_REFLECTOR_UPLOADS = 'concurrent_reflector_uploads'; +export const CONFIG = 'config'; +export const DATA_DIR = 'data_dir'; +export const DOWNLOAD_DIR = 'download_dir'; +export const DOWNLOAD_TIMEOUT = 'download_timeout'; +export const FIXED_PEER_DELAY = 'fixed_peer_delay'; +export const KNOWN_DHT_NODES = 'known_dht_nodes'; +export const LBRYUM_SERVERS = 'lbryum_servers'; +export const MAX_CONNECTIONS_PER_DOWNLOAD = 'max_connections_per_download'; +export const MAX_KEY_FEE = 'max_key_fee'; +export const DEFAULT_WALLET = 'default_wallet'; +export const NETWORK_INTERFACE = 'network_interface'; +export const NODE_RPC_TIMEOUT = 'node_rpc_timeout'; +export const PEER_CONNECT_TIMEOUT = 'peer_connect_timeout'; +export const REFLECT_STREAMS = 'reflect_streams'; +export const REFLECTOR_SERVERS = 'reflector_servers'; +export const S3_HEADERS_DEPTH = 's3_headers_depth'; +export const SAVE_BLOBS = 'save_blobs'; +export const SAVE_FILES = 'save_files'; +export const SHARE_USAGE_DATA = 'share_usage_data'; +export const SPLIT_BUCKETS_UNDER_INDEX = 'split_buckets_under_index'; +export const STREAMING_GET = 'streaming_get'; +export const STREAMING_SERVER = 'streaming_server'; +export const TCP_PORT = 'tcp_port'; +export const TRACK_BANDWIDTH = 'track_bandwidth'; +export const UDP_PORT = 'udp_port'; +export const USE_UPNP = 'use_upnp'; +export const WALLET_DIR = 'wallet_dir'; +export const WALLETS = 'wallets'; diff --git a/ui/constants/errors.js b/ui/constants/errors.js new file mode 100644 index 000000000..11d0fc186 --- /dev/null +++ b/ui/constants/errors.js @@ -0,0 +1,2 @@ +export const ALREADY_CLAIMED = 'once the invite reward has been claimed the referrer cannot be changed'; +export const REFERRER_NOT_FOUND = 'A lbry.tv account could not be found for the referrer you provided.'; diff --git a/ui/constants/settings.js b/ui/constants/settings.js index f2fae2f91..2e55ef158 100644 --- a/ui/constants/settings.js +++ b/ui/constants/settings.js @@ -27,6 +27,20 @@ export const ENABLE_SYNC = 'enable_sync'; export const TO_TRAY_WHEN_CLOSED = 'to_tray_when_closed'; export const ENABLE_PUBLISH_PREVIEW = 'enable-publish-preview'; export const DESKTOP_WINDOW_ZOOM = 'desktop_window_zoom'; +export const SHOW_NSFW = 'showNsfw'; +export const FIRST_RUN_STARTED = 'first_run_started'; +export const FOLLOWING_ACKNOWLEDGED = 'following_acknowledged'; +export const TAGS_ACKNOWLEDGED = 'tags_acknowledged'; +export const REWARDS_ACKNOWLEDGED = 'rewards_acknowledged'; +export const SEARCH_IN_LANGUAGE = 'search_in_language'; +export const HOMEPAGE = 'homepage'; +export const HIDE_REPOSTS = 'hide_reposts'; +export const SUPPORT_OPTION = 'support_option'; +export const TILE_LAYOUT = 'tile_layout'; +export const VIDEO_THEATER_MODE = 'video_theater_mode'; +export const VIDEO_PLAYBACK_RATE = 'video_playback_rate'; +export const CUSTOM_COMMENTS_SERVER_ENABLED = 'custom_comments_server_enabled'; +export const CUSTOM_COMMENTS_SERVER_URL = 'custom_comments_server_url'; export const SETTINGS_GRP = { APPEARANCE: 'appearance', diff --git a/ui/constants/shape_shift.js b/ui/constants/shape_shift.js new file mode 100644 index 000000000..1436f50d0 --- /dev/null +++ b/ui/constants/shape_shift.js @@ -0,0 +1,3 @@ +export const NO_DEPOSITS = 'no_deposits'; +export const RECEIVED = 'received'; +export const COMPLETE = 'complete'; diff --git a/ui/constants/shared_preferences.js b/ui/constants/shared_preferences.js new file mode 100644 index 000000000..478620a08 --- /dev/null +++ b/ui/constants/shared_preferences.js @@ -0,0 +1,31 @@ +/* + * How to use this file: + * Settings exported from here will trigger the setting to be + * sent to the preference middleware when set using the + * usual setDaemonSettings and clearDaemonSettings methods. + * + * See redux/settings/actions in the app for where this is used. + */ + +import * as DAEMON_SETTINGS from './daemon_settings'; +import * as SETTINGS from './settings'; + +// DAEMON +export const SDK_SYNC_KEYS = [DAEMON_SETTINGS.LBRYUM_SERVERS, DAEMON_SETTINGS.SHARE_USAGE_DATA]; + +// CLIENT +export const CLIENT_SYNC_KEYS = [ + SETTINGS.SHOW_MATURE, + SETTINGS.HIDE_REPOSTS, + SETTINGS.SHOW_ANONYMOUS, + SETTINGS.INSTANT_PURCHASE_ENABLED, + SETTINGS.INSTANT_PURCHASE_MAX, + SETTINGS.THEME, + SETTINGS.AUTOPLAY_MEDIA, + SETTINGS.AUTOPLAY_NEXT, + SETTINGS.HIDE_BALANCE, + SETTINGS.HIDE_SPLASH_ANIMATION, + SETTINGS.FLOATING_PLAYER, + SETTINGS.DARK_MODE_TIMES, + SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, +]; diff --git a/ui/constants/sort_options.js b/ui/constants/sort_options.js new file mode 100644 index 000000000..053a173ee --- /dev/null +++ b/ui/constants/sort_options.js @@ -0,0 +1,4 @@ +export const DATE_NEW = 'dateNew'; +export const DATE_OLD = 'dateOld'; +export const TITLE = 'title'; +export const FILENAME = 'filename'; diff --git a/ui/constants/speech_urls.js b/ui/constants/speech_urls.js new file mode 100644 index 000000000..d61abab55 --- /dev/null +++ b/ui/constants/speech_urls.js @@ -0,0 +1,2 @@ +export const SPEECH_STATUS = 'https://spee.ch/api/config/site/publishing'; +export const SPEECH_PUBLISH = 'https://spee.ch/api/claim/publish'; diff --git a/ui/constants/transaction_list.js b/ui/constants/transaction_list.js new file mode 100644 index 000000000..aabba4bba --- /dev/null +++ b/ui/constants/transaction_list.js @@ -0,0 +1,3 @@ +// PAGE SIZE +export const PAGE_SIZE = 50; +export const LATEST_PAGE_SIZE = 20; diff --git a/ui/constants/transaction_types.js b/ui/constants/transaction_types.js index d5463ae50..ced0fd9f5 100644 --- a/ui/constants/transaction_types.js +++ b/ui/constants/transaction_types.js @@ -2,3 +2,8 @@ export const TIP = 'tip'; export const SUPPORT = 'support'; export const CHANNEL = 'channel'; export const UPDATE = 'update'; +export const ALL = 'all'; +export const SPEND = 'spend'; +export const RECEIVE = 'receive'; +export const PUBLISH = 'publish'; +export const ABANDON = 'abandon'; diff --git a/ui/constants/txo_list.js b/ui/constants/txo_list.js new file mode 100644 index 000000000..b702f3438 --- /dev/null +++ b/ui/constants/txo_list.js @@ -0,0 +1,36 @@ +export const ACTIVE = 'active'; // spent, active, all +export const TYPE = 'type'; // all, payment, support, channel, stream, repost +export const SUB_TYPE = 'subtype'; // other, purchase, tip +export const PAGE_SIZE = 'page_size'; +export const PAGE = 'page'; +export const ALL = 'all'; +// dropdown types +export const SENT = 'sent'; +export const RECEIVED = 'received'; +export const SUPPORT = 'support'; +export const CHANNEL = 'channel'; +export const PUBLISH = 'publish'; +export const REPOST = 'repost'; +export const DROPDOWN_TYPES = [ALL, SENT, RECEIVED, SUPPORT, CHANNEL, PUBLISH, REPOST]; +// dropdown subtypes +export const TIP = 'tip'; +export const PURCHASE = 'purchase'; +export const PAYMENT = 'payment'; +export const DROPDOWN_SUBTYPES = [ALL, TIP, PURCHASE, PAYMENT]; + +// rpc params +export const TX_TYPE = 'type'; // = other, stream, repost, channel, support, purchase +export const IS_SPENT = 'is_spent'; +export const IS_NOT_SPENT = 'is_not_spent'; +export const IS_MY_INPUT = 'is_my_input'; +export const IS_MY_OUTPUT = 'is_my_output'; +export const IS_NOT_MY_INPUT = 'is_not_my_input'; +export const IS_NOT_MY_OUTPUT = 'is_not_my_output'; // use to further distinguish payments to self / from self. +export const IS_MY_INPUT_OR_OUTPUT = 'is_my_input_or_output'; +export const EXCLUDE_INTERNAL_TRANSFERS = 'exclude_internal_transfers'; + +// sdk unique types +export const OTHER = 'other'; +export const STREAM = 'stream'; + +export const PAGE_SIZE_DEFAULT = 20; diff --git a/ui/constants/youtube.js b/ui/constants/youtube.js new file mode 100644 index 000000000..f2db86ab1 --- /dev/null +++ b/ui/constants/youtube.js @@ -0,0 +1,11 @@ +export const YOUTUBE_SYNC_NOT_TRANSFERRED = 'not_transferred'; +export const YOUTUBE_SYNC_PENDING = 'pending'; +export const YOUTUBE_SYNC_PENDING_EMAIL = 'pendingemail'; +export const YOUTUBE_SYNC_PENDING_TRANSFER = 'pending_transfer'; +export const YOUTUBE_SYNC_COMPLETED_TRANSFER = 'completed_transfer'; +export const YOUTUBE_SYNC_QUEUED = 'queued'; +export const YOUTUBE_SYNC_SYNCING = 'syncing'; +export const YOUTUBE_SYNC_SYNCED = 'synced'; +export const YOUTUBE_SYNC_FAILED = 'failed'; +export const YOUTUBE_SYNC_PENDINGUPGRADE = 'pendingupgrade'; +export const YOUTUBE_SYNC_ABANDONDED = 'abandoned'; diff --git a/ui/effects/use-lighthouse.js b/ui/effects/use-lighthouse.js index b5b9f31e5..ea56a24e0 100644 --- a/ui/effects/use-lighthouse.js +++ b/ui/effects/use-lighthouse.js @@ -2,7 +2,7 @@ import React from 'react'; import { lighthouse } from 'redux/actions/search'; import { getSearchQueryString } from 'util/query-params'; -import { isURIValid } from 'lbry-redux'; +import { isURIValid } from 'util/lbryURI'; import useThrottle from './use-throttle'; export default function useLighthouse( diff --git a/ui/index.jsx b/ui/index.jsx index 9b39b6ed7..b723d3a74 100644 --- a/ui/index.jsx +++ b/ui/index.jsx @@ -15,10 +15,11 @@ import React, { Fragment, useState, useEffect } from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { doDaemonReady, doAutoUpdate, doOpenModal, doHideModal, doToggle3PAnalytics } from 'redux/actions/app'; -import { Lbry, isURIValid, apiCall } from 'lbry-redux'; +import Lbry, { apiCall } from 'lbry'; +import { isURIValid } from 'util/lbryURI'; import { setSearchApi } from 'redux/actions/search'; import { doSetLanguage, doFetchLanguage, doUpdateIsNightAsync } from 'redux/actions/settings'; -import { Lbryio, doBlackListedOutpointsSubscribe, doFilteredOutpointsSubscribe } from 'lbryinc'; +import { Lbryio, doBlackListedOutpointsSubscribe, doFilteredOutpointsSubscribe, testTheThing } from 'lbryinc'; import rewards from 'rewards'; import { store, persistor, history } from 'store'; import app from './app'; @@ -64,6 +65,8 @@ if (process.env.NODE_ENV === 'production') { }); } +testTheThing(); + if (process.env.SDK_API_URL) { console.warn('SDK_API_URL env var is deprecated. Use SDK_API_HOST instead'); // @eslint-disable-line } diff --git a/ui/lbry.js b/ui/lbry.js new file mode 100644 index 000000000..ac9768711 --- /dev/null +++ b/ui/lbry.js @@ -0,0 +1,251 @@ +// @flow +require('proxy-polyfill'); + +const CHECK_DAEMON_STARTED_TRY_NUMBER = 200; +// +// Basic LBRY sdk connection config +// Offers a proxy to call LBRY sdk methods + +// +const Lbry = { + isConnected: false, + connectPromise: null, + daemonConnectionString: 'http://localhost:5279', + alternateConnectionString: '', + methodsUsingAlternateConnectionString: [], + apiRequestHeaders: { 'Content-Type': 'application/json-rpc' }, + + // Allow overriding daemon connection string (e.g. to `/api/proxy` for lbryweb) + setDaemonConnectionString: (value: string) => { + Lbry.daemonConnectionString = value; + }, + + setApiHeader: (key: string, value: string) => { + Lbry.apiRequestHeaders = Object.assign(Lbry.apiRequestHeaders, { [key]: value }); + }, + + unsetApiHeader: (key) => { + Object.keys(Lbry.apiRequestHeaders).includes(key) && delete Lbry.apiRequestHeaders[key]; + }, + // Allow overriding Lbry methods + overrides: {}, + setOverride: (methodName, newMethod) => { + Lbry.overrides[methodName] = newMethod; + }, + getApiRequestHeaders: () => Lbry.apiRequestHeaders, + + // Returns a human readable media type based on the content type or extension of a file that is returned by the sdk + getMediaType: (contentType: ?string, fileName: ?string) => { + if (fileName) { + const formats = [ + [/\.(mp4|m4v|webm|flv|f4v|ogv)$/i, 'video'], + [/\.(mp3|m4a|aac|wav|flac|ogg|opus)$/i, 'audio'], + [/\.(jpeg|jpg|png|gif|svg|webp)$/i, 'image'], + [/\.(h|go|ja|java|js|jsx|c|cpp|cs|css|rb|scss|sh|php|py)$/i, 'script'], + [/\.(html|json|csv|txt|log|md|markdown|docx|pdf|xml|yml|yaml)$/i, 'document'], + [/\.(pdf|odf|doc|docx|epub|org|rtf)$/i, 'e-book'], + [/\.(stl|obj|fbx|gcode)$/i, '3D-file'], + [/\.(cbr|cbt|cbz)$/i, 'comic-book'], + [/\.(lbry)$/i, 'application'], + ]; + + const res = formats.reduce((ret, testpair) => { + switch (testpair[0].test(ret)) { + case true: + return testpair[1]; + default: + return ret; + } + }, fileName); + return res === fileName ? 'unknown' : res; + } else if (contentType) { + // $FlowFixMe + return /^[^/]+/.exec(contentType)[0]; + } + + return 'unknown'; + }, + + // + // Lbry SDK Methods + // https://lbry.tech/api/sdk + // + status: (params = {}) => daemonCallWithResult('status', params), + stop: () => daemonCallWithResult('stop', {}), + version: () => daemonCallWithResult('version', {}), + + // Claim fetching and manipulation + resolve: (params) => daemonCallWithResult('resolve', params), + get: (params) => daemonCallWithResult('get', params), + claim_search: (params) => daemonCallWithResult('claim_search', params), + claim_list: (params) => daemonCallWithResult('claim_list', params), + channel_create: (params) => daemonCallWithResult('channel_create', params), + channel_update: (params) => daemonCallWithResult('channel_update', params), + channel_import: (params) => daemonCallWithResult('channel_import', params), + channel_list: (params) => daemonCallWithResult('channel_list', params), + stream_abandon: (params) => daemonCallWithResult('stream_abandon', params), + stream_list: (params) => daemonCallWithResult('stream_list', params), + channel_abandon: (params) => daemonCallWithResult('channel_abandon', params), + channel_sign: (params) => daemonCallWithResult('channel_sign', params), + support_create: (params) => daemonCallWithResult('support_create', params), + support_list: (params) => daemonCallWithResult('support_list', params), + stream_repost: (params) => daemonCallWithResult('stream_repost', params), + collection_resolve: (params) => daemonCallWithResult('collection_resolve', params), + collection_list: (params) => daemonCallWithResult('collection_list', params), + collection_create: (params) => daemonCallWithResult('collection_create', params), + collection_update: (params) => daemonCallWithResult('collection_update', params), + + // File fetching and manipulation + file_list: (params = {}) => daemonCallWithResult('file_list', params), + file_delete: (params = {}) => daemonCallWithResult('file_delete', params), + file_set_status: (params = {}) => daemonCallWithResult('file_set_status', params), + blob_delete: (params = {}) => daemonCallWithResult('blob_delete', params), + blob_list: (params = {}) => daemonCallWithResult('blob_list', params), + file_reflect: (params = {}) => daemonCallWithResult('file_reflect', params), + + // Wallet utilities + wallet_balance: (params = {}) => daemonCallWithResult('wallet_balance', params), + wallet_decrypt: () => daemonCallWithResult('wallet_decrypt', {}), + wallet_encrypt: (params = {}) => daemonCallWithResult('wallet_encrypt', params), + wallet_unlock: (params = {}) => daemonCallWithResult('wallet_unlock', params), + wallet_list: (params = {}) => daemonCallWithResult('wallet_list', params), + wallet_send: (params = {}) => daemonCallWithResult('wallet_send', params), + wallet_status: (params = {}) => daemonCallWithResult('wallet_status', params), + address_is_mine: (params = {}) => daemonCallWithResult('address_is_mine', params), + address_unused: (params = {}) => daemonCallWithResult('address_unused', params), + address_list: (params = {}) => daemonCallWithResult('address_list', params), + transaction_list: (params = {}) => daemonCallWithResult('transaction_list', params), + utxo_release: (params = {}) => daemonCallWithResult('utxo_release', params), + support_abandon: (params = {}) => daemonCallWithResult('support_abandon', params), + purchase_list: (params = {}) => daemonCallWithResult('purchase_list', params), + txo_list: (params = {}) => daemonCallWithResult('txo_list', params), + account_list: (params = {}) => daemonCallWithResult('account_list', params), + account_set: (params = {}) => daemonCallWithResult('account_set', params), + + sync_hash: (params = {}) => daemonCallWithResult('sync_hash', params), + sync_apply: (params = {}) => daemonCallWithResult('sync_apply', params), + + // Preferences + preference_get: (params = {}) => daemonCallWithResult('preference_get', params), + preference_set: (params = {}) => daemonCallWithResult('preference_set', params), + + // Comments + comment_list: (params = {}) => daemonCallWithResult('comment_list', params), + comment_create: (params = {}) => daemonCallWithResult('comment_create', params), + comment_hide: (params = {}) => daemonCallWithResult('comment_hide', params), + comment_abandon: (params = {}) => daemonCallWithResult('comment_abandon', params), + comment_update: (params = {}) => daemonCallWithResult('comment_update', params), + + // Connect to the sdk + connect: () => { + if (Lbry.connectPromise === null) { + // $FlowFixMe + Lbry.connectPromise = new Promise((resolve, reject) => { + let tryNum = 0; + // Check every half second to see if the daemon is accepting connections + function checkDaemonStarted() { + tryNum += 1; + Lbry.status() + .then(resolve) + .catch(() => { + if (tryNum <= CHECK_DAEMON_STARTED_TRY_NUMBER) { + setTimeout(checkDaemonStarted, tryNum < 50 ? 400 : 1000); + } else { + reject(new Error('Unable to connect to LBRY')); + } + }); + } + + checkDaemonStarted(); + }); + } + + // Flow thinks this could be empty, but it will always reuturn a promise + // $FlowFixMe + return Lbry.connectPromise; + }, + + publish: (params = {}) => + new Promise((resolve, reject) => { + if (Lbry.overrides.publish) { + Lbry.overrides.publish(params).then(resolve, reject); + } else { + apiCall('publish', params, resolve, reject); + } + }), +}; + +function checkAndParse(response) { + if (response.status >= 200 && response.status < 300) { + return response.json(); + } + return response.json().then((json) => { + let error; + if (json.error) { + const errorMessage = typeof json.error === 'object' ? json.error.message : json.error; + error = new Error(errorMessage); + } else { + error = new Error('Protocol error with unknown response signature'); + } + return Promise.reject(error); + }); +} + +export function apiCall(method: string, params: ?{}, resolve: Function, reject: Function) { + const counter = new Date().getTime(); + const options = { + method: 'POST', + headers: Lbry.apiRequestHeaders, + body: JSON.stringify({ + jsonrpc: '2.0', + method, + params, + id: counter, + }), + }; + + const connectionString = Lbry.methodsUsingAlternateConnectionString.includes(method) + ? Lbry.alternateConnectionString + : Lbry.daemonConnectionString; + return fetch(connectionString + '?m=' + method, options) + .then(checkAndParse) + .then((response) => { + const error = response.error || (response.result && response.result.error); + + if (error) { + return reject(error); + } + return resolve(response.result); + }) + .catch(reject); +} + +function daemonCallWithResult(name: string, params: ?{} = {}): Promise { + return new Promise((resolve, reject) => { + apiCall( + name, + params, + (result) => { + resolve(result); + }, + reject + ); + }); +} + +// This is only for a fallback +// If there is a Lbry method that is being called by an app, it should be added to /flow-typed/Lbry.js +const lbryProxy = new Proxy(Lbry, { + get(target: LbryTypes, name: string) { + if (name in target) { + return target[name]; + } + + return (params = {}) => + new Promise((resolve, reject) => { + apiCall(name, params, resolve, reject); + }); + }, +}); + +export default lbryProxy; diff --git a/ui/modal/modalAffirmPurchase/index.js b/ui/modal/modalAffirmPurchase/index.js index 416ddee7c..67518bc44 100644 --- a/ui/modal/modalAffirmPurchase/index.js +++ b/ui/modal/modalAffirmPurchase/index.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import { doPlayUri, doSetPlayingUri } from 'redux/actions/content'; import { selectPlayingUri } from 'redux/selectors/content'; import { doHideModal, doAnaltyicsPurchaseEvent } from 'redux/actions/app'; -import { makeSelectMetadataForUri } from 'lbry-redux'; +import { makeSelectMetadataForUri } from 'redux/selectors/claims'; import ModalAffirmPurchase from './view'; const select = (state, props) => ({ @@ -10,9 +10,9 @@ const select = (state, props) => ({ playingUri: selectPlayingUri(state), }); -const perform = dispatch => ({ - analyticsPurchaseEvent: fileInfo => dispatch(doAnaltyicsPurchaseEvent(fileInfo)), - setPlayingUri: uri => dispatch(doSetPlayingUri({ uri })), +const perform = (dispatch) => ({ + analyticsPurchaseEvent: (fileInfo) => dispatch(doAnaltyicsPurchaseEvent(fileInfo)), + setPlayingUri: (uri) => dispatch(doSetPlayingUri({ uri })), closeModal: () => dispatch(doHideModal()), loadVideo: (uri, onSuccess) => dispatch(doPlayUri(uri, true, undefined, onSuccess)), }); diff --git a/ui/modal/modalAffirmPurchase/view.jsx b/ui/modal/modalAffirmPurchase/view.jsx index 3b29350ab..ca3222f1c 100644 --- a/ui/modal/modalAffirmPurchase/view.jsx +++ b/ui/modal/modalAffirmPurchase/view.jsx @@ -6,7 +6,7 @@ import { Modal } from 'modal/modal'; import Card from 'component/common/card'; import I18nMessage from 'component/i18nMessage'; import Button from 'component/button'; -import { isURIEqual } from 'lbry-redux'; +import { isURIEqual } from 'util/lbryURI'; // This number is tied to transitions in scss/purchase.scss const ANIMATION_LENGTH = 2500; diff --git a/ui/modal/modalAutoGenerateThumbnail/index.js b/ui/modal/modalAutoGenerateThumbnail/index.js index d769e869b..630a7e0c5 100644 --- a/ui/modal/modalAutoGenerateThumbnail/index.js +++ b/ui/modal/modalAutoGenerateThumbnail/index.js @@ -1,13 +1,13 @@ import { connect } from 'react-redux'; import { doHideModal } from 'redux/actions/app'; -import { doUploadThumbnail } from 'lbry-redux'; +import { doUploadThumbnail } from 'redux/actions/publish'; import { doToast } from 'redux/actions/notifications'; import ModalAutoGenerateThumbnail from './view'; -const perform = dispatch => ({ +const perform = (dispatch) => ({ closeModal: () => dispatch(doHideModal()), - upload: file => dispatch(doUploadThumbnail(null, file, null, null, 'Generated')), - showToast: options => dispatch(doToast(options)), + upload: (file) => dispatch(doUploadThumbnail(null, file, null, null, 'Generated')), + showToast: (options) => dispatch(doToast(options)), }); export default connect(null, perform)(ModalAutoGenerateThumbnail); diff --git a/ui/modal/modalBlockChannel/index.js b/ui/modal/modalBlockChannel/index.js index 66cc4aaa4..242a94e84 100644 --- a/ui/modal/modalBlockChannel/index.js +++ b/ui/modal/modalBlockChannel/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; import { doHideModal } from 'redux/actions/app'; import { doCommentModBlock, doCommentModBlockAsAdmin, doCommentModBlockAsModerator } from 'redux/actions/comments'; import { selectActiveChannelClaim } from 'redux/selectors/app'; diff --git a/ui/modal/modalConfirmAge/view.jsx b/ui/modal/modalConfirmAge/view.jsx index 621eea56b..e569b432e 100644 --- a/ui/modal/modalConfirmAge/view.jsx +++ b/ui/modal/modalConfirmAge/view.jsx @@ -1,5 +1,5 @@ // @flow -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; import React from 'react'; import { Modal } from 'modal/modal'; import Card from 'component/common/card'; diff --git a/ui/modal/modalConfirmThumbnailUpload/index.js b/ui/modal/modalConfirmThumbnailUpload/index.js index add3bee48..3b8256881 100644 --- a/ui/modal/modalConfirmThumbnailUpload/index.js +++ b/ui/modal/modalConfirmThumbnailUpload/index.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import { doHideModal } from 'redux/actions/app'; -import { doUploadThumbnail, doUpdatePublishForm } from 'lbry-redux'; +import { doUploadThumbnail, doUpdatePublishForm } from 'redux/actions/publish'; import ModalConfirmThumbnailUpload from './view'; const perform = (dispatch) => ({ diff --git a/ui/modal/modalConfirmTransaction/index.js b/ui/modal/modalConfirmTransaction/index.js index 62f0cea23..c59a746dd 100644 --- a/ui/modal/modalConfirmTransaction/index.js +++ b/ui/modal/modalConfirmTransaction/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { doSendDraftTransaction, makeSelectClaimForUri, doSendTip } from 'lbry-redux'; +import { doSendDraftTransaction, doSendTip } from 'redux/actions/wallet'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; import { doHideModal } from 'redux/actions/app'; import ModalConfirmTransaction from './view'; import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app'; diff --git a/ui/modal/modalFileSelection/index.js b/ui/modal/modalFileSelection/index.js index 6d0629177..17b1151c1 100644 --- a/ui/modal/modalFileSelection/index.js +++ b/ui/modal/modalFileSelection/index.js @@ -1,12 +1,12 @@ import { connect } from 'react-redux'; import { doHideModal } from 'redux/actions/app'; -import { doUpdatePublishForm } from 'lbry-redux'; +import { doUpdatePublishForm } from 'redux/actions/publish'; import ModaFileSelection from './view'; -const perform = dispatch => ({ - hideModal: props => dispatch(doHideModal(props)), - updatePublishForm: value => dispatch(doUpdatePublishForm(value)), +const perform = (dispatch) => ({ + hideModal: (props) => dispatch(doHideModal(props)), + updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)), }); export default connect(null, perform)(ModaFileSelection); diff --git a/ui/modal/modalFileTimeout/index.js b/ui/modal/modalFileTimeout/index.js index bb78115e8..53db7c1e3 100644 --- a/ui/modal/modalFileTimeout/index.js +++ b/ui/modal/modalFileTimeout/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectMetadataForUri } from 'lbry-redux'; +import { makeSelectMetadataForUri } from 'redux/selectors/claims'; import { doHideModal } from 'redux/actions/app'; import ModalFileTimeout from './view'; @@ -7,11 +7,8 @@ const select = (state, props) => ({ metadata: makeSelectMetadataForUri(props.uri)(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ closeModal: () => dispatch(doHideModal()), }); -export default connect( - select, - perform -)(ModalFileTimeout); +export default connect(select, perform)(ModalFileTimeout); diff --git a/ui/modal/modalMassTipUnlock/index.js b/ui/modal/modalMassTipUnlock/index.js index 9e39eab42..209e46948 100644 --- a/ui/modal/modalMassTipUnlock/index.js +++ b/ui/modal/modalMassTipUnlock/index.js @@ -1,9 +1,10 @@ import { connect } from 'react-redux'; import { doHideModal } from 'redux/actions/app'; -import { selectIsMassClaimingTips, doTipClaimMass, selectUtxoCounts, selectTipsBalance } from 'lbry-redux'; +import { doTipClaimMass } from 'redux/actions/wallet'; +import { selectIsMassClaimingTips, selectUtxoCounts, selectTipsBalance } from 'redux/selectors/wallet'; import ModalSupportsLiquidate from './view'; -const select = state => ({ +const select = (state) => ({ massClaimingTips: selectIsMassClaimingTips(state), utxoCounts: selectUtxoCounts(state), tipsBalance: selectTipsBalance(state) || 0, diff --git a/ui/modal/modalPublish/index.js b/ui/modal/modalPublish/index.js index 3312f3965..910d9b782 100644 --- a/ui/modal/modalPublish/index.js +++ b/ui/modal/modalPublish/index.js @@ -1,7 +1,8 @@ import { connect } from 'react-redux'; import { doHideModal } from 'redux/actions/app'; import ModalPublishSuccess from './view'; -import { doClearPublish, makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; +import { doClearPublish } from 'redux/actions/publish'; import { push } from 'connected-react-router'; const select = (state, props) => ({ diff --git a/ui/modal/modalPublishPreview/index.js b/ui/modal/modalPublishPreview/index.js index f71878860..8a3056169 100644 --- a/ui/modal/modalPublishPreview/index.js +++ b/ui/modal/modalPublishPreview/index.js @@ -1,14 +1,9 @@ import { connect } from 'react-redux'; import { doHideModal } from 'redux/actions/app'; import ModalPublishPreview from './view'; -import { - makeSelectPublishFormValue, - selectPublishFormValues, - selectIsStillEditing, - selectMyChannelClaims, - makeSelectClaimIsStreamPlaceholder, - SETTINGS, -} from 'lbry-redux'; +import { makeSelectPublishFormValue, selectPublishFormValues, selectIsStillEditing } from 'redux/selectors/publish'; +import { selectMyChannelClaims, makeSelectClaimIsStreamPlaceholder } from 'redux/selectors/claims'; +import * as SETTINGS from 'constants/settings'; import { selectFfmpegStatus, makeSelectClientSetting } from 'redux/selectors/settings'; import { doPublishDesktop } from 'redux/actions/publish'; import { doSetClientSetting } from 'redux/actions/settings'; diff --git a/ui/modal/modalRemoveCard/index.js b/ui/modal/modalRemoveCard/index.js index f54d477ec..0cc7f1c93 100644 --- a/ui/modal/modalRemoveCard/index.js +++ b/ui/modal/modalRemoveCard/index.js @@ -1,14 +1,15 @@ import { connect } from 'react-redux'; import { doHideModal } from 'redux/actions/app'; -import { doAbandonTxo, doAbandonClaim, selectTransactionItems, doResolveUri } from 'lbry-redux'; +import { doAbandonTxo, doAbandonClaim, doResolveUri } from 'redux/actions/claims'; +import { selectTransactionItems } from 'redux/selectors/wallet'; import { doToast } from 'redux/actions/notifications'; import ModalRevokeClaim from './view'; -const select = state => ({ +const select = (state) => ({ transactionItems: selectTransactionItems(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ toast: (message, isError) => dispatch(doToast({ message, isError })), closeModal: () => dispatch(doHideModal()), abandonTxo: (txo, cb) => dispatch(doAbandonTxo(txo, cb)), diff --git a/ui/modal/modalRemoveCollection/index.js b/ui/modal/modalRemoveCollection/index.js index 3e23ee3d0..8acbb2595 100644 --- a/ui/modal/modalRemoveCollection/index.js +++ b/ui/modal/modalRemoveCollection/index.js @@ -2,10 +2,10 @@ import { connect } from 'react-redux'; import { makeSelectClaimIsMine, makeSelectIsAbandoningClaimForUri, - doCollectionDelete, makeSelectClaimForClaimId, - makeSelectNameForCollectionId, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { doCollectionDelete } from 'redux/actions/collections'; +import { makeSelectNameForCollectionId } from 'redux/selectors/collections'; import { doHideModal } from 'redux/actions/app'; import ModalRemoveCollection from './view'; diff --git a/ui/modal/modalRemoveFile/index.js b/ui/modal/modalRemoveFile/index.js index 529364bde..bc0fdd321 100644 --- a/ui/modal/modalRemoveFile/index.js +++ b/ui/modal/modalRemoveFile/index.js @@ -2,11 +2,11 @@ import { connect } from 'react-redux'; import { doDeleteFileAndMaybeGoBack } from 'redux/actions/file'; import { makeSelectTitleForUri, - doResolveUri, makeSelectClaimForUri, makeSelectIsAbandoningClaimForUri, makeSelectClaimIsMine, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { doResolveUri } from 'redux/actions/claims'; import { doHideModal } from 'redux/actions/app'; import ModalRemoveFile from './view'; @@ -17,7 +17,7 @@ const select = (state, props) => ({ isAbandoning: makeSelectIsAbandoningClaimForUri(props.uri)(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ closeModal: () => dispatch(doHideModal()), doResolveUri: (uri) => dispatch(doResolveUri(uri)), deleteFile: (uri, deleteFromComputer, abandonClaim, doGoBack) => { diff --git a/ui/modal/modalRevokeClaim/index.js b/ui/modal/modalRevokeClaim/index.js index f54d477ec..ab4042bf7 100644 --- a/ui/modal/modalRevokeClaim/index.js +++ b/ui/modal/modalRevokeClaim/index.js @@ -1,14 +1,15 @@ import { connect } from 'react-redux'; import { doHideModal } from 'redux/actions/app'; -import { doAbandonTxo, doAbandonClaim, selectTransactionItems, doResolveUri } from 'lbry-redux'; +import { doAbandonTxo, doAbandonClaim, doResolveUri } from 'redux/actions/claims'; import { doToast } from 'redux/actions/notifications'; import ModalRevokeClaim from './view'; +import { selectTransactionItems } from 'redux/selectors/wallet'; -const select = state => ({ +const select = (state) => ({ transactionItems: selectTransactionItems(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ toast: (message, isError) => dispatch(doToast({ message, isError })), closeModal: () => dispatch(doHideModal()), abandonTxo: (txo, cb) => dispatch(doAbandonTxo(txo, cb)), diff --git a/ui/modal/modalRouter/index.js b/ui/modal/modalRouter/index.js index c6f84f78c..d6d9238e9 100644 --- a/ui/modal/modalRouter/index.js +++ b/ui/modal/modalRouter/index.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import { selectModal } from 'redux/selectors/app'; import { doOpenModal, doHideModal } from 'redux/actions/app'; -import { selectError } from 'lbry-redux'; +import { selectError } from 'redux/selectors/notifications'; // RENAME THIS 'selectNotificationError' import ModalRouter from './view'; const select = (state, props) => ({ @@ -9,12 +9,9 @@ const select = (state, props) => ({ error: selectError(state), }); -const perform = dispatch => ({ - openModal: props => dispatch(doOpenModal(props)), - hideModal: props => dispatch(doHideModal(props)), +const perform = (dispatch) => ({ + openModal: (props) => dispatch(doOpenModal(props)), + hideModal: (props) => dispatch(doHideModal(props)), }); -export default connect( - select, - perform -)(ModalRouter); +export default connect(select, perform)(ModalRouter); diff --git a/ui/modal/modalSupportsLiquidate/index.js b/ui/modal/modalSupportsLiquidate/index.js index c7e6f8582..83f0db268 100644 --- a/ui/modal/modalSupportsLiquidate/index.js +++ b/ui/modal/modalSupportsLiquidate/index.js @@ -1,13 +1,13 @@ import { connect } from 'react-redux'; import { doHideModal } from 'redux/actions/app'; -import { selectTransactionItems } from 'lbry-redux'; +import { selectTransactionItems } from 'redux/selectors/wallet'; import ModalSupportsLiquidate from './view'; -const select = state => ({ +const select = (state) => ({ transactionItems: selectTransactionItems(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ closeModal: () => dispatch(doHideModal()), }); diff --git a/ui/modal/modalWalletDecrypt/index.js b/ui/modal/modalWalletDecrypt/index.js index 6ed8a8b0d..1da771a05 100644 --- a/ui/modal/modalWalletDecrypt/index.js +++ b/ui/modal/modalWalletDecrypt/index.js @@ -1,19 +1,17 @@ import { connect } from 'react-redux'; -import { doWalletStatus, doWalletDecrypt, selectWalletDecryptSucceeded } from 'lbry-redux'; +import { doWalletStatus, doWalletDecrypt } from 'redux/actions/wallet'; +import { selectWalletDecryptSucceeded } from 'redux/selectors/wallet'; import { doHideModal } from 'redux/actions/app'; import ModalWalletDecrypt from './view'; -const select = state => ({ +const select = (state) => ({ walletDecryptSucceded: selectWalletDecryptSucceeded(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ closeModal: () => dispatch(doHideModal()), - decryptWallet: password => dispatch(doWalletDecrypt(password)), + decryptWallet: (password) => dispatch(doWalletDecrypt(password)), updateWalletStatus: () => dispatch(doWalletStatus()), }); -export default connect( - select, - perform -)(ModalWalletDecrypt); +export default connect(select, perform)(ModalWalletDecrypt); diff --git a/ui/modal/modalWalletEncrypt/index.js b/ui/modal/modalWalletEncrypt/index.js index 3c497bad6..62db4d225 100644 --- a/ui/modal/modalWalletEncrypt/index.js +++ b/ui/modal/modalWalletEncrypt/index.js @@ -1,20 +1,18 @@ import { connect } from 'react-redux'; -import { doWalletStatus, doWalletEncrypt, selectWalletEncryptSucceeded, selectWalletEncryptResult } from 'lbry-redux'; +import { selectWalletEncryptSucceeded, selectWalletEncryptResult } from 'redux/selectors/wallet'; +import { doWalletStatus, doWalletEncrypt } from 'redux/actions/wallet'; import { doHideModal } from 'redux/actions/app'; import ModalWalletEncrypt from './view'; -const select = state => ({ +const select = (state) => ({ walletEncryptSucceded: selectWalletEncryptSucceeded(state), walletEncryptResult: selectWalletEncryptResult(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ closeModal: () => dispatch(doHideModal()), - encryptWallet: password => dispatch(doWalletEncrypt(password)), + encryptWallet: (password) => dispatch(doWalletEncrypt(password)), updateWalletStatus: () => dispatch(doWalletStatus()), }); -export default connect( - select, - perform -)(ModalWalletEncrypt); +export default connect(select, perform)(ModalWalletEncrypt); diff --git a/ui/modal/modalWalletUnlock/index.js b/ui/modal/modalWalletUnlock/index.js index a1b7ba492..1ffefcc19 100644 --- a/ui/modal/modalWalletUnlock/index.js +++ b/ui/modal/modalWalletUnlock/index.js @@ -1,19 +1,17 @@ import { connect } from 'react-redux'; -import { doWalletUnlock, selectWalletUnlockSucceeded } from 'lbry-redux'; +import { doWalletUnlock } from 'redux/actions/wallet'; +import { selectWalletUnlockSucceeded } from 'redux/selectors/wallet'; import { doQuit, doHideModal } from 'redux/actions/app'; import ModalWalletUnlock from './view'; -const select = state => ({ +const select = (state) => ({ walletUnlockSucceded: selectWalletUnlockSucceeded(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ closeModal: () => dispatch(doHideModal()), quit: () => dispatch(doQuit()), - unlockWallet: password => dispatch(doWalletUnlock(password)), + unlockWallet: (password) => dispatch(doWalletUnlock(password)), }); -export default connect( - select, - perform -)(ModalWalletUnlock); +export default connect(select, perform)(ModalWalletUnlock); diff --git a/ui/page/buy/index.js b/ui/page/buy/index.js index 97b12d9d2..8111f187a 100644 --- a/ui/page/buy/index.js +++ b/ui/page/buy/index.js @@ -1,10 +1,11 @@ import { connect } from 'react-redux'; -import { selectGettingNewAddress, selectReceiveAddress, doGetNewAddress } from 'lbry-redux'; +import { selectGettingNewAddress, selectReceiveAddress } from 'redux/selectors/wallet'; +import { doGetNewAddress } from 'redux/actions/wallet'; import { selectUserEmail, selectUser } from 'redux/selectors/user'; import { doUserSetCountry } from 'redux/actions/user'; import BuyPage from './view'; -const select = state => ({ +const select = (state) => ({ receiveAddress: selectReceiveAddress(state), gettingNewAddress: selectGettingNewAddress(state), email: selectUserEmail(state), diff --git a/ui/page/channel/index.js b/ui/page/channel/index.js index 9befca133..049d0f0ea 100644 --- a/ui/page/channel/index.js +++ b/ui/page/channel/index.js @@ -7,8 +7,8 @@ import { selectCurrentChannelPage, makeSelectClaimForUri, makeSelectClaimIsPending, - selectMyUnpublishedCollections, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { selectMyUnpublishedCollections } from 'redux/selectors/collections'; import { selectBlackListedOutpoints, doFetchSubCount, makeSelectSubCountForUri } from 'lbryinc'; import { selectYoutubeChannels } from 'redux/selectors/user'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; diff --git a/ui/page/channel/view.jsx b/ui/page/channel/view.jsx index 5a24c423e..4f46f010a 100644 --- a/ui/page/channel/view.jsx +++ b/ui/page/channel/view.jsx @@ -2,7 +2,7 @@ import * as ICONS from 'constants/icons'; import * as PAGES from 'constants/pages'; import React from 'react'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; import { YOUTUBE_STATUSES } from 'lbryinc'; import Page from 'component/page'; import SubscribeButton from 'component/subscribeButton'; diff --git a/ui/page/channelNew/index.js b/ui/page/channelNew/index.js index 87c54d50e..ea41e8a31 100644 --- a/ui/page/channelNew/index.js +++ b/ui/page/channelNew/index.js @@ -1,6 +1,6 @@ import REWARD_TYPES from 'rewards'; import { connect } from 'react-redux'; -import { selectBalance } from 'lbry-redux'; +import { selectBalance } from 'redux/selectors/wallet'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { doClaimRewardType } from 'redux/actions/rewards'; import ChannelNew from './view'; diff --git a/ui/page/channels/index.js b/ui/page/channels/index.js index bcad229ff..64980a998 100644 --- a/ui/page/channels/index.js +++ b/ui/page/channels/index.js @@ -2,10 +2,10 @@ import { connect } from 'react-redux'; import { selectMyChannelClaims, selectMyChannelUrls, - doFetchChannelListMine, selectFetchingMyChannels, makeSelectClaimIsPending, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { doFetchChannelListMine } from 'redux/actions/claims'; import { doSetActiveChannel } from 'redux/actions/app'; import { selectYoutubeChannels } from 'redux/selectors/user'; import ChannelsPage from './view'; diff --git a/ui/page/channelsFollowing/index.js b/ui/page/channelsFollowing/index.js index c08a2107f..3a3278877 100644 --- a/ui/page/channelsFollowing/index.js +++ b/ui/page/channelsFollowing/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; import { doFetchActiveLivestreams } from 'redux/actions/livestream'; import { selectActiveLivestreams } from 'redux/selectors/livestream'; import { selectSubscriptions } from 'redux/selectors/subscriptions'; diff --git a/ui/page/channelsFollowing/view.jsx b/ui/page/channelsFollowing/view.jsx index c7f9d7222..3f6008f45 100644 --- a/ui/page/channelsFollowing/view.jsx +++ b/ui/page/channelsFollowing/view.jsx @@ -9,7 +9,7 @@ import ClaimListDiscover from 'component/claimListDiscover'; import Page from 'component/page'; import Button from 'component/button'; import Icon from 'component/common/icon'; -import { splitBySeparator } from 'lbry-redux'; +import { splitBySeparator } from 'util/lbryURI'; import { getLivestreamUris } from 'util/livestream'; type Props = { diff --git a/ui/page/collection/index.js b/ui/page/collection/index.js index e9aba72a9..596a40d07 100644 --- a/ui/page/collection/index.js +++ b/ui/page/collection/index.js @@ -3,22 +3,28 @@ import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; import CollectionPage from './view'; import { - doFetchItemsInCollection, - makeSelectCollectionForId, - makeSelectUrlsForCollectionId, - makeSelectIsResolvingCollectionForId, makeSelectTitleForUri, makeSelectThumbnailForUri, makeSelectClaimIsMine, makeSelectClaimIsPending, makeSelectClaimForClaimId, - makeSelectCollectionIsMine, - doCollectionDelete, - doCollectionEdit, makeSelectChannelForClaimUri, +} from 'redux/selectors/claims'; + +import { + makeSelectCollectionForId, + makeSelectUrlsForCollectionId, + makeSelectIsResolvingCollectionForId, + makeSelectCollectionIsMine, makeSelectCountForCollectionId, makeSelectEditedCollectionForId, -} from 'lbry-redux'; +} from 'redux/selectors/collections'; + +import { + doFetchItemsInCollection, + doCollectionDelete, + doCollectionEdit +} from 'redux/actions/collections'; import { selectUser } from 'redux/selectors/user'; const select = (state, props) => { diff --git a/ui/page/collection/view.jsx b/ui/page/collection/view.jsx index 4befdc9d6..eb173f7c4 100644 --- a/ui/page/collection/view.jsx +++ b/ui/page/collection/view.jsx @@ -11,7 +11,7 @@ import CollectionActions from 'component/collectionActions'; import classnames from 'classnames'; import ClaimAuthor from 'component/claimAuthor'; import FileDescription from 'component/fileDescription'; -import { COLLECTIONS_CONSTS } from 'lbry-redux'; +import * as COLLECTIONS_CONSTS from 'constants/collections'; import Icon from 'component/common/icon'; import * as ICONS from 'constants/icons'; import Spinner from 'component/spinner'; diff --git a/ui/page/creatorDashboard/index.js b/ui/page/creatorDashboard/index.js index 9c724308c..a9487dbdf 100644 --- a/ui/page/creatorDashboard/index.js +++ b/ui/page/creatorDashboard/index.js @@ -1,10 +1,10 @@ import { connect } from 'react-redux'; -import { selectMyChannelClaims, selectFetchingMyChannels } from 'lbry-redux'; +import { selectMyChannelClaims, selectFetchingMyChannels } from 'redux/selectors/claims'; import { selectActiveChannelClaim } from 'redux/selectors/app'; import { doSetActiveChannel } from 'redux/actions/app'; import CreatorDashboardPage from './view'; -const select = state => ({ +const select = (state) => ({ channels: selectMyChannelClaims(state), fetchingChannels: selectFetchingMyChannels(state), activeChannelClaim: selectActiveChannelClaim(state), diff --git a/ui/page/discover/index.js b/ui/page/discover/index.js index 20fc81ab5..9ff45a9f0 100644 --- a/ui/page/discover/index.js +++ b/ui/page/discover/index.js @@ -1,6 +1,8 @@ import * as CS from 'constants/claim_search'; import { connect } from 'react-redux'; -import { makeSelectClaimForUri, doResolveUri, SETTINGS } from 'lbry-redux'; +import { doResolveUri } from 'redux/actions/claims'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; +import * as SETTINGS from 'constants/settings'; import { doFetchActiveLivestreams } from 'redux/actions/livestream'; import { selectActiveLivestreams } from 'redux/selectors/livestream'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; diff --git a/ui/page/embedWrapper/index.js b/ui/page/embedWrapper/index.js index 280dfd075..cb25a0658 100644 --- a/ui/page/embedWrapper/index.js +++ b/ui/page/embedWrapper/index.js @@ -1,12 +1,9 @@ import { connect } from 'react-redux'; import EmbedWrapperPage from './view'; -import { - doResolveUri, - makeSelectClaimForUri, - buildURI, - makeSelectStreamingUrlForUri, - makeSelectIsUriResolving, -} from 'lbry-redux'; +import { makeSelectClaimForUri, makeSelectIsUriResolving } from 'redux/selectors/claims'; +import { makeSelectStreamingUrlForUri } from 'redux/selectors/file_info'; +import { doResolveUri } from 'redux/actions/claims'; +import { buildURI } from 'util/lbryURI'; import { doPlayUri } from 'redux/actions/content'; import { makeSelectCostInfoForUri, doFetchCostInfoForUri, selectBlackListedOutpoints } from 'lbryinc'; @@ -25,11 +22,11 @@ const select = (state, props) => { }; }; -const perform = dispatch => { +const perform = (dispatch) => { return { - resolveUri: uri => dispatch(doResolveUri(uri)), - doPlayUri: uri => dispatch(doPlayUri(uri)), - doFetchCostInfoForUri: uri => dispatch(doFetchCostInfoForUri(uri)), + resolveUri: (uri) => dispatch(doResolveUri(uri)), + doPlayUri: (uri) => dispatch(doPlayUri(uri)), + doFetchCostInfoForUri: (uri) => dispatch(doFetchCostInfoForUri(uri)), }; }; diff --git a/ui/page/file/index.js b/ui/page/file/index.js index d00fc2b17..794c0ce78 100644 --- a/ui/page/file/index.js +++ b/ui/page/file/index.js @@ -2,16 +2,16 @@ import { connect } from 'react-redux'; import { doSetContentHistoryItem, doSetPrimaryUri, clearPosition } from 'redux/actions/content'; import { withRouter } from 'react-router-dom'; import { - doFetchFileInfo, - makeSelectFileInfoForUri, makeSelectMetadataForUri, makeSelectClaimIsNsfw, - SETTINGS, makeSelectTagInClaimOrChannelForUri, makeSelectClaimIsStreamPlaceholder, - makeSelectCollectionForId, - COLLECTIONS_CONSTS, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { makeSelectFileInfoForUri } from 'redux/selectors/file_info'; +import { doFetchFileInfo } from 'redux/actions/file_info'; +import { makeSelectCollectionForId } from 'redux/selectors/collections'; +import * as COLLECTIONS_CONSTS from 'constants/collections'; +import * as SETTINGS from 'constants/settings'; import { makeSelectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc'; import { selectShowMatureContent, makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectFileRenderModeForUri, makeSelectContentPositionForUri } from 'redux/selectors/content'; diff --git a/ui/page/fileListDownloaded/index.js b/ui/page/fileListDownloaded/index.js index da461a31f..8981e3ba1 100644 --- a/ui/page/fileListDownloaded/index.js +++ b/ui/page/fileListDownloaded/index.js @@ -3,10 +3,12 @@ import { makeSelectSearchDownloadUrlsForPage, selectDownloadUrlsCount, selectIsFetchingFileList, +} from 'redux/selectors/file_info'; +import { makeSelectMyPurchasesForPage, selectIsFetchingMyPurchases, selectMyPurchasesCount, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; import FileListDownloaded from './view'; import { withRouter } from 'react-router'; diff --git a/ui/page/fileListPublished/index.js b/ui/page/fileListPublished/index.js index 3154874a8..fe0188c00 100644 --- a/ui/page/fileListPublished/index.js +++ b/ui/page/fileListPublished/index.js @@ -4,10 +4,9 @@ import { selectMyClaimsPage, selectMyClaimsPageItemCount, selectFetchingMyClaimsPageError, - doClearPublish, - doFetchClaimListMine, - doCheckPendingClaims, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { doFetchClaimListMine, doCheckPendingClaims } from 'redux/actions/claims'; +import { doClearPublish } from 'redux/actions/publish'; import { selectUploadCount } from 'lbryinc'; import FileListPublished from './view'; import { withRouter } from 'react-router'; diff --git a/ui/page/help/view.jsx b/ui/page/help/view.jsx index 22c6bc9a2..3e8f74f74 100644 --- a/ui/page/help/view.jsx +++ b/ui/page/help/view.jsx @@ -7,7 +7,7 @@ import * as React from 'react'; import { shell } from 'electron'; import WalletBackup from 'component/walletBackup'; // @endif -import { Lbry } from 'lbry-redux'; +import Lbry from 'lbry'; import Native from 'native'; import Button from 'component/button'; import Page from 'component/page'; diff --git a/ui/page/invite/index.js b/ui/page/invite/index.js index 2593f7154..4d4fdfbd7 100644 --- a/ui/page/invite/index.js +++ b/ui/page/invite/index.js @@ -1,4 +1,4 @@ -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; import { connect } from 'react-redux'; import { selectUserInviteStatusFailed, @@ -10,14 +10,14 @@ import { makeSelectClientSetting } from 'redux/selectors/settings'; import { doSetClientSetting } from 'redux/actions/settings'; import InvitePage from './view'; -const select = state => ({ +const select = (state) => ({ isFailed: selectUserInviteStatusFailed(state), isPending: selectUserInviteStatusIsPending(state), inviteAcknowledged: makeSelectClientSetting(state)(SETTINGS.INVITE_ACKNOWLEDGED), authenticated: selectUserVerifiedEmail(state), }); -const perform = dispatch => ({ +const perform = (dispatch) => ({ fetchInviteStatus: () => dispatch(doFetchInviteStatus()), acknowledgeInivte: () => dispatch(doSetClientSetting(SETTINGS.INVITE_ACKNOWLEDGED, true)), }); diff --git a/ui/page/invited/index.js b/ui/page/invited/index.js index 1abc81952..2d5478eed 100644 --- a/ui/page/invited/index.js +++ b/ui/page/invited/index.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import InvitedPage from './view'; -import { makeSelectPermanentUrlForUri } from 'lbry-redux'; +import { makeSelectPermanentUrlForUri } from 'redux/selectors/claims'; import { withRouter } from 'react-router'; const select = (state, props) => { @@ -16,9 +16,4 @@ const select = (state, props) => { }; const perform = () => ({}); -export default withRouter( - connect( - select, - perform - )(InvitedPage) -); +export default withRouter(connect(select, perform)(InvitedPage)); diff --git a/ui/page/library/index.js b/ui/page/library/index.js index 09be08445..42c84a883 100644 --- a/ui/page/library/index.js +++ b/ui/page/library/index.js @@ -1,14 +1,10 @@ import { connect } from 'react-redux'; -import { - selectDownloadUrlsCount, - selectIsFetchingFileList, - selectMyPurchases, - selectIsFetchingMyPurchases, - doPurchaseList, -} from 'lbry-redux'; +import { selectDownloadUrlsCount, selectIsFetchingFileList } from 'redux/selectors/file_info'; +import { selectMyPurchases, selectIsFetchingMyPurchases } from 'redux/selectors/claims'; +import { doPurchaseList } from 'redux/actions/claims'; import LibraryPage from './view'; -const select = state => ({ +const select = (state) => ({ allDownloadedUrlsCount: selectDownloadUrlsCount(state), fetchingFileList: selectIsFetchingFileList(state), myPurchases: selectMyPurchases(state), diff --git a/ui/page/listBlocked/index.js b/ui/page/listBlocked/index.js index 178b11aa9..c2f907684 100644 --- a/ui/page/listBlocked/index.js +++ b/ui/page/listBlocked/index.js @@ -12,7 +12,7 @@ import { selectModeratorTimeoutMap, selectPersonalTimeoutMap, } from 'redux/selectors/comments'; -import { selectMyChannelClaims } from 'lbry-redux'; +import { selectMyChannelClaims } from 'redux/selectors/claims'; import ListBlocked from './view'; const select = (state) => ({ diff --git a/ui/page/lists/index.js b/ui/page/lists/index.js index 68c80c9ed..ef574d5ba 100644 --- a/ui/page/lists/index.js +++ b/ui/page/lists/index.js @@ -1,16 +1,16 @@ import { connect } from 'react-redux'; + +import { doPurchaseList } from 'redux/actions/claims'; +import { selectMyPurchases, selectIsFetchingMyPurchases } from 'redux/selectors/claims'; +import { selectDownloadUrlsCount, selectIsFetchingFileList } from 'redux/selectors/file_info'; + import { - selectDownloadUrlsCount, - selectIsFetchingFileList, - selectMyPurchases, - selectIsFetchingMyPurchases, - doPurchaseList, selectBuiltinCollections, selectMyPublishedMixedCollections, selectMyPublishedPlaylistCollections, selectMyUnpublishedCollections, // should probably distinguish types // selectSavedCollections, // TODO: implement saving and copying collections -} from 'lbry-redux'; +} from 'redux/selectors/collections'; import ListsPage from './view'; diff --git a/ui/page/livestream/index.js b/ui/page/livestream/index.js index 70e68f483..a4ab99d66 100644 --- a/ui/page/livestream/index.js +++ b/ui/page/livestream/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { doResolveUri, makeSelectTagInClaimOrChannelForUri, makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectTagInClaimOrChannelForUri, makeSelectClaimForUri } from 'redux/selectors/claims'; +import { doResolveUri } from 'redux/actions/claims'; import { doSetPlayingUri } from 'redux/actions/content'; import { doUserSetReferrer } from 'redux/actions/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; diff --git a/ui/page/livestream/view.jsx b/ui/page/livestream/view.jsx index a4c7cff33..727c66fb0 100644 --- a/ui/page/livestream/view.jsx +++ b/ui/page/livestream/view.jsx @@ -5,7 +5,7 @@ import Page from 'component/page'; import LivestreamLayout from 'component/livestreamLayout'; import LivestreamComments from 'component/livestreamComments'; import analytics from 'analytics'; -import { Lbry } from 'lbry-redux'; +import Lbry from 'lbry'; type Props = { uri: string, diff --git a/ui/page/livestreamSetup/index.js b/ui/page/livestreamSetup/index.js index cbb092976..c00883391 100644 --- a/ui/page/livestreamSetup/index.js +++ b/ui/page/livestreamSetup/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { selectMyChannelClaims, selectFetchingMyChannels, doClearPublish } from 'lbry-redux'; +import { selectMyChannelClaims, selectFetchingMyChannels } from 'redux/selectors/claims'; +import { doClearPublish } from 'redux/actions/publish'; import { selectActiveChannelClaim } from 'redux/selectors/app'; import { doFetchNoSourceClaims } from 'redux/actions/livestream'; import { diff --git a/ui/page/livestreamSetup/view.jsx b/ui/page/livestreamSetup/view.jsx index 62142021a..301dc5368 100644 --- a/ui/page/livestreamSetup/view.jsx +++ b/ui/page/livestreamSetup/view.jsx @@ -9,7 +9,7 @@ import Spinner from 'component/spinner'; import Button from 'component/button'; import ChannelSelector from 'component/channelSelector'; import Yrbl from 'component/yrbl'; -import { Lbry } from 'lbry-redux'; +import Lbry from 'lbry'; import { toHex } from 'util/hex'; import { FormField } from 'component/common/form'; import CopyableText from 'component/copyableText'; diff --git a/ui/page/ownComments/index.js b/ui/page/ownComments/index.js index b4c1aa364..df53ecfa8 100644 --- a/ui/page/ownComments/index.js +++ b/ui/page/ownComments/index.js @@ -7,7 +7,7 @@ import { makeSelectTotalCommentsCountForUri, makeSelectTopLevelTotalPagesForUri, } from 'redux/selectors/comments'; -import { selectClaimsById } from 'lbry-redux'; +import { selectClaimsById } from 'redux/selectors/claims'; import OwnComments from './view'; diff --git a/ui/page/publish/index.js b/ui/page/publish/index.js index dd5b60f09..d25aed008 100644 --- a/ui/page/publish/index.js +++ b/ui/page/publish/index.js @@ -1,9 +1,10 @@ import { connect } from 'react-redux'; -import { selectBalance, selectFetchingMyChannels } from 'lbry-redux'; +import { selectFetchingMyChannels } from 'redux/selectors/claims'; +import { selectBalance } from 'redux/selectors/wallet'; import { selectUnclaimedRewardValue } from 'redux/selectors/rewards'; import PublishPage from './view'; -const select = state => ({ +const select = (state) => ({ balance: selectBalance(state), totalRewardValue: selectUnclaimedRewardValue(state), fetchingChannels: selectFetchingMyChannels(state), diff --git a/ui/page/repost/index.js b/ui/page/repost/index.js index 30aae741e..6dcd035be 100644 --- a/ui/page/repost/index.js +++ b/ui/page/repost/index.js @@ -1,15 +1,15 @@ import { connect } from 'react-redux'; -import { doResolveUri, selectBalance } from 'lbry-redux'; - +import { doResolveUri } from 'redux/actions/claims'; +import { selectBalance } from 'redux/selectors/wallet'; import RepostPage from './view'; const select = (state, props) => ({ balance: selectBalance(state), }); -const perform = dispatch => ({ - resolveUri: uri => dispatch(doResolveUri(uri)), +const perform = (dispatch) => ({ + resolveUri: (uri) => dispatch(doResolveUri(uri)), }); export default connect(select, perform)(RepostPage); diff --git a/ui/page/search/view.jsx b/ui/page/search/view.jsx index 1499e7c33..a89946c56 100644 --- a/ui/page/search/view.jsx +++ b/ui/page/search/view.jsx @@ -1,7 +1,8 @@ // @flow import { SIMPLE_SITE, SHOW_ADS } from 'config'; import React, { useEffect } from 'react'; -import { Lbry, parseURI, isNameValid } from 'lbry-redux'; +import Lbry from 'lbry'; +import { parseURI, isNameValid } from 'util/lbryURI'; import ClaimList from 'component/claimList'; import Page from 'component/page'; import SearchOptions from 'component/searchOptions'; @@ -33,7 +34,7 @@ export default function SearchPage(props: Props) { let isValid = true; try { ({ streamName } = parseURI(uriFromQuery)); - if (!isNameValid(streamName)) { + if (!streamName || !isNameValid(streamName)) { isValid = false; } } catch (e) { diff --git a/ui/page/send/view.jsx b/ui/page/send/view.jsx index dbab04873..ae3682e48 100644 --- a/ui/page/send/view.jsx +++ b/ui/page/send/view.jsx @@ -4,7 +4,7 @@ import Page from 'component/page'; import LbcSymbol from 'component/common/lbc-symbol'; import WalletSend from 'component/walletSend'; import { URL as SITE_URL, URL_LOCAL, URL_DEV } from 'config'; -import { parseURI, isNameValid, isURIValid, normalizeURI } from 'lbry-redux'; +import { parseURI, isNameValid, isURIValid, normalizeURI } from 'util/lbryURI'; type Props = {}; @@ -31,7 +31,7 @@ export default function SendPage(props: Props) { const isLbryUrl = value.startsWith('lbry://') && value !== 'lbry://'; const error = ''; - const addLbryIfNot = term => { + const addLbryIfNot = (term) => { return term.startsWith('lbry://') ? term : `lbry://${term}`; }; if (wasCopiedFromWeb) { diff --git a/ui/page/settingsCreator/view.jsx b/ui/page/settingsCreator/view.jsx index dd8c5b2ff..b32da3d52 100644 --- a/ui/page/settingsCreator/view.jsx +++ b/ui/page/settingsCreator/view.jsx @@ -10,7 +10,7 @@ import Spinner from 'component/spinner'; import { FormField } from 'component/common/form-components/form-field'; import LbcSymbol from 'component/common/lbc-symbol'; import I18nMessage from 'component/i18nMessage'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; import debounce from 'util/debounce'; const DEBOUNCE_REFRESH_MS = 1000; diff --git a/ui/page/settingsNotifications/view.jsx b/ui/page/settingsNotifications/view.jsx index f086a9682..4f23c2adb 100644 --- a/ui/page/settingsNotifications/view.jsx +++ b/ui/page/settingsNotifications/view.jsx @@ -1,6 +1,7 @@ // @flow import * as ICONS from 'constants/icons'; import * as PAGES from 'constants/pages'; +import * as SETTINGS from 'constants/settings'; import * as React from 'react'; import Page from 'component/page'; @@ -12,7 +13,6 @@ import { useHistory } from 'react-router'; import { Redirect } from 'react-router-dom'; import Yrbl from 'component/yrbl'; import Button from 'component/button'; -import { SETTINGS } from 'lbry-redux'; type Props = { osNotificationsEnabled: boolean, diff --git a/ui/page/show/index.js b/ui/page/show/index.js index 6288ecad4..27e36d716 100644 --- a/ui/page/show/index.js +++ b/ui/page/show/index.js @@ -4,23 +4,24 @@ import { connect } from 'react-redux'; import { withRouter } from 'react-router'; import { PAGE_SIZE } from 'constants/claim'; import { - doResolveUri, makeSelectClaimForUri, makeSelectIsUriResolving, makeSelectTotalPagesForChannel, makeSelectTitleForUri, - normalizeURI, makeSelectClaimIsMine, makeSelectClaimIsPending, makeSelectClaimIsStreamPlaceholder, - doClearPublish, - doPrepareEdit, - doFetchItemsInCollection, +} from 'redux/selectors/claims'; +import { makeSelectCollectionForId, makeSelectUrlsForCollectionId, makeSelectIsResolvingCollectionForId, - COLLECTIONS_CONSTS, -} from 'lbry-redux'; +} from 'redux/selectors/collections'; +import { doResolveUri } from 'redux/actions/claims'; +import { doClearPublish, doPrepareEdit } from 'redux/actions/publish'; +import { doFetchItemsInCollection } from 'redux/actions/collections'; +import { normalizeURI } from 'util/lbryURI'; +import * as COLLECTIONS_CONSTS from 'constants/collections'; import { push } from 'connected-react-router'; import { makeSelectChannelInSubscriptions } from 'redux/selectors/subscriptions'; import { selectBlackListedOutpoints } from 'lbryinc'; diff --git a/ui/page/show/view.jsx b/ui/page/show/view.jsx index 515a1e677..5f1318c5c 100644 --- a/ui/page/show/view.jsx +++ b/ui/page/show/view.jsx @@ -10,7 +10,8 @@ import Page from 'component/page'; import Button from 'component/button'; import Card from 'component/common/card'; import { formatLbryUrlForWeb } from 'util/url'; -import { parseURI, COLLECTIONS_CONSTS } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; +import * as COLLECTIONS_CONSTS from 'constants/collections'; const AbandonedChannelPreview = lazyImport(() => import('component/abandonedChannelPreview' /* webpackChunkName: "abandonedChannelPreview" */) diff --git a/ui/page/tagsFollowing/index.js b/ui/page/tagsFollowing/index.js index 7d03ef6bc..bdd310dfe 100644 --- a/ui/page/tagsFollowing/index.js +++ b/ui/page/tagsFollowing/index.js @@ -4,9 +4,9 @@ import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectSubscriptions } from 'redux/selectors/subscriptions'; import DiscoverPage from './view'; import { makeSelectClientSetting } from 'redux/selectors/settings'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; -const select = state => ({ +const select = (state) => ({ followedTags: selectFollowedTags(state), subscribedChannels: selectSubscriptions(state), email: selectUserVerifiedEmail(state), diff --git a/ui/page/top/index.js b/ui/page/top/index.js index 16130d033..da7f3cd18 100644 --- a/ui/page/top/index.js +++ b/ui/page/top/index.js @@ -1,6 +1,7 @@ import { connect } from 'react-redux'; import TopPage from './view'; -import { doClearPublish, doPrepareEdit, doResolveUris } from 'lbry-redux'; +import { doClearPublish, doPrepareEdit } from 'redux/actions/publish'; +import { doResolveUris } from 'redux/actions/claims'; import { push } from 'connected-react-router'; import * as PAGES from 'constants/pages'; @@ -14,13 +15,13 @@ const select = (state, props) => { }; }; -const perform = dispatch => ({ - beginPublish: name => { +const perform = (dispatch) => ({ + beginPublish: (name) => { dispatch(doClearPublish()); dispatch(doPrepareEdit({ name })); dispatch(push(`/$/${PAGES.UPLOAD}`)); }, - doResolveUris: uris => dispatch(doResolveUris(uris)), + doResolveUris: (uris) => dispatch(doResolveUris(uris)), }); export default connect(select, perform)(TopPage); diff --git a/ui/page/wallet/index.js b/ui/page/wallet/index.js index 2e51e48c3..3b9551596 100644 --- a/ui/page/wallet/index.js +++ b/ui/page/wallet/index.js @@ -1,9 +1,9 @@ import { connect } from 'react-redux'; -import { selectTotalBalance } from 'lbry-redux'; +import { selectTotalBalance } from 'redux/selectors/wallet'; import { doOpenModal } from 'redux/actions/app'; import Wallet from './view'; -const select = state => ({ +const select = (state) => ({ totalBalance: selectTotalBalance(state), }); diff --git a/ui/page/youtubeSync/view.jsx b/ui/page/youtubeSync/view.jsx index 8a0fae563..a2b359495 100644 --- a/ui/page/youtubeSync/view.jsx +++ b/ui/page/youtubeSync/view.jsx @@ -9,7 +9,7 @@ import Card from 'component/common/card'; import I18nMessage from 'component/i18nMessage'; import { Form, FormField } from 'component/common/form'; import { INVALID_NAME_ERROR } from 'constants/claim'; -import { isNameValid } from 'lbry-redux'; +import { isNameValid } from 'util/lbryURI'; import { Lbryio } from 'lbryinc'; import { useHistory } from 'react-router'; import YoutubeTransferStatus from 'component/youtubeTransferStatus'; diff --git a/ui/reducers.js b/ui/reducers.js index 58fc7e1ed..2728436b5 100644 --- a/ui/reducers.js +++ b/ui/reducers.js @@ -1,7 +1,11 @@ import { combineReducers } from 'redux'; import { connectRouter } from 'connected-react-router'; -import { claimsReducer, fileInfoReducer, walletReducer, publishReducer, collectionsReducer } from 'lbry-redux'; -import { costInfoReducer, blacklistReducer, filteredReducer, homepageReducer, statsReducer, webReducer } from 'lbryinc'; +import { costInfoReducer, blacklistReducer, filteredReducer, statsReducer, webReducer } from 'lbryinc'; +import { claimsReducer } from 'redux/reducers/claims'; +import { fileInfoReducer } from 'redux/reducers/file_info'; +import { walletReducer } from 'redux/reducers/wallet'; +import { publishReducer } from 'redux/reducers/publish'; +import { collectionsReducer } from 'redux/reducers/collections'; import appReducer from 'redux/reducers/app'; import tagsReducer from 'redux/reducers/tags'; import contentReducer from 'redux/reducers/content'; @@ -30,7 +34,6 @@ export default (history) => content: contentReducer, costInfo: costInfoReducer, fileInfo: fileInfoReducer, - homepage: homepageReducer, livestream: livestreamReducer, notifications: notificationsReducer, publish: publishReducer, diff --git a/ui/redux/actions/app.js b/ui/redux/actions/app.js index cb0b81332..ac08c8f59 100644 --- a/ui/redux/actions/app.js +++ b/ui/redux/actions/app.js @@ -6,25 +6,16 @@ import { ipcRenderer, remote } from 'electron'; import path from 'path'; import * as ACTIONS from 'constants/action_types'; import * as MODALS from 'constants/modal_types'; +import * as SETTINGS from 'constants/settings'; +import * as DAEMON_SETTINGS from 'constants/daemon_settings'; +import * as SHARED_PREFERENCES from 'constants/shared_preferences'; import { DOMAIN, SIMPLE_SITE } from 'config'; -import { - Lbry, - doBalanceSubscribe, - doFetchFileInfos, - makeSelectClaimForUri, - makeSelectClaimIsMine, - doPopulateSharedUserState, - doFetchChannelListMine, - doFetchCollectionListMine, - doClearPublish, - doPreferenceGet, - doClearSupport, - SHARED_PREFERENCES, - DAEMON_SETTINGS, - SETTINGS, - selectMyChannelClaims, - doCheckPendingClaims, -} from 'lbry-redux'; +import Lbry from 'lbry'; +import { doFetchChannelListMine, doFetchCollectionListMine, doCheckPendingClaims } from 'redux/actions/claims'; +import { makeSelectClaimForUri, makeSelectClaimIsMine, selectMyChannelClaims } from 'redux/selectors/claims'; +import { doFetchFileInfos } from 'redux/actions/file_info'; +import { doClearSupport, doBalanceSubscribe } from 'redux/actions/wallet'; +import { doClearPublish } from 'redux/actions/publish'; import { Lbryio } from 'lbryinc'; import { selectFollowedTagsList } from 'redux/selectors/tags'; import { doToast, doError, doNotificationList } from 'redux/actions/notifications'; @@ -51,8 +42,7 @@ import { } from 'redux/selectors/app'; import { selectDaemonSettings, makeSelectClientSetting } from 'redux/selectors/settings'; import { selectUser, selectUserVerifiedEmail } from 'redux/selectors/user'; -// import { selectDaemonSettings } from 'redux/selectors/settings'; -import { doSyncLoop, doSetPrefsReady } from 'redux/actions/sync'; +import { doSyncLoop, doSetPrefsReady, doPreferenceGet, doPopulateSharedUserState } from 'redux/actions/sync'; import { doAuthenticate } from 'redux/actions/user'; import { lbrySettings as config, version as appVersion } from 'package.json'; import analytics, { SHARE_INTERNAL } from 'analytics'; diff --git a/ui/redux/actions/claims.js b/ui/redux/actions/claims.js new file mode 100644 index 000000000..d97309e33 --- /dev/null +++ b/ui/redux/actions/claims.js @@ -0,0 +1,1038 @@ +// @flow +import * as ACTIONS from 'constants/action_types'; +import * as ABANDON_STATES from 'constants/abandon_states'; +import Lbry from 'lbry'; +import { normalizeURI } from 'util/lbryURI'; +import { doToast } from 'redux/actions/notifications'; +import { + selectMyClaimsRaw, + selectResolvingUris, + selectClaimsByUri, + selectMyChannelClaims, + selectPendingClaimsById, +} from 'redux/selectors/claims'; + +import { doFetchTxoPage } from 'redux/actions/wallet'; +import { selectSupportsByOutpoint } from 'redux/selectors/wallet'; +import { creditsToString } from 'util/format-credits'; +import { batchActions } from 'util/batch-actions'; +import { createNormalizedClaimSearchKey } from 'util/claim'; +import { PAGE_SIZE } from 'constants/claim'; +import { makeSelectClaimIdsForCollectionId } from 'redux/selectors/collections'; +import { doFetchItemsInCollections } from 'redux/actions/collections'; + +let onChannelConfirmCallback; +let checkPendingInterval; + +export function doResolveUris( + uris: Array, + returnCachedClaims: boolean = false, + resolveReposts: boolean = true +) { + return (dispatch: Dispatch, getState: GetState) => { + const normalizedUris = uris.map(normalizeURI); + const state = getState(); + + const resolvingUris = selectResolvingUris(state); + const claimsByUri = selectClaimsByUri(state); + const urisToResolve = normalizedUris.filter((uri) => { + if (resolvingUris.includes(uri)) { + return false; + } + + return returnCachedClaims ? !claimsByUri[uri] : true; + }); + + if (urisToResolve.length === 0) { + return; + } + + const options: { include_is_my_output?: boolean, include_purchase_receipt: boolean } = { + include_purchase_receipt: true, + }; + + if (urisToResolve.length === 1) { + options.include_is_my_output = true; + } + dispatch({ + type: ACTIONS.RESOLVE_URIS_STARTED, + data: { uris: normalizedUris }, + }); + + const resolveInfo: { + [string]: { + stream: ?StreamClaim, + channel: ?ChannelClaim, + claimsInChannel: ?number, + collection: ?CollectionClaim, + }, + } = {}; + + const collectionIds: Array = []; + + return Lbry.resolve({ urls: urisToResolve, ...options }).then(async (result: ResolveResponse) => { + let repostedResults = {}; + const repostsToResolve = []; + const fallbackResolveInfo = { + stream: null, + claimsInChannel: null, + channel: null, + }; + + function processResult(result, resolveInfo = {}, checkReposts = false) { + Object.entries(result).forEach(([uri, uriResolveInfo]) => { + // Flow has terrible Object.entries support + // https://github.com/facebook/flow/issues/2221 + if (uriResolveInfo) { + if (uriResolveInfo.error) { + // $FlowFixMe + resolveInfo[uri] = { ...fallbackResolveInfo }; + } else { + if (checkReposts) { + if (uriResolveInfo.reposted_claim) { + // $FlowFixMe + const repostUrl = uriResolveInfo.reposted_claim.permanent_url; + if (!resolvingUris.includes(repostUrl)) { + repostsToResolve.push(repostUrl); + } + } + } + let result = {}; + if (uriResolveInfo.value_type === 'channel') { + result.channel = uriResolveInfo; + // $FlowFixMe + result.claimsInChannel = uriResolveInfo.meta.claims_in_channel; + } else if (uriResolveInfo.value_type === 'collection') { + result.collection = uriResolveInfo; + // $FlowFixMe + collectionIds.push(uriResolveInfo.claim_id); + } else { + result.stream = uriResolveInfo; + if (uriResolveInfo.signing_channel) { + result.channel = uriResolveInfo.signing_channel; + result.claimsInChannel = + (uriResolveInfo.signing_channel.meta && uriResolveInfo.signing_channel.meta.claims_in_channel) || 0; + } + } + // $FlowFixMe + resolveInfo[uri] = result; + } + } + }); + } + processResult(result, resolveInfo, resolveReposts); + + if (repostsToResolve.length) { + dispatch({ + type: ACTIONS.RESOLVE_URIS_STARTED, + data: { uris: repostsToResolve, debug: 'reposts' }, + }); + repostedResults = await Lbry.resolve({ urls: repostsToResolve, ...options }); + } + processResult(repostedResults, resolveInfo); + + dispatch({ + type: ACTIONS.RESOLVE_URIS_COMPLETED, + data: { resolveInfo }, + }); + + if (collectionIds.length) { + dispatch(doFetchItemsInCollections({ collectionIds: collectionIds, pageSize: 5 })); + } + + return result; + }); + }; +} + +export function doResolveUri(uri: string) { + return doResolveUris([uri]); +} + +export function doFetchClaimListMine( + page: number = 1, + pageSize: number = 99999, + resolve: boolean = true, + filterBy: Array = [] +) { + return (dispatch: Dispatch) => { + dispatch({ + type: ACTIONS.FETCH_CLAIM_LIST_MINE_STARTED, + }); + + let claimTypes = ['stream', 'repost']; + if (filterBy && filterBy.length !== 0) { + claimTypes = claimTypes.filter((t) => filterBy.includes(t)); + } + + // $FlowFixMe + Lbry.claim_list({ + page: page, + page_size: pageSize, + claim_type: claimTypes, + resolve, + }).then((result: StreamListResponse) => { + dispatch({ + type: ACTIONS.FETCH_CLAIM_LIST_MINE_COMPLETED, + data: { + result, + resolve, + }, + }); + }); + }; +} + +export function doAbandonTxo(txo: Txo, cb: (string) => void) { + return (dispatch: Dispatch) => { + if (cb) cb(ABANDON_STATES.PENDING); + const isClaim = txo.type === 'claim'; + const isSupport = txo.type === 'support' && txo.is_my_input === true; + const isTip = txo.type === 'support' && txo.is_my_input === false; + + const data = isClaim ? { claimId: txo.claim_id } : { outpoint: `${txo.txid}:${txo.nout}` }; + + const startedActionType = isClaim ? ACTIONS.ABANDON_CLAIM_STARTED : ACTIONS.ABANDON_SUPPORT_STARTED; + const completedActionType = isClaim ? ACTIONS.ABANDON_CLAIM_SUCCEEDED : ACTIONS.ABANDON_SUPPORT_COMPLETED; + + dispatch({ + type: startedActionType, + data, + }); + + const errorCallback = () => { + if (cb) cb(ABANDON_STATES.ERROR); + dispatch( + doToast({ + message: isClaim ? 'Error abandoning your claim/support' : 'Error unlocking your tip', + isError: true, + }) + ); + }; + + const successCallback = () => { + dispatch({ + type: completedActionType, + data, + }); + + let abandonMessage; + if (isClaim) { + abandonMessage = __('Successfully abandoned your claim.'); + } else if (isSupport) { + abandonMessage = __('Successfully abandoned your support.'); + } else { + abandonMessage = __('Successfully unlocked your tip!'); + } + if (cb) cb(ABANDON_STATES.DONE); + + dispatch( + doToast({ + message: abandonMessage, + }) + ); + }; + + const abandonParams: { + claim_id?: string, + txid?: string, + nout?: number, + } = { + blocking: true, + }; + if (isClaim) { + abandonParams['claim_id'] = txo.claim_id; + } else { + abandonParams['txid'] = txo.txid; + abandonParams['nout'] = txo.nout; + } + + let method; + if (isSupport || isTip) { + method = 'support_abandon'; + } else if (isClaim) { + const { normalized_name: claimName } = txo; + method = claimName.startsWith('@') ? 'channel_abandon' : 'stream_abandon'; + } + + if (!method) { + console.error('No "method" chosen for claim or support abandon'); + return; + } + + Lbry[method](abandonParams).then(successCallback, errorCallback); + }; +} + +export function doAbandonClaim(txid: string, nout: number, cb: (string) => void) { + const outpoint = `${txid}:${nout}`; + + return (dispatch: Dispatch, getState: GetState) => { + const state = getState(); + const myClaims: Array = selectMyClaimsRaw(state); + const mySupports: { [string]: Support } = selectSupportsByOutpoint(state); + + // A user could be trying to abandon a support or one of their claims + const claimToAbandon = myClaims.find((claim) => claim.txid === txid && claim.nout === nout); + const supportToAbandon = mySupports[outpoint]; + + if (!claimToAbandon && !supportToAbandon) { + console.error('No associated support or claim with txid: ', txid); + return; + } + + const data = claimToAbandon + ? { claimId: claimToAbandon.claim_id } + : { outpoint: `${supportToAbandon.txid}:${supportToAbandon.nout}` }; + + const isClaim = !!claimToAbandon; + const startedActionType = isClaim ? ACTIONS.ABANDON_CLAIM_STARTED : ACTIONS.ABANDON_SUPPORT_STARTED; + const completedActionType = isClaim ? ACTIONS.ABANDON_CLAIM_SUCCEEDED : ACTIONS.ABANDON_SUPPORT_COMPLETED; + + dispatch({ + type: startedActionType, + data, + }); + + const errorCallback = () => { + dispatch( + doToast({ + message: isClaim ? 'Error abandoning your claim/support' : 'Error unlocking your tip', + isError: true, + }) + ); + if (cb) cb(ABANDON_STATES.ERROR); + }; + + const successCallback = () => { + dispatch({ + type: completedActionType, + data, + }); + if (cb) cb(ABANDON_STATES.DONE); + + let abandonMessage; + if (isClaim) { + abandonMessage = __('Successfully abandoned your claim.'); + } else if (supportToAbandon) { + abandonMessage = __('Successfully abandoned your support.'); + } else { + abandonMessage = __('Successfully unlocked your tip!'); + } + + dispatch( + doToast({ + message: abandonMessage, + }) + ); + dispatch(doFetchTxoPage()); + }; + + const abandonParams = { + txid, + nout, + blocking: true, + }; + + let method; + if (supportToAbandon) { + method = 'support_abandon'; + } else if (claimToAbandon) { + const { name: claimName } = claimToAbandon; + method = claimName.startsWith('@') ? 'channel_abandon' : 'stream_abandon'; + } + + if (!method) { + console.error('No "method" chosen for claim or support abandon'); + return; + } + + Lbry[method](abandonParams).then(successCallback, errorCallback); + }; +} + +export function doFetchClaimsByChannel(uri: string, page: number = 1) { + return (dispatch: Dispatch) => { + dispatch({ + type: ACTIONS.FETCH_CHANNEL_CLAIMS_STARTED, + data: { uri, page }, + }); + + Lbry.claim_search({ + channel: uri, + valid_channel_signature: true, + page: page || 1, + order_by: ['release_time'], + include_is_my_output: true, + include_purchase_receipt: true, + }).then((result: ClaimSearchResponse) => { + const { items: claims, total_items: claimsInChannel, page: returnedPage } = result; + + dispatch({ + type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED, + data: { + uri, + claimsInChannel, + claims: claims || [], + page: returnedPage || undefined, + }, + }); + }); + }; +} + +export function doClearChannelErrors() { + return { + type: ACTIONS.CLEAR_CHANNEL_ERRORS, + }; +} + +export function doCreateChannel(name: string, amount: number, optionalParams: any, onConfirm: any) { + return (dispatch: Dispatch) => { + dispatch({ + type: ACTIONS.CREATE_CHANNEL_STARTED, + }); + + const createParams: { + name: string, + bid: string, + blocking: true, + title?: string, + cover_url?: string, + thumbnail_url?: string, + description?: string, + website_url?: string, + email?: string, + tags?: Array, + languages?: Array, + } = { + name, + bid: creditsToString(amount), + blocking: true, + }; + + if (optionalParams) { + if (optionalParams.title) { + createParams.title = optionalParams.title; + } + if (optionalParams.coverUrl) { + createParams.cover_url = optionalParams.coverUrl; + } + if (optionalParams.thumbnailUrl) { + createParams.thumbnail_url = optionalParams.thumbnailUrl; + } + if (optionalParams.description) { + createParams.description = optionalParams.description; + } + if (optionalParams.website) { + createParams.website_url = optionalParams.website; + } + if (optionalParams.email) { + createParams.email = optionalParams.email; + } + if (optionalParams.tags) { + createParams.tags = optionalParams.tags.map((tag) => tag.name); + } + if (optionalParams.languages) { + createParams.languages = optionalParams.languages; + } + } + + return ( + Lbry.channel_create(createParams) + // outputs[0] is the certificate + // outputs[1] is the change from the tx, not in the app currently + .then((result: ChannelCreateResponse) => { + const channelClaim = result.outputs[0]; + dispatch({ + type: ACTIONS.CREATE_CHANNEL_COMPLETED, + data: { channelClaim }, + }); + dispatch({ + type: ACTIONS.UPDATE_PENDING_CLAIMS, + data: { + claims: [channelClaim], + }, + }); + dispatch(doCheckPendingClaims(onConfirm)); + return channelClaim; + }) + .catch((error) => { + dispatch({ + type: ACTIONS.CREATE_CHANNEL_FAILED, + data: error.message, + }); + }) + ); + }; +} + +export function doUpdateChannel(params: any, cb: any) { + return (dispatch: Dispatch, getState: GetState) => { + dispatch({ + type: ACTIONS.UPDATE_CHANNEL_STARTED, + }); + const state = getState(); + const myChannels = selectMyChannelClaims(state); + const channelClaim = myChannels.find((myChannel) => myChannel.claim_id === params.claim_id); + + const updateParams = { + claim_id: params.claim_id, + bid: creditsToString(params.amount), + title: params.title, + cover_url: params.coverUrl, + thumbnail_url: params.thumbnailUrl, + description: params.description, + website_url: params.website, + email: params.email, + tags: [], + replace: true, + languages: params.languages || [], + locations: [], + blocking: true, + }; + + if (params.tags) { + updateParams.tags = params.tags.map((tag) => tag.name); + } + + // we'll need to remove these once we add locations/channels to channel page edit/create options + if (channelClaim && channelClaim.value && channelClaim.value.locations) { + updateParams.locations = channelClaim.value.locations; + } + + return Lbry.channel_update(updateParams) + .then((result: ChannelUpdateResponse) => { + const channelClaim = result.outputs[0]; + dispatch({ + type: ACTIONS.UPDATE_CHANNEL_COMPLETED, + data: { channelClaim }, + }); + dispatch({ + type: ACTIONS.UPDATE_PENDING_CLAIMS, + data: { + claims: [channelClaim], + }, + }); + dispatch(doCheckPendingClaims(cb)); + return Boolean(result.outputs[0]); + }) + .then() + .catch((error) => { + dispatch({ + type: ACTIONS.UPDATE_CHANNEL_FAILED, + data: error, + }); + }); + }; +} + +export function doImportChannel(certificate: string) { + return (dispatch: Dispatch) => { + dispatch({ + type: ACTIONS.IMPORT_CHANNEL_STARTED, + }); + + return Lbry.channel_import({ channel_data: certificate }) + .then(() => { + dispatch({ + type: ACTIONS.IMPORT_CHANNEL_COMPLETED, + }); + }) + .catch((error) => { + dispatch({ + type: ACTIONS.IMPORT_CHANNEL_FAILED, + data: error, + }); + }); + }; +} + +export function doFetchChannelListMine(page: number = 1, pageSize: number = 99999, resolve: boolean = true) { + return (dispatch: Dispatch) => { + dispatch({ + type: ACTIONS.FETCH_CHANNEL_LIST_STARTED, + }); + + const callback = (response: ChannelListResponse) => { + dispatch({ + type: ACTIONS.FETCH_CHANNEL_LIST_COMPLETED, + data: { claims: response.items }, + }); + }; + + const failure = (error) => { + dispatch({ + type: ACTIONS.FETCH_CHANNEL_LIST_FAILED, + data: error, + }); + }; + + Lbry.channel_list({ page, page_size: pageSize, resolve }).then(callback, failure); + }; +} + +export function doFetchCollectionListMine(page: number = 1, pageSize: number = 99999) { + return (dispatch: Dispatch) => { + dispatch({ + type: ACTIONS.FETCH_COLLECTION_LIST_STARTED, + }); + + const callback = (response: CollectionListResponse) => { + const { items } = response; + dispatch({ + type: ACTIONS.FETCH_COLLECTION_LIST_COMPLETED, + data: { claims: items }, + }); + dispatch( + doFetchItemsInCollections({ + collectionIds: items.map((claim) => claim.claim_id), + page_size: 5, + }) + ); + }; + + const failure = (error) => { + dispatch({ + type: ACTIONS.FETCH_COLLECTION_LIST_FAILED, + data: error, + }); + }; + + Lbry.collection_list({ page, page_size: pageSize, resolve_claims: 1, resolve: true }).then(callback, failure); + }; +} + +export function doClaimSearch( + options: { + page_size: number, + page: number, + no_totals?: boolean, + any_tags?: Array, + claim_ids?: Array, + channel_ids?: Array, + not_channel_ids?: Array, + not_tags?: Array, + order_by?: Array, + release_time?: string, + has_source?: boolean, + has_no_souce?: boolean, + } = { + no_totals: true, + page_size: 10, + page: 1, + } +) { + const query = createNormalizedClaimSearchKey(options); + return async (dispatch: Dispatch) => { + dispatch({ + type: ACTIONS.CLAIM_SEARCH_STARTED, + data: { query: query }, + }); + + const success = (data: ClaimSearchResponse) => { + const resolveInfo = {}; + const urls = []; + data.items.forEach((stream: Claim) => { + resolveInfo[stream.canonical_url] = { stream }; + urls.push(stream.canonical_url); + }); + + dispatch({ + type: ACTIONS.CLAIM_SEARCH_COMPLETED, + data: { + query, + resolveInfo, + urls, + append: options.page && options.page !== 1, + pageSize: options.page_size, + }, + }); + return resolveInfo; + }; + + const failure = (err) => { + dispatch({ + type: ACTIONS.CLAIM_SEARCH_FAILED, + data: { query }, + error: err, + }); + return false; + }; + + return await Lbry.claim_search({ + ...options, + include_purchase_receipt: true, + }).then(success, failure); + }; +} + +export function doRepost(options: StreamRepostOptions) { + return (dispatch: Dispatch): Promise => { + return new Promise((resolve) => { + dispatch({ + type: ACTIONS.CLAIM_REPOST_STARTED, + }); + + function success(response) { + const repostClaim = response.outputs[0]; + dispatch({ + type: ACTIONS.CLAIM_REPOST_COMPLETED, + data: { + originalClaimId: options.claim_id, + repostClaim, + }, + }); + dispatch({ + type: ACTIONS.UPDATE_PENDING_CLAIMS, + data: { + claims: [repostClaim], + }, + }); + + dispatch(doFetchClaimListMine(1, 10)); + resolve(repostClaim); + } + + function failure(error) { + dispatch({ + type: ACTIONS.CLAIM_REPOST_FAILED, + data: { + error: error.message, + }, + }); + } + + Lbry.stream_repost(options).then(success, failure); + }); + }; +} + +export function doCollectionPublish( + options: { + name: string, + bid: string, + blocking: true, + title?: string, + channel_id?: string, + thumbnail_url?: string, + description?: string, + tags?: Array, + languages?: Array, + claims: Array, + }, + localId: string +) { + return (dispatch: Dispatch): Promise => { + // $FlowFixMe + + const params: { + name: string, + bid: string, + channel_id?: string, + blocking?: true, + title?: string, + thumbnail_url?: string, + description?: string, + tags?: Array, + languages?: Array, + claims: Array, + } = { + name: options.name, + bid: creditsToString(options.bid), + title: options.title, + thumbnail_url: options.thumbnail_url, + description: options.description, + tags: [], + languages: options.languages || [], + locations: [], + blocking: true, + claims: options.claims, + }; + + if (options.tags) { + params['tags'] = options.tags.map((tag) => tag.name); + } + + if (options.channel_id) { + params['channel_id'] = options.channel_id; + } + + return new Promise((resolve) => { + dispatch({ + type: ACTIONS.COLLECTION_PUBLISH_STARTED, + }); + + function success(response) { + const collectionClaim = response.outputs[0]; + dispatch( + batchActions( + { + type: ACTIONS.COLLECTION_PUBLISH_COMPLETED, + data: { claimId: collectionClaim.claim_id }, + }, + // move unpublished collection to pending collection with new publish id + // recent publish won't resolve this second. handle it in checkPending + { + type: ACTIONS.UPDATE_PENDING_CLAIMS, + data: { + claims: [collectionClaim], + }, + } + ) + ); + dispatch({ + type: ACTIONS.COLLECTION_PENDING, + data: { localId: localId, claimId: collectionClaim.claim_id }, + }); + dispatch(doCheckPendingClaims()); + dispatch(doFetchCollectionListMine(1, 10)); + return resolve(collectionClaim); + } + + function failure(error) { + dispatch({ + type: ACTIONS.COLLECTION_PUBLISH_FAILED, + data: { + error: error.message, + }, + }); + } + + return Lbry.collection_create(params).then(success, failure); + }); + }; +} + +export function doCollectionPublishUpdate( + options: { + bid?: string, + blocking?: true, + title?: string, + thumbnail_url?: string, + description?: string, + claim_id: string, + tags?: Array, + languages?: Array, + claims?: Array, + channel_id?: string, + }, + isBackgroundUpdate?: boolean +) { + return (dispatch: Dispatch, getState: GetState): Promise => { + // TODO: implement one click update + + const updateParams: { + bid?: string, + blocking?: true, + title?: string, + thumbnail_url?: string, + channel_id?: string, + description?: string, + claim_id: string, + tags?: Array, + languages?: Array, + claims?: Array, + clear_claims: boolean, + replace?: boolean, + } = isBackgroundUpdate + ? { + blocking: true, + claim_id: options.claim_id, + clear_claims: true, + } + : { + bid: creditsToString(options.bid), + title: options.title, + thumbnail_url: options.thumbnail_url, + description: options.description, + tags: [], + languages: options.languages || [], + locations: [], + blocking: true, + claim_id: options.claim_id, + clear_claims: true, + replace: true, + }; + + if (isBackgroundUpdate && updateParams.claim_id) { + const state = getState(); + updateParams['claims'] = makeSelectClaimIdsForCollectionId(updateParams.claim_id)(state); + } else if (options.claims) { + updateParams['claims'] = options.claims; + } + + if (options.tags) { + updateParams['tags'] = options.tags.map((tag) => tag.name); + } + + if (options.channel_id) { + updateParams['channel_id'] = options.channel_id; + } + + return new Promise((resolve) => { + dispatch({ + type: ACTIONS.COLLECTION_PUBLISH_UPDATE_STARTED, + }); + + function success(response) { + const collectionClaim = response.outputs[0]; + dispatch({ + type: ACTIONS.COLLECTION_PUBLISH_UPDATE_COMPLETED, + data: { + collectionClaim, + }, + }); + dispatch({ + type: ACTIONS.COLLECTION_PENDING, + data: { claimId: collectionClaim.claim_id }, + }); + dispatch({ + type: ACTIONS.UPDATE_PENDING_CLAIMS, + data: { + claims: [collectionClaim], + }, + }); + dispatch(doCheckPendingClaims()); + return resolve(collectionClaim); + } + + function failure(error) { + dispatch({ + type: ACTIONS.COLLECTION_PUBLISH_UPDATE_FAILED, + data: { + error: error.message, + }, + }); + } + + return Lbry.collection_update(updateParams).then(success, failure); + }); + }; +} + +export function doCheckPublishNameAvailability(name: string) { + return (dispatch: Dispatch) => { + dispatch({ + type: ACTIONS.CHECK_PUBLISH_NAME_STARTED, + }); + + return Lbry.claim_list({ name: name }).then((result) => { + dispatch({ + type: ACTIONS.CHECK_PUBLISH_NAME_COMPLETED, + }); + if (result.items.length) { + dispatch({ + type: ACTIONS.FETCH_CLAIM_LIST_MINE_COMPLETED, + data: { + result, + resolve: false, + }, + }); + } + return !(result && result.items && result.items.length); + }); + }; +} + +export function doClearRepostError() { + return { + type: ACTIONS.CLEAR_REPOST_ERROR, + }; +} + +export function doPurchaseList(page: number = 1, pageSize: number = PAGE_SIZE) { + return (dispatch: Dispatch) => { + dispatch({ + type: ACTIONS.PURCHASE_LIST_STARTED, + }); + + const success = (result: PurchaseListResponse) => { + console.log('PL RESULT', result); + return dispatch({ + type: ACTIONS.PURCHASE_LIST_COMPLETED, + data: { + result, + }, + }); + }; + + const failure = (error) => { + dispatch({ + type: ACTIONS.PURCHASE_LIST_FAILED, + data: { + error: error.message, + }, + }); + }; + + Lbry.purchase_list({ + page: page, + page_size: pageSize, + resolve: true, + }).then(success, failure); + }; +} + +export const doCheckPendingClaims = (onChannelConfirmed: Function) => (dispatch: Dispatch, getState: GetState) => { + if (onChannelConfirmed) { + onChannelConfirmCallback = onChannelConfirmed; + } + clearInterval(checkPendingInterval); + const checkTxoList = () => { + const state = getState(); + const pendingById = Object.assign({}, selectPendingClaimsById(state)); + const pendingTxos = (Object.values(pendingById): any).map((p) => p.txid); + // use collections + if (pendingTxos.length) { + Lbry.txo_list({ txid: pendingTxos }) + .then((result) => { + const txos = result.items; + const idsToConfirm = []; + txos.forEach((txo) => { + if (txo.claim_id && txo.confirmations > 0) { + idsToConfirm.push(txo.claim_id); + delete pendingById[txo.claim_id]; + } + }); + return { idsToConfirm, pendingById }; + }) + .then((results) => { + const { idsToConfirm, pendingById } = results; + if (idsToConfirm.length) { + return Lbry.claim_list({ claim_id: idsToConfirm, resolve: true }).then((results) => { + const claims = results.items; + const collectionIds = claims.filter((c) => c.value_type === 'collection').map((c) => c.claim_id); + dispatch({ + type: ACTIONS.UPDATE_CONFIRMED_CLAIMS, + data: { + claims: claims, + pending: pendingById, + }, + }); + if (collectionIds.length) { + dispatch( + doFetchItemsInCollections({ + collectionIds, + }) + ); + } + const channelClaims = claims.filter((claim) => claim.value_type === 'channel'); + if (channelClaims.length && onChannelConfirmCallback) { + channelClaims.forEach((claim) => onChannelConfirmCallback(claim)); + } + if (Object.keys(pendingById).length === 0) { + clearInterval(checkPendingInterval); + } + }); + } + }); + } else { + clearInterval(checkPendingInterval); + } + }; + // do something with onConfirmed (typically get blocklist for channel) + checkPendingInterval = setInterval(() => { + checkTxoList(); + }, 30000); +}; diff --git a/ui/redux/actions/collections.js b/ui/redux/actions/collections.js new file mode 100644 index 000000000..ae060e8c9 --- /dev/null +++ b/ui/redux/actions/collections.js @@ -0,0 +1,487 @@ +// @flow +import * as ACTIONS from 'constants/action_types'; +import { v4 as uuid } from 'uuid'; +import Lbry from 'lbry'; +import { doClaimSearch, doAbandonClaim } from 'redux/actions/claims'; +import { makeSelectClaimForClaimId } from 'redux/selectors/claims'; +import { + makeSelectCollectionForId, + // makeSelectPublishedCollectionForId, // for "save" or "copy" action + makeSelectPublishedCollectionForId, + makeSelectUnpublishedCollectionForId, + makeSelectEditedCollectionForId, +} from 'redux/selectors/collections'; +import * as COLS from 'constants/collections'; + +const getTimestamp = () => { + return Math.floor(Date.now() / 1000); +}; + +const FETCH_BATCH_SIZE = 50; + +export const doLocalCollectionCreate = ( + name: string, + collectionItems: Array, + type: string, + sourceId: string +) => (dispatch: Dispatch) => { + return dispatch({ + type: ACTIONS.COLLECTION_NEW, + data: { + entry: { + id: uuid(), // start with a uuid, this becomes a claimId after publish + name: name, + updatedAt: getTimestamp(), + items: collectionItems || [], + sourceId: sourceId, + type: type, + }, + }, + }); +}; + +export const doCollectionDelete = (id: string, colKey: ?string = undefined) => ( + dispatch: Dispatch, + getState: GetState +) => { + const state = getState(); + const claim = makeSelectClaimForClaimId(id)(state); + const collectionDelete = () => + dispatch({ + type: ACTIONS.COLLECTION_DELETE, + data: { + id: id, + collectionKey: colKey, + }, + }); + if (claim && !colKey) { + // could support "abandon, but keep" later + const { txid, nout } = claim; + return dispatch(doAbandonClaim(txid, nout, collectionDelete)); + } + return collectionDelete(); +}; + +// Given a collection, save its collectionId to be resolved and displayed in Library +// export const doCollectionSave = ( +// id: string, +// ) => (dispatch: Dispatch) => { +// return dispatch({ +// type: ACTIONS.COLLECTION_SAVE, +// data: { +// id: id, +// }, +// }); +// }; + +// Given a collection and name, copy it to a local private collection with a name +// export const doCollectionCopy = ( +// id: string, +// ) => (dispatch: Dispatch) => { +// return dispatch({ +// type: ACTIONS.COLLECTION_COPY, +// data: { +// id: id, +// }, +// }); +// }; + +export const doFetchItemsInCollections = ( + resolveItemsOptions: { + collectionIds: Array, + pageSize?: number, + }, + resolveStartedCallback?: () => void +) => async (dispatch: Dispatch, getState: GetState) => { + /* + 1) make sure all the collection claims are loaded into claims reducer, search/resolve if necessary. + 2) get the item claims for each + 3) format and make sure they're in the order as in the claim + 4) Build the collection objects and update collections reducer + 5) Update redux claims reducer + */ + let state = getState(); + const { collectionIds, pageSize } = resolveItemsOptions; + + dispatch({ + type: ACTIONS.COLLECTION_ITEMS_RESOLVE_STARTED, + data: { ids: collectionIds }, + }); + + if (resolveStartedCallback) resolveStartedCallback(); + + const collectionIdsToSearch = collectionIds.filter((claimId) => !state.claims.byId[claimId]); + + if (collectionIdsToSearch.length) { + await dispatch(doClaimSearch({ claim_ids: collectionIdsToSearch, page: 1, page_size: 9999 })); + } + + const stateAfterClaimSearch = getState(); + + async function fetchItemsForCollectionClaim(claim: CollectionClaim, pageSize?: number) { + const totalItems = claim.value.claims && claim.value.claims.length; + const claimId = claim.claim_id; + const itemOrder = claim.value.claims; + + const sortResults = (items: Array, claimList) => { + const newItems: Array = []; + claimList.forEach((id) => { + const index = items.findIndex((i) => i.claim_id === id); + if (index >= 0) { + newItems.push(items[index]); + } + }); + /* + This will return newItems[] of length less than total_items below + if one or more of the claims has been abandoned. That's ok for now. + */ + return newItems; + }; + + const mergeBatches = ( + arrayOfResults: Array<{ items: Array, total_items: number }>, + claimList: Array + ) => { + const mergedResults: { items: Array, total_items: number } = { + items: [], + total_items: 0, + }; + arrayOfResults.forEach((result) => { + mergedResults.items = mergedResults.items.concat(result.items); + mergedResults.total_items = result.total_items; + }); + + mergedResults.items = sortResults(mergedResults.items, claimList); + return mergedResults; + }; + + try { + const batchSize = pageSize || FETCH_BATCH_SIZE; + const batches: Array> = []; + + for (let i = 0; i < Math.ceil(totalItems / batchSize); i++) { + batches[i] = Lbry.claim_search({ + claim_ids: claim.value.claims, + page: i + 1, + page_size: batchSize, + no_totals: true, + }); + } + const itemsInBatches = await Promise.all(batches); + const result = mergeBatches(itemsInBatches, itemOrder); + + // $FlowFixMe + const itemsById: { claimId: string, items?: ?Array } = { claimId: claimId }; + if (result.items) { + itemsById.items = result.items; + } else { + itemsById.items = null; + } + return itemsById; + } catch (e) { + return { + claimId: claimId, + items: null, + }; + } + } + + function formatForClaimActions(resultClaimsByUri) { + const formattedClaims = {}; + Object.entries(resultClaimsByUri).forEach(([uri, uriResolveInfo]) => { + // Flow has terrible Object.entries support + // https://github.com/facebook/flow/issues/2221 + if (uriResolveInfo) { + let result = {}; + if (uriResolveInfo.value_type === 'channel') { + result.channel = uriResolveInfo; + // $FlowFixMe + result.claimsInChannel = uriResolveInfo.meta.claims_in_channel; + // ALSO SKIP COLLECTIONS + } else if (uriResolveInfo.value_type === 'collection') { + result.collection = uriResolveInfo; + } else { + result.stream = uriResolveInfo; + if (uriResolveInfo.signing_channel) { + result.channel = uriResolveInfo.signing_channel; + result.claimsInChannel = + (uriResolveInfo.signing_channel.meta && uriResolveInfo.signing_channel.meta.claims_in_channel) || 0; + } + } + // $FlowFixMe + formattedClaims[uri] = result; + } + }); + return formattedClaims; + } + + const invalidCollectionIds = []; + const promisedCollectionItemFetches = []; + collectionIds.forEach((collectionId) => { + const claim = makeSelectClaimForClaimId(collectionId)(stateAfterClaimSearch); + if (!claim) { + invalidCollectionIds.push(collectionId); + } else { + promisedCollectionItemFetches.push(fetchItemsForCollectionClaim(claim, pageSize)); + } + }); + + // $FlowFixMe + const collectionItemsById: Array<{ + claimId: string, + items: ?Array, + }> = await Promise.all(promisedCollectionItemFetches); + + const newCollectionObjectsById = {}; + const resolvedItemsByUrl = {}; + collectionItemsById.forEach((entry) => { + // $FlowFixMe + const collectionItems: Array = entry.items; + const collectionId = entry.claimId; + if (collectionItems) { + const claim = makeSelectClaimForClaimId(collectionId)(stateAfterClaimSearch); + + const editedCollection = makeSelectEditedCollectionForId(collectionId)(stateAfterClaimSearch); + const { name, timestamp, value } = claim || {}; + const { title } = value; + const valueTypes = new Set(); + const streamTypes = new Set(); + + let newItems = []; + let isPlaylist; + + if (collectionItems) { + collectionItems.forEach((collectionItem) => { + newItems.push(collectionItem.permanent_url); + valueTypes.add(collectionItem.value_type); + if (collectionItem.value.stream_type) { + streamTypes.add(collectionItem.value.stream_type); + } + resolvedItemsByUrl[collectionItem.canonical_url] = collectionItem; + }); + isPlaylist = + valueTypes.size === 1 && + valueTypes.has('stream') && + ((streamTypes.size === 1 && (streamTypes.has('audio') || streamTypes.has('video'))) || + (streamTypes.size === 2 && streamTypes.has('audio') && streamTypes.has('video'))); + } + + newCollectionObjectsById[collectionId] = { + items: newItems, + id: collectionId, + name: title || name, + itemCount: claim.value.claims.length, + type: isPlaylist ? 'playlist' : 'collection', + updatedAt: timestamp, + }; + + if (editedCollection && timestamp > editedCollection['updatedAt']) { + dispatch({ + type: ACTIONS.COLLECTION_DELETE, + data: { + id: collectionId, + collectionKey: 'edited', + }, + }); + } + } else { + invalidCollectionIds.push(collectionId); + } + }); + const formattedClaimsByUri = formatForClaimActions(collectionItemsById); + + dispatch({ + type: ACTIONS.RESOLVE_URIS_COMPLETED, + data: { resolveInfo: formattedClaimsByUri }, + }); + + dispatch({ + type: ACTIONS.COLLECTION_ITEMS_RESOLVE_COMPLETED, + data: { + resolvedCollections: newCollectionObjectsById, + failedCollectionIds: invalidCollectionIds, + }, + }); +}; + +export const doFetchItemsInCollection = (options: { collectionId: string, pageSize?: number }, cb?: () => void) => { + const { collectionId, pageSize } = options; + const newOptions: { collectionIds: Array, pageSize?: number } = { + collectionIds: [collectionId], + }; + if (pageSize) newOptions.pageSize = pageSize; + return doFetchItemsInCollections(newOptions, cb); +}; + +export const doCollectionEdit = (collectionId: string, params: CollectionEditParams) => async ( + dispatch: Dispatch, + getState: GetState +) => { + const state = getState(); + const collection: Collection = makeSelectCollectionForId(collectionId)(state); + const editedCollection: Collection = makeSelectEditedCollectionForId(collectionId)(state); + const unpublishedCollection: Collection = makeSelectUnpublishedCollectionForId(collectionId)(state); + const publishedCollection: Collection = makeSelectPublishedCollectionForId(collectionId)(state); // needs to be published only + + const generateCollectionItemsFromSearchResult = (results) => { + return ( + Object.values(results) + // $FlowFixMe + .reduce( + ( + acc, + cur: { + stream: ?StreamClaim, + channel: ?ChannelClaim, + claimsInChannel: ?number, + collection: ?CollectionClaim, + } + ) => { + let url; + if (cur.stream) { + url = cur.stream.permanent_url; + } else if (cur.channel) { + url = cur.channel.permanent_url; + } else if (cur.collection) { + url = cur.collection.permanent_url; + } else { + return acc; + } + acc.push(url); + return acc; + }, + [] + ) + ); + }; + + if (!collection) { + return dispatch({ + type: ACTIONS.COLLECTION_ERROR, + data: { + message: 'collection does not exist', + }, + }); + } + + let currentItems = collection.items ? collection.items.concat() : []; + const { claims: passedClaims, order, claimIds, replace, remove, type } = params; + + const collectionType = type || collection.type; + let newItems: Array = currentItems; + + if (passedClaims) { + if (remove) { + const passedUrls = passedClaims.map((claim) => claim.permanent_url); + // $FlowFixMe // need this? + newItems = currentItems.filter((item: string) => !passedUrls.includes(item)); + } else { + passedClaims.forEach((claim) => newItems.push(claim.permanent_url)); + } + } + + if (claimIds) { + const batches = []; + if (claimIds.length > 50) { + for (let i = 0; i < Math.ceil(claimIds.length / 50); i++) { + batches[i] = claimIds.slice(i * 50, (i + 1) * 50); + } + } else { + batches[0] = claimIds; + } + const resultArray = await Promise.all( + batches.map((batch) => { + let options = { claim_ids: batch, page: 1, page_size: 50 }; + return dispatch(doClaimSearch(options)); + }) + ); + + const searchResults = Object.assign({}, ...resultArray); + + if (replace) { + newItems = generateCollectionItemsFromSearchResult(searchResults); + } else { + newItems = currentItems.concat(generateCollectionItemsFromSearchResult(searchResults)); + } + } + + if (order) { + const [movedItem] = currentItems.splice(order.from, 1); + currentItems.splice(order.to, 0, movedItem); + } + + // console.log('p&e', publishedCollection.items, newItems, publishedCollection.items.join(','), newItems.join(',')) + if (editedCollection) { + // delete edited if newItems are the same as publishedItems + if (publishedCollection.items.join(',') === newItems.join(',')) { + dispatch({ + type: ACTIONS.COLLECTION_DELETE, + data: { + id: collectionId, + collectionKey: 'edited', + }, + }); + } else { + dispatch({ + type: ACTIONS.COLLECTION_EDIT, + data: { + id: collectionId, + collectionKey: 'edited', + collection: { + items: newItems, + id: collectionId, + name: params.name || collection.name, + updatedAt: getTimestamp(), + type: collectionType, + }, + }, + }); + } + } else if (publishedCollection) { + dispatch({ + type: ACTIONS.COLLECTION_EDIT, + data: { + id: collectionId, + collectionKey: 'edited', + collection: { + items: newItems, + id: collectionId, + name: params.name || collection.name, + updatedAt: getTimestamp(), + type: collectionType, + }, + }, + }); + } else if (COLS.BUILTIN_LISTS.includes(collectionId)) { + dispatch({ + type: ACTIONS.COLLECTION_EDIT, + data: { + id: collectionId, + collectionKey: 'builtin', + collection: { + items: newItems, + id: collectionId, + name: params.name || collection.name, + updatedAt: getTimestamp(), + type: collectionType, + }, + }, + }); + } else if (unpublishedCollection) { + dispatch({ + type: ACTIONS.COLLECTION_EDIT, + data: { + id: collectionId, + collectionKey: 'unpublished', + collection: { + items: newItems, + id: collectionId, + name: params.name || collection.name, + updatedAt: getTimestamp(), + type: collectionType, + }, + }, + }); + } + return true; +}; diff --git a/ui/redux/actions/comments.js b/ui/redux/actions/comments.js index 3d4a7c359..38ed48494 100644 --- a/ui/redux/actions/comments.js +++ b/ui/redux/actions/comments.js @@ -3,15 +3,10 @@ import * as ACTIONS from 'constants/action_types'; import * as REACTION_TYPES from 'constants/reactions'; import * as PAGES from 'constants/pages'; import { SORT_BY, BLOCK_LEVEL } from 'constants/comment'; -import { - Lbry, - parseURI, - buildURI, - selectClaimsByUri, - selectMyChannelClaims, - isURIEqual, - doClaimSearch, -} from 'lbry-redux'; +import Lbry from 'lbry'; +import { parseURI, buildURI, isURIEqual } from 'util/lbryURI'; +import { selectClaimsByUri, selectMyChannelClaims } from 'redux/selectors/claims'; +import { doClaimSearch } from 'redux/actions/claims'; import { doToast, doSeeNotifications } from 'redux/actions/notifications'; import { makeSelectMyReactionsForComment, @@ -1321,17 +1316,18 @@ export const doUpdateBlockListForPublishedChannel = (channelClaim: ChannelClaim) return Promise.all( blockedUris.map((uri) => { const { channelName, channelClaimId } = parseURI(uri); - - return Comments.moderation_block({ - mod_channel_id: channelClaim.claim_id, - mod_channel_name: channelClaim.name, - // $FlowFixMe - signature: channelSignature.signature, - // $FlowFixMe - signing_ts: channelSignature.signing_ts, - blocked_channel_id: channelClaimId, - blocked_channel_name: channelName, - }); + if (channelName && channelClaimId) { + return Comments.moderation_block({ + mod_channel_id: channelClaim.claim_id, + mod_channel_name: channelClaim.name, + // $FlowFixMe + signature: channelSignature.signature, + // $FlowFixMe + signing_ts: channelSignature.signing_ts, + blocked_channel_id: channelClaimId, + blocked_channel_name: channelName, + }); + } }) ); }; diff --git a/ui/redux/actions/content.js b/ui/redux/actions/content.js index 1053b617a..045987665 100644 --- a/ui/redux/actions/content.js +++ b/ui/redux/actions/content.js @@ -5,20 +5,18 @@ import * as MODALS from 'constants/modal_types'; import { ipcRenderer } from 'electron'; // @endif import { doOpenModal } from 'redux/actions/app'; +import { makeSelectClaimForUri, makeSelectClaimIsMine, makeSelectClaimWasPurchased } from 'redux/selectors/claims'; import { - Lbry, - SETTINGS, makeSelectFileInfoForUri, selectFileInfosByOutpoint, makeSelectUriIsStreamable, selectDownloadingByOutpoint, - makeSelectClaimForUri, - makeSelectClaimIsMine, - makeSelectClaimWasPurchased, - doToast, - makeSelectUrlsForCollectionId, -} from 'lbry-redux'; +} from 'redux/selectors/file_info'; +import { makeSelectUrlsForCollectionId } from 'redux/selectors/collections'; +import { doToast } from 'redux/actions/notifications'; import { doPurchaseUri } from 'redux/actions/file'; +import Lbry from 'lbry'; +import * as SETTINGS from 'constants/settings'; import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc'; import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings'; diff --git a/ui/redux/actions/file.js b/ui/redux/actions/file.js index 73adf0bd4..41cf1a2bb 100644 --- a/ui/redux/actions/file.js +++ b/ui/redux/actions/file.js @@ -1,27 +1,28 @@ // @flow import * as ACTIONS from 'constants/action_types'; +import * as ABANDON_STATES from 'constants/abandon_states'; // @if TARGET='app' import { shell } from 'electron'; // @endif -import { - Lbry, - batchActions, - doAbandonClaim, - makeSelectFileInfoForUri, - selectDownloadingByOutpoint, - makeSelectStreamingUrlForUri, - makeSelectClaimForUri, - selectBalance, - ABANDON_STATES, -} from 'lbry-redux'; +import Lbry from 'lbry'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; +import { doAbandonClaim } from 'redux/actions/claims'; +import { batchActions } from 'util/batch-actions'; + import { doHideModal } from 'redux/actions/app'; import { goBack } from 'connected-react-router'; import { doSetPlayingUri } from 'redux/actions/content'; import { selectPlayingUri } from 'redux/selectors/content'; import { doToast } from 'redux/actions/notifications'; +import { selectBalance } from 'redux/selectors/wallet'; +import { + makeSelectFileInfoForUri, + selectDownloadingByOutpoint, + makeSelectStreamingUrlForUri, +} from 'redux/selectors/file_info'; + type Dispatch = (action: any) => any; type GetState = () => { file: FileState }; - export function doOpenFileInFolder(path: string) { return () => { shell.showItemInFolder(path); diff --git a/ui/redux/actions/file_info.js b/ui/redux/actions/file_info.js new file mode 100644 index 000000000..21e26f2c3 --- /dev/null +++ b/ui/redux/actions/file_info.js @@ -0,0 +1,73 @@ +import * as ACTIONS from 'constants/action_types'; +import Lbry from 'lbry'; +import { selectClaimsByUri } from 'redux/selectors/claims'; +import { selectIsFetchingFileList, selectUrisLoading } from 'redux/selectors/file_info'; + +export function doFetchFileInfo(uri) { + return (dispatch, getState) => { + const state = getState(); + const claim = selectClaimsByUri(state)[uri]; + const outpoint = claim ? `${claim.txid}:${claim.nout}` : null; + const alreadyFetching = !!selectUrisLoading(state)[uri]; + + if (!alreadyFetching) { + dispatch({ + type: ACTIONS.FETCH_FILE_INFO_STARTED, + data: { + outpoint, + }, + }); + + Lbry.file_list({ outpoint, full_status: true, page: 1, page_size: 1 }).then((result) => { + const { items: fileInfos } = result; + const fileInfo = fileInfos[0]; + + dispatch({ + type: ACTIONS.FETCH_FILE_INFO_COMPLETED, + data: { + outpoint, + fileInfo: fileInfo || null, + }, + }); + }); + } + }; +} + +export function doFileList(page = 1, pageSize = 99999) { + return (dispatch, getState) => { + const state = getState(); + const isFetching = selectIsFetchingFileList(state); + + if (!isFetching) { + dispatch({ + type: ACTIONS.FILE_LIST_STARTED, + }); + + Lbry.file_list({ page, page_size: pageSize }).then((result) => { + const { items: fileInfos } = result; + dispatch({ + type: ACTIONS.FILE_LIST_SUCCEEDED, + data: { + fileInfos: fileInfos, + }, + }); + }); + } + }; +} + +export function doFetchFileInfos() { + return (dispatch, getState) => { + const state = getState(); + const isFetchingFileInfo = selectIsFetchingFileList(state); + if (!isFetchingFileInfo) dispatch(doFileList()); + }; +} + +export function doSetFileListSort(page, value) { + return { + type: ACTIONS.SET_FILE_LIST_SORT, + data: { page, value }, + }; +} diff --git a/ui/redux/actions/livestream.js b/ui/redux/actions/livestream.js index 3f94b5a4c..4ff513c08 100644 --- a/ui/redux/actions/livestream.js +++ b/ui/redux/actions/livestream.js @@ -1,6 +1,6 @@ // @flow import * as ACTIONS from 'constants/action_types'; -import { doClaimSearch } from 'lbry-redux'; +import { doClaimSearch } from 'redux/actions/claims'; import { LIVESTREAM_LIVE_API } from 'constants/livestream'; export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dispatch, getState: GetState) => { @@ -79,6 +79,7 @@ export const doFetchActiveLivestreams = ( // live. The UI usually just wants to report the latest claim, so we // query that store it in `latestClaimUri`. doClaimSearch({ + page: 1, page_size: nextOptions.page_size, has_no_source: true, channel_ids: Object.keys(activeLivestreams), diff --git a/ui/redux/actions/notifications.js b/ui/redux/actions/notifications.js index f772b3b31..e070b7814 100644 --- a/ui/redux/actions/notifications.js +++ b/ui/redux/actions/notifications.js @@ -8,7 +8,7 @@ import { selectNotificationsFiltered, selectNotificationCategories, } from 'redux/selectors/notifications'; -import { doResolveUris } from 'lbry-redux'; +import { doResolveUris } from 'redux/actions/claims'; export function doToast(params: ToastParams) { if (!params) { diff --git a/ui/redux/actions/publish.js b/ui/redux/actions/publish.js index e74f5bd02..9078421b0 100644 --- a/ui/redux/actions/publish.js +++ b/ui/redux/actions/publish.js @@ -2,20 +2,28 @@ import * as MODALS from 'constants/modal_types'; import * as ACTIONS from 'constants/action_types'; import * as PAGES from 'constants/pages'; +import { batchActions } from 'util/batch-actions'; +import { doCheckPendingClaims } from 'redux/actions/claims'; import { - batchActions, - selectMyClaims, - doPublish, - doCheckPendingClaims, - doCheckReflectingFiles, - ACTIONS as LBRY_REDUX_ACTIONS, - makeSelectPublishFormValue, makeSelectClaimForUri, -} from 'lbry-redux'; + selectMyClaims, + selectMyChannelClaims, + // selectMyClaimsWithoutChannels, + selectReflectingById, +} from 'redux/selectors/claims'; +import { makeSelectPublishFormValue, selectPublishFormValues, selectMyClaimForUri } from 'redux/selectors/publish'; import { doError } from 'redux/actions/notifications'; import { push } from 'connected-react-router'; import analytics from 'analytics'; import { doOpenModal } from 'redux/actions/app'; +import { CC_LICENSES, COPYRIGHT, OTHER, NONE, PUBLIC_DOMAIN } from 'constants/licenses'; +import { SPEECH_STATUS, SPEECH_PUBLISH } from 'constants/speech_urls'; +import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses'; +import { creditsToString } from 'util/format-credits'; +import Lbry from 'lbry'; +// import LbryFirst from 'extras/lbry-first/lbry-first'; +import { isClaimNsfw } from 'util/claim'; + export const NO_FILE = '---'; export const doPublishDesktop = (filePath: string, preview?: boolean) => (dispatch: Dispatch, getState: () => {}) => { const publishPreview = (previewResponse) => { @@ -57,14 +65,14 @@ export const doPublishDesktop = (filePath: string, preview?: boolean) => (dispat const isEdit = myClaims.some(isMatch); actions.push({ - type: LBRY_REDUX_ACTIONS.UPDATE_PENDING_CLAIMS, + type: ACTIONS.UPDATE_PENDING_CLAIMS, data: { claims: [pendingClaim], }, }); // @if TARGET='app' actions.push({ - type: LBRY_REDUX_ACTIONS.ADD_FILES_REFLECTING, + type: ACTIONS.ADD_FILES_REFLECTING, data: pendingClaim, }); // @endif @@ -114,3 +122,476 @@ export const doPublishDesktop = (filePath: string, preview?: boolean) => (dispat dispatch(doPublish(publishSuccess, publishFail)); }; + +export const doResetThumbnailStatus = () => (dispatch: Dispatch) => { + dispatch({ + type: ACTIONS.UPDATE_PUBLISH_FORM, + data: { + thumbnailPath: '', + thumbnailError: undefined, + }, + }); + + return fetch(SPEECH_STATUS) + .then((res) => res.json()) + .then((status) => { + if (status.disabled) { + throw Error(); + } + + return dispatch({ + type: ACTIONS.UPDATE_PUBLISH_FORM, + data: { + uploadThumbnailStatus: THUMBNAIL_STATUSES.READY, + thumbnail: '', + }, + }); + }) + .catch(() => + dispatch({ + type: ACTIONS.UPDATE_PUBLISH_FORM, + data: { + uploadThumbnailStatus: THUMBNAIL_STATUSES.API_DOWN, + thumbnail: '', + }, + }) + ); +}; + +export const doClearPublish = () => (dispatch: Dispatch) => { + dispatch({ type: ACTIONS.CLEAR_PUBLISH }); + return dispatch(doResetThumbnailStatus()); +}; + +export const doUpdatePublishForm = (publishFormValue: UpdatePublishFormData) => (dispatch: Dispatch) => + dispatch({ + type: ACTIONS.UPDATE_PUBLISH_FORM, + data: { ...publishFormValue }, + }); + +export const doUploadThumbnail = ( + filePath?: string, + thumbnailBlob?: File, + fsAdapter?: any, + fs?: any, + path?: any, + cb?: (string) => void +) => (dispatch: Dispatch) => { + const downMessage = __('Thumbnail upload service may be down, try again later.'); + let thumbnail, fileExt, fileName, fileType; + + const makeid = () => { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 24; i += 1) text += possible.charAt(Math.floor(Math.random() * 62)); + return text; + }; + + const uploadError = (error = '') => { + dispatch( + batchActions( + { + type: ACTIONS.UPDATE_PUBLISH_FORM, + data: { + uploadThumbnailStatus: THUMBNAIL_STATUSES.READY, + thumbnail: '', + nsfw: false, + }, + }, + doError(error) + ) + ); + }; + + dispatch({ + type: ACTIONS.UPDATE_PUBLISH_FORM, + data: { + thumbnailError: undefined, + }, + }); + + const doUpload = (data) => { + return fetch(SPEECH_PUBLISH, { + method: 'POST', + body: data, + }) + .then((res) => res.text()) + .then((text) => (text.length ? JSON.parse(text) : {})) + .then((json) => { + if (!json.success) return uploadError(json.message || downMessage); + if (cb) { + cb(json.data.serveUrl); + } + return dispatch({ + type: ACTIONS.UPDATE_PUBLISH_FORM, + data: { + uploadThumbnailStatus: THUMBNAIL_STATUSES.COMPLETE, + thumbnail: json.data.serveUrl, + }, + }); + }) + .catch((err) => { + let message = err.message; + + // This sucks but ¯\_(ツ)_/¯ + if (message === 'Failed to fetch') { + message = downMessage; + } + + uploadError(message); + }); + }; + + dispatch({ + type: ACTIONS.UPDATE_PUBLISH_FORM, + data: { uploadThumbnailStatus: THUMBNAIL_STATUSES.IN_PROGRESS }, + }); + + if (fsAdapter && fsAdapter.readFile && filePath) { + fsAdapter.readFile(filePath, 'base64').then((base64Image) => { + fileExt = 'png'; + fileName = 'thumbnail.png'; + fileType = 'image/png'; + + const data = new FormData(); + const name = makeid(); + data.append('name', name); + // $FlowFixMe + data.append('file', { uri: 'file://' + filePath, type: fileType, name: fileName }); + return doUpload(data); + }); + } else { + if (filePath && fs && path) { + thumbnail = fs.readFileSync(filePath); + fileExt = path.extname(filePath); + fileName = path.basename(filePath); + fileType = `image/${fileExt.slice(1)}`; + } else if (thumbnailBlob) { + fileExt = `.${thumbnailBlob.type && thumbnailBlob.type.split('/')[1]}`; + fileName = thumbnailBlob.name; + fileType = thumbnailBlob.type; + } else { + return null; + } + + const data = new FormData(); + const name = makeid(); + const file = thumbnailBlob || (thumbnail && new File([thumbnail], fileName, { type: fileType })); + data.append('name', name); + // $FlowFixMe + data.append('file', file); + return doUpload(data); + } +}; + +export const doPrepareEdit = (claim: StreamClaim, uri: string, fileInfo: FileListItem, fs: any) => ( + dispatch: Dispatch +) => { + const { name, amount, value = {} } = claim; + const channelName = (claim && claim.signing_channel && claim.signing_channel.name) || null; + const { + author, + description, + // use same values as default state + // fee will be undefined for free content + fee = { + amount: '0', + currency: 'LBC', + }, + languages, + release_time, + license, + license_url: licenseUrl, + thumbnail, + title, + tags, + } = value; + + const publishData: UpdatePublishFormData = { + name, + bid: Number(amount), + contentIsFree: fee.amount === '0', + author, + description, + fee, + languages, + releaseTime: release_time, + releaseTimeEdited: undefined, + thumbnail: thumbnail ? thumbnail.url : null, + title, + uri, + uploadThumbnailStatus: thumbnail ? THUMBNAIL_STATUSES.MANUAL : undefined, + licenseUrl, + nsfw: isClaimNsfw(claim), + tags: tags ? tags.map((tag) => ({ name: tag })) : [], + }; + + // Make sure custom licenses are mapped properly + // If the license isn't one of the standard licenses, map the custom license and description/url + if (!CC_LICENSES.some(({ value }) => value === license)) { + if (!license || license === NONE || license === PUBLIC_DOMAIN) { + publishData.licenseType = license; + } else if (license && !licenseUrl && license !== NONE) { + publishData.licenseType = COPYRIGHT; + } else { + publishData.licenseType = OTHER; + } + + publishData.otherLicenseDescription = license; + } else { + publishData.licenseType = license; + } + if (channelName) { + publishData['channel'] = channelName; + } + + dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData }); +}; + +export const doPublish = (success: Function, fail: Function, preview: Function) => ( + dispatch: Dispatch, + getState: () => {} +) => { + if (!preview) { + dispatch({ type: ACTIONS.PUBLISH_START }); + } + + const state = getState(); + const myClaimForUri = selectMyClaimForUri(state); + const myChannels = selectMyChannelClaims(state); + // const myClaims = selectMyClaimsWithoutChannels(state); + // get redux publish form + const publishData = selectPublishFormValues(state); + + // destructure the data values + const { + name, + bid, + filePath, + description, + language, + releaseTimeEdited, + // license, + licenseUrl, + useLBRYUploader, + licenseType, + otherLicenseDescription, + thumbnail, + channel, + title, + contentIsFree, + fee, + // uri, + tags, + // locations, + optimize, + isLivestreamPublish, + remoteFileUrl, + } = publishData; + + // Handle scenario where we have a claim that has the same name as a channel we are publishing with. + const myClaimForUriEditing = myClaimForUri && myClaimForUri.name === name ? myClaimForUri : null; + + let publishingLicense; + switch (licenseType) { + case COPYRIGHT: + case OTHER: + publishingLicense = otherLicenseDescription; + break; + default: + publishingLicense = licenseType; + } + + // get the claim id from the channel name, we will use that instead + const namedChannelClaim = myChannels ? myChannels.find((myChannel) => myChannel.name === channel) : null; + const channelId = namedChannelClaim ? namedChannelClaim.claim_id : ''; + + const publishPayload: { + name: ?string, + bid: string, + description?: string, + channel_id?: string, + file_path?: string, + license_url?: string, + license?: string, + thumbnail_url?: string, + release_time?: number, + fee_currency?: string, + fee_amount?: string, + languages?: Array, + tags: Array, + locations?: Array, + blocking: boolean, + optimize_file?: boolean, + preview?: boolean, + remote_url?: string, + } = { + name, + title, + description, + locations: [], + bid: creditsToString(bid), + languages: [language], + tags: tags && tags.map((tag) => tag.name), + thumbnail_url: thumbnail, + blocking: true, + preview: false, + }; + // Temporary solution to keep the same publish flow with the new tags api + // Eventually we will allow users to enter their own tags on publish + // `nsfw` will probably be removed + if (remoteFileUrl) { + publishPayload.remote_url = remoteFileUrl; + } + + if (publishingLicense) { + publishPayload.license = publishingLicense; + } + + if (licenseUrl) { + publishPayload.license_url = licenseUrl; + } + + if (thumbnail) { + publishPayload.thumbnail_url = thumbnail; + } + + if (useLBRYUploader) { + publishPayload.tags.push('lbry-first'); + } + + // Set release time to curret date. On edits, keep original release/transaction time as release_time + if (releaseTimeEdited) { + publishPayload.release_time = releaseTimeEdited; + } else if (myClaimForUriEditing && myClaimForUriEditing.value.release_time) { + publishPayload.release_time = Number(myClaimForUri.value.release_time); + } else if (myClaimForUriEditing && myClaimForUriEditing.timestamp) { + publishPayload.release_time = Number(myClaimForUriEditing.timestamp); + } else { + publishPayload.release_time = Number(Math.round(Date.now() / 1000)); + } + + if (channelId) { + publishPayload.channel_id = channelId; + } + + if (myClaimForUriEditing && myClaimForUriEditing.value && myClaimForUriEditing.value.locations) { + publishPayload.locations = myClaimForUriEditing.value.locations; + } + + if (!contentIsFree && fee && fee.currency && Number(fee.amount) > 0) { + publishPayload.fee_currency = fee.currency; + publishPayload.fee_amount = creditsToString(fee.amount); + } + + if (optimize) { + publishPayload.optimize_file = true; + } + + // Only pass file on new uploads, not metadata only edits. + // The sdk will figure it out + if (filePath && !isLivestreamPublish) publishPayload.file_path = filePath; + + if (preview) { + publishPayload.preview = true; + publishPayload.optimize_file = false; + + return Lbry.publish(publishPayload).then((previewResponse: PublishResponse) => { + return preview(previewResponse); + }, fail); + } + + return Lbry.publish(publishPayload).then((response: PublishResponse) => { + // TODO: Restore LbryFirst + // if (!useLBRYUploader) { + return success(response); + // } + + // $FlowFixMe + // publishPayload.permanent_url = response.outputs[0].permanent_url; + // + // return LbryFirst.upload(publishPayload) + // .then(() => { + // // Return original publish response so app treats it like a normal publish + // return success(response); + // }) + // .catch((error) => { + // return success(response, error); + // }); + }, fail); +}; + +// Calls file_list until any reflecting files are done +export const doCheckReflectingFiles = () => (dispatch: Dispatch, getState: GetState) => { + const state = getState(); + const { checkingReflector } = state.claims; + let reflectorCheckInterval; + + const checkFileList = async () => { + const state = getState(); + const reflectingById = selectReflectingById(state); + const ids = Object.keys(reflectingById); + + const newReflectingById = {}; + const promises = []; + // TODO: just use file_list({claim_id: Array}) + if (Object.keys(reflectingById).length) { + ids.forEach((claimId) => { + promises.push(Lbry.file_list({ claim_id: claimId })); + }); + + Promise.all(promises) + .then((results) => { + results.forEach((res) => { + if (res.items[0]) { + const fileListItem = res.items[0]; + const fileClaimId = fileListItem.claim_id; + const { + is_fully_reflected: done, + uploading_to_reflector: uploading, + reflector_progress: progress, + } = fileListItem; + if (uploading) { + newReflectingById[fileClaimId] = { + fileListItem: fileListItem, + progress, + stalled: !done && !uploading, + }; + } + } + }); + }) + .then(() => { + dispatch({ + type: ACTIONS.UPDATE_FILES_REFLECTING, + data: newReflectingById, + }); + if (!Object.keys(newReflectingById).length) { + dispatch({ + type: ACTIONS.TOGGLE_CHECKING_REFLECTING, + data: false, + }); + clearInterval(reflectorCheckInterval); + } + }); + } else { + dispatch({ + type: ACTIONS.TOGGLE_CHECKING_REFLECTING, + data: false, + }); + clearInterval(reflectorCheckInterval); + } + }; + // do it once... + checkFileList(); + // then start the interval if it's not already started + if (!checkingReflector) { + dispatch({ + type: ACTIONS.TOGGLE_CHECKING_REFLECTING, + data: true, + }); + reflectorCheckInterval = setInterval(() => { + checkFileList(); + }, 5000); + } +}; diff --git a/ui/redux/actions/reactions.js b/ui/redux/actions/reactions.js index ae494e5ee..51d2ac1a4 100644 --- a/ui/redux/actions/reactions.js +++ b/ui/redux/actions/reactions.js @@ -3,7 +3,7 @@ import { Lbryio } from 'lbryinc'; import * as ACTIONS from 'constants/action_types'; import * as REACTION_TYPES from 'constants/reactions'; import { makeSelectMyReactionForUri } from 'redux/selectors/reactions'; -import { makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; export const doFetchReactions = (claimId: string) => (dispatch: Dispatch) => { dispatch({ type: ACTIONS.REACTIONS_LIST_STARTED }); @@ -12,7 +12,7 @@ export const doFetchReactions = (claimId: string) => (dispatch: Dispatch) => { .then((reactions: Array) => { dispatch({ type: ACTIONS.REACTIONS_LIST_COMPLETED, data: { claimId, reactions } }); }) - .catch(error => { + .catch((error) => { dispatch({ type: ACTIONS.REACTIONS_LIST_FAILED, data: error }); }); }; @@ -38,7 +38,7 @@ export const doReactionLike = (uri: string) => (dispatch: Dispatch, getState: Ge .then(() => { dispatch({ type: ACTIONS.REACTIONS_LIKE_COMPLETED, data: { claimId, shouldRemove } }); }) - .catch(error => { + .catch((error) => { dispatch({ type: ACTIONS.REACTIONS_NEW_FAILED, data: error }); }); }; @@ -64,7 +64,7 @@ export const doReactionDislike = (uri: string) => (dispatch: Dispatch, getState: .then(() => { dispatch({ type: ACTIONS.REACTIONS_DISLIKE_COMPLETED, data: { claimId, shouldRemove } }); }) - .catch(error => { + .catch((error) => { dispatch({ type: ACTIONS.REACTIONS_NEW_FAILED, data: error }); }); }; diff --git a/ui/redux/actions/rewards.js b/ui/redux/actions/rewards.js index 98351fd42..7c2792ff0 100644 --- a/ui/redux/actions/rewards.js +++ b/ui/redux/actions/rewards.js @@ -1,5 +1,7 @@ import { Lbryio } from 'lbryinc'; -import { ACTIONS, doToast, doUpdateBalance } from 'lbry-redux'; +import { doUpdateBalance } from 'redux/actions/wallet'; +import { doToast } from 'redux/actions/notifications'; +import * as ACTIONS from 'constants/action_types'; import { selectUnclaimedRewards } from 'redux/selectors/rewards'; import { selectUserIsRewardApproved } from 'redux/selectors/user'; import { doFetchInviteStatus } from 'redux/actions/user'; diff --git a/ui/redux/actions/search.js b/ui/redux/actions/search.js index a439af38d..2a3de1afe 100644 --- a/ui/redux/actions/search.js +++ b/ui/redux/actions/search.js @@ -1,14 +1,10 @@ // @flow import * as ACTIONS from 'constants/action_types'; import { selectShowMatureContent } from 'redux/selectors/settings'; -import { - buildURI, - doResolveUris, - batchActions, - isURIValid, - makeSelectClaimForUri, - makeSelectClaimIsNsfw, -} from 'lbry-redux'; +import { makeSelectClaimForUri, makeSelectClaimIsNsfw } from 'redux/selectors/claims'; +import { doResolveUris } from 'redux/actions/claims'; +import { buildURI, isURIValid } from 'util/lbryURI'; +import { batchActions } from 'util/batch-actions'; import { makeSelectSearchUrisForQuery, selectSearchValue } from 'redux/selectors/search'; import handleFetchResponse from 'util/handle-fetch'; import { getSearchQueryString } from 'util/query-params'; diff --git a/ui/redux/actions/settings.js b/ui/redux/actions/settings.js index 3b327f805..08278b4b4 100644 --- a/ui/redux/actions/settings.js +++ b/ui/redux/actions/settings.js @@ -1,6 +1,10 @@ -import { Lbry, ACTIONS, SHARED_PREFERENCES, doWalletReconnect, SETTINGS, DAEMON_SETTINGS } from 'lbry-redux'; +import Lbry from 'lbry'; +import { doWalletReconnect } from 'redux/actions/wallet'; +import * as SETTINGS from 'constants/settings'; +import * as DAEMON_SETTINGS from 'constants/daemon_settings'; +import * as ACTIONS from 'constants/action_types'; +import * as SHARED_PREFERENCES from 'constants/shared_preferences'; import { doToast } from 'redux/actions/notifications'; -import * as LOCAL_ACTIONS from 'constants/action_types'; import analytics from 'analytics'; import SUPPORTED_LANGUAGES from 'constants/supported_languages'; import { launcher } from 'util/autoLaunch'; @@ -34,12 +38,12 @@ export function doFetchDaemonSettings() { export function doFindFFmpeg() { return (dispatch) => { dispatch({ - type: LOCAL_ACTIONS.FINDING_FFMPEG_STARTED, + type: ACTIONS.FINDING_FFMPEG_STARTED, }); return Lbry.ffmpeg_find().then((done) => { dispatch(doGetDaemonStatus()); dispatch({ - type: LOCAL_ACTIONS.FINDING_FFMPEG_COMPLETED, + type: ACTIONS.FINDING_FFMPEG_COMPLETED, }); }); }; @@ -229,7 +233,7 @@ export function doPushSettingsToPrefs() { return (dispatch) => { return new Promise((resolve, reject) => { dispatch({ - type: LOCAL_ACTIONS.SYNC_CLIENT_SETTINGS, + type: ACTIONS.SYNC_CLIENT_SETTINGS, }); resolve(); }); @@ -278,7 +282,7 @@ export function doFetchLanguage(language) { .then((j) => { window.i18n_messages[language] = j; dispatch({ - type: LOCAL_ACTIONS.DOWNLOAD_LANGUAGE_SUCCESS, + type: ACTIONS.DOWNLOAD_LANGUAGE_SUCCESS, data: { language, }, @@ -286,7 +290,7 @@ export function doFetchLanguage(language) { }) .catch((e) => { dispatch({ - type: LOCAL_ACTIONS.DOWNLOAD_LANGUAGE_FAILURE, + type: ACTIONS.DOWNLOAD_LANGUAGE_FAILURE, }); }); } @@ -328,7 +332,7 @@ export function doSetLanguage(language) { .then((j) => { window.i18n_messages[language] = j; dispatch({ - type: LOCAL_ACTIONS.DOWNLOAD_LANGUAGE_SUCCESS, + type: ACTIONS.DOWNLOAD_LANGUAGE_SUCCESS, data: { language, }, diff --git a/ui/redux/actions/subscriptions.js b/ui/redux/actions/subscriptions.js index e4ddcd318..7d519f29b 100644 --- a/ui/redux/actions/subscriptions.js +++ b/ui/redux/actions/subscriptions.js @@ -3,7 +3,7 @@ import * as ACTIONS from 'constants/action_types'; import REWARDS from 'rewards'; import { Lbryio } from 'lbryinc'; import { doClaimRewardType } from 'redux/actions/rewards'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; import { doAlertWaitingForSync } from 'redux/actions/app'; import { doToast } from 'redux/actions/notifications'; @@ -13,7 +13,11 @@ type SubscriptionArgs = { notificationsDisabled?: boolean, }; -export function doToggleSubscription(subscription: SubscriptionArgs, followToast: boolean, isSubscribed: boolean = false) { +export function doToggleSubscription( + subscription: SubscriptionArgs, + followToast: boolean, + isSubscribed: boolean = false +) { return async (dispatch: Dispatch, getState: GetState) => { const { settings: { daemonSettings }, @@ -59,9 +63,13 @@ export function doToggleSubscription(subscription: SubscriptionArgs, followToast } } if (followToast) { - dispatch(doToast({ - message: __(!isSubscribed ? 'You followed %CHANNEL_NAME%!' : 'Unfollowed %CHANNEL_NAME%.', { CHANNEL_NAME: subscription.channelName }), - })); + dispatch( + doToast({ + message: __(!isSubscribed ? 'You followed %CHANNEL_NAME%!' : 'Unfollowed %CHANNEL_NAME%.', { + CHANNEL_NAME: subscription.channelName, + }), + }) + ); } }; } diff --git a/ui/redux/actions/sync.js b/ui/redux/actions/sync.js index a00a29bb7..4a616bb74 100644 --- a/ui/redux/actions/sync.js +++ b/ui/redux/actions/sync.js @@ -1,6 +1,9 @@ +// @flow import * as ACTIONS from 'constants/action_types'; +import * as SETTINGS from 'constants/settings'; import { Lbryio } from 'lbryinc'; -import { SETTINGS, Lbry, doWalletEncrypt, doWalletDecrypt } from 'lbry-redux'; +import Lbry from 'lbry'; +import { doWalletEncrypt, doWalletDecrypt } from 'redux/actions/wallet'; import { selectGetSyncIsPending, selectSetSyncIsPending, selectSyncIsLocked } from 'redux/selectors/sync'; import { makeSelectClientSetting } from 'redux/selectors/settings'; import { getSavedPassword, getAuthToken } from 'util/saved-passwords'; @@ -13,8 +16,8 @@ const SYNC_INTERVAL = 1000 * 60 * 5; // 5 minutes const NO_WALLET_ERROR = 'no wallet found for this user'; const BAD_PASSWORD_ERROR_NAME = 'InvalidPasswordError'; -export function doSetDefaultAccount(success, failure) { - return (dispatch) => { +export function doSetDefaultAccount(success: () => void, failure: (string) => void) { + return (dispatch: Dispatch) => { dispatch({ type: ACTIONS.SET_DEFAULT_ACCOUNT, }); @@ -62,8 +65,8 @@ export function doSetDefaultAccount(success, failure) { }; } -export function doSetSync(oldHash, newHash, data) { - return (dispatch) => { +export function doSetSync(oldHash: string, newHash: string, data: any) { + return (dispatch: Dispatch) => { dispatch({ type: ACTIONS.SET_SYNC_STARTED, }); @@ -88,7 +91,10 @@ export function doSetSync(oldHash, newHash, data) { }; } -export const doGetSyncDesktop = (cb?, password) => (dispatch, getState) => { +export const doGetSyncDesktop = (cb?: (any, any) => void, password?: string) => ( + dispatch: Dispatch, + getState: GetState +) => { const state = getState(); const syncEnabled = makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state); const getSyncPending = selectGetSyncIsPending(state); @@ -104,8 +110,8 @@ export const doGetSyncDesktop = (cb?, password) => (dispatch, getState) => { }); }; -export function doSyncLoop(noInterval) { - return (dispatch, getState) => { +export function doSyncLoop(noInterval?: boolean) { + return (dispatch: Dispatch, getState: GetState) => { if (!noInterval && syncTimer) clearInterval(syncTimer); const state = getState(); const hasVerifiedEmail = selectUserVerifiedEmail(state); @@ -129,14 +135,14 @@ export function doSyncLoop(noInterval) { } export function doSyncUnsubscribe() { - return (dispatch) => { + return () => { if (syncTimer) { clearInterval(syncTimer); } }; } -export function doGetSync(passedPassword, callback) { +export function doGetSync(passedPassword?: string, callback?: (any, ?boolean) => void) { const password = passedPassword === null || passedPassword === undefined ? '' : passedPassword; function handleCallback(error, hasNewData) { @@ -160,7 +166,7 @@ export function doGetSync(passedPassword, callback) { } // @endif - return (dispatch) => { + return (dispatch: Dispatch) => { dispatch({ type: ACTIONS.GET_SYNC_STARTED, }); @@ -183,8 +189,8 @@ export function doGetSync(passedPassword, callback) { data.unlockFailed = true; throw new Error(); }) - .then((hash) => Lbryio.call('sync', 'get', { hash }, 'post')) - .then((response) => { + .then((hash?: string) => Lbryio.call('sync', 'get', { hash }, 'post')) + .then((response: any) => { const syncHash = response.hash; data.syncHash = syncHash; data.syncData = response.data; @@ -257,7 +263,7 @@ export function doGetSync(passedPassword, callback) { if (noWalletError) { Lbry.sync_apply({ password }) .then(({ hash: walletHash, data: syncApplyData }) => { - dispatch(doSetSync('', walletHash, syncApplyData, password)); + dispatch(doSetSync('', walletHash, syncApplyData)); handleCallback(); }) .catch((syncApplyError) => { @@ -269,8 +275,8 @@ export function doGetSync(passedPassword, callback) { }; } -export function doSyncApply(syncHash, syncData, password) { - return (dispatch) => { +export function doSyncApply(syncHash: string, syncData: any, password: string) { + return (dispatch: Dispatch) => { dispatch({ type: ACTIONS.SYNC_APPLY_STARTED, }); @@ -298,7 +304,7 @@ export function doSyncApply(syncHash, syncData, password) { } export function doCheckSync() { - return (dispatch) => { + return (dispatch: Dispatch) => { dispatch({ type: ACTIONS.GET_SYNC_STARTED, }); @@ -326,15 +332,15 @@ export function doCheckSync() { } export function doResetSync() { - return (dispatch) => + return (dispatch: Dispatch): Promise => new Promise((resolve) => { dispatch({ type: ACTIONS.SYNC_RESET }); resolve(); }); } -export function doSyncEncryptAndDecrypt(oldPassword, newPassword, encrypt) { - return (dispatch) => { +export function doSyncEncryptAndDecrypt(oldPassword: string, newPassword: string, encrypt: boolean) { + return (dispatch: Dispatch) => { const data = {}; return Lbry.sync_hash() .then((hash) => Lbryio.call('sync', 'get', { hash }, 'post')) @@ -360,7 +366,7 @@ export function doSyncEncryptAndDecrypt(oldPassword, newPassword, encrypt) { }; } -export function doSetSyncLock(lock) { +export function doSetSyncLock(lock: boolean) { return { type: ACTIONS.SET_SYNC_LOCK, data: lock, @@ -373,3 +379,153 @@ export function doSetPrefsReady() { data: true, }; } + +type SharedData = { + version: '0.1', + value: { + subscriptions?: Array, + following?: Array<{ uri: string, notificationsDisabled: boolean }>, + tags?: Array, + blocked?: Array, + coin_swap_codes?: Array, + settings?: any, + app_welcome_version?: number, + sharing_3P?: boolean, + unpublishedCollections: CollectionGroup, + editedCollections: CollectionGroup, + builtinCollections: CollectionGroup, + savedCollections: Array, + }, +}; + +function extractUserState(rawObj: SharedData) { + if (rawObj && rawObj.version === '0.1' && rawObj.value) { + const { + subscriptions, + following, + tags, + blocked, + coin_swap_codes, + settings, + app_welcome_version, + sharing_3P, + unpublishedCollections, + editedCollections, + builtinCollections, + savedCollections, + } = rawObj.value; + + return { + ...(subscriptions ? { subscriptions } : {}), + ...(following ? { following } : {}), + ...(tags ? { tags } : {}), + ...(blocked ? { blocked } : {}), + ...(coin_swap_codes ? { coin_swap_codes } : {}), + ...(settings ? { settings } : {}), + ...(app_welcome_version ? { app_welcome_version } : {}), + ...(sharing_3P ? { sharing_3P } : {}), + ...(unpublishedCollections ? { unpublishedCollections } : {}), + ...(editedCollections ? { editedCollections } : {}), + ...(builtinCollections ? { builtinCollections } : {}), + ...(savedCollections ? { savedCollections } : {}), + }; + } + + return {}; +} + +export function doPopulateSharedUserState(sharedSettings: any) { + return (dispatch: Dispatch) => { + const { + subscriptions, + following, + tags, + blocked, + coin_swap_codes, + settings, + app_welcome_version, + sharing_3P, + unpublishedCollections, + editedCollections, + builtinCollections, + savedCollections, + } = extractUserState(sharedSettings); + dispatch({ + type: ACTIONS.USER_STATE_POPULATE, + data: { + subscriptions, + following, + tags, + blocked, + coinSwapCodes: coin_swap_codes, + settings, + welcomeVersion: app_welcome_version, + allowAnalytics: sharing_3P, + unpublishedCollections, + editedCollections, + builtinCollections, + savedCollections, + }, + }); + }; +} + +export function doPreferenceSet(key: string, value: any, version: string, success: Function, fail: Function) { + return (dispatch: Dispatch) => { + const preference = { + type: typeof value, + version, + value, + }; + + const options = { + key, + value: JSON.stringify(preference), + }; + + Lbry.preference_set(options) + .then(() => { + if (success) { + success(preference); + } + }) + .catch((err) => { + dispatch({ + type: ACTIONS.SYNC_FATAL_ERROR, + error: err, + }); + + if (fail) { + fail(); + } + }); + }; +} + +export function doPreferenceGet(key: string, success: Function, fail?: Function) { + return (dispatch: Dispatch) => { + const options = { + key, + }; + + return Lbry.preference_get(options) + .then((result) => { + if (result) { + const preference = result[key]; + return success(preference); + } + + return success(null); + }) + .catch((err) => { + dispatch({ + type: ACTIONS.SYNC_FATAL_ERROR, + error: err, + }); + + if (fail) { + fail(err); + } + }); + }; +} diff --git a/ui/redux/actions/user.js b/ui/redux/actions/user.js index 5738eb3c0..9ff3932e5 100644 --- a/ui/redux/actions/user.js +++ b/ui/redux/actions/user.js @@ -1,11 +1,9 @@ -import { - Lbry, - doFetchChannelListMine, - batchActions, - makeSelectClaimForUri, - isURIValid, - normalizeURI, -} from 'lbry-redux'; +import Lbry from 'lbry'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; +import { doFetchChannelListMine } from 'redux/actions/claims'; +import { isURIValid, normalizeURI } from 'util/lbryURI'; +import { batchActions } from 'util/batch-actions'; + import * as ACTIONS from 'constants/action_types'; import { doClaimRewardType, doRewardList } from 'redux/actions/rewards'; import { selectEmailToVerify, selectPhoneToVerify, selectUserCountryCode, selectUser } from 'redux/selectors/user'; @@ -101,7 +99,7 @@ export function doInstallNewWithParams( function checkAuthBusy() { let time = Date.now(); - return new Promise(function(resolve, reject) { + return new Promise(function (resolve, reject) { (function waitForAuth() { try { sessionStorage.setItem('test', 'available'); diff --git a/ui/redux/actions/wallet.js b/ui/redux/actions/wallet.js new file mode 100644 index 000000000..f8cb70c61 --- /dev/null +++ b/ui/redux/actions/wallet.js @@ -0,0 +1,702 @@ +import * as ACTIONS from 'constants/action_types'; +import Lbry from 'lbry'; +import { doToast } from 'redux/actions/notifications'; +import { + selectBalance, + selectPendingSupportTransactions, + selectTxoPageParams, + selectPendingOtherTransactions, + selectPendingConsolidateTxid, + selectPendingMassClaimTxid, +} from 'redux/selectors/wallet'; +import { creditsToString } from 'util/format-credits'; +import { selectMyClaimsRaw, selectClaimsById } from 'redux/selectors/claims'; +import { doFetchChannelListMine, doFetchClaimListMine, doClaimSearch } from 'redux/actions/claims'; + +const FIFTEEN_SECONDS = 15000; +let walletBalancePromise = null; + +export function doUpdateBalance() { + return (dispatch, getState) => { + const { + wallet: { totalBalance: totalInStore }, + } = getState(); + + if (walletBalancePromise === null) { + walletBalancePromise = Lbry.wallet_balance() + .then((response) => { + walletBalancePromise = null; + + const { available, reserved, reserved_subtotals, total } = response; + const { claims, supports, tips } = reserved_subtotals; + const totalFloat = parseFloat(total); + + if (totalInStore !== totalFloat) { + dispatch({ + type: ACTIONS.UPDATE_BALANCE, + data: { + totalBalance: totalFloat, + balance: parseFloat(available), + reservedBalance: parseFloat(reserved), + claimsBalance: parseFloat(claims), + supportsBalance: parseFloat(supports), + tipsBalance: parseFloat(tips), + }, + }); + } + }) + .catch(() => { + walletBalancePromise = null; + }); + } + + return walletBalancePromise; + }; +} + +export function doBalanceSubscribe() { + return (dispatch) => { + dispatch(doUpdateBalance()); + setInterval(() => dispatch(doUpdateBalance()), 10000); + }; +} + +export function doFetchTransactions(page = 1, pageSize = 999999) { + return (dispatch) => { + dispatch({ + type: ACTIONS.FETCH_TRANSACTIONS_STARTED, + }); + + Lbry.transaction_list({ page, page_size: pageSize }).then((result) => { + dispatch({ + type: ACTIONS.FETCH_TRANSACTIONS_COMPLETED, + data: { + transactions: result.items, + }, + }); + }); + }; +} + +export function doFetchTxoPage() { + return (dispatch, getState) => { + const fetchId = Math.random().toString(36).substr(2, 9); + + dispatch({ + type: ACTIONS.FETCH_TXO_PAGE_STARTED, + data: fetchId, + }); + + const state = getState(); + const queryParams = selectTxoPageParams(state); + + Lbry.txo_list(queryParams) + .then((res) => { + const items = res.items || []; + const claimsById = selectClaimsById(state); + + const channelIds = items.reduce((acc, cur) => { + if (cur.type === 'support' && cur.signing_channel && !claimsById[cur.signing_channel.channel_id]) { + acc.push(cur.signing_channel.channel_id); + } + return acc; + }, []); + + if (channelIds.length) { + const searchParams = { + page_size: 9999, + page: 1, + no_totals: true, + claim_ids: channelIds, + }; + // make sure redux has these channels resolved + dispatch(doClaimSearch(searchParams)); + } + + return res; + }) + .then((res) => { + dispatch({ + type: ACTIONS.FETCH_TXO_PAGE_COMPLETED, + data: { + result: res, + fetchId: fetchId, + }, + }); + }) + .catch((e) => { + dispatch({ + type: ACTIONS.FETCH_TXO_PAGE_COMPLETED, + data: { + error: e.message, + fetchId: fetchId, + }, + }); + }); + }; +} + +export function doUpdateTxoPageParams(params) { + return (dispatch) => { + dispatch({ + type: ACTIONS.UPDATE_TXO_FETCH_PARAMS, + data: params, + }); + + dispatch(doFetchTxoPage()); + }; +} + +export function doFetchSupports(page = 1, pageSize = 99999) { + return (dispatch) => { + dispatch({ + type: ACTIONS.FETCH_SUPPORTS_STARTED, + }); + + Lbry.support_list({ page, page_size: pageSize }).then((result) => { + dispatch({ + type: ACTIONS.FETCH_SUPPORTS_COMPLETED, + data: { + supports: result.items, + }, + }); + }); + }; +} + +export function doFetchUtxoCounts() { + return async (dispatch) => { + dispatch({ + type: ACTIONS.FETCH_UTXO_COUNT_STARTED, + }); + + let resultSets = await Promise.all([ + Lbry.txo_list({ type: 'other', is_not_spent: true, page: 1, page_size: 1 }), + Lbry.txo_list({ type: 'support', is_not_spent: true, page: 1, page_size: 1 }), + ]); + const counts = {}; + const paymentCount = resultSets[0]['total_items']; + const supportCount = resultSets[1]['total_items']; + counts['other'] = typeof paymentCount === 'number' ? paymentCount : 0; + counts['support'] = typeof supportCount === 'number' ? supportCount : 0; + + dispatch({ + type: ACTIONS.FETCH_UTXO_COUNT_COMPLETED, + data: counts, + debug: { resultSets }, + }); + }; +} + +export function doUtxoConsolidate() { + return async (dispatch) => { + dispatch({ + type: ACTIONS.DO_UTXO_CONSOLIDATE_STARTED, + }); + + const results = await Lbry.txo_spend({ type: 'other' }); + const result = results[0]; + + dispatch({ + type: ACTIONS.PENDING_CONSOLIDATED_TXOS_UPDATED, + data: { txids: [result.txid] }, + }); + + dispatch({ + type: ACTIONS.DO_UTXO_CONSOLIDATE_COMPLETED, + data: { txid: result.txid }, + }); + dispatch(doCheckPendingTxs()); + }; +} + +export function doTipClaimMass() { + return async (dispatch) => { + dispatch({ + type: ACTIONS.TIP_CLAIM_MASS_STARTED, + }); + + const results = await Lbry.txo_spend({ type: 'support', is_not_my_input: true }); + const result = results[0]; + + dispatch({ + type: ACTIONS.PENDING_CONSOLIDATED_TXOS_UPDATED, + data: { txids: [result.txid] }, + }); + + dispatch({ + type: ACTIONS.TIP_CLAIM_MASS_COMPLETED, + data: { txid: result.txid }, + }); + dispatch(doCheckPendingTxs()); + }; +} + +export function doGetNewAddress() { + return (dispatch) => { + dispatch({ + type: ACTIONS.GET_NEW_ADDRESS_STARTED, + }); + + Lbry.address_unused().then((address) => { + dispatch({ + type: ACTIONS.GET_NEW_ADDRESS_COMPLETED, + data: { address }, + }); + }); + }; +} + +export function doCheckAddressIsMine(address) { + return (dispatch) => { + dispatch({ + type: ACTIONS.CHECK_ADDRESS_IS_MINE_STARTED, + }); + + Lbry.address_is_mine({ address }).then((isMine) => { + if (!isMine) dispatch(doGetNewAddress()); + + dispatch({ + type: ACTIONS.CHECK_ADDRESS_IS_MINE_COMPLETED, + }); + }); + }; +} + +export function doSendDraftTransaction(address, amount) { + return (dispatch, getState) => { + const state = getState(); + const balance = selectBalance(state); + + if (balance - amount <= 0) { + dispatch( + doToast({ + title: __('Insufficient credits'), + message: __('Insufficient credits'), + }) + ); + return; + } + + dispatch({ + type: ACTIONS.SEND_TRANSACTION_STARTED, + }); + + const successCallback = (response) => { + if (response.txid) { + dispatch({ + type: ACTIONS.SEND_TRANSACTION_COMPLETED, + }); + dispatch( + doToast({ + message: __('You sent %amount% LBRY Credits', { amount: amount }), + linkText: __('History'), + linkTarget: '/wallet', + }) + ); + } else { + dispatch({ + type: ACTIONS.SEND_TRANSACTION_FAILED, + data: { error: response }, + }); + dispatch( + doToast({ + message: __('Transaction failed'), + isError: true, + }) + ); + } + }; + + const errorCallback = (error) => { + dispatch({ + type: ACTIONS.SEND_TRANSACTION_FAILED, + data: { error: error.message }, + }); + dispatch( + doToast({ + message: __('Transaction failed'), + isError: true, + }) + ); + }; + + Lbry.wallet_send({ + addresses: [address], + amount: creditsToString(amount), + }).then(successCallback, errorCallback); + }; +} + +export function doSetDraftTransactionAmount(amount) { + return { + type: ACTIONS.SET_DRAFT_TRANSACTION_AMOUNT, + data: { amount }, + }; +} + +export function doSetDraftTransactionAddress(address) { + return { + type: ACTIONS.SET_DRAFT_TRANSACTION_ADDRESS, + data: { address }, + }; +} + +export function doSendTip(params, isSupport, successCallback, errorCallback, shouldNotify = true) { + return (dispatch, getState) => { + const state = getState(); + const balance = selectBalance(state); + const myClaims = selectMyClaimsRaw(state); + + const shouldSupport = + isSupport || (myClaims ? myClaims.find((claim) => claim.claim_id === params.claim_id) : false); + + if (balance - params.amount <= 0) { + dispatch( + doToast({ + message: __('Insufficient credits'), + isError: true, + }) + ); + return; + } + + const success = (response) => { + if (shouldNotify) { + dispatch( + doToast({ + message: shouldSupport + ? __('You deposited %amount% LBRY Credits as a support!', { amount: params.amount }) + : __('You sent %amount% LBRY Credits as a tip, Mahalo!', { amount: params.amount }), + linkText: __('History'), + linkTarget: '/wallet', + }) + ); + } + + dispatch({ + type: ACTIONS.SUPPORT_TRANSACTION_COMPLETED, + }); + + if (successCallback) { + successCallback(response); + } + }; + + const error = (err) => { + dispatch( + doToast({ + message: __(`There was an error sending support funds.`), + isError: true, + }) + ); + + dispatch({ + type: ACTIONS.SUPPORT_TRANSACTION_FAILED, + data: { + error: err, + }, + }); + + if (errorCallback) { + errorCallback(); + } + }; + + dispatch({ + type: ACTIONS.SUPPORT_TRANSACTION_STARTED, + }); + + Lbry.support_create({ + ...params, + tip: !shouldSupport, + blocking: true, + amount: creditsToString(params.amount), + }).then(success, error); + }; +} + +export function doClearSupport() { + return { + type: ACTIONS.CLEAR_SUPPORT_TRANSACTION, + }; +} + +export function doWalletEncrypt(newPassword) { + return (dispatch) => { + dispatch({ + type: ACTIONS.WALLET_ENCRYPT_START, + }); + + Lbry.wallet_encrypt({ new_password: newPassword }).then((result) => { + if (result === true) { + dispatch({ + type: ACTIONS.WALLET_ENCRYPT_COMPLETED, + result, + }); + } else { + dispatch({ + type: ACTIONS.WALLET_ENCRYPT_FAILED, + result, + }); + } + }); + }; +} + +export function doWalletUnlock(password) { + return (dispatch) => { + dispatch({ + type: ACTIONS.WALLET_UNLOCK_START, + }); + + Lbry.wallet_unlock({ password }).then((result) => { + if (result === true) { + dispatch({ + type: ACTIONS.WALLET_UNLOCK_COMPLETED, + result, + }); + } else { + dispatch({ + type: ACTIONS.WALLET_UNLOCK_FAILED, + result, + }); + } + }); + }; +} + +export function doWalletLock() { + return (dispatch) => { + dispatch({ + type: ACTIONS.WALLET_LOCK_START, + }); + + Lbry.wallet_lock().then((result) => { + if (result === true) { + dispatch({ + type: ACTIONS.WALLET_LOCK_COMPLETED, + result, + }); + } else { + dispatch({ + type: ACTIONS.WALLET_LOCK_FAILED, + result, + }); + } + }); + }; +} + +// Collect all tips for a claim +export function doSupportAbandonForClaim(claimId, claimType, keep, preview) { + return (dispatch) => { + if (preview) { + dispatch({ + type: ACTIONS.ABANDON_CLAIM_SUPPORT_PREVIEW, + }); + } else { + dispatch({ + type: ACTIONS.ABANDON_CLAIM_SUPPORT_STARTED, + }); + } + + const params = { claim_id: claimId }; + if (preview) params['preview'] = true; + if (keep) params['keep'] = keep; + return Lbry.support_abandon(params) + .then((res) => { + if (!preview) { + dispatch({ + type: ACTIONS.ABANDON_CLAIM_SUPPORT_COMPLETED, + data: { claimId, txid: res.txid, effective: res.outputs[0].amount, type: claimType }, + }); + dispatch(doCheckPendingTxs()); + } + return res; + }) + .catch((e) => { + dispatch({ + type: ACTIONS.ABANDON_CLAIM_SUPPORT_FAILED, + data: e.message, + }); + }); + }; +} + +export function doWalletReconnect() { + return (dispatch) => { + dispatch({ + type: ACTIONS.WALLET_RESTART, + }); + let failed = false; + // this basically returns null when it's done. :( + // might be good to dispatch ACTIONS.WALLET_RESTARTED + const walletTimeout = setTimeout(() => { + failed = true; + dispatch({ + type: ACTIONS.WALLET_RESTART_COMPLETED, + }); + dispatch( + doToast({ + message: __('Your servers were not available. Check your url and port, or switch back to defaults.'), + isError: true, + }) + ); + }, FIFTEEN_SECONDS); + Lbry.wallet_reconnect().then(() => { + clearTimeout(walletTimeout); + if (!failed) dispatch({ type: ACTIONS.WALLET_RESTART_COMPLETED }); + }); + }; +} + +export function doWalletDecrypt() { + return (dispatch) => { + dispatch({ + type: ACTIONS.WALLET_DECRYPT_START, + }); + + Lbry.wallet_decrypt().then((result) => { + if (result === true) { + dispatch({ + type: ACTIONS.WALLET_DECRYPT_COMPLETED, + result, + }); + } else { + dispatch({ + type: ACTIONS.WALLET_DECRYPT_FAILED, + result, + }); + } + }); + }; +} + +export function doWalletStatus() { + return (dispatch) => { + dispatch({ + type: ACTIONS.WALLET_STATUS_START, + }); + + Lbry.wallet_status().then((status) => { + if (status) { + dispatch({ + type: ACTIONS.WALLET_STATUS_COMPLETED, + result: status.is_encrypted, + }); + } + }); + }; +} + +export function doSetTransactionListFilter(filterOption) { + return { + type: ACTIONS.SET_TRANSACTION_LIST_FILTER, + data: filterOption, + }; +} + +export function doUpdateBlockHeight() { + return (dispatch) => + Lbry.status().then((status) => { + if (status.wallet) { + dispatch({ + type: ACTIONS.UPDATE_CURRENT_HEIGHT, + data: status.wallet.blocks, + }); + } + }); +} + +// Calls transaction_show on txes until any pending txes are confirmed +export const doCheckPendingTxs = () => (dispatch, getState) => { + const state = getState(); + const pendingTxsById = selectPendingSupportTransactions(state); // {} + const pendingOtherTxes = selectPendingOtherTransactions(state); + + if (!Object.keys(pendingTxsById).length && !pendingOtherTxes.length) { + return; + } + let txCheckInterval; + const checkTxList = () => { + const state = getState(); + const pendingSupportTxs = selectPendingSupportTransactions(state); // {} + const pendingConsolidateTxes = selectPendingOtherTransactions(state); + const pendingConsTxid = selectPendingConsolidateTxid(state); + const pendingMassCLaimTxid = selectPendingMassClaimTxid(state); + + const promises = []; + const newPendingTxes = {}; + const noLongerPendingConsolidate = []; + const types = new Set([]); + // { claimId: {txid: 123, amount 12.3}, } + const entries = Object.entries(pendingSupportTxs); + entries.forEach(([claim, data]) => { + promises.push(Lbry.transaction_show({ txid: data.txid })); + types.add(data.type); + }); + if (pendingConsolidateTxes.length) { + pendingConsolidateTxes.forEach((txid) => promises.push(Lbry.transaction_show({ txid }))); + } + + Promise.all(promises).then((txShows) => { + let changed = false; + txShows.forEach((result) => { + if (pendingConsolidateTxes.includes(result.txid)) { + if (result.height > 0) { + noLongerPendingConsolidate.push(result.txid); + } + } else { + if (result.height <= 0) { + const match = entries.find((entry) => entry[1].txid === result.txid); + newPendingTxes[match[0]] = match[1]; + } else { + changed = true; + } + } + }); + + if (changed) { + dispatch({ + type: ACTIONS.PENDING_SUPPORTS_UPDATED, + data: newPendingTxes, + }); + if (types.has('channel')) { + dispatch(doFetchChannelListMine()); + } + if (types.has('stream')) { + dispatch(doFetchClaimListMine()); + } + } + if (noLongerPendingConsolidate.length) { + if (noLongerPendingConsolidate.includes(pendingConsTxid)) { + dispatch( + doToast({ + message: __('Your wallet is finished consolidating'), + }) + ); + } + if (noLongerPendingConsolidate.includes(pendingMassCLaimTxid)) { + dispatch( + doToast({ + message: __('Your tips have been collected'), + }) + ); + } + dispatch({ + type: ACTIONS.PENDING_CONSOLIDATED_TXOS_UPDATED, + data: { txids: noLongerPendingConsolidate, remove: true }, + }); + } + + if (!Object.keys(pendingTxsById).length && !pendingOtherTxes.length) { + clearInterval(txCheckInterval); + } + }); + }; + + txCheckInterval = setInterval(() => { + checkTxList(); + }, 30000); +}; diff --git a/ui/redux/middleware/shared-state.js b/ui/redux/middleware/shared-state.js new file mode 100644 index 000000000..80117fa90 --- /dev/null +++ b/ui/redux/middleware/shared-state.js @@ -0,0 +1,58 @@ +// @flow +import isEqual from 'util/deep-equal'; +import { doPreferenceSet } from 'redux/actions/sync'; + +const RUN_PREFERENCES_DELAY_MS = 2000; +const SHARED_PREFERENCE_VERSION = '0.1'; +let oldShared = {}; +let timeout; +export const buildSharedStateMiddleware = ( + actions: Array, + sharedStateFilters: {}, + sharedStateCb?: (any) => void +) => ({ getState, dispatch }: { getState: () => { user: any, settings: any }, dispatch: (any) => void }) => ( + next: ({}) => void +) => (action: { type: string, data: any }) => { + const currentState = getState(); + + // We don't care if sync is disabled here, we always want to backup preferences to the wallet + if (!actions.includes(action.type) || typeof action === 'function') { + return next(action); + } + clearTimeout(timeout); + const actionResult = next(action); + // Call `getState` after calling `next` to ensure the state has updated in response to the action + function runPreferences() { + const nextState: { user: any, settings: any } = getState(); + const syncEnabled = + nextState.settings && nextState.settings.clientSettings && nextState.settings.clientSettings.enable_sync; + const hasVerifiedEmail = nextState.user && nextState.user.user && nextState.user.user.has_verified_email; + const preferenceKey = syncEnabled && hasVerifiedEmail ? 'shared' : 'local'; + const shared = {}; + + Object.keys(sharedStateFilters).forEach((key) => { + const filter = sharedStateFilters[key]; + const { source, property, transform } = filter; + let value = nextState[source][property]; + if (transform) { + value = transform(value); + } + + shared[key] = value; + }); + + if (!isEqual(oldShared, shared)) { + // only update if the preference changed from last call in the same session + oldShared = shared; + dispatch(doPreferenceSet(preferenceKey, shared, SHARED_PREFERENCE_VERSION)); + } + + if (sharedStateCb) { + // Pass dispatch to the callback to consumers can dispatch actions in response to preference set + sharedStateCb({ dispatch, getState }); + } + clearTimeout(timeout); + return actionResult; + } + timeout = setTimeout(runPreferences, RUN_PREFERENCES_DELAY_MS); +}; diff --git a/ui/redux/reducers/app.js b/ui/redux/reducers/app.js index 69f958f83..4772ebb2d 100644 --- a/ui/redux/reducers/app.js +++ b/ui/redux/reducers/app.js @@ -1,7 +1,6 @@ // @flow import * as ACTIONS from 'constants/action_types'; -import { ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux'; import { remote } from 'electron'; // @if TARGET='app' @@ -112,7 +111,7 @@ reducers['@@router/LOCATION_CHANGE'] = (state, action) => { }; }; -reducers[ACTIONS.DAEMON_READY] = state => +reducers[ACTIONS.DAEMON_READY] = (state) => Object.assign({}, state, { daemonReady: true, }); @@ -122,29 +121,29 @@ reducers[ACTIONS.PASSWORD_SAVED] = (state, action) => isPasswordSaved: action.data, }); -reducers[ACTIONS.DAEMON_VERSION_MATCH] = state => +reducers[ACTIONS.DAEMON_VERSION_MATCH] = (state) => Object.assign({}, state, { daemonVersionMatched: true, }); -reducers[ACTIONS.DAEMON_VERSION_MISMATCH] = state => +reducers[ACTIONS.DAEMON_VERSION_MISMATCH] = (state) => Object.assign({}, state, { daemonVersionMatched: false, }); -reducers[ACTIONS.UPGRADE_CANCELLED] = state => +reducers[ACTIONS.UPGRADE_CANCELLED] = (state) => Object.assign({}, state, { downloadProgress: null, upgradeDownloadComplete: false, modal: null, }); -reducers[ACTIONS.AUTO_UPDATE_DOWNLOADED] = state => +reducers[ACTIONS.AUTO_UPDATE_DOWNLOADED] = (state) => Object.assign({}, state, { autoUpdateDownloaded: true, }); -reducers[ACTIONS.AUTO_UPDATE_DECLINED] = state => +reducers[ACTIONS.AUTO_UPDATE_DECLINED] = (state) => Object.assign({}, state, { autoUpdateDeclined: true, }); @@ -156,7 +155,7 @@ reducers[ACTIONS.UPGRADE_DOWNLOAD_COMPLETED] = (state, action) => upgradeDownloadCompleted: true, }); -reducers[ACTIONS.UPGRADE_DOWNLOAD_STARTED] = state => +reducers[ACTIONS.UPGRADE_DOWNLOAD_STARTED] = (state) => Object.assign({}, state, { upgradeDownloading: true, }); @@ -166,7 +165,7 @@ reducers[ACTIONS.CHANGE_MODALS_ALLOWED] = (state, action) => modalsAllowed: action.data.modalsAllowed, }); -reducers[ACTIONS.SKIP_UPGRADE] = state => { +reducers[ACTIONS.SKIP_UPGRADE] = (state) => { sessionStorage.setItem('upgradeSkipped', 'true'); return Object.assign({}, state, { @@ -174,12 +173,12 @@ reducers[ACTIONS.SKIP_UPGRADE] = state => { }); }; -reducers[ACTIONS.MEDIA_PLAY] = state => +reducers[ACTIONS.MEDIA_PLAY] = (state) => Object.assign({}, state, { modalsAllowed: false, }); -reducers[ACTIONS.MEDIA_PAUSE] = state => +reducers[ACTIONS.MEDIA_PAUSE] = (state) => Object.assign({}, state, { modalsAllowed: true, }); @@ -214,7 +213,7 @@ reducers[ACTIONS.UPGRADE_DOWNLOAD_PROGRESSED] = (state, action) => downloadProgress: action.data.percent, }); -reducers[ACTIONS.DOWNLOADING_COMPLETED] = state => { +reducers[ACTIONS.DOWNLOADING_COMPLETED] = (state) => { const { badgeNumber } = state; // Don't update the badge number if the window is focused @@ -227,7 +226,7 @@ reducers[ACTIONS.DOWNLOADING_COMPLETED] = state => { }); }; -reducers[ACTIONS.WINDOW_FOCUSED] = state => +reducers[ACTIONS.WINDOW_FOCUSED] = (state) => Object.assign({}, state, { badgeNumber: 0, }); @@ -242,18 +241,18 @@ reducers[ACTIONS.VOLUME_MUTED] = (state, action) => muted: action.data.muted, }); -reducers[ACTIONS.HISTORY_NAVIGATE] = state => +reducers[ACTIONS.HISTORY_NAVIGATE] = (state) => Object.assign({}, state, { modal: undefined, modalProps: {}, }); -reducers[ACTIONS.CLEAR_UPGRADE_TIMER] = state => +reducers[ACTIONS.CLEAR_UPGRADE_TIMER] = (state) => Object.assign({}, state, { checkUpgradeTimer: undefined, }); -reducers[ACTIONS.ADD_COMMENT] = state => +reducers[ACTIONS.ADD_COMMENT] = (state) => Object.assign({}, state, { hasClickedComment: true, }); @@ -279,13 +278,13 @@ reducers[ACTIONS.SET_HAS_NAVIGATED] = (state, action) => hasNavigated: action.data, }); -reducers[ACTIONS.HIDE_MODAL] = state => +reducers[ACTIONS.HIDE_MODAL] = (state) => Object.assign({}, state, { modal: null, modalProps: null, }); -reducers[ACTIONS.TOGGLE_SEARCH_EXPANDED] = state => +reducers[ACTIONS.TOGGLE_SEARCH_EXPANDED] = (state) => Object.assign({}, state, { searchOptionsExpanded: !state.searchOptionsExpanded, }); @@ -318,7 +317,7 @@ reducers[ACTIONS.SET_INCOGNITO] = (state, action) => { }; }; -reducers[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE] = (state, action) => { +reducers[ACTIONS.USER_STATE_POPULATE] = (state, action) => { const { welcomeVersion, allowAnalytics } = action.data; return { ...state, @@ -327,7 +326,7 @@ reducers[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE] = (state, action) => { }; }; -reducers[LBRY_REDUX_ACTIONS.PURCHASE_URI_FAILED] = (state, action) => { +reducers[ACTIONS.PURCHASE_URI_FAILED] = (state, action) => { return { ...state, modal: null, diff --git a/ui/redux/reducers/blocked.js b/ui/redux/reducers/blocked.js index d2ab80763..b1ed5b887 100644 --- a/ui/redux/reducers/blocked.js +++ b/ui/redux/reducers/blocked.js @@ -1,6 +1,5 @@ // @flow import * as ACTIONS from 'constants/action_types'; -import { ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux'; import { handleActions } from 'util/redux-utils'; const defaultState: BlocklistState = { @@ -24,10 +23,7 @@ export default handleActions( blockedChannels: newBlockedChannels, }; }, - [LBRY_REDUX_ACTIONS.USER_STATE_POPULATE]: ( - state: BlocklistState, - action: { data: { blocked: ?Array } } - ) => { + [ACTIONS.USER_STATE_POPULATE]: (state: BlocklistState, action: { data: { blocked: ?Array } }) => { const { blocked } = action.data; const sanitizedBlocked = blocked && blocked.filter((e) => typeof e === 'string'); return { diff --git a/ui/redux/reducers/claims.js b/ui/redux/reducers/claims.js new file mode 100644 index 000000000..c04c8327d --- /dev/null +++ b/ui/redux/reducers/claims.js @@ -0,0 +1,894 @@ +// @flow + +// This file has a lot of FlowFixMe comments +// It's due to Flow's support of Object.{values,entries} +// https://github.com/facebook/flow/issues/2221 +// We could move to es6 Sets/Maps, but those are not recommended for redux +// https://github.com/reduxjs/redux/issues/1499 +// Unsure of the best solution at the momentf +// - Sean + +import * as ACTIONS from 'constants/action_types'; +import mergeClaim from 'util/merge-claim'; + +type State = { + createChannelError: ?string, + createCollectionError: ?string, + channelClaimCounts: { [string]: number }, + claimsByUri: { [string]: string }, + byId: { [string]: Claim }, + pendingById: { [string]: Claim }, // keep pending claims + resolvingUris: Array, + reflectingById: { [string]: ReflectingUpdate }, + myClaims: ?Array, + myChannelClaims: ?Array, + myCollectionClaims: ?Array, + abandoningById: { [string]: boolean }, + fetchingChannelClaims: { [string]: number }, + fetchingMyChannels: boolean, + fetchingMyCollections: boolean, + fetchingClaimSearchByQuery: { [string]: boolean }, + purchaseUriSuccess: boolean, + myPurchases: ?Array, + myPurchasesPageNumber: ?number, + myPurchasesPageTotalResults: ?number, + fetchingMyPurchases: boolean, + fetchingMyPurchasesError: ?string, + claimSearchByQuery: { [string]: Array }, + claimSearchByQueryLastPageReached: { [string]: Array }, + creatingChannel: boolean, + creatingCollection: boolean, + paginatedClaimsByChannel: { + [string]: { + all: Array, + pageCount: number, + itemCount: number, + [number]: Array, + }, + }, + updateChannelError: ?string, + updateCollectionError: ?string, + updatingChannel: boolean, + updatingCollection: boolean, + pendingChannelImport: string | boolean, + repostLoading: boolean, + repostError: ?string, + fetchingClaimListMinePageError: ?string, + myClaimsPageResults: Array, + myClaimsPageNumber: ?number, + myClaimsPageTotalResults: ?number, + isFetchingClaimListMine: boolean, + isCheckingNameForPublish: boolean, + checkingPending: boolean, + checkingReflecting: boolean, +}; + +const reducers = {}; +const defaultState = { + byId: {}, + claimsByUri: {}, + paginatedClaimsByChannel: {}, + channelClaimCounts: {}, + fetchingChannelClaims: {}, + resolvingUris: [], + myChannelClaims: undefined, + myCollectionClaims: [], + myClaims: undefined, + myPurchases: undefined, + myPurchasesPageNumber: undefined, + myPurchasesPageTotalResults: undefined, + purchaseUriSuccess: false, + fetchingMyPurchases: false, + fetchingMyPurchasesError: undefined, + fetchingMyChannels: false, + fetchingMyCollections: false, + abandoningById: {}, + pendingById: {}, + reflectingById: {}, + claimSearchError: false, + claimSearchByQuery: {}, + claimSearchByQueryLastPageReached: {}, + fetchingClaimSearchByQuery: {}, + updateChannelError: '', + updateCollectionError: '', + updatingChannel: false, + creatingChannel: false, + createChannelError: undefined, + updatingCollection: false, + creatingCollection: false, + createCollectionError: undefined, + pendingChannelImport: false, + repostLoading: false, + repostError: undefined, + fetchingClaimListMinePageError: undefined, + myClaimsPageResults: [], + myClaimsPageNumber: undefined, + myClaimsPageTotalResults: undefined, + isFetchingClaimListMine: false, + isFetchingMyPurchases: false, + isCheckingNameForPublish: false, + checkingPending: false, + checkingReflecting: false, +}; + +function handleClaimAction(state: State, action: any): State { + const { resolveInfo }: ClaimActionResolveInfo = action.data; + + const byUri = Object.assign({}, state.claimsByUri); + const byId = Object.assign({}, state.byId); + const channelClaimCounts = Object.assign({}, state.channelClaimCounts); + const pendingById = state.pendingById; + let newResolvingUrls = new Set(state.resolvingUris); + let myClaimIds = new Set(state.myClaims); + + Object.entries(resolveInfo).forEach(([url, resolveResponse]: [string, ResolveResponse]) => { + // $FlowFixMe + const { claimsInChannel, stream, channel: channelFromResolve, collection } = resolveResponse; + const channel = channelFromResolve || (stream && stream.signing_channel); + + if (stream) { + if (pendingById[stream.claim_id]) { + byId[stream.claim_id] = mergeClaim(stream, byId[stream.claim_id]); + } else { + byId[stream.claim_id] = stream; + } + byUri[url] = stream.claim_id; + + // If url isn't a canonical_url, make sure that is added too + byUri[stream.canonical_url] = stream.claim_id; + + // Also add the permanent_url here until lighthouse returns canonical_url for search results + byUri[stream.permanent_url] = stream.claim_id; + newResolvingUrls.delete(stream.canonical_url); + newResolvingUrls.delete(stream.permanent_url); + + if (stream.is_my_output) { + myClaimIds.add(stream.claim_id); + } + } + + if (channel && channel.claim_id) { + if (!stream) { + byUri[url] = channel.claim_id; + } + + if (claimsInChannel) { + channelClaimCounts[url] = claimsInChannel; + channelClaimCounts[channel.canonical_url] = claimsInChannel; + } + + if (pendingById[channel.claim_id]) { + byId[channel.claim_id] = mergeClaim(channel, byId[channel.claim_id]); + } else { + byId[channel.claim_id] = channel; + } + + byUri[channel.permanent_url] = channel.claim_id; + byUri[channel.canonical_url] = channel.claim_id; + newResolvingUrls.delete(channel.canonical_url); + newResolvingUrls.delete(channel.permanent_url); + } + + if (collection) { + if (pendingById[collection.claim_id]) { + byId[collection.claim_id] = mergeClaim(collection, byId[collection.claim_id]); + } else { + byId[collection.claim_id] = collection; + } + byUri[url] = collection.claim_id; + byUri[collection.canonical_url] = collection.claim_id; + byUri[collection.permanent_url] = collection.claim_id; + newResolvingUrls.delete(collection.canonical_url); + newResolvingUrls.delete(collection.permanent_url); + + if (collection.is_my_output) { + myClaimIds.add(collection.claim_id); + } + } + + newResolvingUrls.delete(url); + if (!stream && !channel && !collection && !pendingById[byUri[url]]) { + byUri[url] = null; + } + }); + + return Object.assign({}, state, { + byId, + claimsByUri: byUri, + channelClaimCounts, + resolvingUris: Array.from(newResolvingUrls), + myClaims: Array.from(myClaimIds), + }); +} + +reducers[ACTIONS.RESOLVE_URIS_STARTED] = (state: State, action: any): State => { + const { uris }: { uris: Array } = action.data; + + const oldResolving = state.resolvingUris || []; + const newResolving = oldResolving.slice(); + + uris.forEach((uri) => { + if (!newResolving.includes(uri)) { + newResolving.push(uri); + } + }); + + return Object.assign({}, state, { + resolvingUris: newResolving, + }); +}; + +reducers[ACTIONS.RESOLVE_URIS_COMPLETED] = (state: State, action: any): State => { + return { + ...handleClaimAction(state, action), + }; +}; + +reducers[ACTIONS.FETCH_CLAIM_LIST_MINE_STARTED] = (state: State): State => + Object.assign({}, state, { + isFetchingClaimListMine: true, + }); + +reducers[ACTIONS.FETCH_CLAIM_LIST_MINE_COMPLETED] = (state: State, action: any): State => { + const { result }: { result: ClaimListResponse } = action.data; + const claims = result.items; + const page = result.page; + const totalItems = result.total_items; + + const byId = Object.assign({}, state.byId); + const byUri = Object.assign({}, state.claimsByUri); + const pendingById = Object.assign({}, state.pendingById); + let myClaimIds = new Set(state.myClaims); + let urlsForCurrentPage = []; + + claims.forEach((claim: Claim) => { + const { permanent_url: permanentUri, claim_id: claimId, canonical_url: canonicalUri } = claim; + if (claim.type && claim.type.match(/claim|update/)) { + urlsForCurrentPage.push(permanentUri); + if (claim.confirmations < 1) { + pendingById[claimId] = claim; + if (byId[claimId]) { + byId[claimId] = mergeClaim(claim, byId[claimId]); + } else { + byId[claimId] = claim; + } + } else { + byId[claimId] = claim; + } + byUri[permanentUri] = claimId; + byUri[canonicalUri] = claimId; + myClaimIds.add(claimId); + } + }); + + return Object.assign({}, state, { + isFetchingClaimListMine: false, + myClaims: Array.from(myClaimIds), + byId, + pendingById, + claimsByUri: byUri, + myClaimsPageResults: urlsForCurrentPage, + myClaimsPageNumber: page, + myClaimsPageTotalResults: totalItems, + }); +}; + +reducers[ACTIONS.FETCH_CHANNEL_LIST_STARTED] = (state: State): State => + Object.assign({}, state, { fetchingMyChannels: true }); + +reducers[ACTIONS.FETCH_CHANNEL_LIST_COMPLETED] = (state: State, action: any): State => { + const { claims }: { claims: Array } = action.data; + let myClaimIds = new Set(state.myClaims); + const pendingById = Object.assign({}, state.pendingById); + let myChannelClaims; + const byId = Object.assign({}, state.byId); + const byUri = Object.assign({}, state.claimsByUri); + const channelClaimCounts = Object.assign({}, state.channelClaimCounts); + + if (!claims.length) { + // $FlowFixMe + myChannelClaims = null; + } else { + myChannelClaims = new Set(state.myChannelClaims); + claims.forEach((claim) => { + const { meta } = claim; + const { claims_in_channel: claimsInChannel } = meta; + const { canonical_url: canonicalUrl, permanent_url: permanentUrl, claim_id: claimId, confirmations } = claim; + + byUri[canonicalUrl] = claimId; + byUri[permanentUrl] = claimId; + channelClaimCounts[canonicalUrl] = claimsInChannel; + channelClaimCounts[permanentUrl] = claimsInChannel; + + // $FlowFixMe + myChannelClaims.add(claimId); + if (confirmations < 1) { + pendingById[claimId] = claim; + if (byId[claimId]) { + byId[claimId] = mergeClaim(claim, byId[claimId]); + } else { + byId[claimId] = claim; + } + } else { + byId[claimId] = claim; + } + myClaimIds.add(claimId); + }); + } + + return Object.assign({}, state, { + byId, + pendingById, + claimsByUri: byUri, + channelClaimCounts, + fetchingMyChannels: false, + myChannelClaims: myChannelClaims ? Array.from(myChannelClaims) : null, + myClaims: myClaimIds ? Array.from(myClaimIds) : null, + }); +}; + +reducers[ACTIONS.FETCH_CHANNEL_LIST_FAILED] = (state: State, action: any): State => { + return Object.assign({}, state, { + fetchingMyChannels: false, + }); +}; + +reducers[ACTIONS.FETCH_COLLECTION_LIST_STARTED] = (state: State): State => ({ + ...state, + fetchingMyCollections: true, +}); + +reducers[ACTIONS.FETCH_COLLECTION_LIST_COMPLETED] = (state: State, action: any): State => { + const { claims }: { claims: Array } = action.data; + const myClaims = state.myClaims || []; + let myClaimIds = new Set(myClaims); + const pendingById = Object.assign({}, state.pendingById); + let myCollectionClaimsSet = new Set([]); + const byId = Object.assign({}, state.byId); + const byUri = Object.assign({}, state.claimsByUri); + + if (claims.length) { + myCollectionClaimsSet = new Set(state.myCollectionClaims); + claims.forEach((claim) => { + const { canonical_url: canonicalUrl, permanent_url: permanentUrl, claim_id: claimId, confirmations } = claim; + + byUri[canonicalUrl] = claimId; + byUri[permanentUrl] = claimId; + + // $FlowFixMe + myCollectionClaimsSet.add(claimId); + // we don't want to overwrite a pending result with a resolve + if (confirmations < 1) { + pendingById[claimId] = claim; + if (byId[claimId]) { + byId[claimId] = mergeClaim(claim, byId[claimId]); + } else { + byId[claimId] = claim; + } + } else { + byId[claimId] = claim; + } + myClaimIds.add(claimId); + }); + } + + return { + ...state, + byId, + pendingById, + claimsByUri: byUri, + fetchingMyCollections: false, + myCollectionClaims: Array.from(myCollectionClaimsSet), + myClaims: myClaimIds ? Array.from(myClaimIds) : null, + }; +}; + +reducers[ACTIONS.FETCH_COLLECTION_LIST_FAILED] = (state: State): State => { + return { ...state, fetchingMyCollections: false }; +}; + +reducers[ACTIONS.FETCH_CHANNEL_CLAIMS_STARTED] = (state: State, action: any): State => { + const { uri, page } = action.data; + const fetchingChannelClaims = Object.assign({}, state.fetchingChannelClaims); + + fetchingChannelClaims[uri] = page; + + return Object.assign({}, state, { + fetchingChannelClaims, + currentChannelPage: page, + }); +}; + +reducers[ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED] = (state: State, action: any): State => { + const { + uri, + claims, + claimsInChannel, + page, + totalPages, + }: { + uri: string, + claims: Array, + claimsInChannel?: number, + page: number, + totalPages: number, + } = action.data; + + // byChannel keeps claim_search relevant results by page. If the total changes, erase it. + const channelClaimCounts = Object.assign({}, state.channelClaimCounts); + + const paginatedClaimsByChannel = Object.assign({}, state.paginatedClaimsByChannel); + // check if count has changed - that means cached pagination will be wrong, so clear it + const previousCount = paginatedClaimsByChannel[uri] && paginatedClaimsByChannel[uri]['itemCount']; + const byChannel = claimsInChannel === previousCount ? Object.assign({}, paginatedClaimsByChannel[uri]) : {}; + const allClaimIds = new Set(byChannel.all); + const currentPageClaimIds = []; + const byId = Object.assign({}, state.byId); + const fetchingChannelClaims = Object.assign({}, state.fetchingChannelClaims); + const claimsByUri = Object.assign({}, state.claimsByUri); + + if (claims !== undefined) { + claims.forEach((claim) => { + allClaimIds.add(claim.claim_id); + currentPageClaimIds.push(claim.claim_id); + byId[claim.claim_id] = claim; + claimsByUri[claim.canonical_url] = claim.claim_id; + }); + } + + byChannel.all = allClaimIds; + byChannel.pageCount = totalPages; + byChannel.itemCount = claimsInChannel; + byChannel[page] = currentPageClaimIds; + paginatedClaimsByChannel[uri] = byChannel; + delete fetchingChannelClaims[uri]; + + return Object.assign({}, state, { + paginatedClaimsByChannel, + byId, + fetchingChannelClaims, + claimsByUri, + channelClaimCounts, + currentChannelPage: page, + }); +}; + +reducers[ACTIONS.ABANDON_CLAIM_STARTED] = (state: State, action: any): State => { + const { claimId }: { claimId: string } = action.data; + const abandoningById = Object.assign({}, state.abandoningById); + + abandoningById[claimId] = true; + + return Object.assign({}, state, { + abandoningById, + }); +}; + +reducers[ACTIONS.UPDATE_PENDING_CLAIMS] = (state: State, action: any): State => { + const { claims: pendingClaims }: { claims: Array } = action.data; + const byId = Object.assign({}, state.byId); + const pendingById = Object.assign({}, state.pendingById); + const byUri = Object.assign({}, state.claimsByUri); + let myClaimIds = new Set(state.myClaims); + const myChannelClaims = new Set(state.myChannelClaims); + + // $FlowFixMe + pendingClaims.forEach((claim: Claim) => { + let newClaim; + const { permanent_url: uri, claim_id: claimId, type, value_type: valueType } = claim; + pendingById[claimId] = claim; // make sure we don't need to merge? + const oldClaim = byId[claimId]; + if (oldClaim && oldClaim.canonical_url) { + newClaim = mergeClaim(oldClaim, claim); + } else { + newClaim = claim; + } + if (valueType === 'channel') { + myChannelClaims.add(claimId); + } + + if (type && type.match(/claim|update/)) { + byId[claimId] = newClaim; + byUri[uri] = claimId; + } + myClaimIds.add(claimId); + }); + return Object.assign({}, state, { + myClaims: Array.from(myClaimIds), + byId, + pendingById, + myChannelClaims: Array.from(myChannelClaims), + claimsByUri: byUri, + }); +}; + +reducers[ACTIONS.UPDATE_CONFIRMED_CLAIMS] = (state: State, action: any): State => { + const { + claims: confirmedClaims, + pending: pendingClaims, + }: { claims: Array, pending: { [string]: Claim } } = action.data; + const byId = Object.assign({}, state.byId); + const byUri = Object.assign({}, state.claimsByUri); + // + confirmedClaims.forEach((claim: GenericClaim) => { + const { claim_id: claimId, type } = claim; + let newClaim = claim; + const oldClaim = byId[claimId]; + if (oldClaim && oldClaim.canonical_url) { + newClaim = mergeClaim(oldClaim, claim); + } + if (type && type.match(/claim|update|channel/)) { + byId[claimId] = newClaim; + } + }); + return Object.assign({}, state, { + pendingById: pendingClaims, + byId, + claimsByUri: byUri, + }); +}; + +reducers[ACTIONS.ABANDON_CLAIM_SUCCEEDED] = (state: State, action: any): State => { + const { claimId }: { claimId: string } = action.data; + const byId = Object.assign({}, state.byId); + const newMyClaims = state.myClaims ? state.myClaims.slice() : []; + const newMyChannelClaims = state.myChannelClaims ? state.myChannelClaims.slice() : []; + const claimsByUri = Object.assign({}, state.claimsByUri); + const newMyCollectionClaims = state.myCollectionClaims ? state.myCollectionClaims.slice() : []; + + Object.keys(claimsByUri).forEach((uri) => { + if (claimsByUri[uri] === claimId) { + delete claimsByUri[uri]; + } + }); + const myClaims = newMyClaims.filter((i) => i !== claimId); + const myChannelClaims = newMyChannelClaims.filter((i) => i !== claimId); + const myCollectionClaims = newMyCollectionClaims.filter((i) => i !== claimId); + + delete byId[claimId]; + + return Object.assign({}, state, { + myClaims, + myChannelClaims, + myCollectionClaims, + byId, + claimsByUri, + }); +}; + +reducers[ACTIONS.CLEAR_CHANNEL_ERRORS] = (state: State): State => ({ + ...state, + createChannelError: null, + updateChannelError: null, +}); + +reducers[ACTIONS.CREATE_CHANNEL_STARTED] = (state: State): State => ({ + ...state, + creatingChannel: true, + createChannelError: null, +}); + +reducers[ACTIONS.CREATE_CHANNEL_COMPLETED] = (state: State, action: any): State => { + return Object.assign({}, state, { + creatingChannel: false, + }); +}; + +reducers[ACTIONS.CREATE_CHANNEL_FAILED] = (state: State, action: any): State => { + return Object.assign({}, state, { + creatingChannel: false, + createChannelError: action.data, + }); +}; + +reducers[ACTIONS.UPDATE_CHANNEL_STARTED] = (state: State, action: any): State => { + return Object.assign({}, state, { + updateChannelError: '', + updatingChannel: true, + }); +}; + +reducers[ACTIONS.UPDATE_CHANNEL_COMPLETED] = (state: State, action: any): State => { + return Object.assign({}, state, { + updateChannelError: '', + updatingChannel: false, + }); +}; + +reducers[ACTIONS.UPDATE_CHANNEL_FAILED] = (state: State, action: any): State => { + return Object.assign({}, state, { + updateChannelError: action.data.message, + updatingChannel: false, + }); +}; + +reducers[ACTIONS.CLEAR_COLLECTION_ERRORS] = (state: State): State => ({ + ...state, + createCollectionError: null, + updateCollectionError: null, +}); + +reducers[ACTIONS.COLLECTION_PUBLISH_STARTED] = (state: State): State => ({ + ...state, + creatingCollection: true, + createCollectionError: null, +}); + +reducers[ACTIONS.COLLECTION_PUBLISH_COMPLETED] = (state: State, action: any): State => { + const myCollections = state.myCollectionClaims || []; + const myClaims = state.myClaims || []; + const { claimId } = action.data; + let myClaimIds = new Set(myClaims); + let myCollectionClaimsSet = new Set(myCollections); + myClaimIds.add(claimId); + myCollectionClaimsSet.add(claimId); + return Object.assign({}, state, { + creatingCollection: false, + myClaims: Array.from(myClaimIds), + myCollectionClaims: Array.from(myCollectionClaimsSet), + }); +}; + +reducers[ACTIONS.COLLECTION_PUBLISH_FAILED] = (state: State, action: any): State => { + return Object.assign({}, state, { + creatingCollection: false, + createCollectionError: action.data.error, + }); +}; + +reducers[ACTIONS.COLLECTION_PUBLISH_UPDATE_STARTED] = (state: State, action: any): State => { + return Object.assign({}, state, { + updateCollectionError: '', + updatingCollection: true, + }); +}; + +reducers[ACTIONS.COLLECTION_PUBLISH_UPDATE_COMPLETED] = (state: State, action: any): State => { + return Object.assign({}, state, { + updateCollectionError: '', + updatingCollection: false, + }); +}; + +reducers[ACTIONS.COLLECTION_PUBLISH_UPDATE_FAILED] = (state: State, action: any): State => { + return Object.assign({}, state, { + updateCollectionError: action.data.error, + updatingCollection: false, + }); +}; + +reducers[ACTIONS.IMPORT_CHANNEL_STARTED] = (state: State): State => + Object.assign({}, state, { pendingChannelImports: true }); + +reducers[ACTIONS.IMPORT_CHANNEL_COMPLETED] = (state: State): State => + Object.assign({}, state, { pendingChannelImports: false }); + +reducers[ACTIONS.CLAIM_SEARCH_STARTED] = (state: State, action: any): State => { + const fetchingClaimSearchByQuery = Object.assign({}, state.fetchingClaimSearchByQuery); + fetchingClaimSearchByQuery[action.data.query] = true; + + return Object.assign({}, state, { + fetchingClaimSearchByQuery, + }); +}; + +reducers[ACTIONS.CLAIM_SEARCH_COMPLETED] = (state: State, action: any): State => { + const fetchingClaimSearchByQuery = Object.assign({}, state.fetchingClaimSearchByQuery); + const claimSearchByQuery = Object.assign({}, state.claimSearchByQuery); + const claimSearchByQueryLastPageReached = Object.assign({}, state.claimSearchByQueryLastPageReached); + const { append, query, urls, pageSize } = action.data; + + if (append) { + // todo: check for duplicate urls when concatenating? + claimSearchByQuery[query] = + claimSearchByQuery[query] && claimSearchByQuery[query].length ? claimSearchByQuery[query].concat(urls) : urls; + } else { + claimSearchByQuery[query] = urls; + } + + // the returned number of urls is less than the page size, so we're on the last page + claimSearchByQueryLastPageReached[query] = urls.length < pageSize; + + delete fetchingClaimSearchByQuery[query]; + + return Object.assign({}, state, { + ...handleClaimAction(state, action), + claimSearchByQuery, + claimSearchByQueryLastPageReached, + fetchingClaimSearchByQuery, + }); +}; + +reducers[ACTIONS.CLAIM_SEARCH_FAILED] = (state: State, action: any): State => { + const { query } = action.data; + const claimSearchByQuery = Object.assign({}, state.claimSearchByQuery); + const fetchingClaimSearchByQuery = Object.assign({}, state.fetchingClaimSearchByQuery); + const claimSearchByQueryLastPageReached = Object.assign({}, state.claimSearchByQueryLastPageReached); + + delete fetchingClaimSearchByQuery[query]; + + if (claimSearchByQuery[query] && claimSearchByQuery[query].length !== 0) { + claimSearchByQueryLastPageReached[query] = true; + } else { + claimSearchByQuery[query] = null; + } + + return Object.assign({}, state, { + fetchingClaimSearchByQuery, + claimSearchByQuery, + claimSearchByQueryLastPageReached, + }); +}; + +reducers[ACTIONS.CLAIM_REPOST_STARTED] = (state: State): State => { + return { + ...state, + repostLoading: true, + repostError: null, + }; +}; +reducers[ACTIONS.CLAIM_REPOST_COMPLETED] = (state: State, action: any): State => { + const { originalClaimId, repostClaim } = action.data; + const byId = { ...state.byId }; + const claimsByUri = { ...state.claimsByUri }; + const claimThatWasReposted = byId[originalClaimId]; + + const repostStub = { ...repostClaim, reposted_claim: claimThatWasReposted }; + byId[repostStub.claim_id] = repostStub; + claimsByUri[repostStub.permanent_url] = repostStub.claim_id; + + return { + ...state, + byId, + claimsByUri, + repostLoading: false, + repostError: null, + }; +}; +reducers[ACTIONS.CLAIM_REPOST_FAILED] = (state: State, action: any): State => { + const { error } = action.data; + + return { + ...state, + repostLoading: false, + repostError: error, + }; +}; +reducers[ACTIONS.CLEAR_REPOST_ERROR] = (state: State): State => { + return { + ...state, + repostError: null, + }; +}; +reducers[ACTIONS.ADD_FILES_REFLECTING] = (state: State, action): State => { + const pendingClaim = action.data; + const { reflectingById } = state; + const claimId = pendingClaim && pendingClaim.claim_id; + + reflectingById[claimId] = { fileListItem: pendingClaim, progress: 0, stalled: false }; + + return Object.assign({}, state, { + ...state, + reflectingById: reflectingById, + }); +}; +reducers[ACTIONS.UPDATE_FILES_REFLECTING] = (state: State, action): State => { + const newReflectingById = action.data; + + return Object.assign({}, state, { + ...state, + reflectingById: newReflectingById, + }); +}; +reducers[ACTIONS.TOGGLE_CHECKING_REFLECTING] = (state: State, action): State => { + const checkingReflecting = action.data; + + return Object.assign({}, state, { + ...state, + checkingReflecting, + }); +}; +reducers[ACTIONS.TOGGLE_CHECKING_PENDING] = (state: State, action): State => { + const checking = action.data; + + return Object.assign({}, state, { + ...state, + checkingPending: checking, + }); +}; + +reducers[ACTIONS.PURCHASE_LIST_STARTED] = (state: State): State => { + return { + ...state, + fetchingMyPurchases: true, + fetchingMyPurchasesError: null, + }; +}; + +reducers[ACTIONS.PURCHASE_LIST_COMPLETED] = (state: State, action: any): State => { + const { result }: { result: PurchaseListResponse, resolve: boolean } = action.data; + const page = result.page; + const totalItems = result.total_items; + + let byId = Object.assign({}, state.byId); + let byUri = Object.assign({}, state.claimsByUri); + let urlsForCurrentPage = []; + + result.items.forEach((item) => { + if (!item.claim) { + // Abandoned claim + return; + } + + const { claim, ...purchaseInfo } = item; + claim.purchase_receipt = purchaseInfo; + const claimId = claim.claim_id; + const uri = claim.canonical_url; + + byId[claimId] = claim; + byUri[uri] = claimId; + urlsForCurrentPage.push(uri); + }); + + return Object.assign({}, state, { + byId, + claimsByUri: byUri, + myPurchases: urlsForCurrentPage, + myPurchasesPageNumber: page, + myPurchasesPageTotalResults: totalItems, + fetchingMyPurchases: false, + }); +}; + +reducers[ACTIONS.PURCHASE_LIST_FAILED] = (state: State, action: any): State => { + const { error } = action.data; + + return { + ...state, + fetchingMyPurchases: false, + fetchingMyPurchasesError: error, + }; +}; + +reducers[ACTIONS.PURCHASE_URI_COMPLETED] = (state: State, action: any): State => { + const { uri, purchaseReceipt } = action.data; + + let byId = Object.assign({}, state.byId); + let byUri = Object.assign({}, state.claimsByUri); + let myPurchases = state.myPurchases ? state.myPurchases.slice() : []; + + const claimId = byUri[uri]; + if (claimId) { + let claim = byId[claimId]; + claim.purchase_receipt = purchaseReceipt; + } + + myPurchases.push(uri); + + return { + ...state, + byId, + myPurchases, + purchaseUriSuccess: true, + }; +}; + +reducers[ACTIONS.PURCHASE_URI_FAILED] = (state: State): State => { + return { + ...state, + purchaseUriSuccess: false, + }; +}; + +reducers[ACTIONS.CLEAR_PURCHASED_URI_SUCCESS] = (state: State): State => { + return { + ...state, + purchaseUriSuccess: false, + }; +}; + +export function claimsReducer(state: State = defaultState, action: any) { + const handler = reducers[action.type]; + if (handler) return handler(state, action); + return state; +} diff --git a/ui/redux/reducers/coinSwap.js b/ui/redux/reducers/coinSwap.js index c19502060..57c9f6f35 100644 --- a/ui/redux/reducers/coinSwap.js +++ b/ui/redux/reducers/coinSwap.js @@ -1,6 +1,5 @@ // @flow import * as ACTIONS from 'constants/action_types'; -import { ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux'; import { handleActions } from 'util/redux-utils'; const SWAP_HISTORY_LENGTH_LIMIT = 10; @@ -118,10 +117,7 @@ export default handleActions( coinSwaps: newCoinSwaps, }; }, - [LBRY_REDUX_ACTIONS.USER_STATE_POPULATE]: ( - state: CoinSwapState, - action: { data: { coinSwapCodes: ?Array } } - ) => { + [ACTIONS.USER_STATE_POPULATE]: (state: CoinSwapState, action: { data: { coinSwapCodes: ?Array } }) => { const { coinSwapCodes } = action.data; const newCoinSwaps = []; diff --git a/ui/redux/reducers/collections.js b/ui/redux/reducers/collections.js new file mode 100644 index 000000000..7747169a0 --- /dev/null +++ b/ui/redux/reducers/collections.js @@ -0,0 +1,226 @@ +// @flow +import { handleActions } from 'util/redux-utils'; +import * as ACTIONS from 'constants/action_types'; +import * as COLS from 'constants/collections'; + +const getTimestamp = () => { + return Math.floor(Date.now() / 1000); +}; + +const defaultState: CollectionState = { + builtin: { + watchlater: { + items: [], + id: COLS.WATCH_LATER_ID, + name: 'Watch Later', + updatedAt: getTimestamp(), + type: COLS.COL_TYPE_PLAYLIST, + }, + favorites: { + items: [], + id: COLS.FAVORITES_ID, + name: 'Favorites', + type: COLS.COL_TYPE_PLAYLIST, + updatedAt: getTimestamp(), + }, + }, + resolved: {}, + unpublished: {}, // sync + edited: {}, + pending: {}, + saved: [], + isResolvingCollectionById: {}, + error: null, +}; + +const collectionsReducer = handleActions( + { + [ACTIONS.COLLECTION_NEW]: (state, action) => { + const { entry: params } = action.data; // { id:, items: Array} + // entry + const newListTemplate = { + id: params.id, + name: params.name, + items: [], + updatedAt: getTimestamp(), + type: params.type, + }; + + const newList = Object.assign({}, newListTemplate, { ...params }); + const { unpublished: lists } = state; + const newLists = Object.assign({}, lists, { [params.id]: newList }); + + return { + ...state, + unpublished: newLists, + }; + }, + + [ACTIONS.COLLECTION_DELETE]: (state, action) => { + const { id, collectionKey } = action.data; + const { edited: editList, unpublished: unpublishedList, pending: pendingList } = state; + const newEditList = Object.assign({}, editList); + const newUnpublishedList = Object.assign({}, unpublishedList); + + const newPendingList = Object.assign({}, pendingList); + + if (collectionKey && state[collectionKey] && state[collectionKey][id]) { + const newList = Object.assign({}, state[collectionKey]); + delete newList[id]; + return { + ...state, + [collectionKey]: newList, + }; + } else { + if (newEditList[id]) { + delete newEditList[id]; + } else if (newUnpublishedList[id]) { + delete newUnpublishedList[id]; + } else if (newPendingList[id]) { + delete newPendingList[id]; + } + } + return { + ...state, + edited: newEditList, + unpublished: newUnpublishedList, + pending: newPendingList, + }; + }, + + [ACTIONS.COLLECTION_PENDING]: (state, action) => { + const { localId, claimId } = action.data; + const { resolved: resolvedList, edited: editList, unpublished: unpublishedList, pending: pendingList } = state; + + const newEditList = Object.assign({}, editList); + const newResolvedList = Object.assign({}, resolvedList); + const newUnpublishedList = Object.assign({}, unpublishedList); + const newPendingList = Object.assign({}, pendingList); + + if (localId) { + // new publish + newPendingList[claimId] = Object.assign({}, newUnpublishedList[localId] || {}); + delete newUnpublishedList[localId]; + } else { + // edit update + newPendingList[claimId] = Object.assign({}, newEditList[claimId] || newResolvedList[claimId]); + delete newEditList[claimId]; + } + + return { + ...state, + edited: newEditList, + unpublished: newUnpublishedList, + pending: newPendingList, + }; + }, + + [ACTIONS.COLLECTION_EDIT]: (state, action) => { + const { id, collectionKey, collection } = action.data; + + if (COLS.BUILTIN_LISTS.includes(id)) { + const { builtin: lists } = state; + return { + ...state, + [collectionKey]: { ...lists, [id]: collection }, + }; + } + + if (collectionKey === 'edited') { + const { edited: lists } = state; + return { + ...state, + edited: { ...lists, [id]: collection }, + }; + } + const { unpublished: lists } = state; + return { + ...state, + unpublished: { ...lists, [id]: collection }, + }; + }, + + [ACTIONS.COLLECTION_ERROR]: (state, action) => { + return Object.assign({}, state, { + error: action.data.message, + }); + }, + + [ACTIONS.COLLECTION_ITEMS_RESOLVE_STARTED]: (state, action) => { + const { ids } = action.data; + const { isResolvingCollectionById } = state; + const newResolving = Object.assign({}, isResolvingCollectionById); + ids.forEach((id) => { + newResolving[id] = true; + }); + return Object.assign({}, state, { + ...state, + error: '', + isResolvingCollectionById: newResolving, + }); + }, + [ACTIONS.USER_STATE_POPULATE]: (state, action) => { + const { builtinCollections, savedCollections, unpublishedCollections, editedCollections } = action.data; + return { + ...state, + edited: editedCollections || state.edited, + unpublished: unpublishedCollections || state.unpublished, + builtin: builtinCollections || state.builtin, + saved: savedCollections || state.saved, + }; + }, + [ACTIONS.COLLECTION_ITEMS_RESOLVE_COMPLETED]: (state, action) => { + const { resolvedCollections, failedCollectionIds } = action.data; + const { pending, edited, isResolvingCollectionById, resolved } = state; + const newPending = Object.assign({}, pending); + const newEdited = Object.assign({}, edited); + const newResolved = Object.assign({}, resolved, resolvedCollections); + + const resolvedIds = Object.keys(resolvedCollections); + const newResolving = Object.assign({}, isResolvingCollectionById); + if (resolvedCollections && Object.keys(resolvedCollections).length) { + resolvedIds.forEach((resolvedId) => { + if (newEdited[resolvedId]) { + if (newEdited[resolvedId]['updatedAt'] < resolvedCollections[resolvedId]['updatedAt']) { + delete newEdited[resolvedId]; + } + } + delete newResolving[resolvedId]; + if (newPending[resolvedId]) { + delete newPending[resolvedId]; + } + }); + } + + if (failedCollectionIds && Object.keys(failedCollectionIds).length) { + failedCollectionIds.forEach((failedId) => { + delete newResolving[failedId]; + }); + } + + return Object.assign({}, state, { + ...state, + pending: newPending, + resolved: newResolved, + edited: newEdited, + isResolvingCollectionById: newResolving, + }); + }, + [ACTIONS.COLLECTION_ITEMS_RESOLVE_FAILED]: (state, action) => { + const { ids } = action.data; + const { isResolvingCollectionById } = state; + const newResolving = Object.assign({}, isResolvingCollectionById); + ids.forEach((id) => { + delete newResolving[id]; + }); + return Object.assign({}, state, { + ...state, + isResolvingCollectionById: newResolving, + error: action.data.message, + }); + }, + }, + defaultState +); + +export { collectionsReducer }; diff --git a/ui/redux/reducers/comments.js b/ui/redux/reducers/comments.js index 29a65a885..ca3869f08 100644 --- a/ui/redux/reducers/comments.js +++ b/ui/redux/reducers/comments.js @@ -2,7 +2,7 @@ import * as ACTIONS from 'constants/action_types'; import { handleActions } from 'util/redux-utils'; import { BLOCK_LEVEL } from 'constants/comment'; -import { isURIEqual } from 'lbry-redux'; +import { isURIEqual } from 'util/lbryURI'; const defaultState: CommentsState = { commentById: {}, // commentId -> Comment diff --git a/ui/redux/reducers/file_info.js b/ui/redux/reducers/file_info.js new file mode 100644 index 000000000..ca8479290 --- /dev/null +++ b/ui/redux/reducers/file_info.js @@ -0,0 +1,158 @@ +import * as ACTIONS from 'constants/action_types'; +import * as SORT_OPTIONS from 'constants/sort_options'; +import * as PAGES from 'constants/pages'; + +const reducers = {}; +const defaultState = { + fileListPublishedSort: SORT_OPTIONS.DATE_NEW, + fileListDownloadedSort: SORT_OPTIONS.DATE_NEW, +}; + +reducers[ACTIONS.FILE_LIST_STARTED] = (state) => + Object.assign({}, state, { + isFetchingFileList: true, + }); + +reducers[ACTIONS.FILE_LIST_SUCCEEDED] = (state, action) => { + const { fileInfos } = action.data; + const newByOutpoint = Object.assign({}, state.byOutpoint); + const pendingByOutpoint = Object.assign({}, state.pendingByOutpoint); + + fileInfos.forEach((fileInfo) => { + const { outpoint } = fileInfo; + + if (outpoint) newByOutpoint[fileInfo.outpoint] = fileInfo; + }); + + return Object.assign({}, state, { + isFetchingFileList: false, + byOutpoint: newByOutpoint, + pendingByOutpoint, + }); +}; + +reducers[ACTIONS.FETCH_FILE_INFO_STARTED] = (state, action) => { + const { outpoint } = action.data; + const newFetching = Object.assign({}, state.fetching); + + newFetching[outpoint] = true; + + return Object.assign({}, state, { + fetching: newFetching, + }); +}; + +reducers[ACTIONS.FETCH_FILE_INFO_COMPLETED] = (state, action) => { + const { fileInfo, outpoint } = action.data; + + const newByOutpoint = Object.assign({}, state.byOutpoint); + const newFetching = Object.assign({}, state.fetching); + + newByOutpoint[outpoint] = fileInfo; + delete newFetching[outpoint]; + + return Object.assign({}, state, { + byOutpoint: newByOutpoint, + fetching: newFetching, + }); +}; + +reducers[ACTIONS.FETCH_FILE_INFO_FAILED] = (state, action) => { + const { outpoint } = action.data; + const newFetching = Object.assign({}, state.fetching); + delete newFetching[outpoint]; + + return Object.assign({}, state, { + fetching: newFetching, + }); +}; + +reducers[ACTIONS.DOWNLOADING_STARTED] = (state, action) => { + const { outpoint, fileInfo } = action.data; + + const newByOutpoint = Object.assign({}, state.byOutpoint); + const newDownloading = Object.assign({}, state.downloadingByOutpoint); + + newDownloading[outpoint] = true; + newByOutpoint[outpoint] = fileInfo; + + return Object.assign({}, state, { + downloadingByOutpoint: newDownloading, + byOutpoint: newByOutpoint, + }); +}; + +reducers[ACTIONS.DOWNLOADING_PROGRESSED] = (state, action) => { + const { outpoint, fileInfo } = action.data; + + const newByOutpoint = Object.assign({}, state.byOutpoint); + const newDownloading = Object.assign({}, state.downloadingByOutpoint); + + newByOutpoint[outpoint] = fileInfo; + newDownloading[outpoint] = true; + + return Object.assign({}, state, { + byOutpoint: newByOutpoint, + downloadingByOutpoint: newDownloading, + }); +}; + +reducers[ACTIONS.DOWNLOADING_CANCELED] = (state, action) => { + const { outpoint } = action.data; + + const newDownloading = Object.assign({}, state.downloadingByOutpoint); + delete newDownloading[outpoint]; + + return Object.assign({}, state, { + downloadingByOutpoint: newDownloading, + }); +}; + +reducers[ACTIONS.DOWNLOADING_COMPLETED] = (state, action) => { + const { outpoint, fileInfo } = action.data; + + const newByOutpoint = Object.assign({}, state.byOutpoint); + const newDownloading = Object.assign({}, state.downloadingByOutpoint); + + newByOutpoint[outpoint] = fileInfo; + delete newDownloading[outpoint]; + + return Object.assign({}, state, { + byOutpoint: newByOutpoint, + downloadingByOutpoint: newDownloading, + }); +}; + +reducers[ACTIONS.FILE_DELETE] = (state, action) => { + const { outpoint } = action.data; + + const newByOutpoint = Object.assign({}, state.byOutpoint); + const downloadingByOutpoint = Object.assign({}, state.downloadingByOutpoint); + + delete newByOutpoint[outpoint]; + delete downloadingByOutpoint[outpoint]; + + return Object.assign({}, state, { + byOutpoint: newByOutpoint, + downloadingByOutpoint, + }); +}; + +reducers[ACTIONS.SET_FILE_LIST_SORT] = (state, action) => { + const pageSortStates = { + [PAGES.PUBLISHED]: 'fileListPublishedSort', + [PAGES.DOWNLOADED]: 'fileListDownloadedSort', + }; + const pageSortState = pageSortStates[action.data.page]; + const { value } = action.data; + + return Object.assign({}, state, { + [pageSortState]: value, + }); +}; + +export function fileInfoReducer(state = defaultState, action) { + const handler = reducers[action.type]; + if (handler) return handler(state, action); + return state; +} diff --git a/ui/redux/reducers/publish.js b/ui/redux/reducers/publish.js new file mode 100644 index 000000000..4b0b27149 --- /dev/null +++ b/ui/redux/reducers/publish.js @@ -0,0 +1,134 @@ +// @flow +import { handleActions } from 'util/redux-utils'; +import { buildURI } from 'util/lbryURI'; +import * as ACTIONS from 'constants/action_types'; +import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses'; +import { CHANNEL_ANONYMOUS } from 'constants/claim'; + +type PublishState = { + editingURI: ?string, + fileText: ?string, + filePath: ?string, + remoteFileUrl: ?string, + contentIsFree: boolean, + fileDur: number, + fileSize: number, + fileVid: boolean, + fee: { + amount: number, + currency: string, + }, + title: string, + thumbnail_url: string, + thumbnailPath: string, + uploadThumbnailStatus: string, + thumbnailError: ?boolean, + description: string, + language: string, + releaseTime: ?number, + releaseTimeEdited: ?number, + channel: string, + channelId: ?string, + name: string, + nameError: ?string, + bid: number, + bidError: ?string, + otherLicenseDescription: string, + licenseUrl: string, + tags: Array, + optimize: boolean, + useLBRYUploader: boolean, +}; + +const defaultState: PublishState = { + editingURI: undefined, + fileText: '', + filePath: undefined, + fileDur: 0, + fileSize: 0, + fileVid: false, + remoteFileUrl: undefined, + contentIsFree: true, + fee: { + amount: 1, + currency: 'LBC', + }, + title: '', + thumbnail_url: '', + thumbnailPath: '', + uploadThumbnailStatus: THUMBNAIL_STATUSES.API_DOWN, + thumbnailError: undefined, + description: '', + language: '', + releaseTime: undefined, + releaseTimeEdited: undefined, + nsfw: false, + channel: CHANNEL_ANONYMOUS, + channelId: '', + name: '', + nameError: undefined, + bid: 0.01, + bidError: undefined, + licenseType: 'None', + otherLicenseDescription: 'All rights reserved', + licenseUrl: '', + tags: [], + publishing: false, + publishSuccess: false, + publishError: undefined, + optimize: false, + useLBRYUploader: false, +}; + +export const publishReducer = handleActions( + { + [ACTIONS.UPDATE_PUBLISH_FORM]: (state, action): PublishState => { + const { data } = action; + return { + ...state, + ...data, + }; + }, + [ACTIONS.CLEAR_PUBLISH]: (state: PublishState): PublishState => ({ + ...defaultState, + uri: undefined, + channel: state.channel, + bid: state.bid, + optimize: state.optimize, + language: state.language, + }), + [ACTIONS.PUBLISH_START]: (state: PublishState): PublishState => ({ + ...state, + publishing: true, + publishSuccess: false, + }), + [ACTIONS.PUBLISH_FAIL]: (state: PublishState): PublishState => ({ + ...state, + publishing: false, + }), + [ACTIONS.PUBLISH_SUCCESS]: (state: PublishState): PublishState => ({ + ...state, + publishing: false, + publishSuccess: true, + }), + [ACTIONS.DO_PREPARE_EDIT]: (state: PublishState, action) => { + const { ...publishData } = action.data; + const { channel, name, uri } = publishData; + + // The short uri is what is presented to the user + // The editingUri is the full uri with claim id + const shortUri = buildURI({ + channelName: channel, + streamName: name, + }); + + return { + ...defaultState, + ...publishData, + editingURI: uri, + uri: shortUri, + }; + }, + }, + defaultState +); diff --git a/ui/redux/reducers/rewards.js b/ui/redux/reducers/rewards.js index 4f1b7bfdd..8bd8833d5 100644 --- a/ui/redux/reducers/rewards.js +++ b/ui/redux/reducers/rewards.js @@ -1,5 +1,4 @@ -import { ACTIONS } from 'lbry-redux'; - +import * as ACTIONS from 'constants/action_types'; const reducers = {}; const defaultState = { fetching: false, @@ -10,7 +9,7 @@ const defaultState = { rewardedContentClaimIds: [], }; -reducers[ACTIONS.FETCH_REWARDS_STARTED] = state => +reducers[ACTIONS.FETCH_REWARDS_STARTED] = (state) => Object.assign({}, state, { fetching: true, }); @@ -20,7 +19,7 @@ reducers[ACTIONS.FETCH_REWARDS_COMPLETED] = (state, action) => { const unclaimedRewards = []; const claimedRewards = {}; - userRewards.forEach(reward => { + userRewards.forEach((reward) => { if (reward.transaction_id) { claimedRewards[reward.id] = reward; } else { @@ -70,7 +69,7 @@ reducers[ACTIONS.CLAIM_REWARD_SUCCESS] = (state, action) => { const { reward } = action.data; const { unclaimedRewards } = state; - const index = unclaimedRewards.findIndex(ur => ur.claim_code === reward.claim_code); + const index = unclaimedRewards.findIndex((ur) => ur.claim_code === reward.claim_code); unclaimedRewards.splice(index, 1); const { claimedRewardsById } = state; diff --git a/ui/redux/reducers/settings.js b/ui/redux/reducers/settings.js index 1752e29d3..bc02e4bd6 100644 --- a/ui/redux/reducers/settings.js +++ b/ui/redux/reducers/settings.js @@ -1,6 +1,7 @@ import * as ACTIONS from 'constants/action_types'; +import * as SETTINGS from 'constants/settings'; +import * as SHARED_PREFERENCES from 'constants/shared_preferences'; import moment from 'moment'; -import { ACTIONS as LBRY_REDUX_ACTIONS, SETTINGS, SHARED_PREFERENCES } from 'lbry-redux'; import { getSubsetFromKeysArray } from 'util/sync-settings'; import { getDefaultLanguage } from 'util/default-languages'; import { UNSYNCED_SETTINGS, SIMPLE_SITE } from 'config'; @@ -102,12 +103,12 @@ reducers[ACTIONS.FINDING_FFMPEG_COMPLETED] = (state) => findingFFmpeg: false, }); -reducers[LBRY_REDUX_ACTIONS.DAEMON_SETTINGS_RECEIVED] = (state, action) => +reducers[ACTIONS.DAEMON_SETTINGS_RECEIVED] = (state, action) => Object.assign({}, state, { daemonSettings: action.data.settings, }); -reducers[LBRY_REDUX_ACTIONS.DAEMON_STATUS_RECEIVED] = (state, action) => +reducers[ACTIONS.DAEMON_STATUS_RECEIVED] = (state, action) => Object.assign({}, state, { daemonStatus: action.data.status, }); @@ -148,7 +149,7 @@ reducers[ACTIONS.DOWNLOAD_LANGUAGE_SUCCESS] = (state, action) => { } }; -reducers[LBRY_REDUX_ACTIONS.SHARED_PREFERENCE_SET] = (state, action) => { +reducers[ACTIONS.SHARED_PREFERENCE_SET] = (state, action) => { const { key, value } = action.data; const sharedPreferences = Object.assign({}, state.sharedPreferences); sharedPreferences[key] = value; @@ -166,7 +167,7 @@ reducers[ACTIONS.SYNC_CLIENT_SETTINGS] = (state) => { return Object.assign({}, state, { sharedPreferences: newSharedPreferences }); }; -reducers[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE] = (state, action) => { +reducers[ACTIONS.USER_STATE_POPULATE] = (state, action) => { const { clientSettings: currentClientSettings } = state; const { settings: sharedPreferences } = action.data; const selectedSettings = sharedPreferences ? getSubsetFromKeysArray(sharedPreferences, clientSyncKeys) : {}; @@ -185,7 +186,7 @@ reducers[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE] = (state, action) => { }); }; -reducers[LBRY_REDUX_ACTIONS.SAVE_CUSTOM_WALLET_SERVERS] = (state, action) => { +reducers[ACTIONS.SAVE_CUSTOM_WALLET_SERVERS] = (state, action) => { return Object.assign({}, state, { customWalletServers: action.data }); }; diff --git a/ui/redux/reducers/subscriptions.js b/ui/redux/reducers/subscriptions.js index 1c8d37a4c..4db1e5113 100644 --- a/ui/redux/reducers/subscriptions.js +++ b/ui/redux/reducers/subscriptions.js @@ -1,6 +1,6 @@ // @flow import * as ACTIONS from 'constants/action_types'; -import { parseURI, normalizeURI, isURIEqual, ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux'; +import { parseURI, normalizeURI, isURIEqual } from 'util/lbryURI'; import { handleActions } from 'util/redux-utils'; const defaultState: SubscriptionState = { @@ -80,7 +80,7 @@ export default handleActions( ...state, viewMode: action.data, }), - [LBRY_REDUX_ACTIONS.USER_STATE_POPULATE]: ( + [ACTIONS.USER_STATE_POPULATE]: ( state: SubscriptionState, action: { data: { subscriptions: ?Array, following: ?Array } } ) => { diff --git a/ui/redux/reducers/sync.js b/ui/redux/reducers/sync.js index 11c488548..a53817f0d 100644 --- a/ui/redux/reducers/sync.js +++ b/ui/redux/reducers/sync.js @@ -1,5 +1,4 @@ import * as ACTIONS from 'constants/action_types'; -import { ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux'; const reducers = {}; const defaultState = { @@ -19,7 +18,7 @@ const defaultState = { fatalError: false, }; -reducers[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE] = state => { +reducers[ACTIONS.USER_STATE_POPULATE] = (state) => { const { syncReady } = state; if (!syncReady) { return Object.assign({}, state, { @@ -32,7 +31,7 @@ reducers[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE] = state => { reducers[ACTIONS.SET_PREFS_READY] = (state, action) => Object.assign({}, state, { prefsReady: action.data }); -reducers[ACTIONS.GET_SYNC_STARTED] = state => +reducers[ACTIONS.GET_SYNC_STARTED] = (state) => Object.assign({}, state, { getSyncIsPending: true, getSyncErrorMessage: null, @@ -59,7 +58,7 @@ reducers[ACTIONS.GET_SYNC_FAILED] = (state, action) => getSyncErrorMessage: action.data.error, }); -reducers[ACTIONS.SET_SYNC_STARTED] = state => +reducers[ACTIONS.SET_SYNC_STARTED] = (state) => Object.assign({}, state, { setSyncIsPending: true, setSyncErrorMessage: null, @@ -79,14 +78,14 @@ reducers[ACTIONS.SET_SYNC_COMPLETED] = (state, action) => syncHash: action.data.syncHash, }); -reducers[ACTIONS.SYNC_APPLY_STARTED] = state => +reducers[ACTIONS.SYNC_APPLY_STARTED] = (state) => Object.assign({}, state, { syncApplyPasswordError: false, syncApplyIsPending: true, syncApplyErrorMessage: '', }); -reducers[ACTIONS.SYNC_APPLY_COMPLETED] = state => +reducers[ACTIONS.SYNC_APPLY_COMPLETED] = (state) => Object.assign({}, state, { syncApplyIsPending: false, syncApplyErrorMessage: '', @@ -98,12 +97,12 @@ reducers[ACTIONS.SYNC_APPLY_FAILED] = (state, action) => syncApplyErrorMessage: action.data.error, }); -reducers[ACTIONS.SYNC_APPLY_BAD_PASSWORD] = state => +reducers[ACTIONS.SYNC_APPLY_BAD_PASSWORD] = (state) => Object.assign({}, state, { syncApplyPasswordError: true, }); -reducers[LBRY_REDUX_ACTIONS.SYNC_FATAL_ERROR] = (state, action) => { +reducers[ACTIONS.SYNC_FATAL_ERROR] = (state) => { return Object.assign({}, state, { fatalError: true, }); diff --git a/ui/redux/reducers/tags.js b/ui/redux/reducers/tags.js index 646ecac1a..4edd4fbf7 100644 --- a/ui/redux/reducers/tags.js +++ b/ui/redux/reducers/tags.js @@ -1,6 +1,6 @@ // @flow import * as ACTIONS from 'constants/action_types'; -import { ACTIONS as LBRY_REDUX_ACTIONS, DEFAULT_KNOWN_TAGS, DEFAULT_FOLLOWED_TAGS } from 'lbry-redux'; +import { DEFAULT_KNOWN_TAGS, DEFAULT_FOLLOWED_TAGS } from 'constants/tags'; import { handleActions } from 'util/redux-utils'; function getDefaultKnownTags() { @@ -62,7 +62,7 @@ export default handleActions( followedTags: newFollowedTags, }; }, - [LBRY_REDUX_ACTIONS.USER_STATE_POPULATE]: (state: TagState, action: { data: { tags: ?Array } }) => { + [ACTIONS.USER_STATE_POPULATE]: (state: TagState, action: { data: { tags: ?Array } }) => { const { tags } = action.data; if (Array.isArray(tags)) { return { diff --git a/ui/redux/reducers/wallet.js b/ui/redux/reducers/wallet.js new file mode 100644 index 000000000..873792ac1 --- /dev/null +++ b/ui/redux/reducers/wallet.js @@ -0,0 +1,549 @@ +// @flow +import * as ACTIONS from 'constants/action_types'; +import { handleActions } from 'util/redux-utils'; + +const buildDraftTransaction = () => ({ + amount: undefined, + address: undefined, +}); + +// TODO: Split into common success and failure types +// See details in https://github.com/lbryio/lbry/issues/1307 +type ActionResult = { + type: any, + result: any, +}; + +type WalletState = { + balance: any, + totalBalance: any, + reservedBalance: any, + claimsBalance: any, + supportsBalance: any, + tipsBalance: any, + latestBlock: ?number, + transactions: { [string]: Transaction }, + supports: { [string]: Support }, + abandoningSupportsByOutpoint: { [string]: boolean }, + fetchingTransactions: boolean, + fetchingTransactionsError: string, + gettingNewAddress: boolean, + draftTransaction: any, + sendingSupport: boolean, + walletIsEncrypted: boolean, + walletEncryptPending: boolean, + walletEncryptSucceded: ?boolean, + walletEncryptResult: ?boolean, + walletDecryptPending: boolean, + walletDecryptSucceded: ?boolean, + walletDecryptResult: ?boolean, + walletUnlockPending: boolean, + walletUnlockSucceded: ?boolean, + walletUnlockResult: ?boolean, + walletLockPending: boolean, + walletLockSucceded: ?boolean, + walletLockResult: ?boolean, + walletReconnecting: boolean, + txoFetchParams: {}, + utxoCounts: {}, + txoPage: any, + fetchId: string, + fetchingTxos: boolean, + fetchingTxosError?: string, + consolidatingUtxos: boolean, + pendingConsolidateTxid?: string, + massClaimingTips: boolean, + pendingMassClaimTxid?: string, + pendingSupportTransactions: {}, // { claimId: {txid: 123, amount 12.3}, } + pendingTxos: Array, + abandonClaimSupportError?: string, +}; + +const defaultState = { + balance: undefined, + totalBalance: undefined, + reservedBalance: undefined, + claimsBalance: undefined, + supportsBalance: undefined, + tipsBalance: undefined, + latestBlock: undefined, + transactions: {}, + fetchingTransactions: false, + fetchingTransactionsError: undefined, + supports: {}, + fetchingSupports: false, + abandoningSupportsByOutpoint: {}, + gettingNewAddress: false, + draftTransaction: buildDraftTransaction(), + sendingSupport: false, + walletIsEncrypted: false, + walletEncryptPending: false, + walletEncryptSucceded: null, + walletEncryptResult: null, + walletDecryptPending: false, + walletDecryptSucceded: null, + walletDecryptResult: null, + walletUnlockPending: false, + walletUnlockSucceded: null, + walletUnlockResult: null, + walletLockPending: false, + walletLockSucceded: null, + walletLockResult: null, + transactionListFilter: 'all', + walletReconnecting: false, + txoFetchParams: {}, + utxoCounts: {}, + fetchingUtxoCounts: false, + fetchingUtxoError: undefined, + consolidatingUtxos: false, + pendingConsolidateTxid: null, + massClaimingTips: false, + pendingMassClaimTxid: null, + txoPage: {}, + fetchId: '', + fetchingTxos: false, + fetchingTxosError: undefined, + pendingSupportTransactions: {}, + pendingTxos: [], + + abandonClaimSupportError: undefined, +}; + +export const walletReducer = handleActions( + { + [ACTIONS.FETCH_TRANSACTIONS_STARTED]: (state: WalletState) => ({ + ...state, + fetchingTransactions: true, + }), + + [ACTIONS.FETCH_TRANSACTIONS_COMPLETED]: (state: WalletState, action) => { + const byId = { ...state.transactions }; + + const { transactions } = action.data; + transactions.forEach((transaction) => { + byId[transaction.txid] = transaction; + }); + + return { + ...state, + transactions: byId, + fetchingTransactions: false, + }; + }, + + [ACTIONS.FETCH_TXO_PAGE_STARTED]: (state: WalletState, action) => { + return { + ...state, + fetchId: action.data, + fetchingTxos: true, + fetchingTxosError: undefined, + }; + }, + + [ACTIONS.FETCH_TXO_PAGE_COMPLETED]: (state: WalletState, action) => { + if (state.fetchId !== action.data.fetchId) { + // Leave 'state' and 'fetchingTxos' alone. The latter would ensure + // the spiner would continue spinning for the latest transaction. + return { ...state }; + } + + return { + ...state, + txoPage: action.data.result, + fetchId: '', + fetchingTxos: false, + }; + }, + + [ACTIONS.FETCH_TXO_PAGE_FAILED]: (state: WalletState, action) => { + return { + ...state, + txoPage: {}, + fetchId: '', + fetchingTxos: false, + fetchingTxosError: action.data, + }; + }, + [ACTIONS.FETCH_UTXO_COUNT_STARTED]: (state: WalletState) => { + return { + ...state, + fetchingUtxoCounts: true, + fetchingUtxoError: undefined, + }; + }, + + [ACTIONS.FETCH_UTXO_COUNT_COMPLETED]: (state: WalletState, action) => { + return { + ...state, + utxoCounts: action.data, + fetchingUtxoCounts: false, + }; + }, + [ACTIONS.FETCH_UTXO_COUNT_FAILED]: (state: WalletState, action) => { + return { + ...state, + utxoCounts: {}, + fetchingUtxoCounts: false, + fetchingUtxoError: action.data, + }; + }, + [ACTIONS.DO_UTXO_CONSOLIDATE_STARTED]: (state: WalletState) => { + return { + ...state, + consolidatingUtxos: true, + }; + }, + + [ACTIONS.DO_UTXO_CONSOLIDATE_COMPLETED]: (state: WalletState, action) => { + const { txid } = action.data; + return { + ...state, + consolidatingUtxos: false, + pendingConsolidateTxid: txid, + }; + }, + + [ACTIONS.DO_UTXO_CONSOLIDATE_FAILED]: (state: WalletState, action) => { + return { + ...state, + consolidatingUtxos: false, + }; + }, + + [ACTIONS.TIP_CLAIM_MASS_STARTED]: (state: WalletState) => { + return { + ...state, + massClaimingTips: true, + }; + }, + + [ACTIONS.TIP_CLAIM_MASS_COMPLETED]: (state: WalletState, action) => { + const { txid } = action.data; + return { + ...state, + massClaimingTips: false, + pendingMassClaimTxid: txid, + }; + }, + + [ACTIONS.TIP_CLAIM_MASS_FAILED]: (state: WalletState, action) => { + return { + ...state, + massClaimingTips: false, + }; + }, + + [ACTIONS.PENDING_CONSOLIDATED_TXOS_UPDATED]: (state: WalletState, action) => { + const { pendingTxos, pendingMassClaimTxid, pendingConsolidateTxid } = state; + + const { txids, remove } = action.data; + + if (remove) { + const newTxos = pendingTxos.filter((txo) => !txids.includes(txo)); + const newPendingMassClaimTxid = txids.includes(pendingMassClaimTxid) ? undefined : pendingMassClaimTxid; + const newPendingConsolidateTxid = txids.includes(pendingConsolidateTxid) ? undefined : pendingConsolidateTxid; + return { + ...state, + pendingTxos: newTxos, + pendingMassClaimTxid: newPendingMassClaimTxid, + pendingConsolidateTxid: newPendingConsolidateTxid, + }; + } else { + const newPendingSet = new Set([...pendingTxos, ...txids]); + return { ...state, pendingTxos: Array.from(newPendingSet) }; + } + }, + + [ACTIONS.UPDATE_TXO_FETCH_PARAMS]: (state: WalletState, action) => { + return { + ...state, + txoFetchParams: action.data, + }; + }, + + [ACTIONS.FETCH_SUPPORTS_STARTED]: (state: WalletState) => ({ + ...state, + fetchingSupports: true, + }), + + [ACTIONS.FETCH_SUPPORTS_COMPLETED]: (state: WalletState, action) => { + const byOutpoint = state.supports; + const { supports } = action.data; + + supports.forEach((transaction) => { + const { txid, nout } = transaction; + byOutpoint[`${txid}:${nout}`] = transaction; + }); + + return { ...state, supports: byOutpoint, fetchingSupports: false }; + }, + + [ACTIONS.ABANDON_SUPPORT_STARTED]: (state: WalletState, action: any): WalletState => { + const { outpoint }: { outpoint: string } = action.data; + const currentlyAbandoning = state.abandoningSupportsByOutpoint; + + currentlyAbandoning[outpoint] = true; + + return { + ...state, + abandoningSupportsByOutpoint: currentlyAbandoning, + }; + }, + + [ACTIONS.ABANDON_SUPPORT_COMPLETED]: (state: WalletState, action: any): WalletState => { + const { outpoint }: { outpoint: string } = action.data; + const byOutpoint = state.supports; + const currentlyAbandoning = state.abandoningSupportsByOutpoint; + + delete currentlyAbandoning[outpoint]; + delete byOutpoint[outpoint]; + + return { + ...state, + supports: byOutpoint, + abandoningSupportsByOutpoint: currentlyAbandoning, + }; + }, + + [ACTIONS.ABANDON_CLAIM_SUPPORT_STARTED]: (state: WalletState, action: any): WalletState => { + return { + ...state, + abandonClaimSupportError: undefined, + }; + }, + + [ACTIONS.ABANDON_CLAIM_SUPPORT_PREVIEW]: (state: WalletState, action: any): WalletState => { + return { + ...state, + abandonClaimSupportError: undefined, + }; + }, + + [ACTIONS.ABANDON_CLAIM_SUPPORT_COMPLETED]: (state: WalletState, action: any): WalletState => { + const { + claimId, + type, + txid, + effective, + }: { claimId: string, type: string, txid: string, effective: string } = action.data; + const pendingtxs = Object.assign({}, state.pendingSupportTransactions); + + pendingtxs[claimId] = { txid, type, effective }; + + return { + ...state, + pendingSupportTransactions: pendingtxs, + abandonClaimSupportError: undefined, + }; + }, + + [ACTIONS.ABANDON_CLAIM_SUPPORT_FAILED]: (state: WalletState, action: any): WalletState => { + return { + ...state, + abandonClaimSupportError: action.data, + }; + }, + + [ACTIONS.PENDING_SUPPORTS_UPDATED]: (state: WalletState, action: any): WalletState => { + return { + ...state, + pendingSupportTransactions: action.data, + }; + }, + + [ACTIONS.GET_NEW_ADDRESS_STARTED]: (state: WalletState) => ({ + ...state, + gettingNewAddress: true, + }), + + [ACTIONS.GET_NEW_ADDRESS_COMPLETED]: (state: WalletState, action) => { + const { address } = action.data; + + return { ...state, gettingNewAddress: false, receiveAddress: address }; + }, + + [ACTIONS.UPDATE_BALANCE]: (state: WalletState, action) => ({ + ...state, + totalBalance: action.data.totalBalance, + balance: action.data.balance, + reservedBalance: action.data.reservedBalance, + claimsBalance: action.data.claimsBalance, + supportsBalance: action.data.supportsBalance, + tipsBalance: action.data.tipsBalance, + }), + + [ACTIONS.CHECK_ADDRESS_IS_MINE_STARTED]: (state: WalletState) => ({ + ...state, + checkingAddressOwnership: true, + }), + + [ACTIONS.CHECK_ADDRESS_IS_MINE_COMPLETED]: (state: WalletState) => ({ + ...state, + checkingAddressOwnership: false, + }), + + [ACTIONS.SET_DRAFT_TRANSACTION_AMOUNT]: (state: WalletState, action) => { + const oldDraft = state.draftTransaction; + const newDraft = { ...oldDraft, amount: parseFloat(action.data.amount) }; + + return { ...state, draftTransaction: newDraft }; + }, + + [ACTIONS.SET_DRAFT_TRANSACTION_ADDRESS]: (state: WalletState, action) => { + const oldDraft = state.draftTransaction; + const newDraft = { ...oldDraft, address: action.data.address }; + + return { ...state, draftTransaction: newDraft }; + }, + + [ACTIONS.SEND_TRANSACTION_STARTED]: (state: WalletState) => { + const newDraftTransaction = { ...state.draftTransaction, sending: true }; + + return { ...state, draftTransaction: newDraftTransaction }; + }, + + [ACTIONS.SEND_TRANSACTION_COMPLETED]: (state: WalletState) => + Object.assign({}, state, { + draftTransaction: buildDraftTransaction(), + }), + + [ACTIONS.SEND_TRANSACTION_FAILED]: (state: WalletState, action) => { + const newDraftTransaction = Object.assign({}, state.draftTransaction, { + sending: false, + error: action.data.error, + }); + + return { ...state, draftTransaction: newDraftTransaction }; + }, + + [ACTIONS.SUPPORT_TRANSACTION_STARTED]: (state: WalletState) => ({ + ...state, + sendingSupport: true, + }), + + [ACTIONS.SUPPORT_TRANSACTION_COMPLETED]: (state: WalletState) => ({ + ...state, + sendingSupport: false, + }), + + [ACTIONS.SUPPORT_TRANSACTION_FAILED]: (state: WalletState, action) => ({ + ...state, + error: action.data.error, + sendingSupport: false, + }), + + [ACTIONS.CLEAR_SUPPORT_TRANSACTION]: (state: WalletState) => ({ + ...state, + sendingSupport: false, + }), + + [ACTIONS.WALLET_STATUS_COMPLETED]: (state: WalletState, action) => ({ + ...state, + walletIsEncrypted: action.result, + }), + + [ACTIONS.WALLET_ENCRYPT_START]: (state: WalletState) => ({ + ...state, + walletEncryptPending: true, + walletEncryptSucceded: null, + walletEncryptResult: null, + }), + + [ACTIONS.WALLET_ENCRYPT_COMPLETED]: (state: WalletState, action: ActionResult) => ({ + ...state, + walletEncryptPending: false, + walletEncryptSucceded: true, + walletEncryptResult: action.result, + }), + + [ACTIONS.WALLET_ENCRYPT_FAILED]: (state: WalletState, action: ActionResult) => ({ + ...state, + walletEncryptPending: false, + walletEncryptSucceded: false, + walletEncryptResult: action.result, + }), + + [ACTIONS.WALLET_DECRYPT_START]: (state: WalletState) => ({ + ...state, + walletDecryptPending: true, + walletDecryptSucceded: null, + walletDecryptResult: null, + }), + + [ACTIONS.WALLET_DECRYPT_COMPLETED]: (state: WalletState, action: ActionResult) => ({ + ...state, + walletDecryptPending: false, + walletDecryptSucceded: true, + walletDecryptResult: action.result, + }), + + [ACTIONS.WALLET_DECRYPT_FAILED]: (state: WalletState, action: ActionResult) => ({ + ...state, + walletDecryptPending: false, + walletDecryptSucceded: false, + walletDecryptResult: action.result, + }), + + [ACTIONS.WALLET_UNLOCK_START]: (state: WalletState) => ({ + ...state, + walletUnlockPending: true, + walletUnlockSucceded: null, + walletUnlockResult: null, + }), + + [ACTIONS.WALLET_UNLOCK_COMPLETED]: (state: WalletState, action: ActionResult) => ({ + ...state, + walletUnlockPending: false, + walletUnlockSucceded: true, + walletUnlockResult: action.result, + }), + + [ACTIONS.WALLET_UNLOCK_FAILED]: (state: WalletState, action: ActionResult) => ({ + ...state, + walletUnlockPending: false, + walletUnlockSucceded: false, + walletUnlockResult: action.result, + }), + + [ACTIONS.WALLET_LOCK_START]: (state: WalletState) => ({ + ...state, + walletLockPending: false, + walletLockSucceded: null, + walletLockResult: null, + }), + + [ACTIONS.WALLET_LOCK_COMPLETED]: (state: WalletState, action: ActionResult) => ({ + ...state, + walletLockPending: false, + walletLockSucceded: true, + walletLockResult: action.result, + }), + + [ACTIONS.WALLET_LOCK_FAILED]: (state: WalletState, action: ActionResult) => ({ + ...state, + walletLockPending: false, + walletLockSucceded: false, + walletLockResult: action.result, + }), + + [ACTIONS.SET_TRANSACTION_LIST_FILTER]: (state: WalletState, action: { data: string }) => ({ + ...state, + transactionListFilter: action.data, + }), + + [ACTIONS.UPDATE_CURRENT_HEIGHT]: (state: WalletState, action: { data: number }) => ({ + ...state, + latestBlock: action.data, + }), + [ACTIONS.WALLET_RESTART]: (state: WalletState) => ({ + ...state, + walletReconnecting: true, + }), + + [ACTIONS.WALLET_RESTART_COMPLETED]: (state: WalletState) => ({ + ...state, + walletReconnecting: false, + }), + }, + defaultState +); diff --git a/ui/redux/selectors/app.js b/ui/redux/selectors/app.js index e433ba535..daea35ac1 100644 --- a/ui/redux/selectors/app.js +++ b/ui/redux/selectors/app.js @@ -1,5 +1,5 @@ import { createSelector } from 'reselect'; -import { selectClaimsById, selectMyChannelClaims, makeSelectStakedLevelForChannelUri } from 'lbry-redux'; +import { selectClaimsById, selectMyChannelClaims, makeSelectStakedLevelForChannelUri } from 'redux/selectors/claims'; export const selectState = (state) => state.app || {}; diff --git a/ui/redux/selectors/blocked.js b/ui/redux/selectors/blocked.js index 2eca07d52..9977c6d8f 100644 --- a/ui/redux/selectors/blocked.js +++ b/ui/redux/selectors/blocked.js @@ -1,6 +1,6 @@ // @flow import { createSelector } from 'reselect'; -import { splitBySeparator } from 'lbry-redux'; +import { splitBySeparator } from 'util/lbryURI'; const selectState = (state: { blocked: BlocklistState }) => state.blocked || {}; diff --git a/ui/redux/selectors/claims.js b/ui/redux/selectors/claims.js new file mode 100644 index 000000000..4e0f02960 --- /dev/null +++ b/ui/redux/selectors/claims.js @@ -0,0 +1,635 @@ +// @flow +import { normalizeURI, parseURI } from 'util/lbryURI'; +import { selectSupportsByOutpoint } from 'redux/selectors/wallet'; +import { createSelector } from 'reselect'; +import { isClaimNsfw, filterClaims } from 'util/claim'; +import * as CLAIM from 'constants/claim'; + +const selectState = (state) => state.claims || {}; + +export const selectById = createSelector(selectState, (state) => state.byId || {}); + +export const selectPendingClaimsById = createSelector(selectState, (state) => state.pendingById || {}); + +export const selectClaimsById = createSelector(selectById, selectPendingClaimsById, (byId, pendingById) => { + return Object.assign(byId, pendingById); // do I need merged to keep metadata? +}); + +export const selectClaimIdsByUri = createSelector(selectState, (state) => state.claimsByUri || {}); + +export const selectCurrentChannelPage = createSelector(selectState, (state) => state.currentChannelPage || 1); + +export const selectCreatingChannel = createSelector(selectState, (state) => state.creatingChannel); + +export const selectCreateChannelError = createSelector(selectState, (state) => state.createChannelError); + +export const selectRepostLoading = createSelector(selectState, (state) => state.repostLoading); + +export const selectRepostError = createSelector(selectState, (state) => state.repostError); + +export const selectClaimsByUri = createSelector(selectClaimIdsByUri, selectClaimsById, (byUri, byId) => { + const claims = {}; + + Object.keys(byUri).forEach((uri) => { + const claimId = byUri[uri]; + + // NOTE returning a null claim allows us to differentiate between an + // undefined (never fetched claim) and one which just doesn't exist. Not + // the cleanest solution but couldn't think of anything better right now + if (claimId === null) { + claims[uri] = null; + } else { + claims[uri] = byId[claimId]; + } + }); + + return claims; +}); + +export const selectAllClaimsByChannel = createSelector(selectState, (state) => state.paginatedClaimsByChannel || {}); + +export const selectPendingIds = createSelector(selectState, (state) => Object.keys(state.pendingById) || []); + +export const selectPendingClaims = createSelector(selectPendingClaimsById, (pendingById) => Object.values(pendingById)); + +export const makeSelectClaimIsPending = (uri: string) => + createSelector(selectClaimIdsByUri, selectPendingClaimsById, (idsByUri, pendingById) => { + const claimId = idsByUri[normalizeURI(uri)]; + + if (claimId) { + return Boolean(pendingById[claimId]); + } + return false; + }); + +export const makeSelectClaimIdIsPending = (claimId: string) => + createSelector(selectPendingClaimsById, (pendingById) => { + return Boolean(pendingById[claimId]); + }); + +export const makeSelectClaimIdForUri = (uri: string) => + createSelector(selectClaimIdsByUri, (claimIds) => claimIds[uri]); + +export const selectReflectingById = createSelector(selectState, (state) => state.reflectingById); + +export const makeSelectClaimForClaimId = (claimId: string) => createSelector(selectClaimsById, (byId) => byId[claimId]); + +export const makeSelectClaimForUri = (uri: string, returnRepost: boolean = true) => + createSelector(selectClaimIdsByUri, selectClaimsById, (byUri, byId) => { + let validUri; + try { + parseURI(uri); + validUri = true; + } catch (e) {} + + if (validUri && byUri) { + const claimId = uri && byUri[normalizeURI(uri)]; + const claim = byId[claimId]; + + // Make sure to return the claim as is so apps can check if it's been resolved before (null) or still needs to be resolved (undefined) + if (claimId === null) { + return null; + } else if (claimId === undefined) { + return undefined; + } + + const repostedClaim = claim && claim.reposted_claim; + if (repostedClaim && returnRepost) { + const channelUrl = + claim.signing_channel && (claim.signing_channel.canonical_url || claim.signing_channel.permanent_url); + + return { + ...repostedClaim, + repost_url: normalizeURI(uri), + repost_channel_url: channelUrl, + repost_bid_amount: claim && claim.meta && claim.meta.effective_amount, + }; + } else { + return claim; + } + } + }); + +export const selectMyClaimsRaw = createSelector(selectState, selectClaimsById, (state, byId) => { + const ids = state.myClaims; + if (!ids) { + return ids; + } + + const claims = []; + ids.forEach((id) => { + if (byId[id]) { + // I'm not sure why this check is necessary, but it ought to be a quick fix for https://github.com/lbryio/lbry-desktop/issues/544 + claims.push(byId[id]); + } + }); + return claims; +}); + +export const selectAbandoningIds = createSelector(selectState, (state) => Object.keys(state.abandoningById || {})); + +export const makeSelectAbandoningClaimById = (claimId: string) => + createSelector(selectAbandoningIds, (ids) => ids.includes(claimId)); + +export const makeSelectIsAbandoningClaimForUri = (uri: string) => + createSelector(selectClaimIdsByUri, selectAbandoningIds, (claimIdsByUri, abandoningById) => { + const claimId = claimIdsByUri[normalizeURI(uri)]; + return abandoningById.indexOf(claimId) >= 0; + }); + +export const selectMyActiveClaims = createSelector( + selectMyClaimsRaw, + selectAbandoningIds, + (claims, abandoningIds) => + new Set( + claims && + claims.map((claim) => claim.claim_id).filter((claimId) => Object.keys(abandoningIds).indexOf(claimId) === -1) + ) +); + +export const makeSelectClaimIsMine = (rawUri: string) => { + let uri; + try { + uri = normalizeURI(rawUri); + } catch (e) {} + + return createSelector(selectClaimsByUri, selectMyActiveClaims, (claims, myClaims) => { + try { + parseURI(uri); + } catch (e) { + return false; + } + + return ( + claims && + claims[uri] && + (claims[uri].is_my_output || (claims[uri].claim_id && myClaims.has(claims[uri].claim_id))) + ); + }); +}; + +export const selectMyPurchases = createSelector(selectState, (state) => state.myPurchases); + +export const selectPurchaseUriSuccess = createSelector(selectState, (state) => state.purchaseUriSuccess); + +export const selectMyPurchasesCount = createSelector(selectState, (state) => state.myPurchasesPageTotalResults); + +export const selectIsFetchingMyPurchases = createSelector(selectState, (state) => state.fetchingMyPurchases); + +export const selectFetchingMyPurchasesError = createSelector(selectState, (state) => state.fetchingMyPurchasesError); + +export const makeSelectMyPurchasesForPage = (query: ?string, page: number = 1) => + createSelector( + selectMyPurchases, + selectClaimsByUri, + (myPurchases: Array, claimsByUri: { [string]: Claim }) => { + if (!myPurchases) { + return undefined; + } + + if (!query) { + // ensure no duplicates from double purchase bugs + // return [...new Set(myPurchases)]; + return Array.from(new Set(myPurchases)); + } + + const fileInfos = myPurchases.map((uri) => claimsByUri[uri]); + const matchingFileInfos = filterClaims(fileInfos, query); + const start = (Number(page) - 1) * Number(CLAIM.PAGE_SIZE); + const end = Number(page) * Number(CLAIM.PAGE_SIZE); + return matchingFileInfos && matchingFileInfos.length + ? matchingFileInfos.slice(start, end).map((fileInfo) => fileInfo.canonical_url || fileInfo.permanent_url) + : []; + } + ); + +export const makeSelectClaimWasPurchased = (uri: string) => + createSelector(makeSelectClaimForUri(uri), (claim) => { + return claim && claim.purchase_receipt !== undefined; + }); + +export const selectAllFetchingChannelClaims = createSelector(selectState, (state) => state.fetchingChannelClaims || {}); + +export const makeSelectFetchingChannelClaims = (uri: string) => + createSelector(selectAllFetchingChannelClaims, (fetching) => fetching && fetching[uri]); + +export const makeSelectClaimsInChannelForPage = (uri: string, page?: number) => + createSelector(selectClaimsById, selectAllClaimsByChannel, (byId, allClaims) => { + const byChannel = allClaims[uri] || {}; + const claimIds = byChannel[page || 1]; + + if (!claimIds) return claimIds; + + return claimIds.map((claimId) => byId[claimId]); + }); + +// THIS IS LEFT OVER FROM ONE TAB CHANNEL_CONTENT +export const makeSelectTotalClaimsInChannelSearch = (uri: string) => + createSelector(selectClaimsById, selectAllClaimsByChannel, (byId, allClaims) => { + const byChannel = allClaims[uri] || {}; + return byChannel['itemCount']; + }); + +// THIS IS LEFT OVER FROM ONE_TAB CHANNEL CONTENT +export const makeSelectTotalPagesInChannelSearch = (uri: string) => + createSelector(selectClaimsById, selectAllClaimsByChannel, (byId, allClaims) => { + const byChannel = allClaims[uri] || {}; + return byChannel['pageCount']; + }); + +export const makeSelectMetadataForUri = (uri: string) => + createSelector(makeSelectClaimForUri(uri), (claim) => { + const metadata = claim && claim.value; + return metadata || (claim === undefined ? undefined : null); + }); + +export const makeSelectMetadataItemForUri = (uri: string, key: string) => + createSelector(makeSelectMetadataForUri(uri), (metadata: ChannelMetadata | StreamMetadata) => { + return metadata ? metadata[key] : undefined; + }); + +export const makeSelectTitleForUri = (uri: string) => + createSelector(makeSelectMetadataForUri(uri), (metadata) => metadata && metadata.title); + +export const makeSelectDateForUri = (uri: string) => + createSelector(makeSelectClaimForUri(uri), (claim) => { + const timestamp = + claim && + claim.value && + (claim.value.release_time + ? claim.value.release_time * 1000 + : claim.meta && claim.meta.creation_timestamp + ? claim.meta.creation_timestamp * 1000 + : null); + if (!timestamp) { + return undefined; + } + const dateObj = new Date(timestamp); + return dateObj; + }); + +export const makeSelectAmountForUri = (uri: string) => + createSelector(makeSelectClaimForUri(uri), (claim) => { + return claim && claim.amount; + }); + +export const makeSelectEffectiveAmountForUri = (uri: string) => + createSelector(makeSelectClaimForUri(uri, false), (claim) => { + return ( + claim && claim.meta && typeof claim.meta.effective_amount === 'string' && Number(claim.meta.effective_amount) + ); + }); + +export const makeSelectContentTypeForUri = (uri: string) => + createSelector(makeSelectClaimForUri(uri), (claim) => { + const source = claim && claim.value && claim.value.source; + return source ? source.media_type : undefined; + }); + +export const makeSelectThumbnailForUri = (uri: string) => + createSelector(makeSelectClaimForUri(uri), (claim) => { + const thumbnail = claim && claim.value && claim.value.thumbnail; + return thumbnail && thumbnail.url ? thumbnail.url.trim().replace(/^http:\/\//i, 'https://') : undefined; + }); + +export const makeSelectCoverForUri = (uri: string) => + createSelector(makeSelectClaimForUri(uri), (claim) => { + const cover = claim && claim.value && claim.value.cover; + return cover && cover.url ? cover.url.trim().replace(/^http:\/\//i, 'https://') : undefined; + }); + +export const selectIsFetchingClaimListMine = createSelector(selectState, (state) => state.isFetchingClaimListMine); + +export const selectMyClaimsPage = createSelector(selectState, (state) => state.myClaimsPageResults || []); + +export const selectMyClaimsPageNumber = createSelector( + selectState, + (state) => (state.claimListMinePage && state.claimListMinePage.items) || [], + + (state) => (state.txoPage && state.txoPage.page) || 1 +); + +export const selectMyClaimsPageItemCount = createSelector(selectState, (state) => state.myClaimsPageTotalResults || 1); + +export const selectFetchingMyClaimsPageError = createSelector( + selectState, + (state) => state.fetchingClaimListMinePageError +); + +export const selectMyClaims = createSelector( + selectMyActiveClaims, + selectClaimsById, + selectAbandoningIds, + (myClaimIds, byId, abandoningIds) => { + const claims = []; + + myClaimIds.forEach((id) => { + const claim = byId[id]; + + if (claim && abandoningIds.indexOf(id) === -1) claims.push(claim); + }); + + return [...claims]; + } +); + +export const selectMyClaimsWithoutChannels = createSelector(selectMyClaims, (myClaims) => + myClaims.filter((claim) => claim && !claim.name.match(/^@/)).sort((a, b) => a.timestamp - b.timestamp) +); + +export const selectMyClaimUrisWithoutChannels = createSelector(selectMyClaimsWithoutChannels, (myClaims) => { + return myClaims + .sort((a, b) => { + if (a.height < 1) { + return -1; + } else if (b.height < 1) { + return 1; + } else { + return b.timestamp - a.timestamp; + } + }) + .map((claim) => { + return claim.canonical_url || claim.permanent_url; + }); +}); + +export const selectAllMyClaimsByOutpoint = createSelector( + selectMyClaimsRaw, + (claims) => new Set(claims && claims.length ? claims.map((claim) => `${claim.txid}:${claim.nout}`) : null) +); + +export const selectMyClaimsOutpoints = createSelector(selectMyClaims, (myClaims) => { + const outpoints = []; + + myClaims.forEach((claim) => outpoints.push(`${claim.txid}:${claim.nout}`)); + + return outpoints; +}); + +export const selectFetchingMyChannels = createSelector(selectState, (state) => state.fetchingMyChannels); + +export const selectFetchingMyCollections = createSelector(selectState, (state) => state.fetchingMyCollections); + +export const selectMyChannelClaims = createSelector(selectState, selectClaimsById, (state, byId) => { + const ids = state.myChannelClaims; + if (!ids) { + return ids; + } + + const claims = []; + ids.forEach((id) => { + if (byId[id]) { + // I'm not sure why this check is necessary, but it ought to be a quick fix for https://github.com/lbryio/lbry-desktop/issues/544 + claims.push(byId[id]); + } + }); + + return claims; +}); + +export const selectMyChannelUrls = createSelector(selectMyChannelClaims, (claims) => + claims ? claims.map((claim) => claim.canonical_url || claim.permanent_url) : undefined +); + +export const selectMyCollectionIds = createSelector(selectState, (state) => state.myCollectionClaims); + +export const selectResolvingUris = createSelector(selectState, (state) => state.resolvingUris || []); + +export const selectChannelImportPending = createSelector(selectState, (state) => state.pendingChannelImport); + +export const makeSelectIsUriResolving = (uri: string) => + createSelector(selectResolvingUris, (resolvingUris) => resolvingUris && resolvingUris.indexOf(uri) !== -1); + +export const selectPlayingUri = createSelector(selectState, (state) => state.playingUri); + +export const selectChannelClaimCounts = createSelector(selectState, (state) => state.channelClaimCounts || {}); + +export const makeSelectPendingClaimForUri = (uri: string) => + createSelector(selectPendingClaimsById, (pendingById) => { + let uriStreamName; + let uriChannelName; + try { + ({ streamName: uriStreamName, channelName: uriChannelName } = parseURI(uri)); + } catch (e) { + return null; + } + const pendingClaims = (Object.values(pendingById): any); + const matchingClaim = pendingClaims.find((claim: GenericClaim) => { + return claim.normalized_name === uriChannelName || claim.normalized_name === uriStreamName; + }); + return matchingClaim || null; + }); + +export const makeSelectTotalItemsForChannel = (uri: string) => + createSelector(selectChannelClaimCounts, (byUri) => byUri && byUri[normalizeURI(uri)]); + +export const makeSelectTotalPagesForChannel = (uri: string, pageSize: number = 10) => + createSelector( + selectChannelClaimCounts, + (byUri) => byUri && byUri[uri] && Math.ceil(byUri[normalizeURI(uri)] / pageSize) + ); + +export const makeSelectNsfwCountFromUris = (uris: Array) => + createSelector(selectClaimsByUri, (claims) => + uris.reduce((acc, uri) => { + const claim = claims[uri]; + if (claim && isClaimNsfw(claim)) { + return acc + 1; + } + return acc; + }, 0) + ); + +export const makeSelectOmittedCountForChannel = (uri: string) => + createSelector( + makeSelectTotalItemsForChannel(uri), + makeSelectTotalClaimsInChannelSearch(uri), + (claimsInChannel, claimsInSearch) => { + if (claimsInChannel && typeof claimsInSearch === 'number' && claimsInSearch >= 0) { + return claimsInChannel - claimsInSearch; + } else return 0; + } + ); + +export const makeSelectClaimIsNsfw = (uri: string) => + createSelector( + makeSelectClaimForUri(uri), + // Eventually these will come from some list of tags that are considered adult + // Or possibly come from users settings of what tags they want to hide + // For now, there is just a hard coded list of tags inside `isClaimNsfw` + // selectNaughtyTags(), + (claim: Claim) => { + if (!claim) { + return false; + } + + return isClaimNsfw(claim); + } + ); + +// Returns the associated channel uri for a given claim uri +// accepts a regular claim uri lbry://something +// returns the channel uri that created this claim lbry://@channel +export const makeSelectChannelForClaimUri = (uri: string, includePrefix: boolean = false) => + createSelector(makeSelectClaimForUri(uri), (claim: ?Claim) => { + if (!claim || !claim.signing_channel || !claim.is_channel_signature_valid) { + return null; + } + + const { canonical_url: canonicalUrl, permanent_url: permanentUrl } = claim.signing_channel; + + if (canonicalUrl) { + return includePrefix ? canonicalUrl : canonicalUrl.slice('lbry://'.length); + } else { + return includePrefix ? permanentUrl : permanentUrl.slice('lbry://'.length); + } + }); + +export const makeSelectChannelPermUrlForClaimUri = (uri: string, includePrefix: boolean = false) => + createSelector(makeSelectClaimForUri(uri), (claim: ?Claim) => { + if (claim && claim.value_type === 'channel') { + return claim.permanent_url; + } + if (!claim || !claim.signing_channel || !claim.is_channel_signature_valid) { + return null; + } + return claim.signing_channel.permanent_url; + }); + +export const makeSelectMyChannelPermUrlForName = (name: string) => + createSelector(selectMyChannelClaims, (claims) => { + const matchingClaim = claims && claims.find((claim) => claim.name === name); + return matchingClaim ? matchingClaim.permanent_url : null; + }); + +export const makeSelectTagsForUri = (uri: string) => + createSelector(makeSelectMetadataForUri(uri), (metadata: ?GenericMetadata) => { + return (metadata && metadata.tags) || []; + }); + +export const selectFetchingClaimSearchByQuery = createSelector( + selectState, + (state) => state.fetchingClaimSearchByQuery || {} +); + +export const selectFetchingClaimSearch = createSelector( + selectFetchingClaimSearchByQuery, + (fetchingClaimSearchByQuery) => Boolean(Object.keys(fetchingClaimSearchByQuery).length) +); + +export const selectClaimSearchByQuery = createSelector(selectState, (state) => state.claimSearchByQuery || {}); + +export const selectClaimSearchByQueryLastPageReached = createSelector( + selectState, + (state) => state.claimSearchByQueryLastPageReached || {} +); + +export const makeSelectShortUrlForUri = (uri: string) => + createSelector(makeSelectClaimForUri(uri), (claim) => claim && claim.short_url); + +export const makeSelectCanonicalUrlForUri = (uri: string) => + createSelector(makeSelectClaimForUri(uri), (claim) => claim && claim.canonical_url); + +export const makeSelectPermanentUrlForUri = (uri: string) => + createSelector(makeSelectClaimForUri(uri), (claim) => claim && claim.permanent_url); + +export const makeSelectSupportsForUri = (uri: string) => + createSelector(selectSupportsByOutpoint, makeSelectClaimForUri(uri), (byOutpoint, claim: ?StreamClaim) => { + if (!claim || !claim.is_my_output) { + return null; + } + + const { claim_id: claimId } = claim; + let total = 0; + + Object.values(byOutpoint).forEach((support) => { + // $FlowFixMe + const { claim_id, amount } = support; + total = claim_id === claimId && amount ? total + parseFloat(amount) : total; + }); + + return total; + }); + +export const selectUpdatingChannel = createSelector(selectState, (state) => state.updatingChannel); + +export const selectUpdateChannelError = createSelector(selectState, (state) => state.updateChannelError); + +export const makeSelectReflectingClaimForUri = (uri: string) => + createSelector(selectClaimIdsByUri, selectReflectingById, (claimIdsByUri, reflectingById) => { + const claimId = claimIdsByUri[normalizeURI(uri)]; + return reflectingById[claimId]; + }); + +export const makeSelectMyStreamUrlsForPage = (page: number = 1) => + createSelector(selectMyClaimUrisWithoutChannels, (urls) => { + const start = (Number(page) - 1) * Number(CLAIM.PAGE_SIZE); + const end = Number(page) * Number(CLAIM.PAGE_SIZE); + + return urls && urls.length ? urls.slice(start, end) : []; + }); + +export const selectMyStreamUrlsCount = createSelector(selectMyClaimUrisWithoutChannels, (channels) => channels.length); + +export const makeSelectTagInClaimOrChannelForUri = (uri: string, tag: string) => + createSelector(makeSelectClaimForUri(uri), (claim) => { + const claimTags = (claim && claim.value && claim.value.tags) || []; + const channelTags = + (claim && claim.signing_channel && claim.signing_channel.value && claim.signing_channel.value.tags) || []; + return claimTags.includes(tag) || channelTags.includes(tag); + }); + +export const makeSelectClaimHasSource = (uri: string) => + createSelector(makeSelectClaimForUri(uri), (claim) => { + if (!claim) { + return false; + } + + return Boolean(claim.value.source); + }); + +export const makeSelectClaimIsStreamPlaceholder = (uri: string) => + createSelector(makeSelectClaimForUri(uri), (claim) => { + if (!claim) { + return false; + } + + return Boolean(claim.value_type === 'stream' && !claim.value.source); + }); + +export const makeSelectTotalStakedAmountForChannelUri = (uri: string) => + createSelector(makeSelectClaimForUri(uri), (claim) => { + if (!claim || !claim.amount || !claim.meta || !claim.meta.support_amount) { + return 0; + } + + return parseFloat(claim.amount) + parseFloat(claim.meta.support_amount) || 0; + }); + +export const makeSelectStakedLevelForChannelUri = (uri: string) => + createSelector(makeSelectTotalStakedAmountForChannelUri(uri), (amount) => { + let level = 1; + switch (true) { + case amount >= CLAIM.LEVEL_2_STAKED_AMOUNT && amount < CLAIM.LEVEL_3_STAKED_AMOUNT: + level = 2; + break; + case amount >= CLAIM.LEVEL_3_STAKED_AMOUNT && amount < CLAIM.LEVEL_4_STAKED_AMOUNT: + level = 3; + break; + case amount >= CLAIM.LEVEL_4_STAKED_AMOUNT && amount < CLAIM.LEVEL_5_STAKED_AMOUNT: + level = 4; + break; + case amount >= CLAIM.LEVEL_5_STAKED_AMOUNT: + level = 5; + break; + } + return level; + }); + +export const selectUpdatingCollection = createSelector(selectState, (state) => state.updatingCollection); + +export const selectUpdateCollectionError = createSelector(selectState, (state) => state.updateCollectionError); + +export const selectCreatingCollection = createSelector(selectState, (state) => state.creatingCollection); + +export const selectCreateCollectionError = createSelector(selectState, (state) => state.createCollectionError); diff --git a/ui/redux/selectors/collections.js b/ui/redux/selectors/collections.js new file mode 100644 index 000000000..80c092e72 --- /dev/null +++ b/ui/redux/selectors/collections.js @@ -0,0 +1,250 @@ +// @flow +import fromEntries from '@ungap/from-entries'; +import { createSelector } from 'reselect'; +import { selectMyCollectionIds, makeSelectClaimForUri } from 'redux/selectors/claims'; +import { parseURI } from 'util/lbryURI'; + +const selectState = (state: { collections: CollectionState }) => state.collections; + +export const selectSavedCollectionIds = createSelector(selectState, (collectionState) => collectionState.saved); + +export const selectBuiltinCollections = createSelector(selectState, (state) => state.builtin); +export const selectResolvedCollections = createSelector(selectState, (state) => state.resolved); + +export const selectMyUnpublishedCollections = createSelector(selectState, (state) => state.unpublished); + +export const selectMyEditedCollections = createSelector(selectState, (state) => state.edited); + +export const selectPendingCollections = createSelector(selectState, (state) => state.pending); + +export const makeSelectEditedCollectionForId = (id: string) => + createSelector(selectMyEditedCollections, (eLists) => eLists[id]); + +export const makeSelectPendingCollectionForId = (id: string) => + createSelector(selectPendingCollections, (pending) => pending[id]); + +export const makeSelectPublishedCollectionForId = (id: string) => + createSelector(selectResolvedCollections, (rLists) => rLists[id]); + +export const makeSelectUnpublishedCollectionForId = (id: string) => + createSelector(selectMyUnpublishedCollections, (rLists) => rLists[id]); + +export const makeSelectCollectionIsMine = (id: string) => + createSelector( + selectMyCollectionIds, + selectMyUnpublishedCollections, + selectBuiltinCollections, + (publicIds, privateIds, builtinIds) => { + return Boolean(publicIds.includes(id) || privateIds[id] || builtinIds[id]); + } + ); + +export const selectMyPublishedCollections = createSelector( + selectResolvedCollections, + selectPendingCollections, + selectMyEditedCollections, + selectMyCollectionIds, + (resolved, pending, edited, myIds) => { + // all resolved in myIds, plus those in pending and edited + const myPublishedCollections = fromEntries( + Object.entries(pending).concat( + Object.entries(resolved).filter( + ([key, val]) => + myIds.includes(key) && + // $FlowFixMe + !pending[key] + ) + ) + ); + // now add in edited: + Object.entries(edited).forEach(([id, item]) => { + myPublishedCollections[id] = item; + }); + return myPublishedCollections; + } +); + +export const selectMyPublishedMixedCollections = createSelector(selectMyPublishedCollections, (published) => { + const myCollections = fromEntries( + // $FlowFixMe + Object.entries(published).filter(([key, collection]) => { + // $FlowFixMe + return collection.type === 'collection'; + }) + ); + return myCollections; +}); + +export const selectMyPublishedPlaylistCollections = createSelector(selectMyPublishedCollections, (published) => { + const myCollections = fromEntries( + // $FlowFixMe + Object.entries(published).filter(([key, collection]) => { + // $FlowFixMe + return collection.type === 'playlist'; + }) + ); + return myCollections; +}); + +export const makeSelectMyPublishedCollectionForId = (id: string) => + createSelector(selectMyPublishedCollections, (myPublishedCollections) => myPublishedCollections[id]); + +// export const selectSavedCollections = createSelector( +// selectResolvedCollections, +// selectSavedCollectionIds, +// (resolved, myIds) => { +// const mySavedCollections = fromEntries( +// Object.entries(resolved).filter(([key, val]) => myIds.includes(key)) +// ); +// return mySavedCollections; +// } +// ); + +export const makeSelectIsResolvingCollectionForId = (id: string) => + createSelector(selectState, (state) => { + return state.isResolvingCollectionById[id]; + }); + +export const makeSelectCollectionForId = (id: string) => + createSelector( + selectBuiltinCollections, + selectResolvedCollections, + selectMyUnpublishedCollections, + selectMyEditedCollections, + selectPendingCollections, + (bLists, rLists, uLists, eLists, pLists) => { + const collection = bLists[id] || uLists[id] || eLists[id] || pLists[id] || rLists[id]; + return collection; + } + ); + +export const makeSelectClaimUrlInCollection = (url: string) => + createSelector( + selectBuiltinCollections, + selectMyPublishedCollections, + selectMyUnpublishedCollections, + selectMyEditedCollections, + selectPendingCollections, + (bLists, myRLists, uLists, eLists, pLists) => { + const collections = [bLists, uLists, eLists, myRLists, pLists]; + const itemsInCollections = []; + collections.map((list) => { + Object.entries(list).forEach(([key, value]) => { + // $FlowFixMe + value.items.map((item) => { + itemsInCollections.push(item); + }); + }); + }); + return itemsInCollections.includes(url); + } + ); + +export const makeSelectCollectionForIdHasClaimUrl = (id: string, url: string) => + createSelector(makeSelectCollectionForId(id), (collection) => collection && collection.items.includes(url)); + +export const makeSelectUrlsForCollectionId = (id: string) => + createSelector(makeSelectCollectionForId(id), (collection) => collection && collection.items); + +export const makeSelectClaimIdsForCollectionId = (id: string) => + createSelector(makeSelectCollectionForId(id), (collection) => { + const items = (collection && collection.items) || []; + const ids = items.map((item) => { + const { claimId } = parseURI(item); + return claimId; + }); + return ids; + }); + +export const makeSelectIndexForUrlInCollection = (url: string, id: string) => + createSelector( + (state) => state.content.shuffleList, + makeSelectUrlsForCollectionId(id), + makeSelectClaimForUri(url), + (shuffleState, urls, claim) => { + const shuffleUrls = shuffleState && shuffleState.collectionId === id && shuffleState.newUrls; + const listUrls = shuffleUrls || urls; + + const index = listUrls && listUrls.findIndex((u) => u === url); + if (index > -1) { + return index; + } else if (claim) { + const index = listUrls && listUrls.findIndex((u) => u === claim.permanent_url); + if (index > -1) return index; + return claim; + } + return null; + } + ); + +export const makeSelectPreviousUrlForCollectionAndUrl = (id: string, url: string) => + createSelector( + (state) => state.content.shuffleList, + (state) => state.content.loopList, + makeSelectIndexForUrlInCollection(url, id), + makeSelectUrlsForCollectionId(id), + (shuffleState, loopState, index, urls) => { + const loopList = loopState && loopState.collectionId === id && loopState.loop; + const shuffleUrls = shuffleState && shuffleState.collectionId === id && shuffleState.newUrls; + + if (index > -1) { + const listUrls = shuffleUrls || urls; + let nextUrl; + if (index === 0 && loopList) { + nextUrl = listUrls[listUrls.length - 1]; + } else { + nextUrl = listUrls[index - 1]; + } + return nextUrl || null; + } else { + return null; + } + } + ); + +export const makeSelectNextUrlForCollectionAndUrl = (id: string, url: string) => + createSelector( + (state) => state.content.shuffleList, + (state) => state.content.loopList, + makeSelectIndexForUrlInCollection(url, id), + makeSelectUrlsForCollectionId(id), + (shuffleState, loopState, index, urls) => { + const loopList = loopState && loopState.collectionId === id && loopState.loop; + const shuffleUrls = shuffleState && shuffleState.collectionId === id && shuffleState.newUrls; + + if (index > -1) { + const listUrls = shuffleUrls || urls; + // We'll get the next playble url + let remainingUrls = listUrls.slice(index + 1); + if (!remainingUrls.length && loopList) { + remainingUrls = listUrls.slice(0); + } + const nextUrl = remainingUrls && remainingUrls[0]; + return nextUrl || null; + } else { + return null; + } + } + ); + +export const makeSelectNameForCollectionId = (id: string) => + createSelector(makeSelectCollectionForId(id), (collection) => { + return (collection && collection.name) || ''; + }); + +export const makeSelectCountForCollectionId = (id: string) => + createSelector(makeSelectCollectionForId(id), (collection) => { + if (collection) { + if (collection.itemCount !== undefined) { + return collection.itemCount; + } + let itemCount = 0; + collection.items.map((item) => { + if (item) { + itemCount += 1; + } + }); + return itemCount; + } + return null; + }); diff --git a/ui/redux/selectors/comments.js b/ui/redux/selectors/comments.js index e19e236f4..045bf001d 100644 --- a/ui/redux/selectors/comments.js +++ b/ui/redux/selectors/comments.js @@ -3,7 +3,8 @@ import { createSelector } from 'reselect'; import { selectMutedChannels } from 'redux/selectors/blocked'; import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc'; -import { selectClaimsById, isClaimNsfw, selectMyActiveClaims } from 'lbry-redux'; +import { selectClaimsById, selectMyActiveClaims } from 'redux/selectors/claims'; +import { isClaimNsfw } from 'util/claim'; const selectState = (state) => state.comments || {}; @@ -11,7 +12,6 @@ export const selectCommentsById = createSelector(selectState, (state) => state.c export const selectIsFetchingComments = createSelector(selectState, (state) => state.isLoading); export const selectIsFetchingCommentsById = createSelector(selectState, (state) => state.isLoadingById); export const selectIsFetchingCommentsByParentId = createSelector(selectState, (state) => state.isLoadingByParentId); -export const selectIsPostingComment = createSelector(selectState, (state) => state.isCommenting); export const selectIsFetchingReacts = createSelector(selectState, (state) => state.isFetchingReacts); export const selectOthersReactsById = createSelector(selectState, (state) => state.othersReactsByCommentId); diff --git a/ui/redux/selectors/content.js b/ui/redux/selectors/content.js index f018663bb..44305be42 100644 --- a/ui/redux/selectors/content.js +++ b/ui/redux/selectors/content.js @@ -5,11 +5,10 @@ import { selectClaimsByUri, makeSelectClaimIsNsfw, makeSelectClaimIsMine, - makeSelectMediaTypeForUri, - selectBalance, makeSelectContentTypeForUri, - makeSelectFileNameForUri, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { makeSelectMediaTypeForUri, makeSelectFileNameForUri } from 'redux/selectors/file_info'; +import { selectBalance } from 'redux/selectors/wallet'; import { makeSelectCostInfoForUri } from 'lbryinc'; import { selectShowMatureContent } from 'redux/selectors/settings'; import * as RENDER_MODES from 'constants/file_render_modes'; diff --git a/ui/redux/selectors/file_info.js b/ui/redux/selectors/file_info.js index 26d48204a..55dc16499 100644 --- a/ui/redux/selectors/file_info.js +++ b/ui/redux/selectors/file_info.js @@ -1,11 +1,20 @@ -import { selectClaimsByUri, selectIsFetchingClaimListMine, selectMyClaims } from 'lbry-redux'; import { createSelector } from 'reselect'; +import { + selectClaimsByUri, + selectIsFetchingClaimListMine, + selectMyClaims, + makeSelectContentTypeForUri, + makeSelectClaimForUri, +} from 'redux/selectors/claims'; +import { buildURI } from 'util/lbryURI'; +import Lbry from 'lbry'; +import { PAGE_SIZE } from 'constants/claim'; -export const selectState = state => state.fileInfo || {}; +export const selectState = (state) => state.fileInfo || {}; -export const selectFileInfosByOutpoint = createSelector(selectState, state => state.byOutpoint || {}); +export const selectFileInfosByOutpoint = createSelector(selectState, (state) => state.byOutpoint || {}); -export const selectIsFetchingFileList = createSelector(selectState, state => state.isFetchingFileList); +export const selectIsFetchingFileList = createSelector(selectState, (state) => state.isFetchingFileList); export const selectIsFetchingFileListDownloadedOrPublished = createSelector( selectIsFetchingFileList, @@ -13,45 +22,46 @@ export const selectIsFetchingFileListDownloadedOrPublished = createSelector( (isFetchingFileList, isFetchingClaimListMine) => isFetchingFileList || isFetchingClaimListMine ); -export const makeSelectFileInfoForUri = uri => +export const makeSelectFileInfoForUri = (uri) => createSelector(selectClaimsByUri, selectFileInfosByOutpoint, (claims, byOutpoint) => { const claim = claims[uri]; const outpoint = claim ? `${claim.txid}:${claim.nout}` : undefined; return outpoint ? byOutpoint[outpoint] : undefined; }); -export const selectDownloadingByOutpoint = createSelector(selectState, state => state.downloadingByOutpoint || {}); +export const selectDownloadingByOutpoint = createSelector(selectState, (state) => state.downloadingByOutpoint || {}); -export const makeSelectDownloadingForUri = uri => +export const makeSelectDownloadingForUri = (uri) => createSelector(selectDownloadingByOutpoint, makeSelectFileInfoForUri(uri), (byOutpoint, fileInfo) => { if (!fileInfo) return false; return byOutpoint[fileInfo.outpoint]; }); -export const selectUrisLoading = createSelector(selectState, state => state.urisLoading || {}); +export const selectUrisLoading = createSelector(selectState, (state) => state.urisLoading || {}); -export const makeSelectLoadingForUri = uri => createSelector(selectUrisLoading, byUri => byUri && byUri[uri]); +export const makeSelectLoadingForUri = (uri) => + createSelector(selectUrisLoading, makeSelectClaimForUri(uri), (fetchingByOutpoint, claim) => { + if (!claim) { + return false; + } + + const { txid, nout } = claim; + const outpoint = `${txid}:${nout}`; + const isFetching = fetchingByOutpoint[outpoint]; + return isFetching; + }); export const selectFileInfosDownloaded = createSelector( selectFileInfosByOutpoint, selectMyClaims, (byOutpoint, myClaims) => - Object.values(byOutpoint).filter(fileInfo => { - const myClaimIds = myClaims.map(claim => claim.claim_id); + Object.values(byOutpoint).filter((fileInfo) => { + const myClaimIds = myClaims.map((claim) => claim.claim_id); return fileInfo && myClaimIds.indexOf(fileInfo.claim_id) === -1 && (fileInfo.completed || fileInfo.written_bytes); }) ); -// export const selectFileInfoForUri = (state, props) => { -// const claims = selectClaimsByUri(state), -// claim = claims[props.uri], -// fileInfos = selectAllFileInfos(state), -// outpoint = claim ? `${claim.txid}:${claim.nout}` : undefined; - -// return outpoint && fileInfos ? fileInfos[outpoint] : undefined; -// }; - export const selectDownloadingFileInfos = createSelector( selectDownloadingByOutpoint, selectFileInfosByOutpoint, @@ -59,7 +69,7 @@ export const selectDownloadingFileInfos = createSelector( const outpoints = Object.keys(downloadingByOutpoint); const fileInfos = []; - outpoints.forEach(outpoint => { + outpoints.forEach((outpoint) => { const fileInfo = fileInfosByOutpoint[outpoint]; if (fileInfo) fileInfos.push(fileInfo); @@ -69,10 +79,10 @@ export const selectDownloadingFileInfos = createSelector( } ); -export const selectTotalDownloadProgress = createSelector(selectDownloadingFileInfos, fileInfos => { +export const selectTotalDownloadProgress = createSelector(selectDownloadingFileInfos, (fileInfos) => { const progress = []; - fileInfos.forEach(fileInfo => { + fileInfos.forEach((fileInfo) => { progress.push((fileInfo.written_bytes / fileInfo.total_bytes) * 100); }); @@ -82,4 +92,95 @@ export const selectTotalDownloadProgress = createSelector(selectDownloadingFileI return -1; }); -export const selectFileInfoErrors = createSelector(selectState, state => state.errors || {}); +export const selectFileInfoErrors = createSelector(selectState, (state) => state.errors || {}); + +export const selectFileListPublishedSort = createSelector(selectState, (state) => state.fileListPublishedSort); + +export const selectFileListDownloadedSort = createSelector(selectState, (state) => state.fileListDownloadedSort); + +export const selectDownloadedUris = createSelector( + selectFileInfosDownloaded, + // We should use permament_url but it doesn't exist in file_list + (info) => info.slice().map((claim) => `lbry://${claim.claim_name}#${claim.claim_id}`) +); + +export const makeSelectMediaTypeForUri = (uri) => + createSelector(makeSelectFileInfoForUri(uri), makeSelectContentTypeForUri(uri), (fileInfo, contentType) => { + if (!fileInfo && !contentType) { + return undefined; + } + + const fileName = fileInfo && fileInfo.file_name; + return Lbry.getMediaType(contentType, fileName); + }); + +export const makeSelectUriIsStreamable = (uri) => + createSelector(makeSelectMediaTypeForUri(uri), (mediaType) => { + const isStreamable = ['audio', 'video', 'image'].indexOf(mediaType) !== -1; + return isStreamable; + }); + +export const makeSelectDownloadPathForUri = (uri) => + createSelector(makeSelectFileInfoForUri(uri), (fileInfo) => { + return fileInfo && fileInfo.download_path; + }); + +export const makeSelectFilePartlyDownloaded = (uri) => + createSelector(makeSelectFileInfoForUri(uri), (fileInfo) => { + if (!fileInfo) { + return false; + } + + return fileInfo.written_bytes > 0 || fileInfo.blobs_completed > 0; + }); + +export const makeSelectFileNameForUri = (uri) => + createSelector(makeSelectFileInfoForUri(uri), (fileInfo) => { + return fileInfo && fileInfo.file_name; + }); + +export const selectDownloadUrlsCount = createSelector(selectDownloadedUris, (uris) => uris.length); + +function filterFileInfos(fileInfos, query) { + if (query) { + const queryMatchRegExp = new RegExp(query, 'i'); + return fileInfos.filter((fileInfo) => { + const { metadata } = fileInfo; + + return ( + (metadata.title && metadata.title.match(queryMatchRegExp)) || + (fileInfo.channel_name && fileInfo.channel_name.match(queryMatchRegExp)) || + (fileInfo.claim_name && fileInfo.claim_name.match(queryMatchRegExp)) + ); + }); + } + + return fileInfos; +} + +export const makeSelectSearchDownloadUrlsForPage = (query, page = 1) => + createSelector(selectFileInfosDownloaded, (fileInfos) => { + const matchingFileInfos = filterFileInfos(fileInfos, query); + const start = (Number(page) - 1) * Number(PAGE_SIZE); + const end = Number(page) * Number(PAGE_SIZE); + + return matchingFileInfos && matchingFileInfos.length + ? matchingFileInfos.slice(start, end).map((fileInfo) => + buildURI({ + streamName: fileInfo.claim_name, + channelName: fileInfo.channel_name, + channelClaimId: fileInfo.channel_claim_id, + }) + ) + : []; + }); + +export const makeSelectSearchDownloadUrlsCount = (query) => + createSelector(selectFileInfosDownloaded, (fileInfos) => { + return fileInfos && fileInfos.length ? filterFileInfos(fileInfos, query).length : 0; + }); + +export const makeSelectStreamingUrlForUri = (uri) => + createSelector(makeSelectFileInfoForUri(uri), (fileInfo) => { + return fileInfo && fileInfo.streaming_url; + }); diff --git a/ui/redux/selectors/livestream.js b/ui/redux/selectors/livestream.js index a48c4e10a..2ad1c0e6e 100644 --- a/ui/redux/selectors/livestream.js +++ b/ui/redux/selectors/livestream.js @@ -1,6 +1,6 @@ // @flow import { createSelector } from 'reselect'; -import { selectMyClaims, selectPendingClaims } from 'lbry-redux'; +import { selectMyClaims, selectPendingClaims } from 'redux/selectors/claims'; const selectState = (state) => state.livestream || {}; diff --git a/ui/redux/selectors/publish.js b/ui/redux/selectors/publish.js index c0038e3d2..d75a45cae 100644 --- a/ui/redux/selectors/publish.js +++ b/ui/redux/selectors/publish.js @@ -1,51 +1,63 @@ import { createSelector } from 'reselect'; -import { parseURI, selectClaimsById, selectMyClaimsWithoutChannels, selectResolvingUris, buildURI } from 'lbry-redux'; +import { parseURI, buildURI } from 'util/lbryURI'; +import { + selectClaimsById, + selectMyClaimsWithoutChannels, + selectResolvingUris, + selectClaimsByUri, +} from 'redux/selectors/claims'; -const selectState = state => state.publish || {}; +const selectState = (state) => state.publish || {}; + +export const selectIsStillEditing = createSelector(selectState, (publishState) => { + const { editingURI, uri } = publishState; + + if (!editingURI || !uri) { + return false; + } + + const { isChannel: currentIsChannel, streamName: currentClaimName, channelName: currentContentName } = parseURI(uri); + const { isChannel: editIsChannel, streamName: editClaimName, channelName: editContentName } = parseURI(editingURI); + + // Depending on the previous/current use of a channel, we need to compare different things + // ex: going from a channel to anonymous, the new uri won't return contentName, so we need to use claimName + const currentName = currentIsChannel ? currentContentName : currentClaimName; + const editName = editIsChannel ? editContentName : editClaimName; + return currentName === editName; +}); export const selectPublishFormValues = createSelector( selectState, - state => { - const { pendingPublish, ...formValues } = state; - return formValues; - } -); + (state) => state.settings, + selectIsStillEditing, + (publishState, settingsState, isStillEditing) => { + const { languages, ...formValues } = publishState; + const language = languages && languages.length && languages[0]; + const { clientSettings } = settingsState; + const { language: languageSet } = clientSettings; -export const makeSelectPublishFormValue = item => - createSelector( - selectState, - state => state[item] - ); - -// Is the current uri the same as the uri they clicked "edit" on -export const selectIsStillEditing = createSelector( - selectPublishFormValues, - publishState => { - const { editingURI, uri } = publishState; - - if (!editingURI || !uri) { - return false; + let actualLanguage; + // Sets default if editing a claim with a set language + if (!language && isStillEditing && languageSet) { + actualLanguage = languageSet; + } else { + actualLanguage = language || languageSet || 'en'; } - const { isChannel: currentIsChannel, claimName: currentClaimName, contentName: currentContentName } = parseURI(uri); - const { isChannel: editIsChannel, claimName: editClaimName, contentName: editContentName } = parseURI(editingURI); - - // Depending on the previous/current use of a channel, we need to compare different things - // ex: going from a channel to anonymous, the new uri won't return contentName, so we need to use claimName - const currentName = currentIsChannel ? currentContentName : currentClaimName; - const editName = editIsChannel ? editContentName : editClaimName; - return currentName === editName; + return { ...formValues, language: actualLanguage }; } ); +export const makeSelectPublishFormValue = (item) => createSelector(selectState, (state) => state[item]); + export const selectMyClaimForUri = createSelector( selectPublishFormValues, selectIsStillEditing, selectClaimsById, selectMyClaimsWithoutChannels, ({ editingURI, uri }, isStillEditing, claimsById, myClaims) => { - const { contentName, claimName } = parseURI(uri); - const { claimId: editClaimId } = parseURI(editingURI); + const { channelName: contentName, streamName: claimName } = parseURI(uri); + const { streamClaimId: editClaimId } = parseURI(editingURI); // If isStillEditing // They clicked "edit" from the file page @@ -53,7 +65,7 @@ export const selectMyClaimForUri = createSelector( // Get the claim so they can edit without re-uploading a new file return isStillEditing ? claimsById[editClaimId] - : myClaims.find(claim => + : myClaims.find((claim) => !contentName ? claim.name === claimName : claim.name === contentName || claim.name === claimName ); } @@ -68,8 +80,8 @@ export const selectIsResolvingPublishUris = createSelector( const { isChannel } = parseURI(uri); let isResolvingShortUri; - if (isChannel) { - const shortUri = buildURI({ contentName: name }); + if (isChannel && name) { + const shortUri = buildURI({ streamName: name }); isResolvingShortUri = resolvingUris.includes(shortUri); } @@ -79,3 +91,31 @@ export const selectIsResolvingPublishUris = createSelector( return false; } ); + +export const selectTakeOverAmount = createSelector( + selectState, + selectMyClaimForUri, + selectClaimsByUri, + ({ name }, myClaimForUri, claimsByUri) => { + if (!name) { + return null; + } + + // We only care about the winning claim for the short uri + const shortUri = buildURI({ streamName: name }); + const claimForShortUri = claimsByUri[shortUri]; + + if (!myClaimForUri && claimForShortUri) { + return claimForShortUri.meta.effective_amount; + } else if (myClaimForUri && claimForShortUri) { + // https://github.com/lbryio/lbry/issues/1476 + // We should check the current effective_amount on my claim to see how much additional lbc + // is needed to win the claim. Currently this is not possible during a takeover. + // With this, we could say something like, "You have x lbc in support, if you bid y additional LBC you will control the claim" + // For now just ignore supports. We will just show the winning claim's bid amount + return claimForShortUri.meta.effective_amount || claimForShortUri.amount; + } + + return null; + } +); diff --git a/ui/redux/selectors/reactions.js b/ui/redux/selectors/reactions.js index 76feeebdc..1a225924b 100644 --- a/ui/redux/selectors/reactions.js +++ b/ui/redux/selectors/reactions.js @@ -1,6 +1,6 @@ import * as REACTION_TYPES from 'constants/reactions'; import { createSelector } from 'reselect'; -import { makeSelectClaimForUri } from 'lbry-redux'; +import { makeSelectClaimForUri } from 'redux/selectors/claims'; const selectState = (state) => state.reactions || {}; diff --git a/ui/redux/selectors/search.js b/ui/redux/selectors/search.js index 2298cabb2..82e17a220 100644 --- a/ui/redux/selectors/search.js +++ b/ui/redux/selectors/search.js @@ -3,15 +3,15 @@ import { getSearchQueryString } from 'util/query-params'; import { selectShowMatureContent } from 'redux/selectors/settings'; import { SEARCH_OPTIONS } from 'constants/search'; import { - parseURI, selectClaimsByUri, makeSelectClaimForUri, makeSelectClaimForClaimId, makeSelectClaimIsNsfw, - isClaimNsfw, makeSelectPendingClaimForUri, makeSelectIsUriResolving, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; +import { parseURI } from 'util/lbryURI'; +import { isClaimNsfw } from 'util/claim'; import { createSelector } from 'reselect'; import { createNormalizedSearchKey, getRecommendationSearchOptions } from 'util/search'; import { selectMutedChannels } from 'redux/selectors/blocked'; @@ -187,7 +187,7 @@ export const makeSelectRecommendedRecsysIdForClaimId = (claimId: string) => export const makeSelectWinningUriForQuery = (query: string) => { const uriFromQuery = `lbry://${query}`; - let channelUriFromQuery; + let channelUriFromQuery = ''; try { const { isChannel } = parseURI(uriFromQuery); if (!isChannel) { diff --git a/ui/redux/selectors/settings.js b/ui/redux/selectors/settings.js index c10a6b052..0cf497fba 100644 --- a/ui/redux/selectors/settings.js +++ b/ui/redux/selectors/settings.js @@ -1,4 +1,6 @@ -import { SETTINGS, DAEMON_SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; +import * as DAEMON_SETTINGS from 'constants/daemon_settings'; + import { createSelector } from 'reselect'; import { ENABLE_MATURE } from 'config'; import { getDefaultHomepageKey, getDefaultLanguage } from 'util/default-languages'; diff --git a/ui/redux/selectors/subscriptions.js b/ui/redux/selectors/subscriptions.js index 1b43ae0f4..ec0b339c8 100644 --- a/ui/redux/selectors/subscriptions.js +++ b/ui/redux/selectors/subscriptions.js @@ -1,12 +1,11 @@ import { SUGGESTED_FEATURED, SUGGESTED_TOP_SUBSCRIBED } from 'constants/subscriptions'; import { createSelector } from 'reselect'; +import { parseURI, isURIEqual } from 'util/lbryURI'; import { selectAllFetchingChannelClaims, makeSelectChannelForClaimUri, - parseURI, makeSelectClaimForUri, - isURIEqual, -} from 'lbry-redux'; +} from 'redux/selectors/claims'; import { swapKeyAndValue } from 'util/swap-json'; // Returns the entire subscriptions state diff --git a/ui/redux/selectors/wallet.js b/ui/redux/selectors/wallet.js new file mode 100644 index 000000000..cc1b58ec7 --- /dev/null +++ b/ui/redux/selectors/wallet.js @@ -0,0 +1,273 @@ +import { createSelector } from 'reselect'; +import * as TRANSACTIONS from 'constants/transaction_types'; +import { PAGE_SIZE, LATEST_PAGE_SIZE } from 'constants/transaction_list'; +import { selectClaimIdsByUri } from 'redux/selectors/claims'; +import parseData from 'util/parse-data'; +export const selectState = (state) => state.wallet || {}; + +export const selectWalletState = selectState; + +export const selectWalletIsEncrypted = createSelector(selectState, (state) => state.walletIsEncrypted); + +export const selectWalletEncryptPending = createSelector(selectState, (state) => state.walletEncryptPending); + +export const selectWalletEncryptSucceeded = createSelector(selectState, (state) => state.walletEncryptSucceded); + +export const selectPendingSupportTransactions = createSelector( + selectState, + (state) => state.pendingSupportTransactions +); + +export const selectPendingOtherTransactions = createSelector(selectState, (state) => state.pendingTxos); + +export const selectAbandonClaimSupportError = createSelector(selectState, (state) => state.abandonClaimSupportError); + +export const makeSelectPendingAmountByUri = (uri) => + createSelector(selectClaimIdsByUri, selectPendingSupportTransactions, (claimIdsByUri, pendingSupports) => { + const uriEntry = Object.entries(claimIdsByUri).find(([u, cid]) => u === uri); + const claimId = uriEntry && uriEntry[1]; + const pendingSupport = claimId && pendingSupports[claimId]; + return pendingSupport ? pendingSupport.effective : undefined; + }); + +export const selectWalletEncryptResult = createSelector(selectState, (state) => state.walletEncryptResult); + +export const selectWalletDecryptPending = createSelector(selectState, (state) => state.walletDecryptPending); + +export const selectWalletDecryptSucceeded = createSelector(selectState, (state) => state.walletDecryptSucceded); + +export const selectWalletDecryptResult = createSelector(selectState, (state) => state.walletDecryptResult); + +export const selectWalletUnlockPending = createSelector(selectState, (state) => state.walletUnlockPending); + +export const selectWalletUnlockSucceeded = createSelector(selectState, (state) => state.walletUnlockSucceded); + +export const selectWalletUnlockResult = createSelector(selectState, (state) => state.walletUnlockResult); + +export const selectWalletLockPending = createSelector(selectState, (state) => state.walletLockPending); + +export const selectWalletLockSucceeded = createSelector(selectState, (state) => state.walletLockSucceded); + +export const selectWalletLockResult = createSelector(selectState, (state) => state.walletLockResult); + +export const selectBalance = createSelector(selectState, (state) => state.balance); + +export const selectTotalBalance = createSelector(selectState, (state) => state.totalBalance); + +export const selectReservedBalance = createSelector(selectState, (state) => state.reservedBalance); + +export const selectClaimsBalance = createSelector(selectState, (state) => state.claimsBalance); + +export const selectSupportsBalance = createSelector(selectState, (state) => state.supportsBalance); + +export const selectTipsBalance = createSelector(selectState, (state) => state.tipsBalance); + +export const selectTransactionsById = createSelector(selectState, (state) => state.transactions || {}); + +export const selectSupportsByOutpoint = createSelector(selectState, (state) => state.supports || {}); + +export const selectTotalSupports = createSelector(selectSupportsByOutpoint, (byOutpoint) => { + let total = parseFloat('0.0'); + + Object.values(byOutpoint).forEach((support) => { + const { amount } = support; + total = amount ? total + parseFloat(amount) : total; + }); + + return total; +}); + +export const selectTransactionItems = createSelector(selectTransactionsById, (byId) => { + const items = []; + + Object.keys(byId).forEach((txid) => { + const tx = byId[txid]; + + // ignore dust/fees + // it is fee only txn if all infos are also empty + if ( + Math.abs(tx.value) === Math.abs(tx.fee) && + tx.claim_info.length === 0 && + tx.support_info.length === 0 && + tx.update_info.length === 0 && + tx.abandon_info.length === 0 + ) { + return; + } + + const append = []; + + append.push( + ...tx.claim_info.map((item) => + Object.assign({}, tx, item, { + type: item.claim_name[0] === '@' ? TRANSACTIONS.CHANNEL : TRANSACTIONS.PUBLISH, + }) + ) + ); + append.push( + ...tx.support_info.map((item) => + Object.assign({}, tx, item, { + type: !item.is_tip ? TRANSACTIONS.SUPPORT : TRANSACTIONS.TIP, + }) + ) + ); + append.push(...tx.update_info.map((item) => Object.assign({}, tx, item, { type: TRANSACTIONS.UPDATE }))); + append.push(...tx.abandon_info.map((item) => Object.assign({}, tx, item, { type: TRANSACTIONS.ABANDON }))); + + if (!append.length) { + append.push( + Object.assign({}, tx, { + type: tx.value < 0 ? TRANSACTIONS.SPEND : TRANSACTIONS.RECEIVE, + }) + ); + } + + items.push( + ...append.map((item) => { + // value on transaction, amount on outpoint + // amount is always positive, but should match sign of value + const balanceDelta = parseFloat(item.balance_delta); + const value = parseFloat(item.value); + const amount = balanceDelta || value; + const fee = parseFloat(tx.fee); + + return { + txid, + timestamp: tx.timestamp, + date: tx.timestamp ? new Date(Number(tx.timestamp) * 1000) : null, + amount, + fee, + claim_id: item.claim_id, + claim_name: item.claim_name, + type: item.type || TRANSACTIONS.SPEND, + nout: item.nout, + confirmations: tx.confirmations, + }; + }) + ); + }); + + return items.sort((tx1, tx2) => { + if (!tx1.timestamp && !tx2.timestamp) { + return 0; + } else if (!tx1.timestamp && tx2.timestamp) { + return -1; + } else if (tx1.timestamp && !tx2.timestamp) { + return 1; + } + + return tx2.timestamp - tx1.timestamp; + }); +}); + +export const selectRecentTransactions = createSelector(selectTransactionItems, (transactions) => { + const threshold = new Date(); + threshold.setDate(threshold.getDate() - 7); + return transactions.filter((transaction) => { + if (!transaction.date) { + return true; // pending transaction + } + + return transaction.date > threshold; + }); +}); + +export const selectHasTransactions = createSelector( + selectTransactionItems, + (transactions) => transactions && transactions.length > 0 +); + +export const selectIsFetchingTransactions = createSelector(selectState, (state) => state.fetchingTransactions); + +/** + * CSV of 'selectTransactionItems'. + */ +export const selectTransactionsFile = createSelector(selectTransactionItems, (transactions) => { + if (!transactions || transactions.length === 0) { + // No data. + return undefined; + } + + const parsed = parseData(transactions, 'csv'); + if (!parsed) { + // Invalid data, or failed to parse. + return null; + } + + return parsed; +}); + +export const selectIsSendingSupport = createSelector(selectState, (state) => state.sendingSupport); + +export const selectReceiveAddress = createSelector(selectState, (state) => state.receiveAddress); + +export const selectGettingNewAddress = createSelector(selectState, (state) => state.gettingNewAddress); + +export const selectDraftTransaction = createSelector(selectState, (state) => state.draftTransaction || {}); + +export const selectDraftTransactionAmount = createSelector(selectDraftTransaction, (draft) => draft.amount); + +export const selectDraftTransactionAddress = createSelector(selectDraftTransaction, (draft) => draft.address); + +export const selectDraftTransactionError = createSelector(selectDraftTransaction, (draft) => draft.error); + +export const selectBlocks = createSelector(selectState, (state) => state.blocks); + +export const selectCurrentHeight = createSelector(selectState, (state) => state.latestBlock); + +export const selectTransactionListFilter = createSelector(selectState, (state) => state.transactionListFilter || ''); + +export const selectFilteredTransactions = createSelector( + selectTransactionItems, + selectTransactionListFilter, + (transactions, filter) => { + return transactions.filter((transaction) => { + return filter === TRANSACTIONS.ALL || filter === transaction.type; + }); + } +); + +export const selectTxoPageParams = createSelector(selectState, (state) => state.txoFetchParams); + +export const selectTxoPage = createSelector(selectState, (state) => (state.txoPage && state.txoPage.items) || []); + +export const selectTxoPageNumber = createSelector(selectState, (state) => (state.txoPage && state.txoPage.page) || 1); + +export const selectTxoItemCount = createSelector( + selectState, + (state) => (state.txoPage && state.txoPage.total_items) || 1 +); + +export const selectFetchingTxosError = createSelector(selectState, (state) => state.fetchingTxosError); + +export const selectIsFetchingTxos = createSelector(selectState, (state) => state.fetchingTxos); + +export const makeSelectFilteredTransactionsForPage = (page = 1) => + createSelector(selectFilteredTransactions, (filteredTransactions) => { + const start = (Number(page) - 1) * Number(PAGE_SIZE); + const end = Number(page) * Number(PAGE_SIZE); + return filteredTransactions && filteredTransactions.length ? filteredTransactions.slice(start, end) : []; + }); + +export const makeSelectLatestTransactions = createSelector(selectTransactionItems, (transactions) => { + return transactions && transactions.length ? transactions.slice(0, LATEST_PAGE_SIZE) : []; +}); + +export const selectFilteredTransactionCount = createSelector( + selectFilteredTransactions, + (filteredTransactions) => filteredTransactions.length +); + +export const selectIsWalletReconnecting = createSelector(selectState, (state) => state.walletReconnecting); + +export const selectIsFetchingUtxoCounts = createSelector(selectState, (state) => state.fetchingUtxoCounts); + +export const selectIsConsolidatingUtxos = createSelector(selectState, (state) => state.consolidatingUtxos); + +export const selectIsMassClaimingTips = createSelector(selectState, (state) => state.massClaimingTips); + +export const selectPendingConsolidateTxid = createSelector(selectState, (state) => state.pendingConsolidateTxid); + +export const selectPendingMassClaimTxid = createSelector(selectState, (state) => state.pendingMassClaimTxid); + +export const selectUtxoCounts = createSelector(selectState, (state) => state.utxoCounts); diff --git a/ui/rewards.js b/ui/rewards.js index 95e445ec7..0847a2202 100644 --- a/ui/rewards.js +++ b/ui/rewards.js @@ -1,4 +1,4 @@ -import { Lbry } from 'lbry-redux'; +import Lbry from 'lbry'; import { doToast } from 'redux/actions/notifications'; import { Lbryio } from 'lbryinc'; @@ -26,7 +26,7 @@ rewards.claimReward = (type, rewardParams) => { return; } - Lbryio.call('reward', 'claim', params, 'post').then(reward => { + Lbryio.call('reward', 'claim', params, 'post').then((reward) => { const message = reward.reward_notification || `You have claimed a ${reward.reward_amount} LBRY Credit reward.`; // Display global notice @@ -46,7 +46,7 @@ rewards.claimReward = (type, rewardParams) => { } return new Promise((resolve, reject) => { - Lbry.address_unused().then(address => { + Lbry.address_unused().then((address) => { const params = { reward_type: type, wallet_address: address, @@ -56,11 +56,11 @@ rewards.claimReward = (type, rewardParams) => { switch (type) { case rewards.TYPE_FIRST_CHANNEL: Lbry.channel_list({ page: 1, page_size: 10 }) - .then(claims => { + .then((claims) => { const claim = claims.items && claims.items.find( - foundClaim => + (foundClaim) => foundClaim.name.length && foundClaim.name[0] === '@' && foundClaim.txid.length && @@ -78,11 +78,11 @@ rewards.claimReward = (type, rewardParams) => { case rewards.TYPE_FIRST_PUBLISH: Lbry.stream_list({ page: 1, page_size: 10 }) - .then(claims => { + .then((claims) => { const claim = claims.items && claims.items.find( - foundClaim => + (foundClaim) => foundClaim.name.length && foundClaim.name[0] !== '@' && foundClaim.txid.length && diff --git a/ui/store.js b/ui/store.js index c7f415253..bf78ea949 100644 --- a/ui/store.js +++ b/ui/store.js @@ -9,7 +9,8 @@ import thunk from 'redux-thunk'; import { createMemoryHistory, createBrowserHistory } from 'history'; import { routerMiddleware } from 'connected-react-router'; import createRootReducer from './reducers'; -import { Lbry, buildSharedStateMiddleware, ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux'; +import Lbry from 'lbry'; +import { buildSharedStateMiddleware } from 'redux/middleware/shared-state'; import { doSyncLoop } from 'redux/actions/sync'; import { getAuthToken } from 'util/saved-passwords'; import { generateInitialUrl } from 'util/url'; @@ -131,14 +132,14 @@ const triggerSharedStateActions = [ ACTIONS.ADD_COIN_SWAP, ACTIONS.REMOVE_COIN_SWAP, ACTIONS.TOGGLE_TAG_FOLLOW, - LBRY_REDUX_ACTIONS.CREATE_CHANNEL_COMPLETED, + ACTIONS.CREATE_CHANNEL_COMPLETED, ACTIONS.SYNC_CLIENT_SETTINGS, // Disabled until we can overwrite preferences - LBRY_REDUX_ACTIONS.SHARED_PREFERENCE_SET, - LBRY_REDUX_ACTIONS.COLLECTION_EDIT, - LBRY_REDUX_ACTIONS.COLLECTION_DELETE, - LBRY_REDUX_ACTIONS.COLLECTION_NEW, - LBRY_REDUX_ACTIONS.COLLECTION_PENDING, + ACTIONS.SHARED_PREFERENCE_SET, + ACTIONS.COLLECTION_EDIT, + ACTIONS.COLLECTION_DELETE, + ACTIONS.COLLECTION_NEW, + ACTIONS.COLLECTION_PENDING, // MAYBE COLLECTOIN SAVE // ACTIONS.SET_WELCOME_VERSION, // ACTIONS.SET_ALLOW_ANALYTICS, diff --git a/ui/util/batch-actions.js b/ui/util/batch-actions.js new file mode 100644 index 000000000..0af1c4294 --- /dev/null +++ b/ui/util/batch-actions.js @@ -0,0 +1,7 @@ +// https://github.com/reactjs/redux/issues/911 +export function batchActions(...actions) { + return { + type: 'BATCH_ACTIONS', + actions, + }; +} diff --git a/ui/util/buildHomepage.js b/ui/util/buildHomepage.js index f0da10180..651528017 100644 --- a/ui/util/buildHomepage.js +++ b/ui/util/buildHomepage.js @@ -2,7 +2,7 @@ import * as PAGES from 'constants/pages'; import * as ICONS from 'constants/icons'; import * as CS from 'constants/claim_search'; -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; import moment from 'moment'; import { toCapitalCase } from 'util/string'; import { useIsLargeScreen } from 'effects/use-screensize'; diff --git a/ui/util/claim.js b/ui/util/claim.js index 157a1a52f..3049283f0 100644 --- a/ui/util/claim.js +++ b/ui/util/claim.js @@ -1,4 +1,70 @@ // @flow +import { MATURE_TAGS } from 'constants/tags'; + +const matureTagMap = MATURE_TAGS.reduce((acc, tag) => ({ ...acc, [tag]: true }), {}); + +export const isClaimNsfw = (claim: Claim): boolean => { + if (!claim) { + throw new Error('No claim passed to isClaimNsfw()'); + } + + if (!claim.value) { + return false; + } + + const tags = claim.value.tags || []; + for (let i = 0; i < tags.length; i += 1) { + const tag = tags[i].toLowerCase(); + if (matureTagMap[tag]) { + return true; + } + } + + return false; +}; + +export function createNormalizedClaimSearchKey(options: { page: number, release_time?: string }) { + // Ignore page because we don't care what the last page searched was, we want everything + // Ignore release_time because that will change depending on when you call claim_search ex: release_time: ">12344567" + const { page: optionToIgnoreForQuery, release_time: anotherToIgnore, ...rest } = options; + const query = JSON.stringify(rest); + return query; +} + +export function concatClaims(claimList: Array = [], concatClaimList: Array = []): Array { + if (!claimList || claimList.length === 0) { + if (!concatClaimList) { + return []; + } + return concatClaimList.slice(); + } + + const claims = claimList.slice(); + concatClaimList.forEach((claim) => { + if (!claims.some((item) => item.claim_id === claim.claim_id)) { + claims.push(claim); + } + }); + + return claims; +} + +export function filterClaims(claims: Array, query: ?string): Array { + if (query) { + const queryMatchRegExp = new RegExp(query, 'i'); + return claims.filter((claim) => { + const { value } = claim; + + return ( + (value.title && value.title.match(queryMatchRegExp)) || + (claim.signing_channel && claim.signing_channel.name.match(queryMatchRegExp)) || + (claim.name && claim.name.match(queryMatchRegExp)) + ); + }); + } + + return claims; +} export function getChannelIdFromClaim(claim: ?Claim) { if (claim) { diff --git a/ui/util/context-menu.js b/ui/util/context-menu.js index 2d5155494..7d047af65 100644 --- a/ui/util/context-menu.js +++ b/ui/util/context-menu.js @@ -1,5 +1,5 @@ import { clipboard, remote, shell } from 'electron'; -import { convertToShareLink } from 'lbry-redux'; +import { convertToShareLink } from 'util/lbryURI'; const isDev = process.env.NODE_ENV !== 'production'; function injectDevelopmentTemplate(event, templates) { diff --git a/ui/util/form-validation.js b/ui/util/form-validation.js index 5841925bc..34148d2de 100644 --- a/ui/util/form-validation.js +++ b/ui/util/form-validation.js @@ -1,5 +1,5 @@ // @flow -import { regexAddress } from 'lbry-redux'; +import { regexAddress } from 'util/lbryURI'; export default function validateSendTx(address: string) { const errors = { @@ -13,4 +13,4 @@ export default function validateSendTx(address: string) { } return errors; -}; +} diff --git a/ui/util/format-credits.js b/ui/util/format-credits.js new file mode 100644 index 000000000..8c0ceb42e --- /dev/null +++ b/ui/util/format-credits.js @@ -0,0 +1,60 @@ +function numberWithCommas(x) { + var parts = x.toString().split('.'); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); + return parts.join('.'); +} + +export function formatCredits(amount, precision, shortFormat = false) { + let actualAmount = parseFloat(amount); + let actualPrecision = parseFloat(precision); + let suffix = ''; + + if (Number.isNaN(actualAmount) || actualAmount === 0) return '0'; + + if (actualAmount >= 1000000) { + if (precision <= 7) { + if (shortFormat) { + actualAmount = actualAmount / 1000000; + suffix = 'M'; + } else { + actualPrecision -= 7; + } + } + } else if (actualAmount >= 1000) { + if (precision <= 4) { + if (shortFormat) { + actualAmount = actualAmount / 1000; + suffix = 'K'; + } else { + actualPrecision -= 4; + } + } + } + + return ( + numberWithCommas(actualAmount.toFixed(actualPrecision >= 0 ? actualPrecision : 1).replace(/\.*0+$/, '')) + suffix + ); +} + +export function formatFullPrice(amount, precision = 1) { + let formated = ''; + + const quantity = amount.toString().split('.'); + const fraction = quantity[1]; + + if (fraction) { + const decimals = fraction.split(''); + const first = decimals.filter((number) => number !== '0')[0]; + const index = decimals.indexOf(first); + + // Set format fraction + formated = `.${fraction.substring(0, index + precision)}`; + } + + return parseFloat(quantity[0] + formated); +} + +export function creditsToString(amount) { + const creditString = parseFloat(amount).toFixed(8); + return creditString; +} diff --git a/ui/util/lbryURI.js b/ui/util/lbryURI.js new file mode 100644 index 000000000..bdc1ad0a1 --- /dev/null +++ b/ui/util/lbryURI.js @@ -0,0 +1,330 @@ +// @flow +const isProduction = process.env.NODE_ENV === 'production'; +const channelNameMinLength = 1; +const claimIdMaxLength = 40; + +// see https://spec.lbry.com/#urls +export const regexInvalidURI = /[ =&#:$@%?;/\\"<>%{}|^~[\]`\u{0000}-\u{0008}\u{000b}-\u{000c}\u{000e}-\u{001F}\u{D800}-\u{DFFF}\u{FFFE}-\u{FFFF}]/u; +export const regexAddress = /^(b|r)(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/; +const regexPartProtocol = '^((?:lbry://)?)'; +const regexPartStreamOrChannelName = '([^:$#/]*)'; +const regexPartModifierSeparator = '([:$#]?)([^/]*)'; +const queryStringBreaker = '^([\\S]+)([?][\\S]*)'; +const separateQuerystring = new RegExp(queryStringBreaker); + +const MOD_SEQUENCE_SEPARATOR = '*'; +const MOD_CLAIM_ID_SEPARATOR_OLD = '#'; +const MOD_CLAIM_ID_SEPARATOR = ':'; +const MOD_BID_POSITION_SEPARATOR = '$'; + +/** + * Parses a LBRY name into its component parts. Throws errors with user-friendly + * messages for invalid names. + * + * Returns a dictionary with keys: + * - path (string) + * - isChannel (boolean) + * - streamName (string, if present) + * - streamClaimId (string, if present) + * - channelName (string, if present) + * - channelClaimId (string, if present) + * - primaryClaimSequence (int, if present) + * - secondaryClaimSequence (int, if present) + * - primaryBidPosition (int, if present) + * - secondaryBidPosition (int, if present) + */ + +export function parseURI(url: string, requireProto: boolean = false): LbryUrlObj { + // Break into components. Empty sub-matches are converted to null + + const componentsRegex = new RegExp( + regexPartProtocol + // protocol + regexPartStreamOrChannelName + // stream or channel name (stops at the first separator or end) + regexPartModifierSeparator + // modifier separator, modifier (stops at the first path separator or end) + '(/?)' + // path separator, there should only be one (optional) slash to separate the stream and channel parts + regexPartStreamOrChannelName + + regexPartModifierSeparator + ); + // chop off the querystring first + let QSStrippedURL, qs; + const qsRegexResult = separateQuerystring.exec(url); + if (qsRegexResult) { + [QSStrippedURL, qs] = qsRegexResult.slice(1).map((match) => match || null); + } + + const cleanURL = QSStrippedURL || url; + const regexMatch = componentsRegex.exec(cleanURL) || []; + const [proto, ...rest] = regexMatch.slice(1).map((match) => match || null); + const path = rest.join(''); + const [ + streamNameOrChannelName, + primaryModSeparator, + primaryModValue, + pathSep, // eslint-disable-line no-unused-vars + possibleStreamName, + secondaryModSeparator, + secondaryModValue, + ] = rest; + const searchParams = new URLSearchParams(qs || ''); + const startTime = searchParams.get('t'); + + // Validate protocol + if (requireProto && !proto) { + throw new Error(__('LBRY URLs must include a protocol prefix (lbry://).')); + } + + // Validate and process name + if (!streamNameOrChannelName) { + throw new Error(__('URL does not include name.')); + } + + rest.forEach((urlPiece) => { + if (urlPiece && urlPiece.includes(' ')) { + throw new Error(__('URL can not include a space')); + } + }); + + const includesChannel = streamNameOrChannelName.startsWith('@'); + const isChannel = streamNameOrChannelName.startsWith('@') && !possibleStreamName; + const channelName = includesChannel && streamNameOrChannelName.slice(1); + + if (includesChannel) { + if (!channelName) { + throw new Error(__('No channel name after @.')); + } + + if (channelName.length < channelNameMinLength) { + throw new Error( + __(`Channel names must be at least %channelNameMinLength% characters.`, { + channelNameMinLength, + }) + ); + } + } + + // Validate and process modifier + const [primaryClaimId, primaryClaimSequence, primaryBidPosition] = parseURIModifier( + primaryModSeparator, + primaryModValue + ); + const [secondaryClaimId, secondaryClaimSequence, secondaryBidPosition] = parseURIModifier( + secondaryModSeparator, + secondaryModValue + ); + const streamName = includesChannel ? possibleStreamName : streamNameOrChannelName; + const streamClaimId = includesChannel ? secondaryClaimId : primaryClaimId; + const channelClaimId = includesChannel && primaryClaimId; + + return { + isChannel, + path, + ...(streamName ? { streamName } : {}), + ...(streamClaimId ? { streamClaimId } : {}), + ...(channelName ? { channelName } : {}), + ...(channelClaimId ? { channelClaimId } : {}), + ...(primaryClaimSequence ? { primaryClaimSequence: parseInt(primaryClaimSequence, 10) } : {}), + ...(secondaryClaimSequence ? { secondaryClaimSequence: parseInt(secondaryClaimSequence, 10) } : {}), + ...(primaryBidPosition ? { primaryBidPosition: parseInt(primaryBidPosition, 10) } : {}), + ...(secondaryBidPosition ? { secondaryBidPosition: parseInt(secondaryBidPosition, 10) } : {}), + ...(startTime ? { startTime: parseInt(startTime, 10) } : {}), + + // The values below should not be used for new uses of parseURI + // They will not work properly with canonical_urls + claimName: streamNameOrChannelName, + claimId: primaryClaimId, + ...(streamName ? { contentName: streamName } : {}), + ...(qs ? { queryString: qs } : {}), + }; +} + +function parseURIModifier(modSeperator: ?string, modValue: ?string) { + let claimId; + let claimSequence; + let bidPosition; + + if (modSeperator) { + if (!modValue) { + throw new Error(__(`No modifier provided after separator %modSeperator%.`, { modSeperator })); + } + + if (modSeperator === MOD_CLAIM_ID_SEPARATOR || MOD_CLAIM_ID_SEPARATOR_OLD) { + claimId = modValue; + } else if (modSeperator === MOD_SEQUENCE_SEPARATOR) { + claimSequence = modValue; + } else if (modSeperator === MOD_BID_POSITION_SEPARATOR) { + bidPosition = modValue; + } + } + + if (claimId && (claimId.length > claimIdMaxLength || !claimId.match(/^[0-9a-f]+$/))) { + throw new Error(__(`Invalid claim ID %claimId%.`, { claimId })); + } + + if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]*$/)) { + throw new Error(__('Claim sequence must be a number.')); + } + + if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]*$/)) { + throw new Error(__('Bid position must be a number.')); + } + + return [claimId, claimSequence, bidPosition]; +} + +/** + * Takes an object in the same format returned by parse() and builds a URI. + * + * The channelName key will accept names with or without the @ prefix. + */ +export function buildURI(UrlObj: LbryUrlObj, includeProto: boolean = true, protoDefault: string = 'lbry://'): string { + const { + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryClaimSequence, + primaryBidPosition, + secondaryClaimSequence, + secondaryBidPosition, + startTime, + ...deprecatedParts + } = UrlObj; + const { claimId, claimName, contentName } = deprecatedParts; + + if (!isProduction) { + if (claimId) { + console.error(__("'claimId' should no longer be used. Use 'streamClaimId' or 'channelClaimId' instead")); + } + if (claimName) { + console.error(__("'claimName' should no longer be used. Use 'streamClaimName' or 'channelClaimName' instead")); + } + if (contentName) { + console.error(__("'contentName' should no longer be used. Use 'streamName' instead")); + } + } + + if (!claimName && !channelName && !streamName) { + console.error( + __("'claimName', 'channelName', and 'streamName' are all empty. One must be present to build a url.") + ); + } + + const formattedChannelName = channelName && (channelName.startsWith('@') ? channelName : `@${channelName}`); + const primaryClaimName = claimName || contentName || formattedChannelName || streamName; + const primaryClaimId = claimId || (formattedChannelName ? channelClaimId : streamClaimId); + const secondaryClaimName = (!claimName && contentName) || (formattedChannelName ? streamName : null); + const secondaryClaimId = secondaryClaimName && streamClaimId; + + return ( + (includeProto ? protoDefault : '') + + // primaryClaimName will always exist here because we throw above if there is no "name" value passed in + // $FlowFixMe + primaryClaimName + + (primaryClaimId ? `#${primaryClaimId}` : '') + + (primaryClaimSequence ? `:${primaryClaimSequence}` : '') + + (primaryBidPosition ? `${primaryBidPosition}` : '') + + (secondaryClaimName ? `/${secondaryClaimName}` : '') + + (secondaryClaimId ? `#${secondaryClaimId}` : '') + + (secondaryClaimSequence ? `:${secondaryClaimSequence}` : '') + + (secondaryBidPosition ? `${secondaryBidPosition}` : '') + + (startTime ? `?t=${startTime}` : '') + ); +} + +/* Takes a parseable LBRY URL and converts it to standard, canonical format */ +export function normalizeURI(URL: string) { + const { + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryClaimSequence, + primaryBidPosition, + secondaryClaimSequence, + secondaryBidPosition, + startTime, + } = parseURI(URL); + + return buildURI({ + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryClaimSequence, + primaryBidPosition, + secondaryClaimSequence, + secondaryBidPosition, + startTime, + }); +} + +export function isURIValid(URL: string): boolean { + try { + parseURI(normalizeURI(URL)); + } catch (error) { + return false; + } + + return true; +} + +export function isNameValid(claimName: string) { + return !regexInvalidURI.test(claimName); +} + +export function isURIClaimable(URL: string) { + let parts; + try { + parts = parseURI(normalizeURI(URL)); + } catch (error) { + return false; + } + + return parts && parts.streamName && !parts.streamClaimId && !parts.isChannel; +} + +export function convertToShareLink(URL: string) { + const { + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryBidPosition, + primaryClaimSequence, + secondaryBidPosition, + secondaryClaimSequence, + } = parseURI(URL); + return buildURI( + { + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryBidPosition, + primaryClaimSequence, + secondaryBidPosition, + secondaryClaimSequence, + }, + true, + 'https://open.lbry.com/' + ); +} + +export function splitBySeparator(uri: string) { + const protocolLength = 7; + return uri.startsWith('lbry://') ? uri.slice(protocolLength).split(/[#:*]/) : uri.split(/#:\*\$/); +} + +export function isURIEqual(uriA: string, uriB: string) { + const parseA = parseURI(normalizeURI(uriA)); + const parseB = parseURI(normalizeURI(uriB)); + if (parseA.isChannel) { + if (parseB.isChannel && parseA.channelClaimId === parseB.channelClaimId) { + return true; + } + } else if (parseA.streamClaimId === parseB.streamClaimId) { + return true; + } else { + return false; + } +} diff --git a/ui/util/merge-claim.js b/ui/util/merge-claim.js new file mode 100644 index 000000000..83262d74d --- /dev/null +++ b/ui/util/merge-claim.js @@ -0,0 +1,7 @@ +/* +new claim = { ...maybeResolvedClaim, ...pendingClaim, meta: maybeResolvedClaim['meta'] } + */ + +export default function mergeClaims(maybeResolved, pending) { + return { ...maybeResolved, ...pending, meta: maybeResolved.meta }; +} diff --git a/ui/util/remark-lbry.js b/ui/util/remark-lbry.js index ad0ea9145..07d1d2a13 100644 --- a/ui/util/remark-lbry.js +++ b/ui/util/remark-lbry.js @@ -1,4 +1,4 @@ -import { parseURI } from 'lbry-redux'; +import { parseURI } from 'util/lbryURI'; import visit from 'unist-util-visit'; const protocol = 'lbry://'; diff --git a/ui/util/search.js b/ui/util/search.js index 968e63761..37a6feec9 100644 --- a/ui/util/search.js +++ b/ui/util/search.js @@ -1,6 +1,6 @@ // @flow -import { isNameValid, isURIValid, normalizeURI, parseURI } from 'lbry-redux'; +import { isNameValid, isURIValid, normalizeURI, parseURI } from 'util/lbryURI'; import { URL as SITE_URL, URL_LOCAL, URL_DEV, SIMPLE_SITE } from 'config'; import { SEARCH_OPTIONS } from 'constants/search'; diff --git a/ui/util/sync-settings.js b/ui/util/sync-settings.js index fa4f8d7bd..a7e1e9c67 100644 --- a/ui/util/sync-settings.js +++ b/ui/util/sync-settings.js @@ -1,4 +1,4 @@ -import { DAEMON_SETTINGS } from 'lbry-redux'; +import * as DAEMON_SETTINGS from 'constants/daemon_settings'; import isEqual from 'util/deep-equal'; export function stringifyServerParam(configList) { @@ -10,7 +10,7 @@ export function stringifyServerParam(configList) { export const getSubsetFromKeysArray = (obj, keys) => Object.keys(obj) - .filter(i => keys.includes(i)) + .filter((i) => keys.includes(i)) .reduce((acc, key) => { acc[key] = obj[key]; return acc; diff --git a/ui/util/transifex-upload.js b/ui/util/transifex-upload.js new file mode 100644 index 000000000..e34a2a258 --- /dev/null +++ b/ui/util/transifex-upload.js @@ -0,0 +1,78 @@ +const apiBaseUrl = 'https://www.transifex.com/api/2/project'; +const resource = 'app-strings'; + +export function doTransifexUpload(contents, project, token, success, fail) { + const url = `${apiBaseUrl}/${project}/resources/`; + const updateUrl = `${apiBaseUrl}/${project}/resource/${resource}/content/`; + const headers = { + Authorization: `Basic ${Buffer.from(`api:${token}`).toString('base64')}`, + 'Content-Type': 'application/json', + }; + + const req = { + accept_translations: true, + i18n_type: 'KEYVALUEJSON', + name: resource, + slug: resource, + content: contents, + }; + + function handleResponse(text) { + let json; + try { + // transifex api returns Python dicts for some reason. + // Any way to get the api to return valid JSON? + json = JSON.parse(text); + } catch (e) { + // ignore + } + + if (success) { + success(json || text); + } + } + + function handleError(err) { + if (fail) { + fail(err.message ? err.message : 'Could not upload strings resource to Transifex'); + } + } + + // check if the resource exists + fetch(updateUrl, { headers }) + .then((response) => response.json()) + .then(() => { + // perform an update + fetch(updateUrl, { + method: 'PUT', + headers, + body: JSON.stringify({ content: contents }), + }) + .then((response) => { + if (response.status !== 200 && response.status !== 201) { + throw new Error('failed to update transifex'); + } + + return response.text(); + }) + .then(handleResponse) + .catch(handleError); + }) + .catch(() => { + // resource doesn't exist, create a fresh resource + fetch(url, { + method: 'POST', + headers, + body: JSON.stringify(req), + }) + .then((response) => { + if (response.status !== 200 && response.status !== 201) { + throw new Error('failed to upload to transifex'); + } + + return response.text(); + }) + .then(handleResponse) + .catch(handleError); + }); +} diff --git a/ui/util/url.js b/ui/util/url.js index ecae03ca6..a0e1dc163 100644 --- a/ui/util/url.js +++ b/ui/util/url.js @@ -1,7 +1,8 @@ // Can't use aliases here because we're doing exports/require const PAGES = require('../constants/pages'); -const { parseURI, buildURI, COLLECTIONS_CONSTS } = require('lbry-redux'); +const { parseURI, buildURI } = require('../util/lbryURI'); +const COLLECTIONS_CONSTS = require('../constants/collections'); function encodeWithSpecialCharEncode(string) { // encodeURIComponent doesn't encode `'` and others diff --git a/ui/util/zoomWindow.js b/ui/util/zoomWindow.js index f0fab1ee5..c4a6b8a37 100644 --- a/ui/util/zoomWindow.js +++ b/ui/util/zoomWindow.js @@ -1,5 +1,5 @@ import { webFrame } from 'electron'; -import { SETTINGS } from 'lbry-redux'; +import * as SETTINGS from 'constants/settings'; const isDev = process.env.NODE_ENV !== 'production'; export const ZOOM = { diff --git a/web/.env.ody b/web/.env.ody new file mode 100644 index 000000000..2d9e1313e --- /dev/null +++ b/web/.env.ody @@ -0,0 +1,92 @@ +# Copy this file to .env to make modifications + +# Base config + +WEBPACK_WEB_PORT=9090 +WEBPACK_ELECTRON_PORT=9091 +WEB_SERVER_PORT=1337 + +WELCOME_VERSION=1.0 + +# Custom Site info +DOMAIN=lbry.tv +URL=https://lbry.tv + +# UI +SITE_TITLE=lbry.tv +SITE_NAME=local.lbry.tv +SITE_DESCRIPTION=Meet LBRY, an open, free, and community-controlled content wonderland. +LOGO_TITLE=local.lbry.tv + +##### ODYSEE SETTINGS ####### + +MATOMO_URL=https://analytics.lbry.com/ +MATOMO_ID=4 + +# Base config +WEBPACK_WEB_PORT=9090 +WEBPACK_ELECTRON_PORT=9091 +WEB_SERVER_PORT=1337 + +## APIS +LBRY_API_URL=https://api.odysee.com +#LBRY_WEB_API=https://api.na-backend.odysee.com +#LBRY_WEB_STREAMING_API=https://cdn.lbryplayer.xyz +# deprecated: +#LBRY_WEB_BUFFER_API=https://collector-service.api.lbry.tv/api/v1/events/video +#COMMENT_SERVER_API=https://comments.lbry.com/api/v2 +WELCOME_VERSION=1.0 + +# STRIPE +STRIPE_PUBLIC_KEY='pk_live_e8M4dRNnCCbmpZzduEUZBgJO' + +## UI + +LOADING_BAR_COLOR=#e50054 + +# IMAGE ASSETS +YRBL_HAPPY_IMG_URL=https://spee.ch/spaceman-happy:a.png +YRBL_SAD_IMG_URL=https://spee.ch/spaceman-sad:d.png +LOGIN_IMG_URL=https://spee.ch/login:b.png +LOGO=https://spee.ch/odysee-logo-png:3.png +LOGO_TEXT_LIGHT=https://spee.ch/odysee-white-png:f.png +LOGO_TEXT_DARK=https://spee.ch/odysee-png:2.png +AVATAR_DEFAULT=https://spee.ch/spaceman-png:2.png +FAVICON=https://spee.ch/favicon-png:c.png + +# LOCALE +DEFAULT_LANGUAGE=en + +## LINKED CONTENT WHITELIST +KNOWN_APP_DOMAINS=open.lbry.com,lbry.tv,lbry.lat,odysee.com + +## CUSTOM CONTENT +# If the following is true, copy custom/homepage.example.js to custom/homepage.js and modify +CUSTOM_HOMEPAGE=true + +# Add channels to auto-follow on firstrun (space delimited) +AUTO_FOLLOW_CHANNELS=lbry://@Odysee#80d2590ad04e36fb1d077a9b9e3a8bba76defdf8 lbry://@OdyseeHelp#b58dfaeab6c70754d792cdd9b56ff59b90aea334 + +## FEATURES AND LIMITS +SIMPLE_SITE=true +BRANDED_SITE=odysee +# SIMPLE_SITE REPLACEMENTS +ENABLE_MATURE=false +ENABLE_UI_NOTIFICATIONS=true +ENABLE_WILD_WEST=true +SHOW_TAGS_INTRO=false + +# CENTRALIZED FEATURES +ENABLE_COMMENT_REACTIONS=true +ENABLE_FILE_REACTIONS=true +ENABLE_CREATOR_REACTIONS=true +ENABLE_NO_SOURCE_CLAIMS=true +ENABLE_PREROLL_ADS=false +SHOW_ADS=true + +CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS=4 +CHANNEL_STAKED_LEVEL_LIVESTREAM=3 +WEB_PUBLISH_SIZE_LIMIT_GB=4 + +#SEARCH TYPES - comma-delimited +LIGHTHOUSE_DEFAULT_TYPES=audio,video diff --git a/web/component/ads/index.js b/web/component/ads/index.js index 6724d4bd0..98730c034 100644 --- a/web/component/ads/index.js +++ b/web/component/ads/index.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import { selectTheme } from 'redux/selectors/settings'; -import { makeSelectClaimForUri, makeSelectClaimIsNsfw } from 'lbry-redux'; +import { makeSelectClaimForUri, makeSelectClaimIsNsfw } from 'redux/selectors/claims'; import Ads from './view'; const select = (state, props) => ({ theme: selectTheme(state), diff --git a/web/component/fileViewerEmbeddedEnded/index.js b/web/component/fileViewerEmbeddedEnded/index.js index 1903aec38..4a53330ea 100644 --- a/web/component/fileViewerEmbeddedEnded/index.js +++ b/web/component/fileViewerEmbeddedEnded/index.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import fileViewerEmbeddedEnded from './view'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; -import { makeSelectTagInClaimOrChannelForUri } from 'lbry-redux'; +import { makeSelectTagInClaimOrChannelForUri } from 'redux/selectors/claims'; import { PREFERENCE_EMBED } from 'constants/tags'; export default connect((state, props) => ({ diff --git a/web/lbry.js b/web/lbry.js new file mode 100644 index 000000000..c78c201de --- /dev/null +++ b/web/lbry.js @@ -0,0 +1,247 @@ +// @flow +require('proxy-polyfill'); + +const CHECK_DAEMON_STARTED_TRY_NUMBER = 200; +// +// Basic LBRY sdk connection config +// Offers a proxy to call LBRY sdk methods + +// +const Lbry = { + isConnected: false, + connectPromise: null, + daemonConnectionString: 'http://localhost:5279', + alternateConnectionString: '', + methodsUsingAlternateConnectionString: [], + apiRequestHeaders: { 'Content-Type': 'application/json-rpc' }, + + // Allow overriding daemon connection string (e.g. to `/api/proxy` for lbryweb) + setDaemonConnectionString: (value) => { + Lbry.daemonConnectionString = value; + }, + + setApiHeader: (key, value) => { + Lbry.apiRequestHeaders = Object.assign(Lbry.apiRequestHeaders, { [key]: value }); + }, + + unsetApiHeader: (key) => { + Object.keys(Lbry.apiRequestHeaders).includes(key) && delete Lbry.apiRequestHeaders['key']; + }, + // Allow overriding Lbry methods + overrides: {}, + setOverride: (methodName, newMethod) => { + Lbry.overrides[methodName] = newMethod; + }, + getApiRequestHeaders: () => Lbry.apiRequestHeaders, + + // Returns a human readable media type based on the content type or extension of a file that is returned by the sdk + getMediaType: (contentType, fileName) => { + if (fileName) { + const formats = [ + [/\.(mp4|m4v|webm|flv|f4v|ogv)$/i, 'video'], + [/\.(mp3|m4a|aac|wav|flac|ogg|opus)$/i, 'audio'], + [/\.(jpeg|jpg|png|gif|svg|webp)$/i, 'image'], + [/\.(h|go|ja|java|js|jsx|c|cpp|cs|css|rb|scss|sh|php|py)$/i, 'script'], + [/\.(html|json|csv|txt|log|md|markdown|docx|pdf|xml|yml|yaml)$/i, 'document'], + [/\.(pdf|odf|doc|docx|epub|org|rtf)$/i, 'e-book'], + [/\.(stl|obj|fbx|gcode)$/i, '3D-file'], + [/\.(cbr|cbt|cbz)$/i, 'comic-book'], + [/\.(lbry)$/i, 'application'], + ]; + + const res = formats.reduce((ret, testpair) => { + switch (testpair[0].test(ret)) { + case true: + return testpair[1]; + default: + return ret; + } + }, fileName); + return res === fileName ? 'unknown' : res; + } else if (contentType) { + // $FlowFixMe + return /^[^/]+/.exec(contentType)[0]; + } + + return 'unknown'; + }, + + // + // Lbry SDK Methods + // https://lbry.tech/api/sdk + // + status: (params = {}) => daemonCallWithResult('status', params), + stop: () => daemonCallWithResult('stop', {}), + version: () => daemonCallWithResult('version', {}), + + // Claim fetching and manipulation + resolve: (params) => daemonCallWithResult('resolve', params), + get: (params) => daemonCallWithResult('get', params), + claim_search: (params) => daemonCallWithResult('claim_search', params), + claim_list: (params) => daemonCallWithResult('claim_list', params), + channel_create: (params) => daemonCallWithResult('channel_create', params), + channel_update: (params) => daemonCallWithResult('channel_update', params), + channel_import: (params) => daemonCallWithResult('channel_import', params), + channel_list: (params) => daemonCallWithResult('channel_list', params), + stream_abandon: (params) => daemonCallWithResult('stream_abandon', params), + stream_list: (params) => daemonCallWithResult('stream_list', params), + channel_abandon: (params) => daemonCallWithResult('channel_abandon', params), + channel_sign: (params) => daemonCallWithResult('channel_sign', params), + support_create: (params) => daemonCallWithResult('support_create', params), + support_list: (params) => daemonCallWithResult('support_list', params), + stream_repost: (params) => daemonCallWithResult('stream_repost', params), + collection_resolve: (params) => daemonCallWithResult('collection_resolve', params), + collection_list: (params) => daemonCallWithResult('collection_list', params), + collection_create: (params) => daemonCallWithResult('collection_create', params), + collection_update: (params) => daemonCallWithResult('collection_update', params), + + // File fetching and manipulation + file_list: (params = {}) => daemonCallWithResult('file_list', params), + file_delete: (params = {}) => daemonCallWithResult('file_delete', params), + file_set_status: (params = {}) => daemonCallWithResult('file_set_status', params), + blob_delete: (params = {}) => daemonCallWithResult('blob_delete', params), + blob_list: (params = {}) => daemonCallWithResult('blob_list', params), + + // Wallet utilities + wallet_balance: (params = {}) => daemonCallWithResult('wallet_balance', params), + wallet_decrypt: () => daemonCallWithResult('wallet_decrypt', {}), + wallet_encrypt: (params = {}) => daemonCallWithResult('wallet_encrypt', params), + wallet_unlock: (params = {}) => daemonCallWithResult('wallet_unlock', params), + wallet_list: (params = {}) => daemonCallWithResult('wallet_list', params), + wallet_send: (params = {}) => daemonCallWithResult('wallet_send', params), + wallet_status: (params = {}) => daemonCallWithResult('wallet_status', params), + address_is_mine: (params = {}) => daemonCallWithResult('address_is_mine', params), + address_unused: (params = {}) => daemonCallWithResult('address_unused', params), + address_list: (params = {}) => daemonCallWithResult('address_list', params), + transaction_list: (params = {}) => daemonCallWithResult('transaction_list', params), + utxo_release: (params = {}) => daemonCallWithResult('utxo_release', params), + support_abandon: (params = {}) => daemonCallWithResult('support_abandon', params), + purchase_list: (params = {}) => daemonCallWithResult('purchase_list', params), + txo_list: (params = {}) => daemonCallWithResult('txo_list', params), + + sync_hash: (params = {}) => daemonCallWithResult('sync_hash', params), + sync_apply: (params = {}) => daemonCallWithResult('sync_apply', params), + + // Preferences + preference_get: (params = {}) => daemonCallWithResult('preference_get', params), + preference_set: (params = {}) => daemonCallWithResult('preference_set', params), + + // Comments + comment_list: (params = {}) => daemonCallWithResult('comment_list', params), + comment_create: (params = {}) => daemonCallWithResult('comment_create', params), + comment_hide: (params = {}) => daemonCallWithResult('comment_hide', params), + comment_abandon: (params = {}) => daemonCallWithResult('comment_abandon', params), + comment_update: (params = {}) => daemonCallWithResult('comment_update', params), + + // Connect to the sdk + connect: () => { + if (Lbry.connectPromise === null) { + Lbry.connectPromise = new Promise((resolve, reject) => { + let tryNum = 0; + // Check every half second to see if the daemon is accepting connections + function checkDaemonStarted() { + tryNum += 1; + Lbry.status() + .then(resolve) + .catch(() => { + if (tryNum <= CHECK_DAEMON_STARTED_TRY_NUMBER) { + setTimeout(checkDaemonStarted, tryNum < 50 ? 400 : 1000); + } else { + reject(new Error('Unable to connect to LBRY')); + } + }); + } + + checkDaemonStarted(); + }); + } + + // Flow thinks this could be empty, but it will always reuturn a promise + // $FlowFixMe + return Lbry.connectPromise; + }, + + publish: (params = {}) => + new Promise((resolve, reject) => { + if (Lbry.overrides.publish) { + Lbry.overrides.publish(params).then(resolve, reject); + } else { + apiCall('publish', params, resolve, reject); + } + }), +}; + +function checkAndParse(response) { + if (response.status >= 200 && response.status < 300) { + return response.json(); + } + return response.json().then((json) => { + let error; + if (json.error) { + const errorMessage = typeof json.error === 'object' ? json.error.message : json.error; + error = new Error(errorMessage); + } else { + error = new Error('Protocol error with unknown response signature'); + } + return Promise.reject(error); + }); +} + +function apiCall(method, params, resolve, reject) { + const counter = new Date().getTime(); + const options = { + method: 'POST', + headers: Lbry.apiRequestHeaders, + body: JSON.stringify({ + jsonrpc: '2.0', + method, + params, + id: counter, + }), + }; + + const connectionString = Lbry.methodsUsingAlternateConnectionString.includes(method) + ? Lbry.alternateConnectionString + : Lbry.daemonConnectionString; + return fetch(connectionString + '?m=' + method, options) + .then(checkAndParse) + .then((response) => { + const error = response.error || (response.result && response.result.error); + + if (error) { + return reject(error); + } + return resolve(response.result); + }) + .catch(reject); +} + +function daemonCallWithResult(name, params = {}) { + return new Promise((resolve, reject) => { + apiCall( + name, + params, + (result) => { + resolve(result); + }, + reject + ); + }); +} + +// This is only for a fallback +// If there is a Lbry method that is being called by an app, it should be added to /flow-typed/Lbry.js +const lbryProxy = new Proxy(Lbry, { + get(target, name) { + if (name in target) { + return target[name]; + } + + return (params = {}) => + new Promise((resolve, reject) => { + apiCall(name, params, resolve, reject); + }); + }, +}); + +module.exports = { lbryProxy, apiCall }; diff --git a/web/package.json b/web/package.json index c75e33d12..a74fb14e0 100644 --- a/web/package.json +++ b/web/package.json @@ -31,8 +31,6 @@ "koa-logger": "^3.2.1", "koa-send": "^5.0.0", "koa-static": "^5.0.0", - "lbry-redux": "lbryio/lbry-redux#508e8d36fd91106beb7d6b4edb9f726dae0e6264", - "lbryinc": "lbryio/lbryinc#8f9a58bfc8312a65614fd7327661cdcc502c4e59", "mysql": "^2.17.1", "node-fetch": "^2.6.1", "uuid": "^8.3.0" diff --git a/web/src/html.js b/web/src/html.js index 8367814cb..3ff8c6cd8 100644 --- a/web/src/html.js +++ b/web/src/html.js @@ -12,11 +12,11 @@ const { LBRY_WEB_API, } = require('../../config.js'); -const { Lbry } = require('lbry-redux'); +const { lbryProxy: Lbry } = require('../lbry'); const { generateEmbedUrl, generateStreamUrl, generateDirectUrl } = require('../../ui/util/web'); const PAGES = require('../../ui/constants/pages'); const { CATEGORY_METADATA } = require('./category-metadata'); -const { parseURI, normalizeURI } = require('lbry-redux'); +const { parseURI, normalizeURI } = require('./lbryURI'); const fs = require('fs'); const path = require('path'); const moment = require('moment'); diff --git a/web/src/lbryURI.js b/web/src/lbryURI.js new file mode 100644 index 000000000..34e071f27 --- /dev/null +++ b/web/src/lbryURI.js @@ -0,0 +1,342 @@ +// @flow +const isProduction = process.env.NODE_ENV === 'production'; +const channelNameMinLength = 1; +const claimIdMaxLength = 40; + +// see https://spec.lbry.com/#urls +const regexInvalidURI = /[ =&#:$@%?;/\\"<>%{}|^~[\]`\u{0000}-\u{0008}\u{000b}-\u{000c}\u{000e}-\u{001F}\u{D800}-\u{DFFF}\u{FFFE}-\u{FFFF}]/u; +// const regexAddress = /^(b|r)(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/; +const regexPartProtocol = '^((?:lbry://)?)'; +const regexPartStreamOrChannelName = '([^:$#/]*)'; +const regexPartModifierSeparator = '([:$#]?)([^/]*)'; +const queryStringBreaker = '^([\\S]+)([?][\\S]*)'; +const separateQuerystring = new RegExp(queryStringBreaker); + +const MOD_SEQUENCE_SEPARATOR = '*'; +const MOD_CLAIM_ID_SEPARATOR_OLD = '#'; +const MOD_CLAIM_ID_SEPARATOR = ':'; +const MOD_BID_POSITION_SEPARATOR = '$'; + +/** + * Parses a LBRY name into its component parts. Throws errors with user-friendly + * messages for invalid names. + * + * Returns a dictionary with keys: + * - path (string) + * - isChannel (boolean) + * - streamName (string, if present) + * - streamClaimId (string, if present) + * - channelName (string, if present) + * - channelClaimId (string, if present) + * - primaryClaimSequence (int, if present) + * - secondaryClaimSequence (int, if present) + * - primaryBidPosition (int, if present) + * - secondaryBidPosition (int, if present) + */ + +function parseURI(url, requireProto = false) { + // Break into components. Empty sub-matches are converted to null + + const componentsRegex = new RegExp( + regexPartProtocol + // protocol + regexPartStreamOrChannelName + // stream or channel name (stops at the first separator or end) + regexPartModifierSeparator + // modifier separator, modifier (stops at the first path separator or end) + '(/?)' + // path separator, there should only be one (optional) slash to separate the stream and channel parts + regexPartStreamOrChannelName + + regexPartModifierSeparator + ); + // chop off the querystring first + let QSStrippedURL, qs; + const qsRegexResult = separateQuerystring.exec(url); + if (qsRegexResult) { + [QSStrippedURL, qs] = qsRegexResult.slice(1).map((match) => match || null); + } + + const cleanURL = QSStrippedURL || url; + const regexMatch = componentsRegex.exec(cleanURL) || []; + const [proto, ...rest] = regexMatch.slice(1).map((match) => match || null); + const path = rest.join(''); + const [ + streamNameOrChannelName, + primaryModSeparator, + primaryModValue, + pathSep, // eslint-disable-line no-unused-vars + possibleStreamName, + secondaryModSeparator, + secondaryModValue, + ] = rest; + const searchParams = new URLSearchParams(qs || ''); + const startTime = searchParams.get('t'); + + // Validate protocol + if (requireProto && !proto) { + throw new Error(__('LBRY URLs must include a protocol prefix (lbry://).')); + } + + // Validate and process name + if (!streamNameOrChannelName) { + throw new Error(__('URL does not include name.')); + } + + rest.forEach((urlPiece) => { + if (urlPiece && urlPiece.includes(' ')) { + throw new Error(__('URL can not include a space')); + } + }); + + const includesChannel = streamNameOrChannelName.startsWith('@'); + const isChannel = streamNameOrChannelName.startsWith('@') && !possibleStreamName; + const channelName = includesChannel && streamNameOrChannelName.slice(1); + + if (includesChannel) { + if (!channelName) { + throw new Error(__('No channel name after @.')); + } + + if (channelName.length < channelNameMinLength) { + throw new Error( + __(`Channel names must be at least %channelNameMinLength% characters.`, { + channelNameMinLength, + }) + ); + } + } + + // Validate and process modifier + const [primaryClaimId, primaryClaimSequence, primaryBidPosition] = parseURIModifier( + primaryModSeparator, + primaryModValue + ); + const [secondaryClaimId, secondaryClaimSequence, secondaryBidPosition] = parseURIModifier( + secondaryModSeparator, + secondaryModValue + ); + const streamName = includesChannel ? possibleStreamName : streamNameOrChannelName; + const streamClaimId = includesChannel ? secondaryClaimId : primaryClaimId; + const channelClaimId = includesChannel && primaryClaimId; + + return { + isChannel, + path, + ...(streamName ? { streamName } : {}), + ...(streamClaimId ? { streamClaimId } : {}), + ...(channelName ? { channelName } : {}), + ...(channelClaimId ? { channelClaimId } : {}), + ...(primaryClaimSequence ? { primaryClaimSequence: parseInt(primaryClaimSequence, 10) } : {}), + ...(secondaryClaimSequence ? { secondaryClaimSequence: parseInt(secondaryClaimSequence, 10) } : {}), + ...(primaryBidPosition ? { primaryBidPosition: parseInt(primaryBidPosition, 10) } : {}), + ...(secondaryBidPosition ? { secondaryBidPosition: parseInt(secondaryBidPosition, 10) } : {}), + ...(startTime ? { startTime: parseInt(startTime, 10) } : {}), + + // The values below should not be used for new uses of parseURI + // They will not work properly with canonical_urls + claimName: streamNameOrChannelName, + claimId: primaryClaimId, + ...(streamName ? { contentName: streamName } : {}), + ...(qs ? { queryString: qs } : {}), + }; +} + +function parseURIModifier(modSeperator, modValue) { + let claimId; + let claimSequence; + let bidPosition; + + if (modSeperator) { + if (!modValue) { + throw new Error(__(`No modifier provided after separator %modSeperator%.`, { modSeperator })); + } + + if (modSeperator === MOD_CLAIM_ID_SEPARATOR || MOD_CLAIM_ID_SEPARATOR_OLD) { + claimId = modValue; + } else if (modSeperator === MOD_SEQUENCE_SEPARATOR) { + claimSequence = modValue; + } else if (modSeperator === MOD_BID_POSITION_SEPARATOR) { + bidPosition = modValue; + } + } + + if (claimId && (claimId.length > claimIdMaxLength || !claimId.match(/^[0-9a-f]+$/))) { + throw new Error(__(`Invalid claim ID %claimId%.`, { claimId })); + } + + if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]*$/)) { + throw new Error(__('Claim sequence must be a number.')); + } + + if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]*$/)) { + throw new Error(__('Bid position must be a number.')); + } + + return [claimId, claimSequence, bidPosition]; +} + +/** + * Takes an object in the same format returned by parse() and builds a URI. + * + * The channelName key will accept names with or without the @ prefix. + */ +function buildURI(UrlObj, includeProto = true, protoDefault = 'lbry://') { + const { + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryClaimSequence, + primaryBidPosition, + secondaryClaimSequence, + secondaryBidPosition, + startTime, + ...deprecatedParts + } = UrlObj; + const { claimId, claimName, contentName } = deprecatedParts; + + if (!isProduction) { + if (claimId) { + console.error(__("'claimId' should no longer be used. Use 'streamClaimId' or 'channelClaimId' instead")); + } + if (claimName) { + console.error(__("'claimName' should no longer be used. Use 'streamClaimName' or 'channelClaimName' instead")); + } + if (contentName) { + console.error(__("'contentName' should no longer be used. Use 'streamName' instead")); + } + } + + if (!claimName && !channelName && !streamName) { + console.error( + __("'claimName', 'channelName', and 'streamName' are all empty. One must be present to build a url.") + ); + } + + const formattedChannelName = channelName && (channelName.startsWith('@') ? channelName : `@${channelName}`); + const primaryClaimName = claimName || contentName || formattedChannelName || streamName; + const primaryClaimId = claimId || (formattedChannelName ? channelClaimId : streamClaimId); + const secondaryClaimName = (!claimName && contentName) || (formattedChannelName ? streamName : null); + const secondaryClaimId = secondaryClaimName && streamClaimId; + + return ( + (includeProto ? protoDefault : '') + + // primaryClaimName will always exist here because we throw above if there is no "name" value passed in + // $FlowFixMe + primaryClaimName + + (primaryClaimId ? `#${primaryClaimId}` : '') + + (primaryClaimSequence ? `:${primaryClaimSequence}` : '') + + (primaryBidPosition ? `${primaryBidPosition}` : '') + + (secondaryClaimName ? `/${secondaryClaimName}` : '') + + (secondaryClaimId ? `#${secondaryClaimId}` : '') + + (secondaryClaimSequence ? `:${secondaryClaimSequence}` : '') + + (secondaryBidPosition ? `${secondaryBidPosition}` : '') + + (startTime ? `?t=${startTime}` : '') + ); +} + +/* Takes a parseable LBRY URL and converts it to standard, canonical format */ +function normalizeURI(URL) { + const { + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryClaimSequence, + primaryBidPosition, + secondaryClaimSequence, + secondaryBidPosition, + startTime, + } = parseURI(URL); + + return buildURI({ + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryClaimSequence, + primaryBidPosition, + secondaryClaimSequence, + secondaryBidPosition, + startTime, + }); +} + +function isURIValid(URL) { + try { + parseURI(normalizeURI(URL)); + } catch (error) { + return false; + } + + return true; +} + +function isNameValid(claimName) { + return !regexInvalidURI.test(claimName); +} + +function isURIClaimable(URL) { + let parts; + try { + parts = parseURI(normalizeURI(URL)); + } catch (error) { + return false; + } + + return parts && parts.streamName && !parts.streamClaimId && !parts.isChannel; +} + +function convertToShareLink(URL) { + const { + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryBidPosition, + primaryClaimSequence, + secondaryBidPosition, + secondaryClaimSequence, + } = parseURI(URL); + return buildURI( + { + streamName, + streamClaimId, + channelName, + channelClaimId, + primaryBidPosition, + primaryClaimSequence, + secondaryBidPosition, + secondaryClaimSequence, + }, + true, + 'https://open.lbry.com/' + ); +} + +function splitBySeparator(uri) { + const protocolLength = 7; + return uri.startsWith('lbry://') ? uri.slice(protocolLength).split(/[#:*]/) : uri.split(/#:\*\$/); +} + +function isURIEqual(uriA, uriB) { + const parseA = parseURI(normalizeURI(uriA)); + const parseB = parseURI(normalizeURI(uriB)); + if (parseA.isChannel) { + if (parseB.isChannel && parseA.channelClaimId === parseB.channelClaimId) { + return true; + } + } else if (parseA.streamClaimId === parseB.streamClaimId) { + return true; + } else { + return false; + } +} + +module.exports = { + parseURI, + buildURI, + normalizeURI, + isURIValid, + isURIEqual, + isNameValid, + isURIClaimable, + splitBySeparator, + convertToShareLink, +}; diff --git a/web/src/rss.js b/web/src/rss.js index 1dfe52535..0974000e3 100644 --- a/web/src/rss.js +++ b/web/src/rss.js @@ -1,6 +1,6 @@ const { generateStreamUrl } = require('../../ui/util/web'); const { URL, SITE_NAME, LBRY_WEB_API } = require('../../config.js'); -const { Lbry } = require('lbry-redux'); +const { lbryProxy: Lbry } = require('../lbry'); const Rss = require('rss'); const Mime = require('mime-types'); diff --git a/web/webpack.config.js b/web/webpack.config.js index 120747802..47288fa52 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -170,7 +170,7 @@ const webConfig = { modules: [UI_ROOT, __dirname], alias: { - lbryinc: 'lbryinc/dist/bundle.es.js', + // lbryinc: '../extras/lbryinc', electron: `${WEB_PLATFORM_ROOT}/stubs/electron.js`, fs: `${WEB_PLATFORM_ROOT}/stubs/fs.js`, }, diff --git a/web/yarn.lock b/web/yarn.lock index 3f9d5701e..ba82cc85f 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -3331,20 +3331,6 @@ latest-version@^3.0.0: dependencies: package-json "^4.0.0" -lbry-redux@lbryio/lbry-redux#508e8d36fd91106beb7d6b4edb9f726dae0e6264: - version "0.0.1" - resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/508e8d36fd91106beb7d6b4edb9f726dae0e6264" - dependencies: - proxy-polyfill "0.1.6" - reselect "^3.0.0" - uuid "^8.3.1" - -lbryinc@lbryio/lbryinc#8f9a58bfc8312a65614fd7327661cdcc502c4e59: - version "0.0.1" - resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/8f9a58bfc8312a65614fd7327661cdcc502c4e59" - dependencies: - reselect "^3.0.0" - lcid@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" @@ -4290,11 +4276,6 @@ proxy-addr@~2.0.5: forwarded "~0.1.2" ipaddr.js "1.9.0" -proxy-polyfill@0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/proxy-polyfill/-/proxy-polyfill-0.1.6.tgz#ef41ec6c66f534db15db36c54493a62d184b364e" - integrity sha1-70HsbGb1NNsV2zbFRJOmLRhLNk4= - prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -4561,11 +4542,6 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= -reselect@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147" - integrity sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc= - resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -5416,11 +5392,6 @@ uuid@^8.3.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== -uuid@^8.3.1: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - vary@^1.1.2, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" diff --git a/webpack.base.config.js b/webpack.base.config.js index eca42f75d..fad7ab3ca 100644 --- a/webpack.base.config.js +++ b/webpack.base.config.js @@ -78,7 +78,7 @@ let baseConfig = { homepage: 'util/homepage.js', homepages: process.env.CUSTOM_HOMEPAGE === 'true' ? path.resolve(__dirname, 'custom/homepages/v2/index.js') : ('homepages/index.js'), memes: process.env.CUSTOM_HOMEPAGE === 'true' ? path.resolve(__dirname, 'custom/homepages/meme/index.js') : path.resolve(__dirname, 'homepages/meme/index.js'), - lbryinc: 'lbryinc/dist/bundle.es.js', + lbryinc: 'extras/lbryinc', // Build optimizations for 'redux-persist-transform-filter' 'redux-persist-transform-filter': 'redux-persist-transform-filter/index.js', 'lodash.get': 'lodash-es/get', diff --git a/yarn.lock b/yarn.lock index 22e0a95f3..27c13a1ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10173,21 +10173,6 @@ lazy-val@^1.0.4: yargs "^13.2.2" zstd-codec "^0.1.1" -lbry-redux@lbryio/lbry-redux#32b578707116d45f5b51b7ab523d200e75668676: - version "0.0.1" - resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/32b578707116d45f5b51b7ab523d200e75668676" - dependencies: - "@ungap/from-entries" "^0.2.1" - proxy-polyfill "0.1.6" - reselect "^3.0.0" - uuid "^8.3.1" - -lbryinc@lbryio/lbryinc#0b4e41ef90d6347819dd3453f2f9398a5c1b4f36: - version "0.0.1" - resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/0b4e41ef90d6347819dd3453f2f9398a5c1b4f36" - dependencies: - reselect "^3.0.0" - lcid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" @@ -13077,6 +13062,7 @@ proxy-from-env@^1.0.0: proxy-polyfill@0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/proxy-polyfill/-/proxy-polyfill-0.1.6.tgz#ef41ec6c66f534db15db36c54493a62d184b364e" + integrity sha1-70HsbGb1NNsV2zbFRJOmLRhLNk4= prr@~1.0.1: version "1.0.1" @@ -14227,10 +14213,6 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= -reselect@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147" - reselect@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" @@ -16513,7 +16495,7 @@ uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" -uuid@^8.3.1, uuid@^8.3.2: +uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==