diff --git a/Gemfile b/Gemfile
index 01e455c69..dd74f1c2f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -85,6 +85,7 @@ gem "stream_rails", "~> 2.5"
gem "stripe", "~> 3.9"
gem "therubyracer", "~> 0.12", platforms: :ruby
gem "timber", "~> 2.6"
+gem 'twilio-ruby', '~> 5.10.3'
gem "twitter", "~> 6.2"
gem "uglifier", "~> 3.2"
gem "validate_url", "~> 1.0"
diff --git a/Gemfile.lock b/Gemfile.lock
index c2451ab28..cf4134cf2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -841,6 +841,10 @@ GEM
msgpack (~> 1.0)
timecop (0.9.1)
trollop (2.1.2)
+ twilio-ruby (5.10.3)
+ faraday (~> 0.9)
+ jwt (>= 1.5, <= 2.5)
+ nokogiri (>= 1.6, < 2.0)
twitter (6.2.0)
addressable (~> 2.3)
buftok (~> 0.2.0)
@@ -1020,6 +1024,7 @@ DEPENDENCIES
therubyracer (~> 0.12)
timber (~> 2.6)
timecop (~> 0.9)
+ twilio-ruby (~> 5.10.3)
twitter (~> 6.2)
uglifier (~> 3.2)
validate_url (~> 1.0)
diff --git a/app/assets/stylesheets/chat.scss b/app/assets/stylesheets/chat.scss
index 8647313a0..db86f7b77 100644
--- a/app/assets/stylesheets/chat.scss
+++ b/app/assets/stylesheets/chat.scss
@@ -79,13 +79,20 @@
height: inherit;
max-height: inherit;
display: flex;
- flex-direction: column;
+ flex-direction: row;
justify-content: space-between;
border: 1px solid $outline-color;
box-shadow: $bold-shadow;
background: white;
}
+.activechatchannel__conversation{
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ flex-grow: 3;
+}
+
.activechatchannel__messages {
font-size: 15px;
padding: 10px 0px;
@@ -137,6 +144,36 @@
}
+.activechatchannel__activecontent{
+ border-left: 2px solid $outline-color;
+ padding: 13px;
+ min-width: 290px;
+ margin-left: -180px;
+ background: white;
+ flex-basis: 80%;
+ z-index: 200;
+ position: relative;
+ box-sizing: border-box;
+ overflow-y: scroll;
+ @media screen and ( min-width: 1000px ){
+ margin-left: initial;
+ min-width: 0px;
+ padding: 18px;
+ max-width:480px;
+ }
+}
+
+.activechatchannel__activecontentexitbutton{
+ border: 0px;
+ font-size: 36px;
+ color: $medium-gray;
+ padding:0px;
+ background: transparent;
+ position: absolute;
+ left: 6px;
+ top:-4px;
+}
+
.chatchannels {
display: flex;
flex-direction: column;
@@ -294,3 +331,46 @@
.messagelist__sentinel {
height: 5px;
}
+
+.chatcodeeditor{
+ position: absolute;
+ top: 40px;
+ left: 0px;
+ right: 24px;
+ bottom: 0px;
+}
+
+.CodeMirror{
+ padding: 12px;
+ font-size:0.9em;
+}
+
+.cursorelement{
+ border-left: 3px solid $red;
+ animation: blinker 1s linear infinite;
+ padding: 0px;
+ z-index: 0;
+}
+
+@keyframes blinker {
+ 50% {
+ opacity: 0;
+ }
+}
+
+.chat .container {
+ box-shadow: 0px 0px 0px #ffffff !important;
+ border: 0px !important;
+ margin-top: 8px !important;
+ .title{
+ width:95%;
+ font-size:18px;
+ h1{
+ font-size:28px !important;
+ }
+ }
+ .body {
+ font-size:20px;
+ width:96%;
+ }
+}
\ No newline at end of file
diff --git a/app/controllers/api/v0/articles_controller.rb b/app/controllers/api/v0/articles_controller.rb
index 8639ef4ef..56fc6aa2a 100644
--- a/app/controllers/api/v0/articles_controller.rb
+++ b/app/controllers/api/v0/articles_controller.rb
@@ -16,8 +16,12 @@ module Api
end
def show
- @article = Article.includes(:user).find(params[:id]).decorate
- not_found unless @article.published
+ if params[:id] == "by_path"
+ @article = Article.includes(:user).find_by_path(params[:url])&.decorate
+ else
+ @article = Article.includes(:user).find(params[:id])&.decorate
+ end
+ not_found unless @article&.published
end
def onboarding
diff --git a/app/controllers/api/v0/users_controller.rb b/app/controllers/api/v0/users_controller.rb
index f420d7608..1d286ff39 100644
--- a/app/controllers/api/v0/users_controller.rb
+++ b/app/controllers/api/v0/users_controller.rb
@@ -15,6 +15,10 @@ module Api
end
end
+ def show
+ @user = User.find(params[:id])
+ end
+
def less_than_one_day_old?(user)
range = (Time.now.beginning_of_day - 1.day)..(Time.now)
user_identity_age = user.github_created_at ||
diff --git a/app/controllers/chat_channels_controller.rb b/app/controllers/chat_channels_controller.rb
index 7be942754..5aee01d2f 100644
--- a/app/controllers/chat_channels_controller.rb
+++ b/app/controllers/chat_channels_controller.rb
@@ -4,13 +4,15 @@ 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
end
def show
- @chat_channel = ChatChannel.includes(:messages).find_by(id: params[:id])
+ @chat_channel = current_user.chat_channels.includes(:messages).find_by(id: params[:id])
if @chat_channel
@chat_channel
else
@@ -71,7 +73,7 @@ class ChatChannelsController < ApplicationController
def render_unopened_json_response
if current_user.has_role?(:super_admin) || Rails.env.development?
@chat_channels_memberships = current_user.
- chat_channel_memberships.
+ chat_channel_memberships.includes(:chat_channel).
where(has_unopened_messages: true).order("updated_at DESC")
else
@chat_channels_memberships = []
@@ -79,19 +81,26 @@ class ChatChannelsController < ApplicationController
render "index.json"
end
+ def render_additional_json_response
+ @chat_channels_memberships = current_user.
+ chat_channel_memberships.includes(:chat_channel).limit(50).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").
- includes(:chat_channel_memberships)
- @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
+ 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
+ # @twilio_token = TwilioToken.new(current_user).get
end
end
diff --git a/app/controllers/pusher_controller.rb b/app/controllers/pusher_controller.rb
index 99f57cff0..acd5481dc 100644
--- a/app/controllers/pusher_controller.rb
+++ b/app/controllers/pusher_controller.rb
@@ -20,7 +20,7 @@ class PusherController < ApplicationController
end
def valid_presence_channel
- id = params[:channel_name].split("presence-channel-")[1]
+ id = params[:channel_name].split("presence-channel-")[1].split("-")[0]
channel = ChatChannel.find(id)
channel.has_member?(current_user)
end
diff --git a/app/javascript/chat/actions.js b/app/javascript/chat/actions.js
index 992aba23f..de8743738 100644
--- a/app/javascript/chat/actions.js
+++ b/app/javascript/chat/actions.js
@@ -2,6 +2,7 @@ export function getAllMessages(channelId, successCb, failureCb) {
fetch(`/chat_channels/${channelId}`, {
Accept: 'application/json',
'Content-Type': 'application/json',
+ credentials: 'same-origin',
})
.then(response => response.json())
.then(successCb)
@@ -71,6 +72,16 @@ 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',
+ })
+ .then(response => response.json())
+ .then(successCb)
+ .catch(failureCb);
+}
export function sendKeys(subscription, successCb, failureCb) {
fetch(`/push_notification_subscriptions`, {
@@ -88,4 +99,15 @@ export function sendKeys(subscription, successCb, failureCb) {
.then(response => response.json())
.then(successCb)
.catch(failureCb);
+}
+
+export function getContent(url, successCb, failureCb) {
+ fetch(`${url}`, {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ credentials: 'same-origin',
+ })
+ .then(response => response.json())
+ .then(successCb)
+ .catch(failureCb);
}
\ No newline at end of file
diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx
index 4bfe02f63..af809a49a 100644
--- a/app/javascript/chat/chat.jsx
+++ b/app/javascript/chat/chat.jsx
@@ -1,11 +1,12 @@
import { h, Component } from 'preact';
import PropTypes from 'prop-types';
-import { conductModeration, getAllMessages, sendMessage, sendOpen } from './actions';
+import { conductModeration, getAllMessages, sendMessage, sendOpen, getAdditionalChannels, getContent } from './actions';
import { hideMessages, scrollToBottom, setupObserver, setupNotifications, getNotificationState } from './util';
import Alert from './alert';
import Channels from './channels';
import Compose from './compose';
import Message from './message';
+import Content from './content';
import setupPusher from '../src/utils/pusher';
export default class Chat extends Component {
@@ -30,7 +31,8 @@ export default class Chat extends Component {
activeChannelId: chatOptions.activeChannelId,
showChannelsList: chatOptions.showChannelsList,
showTimestamp: chatOptions.showTimestamp,
- notificationsPermission: null
+ notificationsPermission: null,
+ activeContent: null
};
}
@@ -39,14 +41,13 @@ export default class Chat extends Component {
if ( index < 6 ) {
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,
- });
- }
+ setupPusher(this.props.pusherKey, {
+ channelId: `presence-channel-${channel.id}`,
+ messageCreated: this.receiveNewMessage,
+ channelCleared: this.clearChannel,
+ redactUserMessages: this.redactUserMessages,
+ liveCoding: this.liveCoding
+ });
});
setupObserver(this.observerCallback);
setupPusher(this.props.pusherKey, {
@@ -61,6 +62,8 @@ export default class Chat extends Component {
null,
);
this.setState({notificationsPermission: getNotificationState()});
+ getAdditionalChannels(this.loadAdditionalChannels);
+ document.getElementById("messageform").focus();
}
componentDidUpdate() {
@@ -69,11 +72,23 @@ export default class Chat extends Component {
}
}
+ liveCoding = e => {
+ if (this.state.activeContent === {type_of: "code_editor"}) {
+ return
+ }
+ this.setState({activeContent: {type_of: "code_editor"}})
+ }
+
+ loadAdditionalChannels = channels => {
+ this.setState({chatChannels: channels});
+ }
+
setupChannel = channelId => {
if (this.state.messages[channelId].length === 0 || this.state.messages[channelId][0].reception_method === 'pushed'){
getAllMessages(channelId, this.receiveAllMessages);
}
};
+
observerCallback = entries => {
entries.forEach(entry => {
@@ -156,7 +171,9 @@ export default class Chat extends Component {
handleMessageSubmit = message => {
// should check if user has the priviledge
- if (message[0] === '/') {
+ if (message.startsWith('/code')) {
+ this.setState({activeContent: {type_of: "code_editor"}})
+ } else if (message[0] === '/') {
conductModeration(
this.state.activeChannelId,
message,
@@ -180,8 +197,9 @@ export default class Chat extends Component {
activeChannelId: parseInt(e.target.dataset.channelId),
scrolled: false,
showAlert: false,
+ activeContent: null
});
- window.history.replaceState(null, null, "/gether/"+e.target.dataset.channelSlug);
+ window.history.replaceState(null, null, "/connect/"+e.target.dataset.channelSlug);
document.getElementById("messageform").focus();
if (window.ga && ga.create) {
ga('send', 'pageview', location.pathname + location.search);
@@ -218,6 +236,26 @@ export default class Chat extends Component {
});
}
+ triggerActiveContent = e => {
+ const target = e.target
+ if (e.target.dataset.content && e.target.dataset.content != "exit") {
+ e.preventDefault();
+ this.setState({activeContent: {type_of: "loading"}})
+ getContent('/api/'+target.dataset.content, this.setActiveContent, null)
+ }
+ else if (target.tagName.toLowerCase() === 'a' && target.href.startsWith('https://dev.to/')) {
+ e.preventDefault();
+ getContent(`/api/articles/by_path?url=${target.href.split('https://dev.to')[1]}`, this.setActiveContent, null)
+ } else if (target.dataset.content === "exit") {
+ e.preventDefault();
+ this.setState({activeContent: null})
+ }
+ }
+
+ setActiveContent = response => {
+ this.setState({activeContent: response});
+ }
+
handleChannelOpenSuccess = response => {
const newChannelsObj = this.state.chatChannels.map(channel => {
if (parseInt(response.channel) === channel["id"]){
@@ -232,17 +270,19 @@ export default class Chat extends Component {
console.error(err);
};
- renderMessage = () => {
+ renderMessages = () => {
const { activeChannelId, messages, showTimestamp } = this.state;
return messages[activeChannelId].map(message => (