Fong-/CS169-PenPal-Gladiators

View on GitHub
app/assets/javascripts/homepage/conversation.coffee

Summary

Maintainability
Test Coverage
conversation = angular.module("Conversation", ["SharedServices"])

conversation.controller("ConversationController", ["$scope", "$stateParams", "API", "TimeUtil", "AppState", "$rootScope", "ConversationService", "ConversationData", ($scope, $stateParams, API, TimeUtil, AppState, $rootScope, ConversationService, ConversationData) ->
    # Constants
    POST_SUBMISSION_TIMEOUT_PERIOD_MS = 5000
    CONVERSATION_POLL_PERIOD = 10 # seconds
    READ_POSTS = 1
    EDIT_OR_ADD_POST = 2
    EDIT_SUMMARY = 3
    EDIT_RESOLUTION = 4
    EDIT_POST_PLACEHOLDER_TEXT = ""
    NEW_POST_PLACEHOLDER_TEXT = "Write a new post here and submit when you are finished."
    EDIT_SUMMARY_PLACEHOLDER_TEXT = "Summarize your opponent's viewpoint and click Submit when you are finished. If your opponent accepts your summary, it will become a post."
    BEGIN_RESOLUTION_PLACEHOLDER_TEXT = "Be the first to write a statement of resolution between you and your opponent!"
    EDIT_RESOLUTION_PLACEHOLDER_TEXT = "Help edit the final statement of resolution between you and your opponent."
    # State variables
    conversationId = parseInt($stateParams["id"])
    postSubmissionInProgress = false
    conversation = {}
    postsById = {}
    currentPostEditId = null
    postSubmissionTimer = null
    lastUpdateTime = 0
    isEditingTitle = false
    conversationPageState = READ_POSTS
    previousPostContainerOffset = 0
    username = AppState.user.username
    # Scope methods and models
    $scope.editPostText = ""
    $scope.postSubmitError = ""
    $scope.title = -> if "title" of conversation then conversation.title else ""
    $scope.editTitleClicked = ->
        title = document.getElementById("title-text")
        if title
            title.setAttribute("contentEditable", true)
            title.focus()
            isEditingTitle = true
    $scope.titleTextLostFocus = -> $scope.finishEditingTitle(true)
    $scope.finishEditingTitle = (updateTitle) ->
        if not isEditingTitle then return
        title = document.getElementById("title-text")
        if !title then return
        title.setAttribute("contentEditable", false)
        if updateTitle and title.textContent != "" and title.textContent != conversation.title
            API.editTitleByConversationId(conversationId, title.textContent).success (response) ->
                updateConversationData(false)
                $rootScope.$broadcast("reloadSidebarArenas")
            isEditingTitle = false
            title.blur()
        else
            title.textContent = conversation.title

    $scope.timeElapsedMessage = (timestamp) -> TimeUtil.timeIntervalAsString(TimeUtil.timeSince1970InSeconds() - TimeUtil.timeFromTimestampInSeconds(timestamp)) + " ago"
    $scope.shouldHidePostEditor = -> conversationPageState is READ_POSTS
    $scope.postLengthClass = -> if conversationPageState is READ_POSTS then "posts-full-length" else "posts-shortened"
    $scope.postContentClass = (id) ->
        if postsById[id].type is "summary"
            return "post-content-summary"
        if postsById[id].type is "resolution"
            return "post-content-resolution"
        return ""
    $scope.authorClass = (id) ->
        if $scope.authorDisplayNameForPost(id) == username then "self-post" else "other-post"
    $scope.addPostClicked = ->
        savePostInProgress()
        $scope.postSubmitError = ""
        $scope.editPostText = ConversationService.getAddPostText(conversationId)
        currentPostEditId = null
        expandEditorViewForState(EDIT_OR_ADD_POST)

    $scope.shouldDisplayEmptyConversationsMessage = -> !$scope.posts or $scope.posts.length == 0
    $scope.cancelPostClicked = -> closeTextEditor()
    $scope.escapeTextEditor = -> closeTextEditor()
    $scope.authorDisplayNameForPost = (id) ->
        if postsById[id].type is "resolution"
            return "You and #{conversation.opponent.username}"
        return postsById[id].author.username
    $scope.shouldDisableSubmitPost = ->
        if postSubmissionInProgress or $scope.editPostText == ""
            return true
        if conversationPageState is EDIT_OR_ADD_POST and currentPostEditId != null
            return $scope.editPostText == postsById[currentPostEditId].text
        if conversationPageState is EDIT_SUMMARY and conversation.pendingSummaries
            return $scope.editPostText == conversation.pendingSummaries.opposing
        if conversationPageState is EDIT_RESOLUTION
            return !conversation.resolution or $scope.editPostText == conversation.resolution.text
        return false

    $scope.shouldDisableEditor = -> postSubmissionInProgress
    $scope.submitPostClicked = ->
        $scope.postSubmitError = ""
        postSubmissionInProgress = true
        if conversationPageState is EDIT_SUMMARY
            ConversationService.updateSummaryText(conversationId, "")
            API.editSummaryByConversationId(conversationId, $scope.editPostText).success (response) -> handlePostEditResponse(response, false)
        else if conversationPageState is EDIT_RESOLUTION
            ConversationService.updateResolutionText(conversationId, "")
            API.editResolutionByConversationId(conversationId, $scope.editPostText).success (response) -> handlePostEditResponse(response, false)
        else if currentPostEditId is null
            ConversationService.updateAddPostText(conversationId, "")
            API.createPostByConversationId(conversationId, $scope.editPostText).success (response) -> handlePostEditResponse(response, true)
        else
            API.editPostById(currentPostEditId, $scope.editPostText).success (response) -> handlePostEditResponse(response, false)
        postSubmissionTimer = setTimeout(->
            $scope.postSubmitError = "Post submission failed. Please try again later."
            postSubmissionInProgress = false
            $scope.$apply()
        , POST_SUBMISSION_TIMEOUT_PERIOD_MS)

    $scope.shouldDisplayPostEdit = (id) -> postsById[id].author.id == AppState.user.id and postsById[id].type is "post"
    $scope.editPostClicked = (id) ->
        savePostInProgress()
        currentPostEditId = id
        $scope.editPostText = postsById[id].text
        expandEditorViewForState(EDIT_OR_ADD_POST)

    $scope.shouldDisableAddPost = -> conversationPageState is EDIT_OR_ADD_POST
    $scope.shouldHideSummary = -> conversationPageState isnt EDIT_SUMMARY
    $scope.postEditorWidthClass = -> if conversationPageState isnt EDIT_SUMMARY then "post-editor-full" else "post-editor-half"
    $scope.shouldDisableProposeSummary = -> conversationPageState is EDIT_SUMMARY
    $scope.proposeSummaryClicked = ->
        savePostInProgress()
        $scope.editPostText = if conversation.pendingSummaries && conversation.pendingSummaries.opposing != "" then conversation.pendingSummaries.opposing else ConversationService.getSummaryText(conversationId)
        expandEditorViewForState(EDIT_SUMMARY)

    $scope.ownPendingSummaryText = -> if conversation.pendingSummaries then conversation.pendingSummaries.own else ""
    $scope.ownPendingSummaryExists = -> $scope.ownPendingSummaryText() != ""
    $scope.opponentDisplayName = -> if conversation.opponent then conversation.opponent.username else ""
    $scope.approveSummaryClicked = ->
        API.approveSummaryByConversationId(conversation.id).success (response) ->
            updateConversationData(true)
            conversationPageState = READ_POSTS

    $scope.approveResolutionClicked = ->
        API.approveResolutionByConversationId(conversation.id).success (response) ->
            updateConversationData(true)
            conversationPageState = READ_POSTS

    $scope.editorPlaceholderText = ->
        if conversationPageState is EDIT_OR_ADD_POST
            return if currentPostEditId is null then NEW_POST_PLACEHOLDER_TEXT else EDIT_POST_PLACEHOLDER_TEXT
        if conversationPageState is EDIT_SUMMARY
            return EDIT_SUMMARY_PLACEHOLDER_TEXT
        if conversationPageState is EDIT_RESOLUTION
            return if !conversation.resolution or conversation.resolution.state is "unstarted" then BEGIN_RESOLUTION_PLACEHOLDER_TEXT else EDIT_RESOLUTION_PLACEHOLDER_TEXT
        return ""

    $scope.showLastEditedMessage = -> conversationPageState is EDIT_RESOLUTION
    $scope.lastEditedMessage = ->
        if !conversation.resolution or conversation.resolution.state is "unstarted" then return ""
        timeElapsed = $scope.timeElapsedMessage(conversation.resolution.updated_time)
        latestAuthorName = if conversation.resolution.updated_by_id is conversation.self.id then "you" else conversation.opponent.username
        return "Last edited by #{latestAuthorName} #{timeElapsed.toLowerCase()}"
    $scope.shouldDisableEditResolution = -> conversationPageState is EDIT_RESOLUTION
    $scope.shouldDisableApproveResolution = -> !conversation.resolution or
        conversation.resolution.updated_by_id is AppState.user.id or
        conversation.resolution.state != "in_progress" or
        conversation.resolution.text != $scope.editPostText

    $scope.showApproveResolution = -> conversationPageState is EDIT_RESOLUTION
    $scope.editResolutionClicked = ->
        savePostInProgress()
        $scope.editPostText = if conversation.resolution && conversation.resolution.text != "" then conversation.resolution.text else ConversationService.getResolutionText(conversationId)
        expandEditorViewForState(EDIT_RESOLUTION)

    savePostInProgress = () ->
        if conversationPageState == EDIT_OR_ADD_POST && currentPostEditId == null
            # User was writing a new post
            ConversationService.updateAddPostText(conversationId, $scope.editPostText)
        else if conversationPageState == EDIT_SUMMARY
            # User was writing a summary
            ConversationService.updateSummaryText(conversationId, $scope.editPostText)
        else if conversationPageState == EDIT_RESOLUTION
            # User was writing the resolution
            ConversationService.updateResolutionText(conversationId, $scope.editPostText)

    closeTextEditor = () ->
        savePostInProgress()
        conversationPageState = READ_POSTS

    expandEditorViewForState = (state) ->
        if conversationPageState is READ_POSTS and state isnt READ_POSTS
            postsContainer = document.getElementById("posts-container")
            if postsContainer
                previousPostContainerOffset = postsContainer.scrollTop + postsContainer.clientHeight
                setTimeout -> postsContainer.scrollTop = previousPostContainerOffset - postsContainer.clientHeight
            dispatchFocusTextarea()
        conversationPageState = state

    dispatchScrollElementToBottom = (elementId) ->
        setTimeout ->
            postsContainer = document.getElementById(elementId)
            postsContainer.scrollTop = postsContainer.scrollHeight if postsContainer

    handlePostEditResponse = (response, scrollToEnd) ->
        clearTimeout(postSubmissionTimer) if postSubmissionTimer
        postSubmissionInProgress = false
        unless "error" of response
            if conversationPageState isnt EDIT_SUMMARY and conversationPageState isnt EDIT_RESOLUTION
                $scope.editPostText = ""
                conversationPageState = READ_POSTS
        currentPostEditId = null
        updateConversationData(scrollToEnd)

    dispatchFocusTextarea = ->
        setTimeout ->
            textareas = document.getElementsByTagName("textarea")
            textareas[0].focus() if textareas.length > 0

    updateConversationData = (scrollToEnd) ->
        API.requestConversationById(conversationId).success (response) ->
            parseConversation(response, scrollToEnd)

    shouldPollConversationData = -> TimeUtil.timeSince1970InSeconds() - lastUpdateTime > CONVERSATION_POLL_PERIOD
    conversationPollingProcess = setInterval( ->
        updateConversationData(false) if shouldPollConversationData()
    , CONVERSATION_POLL_PERIOD * 1000)

    $rootScope.$on "conversationPageWillLoad", (scope, args) ->
        if conversationId isnt args.conversationId then clearInterval conversationPollingProcess
    $rootScope.$on "$locationChangeStart", (event, next, current) ->
        savePostInProgress()

    parseConversation = (response, scrollToEnd) ->
        conversation.id = response.id
        if not isEditingTitle then conversation.title = response.title
        # The "own" summary is the summary of the user's own viewpoint, written by the opposing gladiator.
        # The "opposing" summary is the summary of the opposing viewpoint, written by the user.
        conversation.pendingSummaries = { own: "", opposing: "" }
        conversation.opponent = null
        conversation.resolution = response.resolution
        $scope.posts = if "posts" of response then response.posts.reverse() else []
        for post in response.posts
            post.type = post.post_type
            delete post["post_type"]
            postsById[post.id] = post
        if response.summaries[0].author.id == AppState.user.id
            selfIndex = 0
        else if response.summaries[1].author.id == AppState.user.id
            selfIndex = 1
        conversation.pendingSummaries.own = response.summaries[1 - selfIndex].text
        conversation.pendingSummaries.opposing = response.summaries[selfIndex].text
        conversation.opponent = response.summaries[1 - selfIndex].author
        conversation.self = response.summaries[selfIndex].author
        dispatchScrollElementToBottom("posts-container") if scrollToEnd
        lastUpdateTime = TimeUtil.timeSince1970InSeconds()

    parseConversation(ConversationData.data, true)
])

conversation.service("ConversationService", () ->
    conversationTexts = {}
    getConversationText = (conversationId) ->
        if conversationId of conversationTexts
            return conversationTexts[conversationId]
        else
            c = {addPostText: "", summaryText: "", resolutionText: ""}
            conversationTexts[conversationId] = c
            return c
    # getter and setter for posts in progress
    this.updateAddPostText = (conversationId, text) ->
        getConversationText(conversationId).addPostText = text
    this.updateSummaryText = (conversationId, text) ->
        getConversationText(conversationId).summaryText = text
    this.updateResolutionText = (conversationId, text) ->
        getConversationText(conversationId).resolutionText = text
    this.getAddPostText = (conversationId) ->
        return getConversationText(conversationId).addPostText
    this.getSummaryText = (conversationId) ->
        return getConversationText(conversationId).summaryText
    this.getResolutionText = (conversationId) ->
        return getConversationText(conversationId).resolutionText
    this.hasUnsavedProgress = (conversationId) ->
        c = getConversationText(conversationId)
        return (c.addPostText != "" or c.summaryText != "" or c.resolutionText != "")
    return
)