/*
* Copyright (c) 2011 Lockheed Martin Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eurekastreams.web.client.ui.pages.discover;
import java.util.Collection;
import java.util.HashMap;
import org.eurekastreams.server.action.request.profile.SetFollowingStatusRequest;
import org.eurekastreams.server.domain.Page;
import org.eurekastreams.server.domain.dto.StreamDTO;
import org.eurekastreams.server.domain.dto.StreamDiscoverListsDTO;
import org.eurekastreams.web.client.events.EventBus;
import org.eurekastreams.web.client.events.Observer;
import org.eurekastreams.web.client.events.UpdateHistoryEvent;
import org.eurekastreams.web.client.events.data.BaseDataRequestResponseEvent;
import org.eurekastreams.web.client.events.data.DeletedGroupMemberResponseEvent;
import org.eurekastreams.web.client.events.data.DeletedPersonFollowerResponseEvent;
import org.eurekastreams.web.client.events.data.GotFeaturedStreamsPageResponseEvent;
import org.eurekastreams.web.client.events.data.GotStreamDiscoverListsDTOResponseEvent;
import org.eurekastreams.web.client.events.data.InsertedBlockedSuggestionResponseEvent;
import org.eurekastreams.web.client.events.data.InsertedGroupMemberResponseEvent;
import org.eurekastreams.web.client.events.data.InsertedPersonFollowerResponseEvent;
import org.eurekastreams.web.client.history.CreateUrlRequest;
import org.eurekastreams.web.client.jsni.WidgetJSNIFacadeImpl;
import org.eurekastreams.web.client.model.StreamsDiscoveryModel;
import org.eurekastreams.web.client.ui.Session;
import org.eurekastreams.web.client.ui.common.LabeledTextBox;
import org.eurekastreams.web.client.ui.common.pager.PagerComposite;
import org.eurekastreams.web.client.ui.pages.master.CoreCss;
import org.eurekastreams.web.client.ui.pages.master.StaticResourceBundle;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Hyperlink;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.Widget;
/**
* Discover Page.
*/
public class DiscoverContent extends Composite
{
/**
* Binder for building UI.
*/
private static LocalUiBinder binder = GWT.create(LocalUiBinder.class);
/**
* Number of streams to show in the "Most Active Streams" section.
*/
private static final int MOST_ACTIVE_STREAMS_PAGE_SIZE = 9;
/**
* Number of featured streams to show.
*/
private static final int FEATURED_STREAMS_PAGE_SIZE = 3;
/**
* CSS resource.
*/
interface DiscoverStyle extends CssResource
{
/** @return CSS style for search bar when it is the topmost item because the feature streams panel is hidden. */
String searchBarAtTop();
}
/** Local styles. */
@UiField
DiscoverStyle style;
/** Global styles. */
@UiField(provided = true)
CoreCss coreCss;
/** Search bar (accessible for styling). */
@UiField
DivElement searchBar;
/**
* Flow Panel to contain the stream search box.
*/
@UiField
FlowPanel searchFlowPanel;
/**
* Search button.
*/
@UiField
Label goSearch;
/**
* UI element for streams.
*/
@UiField
PagerComposite featuredStreamsComposite;
/**
* UI element for streams.
*/
@UiField
PagerComposite mostActiveStreamsComposite;
/**
* UI element for streams.
*/
@UiField
FlowPanel suggestedStreamsPanel;
/**
* UI element for streams.
*/
@UiField
FlowPanel mostViewedStreamsPanel;
/**
* UI element for streams.
*/
@UiField
FlowPanel mostFollowedStreamsPanel;
/**
* UI element for streams.
*/
@UiField
FlowPanel mostRecentStreamsPanel;
/**
* Search box.
*/
@UiField
LabeledTextBox searchBox;
/** Link to create group page. */
@UiField
Hyperlink createGroupButton;
/** Element showing message when list is empty. */
@UiField
DivElement suggestionsEmptyLabel;
/** Element showing message when list is empty. */
@UiField
DivElement mostViewedEmptyLabel;
/**
* JSNI.
*/
private final WidgetJSNIFacadeImpl jsniFacade = new WidgetJSNIFacadeImpl();
/**
* Default constructor.
*/
public DiscoverContent()
{
coreCss = StaticResourceBundle.INSTANCE.coreCss();
initWidget(binder.createAndBindUi(this));
UIObject.setVisible(suggestionsEmptyLabel, false);
UIObject.setVisible(mostViewedEmptyLabel, false);
createGroupButton.setTargetHistoryToken(Session.getInstance()
.generateUrl(new CreateUrlRequest(Page.NEW_GROUP)));
EventBus.getInstance().addObserver(GotStreamDiscoverListsDTOResponseEvent.class,
new Observer<GotStreamDiscoverListsDTOResponseEvent>()
{
public void update(final GotStreamDiscoverListsDTOResponseEvent event)
{
if (event.getResponse() != null)
{
// Note: Need to detach the observer before building the page to avoid an ininite loop. One
// of the paged lists on the page (featured streams?) causes a fetch from its model; that
// model gets its data from the StreamsDiscoveryModel, so another
// GotStreamDiscoverListsDTOResponseEvent gets raised. Since the EventBus dispatches events
// immediately (instead of the queuing them which would be preferred), the event will fire
// before buildPage returns.
EventBus.getInstance().removeObserver(event, this);
buildPage(event.getResponse());
}
}
});
EventBus.getInstance().addObserver(GotFeaturedStreamsPageResponseEvent.class,
new Observer<GotFeaturedStreamsPageResponseEvent>()
{
public void update(final GotFeaturedStreamsPageResponseEvent ev)
{
boolean featuredVisible = ev.getResponse().getTotal() > 0;
featuredStreamsComposite.setVisible(featuredVisible);
if (featuredVisible)
{
searchBar.removeClassName(style.searchBarAtTop());
}
else
{
searchBar.addClassName(style.searchBarAtTop());
}
}
});
// when someone follows or unfollows any stream, update the suggestions
EventBus.getInstance().addObservers(
new Observer<BaseDataRequestResponseEvent<SetFollowingStatusRequest, Integer>>()
{
public void update(final BaseDataRequestResponseEvent<SetFollowingStatusRequest, Integer> inArg1)
{
updateSuggestionsIfNeeded();
}
}, InsertedPersonFollowerResponseEvent.class, InsertedGroupMemberResponseEvent.class,
DeletedPersonFollowerResponseEvent.class, DeletedGroupMemberResponseEvent.class);
// also when someone blocks a suggestion
EventBus.getInstance().addObserver(InsertedBlockedSuggestionResponseEvent.class,
new Observer<InsertedBlockedSuggestionResponseEvent>()
{
public void update(final InsertedBlockedSuggestionResponseEvent inArg1)
{
updateSuggestionsIfNeeded();
}
});
// now fetch the data!
StreamsDiscoveryModel.getInstance().fetch(null, true);
}
/**
* Build the page.
*
* @param inDiscoverLists
* the data to display
*/
private void buildPage(final StreamDiscoverListsDTO inDiscoverLists)
{
// --------------------
// FEATURED STREAMS
// Note: the data needed for this list is built from the FeaturedStreamsModel, but is actually fetched and
// stored in the StreamsDiscoveryModel cache, so this request won't hit the server
featuredStreamsComposite.setHeader("Featured Streams");
featuredStreamsComposite.init(new FeaturedStreamsPagerUiStrategy(FEATURED_STREAMS_PAGE_SIZE));
featuredStreamsComposite.load();
// --------------------
// MOST ACTIVE STREAMS
// Note: the data needed for this list is built from the MostActiveStreamsModel, but is actually fetched and
// stored in the StreamsDiscoveryModel cache, so this request won't hit the server
mostActiveStreamsComposite.setHeader("Most Active Streams");
mostActiveStreamsComposite.init(new MostActiveStreamsPagerUiStrategy(MOST_ACTIVE_STREAMS_PAGE_SIZE));
mostActiveStreamsComposite.load();
populateSuggestedStreams(inDiscoverLists.getSuggestedStreams());
if (inDiscoverLists.getMostViewedStreams() != null)
{
for (StreamDTO stream : inDiscoverLists.getMostViewedStreams())
{
mostViewedStreamsPanel.add(new DiscoverListItemPanel(stream,
DiscoverListItemPanel.ListItemType.DAILY_VIEWERS));
}
}
UIObject.setVisible(mostViewedEmptyLabel, mostViewedStreamsPanel.getWidgetCount() == 0);
if (inDiscoverLists.getMostFollowedStreams() != null)
{
for (StreamDTO stream : inDiscoverLists.getMostFollowedStreams())
{
mostFollowedStreamsPanel.add(new DiscoverListItemPanel(stream,
DiscoverListItemPanel.ListItemType.FOLLOWERS));
}
}
if (inDiscoverLists.getMostRecentStreams() != null)
{
for (StreamDTO stream : inDiscoverLists.getMostRecentStreams())
{
mostRecentStreamsPanel.add(new DiscoverListItemPanel(stream,
DiscoverListItemPanel.ListItemType.TIME_AGO));
}
}
searchBox.addKeyUpHandler(new KeyUpHandler()
{
public void onKeyUp(final KeyUpEvent ev)
{
if (ev.getNativeKeyCode() == KeyCodes.KEY_ENTER && !ev.isAnyModifierKeyDown())
{
doSearch();
}
}
});
goSearch.addClickHandler(new ClickHandler()
{
public void onClick(final ClickEvent arg0)
{
doSearch();
}
});
}
/**
* Displays the list of suggested streams.
*
* @param inSuggestedStreams
* list of suggested streams.
*/
private void populateSuggestedStreams(final Collection<StreamDTO> inSuggestedStreams)
{
suggestedStreamsPanel.clear();
if (inSuggestedStreams != null)
{
for (final StreamDTO stream : inSuggestedStreams)
{
suggestedStreamsPanel.add(new DiscoverListItemPanel(stream,
DiscoverListItemPanel.ListItemType.MUTUAL_FOLLOWERS, true));
}
}
UIObject.setVisible(suggestionsEmptyLabel, suggestedStreamsPanel.getWidgetCount() == 0);
}
/**
* Decides if the suggestions list should be refreshed and triggers the refresh if so.
*/
private void updateSuggestionsIfNeeded()
{
// Note: Currently the suggestions list is "live" - any follow or unfollow will cause it to refresh, since a
// user's list of followed streams determines what the suggestions should be. But should that decision change
// (e.g. only refresh when following one of the suggested streams), this would be the place to put in the check.
EventBus.getInstance().addObserver(GotStreamDiscoverListsDTOResponseEvent.class,
new Observer<GotStreamDiscoverListsDTOResponseEvent>()
{
public void update(final GotStreamDiscoverListsDTOResponseEvent event)
{
// Note: Wire up a "one-shot" observer so we only hear about this one query. The paging on the
// active and featured streams also generates GotStreamDiscoverListsDTOResponseEvents, and we
// don't want to refresh everytime someone switches pages.
EventBus.getInstance().removeObserver(event, this);
populateSuggestedStreams(event.getResponse().getSuggestedStreams());
}
});
// Note: ok to use the cached data flag because we are being called from a person/group follow/unfollow success
// event, and the models that raise those events clear the StreamsDiscoveryModel. So if the data comes from
// cache, it was just loaded by another subscriber asking for it.
StreamsDiscoveryModel.getInstance().fetch(null, true);
}
/**
* Performs the search.
*/
private void doSearch()
{
EventBus.getInstance().notifyObservers(
new UpdateHistoryEvent(new CreateUrlRequest(Page.SEARCH, generateParams(searchBox.getText()), false)));
}
/**
* Creates a hashmap for the history parameters to pass to the search page.
*
* @param query
* the search string.
* @return the hashmap of all necessary initial search parameters.
*/
private HashMap<String, String> generateParams(final String query)
{
HashMap<String, String> params = new HashMap<String, String>();
params.put("query", query);
params.put("startIndex", "0");
params.put("endIndex", "9");
return params;
}
/**
* Binder for building UI.
*/
interface LocalUiBinder extends UiBinder<Widget, DiscoverContent>
{
}
}