TutorialsXamarinEngineering

Building a Chat App Using Weavy and Telerik Conversational UI for Xamarin

Apr 22, 2021

The best way to follow along with the tutorials is to make sure you have a local development instance of the Weavy backend up and running.

Note: this tutorial was written for Weavy v8, which uses a different codebase than the current version of Weavy.

 

Introduction

This tutorial will show you how you can use Weavy's conversation API to build a Xamarin.Forms chat app with some of Telerik's awesome UI components. Please note that the end result of this tutorial is not a ready-to-use, fully-featured chat app, but instead, a showcase of what you can do with Weavy and external component providers such as Telerik.

Requirements

 

The app

The app we are going to build will have the following functionallity:

  • Authentication
  • List conversations from Weavy
  • Create a new conversation *
  • View a conversation *
  • Send messages in a conversation *
  • Receive messages and image attachments in a conversation *
  • Real time with SignalR *

*These features are available in the repo below. In this tutorial we'll go over the fundamentals and some basic features.

The repo is available at https://github.com/weavy/weavy-telerik-xamarin-chat. Feel free to clone, add, modify and use it as you like. Again, this is only a show case app and not intended for production. Use it as a guide when building your own chat app :)

Telerik Xamarin UI components

In this project, we are going to use the following Telerik Xamarin UI components:

Telerik component Used for
ListView List the conversations
AutoCompleteView Display Weavy users when creating a new conversation
Conversational UI Display and send messages in a conversation


MVVM Framework

For this app we will be using MVVM Cross, which is a MVVM pattern framework that you can use together will Xamarin.Forms. For more information and details, take a look at https://www.mvvmcross.com/. Don't worry, if you have used the MVVM pattern before with Xamarin.Forms, you should be familiar even though you havn't used MVVM Cross.

TIP! There is a great Visual Studio template for creating new MVVM Cross projects. It's recommended to install the extension to get up and running with a MVVM Cross Xamarin.Forms projects quickly.

Let's get to it!

Ok, let's start the step by step tutorial. Please note that we will not cover every little detail in this guide. For the complete show case app, please refer to the Github repo.

Step 1 - Setup the project

If you haven't yet installed the MVVM Cross VS template mentioned above, do it now.

  1. When you are done, create a new project and make sure it is a MvxScaffolding MVVMCross Forms project.
  2. Click Next and make sure the Single View type is selected
  3. Click Next and Select iOS and Android platforms for this tutorial.
  4. Click Done and the project is created for you

Step 2 - Some constants!

We need some settings that we are going to use in the app. For this purpose we just create a static helper class to hold the values we need. In a real world scenario, you should store these settings in a secure storage provider which is available for each platform and Xamarin.Forms.

Create the following Constants.cs class in the [YourProjectName].Core project

public class Constants {
    // feel free to try out this chat app against the test site https://showcase.weavycloud.com/. We do recommend that you set up your own Weavy though. Take a look at https://docs.weavy.com for more information..
    public static string RootUrl = "https://showcase.weavycloud.com/";

    public static User Me { get; internal set; }
}
NOTE! If you have setup your own local Weavy development environment, change the RootUrl above to your url. Want to know how to setup Weavy? Check out this article: Get started with the Server SDK

Step 3 - RestService and Authentication

In order to make the API calls to Weavy we need a Rest service to handle the requests. Each api request also need a Bearer token that authenticates the user against Weavy. The Rest calls will be made in a class called RestService and the token generation will be made in a class called TokenService.

1. Create a new folder in the [YourProjectName].Core called Services. In that folder, create a new file called IRestService.cs with the following content:

public interface IRestService {
    Task<T> GetAsync<T>(string resource);
    Task<HttpResponseMessage> PostAsync(string resource, string jsonData = "");
}

2. In the same folder, create a new file called ITokenService.cs with the following content.

public interface ITokenService {
    string GenerateToken();
}
TIP! If you name the service intefaces with the suffix Service like we just did above, MVVM Cross will automatically wire up the corresponding implementation in the Dependency Container. Take a look in the App.cs file for more information.

