Bug 942295 - Add basic filter support to Hub panels. r=lucasr
authorJosh Dover <gerfuls@gmail.com>
Wed, 05 Feb 2014 11:00:06 -0800
changeset 170199 0106724dcbfd0f75610dc6d56ac3da56c80ba49f
parent 170198 29273bf136ee745edb00eff3fb3737f1afbbdb42
child 170200 e7c91056d5d5c8b077364be9380c472f92e20dcf
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
reviewerslucasr
bugs942295
milestone30.0a1
Bug 942295 - Add basic filter support to Hub panels. r=lucasr
mobile/android/base/db/BrowserContract.java
mobile/android/base/db/HomeProvider.java
mobile/android/base/home/DynamicPanel.java
mobile/android/base/home/FramePanelLayout.java
mobile/android/base/home/PanelLayout.java
mobile/android/base/util/StringUtils.java
mobile/android/modules/HomeProvider.jsm
--- a/mobile/android/base/db/BrowserContract.java
+++ b/mobile/android/base/db/BrowserContract.java
@@ -328,16 +328,17 @@ public class BrowserContract {
         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/homeitem";
 
         public static final String DATASET_ID = "dataset_id";
         public static final String URL = "url";
         public static final String TITLE = "title";
         public static final String DESCRIPTION = "description";
         public static final String IMAGE_URL = "image_url";
         public static final String CREATED = "created";
+        public static final String FILTER = "filter";
     }
 
     /*
      * Contains names and schema definitions for tables and views
      * no longer being used by current ContentProviders. These values are used
      * to make incremental updates to the schema during a database upgrade. Will be
      * removed with bug 947018.
      */
--- a/mobile/android/base/db/HomeProvider.java
+++ b/mobile/android/base/db/HomeProvider.java
@@ -99,30 +99,32 @@ public class HomeProvider extends SQLite
         }
 
         final String[] itemsColumns = new String[] {
             HomeItems._ID,
             HomeItems.DATASET_ID,
             HomeItems.URL,
             HomeItems.TITLE,
             HomeItems.DESCRIPTION,
-            HomeItems.IMAGE_URL
+            HomeItems.IMAGE_URL,
+            HomeItems.FILTER
         };
 
         final MatrixCursor c = new MatrixCursor(itemsColumns);
         for (int i = 0; i < items.length(); i++) {
             try {
                 final JSONObject item = items.getJSONObject(i);
                 c.addRow(new Object[] {
                     item.getInt("id"),
                     item.getString("dataset_id"),
                     item.getString("url"),
                     item.getString("title"),
                     item.getString("description"),
-                    item.getString("image_url")
+                    item.getString("image_url"),
+                    item.getString("filter")
                 });
             } catch (JSONException e) {
                 Log.e(LOGTAG, "Error creating cursor row for fake home item", e);
             }
         }
         return c;
     }
 
--- a/mobile/android/base/home/DynamicPanel.java
+++ b/mobile/android/base/home/DynamicPanel.java
@@ -6,16 +6,18 @@
 package org.mozilla.gecko.home;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserContract.HomeItems;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.HomeConfig.PanelConfig;
 import org.mozilla.gecko.home.PanelLayout.DatasetHandler;
+import org.mozilla.gecko.home.PanelLayout.DatasetRequest;
+import org.mozilla.gecko.db.DBUtils;
 
 import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
