Try our conversational search powered by Generative AI!

Peter Sunna
Nov 19, 2009
  9563
(2 votes)

As seen on Tech Forum: The Twitter channel gadget

Inspired by Twingly channels me, Mats Hellström and Rachel Goldthorpe decided to create a Twitter channel gadget for EPiServer.

The project consist of two parts:

  1. A scheduled job that imports tweets with a specific keyword into a dynamic data store
  2. A gadget that shows the imported tweets and lets the editors comment specific tweets (for other editors to read) as well as search among tweets and their comments

What is this good for? Well, you get to comment what’s being said about for example your brand on Twitter but you only need to share the comments with your loved ones.

Instead of 2000 more words, here are two screenshots:

Twitter channel in action - search 

Twitter channel in action - search and comment

The search result listing is of course updated as you type (to stress test the dynamic data store) and after lots of usability studies we decided that comments should be added by clicking the profile image. When finished typing, hit tab to save the comment.

Now we’re just pulling tweets from Twitter, but his could easily be extended to grab data from other services as well. Think big, think YQL,

The tweet importer

The tweet importer uses the Twitter search API which return Json objects. These objects are serialized into C# objects using the DataContractJsonSerializer that are later pushed into the dynamic data store. Note that we’re using the DataMember attribute to tell which properties the Json serializer should consider, but the attribute EPiServerDataMember to tell which properties that should be saved into the dynamic data store.

[DataContract]
[EPiServerDataContract]
public class TwitterItem
{
    [DataMember(Name = "text", Order = 0)]
    [EPiServerDataMember]
    public string Text { get; set; }
 
    [DataMember(Name = "to_user_id", Order = 1)]
    public int? ToUserId { get; set; }
 
    [DataMember(Name = "from_user", Order = 2)]
    [EPiServerDataMember]
    public string FromUser { get; set; }
 
    [DataMember(Name = "id", Order = 3)]
    [EPiServerDataMember]
    public long TweetId { get; set; }
 
    [DataMember(Name = "from_user_id", Order = 4)]
    public int? FromUserId { get; set; }
 
    [DataMember(Name = "iso_language_code", Order = 5)]
    public string IsoLanguageCode { get; set; }
 
    [DataMember(Name = "profile_image_url", Order = 6)]
    [EPiServerDataMember]
    public string ProfileImageUrl { get; set; }
 
    [DataMember(Name = "created_at", Order = 7)]
    public string CreatedAt { get; set; }
 
    [EPiServerDataMember]
    public DateTime StatusDate { get; set; }
}

Each TwitterItem is embedded in a ChannelItem, which will be the type our dynamic data store will contain . Remember that the dynamic data store can contain one and only one type. The ChannelItem contains the tweet, its comments and a last updated property. Since we want all public properties to go into the dynamic data store we don’t need to use the EPiServerDataContract attribute.

public class ChannelItem
{
    public ChannelItem()
    {
        Comments = new List<String>();
    }
    public ChannelItem(TwitterItem tweet)
    {
        Comments = new List<String>();
        Tweet = tweet;
        LastUpdated = tweet.StatusDate;
    }
    public TwitterItem Tweet { get; set; }
    public System.DateTime LastUpdated { get; set; }
    public List<String> Comments { get; set; }
}

We’re using the TweetId to make sure that we don’t import duplicates of a tweet, meaning that we can skip to implement the IDynamicData interface and its Id property which can be used if you’d like to assign Guid:s to your objects.

private static int AddTweetsToDynamicDataStore(List<TwitterItem> tweets)
{
    DynamicDataStore<ChannelItem> channelStore = 
        DynamicDataStore<ChannelItem>.CreateStore(ChannelStoreName, 
        true);
 
    int importCount = 0;
    foreach (var item in tweets)
    {
        var currentItems = (from c in channelStore where 
                                c.Tweet.TweetId == item.TweetId 
                            select c).ToList();
        if (currentItems.Count > 0)
        {
            continue;
        }
 
        ChannelItem cItem = new ChannelItem(item);
        channelStore.Save(cItem);
        importCount++;
 
    }
 
    return importCount;
}

 

The gadget

The scheduled job makes sure that the Twitter channel is updated regularly with beautiful tweets on a particular subject. It’s time to show the tweets in a gadget and let editors search and make comments. It’s Mvc time, this beautiful little framework that EPiServer SiteCenter is built upon.

Let’s start by looking at the different Views we’ve got:

Tweets

Takes a list of ChannelItems and renders the tweets and their comments (using the Comments View).

Comments

Takes a ChannelItem and renders its comments.

Index

Takes a list of ChannelItems. Renders a search box and then passes the ChannelItems to the Tweets View.

