Skip navigation

Developer

5 Posts authored by: orson.bushnell

Compared to my previous post this will be a short tutorial. The issue here is editing content. When you load a content object the text is formatted for display, with tappable areas containing extra information. Unfortunately this extra information is problematic when submitting edited content. So now we are adding methods to reload the content with the text formatted for editing. These methods are also used to lock the content so multiple people can't edit at the same time.

 

Before we look at the new methods, lets look at the changes to the content classes.

In JiveContentBody there is a new property: editableValue. This shows which type of formatting the text is using. YES for simple html tags suitable for editing and NO for the more complex html tags suitable for interactive display.

In JiveDocument there is a new property: editingBy. This shows who currently has the document locked for editing, if anyone. This is intended to be used as a quick check of lock ownership before you try to use any of the new apis. But this is time sensitive information. If you display a lock symbol on the content be sure to periodically update the content to maintain the validity of the lock display.

 

So what are the new methods?

In JivePlatformVersion there is a new version check method:

- (BOOL)supportsContentEditingAPI;

 

In Jive there are 4 new methods:

- (void) getEditableContent:(JiveContent *)content withOptions:(JiveReturnFieldsRequestOptions *)options onComplete:(JiveContentCompleteBlock)complete onError:(JiveErrorBlock)error;

- (void) lockContentForEditing:(JiveContent *)content withOptions:(JiveReturnFieldsRequestOptions *)options onComplete:(JiveContentCompleteBlock)complete onError:(JiveErrorBlock)error;

- (void) saveContentWhileEditing:(JiveContent *)content withOptions:(JiveMinorCommentRequestOptions *)options onComplete:(JiveContentCompleteBlock)complete onError:(JiveErrorBlock)error;

- (void) unlockContent:(JiveContent *)content onComplete:(JiveCompletedBlock)complete onError:(JiveErrorBlock)error;


The getEditableContent method gets the editable text without setting the lock.

The lockContentForEditing method returns the editable text and sets the lock. This method is reentrant, allowing you to reset the lock on the server. Be sure to do this periodically as the server does have a timeout.

The saveContentWhileEditing method publishes the content while continuing to edit. When used with the draft flag this can be used to implement an autosave feature.

The unlockContent method releases the lock, effectively canceling the edit session.

 

All of these methods will return an error if someone else is editing the content. But they will not prevent the same user from editing the content using 2 different programs. In this case the final edit will "win".

 

But wait, these methods either cancel the edit or continue editing. How do I publish my changes?

Using the existing updateContent methods. They have been updated to release the lock, if present, as part of the publish process.

 

Currently only JiveDocument supports locking, so why should I use the locking methods for other content types?

For forward compatibility. Just because document is the only content type that currently supports locking doesn't mean we won't add it to other types in the future.

 

So what is the flow for editing?

Editing API flow.gif

 

You say the display html has extra stuff in it compared to the editing html. Can you give me an example?

Display HTML

Editable HTML

<body><!-- [DocumentBodyStart:c4282aa4-ff2d-4941-9e7a-50618c508ce3] --><div class="jive-rendered-content"><p>just some text</p></div><!-- [DocumentBodyEnd:c4282aa4-ff2d-4941-9e7a-50618c508ce3] --></body><body><p>just some text</p></body>

If it is that different for a single line of text, imagine what it looks like with links and images.

In my last post I talked about the new direction for the SDK. While I am still trying to get that done, BUGS have been filed and changes have been made. So today I am going to talk about one of the more involved changes that will affect how the SDK is used.

 

For most Jive instances everything works as before. But there are 2 situations where things need to change.

 

The first situation is url redirection. There are cases where you need to manually update the URL used to connect to the instance. This is most commonly found in internal networks where IT has provided a short name that resolves to the full URL. Both are accessible but only the full URL should be used in @mentions and other links.

As part of the instance validation the server sends the URL that should be used. If this URL is different than the entered URL you need to test to see if it can be reached.

 

Which leads to the second situation. In this case the server wants you to use a different URL but it is not reachable. In this case the URLs returned by the server should be updated to match the URL entered. This scenario is handled by the SDK once you determine that the redirect cannot be done. This most commonly happens with large instances that need a load balancer. The servers can be configured to include an instance identifier. For the average user this identifier needs to be removed, and the SDK handles this.

 