3. Now it's time to add the classes for the interfaces above. In the same corresponding files (or if you prefer to create new ones), add the following implementation for the RestService.

public class RestService : IRestService {
    private readonly ITokenService _tokenService;
    private HttpClient _client;

    public RestService(ITokenService tokenService)
    {
        _tokenService = tokenService;
        _client = new HttpClient()
        {
            BaseAddress = new Uri(Constants.RootUrl)
        };
        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _tokenService.GenerateToken());
    }

    public async Task<T> GetAsync<T>(string resource)
    {
        var response = await _client.GetAsync(resource);
        var data = await response.Content.ReadAsStringAsync();
        return JsonConvert.DeserializeObject<T>(data);
    }

    public async Task<HttpResponseMessage> PostAsync(string resource, string jsonData = "")
    {
        var content = new StringContent(jsonData, Encoding.UTF8, "application/json");
        return await _client.PostAsync(resource, content);
    }

}

Now we have everything we need to call the Weavy Rest api that you will create later in this tutorial.

Add the following implementation of the ITokenService interface.

public class TokenService : ITokenService {
    public string GenerateToken()
    {
        // a ready to use, long lived demo token with a pre-defined user. This is only for demo purposes!
        // This token only works if Constants.RootUrl is set to https://showcase.weavycloud.com/. If you are using a local/remote Weavy of your own,
        // please take a look at https://docs.weavy.com/client/authentication on how to setup a Client and generate tokens
        return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJvbGl2ZXIiLCJuYW1lIjoiT2xpdmVyIFdpbnRlciIsImV4cCI6MjUxNjIzOTAyMiwiaXNzIjoic3RhdGljLWZvci1kZW1vIiwiY2xpZW50X2lkIjoiV2VhdnlEZW1vIiwiZGlyIjoiY2hhdC1kZW1vLWRpciIsImVtYWlsIjoib2xpdmVyLndpbnRlckBleGFtcGxlLmNvbSIsInVzZXJuYW1lIjoib2xpdmVyIn0.VuF_YzdhzSr5-tordh0QZbLmkrkL6GYkWfMtUqdQ9FM";

        // generate a token for Weavy. In a real world appplication, this should based on the signed in user from your host system.
        // check out the Weavy Docs (https://docs.weavy.com/client/authentication) how to create a jwt token and what different claims you can set.
        // The ClientId and ClientSecret constants is the id and secret of the Authentication Client created in the Weavy installation you are using.
        //return new JwtBuilder()
        //    .WithAlgorithm(new HMACSHA256Algorithm()) // symmetric
        //    .WithSecret(Constants.ClientSecret)
        //    .AddClaim("exp", DateTimeOffset.UtcNow.AddMinutes(1).ToUnixTimeSeconds())
        //    .AddClaim("iss", Constants.ClientId)
        //    .AddClaim("sub", "1")
        //    .AddClaim("email", "johndoe@test.com")
        //    .AddClaim("name", "John Doe")
        //    .Encode();
    }
}

Here we are using JWT.Net for the token generation. If you don't want to use the demo tokens (or if you are using your own local Weavy) and create real tokens, you have to add the following nuget package: https://www.nuget.org/packages/JWT/7.3.1

As noted in the code comments, in this scenario we are using a hard-coded JWT token for test purposes only. If you are building a real app, the authentication flow would be to first authenticate the user agains your own host system and then create a JWT token with the user's information. The token should then be sent to Weavy in the requests as the Bearer token in the Authorization header.

TIP! In the showcase app at Github, an authentication flow with a Sign In page is already created for you. There is also an AuthenticationService where you can return true/false if the user is authenticated and then show the appropriate screen at startup.

The pre-defined JWT tokens we have prepared for this tutorial/demo are defined below. Feel free to use these tokens for the https://showcase.weavycloud.com/ Weavy test site. This is the RootUrl that needs to be set in the Constants.cs class for this to work.

