org.eurekastreams.web.client.ui.common.notification.dialog.NotificationsDialogContent.java Source code

Java tutorial

Introduction

Here is the source code for org.eurekastreams.web.client.ui.common.notification.dialog.NotificationsDialogContent.java

Source

/*
 * Copyright (c) 2010-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.common.notification.dialog;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eurekastreams.server.domain.InAppNotificationDTO;
import org.eurekastreams.web.client.events.DialogLinkClickedEvent;
import org.eurekastreams.web.client.events.EventBus;
import org.eurekastreams.web.client.events.NotificationClickedEvent;
import org.eurekastreams.web.client.events.NotificationDeleteRequestEvent;
import org.eurekastreams.web.client.events.Observer;
import org.eurekastreams.web.client.events.UpdateRawHistoryEvent;
import org.eurekastreams.web.client.events.data.GotNotificationListResponseEvent;
import org.eurekastreams.web.client.model.NotificationListModel;
import org.eurekastreams.web.client.ui.Session;
import org.eurekastreams.web.client.ui.common.dialog.BaseDialogContent;
import org.eurekastreams.web.client.ui.common.notification.NotificationSettingsWidget;
import org.eurekastreams.web.client.ui.pages.master.CoreCss;
import org.eurekastreams.web.client.ui.pages.master.MasterComposite;
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.dom.client.Style.Display;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
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.uibinder.client.UiHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.Widget;

/**
 * Dialog content (i.e. main panel) for showing notifications.
 */
public class NotificationsDialogContent extends BaseDialogContent {
    /** Main content widget. */
    private final Widget main;

    /** To unwire the observer when done with dialog. */
    private Observer<DialogLinkClickedEvent> linkClickedObserver;

    /** Binder for building UI. */
    private static LocalUiBinder binder = GWT.create(LocalUiBinder.class);

    /** Local styles. */
    @UiField
    LocalStyle style;

    /** Global CSS. */
    @UiField(provided = true)
    CoreCss coreCss;

    /** The list of sources. */
    @UiField
    FlowPanel sourceFiltersPanel;

    /** Panel making a shadow on the source filter widgets. */
    @UiField
    SimplePanel shadowPanel;

    /** Scroll panel holding the notification list. */
    @UiField
    ScrollPanel notificationListScrollPanel;

    /** The displayed list of notifications. */
    @UiField
    FlowPanel notificationListPanel;

    /** Element to indicate no notifications. */
    @UiField
    DivElement noNotificationsUi;

    /** Selector for all (read+unread) notifications. */
    @UiField
    Label allFilterUi;

    /** Selector for unread only notifications. */
    @UiField
    Label unreadFilterUi;

    /** Button to mark all notifications as read. */
    @UiField
    Label markAllReadButton;

    /** Button to switch show settings view. */
    @UiField
    Label settingsButton;

    /** Current all/unread selector. */
    private Widget currentReadFilterWidget;

    /** Notifications. */
    private List<InAppNotificationDTO> allNotifications;

    /** All notification widgets (cached to prevent re-creating when changing filters). */
    private final Map<Long, NotificationWidget> notifWidgetIndex = new HashMap<Long, NotificationWidget>();

    /** The notifications currently being displayed. */
    private final Collection<InAppNotificationDTO> notifsShowing = new ArrayList<InAppNotificationDTO>();

    /** Source representing all notifications. */
    private Source rootSource;

    /** Index of actual sources. */
    private Map<String, Source> sourceIndex;

    /** Currently-selected source. */
    private Source currentSource;

    /** Currently selected show read option. */
    private boolean currentShowRead = false;

    /** See explanation where this is used. */
    private final boolean manuallyHandleInternalLinks;

    /** Observer (allow unlinking). */
    private final Observer<NotificationClickedEvent> notificationClickedObserver = // \n
            new Observer<NotificationClickedEvent>() {
                public void update(final NotificationClickedEvent ev) {
                    handleNotificationClicked(ev.getNotification(), ev.getWidget());
                }
            };
    /** Observer (allow unlinking). */
    private final Observer<NotificationDeleteRequestEvent> notificationDeleteRequestObserver = // \n
            new Observer<NotificationDeleteRequestEvent>() {
                public void update(final NotificationDeleteRequestEvent ev) {
                    handleNotificationDeleteRequest(ev.getResponse());
                }
            };

