Raw Source
nottheswimmer / Webex Teams Plus

// ==UserScript==
// @name         Webex Teams Plus
// @version      0.1.0
// @namespace    https://github.com/nottheswimmer
// @match        https://teams.webex.com/*
// @license      MIT
// @author       Michael Phelps
// @description  a script that tries to make Webex Teams better!
// @grant        GM_addStyle
// @require http://code.jquery.com/jquery-3.4.1.min.js
// @icon        https://teams.webex.com/images/webex-teams-logo-881018cdbe9ae05cff97b96e5f3614d8.svg
// @updateURL   https://raw.githubusercontent.com/nottheswimmer/webex-teams-plus/latest/webex-teams-plus.user.js
// @downloadURL https://raw.githubusercontent.com/nottheswimmer/webex-teams-plus/latest/webex-teams-plus.user.js
// ==/UserScript==

const styling = `
.md-list-item--space {
    height: 28px;
}

.md-avatar.md-avatar--40 {
    width: 20px;
    height: 20px;
}

.resizer {
    width: 220px;
    min-width: 220px !important;
}

#spacesLabel, #peopleLabel {
    color: #fff;
}

.activity-threading-reply {
    visibility: hidden;
    height: 0;
    padding-top: 0 !important;
}

.activity-reply-thread-section {
    margin: 0 0 5px 4.1rem !important;
}

.activity-threading-list-section > .activity-threading-reply {
    visibility: visible;
    height: auto;
    padding-top: 4px !important;
    margin-left: 0;
}

.activity-item--message {
    font-size: 15px;
}

.activity-reply-thread-btn {
    box-shadow: none !important;
    font-size: 13px;
}

.activity-threading-overlay, .activity-threading-wrapper {
    position: absolute;
    height: 100%;
    width: max(calc(40% - 16px), 422px);
    left: calc(100% - max(calc(40% - 32px), 422px));
}

.activity-threading-section {
    height: 100%;
    max-height: 100%;
}

.activity-threading-list-section {
    overflow-y: auto;
    height: calc(100% - 190px);
}

.wtp-next-to-thread {
    width: calc(100% - max(calc(40% - 50px), 422px)) !important;
}

#activities .activity-item.activity-threading-reply {
    border-left: 0 !important;
    border-radius: 2px !important;
    padding-left: .3125rem !important;
    padding-right: 1.25rem !important;
    margin-left: 1.25rem !important;
}
`

const peopleLabelHtml = `
<div aria-level="1"
aria-label="People"
id="peopleLabel"
data-qa="virtual-list-item"
style="padding-left: 15px;">
<strong>People</strong>
</div>
`

const spacesLabelHtml = `
<div aria-level="1"
aria-label="Spaces"
id="spacesLabel"
data-qa="virtual-list-item"
style="padding-left: 15px;">
<strong>Spaces</strong>
</div>
`

const sContainer = '#conversation-list';
const sSpaces = sContainer + ' > div.space-list-item-wrapper';
const sViewOlderSpacesButton = '.convo-list-load-more';
const sWTPPerson = '.wtp-person';
const sWTPSpace = '.wtp-space';
const sWTPDMLabel = '#peopleLabel';
const sWTPSpacesLabel = '#spacesLabel';

async function updateReplyCount() {
    $('.activity-reply-thread-btn').each(function (index) {
        // Get their parent (same level as a reply)
        let parent = $(this).parent();
        // All the replies before it up until a main post
        // "the replies are all activity items prior to this button until you get to an item that is an activity item but is not a reply"
        let replies = $($(parent).prevUntil(':not(.activity-threading-reply).activity-item')).filter('.activity-item');
        // Count the number of replies
        let numReplies = replies.length;
        // Get the thread the replies are to
        let thread = replies.prev();
        // Get the date of that thread's last reply
        let prevReplyDateMarker = $(thread).find('.activity-item-last-reply-date');
        // Hide and get that date we're moving it
        if (prevReplyDateMarker.is(":visible")) {
            prevReplyDateMarker.hide();
        }
        let prevReplyDate = prevReplyDateMarker.text();
        // Plan what the new HTML will be...
        let newHtml = '<a href="#"><strong>' + numReplies + (numReplies === 1 ? ' reply' : ' replies') + '</strong></a> ' + prevReplyDate;

        // Update the reply button text
        if ($(this).html() !== newHtml) {
            $(this).html('<a href="#"><strong>' + numReplies + (numReplies === 1 ? ' reply' : ' replies') + '</strong></a> ' + prevReplyDate);
        }
    });
}


