Merge branch 'internal-subscriptions'

This commit is contained in:
Jeremy Kauffman 2018-05-15 11:16:40 -04:00
commit 1c0275d69f
38 changed files with 1090 additions and 294 deletions

View file

@ -34,6 +34,7 @@
"singleQuote": true "singleQuote": true
}], }],
"func-names": ["warn", "as-needed"], "func-names": ["warn", "as-needed"],
"jsx-a11y/label-has-for": 0 "jsx-a11y/label-has-for": 0,
"import/prefer-default-export": 0
} }
} }

View file

@ -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>

12
.idea/lbry-app.iml generated Normal file
View file

@ -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>

6
.idea/misc.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="FLOW" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View file

@ -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>

6
.idea/vcs.xml generated Normal file
View file

@ -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>

547
.idea/workspace.xml generated Normal file
View file

@ -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="{&quot;axios&quot;: &quot;^0.18.0&quot;...}" />
</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="{&quot;axios&quot;: &quot;^0.18.0&quot;...}" />
</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="{&quot;axios&quot;: &quot;^0.18.0&quot;...}" />
</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="{&quot;axios&quot;: &quot;^0.18.0&quot;...}" />
</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="{&quot;axios&quot;: &quot;^0.18.0&quot;...}" />
</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="{&quot;axios&quot;: &quot;^0.18.0&quot;...}" />
</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>

View file

