This is a migrated thread and some comments may be shown as answers.

Avatars from raw image data

8 Answers 408 Views
Chat (Conversational UI)
This is a migrated thread and some comments may be shown as answers.
n/a
Top achievements
Rank 1
n/a asked on 22 May 2019, 05:24 PM

Hi,

In our chat application we receive Author avatars as byte arrays. I see that in the chat control the Avatar property on Author is a string and it isn't clear how one would use a dynamic image, is there any guidance on achieving this?

thanks

8 Answers, 1 is accepted

Sort by
0
Lance | Senior Manager Technical Support
Telerik team
answered on 23 May 2019, 12:20 AM
Hi Patrick,

The Avatar property uses the file (or url) path approach for the image rather than an ImageSource that you can load a byte[] into a MemoryStream. The solution is to save the image to the local folder of the app and use the Path to the file.

If all the images for the chat participants are only byte arrays, I'm assuming you're retrieving them via HttpClient? In this case, you can just save to disk at that time and not pass around a byte[]. Doing this will also give you a small performance gain as file IO is usually faster than awaiting a network request.

For example:

private async Task SetupUserAuthorAsync()
{
    base.OnAppearing();
 
    var username = "John Doe";
 
    var localFolder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
    var avatarFilePath = Path.Combine(localFolder, $"{username}.png");
 
    // If the image hasn't been downloaded and saved yet, do it now. This will only have to be done once
    if (!File.Exists(avatarFilePath))
    {
        var url = "https://myApiRoot/users/GetProfileImage?" + username;
        using (var client = new HttpClient())
        {
            var imageBytes = await client.GetByteArrayAsync(url);
 
            File.WriteAllBytes(avatarFilePath, imageBytes);
        }
    }
 
    var userAuthor = new Author();
    userAuthor.Name = username;
    userAuthor.Avatar = avatarFilePath;
}

If you do want to check for image changes, you could occasionally force a new download and save cycle.

Regards,
Lance | Technical Support Engineer, Principal
Progress Telerik
Do you want to have your say when we set our development plans? Do you want to know when a feature you care about is added or when a bug fixed? Explore the Telerik Feedback Portal and vote to affect the priority of the items
0
n/a
Top achievements
Rank 1
answered on 23 May 2019, 09:36 AM

Hi Lance,

Thanks for your reply, that's much appreciated. In terms of the the performance gain this wouldn't be a concern for us as we use Akavache to perform caching of the HTTP requests and have cache expiry rules etc. built into the app logic; this is because we see calls to the avatar download endpoint as simply another in a much longer list of API calls our app makes.

This does mean however, that we will need to now have a separate file-based caching mechanism just for avatars which is slightly frustrating as it means we are making specialisations on the app backend to support the limitation of a single UI widget which isn't desirable.

We can probably work around this for now, but out of interest why is the control limited in this way? Was it an explicit design decision or a technical limitation? Is there a chance that in a future version this could be extended to allow ImageSource too, or an extensibility point put in so the Avatar image can be templated and bound up a different way?

many thanks

0
Lance | Senior Manager Technical Support
Telerik team
answered on 23 May 2019, 03:15 PM
Hi Patrick,

That approach is only for the basic built-in template. You can always define your own message ItemTemplate that has a Xamarin.Forms Image (or an FFImageloading CachedImage). We have a full example of this approach in the RadChat ItemTemplateSelector documentation article.

The only thing you would do differently from that example is change the Source property of the Image to use a "profile pic" property on your message model

For example:

<Style x:Key="MessageImageStyle" TargetType="Image">
<!-- Old -->
    <!-- <Setter Property="Source" Value="{Binding Author.Avatar}" />-->
<!-- New -->
    <Image Source="{Binding Data.ProfileImageSource}"  />
    <Setter Property="WidthRequest" Value="30" />
    <Setter Property="HeightRequest" Value="30" />
</Style>
 