When I created the Example project I skipped the version check because I hard coded the instance URL. To demonstrate what is needed now I have added a view for the user to enter the instance URL. I then validate it and transition to the original login view.

Here is the validation code:

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    // Assume the default instance.
    NSString *instanceString = @"https://community.jivesoftware.com/";
    // Check to see if the user entered a different url.
    if (self.communityURL.text.length > 0) {
        instanceString = self.communityURL.text;
        if (![instanceString hasPrefix:@"http"]) {
            instanceString = [@"http://" stringByAppendingString:instanceString];
            // But what if it should be https:// ?
        }
        // The SDK assumes the URL has a / at the end. So make sure it does.
        if (![instanceString hasSuffix:@"/"]) {
            instanceString = [instanceString stringByAppendingString:@"/"];
        }
    }

Here I do simple formatting of the entered value. Make sure it has the correct prefix and suffix. You should validate both http and https prefixes, preferring the https version if they both work.

    NSURL *instanceURL = [NSURL URLWithString:instanceString];
    __block JVJiveFactory *factory = nil;
    [self.activityIndicator startAnimating];
    [self.communityURL resignFirstResponder];
    self.communityURL.enabled = NO;
    // Is it a valid instance?
    factory = [[JVJiveFactory alloc] initWithInstanceURL:instanceURL
                                                complete:^(JivePlatformVersion *version) {
                                                    [self checkForRedirect:version
                                                                   fromURL:instanceURL
                                                                   factory:factory];
                                                } error:^(NSError *error) {
                                                    [self.activityIndicator stopAnimating];
                                                    self.communityURL.enabled = YES;
                                                    [self.communityURL becomeFirstResponder];
                                                }];  
    return NO;
}

Then I create the jive factory class with blocks to handle the version check results. The simple interface elements are to disable user interaction while performing the check. If the check fails, by returning an error, I reenable interaction. You may want to do more to better inform the user of what happened. Alternatively you could dynamically update the GO button on the keyboard as the user types. If the check succeeds we proceed to the first redirect check.

- (id)initWithInstanceURL:(NSURL *)instanceURL
                 complete:(JivePlatformVersionBlock)completeBlock
                    error:(JiveErrorBlock)errorBlock {
    self = [super init];
    if (self) {
        self.jive = [[Jive alloc] initWithJiveInstance:instanceURL
                                 authorizationDelegate:self];
        [self.jive versionForInstance:instanceURL
                           onComplete:completeBlock
                              onError:errorBlock];
    }

    return self;
}

As part of creating the jive factory class I create a Jive object and start the version check.

 

- (void)checkForRedirect:(JivePlatformVersion *)version
                 fromURL:(NSURL *)targetURL
                 factory:(JVJiveFactory *)initialFactory {
    // Not all instances report their url in the version.
    if (!version.instanceURL || [version.instanceURL isEqual:targetURL]) {
        [self advanceToLogin:initialFactory];
    } else {
        // Attempt to redirect to the server's instance url.
        __block JVJiveFactory *factory = nil;
        factory = [[JVJiveFactory alloc] initWithInstanceURL:version.instanceURL
                                                    complete:^(JivePlatformVersion *redirectVersion) {
                                                        // Direct access granted.
                                                        self.communityURL.text = redirectVersion.instanceURL.absoluteString;
                                                        [self advanceToLogin:factory];
                                                    }
                                                       error:^(NSError *error) {
                                                           // The server lied, bad server. Use the original url.
                                                           [self advanceToLogin:initialFactory];
                                                       }];
    }
}

There are 4 possibilities here: the server reported the same url that the user entered, the server didn't report a url, the server reported a different url that should be used instead of the user entered url, and finally the server requires the SDK to rewrite the urls.

In the first 2 cases we just go to the login screen normally. For the last 2 cases we need to check the server reported URL to distinguish them, by creating a second JVJiveFactory with the server reported URL. If this second check succeeds we use the server reported URL as if the user had entered it instead. Otherwise we continue with the user entered URL.

 

- (void)advanceToLogin:(JVJiveFactory *)factory {
    [self.activityIndicator stopAnimating];
    [JVJiveFactory setInstance:factory];
    [self performSegueWithIdentifier:@"Community" sender:self];
}

Here we update the UI and set the jive factory instance to the validated factory object.

 

The second redirect check happens after the user enters his credentials and tries to login. You need to change the completeBlock passed to the Jive me:onError: method.

In the example this is in the JVJiveFactory loginWithName:password:complete:error: method.

    [self.jive me:^(JivePerson *me) {
        JivePlatformVersion *platformVersion = self.jive.platformVersion;
    
        // url check.
        if (platformVersion.instanceURL) {
            // It's all good.
            if (completeBlock) {
                completeBlock(me);
            }
        } else {
            // NO!!! We have to make sure we have the right URL.
            [self doubleCheckInstanceURLForMe:me onComplete:completeBlock];
        }
    }

The new completeBlock checks to see if the version already includes the instance URL. If not it does another call to the server to get the instance URL.

 

- (void)doubleCheckInstanceURLForMe:(JivePerson *)me
                         onComplete:(JivePersonCompleteBlock)completeBlock {
    [self.jive propertyWithName:JivePropertyNames.instanceURL
                     onComplete:^(JiveProperty *property) {
                         NSString *instanceString = property.valueAsString;
                     
                         // The SDK assumes the URL has a / at the end. So make sure it does.
                         if (![instanceString hasSuffix:@"/"]) {
                             instanceString = [instanceString stringByAppendingString:@"/"];
                         }
                     
                         NSURL *instanceURL = [NSURL URLWithString:instanceString];
                     
                         // Yes! We have a server url.
                         if ([instanceString isEqualToString:self.jive.jiveInstanceURL.absoluteString]) {
                             // Everything matches up.
                             if (completeBlock) {
                                 completeBlock(me);
                             }
                         } else {
                             [self loginWithNewInstanceURL:instanceURL me:me completeBlock:completeBlock];
                         }
                     } onError:^(NSError *error) {
                         // No! We are stuck with what works.
                         if (completeBlock) {
                             completeBlock(me);
                         }
                     }];
}

If there is an error or the server URL matches the entered URL we continue accept the current login. If not then we try logging in again with the new URL.

 

- (void)loginWithNewInstanceURL:(NSURL *)instanceURL me:(JivePerson *)me completeBlock:(JivePersonCompleteBlock)completeBlock
{
    // Make sure we have the correct me object.
    Jive *oldJive = self.jive;

    self.jive = [[Jive alloc] initWithJiveInstance:instanceURL
                             authorizationDelegate:self];
    [self.jive me:completeBlock
          onError:^(NSError *error) {
              // Fall back to what works.
              self.jive = oldJive;
              if (completeBlock) {
                  completeBlock(me);
              }
          }];
}

We start by creating a new Jive instance and try to login with it, returning to the previous login attempt if this one fails.

 

That's a lot of code for something the SDK should just handle. Unfortunately because of the number of NSOperations involved and the different places they need to be, they can't simply be put into the SDK. Feel free to prove me wrong on this.

Since my last post the Jive iOS SDK has started to be redesigned. But this post is mostly going to be about the new Example project that is part of Jive iOS SDK on github.

 

To start with the example project is designed to only access 1 Jive instance, https://community.jivesoftware.com. Since I already know the URL is valid I skip the version check and go straight to the me call.

From JVJiveFactory.m:

- (void)loginWithName:(NSString *)userName
             password:(NSString *)password
             complete:(JivePersonCompleteBlock)completeBlock
                error:(JiveErrorBlock)errorBlock {
    JiveErrorBlock errorBlockCopy = [errorBlock copy];
   
    self.userName = userName;
    self.password = password;
    self.jive = [[Jive alloc] initWithJiveInstance:[NSURL URLWithString:@"https://community.jivesoftware.com"]
                             authorizationDelegate:self];
    self.credentials = nil;
    [self.jive me:completeBlock
          onError:^(NSError *error) {
              [self handleLoginError:error withErrorBlock:errorBlockCopy];
          }];
}

Here you see the creation of the Jive instance, using the JVJiveFactory as the authorizationDelegate, and the first call to that instance.

 

The next step is to retrieve the list of people being followed.

From JVMasterViewController.m:

- (void)viewDidLoad
{
    [super viewDidLoad];
    if (self.me) {
        _objects = [@[self.me] mutableCopy];
        [self.me followingWithOptions:nil
                           onComplete:^(NSArray *objects) {
                               [self addFollowers:objects];
                           } onError:nil];
    }
}

- (void)addFollowers:(NSArray *)objects {
    [_objects addObjectsFromArray:objects];
    [self.tableView reloadData];
}

I didn't pass in an error handler here. From one perspective it is not necessary as in this instance it can be safely ignored without changing the user experience. But I really didn't put one in as this is an example and it is left to you to implement proper error handling. You will see this repeated throughout the rest of the project.

 

Selecting a person from the list will take you to the blog posts screen. To fill that in we start with one of the newly redesigned methods.

From JVDetailViewController.m:

- (void)setDetailItem:(id)newDetailItem
{
    if (_detailItem != newDetailItem) {
        JivePerson *person = newDetailItem;
        _detailItem = newDetailItem;
        self.title = person.displayName;
        [person blogWithOptions:nil
                     onComplete:^(JiveBlog *blog) {
                         self.blog = blog;
                     } onError:nil];
    }
}

Following the standard storyboard transition pattern, we have a detail item that holds the JivePerson that was selected. From this we retrieve the JiveBlog object by calling the new blogWithOptions:onComplete:onError: method.

This demonstrates the structure that is being added to the SDK. Instead of always using the Jive instance, once you have a valid JiveObject you just call methods directly on that object.

As of this writing JivePerson is the only class that has been updated. But it will soon be joined by the rest of the SDK.

 

Speaking of a method that will soon be replaced. Now that we have loaded the JiveBlog we need to get the JivePosts in the blog.

From JVDetailViewController.m:

- (void)setBlog:(JiveBlog *)blog {
    NSOperation *operation = [[JVJiveFactory jiveInstance] contentsOperationWithURL:blog.contentsRef
                                                                         onComplete:^(NSArray *contents) {
                                                                             [self.activityIndicator stopAnimating];
                                                                             self.tableViewController.contents = contents;
                                                                         } onError:nil];
   
    _blog = blog;
    self.title = blog.name;
    [self.operationQueue addOperation:operation];
    [self.activityIndicator startAnimating];
}

Here we see the contentsOperationWithURL:onComplete:onError: method used to retrieve the JivePosts in the blog. This method will soon be replaced by a JiveBlog method.

In my previous post I talked about getting the JivePerson object for the user and loading the list of custom streams. In this post I will walk you through the Inbox calls needed to show and manipulate the user's inbox.

 

To start we need to specify the initial options. Lets start by loading the first 10 unread items:

JiveInboxOptions *unreadInboxOptions = [JiveInboxOptions new];
unreadInboxOptions.unread = YES;
unreadInboxOptions.count = 10;


 

Then we call the server:

[communityInstance inbox:unreadInboxOptions
              onComplete:^(NSArray *inboxEntries, NSDate *earliestDate, NSDate *latestDate)
  {[self showInboxEntries:inboxEntries from:earliestDate to:latestDate];}
                 onError:^(NSError *error) {[self handleInboxError:error];}];


The inboxEntries are JiveInboxEntry objects. They are sorted by date, but they include a thread id in the entry.jive.collection parameter that can be used to group the entries.

 

All JiveInboxEntry items include a summary of the full content suitable for display in a compact space.

A JiveInboxEntry can represent 3 types of content: general, acclaim, and notice.

A general item represents a link to viewable content through the contentURL parameter.

An acclaim item represents a grouping of JiveInboxEntry objects. The entryCollectionId parameter is the thread id of the related entries.

Finally, a notice item is just viewable content that does not link to anything else.

 

You can get the content from a general inbox entry item using this call:

[communityInstance contentFromURL:entryItem.contentURL
                       onComplete:^(JiveContent *content) {[self displayContent:content];}
                          onError:^(NSError *error) {[self handleContentFromURLError:error];}];


 

Remember to mark the item as read:

[communityInstance markInboxEntries:@[entryItem]
                             asRead:YES
                         onComplete:^{[self inboxEntriesMarkedAsRead];}
                            onError:^(NSError *error)
  {[self handleInboxEntriesMarkedAsReadError:error];}];


You can see that the entryItem is passed to the method in an NSArray. You can use this to implement a batch mark interface. This method can also be used to mark items as unread.

 

For consistency, you should use the unreadInboxOptions.after or unreadInboxOptions.before parameters to load additional entries. For example if loadedEarliestDate is the earliestDate from the above call:

unreadInboxOptions.before = loadedEarliestDate;
[communityInstance inbox:unreadInboxOptions
              onComplete:^(NSArray *inboxEntries, NSDate *earliestDate, NSDate *latestDate)
{[self showMoreInboxEntries:inboxEntries from:earliestDate to:latestDate];}
                 onError:^(NSError *error) {[self handleInboxError:error];}];


This would load the 10 items previous to the already loaded items.

This is the first in a series of posts on how to use the Jive iOS SDK to make your own native Jive community iOS app.

 

The Jive iOS SDK is designed as an Objective-C wrapper around the existing Jive REST api. The goal is to handle the process of communicating with the server while providing an object oriented interface to the app developer. You, as the developer, should then be able to focus your expensive talents on making amazing apps that the users of your Jive community will enjoy using.

 

The Jive iOS SDK can be used in 2 different ways: as a black box or as a source of operations that can be queued. In the following examples I will describe using the SDK from the black box perspective. Using it as a source of operations will follow the same structure but will require the use of NSOperationQueue.

 

Creating a Jive iOS app using the Jive iOS SDK begins with validating the server URL. This is trivially done using the getVersionForInstance method.

    NSURL *instanceURL = [NSURL URLWithString:@"http://community.jivesoftware.com/"];
    [Jive getVersionForInstance:instanceURL
                     onComplete:^(JivePlatformVersion *version) {[self instanceVerified:version];}
                        onError:^(NSError *error) {[self instanceError:error];}];


A word about blocks. You can see in the above example that we use blocks for completion and error handling. In the examples used here I will implement a block by calling a method on self to indicate that you are responsible for implementing the block. All SDK methods are designed to accept nil for any block.

 

You will need to create a class that implements the JiveAuthorizationDelegate protocol. This protocol is designed to let you control the caching policy for the users login credentials. The only method credentialsForJiveInstance: is used by the SDK to get a credentials object. Each method will call this method and use the returned object to sign the request.

We provide a simple authorization class for basic http credentials.

- (id<JiveCredentials>)credentialsForJiveInstance:(NSURL*) url {
    return [[JiveHTTPBasicAuthCredentials alloc] initWithUsername:self.username
                                                         password:self.password];
}


jive_devslanding_transparent.png

The next step is to setup a Jive instance.

    MyAuthorizationDelegate *communityAuthorizationDelegate = [[MyAuthorizationDelegate alloc] initWithUserName:(NSString *)userName
                                                                                                       password:(NSString *)password];
    Jive *communityInstance = [[Jive alloc] initWithJiveInstance:instanceURL
                                           authorizationDelegate:communityAuthorizationDelegate];


 

A good place to start is to get the users JivePerson object.

    [communityInstance me:^(JivePerson *me) { [self handleMeObject:me]; }
                  onError:^(NSError *error) { [self handleMeError:error]; }];


 

From here you can get the list of custom streams the user has created.

This call takes a JiveReturnFieldsRequestOptions which can be used to specify that only certain fields should be returned. This can be used to limit the bandwidth consumed by a request, especially requests that return NSArrays. This could be used to get just the names of the available streams and then subsequent JiveStream calls could be made just for the specific streams that the user wants to look at.

    JiveReturnFieldsRequestOptions *streamFields = [JiveReturnFieldsRequestOptions new];
    [streamFields addField:@"name"];
    [communityInstance streams:me
                   withOptions:streamFields
                    onComplete:^(NSArray *streams) { [self handleStreams:streams]; }
                       onError:^(NSError *error) { [self handleStreamsError:error]; }];


This method returns an NSArray of JiveStream objects.

 

A word about Options. Most of the methods in the Jive iOS SDK take an options parameter. Providing nil in this parameter will select the default options for the method.

    [communityInstance stream:streams[0]
                  withOptions:nil
                   onComplete:^(JiveStream *stream) { [self handleStream:stream]; }
                      onError:^(NSError *error) { [self handleStreamError:error]; }];


 

This also demonstrates a fundamental behavior of the SDK, when in doubt reload the object. Most Jive classes have a method to reload an object that follows the pattern of the stream method above.

 

Finally a note about the class hierarchy. There are 3 primary classes: JiveContent, JivePerson, and JivePlace. JivePerson represents a Jive user. JiveContent is the base of a group of content classes the users can create and view. JivePlace is the base of a group of classes that allow users to organize and control content.

 

Next time, the Inbox.

Filter Blog