Oliver Winter
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJvbGl2ZXIiLCJuYW1lIjoiT2xpdmVyIFdpbnRlciIsImV4cCI6MjUxNjIzOTAyMiwiaXNzIjoic3RhdGljLWZvci1kZW1vIiwiY2xpZW50X2lkIjoiV2VhdnlEZW1vIiwiZGlyIjoiY2hhdC1kZW1vLWRpciIsImVtYWlsIjoib2xpdmVyLndpbnRlckBleGFtcGxlLmNvbSIsInVzZXJuYW1lIjoib2xpdmVyIn0.VuF_YzdhzSr5-tordh0QZbLmkrkL6GYkWfMtUqdQ9FM

Lilly Diaz
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJsaWxseSIsIm5hbWUiOiJMaWxseSBEaWF6IiwiZXhwIjoyNTE2MjM5MDIyLCJpc3MiOiJzdGF0aWMtZm9yLWRlbW8iLCJjbGllbnRfaWQiOiJXZWF2eURlbW8iLCJkaXIiOiJjaGF0LWRlbW8tZGlyIiwiZW1haWwiOiJsaWxseS5kaWF6QGV4YW1wbGUuY29tIiwidXNlcm5hbWUiOiJsaWxseSJ9.rQvgplTyCAfJYYYPKxVgPX0JTswls9GZppUwYMxRMY0

Samara Kaur
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzYW1hcmEiLCJuYW1lIjoiU2FtYXJhIEthdXIiLCJleHAiOjI1MTYyMzkwMjIsImlzcyI6InN0YXRpYy1mb3ItZGVtbyIsImNsaWVudF9pZCI6IldlYXZ5RGVtbyIsImRpciI6ImNoYXQtZGVtby1kaXIiLCJlbWFpbCI6InNhbWFyYS5rYXVyQGV4YW1wbGUuY29tIiwidXNlcm5hbWUiOiJzYW1hcmEifQ.UKLmVTsyN779VY9JLTLvpVDLc32Coem_0evAkzG47kM

Adam Mercer
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZGFtIiwibmFtZSI6IkFkYW0gTWVyY2VyIiwiZXhwIjoyNTE2MjM5MDIyLCJpc3MiOiJzdGF0aWMtZm9yLWRlbW8iLCJjbGllbnRfaWQiOiJXZWF2eURlbW8iLCJkaXIiOiJjaGF0LWRlbW8tZGlyIiwiZW1haWwiOiJhZGFtLm1lcmNlckBleGFtcGxlLmNvbSIsInVzZXJuYW1lIjoiYWRhbSJ9.c4P-jeQko3F_-N4Ou0JQQREePQ602tNDhO1wYKBhjX8
IMPORTANT! When generating JWT tokens in a real-world app, the tokens should be very short-lived for maximum security.

Step 4 - The Conversation Service

Now that we have a rest service and a way to generate valid JWT tokens, we need a service class to handle all the functionality for a Weavy conversation. This involves getting all the user's conversations, create a new conversation, getting the messages in a specific conversation, and so on.

1. Create a new interface called IConversationService.cs in the Services folder.

public interface IConversationService {
    Task<IEnumerable<Conversation>> Get();
}

The method returns a list of Conversation which we haven't yet created. So let's do that. Create a new folder in the [YourProjectName].Core project called Models. In that folder, create the following class.

public class Conversation {

    public int Id { get; set; }

    [JsonProperty("is_room")]
    public bool IsRoom { get; set; }

    public string Name { get; set; }

    public string Description { get; set; }

    [JsonProperty("thumb")]
    public string ThumbUrl { get; set; }

    [JsonProperty("last_message")]
    public Message LastMessage { get; set; }

    public IEnumerable<Member> Members { get; set; }

    [JsonProperty("is_read")]
    public bool IsRead { get; set; }

    [JsonProperty("is_pinned")]
    public bool IsPinned { get; set; }

