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 (