Gitlab Mods

Adds colored sections, extra functionality, and avatars to Gitlab. For avatars, detects locally-running image server to use for replacements of avatars. Use http-server (https://www.npmjs.com/package/http-server) for node.js for simple image hosting. Recommend image size of 100x100.

Fra 14.06.2019. Se den seneste versjonen.

// ==UserScript==
// @name			Gitlab Mods
// @namespace		COMDSPDSA
// @version			3.9
// @description		Adds colored sections, extra functionality, and avatars to Gitlab. For avatars, detects locally-running image server to use for replacements of avatars. Use http-server (https://www.npmjs.com/package/http-server) for node.js for simple image hosting. Recommend image size of 100x100.
// @author			Dan Overlander
// @include			*/gitlab.dell.com*
// @ nope include         *pivotaltracker.com*
// @require			https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
// @require         https://greasyfork.org/scripts/23115-tampermonkey-support-library/code/Tampermonkey%20Support%20Library.js?version=698066
// @require		    https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.5.2/underscore-min.js
// @require         https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment-with-locales.min.js
// @grant           GM_setValue
// @grant           GM_getValue

// ==/UserScript==

// TODO: finish installButton method.
// Since v03.80: Expanded bot detection to include array created via regex
// Since v03.70: Optimized avatar location methods
// Since v03.60: Button bug fixes. Renamed comparisonSize. Moved default- and small- font sizes to userprefs. Hides the "nothing here block" divs. Scrolls to collapsed div when tall divs are collapsed. All states moved to global.states.  Avatar ping functionality disabled if server isn't running.
// Since v03.50: argh. They change the DOM, breaking the expand/collapse functions. Fixed and in a more flexible manner.
// Since v03.30: Gitlab changed a classname, which broke the Discussions button. Fixed in a more flexible manner.
// Since v03.20: Moving .fingery class to global script. Moving out setTamperIcon
// Since v03.10: Made comparisonSize font alteration more inclusive. Filtering out "Americas" from avatar names. Removes vertical margin from fingery class
// Since v03.00: Set title TFS/PT scan to use case-insensivite checks
// Since v02.20: Links TFS and PT values in title to their respective URLs. Adds Font+/- button for comparison text
// Since v02.10: Since Gitlab removed their Expand All button (?!), this adds it back in...
// Since v02.00: Altered the logic of colorizing discussions to search for a more universal pass-fail value. Changes Discussions buttons from icon to text (and is therefore larger)
// Since v01.81: Removed old coffee cup button code. Added Collapse All Button with diff-collapsing function. Bug tweaks.
// Since v01.80: Some conversations were not being colorized.
// Since v01.75: Removes coffee-cup as it is native functionality, now. Yay Gitlab! Fixes a name-finding method that sometimes forgot to add a comma after spaces, for avatar images.
// Since v01.7: Fixes the click functionality of the tamperlabel, also makes it look clickable
// Since v01.6: Adds ability to hide header messages. Adds ability to erase Gitlab Mods memory by clicking on Tampermonkey icon (on this page, a battery in the lower-right corner)
// Since v01.5: Added a conditional close-footer button, close broadcast message button.  Adds memory states for these buttons (only for messages already seen!) as well as the close-tree (coffee) button. Expands breadcrumb dropdown.
// Since v01.4: Linked the notifications to the TODO page. Removed a log statement. Resets TODO page title when timer runs out if reload is cancelled. Slows down reminder by 1 hour (up to 8 times) because coming back to your computer after an extended period and receiving TONS of notifications is annoying
// Since v01.3: Re-enables the Tampericon
// Since v01.2: Adds "hide tree" button (looks like a coffee cup)
// Since v01.1: Updates document title with countdown timer indication.  Adds hourly reminder of existing TODOs. Removes conversation button from non-conversation pages.
// Since v01.0: Added the countdown indicator to the page title for TODO indication; cancelling reverts the title to original
// Since v00.0: init, copying from GIT Avatars script

/*
 * tm is an object included via @require from DorkForce's Tampermonkey Assist script
 */

(function() {
    'use strict';

    var TIMEOUT = 750,
        avatarHost = 'localhost:8080/', // for setPhoto
        pingPhoto = '!none', // pinging for setPhoto
        imageExt = '.png',
        global = {
            ids: {
                scriptName: 'Gitlab Mods',
                prefsName: 'gitlabPrefs',
                triggerElements: ['.content', '#root']
            },
            states: {
                debugMode: true,
                appendClickProcessed: false,
                // TODO: Move other states here
                areClassesAdded: false,
                arePrButtonsAdded: false,
                areInterfaceButtonsAdded: false,
                todoMonitorInitialized: false,
                titlesLinked: false,
                isMouseMoved: false,
                avatarPingFailed: false
            },
            prefs: undefined,
            reload: {
                lapsed: 0,
                timespan: 10000,
                title: document.title,
                todoTimer: undefined
            }
        },
        properName = function(thisName) {
            var firstName = '',
                lastName = '',
                midName = '';

            thisName = thisName
                .replace('https://gitlab.dell.com/', '')
                .replace(' - Dell Team', '')
                .replace('\'s avatar', '')
                .replace('@', '')
                .replace(/@/g, '')
                .replace(/\//g, '')
                .replace(/_/g, '-');
            firstName = thisName.substring(0, thisName.indexOf('-'));
            lastName = thisName.substring(thisName.indexOf('-')+1, thisName.length);
            if ((firstName.length === 0 || lastName.length === 0) && thisName.indexOf(',') < 0) {
                return;
            }
            if (thisName.indexOf(',') < 0) {
                thisName = lastName + ', ' + firstName;
            }
            if (thisName.indexOf('-') > 0) {
                midName = thisName.substring(0, thisName.indexOf('-'));
                thisName = thisName.substring(thisName.indexOf('-')+1, thisName.length);
                thisName = thisName + ' ' + midName;
            }
            thisName = thisName
                .replace(/(\r\n\t|\n|\r\t)/gm,'') // no line breaks or tabs
                .replace(/\s\s/, '') // no double spaces
                .replace(/ ,/, ',') // no spaces before commas
                .replace(/\%20/, '') // no %20 characters
                .replace(/Americas/g, '')
                .trim(); // seriously, no extra spaces
            thisName = thisName.replace(',', ', ').replace('  ', ' '); // there's probably a less-stupid way of REALLY making sure it's always "COMMA SPACE"
            return thisName;
        },
        updateImg = function(img, thisName) {
            if (thisName != null) {
                if (thisName !== ', ') {
                    $(img).prop('src', 'http://' + avatarHost + thisName + imageExt);
                } else {
                    tm.log('updateImg: invalid user name for ' + img.src + ': ' + thisName + '(' + thisName.length + ' chars)');
                }
            }
        },
        page = {
            initialize: function () {
                setTimeout(function () {
                    page.setPrefs();
                    page.addClasses();
                    page.adjustStyles();
                    tm.setTamperIcon(global.ids.scriptName, global.ids.prefsName, global.prefs);
                    page.addButtons();
                    page.appendClickFunctions();
                    page.expandBreadcrumb();
                    page.setAvatars();
                    page.monitorTodos();
                    page.linkWorkItems();
                }, TIMEOUT);
            },
            setPrefs: function() {
                global.prefs = GM_getValue(global.ids.prefsName) != null ? JSON.parse(GM_getValue(global.ids.prefsName)) : {};
                if (global.prefs.todosCount == null) global.prefs.todosCount = '0';
                if (global.prefs.todosTimestamp == null) global.prefs.todosTimestamp = moment();
                if (global.prefs.reloadTimesReminded == null) global.prefs.reloadTimesReminded = '0';
                if (global.prefs.reloadReminderCount == null) global.prefs.reloadReminderCount = '0';
                if (global.prefs.archivedMessages == null || typeof global.prefs.archivedMessages === 'string') global.prefs.archivedMessages = [];
                if (global.prefs.archivedBroadcasts == null || typeof global.prefs.archivedMessages === 'string') global.prefs.archivedBroadcasts = [];
                if (global.prefs.currentFontSize == null) global.prefs.currentFontSize = global.defaultLineSize;
                if (global.prefs.defaultLineSize == null) global.prefs.defaultLineSize = '12px';
                if (global.prefs.smallLineSize == null) global.prefs.smallLineSize = '10px';
                $('.line, .line span').css('font-size', global.prefs.currentFontSize);
            },
            addClasses: function () {
                if (!global.states.areClassesAdded) {
                    global.states.areClassesAdded = true;

                    // generic
                    tm.addGlobalStyle('.tamperlabel { cursor: pointer; }');
                    tm.addGlobalStyle('.btn-headerly { padding:3px 10px; height:30px; margin-top:5px; background:#414187; border-color:darkslategray; }');
                    tm.addGlobalStyle('.btn-headerly:hover { background-color:steelblue; border-color:black; }');

                    // pivotaltracker
                    tm.addGlobalStyle('.hbsAvatar__initials .tmAvatar { position:relative; width:25px; height:25px; left:-14px; }');

                    // colored backgrounds
                    tm.addGlobalStyle('.merge-request-tabs-container {background:bisque; }');
                    tm.addGlobalStyle('.merge-request-tabs {background:burlywood; }');
                    tm.addGlobalStyle('.mr-widget-content {background:cornflowerblue; }');

                    // UI-sizing
                    tm.addGlobalStyle('.approvals-required-text .avatar {width:24px; height:24px; }');
                    tm.addGlobalStyle('.note-text ul {margin:0px !important; }');
                    tm.addGlobalStyle('.system-note {font-size:0.7em; padding:0; margin:10px 0px 0px 0px; }');
                    tm.addGlobalStyle('.timeline-entry:hover {background:aliceblue; }');
                    tm.addGlobalStyle('.note-text .gfm-merge_request {background:aliceblue; padding:10px; float:right; position:relative; top:-23px; margin-bottom:-28px; }');

                    tm.addGlobalStyle('.tamperNewIcon {position:relative; top:-10px; }');

                    tm.addGlobalStyle('.nothing-here-block {display: none;}');
                }
            },
            adjustStyles: function() {
                var lePass = 'palegreen',
                    leFail = 'orange',
                    botDivs = [],
                    botDivsRegex = new RegExp("Gitbot|ServiceCOMTeamCity");

                botDivs = $(".discussion-notes .timeline-content").filter(function () {
                    return botDivsRegex.test($(this).text());
                });

                botDivs.each(function(discussion) {
                    if ($(this).closest('.card').find('button:contains("Resolve discussion")').length > 0) {
                        $(this).css('background-color', leFail);
                    } else {
                        if ($(this).text().indexOf('Resolved') > 0) {
                            $(this).css('background-color', lePass);
                        }
                    }
                });

                $('.timeline-entry .discussion-header').each(function(discussion) {
                    //
                    $(this).closest('.timeline-entry :contains("Resolve discussion")').find('.discussion-header').css('background-color', leFail);
                    $(this).closest('.timeline-entry :contains("Resolved")').css('background-color', lePass);
                });
            },
            addButtons: function() {
                $('button:contains("Expand all")').hide();
                var conversationScrollTarget = $('.js-mr-approvals').length > 0 ? $('.js-mr-approvals') :
                    $('button:contains("Approve")').parent().length > 0 ? $('button:contains("Approve")').parent() :
                    $('strong:contains("Merge request approved")').parent().parent();
                var idConversations = 'idConversations',
                    conversationsClass = '.header-new',
                    addConversationsButton = function () {
                        var buttonAnchor = $(conversationsClass),
                            conversationsAction = function () {
                                $('html, body').animate({ scrollTop: conversationScrollTarget.offset().top -100 }, 500);
                                return false;
                            };
                        buttonAnchor.before('<a id="' + idConversations + '" class="btn btn-default btn-headerly fingery">Discussions</a>');
                        $('#' + idConversations).click(conversationsAction);
                    },
                    addCollapseExpandButtonToDom = function(bId, bAnchor, bAction, bText) {
                        bAnchor.after('<a id="' + bId + '" class="btn btn-default append-right-8 fingery">' + bText + '</a>');
                        $('#' + bId).unbind('click').click(bAction);
                    },

                    idCollapse = 'idCollapse',
                    collapseClass = '.is-compare-versions-header',
                    addCollapseButton = function() {
                        var addCollapseButtonToDom = function () {
                            addCollapseExpandButtonToDom(idCollapse, buttonAnchor, collapseAction, 'Collapse All');
                        },
                            buttonAnchor = $(collapseClass),
                            collapseAction = function () {
                                _.each($('.diff-content'), (diff) => {
                                    if ($(diff).height() > 0) {
                                        $(diff).closest('.diff-file').find('.file-title').click();
                                    }
                                });
                                $(this).remove();
                                addExpandButton();
                            };
                        addCollapseButtonToDom();
                        $('a:contains("Expand all")').hide();
                    },

                    idExpand = 'idExpand',
                    expandClass = '.is-compare-versions-header',
                    addExpandButton = function() {
                        var addExpandButtonToDom = function () {
                            addCollapseExpandButtonToDom(idExpand, buttonAnchor, expandAction, 'Expand All');
                        },
                            buttonAnchor = $(expandClass),
                            expandAction = function () {
                                _.each($('.diff-collapsed'), (diff) => {
                                    $(diff).closest('.diff-file').find('.file-title').click();
                                });
                                $(this).remove();
                                addCollapseButton();
                            };
                        addExpandButtonToDom();
                    },

                    idComparisonFont = 'idComparisonFont',
                    comparisonFontClass = '.is-compare-versions-header',
                    addComparisonFontButton = function() {
                        var buttonAnchor = $(comparisonFontClass),
                            comparisonFontAction = function () {
                                var buttonText = $('#' + idComparisonFont).text();
                                if (global.prefs.currentFontSize === global.prefs.defaultLineSize) {
                                    global.prefs.currentFontSize = global.prefs.smallLineSize;
                                    $('#' + idComparisonFont).text(buttonText.replace('-', '+'));
                                } else {
                                    global.prefs.currentFontSize = global.prefs.defaultLineSize;
                                    $('#' + idComparisonFont).text(buttonText.replace('+', '-'));
                                    // actual setting of font size done during monitoring of page, so mouse movement can trigger it
                                }
                                tm.savePreferences(global.ids.prefsName,global.prefs);
                            };
                        buttonAnchor.after('<a id="' + idComparisonFont + '" class="btn btn-default append-right-8 fingery">Font -</a>');
                        $('#' + idComparisonFont).unbind('click').click( comparisonFontAction);
                        if (global.prefs.currentFontSize !== global.prefs.defaultLineSize) { // to fix UI if button was previously saved in alternate setting
                            global.prefs.currentFontSize = global.prefs.defaultLineSize;
                            comparisonFontAction();
                        }
                    },

                    idHeader = 'idHeader',
                    headerClass = '.header-message',
                    addHeaderButton = function() {
                        var buttonAnchor = $(headerClass),
                            headerAction = function () {
                                var msg = $(headerClass + ' p').text();
                                if (!_.contains(global.prefs.archivedMessages, msg)) {
                                    global.prefs.archivedMessages.push(msg);
                                    tm.savePreferences(global.ids.prefsName,global.prefs);
                                }
                                buttonAnchor.hide();
                                // special for header
                                $('.navbar').css('top', '0px');
                                $('.nav-sidebar').css('top', '40px');
                                $('.content-wrapper').css('margin-top', '40px');
                                return false;
                            };
                        buttonAnchor.prepend('<i id="' + idHeader + '" class="fa fa-times outlined fingery"></i>');
                        $('#' + idHeader).click(headerAction);
                        // if a previously-hidden header message is showing again
                        if ($(headerClass).css('display') !== 'none') {
                            _.each(global.prefs.archivedMessages, function(msg) {
                                if ($(headerClass + ' p').text() == msg) {
                                    headerAction();
                                }
                            });
                        }
                    },

                    idFooter = 'idFooter',
                    footerClass = '.footer-message',
                    addFooterButton = function() {
                        var buttonAnchor = $(footerClass),
                            footerAction = function () {
                                var msg = $(footerClass + ' p').text();
                                if (!_.contains(global.prefs.archivedMessages, msg)) {
                                    global.prefs.archivedMessages.push(msg);
                                    tm.savePreferences(global.ids.prefsName,global.prefs);
                                }
                                buttonAnchor.hide();
                                return false;
                            };
                        buttonAnchor.prepend('<i id="' + idFooter + '" class="fa fa-times outlined fingery"></i>');
                        $('#' + idFooter).click(footerAction);
                        // if a previously-hidden footer message is showing again
                        if ($(footerClass).css('display') !== 'none') {
                            _.each(global.prefs.archivedMessages, function(msg) {
                                if ($(footerClass + ' p').text() == msg) {
                                    footerAction();
                                }
                            });
                        }
                    },

                    idBroadcast = 'idBroadcast',
                    broadcastClass = '.broadcast-message',
                    addBroadcastButton = function() {
                        var buttonAnchor = $(broadcastClass),
                             broadcastAction = function () {
                                var msg = $(broadcastClass + ' p').text();
                                if (!_.contains(global.prefs.archivedBroadcasts, msg)) {
                                    global.prefs.archivedBroadcasts.push(msg);
                                    tm.savePreferences(global.ids.prefsName,global.prefs);
                                }
                                buttonAnchor.hide();
                                return false;
                            };
                        buttonAnchor.prepend('<i id="' + idBroadcast + '" class="fa fa-times outlined fingery"></i>');
                        $('#' + idBroadcast).click( broadcastAction);
                        // if a previously-hidden  broadcast message is showing again
                        if ($( broadcastClass).css('display') !== 'none') {
                            _.each(global.prefs.archivedBroadcasts, function(msg) {
                                if ($( broadcastClass + ' p').text() == msg) {
                                     broadcastAction();
                                }
                            });
                        }
                    };

                if (document.URL.indexOf('merge_requests') < 0) {
                    $('#' + idConversations).remove();
                    $('#' + idCollapse).remove();
                    $('#' + idExpand).remove();
                    global.states.arePrButtonsAdded = false;
                } else {
                    if (!global.states.arePrButtonsAdded) {
                        global.states.arePrButtonsAdded = true;

                        tm.getContainer({
                            'el': conversationsClass,
                            'max': 100,
                            'spd': 1000
                        }).then(function($container){
                            addConversationsButton();
                        });

                        tm.getContainer({
                            'el': collapseClass,
                            'max': 100,
                            'spd': 1000
                        }).then(function($container){
                            addComparisonFontButton();
                            addCollapseButton();
                        });

//                         tm.getContainer({ // shares same container as collapse button; dont need
//                             'el': comparisonFontClass,
//                             'max': 100,
//                             'spd': 1000
//                         }).then(function($container){
//                             addComparisonFontButton();
//                         });

                    }
                }

                if (!global.states.areInterfaceButtonsAdded) {
                    global.states.areInterfaceButtonsAdded = true;

                    tm.getContainer({
                        'el': headerClass,
                        'max': 100,
                        'spd': 1000
                    }).then(function($container){
                        addHeaderButton();
                    });

                    tm.getContainer({
                        'el': footerClass,
                        'max': 100,
                        'spd': 1000
                    }).then(function($container){
                        addFooterButton();
                    });

                    tm.getContainer({
                        'el': broadcastClass,
                        'max': 100,
                        'spd': 1000
                    }).then(function($container){
                        addBroadcastButton();
                    });
                }

            },
            appendClickFunctions: function() {
                function appendFileTitleClicks() {
                    if (!global.states.appendClickProcessed) {
                        global.states.appendClickProcessed = true;
                        var scrollAfterCollapse = function() {
                            if ($(this).offset().top < $(window).scrollTop() + 125) {
                                $('html, body').animate({ scrollTop: $(this).offset().top-160 }, 1000);
                            }
                        };
                        $('.file-title').on('click', scrollAfterCollapse);
                    }
                };

                tm.getContainer({
                    'el': '.file-title',
                    'max': 100,
                    'spd': 1000
                }).then(function($container){
                    appendFileTitleClicks();
                });

            },
            expandBreadcrumb: function() {
                $('.breadcrumbs .dropdown li').prependTo('.breadcrumbs-list .dropdown');
                $('.breadcrumbs-list .dropdown button').hide();
            },
            setAvatars: function () {
                if (!global.states.avatarPingFailed) {
                    tm.ping(avatarHost + pingPhoto + imageExt, function callback (response) {
                        if (response === 'responded') {
                            var avatarArray = [],
                                thisName = 'none';

                            // pivotaltracker
                            //                         tm.getContainer({
                            //                             'el': '.hbsAvatar__initials'
                            //                         }).then(function($container){
                            //                             _.each($('.hbsAvatar__initials'), function (span) {
                            //                                 if ($(span).parent().find('.tmAvatar').length === 0) {
                            //                                     $(span).append('<img class="tmAvatar" />');
                            //                                 }
                            //                                 thisName = properName( $(span).closest('.row').find('.hbsAvatarName').text().replace(/ /g, '-') );
                            //                                 if (thisName != null) {
                            //                                     updateImg($(span).parent().find('.tmAvatar'), thisName);
                            //                                 }
                            //                             });
                            //                         });

                            // gitlab
                            var avatars = [
                                {
                                    element: '.header-user-avatar', // main header (user) image
                                    getImgSource: null,
                                    getNameSource: function(el) { return $('.user-name').text(); }
                                },{
                                    element: '.btn-link .author', // right side-bar (collapsed) author
                                    getImgSource: function(el) { return $(el).parent().find('img'); },
                                    getNameSource: function(el) { return $(el).next().text(); }
                                },{
                                    element: '.participants-author img', // right side-bar, list of participants
                                    getImgSource: null,
                                    getNameSource: function(el) { return $(el).next().find('div').text(); }
                                },{
                                    element: '.issuable-meta .author-link img', // Opened X hours ago by...
                                    getImgSource: null,
                                    getNameSource: function(el) { return $(el).next().text(); }
                                },{
                                    element: '.author-link.inline img', // Merged by...
                                    getImgSource: null,
                                    getNameSource: function(el) { return $(el).parent().prop('title'); }
                                },{
                                    element: '.user-avatar-link img', // Approvers
                                    getImgSource: null,
                                    getNameSource: function(el) { return $(el).prop('alt'); }
                                },{
                                    element: '.header-user-dropdown-global img', // ????????
                                    getImgSource: null,
                                    getNameSource: function(el) { return $(el).parent().prop('href'); }
                                },{
                                    element: '.avatar-cell img', // ????????
                                    getImgSource: null,
                                    getNameSource: function(el) { return $(el).prop('title'); }
                                },{
                                    element: '.avatar-holder img', // ????????
                                    getImgSource: null,
                                    getNameSource: function(el) { return $(el).parent().parent().next().find('.cover-title').text(); }
                                },{
                                    element: '.list-item-name img', // ????????
                                    getImgSource: null,
                                    getNameSource: function(el) { return $(el).next().find('.member').text(); }
                                },{
                                    element: '.user-popover img', // ????????
                                    getImgSource: null,
                                    getNameSource: function(el) { return $(el).parent().parent().next().find('h5').text(); }
                                },{
                                    element: '.system-note-image img', // ????????
                                    getImgSource: null,
                                    getNameSource: function(el) { return $(el).parent().prop('href'); }
                                },{
                                    element: '.assignee .author-link img', // right side-bar, assigned-to
                                    getImgSource: null,
                                    getNameSource: function(el) { return $(el).next().text(); }
                                },
                            ];

                            avatars.forEach(avatar => {
                                tm.getContainer({
                                    'el': avatar.element
                                }).then(function($container){
                                    var iter = $(avatar.element).is('img') ? $(avatar.element) : avatar.getImgSource(avatar.element);
                                    _.each(iter, function (img) {
                                        thisName = properName(avatar.getNameSource(img));
                                        updateImg(img, thisName);
                                    });
                                });
                            });

                        } else {
                            global.states.avatarPingFailed = true;
                        }
                    });
                }
            },
            monitorTodos: function () {
                if (document.URL.indexOf('todos') < 0) return; // leave if not on the TODO page

                var indicateSecondsTimer,
                    todosCount =  $('.todos-count').text().replace(/(\r\n\t|\n|\r\t)/gm,''),
                    notificationIcon = 'https://gitlab.dell.com/assets/favicon-7901bd695fb93edb07975966062049829afb56cf11511236e61bcf425070e36e.png',
                    indicateSeconds = function() {
                        var secondsRemaining = (Math.floor(global.reload.timespan/1000) - global.reload.lapsed);
                        document.title = todosCount + ' : (' + secondsRemaining + ')';
                        global.reload.lapsed++;
                        if (secondsRemaining > 0) {
                            indicateSecondsTimer = setTimeout(indicateSeconds, 1000);
                        } else {
                            document.title = global.reload.title;
                        }
                    },
                    triggerNotification = function(notifyMessage) {
                        var notification = new Notification(notifyMessage, {
                            icon: notificationIcon,
                            body: todosCount
                        });
                        notification.onclick = function () {
                            window.open('https://gitlab.dell.com/dashboard/todos');
                        };
                    }

                if (!global.states.todoMonitorInitialized) {
                    global.states.todoMonitorInitialized = true;
                    global.reload.lapsed = 0;

                    var duration = moment.duration(moment().diff(global.prefs.todosTimestamp)),
                        hours = duration.asHours();

                    if (Number(todosCount) > 0 && hours > 1) {
                        global.prefs.reloadTimesReminded = Number(global.prefs.reloadTimesReminded) + 1;
                        tm.savePreferences(global.ids.prefsName,global.prefs);
                    }

                    if (Notification.permission === 'granted' && todosCount !== global.prefs.todosCount) {
                        // trigger notification on TODO count change
                        triggerNotification('TODOs on GitLab:');
                    } else if (Number(todosCount) > 0 && hours > 1 && Number(global.prefs.reloadTimesReminded) > Number(global.prefs.reloadReminderCount)) {
                        // trigger notification on hourly timeout if there ARE any
                        global.prefs.reloadTimesReminded = 0;
                        global.prefs.reloadReminderCount = Number(global.prefs.reloadReminderCount) + 1;
                        triggerNotification('Reminder- GitLab TODOs:');
                        global.prefs.todosTimestamp = moment();
                        if (Number(global.prefs.reloadReminderCount) > 8) {
                            global.prefs.reloadReminderCount = 0;
                            global.prefs.reloadTimesReminded = 0;
                        }
                        tm.savePreferences(global.ids.prefsName,global.prefs);
                    }

                    // update stored count
                    if(todosCount !== global.prefs.todosCount) {
                        global.prefs.todosCount = todosCount;
                        global.prefs.reloadReminderCount = 0;
                        global.prefs.reloadTimesReminded = 0;
                        tm.savePreferences(global.ids.prefsName,global.prefs);
                    }

                    // Reload in X seconds
                    global.reload.todoTimer = setTimeout(function() {window.location.reload(false);}, global.reload.timespan);

                    var buttonAnchor = $('.page-title');
                    var buttonCancelReload = 'Cancel-Reload';
                    var timeoutElement = function () {
                        clearTimeout(global.reload.todoTimer);
                        clearTimeout(indicateSecondsTimer);
                        $('#' + buttonCancelReload).css('display', 'none');
                        document.title = global.reload.title;
                        return false;
                    };
                    buttonAnchor.after('<button id="' + buttonCancelReload + '" style="margin-left:50px; border-radius:15px; border:0px; background:lightgoldenrodyellow; padding:5px 15px; ">' + buttonCancelReload + '</button>');
                    $('#' + buttonCancelReload).click(timeoutElement);

                    var buttonNotifyMe = 'Notify-Me';
                    var notifyMe = function() {
                        if (Notification.permission !== "granted")
                            Notification.requestPermission();
                        else {
                            var notification = new Notification('Permission Granted', {
                                icon: notificationIcon,
                                body: "Notifications have been allowed.",
                            });

                            //                             notification.onclick = function () {
                            //                                 window.open("http://stackoverflow.com/a/13328397/1269037");
                            //                             };
                        }
                        $('#' + buttonNotifyMe).css('display', 'none');
                        return false;
                    }

                    if (Notification.permission !== 'granted') {
                        buttonAnchor.after('<button id="' + buttonNotifyMe + '" style="margin-left:50px; border-radius:15px; border:0px; background:lightgrey; padding:5px 15px; ">' + buttonNotifyMe + '</button>');
                        $('#' + buttonNotifyMe).click(notifyMe);
                    }

                    setTimeout(indicateSeconds, 1000);

                }
            },
            linkWorkItems: function () {
                if (!global.states.titlesLinked) {
                    global.states.titlesLinked = true;
                    var linkText, linkHref,
                        titleText = $('h2.title').text();
                    if (titleText.toUpperCase().indexOf('PT#') > -1) {
                        linkText = titleText.match(/PT#[0-9]*/gi).toString().split(',');
                        linkText.forEach(thisLink => {
                            linkHref = '<a href="https://www.pivotaltracker.com/n/projects/2210391/stories/' + thisLink.replace(/PT\#/gi, '') + '" target="blank">' + thisLink + '</a>';
                            titleText = titleText.replace(thisLink, linkHref);
                        });
                    }
                    if (titleText.toUpperCase().indexOf('TFS#') > -1) {
                        linkText = titleText.match(/TFS#[0-9]*/gi).toString().split(',');
                        linkText.forEach(thisLink => {
                            linkHref = '<a href="http://tfs2.dell.com:8080/tfs/eDell/eDellPrograms/_workitems?id=' + thisLink.replace(/TFS\#/gi, '') + '" target="blank">' + thisLink + '</a>';
                            titleText = titleText.replace(thisLink, linkHref);
                        });
                    }
                    $('h2.title').html(titleText);
                }
            }
        },
        utils = {
            initScript: function () {
                _.each(global.ids.triggerElements, (trigger) => {
                    tm.getContainer({
                        'el': trigger,
                        'max': 100,
                        'spd': 1000
                    }).then(function($container){
                        page.initialize();
                    });
                });
            }
        };

    (function() { // Global Functions
        if (global.states.debugMode) tm.log('Global initialization of ' + global.ids.scriptName);
        utils.initScript();
        $(document).mousemove(function(e) {
            if (!global.states.isMouseMoved) {
                global.states.isMouseMoved = true;
                setTimeout(function() {
                    global.states.isMouseMoved = false;
                }, TIMEOUT * 2);
                utils.initScript();
            }
        });
    })(); // Global Functions

})();