From ff0478ade32fb0c3e0b7261eb74e8fa2eee62b1d Mon Sep 17 00:00:00 2001
From: Sean Yesmunt <sean@lbry.io>
Date: Mon, 18 Feb 2019 12:24:56 -0500
Subject: [PATCH] feat: add support for search filters

---
 package.json                                  |   8 +-
 .../common/form-components/form-field.jsx     |  22 +++-
 .../component/fileListSearch/index.js         |   9 +-
 .../component/fileListSearch/view.jsx         |  46 ++-----
 src/renderer/component/fileTile/view.jsx      |  12 +-
 src/renderer/component/searchOptions/index.js |  16 +++
 src/renderer/component/searchOptions/view.jsx | 123 ++++++++++++++++++
 src/renderer/component/wunderbar/index.js     |   7 +-
 src/renderer/component/wunderbar/view.jsx     |   9 +-
 src/renderer/constants/icons.js               |   3 +
 src/renderer/page/search/index.js             |   6 -
 src/renderer/page/search/view.jsx             |  31 +----
 src/renderer/scss/component/_button.scss      |   6 +-
 src/renderer/scss/component/_form-field.scss  |  30 +++++
 src/renderer/scss/component/_media.scss       |  26 +++-
 src/renderer/scss/component/_search.scss      |  44 +++++++
 src/renderer/scss/init/_gui.scss              |   4 +-
 src/renderer/scss/init/_mixins.scss           |  50 -------
 src/renderer/store.js                         |   5 +-
 yarn.lock                                     |  36 +++--
 20 files changed, 316 insertions(+), 177 deletions(-)
 create mode 100644 src/renderer/component/searchOptions/index.js
 create mode 100644 src/renderer/component/searchOptions/view.jsx

diff --git a/package.json b/package.json
index be807332d..0a67781fb 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
     "postinstall": "electron-builder install-app-deps && node build/downloadDaemon.js"
   },
   "dependencies": {
+    "@lbry/components": "^2.2.0",
     "@types/three": "^0.93.1",
     "bluebird": "^3.5.1",
     "breakdance": "^3.0.1",
@@ -52,7 +53,7 @@
     "hast-util-sanitize": "^1.1.2",
     "keytar": "^4.2.1",
     "lbry-format": "https://github.com/lbryio/lbry-format.git",
-    "lbry-redux": "lbryio/lbry-redux#42c185e922a7c6091b0e1580bacbfd8e02f45a91",
+    "lbry-redux": "lbryio/lbry-redux#3ab065b11a52d3e2e6a50a25459f9ff0aac03b13",
     "lbryinc": "lbryio/lbryinc#60d80401891743f991c040bafa8e51da7e939777",
     "localforage": "^1.7.1",
     "mammoth": "^1.4.6",
@@ -62,8 +63,8 @@
     "node-fetch": "^2.3.0",
     "qrcode.react": "^0.8.0",
     "rc-progress": "^2.0.6",
-    "react": "^16.6.0",
-    "react-dom": "^16.6.0",
+    "react": "^16.8.2",
+    "react-dom": "^16.8.2",
     "react-feather": "^1.0.8",
     "react-modal": "^3.1.7",
     "react-paginate": "^5.2.1",
@@ -89,7 +90,6 @@
     "y18n": "^4.0.0"
   },
   "devDependencies": {
-    "@lbry/components": "^2.2.0",
     "babel-eslint": "^8.2.2",
     "babel-plugin-module-resolver": "^3.1.1",
     "babel-polyfill": "^6.26.0",
diff --git a/src/renderer/component/common/form-components/form-field.jsx b/src/renderer/component/common/form-components/form-field.jsx
index 7ed2209d2..3e2ba111c 100644
--- a/src/renderer/component/common/form-components/form-field.jsx
+++ b/src/renderer/component/common/form-components/form-field.jsx
@@ -29,14 +29,16 @@ type Props = {
     disabled?: boolean,
   },
   inputButton: ?React.Node,
+  blockWrap: boolean,
 };
 
 export class FormField extends React.PureComponent<Props> {
   static defaultProps = {
     labelOnLeft: false,
+    blockWrap: true,
   };
 
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
     this.input = React.createRef();
   }
@@ -66,31 +68,39 @@ export class FormField extends React.PureComponent<Props> {
       autoFocus,
       inputButton,
       labelOnLeft,
+      blockWrap,
       ...inputProps
     } = this.props;
     const errorMessage = typeof error === 'object' ? error.message : error;
 