@@ -49,17 +51,17 @@ import java.util.EnumSet;
  *
  * See {@code PanelLayout} for more details on how {@code DynamicPanel}
  * receives dataset requests and delivers them back to the {@code PanelLayout}.
  */
 public class DynamicPanel extends HomeFragment {
     private static final String LOGTAG = "GeckoDynamicPanel";
 
     // Dataset ID to be used by the loader
-    private static final String DATASET_ID = "dataset_id";
+    private static final String DATASET_REQUEST = "dataset_request";
 
     // The panel layout associated with this panel
     private PanelLayout mLayout;
 
     // The configuration associated with this panel
     private PanelConfig mPanelConfig;
 
     // Callbacks used for the loader
@@ -162,30 +164,30 @@ public class DynamicPanel extends HomeFr
     }
 
     /**
      * Used by the PanelLayout to make load and reset requests to
      * the holding fragment.
      */
     private class PanelDatasetHandler implements DatasetHandler {
         @Override
-        public void requestDataset(String datasetId) {
-            Log.d(LOGTAG, "Requesting dataset: " + datasetId);
+        public void requestDataset(DatasetRequest request) {
+            Log.d(LOGTAG, "Requesting request: " + request);
 
             // Ignore dataset requests while the fragment is not
             // allowed to load its content.
             if (!getCanLoadHint()) {
                 return;
             }
 
             final Bundle bundle = new Bundle();
-            bundle.putString(DATASET_ID, datasetId);
+            bundle.putParcelable(DATASET_REQUEST, request);
 
             // Ensure one loader per dataset
-            final int loaderId = generateLoaderId(datasetId);
+            final int loaderId = generateLoaderId(request.datasetId);
             getLoaderManager().restartLoader(loaderId, bundle, mLoaderCallbacks);
         }
 
         @Override
         public void resetDataset(String datasetId) {
             Log.d(LOGTAG, "Resetting dataset: " + datasetId);
 
             final LoaderManager lm = getLoaderManager();
@@ -199,61 +201,75 @@ public class DynamicPanel extends HomeFr
             }
         }
     }
 
     /**
      * Cursor loader for the panel datasets.
      */
     private static class PanelDatasetLoader extends SimpleCursorLoader {
-        private final String mDatasetId;
+        private final DatasetRequest mRequest;
 
-        public PanelDatasetLoader(Context context, String datasetId) {
+        public PanelDatasetLoader(Context context, DatasetRequest request) {
             super(context);
-            mDatasetId = datasetId;
+            mRequest = request;
         }
 
-        public String getDatasetId() {
-            return mDatasetId;
+        public DatasetRequest getRequest() {
+            return mRequest;
         }
 
         @Override
         public Cursor loadCursor() {
             final ContentResolver cr = getContext().getContentResolver();
 
-            final String selection = HomeItems.DATASET_ID + " = ?";
-            final String[] selectionArgs = new String[] { mDatasetId };
+            final String selection;
+            final String[] selectionArgs;
+
+            // Null represents the root filter
+            if (mRequest.filter == null) {
+                selection = DBUtils.concatenateWhere(HomeItems.DATASET_ID + " = ?", HomeItems.FILTER + " IS NULL");
+                selectionArgs = new String[] { mRequest.datasetId };
+            } else {
+                selection = DBUtils.concatenateWhere(HomeItems.DATASET_ID + " = ?", HomeItems.FILTER + " = ?");
+                selectionArgs = new String[] { mRequest.datasetId, mRequest.filter };
+            }
 
             // XXX: You can use CONTENT_FAKE_URI for development to pull items from fake_home_items.json.
             return cr.query(HomeItems.CONTENT_URI, null, selection, selectionArgs, null);
         }
     }
 
     /**
      * LoaderCallbacks implementation that interacts with the LoaderManager.
      */
     private class PanelLoaderCallbacks implements LoaderCallbacks<Cursor> {
         @Override
         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
-            final String datasetId = args.getString(DATASET_ID);
+            final DatasetRequest request = (DatasetRequest) args.getParcelable(DATASET_REQUEST);
 
-            Log.d(LOGTAG, "Creating loader for dataset: " + datasetId);
-            return new PanelDatasetLoader(getActivity(), datasetId);
+            Log.d(LOGTAG, "Creating loader for request: " + request);
+            return new PanelDatasetLoader(getActivity(), request);
         }
 
         @Override
         public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
-            final PanelDatasetLoader datasetLoader = (PanelDatasetLoader) loader;
+            final DatasetRequest request = getRequestFromLoader(loader);
 
-            Log.d(LOGTAG, "Finished loader for dataset: " + datasetLoader.getDatasetId());
-            mLayout.deliverDataset(datasetLoader.getDatasetId(), cursor);
+            Log.d(LOGTAG, "Finished loader for request: " + request);
+            mLayout.deliverDataset(request, cursor);
         }
 
         @Override
         public void onLoaderReset(Loader<Cursor> loader) {
-            final PanelDatasetLoader datasetLoader = (PanelDatasetLoader) loader;
-            Log.d(LOGTAG, "Resetting loader for dataset: " + datasetLoader.getDatasetId());
+            final DatasetRequest request = getRequestFromLoader(loader);
+            Log.d(LOGTAG, "Resetting loader for request: " + request);
             if (mLayout != null) {
-                mLayout.releaseDataset(datasetLoader.getDatasetId());
+                mLayout.releaseDataset(request.datasetId);
             }
         }
+
+        private DatasetRequest getRequestFromLoader(Loader<Cursor> loader) {
+            final PanelDatasetLoader datasetLoader = (PanelDatasetLoader) loader;
+            return datasetLoader.getRequest();
+        }
     }
 }