    public string ThumbUrlFull => $"{Constants.RootUrl}{(ThumbUrl.Replace("{options}", "64"))}";

    public string ConversationTitle => IsRoom ? Name ?? string.Join(", ", Members.Select(x => x.Name)) : Members.FirstOrDefault(x => x.Id != Constants.Me.Id)?.Name;

}

A conversation can have two or more Members. Create a new class in the Models folder called Member.cs

public class Member
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string Name { get; set; }

    [JsonProperty("thumb")]
    public string ThumbUrl { get; set; }

    [JsonProperty("delivered_at")]
    public DateTime DeliveredAt { get; set; }

    [JsonProperty("read_at")]
    public DateTime ReadAt { get; set; }

    public Precence Precence { get; set; }

    public string ThumbUrlFull => $"{Constants.RootUrl}{(ThumbUrl.Replace("{options}", "64"))}";
}

public enum Precence
{
    Away = 0,
    Active = 1
}

Now, let's create the implementation of the ConversationService. Add the following code.

public class ConversationService : IConversationService {
    private readonly IRestService _restService;

    public ConversationService(IRestService restService) {
        _restService = restService;
    }

    public async Task<IEnumerable<Conversation>> Get() {
        return await _restService.GetAsync<IEnumerable<Conversation>>("/api/conversations");
    }
}

In the code above, we are injecting the IRestService to the ConversationService. In the Get method, we are doing a Get request to fetch all the user's ongoing conversations from Weavy. The /api/conversations endpoint in Weavy is not created yet. In Weavy, you can add any API endpoints you want and need. We'll take a look at that later in this tutorial.

Step 5 - The HomePage.xaml and the HomePageViewModel.xaml.cs

MVVM Cross automatically wires up the Views (Pages) and the corresponding ViewModel. Now it's time to create the home page where we are going to list the user's ongoing conversations.

1. Create a new folder in the [YourProjectName].Core project's ViewModels folder called Home. In that folder, create a new class called HomeViewModel.cs. Make sure the class inherits from BaseViewModel.

public class HomeViewModel : BaseViewModel {
    ...
}

Now, let's use the ConversationService we created in the previous step. We use the service to get all the user's ongoing conversations in Weavy and put them in a Telerik ListView control.

public class HomeViewModel : BaseViewModel {
    private readonly IConversationService _conversationService;

    public HomeViewModel(IConversationService conversationService){
            _conversationService = conversationService;
    }

    private ObservableCollection<ConversationItem> _list;
    public ObservableCollection<ConversationItem> List
    {
        get => _list;
        set => SetProperty(ref _list, value);
    }

    public override void Prepare() {
        // load conversation list
        Task.Run(async () => await LoadConversations()).Wait();
    }

    private async Task LoadConversations() {

        List.Clear();

        var conversations = await _conversationService.Get();

        if(conversations != null && conversations.Count() > 0) {
            foreach (var conversation in conversations) {
                List.Add(new ConversationItem {
                    Id = conversation.Id,
                    Name = conversation.Name,
                    IsRoom = conversation.IsRoom,
                    IsRead = conversation.IsRead,
                    ConversationTitle = conversation.ConversationTitle,
                    LastMessageAt = conversation.LastMessage.CreatedAt,
                    LastMessageByName = conversation.LastMessage.CreatedBy.Id == Constants.Me.Id ? "Me: " : (conversation.IsRoom ? $"{conversation.LastMessage.CreatedBy.Name.Split(' ')[0]}: " : ""),
                    Description = conversation.LastMessage.Text,
                    ThumbUrl = conversation.ThumbUrlFull
                });
            }
        }
    }
}

The ConversationItem class is a ViewModel for a Conversation entity that is more suitable for the View than the core model. The view model has bindable properties which are necessary to make the View (Page) reactive to changes in the viewmodel. For example, if the IsRead flag changes on a specific conversation, we want the UI to update accordingly. Add the ConversationItem class to the ViewModels folder.