async function spacesThenContacts() {
    // If the spaces area exists...
    if ($(sContainer)) {
        let updated = false;
        // Get an original copy of the spaces in it
        let original = $(sSpaces)
        // Create a sorted version (contacts first then spaces)
        let sorted = original.sort(
            function (a, b) {
                let aIsPerson = $(a).find('.md-avatar--group').length === 1 ? 0 : 1
                let bIsPerson = $(b).find('.md-avatar--group').length === 1 ? 0 : 1

                if (aIsPerson === 1) {
                    $(a).addClass(sWTPPerson.substr(1));
                } else {
                    $(a).addClass(sWTPSpace.substr(1));
                }

                if (bIsPerson === 1) {
                    $(b).addClass(sWTPPerson.substr(1));
                } else {
                    $(b).addClass(sWTPSpace.substr(1));
                }

                let sortVal = (aIsPerson < bIsPerson) ? -1 : (aIsPerson > bIsPerson) ? 1 : 0;

                // If the order changes (sortVal is -1), set updated to true
                if (sortVal === -1) {
                    updated = true;
                }
                return sortVal;
            });

        // if updated was set to true by the sorted function update the DOM
        if (updated) {
            sorted.appendTo($(sContainer));
            if ($(sViewOlderSpacesButton)) {
                $(sViewOlderSpacesButton).appendTo($(sContainer));
            }

            // Put direct messages above first person
            let peopleLabel = $(sWTPDMLabel);
            let firstPerson = $(sWTPPerson + ':first');
            if (firstPerson.length > 0) {
                if (peopleLabel.length > 0) {
                    firstPerson.prepend($(peopleLabel));
                } else {
                    firstPerson.prepend(peopleLabelHtml);
                }
            }

            // Put spaces above first space
            let spacesLabel = $(sWTPSpacesLabel);
            let firstSpace = $(sWTPSpace + ':first');
            if (firstSpace.length > 0) {
                if (spacesLabel.length > 0) {
                    firstSpace.prepend($(spacesLabel));
                } else {
                    firstSpace.prepend(spacesLabelHtml);
                }
            }

        }
    }
}

let lastActivityUpdate = 0;
function activityUpdates() {
    lastActivityUpdate = Date.now();
    updateReplyCount().catch((e) =>
        console.log(e)
    );

    // If a thread is open, add a class to the main body.
    if ($('.activity-threading-wrapper').length !== 0) {
        $('.activity-body').addClass("wtp-next-to-thread");
    } else {
        $('.activity-body').removeClass("wtp-next-to-thread");
    }
}

let lastConversationListUpdate = 0;
function conversationUpdates() {
    lastConversationListUpdate = Date.now();
    spacesThenContacts().catch((e) =>
        console.log(e)
    );
}

(function () {
    // Add styling
    GM_addStyle(styling);

    // Once the document is ready...
    $(document).ready(function () {

        // Bind an event to occur every time #activities is modified
        $(document).on("DOMSubtreeModified", '#activities', function () {
            if (Date.now() - lastActivityUpdate > 100) {
                activityUpdates();
            }
        });

        // Bind an event to occur every time #conversation-list is modified
        $(document).on("DOMSubtreeModified", '#conversation-list', function () {
            if (Date.now() - lastConversationListUpdate > 100) {
                conversationUpdates();
            }
        });

        // Run the binded events above every two seconds to ensure missed events
        // are still triggered
        setInterval(function(){
            conversationUpdates();
            activityUpdates();
        }, 2000);

    });
})
();