Notification Center inside of SharePoint On-Prem

Sometimes my activities information on mysites are not as important for me because I know what I did on SharePoint portal in past few days.

Instead of that I need Notification Center functionality on SharePoint as we have it on any other social portals like Facebook, Twitter, Instagram etc.
I need in-time information about who wrote comment to my article, my video, my gallery etc.

So I made this functionality available for SharePoint On-Prem few weeks ago and in article below you could find in which way I made this.

First of all, we need separated database named NotificationCenterDB in which we will save notifications and last sync timestamp.

For that reason we need two tables:

  • MyNotifications
  • SyncData
CREATE DATABASE NotificationCenterDB;
GO

USE [NotificationCenterDB]
GO
CREATE TABLE MyNotifications (
    [CommentID] BIGINT NOT NULL PRIMARY KEY,
    [User_RecordID] BIGINT  NOT NULL,
    [Author_RecordID]   BIGINT  NOT NULL,
    [Url] NVARCHAR (2084) NOT NULL,
    [LastModifiedTime] DATETIME DEFAULT (getutcdate()) NOT NULL,
    [Title] NVARCHAR (500) DEFAULT (NULL) NULL,
    [Comment] NVARCHAR (4000) NOT NULL,
    [IsHighPriority] BIT DEFAULT ((0)) NOT NULL
);
GO

USE [NotificationCenterDB]
GO
CREATE TABLE SyncData (
    [ID] INT IDENTITY(1,1) PRIMARY KEY,
    [Table] NVARCHAR (255) NOT NULL,
    [LastSyncTime] DATETIME DEFAULT (getutcdate()) NOT NULL,
);
GO

After that we have to create SharePoint Timer Job which will sync modified records from SharePoint User Profile Synchronization (UPS) Social DB to our NotificationCenterDB.

For that reason we have to add DB connections to our SharePoint Solution Visual Studio Project.  I decided to go LINQ to SQL Classes so we have to create new DBML file.

We have to create two instances -> one for connection to Social DB, second for connection to NotificationCenterDB.

Into DBSPSocial.dbml we have to drop SocialComment and Url tables from Social DB.

Into DBSPNotificationCenter.dbml we have to drop newly created MyNotification and SyncData tables from 
NotificationCenterDB.

After that we can finally create our Timer Job named NotificationCenterTJ inherited from SPJobDefinition abstract class.

class NotificationCenterTJ : SPJobDefinition
{
    private string dbSocialPath = @"Data Source=***";
    private string dbNotificationCenterPath = @"Data Source=***";

    public NotificationCenterTJ() : base() { }

    public NotificationCenterTJ(string jobName, SPService service, SPServer server, SPJobLockType targetType)
        : base(jobName, service, server, targetType) { }

    public NotificationCenterTJ(string jobName, SPWebApplication webApplication)
        : base(jobName, webApplication, null, SPJobLockType.Job)
    {
        this.Title = "Notification Center Timer Job";
    }