<DataTemplate x:Key="ImportantMessageTemplate">
    <Grid Margin="0, 2, 0, 10">
        <Image Style="{StaticResource MessageImageStyle}" />
        <telerikPrimitives:RadBorder CornerRadius="0, 7, 7, 7" Margin="45, 0, 50, 0" HorizontalOptions="Start" BackgroundColor="#FF0000">
            <StackLayout Orientation="Horizontal" Margin="20, 0, 20, 0">
                <Label Text="! " FontAttributes="Bold" FontSize="Medium" />
                <Label Text="{Binding Text}" FontSize="Medium" />
            </StackLayout>
        </telerikPrimitives:RadBorder>
    </Grid>
</DataTemplate>


Regarding changing the Author.Avatar property, you can ask the development team to switch to using a more commonly accepted ImageSource type that has implicit conversion for string paths. For your convenience, I have created this feedback on your behalf here: https://feedback.telerik.com/xamarin/1410330-use-imagesource-type-for-author-avatar-property 

Regards,
Lance | Technical Support Engineer, Principal
Progress Telerik
Do you want to have your say when we set our development plans? Do you want to know when a feature you care about is added or when a bug fixed? Explore the Telerik Feedback Portal and vote to affect the priority of the items
0
n/a
Top achievements
Rank 1
answered on 23 May 2019, 04:40 PM

Nice, thanks Lance! I'll give that a shot.

0
Accepted
Lance | Senior Manager Technical Support
Telerik team
answered on 23 May 2019, 07:01 PM
Hello Patrick,

I can understand if this is overwhelming at first, so I thought I would spend a couple hours to put together a clear example of how these things work together, along with the customization to use a custom template and a more elegant way of using a byte[] on the author model.

First, let's create an ExtendedAuthor model that inherits from Author. There are other ways you could do this, but I think this is probably the simplest and still allows you to use our examples.

public class ExtendedAuthor : Author
{
    public ImageSource ProfileImageSource { get; set; }
}

Now, let's use the MVVM Documentation's tutorial as a base lesson because it has two Authors in the view model. You will want to complete that tutorial to ctach up, it has the models and data as a base to move forward. With done, you can make the changes below.

Now, in my demo, you're going to use your ExtendedAuthor class instead of Author and set the ProfileImageSource property (note: in the example I use SkiaSharp to create a byte[] for the image, this is where you would use your byte[] source):

public class MainPageViewModel : NotifyPropertyChangedBase
{
    public MainPageViewModel()
    {
        // Both authors have a byte[] as the profile image source
        this.Me = new ExtendedAuthor
        {
            Name = "human",
            ProfileImageSource = ImageSource.FromStream(() => new MemoryStream(GenerateProfileImageBytes()))
        };
 
        this.Bot = new ExtendedAuthor
        {
            Name = "Bot",
            ProfileImageSource = ImageSource.FromStream(() => new MemoryStream(GenerateProfileImageBytes()))
        };
 
        this.Items = new ObservableCollection<SimpleChatItem>();
 
        // Simulate async data loading
        Device.StartTimer(TimeSpan.FromMilliseconds(500), () =>
        {
            this.Items.Add(new SimpleChatItem { Author = this.Bot, Text = "Hi." });
            this.Items.Add(new SimpleChatItem { Author = this.Bot, Text = "How can I help you?" });
            return false;
        });
    }
 
    public ExtendedAuthor Me { get; set; }
 
    public ExtendedAuthor Bot { get; set; }
 
    public ObservableCollection<SimpleChatItem> Items { get; set; }
 
    private byte[] GenerateProfileImageBytes()
    {
        using (var bmp = new SKBitmap(36, 36))
        {
            var rand = new Random();
            var r = (byte)rand.Next(0, byte.MaxValue);
            var g = (byte)rand.Next(0, byte.MaxValue);
            var b = (byte)rand.Next(0, byte.MaxValue);
 
            var color = new SKColor(r, g, b, 255);
 
            for (int i = 0; i < bmp.Width; i++)
            {
                for (int j = 0; j < bmp.Height; j++)
                {
                    bmp.SetPixel(i, j, color);
                }
            }
 
            using (SKImage image = SKImage.FromPixels(bmp.PeekPixels()))
            {
                var data = image.Encode(SKEncodedImageFormat.Png, 100);
                return data.ToArray();
            }
        }
    }
}