+    const Wrapper = blockWrap
+      ? ({ children: innerChildren }) => <fieldset-section>{innerChildren}</fieldset-section>
+      : ({ children: innerChildren }) => <React.Fragment>{innerChildren}</React.Fragment>;
+
     let input;
     if (type) {
       if (type === 'radio') {
         input = (
-          <fieldset-section>
+          <Wrapper>
             <radio-element>
               <input id={name} type="radio" {...inputProps} />
               <label htmlFor={name}>{label}</label>
               <radio-toggle onClick={inputProps.onChange} />
             </radio-element>
-          </fieldset-section>
+          </Wrapper>
         );
       } else if (type === 'checkbox') {
+        // web components treat props weird
+        // we need to fully remove it for proper component:attribute css styling
+        const elementProps = inputProps.disabled ? { disabled: true } : {};
         input = (
-          <fieldset-section>
-            <checkbox-element>
+          <Wrapper>
+            <checkbox-element {...elementProps}>
               <input id={name} type="checkbox" {...inputProps} />
               <label htmlFor={name}>{label}</label>
               <checkbox-toggle onClick={inputProps.onChange} />
             </checkbox-element>
-          </fieldset-section>
+          </Wrapper>
         );
       } else if (type === 'setting') {
         // 'setting' should only be used for settings. Forms should use "checkbox"
diff --git a/src/renderer/component/fileListSearch/index.js b/src/renderer/component/fileListSearch/index.js
index 351776461..32b04d220 100644
--- a/src/renderer/component/fileListSearch/index.js
+++ b/src/renderer/component/fileListSearch/index.js
@@ -1,9 +1,14 @@
 import { connect } from 'react-redux';
-import { makeSelectSearchUris, selectIsSearching, selectSearchDownloadUris } from 'lbry-redux';
+import {
+  makeSelectSearchUris,
+  selectIsSearching,
+  selectSearchDownloadUris,
+  makeSelectQueryWithOptions,
+} from 'lbry-redux';
 import FileListSearch from './view';
 
 const select = (state, props) => ({
-  uris: makeSelectSearchUris(props.query)(state),
+  uris: makeSelectSearchUris(makeSelectQueryWithOptions()(state))(state),
   downloadUris: selectSearchDownloadUris(props.query)(state),
   isSearching: selectIsSearching(state),
 });
diff --git a/src/renderer/component/fileListSearch/view.jsx b/src/renderer/component/fileListSearch/view.jsx
index 35f95b9ab..d4398fb2b 100644
--- a/src/renderer/component/fileListSearch/view.jsx
+++ b/src/renderer/component/fileListSearch/view.jsx
@@ -11,26 +11,11 @@ type Props = {
   query: string,
   isSearching: boolean,
   uris: ?Array<string>,
-  downloadUris: ?Array<string>,
 };
 
 class FileListSearch extends React.PureComponent<Props> {
   render() {
-    const { uris, query, downloadUris, isSearching } = this.props;
-
-    const fileResults = [];
-    const channelResults = [];
-    if (uris && uris.length) {
-      uris.forEach(uri => {
-        const isChannel = parseURI(uri).claimName[0] === '@';
-        if (isChannel) {
-          channelResults.push(uri);
-        } else {
-          fileResults.push(uri);
-        }
-      });
-    }
-
+    const { uris, query, isSearching } = this.props;
     return (
       query && (
         <React.Fragment>
@@ -38,26 +23,15 @@ class FileListSearch extends React.PureComponent<Props> {
             <section className="search__results-section">
               <div className="search__results-title">{__('Search Results')}</div>
               <HiddenNsfwClaims uris={uris} />
-              {!isSearching && fileResults.length ? (
-                fileResults.map(uri => <FileTile key={uri} uri={uri} />)
-              ) : (
-                <NoResults />
-              )}
-            </section>
-
-            <section className="search__results-section">
-              <div className="search__results-title">{__('Channels')}</div>
-              {!isSearching && channelResults.length ? (
-                channelResults.map(uri => <ChannelTile key={uri} uri={uri} />)
-              ) : (
-                <NoResults />
-              )}
-            </section>
-
-            <section className="search__results-section">
-              <div className="search__results-title">{__('Your downloads')}</div>
-              {downloadUris && downloadUris.length ? (
-                downloadUris.map(uri => <FileTile hideNoResult key={uri} uri={uri} />)
+              {!isSearching && uris && uris.length ? (
+                uris.map(
+                  uri =>
+                    parseURI(uri).claimName[0] === '@' ? (
+                      <ChannelTile key={uri} uri={uri} />
+                    ) : (
+                      <FileTile key={uri} uri={uri} />
+                    )
+                )
               ) : (
                 <NoResults />
               )}
diff --git a/src/renderer/component/fileTile/view.jsx b/src/renderer/component/fileTile/view.jsx
index 9527ed0dc..863c8fae5 100644
--- a/src/renderer/component/fileTile/view.jsx
+++ b/src/renderer/component/fileTile/view.jsx
@@ -156,10 +156,12 @@ class FileTile extends React.PureComponent<Props> {
             <Fragment>
               <div className="media__title">
                 {(title || name) && (
-                  <TruncatedText text={title || name} lines={size === 'small' ? 2 : 3} />
+                  <TruncatedText text={title || name} lines={size !== 'small' ? 1 : 2} />
                 )}
               </div>
 
+              {size === 'small' && this.renderFileProperties()}
+
               {size !== 'small' ? (
                 <div className="media__subtext">
                   {__('Published to')} <UriIndicator uri={uri} link />{' '}
@@ -169,9 +171,9 @@ class FileTile extends React.PureComponent<Props> {
                 <Fragment>
                   <div className="media__subtext">
                     <UriIndicator uri={uri} link />
-                  </div>
-                  <div className="media__subtext">
-                    <DateTime timeAgo block={height} />
+                    <div>
+                      <DateTime timeAgo block={height} />
+                    </div>
                   </div>
                 </Fragment>
               )}
@@ -184,7 +186,7 @@ class FileTile extends React.PureComponent<Props> {
             </div>
           )}
 
-          {this.renderFileProperties()}
+          {size !== 'small' && this.renderFileProperties()}
 
           {!name && (
             <Yrbl
diff --git a/src/renderer/component/searchOptions/index.js b/src/renderer/component/searchOptions/index.js
new file mode 100644
index 000000000..f3050be13
--- /dev/null
+++ b/src/renderer/component/searchOptions/index.js
@@ -0,0 +1,16 @@
+import { connect } from 'react-redux';
+import { selectSearchOptions, doUpdateSearchOptions } from 'lbry-redux';
+import SearchOptions from './view';
+
+const select = state => ({
+  options: selectSearchOptions(state),
+});
+
+const perform = dispatch => ({
+  setSearchOption: (option, value) => dispatch(doUpdateSearchOptions({ [option]: value })),
+});
+
+export default connect(
+  select,
+  perform
+)(SearchOptions);
diff --git a/src/renderer/component/searchOptions/view.jsx b/src/renderer/component/searchOptions/view.jsx
new file mode 100644
index 000000000..39df0f152
--- /dev/null
+++ b/src/renderer/component/searchOptions/view.jsx
@@ -0,0 +1,123 @@
+// @flow
+import * as ICONS from 'constants/icons';
+import React, { useState } from 'react';
+import { SEARCH_OPTIONS } from 'lbry-redux';
+import { Form, FormField } from 'component/common/form';
+import posed from 'react-pose';
+import Button from 'component/button';
+
+const ExpandableOptions = posed.div({
+  hide: { height: 0, opacity: 0 },
+  show: { height: 280, opacity: 1 },
+});
+
+type Props = {
+  setSearchOption: (string, boolean | string | number) => void,
+  options: {},
+};
+
+const SearchOptions = (props: Props) => {
+  const { options, setSearchOption } = props;
+  const [expanded, setExpanded] = useState(false);
+  const resultCount = options[SEARCH_OPTIONS.RESULT_COUNT];
+
+  return (
+    <div className="card card--section search__options-wrapper">
+      <div className="card--space-between">
+        <Button
+          label={__('SEARCH OPTIONS')}
+          icon={ICONS.OPTIONS}
+          onClick={() => setExpanded(!expanded)}
+        />
+        {/* 
+          Will be added back when api is ready
+          <div className="media__action-group">
+          <span>{__('Find what you were looking for?')}</span>
+          <Button description={__('Yes')} icon={ICONS.YES} />
+          <Button description={__('No')} icon={ICONS.NO} />
+        </div> */}
+      </div>
+      <ExpandableOptions pose={expanded ? 'show' : 'hide'}>
+        {expanded && (
+          <Form className="card__content search__options">
+            <fieldset>
+              <legend className="search__legend--1">{__('Search For')}</legend>
+              {[
+                {
+                  option: SEARCH_OPTIONS.INCLUDE_FILES,
+                  label: __('Files'),
+                },
+                {
+                  option: SEARCH_OPTIONS.INCLUDE_CHANNELS,
+                  label: __('Channels'),
+                },
+                {
+                  option: SEARCH_OPTIONS.INCLUDE_FILES_AND_CHANNELS,
+                  label: __('Everything'),
+                },
+              ].map(({ option, label }) => (
+                <FormField
+                  key={option}
+                  type="radio"
+                  blockWrap={false}
+                  label={label}
+                  checked={options[SEARCH_OPTIONS.CLAIM_TYPE] === option}
+                  onChange={() => setSearchOption(SEARCH_OPTIONS.CLAIM_TYPE, option)}
+                />
+              ))}
+            </fieldset>
+
+            <fieldset>
+              <legend className="search__legend--2">{__('File Types')}</legend>
+              {[
+                {
+                  option: SEARCH_OPTIONS.MEDIA_VIDEO,
+                  label: __('Videos'),
+                },
+                {
+                  option: SEARCH_OPTIONS.MEDIA_AUDIO,
+                  label: __('Sounds'),
+                },
+                {
+                  option: SEARCH_OPTIONS.MEDIA_IMAGE,
+                  label: __('Images'),
+                },
+                {
+                  option: SEARCH_OPTIONS.MEDIA_TEXT,
+                  label: __('Text'),
+                },
+                {
+                  option: SEARCH_OPTIONS.MEDIA_APPLICATION,
+                  label: __('Other Files'),
+                },
+              ].map(({ option, label }) => (
+                <FormField
+                  key={option}
+                  type="checkbox"
+                  blockWrap={false}
+                  disabled={options[SEARCH_OPTIONS.CLAIM_TYPE] === SEARCH_OPTIONS.INCLUDE_CHANNELS}
+                  label={label}
+                  checked={options[option]}
+                  onChange={() => setSearchOption(option, !options[option])}
+                />
+              ))}
+            </fieldset>
+
+            <fieldset>
+              <legend className="search__legend--3">{__('Other Options')}</legend>
+              <FormField
+                type="number"
+                value={resultCount}
+                onChange={e => setSearchOption(SEARCH_OPTIONS.RESULT_COUNT, e.target.value)}
+                blockWrap={false}
+                label={__('Returned Results')}
+              />
+            </fieldset>
+          </Form>
+        )}
+      </ExpandableOptions>
+    </div>
+  );
+};
+
+export default SearchOptions;
diff --git a/src/renderer/component/wunderbar/index.js b/src/renderer/component/wunderbar/index.js
index 8c8107940..beb65642d 100644
--- a/src/renderer/component/wunderbar/index.js
+++ b/src/renderer/component/wunderbar/index.js
@@ -10,8 +10,6 @@ import {
   doToast,
 } from 'lbry-redux';
 import analytics from 'analytics';
-import { makeSelectClientSetting } from 'redux/selectors/settings';
-import * as settings from 'constants/settings';
 import { doNavigate } from 'redux/actions/navigation';
 import Wunderbar from './view';
 
@@ -27,13 +25,12 @@ const select = state => {
     ...searchState,
     wunderbarValue,
     suggestions: selectSearchSuggestions(state),
-    resultCount: makeSelectClientSetting(settings.RESULT_COUNT)(state),
   };
 };
 
 const perform = dispatch => ({
-  onSearch: (query, size) => {
-    dispatch(doSearch(query, size));
+  onSearch: query => {
+    dispatch(doSearch(query));
     dispatch(doNavigate(`/search`, { query }));
     analytics.apiLogSearch();
   },
diff --git a/src/renderer/component/wunderbar/view.jsx b/src/renderer/component/wunderbar/view.jsx
index 4b546094d..85e38c243 100644
--- a/src/renderer/component/wunderbar/view.jsx
+++ b/src/renderer/component/wunderbar/view.jsx
@@ -12,13 +12,12 @@ const ESC_KEY_CODE = 27;
 
 type Props = {
   updateSearchQuery: string => void,
-  onSearch: (string, ?number) => void,
+  onSearch: string => void,
   onSubmit: (string, {}) => void,
   wunderbarValue: ?string,
   suggestions: Array<string>,
   doFocus: () => void,
   doBlur: () => void,
-  resultCount: number,
   focused: boolean,
   doShowSnackBar: ({}) => void,
 };
@@ -82,7 +81,7 @@ class WunderBar extends React.PureComponent<Props> {
   }
 
   handleSubmit(value: string, suggestion?: { value: string, type: string }) {
-    const { onSubmit, onSearch, resultCount } = this.props;
+    const { onSubmit, onSearch } = this.props;
     const query = value.trim();
     const getParams = () => {
       const parts = query.split('?');
@@ -98,7 +97,7 @@ class WunderBar extends React.PureComponent<Props> {
     // User selected a suggestion
     if (suggestion) {
       if (suggestion.type === 'search') {
-        onSearch(query, resultCount);
+        onSearch(query);
       } else if (isURIValid(query)) {
         const params = getParams();
         const uri = normalizeURI(query);
@@ -125,7 +124,7 @@ class WunderBar extends React.PureComponent<Props> {
         });
       }
     } catch (e) {
-      onSearch(query, resultCount);
+      onSearch(query);
     }
   }
 
diff --git a/src/renderer/constants/icons.js b/src/renderer/constants/icons.js
index 52754f7d3..945023cee 100644
--- a/src/renderer/constants/icons.js
+++ b/src/renderer/constants/icons.js
@@ -41,3 +41,6 @@ export const WALLET = 'CreditCard';
 export const SETTINGS = 'Settings';
 export const INVITE = 'Users';
 export const FILE = 'File';
+export const OPTIONS = 'Sliders';
+export const YES = 'ThumbsUp';
+export const NO = 'ThumbsDown';
diff --git a/src/renderer/page/search/index.js b/src/renderer/page/search/index.js
index d47469617..5952146a3 100644
--- a/src/renderer/page/search/index.js
+++ b/src/renderer/page/search/index.js
@@ -1,22 +1,16 @@
 import { connect } from 'react-redux';
-import * as settings from 'constants/settings';
 import { selectIsSearching, makeSelectCurrentParam, doUpdateSearchQuery } from 'lbry-redux';
-import { doSetClientSetting } from 'redux/actions/settings';
 import { doNavigate } from 'redux/actions/navigation';
-import { makeSelectClientSetting } from 'redux/selectors/settings';
 import SearchPage from './view';
 
 const select = state => ({
   isSearching: selectIsSearching(state),
   query: makeSelectCurrentParam('query')(state),
-  showUnavailable: makeSelectClientSetting(settings.SHOW_UNAVAILABLE)(state),
-  resultCount: makeSelectClientSetting(settings.RESULT_COUNT)(state),
 });
 
 const perform = dispatch => ({
   navigate: path => dispatch(doNavigate(path)),
   updateSearchQuery: query => dispatch(doUpdateSearchQuery(query)),
-  setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
 });
 
 export default connect(
diff --git a/src/renderer/page/search/view.jsx b/src/renderer/page/search/view.jsx
index fd947d965..ffc38346b 100644
--- a/src/renderer/page/search/view.jsx
+++ b/src/renderer/page/search/view.jsx
@@ -1,5 +1,4 @@
 // @flow
-import * as SETTINGS from 'constants/settings';
 import * as ICONS from 'constants/icons';
 import * as React from 'react';
 import { isURIValid, normalizeURI, parseURI } from 'lbry-redux';
@@ -9,32 +8,15 @@ import FileListSearch from 'component/fileListSearch';
 import Page from 'component/page';
 import ToolTip from 'component/common/tooltip';
 import Icon from 'component/common/icon';
+import SearchOptions from 'component/searchOptions';
 
 type Props = {
   query: ?string,
-  resultCount: number,
-  setClientSetting: (string, number | boolean) => void,
 };
 
 class SearchPage extends React.PureComponent<Props> {
-  constructor() {
-    super();
-    (this: any).onShowUnavailableChange = this.onShowUnavailableChange.bind(this);
-    (this: any).onSearchResultCountChange = this.onSearchResultCountChange.bind(this);
-  }
-
-  onSearchResultCountChange(event: SyntheticInputEvent<*>) {
-    const count = Number(event.target.value);
-    this.props.setClientSetting(SETTINGS.RESULT_COUNT, count);
-  }
-
-  onShowUnavailableChange(event: SyntheticInputEvent<*>) {
-    this.props.setClientSetting(SETTINGS.SHOW_UNAVAILABLE, event.target.checked);
-  }
-
   render() {
-    const { query, resultCount } = this.props;
-
+    const { query } = this.props;
     const isValid = isURIValid(query);
 
     let uri;
@@ -69,14 +51,9 @@ class SearchPage extends React.PureComponent<Props> {
               </header>
             )}
 
-          {/*
-            Commented out until I figure out what to do with it in my next PR 
-            <div>
-              <FormField type="text" value={resultCount} label={__("Returned results")} / 
-            </div>
-          */}
-
           <div className="search__results-wrapper">
+            <SearchOptions />
+
             <FileListSearch query={query} />
             <div className="help">{__('These search results are provided by LBRY, Inc.')}</div>
           </div>
diff --git a/src/renderer/scss/component/_button.scss b/src/renderer/scss/component/_button.scss
index 35e827908..24b9d49b5 100644
--- a/src/renderer/scss/component/_button.scss
+++ b/src/renderer/scss/component/_button.scss
@@ -97,7 +97,11 @@
 }
 
 .button--uri-indicator {
-  color: rgba($lbry-white, 0.9);
+  width: 100%;
+  overflow: hidden;
+  text-align: left;
+  text-overflow: ellipsis;
+  white-space: nowrap;
   transition: color 0.2s;
 
   &:hover {
diff --git a/src/renderer/scss/component/_form-field.scss b/src/renderer/scss/component/_form-field.scss
index 9b41662af..002c9f974 100644
--- a/src/renderer/scss/component/_form-field.scss
+++ b/src/renderer/scss/component/_form-field.scss
@@ -2,6 +2,17 @@
 
 // lbry/components overrides and minor styles
 
+input[type='number'] {
+  padding: var(--spacing-s);
+  width: 8em;
+}
+
+checkbox-element {
+  &[disabled='true'] {
+    opacity: 0.3;
+  }
+}
+
 checkbox-element,
 radio-element,
 fieldset:last-child,
@@ -9,6 +20,21 @@ fieldset-section:last-child {
   margin-bottom: 0;
 }
 
+checkbox-element,
+radio-element {
+  input[type='checkbox']:checked + label {
+    color: $lbry-black;
+
+    [data-mode='dark'] & {
+      color: $lbry-white;
+
+      &:hover {
+        color: $lbry-teal-4;
+      }
+    }
+  }
+}
+
 fieldset-group.fieldset-group--smushed {
   justify-content: flex-start;
 
@@ -41,6 +67,10 @@ form {
       background-color: $lbry-teal-5;
       border-color: $lbry-teal-5;
     }
+
+    legend {
+      background-color: $lbry-cyan-5;
+    }
   }
 }
 
diff --git a/src/renderer/scss/component/_media.scss b/src/renderer/scss/component/_media.scss
index b29641c73..74e90998d 100644
--- a/src/renderer/scss/component/_media.scss
+++ b/src/renderer/scss/component/_media.scss
@@ -14,6 +14,7 @@
 .media-tile {
   display: flex;
   font-size: 1.5rem;
+  position: relative;
 
   &:not(:last-of-type) {
     margin-bottom: var(--spacing-vertical-large);
@@ -26,6 +27,7 @@
   .media__info {
     margin-left: var(--spacing-vertical-medium);
     width: calc(80% - 20rem);
+    min-width: 40rem;
   }
 }
 
@@ -38,7 +40,7 @@
 
   .media__info {
     margin-left: var(--spacing-vertical-large);
-    width: calc(80% - 30rem);
+    flex: 1;
   }
 
   .media__subtext {
@@ -54,21 +56,26 @@
   }
 
   .media__thumb {
-    width: 10em;
+    width: 11em;
   }
 
   .media__info {
-    // padding-left: var(--spacing-vertical-medium);
     width: calc(100% - 10em);
+    min-width: auto;
+    position: relative;
   }
 
   .media__title {
     margin-bottom: var(--spacing-vertical-small);
   }
 
+  .media__subtext:last-child {
+    margin-bottom: 0;
+  }
+
   .media__properties {
-    bottom: -1.5rem;
-    left: calc(-100% - 1.5rem);
+    bottom: 0.5rem;
+    left: calc(-100% - -2rem);
     position: absolute;
     padding: 0 var(--spacing-vertical-small);
     border-radius: 5px;
@@ -143,7 +150,7 @@
 
 .media__action-group {
   > *:not(:last-child) {
-    margin-right: var(--spacing-vertical-large);
+    margin-right: var(--spacing-vertical-medium);
   }
 }
 
@@ -193,6 +200,11 @@
 
 .media__subtitle {
   font-size: 0.8em;
+  color: rgba($lbry-black, 0.8);
+
+  [data-mode='dark'] & {
+    color: rgba($lbry-white, 0.8);
+  }
 }
 
 .media__subtitle--large {
@@ -412,7 +424,7 @@
       color: $lbry-white;
     }
 
-    .media__subtext {
+    .media__subtitle {
       color: mix($lbry-cyan-5, $lbry-white, 20%);
     }
   }
diff --git a/src/renderer/scss/component/_search.scss b/src/renderer/scss/component/_search.scss
index fbcdfc78a..5155333c1 100644
--- a/src/renderer/scss/component/_search.scss
+++ b/src/renderer/scss/component/_search.scss
@@ -12,6 +12,10 @@
     color: rgba($lbry-white, 0.6);
   }
 
+  .media__subtitle {
+    color: rgba($lbry-white, 0.9);
+  }
+
   html[data-mode='dark'] & {
     background-color: transparent;
     border-bottom: 1px solid rgba($lbry-white, 0.1);
@@ -46,3 +50,43 @@
   @extend .media-group__header-title;
   margin-bottom: var(--spacing-vertical-large);
 }
+
+.search__options-wrapper {
+  font-size: 1.25em;
+}
+
+.search__options {
+  margin-top: var(--spacing-vertical-large);
+
+  legend {
+    &.search__legend--1 {
+      background-color: $lbry-teal-1;
+    }
+
+    &.search__legend--2 {
+      background-color: $lbry-cyan-1;
+    }
+
+    &.search__legend--3 {
+      background-color: $lbry-pink-1;
+    }
+
+    [data-mode='dark'] & {
+      &.search__legend--1 {
+        background-color: $lbry-teal-5;
+      }
+
+      &.search__legend--2 {
+        background-color: $lbry-cyan-5;
+      }
+
+      &.search__legend--3 {
+        background-color: $lbry-pink-5;
+      }
+    }
+  }
+
+  fieldset:not(:first-child) {
+    margin-top: var(--spacing-vertical-large);
+  }
+}
diff --git a/src/renderer/scss/init/_gui.scss b/src/renderer/scss/init/_gui.scss
index 2b3be69d0..f3de30d6b 100644
--- a/src/renderer/scss/init/_gui.scss
+++ b/src/renderer/scss/init/_gui.scss
@@ -86,7 +86,9 @@ code {
 }
 
 .truncated-text {
-  @include truncate;
+  display: -webkit-box;
+  overflow: hidden;
+  -webkit-box-orient: vertical;
 }
 
 .busy-indicator__loader {
diff --git a/src/renderer/scss/init/_mixins.scss b/src/renderer/scss/init/_mixins.scss
index 9ee779c07..471d84666 100644
--- a/src/renderer/scss/init/_mixins.scss
+++ b/src/renderer/scss/init/_mixins.scss
@@ -1,30 +1,3 @@
-@mixin between {
-  display: flex;
-  justify-content: space-between;
-}
-
-@mixin ellipsis {
-  // to take over for truncate on LBRY Web
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-@mixin font-mono {
-  font-family: Inconsolata, 'Fira Mono', 'Droid Sans Mono', 'Source Code Pro', Consolas,
-    'Lucida Console', 'Courier New', Courier, monospace;
-}
-
-@mixin font-sans {
-  font-family: 'Inter UI', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,
-    sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
-}
-
-@mixin font-serif {
-  font-family: 'Apple Garamond', Baskerville, Georgia, 'Times New Roman', 'Droid Serif', Times,
-    'Source Serif Pro', serif;
-}
-
 @mixin placeholder {
   animation: pulse 2s infinite ease-in-out;
   background-color: $lbry-gray-2;
@@ -33,26 +6,3 @@
     background-color: rgba($lbry-white, 0.1);
   }
 }
-
-@mixin thumbnail {
-  &::before,
-  &::after {
-    content: '';
-  }
-
-  &::before {
-    float: left;
-    padding-top: var(--video-aspect-ratio);
-  }
-
-  &::after {
-    clear: both;
-    display: block;
-  }
-}
-
-@mixin truncate {
-  display: -webkit-box;
-  overflow: hidden;
-  -webkit-box-orient: vertical;
-}
diff --git a/src/renderer/store.js b/src/renderer/store.js
index 3b1da6665..3ac37293a 100644
--- a/src/renderer/store.js
+++ b/src/renderer/store.js
@@ -102,12 +102,13 @@ const fileInfoFilter = createFilter('fileInfo', [
 const appFilter = createFilter('app', ['hasClickedComment']);
 // We only need to persist the receiveAddress for the wallet
 const walletFilter = createFilter('wallet', ['receiveAddress']);
+const searchFilter = createFilter('search', ['options']);
 
 const persistOptions = {
-  whitelist: ['subscriptions', 'publish', 'wallet', 'content', 'fileInfo', 'app'],
+  whitelist: ['subscriptions', 'publish', 'wallet', 'content', 'fileInfo', 'app', 'search'],
   // Order is important. Needs to be compressed last or other transforms can't
   // read the data
-  transforms: [walletFilter, contentFilter, fileInfoFilter, appFilter, compressor],
+  transforms: [walletFilter, contentFilter, fileInfoFilter, appFilter, searchFilter, compressor],
   debounce: 10000,
   storage: localForage,
 };
diff --git a/yarn.lock b/yarn.lock
index c8caad665..4fd479e9e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5660,9 +5660,9 @@ lazy-val@^1.0.3:
     tar-stream "^1.6.2"
     zstd-codec "^0.1.1"
 
-lbry-redux@lbryio/lbry-redux#42c185e922a7c6091b0e1580bacbfd8e02f45a91:
+lbry-redux@lbryio/lbry-redux#2b725cb31729234ba73117e2a74688b8bba26e7c:
   version "0.0.1"
-  resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/42c185e922a7c6091b0e1580bacbfd8e02f45a91"
+  resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/2b725cb31729234ba73117e2a74688b8bba26e7c"
   dependencies:
     proxy-polyfill "0.1.6"
     reselect "^3.0.0"
@@ -7868,14 +7868,15 @@ rc@^1.0.1, rc@^1.1.2, rc@^1.1.6, rc@^1.2.7:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
-react-dom@^16.6.0:
-  version "16.7.0"
-  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.7.0.tgz#a17b2a7ca89ee7390bc1ed5eb81783c7461748b8"
+react-dom@^16.8.2:
+  version "16.8.2"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.2.tgz#7c8a69545dd554d45d66442230ba04a6a0a3c3d3"
+  integrity sha512-cPGfgFfwi+VCZjk73buu14pYkYBR1b/SRMSYqkLDdhSEHnSwcuYTPu6/Bh6ZphJFIk80XLvbSe2azfcRzNF+Xg==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
     prop-types "^15.6.2"
-    scheduler "^0.12.0"
+    scheduler "^0.13.2"
 
 react-feather@^1.0.8:
   version "1.1.1"
@@ -7952,14 +7953,15 @@ react@^0.14.5:
     envify "^3.0.0"
     fbjs "^0.6.1"
 
-react@^16.6.0:
-  version "16.7.0"
-  resolved "https://registry.yarnpkg.com/react/-/react-16.7.0.tgz#b674ec396b0a5715873b350446f7ea0802ab6381"
+react@^16.8.2:
+  version "16.8.2"
+  resolved "https://registry.yarnpkg.com/react/-/react-16.8.2.tgz#83064596feaa98d9c2857c4deae1848b542c9c0c"
+  integrity sha512-aB2ctx9uQ9vo09HVknqv3DGRpI7OIGJhCx3Bt0QqoRluEjHSaObJl+nG12GDdYH6sTgE7YiPJ6ZUyMx9kICdXw==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
     prop-types "^15.6.2"
-    scheduler "^0.12.0"
+    scheduler "^0.13.2"
 
 read-config-file@3.1.0, read-config-file@^3.0.0:
   version "3.1.0"
@@ -8532,13 +8534,6 @@ sass-loader@^6.0.7:
     neo-async "^2.5.0"
     pify "^3.0.0"
 
-sass@^1.17.0:
-  version "1.17.0"
-  resolved "https://registry.yarnpkg.com/sass/-/sass-1.17.0.tgz#e370b9302af121c9eadad5639619127772094ae6"
-  integrity sha512-aFi9RQqrCYkHB2DaLKBBbdUhos1N5o3l1ke9N5JqWzgSPmYwZsdmA+ViPVatUy/RPA21uejgYVUXM7GCh8lcdw==
-  dependencies:
-    chokidar "^2.0.0"
-
 sax@1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
@@ -8551,9 +8546,10 @@ sax@~1.1.1:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.1.6.tgz#5d616be8a5e607d54e114afae55b7eaf2fcc3240"
 
-scheduler@^0.12.0:
-  version "0.12.0"
-  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.12.0.tgz#8ab17699939c0aedc5a196a657743c496538647b"
+scheduler@^0.13.2:
+  version "0.13.2"
+  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.2.tgz#969eaee2764a51d2e97b20a60963b2546beff8fa"
+  integrity sha512-qK5P8tHS7vdEMCW5IPyt8v9MJOHqTrOUgPXib7tqm9vh834ibBX5BNhwkplX/0iOzHW5sXyluehYfS9yrkz9+w==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
-- 
2.45.3