Update chat app (#326)
* Fix spacing * Update webpacker gem to 3.5 * Add custom preact-compact config * Add babel-loader to resolve a weird issue * Add react-textarea-autocomplete * Move 2 dependencies into devDependencies * Add babel-eslint & eslint-plugin-babel * Refactor chat * Add fixed width to chat's channels * Add timestamp for chat * Fix long word issue * Add showTimestamp options to chat * Add profile image to chat * Make profile image in chat circle * Improve message send logic * Add proper user session handling for chat * Choose profile user image source for chat * Adjust user's text color * Fix lint
This commit is contained in:
parent
e21544be52
commit
7cb65649e4
19 changed files with 899 additions and 758 deletions
12
.babelrc
12
.babelrc
|
|
@ -11,12 +11,12 @@
|
|||
"preact"
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": [
|
||||
"transform-es2015-modules-commonjs",
|
||||
["transform-react-jsx", { "pragma":"h" }],
|
||||
"transform-class-properties"
|
||||
]
|
||||
"test": {
|
||||
"plugins": [
|
||||
"transform-es2015-modules-commonjs",
|
||||
["transform-react-jsx", { "pragma":"h" }],
|
||||
"transform-class-properties"
|
||||
]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
|
|
|
|||
20
Gemfile.lock
20
Gemfile.lock
|
|
@ -189,7 +189,7 @@ GEM
|
|||
after_commit_action (~> 1.0)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.3)
|
||||
crass (1.0.4)
|
||||
csv_shaper (1.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
dalli (2.7.6)
|
||||
|
|
@ -244,7 +244,7 @@ GEM
|
|||
environs (1.1.0)
|
||||
equalizer (0.0.11)
|
||||
errbase (0.0.3)
|
||||
erubi (1.7.0)
|
||||
erubi (1.7.1)
|
||||
eventmachine (1.2.5)
|
||||
excon (0.60.0)
|
||||
execjs (2.7.0)
|
||||
|
|
@ -539,7 +539,7 @@ GEM
|
|||
logging (2.2.2)
|
||||
little-plugger (~> 1.1)
|
||||
multi_json (~> 1.10)
|
||||
loofah (2.2.0)
|
||||
loofah (2.2.2)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
lumberjack (1.0.12)
|
||||
|
|
@ -622,14 +622,14 @@ GEM
|
|||
multi_json (~> 1.0)
|
||||
pusher-signature (~> 0.1.8)
|
||||
pusher-signature (0.1.8)
|
||||
rack (2.0.4)
|
||||
rack (2.0.5)
|
||||
rack-host-redirect (1.3.0)
|
||||
rack
|
||||
rack-protection (2.0.1)
|
||||
rack
|
||||
rack-proxy (0.6.3)
|
||||
rack-proxy (0.6.4)
|
||||
rack
|
||||
rack-test (0.8.2)
|
||||
rack-test (1.0.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-timeout (0.4.2)
|
||||
rack_session_access (0.1.1)
|
||||
|
|
@ -655,8 +655,8 @@ GEM
|
|||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.0.3)
|
||||
loofah (~> 2.0)
|
||||
rails-html-sanitizer (1.0.4)
|
||||
loofah (~> 2.2, >= 2.2.2)
|
||||
rails-observers (0.1.5)
|
||||
activemodel (>= 4.0)
|
||||
rails_12factor (0.0.3)
|
||||
|
|
@ -671,7 +671,7 @@ GEM
|
|||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
rainbow (3.0.0)
|
||||
rake (12.3.0)
|
||||
rake (12.3.1)
|
||||
rb-fsevent (0.10.2)
|
||||
rb-inotify (0.9.10)
|
||||
ffi (>= 0.5.0, < 2)
|
||||
|
|
@ -881,7 +881,7 @@ GEM
|
|||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff
|
||||
webpacker (3.2.2)
|
||||
webpacker (3.5.3)
|
||||
activesupport (>= 4.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 4.2)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
}
|
||||
|
||||
.chat__channels {
|
||||
width: 276px;
|
||||
width: 160px;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
|
|
@ -33,15 +33,11 @@
|
|||
.activechatchannel__messages {
|
||||
font-size: 15px;
|
||||
padding: 10px 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
height: 300px;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
text-align: left;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
|
|
@ -75,8 +71,9 @@
|
|||
|
||||
// Chatmessage
|
||||
.chatmessage {
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
display: flex;
|
||||
// justify-content: space-between;
|
||||
|
||||
&:hover {
|
||||
background: #e3e3e3;
|
||||
|
|
@ -89,6 +86,7 @@
|
|||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: inherit;
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
.chatchanneltab {
|
||||
|
|
@ -140,23 +138,43 @@
|
|||
border-top: 1px solid #c9c9c9
|
||||
}
|
||||
|
||||
.chatmessage__username {
|
||||
.chatmessage__body {
|
||||
flex-grow: 1;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.chatmessage__timestamp {
|
||||
font-size: 11px;
|
||||
color: lightgray;
|
||||
min-width: 95px;
|
||||
}
|
||||
|
||||
.chatmessagebody__profileimage {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin-right: 3px;
|
||||
border-radius: 20px;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.chatmessagebody__username {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chatmessage__username--link {
|
||||
.chatmessagebody__username--link {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.chatmessage__divider {
|
||||
.chatmessagebody__divider {
|
||||
|
||||
}
|
||||
|
||||
.chatmessage__message {
|
||||
|
||||
.chatmessagebody__message {
|
||||
}
|
||||
|
||||
.chatmessage__currentuser {
|
||||
.chatmessagebody__currentuser {
|
||||
background: black;
|
||||
color: white;
|
||||
padding: 1px 5px;
|
||||
|
|
|
|||
|
|
@ -40,17 +40,13 @@ class MessagesController < ApplicationController
|
|||
user_id: new_message.user.id,
|
||||
chat_channel_id: new_message.chat_channel.id,
|
||||
username: new_message.user.username,
|
||||
profile_image_url: ProfileImage.new(new_message.user).get(90),
|
||||
message: new_message.message_html,
|
||||
timestamp: new_message.timestamp,
|
||||
color: darker_user_color(new_message),
|
||||
timestamp: new_message.created_at,
|
||||
color: new_message.preferred_user_color,
|
||||
}.to_json
|
||||
end
|
||||
|
||||
def darker_user_color(new_message)
|
||||
HexComparer.new([new_message.user.bg_color_hex || "#000000",
|
||||
new_message.user.text_color_hex || "#000000"]).brightness(0.9)
|
||||
end
|
||||
|
||||
def message_params
|
||||
params.require(:message).permit(:message_markdown, :user_id, :chat_channel_id)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
module.exports = {
|
||||
parser: 'babel-eslint',
|
||||
extends: ['airbnb', 'prettier'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2017,
|
||||
|
|
@ -12,7 +13,7 @@ module.exports = {
|
|||
jest: true,
|
||||
browser: true,
|
||||
},
|
||||
plugins: ['import'],
|
||||
plugins: ['import', 'babel'],
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
|
|
|
|||
|
|
@ -1,27 +1,26 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { conductModeration, getAllMessages, sendMessage } from './actions';
|
||||
import { hideMessages, scrollToBottom, setupObserver } from './util';
|
||||
import {
|
||||
hideMessages,
|
||||
scrollToBottom,
|
||||
setupObserver,
|
||||
} from './util';
|
||||
import Alert from './alert';
|
||||
import Channels from './channels';
|
||||
import Compose from './compose';
|
||||
import Message from './message';
|
||||
import setupPusher from './pusher';
|
||||
|
||||
class Chat extends Component {
|
||||
export default class Chat extends Component {
|
||||
static propTypes = {
|
||||
pusherKey: PropTypes.number.isRequired,
|
||||
chatChannels: PropTypes.array.isRequired,
|
||||
chatOptions: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleFailure = this.handleFailure.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handleMessageSubmit = this.handleMessageSubmit.bind(this);
|
||||
this.handleSubmitOnClick = this.handleSubmitOnClick.bind(this);
|
||||
this.handleSuccess = this.handleSuccess.bind(this);
|
||||
this.observerCallback = this.observerCallback.bind(this);
|
||||
this.receiveAllMessages = this.receiveAllMessages.bind(this);
|
||||
this.receiveNewMessage = this.receiveNewMessage.bind(this);
|
||||
this.clearChannel = this.clearChannel.bind(this);
|
||||
this.redactUserMessages = this.redactUserMessages.bind(this);
|
||||
this.handleSwitchChannel = this.handleSwitchChannel.bind(this);
|
||||
const chatChannels = JSON.parse(this.props.chatChannels);
|
||||
const chatOptions = JSON.parse(this.props.chatOptions);
|
||||
this.state = {
|
||||
|
|
@ -34,6 +33,7 @@ class Chat extends Component {
|
|||
chatChannels,
|
||||
activeChannelId: chatChannels[0].id,
|
||||
showChannelsList: chatOptions.showChannelsList,
|
||||
showTimestamp: chatOptions.showTimestamp,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ class Chat extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
setupChannel(channelId) {
|
||||
setupChannel = channelId => {
|
||||
getAllMessages(channelId, this.receiveAllMessages);
|
||||
setupPusher(this.props.pusherKey, {
|
||||
channelId,
|
||||
|
|
@ -58,9 +58,9 @@ class Chat extends Component {
|
|||
channelCleared: this.clearChannel,
|
||||
redactUserMessages: this.redactUserMessages,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
observerCallback(entries) {
|
||||
observerCallback = entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
this.setState({ scrolled: false, showAlert: false });
|
||||
|
|
@ -68,15 +68,15 @@ class Chat extends Component {
|
|||
this.setState({ scrolled: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
receiveAllMessages(res) {
|
||||
receiveAllMessages = res => {
|
||||
const { chatChannelId, messages } = res;
|
||||
const newMessages = { ...this.state.messages, [chatChannelId]: messages };
|
||||
this.setState({ messages: newMessages });
|
||||
}
|
||||
};
|
||||
|
||||
receiveNewMessage(message) {
|
||||
receiveNewMessage = message => {
|
||||
const receivedChatChannelId = message.chat_channel_id;
|
||||
const newMessages = this.state.messages[receivedChatChannelId].slice();
|
||||
newMessages.push(message);
|
||||
|
|
@ -94,31 +94,38 @@ class Chat extends Component {
|
|||
[receivedChatChannelId]: newMessages,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
redactUserMessages(res) {
|
||||
redactUserMessages = res => {
|
||||
// This is shallow clone. This might cause a problem
|
||||
const clonedMessages = Object.assign({}, this.state.messages);
|
||||
const newMessages = hideMessages(clonedMessages, res.userId);
|
||||
this.setState({ messages: newMessages });
|
||||
}
|
||||
};
|
||||
|
||||
clearChannel(res) {
|
||||
clearChannel = res => {
|
||||
const newMessages = { ...this.state.messages, [res.chat_channel_id]: [] };
|
||||
this.setState({ messages: newMessages });
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDown(e) {
|
||||
if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
if (e.target.value.length > 0) {
|
||||
handleKeyDown = e => {
|
||||
const enterPressed = e.keyCode === 13;
|
||||
const targetValue = e.target.value;
|
||||
const messageIsEmpty = targetValue.length === 0;
|
||||
const shiftPressed = e.shiftKey;
|
||||
|
||||
if (enterPressed) {
|
||||
if(messageIsEmpty) {
|
||||
e.preventDefault();
|
||||
} else if (!messageIsEmpty && !shiftPressed) {
|
||||
e.preventDefault();
|
||||
this.handleMessageSubmit(e.target.value);
|
||||
e.target.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleMessageSubmit(message) {
|
||||
handleMessageSubmit = message => {
|
||||
// should check if user has the priviledge
|
||||
if (message[0] === '/') {
|
||||
conductModeration(
|
||||
|
|
@ -135,50 +142,51 @@ class Chat extends Component {
|
|||
this.handleFailure,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleSwitchChannel(e) {
|
||||
handleSwitchChannel = e => {
|
||||
e.preventDefault();
|
||||
this.setState({
|
||||
activeChannelId: e.target.dataset.channelId,
|
||||
scrolled: false,
|
||||
showAlert: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleSubmitOnClick(e) {
|
||||
handleSubmitOnClick = e => {
|
||||
e.preventDefault();
|
||||
const message = document.getElementById('messageform').value;
|
||||
if (message.length > 0) {
|
||||
this.handleMessageSubmit(message);
|
||||
document.getElementById('messageform').value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleSuccess(response) {
|
||||
handleSuccess = response => {
|
||||
if (response.status === 'error') {
|
||||
this.receiveNewMessage(response.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleFailure(err) {
|
||||
handleFailure = err => {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
renderMessage() {
|
||||
const { activeChannelId, messages } = this.state;
|
||||
renderMessage = () => {
|
||||
const { activeChannelId, messages, showTimestamp } = this.state;
|
||||
return messages[activeChannelId].map(message => (
|
||||
<Message
|
||||
user={message.username}
|
||||
profileImageUrl={message.profile_image_url}
|
||||
message={message.message}
|
||||
timeStamp={message.timestamp}
|
||||
timestamp={showTimestamp ? message.timestamp : null}
|
||||
color={message.color}
|
||||
type={message.type}
|
||||
/>
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
renderChatChannels() {
|
||||
renderChatChannels = () => {
|
||||
if (this.state.showChannelsList) {
|
||||
return (
|
||||
<div className="chat__channels">
|
||||
|
|
@ -191,27 +199,25 @@ class Chat extends Component {
|
|||
);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
renderActiveChatChannel() {
|
||||
return (
|
||||
<div className="activechatchannel">
|
||||
<div className="activechatchannel__messages" id="messagelist">
|
||||
{this.renderMessage()}
|
||||
<div id="messagelist__sentinel" />
|
||||
</div>
|
||||
<div className="activechatchannel__alerts">
|
||||
<Alert showAlert={this.state.showAlert} />
|
||||
</div>
|
||||
<div className="activechatchannel__form">
|
||||
<Compose
|
||||
handleKeyDown={this.handleKeyDown}
|
||||
handleSubmitOnClick={this.handleSubmitOnClick}
|
||||
/>
|
||||
</div>
|
||||
renderActiveChatChannel = () => (
|
||||
<div className="activechatchannel">
|
||||
<div className="activechatchannel__messages" id="messagelist">
|
||||
{this.renderMessage()}
|
||||
<div id="messagelist__sentinel" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className="activechatchannel__alerts">
|
||||
<Alert showAlert={this.state.showAlert} />
|
||||
</div>
|
||||
<div className="activechatchannel__form">
|
||||
<Compose
|
||||
handleKeyDown={this.handleKeyDown}
|
||||
handleSubmitOnClick={this.handleSubmitOnClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
|
@ -222,11 +228,3 @@ class Chat extends Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
Chat.propTypes = {
|
||||
pusherKey: PropTypes.number.isRequired,
|
||||
chatChannels: PropTypes.array.isRequired,
|
||||
chatOptions: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,29 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { adjustTimestamp } from './util';
|
||||
import ErrorMessage from './messages/errorMessage';
|
||||
import Hiddenmessage from './messages/hiddenMessage';
|
||||
|
||||
/*
|
||||
* The prop also contain timeStamp, which is currently not in used
|
||||
*
|
||||
*/
|
||||
|
||||
const Message = ({ user, message, color, type }) => {
|
||||
const Message = ({
|
||||
user,
|
||||
message,
|
||||
color,
|
||||
type,
|
||||
timestamp,
|
||||
profileImageUrl,
|
||||
}) => {
|
||||
const spanStyle = { color };
|
||||
|
||||
if (type === 'error') {
|
||||
return <ErrorMessage message={message} />;
|
||||
} else if (type === 'hidden') {
|
||||
return <Hiddenmessage user={user} color={color} />;
|
||||
return (
|
||||
<Hiddenmessage
|
||||
user={user}
|
||||
color={color}
|
||||
profileImageUrl={profileImageUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const re = new RegExp(`@${window.currentUser.username}`);
|
||||
|
|
@ -23,31 +32,47 @@ const Message = ({ user, message, color, type }) => {
|
|||
|
||||
if (match) {
|
||||
messageArea = (
|
||||
<span className="chatmessage__message">
|
||||
<span className="chatmessagebody__message">
|
||||
{message.substr(0, match.index)}
|
||||
<span className="chatmessage__currentuser">
|
||||
<span className="chatmessagebody__currentuser">
|
||||
{`@${window.currentUser.username}`}
|
||||
</span>
|
||||
{message.substr(match.index + match[0].length)}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
messageArea = <span className="chatmessage__message">{message}</span>;
|
||||
messageArea = <span className="chatmessagebody__message">{message}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chatmessage">
|
||||
<span className="chatmessage__username" style={spanStyle}>
|
||||
<a
|
||||
className="chatmessage__username--link"
|
||||
href={`/${user}`}
|
||||
target="_blank"
|
||||
>
|
||||
{user}
|
||||
<div className="chatmessage__body">
|
||||
<a href={`/${user}`} target="_blank">
|
||||
<img
|
||||
className="chatmessagebody__profileimage"
|
||||
src={profileImageUrl}
|
||||
alt={`${user} profile`}
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
<span className="chatmessage__divider">: </span>
|
||||
{messageArea}
|
||||
<span className="chatmessagebody__username" style={spanStyle}>
|
||||
<a
|
||||
className="chatmessagebody__username--link"
|
||||
href={`/${user}`}
|
||||
target="_blank"
|
||||
>
|
||||
{user}
|
||||
</a>
|
||||
</span>
|
||||
<span className="chatmessagebody__divider">: </span>
|
||||
{messageArea}
|
||||
</div>
|
||||
{timestamp ? (
|
||||
<span className="chatmessage__timestamp">
|
||||
{`${adjustTimestamp(timestamp)}`}
|
||||
</span>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -57,12 +82,14 @@ Message.propTypes = {
|
|||
color: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
type: PropTypes.string,
|
||||
// hidden: PropTypes.bool,
|
||||
// error: PropTypes.bool,
|
||||
timestamp: PropTypes.string,
|
||||
profileImageUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
Message.defaultProps = {
|
||||
type: 'normalMessage',
|
||||
timestamp: null,
|
||||
profileImageUrl: '',
|
||||
};
|
||||
|
||||
export default Message;
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ const ErrorMessage = ({ message }) => {
|
|||
const errorStyle = { color: 'darksalmon', 'font-size': '13px' };
|
||||
return (
|
||||
<div className="chatmessage">
|
||||
<span className="chatmessage__message" style={errorStyle}>
|
||||
<span className="chatmessage__body" style={errorStyle}>
|
||||
{'Sorry '}
|
||||
<span className="chatmessage__currentuser">
|
||||
<span className="chatmessagebody__currentuser">
|
||||
{`@${window.currentUser.username}`}
|
||||
</span>
|
||||
{` ${message}`}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,35 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const HiddenMessage = ({ user, color }) => {
|
||||
const HiddenMessage = ({ user, color, profileImageUrl }) => {
|
||||
const spanStyle = { color };
|
||||
return (
|
||||
<div className="chatmessage">
|
||||
<span className="chatmessage__username" style={spanStyle}>
|
||||
<a
|
||||
className="chatmessage__username--link"
|
||||
href={`/${user}`}
|
||||
target="_blank"
|
||||
>
|
||||
{user}
|
||||
<div className="chatmessage__body">
|
||||
<a href={`/${user}`} target="_blank">
|
||||
<img
|
||||
className="chatmessagebody__profileimage"
|
||||
src={profileImageUrl}
|
||||
alt={`${user} profile`}
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
<span className="chatmessage__divider">: </span>
|
||||
<span className="chatmessage__message" style={{ color: 'lightgray' }}>
|
||||
{'<message removed>'}
|
||||
</span>
|
||||
<span className="chatmessagebody__username" style={spanStyle}>
|
||||
<a
|
||||
className="chatmessagebody__username--link"
|
||||
href={`/${user}`}
|
||||
target="_blank"
|
||||
>
|
||||
{user}
|
||||
</a>
|
||||
</span>
|
||||
<span className="chatmessagebody__divider">: </span>
|
||||
<span
|
||||
className="chatmessagebody__message"
|
||||
style={{ color: 'lightgray' }}
|
||||
>
|
||||
{'<message removed>'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -25,6 +37,7 @@ const HiddenMessage = ({ user, color }) => {
|
|||
HiddenMessage.propTypes = {
|
||||
user: PropTypes.string.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
profileImageUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default HiddenMessage;
|
||||
|
|
|
|||
|
|
@ -54,3 +54,15 @@ export function hideMessages(messages, userId) {
|
|||
);
|
||||
return cleanedMessages;
|
||||
}
|
||||
|
||||
export function adjustTimestamp(timestamp) {
|
||||
let time = new Date(timestamp);
|
||||
const options = {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
};
|
||||
time = new Intl.DateTimeFormat('en-US', options).format(time);
|
||||
return time;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ class Message < ApplicationRecord
|
|||
before_validation :evaluate_markdown
|
||||
before_validation :evaluate_channel_permission
|
||||
|
||||
def timestamp
|
||||
created_at.strftime("%H:%M:%S")
|
||||
def preferred_user_color
|
||||
color_options = [user.bg_color_hex || "#000000", user.text_color_hex || "#000000"]
|
||||
HexComparer.new(color_options).brightness(0.9)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
|
||||
json.messages @chat_channel.messages.order("created_at DESC").limit(50).reverse do |message|
|
||||
json.user_id message.user.id
|
||||
json.username message.user.username
|
||||
json.profile_image_url ProfileImage.new(message.user).get(90)
|
||||
json.message message.message_markdown
|
||||
json.timestamp message.timestamp
|
||||
json.color message.user.bg_color_hex
|
||||
json.timestamp message.created_at
|
||||
json.color message.preferred_user_color
|
||||
end
|
||||
|
||||
json.key_format! camelize: :lower
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
<% if user_signed_in? %>
|
||||
<%= javascript_pack_tag 'chat', defer: true %>
|
||||
<% end %>
|
||||
<% title "DEV Chat" %>
|
||||
<link rel="canonical" href="https://dev.to/chat"/>
|
||||
<meta name="description" content="DEV Chat">
|
||||
|
||||
<style>
|
||||
<% if user_signed_in? %>
|
||||
<%= javascript_pack_tag 'chat', defer: true %>
|
||||
<style>
|
||||
.inner-content {
|
||||
height: calc(100vh - 115px);
|
||||
margin: 100px auto;
|
||||
|
|
@ -13,11 +14,14 @@
|
|||
max-width: 1240px;
|
||||
}
|
||||
|
||||
.live-chat {
|
||||
height: calc(100vh - 115px);
|
||||
}
|
||||
</style>
|
||||
.live-chat {
|
||||
height: calc(100vh - 115px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="inner-content">
|
||||
<div id="chat" class="live-chat" data-pusher-key="<%= ENV["PUSHER_KEY"] %>" data-chat-channels="<%= @chat_channels %>" data-chat-options="<%= {showChannelsList:true}.to_json %>">
|
||||
</div>
|
||||
<div class="inner-content">
|
||||
<div id="chat" class="live-chat" data-pusher-key="<%= ENV["PUSHER_KEY"] %>" data-chat-channels="<%= @chat_channels %>" data-chat-options="<%= {showChannelsList:true, showTimestamp: true}.to_json %>">
|
||||
</div>
|
||||
<% else %>
|
||||
<%= render "devise/registrations/registration_form" %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
<% if (Flipflop.sendbird?) %>
|
||||
<div id="sb_chat" class="live-chat" data-sendbird-app-id="<%= ENV["SENDBIRD_APP_ID"] %>" data-sendbird-livechat-url="<%= ENV["SENDBIRD_LIVECHAT_URL"] %>"></div>
|
||||
<% else %>
|
||||
<div id="chat" class="live-chat" data-pusher-key="<%= ENV["PUSHER_KEY"] %>" data-chat-channels="<%= @chat_channels %>" data-chat-options="<%= {showChannelsList:false}.to_json %>">
|
||||
<div id="chat" class="live-chat" data-pusher-key="<%= ENV["PUSHER_KEY"] %>" data-chat-channels="<%= @chat_channels %>" data-chat-options="<%= {showChannelsList:false, showTimestamp: false}.to_json %>">
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
9
config/webpack/custom.js
Normal file
9
config/webpack/custom.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
resolve: {
|
||||
alias: {
|
||||
react: 'preact-compat',
|
||||
'react-dom': 'preact-compat',
|
||||
'create-react-class': 'preact-compat/lib/create-react-class',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
const { environment } = require('@rails/webpacker')
|
||||
const { environment } = require('@rails/webpacker');
|
||||
const customConfig = require('./custom');
|
||||
|
||||
module.exports = environment
|
||||
environment.config.set('resolve.extensions', ['.foo', '.bar']);
|
||||
environment.config.set('output.filename', '[name].js');
|
||||
environment.config.merge(customConfig);
|
||||
environment.config.delete('output.chunkFilename');
|
||||
|
||||
module.exports = environment;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20180516184437) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
|
|
|
|||
10
package.json
10
package.json
|
|
@ -47,13 +47,17 @@
|
|||
"@storybook/addons": "^3.3.15",
|
||||
"@storybook/react": "^3.3.15",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "8",
|
||||
"babel-jest": "^22.1.0",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-plugin-transform-react-jsx": "^6.24.1",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-preact": "^1.1.0",
|
||||
"eslint": "^4.9.0",
|
||||
"eslint-config-airbnb": "^16.1.0",
|
||||
"eslint-config-airbnb-base": "^12.0.0",
|
||||
"eslint-config-prettier": "^2.9.0",
|
||||
"eslint-plugin-babel": "^5.1.0",
|
||||
"eslint-plugin-ignore-erb": "^0.1.1",
|
||||
"eslint-plugin-import": "^2.10.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.0.2",
|
||||
|
|
@ -66,11 +70,11 @@
|
|||
"preact-render-spy": "^1.2.1",
|
||||
"preact-render-to-json": "^3.6.6",
|
||||
"prettier": "^1.11.1",
|
||||
"webpack-dev-server": "^2.9.1"
|
||||
"webpack-dev-server": "^2.11.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rails/webpacker": "^3.0.1",
|
||||
"babel-plugin-transform-react-jsx": "^6.24.1",
|
||||
"@rails/webpacker": "^3.5.3",
|
||||
"@webscopeio/react-textarea-autocomplete": "^2.2.0",
|
||||
"preact": "^8.2.5",
|
||||
"prop-types": "^15.6.0",
|
||||
"pusher-js": "^4.2.2",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue