Add top bar indicator and other Connect features (#424)
* Add algolia public search key * Add status to chat_channels
This commit is contained in:
parent
82dfc57b63
commit
87c6a758bb
26 changed files with 374 additions and 97 deletions
41
app/assets/images/connect.svg
Normal file
41
app/assets/images/connect.svg
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 60.064 60.064" style="enable-background:new 0 0 60.064 60.064;" xml:space="preserve">
|
||||
<path d="M59.84,7.897c-0.218-0.268-0.556-0.393-0.893-0.353c-0.077,0.004-0.149,0.017-0.224,0.039L0.738,23.354
|
||||
C0.312,23.47,0.012,23.852,0,24.293c-0.011,0.441,0.269,0.838,0.688,0.976l21.217,6.952l-1.898,15.182
|
||||
c-0.05,0.4,0.145,0.791,0.494,0.991c0.155,0.089,0.327,0.133,0.498,0.133c0.215,0,0.43-0.069,0.608-0.206l7.765-5.946l6.807,9.725
|
||||
c0.188,0.269,0.494,0.427,0.819,0.427c0.022,0,0.045-0.001,0.068-0.002c0.35-0.024,0.661-0.229,0.821-0.542l22.063-43
|
||||
C60.134,8.631,60.09,8.205,59.84,7.897z M52.895,11.241L22.861,30.429L4.484,24.408L52.895,11.241z M22.288,45.281l1.382-11.053
|
||||
l4.555,6.507L22.288,45.281z M36.868,49.594L24.418,31.808l32.1-20.508L36.868,49.594z"/>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -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 ) {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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" ? <img src = {channel.channel_users[name.replace('@','')].profile_image} /> : ''
|
||||
let channelColor = 'transparent'
|
||||
if (channel.channel_type === "direct" && isActive) {
|
||||
channelColor = channel.channel_users[name.replace('@','')].darker_color;
|
||||
} else if ( isActive ) {
|
||||
channelColor = '#4e57ef'
|
||||
}
|
||||
return (
|
||||
<button
|
||||
className='chatchanneltabbutton'
|
||||
|
|
@ -17,14 +28,17 @@ const Channels = ({ activeChannelId, chatChannels, handleSwitchChannel }) => {
|
|||
data-channel-id={channel.id}
|
||||
data-channel-slug={modififedSlug}
|
||||
>
|
||||
<span className={`chatchanneltab ${otherClassname}`}
|
||||
<span className={`chatchanneltab ${otherClassname} chatchanneltab--${newMessagesIndicatorClass}`}
|
||||
data-channel-id={channel.id}
|
||||
data-channel-slug={modififedSlug}
|
||||
style={{border:`1px solid ${channelColor}`, boxShadow: `3px 3px 0px ${channelColor}`}}
|
||||
>
|
||||
<span
|
||||
data-channel-slug={modififedSlug}
|
||||
className={"chatchanneltabindicator " + newMessagesIndicatorClass}
|
||||
></span> {name}
|
||||
className={"chatchanneltabindicator chatchanneltabindicator--" + newMessagesIndicatorClass}
|
||||
>
|
||||
{indicatorPic}
|
||||
</span> {name}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="chat__channels">
|
||||
{notificationsButton}
|
||||
<input placeholder='Filter' onKeyUp={this.triggerChannelFilter} />
|
||||
<Channels
|
||||
activeChannelId={this.state.activeChannelId}
|
||||
chatChannels={this.state.chatChannels}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ export default class CodeEditor extends Component {
|
|||
}
|
||||
|
||||
liveCoding = e => {
|
||||
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 <div id="codeeditor" className="chatcodeeditor"></div>
|
||||
return <div id="codeeditor" className="chatcodeeditor">
|
||||
<div className="chatcodeeditor__header">Experimental (WIP)</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,9 @@ function display(props) {
|
|||
margin:" 15px auto",
|
||||
display: "block",
|
||||
borderRadius: "500px"}} />
|
||||
<h1 style={{textAlign: "center"}}>{props.resource.name}</h1>
|
||||
<h1 style={{textAlign: "center"}}>
|
||||
<a href={"/"+props.resource.username}>{props.resource.name}</a>
|
||||
</h1>
|
||||
<div style={{fontStyle: "italic"}}>
|
||||
{props.resource.summary}
|
||||
</div>
|
||||
|
|
@ -62,7 +64,7 @@ function display(props) {
|
|||
<h3>
|
||||
<a href={'/'+props.resource.user.username} class="author">
|
||||
<img class="profile-pic" src={props.resource.user.profile_image_90} alt={props.resource.user.username}/>
|
||||
<span itemprop="name">{props.resource.user.name}</span>
|
||||
<span>{props.resource.user.name}</span>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
<span style={{ color: "#fefa87"}}>(beta)</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
|
@ -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(<UnopenedChannelNotice unopenedChannels={[]} pusherKey={document.body.dataset.pusherKey} />, 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(<UnopenedChannelNotice unopenedChannels={json} pusherKey={document.body.dataset.pusherKey} />, 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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
document.activeElement.blur();
|
||||
document.getElementById("nav-search").value = search_query
|
||||
document.getElementById("substories").innerHTML = '<div class="query-results-nothing"><div class="query-results-loader"></div><br/></div>'
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<% if user_signed_in? %>
|
||||
<% end %>
|
||||
|
||||
<% title "DEV Connect" %>
|
||||
<%= content_for :page_meta do %>
|
||||
<link rel="canonical" href="https://dev.to/connect"/>
|
||||
|
|
@ -12,24 +11,27 @@
|
|||
<%= javascript_pack_tag 'chat', defer: true %>
|
||||
<style>
|
||||
.inner-content {
|
||||
height: calc(100vh - 115px);
|
||||
height: calc(100vh - 121px);
|
||||
margin: 100px auto;
|
||||
width: 96%;
|
||||
max-width: 1240px;
|
||||
}
|
||||
|
||||
.live-chat {
|
||||
height: calc(100vh - 115px);
|
||||
height: calc(100vh - 121px);
|
||||
}
|
||||
</style>
|
||||
<div class="inner-content">
|
||||
<div class="chat-page-header">DEV Connect is Beta —— Post any thoughts in the "Meta" channel</div>
|
||||
<div class="chat-page-header">DEV Connect is Beta (Work in Progress ❤️)</div>
|
||||
<div
|
||||
id="chat"
|
||||
class="live-chat"
|
||||
data-pusher-key="<%= ENV["PUSHER_KEY"] %>"
|
||||
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 %>"
|
||||
>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@
|
|||
<% end %>
|
||||
</div>
|
||||
<a href="/new" id="write-link" class="cta nav-link write">WRITE A POST</a>
|
||||
<a href="/connect" id="connect-link" class="nav-link connect-icon" aria-label="Connect">
|
||||
<%=inline_svg("connect.svg", size: '100% * 100%')%>
|
||||
<div class="connect-number" id="connect-number"></div>
|
||||
</a>
|
||||
<a href="/notifications" id="notifications-link" class="nav-link notifications-icon" aria-label="Notifications">
|
||||
<%=inline_svg("bell.svg", size: '100% * 100%')%>
|
||||
<div class="notifications-number" id="notifications-number"></div>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
5
db/migrate/20180612214259_add_status_to_chat_channels.rb
Normal file
5
db/migrate/20180612214259_add_status_to_chat_channels.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class AddStatusToChatChannels < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
add_column :chat_channels, :status, :string, default: "active"
|
||||
end
|
||||
end
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue