Use the built-in collaboration feature

PureWeb's collaboration feature makes it possible for several users, in diverse locations and running different clients, to interact simultaneously with the same service session. This is a popular feature which opens up new opportunities for peer consultation, tele-maintenance, and off-site training.

To take advantage of the built-in collaboration functionality, you use a client-side function to request a share URL. The generated URL can then be provided to end users who want to join the session.

Note that the built-in feature provides shallow collaboration: there is no difference, from a functionality perspective, between the host and the participant. You can extend this functionality if your application-specific requirements call for tighter controls.

If it is important that some sensitive data be only available to the host and shielded from participants, consider storing that data in a private data store; see Session storage.

How-to

Generate a share URL

APIs:

WebClient.getSessionShareUrlAsync() | HTML5 | Android
PWWebClient.getSessionShareUrlAsyncWithPassword() | iOS

To generate a share URL, you simply call getSessionShareUrlAsync from the client. The service will respond by providing the requested URL.

The method has three key parameters (the descriptor and criteria parameters are not used and can be safely ignored):

  • password: The password that the participants will need to enter when using the share URL to join a session.
  • timeout: The length of time that the share URL will remain valid for new collaborators to join. This is optional, if not provided, the URL will remain valid indefinitely, unless you explicitly call invalidateShareUrlAsync.
  • callback: The function that is called when the service returns the share URL; this is typically used to display the URL to the end user.

HTML5