With these Views we just need something that receives requests from the gadget, modifies and/or collects some data in the dynamic data store and then passes it to the right view. We’re talking about at the heart in out twitter gadget, the Controller. These are the main Actions in our Controller:

Index

Responsible for fetching all channel items and passing them to the Index View.

AddComment

Responsible for adding an incoming comment to a tweet and passing the resulting ChannelItem to the Comment View.

public ActionResult AddComment(string comment, string tweetId)
{
    ChannelItem item = (from c in channelStore
                         where c.Tweet.TweetId == long.Parse(tweetId)
                         select c).ToList()[0];
 
    // Add time and user to comment and modify last updated date on tweet
    item.LastUpdated = DateTime.Now;
    item.Comments.Add(string.Format("{0} [by {1} on {2}]", 
        comment, User.Identity.Name, item.LastUpdated.ToTwitterDate()));
    channelStore.Save(item);
 
    // Return all comments for tweet
    return View("Comments", item);
}

GetFilteredTweets

Responsible for fetching all ChannelItems that contains a specific keyword and then passing them to the Tweets View. Pay attention to how we’re using Count to search for a string in the comments collection.

public ActionResult GetFilteredTweets(Guid gadgetId, string searchTerm)
{
    var items = (from c in channelStore
                 where
                     c.Tweet.Text.Contains(searchTerm) ||
                     c.Tweet.FromUser.Contains(searchTerm) ||
                     c.Comments.Count(s => s.Contains(searchTerm)) > 0
                 orderby c.LastUpdated descending
                 select c).ToList();
    return View("Tweets", items);
}

Now we just need to add an event handler to the search box (fire off a request to the GetFilteredTweets Action whenever a user starts typing) as well as an event handler when adding a comment (send comment to AddComment Action). Looking at the code below you’ll see that we’re using the ajax method inside the gadgetContext. This method takes care of sending the gadget id so we don’t need to worry about that.

twitterChannel.init = function(e, gadgetContext) {
    twitterChannel.gadgetInstance = gadgetContext;
 
    $("#searchfield").keyup(function() {
        twitterChannel.filterTweets($(this).val());
    });
    twitterChannel.addCommentClickHandler();
 
};
 
twitterChannel.filterTweets = function(searchTerm) {
    twitterChannel.gadgetInstance.ajax({
        type: "GET",
        url: twitterChannel.gadgetInstance.getActionPath(
            { action: "GetFilteredTweets" }),
        data: "searchTerm=" + searchTerm,
        contentType: "application/json; charset=utf-8",
        dataType: "html",
        success: function(result) {
            $("#twitterchannel").html(result);
            twitterChannel.addCommentClickHandler();
        }
    });
}
 
twitterChannel.addComment = function(target, tweetId, comment) {
    twitterChannel.gadgetInstance.ajax({
        type: "POST",
        url: twitterChannel.gadgetInstance.getActionPath({ action: "AddComment" }),
        data: { tweetId: tweetId, comment: comment },
        dataType: "html",
        success: function(result) {
            $(target).parent().find(".edit").addClass("hidden");
            $(target).html(result);
        }
    });
}

Whenever a tweet is commented its last updated property is updated, making newly commented tweets appear on top.

The gadget also has a setting where the editor can select how many tweets to show. This is handled by the Configure View and the Configure and SaveSettings Actions. The settings are stored in, you guessed it, the dynamic data store.

Happy tweet commenting!

Download

    

Don’t forget to also grab the Scheduled Jobs Monitor gadget by Björn Olsson.

Nov 19, 2009

Comments

Sep 21, 2010 10:32 AM

Nice work!
/ Pelle

Sep 21, 2010 10:32 AM

The download now includes the Css file as well...

Please login to comment.
Latest blogs
Optimizely and the never-ending story of the missing globe!

I've worked with Optimizely CMS for 14 years, and there are two things I'm obsessed with: Link validation and the globe that keeps disappearing on...

Tomas Hensrud Gulla | Apr 18, 2024 | Syndicated blog

Visitor Groups Usage Report For Optimizely CMS 12

This add-on offers detailed information on how visitor groups are used and how effective they are within Optimizely CMS. Editors can monitor and...

Adnan Zameer | Apr 18, 2024 | Syndicated blog

Azure AI Language – Abstractive Summarisation in Optimizely CMS

In this article, I show how the abstraction summarisation feature provided by the Azure AI Language platform, can be used within Optimizely CMS to...

Anil Patel | Apr 18, 2024 | Syndicated blog

Fix your Search & Navigation (Find) indexing job, please

Once upon a time, a colleague asked me to look into a customer database with weird spikes in database log usage. (You might start to wonder why I a...

Quan Mai | Apr 17, 2024 | Syndicated blog