--- a/mobile/android/base/home/FramePanelLayout.java
+++ b/mobile/android/base/home/FramePanelLayout.java
@@ -33,13 +33,15 @@ class FramePanelLayout extends PanelLayo
         addView(mChildView);
     }
 
     @Override
     public void load() {
         Log.d(LOGTAG, "Loading");
 
         if (mChildView instanceof DatasetBacked) {
-            Log.d(LOGTAG, "Requesting child dataset: " + mChildConfig.getDatasetId());
-            requestDataset(mChildConfig.getDatasetId());
+            // TODO: get filter from ViewEntry
+            DatasetRequest request = new DatasetRequest(mChildConfig.getDatasetId(), null);
+            Log.d(LOGTAG, "Requesting child request: " + request);
+            requestDataset(request);
         }
     }
 }
--- a/mobile/android/base/home/PanelLayout.java
+++ b/mobile/android/base/home/PanelLayout.java
@@ -3,26 +3,33 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.HomeConfig.PanelConfig;
 import org.mozilla.gecko.home.HomeConfig.ViewConfig;
+import org.mozilla.gecko.util.StringUtils;
 
 import android.content.Context;
 import android.database.Cursor;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.text.TextUtils;
 import android.util.Log;
+import android.view.KeyEvent;
 import android.view.View;
 import android.widget.FrameLayout;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Deque;