webClient.getSessionShareUrlAsync('SomePassword', '', 1800000, '', function(myShareUrlCallback, exception) {
    // Do something useful on receiving URL
};

iOS (Obj-C)

[[PWFramework sharedInstance].client getSessionShareUrlAsyncWithPassword:@"SomePassword"
	    shareDescriptor:@""
		shareTimeout:1800000
                    
	completion:^(NSURL *myShareUrlCallback, NSError *error) {
        // Do something useful on receiving URL
    }];

Android

framework.getWebClient().getSessionShareUrlAsync("SomePassword", "", 1800000, "", new myShareUrlCallback());
...
protected class onReceivingShareURL implements GetSessionShareUrlCallback {
    // Do something useful on receiving URL
};

The end user is responsible for communicating the share URL to participants who want to join the session, using whatever mechanism is most appropriate such as email or chat.

The format of this URL is very similar to that of a non-collaboration client (see About application URLs), but the scheme contains the word "share" instead of "app" or "view":

[protocol]://[host:port]/pureweb/share?...

When testing collaboration on your development machine, do not launch the share URL in another tab of the same browser window. Instead, launch the URL in another browser, or a separate browser instance, or an incognito window. This is because browser tabs share the same cookies and this interferes with how PureWeb manages sessions.

Also, do not access the PureWeb server using localhost as a shortcut when generating share URLs, as this does not work when collaborating with users outside the network. Rather, use the server's full IP address or host name.


Join a collaboration session

All that's needed to join a collaboration session is to navigate to the share URL and know the password. The PureWeb server manages everything else under the hood.

When an invited participant enters the share URL in the browser, this redirects to the PureWeb server's Collaboration Login page, which displays a prompt for the session's password. Assuming the password is valid, when the user clicks the Sign In button, the server launches a new instance of the client application (using the same connect method call that is used when launching the client in non-collaboration mode).

Although rarely needed, it is possible to change this workflow, for example to bypass the collaboration login page, or to restrict access to authenticated users. See Manage collaboration access.

Each client application that joins a collaboration session runs its own thread. The maximum number of participants that can join a given service session depends on the GPU capacity of the server node on which the service is running.

Joining secure sessions from mobile clients

When a collaboration participant using a mobile app logs into a collaboration session from the PureWeb server, in the background the share URL is redirected to a location in the format <app-name>://<url>, which no longer includes the http(s) scheme. Because of this redirect, a different mechanism must be used to indicate whether the connection should be to a secure URL.

The way to handle this is to add a configuration setting in your client, which end users can toggle to enable or disable secure connections as needed.

For example, the Scribble and Asteroids sample applications both have a Secure Collaborator toggle in the client app settings; when this toggle is on, the client connects as https; when the toggle is off, it connects as http.

Below is the relevant code from the sample applications:

iOS (Obj-C)

There is a boolean value that gets set based on whether the Secure Collaboration toggle in the settings is on or off:

BOOL secureScheme = [[NSUserDefaults standardUserDefaults] boolForKey:@"pureweb_collab_secure"];

The URLByReplacingScheme function replaces the scheme with http or https based on this boolean value, then assigns the edited string to the appURL variable (which is used to establish the connection) :

appURL = [appURL URLByReplacingScheme:secureScheme];

Android

There is a boolean value that gets set based on whether the Secure Collaboration toggle in the settings is on or off:

boolean useSecureConnection = preferences.getBoolean(PUREWEB_SECURE_COLLABORATION, PUREWEB_SECURE_DEFAULT);					

The connectUrl string then gets edited based on this value:

String connectUrl = rawUriStr.replace(scheme, useSecureConnection ? "https" : "http");

Example

Collaborating in Scribble

The snippets below illustrates how basic collaboration is enabled in the sample Scribble client, from creating the user interface element that triggers the request, to the callback that displays this URL to end users.

HTML5

In order to keep the example below short and agnostic of any third-party library, the dialog box that presents the share URL is using the prompt() synchronous JavaScript call, but this is not best practice since getSessionShareUrlAsync is asynchronous. Best practice would be to use a modal dialog.

The Scribble sample application's interface has a Share button which, when pressed, calls the generateShareURL function:

<button id="share" onclick="generateShareURL();">Share</button>

The generateShareURL function uses the getSessionShareUrlAsync method to get the share URL. The callback to getSessionShareUrlAsync is defined to run the getUrl function, which displays the share URL in a prompt window :

//Asynchronously create or revoke a share URL.
function generateShareUrl(){
    //Grab a local ref to the webclient (save some typing)
	var webClient = pureweb.getFramework().getClient();

    //If we don't have a share URL...
    if ((shareUrl === undefined) || (shareUrl === null)) {
        //Stop listening for disconnection events (as we expect the user to background the browser for emailing the collab url)	
        setDisconnectOnUnload(false);
        //Generate a share URL (on the service)
        webClient.getSessionShareUrlAsync('Scientific', '', 1800000, '', function(getUrl, exception) {
            //Call back for share URL generation:
            //If we got a valid Share URL
            if ((getUrl !== null) && (getUrl !== undefined)) {
                //Set it locally
                shareUrl = getUrl;
                
				if (window.prompt("Here is your collaboration URL:",getUrl)){
                    //Reattach the listeners for disconnection events
                    setDisconnectOnUnload(true);
                }

            } else {
                alert('An error occurred creating the share URL: ' + exception.description);
            }
        });
   } else {
        //If a share URL already exists, we just want to invalidate it
        webClient.invalidateSessionShareUrlAsync(shareUrl, function(exception) {
            if ((exception !== undefined) && (exception !== null)){
                alert('An error occurred invalidating the share URL: ' + exception);
            } else {
                shareUrl = null;
            }
        });
    }
}

iOS (Obj-C)

The Scribble sample application's interface has a Share button which, when pressed, calls the shareButtonPushed function:

UISegmentedControl *seg2 = [[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObjects:@"Share",nil]];
[seg2 addTarget:self action:@selector(shareButtonPushed:) forControlEvents:UIControlEventValueChanged];
seg2.frame = CGRectMake(95, 7, 50, 30);
seg2.segmentedControlStyle = UISegmentedControlStyleBar;
seg2.momentary = YES;
seg2.tintColor = [UIColor darkGrayColor];
[container addSubview:seg2];
self.shareButton = seg2;				

The shareButtonPushed function uses getSessionShareUrlAsync to get the share URL. The callback to getSessionShareUrlAsync is defined to run the shareUrl function, which calls presentMailComposerWithShareURL and also provides some error handling.

- (IBAction)shareButtonPressed:(UIBarButtonItem *)sender {

    //request a share url from the server
	[[PWFramework sharedInstance].client getSessionShareUrlAsyncWithPassword:@"Scientific"
	    shareDescriptor:@""
		shareTimeout:1800000
                    
	completion:^(NSURL *shareURL, NSError *error) {

		if (error) {
			PWLogError(@"share url created failed with error %@", error);
			return;
		}
	[self presentMailComposerWithShareURL:shareURL];
	}];
}					

The presentMailComposerWithShareURL function creates an email that contains the share URL.

- (void) presentMailComposerWithShareURL:(NSURL *) shareURL {

    MFMailComposeViewController *mailController = [MFMailComposeViewController new];
    
    mailController.mailComposeDelegate = self;
    [mailController setSubject:@"Join My Shared PureWeb Session."];
    [mailController setMessageBody:[shareURL absoluteString] isHTML:NO];

    mailController.modalPresentationStyle = UIModalPresentationFormSheet;

    [self presentViewController:mailController animated:YES completion:nil];
}
		

Android

The Scribble sample application's interface has a Share menu option which, when selected, calls the getSessionShareUrlAsync method. The callback to getSessionShareUrlAsync is defined to run the ShareRequestCompleted function:

@Override
public boolean onPrepareOptionsMenu(Menu menu) {
    ...
    menu.findItem(R.id.share).setEnabled(connected);
    return true;
}

public boolean onOptionsItemSelected(MenuItem item) {
	switch (item.getItemId()) {

	    ...
		
		case R.id.share:
		    framework.getWebClient().getSessionShareUrlAsync("Scientific", "", 1800000, "", new ShareRequestCompleted());
		    return true;
	    ...

	}
}

The ShareRequestCompleted callback creates an email form that contains the share URL, and also provides the logic for handling exceptions:

protected class ShareRequestCompleted implements GetSessionShareUrlCallback {
    public ShareRequestCompleted() {}

    public void invoke(String shareUrl, final Throwable exception) {
        if (shareUrl != null){
             Intent emailIntent = new Intent(Intent.ACTION_SEND);
             String[] recipients = new String[]{"recipient@your-company.com"};
             emailIntent.putExtra(Intent.EXTRA_EMAIL, recipients);
             String appName = getResources().getString(R.string.app_name);
             emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Please join my " + appName + " session");
             emailIntent.putExtra(Intent.EXTRA_TEXT, shareUrl);
             emailIntent.setType("text/plain");
             startActivity(Intent.createChooser(emailIntent, "Send mail..."));
        } else{
             UiDispatcherUtil.beginInvoke(new Runnable(){
                 public void run() {
                     AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(PureWebActivity.this);
                     errorDialogBuilder.setIcon(pureweb.samples.R.drawable.ic_pureweb);
                    
                     errorDialogBuilder.setTitle("An error occurred creating the Share URL");
                     errorDialogBuilder.setMessage("Unexpected exception: " + exception.getMessage());
                     errorDialogBuilder.setCancelable(false);
                     errorDialogBuilder.setPositiveButton("OK", new DialogInterface.OnClickListener(){
                         public void onClick(DialogInterface dialog, int id){
                             dialog.dismiss();
                         }
                     });
                 }
             });
        }
    }
}