Creating Custom Messages

Developing a Custom Message Type and its Message Type View is a key step in building a Custom Messaging Experiences.

Many projects will need to implement Custom Message Type components. Some of these Message Type components will be simple informational messages, and others will provide rich interactive messages. Message Type components that are interactive and allow participants in the conversation to interact using UIs provided within these Messages help form what Layer refers to as Messaging Experiences.

A Messaging Experience is:

  • a Message that is sent within a Conversation
  • is visible to participants of that Conversation
  • Enables UI interactions within that Message
  • Shares state changes from that UI across all participants

Basic Concepts

Each message type consists of 4 classes: Model, Serializer, View, and Presenter.

  • Model is an subclass of LYRUIMessageType, and contains all necessary properties for the implemented Message type
  • Serializer, conforming to LYRUIMessageTypeSerializing protocol, creates an Model instance from provided LYRMessage, and in case of sending the message, it serializes Model into the LYRMessage (an LYRUIBaseMessageTypeSerializer can be used as a root class for Serializer, it contains multiple helper methods commonly used for serialization/deserialization)
  • View is a UIView subclass, used to represent the message in the UI
  • Presenter, conforming to LYRUIMessageItemContentPresenting protocol, is used to properly set up the View with the Model properties, and to calculate proper height for the message (an LYRUIMessageItemContentBasePresenter can be used as a root class for Presenter, it contains multiple helper methods commonly used for setting up the View, and calculating it’s height)

Opinion Message Example

Goals: Introduce the basics of creating a custom Message Type that uses a LYRUIStandardMessageContainerView .

An Opinion Message is a sample message used to allow a user to rate and comment on a previous message. Key data that this message will contain:

Name Type Description
comment NSString A comment that the user has about some item of content
rating NSUInteger A number from 1-5 indicating the level of enthusiasm for their comment (1: Comment is simply brain storming throwing ideas out there; 5: This is It! This is the Idea! We are Geniuses!)
ratingDescription NSString The Message that an opinion is being expressed about
author NSString The author of the message described by description

The Opinion Message Type Model

This Opinion Message Type Model is a subclass of LYRUIMessageType

Step 1: Setup a Basic Message Type Model Class

At a minimum, your class should setup

  • MIMEType - Set this property to the MIME Type of this Model
  • metadata - This property should return an LYRUIMessageMetadata object with info to present in the container view
  • summary - An NSString used by default to represent the message, for example on conversations list, for last message in each conversation. It should describe the content of message.
  • An initializer method, for creating an object with all the message properties, as well as action, sender, sentAt, and status from the base class
#import "LYRUIMessageType.h"

@interface OpinionMessage : LYRUIMessageType

@property (nonatomic, readonly, nullable) NSString *comment;
@property (nonatomic, readonly) NSUInteger rating;
@property (nonatomic, readonly, nullable) NSString *ratingDescription;
@property (nonatomic, readonly, nullable) NSString *author;

- (instancetype)initWithComment:(nullable NSString *)comment
                         rating:(NSUInteger)rating
              ratingDescription:(nullable NSString *)ratingDescription
                         author:(nullable NSString *)author
                         action:(nullable LYRUIMessageAction *)action
                         sender:(nullable LYRIdentity *)sender
                         sentAt:(nullable NSDate *)sentAt
                         status:(nullable LYRUIMessageTypeStatus *)status;

@end
#import "OpinionMessage.h"

@interface OpinionMessage ()

@property (nonatomic, readwrite, nullable) NSString *comment;
@property (nonatomic, readwrite) NSUInteger rating;
@property (nonatomic, readwrite, nullable) NSString *ratingDescription;
@property (nonatomic, readwrite, nullable) NSString *author;

@end

@implementation OpinionMessage

