| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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.waveprotocol.wave.client.wavepanel.render; |
| |
| import com.google.common.base.Preconditions; |
| |
| import org.waveprotocol.wave.client.account.ProfileManager; |
| import org.waveprotocol.wave.client.scheduler.TimerService; |
| import org.waveprotocol.wave.client.state.ThreadReadStateMonitor; |
| import org.waveprotocol.wave.client.wavepanel.view.BlipMetaView; |
| import org.waveprotocol.wave.client.wavepanel.view.BlipView; |
| import org.waveprotocol.wave.client.wavepanel.view.ConversationView; |
| import org.waveprotocol.wave.client.wavepanel.view.InlineThreadView; |
| import org.waveprotocol.wave.client.wavepanel.view.ParticipantView; |
| import org.waveprotocol.wave.client.wavepanel.view.ParticipantsView; |
| import org.waveprotocol.wave.client.wavepanel.view.ThreadView; |
| import org.waveprotocol.wave.client.wavepanel.view.dom.ModelAsViewProvider; |
| import org.waveprotocol.wave.client.wavepanel.view.dom.full.BlipQueueRenderer.PagingHandler; |
| import org.waveprotocol.wave.model.conversation.Conversation; |
| import org.waveprotocol.wave.model.conversation.Conversation.Anchor; |
| import org.waveprotocol.wave.model.conversation.ConversationBlip; |
| import org.waveprotocol.wave.model.conversation.ConversationThread; |
| import org.waveprotocol.wave.model.conversation.ObservableConversation; |
| import org.waveprotocol.wave.model.conversation.ObservableConversationBlip; |
| import org.waveprotocol.wave.model.conversation.ObservableConversationThread; |
| import org.waveprotocol.wave.model.conversation.ObservableConversationView; |
| import org.waveprotocol.wave.model.supplement.ObservableSupplementedWave; |
| import org.waveprotocol.wave.model.util.CollectionUtils; |
| import org.waveprotocol.wave.model.util.IdentityMap; |
| import org.waveprotocol.wave.model.util.IdentityMap.ProcV; |
| import org.waveprotocol.wave.model.wave.ParticipantId; |
| |
| /** |
| * Renderer the conversation update. |
| * |
| */ |
| public class LiveConversationViewRenderer |
| implements ObservableConversationView.Listener, PagingHandler { |
| |
| private class LiveConversationRenderer implements ObservableConversation.Listener, |
| ObservableConversation.AnchorListener, PagingHandler { |
| private final ObservableConversation conversation; |
| private final LiveProfileRenderer profileRenderer; |
| |
| LiveConversationRenderer( |
| ObservableConversation conversation, LiveProfileRenderer profileRenderer) { |
| this.conversation = conversation; |
| this.profileRenderer = profileRenderer; |
| } |
| |
| private LiveConversationRenderer init() { |
| profileRenderer.init(); |
| // Note: blip contributions are only monitored once a blip is paged in. |
| for (ParticipantId participant : conversation.getParticipantIds()) { |
| profileRenderer.monitorParticipation(conversation, participant); |
| } |
| conversation.addListener((ObservableConversation.AnchorListener) this); |
| conversation.addListener((ObservableConversation.Listener) this); |
| return this; |
| } |
| |
| public void destroy() { |
| conversation.removeListener((ObservableConversation.AnchorListener) this); |
| conversation.removeListener((ObservableConversation.Listener) this); |
| profileRenderer.destroy(); |
| } |
| |
| @Override |
| public void onParticipantAdded(ParticipantId participant) { |
| ParticipantsView participantUi = views.getParticipantsView(conversation); |
| // Note: this does not insert the participant in the correct order. |
| participantUi.appendParticipant(conversation, participant); |
| profileRenderer.monitorParticipation(conversation, participant); |
| } |
| |
| @Override |
| public void onParticipantRemoved(ParticipantId participant) { |
| ParticipantView participantUi = views.getParticipantView(conversation, participant); |
| if (participantUi != null) { |
| participantUi.remove(); |
| } |
| profileRenderer.unmonitorParticipation(conversation, participant); |
| } |
| |
| @Override |
| public void onThreadAdded(ObservableConversationThread thread) { |
| ObservableConversationBlip parentBlip = thread.getParentBlip(); |
| BlipView blipView = views.getBlipView(parentBlip); |
| |
| if (blipView != null) { |
| ConversationThread next = findBefore(thread, parentBlip.getReplyThreads()); |
| replyHandler.presentAfter(blipView, next, thread); |
| } else { |
| throw new IllegalStateException("blipView not present"); |
| } |
| } |
| |
| @Override |
| public void onInlineThreadAdded(ObservableConversationThread thread, int location) { |
| // inline threads are ignored for now. |
| } |
| |
| @Override |
| public void onThreadDeleted(ObservableConversationThread thread) { |
| InlineThreadView threadView = views.getInlineThreadView(thread); |
| if (threadView != null) { |
| threadView.remove(); |
| } |
| } |
| |
| @Override |
| public void onBlipAdded(ObservableConversationBlip blip) { |
| ConversationThread parentThread = blip.getThread(); |
| ThreadView threadView = viewOf(parentThread); |
| if (threadView != null) { |
| ConversationBlip ref = findBefore(blip, parentThread.getBlips()); |
| BlipView refView = viewOf(ref); |
| |
| // Render the new blip. |
| threadView.insertBlipAfter(refView, blip); |
| bubbleBlipCountUpdate(blip); |
| } else { |
| throw new IllegalStateException("threadView not present"); |
| } |
| } |
| |
| @Override |
| public void onBlipDeleted(ObservableConversationBlip blip) { |
| BlipView blipView = views.getBlipView(blip); |
| if (blipView != null) { |
| // TODO(user): Hide parent thread if it becomes empty. |
| blipView.remove(); |
| } |
| for (ParticipantId contributor : blip.getContributorIds()) { |
| profileRenderer.unmonitorContribution(blip, contributor); |
| } |
| bubbleBlipCountUpdate(blip); |
| } |
| |
| private void bubbleBlipCountUpdate(ConversationBlip blip) { |
| ConversationThread thread = blip.getThread(); |
| ThreadView threadUi = viewOf(thread); |
| threadUi.setTotalBlipCount(readMonitor.getTotalCount(thread)); |
| ConversationBlip parentBlip = thread.getParentBlip(); |
| if (parentBlip != null) { |
| bubbleBlipCountUpdate(parentBlip); |
| } |
| } |
| |
| @Override |
| public void onBlipContributorAdded(ObservableConversationBlip blip, ParticipantId contributor) { |
| profileRenderer.monitorContribution(blip, contributor); |
| } |
| |
| @Override |
| public void onBlipContributorRemoved( |
| ObservableConversationBlip blip, ParticipantId contributor) { |
| profileRenderer.unmonitorContribution(blip, contributor); |
| } |
| |
| @Override |
| public void onBlipSumbitted(ObservableConversationBlip blip) { |
| } |
| |
| @Override |
| public void onBlipTimestampChanged( |
| ObservableConversationBlip blip, long oldTimestamp, long newTimestamp) { |
| BlipView blipUi = views.getBlipView(blip); |
| BlipMetaView metaUi = blipUi != null ? blipUi.getMeta() : null; |
| if (metaUi != null) { |
| blipRenderer.renderTime(blip, metaUi); |
| } |
| } |
| |
| @Override |
| public void pageIn(ConversationBlip blip) { |
| // listen to the contributors on the blip |
| for (ParticipantId contributor : blip.getContributorIds()) { |
| profileRenderer.monitorContribution(blip, contributor); |
| } |
| } |
| |
| @Override |
| public void pageOut(ConversationBlip blip) { |
| for (ParticipantId contributor : blip.getContributorIds()) { |
| profileRenderer.unmonitorContribution(blip, contributor); |
| } |
| } |
| |
| @Override |
| public void onAnchorChanged(Anchor oldAnchor, Anchor newAnchor) { |
| // Since anchors are application-level immutable, this is a rare case, so |
| // the gain in simplicity of implementing it as removal then addition |
| // outweighs the efficiency gain from implementing a |
| // conversation-view-move mechanism. |
| if (oldAnchor != null) { |
| // Remove old view. |
| ConversationView oldUi = viewOf(conversation); |
| if (oldUi != null) { |
| oldUi.remove(); |
| } |
| } |
| if (newAnchor != null) { |
| // Insert new view. |
| BlipView containerUi = viewOf(newAnchor.getBlip()); |
| if (containerUi != null) { |
| ConversationView convUi = containerUi.insertConversationBefore(null, conversation); |
| } |
| } |
| } |
| |
| /** |
| * Finds the predecessor of an item in an iterable. This method runs in |
| * linear time. |
| */ |
| private <T> T findBefore(T o, Iterable<? extends T> xs) { |
| T last = null; |
| for (T x : xs) { |
| if (x.equals(o)) { |
| return last; |
| } |
| last = x; |
| } |
| throw new IllegalArgumentException("Item " + o + " not found in " + xs); |
| } |
| } |
| |
| private final TimerService timer; |
| private final ObservableConversationView wave; |
| private final ModelAsViewProvider views; |
| private final ShallowBlipRenderer blipRenderer; |
| private final ReplyManager replyHandler; |
| private final ThreadReadStateMonitor readMonitor; |
| private final ProfileManager profiles; |
| private final LiveSupplementRenderer supplementRenderer; |
| private final IdentityMap<Conversation, LiveConversationRenderer> conversationRenderers = |
| CollectionUtils.createIdentityMap(); |
| |
| LiveConversationViewRenderer(TimerService timer, ObservableConversationView wave, |
| ModelAsViewProvider views, ShallowBlipRenderer blipRenderer, ReplyManager replyHandler, |
| ThreadReadStateMonitor readMonitor, ProfileManager profiles, |
| LiveSupplementRenderer supplementRenderer) { |
| this.timer = timer; |
| this.wave = wave; |
| this.views = views; |
| this.blipRenderer = blipRenderer; |
| this.replyHandler = replyHandler; |
| this.readMonitor = readMonitor; |
| this.profiles = profiles; |
| this.supplementRenderer = supplementRenderer; |
| } |
| |
| /** |
| * Creates a live renderer for a wave. The renderer will start incremental |
| * updates of an existing rendering once it is {@link #init initialized}. |
| */ |
| public static LiveConversationViewRenderer create(TimerService timer, |
| ObservableConversationView wave, ModelAsViewProvider views, ShallowBlipRenderer blipRenderer, |
| ReplyManager replyHandler, ThreadReadStateMonitor readMonitor, ProfileManager profiles, |
| ObservableSupplementedWave supplement) { |
| LiveSupplementRenderer supplementRenderer = |
| LiveSupplementRenderer.create(supplement, views, readMonitor); |
| return new LiveConversationViewRenderer( |
| timer, wave, views, blipRenderer, replyHandler, readMonitor, profiles, supplementRenderer); |
| } |
| |
| /** |
| * Observes the conversations to which this renderer is bound, updating their |
| * renderings as the conversation changes. |
| */ |
| public void init() { |
| supplementRenderer.init(); |
| for (ObservableConversation conv : wave.getConversations()) { |
| observe(conv); |
| } |
| |
| wave.addListener(this); |
| } |
| |
| /** |
| * Destroys this renderer, releasing its resources. It is no longer usable |
| * after a call to this method. |
| */ |
| public void destroy() { |
| wave.removeListener(this); |
| conversationRenderers.each(new ProcV<Conversation, LiveConversationRenderer>() { |
| @Override |
| public void apply(Conversation c, LiveConversationRenderer value) { |
| value.destroy(); |
| } |
| }); |
| supplementRenderer.destroy(); |
| } |
| |
| /** |
| * Observes a conversation, updating its view as it changes. |
| * |
| * @param conversation conversation to observe |
| */ |
| private void observe(ObservableConversation conversation) { |
| LiveProfileRenderer profileRenderer = |
| LiveProfileRenderer.create(timer, profiles, views, blipRenderer); |
| LiveConversationRenderer renderer = new LiveConversationRenderer(conversation, profileRenderer); |
| renderer.init(); |
| conversationRenderers.put(conversation, renderer); |
| } |
| |
| /** |
| * Stops observing a conversation, releasing any resources that were used to |
| * observe it. |
| * |
| * @param conversation conversation to stop observing |
| */ |
| private void unobserve(ObservableConversation conversation) { |
| LiveConversationRenderer renderer = conversationRenderers.get(conversation); |
| if (renderer != null) { |
| conversationRenderers.remove(conversation); |
| renderer.destroy(); |
| } |
| } |
| |
| private ThreadView viewOf(ConversationThread thread) { |
| return thread == null ? null // \u2620 |
| : (thread.getConversation().getRootThread() == thread) // \u2620 |
| ? views.getRootThreadView(thread) // \u2620 |
| : views.getInlineThreadView(thread); |
| } |
| |
| private BlipView viewOf(ConversationBlip ref) { |
| return ref == null ? null : views.getBlipView(ref); |
| } |
| |
| private ConversationView viewOf(Conversation ref) { |
| return ref == null ? null : views.getConversationView(ref); |
| } |
| |
| @Override |
| public void pageIn(ConversationBlip blip) { |
| LiveConversationRenderer renderer = conversationRenderers.get(blip.getConversation()); |
| Preconditions.checkState(renderer != null); |
| renderer.pageIn(blip); |
| } |
| |
| @Override |
| public void pageOut(ConversationBlip blip) { |
| LiveConversationRenderer renderer = conversationRenderers.get(blip.getConversation()); |
| Preconditions.checkState(renderer != null); |
| renderer.pageOut(blip); |
| } |
| |
| // |
| // Note: the live maintenance of nested conversations is not completely |
| // correct, because the conversation model does not broadcast correct and |
| // consistent events. The rendering is only as correct as the model events, |
| // and it is not considered to be worthwhile for the rendering to generate the |
| // correct events manually rather than wait for the model events to be fixed. |
| // |
| // Additionally, the conversation model does not expose the conversations |
| // anchored at a particular blip, which makes a stable sibling ordering of |
| // conversations infeasible. |
| // |
| |
| @Override |
| public void onConversationAdded(ObservableConversation conversation) { |
| BlipView container = viewOf(conversation.getAnchor().getBlip()); |
| if (container != null) { |
| ConversationView conversationUi = container.insertConversationBefore(null, conversation); |
| } |
| |
| observe(conversation); |
| } |
| |
| @Override |
| public void onConversationRemoved(ObservableConversation conversation) { |
| unobserve(conversation); |
| |
| ConversationView convUi = viewOf(conversation); |
| if (convUi != null) { |
| convUi.remove(); |
| } |
| } |
| } |