    /**
     * Constructor.
     */
    public NotificationsDialogContent() {
        // -- determine if IE workaround is needed (see explanation where used) --
        manuallyHandleInternalLinks = MasterComposite.getUserAgent().contains("msie");

        // -- build UI --
        coreCss = StaticResourceBundle.INSTANCE.coreCss();
        main = binder.createAndBindUi(this);
        currentReadFilterWidget = unreadFilterUi;

        // -- setup events --
        final EventBus eventBus = Session.getInstance().getEventBus();

        eventBus.addObserver(GotNotificationListResponseEvent.class,
                new Observer<GotNotificationListResponseEvent>() {
                    public void update(final GotNotificationListResponseEvent ev) {
                        eventBus.removeObserver(ev, this);
                        storeReceivedNotifications(ev.getResponse());
                        selectSource(currentSource);
                    }
                });
        eventBus.addObserver(NotificationClickedEvent.class, notificationClickedObserver);
        eventBus.addObserver(NotificationDeleteRequestEvent.class, notificationDeleteRequestObserver);

        // -- request data --
        NotificationListModel.getInstance().fetch(null, false);
    }

    /**
     * Invoked on closing before the dialog is removed from screen.
     */
    @Override
    public void beforeHide() {
        if (linkClickedObserver != null) {
            Session.getInstance().getEventBus().removeObserver(DialogLinkClickedEvent.class, linkClickedObserver);
            linkClickedObserver = null;
        }
        EventBus.getInstance().removeObserver(NotificationClickedEvent.class, notificationClickedObserver);
        EventBus.getInstance().removeObserver(NotificationDeleteRequestEvent.class,
                notificationDeleteRequestObserver);
    }

    /**
     * {@inheritDoc}
     */
    public Widget getBody() {
        return main;
    }