+import java.util.EnumSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.WeakHashMap;
 
 /**
  * {@code PanelLayout} is the base class for custom layouts to be
  * used in {@code DynamicPanel}. It provides the basic framework
  * that enables custom layouts to request and reset datasets and
  * create panel views. Furthermore, it automates most of the process
  * of binding panel views with their respective datasets.
  *
@@ -52,84 +59,128 @@ import java.util.List;
  * to create the views dynamically created based on {@code ViewConfig}. This
  * allows {@code PanelLayout} to auto-bind datasets with panel views.
  * {@code PanelLayout} subclasses are free to have any type of views to arrange
  * the panel views in different ways.
  */
 abstract class PanelLayout extends FrameLayout {
     private static final String LOGTAG = "GeckoPanelLayout";
 
-    private final List<ViewEntry> mViewEntries;
+    protected final Map<View, ViewState> mViewStateMap;
     private final DatasetHandler mDatasetHandler;
     private final OnUrlOpenListener mUrlOpenListener;
 
     /**
      * To be used by panel views to express that they are
      * backed by datasets.
      */
     public interface DatasetBacked {
         public void setDataset(Cursor cursor);
     }
 
     /**
+     * To be used by requests made to {@code DatasetHandler}s to couple dataset ID with current
+     * filter for queries on the database.
+     */
+    public static class DatasetRequest implements Parcelable {
+        public final String datasetId;
+        public final String filter;
+
+        private DatasetRequest(Parcel in) {
+            this.datasetId = in.readString();
+            this.filter = in.readString();
+        }
+
+        public DatasetRequest(String datasetId, String filter) {
+            this.datasetId = datasetId;
+            this.filter = filter;
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeString(datasetId);
+            dest.writeString(filter);
+        }
+
+        public String toString() {
+            return "{dataset: " + datasetId + ", filter: " + filter + "}";
+        }
+
+        public static final Creator<DatasetRequest> CREATOR = new Creator<DatasetRequest>() {
+            public DatasetRequest createFromParcel(Parcel in) {
+                return new DatasetRequest(in);
+            }
+
+            public DatasetRequest[] newArray(int size) {
+                return new DatasetRequest[size];
+            }
+        };
+    }
+
+    /**
      * Defines the contract with the component that is responsible
      * for handling datasets requests.
      */
     public interface DatasetHandler {
         /**
          * Requests a dataset to be fetched and auto-bound to the
          * panel views backed by it.
          */
-        public void requestDataset(String datasetId);
+        public void requestDataset(DatasetRequest request);
 
         /**
          * Releases any resources associated with a previously loaded
          * dataset. It will do nothing if the dataset with the given ID
          * hasn't been loaded before.
          */
         public void resetDataset(String datasetId);
     }
 
     public interface PanelView {
         public void setOnUrlOpenListener(OnUrlOpenListener listener);
     }
 
     public PanelLayout(Context context, PanelConfig panelConfig, DatasetHandler datasetHandler, OnUrlOpenListener urlOpenListener) {
         super(context);
-        mViewEntries = new ArrayList<ViewEntry>();
+        mViewStateMap = new WeakHashMap<View, ViewState>();
         mDatasetHandler = datasetHandler;
         mUrlOpenListener = urlOpenListener;
     }
 
     /**
      * Delivers the dataset as a {@code Cursor} to be bound to the
      * panel views backed by it. This is used by the {@code DatasetHandler}
      * in response to a dataset request.
      */
-    public final void deliverDataset(String datasetId, Cursor cursor) {
-        Log.d(LOGTAG, "Delivering dataset: " + datasetId);
-        updateViewsWithDataset(datasetId, cursor);
+    public final void deliverDataset(DatasetRequest request, Cursor cursor) {
+        Log.d(LOGTAG, "Delivering request: " + request);
+        updateViewsWithDataset(request.datasetId, cursor);
     }
 
     /**
      * Releases any references to the given dataset from all
      * existing panel views.
      */
     public final void releaseDataset(String datasetId) {
         Log.d(LOGTAG, "Resetting dataset: " + datasetId);
         updateViewsWithDataset(datasetId, null);
     }
 
     /**
      * Requests a dataset to be loaded and bound to any existing
      * panel view backed by it.
      */
-    protected final void requestDataset(String datasetId) {
-        Log.d(LOGTAG, "Requesting dataset: " + datasetId);
-        mDatasetHandler.requestDataset(datasetId);
+    protected final void requestDataset(DatasetRequest request) {
+        Log.d(LOGTAG, "Requesting request: " + request);
+        mDatasetHandler.requestDataset(request);
     }
 
     /**
      * Releases any resources associated with a previously
      * loaded dataset e.g. close any associated {@code Cursor}.
      */
     protected final void resetDataset(String datasetId) {
         mDatasetHandler.resetDataset(datasetId);
@@ -154,54 +205,49 @@ abstract class PanelLayout extends Frame
             case GRID:
                 view = new PanelGridView(getContext(), viewConfig);
                 break;
 
             default:
                 throw new IllegalStateException("Unrecognized view type in " + getClass().getSimpleName());
         }
 
-        final ViewEntry entry = new ViewEntry(view, viewConfig);
-        mViewEntries.add(entry);
+        final ViewState state = new ViewState(viewConfig);
+        // TODO: Push initial filter here onto ViewState
+        mViewStateMap.put(view, state);
 
-        ((PanelView) view).setOnUrlOpenListener(mUrlOpenListener);
+        ((PanelView) view).setOnUrlOpenListener(new PanelUrlOpenListener(state));
+        view.setOnKeyListener(new PanelKeyListener(state));
 
         return view;
     }
 
     /**
      * Dispose any dataset references associated with the
      * given view.
      */
     protected final void disposePanelView(View view) {
         Log.d(LOGTAG, "Disposing panel view");
+        if (mViewStateMap.containsKey(view)) {
+            // Release any Cursor references from the view
+            // if it's backed by a dataset.
+            maybeSetDataset(view, null);
 
-        final int count = mViewEntries.size();
-        for (int i = 0; i < count; i++) {
-            final View entryView = mViewEntries.get(i).getView();
-            if (view == entryView) {
-                // Release any Cursor references from the view
-                // if it's backed by a dataset.
-                maybeSetDataset(entryView, null);
-
-                // Remove the view entry from the list
-                mViewEntries.remove(i);
-                break;
-            }
+            // Remove the view entry from the map
+            mViewStateMap.remove(view);
         }
     }
 
     private void updateViewsWithDataset(String datasetId, Cursor cursor) {
-        final int count = mViewEntries.size();
-        for (int i = 0; i < count; i++) {
-            final ViewEntry entry = mViewEntries.get(i);
+        for (Map.Entry<View, ViewState> entry : mViewStateMap.entrySet()) {
+            final ViewState detail = entry.getValue();
 
             // Update any views associated with the given dataset ID
-            if (TextUtils.equals(entry.getDatasetId(), datasetId)) {
-                final View view = entry.getView();
+            if (TextUtils.equals(detail.getDatasetId(), datasetId)) {
+                final View view = entry.getKey();
                 maybeSetDataset(view, cursor);
             }
         }
     }
 
     private void maybeSetDataset(View view, Cursor cursor) {
         if (view instanceof DatasetBacked) {
             final DatasetBacked dsb = (DatasetBacked) view;
@@ -213,28 +259,116 @@ abstract class PanelLayout extends Frame
      * Must be implemented by {@code PanelLayout} subclasses to define
      * what happens then the layout is first loaded. Should set initial
      * UI state and request any necessary datasets.
      */
     public abstract void load();
 
     /**
      * Represents a 'live' instance of a panel view associated with
-     * the {@code PanelLayout}.
+     * the {@code PanelLayout}. Is responsible for tracking the history stack of filters.
      */
-    private static class ViewEntry {
-        private final View mView;
+    protected static class ViewState {
         private final ViewConfig mViewConfig;
+        private Deque<String> mFilterStack;
 
-        public ViewEntry(View view, ViewConfig viewConfig) {
-            mView = view;
+        public ViewState(ViewConfig viewConfig) {
             mViewConfig = viewConfig;
         }
 
-        public View getView() {
-            return mView;
-        }
-
         public String getDatasetId() {
             return mViewConfig.getDatasetId();
         }
+
+        /**
+         * Used to find the current filter that this view is displaying, or null if none.
+         */
+        public String getCurrentFilter() {
+            if (mFilterStack == null) {
+                return null;
+            } else {
+                return mFilterStack.peek();
+            }
+        }
+
+        /**
+         * Adds a filter to the history stack for this view.
+         */
+        public void pushFilter(String filter) {
+            if (mFilterStack == null) {
+                mFilterStack = new LinkedList<String>();
+            }
+
+            mFilterStack.push(filter);
+        }
+
+        public String popFilter() {
+            if (getCurrentFilter() != null) {
+                mFilterStack.pop();
+            }
+
+            return getCurrentFilter();
+        }
+    }
+
+    /**
+     * Pushes filter to {@code ViewState}'s stack and makes request for new filter value.
+     */
+    private void pushFilterOnView(ViewState viewState, String filter) {
+        viewState.pushFilter(filter);
+        mDatasetHandler.requestDataset(new DatasetRequest(viewState.getDatasetId(), filter));
+    }
+
+    /**
+     * Pops filter from {@code ViewState}'s stack and makes request for previous filter value.
+     *
+     * @return whether the filter has changed
+     */
+    private boolean popFilterOnView(ViewState viewState) {
+        String currentFilter = viewState.getCurrentFilter();
+        String filter = viewState.popFilter();
+
+        if (!TextUtils.equals(currentFilter, filter)) {
+            mDatasetHandler.requestDataset(new DatasetRequest(viewState.getDatasetId(), filter));
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Custom listener so that we can intercept any filter URLs and make a new dataset request
+     * rather than forwarding them to the default listener.
+     */
+    private class PanelUrlOpenListener implements OnUrlOpenListener {
+        private ViewState mViewState;
+
+        public PanelUrlOpenListener(ViewState viewState) {
+            mViewState = viewState;
+        }
+
+        @Override
+        public void onUrlOpen(String url, EnumSet<Flags> flags) {
+            if (StringUtils.isFilterUrl(url)) {
+                pushFilterOnView(mViewState, StringUtils.getFilterFromUrl(url));
+            } else {
+                mUrlOpenListener.onUrlOpen(url, flags);
+            }
+        }
+    }
+
+    private class PanelKeyListener implements View.OnKeyListener {
+        private ViewState mViewState;
+
+        public PanelKeyListener(ViewState viewState) {
+            mViewState = viewState;
+        }
+
+        @Override
+        public boolean onKey(View v, int keyCode, KeyEvent event) {
+            if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
+                return popFilterOnView(mViewState);
+            }
+
+            return false;
+        }
     }
 }
--- a/mobile/android/base/util/StringUtils.java
+++ b/mobile/android/base/util/StringUtils.java
@@ -4,16 +4,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.util;
 
 import android.net.Uri;
 import android.text.TextUtils;
 
 public class StringUtils {
+
+    private static final String FILTER_URL_PREFIX = "filter://";
+
     /*
      * This method tries to guess if the given string could be a search query or URL,
      * and returns a previous result if there is ambiguity
      *
      * Search examples:
      *  foo
      *  foo bar.com
      *  foo http://bar.com
@@ -130,9 +133,25 @@ public class StringUtils {
             if (TextUtils.isEmpty(value)) {
                 return null;
             }
             return value;
         }
 
         return null;
     }
+
+    public static boolean isFilterUrl(String url) {
+        if (TextUtils.isEmpty(url)) {
+            return false;
+        }
+
+        return url.startsWith(FILTER_URL_PREFIX);
+    }
+
+    public static String getFilterFromUrl(String url) {
+        if (TextUtils.isEmpty(url)) {
+            return null;
+        }
+
+        return url.substring(FILTER_URL_PREFIX.length());
+    }
 }
--- a/mobile/android/modules/HomeProvider.jsm
+++ b/mobile/android/modules/HomeProvider.jsm
@@ -46,22 +46,23 @@ const SQL = {
   createItemsTable:
     "CREATE TABLE items (" +
       "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
       "dataset_id TEXT NOT NULL, " +
       "url TEXT," +
       "title TEXT," +
       "description TEXT," +
       "image_url TEXT," +
+      "filter TEXT," +
       "created INTEGER" +
     ")",
 
   insertItem:
-    "INSERT INTO items (dataset_id, url, title, description, image_url, created) " +
-      "VALUES (:dataset_id, :url, :title, :description, :image_url, :created)",
+    "INSERT INTO items (dataset_id, url, title, description, image_url, filter, created) " +
+      "VALUES (:dataset_id, :url, :title, :description, :image_url, :filter, :created)",
 
   deleteFromDataset:
     "DELETE FROM items WHERE dataset_id = :dataset_id"
 }
 
 /**
  * Technically this function checks to see if the user is on a local network,
  * but we express this as "wifi" to the user.
@@ -237,16 +238,17 @@ HomeStorage.prototype = {
         for (let item of data) {
           // XXX: Directly pass item as params? More validation for item? Batch insert?
           let params = {
             dataset_id: this.datasetId,
             url: item.url,
             title: item.title,
             description: item.description,
             image_url: item.image_url,
+            filter: item.filter,
             created: Date.now()
           };
           yield db.executeCached(SQL.insertItem, params);
         }
       } finally {
         yield db.close();
       }
     }.bind(this));