From d8709c033417c75ffa9ec26ac5127880637bc99c Mon Sep 17 00:00:00 2001 From: Mario See Date: Fri, 15 Mar 2019 14:39:57 -0400 Subject: [PATCH] Implement open inbox for dev-connect (#1563) --- app/assets/javascripts/initializePage.js.erb | 1 + .../initializers/initializeAllChatButtons.js | 106 ++++++++++++++ .../initializers/initializeAllFollowButts.js | 21 +-- app/assets/javascripts/utilities/sendFetch.js | 12 ++ .../stylesheets/user-profile-header.scss | 135 +++++++++++++++++- app/controllers/chat_channels_controller.rb | 19 +++ app/controllers/follows_controller.rb | 7 +- app/models/user.rb | 1 + app/policies/user_policy.rb | 1 + app/views/articles/_user_metadata.html.erb | 11 +- app/views/users/_misc.html.erb | 16 +++ app/views/users/show.html.erb | 16 +++ config/routes.rb | 1 + .../20190121172642_add_inbox_to_user.rb | 5 + .../20190121191754_change_inbox_name.rb | 5 + docs/vocabulary.md | 10 +- spec/models/user_spec.rb | 1 + spec/requests/follows_show_spec.rb | 2 +- 18 files changed, 347 insertions(+), 23 deletions(-) create mode 100644 app/assets/javascripts/initializers/initializeAllChatButtons.js create mode 100644 db/migrate/20190121172642_add_inbox_to_user.rb create mode 100644 db/migrate/20190121191754_change_inbox_name.rb diff --git a/app/assets/javascripts/initializePage.js.erb b/app/assets/javascripts/initializePage.js.erb index 27051df5e..c56b8e307 100644 --- a/app/assets/javascripts/initializePage.js.erb +++ b/app/assets/javascripts/initializePage.js.erb @@ -12,6 +12,7 @@ function callInitalizers(){ if (document.getElementsByTagName('body')[0].getAttribute('data-loaded') == "true") { clearInterval(waitingForDataLoad); if (document.getElementsByTagName('body')[0].getAttribute('data-user-status') == "logged-in") { + initializeAllChatButtons(); initializeBaseUserData(); } initializeReadingListPage(); diff --git a/app/assets/javascripts/initializers/initializeAllChatButtons.js b/app/assets/javascripts/initializers/initializeAllChatButtons.js new file mode 100644 index 000000000..34d6a21a2 --- /dev/null +++ b/app/assets/javascripts/initializers/initializeAllChatButtons.js @@ -0,0 +1,106 @@ +function initModal() { + var modal = document.querySelector('.modal'); + modal.querySelector('.close-modal').addEventListener('click', toggleModal); + modal.querySelector('.overlay').addEventListener('click', toggleModal); +} + +function toggleModal() { + var modal = document.querySelector('.modal'); + var currentState = modal.style.display; + + if (currentState === 'none') { + showChatModal(modal); + } else { + hideChatModal(modal); + } +} + +function showChatModal(modal) { + modal.style.display = 'block'; + document.getElementById('new-message').focus(); +} + +function hideChatModal(modal) { + modal.style.display = 'none'; +} + +function handleChatButtonPress(form) { + var message = document.getElementById('new-message').value; + var formDataInfo = JSON.parse(form.dataset.info); + var formData = new FormData(); + + if (message.replace(/\s/g, '').length === 0) { + return; + } + + formData.append('user_id', formDataInfo.id); + formData.append('message', message); + formData.append('controller', 'chat_channels'); + + getCsrfToken() + .then(sendFetch('chat-creation', formData)) + .then(() => { + window.location.href = `/connect/@${formDataInfo.username}`; + }); +} + +function addButtonClickHandle(response, button, modalInfo) { + var linkWrap = document.getElementById("user-connect-redirect"); + var form = document.getElementById('new-message-form'); + button.classList.add('showing'); + if (modalInfo.showChat === "open" && response !== "mutual") { + linkWrap.removeAttribute("href") // remove link + button.addEventListener('click', toggleModal); + button.style.display = 'initial'; // show button + linkWrap.style.display = 'initial'; // show button + form.onsubmit = function() {handleChatButtonPress(form); return false;}; + } else if (response === 'mutual') { + button.removeEventListener('click', toggleModal); + button.style.display = 'initial'; // show button + linkWrap.style.display = 'initial'; // show button + } +} + +function fetchButton(button, modalInfo) { + var dataRequester; + // button.dataset.fetched = 'fetched'; // telling initialize that this button has been fetched + if (window.XMLHttpRequest) { + dataRequester = new XMLHttpRequest(); + } else { + dataRequester = new ActiveXObject('Microsoft.XMLHTTP'); + } + dataRequester.onreadystatechange = function() { + if (dataRequester.readyState === XMLHttpRequest.DONE && dataRequester.status === 200) { + addButtonClickHandle(dataRequester.response, button, modalInfo); + } + } + dataRequester.open('GET', '/follows/' + modalInfo.id + '?followable_type=' + modalInfo.className); + dataRequester.send(); +} + +function initializeChatButton(button, modalInfo) { + // if user logged out, do nothing + var user = userData(); + if (user === null || user.id === modalInfo.id) { + return; + } + fetchButton(button, modalInfo); +} + +// finds all elements with chat action button class +function initializeAllChatButtons() { + var buttons = document.getElementsByClassName('chat-action-button'); + var modal = document.getElementById('new-message-form'); + var i; + + if (!modal) { + return; + } + + var modalInfo = JSON.parse(modal.dataset.info); + initModal(); + + for (i = 0; i < buttons.length; i += 1) { + initializeChatButton(buttons[i], modalInfo); + } +} diff --git a/app/assets/javascripts/initializers/initializeAllFollowButts.js b/app/assets/javascripts/initializers/initializeAllFollowButts.js index 88bcb4c3f..6364f0881 100644 --- a/app/assets/javascripts/initializers/initializeAllFollowButts.js +++ b/app/assets/javascripts/initializers/initializeAllFollowButts.js @@ -73,9 +73,12 @@ function handleTagButtAssignment(user, butt, buttInfo) { function assignInitialButtResponse(response, butt) { butt.classList.add('showing'); - if (response === 'true') { + if (response === 'true' || response === 'mutual') { assignState(butt, 'unfollow'); } + else if (response === 'follow-back') { + assignState(butt, 'follow-back') + } else if (response === 'false') { assignState(butt, 'follow'); } @@ -114,7 +117,7 @@ function handleOptimisticButtRender(butt) { }catch (err) {return} }); }catch (err) {return} - + handleFollowButtPress(butt); } } @@ -129,15 +132,16 @@ function handleFollowButtPress(butt) { } function assignState(butt, newState) { - var style = ''; - if (butt.dataset.info) { - style = JSON.parse(butt.dataset.info).style; - } + var style = JSON.parse(butt.dataset.info).style; butt.classList.add('showing'); - if (newState === 'follow') { + if (newState === 'follow' || newState === 'follow-back') { butt.dataset.verb = 'unfollow'; butt.classList.remove('following-butt'); - addFollowText(butt, style); + if (newState === 'follow-back') { + addFollowText(butt, newState); + } else if (newState === 'follow') { + addFollowText(butt, style); + } } else if (newState === 'login') { addFollowText(butt, style); } else if (newState === 'self') { @@ -167,4 +171,3 @@ function addFollowingText(butt, style) { butt.innerHTML = '✓ FOLLOWING'; } } - diff --git a/app/assets/javascripts/utilities/sendFetch.js b/app/assets/javascripts/utilities/sendFetch.js index 9e0ba27cd..682c5080e 100644 --- a/app/assets/javascripts/utilities/sendFetch.js +++ b/app/assets/javascripts/utilities/sendFetch.js @@ -49,6 +49,18 @@ function sendFetch(switchStatement, body) { credentials: 'same-origin', }); }; + case 'chat-creation': + return function(csrfToken) { + body.append('authenticity_token', csrfToken); + return window.fetch('/open_chat', { + method: 'POST', + headers: { + 'X-CSRF-Token': csrfToken, + }, + body: body, + credentials: 'same-origin', + }); + }; case 'comment-creation': return function(csrfToken) { return window.fetch('/comments', { diff --git a/app/assets/stylesheets/user-profile-header.scss b/app/assets/stylesheets/user-profile-header.scss index 679b5c009..4e4ac59db 100644 --- a/app/assets/stylesheets/user-profile-header.scss +++ b/app/assets/stylesheets/user-profile-header.scss @@ -279,13 +279,13 @@ text-align:left; } } - .user-profile-follow-button-wrapper{ + .user-profile-follow-button-wrapper { position:relative; z-index:5; display:inline-block; margin-left: 5px; } - .user-profile-follow-button{ + .user-profile-follow-button { background: $green; color:white; border:0; @@ -314,4 +314,135 @@ min-width: 140px; } } + + // same as above, just add class to above style + .user-profile-chat-button-wrapper { + position:relative; + z-index:5; + display:inline-block; + margin-left: 5px; + } + + .user-profile-chat-button { + background: $green; + color:white; + border:0; + font-size:17px; + border-radius:3px; + vertical-align:3px; + padding:2px 6px; + cursor:pointer; + min-width: 60px; + font-family: "HelveticaNeue-CondensedBold", "HelveticaNeueBoldCondensed", "HelveticaNeue-Bold-Condensed", "Helvetica Neue Bold Condensed", "HelveticaNeueBold", "HelveticaNeue-Bold", "Helvetica Neue Bold", "HelveticaNeue", "Helvetica Neue", "TeXGyreHerosCnBold", "Helvetica", "Tahoma", "Geneva", "Arial Narrow", "Arial", sans-serif; + -webkit-appearance: none; + font-stretch: condensed; + &.showing{ + opacity:1; + } + @media screen and ( min-width: 430px ){ + font-size:18px; + padding:3px 8px; + vertical-align:7px; + border-radius:5px; + min-width: 80px; + } + @media screen and ( min-width: 950px ){ + font-size:22px; + padding:4px 8px; + vertical-align:12px; + border-radius:5px; + min-width: 80px; + } + } + + // new message modal for open inbox + .modal { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + z-index: 990; + } + + .modal .overlay { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + z-index: 995; + background: rgba(0,0,0,0.85); + } + + .modal-content { + z-index: 999; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + max-height: 80%; + overflow: auto; + background: #fff; + box-sizing: border-box; + padding: 20px; + border: 1px solid #d6d6d6; + box-shadow: 3px 3px 0px #bababa; + border-radius: 3px; + max-width: 90%; + width: 80%; + + .message-form { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-around; + align-items: center; + align-content: stretch; + + textarea { + border-radius: 3px; + border: 1px solid #d6d6d6; + font-size: 14px; + margin: 10px; + padding: 5px; + width: calc(100% - 100px); + order: 1; + flex-grow: 1; + resize: vertical; + max-height:30%; + } + + .submit-message { + margin: 10px 10px 10px 0; + border-radius: 3px; + background: $dark-purple; + font-weight: 600px; + font-family: $helvetica-condensed; + width: 80px; + color: white; + font-size: 18px; + height: 80px; + order: 10; + flex-grow: 1; + } + } + + .close-modal { + position: absolute; + right: 10px; + top: 10px; + cursor: pointer; + font-size: 18px; + opacity: 0.5; + background: none; + border: none; + padding: 0 0 0 10px; + transition: opacity 0.2s ease; + &:hover { + opacity: 0.9; + } + } + } + } diff --git a/app/controllers/chat_channels_controller.rb b/app/controllers/chat_channels_controller.rb index 60e05a457..649ba39b8 100644 --- a/app/controllers/chat_channels_controller.rb +++ b/app/controllers/chat_channels_controller.rb @@ -93,6 +93,25 @@ class ChatChannelsController < ApplicationController end end + def open_chat + chat_recipient = User.find(params[:user_id]) + if chat_recipient.inbox_type == "open" + chat = ChatChannel.create_with_users([current_user, chat_recipient], "direct") + # get message param to generate message to send + # message_markdown = "Hi #{chat_recipient.username}! I am #{current_user.username}. I can message you on DEV Connect because your inbox is open." + message_markdown = params[:message] + message = Message.new( + chat_channel: chat, + message_markdown: message_markdown, + user: current_user, + ) + chat.messages.append(message) + render json: { status: "success", message: "chat channel created!" }, status: 200 + else + render json: { status: "error", message: "not allowed!" }, status: 400 + end + end + private def chat_channel_params diff --git a/app/controllers/follows_controller.rb b/app/controllers/follows_controller.rb index ee7dc05f8..2eb0c9d51 100644 --- a/app/controllers/follows_controller.rb +++ b/app/controllers/follows_controller.rb @@ -10,8 +10,13 @@ class FollowsController < ApplicationController if current_user.id == params[:id].to_i && params[:followable_type] == "User" render plain: "self" return + elsif params[:followable_type] == "User" && FollowChecker.new(current_user, params[:followable_type], params[:id]).cached_follow_check && FollowChecker.new(User.find(params[:id]), params[:followable_type], current_user.id).cached_follow_check + render plain: "mutual" + elsif params[:followable_type] == "User" && FollowChecker.new(User.find(params[:id]), params[:followable_type], current_user.id).cached_follow_check + render plain: "follow-back" + else + render plain: FollowChecker.new(current_user, params[:followable_type], params[:id]).cached_follow_check end - render plain: FollowChecker.new(current_user, params[:followable_type], params[:id]).cached_follow_check end def create diff --git a/app/models/user.rb b/app/models/user.rb index 14f586f3a..b4fd0ab74 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -131,6 +131,7 @@ class User < ApplicationRecord length: { maximum: 500 } validates :mentee_description, :mentor_description, length: { maximum: 1000 } + validates :inbox_type, inclusion: { in: ["open", "private"] } validate :conditionally_validate_summary validate :validate_mastodon_url validate :validate_feed_url diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index fd842555e..3c3d8a6b9 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -98,6 +98,7 @@ class UserPolicy < ApplicationPolicy mostly_work_with name offering_mentorship + inbox_type permit_adjacent_sponsors password password_confirmation diff --git a/app/views/articles/_user_metadata.html.erb b/app/views/articles/_user_metadata.html.erb index e4e882a7f..03aad27af 100644 --- a/app/views/articles/_user_metadata.html.erb +++ b/app/views/articles/_user_metadata.html.erb @@ -40,11 +40,12 @@ <% else %> <% if @user.email_public && - @user.employer_name.present? && - @user.location.present? && - @user.education.present? && - @user.education.size > 25 && - @user.summary.to_s.size < 145 %> + @user.employer_name.present? && + @user.location.present? && + @user.education.present? && + @user.education.size > 25 && + @user.summary.to_s.size < 145 + %>