- (instancetype)initWithComment:(NSString *)comment
                         rating:(NSUInteger)rating
              ratingDescription:(NSString *)ratingDescription
                         author:(NSString *)author
                         action:(LYRUIMessageAction *)action
                         sender:(LYRIdentity *)sender
                         sentAt:(NSDate *)sentAt
                         status:(LYRUIMessageTypeStatus *)status {
    self = [super initWithAction:action sender:sender sentAt:sentAt status:status];
    if (self) {
        self.comment = comment;
        self.rating = rating;
        self.ratingDescription = ratingDescription;
        self.author = author;
    }
    return self;
}

+ (NSString *)MIMEType {
    return @"application/vnd.layer.opinion+json";
}

- (NSString *)summary {
    return [NSString stringWithFormat:@"An %lu star rating", (unsigned long)self.rating];
}

- (LYRUIMessageMetadata *)metadata {
    return [[LYRUIMessageMetadata alloc] initWithDescription:self.ratingDescription
                                                       title:@"Opinion"
                                                      footer:self.author];
}

@end
Step 2: Serializing and Deserializing a Message object

Messages are transported as a LYRMessage objects, with one, or more LYRMessagePart, therefore each message needs to be deserialized into it’s model type. This is done using the -typedMessageWithMessagePart: method of the Serializer.

Serializer also needs to know, how to convert the message Model object into LYRMessagePart objects, used to create a new LYRMessage. This is done using the -layerMessagePartsWithTypedMessage:parentNodeId:role:MIMETypeAttributes method. This method knows what properties need to be written to the LYRMessage body (usually in JSON format), and creates the LYRMessagePart that will transmit this Model’s data to all participants.

#import "LYRUIBaseMessageTypeSerializer.h"
#import "OpinionMessage.h"

@interface OpinionMessageSerializer : LYRUIBaseMessageTypeSerializer<OpinionMessage *>
@end
#import "OpinionMessageSerializer.h"
#import "LYRMessage+LYRUIHelpers.h"
#import "LYRMessagePart+LYRUIHelpers.h"
#import "LYRUIMessageActionSerializer.h"
#import <LayerKit/LayerKit.h>

@implementation OpinionMessageSerializer

- (OpinionMessage *)typedMessageWithMessagePart:(LYRMessagePart *)messagePart {
    LYRUIMessageAction *action = [self.actionSerializer actionFromProperties:messagePart.properties];
    return [[OpinionMessage alloc] initWithComment:messagePart.properties[@"comment"]
                                            rating:[messagePart.properties[@"rating"] unsignedIntegerValue]
                                 ratingDescription:messagePart.properties[@"rating_description"]
                                            author:messagePart.properties[@"author"]
                                            action:action
                                            sender:messagePart.message.sender
                                            sentAt:messagePart.message.sentAt
                                            status:[self statusWithMessage:messagePart.message]];
}

- (NSArray<LYRMessagePart *> *)layerMessagePartsWithTypedMessage:(OpinionMessage *)message
                                                    parentNodeId:(NSString *)parentNodeId
                                                            role:(NSString *)role
                                              MIMETypeAttributes:(NSDictionary *)MIMETypeAttributes {
    NSMutableDictionary *messageJson = [[NSMutableDictionary alloc] init];
    messageJson[@"comment"] = message.comment;
    messageJson[@"rating"] = @(message.rating);
    messageJson[@"rating_description"] = message.ratingDescription;
    messageJson[@"author"] = message.author;
    [messageJson addEntriesFromDictionary:[self.actionSerializer propertiesForAction:message.action]];

    NSError *error = nil;
    NSData *messageJsonData = [NSJSONSerialization dataWithJSONObject:messageJson options:0 error:&error];
    if (error) {
        NSLog(@"Failed to serialize opinion message JSON object: %@", error);
        return nil;
    }
    NSString *MIMEType = [self MIMETypeForContentType:message.MIMEType
                                         parentNodeId:parentNodeId
                                                 role:role
                                           attributes:MIMETypeAttributes];
    LYRMessagePart *messagePart = [LYRMessagePart messagePartWithMIMEType:MIMEType data:messageJsonData];
    return @[messagePart];
}

- (LYRMessageOptions *)messageOptionsForTypedMessage:(OpinionMessage *)messageType {
    LYRMessageOptions *messageOptions = [self defaultMessageOptionsWithPushNotificationText:@"sent you an opinion."];
    return messageOptions;
}

