/* * Copyright (c) 2009-2013 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.widgets.activity; import java.util.ArrayList; import org.eurekastreams.server.action.request.stream.PostActivityRequest; import org.eurekastreams.server.domain.DomainConversionUtility; import org.eurekastreams.server.domain.EntityType; import org.eurekastreams.server.domain.stream.ActivityDTO; import org.eurekastreams.server.domain.stream.StreamScope; import org.eurekastreams.server.domain.stream.StreamScope.ScopeType; import org.eurekastreams.web.client.events.EventBus; import org.eurekastreams.web.client.events.MessageAttachmentChangedEvent; import org.eurekastreams.web.client.events.MessageStreamAppendEvent; import org.eurekastreams.web.client.events.Observer; import org.eurekastreams.web.client.events.data.GotAllPopularHashTagsResponseEvent; import org.eurekastreams.web.client.events.data.GotSystemSettingsResponseEvent; import org.eurekastreams.web.client.events.data.PostableStreamScopeChangeEvent; import org.eurekastreams.web.client.model.ActivityModel; import org.eurekastreams.web.client.model.AllPopularHashTagsModel; import org.eurekastreams.web.client.model.SystemSettingsModel; import org.eurekastreams.web.client.ui.Session; import org.eurekastreams.web.client.ui.TimerFactory; import org.eurekastreams.web.client.ui.TimerHandler; import org.eurekastreams.web.client.ui.common.autocomplete.ExtendedTextArea; import org.eurekastreams.server.domain.stream.LinkInformation; import org.eurekastreams.web.client.ui.common.avatar.AvatarWidget.Size; import org.eurekastreams.web.client.ui.common.stream.attach.Attachment; import org.eurekastreams.web.client.ui.common.stream.attach.bookmark.AddLinkComposite; import org.eurekastreams.web.client.ui.common.stream.attach.bookmark.Bookmark; import org.eurekastreams.web.client.ui.common.stream.decorators.ActivityDTOPopulator; import org.eurekastreams.web.client.ui.common.stream.decorators.ActivityDTOPopulatorStrategy; import org.eurekastreams.web.client.ui.common.stream.decorators.object.NotePopulator; import org.eurekastreams.web.client.ui.common.stream.decorators.verb.PostPopulator; import org.eurekastreams.web.client.ui.common.stream.renderers.AvatarRenderer; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.FocusEvent; import com.google.gwt.event.dom.client.FocusHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.KeyDownHandler; import com.google.gwt.event.dom.client.KeyUpEvent; import com.google.gwt.event.dom.client.KeyUpHandler; import com.google.gwt.event.dom.client.MouseOverEvent; import com.google.gwt.event.dom.client.MouseOverHandler; 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.HTMLPanel; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.PushButton; import com.google.gwt.user.client.ui.Widget; /** * Post box. */ public class PostBoxComposite extends Composite { /** Max auto-complete hashtags to show. */ private static final int MAX_HASH_TAG_AUTOCOMPLETE_ENTRIES = 10; /** Hide delay after blur on post box. */ private static final Integer BLUR_DELAY = 500; /** Max chars for post. */ private static final Integer POST_MAX = 250; /** Padding for hashtag dropdown. */ private static final int HASH_TAG_DROP_DOWN_PADDING = 14; /** * Binder for building UI. */ private static LocalUiBinder binder = GWT.create(LocalUiBinder.class); /** * * Binder for building UI. */ interface LocalUiBinder extends UiBinder<Widget, PostBoxComposite> { } /** * Post box CssResource style. */ interface PostBoxStyle extends CssResource { /** * Visible post box style. * * @return Visible post box style. */ String visiblePostBox(); /** * Post char count over limit. * * @return Post char count over limit. */ String postCharCountOverLimit(); /** * Post button inactive. * * @return post button inactive. */ String postButtonInactive(); /** * Active hashtag style. * * @return Active hashtag style. */ String activeHashTag(); } /** * Post box CssResource style. */ @UiField PostBoxStyle style; /** * UI element for poster avatar. */ @UiField HTMLPanel posterAvatar; /** * UI element for post panel. */ @UiField HTMLPanel postPanel; /** * UI element for post box. */ @UiField ExtendedTextArea postBox; /** * UI element for post options. */ @UiField DivElement postOptions; /** * UI element for post button. */ @UiField PushButton postButton; /** * UI element for post char count. */ @UiField DivElement postCharCount; /** * UI element for add link composite. */ @UiField AddLinkComposite addLinkComposite; /** * Hash Tags. */ @UiField FlowPanel hashTags; /** * The content warning. */ @UiField FlowPanel contentWarning; /** * The content warning container. */ @UiField DivElement contentWarningContainer; /** * Currently active item. */ private Integer activeItemIndex = null; /** * Timer factory. */ private final TimerFactory timerFactory = new TimerFactory(); /** * Current stream to post to. */ private StreamScope currentStream = new StreamScope(ScopeType.PERSON, Session.getInstance().getCurrentPerson() .getAccountId()); /** * Avatar Renderer. */ private final AvatarRenderer avatarRenderer = new AvatarRenderer(); /** * Activity Populator. */ private final ActivityDTOPopulator activityPopulator = new ActivityDTOPopulator(); /** * Attachment. */ private Attachment attachment = null; /** * All hash tags. */ private ArrayList<String> allHashTags; /** * Default constructor. */ public PostBoxComposite() { initWidget(binder.createAndBindUi(this)); buildPage(); } /** * Build page. */ private void buildPage() { posterAvatar.add(avatarRenderer.render(Session.getInstance().getCurrentPerson().getEntityId(), Session .getInstance().getCurrentPerson().getAvatarId(), EntityType.PERSON, Size.Small)); postCharCount.setInnerText(POST_MAX.toString()); checkPostBox(); postBox.setLabel("Post to your stream..."); postBox.reset(); addEvents(); postBox.addKeyUpHandler(new KeyUpHandler() { public void onKeyUp(final KeyUpEvent event) { checkPostBox(); } }); postBox.addFocusHandler(new FocusHandler() { public void onFocus(final FocusEvent event) { postOptions.addClassName(style.visiblePostBox()); } }); postBox.addBlurHandler(new BlurHandler() { public void onBlur(final BlurEvent event) { timerFactory.runTimer(BLUR_DELAY, new TimerHandler() { public void run() { if (postBox.getText().trim().length() == 0 && !addLinkComposite.inAddMode() && attachment == null) { postOptions.removeClassName(style.visiblePostBox()); postBox.getElement().getStyle().clearHeight(); postBox.reset(); } } }); } }); setupPostButtonClickHandler(); postBox.addKeyDownHandler(new KeyDownHandler() { public void onKeyDown(final KeyDownEvent event) { if ((event.getNativeKeyCode() == KeyCodes.KEY_TAB || event.getNativeKeyCode() == KeyCodes.KEY_ENTER) && !event.isAnyModifierKeyDown() && activeItemIndex != null) { hashTags.getWidget(activeItemIndex).getElement() .dispatchEvent(Document.get().createClickEvent(1, 0, 0, 0, 0, false, false, false, false)); event.preventDefault(); event.stopPropagation(); // clearSearch(); } else if (event.getNativeKeyCode() == KeyCodes.KEY_DOWN && activeItemIndex != null) { if (activeItemIndex + 1 < hashTags.getWidgetCount()) { selectItem((Label) hashTags.getWidget(activeItemIndex + 1)); } event.preventDefault(); event.stopPropagation(); } else if (event.getNativeKeyCode() == KeyCodes.KEY_UP && activeItemIndex != null) { if (activeItemIndex - 1 >= 0) { selectItem((Label) hashTags.getWidget(activeItemIndex - 1)); } event.preventDefault(); event.stopPropagation(); } else if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER && event.isControlKeyDown()) { postButton.getElement().dispatchEvent( Document.get().createClickEvent(1, 0, 0, 0, 0, false, false, false, false)); event.preventDefault(); event.stopPropagation(); } } }); postBox.addKeyUpHandler(new KeyUpHandler() { public void onKeyUp(final KeyUpEvent event) { int tagsAdded = 0; String postText = postBox.getText(); if (!postText.endsWith(" ")) { String[] words = postText.split("\\s"); if (words.length >= 1) { final String lastWord = words[words.length - 1]; if (lastWord.startsWith("#")) { boolean activeItemSet = false; Label firstItem = null; for (String hashTag : allHashTags) { if (hashTag.startsWith(lastWord)) { // get list ready on first tag added if (tagsAdded == 0) { hashTags.clear(); String boxHeight = postBox.getElement().getStyle().getHeight() .replace("px", ""); if (boxHeight.isEmpty()) { boxHeight = "44"; } hashTags.getElement() .getStyle() .setTop(Integer.parseInt(boxHeight) + HASH_TAG_DROP_DOWN_PADDING, Unit.PX); hashTags.setVisible(true); } final String hashTagFinal = hashTag; final Label tagLbl = new Label(hashTagFinal); tagLbl.addClickHandler(new ClickHandler() { public void onClick(final ClickEvent event) { String postText = postBox.getText(); postText = postText.substring(0, postText.length() - lastWord.length()) + hashTagFinal + " "; postBox.setText(postText); hashTags.setVisible(false); hashTags.clear(); activeItemIndex = null; } }); tagLbl.addMouseOverHandler(new MouseOverHandler() { public void onMouseOver(final MouseOverEvent arg0) { selectItem(tagLbl); } }); hashTags.add(tagLbl); if (firstItem == null) { firstItem = tagLbl; } if (activeItemIndex != null && activeItemIndex.equals(hashTags.getWidgetCount() - 1)) { activeItemIndex = null; activeItemSet = true; selectItem(tagLbl); } tagsAdded++; if (tagsAdded >= MAX_HASH_TAG_AUTOCOMPLETE_ENTRIES) { break; } } } if (!activeItemSet && firstItem != null) { activeItemIndex = null; selectItem(firstItem); } } } } if (tagsAdded == 0) { hashTags.setVisible(false); activeItemIndex = null; } } }); AllPopularHashTagsModel.getInstance().fetch(null, true); SystemSettingsModel.getInstance().fetch(null, true); hashTags.setVisible(false); } /** * Add events. */ private void addEvents() { EventBus.getInstance().addObserver(MessageStreamAppendEvent.class, new Observer<MessageStreamAppendEvent>() { public void update(final MessageStreamAppendEvent event) { attachment = null; addLinkComposite.close(); postBox.setText(""); postBox.reset(); postBox.getElement().getStyle().clearHeight(); postOptions.removeClassName(style.visiblePostBox()); checkPostBox(); } }); EventBus.getInstance().addObserver(PostableStreamScopeChangeEvent.class, new Observer<PostableStreamScopeChangeEvent>() { public void update(final PostableStreamScopeChangeEvent stream) { currentStream = stream.getResponse(); if (currentStream != null && !"".equals(currentStream.getDisplayName())) { if (currentStream.getScopeType().equals(ScopeType.PERSON)) { if (currentStream.getDisplayName().endsWith("s")) { postBox.setLabel("Post to " + currentStream.getDisplayName() + "' stream..."); } else { postBox.setLabel("Post to " + currentStream.getDisplayName() + "'s stream..."); } } else { postBox.setLabel("Post to the " + currentStream.getDisplayName() + " stream..."); } } else { postBox.setLabel("Post to your stream..."); } postPanel.setVisible(stream.getResponse().getScopeType() != null); } }); EventBus.getInstance().addObserver(MessageAttachmentChangedEvent.class, new Observer<MessageAttachmentChangedEvent>() { public void update(final MessageAttachmentChangedEvent evt) { attachment = evt.getAttachment(); // When adding an attachment, // make the "Post" Button visible. checkPostBox(); } }); EventBus.getInstance().addObserver(GotSystemSettingsResponseEvent.class, new Observer<GotSystemSettingsResponseEvent>() { public void update(final GotSystemSettingsResponseEvent event) { String warning = event.getResponse().getContentWarningText(); if (warning != null && !warning.isEmpty()) { contentWarning.getElement().setInnerHTML(warning); } else { contentWarning.setVisible(false); contentWarningContainer.getStyle().setDisplay(Display.NONE); } } }); Session.getInstance() .getEventBus() .addObserver(GotAllPopularHashTagsResponseEvent.class, new Observer<GotAllPopularHashTagsResponseEvent>() { public void update(final GotAllPopularHashTagsResponseEvent event) { allHashTags = new ArrayList<String>(event.getResponse()); } }); } /** * Check the post box. */ private void checkPostBox() { postCharCount.setInnerText(Integer.toString(POST_MAX - postBox.getText().length())); if (POST_MAX - postBox.getText().length() < 0) { postCharCount.addClassName(style.postCharCountOverLimit()); } else { postCharCount.removeClassName(style.postCharCountOverLimit()); } if ((postBox.getText().length() > 0 && POST_MAX - postBox.getText().length() >= 0) || attachment != null) { postButton.removeStyleName(style.postButtonInactive()); } else { postButton.addStyleName(style.postButtonInactive()); } } /** * Select an item. * * @param item * the item. */ private void selectItem(final Label item) { if (activeItemIndex != null) { hashTags.getWidget(activeItemIndex).removeStyleName(style.activeHashTag()); } item.addStyleName(style.activeHashTag()); activeItemIndex = hashTags.getWidgetIndex(item); } /** * Setup the post button click handler - to prevent the checkstyle error of too-long method name. */ private void setupPostButtonClickHandler() { postButton.addClickHandler(new ClickHandler() { /** * Handle the button click. * * @param inEvent * the click event */ public void onClick(final ClickEvent inEvent) { if (!postButton.getStyleName().contains(style.postButtonInactive())) { if (!isLinkAttached() && addLinkComposite.inAddMode() && doesUnattachedLinkTextHaveText()) { // Assume that the user meant to attach the link, // so attach the link to the message. // Grab the link text from the unattached link textbox LinkInformation linkInformation = new LinkInformation(); String linkText = addLinkComposite.getLinkText(); linkInformation.setUrl(linkText); linkInformation.setTitle(linkText); Bookmark bookmark = new Bookmark(linkInformation); attachment = bookmark; } ActivityDTOPopulatorStrategy objectStrat = attachment != null ? attachment.getPopulator() : new NotePopulator(); ActivityDTO activity = activityPopulator.getActivityDTO(postBox.getText(), DomainConversionUtility.convertToEntityType(currentStream.getScopeType()), currentStream.getUniqueKey(), new PostPopulator(), objectStrat); PostActivityRequest postRequest = new PostActivityRequest(activity); ActivityModel.getInstance().insert(postRequest); postButton.addStyleName(style.postButtonInactive()); } } }); } /** * Checks whether there's text in an unattached link. * * @return boolean true or false */ private boolean doesUnattachedLinkTextHaveText() { String linkText = addLinkComposite.getLinkText(); String basicLegitimateHttpPattern = "://"; // Does the unattached link have "http://" and a period if (linkText.toLowerCase().contains(basicLegitimateHttpPattern) && linkText.contains(".")) { return true; } return false; } /** * Returns whether a link is attached to a user's unsubmitted post. * * @return boolean true or false */ private boolean isLinkAttached() { return addLinkComposite.hasAttachment(); } }