@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
### Changed ### Changed
* Add flair to snackbar ([#1313](https://github.com/lbryio/lbry-app/pull/1313)) * 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)) * 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 ### Fixed
* Fix content-type not shown correctly in file description ([#863](https://github.com/lbryio/lbry-app/pull/863)) * Fix content-type not shown correctly in file description ([#863](https://github.com/lbryio/lbry-app/pull/863))

View file

@ -2,7 +2,6 @@
import mixpanel from 'mixpanel-browser'; import mixpanel from 'mixpanel-browser';
import Lbryio from 'lbryio'; import Lbryio from 'lbryio';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
import type { Subscription } from 'redux/reducers/subscriptions';
if (isDev) { if (isDev) {
mixpanel.init('691723e855cabb9d27a7a79002216967'); mixpanel.init('691723e855cabb9d27a7a79002216967');
@ -15,8 +14,6 @@ type Analytics = {
setUser: Object => void, setUser: Object => void,
toggle: (boolean, ?boolean) => void, toggle: (boolean, ?boolean) => void,
apiLogView: (string, string, string) => void, apiLogView: (string, string, string) => void,
apiLogSubscribe: Subscription => void,
apiLogUnsubscribe: Subscription => void,
}; };
let analyticsEnabled: boolean = false; let analyticsEnabled: boolean = false;
@ -56,20 +53,6 @@ const analytics: Analytics = {
}).catch(() => {}); }).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; export default analytics;

View file

@ -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;

View file

@ -3,9 +3,10 @@ import * as React from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import Button from 'component/button'; import Button from 'component/button';
import path from 'path'; import path from 'path';
import type { Claim } from 'types/claim';
type Props = { type Props = {
claim: {}, claim: Claim,
fileInfo: { fileInfo: {
download_path: string, download_path: string,
}, },

View file

@ -53,7 +53,6 @@ class FileList extends React.PureComponent<Props, State> {
if (fileInfo1.pending) { if (fileInfo1.pending) {
return -1; return -1;
} }
const height1 = this.props.claimsById[fileInfo1.claim_id] const height1 = this.props.claimsById[fileInfo1.claim_id]
? this.props.claimsById[fileInfo1.claim_id].height ? this.props.claimsById[fileInfo1.claim_id].height
: 0; : 0;
@ -145,6 +144,10 @@ class FileList extends React.PureComponent<Props, State> {
const { sortBy } = this.state; const { sortBy } = this.state;
const content = []; const content = [];
if (!fileInfos) {
return null;
}
this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => { this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
const { const {
channel_name: channelName, channel_name: channelName,

View file

@ -1,33 +1,98 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import classnames from 'classnames'; 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 = { type Props = {
children: React.Node, children: React.Node | Array<React.Node>,
pageTitle: ?string, pageTitle: ?string,
noPadding: ?boolean, noPadding: ?boolean,
extraPadding: ?boolean, extraPadding: ?boolean,
notContained: ?boolean, // No max-width, but keep the padding notContained: ?boolean, // No max-width, but keep the padding
loading: ?boolean,
}; };
const Page = (props: Props) => { type State = {
const { pageTitle, children, noPadding, extraPadding, notContained } = props; showLoader: ?boolean,
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>
);
}; };
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; export default Page;

View file

@ -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);

View file

@ -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;

View file

@ -3,7 +3,7 @@ import React from 'react';
import { MODALS } from 'lbry-redux'; import { MODALS } from 'lbry-redux';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
import Button from 'component/button'; import Button from 'component/button';
import type { Subscription } from 'redux/reducers/subscriptions'; import type { Subscription } from 'types/subscription';
type SubscribtionArgs = { type SubscribtionArgs = {
channelName: string, channelName: string,

View file

@ -3,21 +3,18 @@ import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import { buildURI } from 'lbry-redux'; import { buildURI } from 'lbry-redux';
import classnames from 'classnames'; import classnames from 'classnames';
// import Icon from 'component/common/icon'; import type { Claim } from 'types/claim';
type Props = { type Props = {
isResolvingUri: boolean, isResolvingUri: boolean,
resolveUri: string => void, claim: Claim,
claim: {
channel_name: string,
has_signature: boolean,
signature_is_valid: boolean,
value: {
publisherSignature: { certificateId: string },
},
},
uri: string,
link: ?boolean, 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> { class UriIndicator extends React.PureComponent<Props> {

View file

@ -1,6 +1,6 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import Spinner from 'component/common/spinner'; import Spinner from 'component/spinner';
type Props = { type Props = {
spinner: boolean, spinner: boolean,
@ -16,7 +16,7 @@ class LoadingScreen extends React.PureComponent<Props> {
const { status, spinner } = this.props; const { status, spinner } = this.props;
return ( return (
<div className="content__loading"> <div className="content__loading">
{spinner && <Spinner />} {spinner && <Spinner light />}
<span className="content__loading-text">{status}</span> <span className="content__loading-text">{status}</span>
</div> </div>

View file

@ -2,6 +2,7 @@
import React from 'react'; import React from 'react';
import { Lbry } from 'lbry-redux'; import { Lbry } from 'lbry-redux';
import classnames from 'classnames'; import classnames from 'classnames';
import type { Claim } from 'types/claim';
import VideoPlayer from './internal/player'; import VideoPlayer from './internal/player';
import VideoPlayButton from './internal/play-button'; import VideoPlayButton from './internal/play-button';
import LoadingScreen from './internal/loading-screen'; import LoadingScreen from './internal/loading-screen';
@ -26,7 +27,7 @@ type Props = {
contentType: string, contentType: string,
changeVolume: number => void, changeVolume: number => void,
volume: number, volume: number,
claim: {}, claim: Claim,
uri: string, uri: string,
doPlay: () => void, doPlay: () => void,
doPause: () => void, doPause: () => void,

View file

@ -3,11 +3,12 @@ import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import UriIndicator from 'component/uriIndicator'; import UriIndicator from 'component/uriIndicator';
import type { Claim } from 'types/claim';
type Props = { type Props = {
uri: string, uri: string,
title: string, title: string,
claim: { claim_id: string }, claim: Claim,
errorMessage: string, errorMessage: string,
isPending: boolean, isPending: boolean,
sendSupport: (number, string, string) => void, sendSupport: (number, string, string) => void,

View file

@ -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 WINDOW_FOCUSED = 'WINDOW_FOCUSED';
export const DAEMON_READY = 'DAEMON_READY'; export const DAEMON_READY = 'DAEMON_READY';
export const DAEMON_VERSION_MATCH = 'DAEMON_VERSION_MATCH'; 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_STARTED = 'CHECK_SUBSCRIPTION_STARTED';
export const CHECK_SUBSCRIPTION_COMPLETED = 'CHECK_SUBSCRIPTION_COMPLETED'; export const CHECK_SUBSCRIPTION_COMPLETED = 'CHECK_SUBSCRIPTION_COMPLETED';
export const CHECK_SUBSCRIPTIONS_SUBSCRIBE = 'CHECK_SUBSCRIPTIONS_SUBSCRIBE'; 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 // Video controls
export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE'; export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE';

View file

@ -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';

View file

@ -6,6 +6,7 @@ import ReactPaginate from 'react-paginate';
import SubscribeButton from 'component/subscribeButton'; import SubscribeButton from 'component/subscribeButton';
import Page from 'component/page'; import Page from 'component/page';
import FileList from 'component/fileList'; import FileList from 'component/fileList';
import type { Claim } from 'types/claim';
type Props = { type Props = {
uri: string, uri: string,
@ -13,10 +14,7 @@ type Props = {
totalPages: number, totalPages: number,
fetching: boolean, fetching: boolean,
params: { page: number }, params: { page: number },
claim: { claim: Claim,
name: string,
claim_id: string,
},
claimsInChannel: Array<{}>, claimsInChannel: Array<{}>,
fetchClaims: (string, number) => void, fetchClaims: (string, number) => void,
fetchClaimCount: string => void, fetchClaimCount: string => void,
@ -58,8 +56,8 @@ class ChannelPage extends React.PureComponent<Props> {
} }
render() { render() {
const { fetching, claimsInChannel, claim, uri, page, totalPages } = this.props; const { fetching, claimsInChannel, claim, page, totalPages } = this.props;
const { name } = claim; const { name, permanent_url: permanentUrl } = claim;
let contentList; let contentList;
if (fetching) { if (fetching) {
@ -78,7 +76,7 @@ class ChannelPage extends React.PureComponent<Props> {
<section className="card__channel-info card__channel-info--large"> <section className="card__channel-info card__channel-info--large">
<h1>{name}</h1> <h1>{name}</h1>
<div className="card__actions card__actions--no-margin"> <div className="card__actions card__actions--no-margin">
<SubscribeButton uri={uri} channelName={name} /> <SubscribeButton uri={permanentUrl} channelName={name} />
</div> </div>
</section> </section>
<section>{contentList}</section> <section>{contentList}</section>

View file

@ -16,18 +16,10 @@ import SubscribeButton from 'component/subscribeButton';
import Page from 'component/page'; import Page from 'component/page';
import player from 'render-media'; import player from 'render-media';
import * as settings from 'constants/settings'; import * as settings from 'constants/settings';
import type { Claim } from 'types/claim';
type Props = { type Props = {
claim: { claim: Claim,
claim_id: string,
height: number,
channel_name: string,
value: {
publisherSignature: ?{
certificateId: ?string,
},
},
},
fileInfo: {}, fileInfo: {},
metadata: { metadata: {
title: string, title: string,

View file

@ -5,16 +5,13 @@ import ChannelPage from 'page/channel';
import FilePage from 'page/file'; import FilePage from 'page/file';
import Page from 'component/page'; import Page from 'component/page';
import Button from 'component/button'; import Button from 'component/button';
import type { Claim } from 'types/claim';
type Props = { type Props = {
isResolvingUri: boolean, isResolvingUri: boolean,
resolveUri: string => void, resolveUri: string => void,
uri: string, uri: string,
claim: { claim: Claim,
name: string,
txid: string,
nout: number,
},
blackListedOutpoints: Array<{ blackListedOutpoints: Array<{
txid: string, txid: string,
nout: number, nout: number,

View file

@ -1,27 +1,25 @@
import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
selectSubscriptionsFromClaims, selectSubscriptionClaims,
selectSubscriptions, selectSubscriptions,
selectHasFetchedSubscriptions, selectSubscriptionsBeingFetched,
selectIsFetchingSubscriptions,
selectNotifications, selectNotifications,
} from 'redux/selectors/subscriptions'; } from 'redux/selectors/subscriptions';
import { doFetchClaimsByChannel } from 'redux/actions/content'; import { doFetchClaimsByChannel } from 'redux/actions/content';
import { import { setSubscriptionNotifications, doFetchMySubscriptions } from 'redux/actions/subscriptions';
setHasFetchedSubscriptions,
setSubscriptionNotifications,
} from 'redux/actions/subscriptions';
import SubscriptionsPage from './view'; import SubscriptionsPage from './view';
const select = state => ({ const select = state => ({
hasFetchedSubscriptions: state.subscriptions.hasFetchedSubscriptions, isFetchingSubscriptions: selectIsFetchingSubscriptions(state),
savedSubscriptions: selectSubscriptions(state), subscriptionsBeingFetched: selectSubscriptionsBeingFetched(state),
subscriptions: selectSubscriptionsFromClaims(state), subscriptions: selectSubscriptions(state),
subscriptionClaims: selectSubscriptionClaims(state),
notifications: selectNotifications(state), notifications: selectNotifications(state),
}); });
export default connect(select, { export default connect(select, {
doFetchClaimsByChannel, doFetchClaimsByChannel,
setHasFetchedSubscriptions,
setSubscriptionNotifications, setSubscriptionNotifications,
doFetchMySubscriptions,
})(SubscriptionsPage); })(SubscriptionsPage);

View file

@ -1,39 +1,28 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import Page from 'component/page'; import Page from 'component/page';
import CategoryList from 'component/common/category-list'; import type { Subscription } from 'types/subscription';
import type { Subscription } from 'redux/reducers/subscriptions';
import * as NOTIFICATION_TYPES from 'constants/notification_types'; import * as NOTIFICATION_TYPES from 'constants/notification_types';
import Button from 'component/button'; import Button from 'component/button';
import FileList from 'component/fileList';
type SavedSubscriptions = Array<Subscription>; import type { Claim } from 'types/claim';
type Props = { type Props = {
doFetchClaimsByChannel: (string, number) => any, doFetchClaimsByChannel: (string, number) => void,
savedSubscriptions: SavedSubscriptions, doFetchMySubscriptions: () => void,
// TODO build out claim types setSubscriptionNotifications: ({}) => void,
subscriptions: Array<any>, subscriptions: Array<Subscription>,
setHasFetchedSubscriptions: () => void, isFetchingSubscriptions: boolean,
hasFetchedSubscriptions: boolean, subscriptionClaims: Array<{ uri: string, claims: Array<Claim> }>,
subscriptionsBeingFetched: {},
notifications: {},
}; };
export default class extends React.PureComponent<Props> { 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() { componentDidMount() {
const { const { notifications, setSubscriptionNotifications, doFetchMySubscriptions } = this.props;
savedSubscriptions, doFetchMySubscriptions();
setHasFetchedSubscriptions,
notifications,
setSubscriptionNotifications,
} = this.props;
if (savedSubscriptions.length) {
this.fetchSubscriptions(savedSubscriptions);
setHasFetchedSubscriptions();
}
const newNotifications = {}; const newNotifications = {};
Object.keys(notifications).forEach(cur => { Object.keys(notifications).forEach(cur => {
if (notifications[cur].type === NOTIFICATION_TYPES.DOWNLOADING) { if (notifications[cur].type === NOTIFICATION_TYPES.DOWNLOADING) {
@ -43,40 +32,37 @@ export default class extends React.PureComponent<Props> {
setSubscriptionNotifications(newNotifications); setSubscriptionNotifications(newNotifications);
} }
componentWillReceiveProps(props: Props) { componentDidUpdate() {
const { savedSubscriptions, hasFetchedSubscriptions, setHasFetchedSubscriptions } = props; const {
subscriptions,
subscriptionClaims,
doFetchClaimsByChannel,
subscriptionsBeingFetched,
} = this.props;
if (!hasFetchedSubscriptions && savedSubscriptions.length) { const subscriptionClaimMap = {};
this.fetchSubscriptions(savedSubscriptions); subscriptionClaims.forEach(claim => {
setHasFetchedSubscriptions(); subscriptionClaimMap[claim.uri] = 1;
} });
}
fetchSubscriptions(savedSubscriptions: SavedSubscriptions) { subscriptions.forEach(sub => {
const { doFetchClaimsByChannel } = this.props; if (!subscriptionClaimMap[sub.uri] && !subscriptionsBeingFetched[sub.uri]) {
if (savedSubscriptions.length) {
// can this use batchActions?
savedSubscriptions.forEach(sub => {
doFetchClaimsByChannel(sub.uri, 1); doFetchClaimsByChannel(sub.uri, 1);
}); }
} });
} }
render() { 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) let claimList = [];
const someClaimsNotLoaded = Boolean( subscriptionClaims.forEach(claimData => {
subscriptions.find(subscription => !subscription.claims.length) claimList = claimList.concat(claimData.claims);
); });
const fetchingSubscriptions =
!!savedSubscriptions.length &&
(subscriptions.length !== savedSubscriptions.length || someClaimsNotLoaded);
return ( return (
<Page noPadding isLoading={fetchingSubscriptions}> <Page notContained loading={isFetchingSubscriptions}>
{!savedSubscriptions.length && ( {!subscriptions.length && (
<div className="page__empty"> <div className="page__empty">
{__("It looks like you aren't subscribed to any channels yet.")} {__("It looks like you aren't subscribed to any channels yet.")}
<div className="card__actions card__actions--center"> <div className="card__actions card__actions--center">
@ -84,28 +70,7 @@ export default class extends React.PureComponent<Props> {
</div> </div>
</div> </div>
)} )}
{!!savedSubscriptions.length && ( {!!claimList.length && <FileList hideFilter sortByHeight fileInfos={claimList} />}
<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>
)}
</Page> </Page>
); );
} }

View file

@ -138,6 +138,7 @@ export function doUpdateLoadStatus(uri, outpoint) {
: acc, : acc,
0 0
); );
const notif = new window.Notification(notifications[uri].subscription.channelName, { const notif = new window.Notification(notifications[uri].subscription.channelName, {
body: `Posted ${fileInfo.metadata.title}${ body: `Posted ${fileInfo.metadata.title}${
count > 1 && count < 10 ? ` and ${count - 1} other new items` : '' count > 1 && count < 10 ? ` and ${count - 1} other new items` : ''

View file

@ -2,56 +2,127 @@
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import * as NOTIFICATION_TYPES from 'constants/notification_types'; import * as NOTIFICATION_TYPES from 'constants/notification_types';
import type { import type {
Subscription,
Dispatch, Dispatch,
SubscriptionState, SubscriptionState,
SubscriptionNotifications, SubscriptionNotifications,
} from 'redux/reducers/subscriptions'; } from 'redux/reducers/subscriptions';
import type { Subscription } from 'types/subscription';
import { selectSubscriptions } from 'redux/selectors/subscriptions'; 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 { doPurchaseUri } from 'redux/actions/content';
import { doNavigate } from 'redux/actions/navigation'; import Promise from 'bluebird';
import analytics from 'analytics'; import Lbryio from 'lbryio';
const CHECK_SUBSCRIPTIONS_INTERVAL = 60 * 60 * 1000; const CHECK_SUBSCRIPTIONS_INTERVAL = 60 * 60 * 1000;
const SUBSCRIPTION_DOWNLOAD_LIMIT = 1; const SUBSCRIPTION_DOWNLOAD_LIMIT = 1;
export const doChannelSubscribe = (subscription: Subscription) => (dispatch: Dispatch) => { export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: () => any) => {
dispatch({ const {
type: ACTIONS.CHANNEL_SUBSCRIBE, subscriptions: subscriptionState,
data: subscription, 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({ dispatch({
type: ACTIONS.CHANNEL_UNSUBSCRIBE, type: ACTIONS.SET_SUBSCRIPTION_LATEST,
data: subscription, data: {
subscription,
uri,
},
}); });
analytics.apiLogUnsubscribe(subscription); export const setSubscriptionNotification = (
}; subscription: Subscription,
uri: string,
export const doCheckSubscriptions = () => ( notificationType: string
dispatch: Dispatch, ) => (dispatch: Dispatch) =>
getState: () => SubscriptionState
) => {
const checkSubscriptionsTimer = setInterval(
() =>
selectSubscriptions(getState()).map((subscription: Subscription) =>
dispatch(doCheckSubscription(subscription, true))
),
CHECK_SUBSCRIPTIONS_INTERVAL
);
dispatch({ dispatch({
type: ACTIONS.CHECK_SUBSCRIPTIONS_SUBSCRIBE, type: ACTIONS.SET_SUBSCRIPTION_NOTIFICATION,
data: { checkSubscriptionsTimer }, data: {
subscription,
uri,
type: notificationType,
},
}); });
};
export const doCheckSubscription = (subscription: Subscription, notify?: boolean) => ( export const doCheckSubscription = (subscription: Subscription, notify?: boolean) => (
dispatch: Dispatch 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) => ( export const setSubscriptionNotifications = (notifications: SubscriptionNotifications) => (
dispatch: Dispatch dispatch: Dispatch
) => ) =>
@ -149,5 +195,68 @@ export const setSubscriptionNotifications = (notifications: SubscriptionNotifica
}, },
}); });
export const setHasFetchedSubscriptions = () => (dispatch: Dispatch) => export const doChannelSubscribe = (subscription: Subscription) => (
dispatch({ type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS }); 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 },
});
};

View file

@ -2,12 +2,7 @@
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import * as NOTIFICATION_TYPES from 'constants/notification_types'; import * as NOTIFICATION_TYPES from 'constants/notification_types';
import { handleActions } from 'util/redux-utils'; import { handleActions } from 'util/redux-utils';
import type { Subscription } from 'types/subscription';
export type Subscription = {
channelName: string,
uri: string,
latest: ?string,
};
export type NotificationType = export type NotificationType =
| NOTIFICATION_TYPES.DOWNLOADING | NOTIFICATION_TYPES.DOWNLOADING
@ -24,8 +19,8 @@ export type SubscriptionNotifications = {
// Subscription redux types // Subscription redux types
export type SubscriptionState = { export type SubscriptionState = {
subscriptions: Array<Subscription>, subscriptions: Array<Subscription>,
hasFetchedSubscriptions: boolean,
notifications: SubscriptionNotifications, notifications: SubscriptionNotifications,
loading: boolean,
}; };
// Subscription action types // Subscription action types
@ -39,10 +34,6 @@ type doChannelUnsubscribe = {
data: Subscription, data: Subscription,
}; };
type HasFetchedSubscriptions = {
type: ACTIONS.HAS_FETCHED_SUBSCRIPTIONS,
};
type setSubscriptionLatest = { type setSubscriptionLatest = {
type: ACTIONS.SET_SUBSCRIPTION_LATEST, type: ACTIONS.SET_SUBSCRIPTION_LATEST,
data: { data: {
@ -75,10 +66,14 @@ type CheckSubscriptionCompleted = {
type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED, type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED,
}; };
type fetchedSubscriptionsSucess = {
type: ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS,
data: Array<Subscription>,
};
export type Action = export type Action =
| doChannelSubscribe | doChannelSubscribe
| doChannelUnsubscribe | doChannelUnsubscribe
| HasFetchedSubscriptions
| setSubscriptionLatest | setSubscriptionLatest
| setSubscriptionNotification | setSubscriptionNotification
| CheckSubscriptionStarted | CheckSubscriptionStarted
@ -88,8 +83,8 @@ export type Dispatch = (action: Action) => any;
const defaultState = { const defaultState = {
subscriptions: [], subscriptions: [],
hasFetchedSubscriptions: false,
notifications: {}, notifications: {},
loading: false,
}; };
export default handleActions( export default handleActions(
@ -122,10 +117,6 @@ export default handleActions(
subscriptions: newSubscriptions, subscriptions: newSubscriptions,
}; };
}, },
[ACTIONS.HAS_FETCHED_SUBSCRIPTIONS]: (state: SubscriptionState): SubscriptionState => ({
...state,
hasFetchedSubscriptions: true,
}),
[ACTIONS.SET_SUBSCRIPTION_LATEST]: ( [ACTIONS.SET_SUBSCRIPTION_LATEST]: (
state: SubscriptionState, state: SubscriptionState,
action: setSubscriptionLatest action: setSubscriptionLatest
@ -155,6 +146,22 @@ export default handleActions(
...state, ...state,
notifications: action.data.notifications, 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 defaultState
); );

View file

@ -1,15 +1,21 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { selectAllClaimsByChannel, selectClaimsById } from 'lbry-redux'; import {
selectAllClaimsByChannel,
selectClaimsById,
selectAllFetchingChannelClaims,
} from 'lbry-redux';
// get the entire subscriptions state // get the entire subscriptions state
const selectState = state => state.subscriptions || {}; const selectState = state => state.subscriptions || {};
export const selectIsFetchingSubscriptions = createSelector(selectState, state => state.loading);
export const selectNotifications = createSelector(selectState, state => state.notifications); export const selectNotifications = createSelector(selectState, state => state.notifications);
// list of saved channel names and uris // list of saved channel names and uris
export const selectSubscriptions = createSelector(selectState, state => state.subscriptions); export const selectSubscriptions = createSelector(selectState, state => state.subscriptions);
export const selectSubscriptionsFromClaims = createSelector( export const selectSubscriptionClaims = createSelector(
selectAllClaimsByChannel, selectAllClaimsByChannel,
selectClaimsById, selectClaimsById,
selectSubscriptions, 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({ fetchedSubscriptions.push({
claims: channelClaims, claims: channelClaims,
channelName: subscription.channelName, channelName: subscription.channelName,
@ -50,3 +53,19 @@ export const selectSubscriptionsFromClaims = createSelector(
return fetchedSubscriptions; 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;
}
);

View file

@ -221,6 +221,9 @@ p {
margin-top: 200px; margin-top: 200px;
text-align: center; text-align: center;
font-family: 'metropolis-medium'; font-family: 'metropolis-medium';
display: flex;
flex-direction: column;
align-items: center;
} }
.columns { .columns {

View file

@ -299,23 +299,10 @@
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
margin-bottom: 60px; margin-bottom: 60px;
width: calc((100% / 4) - (60px / 4));
@media only screen and (max-width: $medium-breakpoint) { &:not(:nth-child(4n + 1)) {
width: calc((100% / 3) - (40px / 3)); margin-left: 20px;
&: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;
}
} }
} }
} }
@ -341,8 +328,8 @@
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
overflow: visible; overflow: visible;
// 35 px to handle to padding between cards // 31 px to handle to padding between cards
width: calc((100% / 3) - 35px); width: calc((100% / 4) - 31px);
} }
.card:not(:first-of-type) { .card:not(:first-of-type) {
@ -352,13 +339,6 @@
.card:last-of-type { .card:last-of-type {
margin-right: 20px; 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 { .card__success-msg {

View file

@ -10,7 +10,6 @@
height: 100%; height: 100%;
width: 6px; width: 6px;
margin: 0 2px; margin: 0 2px;
background-color: var(--color-white);
animation: sk-stretchdelay 1.2s infinite ease-in-out; animation: sk-stretchdelay 1.2s infinite ease-in-out;
&.rect2 { &.rect2 {
@ -31,6 +30,12 @@
} }
} }
.spinner--light {
.rect {
background-color: var(--color-white);
}
}
.spinner--dark { .spinner--dark {
.rect { .rect {
background-color: var(--color-black); background-color: var(--color-black);

View file

@ -102,6 +102,7 @@ const store = createStore(
const compressor = createCompressor(); const compressor = createCompressor();
const saveClaimsFilter = createFilter('claims', ['byId', 'claimsByUri']); const saveClaimsFilter = createFilter('claims', ['byId', 'claimsByUri']);
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']); const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
// We only need to persist the receiveAddress for the wallet // We only need to persist the receiveAddress for the wallet
const walletFilter = createFilter('wallet', ['receiveAddress']); const walletFilter = createFilter('wallet', ['receiveAddress']);

View file

@ -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,
},
},
};

View file

@ -0,0 +1,7 @@
// @flow
export type Subscription = {
channelName: string, // @CryptoCandor,
uri: string, // lbry://@CryptoCandor#9152f3b054f692076a6882d1b58a30e8781cc8e6
latest: string, // substratum#b0ab143243020e7831fd070d9f871e1fda948620
};

12
src/renderer/util/dom.js Normal file
View file

@ -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;
};