    public override void Execute(Guid contentDbId)
    {
        try
        {
            DBSPSocialDataContext dbSocial = new DBSPSocialDataContext(dbSocialPath);
            DBSPNotificationCenterDataContext dbNotificationCenter = new DBSPNotificationCenterDataContext(dbNotificationCenterPath);

            DateTime lastSyncTime = (DateTime)System.Data.SqlTypes.SqlDateTime.MinValue;

            SyncData syncData = null;
            var syncDatas = dbNotificationCenter.SyncDatas.Where(x => x.Table == "MyNotifications");
            if (syncDatas.Count() > 0)
            {
                syncData = syncDatas.First();
                lastSyncTime = syncData.LastSyncTime;
            }

            if (syncData == null)
            {
                syncData = new SyncData();
                syncData.Table = "MyNotifications";
                syncData.LastSyncTime = lastSyncTime;
                dbNotificationCenter.SyncDatas.InsertOnSubmit(syncData);
                dbNotificationCenter.SubmitChanges();
            }

            foreach (var urlItem in dbSocial.Urls)
            {
                string url = urlItem.Url1;

                UserProfile up = null;
                try
                {
                    SPSecurity.RunWithElevatedPrivileges(delegate ()
                    {
                        using (SPSite site = new SPSite(url))
                        {
                            using (SPWeb web = site.OpenWeb())
                            {
                                SPUser author = web.GetFile(url).Author;

                                var pm = new UserProfileManager(SPServiceContext.GetContext(site));
                                up = pm.GetUserProfile(author.LoginName);
                            }
                        }
                    });
                }
                catch { }

                if (up != null)
                {
                    foreach (var komentarItem in dbSocial.SocialComments.Where(x => x.UrlID == urlItem.UrlID && x.LastModifiedTime >= lastSyncTime))
                    {
                        MyNotification myNotification = null;

                        var currComments = dbNotificationCenter.MyNotifications.Where(x => x.CommentID == komentarItem.CommentID);
                        if (currComments.Count() > 0)
                            myNotification = currComments.First();
                        else
                        {
                            myNotification = new MyNotification();
                            myNotification.CommentID = komentarItem.CommentID;
                        }

                        myNotification.User_RecordID = up.RecordId;
                        myNotification.Author_RecordID = komentarItem.User_RecordID;
                        myNotification.Url = url;
                        myNotification.LastModifiedTime = komentarItem.LastModifiedTime;
                        myNotification.Title = komentarItem.Title;
                        myNotification.Comment = komentarItem.Comment;
                        myNotification.IsHighPriority = komentarItem.IsHighPriority;

                        if (currComments.Count() == 0)
                            dbNotificationCenter.MyNotifications.InsertOnSubmit(myNotification);

                        dbNotificationCenter.SubmitChanges();
                    }
                }
            }

            try
            {
                lastSyncTime = dbNotificationCenter.MyNotifications.Select(x => x.LastModifiedTime).OrderByDescending(x => x).First();
            }
            catch { }

            syncData.LastSyncTime = lastSyncTime;
            dbNotificationCenter.SubmitChanges();
        }
        catch (Exception ex)
        {
            Logger.ToLog(ex, "Error NotificationCenterTJ");
        }
    }
}

First of all we have to read last sync time and we sync only new comments from SocialComments table.
We have to read author of specific item (from User Profile Manager) in which comment was posted.
And in the end we have to create new record inside of MyNotifications table where we have to append comment to author of specific item in which comment is posted.

Last thing is rendering of my notifications. I wanted to add it into Suitenav of SharePoint like in image below.

For that reason I have to wait until function _o365sg2c.O365Shell._renderInternal$p from suitenav.js is loaded and then I change _o365sg2c._rightMenusMouseRenderer.render function and add _o365sg2c._rightMenusMouseRenderer._renderNotificationCenter$p function.

ClearSuiteLinksCache();

$(document).ready(function () {
    SP.SOD.executeFunc("suitenav.js", "_o365sg2c.O365Shell._renderInternal$p", function () {

        _o365sg2c._rightMenusMouseRenderer.render = function (j, i) {
            var a = document.createElement("span");
            a.className = "o365cs-nav-rightMenus";
            var b = document.createElement("div");
            b.className = "o365cs-nav-topItem";
            _o365sg2c._rightMenusMouseRenderer._renderShareButton$p(b, i);
            a.appendChild(b);
            var c = document.createElement("div");
            c.className = "o365cs-nav-topItem o365cs-rsp-off-hide o365cs-rsp-m-hide o365cs-rsp-tw-hide o365cs-rsp-tn-hideIfAffordanceOn";
            _o365sg2c._rightMenusMouseRenderer._renderAffordance$p(c);
            a.appendChild(c);
            if (_o365sg2c.O365Shell._$$pf_ShellData$p.WorkloadLinks || _o365sg2c.O365Shell._$$pf_ShellData$p.PinnedApps) {
                var e = document.createElement("div");
                e.className = "o365cs-nav-topItem o365cs-rsp-off-hide o365cs-rsp-m-hide o365cs-rsp-tn-hideIfAffordanceOff";
                _o365sg2c._rightMenusMouseRenderer._renderNavMenu$p(e);
                a.appendChild(e)
            }

            var rr87 = document.createElement("div");
            rr87.className = "o365cs-nav-topItem o365cs-rsp-tn-hideIfAffordanceOff";
            _o365sg2c._rightMenusMouseRenderer._renderNotificationCenter$p(rr87);
            a.appendChild(rr87);

            var d = document.createElement("div");
            d.className = "o365cs-nav-topItem o365cs-rsp-tn-hideIfAffordanceOff";
            _o365sg2c._rightMenusMouseRenderer._renderSettings$p(d);
            a.appendChild(d);
            var g = document.createElement("div");
            g.className = "o365cs-nav-topItem o365cs-rsp-tn-hideIfAffordanceOff";
            _o365sg2c._rightMenusMouseRenderer._renderHelp$p(g);
            a.appendChild(g);

            if (_o365sg2c.O365Shell._$$pf_ShellData$p.UserDisplayName) {
                var h = document.createElement("div");
                h.className = "o365cs-nav-topItem o365cs-rsp-tn-hideIfAffordanceOn";
                _o365sg2c._rightMenusMouseRenderer._renderMe$p(h);
                a.appendChild(h)
            }
            if (_o365sg2c.O365Shell._$$pf_ClientData$p.SignInLink) {
                var f = document.createElement("div");
                f.className = "o365cs-nav-topItem o365cs-rsp-tn-hideIfAffordanceOn";
                _o365sg2c._rightMenusMouseRenderer._renderSignIn$p(f);
                a.appendChild(f)
            }
            j.appendChild(a)
        };

        _o365sg2c._rightMenusMouseRenderer._renderNotificationCenter$p = function (c) {
            var b = _o365sg2c._controls.button();
            b.id = "O365_MainLink_NotificationCenter";
            _o365sg2c._domElementExtensions.addClass(b, "o365cs-nav-item o365cs-nav-button o365cs-topnavText ms-bgc-tdr-h");
            _o365sg2c._domElementExtensions.ariaHasPopup(b, true);
            _o365sg2c._domElementExtensions.ariaLabel(b, "Notification Center");
            _o365sg2c._domElementExtensions.role(b, "menuitem");
            var f = _o365sg2c._controls.icon(new _o365sg2cm.ShellIconId("bell", 18));
            b.appendChild(f);

            var rr87Nr = $("<div id='O365_MainLink_NotificationCenter_Nr' style='display:none; right:7px; top:8px; position:absolute; border-radius:16px; background-color:white; color:black; min-width:16px; height:16px; text-align:center;'></div>")[0];
            b.appendChild(rr87Nr);

            c.id = "O365_MainLink_NotificationCenter_OuterDiv"
            c.appendChild(b);

            $.ajax({
                url: "/_vti_bin/Xnet.Test/TestService.svc/NotificationCenter_GetMyNotifications",
                data: JSON.stringify({
                    outerDivId: c.id
                }),
                type: "POST",
                cache: false,
                dataType: 'json',
                contentType: "application/json; charset=utf-8"
            })
            .done(function (data) {
                if (data.Data.nrOfNewNotifications > 0) {
                    var rr87Nr = $("div#O365_MainLink_NotificationCenter_Nr");
                    rr87Nr.css("display", "block");
                    rr87Nr.text(data.Data.nrOfNewNotifications);
                }

                var a = _o365sg2c._controls.contextMenu(b);
                a.style[_o365sg2c.O365Shell._$$pf_ClientData$p.IsRTL ? "left" : "right"] = "-1px";
                a.style["max-width"] = "460px";

                console.log(data.Data.render);
                var rr87 = $("<div style='padding:0px; margin:10px; font-size:12px; text-align:left;'>" + data.Data.render + "</div>")[0];
                a.appendChild(rr87);

                c.style.position = "relative";
                c.appendChild(a)
            });
        };
    });
});