public class ConversationItem : BaseViewModel {
    private int _id;
    public int Id {
        get => _id;
        set => SetProperty(ref _id, value);
    }

    private string _description;
    public string Description {
        get => _description;
        set => SetProperty(ref _description, value);
    }

    private DateTime _lastMessageAt;
    public DateTime LastMessageAt {
        get => _lastMessageAt;
        set => SetProperty(ref _lastMessageAt, value);
    }

    private string _name;
    public string Name {
        get => _name;
        set => SetProperty(ref _name, value);
    }

    public ImageSource ImageSource => new UriImageSource() { Uri = new Uri(ThumbUrl) };

    private string _thumbUrl;
    public string ThumbUrl {
        get => _thumbUrl;
        set => SetProperty(ref _thumbUrl, value);
    }

    public string ConversationTitle { get; internal set; }

    private string _lastMessageByName;
    public string LastMessageByName {
        get => _lastMessageByName;
        set => SetProperty(ref _lastMessageByName, value);
    }

    private bool _isRead;
    public bool IsRead {
        get => _isRead;
        set => SetProperty(ref _isRead, value);
    }

    public bool IsRoom { get; internal set; }
}

Ok, now that we have our view model, we need a Page to show it on. All the Views are created in the [YourProjectName].UI project. In the Pages folder, add a new Content Page and name it HomePage.xaml. Replace the code in the .xmal file with:

<?xml version="1.0" encoding="utf-8" ?>
<views:MvxContentPage x:TypeArguments="viewModels:HomeViewModel"
                        xmlns="http://xamarin.com/schemas/2014/forms"
                        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                        xmlns:views="clr-namespace:MvvmCross.Forms.Views;assembly=MvvmCross.Forms"
                        xmlns:mvx="clr-namespace:MvvmCross.Forms.Bindings;assembly=MvvmCross.Forms"
                        xmlns:local="clr-namespace:WeavyTelerikChat.UI.Pages"
                        xmlns:localviews="clr-namespace:WeavyTelerikChat.UI.Views"
                        xmlns:telerikDataControls="clr-namespace:Telerik.XamarinForms.DataControls;assembly=Telerik.XamarinForms.DataControls"
                        xmlns:telerikListView="clr-namespace:Telerik.XamarinForms.DataControls.ListView;assembly=Telerik.XamarinForms.DataControls"
                        xmlns:telerikListViewCommands="clr-namespace:Telerik.XamarinForms.DataControls.ListView.Commands;assembly=Telerik.XamarinForms.DataControls"
                        x:Class="WeavyTelerikChat.UI.Pages.HomePage"
                        xmlns:viewModels="clr-namespace:WeavyTelerikChat.Core.ViewModels.Home;assembly=WeavyTelerikChat.Core"
                        Title="Messenger">

    <ContentPage.Content>
        <StackLayout VerticalOptions="FillAndExpand">

            <telerikDataControls:RadListView x:Name="listView"
                                            ItemsSource="{Binding List}"
                                            SelectionMode="Single"
                                            SelectedItem="{mvx:MvxBind ItemSelected}">
                <telerikDataControls:RadListView.Commands>
                    <telerikListViewCommands:ListViewUserCommand 
                                            Id="ItemTap" 
                                            Command="{Binding TappedCommand}" />
                </telerikDataControls:RadListView.Commands>
                <telerikDataControls:RadListView.ItemTemplate>
                    <DataTemplate>
                        <telerikListView:ListViewTemplateCell>
                            <telerikListView:ListViewTemplateCell.View>
                                <localviews:ConversationCell />
                            </telerikListView:ListViewTemplateCell.View>
                        </telerikListView:ListViewTemplateCell>
                    </DataTemplate>
                </telerikDataControls:RadListView.ItemTemplate>
            </telerikDataControls:RadListView>
        </StackLayout>
    </ContentPage.Content>
</views:MvxContentPage>
NOTE! Replace the MyProject in the .xaml file above with the name of your project.

Replace the generated HomePage.xaml.cs with:

namespace MyProject.UI.Pages {

    [XamlCompilation(XamlCompilationOptions.Compile)]
    [MvxContentPagePresentation(WrapInNavigationPage = true)]
    public partial class HomePage : MvxContentPage<HomeViewModel> {
        public HomePage() {
            InitializeComponent();
        }

        protected override void OnAppearing() {
            base.OnAppearing();

            if (Application.Current.MainPage is NavigationPage navigationPage) {
                navigationPage.BarTextColor = Color.White;
                navigationPage.BarBackgroundColor = (Color)Application.Current.Resources["PrimaryColor"];
            }
        }
    }
}
NOTE! Again, replace the MyProject in the .xaml.cs file above with the name of your project.

Step 6 - Create the Weavy API endpoints

Weavy comes with a bunch of existing API endpoints that can come in handy when building either apps for the web or mobile. Weavy also lets you add any API endpoint you want and need for any special needs. At the time of writing, no pre-defined API endpoints for the Conversation and Messages exist when you install and run your Weavy instance.

NOTE! If you don't want to add these API endpoints now, and if you are testing against the pre-configured demo site, https://showcase.weavycloud.com/, we have already added the needed API endpoints to that Weavy site. You can also get the code for that Weavy site at Github.

It's pretty easy to add your own API endpoints. Open up the Weavy solution in Visual Studio and create a new API Controller under the Areas/Api/Controllers folder and name it ConversationsController.cs. Add the following code.

namespace Weavy.Areas.Api.Controllers {

    /// <summary>
    /// Api controller for manipulating Conversations.
    /// </summary>
    [RoutePrefix("api")]
    public class ConversationsController : WeavyApiController {


        /// <summary>
        /// Get all <see cref="Conversation" /> for the current user.
        /// </summary>
        /// <example>GET /api/conversations</example>
        /// <returns>The users conversations.</returns>
        [HttpGet]
        [ResponseType(typeof(IEnumerable<Conversation>))]
        [Route("conversations")]
        public IHttpActionResult List() {
            var conversations = ConversationService.Search(new ConversationQuery());
            return Ok(conversations);
        }

    }
}

That's it! Well, that was pretty easy... ;) Compile the Weavy solution and make sure the updates are fully functional by going to https://[yourweavyurl]/api/conversations. Your active conversations should be returned in JSON format.

Step 7 - Test the app

This section depends if you are using the pre-defined Weavy demo installation at https://showcase.weavycloud.com/ or if you have set up a local Weavy.

Testing against https://showcase.weavycloud.com/

Make sure Android is set as the start-up project and hit Start! The Android emulator should start up with your app. Since you are using the pre-defined JWT tokens from Step 3, the user will be authenticated and the user's conversations will show up (if any)

Testing against a local Weavy installation

Make sure Android is set as the start-up project and hit Start! The Android emulator should start up with your app.

Make sure you are using a valid JWT token. For more information on how to set up and create the token, please refer to this article: Authentication. The article describes how to authenticate from the Weavy client, but the same goes for any external app such as this one. The steps consist of:

  • Set up an Authentication client under Manage -> Clients in Weavy
  • Generate a JWT using a tool such as the one references in Step 3 in this tutorial.

When the apps is running, you will notice that there are no conversations in the list. This is because the user is created in Weavy the first time it's authenticated. We don't yet have any functionality to create a new one either. But don't worry, take a look at the Next steps below how you could continue.

NOTE! You can of course run this app on iOS as well. This requires you to have a Mac setup and paired to your Visual Studio.

Next steps

The showcase app at https://github.com/weavy/weavy-telerik-xamarin-chat has a lot more features beyond the scope of this tutorial. These features include:

  • Create a new conversation
  • List and autocomplete users in Weavy when creating a new conversation
  • Select a conversation and list messages
  • Send messages in a conversation
  • Mark conversations as unread when new messages arrive
  • A SignalR connection to Weavy to track changes in real-time, such as new messages and read/unread status.
Weavy

Share this post