Next, we need to define an "out of the box" ChatItemTemplateSelector that will let us use our own DataTemplate for the chat items. This can initially appear complicated with the number of templates, but it's actually straightforward. I've written in some code comments to clarify

<ContentPage.Resources>
    <converters:SimpleChatItemConverter x:Key="SimpleChatItemConverter" />
 
    <!-- Image is on the left side and it's source is bound to the ExtendedAuthor's ImageSource property-->
    <DataTemplate x:Key="NormalIncomingMessageTemplate">
        <Grid Margin="10">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
 
            <Image Source="{Binding Author.ProfileImageSource}"
                   WidthRequest="30"
                   HeightRequest="30"
                   Grid.Column="0"/>
 
            <primitives:RadBorder CornerRadius="7"
                                  Margin="20,0,0,0"
                                  Padding="5"
                                  Grid.Column="1">
                <Label Text="{Binding Text}" HorizontalTextAlignment="Start"/>
            </primitives:RadBorder>
        </Grid>
    </DataTemplate>
 
    <!-- In this template, the image is on the right side, and the margins adjusted appropriately -->
    <DataTemplate x:Key="NormalOutgoingMessageTemplate">
        <Grid Margin="10">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
 
            <primitives:RadBorder CornerRadius="7"
                                  Margin="0,0,20,0"
                                  Padding="5"
                                  Grid.Column="0">
                <Label Text="{Binding Text}" HorizontalTextAlignment="End"/>
            </primitives:RadBorder>
 
            <Image Source="{Binding Author.ProfileImageSource}"
                   WidthRequest="30"
                   HeightRequest="30"
                   Grid.Column="1" />
        </Grid>
    </DataTemplate>
 
    <!--
    Here is the logic we use to decide which template is going to be applied:
     
    -If there's just one message from that author, we use the "Single" template
    -If there are two messages from that author, we use the "First" and "Last" template
    -If there are three or more, we use the "First", "Middle" and "Last" templates
     
    To keep this demo simple, I use the same template for all incoming messages, and the same template for all outgoing messages.
    You could use clever styling to add rounded corners for different messages
     
    -->
    <telerikConversationalUI:ChatItemTemplateSelector x:Key="SimpleChatItemTemplateSelector"
             IncomingSingleTextMessageTemplate="{StaticResource NormalIncomingMessageTemplate}"
             IncomingFirstTextMessageTemplate="{StaticResource NormalIncomingMessageTemplate}"
             IncomingMiddleTextMessageTemplate="{StaticResource NormalIncomingMessageTemplate}"
             IncomingLastTextMessageTemplate="{StaticResource NormalIncomingMessageTemplate}"
             OutgoingSingleTextMessageTemplate="{StaticResource NormalOutgoingMessageTemplate}"
             OutgoingFirstTextMessageTemplate="{StaticResource NormalOutgoingMessageTemplate}"
             OutgoingMiddleTextMessageTemplate="{StaticResource NormalOutgoingMessageTemplate}"
             OutgoingLastTextMessageTemplate="{StaticResource NormalOutgoingMessageTemplate}"/>
</ContentPage.Resources>


With that in place, you can now set the selector to the RadChat instance:

<Grid>
    <telerikConversationalUI:RadChat x:Name="chat"
                 Author="{Binding Me}"
                 ItemsSource="{Binding Items}"
                 ItemConverter="{StaticResource SimpleChatItemConverter}"
                 ItemTemplateSelector="{StaticResource SimpleChatItemTemplateSelector}"/>
</Grid>

Here's the result at runtime:



I hope this helps.

Regards,
Lance | Technical Support Engineer, Principal
Progress Telerik
Do you want to have your say when we set our development plans? Do you want to know when a feature you care about is added or when a bug fixed? Explore the Telerik Feedback Portal and vote to affect the priority of the items
0
n/a
Top achievements
Rank 1
answered on 26 May 2019, 02:20 PM

Lance,

That was a fantastic response, thankyou! This is the pretty much the exact path I was taking when I saw your post so it's great to get validation on the approach. The only difference I have is that the additional properties on the extended Author class are bindable as in our app the imagesource may already be available, or might require an HTTP request if there is a cache miss so the process is asynchronous and the image may not always be available upfront; beyond that it works the same way.

At first glance I thought it would be a real pain to replicate the original look and feel, but using ilSpy I was able to comb through the compiled resource dictionary and rip enough of the original border radii, colors, margins etc. that it looks perfect, and I'm better placed to make further changes.

The way you deal with the template's wasn't obvious to me, but once you made how that works clear I see it is very straightforward, I can see the template converter simply scans around a bit and compares the authors of the surrounding messages to determine whether this message is a single, first, middle or last which makes complete sense.

regards,

0
n/a
Top achievements
Rank 1
answered on 26 May 2019, 03:52 PM

Hi Lance,

Quick question;

I'm getting some rendering glitches which I thought were my problem, but looking more closely it appears that it happens with the default templates too (especially on iOS).

The first/middle/last template switching is not always correctly respected, and I'm not sure it can work properly this way unless you've done some Telerik magic;

The Xamarin docs about data templates mention under the "limitations" section that a data template selector should always return the same template for the same data item (https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/templates/data-templates/selector#limitations). From what I can see the ChatItemTemplateSelector class potentially breaks this rule.

Have you guys worked around this somehow with the chat control? The Xamarin guys describe the limitation as;

"The DataTemplateSelector subclass must always return the same template for the same data if queried multiple times."

The template selector used by the chat UI doesn't seem to adhere to this, and may be the reason why it sometimes gets a bit glitchy.

 

0
Lance | Senior Manager Technical Support
Telerik team
answered on 28 May 2019, 02:51 PM
Hi Patrick,

Correct, that rule is meant for one-to-one comparison for individual data items without consideration for adjacent items. The feature the development team built for the RadChat is an extra level of customization you can use

You don't have to use our base and can make your own TemplateSelector that either returns a left-aligned template or a right-aligned template. As long as the base is DataTemplateSelector, it should work.

Problem

Ultimately, the guideline is still followed because when certain conditions are met, you'll get the expected template for specific items in that consecutive run.

What you might be seeing is a timing issue. If the logic doesn't have all the items at once, then it may apply the wrong template thinking there are only X numer of consecutive items. Try using an ObservableRangeCollection and when you have multiple items from the author, use AddRange() to add the items all at once so that CollectionChanged event is invoke once (this is used internally to render items).

Further Investigation

If this doesn't help, this will require a Support Ticket so that you can work directly with the support team. Please go to your Support Tickets page and start a new Support Ticket for the RadChat. That will put you directly in contact with the team and they'll get back to you within the 24 hour time frame.

Tip - I highly recommend making sure that you share all the code that you can so we can reproduce the problem. This will drastically reduce the chance that the engineer will need to reply asking for the code (which may cost you more time).

I'm always watching the threads and will leave a note for the team to make sure they review our previous conversations, if needed.

Regards,
Lance | Technical Support Engineer, Principal
Progress Telerik
Do you want to have your say when we set our development plans? Do you want to know when a feature you care about is added or when a bug fixed? Explore the Telerik Feedback Portal and vote to affect the priority of the items
Tags
Chat (Conversational UI)
Asked by
n/a
Top achievements
Rank 1
Answers by
Lance | Senior Manager Technical Support
Telerik team
n/a
Top achievements
Rank 1
Share this question
or