Add top bar indicator and other Connect features (#424)

* Add algolia public search key

* Add status to chat_channels
This commit is contained in:
Ben Halpern 2018-06-12 18:24:57 -04:00 committed by GitHub
parent 82dfc57b63
commit 87c6a758bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 374 additions and 97 deletions

View 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

View file

@ -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 ) {

View file

@ -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 = {

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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

View file

@ -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) {

View file

@ -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>
);

View file

@ -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}

View file

@ -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>
}

View file

@ -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>

View file

@ -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'),
);

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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);

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -0,0 +1,5 @@
class AddStatusToChatChannels < ActiveRecord::Migration[5.1]
def change
add_column :chat_channels, :status, :string, default: "active"
end
end

View file

@ -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

View file

@ -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