Uploaded image for project: 'Wave'
  1. Wave
  2. WAVE-169

Implement private reply feature

Add voteWatch issue


    • Bug
    • Status: Open
    • Minor
    • Resolution: Unresolved
    • None
    • None
    • None


      This is a starter project to add the private-reply feature from Google Wave.

      First, ensure that you are comfortable working in the Wave-In-A-Box development environment (see http://www.waveprotocol.org/code), and consider doing the sample tutorial.

      Most of the code to handle existing private replies is already present in Wave-In-A-Box. However, there is no UI action for creating them, so that code is not currently exercised. This project adds a logical UI action for creating a private reply, in order to exercise that code path. A separate project will add the physical UI controls for handling the appropriate gestures (drop-down menus etc.).

      1) Piggy-back off the normal reply action.

      All the edit-related UI actions are implemented in client/wavepanel/impl/edit/Actions.java. First, hijack the existing reply action, and change it to insert a whole conversation object (a private reply) rather than just a regular reply thread.

      The conversation model interprets a wave as a collection of Conversations. Some of these Conversations are anchored at blips of other Conversations. There is no conversation-model API for creating private replies directly. Instead, a private reply is formed by creating a new Conversation in the collection, and then setting its anchor to point at the appropriate blip. The collection of conversations in a wave is exposed through the type model.conversation.ConversationView (the word 'view' is unfortunately overloaded: in the model code, it is a legacy name that just means a collection; in the client code, it means a UI object).

      Replace the reply code in Actions.reply(BlipView) with the private-reply version:

      • ConversationBlip reply =
      • blip.appendInlineReplyThread(blip.getContent().size() - 1).appendBlip()
        + Conversation conv = wave.createConversation();
        + ConversationBlip reply = conv.getRootThread().appendBlip();
        + conv.setAnchor(blip.getConversation().createAnchor(blip));

      This code adds a dependency on the conversation collection ('wave').

      2) Inject the dependency.

      Undercurrent follows a manual dependency-injection (DI) style. In short, this just means that constructors are not public, and generally just set fields to parameters; no work, and no method calls or calls to other constructors. Any logic required for construction is put in static factory methods. This makes it easy to instantiate objects with mocks for testing.

      Threading through the dependency on the whole conversation model requires the following additions (Eclipse can do many of these for you automatically).

      • Add a 'private final ConversationView wave;' field to Actions;
      • Add it to the Actions constructor, following the DI pattern;
      • Add a 'ConverstionView wave' parameter to EditActionsBuilder.createAndInstall
      • Update StageThree.DefaultProvider.createEditActions() with the local variable:

      ModelAsViewProvider views = stageTwo.getModelAsViewProvider();
      + ConversationView wave = stageTwo.getConversations();
      BlipQueueRenderer blipQueue = stageTwo.getBlipQueue();

      • Actions actions =
        EditActionsBuilder.createAndInstall(panel, views, profiles, edit, blipQueue, focus);
        + Actions actions =
        EditActionsBuilder.createAndInstall(panel, views, wave, profiles, edit, blipQueue, focus);

      This threading touches on two parts of Undercurrent: the layout of wave panel features, and the staged loading process. First, Wave panel features are found in subpackages of wavepanel.impl (like edit, focus, reader, etc), and each such subpackage has a Builder for creating and installing that feature on demand. Second, related feature groups are bundled into 'stages' (StageZero, StageOne, ...). Editing features are bundled into StageThree. Each stage has a provider class for creating and configuring each component of that stage. Applications can override parts of these providers in order to customize or replace the various components in according to their specific needs. The StageThree.DefaultProvider.createEditActions() method provides the default implementation and configuration of the wave-panel editing actions.

      3) Test the feature in the wave panel harness

      Start up the wave panel test harness, to try out this feature:
      $ ant waveharness_hosted
      then visit the URL produced by the GWT code server (this is straight from the tutorial). Once the code has been loaded and the page is ready, click reply on a blip. Notice nothing happens in the UI, but there is an exception reported at the bottom of the GWT code server log (in the tab that started up for your browser session). Unfortunately, there is not a clear indication that something went wrong - you just have to get used to checking the GWT code server tabs periodically for exceptions while testing.

      The exception is an NPE in LiveConversationViewRenderer.onConversationAdded().

      4. Fix the renderer bug.

      In Undercurrent, live rendering is performed by renderers that observe models. The main renderer for a wave is the conversation renderer. It has event callbacks for conversation events, and these are invoked as the conversation model(s) change.

      The code is:

      public void onConversationAdded(ObservableConversation conversation) {
      BlipView container = viewOf(conversation.getAnchor().getBlip());
      if (container != null)

      { ... }


      The bug is that this code assumes that any conversations that show up dynamically have anchors. This is generally true, since the main way that conversations show up dynamically is because of private-reply addition, and private replies have anchors. However, since the Wave platform has no transaction facility, and most event APIs in Wave are synchronous, event callbacks can often fire at inconvenient times (i.e., during the middle of a compound action, revealing the model in a transient intermediate state). Recall that the code to add the private reply created a new conversation first, then set its anchor. This causes this onConversationAdded event to fire before the anchor is set.

      A simple null check fixes this. Note that the LiveConversationViewRenderer ensures that all conversations in view are being observed, and their events are handled by a ConversationUpdater. The anchor being set after the conversation has been added is already handled by ConversationUpdater.onAnchorChanged().

      • BlipView container = viewOf(conversation.getAnchor().getBlip());
      • if (container != null) { - ConversationView conversationUi = container.insertConversationBefore(null, conversation); - }

        + Anchor location = conversation.getAnchor();
        + if (location != null)

        Unknown macro: {+ BlipView container = viewOf(location.getBlip());+ if (container != null) { + ... + }+ }


      After saving this change, refresh the browser (you do not need to restart the GWT code server), and try to reply to a blip again.

      5. Fix the other renderer bug.

      Again, nothing happens in the UI, but there is an exception in the OOPHM log: an ISE in LiveConversationViewRenderer$ConversationUpdater.onBlipAdded(). Again, this is a transient intermediate model state issue. Since the new private reply won't be rendered until it is anchored somewhere to the conversation structure, there is no thread view for the new conversation's root thread. The onBlipAdded event is firing due to the code in Actions.reply() adding a root blip to the new conversation, and it is trying to render that new blip and attach the rendering to the rendering of its containing thread. Since it can not find that rendering (because it does not exist yet), it throws an ISE.

      Since it is generally accepted that not all parts of the model are necessarily rendered at all times, throwing an ISE in that situation does not make sense. Just delete the throw statement (and similarly in onThreadAdded() in the same class.

      Save the changes and refresh the browser. Observe that creating private replies now works correctly in the harness.

      6. Test concurrent behaviour in the full client.

      The feature is now working in the isolated environment of the wave harness. The next step is to try it out in the full web client. Build and run the Wave In A Box server, then launch the GWT code server for the full client:
      $ ant hosted_gwt
      In two browser windows, visit the URL:
      (the URL is not produced for you anymore, like in the harness, because of how hostnames are set up for the Wave In a Box server). If you get redirected around because of needing to login, ensure that once you've returned to the main page, that the ?gwt.codesvr=... URL parameter is put back.

      Create a wave in one window, and open it in the other window. Then create a private reply in one window, and observe it show up live in the other window.

      Congratulations! You've added the code to create private replies.
      Now that you've verified that the code works, restore the original implementation of Actions.reply(BlipView) to a regular reply, but add a method Actions.replyPrivately(BlipView) to hold the private-reply creation code. Later, appropriate UI controls can be added to call that action.

      Issue imported from http://code.google.com/p/wave-protocol/issues/detail?id=168

      Owner: hearn...@google.com
      Label: Type-Defect
      Label: Priority-Medium
      Label: StarterProject
      Stars: 2
      State: open
      Status: Accepted




            Unassigned Unassigned
            Anonymous Anonymous




                Issue deployment