@end
Step 3: Presenting the message in Messages List

To represent the message in the messages list, the Presenter is used to create a proper View, and present it. A simple View for representing the Opionion Message, can contain a single label, used to represent rating with string containing emoji stars.

#import <UIKit/UIKit.h>

@interface OpinionMessageView : UIView

@property (nonatomic, readonly) UILabel *ratingLabel;

@end
#import "OpinionMessageView.h"

@interface OpinionMessageView ()

@property (nonatomic, readwrite) UILabel *ratingLabel;

@end

@implementation OpinionMessageView

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self addRatingLabel];
    }
    return self;
}

- (void)addRatingLabel {
    self.ratingLabel = [[UILabel alloc] init];
    [self addSubview:self.ratingLabel];
    self.ratingLabel.translatesAutoresizingMaskIntoConstraints = NO;
    [self.ratingLabel.topAnchor constraintEqualToAnchor:self.topAnchor].active = YES;
    [self.ratingLabel.leftAnchor constraintEqualToAnchor:self.leftAnchor].active = YES;
    [self.ratingLabel.rightAnchor constraintEqualToAnchor:self.rightAnchor].active = YES;
    [self.ratingLabel.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = YES;
}

@end

The presenter needs to implement 2 methods: -viewForMessage: used for creating, and setting up a View, and -viewHeightForMessage:minWidth:maxWidth: used to calculate size of the View. In addition to these 2 methods, there’s a method used for providing the background color for the message: -backgroundColorForMessage: (it will change the background color of the view containing the message view, as well as the border of message).

#import "LYRUIMessageItemContentBasePresenter.h"

@interface OpinionMessagePresenter : LYRUIMessageItemContentBasePresenter
@end
#import "OpinionMessagePresenter.h"
#import "OpinionMessageView.h"
#import "OpinionMessage.h"
#import "LYRUIReusableViewsQueue.h"

@implementation OpinionMessagePresenter

- (UIView *)viewForMessage:(OpinionMessage *)message {
    OpinionMessageView *opinionMessageView = [self.reusableViewsQueue dequeueReusableViewOfType:[OpinionMessageView class]];
    if (opinionMessageView == nil) {
        opinionMessageView = [[OpinionMessageView alloc] init];
    }
    opinionMessageView.ratingLabel.text = [self stringForRating:message.rating];
    return opinionMessageView;
}

- (NSString *)stringForRating:(NSUInteger)rating {
    NSMutableString *ratingString = [[NSMutableString alloc] init];
    for (NSUInteger i = 0; i <= 5; i += 1) {
        if (i <= rating) {
            [ratingString appendString:@"★"];
        } else {
            [ratingString appendString:@"☆"];
        }
    }
    return ratingString;
}

- (UIColor *)backgroundColorForMessage:(OpinionMessage *)message {
    return [UIColor redColor];
}

- (CGFloat)viewHeightForMessage:(OpinionMessage *)message minWidth:(CGFloat)minWidth maxWidth:(CGFloat)maxWidth {
    return 32.0;
}

@end

Note

LYRUIMessageItemContentBasePresenter contains an LYRUIReusableViewsQueue used for reusing the message views that went off screen (similar to UITableView). When subclassing the base presenter, it’s good to utilize the queue, to speed up the process of providing message views.

Step 4: Registering new message type in XDK

Every new message type needs to be registered in the XDK, so the XDK will know how to handle an LYRMessage with giver MIMEType, and how to present it on the messages list. This is done using the `` method of the LYRUIConfiguration object.

[layerUIConfiguration registerMessageTypeClass:[OpinionMessage class]
                           withSerializerClass:[OpinionMessageSerializer class]
                         contentPresenterClass:[OpinionMessagePresenter class]
                       containerPresenterClass:[LYRUIStandardMessageContainerViewPresenter class]];

After registering new message types, and passing the configuration object to the LYRUIMessagesList, new messages will be rendered in the list.

Introduction UI API Reference