    /**
     * {@inheritDoc}
     */
    public String getTitle() {
        return "Notifications";
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getCssName() {
        return style.modal();
    }

    /**
     * Responds appropriately to a notification being clicked.
     *
     * @param item
     *            The notification.
     * @param widget
     *            The notification's widget.
     */
    private void handleNotificationClicked(final InAppNotificationDTO item, final Widget widget) {
        // tell server notification is read
        if (!item.isRead()) {
            NotificationListModel.getInstance().update(item.getId());
        }

        // dismiss the dialog when clicking on a notification with an internal URL. (Dialog will be discarded, so no
        // UI or local data updates are required.)
        final String url = item.getUrl();
        boolean hasInternalUrl = url != null && !url.isEmpty() && url.charAt(0) == '#';
        if (hasInternalUrl) {
            close();

            // For some reason, in IE (7 & 8), if the URL fragment is completely empty, then clicking one of the
            // notification links will update the URL in the address bar, but the HistoryHandler will never be notified
            // of it. So we need to force the app to go to the desired URL.
            // Also, IE seems to lose the history stack when clicking on a plain link, so we manually handle internal
            // links for all cases (not just the empty history token).
            if (manuallyHandleInternalLinks) {
                EventBus.getInstance().notifyObservers(new UpdateRawHistoryEvent(url.substring(1)));
            }
        }
        // not closing dialog, so update UI and local data if item was just read
        else if (!item.isRead()) {
            item.setRead(true);

            // work up the source tree, reducing the unread count and hiding sources as applicable. (Note that the
            // starting source may not be the current source. This happens when the user is viewing "All" or
            // "Streams"/"Apps".)
            Source source = getSource(item);
            while (source != null) {
                source.decrementUnreadCount();
                updateDisplayString(source);
                if (source.getUnreadCount() == 0 && source != rootSource) {
                    source.getWidget().addStyleName(style.sourceFilterAllRead());
                }

                source = source.getParent();
            }

            // in unread view, insure no read items or empty sources are showing
            if (!currentShowRead) {
                if (currentSource.getUnreadCount() == 0) {
                    // Note: if already showing the root source, then this will redraw it with the "none" message.
                    selectSource(rootSource);
                } else {
                    widget.removeFromParent();
                }
                setShadowHeight();
            }
        }
    }

    /**
     * Deletes a notification on request.
     *
     * @param item
     *            The notification.
     */
    private void handleNotificationDeleteRequest(final InAppNotificationDTO item) {
        if (notifsShowing.remove(item)) {
            allNotifications.remove(item);
            notifWidgetIndex.remove(item.getId());

            // work up the source tree, updating/hiding/removing sources as applicable. (Note that the starting source
            // may not be the current source. This happens when the user is viewing "All" or "Streams"/"Apps".)
            Source source = getSource(item);
            while (source != null) {
                source.decrementTotalCount();
                if (source.getTotalCount() == 0 && source != rootSource) {
                    source.getWidget().removeFromParent();
                } else if (!item.isRead()) {
                    source.decrementUnreadCount();
                    updateDisplayString(source);
                    if (source.getUnreadCount() == 0 && source != rootSource) {
                        source.getWidget().addStyleName(style.sourceFilterAllRead());
                    }
                }

                source = source.getParent();
            }

            NotificationListModel.getInstance().delete(item.getId());

            // switch source if no notifications left to show. (If already showing the root source, this will just
            // redraw it with the "none" message.)
            if (currentSource.getTotalCount() == 0 || (!currentShowRead && currentSource.getUnreadCount() == 0)) {
                selectSource(rootSource);
            }

            setShadowHeight();
        }
    }

    /**
     * Gets the source for a given notification.
     *
     * @param item
     *            Notification.
     * @return Source.
     */
    private Source getSource(final InAppNotificationDTO item) {
        Source source = sourceIndex.get(SourceListBuilder.buildSourceKey(item));
        return source == null ? rootSource : source;
    }

    /**
     * Handles the received list of notifications.
     *
     * @param list
     *            List of notifications.
     */
    private void storeReceivedNotifications(final List<InAppNotificationDTO> list) {
        allNotifications = list;

        SourceListBuilder builder = new SourceListBuilder(list,
                Session.getInstance().getCurrentPerson().getAccountId());
        rootSource = builder.getRootSource();
        sourceIndex = builder.getSourceIndex();

        shadowPanel.setVisible(false);
        for (Source source : builder.getSourceList()) {
            addSourceFilter(source, !source.isCategorySource());
        }

        currentSource = rootSource;

        // set up shadow
        setShadowHeight();
        shadowPanel.setVisible(true);
    }

    /**
     * Adjusts the height of the shadow to the displayed sources.
     */
    private void setShadowHeight() {
        shadowPanel.setHeight("0px");
        shadowPanel.setHeight(sourceFiltersPanel.getOffsetHeight() + "px");
    }

    /**
     * Creates and adds the widget for a source filter.
     *
     * @param source
     *            Source data.
     * @param indent
     *            If the label should be indented.
     */
    private void addSourceFilter(final Source source, final boolean indent) {
        int count = source.getUnreadCount();
        String text = count > 0 ? source.getDisplayName() + " (" + count + ")" : source.getDisplayName();

        final Label label = new Label(text);
        label.addStyleName(style.sourceFilter());
        label.addStyleName(StaticResourceBundle.INSTANCE.coreCss().buttonLabel());
        label.addStyleName(StaticResourceBundle.INSTANCE.coreCss().ellipsis());
        if (count == 0 && source != rootSource) {
            label.addStyleName(style.sourceFilterAllRead());
        }
        if (indent) {
            label.addStyleName(style.sourceFilterIndented());
        }
        label.addClickHandler(new ClickHandler() {
            public void onClick(final ClickEvent inEvent) {
                selectSource(source);
            }
        });

        sourceFiltersPanel.add(label);

        source.setWidget(label);
    }

    /**
     * Updates the display to show a new source.
     *
     * @param newSource
     *            New source.
     */
    private void selectSource(final Source newSource) {
        currentSource.getWidget().removeStyleName(style.filterSelected());

        currentSource = newSource;
        currentSource.getWidget().addStyleName(style.filterSelected());
        displayNotifications(currentSource.getFilter(), currentShowRead);
    }

    /**
     * Displays the appropriate subset of notifications.
     *
     * @param filter
     *            Filter for notifications.
     * @param showRead
     *            If read notifications should be displayed (unread are always displayed).
     */
    private void displayNotifications(final Source.Filter filter, final boolean showRead) {
        noNotificationsUi.getStyle().setDisplay(Display.NONE);
        notificationListScrollPanel.setVisible(false);

        notificationListPanel.clear();
        notifsShowing.clear();

        for (InAppNotificationDTO item : allNotifications) {
            if (filter.shouldDisplay(item) && (showRead || !item.isRead())) {
                notifsShowing.add(item);
                notificationListPanel.add(getNotificationWidget(item));
            }
        }
        if (notifsShowing.isEmpty()) {
            noNotificationsUi.getStyle().clearDisplay();
        } else {
            notificationListScrollPanel.scrollToTop();
            notificationListScrollPanel.setVisible(true);
        }
    }

    /**
     * Gets/creates a notification widget for the given notification.
     *
     * @param item
     *            Notification.
     * @return Widget.
     */
    private NotificationWidget getNotificationWidget(final InAppNotificationDTO item) {
        NotificationWidget widget = notifWidgetIndex.get(item.getId());
        if (widget == null) {
            widget = new NotificationWidget(item, manuallyHandleInternalLinks);
            notifWidgetIndex.put(item.getId(), widget);
        }
        return widget;
    }

    /**
     * Shows all (unread+read) or just unread notifications.
     *
     * @param ev
     *            Event.
     */
    @UiHandler({ "allFilterUi", "unreadFilterUi" })
    void onFilterClick(final ClickEvent ev) {
        Widget selector = (Widget) ev.getSource();
        if (selector != currentReadFilterWidget) {
            currentReadFilterWidget.removeStyleName(style.filterSelected());
            currentReadFilterWidget = selector;
            currentReadFilterWidget.addStyleName(style.filterSelected());
            currentShowRead = !currentShowRead;

            if (currentShowRead) {
                sourceFiltersPanel.removeStyleName(style.sourceFilterListUnreadOnly());
            } else {
                sourceFiltersPanel.addStyleName(style.sourceFilterListUnreadOnly());
                if (currentSource != rootSource && currentSource.getUnreadCount() == 0) {
                    setShadowHeight();
                    selectSource(rootSource);
                    return;
                }
            }

            setShadowHeight();
            displayNotifications(currentSource.getFilter(), currentShowRead);
        }
    }

    /**
     * Marks all notifications read.
     *
     * @param ev
     *            Event.
     */
    @UiHandler("markAllReadButton")
    void onMarkAllReadClick(final ClickEvent ev) {
        // update on server
        ArrayList<Long> ids = new ArrayList<Long>();
        for (InAppNotificationDTO item : notifsShowing) {
            if (!item.isRead()) {
                item.setRead(true);
                ids.add(item.getId());
            }
        }
        if (ids.isEmpty()) {
            return;
        }
        NotificationListModel.getInstance().update(ids);

        // update the sources (unread counts and display)
        // This process needs to work upwards and downwards from the currently-displayed source. (e.g. marking all as
        // read on the Streams source needs to both decrement the appropriate number from "All"'s unread count as well
        // as zero out the unread count for every one of its child sources.) Given there are only three levels and very
        // few non-leaf sources, it is easiest to code for the specific cases.

        if (currentSource == rootSource) {
            // Root source. The root source contains everything, so set every source's unread count to zero.
            for (Source source : sourceIndex.values()) {
                source.setUnreadCount(0);
                updateDisplayString(source);
                if (source != rootSource) {
                    source.getWidget().addStyleName(style.sourceFilterAllRead());
                }
            }
        } else if (currentSource.isCategorySource()) {
            // Non-root category source. Set its and children's unread counts to zero; subtract from root's count.
            for (Source source : sourceIndex.values()) {
                if (source == currentSource || source.getParent() == currentSource) {
                    source.setUnreadCount(0);
                    updateDisplayString(source);
                    source.getWidget().addStyleName(style.sourceFilterAllRead());
                }
            }
            rootSource.setUnreadCount(rootSource.getUnreadCount() - ids.size());
            updateDisplayString(rootSource);
        } else {
            // Leaf source. Work upwards.
            int number = ids.size();
            for (Source source = currentSource; source != null; source = source.getParent()) {
                int unreadCount = source.getUnreadCount() - number;
                source.setUnreadCount(unreadCount);
                updateDisplayString(source);
                if (source != rootSource && unreadCount == 0) {
                    source.getWidget().addStyleName(style.sourceFilterAllRead());
                }
            }
        }

        // select a different source (or redraw root) if unread-only filter is active
        if (!currentShowRead) {
            setShadowHeight();
            selectSource(rootSource);
        }

        // add the already-read style to all the individual notification widgets (since the widgets are cached and
        // reused until the dialog is closed)
        for (long id : ids) {
            notifWidgetIndex.get(id).addReadStyle();
        }
    }

    /**
     * Shows the settings view.
     *
     * @param ev
     *            Event.
     */
    @UiHandler("settingsButton")
    void showSettings(final ClickEvent ev) {
        final NotificationSettingsWidget settings = new NotificationSettingsWidget(true);
        settings.setCloseCommand(new Command() {
            public void execute() {
                settings.removeFromParent();
                main.setVisible(true);
            }
        });

        // This assumes the dialog hosts the dialog content in a 1) dedicated panel which 2) allows multiple children.
        HasWidgets parent = (HasWidgets) main.getParent();
        main.setVisible(false);
        parent.add(settings);
    }

    /**
     * Causes the source to apply the current display string to the widget.
     *
     * @param source
     *            Source.
     */
    private void updateDisplayString(final Source source) {
        Label widget = source.getWidget();
        if (usingMozillaBinding(widget.getElement())) {
            int index = sourceFiltersPanel.getWidgetIndex(widget);
            if (index >= 0) {
                widget.removeFromParent();
                widget.setText(source.getDisplayString());
                sourceFiltersPanel.insert(widget, index);
            }
        } else {
            widget.setText(source.getDisplayString());
        }
    }

    /**
     * Determines if the source's widget is using a Mozilla binding. The purpose is to check for the XUL ellipsis
     * binding, since that binding causes text updates to fail, thus we must do some trickery to work around it.
     *
     * @param elem
     *            Element to check.
     * @return If using -moz-binding.
     */
    private static native boolean usingMozillaBinding(final Element elem)
    /*-{
        var v = $wnd.jQuery(elem).css('-moz-binding');
        return v ? v !== 'none' : false;
    }-*/;

    /**
     * Local styles.
     */
    interface LocalStyle extends CssResource {
        /** @return Extra style for entire modal. */
        @ClassName("modal")
        String modal();

        /** @return Style applied to the source list to only show sources with unread notifs. */
        @ClassName("source-filter-list-unread-only")
        String sourceFilterListUnreadOnly();

        /** @return Style applied to sources where all notifs are read. */
        @ClassName("source-filter-all-read")
        String sourceFilterAllRead();

        /** @return Style for sources. */
        @ClassName("source-filter")
        String sourceFilter();

        /** @return Added style for a selected filter (the selected source or unread/all). */
        @ClassName("filter-selected")
        String filterSelected();

        /** @return Added style for indented sources. */
        @ClassName("source-filter-indented")
        String sourceFilterIndented();
    }

    /**
     * Binder for building UI.
     */
    interface LocalUiBinder extends UiBinder<Widget, NotificationsDialogContent> {
    }
}