As you can see in code above I added new div element which is rendered inside of _renderNotificationCenter$p function.
Inside of this function I call NotificationCenter_GetMyNotifications from my custom WebService function which return html for all my notifications.

Here you can see NotificationCenter_GetMyNotifications function mentioned above:

public ServiceResult<MyNotificationsWithRender> NotificationCenter_GetMyNotifications(string outerDivId)
{
    var serviceResult = ServiceResult<MyNotificationsWithRender>.Default;
    MyNotificationsWithRender _notfyWithRender = new MyNotificationsWithRender();
    serviceResult = ServiceResult<MyNotificationsWithRender>.Get(_notfyWithRender);

    try
    {
        var myNotifications = NotificationCenter.GetMyNotifications();
        _notfyWithRender.nrOfNewNotifications = myNotifications.nrOfNewNotifications;
        _notfyWithRender.render = NotificationCenter.RenderMyNotifications(myNotifications, outerDivId);

        serviceResult = ServiceResult<MyNotificationsWithRender>.Get(_notfyWithRender);
        return serviceResult;
    }
    catch (Exception _ex)
    {
        serviceResult = ServiceResult<MyNotificationsWithRender>.Error(_ex.Message);
    }

    return serviceResult;
}

Here is MyNotificationsWithRender class which is response to NotificationCenter_GetMyNotifications function call:

public class MyNotificationsWithRender
{
    public int nrOfNewNotifications { get; set; }
    public string render { get; set; }
}

Here is GetMyNotifications() function which is called from 
NotificationCenter_GetMyNotifications to get all my notifications:

public static MyNotifications GetMyNotifications()
{
    MyNotifications returnV = new MyNotifications();

    try
    {
        SPSite cSite = SPContext.Current.Site;
        SPUser cUser = SPContext.Current.Web.CurrentUser;

        SPSecurity.RunWithElevatedPrivileges(delegate ()
        {
            var profileManager = new UserProfileManager(SPServiceContext.GetContext(cSite));
            var cUserProfile = profileManager.GetUserProfile(cUser.LoginName);

            returnV = GetMyNotifications(cUserProfile.RecordId);
        });
    }
    catch (Exception ex)
    {
        Logger.ToLog(ex, "Error GetMyNotifications(SPContext)");
    }

    return returnV;
}

private static MyNotifications GetMyNotifications(long cUser_RecordID)
{
    MyNotifications returnV = new MyNotifications();

    try
    {
        if (!String.IsNullOrEmpty(dbNotificationCenterPath))
        {
            DBSPNotificationCenterDataContext dbNotificationCenter = new DBSPNotificationCenterDataContext(dbNotificationCenterPath);

            returnV.myNotifications = dbNotificationCenter.MyNotifications.Where(x => x.User_RecordID == cUser_RecordID && x.Author_RecordID != cUser_RecordID).OrderByDescending(x => x.LastModifiedTime);

            returnV.nrOfNewNotifications = returnV.myNotifications.Where(x => x.User_RecordID == cUser_RecordID && x.Author_RecordID != cUser_RecordID && x.LastModifiedTime >= new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, 0, 0, 0).AddDays(-2)).Count();
        }
    }
    catch (Exception _ex)
    {
        Logger.ToLog(_ex, "Error GetMyNotifications(long)");
    }

    return returnV;
}

And here is RenderMyNotifications function which
is called from NotificationCenter_GetMyNotifications to make html for all my notifications:

public static string RenderMyNotifications(MyNotifications myNotifications, string outerDivId)
{
    List<string> returnV = new List<string>();

    try
    {
        returnV.Add("<div style='left: 0px; right: 0px;'><div class='ms-microfeed-threadsDiv'>");

        var pm = new UserProfileManager(SPServiceContext.GetContext(SPContext.Current.Site));

        if (myNotifications.myNotifications != null && myNotifications.myNotifications.Count() > 0)
        {
            int cID = 1;
            foreach (var myNotification in myNotifications.myNotifications)
            {
                var up = pm.GetUserProfile(myNotification.Author_RecordID);

                var webUrl = new SPSite(myNotification.Url).OpenWeb().Url;

                returnV.Add("<div class='ms-microfeed-thread'><div class='ms-microfeed-rootDiv'><div class='ms-microfeed-message'><div>");

                returnV.Add("<div class='ms-microfeed-userThumbnailArea ms-microfeed-userThumbnailAreaRootPadding'><div class='ms-table ms-core-tableNoSpace'><div class='ms-tableRow'><div class='ms-tableCell'><span class='ms-imnSpan'><a href='#' onclick='WriteDocEngagementLog("DocModifiedByPresenceClick", "ODModifiedByPresenceClick"); IMNImageOnClick(event);return false;' class='ms-imnlink ms-spimn-presenceLink' tabindex='-1'><span class='ms-spimn-presenceWrapper ms-spimn-imgSize-5x48'><img name='imnmark' title='' showofflinepawn='1' class='ms-spimn-img ms-spimn-presence-disconnected-5x48x32' src='/_layouts/15/images/spimn.png?rev=40' alt='' sip='" + up.SipAddress + "' id='imn_" + cID + ",type=smtp'></span></a></span></div><div class='ms-tableCell ms-verticalAlignTop'><div class='ms-peopleux-userImgDiv'><span class='ms-imnSpan'><a href='#' onclick='WriteDocEngagementLog("DocModifiedByPresenceClick", "ODModifiedByPresenceClick"); IMNImageOnClick(event);return false;' class='ms-imnlink' tabindex='-1'><img name='imnmark' title='' showofflinepawn='1' class=' ms-hide' src='/_layouts/15/images/spimn.png?rev=40' alt='' sip='" + up.SipAddress + "' id='imn_" + (cID + 1) + ",type=smtp'></a><a class='ms-subtleLink ms-peopleux-imgUserLink' onclick='WriteDocEngagementLog('DocModifiedByNameClick', 'ODModifiedByNameClick'); if(typeof(WriteSearchClickLog) != 'undefined'){ WriteSearchClickLog(event); }; GoToLinkOrDialogNewWindow(this);return false;' href='" + up.PersonalUrl + "'><span class='ms-peopleux-userImgWrapper' style='width:48px; height:48px'><img class='ms-peopleux-userImg' style='min-width:48px; min-height:48px; clip:rect(0px, 48px, 48px, 0px); max-width:48px' src='" + (!String.IsNullOrEmpty(up.PictureUrl) ? up.PictureUrl : "/_layouts/15/images/PersonPlaceholder.42x42x32.png?rev=40") + "' alt='" + up.DisplayName + "'></span></a></span></div></div></div></div></div>");

                returnV.Add("<div class='ms-microfeed-rootBody'>");

                returnV.Add("<div class='ms-microfeed-rightAlignedDiv'><div><div placeholderdivforhiddenelement='true' class='ms-microfeed-deleteDiv' style='width: 24px; height: 24px;'></div><div class='ms-microfeed-deleteDiv ms-hidden' hashover='false' hasfocus='false' fixedwidth='24px' fixedheight='24px' style=''><button title='Hide this activity' class='ms-microfeed-button ms-microfeed-deleteButton' type='button'><span class='ms-microfeed-deleteButtonImageParent'><img src='/_catalogs/theme/Themed/61FA4673/spcommon-B35BB0A9.themedpng?ctag=9' class='ms-microfeed-deleteButtonImage'></span></button></div></div></div>");

                returnV.Add("<div class='ms-microfeed-text ms-microfeed-rootText'><span class='ms-microfeed-userName ms-textLarge ms-subtleLink'><span class='ms-noWrap ms-imnSpan'><a href='#' onclick='WriteDocEngagementLog("DocModifiedByPresenceClick", "ODModifiedByPresenceClick"); IMNImageOnClick(event);return false;' class='ms-imnlink' tabindex='-1'><img name='imnmark' title='' showofflinepawn='1' class=' ms-hide' src='/_layouts/15/images/spimn.png?rev=40' alt='' sip='" + up.SipAddress + "' id='imn_" + (cID + 2) + ",type=smtp'></a><a class='ms-subtleLink' onclick='WriteDocEngagementLog('DocModifiedByNameClick', 'ODModifiedByNameClick'); if(typeof(WriteSearchClickLog) != 'undefined'){ WriteSearchClickLog(event); }; GoToLinkOrDialogNewWindow(this);return false;' href='" + up.PersonalUrl + "'>" + up.DisplayName + "</a></span></span><br><span class='ms-microfeed-postBody ms-textSmall'><span id='ms-actorElement' class='ms-bold ms-subtleLink'><span class='ms-noWrap ms-imnSpan'><a href='#' onclick='WriteDocEngagementLog("DocModifiedByPresenceClick", "ODModifiedByPresenceClick"); IMNImageOnClick(event);return false;' class='ms-imnlink' tabindex='-1'><img name='imnmark' title='' showofflinepawn='1' class=' ms-hide' src='/_layouts/15/images/spimn.png?rev=40' alt='' sip='" + up.SipAddress + "' id='imn_" + (cID + 3) + ",type=smtp'></a><a class='ms-subtleLink' onclick='WriteDocEngagementLog('DocModifiedByNameClick', 'ODModifiedByNameClick'); if(typeof(WriteSearchClickLog) != 'undefined'){ WriteSearchClickLog(event); }; GoToLinkOrDialogNewWindow(this);return false;' href='" + up.PersonalUrl + "'>" + up.DisplayName + "</a></span></span> posted a note on <a id='ms-externalLink' class='' href='" + myNotification.Url + "' target='_blank'>" + myNotification.Title + "</a>:<br><a id='ms-externalLink' class='' href='" + webUrl + "/_layouts/15/socialdataframe.aspx?Url=" + HttpUtility.UrlEncode(myNotification.Url) + "&Title=" + HttpUtility.UrlEncode(myNotification.Title) + "&mode=1' target='_blank'>Več o tem</a></span></div>");

                returnV.Add("<div class='ms-microfeed-messageFooter'><div class='ms-microfeed-likesIndicatorText ms-metadata ms-link ms-hide' numlikers='0'><span><span class=''></span></span></div><span class='ms-metadata'><span class='ms-microfeed-postedTime'>" + SPRelativeDateTime.GetRelativeDateString(SPContext.Current.Web, DateTime.Now, myNotification.LastModifiedTime) + "</span></span></div>");

                returnV.Add("<div class='ms-clear'></div>");

                returnV.Add("</div>");

                returnV.Add("</div></div></div></div>");

                cID += 4;
            }
        }
        else
        {
            returnV.Add("<div style='text-align:center; padding-top:5px;'>Nimate nobenih obvestil</div>");
        }

        returnV.Add("</div></div>");

        returnV.Add("<style>div#" + outerDivId + " div.ms-microfeed-thread{ margin: 5px 0px; padding: 5px 10px; }</style>");

        if (myNotifications.nrOfNewNotifications > 0)
        {
            returnV.Add("<style>div#" + outerDivId + " div.ms-microfeed-thread:nth-child(-n+" + myNotifications.nrOfNewNotifications + "){ background-color: #fff19d; border: 1px #d7d889 solid; }</style>");
        }
    }
    catch (Exception ex)
    {
        Logger.ToLog(ex, "Error RenderMyNotifications");
    }

    return String.Join("", returnV.ToArray());
}

Happy coding folks!

Cheers!
Gašper Rupnik

{End.}

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Powered by WordPress.com.

Up ↑

%d bloggers like this: