diff --git a/app/assets/images/connect.svg b/app/assets/images/connect.svg new file mode 100644 index 000000000..0725c67da --- /dev/null +++ b/app/assets/images/connect.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/javascripts/initializers/initScrolling.js.erb b/app/assets/javascripts/initializers/initScrolling.js.erb index d89ad77dc..763588d85 100644 --- a/app/assets/javascripts/initializers/initScrolling.js.erb +++ b/app/assets/javascripts/initializers/initScrolling.js.erb @@ -6,7 +6,8 @@ function checkIfNearBottomOfPage() { var elCheck = document.getElementById("index-container"); if (elCheck) { - client = algoliasearch('<%= ENV["ALGOLIASEARCH_APPLICATION_ID"] %>', '<%= ENV["ALGOLIASEARCH_SEARCH_ONLY_KEY"] %>'); + var publicSearchKey = '<%= ENV["ALGOLIASEARCH_PUBLIC_SEARCH_ONLY_KEY"] %>' + client = algoliasearch('<%= ENV["ALGOLIASEARCH_APPLICATION_ID"] %>', publicSearchKey); initScrolling.called = true; if (document.getElementsByClassName("single-article").length < 2 || location.search.indexOf("q=") > -1 ) { diff --git a/app/assets/javascripts/initializers/initializeFetchFollowedArticles.js.erb b/app/assets/javascripts/initializers/initializeFetchFollowedArticles.js.erb index df09ba699..202aa0d46 100644 --- a/app/assets/javascripts/initializers/initializeFetchFollowedArticles.js.erb +++ b/app/assets/javascripts/initializers/initializeFetchFollowedArticles.js.erb @@ -20,7 +20,8 @@ function algoliaFollowedArticles(){ if (user.followed_user_ids){ followedUsersArray = user.followed_user_ids.map(function(id){return 'user_'+id}); } - var client = algoliasearch('<%= ENV["ALGOLIASEARCH_APPLICATION_ID"] %>', '<%= ENV["ALGOLIASEARCH_SEARCH_ONLY_KEY"] %>'); + var publicSearchKey = '<%= ENV["ALGOLIASEARCH_PUBLIC_SEARCH_ONLY_KEY"] %>' + var client = algoliasearch('<%= ENV["ALGOLIASEARCH_APPLICATION_ID"] %>', publicSearchKey); var index = client.initIndex('ordered_articles_<%= Rails.env %>'); var searchObj = { diff --git a/app/assets/javascripts/initializers/initializeReadingListPage.js.erb b/app/assets/javascripts/initializers/initializeReadingListPage.js.erb index e1691c042..83ec129c7 100644 --- a/app/assets/javascripts/initializers/initializeReadingListPage.js.erb +++ b/app/assets/javascripts/initializers/initializeReadingListPage.js.erb @@ -1,6 +1,7 @@ function initializeReadingListPage(){ if ( getCurrentPage("reading_list_items-index") && checkUserLoggedIn() ) { - client = algoliasearch('<%= ENV["ALGOLIASEARCH_APPLICATION_ID"] %>', '<%= ENV["ALGOLIASEARCH_SEARCH_ONLY_KEY"] %>'); + var publicSearchKey = '<%= ENV["ALGOLIASEARCH_PUBLIC_SEARCH_ONLY_KEY"] %>' + client = algoliasearch('<%= ENV["ALGOLIASEARCH_APPLICATION_ID"] %>', publicSearchKey); readinglistIndex = client.initIndex('ordered_articles_<%= Rails.env %>'); commentsIndex = client.initIndex('ordered_comments_<%= Rails.env %>'); var params = getQueryParams(document.location.search); diff --git a/app/assets/stylesheets/chat.scss b/app/assets/stylesheets/chat.scss index db86f7b77..88e86ed60 100644 --- a/app/assets/stylesheets/chat.scss +++ b/app/assets/stylesheets/chat.scss @@ -43,6 +43,14 @@ } } +.chat__channels input { + width: 96%; + border-radius: 3px; + border: 1px solid $light-medium-gray; + box-sizing: border-box; + padding: 5px; +} + .chat_chatconfig{ position: absolute; bottom: 0px; @@ -218,19 +226,34 @@ box-shadow: 3px 3px 0px $dark-purple; } +.chatchanneltab--new { + background: lighten($yellow, 10%); +} + .chatchanneltabindicator{ display: inline-block; - background: white; + background: $light-medium-gray; border: 2px solid $light-medium-gray; - padding: 0.3em; + min-height: 0.3em; + min-width: 0.3em; border-radius: 100px; - vertical-align: -0.05em; + // vertical-align: -0.05em; margin-right: 3px; + img { + height: 20px; + width: 20px; + border-radius: 100px; + vertical-align: -7px; + border: 3px solid $light-medium-gray; + } } .chatchanneltabindicator--new{ background: $yellow; border: 2px solid $black; + img { + border: 3px solid $yellow; + } } .chatchannels__channelslist { @@ -339,6 +362,13 @@ right: 24px; bottom: 0px; } +.chatcodeeditor__header{ + position: absolute; + top: -27px; + left: 35px; + font-family: $monospace; + color: $dark-gray; +} .CodeMirror{ padding: 12px; diff --git a/app/assets/stylesheets/top-bar.scss b/app/assets/stylesheets/top-bar.scss index 1a593d318..894f80393 100644 --- a/app/assets/stylesheets/top-bar.scss +++ b/app/assets/stylesheets/top-bar.scss @@ -44,7 +44,7 @@ display:none; } .write{ - width:111px; + width:100px; padding: 3px 0px; height: auto; top:12px; @@ -54,11 +54,49 @@ border: 2px solid $black; @media screen and ( min-width: 560px ){ display:block; - right:100px; + right:140px; } @media screen and ( min-width: 700px ){ display:block; - right:120px; + right:165px; + } + } + .connect-icon{ + width:29px; + height:48px; + display:block; + text-align:center; + z-index:10; + top:1px; + right:90px; + @media screen and ( min-width:700px ){ + right:110px; + } + img{ + margin-top:6px; + height:33px; + } + .connect-number{ + position:absolute; + top:6px; + left:17px; + background:$green; + padding:2px 7px; + border-radius:8px; + color:white; + display:none; + font-size:1.05em; + &.showing{ + display:block; + } + } + &:hover{ + img{ + opacity:0.9; + } + .connect-number{ + background:$green; + } } } .notifications-icon{ @@ -80,7 +118,7 @@ position:absolute; top:2px; left:11px; - background:$red; + background:$dark-purple; padding:2px 7px; border-radius:8px; color:white; @@ -95,7 +133,7 @@ opacity:0.9; } .notifications-number{ - background:#e92525; + background:$dark-purple; } } @@ -105,9 +143,12 @@ top:7px; left:calc(50px + 5vw); bottom: 0px; - width: calc(55vw - 30px); + width: calc(55vw - 63px); @media screen and ( min-width: 350px ){ - width: calc(61vw - 38px); + width: calc(61vw - 75px); + } + @media screen and ( min-width: 450px ){ + width: calc(61vw - 45px); } @media screen and ( min-width: 560px ){ width: calc(45vw - 30px); diff --git a/app/controllers/chat_channels_controller.rb b/app/controllers/chat_channels_controller.rb index 932078cfc..4d9be765a 100644 --- a/app/controllers/chat_channels_controller.rb +++ b/app/controllers/chat_channels_controller.rb @@ -4,8 +4,6 @@ class ChatChannelsController < ApplicationController def index if params[:state] == "unopened" render_unopened_json_response - elsif params[:state] == "additional" - render_additional_json_response else render_channels_html end @@ -30,6 +28,7 @@ class ChatChannelsController < ApplicationController end membership = @chat_channel.chat_channel_memberships.where(user_id: current_user.id).first membership.update(last_opened_at: 1.seconds.from_now, has_unopened_messages: false) + @chat_channel.index! render json: { status: "success", channel: params[:id] }, status: 200 end @@ -71,7 +70,7 @@ class ChatChannelsController < ApplicationController end def render_unopened_json_response - if current_user.has_role?(:super_admin) || Rails.env.development? + if current_user @chat_channels_memberships = current_user. chat_channel_memberships.includes(:chat_channel). where(chat_channels: {channel_type: "direct"}, has_unopened_messages: true). @@ -82,26 +81,31 @@ class ChatChannelsController < ApplicationController render "index.json" end + def render_additional_json_response @chat_channels_memberships = current_user. chat_channel_memberships.includes(:chat_channel).limit(200).order("updated_at DESC") render "index.json" end - + def render_channels_html return unless current_user - @chat_channels = current_user.chat_channels. - order("last_message_at DESC"). - limit(25) - @chat_channels.each do |channel| - channel.current_user = current_user - end slug = if params[:slug] && params[:slug].start_with?("@") [current_user.username, params[:slug].gsub("@", "")].sort.join("/") else params[:slug] end - @active_channel = ChatChannel.find_by_slug(slug) || @chat_channels.first + @active_channel = ChatChannel.find_by_slug(slug) + @active_channel.current_user = current_user if @active_channel + generate_algolia_search_key # @twilio_token = TwilioToken.new(current_user).get end + + def generate_algolia_search_key + current_user_id = current_user.id + params = {filters: "viewable_by:#{current_user_id} AND status = true"} + @secured_algolia_key = Algolia.generate_secured_api_key( + ENV["ALGOLIASEARCH_SEARCH_ONLY_KEY"], params, + ) + end end diff --git a/app/javascript/chat/actions.js b/app/javascript/chat/actions.js index 7dc34aabb..47f9f6e92 100644 --- a/app/javascript/chat/actions.js +++ b/app/javascript/chat/actions.js @@ -72,15 +72,23 @@ export function conductModeration( .catch(failureCb); } -export function getAdditionalChannels(successCb, failureCb) { - fetch(`/chat_channels?state=additional`, { - Accept: 'application/json', - 'Content-Type': 'application/json', - credentials: 'same-origin', +export function getChannels(query,retrievalID, props, successCb, failureCb) { + const client = algoliasearch(props.algoliaId, props.algoliaKey); + const index = client.initIndex(props.algoliaIndex); + index.search(query,{ + hitsPerPage: 100, }) - .then(response => response.json()) - .then(successCb) - .catch(failureCb); + .then(function(content) { + let channels = content.hits + if (retrievalID === null || content.hits.filter(e => e.id === retrievalID).length === 1) { + successCb(channels) + } else { + index.getObjects([`${retrievalID}`], function(err, content) { + channels.unshift(content.results[0]); + successCb(channels); + }); + } + }); } export function sendKeys(subscription, successCb, failureCb) { diff --git a/app/javascript/chat/channels.jsx b/app/javascript/chat/channels.jsx index 96e77083a..0e3f13245 100644 --- a/app/javascript/chat/channels.jsx +++ b/app/javascript/chat/channels.jsx @@ -3,13 +3,24 @@ import PropTypes from 'prop-types'; const Channels = ({ activeChannelId, chatChannels, handleSwitchChannel }) => { const channels = chatChannels.map(channel => { + const isActive = parseInt(activeChannelId, 10) === channel.id + const lastOpened = channel.last_opened_at ? channel.last_opened_at : channel.channel_users[window.currentUser.username].last_opened_at + const isUnopened = new Date(channel.last_message_at) > new Date(lastOpened); + const otherClassname = - parseInt(activeChannelId, 10) === channel.id + isActive ? 'chatchanneltab--active' : 'chatchanneltab--inactive'; const name = channel.channel_type === "direct" ? '@'+channel.slug.replace(`${window.currentUser.username}/`, '').replace(`/${window.currentUser.username}`, '') : channel.channel_name - const newMessagesIndicatorClass = new Date(channel.last_opened_at) < new Date(channel.last_message_at) ? "chatchanneltabindicator--new" : "chatchanneltabindicator--old" + const newMessagesIndicatorClass = isUnopened ? "new" : "old" const modififedSlug = channel.channel_type === "direct" ? name : channel.slug; + const indicatorPic = channel.channel_type === "direct" ? : '' + let channelColor = 'transparent' + if (channel.channel_type === "direct" && isActive) { + channelColor = channel.channel_users[name.replace('@','')].darker_color; + } else if ( isActive ) { + channelColor = '#4e57ef' + } return ( ); diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx index d5f3f9792..54bfb7e88 100644 --- a/app/javascript/chat/chat.jsx +++ b/app/javascript/chat/chat.jsx @@ -1,6 +1,6 @@ import { h, Component } from 'preact'; import PropTypes from 'prop-types'; -import { conductModeration, getAllMessages, sendMessage, sendOpen, getAdditionalChannels, getContent } from './actions'; +import { conductModeration, getAllMessages, sendMessage, sendOpen, getChannels, getContent } from './actions'; import { hideMessages, scrollToBottom, setupObserver, setupNotifications, getNotificationState } from './util'; import Alert from './alert'; import Channels from './channels'; @@ -21,10 +21,7 @@ export default class Chat extends Component { const chatChannels = JSON.parse(this.props.chatChannels); const chatOptions = JSON.parse(this.props.chatOptions); this.state = { - messages: chatChannels.reduce( - (accumulator, target) => ({ ...accumulator, [target.id]: [] }), - {}, - ), + messages: [], scrolled: false, showAlert: false, chatChannels, @@ -32,13 +29,14 @@ export default class Chat extends Component { showChannelsList: chatOptions.showChannelsList, showTimestamp: chatOptions.showTimestamp, notificationsPermission: null, - activeContent: null + activeContent: null, + isMobileDevice: typeof window.orientation !== "undefined", }; } componentDidMount() { this.state.chatChannels.forEach((channel, index) => { - if ( index < 6 ) { + if ( index < 3 ) { this.setupChannel(channel.id); } if (channel.channel_type === "open") { @@ -49,14 +47,6 @@ export default class Chat extends Component { redactUserMessages: this.redactUserMessages, liveCoding: null }); - } else { - setupPusher(this.props.pusherKey, { - channelId: `presence-channel-${channel.id}`, - messageCreated: this.receiveNewMessage, - channelCleared: this.clearChannel, - redactUserMessages: this.redactUserMessages, - liveCoding: this.liveCoding - }); } }); setupObserver(this.observerCallback); @@ -66,16 +56,20 @@ export default class Chat extends Component { channelCleared: this.clearChannel, redactUserMessages: this.redactUserMessages, }); - sendOpen( - this.state.activeChannelId, - this.handleChannelOpenSuccess, - null, - ); + if (this.state.activeChannelId) { + sendOpen( + this.state.activeChannelId, + this.handleChannelOpenSuccess, + null, + ); + } this.setState({notificationsPermission: getNotificationState()}); if (this.state.showChannelsList) { - getAdditionalChannels(this.loadAdditionalChannels); + getChannels('', this.state.activeChannelId, this.props, this.loadChannels); + } + if (!this.state.isMobileDevice) { + document.getElementById("messageform").focus(); } - document.getElementById("messageform").focus(); } componentDidUpdate() { @@ -91,9 +85,32 @@ export default class Chat extends Component { this.setState({activeContent: {type_of: "code_editor"}}) } - loadAdditionalChannels = channels => { + loadChannels = channels => { this.setState({chatChannels: channels}); - this.setupChannel(this.state.activeChannelId); + if (this.state.activeChannelId) { + this.setupChannel(this.state.activeChannelId); + } else { + const channel = channels[0] + const channelSlug = channel.channel_type === 'direct' ? + '@'+channel.slug.replace(`${window.currentUser.username}/`, '').replace(`/${window.currentUser.username}`, '') : + channel.slug + this.triggerSwitchChannel(channel.id, channelSlug) + this.setupChannel(channel.id) + } + channels.forEach((channel, index) => { + if ( index < 3 ) { + this.setupChannel(channel.id); + } + if (channel.channel_type === "invite_only"){ + setupPusher(this.props.pusherKey, { + channelId: `presence-channel-${channel.id}`, + messageCreated: this.receiveNewMessage, + channelCleared: this.clearChannel, + redactUserMessages: this.redactUserMessages, + liveCoding: this.liveCoding + }); + } + }); } setupChannel = channelId => { @@ -101,6 +118,13 @@ export default class Chat extends Component { this.state.messages[channelId][0].reception_method === 'pushed'){ getAllMessages(channelId, this.receiveAllMessages); } + setupPusher(this.props.pusherKey, { + channelId: `presence-channel-${channelId}`, + messageCreated: this.receiveNewMessage, + channelCleared: this.clearChannel, + redactUserMessages: this.redactUserMessages, + liveCoding: this.liveCoding + }); }; @@ -134,12 +158,22 @@ export default class Chat extends Component { this.state.activeChannelId === receivedChatChannelId ? { showAlert: this.state.scrolled } : {}; - const newChannelsObj = this.state.chatChannels.map(channel => { + let newMessageChannelIndex = 0 + let newMessageChannel = null; + let newChannelsObj = this.state.chatChannels.map((channel, index) => { if (receivedChatChannelId === channel["id"]){ channel["last_message_at"] = new Date(); + newMessageChannelIndex = index; + newMessageChannel = channel; } return channel; }); + + if (newMessageChannelIndex > 0) { + newChannelsObj.splice(newMessageChannelIndex, 1); + newChannelsObj.unshift(newMessageChannel); + } + if (receivedChatChannelId === this.state.activeChannelId) { sendOpen( receivedChatChannelId, @@ -209,24 +243,31 @@ export default class Chat extends Component { handleSwitchChannel = e => { e.preventDefault(); - this.setupChannel(e.target.dataset.channelId); + this.triggerSwitchChannel(e.target.dataset.channelId, e.target.dataset.channelSlug); + }; + + triggerSwitchChannel = (id, slug) => { + this.setupChannel(id); this.setState({ - activeChannelId: parseInt(e.target.dataset.channelId), + activeChannelId: parseInt(id), scrolled: false, showAlert: false, activeContent: null }); - window.history.replaceState(null, null, "/connect/"+e.target.dataset.channelSlug); - document.getElementById("messageform").focus(); + window.history.replaceState(null, null, "/connect/"+slug); + if (!this.state.isMobileDevice) { + document.getElementById("messageform").focus(); + } if (window.ga && ga.create) { ga('send', 'pageview', location.pathname + location.search); } sendOpen( - e.target.dataset.channelId, + id, this.handleChannelOpenSuccess, null, ); - }; + + } handleSubmitOnClick = e => { e.preventDefault(); @@ -311,6 +352,10 @@ export default class Chat extends Component { )); }; + triggerChannelFilter = e => { + getChannels(e.target.value, null, this.props, this.loadChannels); + } + renderChatChannels = () => { if (this.state.showChannelsList) { const notificationsPermission = this.state.notificationsPermission; @@ -326,6 +371,7 @@ export default class Chat extends Component { return (
{notificationsButton} + { - console.log("live coding") if (e.keyPressed === true || e.value.length > 0) { let cm = document.querySelector(".CodeMirror").CodeMirror const cursorCoords = e.cursorPos @@ -51,7 +50,6 @@ export default class CodeEditor extends Component { cm.setBookmark(e.cursorPos, { widget: cursorElement }); } else { let cm = document.querySelector(".CodeMirror").CodeMirror - console.log("NEW LIVE CODING") channel.trigger('client-livecode', { value: cm.getValue(), cursorPos: cm.getCursor(), @@ -61,7 +59,9 @@ export default class CodeEditor extends Component { render() { - return
+ return
+
Experimental (WIP)
+
} diff --git a/app/javascript/chat/content.jsx b/app/javascript/chat/content.jsx index ad42974c1..ad5d0034a 100644 --- a/app/javascript/chat/content.jsx +++ b/app/javascript/chat/content.jsx @@ -49,7 +49,9 @@ function display(props) { margin:" 15px auto", display: "block", borderRadius: "500px"}} /> -

{props.resource.name}

+

+ {props.resource.name} +

{props.resource.summary}
@@ -62,7 +64,7 @@ function display(props) {

{props.resource.user.username}/ - {props.resource.user.name} + {props.resource.user.name}

diff --git a/app/javascript/packs/chat.jsx b/app/javascript/packs/chat.jsx index c4865cef5..60bd59f67 100644 --- a/app/javascript/packs/chat.jsx +++ b/app/javascript/packs/chat.jsx @@ -13,7 +13,7 @@ HTMLDocument.prototype.ready = new Promise(resolve => { document.ready.then( getUserDataAndCsrfToken().then(currentUser => { if (document.getElementById('chat')) { - const { chatChannels, pusherKey, chatOptions } = document.getElementById( + const { chatChannels, pusherKey, chatOptions, algoliaKey, algoliaId, algoliaIndex } = document.getElementById( 'chat', ).dataset; window.currentUser = currentUser; @@ -25,6 +25,9 @@ document.ready.then( pusherKey={pusherKey} chatChannels={chatChannels} chatOptions={chatOptions} + algoliaId={algoliaId} + algoliaKey={algoliaKey} + algoliaIndex={algoliaIndex} />, document.getElementById('chat'), ); diff --git a/app/javascript/src/utils/getUnopenedChannels.jsx b/app/javascript/src/utils/getUnopenedChannels.jsx index f3e9c0e95..a9317c32a 100644 --- a/app/javascript/src/utils/getUnopenedChannels.jsx +++ b/app/javascript/src/utils/getUnopenedChannels.jsx @@ -18,10 +18,17 @@ class UnopenedChannelNotice extends Component { channelId: `private-message-notifications-${window.currentUser.id}`, messageCreated: this.receiveNewMessage, }); + const component = this; + document.getElementById("connect-link").onclick = function(){ + //Hack, should probably be its own component in future + document.getElementById("connect-number").classList.remove("showing"); + component.setState({visible: false}); + } } receiveNewMessage = e => { - if (location.pathname.startsWith("/connect") || location.pathname.startsWith("/%F0%9F%92%8C")) { + console.log(location.pathname) + if (location.pathname.startsWith("/connect")) { return } let channels = this.state.unopenedChannels; @@ -31,6 +38,14 @@ class UnopenedChannelNotice extends Component { channels.push(newObj); } this.setState({visible: channels.length > 0, unopenedChannels: channels}) + + const number = document.getElementById("connect-number") + number.classList.add("showing") + number.innerHTML = channels.length + const component = this; + setTimeout(function(){ + component.setState({visible: false}); + }, 7500) } handleClick = e => { @@ -44,6 +59,7 @@ class UnopenedChannelNotice extends Component { style={{ background: "#66e2d5", color: "black", + border: "1px solid black", padding: "2px 7px", display: "inline-block", margin: "3px 6px", @@ -59,16 +75,15 @@ class UnopenedChannelNotice extends Component { top: '44px', right: 0, left: 0, - background: '#333333', + background: '#66e2d5', borderBottom: '1px solid black', - color: 'white', + color: 'black', fontWeight: 'bold', textAlign: 'center', - fontSize: '15px', + fontSize: '17px', opacity: '0.94', - padding: '12px 5px 3px'}}> + padding: '19px 5px 14px'}}> New Message from {channels} - (beta) ); } @@ -76,7 +91,8 @@ class UnopenedChannelNotice extends Component { } export default function getUnopenedChannels(user, successCb) { - if (location.pathname.startsWith("/connect") || location.pathname.startsWith("/%F0%9F%92%8C")) { + render(, document.getElementById('message-notice')); + if (location.pathname.startsWith("/connect")) { return } fetch('/chat_channels?state=unopened', { @@ -85,7 +101,11 @@ export default function getUnopenedChannels(user, successCb) { }) .then(response => response.json()) .then(json => { - render(, document.getElementById('message-notice')); + if (json.length > 0) { + const number = document.getElementById("connect-number") + number.classList.add("showing") + number.innerHTML = json.length + } }) .catch(error => { console.log(error); diff --git a/app/models/chat_channel.rb b/app/models/chat_channel.rb index c7cd6de9f..ed14f03d7 100644 --- a/app/models/chat_channel.rb +++ b/app/models/chat_channel.rb @@ -1,4 +1,5 @@ class ChatChannel < ApplicationRecord + include AlgoliaSearch attr_accessor :current_user has_many :messages @@ -6,8 +7,17 @@ class ChatChannel < ApplicationRecord has_many :users, through: :chat_channel_memberships validates :channel_type, presence: true, inclusion: { in: %w(open invite_only direct) } + validates :status, presence: true, inclusion: { in: %w(active inactive) } validates :slug, uniqueness: true, presence: true + algoliasearch index_name: "SecuredChatChannel_#{Rails.env}" do + attribute :id, :viewable_by, :slug, :channel_type, + :channel_name, :channel_users, :last_message_at, :status + searchableAttributes [:channel_name,:channel_slug] + attributesForFaceting ["filterOnly(viewable_by)","filterOnly(status)"] + ranking ["desc(last_message_at)"] + end + def clear_channel messages.each(&:destroy!) Pusher.trigger(pusher_channels, "channel-cleared", { chat_channel_id: id }.to_json) @@ -34,11 +44,12 @@ class ChatChannel < ApplicationRecord else slug = contrived_name.to_s.downcase.tr(" ", "-").gsub(/[^\w-]/, "").tr("_", "") + "-" + rand(100000).to_s(26) end - channel = create( + channel = where(slug: slug).first_or_create( channel_type: channel_type, channel_name: contrived_name, slug: slug, last_message_at: 1.week.ago, + status: "active", ) channel.add_users(users) channel @@ -71,4 +82,22 @@ class ChatChannel < ApplicationRecord slug end end + + def viewable_by + chat_channel_memberships.pluck(:user_id) + end + + def channel_users + pics_obj = {} + chat_channel_memberships.includes(:user).each do |m| + pics_obj[m.user.username] = { + profile_image: ProfileImage.new(m.user).get(90), + darker_color: m.user.decorate.darker_color, + name: m.user.name, + last_opened_at: m.last_opened_at, + username: m.user.username, + } + end + pics_obj + end end diff --git a/app/models/follow.rb b/app/models/follow.rb index 49594a885..0e2c450c6 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -28,6 +28,7 @@ class Follow < ApplicationRecord after_save :touch_user_followed_at after_create :send_email_notification after_create :create_chat_channel + before_destroy :modify_chat_channel_status validates :followable_id, uniqueness: { scope: [:followable_type, :follower_id] } @@ -79,4 +80,12 @@ class Follow < ApplicationRecord end end handle_asynchronously :send_email_notification + + def modify_chat_channel_status + if followable_type == "User" && followable.following?(follower) + follower.chat_channels. + where("slug LIKE ? OR slug like ?", "%/#{followable.username}%", "%#{followable.username}/%"). + first.update(status:"inactive") + end + end end diff --git a/app/models/message.rb b/app/models/message.rb index 0af7550b2..c021a6eb4 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -44,6 +44,7 @@ class Message < ApplicationRecord def update_chat_channel_last_message_at chat_channel.touch(:last_message_at) + chat_channel.index! end def update_all_has_unopened_messages_statuses diff --git a/app/models/user.rb b/app/models/user.rb index bd0f89fa1..12c50ccac 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -324,6 +324,10 @@ class User < ApplicationRecord if username_changed? self.old_old_username = old_username self.old_username = username_was + chat_channels.find_each do |c| + c.slug = c.slug.gsub(username_was, username) + c.save + end end end diff --git a/app/views/api/v0/users/show.json.jbuilder b/app/views/api/v0/users/show.json.jbuilder index 98c998c36..87437810b 100644 --- a/app/views/api/v0/users/show.json.jbuilder +++ b/app/views/api/v0/users/show.json.jbuilder @@ -4,8 +4,5 @@ json.name @user.name json.summary @user.summary json.twitter_username @user.twitter_username json.github_username @user.github_username -json.website_url @user.name -json.name @user.name -json.name @user.name -json.name @user.name +json.website_url @user.website_url json.profile_image ProfileImage.new(@user).get(320) \ No newline at end of file diff --git a/app/views/articles/_algolia_search.html.erb b/app/views/articles/_algolia_search.html.erb index a9ffaa6b4..4ddd2ce9a 100644 --- a/app/views/articles/_algolia_search.html.erb +++ b/app/views/articles/_algolia_search.html.erb @@ -25,7 +25,7 @@ document.activeElement.blur(); document.getElementById("nav-search").value = search_query document.getElementById("substories").innerHTML = '

' - var client = algoliasearch('<%= ENV["ALGOLIASEARCH_APPLICATION_ID"] %>', '<%= ENV["ALGOLIASEARCH_SEARCH_ONLY_KEY"] %>'); + var client = algoliasearch('<%= ENV["ALGOLIASEARCH_APPLICATION_ID"] %>', '<%= ENV["ALGOLIASEARCH_PUBLIC_SEARCH_ONLY_KEY"] %>'); var index = client.initIndex('searchables_<%= Rails.env %>'); if (document.getElementById("query-wrapper")) { search(query, index, filters); diff --git a/app/views/chat_channels/index.html.erb b/app/views/chat_channels/index.html.erb index 26c49dd80..9cb1c4119 100644 --- a/app/views/chat_channels/index.html.erb +++ b/app/views/chat_channels/index.html.erb @@ -1,5 +1,4 @@ -<% if user_signed_in? %> -<% end %> + <% title "DEV Connect" %> <%= content_for :page_meta do %> @@ -12,24 +11,27 @@ <%= javascript_pack_tag 'chat', defer: true %>
-
DEV Connect is Beta —— Post any thoughts in the "Meta" channel
+
DEV Connect is Beta (Work in Progress ❤️)
" - data-chat-channels="<%= @chat_channels.to_json(methods: :last_opened_at) %>" - data-chat-options="<%= {showChannelsList:true, showTimestamp: true, activeChannelId: @active_channel.id }.to_json %>" + data-algolia-id="<%= ENV["ALGOLIASEARCH_APPLICATION_ID"] %>" + data-algolia-key="<%= @secured_algolia_key %>" + data-algolia-index="<%= "SecuredChatChannel_#{Rails.env}" %>" + data-chat-channels="<%= [] %>" + data-chat-options="<%= {showChannelsList:true, showTimestamp: true, activeChannelId: @active_channel&.id }.to_json %>" data-twilio-token="<%= @twilio_token %>" >
diff --git a/app/views/layouts/_top_bar.html.erb b/app/views/layouts/_top_bar.html.erb index 7f184b261..594184728 100644 --- a/app/views/layouts/_top_bar.html.erb +++ b/app/views/layouts/_top_bar.html.erb @@ -9,6 +9,10 @@ <% end %>
WRITE A POST + + <%=inline_svg("connect.svg", size: '100% * 100%')%> +
+
<%=inline_svg("bell.svg", size: '100% * 100%')%>
diff --git a/config/initializers/algoliasearch.rb b/config/initializers/algoliasearch.rb index e8104fc4e..85388fbc1 100644 --- a/config/initializers/algoliasearch.rb +++ b/config/initializers/algoliasearch.rb @@ -1 +1,15 @@ AlgoliaSearch.configuration = { application_id: ENV["ALGOLIASEARCH_APPLICATION_ID"], api_key: ENV["ALGOLIASEARCH_API_KEY"] } + +if Rails.env.test? + ENV["ALGOLIASEARCH_PUBLIC_SEARCH_ONLY_KEY"] = "TEST_KEY" +else + # Restrtict Access to private indices + params = { + restrictIndices: "searchables_#{Rails.env},ordered_articles_#{Rails.env},ordered_articles_by_published_at_#{Rails.env},ordered_articles_by_positive_reactions_count_#{Rails.env},ordered_comments_#{Rails.env}", + } + secured_algolia_key = Algolia.generate_secured_api_key( + ENV["ALGOLIASEARCH_SEARCH_ONLY_KEY"], params, + ) + + ENV["ALGOLIASEARCH_PUBLIC_SEARCH_ONLY_KEY"] = secured_algolia_key +end \ No newline at end of file diff --git a/db/migrate/20180612214259_add_status_to_chat_channels.rb b/db/migrate/20180612214259_add_status_to_chat_channels.rb new file mode 100644 index 000000000..0387142ba --- /dev/null +++ b/db/migrate/20180612214259_add_status_to_chat_channels.rb @@ -0,0 +1,5 @@ +class AddStatusToChatChannels < ActiveRecord::Migration[5.1] + def change + add_column :chat_channels, :status, :string, default: "active" + end +end diff --git a/db/schema.rb b/db/schema.rb index 980cf8504..2250e83cc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180609191539) do +ActiveRecord::Schema.define(version: 20180612214259) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -180,6 +180,7 @@ ActiveRecord::Schema.define(version: 20180609191539) do t.datetime "created_at", null: false t.datetime "last_message_at", default: "2017-01-01 05:00:00" t.string "slug" + t.string "status", default: "active" t.datetime "updated_at", null: false end diff --git a/spec/models/chat_channel_spec.rb b/spec/models/chat_channel_spec.rb index 04cb216f6..b158eaded 100644 --- a/spec/models/chat_channel_spec.rb +++ b/spec/models/chat_channel_spec.rb @@ -17,5 +17,4 @@ RSpec.describe ChatChannel, type: :model do expect(chat_channel.users.size).to eq(2) expect(chat_channel.has_member?(User.first)).to eq(true) end - end