diff --git a/.eslintrc.json b/.eslintrc.json index 56319633d..eb8c5c0fa 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -34,6 +34,7 @@ "singleQuote": true }], "func-names": ["warn", "as-needed"], - "jsx-a11y/label-has-for": 0 + "jsx-a11y/label-has-for": 0, + "import/prefer-default-export": 0 } } diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..c6cc8c819 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ +<component name="InspectionProjectProfileManager"> + <profile version="1.0"> + <option name="myName" value="Project Default" /> + <inspection_tool class="Eslint" enabled="true" level="ERROR" enabled_by_default="true" /> + </profile> +</component> \ No newline at end of file diff --git a/.idea/lbry-app.iml b/.idea/lbry-app.iml new file mode 100644 index 000000000..24643cc37 --- /dev/null +++ b/.idea/lbry-app.iml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="WEB_MODULE" version="4"> + <component name="NewModuleRootManager"> + <content url="file://$MODULE_DIR$"> + <excludeFolder url="file://$MODULE_DIR$/.tmp" /> + <excludeFolder url="file://$MODULE_DIR$/temp" /> + <excludeFolder url="file://$MODULE_DIR$/tmp" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..3668dc8ca --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="JavaScriptSettings"> + <option name="languageLevel" value="FLOW" /> + </component> +</project> \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..b37b46a86 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/lbry-app.iml" filepath="$PROJECT_DIR$/.idea/lbry-app.iml" /> + </modules> + </component> +</project> \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..94a25f7f4 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="$PROJECT_DIR$" vcs="Git" /> + </component> +</project> \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 000000000..a51b0ec12 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,547 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ChangeListManager"> + <list default="true" id="061e089e-71dc-4d6b-b27c-9c614b097257" name="Default" comment=""> + <change type="NEW" beforePath="" afterPath="$PROJECT_DIR$/src/renderer/types/claim.js" /> + <change type="MODIFICATION" beforePath="$PROJECT_DIR$/src/renderer/component/fileDetails/view.jsx" afterPath="$PROJECT_DIR$/src/renderer/component/fileDetails/view.jsx" /> + <change type="MODIFICATION" beforePath="$PROJECT_DIR$/src/renderer/component/uriIndicator/view.jsx" afterPath="$PROJECT_DIR$/src/renderer/component/uriIndicator/view.jsx" /> + <change type="MODIFICATION" beforePath="$PROJECT_DIR$/src/renderer/component/video/view.jsx" afterPath="$PROJECT_DIR$/src/renderer/component/video/view.jsx" /> + <change type="MODIFICATION" beforePath="$PROJECT_DIR$/src/renderer/component/walletSendTip/view.jsx" afterPath="$PROJECT_DIR$/src/renderer/component/walletSendTip/view.jsx" /> + <change type="MODIFICATION" beforePath="$PROJECT_DIR$/src/renderer/page/channel/view.jsx" afterPath="$PROJECT_DIR$/src/renderer/page/channel/view.jsx" /> + <change type="MODIFICATION" beforePath="$PROJECT_DIR$/src/renderer/page/file/view.jsx" afterPath="$PROJECT_DIR$/src/renderer/page/file/view.jsx" /> + <change type="MODIFICATION" beforePath="$PROJECT_DIR$/src/renderer/page/show/view.jsx" afterPath="$PROJECT_DIR$/src/renderer/page/show/view.jsx" /> + <change type="MODIFICATION" beforePath="$PROJECT_DIR$/src/renderer/page/subscriptions/view.jsx" afterPath="$PROJECT_DIR$/src/renderer/page/subscriptions/view.jsx" /> + </list> + <ignored path="$PROJECT_DIR$/.tmp/" /> + <ignored path="$PROJECT_DIR$/temp/" /> + <ignored path="$PROJECT_DIR$/tmp/" /> + <option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" /> + <option name="TRACKING_ENABLED" value="true" /> + <option name="SHOW_DIALOG" value="false" /> + <option name="HIGHLIGHT_CONFLICTS" value="true" /> + <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" /> + <option name="LAST_RESOLUTION" value="IGNORE" /> + </component> + <component name="FileEditorManager"> + <leaf SIDE_TABS_SIZE_LIMIT_KEY="300"> + <file leaf-file-name="view.jsx" pinned="false" current-in-tab="false"> + <entry file="file://$PROJECT_DIR$/src/renderer/modal/modalRouter/view.jsx"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="0"> + <caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" /> + <folding> + <element signature="e#0#26#0" expanded="true" /> + <marker date="1526068149928" expanded="true" signature="5366:5471" ph="{...}" /> + </folding> + </state> + </provider> + </entry> + </file> + <file leaf-file-name="index.js" pinned="false" current-in-tab="false"> + <entry file="file://$PROJECT_DIR$/src/renderer/modal/modalRouter/index.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="216"> + <caret line="12" column="0" lean-forward="false" selection-start-line="12" selection-start-column="0" selection-end-line="12" selection-end-column="0" /> + <folding> + <marker date="1525123536618" expanded="true" signature="1172:1177" ph="{...}" /> + </folding> + </state> + </provider> + </entry> + </file> + <file leaf-file-name="package.json" pinned="false" current-in-tab="false"> + <entry file="file://$PROJECT_DIR$/package.json"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="1332"> + <caret line="74" column="0" lean-forward="false" selection-start-line="74" selection-start-column="0" selection-end-line="74" selection-end-column="0" /> + <folding> + <marker date="1526397284183" expanded="true" signature="2668:3140" ph="{"axios": "^0.18.0"...}" /> + </folding> + </state> + </provider> + </entry> + </file> + <file leaf-file-name="join.js" pinned="false" current-in-tab="false"> + <entry file="file://$PROJECT_DIR$/node_modules/bluebird/js/release/join.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="144"> + <caret line="8" column="11" lean-forward="false" selection-start-line="8" selection-start-column="11" selection-end-line="8" selection-end-column="11" /> + <folding /> + </state> + </provider> + </entry> + </file> + <file leaf-file-name="view.jsx" pinned="false" current-in-tab="true"> + <entry file="file://$PROJECT_DIR$/src/renderer/page/file/view.jsx"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="287"> + <caret line="18" column="15" lean-forward="false" selection-start-line="18" selection-start-column="14" selection-end-line="18" selection-end-column="15" /> + <folding /> + </state> + </provider> + </entry> + </file> + </leaf> + </component> + <component name="FindInProjectRecents"> + <findStrings> + <find>TYPE_FEATURED_DOWNLOAD</find> + <find>TYPE_FEATURED_DOW</find> + <find>doOpen</find> + <find>doNotify</find> + <find>settings</find> + </findStrings> + </component> + <component name="Git.Settings"> + <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> + </component> + <component name="IdeDocumentHistory"> + <option name="CHANGED_PATHS"> + <list> + <option value="$PROJECT_DIR$/src/renderer/modal/modalRouter/index.js" /> + <option value="$PROJECT_DIR$/package.json" /> + <option value="$PROJECT_DIR$/README.md" /> + <option value="$PROJECT_DIR$/src/renderer/page/file/view.jsx" /> + </list> + </option> + </component> + <component name="JsBuildToolGruntFileManager" detection-done="true" sorting="DEFINITION_ORDER" /> + <component name="JsBuildToolPackageJson" detection-done="true" sorting="DEFINITION_ORDER"> + <package-json value="$PROJECT_DIR$/package.json" /> + </component> + <component name="JsFlowSettings"> + <service-enabled>true</service-enabled> + <exe-path /> + <annotation-enable>false</annotation-enable> + <other-services-enabled>true</other-services-enabled> + <auto-save>true</auto-save> + </component> + <component name="JsGulpfileManager"> + <detection-done>true</detection-done> + <sorting>DEFINITION_ORDER</sorting> + </component> + <component name="NodeModulesDirectoryManager"> + <handled-path value="$PROJECT_DIR$/dist/linux-unpacked/resources/app.asar.unpacked/node_modules" /> + <handled-path value="$PROJECT_DIR$/node_modules" /> + </component> + <component name="PhpWorkspaceProjectConfiguration" backward_compatibility_performed="true" /> + <component name="ProjectFrameBounds" extendedState="6"> + <option name="y" value="24" /> + <option name="width" value="1920" /> + <option name="height" value="1056" /> + </component> + <component name="ProjectLevelVcsManager" settingsEditedManually="true" /> + <component name="ProjectView"> + <navigator currentView="ProjectPane" proportions="" version="1"> + <flattenPackages /> + <showMembers /> + <showModules /> + <showLibraryContents /> + <hideEmptyPackages /> + <abbreviatePackageNames /> + <autoscrollToSource /> + <autoscrollFromSource /> + <sortByType /> + <manualOrder /> + <foldersAlwaysOnTop value="true" /> + </navigator> + <panes> + <pane id="Scope" /> + <pane id="ProjectPane"> + <subPane> + <expand> + <path> + <item name="lbry-app" type="b2602c69:ProjectViewProjectNode" /> + <item name="lbry-app" type="2a2b976b:PhpTreeStructureProvider$1" /> + </path> + </expand> + <select /> + </subPane> + </pane> + <pane id="Scratches" /> + </panes> + </component> + <component name="PropertiesComponent"> + <property name="WebServerToolWindowFactoryState" value="false" /> + <property name="nodejs_interpreter_path" value="/usr/local/bin/node" /> + <property name="HbShouldOpenHtmlAsHb" value="" /> + <property name="node.js.path.for.package.eslint" value="project" /> + <property name="node.js.detected.package.eslint" value="true" /> + <property name="node.js.selected.package.eslint" value="$PROJECT_DIR$/node_modules/eslint" /> + <property name="last_opened_file_path" value="$PROJECT_DIR$" /> + </component> + <component name="RunDashboard"> + <option name="ruleStates"> + <list> + <RuleState> + <option name="name" value="ConfigurationTypeDashboardGroupingRule" /> + </RuleState> + <RuleState> + <option name="name" value="StatusDashboardGroupingRule" /> + </RuleState> + </list> + </option> + </component> + <component name="ShelveChangesManager" show_recycled="false"> + <option name="remove_strategy" value="false" /> + </component> + <component name="SvnConfiguration"> + <configuration /> + </component> + <component name="TaskManager"> + <task active="true" id="Default" summary="Default task"> + <changelist id="061e089e-71dc-4d6b-b27c-9c614b097257" name="Default" comment="" /> + <created>1522354295512</created> + <option name="number" value="Default" /> + <option name="presentableId" value="Default" /> + <updated>1522354295512</updated> + <workItem from="1522354299246" duration="1866000" /> + <workItem from="1522643117648" duration="2000" /> + <workItem from="1524064618567" duration="2674000" /> + <workItem from="1525381677126" duration="197000" /> + <workItem from="1525382363305" duration="597000" /> + <workItem from="1525461458836" duration="70000" /> + <workItem from="1526067492327" duration="897000" /> + <workItem from="1526397339713" duration="27000" /> + </task> + <servers /> + </component> + <component name="TimeTrackingManager"> + <option name="totallyTimeSpent" value="6330000" /> + </component> + <component name="ToolWindowManager"> + <frame x="0" y="24" width="1920" height="1055" extended-state="6" /> + <editor active="true" /> + <layout> + <window_info id="Project" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="0" side_tool="false" content_ui="combo" /> + <window_info id="TODO" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="6" side_tool="false" content_ui="tabs" /> + <window_info id="Event Log" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="7" side_tool="true" content_ui="tabs" /> + <window_info id="Version Control" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="7" side_tool="false" content_ui="tabs" /> + <window_info id="npm" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="2" side_tool="true" content_ui="tabs" /> + <window_info id="Run" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="2" side_tool="false" content_ui="tabs" /> + <window_info id="Structure" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" /> + <window_info id="Terminal" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="7" side_tool="false" content_ui="tabs" /> + <window_info id="Favorites" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="2" side_tool="true" content_ui="tabs" /> + <window_info id="Debug" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="3" side_tool="false" content_ui="tabs" /> + <window_info id="Cvs" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="4" side_tool="false" content_ui="tabs" /> + <window_info id="Message" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" /> + <window_info id="Commander" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" /> + <window_info id="Inspection" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="5" side_tool="false" content_ui="tabs" /> + <window_info id="Hierarchy" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="2" side_tool="false" content_ui="combo" /> + <window_info id="Find" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" /> + <window_info id="Database" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="3" side_tool="false" content_ui="tabs" /> + <window_info id="Ant Build" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" /> + </layout> + </component> + <component name="TypeScriptGeneratedFilesManager"> + <option name="version" value="1" /> + </component> + <component name="VcsContentAnnotationSettings"> + <option name="myLimit" value="2678400000" /> + </component> + <component name="XDebuggerManager"> + <breakpoint-manager /> + <watches-manager /> + </component> + <component name="editorHistoryManager"> + <entry file="file://$PROJECT_DIR$/src/renderer/modal/modalRouter/view.jsx"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="0"> + <caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" /> + <folding> + <element signature="e#0#26#0" expanded="true" /> + <marker date="1526068149928" expanded="true" signature="5366:5471" ph="{...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/modal/modalRouter/index.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="216"> + <caret line="12" column="0" lean-forward="false" selection-start-line="12" selection-start-column="0" selection-end-line="12" selection-end-column="0" /> + <folding> + <marker date="1525123536618" expanded="true" signature="1172:1177" ph="{...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/package.json"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="1332"> + <caret line="74" column="0" lean-forward="false" selection-start-line="74" selection-start-column="0" selection-end-line="74" selection-end-column="0" /> + <folding> + <marker date="1526397284183" expanded="true" signature="2668:3140" ph="{"axios": "^0.18.0"...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/node_modules/bluebird/js/release/join.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="144"> + <caret line="8" column="11" lean-forward="false" selection-start-line="8" selection-start-column="11" selection-end-line="8" selection-end-column="11" /> + <folding /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/modal/modalRouter/view.jsx"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="0"> + <caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" /> + <folding> + <element signature="e#0#26#0" expanded="true" /> + <marker date="1526068149928" expanded="true" signature="5366:5471" ph="{...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/package.json"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="1332"> + <caret line="74" column="0" lean-forward="false" selection-start-line="74" selection-start-column="0" selection-end-line="74" selection-end-column="0" /> + <folding> + <marker date="1526397284183" expanded="true" signature="2668:3140" ph="{"axios": "^0.18.0"...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/modal/modalRouter/index.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="216"> + <caret line="12" column="0" lean-forward="false" selection-start-line="12" selection-start-column="0" selection-end-line="12" selection-end-column="0" /> + <folding> + <marker date="1525123536618" expanded="true" signature="1172:1177" ph="{...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/node_modules/bluebird/js/release/join.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="0"> + <caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" /> + <folding /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/modal/modalRouter/view.jsx"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="0"> + <caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" /> + <folding> + <element signature="e#0#26#0" expanded="true" /> + <marker date="1526068149928" expanded="true" signature="5366:5471" ph="{...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/modal/modalRouter/index.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="216"> + <caret line="12" column="0" lean-forward="false" selection-start-line="12" selection-start-column="0" selection-end-line="12" selection-end-column="0" /> + <folding> + <marker date="1525123536618" expanded="true" signature="1172:1177" ph="{...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/package.json"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="2322"> + <caret line="129" column="38" lean-forward="false" selection-start-line="129" selection-start-column="38" selection-end-line="129" selection-end-column="38" /> + <folding> + <marker date="1526397284183" expanded="true" signature="2668:3140" ph="{"axios": "^0.18.0"...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/modal/modalRouter/view.jsx"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="0"> + <caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" /> + <folding> + <element signature="e#0#26#0" expanded="true" /> + <marker date="1526068149928" expanded="true" signature="5366:5471" ph="{...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/modal/modalRouter/index.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="144"> + <caret line="19" column="68" lean-forward="true" selection-start-line="19" selection-start-column="68" selection-end-line="19" selection-end-column="68" /> + <folding> + <marker date="1525123536618" expanded="true" signature="1172:1177" ph="{...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/page/rewards/view.jsx"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="-122"> + <caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/redux/reducers/rewards.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="-28"> + <caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/redux/actions/rewards.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="-832"> + <caret line="13" column="61" lean-forward="false" selection-start-line="13" selection-start-column="36" selection-end-line="13" selection-end-column="61" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/rewards.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="324"> + <caret line="18" column="0" lean-forward="false" selection-start-line="18" selection-start-column="0" selection-end-line="18" selection-end-column="0" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/modal/modalRouter/view.jsx"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="0"> + <caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" /> + <folding> + <element signature="e#0#26#0" expanded="true" /> + <marker date="1526068149928" expanded="true" signature="5366:5471" ph="{...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/modal/modalRouter/index.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="216"> + <caret line="12" column="0" lean-forward="false" selection-start-line="12" selection-start-column="0" selection-end-line="12" selection-end-column="0" /> + <folding> + <marker date="1525123536618" expanded="true" signature="1172:1177" ph="{...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/package.json"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="693"> + <caret line="129" column="38" lean-forward="false" selection-start-line="129" selection-start-column="38" selection-end-line="129" selection-end-column="38" /> + <folding> + <marker date="1526397284183" expanded="true" signature="2668:3140" ph="{"axios": "^0.18.0"...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/modal/modalRouter/view.jsx"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="0"> + <caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" /> + <folding> + <element signature="e#0#26#0" expanded="true" /> + <marker date="1526068149928" expanded="true" signature="5366:5471" ph="{...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/modal/modalRouter/index.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="144"> + <caret line="19" column="68" lean-forward="true" selection-start-line="19" selection-start-column="68" selection-end-line="19" selection-end-column="68" /> + <folding> + <marker date="1525123536618" expanded="true" signature="1172:1177" ph="{...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/page/rewards/view.jsx"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="-122"> + <caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/redux/reducers/rewards.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="-28"> + <caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/redux/actions/rewards.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="-832"> + <caret line="13" column="61" lean-forward="false" selection-start-line="13" selection-start-column="36" selection-end-line="13" selection-end-column="61" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/rewards.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="324"> + <caret line="18" column="0" lean-forward="false" selection-start-line="18" selection-start-column="0" selection-end-line="18" selection-end-column="0" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/modal/modalRouter/view.jsx"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="0"> + <caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" /> + <folding> + <element signature="e#0#26#0" expanded="true" /> + <marker date="1526068149928" expanded="true" signature="5366:5471" ph="{...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/modal/modalRouter/index.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="216"> + <caret line="12" column="0" lean-forward="false" selection-start-line="12" selection-start-column="0" selection-end-line="12" selection-end-column="0" /> + <folding> + <marker date="1525123536618" expanded="true" signature="1172:1177" ph="{...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/package.json"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="1332"> + <caret line="74" column="0" lean-forward="false" selection-start-line="74" selection-start-column="0" selection-end-line="74" selection-end-column="0" /> + <folding> + <marker date="1526397284183" expanded="true" signature="2668:3140" ph="{"axios": "^0.18.0"...}" /> + </folding> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/CONTRIBUTING.md"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="540"> + <caret line="168" column="0" lean-forward="true" selection-start-line="168" selection-start-column="0" selection-end-line="168" selection-end-column="0" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/README.md"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="846"> + <caret line="47" column="81" lean-forward="true" selection-start-line="47" selection-start-column="81" selection-end-line="47" selection-end-column="81" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/node_modules/bluebird/js/release/join.js"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="144"> + <caret line="8" column="11" lean-forward="false" selection-start-line="8" selection-start-column="11" selection-end-line="8" selection-end-column="11" /> + <folding /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/src/renderer/page/file/view.jsx"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="287"> + <caret line="18" column="15" lean-forward="false" selection-start-line="18" selection-start-column="14" selection-end-line="18" selection-end-column="15" /> + <folding /> + </state> + </provider> + </entry> + </component> +</project> \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d31dbbc93..f00467470 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Changed * Add flair to snackbar ([#1313](https://github.com/lbryio/lbry-app/pull/1313)) * Made font in price badge larger ([#1420](https://github.com/lbryio/lbry-app/pull/1420)) + * Store subscriptions in internal database ([#1424](https://github.com/lbryio/lbry-app/pull/1424)) ### Fixed * Fix content-type not shown correctly in file description ([#863](https://github.com/lbryio/lbry-app/pull/863)) diff --git a/src/renderer/analytics.js b/src/renderer/analytics.js index 2a03b7428..a4fd24258 100644 --- a/src/renderer/analytics.js +++ b/src/renderer/analytics.js @@ -2,7 +2,6 @@ import mixpanel from 'mixpanel-browser'; import Lbryio from 'lbryio'; import isDev from 'electron-is-dev'; -import type { Subscription } from 'redux/reducers/subscriptions'; if (isDev) { mixpanel.init('691723e855cabb9d27a7a79002216967'); @@ -15,8 +14,6 @@ type Analytics = { setUser: Object => void, toggle: (boolean, ?boolean) => void, apiLogView: (string, string, string) => void, - apiLogSubscribe: Subscription => void, - apiLogUnsubscribe: Subscription => void, }; let analyticsEnabled: boolean = false; @@ -56,20 +53,6 @@ const analytics: Analytics = { }).catch(() => {}); } }, - apiLogSubscribe: (subscription: Subscription): void => { - if (analyticsEnabled) { - Lbryio.call('subscription', 'new', { - subscription, - }).catch(() => {}); - } - }, - apiLogUnsubscribe: (subscription: Subscription): void => { - if (analyticsEnabled) { - Lbryio.call('subscription', 'delete', { - subscription, - }).catch(() => {}); - } - }, }; export default analytics; diff --git a/src/renderer/component/common/spinner.jsx b/src/renderer/component/common/spinner.jsx index 7ba514607..e69de29bb 100644 --- a/src/renderer/component/common/spinner.jsx +++ b/src/renderer/component/common/spinner.jsx @@ -1,28 +0,0 @@ -// @flow -import * as React from 'react'; -import classnames from 'classnames'; - -type Props = { - dark?: boolean, -}; - -class Spinner extends React.Component<Props> { - static defaultProps = { - dark: false, - }; - - render() { - const { dark } = this.props; - return ( - <div className={classnames('spinner', { 'spinner--dark': dark })}> - <div className="rect rect1" /> - <div className="rect rect2" /> - <div className="rect rect3" /> - <div className="rect rect4" /> - <div className="rect rect5" /> - </div> - ); - } -} - -export default Spinner; diff --git a/src/renderer/component/fileDetails/view.jsx b/src/renderer/component/fileDetails/view.jsx index e49af3417..c793d25ef 100644 --- a/src/renderer/component/fileDetails/view.jsx +++ b/src/renderer/component/fileDetails/view.jsx @@ -3,9 +3,10 @@ import * as React from 'react'; import ReactMarkdown from 'react-markdown'; import Button from 'component/button'; import path from 'path'; +import type { Claim } from 'types/claim'; type Props = { - claim: {}, + claim: Claim, fileInfo: { download_path: string, }, diff --git a/src/renderer/component/fileList/view.jsx b/src/renderer/component/fileList/view.jsx index ffe8ecbff..8bc26d362 100644 --- a/src/renderer/component/fileList/view.jsx +++ b/src/renderer/component/fileList/view.jsx @@ -53,7 +53,6 @@ class FileList extends React.PureComponent<Props, State> { if (fileInfo1.pending) { return -1; } - const height1 = this.props.claimsById[fileInfo1.claim_id] ? this.props.claimsById[fileInfo1.claim_id].height : 0; @@ -145,6 +144,10 @@ class FileList extends React.PureComponent<Props, State> { const { sortBy } = this.state; const content = []; + if (!fileInfos) { + return null; + } + this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => { const { channel_name: channelName, diff --git a/src/renderer/component/page/view.jsx b/src/renderer/component/page/view.jsx index 40ec8564b..9af308b79 100644 --- a/src/renderer/component/page/view.jsx +++ b/src/renderer/component/page/view.jsx @@ -1,33 +1,98 @@ // @flow import * as React from 'react'; import classnames from 'classnames'; +import Spinner from 'component/spinner'; +import { isShowingChildren } from 'util/dom'; + +// time in ms to wait to show loading spinner +const LOADER_TIMEOUT = 1500; type Props = { - children: React.Node, + children: React.Node | Array<React.Node>, pageTitle: ?string, noPadding: ?boolean, extraPadding: ?boolean, notContained: ?boolean, // No max-width, but keep the padding + loading: ?boolean, }; -const Page = (props: Props) => { - const { pageTitle, children, noPadding, extraPadding, notContained } = props; - return ( - <main - className={classnames('main', { - 'main--contained': !notContained && !noPadding && !extraPadding, - 'main--no-padding': noPadding, - 'main--extra-padding': extraPadding, - })} - > - {pageTitle && ( - <div className="page__header"> - {pageTitle && <h1 className="page__title">{pageTitle}</h1>} - </div> - )} - {children} - </main> - ); +type State = { + showLoader: ?boolean, }; +class Page extends React.PureComponent<Props, State> { + static getDerivedStateFromProps(nextProps: Props, prevState: State) { + const { children } = nextProps; + const { showLoader } = prevState; + + // If we aren't showing the loader, don't bother updating + if (!showLoader) { + return null; + } + + if (isShowingChildren(children)) { + return { + showLoader: false, + }; + } + return null; + } + + constructor() { + super(); + + this.state = { + showLoader: false, + }; + + this.loaderTimeout = null; + } + + componentDidMount() { + const { children } = this.props; + + if (!isShowingChildren(children)) + this.loaderTimeout = setTimeout(() => { + this.setState({ showLoader: true }); + }, LOADER_TIMEOUT); + } + + componentWillUnmount() { + this.loaderTimeout = null; + } + + loaderTimeout: ?TimeoutID; + + render() { + const { pageTitle, children, noPadding, extraPadding, notContained, loading } = this.props; + const { showLoader } = this.state; + + // We don't want to show the loading spinner right away if it will only flash on the + // screen for a short time, wait until we know it will be loading for a bit before showing it + const shouldShowLoader = !isShowingChildren(children) && showLoader; + + return ( + <main + className={classnames('main', { + 'main--contained': !notContained && !noPadding && !extraPadding, + 'main--no-padding': noPadding, + 'main--extra-padding': extraPadding, + })} + > + {pageTitle && ( + <div className="page__header"> + {pageTitle && <h1 className="page__title">{pageTitle}</h1>} + </div> + )} + {!loading && children} + {shouldShowLoader && ( + <div className="page__empty"> + <Spinner /> + </div> + )} + </main> + ); + } +} + export default Page; diff --git a/src/renderer/component/spinner/index.js b/src/renderer/component/spinner/index.js new file mode 100644 index 000000000..6589e05da --- /dev/null +++ b/src/renderer/component/spinner/index.js @@ -0,0 +1,9 @@ +import { connect } from 'react-redux'; +import { selectTheme } from 'redux/selectors/settings'; +import Spinner from './view'; + +const mapStateToProps = state => ({ + theme: selectTheme(state), +}); + +export default connect(mapStateToProps, null)(Spinner); diff --git a/src/renderer/component/spinner/view.jsx b/src/renderer/component/spinner/view.jsx new file mode 100644 index 000000000..bb44ee6f7 --- /dev/null +++ b/src/renderer/component/spinner/view.jsx @@ -0,0 +1,36 @@ +// @flow +import * as React from 'react'; +import classnames from 'classnames'; +import { DARK_THEME, LIGHT_THEME } from 'constants/themes'; + +type Props = { + dark?: boolean, // always a dark spinner + light?: boolean, // always a light spinner + theme: string, +}; + +const Spinner = (props: Props) => { + const { dark, light, theme } = props; + + return ( + <div + className={classnames('spinner', { + 'spinner--dark': !light && (dark || theme === LIGHT_THEME), + 'spinner--light': !dark && (light || theme === DARK_THEME), + })} + > + <div className="rect rect1" /> + <div className="rect rect2" /> + <div className="rect rect3" /> + <div className="rect rect4" /> + <div className="rect rect5" /> + </div> + ); +}; + +Spinner.defaultProps = { + dark: false, + light: false, +}; + +export default Spinner; diff --git a/src/renderer/component/subscribeButton/view.jsx b/src/renderer/component/subscribeButton/view.jsx index 1a341447f..52516dfb8 100644 --- a/src/renderer/component/subscribeButton/view.jsx +++ b/src/renderer/component/subscribeButton/view.jsx @@ -3,7 +3,7 @@ import React from 'react'; import { MODALS } from 'lbry-redux'; import * as icons from 'constants/icons'; import Button from 'component/button'; -import type { Subscription } from 'redux/reducers/subscriptions'; +import type { Subscription } from 'types/subscription'; type SubscribtionArgs = { channelName: string, diff --git a/src/renderer/component/uriIndicator/view.jsx b/src/renderer/component/uriIndicator/view.jsx index 499b11fba..98c249932 100644 --- a/src/renderer/component/uriIndicator/view.jsx +++ b/src/renderer/component/uriIndicator/view.jsx @@ -3,21 +3,18 @@ import React from 'react'; import Button from 'component/button'; import { buildURI } from 'lbry-redux'; import classnames from 'classnames'; -// import Icon from 'component/common/icon'; +import type { Claim } from 'types/claim'; type Props = { isResolvingUri: boolean, - resolveUri: string => void, - claim: { - channel_name: string, - has_signature: boolean, - signature_is_valid: boolean, - value: { - publisherSignature: { certificateId: string }, - }, - }, - uri: string, + claim: Claim, link: ?boolean, + // Lint thinks we aren't using these, even though we are. + // Possibly because the resolve function is an arrow function that is passed in props? + /* eslint-disable react/no-unused-prop-types */ + resolveUri: string => void, + uri: string, + /* eslint-enable react/no-unused-prop-types */ }; class UriIndicator extends React.PureComponent<Props> { diff --git a/src/renderer/component/video/internal/loading-screen.jsx b/src/renderer/component/video/internal/loading-screen.jsx index ccada9d41..f313b412d 100644 --- a/src/renderer/component/video/internal/loading-screen.jsx +++ b/src/renderer/component/video/internal/loading-screen.jsx @@ -1,6 +1,6 @@ // @flow import React from 'react'; -import Spinner from 'component/common/spinner'; +import Spinner from 'component/spinner'; type Props = { spinner: boolean, @@ -16,7 +16,7 @@ class LoadingScreen extends React.PureComponent<Props> { const { status, spinner } = this.props; return ( <div className="content__loading"> - {spinner && <Spinner />} + {spinner && <Spinner light />} <span className="content__loading-text">{status}</span> </div> diff --git a/src/renderer/component/video/view.jsx b/src/renderer/component/video/view.jsx index c1663c5e3..761a19e10 100644 --- a/src/renderer/component/video/view.jsx +++ b/src/renderer/component/video/view.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { Lbry } from 'lbry-redux'; import classnames from 'classnames'; +import type { Claim } from 'types/claim'; import VideoPlayer from './internal/player'; import VideoPlayButton from './internal/play-button'; import LoadingScreen from './internal/loading-screen'; @@ -26,7 +27,7 @@ type Props = { contentType: string, changeVolume: number => void, volume: number, - claim: {}, + claim: Claim, uri: string, doPlay: () => void, doPause: () => void, diff --git a/src/renderer/component/walletSendTip/view.jsx b/src/renderer/component/walletSendTip/view.jsx index 9af0da866..a8d10a8a5 100644 --- a/src/renderer/component/walletSendTip/view.jsx +++ b/src/renderer/component/walletSendTip/view.jsx @@ -3,11 +3,12 @@ import React from 'react'; import Button from 'component/button'; import { FormField } from 'component/common/form'; import UriIndicator from 'component/uriIndicator'; +import type { Claim } from 'types/claim'; type Props = { uri: string, title: string, - claim: { claim_id: string }, + claim: Claim, errorMessage: string, isPending: boolean, sendSupport: (number, string, string) => void, diff --git a/src/renderer/constants/action_types.js b/src/renderer/constants/action_types.js index 27678804b..3ffe23e6e 100644 --- a/src/renderer/constants/action_types.js +++ b/src/renderer/constants/action_types.js @@ -1,3 +1,13 @@ +/* + Constants for redux actions + All names should be in present tense + ex: + XXX_START + XXX_SUCCESS + XXX_FAIL + XXX_COMPLETE // if there is no fail case +*/ + export const WINDOW_FOCUSED = 'WINDOW_FOCUSED'; export const DAEMON_READY = 'DAEMON_READY'; export const DAEMON_VERSION_MATCH = 'DAEMON_VERSION_MATCH'; @@ -166,6 +176,9 @@ export const SET_SUBSCRIPTION_NOTIFICATIONS = 'SET_SUBSCRIPTION_NOTIFICATIONS'; 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'; // Video controls export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE'; diff --git a/src/renderer/constants/themes.js b/src/renderer/constants/themes.js new file mode 100644 index 000000000..0b9cc53b6 --- /dev/null +++ b/src/renderer/constants/themes.js @@ -0,0 +1,4 @@ +// css theme values +// saved in settings and found at /static/themes/{theme}.css +export const DARK_THEME = 'dark'; +export const LIGHT_THEME = 'light'; diff --git a/src/renderer/page/channel/view.jsx b/src/renderer/page/channel/view.jsx index 59a5b5531..693cb3c89 100644 --- a/src/renderer/page/channel/view.jsx +++ b/src/renderer/page/channel/view.jsx @@ -6,6 +6,7 @@ import ReactPaginate from 'react-paginate'; import SubscribeButton from 'component/subscribeButton'; import Page from 'component/page'; import FileList from 'component/fileList'; +import type { Claim } from 'types/claim'; type Props = { uri: string, @@ -13,10 +14,7 @@ type Props = { totalPages: number, fetching: boolean, params: { page: number }, - claim: { - name: string, - claim_id: string, - }, + claim: Claim, claimsInChannel: Array<{}>, fetchClaims: (string, number) => void, fetchClaimCount: string => void, @@ -58,8 +56,8 @@ class ChannelPage extends React.PureComponent<Props> { } render() { - const { fetching, claimsInChannel, claim, uri, page, totalPages } = this.props; - const { name } = claim; + const { fetching, claimsInChannel, claim, page, totalPages } = this.props; + const { name, permanent_url: permanentUrl } = claim; let contentList; if (fetching) { @@ -78,7 +76,7 @@ class ChannelPage extends React.PureComponent<Props> { <section className="card__channel-info card__channel-info--large"> <h1>{name}</h1> <div className="card__actions card__actions--no-margin"> - <SubscribeButton uri={uri} channelName={name} /> + <SubscribeButton uri={permanentUrl} channelName={name} /> </div> </section> <section>{contentList}</section> diff --git a/src/renderer/page/file/view.jsx b/src/renderer/page/file/view.jsx index 40cc010a2..bf7c34846 100644 --- a/src/renderer/page/file/view.jsx +++ b/src/renderer/page/file/view.jsx @@ -16,18 +16,10 @@ import SubscribeButton from 'component/subscribeButton'; import Page from 'component/page'; import player from 'render-media'; import * as settings from 'constants/settings'; +import type { Claim } from 'types/claim'; type Props = { - claim: { - claim_id: string, - height: number, - channel_name: string, - value: { - publisherSignature: ?{ - certificateId: ?string, - }, - }, - }, + claim: Claim, fileInfo: {}, metadata: { title: string, diff --git a/src/renderer/page/show/view.jsx b/src/renderer/page/show/view.jsx index aa6e042ad..afa753be6 100644 --- a/src/renderer/page/show/view.jsx +++ b/src/renderer/page/show/view.jsx @@ -5,16 +5,13 @@ import ChannelPage from 'page/channel'; import FilePage from 'page/file'; import Page from 'component/page'; import Button from 'component/button'; +import type { Claim } from 'types/claim'; type Props = { isResolvingUri: boolean, resolveUri: string => void, uri: string, - claim: { - name: string, - txid: string, - nout: number, - }, + claim: Claim, blackListedOutpoints: Array<{ txid: string, nout: number, diff --git a/src/renderer/page/subscriptions/index.js b/src/renderer/page/subscriptions/index.js index 4d1ce6409..433adc80c 100644 --- a/src/renderer/page/subscriptions/index.js +++ b/src/renderer/page/subscriptions/index.js @@ -1,27 +1,25 @@ -import React from 'react'; import { connect } from 'react-redux'; import { - selectSubscriptionsFromClaims, + selectSubscriptionClaims, selectSubscriptions, - selectHasFetchedSubscriptions, + selectSubscriptionsBeingFetched, + selectIsFetchingSubscriptions, selectNotifications, } from 'redux/selectors/subscriptions'; import { doFetchClaimsByChannel } from 'redux/actions/content'; -import { - setHasFetchedSubscriptions, - setSubscriptionNotifications, -} from 'redux/actions/subscriptions'; +import { setSubscriptionNotifications, doFetchMySubscriptions } from 'redux/actions/subscriptions'; import SubscriptionsPage from './view'; const select = state => ({ - hasFetchedSubscriptions: state.subscriptions.hasFetchedSubscriptions, - savedSubscriptions: selectSubscriptions(state), - subscriptions: selectSubscriptionsFromClaims(state), + isFetchingSubscriptions: selectIsFetchingSubscriptions(state), + subscriptionsBeingFetched: selectSubscriptionsBeingFetched(state), + subscriptions: selectSubscriptions(state), + subscriptionClaims: selectSubscriptionClaims(state), notifications: selectNotifications(state), }); export default connect(select, { doFetchClaimsByChannel, - setHasFetchedSubscriptions, setSubscriptionNotifications, + doFetchMySubscriptions, })(SubscriptionsPage); diff --git a/src/renderer/page/subscriptions/view.jsx b/src/renderer/page/subscriptions/view.jsx index 27f3fac45..1ea53e306 100644 --- a/src/renderer/page/subscriptions/view.jsx +++ b/src/renderer/page/subscriptions/view.jsx @@ -1,39 +1,28 @@ // @flow import React from 'react'; import Page from 'component/page'; -import CategoryList from 'component/common/category-list'; -import type { Subscription } from 'redux/reducers/subscriptions'; +import type { Subscription } from 'types/subscription'; import * as NOTIFICATION_TYPES from 'constants/notification_types'; import Button from 'component/button'; - -type SavedSubscriptions = Array<Subscription>; +import FileList from 'component/fileList'; +import type { Claim } from 'types/claim'; type Props = { - doFetchClaimsByChannel: (string, number) => any, - savedSubscriptions: SavedSubscriptions, - // TODO build out claim types - subscriptions: Array<any>, - setHasFetchedSubscriptions: () => void, - hasFetchedSubscriptions: boolean, + doFetchClaimsByChannel: (string, number) => void, + doFetchMySubscriptions: () => void, + setSubscriptionNotifications: ({}) => void, + subscriptions: Array<Subscription>, + isFetchingSubscriptions: boolean, + subscriptionClaims: Array<{ uri: string, claims: Array<Claim> }>, + subscriptionsBeingFetched: {}, + notifications: {}, }; export default class extends React.PureComponent<Props> { - // setHasFetchedSubscriptions is a terrible hack - // it allows the subscriptions to load correctly when refresing on the subscriptions page - // currently the page is rendered before the state is rehyrdated - // that causes this component to be rendered with zero savedSubscriptions - // we need to wait until persist/REHYDRATE has fired before rendering the page componentDidMount() { - const { - savedSubscriptions, - setHasFetchedSubscriptions, - notifications, - setSubscriptionNotifications, - } = this.props; - if (savedSubscriptions.length) { - this.fetchSubscriptions(savedSubscriptions); - setHasFetchedSubscriptions(); - } + const { notifications, setSubscriptionNotifications, doFetchMySubscriptions } = this.props; + doFetchMySubscriptions(); + const newNotifications = {}; Object.keys(notifications).forEach(cur => { if (notifications[cur].type === NOTIFICATION_TYPES.DOWNLOADING) { @@ -43,40 +32,37 @@ export default class extends React.PureComponent<Props> { setSubscriptionNotifications(newNotifications); } - componentWillReceiveProps(props: Props) { - const { savedSubscriptions, hasFetchedSubscriptions, setHasFetchedSubscriptions } = props; + componentDidUpdate() { + const { + subscriptions, + subscriptionClaims, + doFetchClaimsByChannel, + subscriptionsBeingFetched, + } = this.props; - if (!hasFetchedSubscriptions && savedSubscriptions.length) { - this.fetchSubscriptions(savedSubscriptions); - setHasFetchedSubscriptions(); - } - } + const subscriptionClaimMap = {}; + subscriptionClaims.forEach(claim => { + subscriptionClaimMap[claim.uri] = 1; + }); - fetchSubscriptions(savedSubscriptions: SavedSubscriptions) { - const { doFetchClaimsByChannel } = this.props; - if (savedSubscriptions.length) { - // can this use batchActions? - savedSubscriptions.forEach(sub => { + subscriptions.forEach(sub => { + if (!subscriptionClaimMap[sub.uri] && !subscriptionsBeingFetched[sub.uri]) { doFetchClaimsByChannel(sub.uri, 1); - }); - } + } + }); } render() { - const { subscriptions, savedSubscriptions } = this.props; + const { subscriptions, subscriptionClaims, isFetchingSubscriptions } = this.props; - // TODO: if you are subscribed to an empty channel, this will always be true (but it should not be) - const someClaimsNotLoaded = Boolean( - subscriptions.find(subscription => !subscription.claims.length) - ); - - const fetchingSubscriptions = - !!savedSubscriptions.length && - (subscriptions.length !== savedSubscriptions.length || someClaimsNotLoaded); + let claimList = []; + subscriptionClaims.forEach(claimData => { + claimList = claimList.concat(claimData.claims); + }); return ( - <Page noPadding isLoading={fetchingSubscriptions}> - {!savedSubscriptions.length && ( + <Page notContained loading={isFetchingSubscriptions}> + {!subscriptions.length && ( <div className="page__empty"> {__("It looks like you aren't subscribed to any channels yet.")} <div className="card__actions card__actions--center"> @@ -84,28 +70,7 @@ export default class extends React.PureComponent<Props> { </div> </div> )} - {!!savedSubscriptions.length && ( - <div> - {!!subscriptions.length && - subscriptions.map(subscription => { - if (!subscription.claims.length) { - // will need to update when you can subscribe to empty channels - // for now this prevents issues with FeaturedCategory being rendered - // before the names (claim uris) are populated - return ''; - } - - return ( - <CategoryList - key={subscription.channelName} - categoryLink={subscription.uri} - category={subscription.channelName} - names={subscription.claims} - /> - ); - })} - </div> - )} + {!!claimList.length && <FileList hideFilter sortByHeight fileInfos={claimList} />} </Page> ); } diff --git a/src/renderer/redux/actions/content.js b/src/renderer/redux/actions/content.js index 1ae851565..8dae86f8f 100644 --- a/src/renderer/redux/actions/content.js +++ b/src/renderer/redux/actions/content.js @@ -138,6 +138,7 @@ export function doUpdateLoadStatus(uri, outpoint) { : acc, 0 ); + const notif = new window.Notification(notifications[uri].subscription.channelName, { body: `Posted ${fileInfo.metadata.title}${ count > 1 && count < 10 ? ` and ${count - 1} other new items` : '' diff --git a/src/renderer/redux/actions/subscriptions.js b/src/renderer/redux/actions/subscriptions.js index 6f0dba49f..b98e7b7a5 100644 --- a/src/renderer/redux/actions/subscriptions.js +++ b/src/renderer/redux/actions/subscriptions.js @@ -2,56 +2,127 @@ import * as ACTIONS from 'constants/action_types'; import * as NOTIFICATION_TYPES from 'constants/notification_types'; import type { - Subscription, Dispatch, SubscriptionState, SubscriptionNotifications, } from 'redux/reducers/subscriptions'; +import type { Subscription } from 'types/subscription'; import { selectSubscriptions } from 'redux/selectors/subscriptions'; -import { Lbry, buildURI } from 'lbry-redux'; +import { Lbry, buildURI, parseURI } from 'lbry-redux'; import { doPurchaseUri } from 'redux/actions/content'; -import { doNavigate } from 'redux/actions/navigation'; -import analytics from 'analytics'; +import Promise from 'bluebird'; +import Lbryio from 'lbryio'; const CHECK_SUBSCRIPTIONS_INTERVAL = 60 * 60 * 1000; const SUBSCRIPTION_DOWNLOAD_LIMIT = 1; -export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch) => { - dispatch({ - type: ACTIONS.CHANNEL_SUBSCRIBE, - data: subscription, - }); +export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: () => any) => { + const { + subscriptions: subscriptionState, + settings: { daemonSettings }, + } = getState(); + const { subscriptions: reduxSubscriptions } = subscriptionState; + const { share_usage_data: isSharingData } = daemonSettings; - analytics.apiLogSubscribe(subscription); + if (!isSharingData && isSharingData !== undefined) { + // They aren't sharing their data, subscriptions will be handled by persisted redux state + return; + } - dispatch(doCheckSubscription(subscription, true)); + // most of this logic comes from scenarios where the db isn't synced with redux + // this will happen if the user stops sharing data + dispatch({ type: ACTIONS.FETCH_SUBSCRIPTIONS_START }); + + Lbryio.call('subscription', 'list') + .then(dbSubscriptions => { + const storedSubscriptions = dbSubscriptions || []; + + // User has no subscriptions in db or redux + if (!storedSubscriptions.length && (!reduxSubscriptions || !reduxSubscriptions.length)) { + return []; + } + + // There is some mismatch between redux state and db state + // If something is in the db, but not in redux, add it to redux + // If something is in redux, but not in the db, add it to the db + if (storedSubscriptions.length !== reduxSubscriptions.length) { + const dbSubMap = {}; + const reduxSubMap = {}; + const subsNotInDB = []; + const subscriptionsToReturn = reduxSubscriptions.slice(); + + storedSubscriptions.forEach(sub => { + dbSubMap[sub.claim_id] = 1; + }); + + reduxSubscriptions.forEach(sub => { + const { claimId } = parseURI(sub.uri); + reduxSubMap[claimId] = 1; + + if (!dbSubMap[claimId]) { + subsNotInDB.push({ + claim_id: claimId, + channel_name: sub.channelName, + }); + } + }); + + storedSubscriptions.forEach(sub => { + if (!reduxSubMap[sub.claim_id]) { + const uri = `lbry://${sub.channel_name}#${sub.claim_id}`; + subscriptionsToReturn.push({ uri, channelName: sub.channel_name }); + } + }); + + return Promise.all(subsNotInDB.map(payload => Lbryio.call('subscription', 'new', payload))) + .then(() => subscriptionsToReturn) + .catch( + () => + // let it fail, we will try again when the navigate to the subscriptions page + subscriptionsToReturn + ); + } + + // DB is already synced, just return the subscriptions in redux + return reduxSubscriptions; + }) + .then(subscriptions => { + dispatch({ + type: ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS, + data: subscriptions, + }); + }) + .catch(() => { + dispatch({ + type: ACTIONS.FETCH_SUBSCRIPTIONS_FAIL, + }); + }); }; -export const doChannelUnsubscribe = (subscription: Subscription) => (dispatch: Dispatch) => { +export const setSubscriptionLatest = (subscription: Subscription, uri: string) => ( + dispatch: Dispatch +) => dispatch({ - type: ACTIONS.CHANNEL_UNSUBSCRIBE, - data: subscription, + type: ACTIONS.SET_SUBSCRIPTION_LATEST, + data: { + subscription, + uri, + }, }); - analytics.apiLogUnsubscribe(subscription); -}; - -export const doCheckSubscriptions = () => ( - dispatch: Dispatch, - getState: () => SubscriptionState -) => { - const checkSubscriptionsTimer = setInterval( - () => - selectSubscriptions(getState()).map((subscription: Subscription) => - dispatch(doCheckSubscription(subscription, true)) - ), - CHECK_SUBSCRIPTIONS_INTERVAL - ); +export const setSubscriptionNotification = ( + subscription: Subscription, + uri: string, + notificationType: string +) => (dispatch: Dispatch) => dispatch({ - type: ACTIONS.CHECK_SUBSCRIPTIONS_SUBSCRIBE, - data: { checkSubscriptionsTimer }, + type: ACTIONS.SET_SUBSCRIPTION_NOTIFICATION, + data: { + subscription, + uri, + type: notificationType, + }, }); -}; export const doCheckSubscription = (subscription: Subscription, notify?: boolean) => ( dispatch: Dispatch @@ -114,31 +185,6 @@ export const doCheckSubscription = (subscription: Subscription, notify?: boolean }); }; -export const setSubscriptionLatest = (subscription: Subscription, uri: string) => ( - dispatch: Dispatch -) => - dispatch({ - type: ACTIONS.SET_SUBSCRIPTION_LATEST, - data: { - subscription, - uri, - }, - }); - -export const setSubscriptionNotification = ( - subscription: Subscription, - uri: string, - notificationType: string -) => (dispatch: Dispatch) => - dispatch({ - type: ACTIONS.SET_SUBSCRIPTION_NOTIFICATION, - data: { - subscription, - uri, - type: notificationType, - }, - }); - export const setSubscriptionNotifications = (notifications: SubscriptionNotifications) => ( dispatch: Dispatch ) => @@ -149,5 +195,68 @@ export const setSubscriptionNotifications = (notifications: SubscriptionNotifica }, }); -export const setHasFetchedSubscriptions = () => (dispatch: Dispatch) => - dispatch({ type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS }); +export const doChannelSubscribe = (subscription: Subscription) => ( + dispatch: Dispatch, + getState: () => any +) => { + const { + settings: { daemonSettings }, + } = getState(); + const { share_usage_data: isSharingData } = daemonSettings; + + dispatch({ + type: ACTIONS.CHANNEL_SUBSCRIBE, + data: subscription, + }); + + // if the user isn't sharing data, keep the subscriptions entirely in the app + if (isSharingData) { + const { claimId } = parseURI(subscription.uri); + // They are sharing data, we can store their subscriptions in our internal database + Lbryio.call('subscription', 'new', { + channel_name: subscription.channelName, + claim_id: claimId, + }); + } + + dispatch(doCheckSubscription(subscription, true)); +}; + +export const doChannelUnsubscribe = (subscription: Subscription) => ( + dispatch: Dispatch, + getState: () => any +) => { + const { + settings: { daemonSettings }, + } = getState(); + const { share_usage_data: isSharingData } = daemonSettings; + + dispatch({ + type: ACTIONS.CHANNEL_UNSUBSCRIBE, + data: subscription, + }); + + if (isSharingData) { + const { claimId } = parseURI(subscription.uri); + Lbryio.call('subscription', 'delete', { + claim_id: claimId, + }); + } +}; + +export const doCheckSubscriptions = () => ( + dispatch: Dispatch, + getState: () => SubscriptionState +) => { + const checkSubscriptionsTimer = setInterval( + () => + selectSubscriptions(getState()).map((subscription: Subscription) => + dispatch(doCheckSubscription(subscription, true)) + ), + CHECK_SUBSCRIPTIONS_INTERVAL + ); + dispatch({ + type: ACTIONS.CHECK_SUBSCRIPTIONS_SUBSCRIBE, + data: { checkSubscriptionsTimer }, + }); +}; diff --git a/src/renderer/redux/reducers/subscriptions.js b/src/renderer/redux/reducers/subscriptions.js index 7a54e2d46..348528007 100644 --- a/src/renderer/redux/reducers/subscriptions.js +++ b/src/renderer/redux/reducers/subscriptions.js @@ -2,12 +2,7 @@ import * as ACTIONS from 'constants/action_types'; import * as NOTIFICATION_TYPES from 'constants/notification_types'; import { handleActions } from 'util/redux-utils'; - -export type Subscription = { - channelName: string, - uri: string, - latest: ?string, -}; +import type { Subscription } from 'types/subscription'; export type NotificationType = | NOTIFICATION_TYPES.DOWNLOADING @@ -24,8 +19,8 @@ export type SubscriptionNotifications = { // Subscription redux types export type SubscriptionState = { subscriptions: Array<Subscription>, - hasFetchedSubscriptions: boolean, notifications: SubscriptionNotifications, + loading: boolean, }; // Subscription action types @@ -39,10 +34,6 @@ type doChannelUnsubscribe = { data: Subscription, }; -type HasFetchedSubscriptions = { - type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS, -}; - type setSubscriptionLatest = { type: ACTIONS.SET_SUBSCRIPTION_LATEST, data: { @@ -75,10 +66,14 @@ type CheckSubscriptionCompleted = { type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED, }; +type fetchedSubscriptionsSucess = { + type: ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS, + data: Array<Subscription>, +}; + export type Action = | doChannelSubscribe | doChannelUnsubscribe - | HasFetchedSubscriptions | setSubscriptionLatest | setSubscriptionNotification | CheckSubscriptionStarted @@ -88,8 +83,8 @@ export type Dispatch = (action: Action) => any; const defaultState = { subscriptions: [], - hasFetchedSubscriptions: false, notifications: {}, + loading: false, }; export default handleActions( @@ -122,10 +117,6 @@ export default handleActions( subscriptions: newSubscriptions, }; }, - [ACTIONS.HAS_FETCHED_SUBSCRIPTIONS]: (state: SubscriptionState): SubscriptionState => ({ - ...state, - hasFetchedSubscriptions: true, - }), [ACTIONS.SET_SUBSCRIPTION_LATEST]: ( state: SubscriptionState, action: setSubscriptionLatest @@ -155,6 +146,22 @@ export default handleActions( ...state, notifications: action.data.notifications, }), + [ACTIONS.FETCH_SUBSCRIPTIONS_START]: (state: SubscriptionState): SubscriptionState => ({ + ...state, + loading: true, + }), + [ACTIONS.FETCH_SUBSCRIPTIONS_FAIL]: (state: SubscriptionState): SubscriptionState => ({ + ...state, + loading: false, + }), + [ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS]: ( + state: SubscriptionState, + action: fetchedSubscriptionsSucess + ): SubscriptionState => ({ + ...state, + loading: false, + subscriptions: action.data, + }), }, defaultState ); diff --git a/src/renderer/redux/selectors/subscriptions.js b/src/renderer/redux/selectors/subscriptions.js index 8f5f9d6ba..1e9ca71cd 100644 --- a/src/renderer/redux/selectors/subscriptions.js +++ b/src/renderer/redux/selectors/subscriptions.js @@ -1,15 +1,21 @@ import { createSelector } from 'reselect'; -import { selectAllClaimsByChannel, selectClaimsById } from 'lbry-redux'; +import { + selectAllClaimsByChannel, + selectClaimsById, + selectAllFetchingChannelClaims, +} from 'lbry-redux'; // get the entire subscriptions state const selectState = state => state.subscriptions || {}; +export const selectIsFetchingSubscriptions = createSelector(selectState, state => state.loading); + export const selectNotifications = createSelector(selectState, state => state.notifications); // list of saved channel names and uris export const selectSubscriptions = createSelector(selectState, state => state.subscriptions); -export const selectSubscriptionsFromClaims = createSelector( +export const selectSubscriptionClaims = createSelector( selectAllClaimsByChannel, selectClaimsById, selectSubscriptions, @@ -37,9 +43,6 @@ export const selectSubscriptionsFromClaims = createSelector( }); } - // all we really need is a uri for each claim - channelClaims = channelClaims.map(claim => `${claim.name}#${claim.claim_id}`); - fetchedSubscriptions.push({ claims: channelClaims, channelName: subscription.channelName, @@ -50,3 +53,19 @@ export const selectSubscriptionsFromClaims = createSelector( return fetchedSubscriptions; } ); + +export const selectSubscriptionsBeingFetched = createSelector( + selectSubscriptions, + selectAllFetchingChannelClaims, + (subscriptions, fetchingChannelClaims) => { + const fetchingSubscriptionMap = {}; + subscriptions.forEach(sub => { + const isFetching = fetchingChannelClaims && fetchingChannelClaims[sub.uri]; + if (isFetching) { + fetchingSubscriptionMap[sub.uri] = 1; + } + }); + + return fetchingSubscriptionMap; + } +); diff --git a/src/renderer/scss/_gui.scss b/src/renderer/scss/_gui.scss index 9904a94f9..8fc1aeeb9 100644 --- a/src/renderer/scss/_gui.scss +++ b/src/renderer/scss/_gui.scss @@ -221,6 +221,9 @@ p { margin-top: 200px; text-align: center; font-family: 'metropolis-medium'; + display: flex; + flex-direction: column; + align-items: center; } .columns { diff --git a/src/renderer/scss/component/_card.scss b/src/renderer/scss/component/_card.scss index e190cd177..8bd63a98d 100644 --- a/src/renderer/scss/component/_card.scss +++ b/src/renderer/scss/component/_card.scss @@ -299,23 +299,10 @@ display: inline-block; vertical-align: top; margin-bottom: 60px; + width: calc((100% / 4) - (60px / 4)); - @media only screen and (max-width: $medium-breakpoint) { - width: calc((100% / 3) - (40px / 3)); - - &:not(:nth-child(3n + 1)) { - margin-left: 20px; - } - } - } - - @media only screen and (min-width: $medium-breakpoint) { - .card { - width: calc((100% / 4) - (60px / 4)); - - &:not(:nth-child(4n + 1)) { - margin-left: 20px; - } + &:not(:nth-child(4n + 1)) { + margin-left: 20px; } } } @@ -341,8 +328,8 @@ display: inline-block; vertical-align: top; overflow: visible; - // 35 px to handle to padding between cards - width: calc((100% / 3) - 35px); + // 31 px to handle to padding between cards + width: calc((100% / 4) - 31px); } .card:not(:first-of-type) { @@ -352,13 +339,6 @@ .card:last-of-type { margin-right: 20px; } - - @media only screen and (min-width: $medium-breakpoint) { - .card { - // 31 px to handle to padding between cards - width: calc((100% / 4) - 31px); - } - } } .card__success-msg { diff --git a/src/renderer/scss/component/_spinner.scss b/src/renderer/scss/component/_spinner.scss index 09433cbb1..bdeda87ab 100644 --- a/src/renderer/scss/component/_spinner.scss +++ b/src/renderer/scss/component/_spinner.scss @@ -10,7 +10,6 @@ height: 100%; width: 6px; margin: 0 2px; - background-color: var(--color-white); animation: sk-stretchdelay 1.2s infinite ease-in-out; &.rect2 { @@ -31,6 +30,12 @@ } } +.spinner--light { + .rect { + background-color: var(--color-white); + } +} + .spinner--dark { .rect { background-color: var(--color-black); diff --git a/src/renderer/store.js b/src/renderer/store.js index 7ab581a7b..690387ffe 100644 --- a/src/renderer/store.js +++ b/src/renderer/store.js @@ -102,6 +102,7 @@ const store = createStore( const compressor = createCompressor(); const saveClaimsFilter = createFilter('claims', ['byId', 'claimsByUri']); const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']); + // We only need to persist the receiveAddress for the wallet const walletFilter = createFilter('wallet', ['receiveAddress']); diff --git a/src/renderer/types/claim.js b/src/renderer/types/claim.js new file mode 100644 index 000000000..2f858a9a6 --- /dev/null +++ b/src/renderer/types/claim.js @@ -0,0 +1,30 @@ +// @flow + +// Actual claim type has more values than this +// Add them as they are used +export type Claim = { + address: string, + amount: number, + claim_id: string, + claim_sequence: number, + decoded_claim: boolean, + depth: number, + effective_amount: number, + has_signature: boolean, + height: number, + has_signature: boolean, + hex: string, + name: string, + nout: number, + permanent_url: string, + channel_name: ?string, + txid: string, + nout: number, + signature_is_valid: boolean, + valid_at_height: number, + value: { + publisherSignature: ?{ + certificateId: ?string, + }, + }, +}; diff --git a/src/renderer/types/subscription.js b/src/renderer/types/subscription.js new file mode 100644 index 000000000..dc7a4c744 --- /dev/null +++ b/src/renderer/types/subscription.js @@ -0,0 +1,7 @@ +// @flow + +export type Subscription = { + channelName: string, // @CryptoCandor, + uri: string, // lbry://@CryptoCandor#9152f3b054f692076a6882d1b58a30e8781cc8e6 + latest: string, // substratum#b0ab143243020e7831fd070d9f871e1fda948620 +}; diff --git a/src/renderer/util/dom.js b/src/renderer/util/dom.js new file mode 100644 index 000000000..3f93a94a8 --- /dev/null +++ b/src/renderer/util/dom.js @@ -0,0 +1,12 @@ +// @flow +import * as React from 'react'; + +// is a child component being rendered? +export const isShowingChildren = (children: React.Node): boolean => { + if (Array.isArray(children)) { + const firstChildIndex = children.findIndex(child => child); + return firstChildIndex > -1; + } + + return !!children; +};