From b440e539ea7d138704daed7bda854a7905a62b65 Mon Sep 17 00:00:00 2001
From: Sarthak <7lovesharma7@gmail.com>
Date: Mon, 9 Dec 2019 15:15:15 +0530
Subject: [PATCH 01/10] =?UTF-8?q?Feature=20=F0=9F=9A=80=20:=20Ability=20to?=
=?UTF-8?q?=20delete=20messages=20in=20chat=20channels?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Sending message ID to frontend
- Deleting Message
- Use pusher to delete message realtime
---
app/assets/stylesheets/chat.scss | 65 +++++++++++++
app/controllers/messages_controller.rb | 41 +++++++-
.../__snapshots__/chat.test.jsx.snap | 34 +++++++
.../__snapshots__/message.test.jsx.snap | 56 +++++++----
app/javascript/chat/actions.js | 20 ++++
app/javascript/chat/chat.jsx | 95 ++++++++++++++++++-
app/javascript/chat/message.jsx | 60 ++++++++----
.../src/utils/getUnopenedChannels.jsx | 15 +--
app/javascript/src/utils/pusher.js | 1 +
app/policies/message_policy.rb | 10 ++
app/views/chat_channels/show.json.jbuilder | 1 +
config/routes.rb | 1 +
12 files changed, 352 insertions(+), 47 deletions(-)
diff --git a/app/assets/stylesheets/chat.scss b/app/assets/stylesheets/chat.scss
index 30a71a795bc07..c7685796e9f66 100644
--- a/app/assets/stylesheets/chat.scss
+++ b/app/assets/stylesheets/chat.scss
@@ -1226,4 +1226,69 @@
.chatchanneljumpback__hide{
display: none;
+}
+
+.message__info__actions{
+ display: grid;
+ grid-template-columns: 4fr 1fr;
+}
+.message__actions{
+ display: none;
+ justify-content: end;
+}
+.message__actions span {
+ margin-left: 7px;
+ font-size: 13px;
+ font-weight: bold;
+ cursor: pointer;
+}
+.chatmessage:hover .message__actions{
+ display: flex;
+}
+
+.message__delete__modal{
+ position: fixed;
+ background-color: rgba(255, 255, 255, 0.25);
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 999;
+ display: block;
+ transition: all 0.3s;
+}
+.modal__content{
+ width: 400px;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ -webkit-transform: translate(-50%, -50%);
+ transform: translate(-50%, -50%);
+ padding: 1em;
+ border: 2px solid;
+ background: var(--theme-background, #f5f6f7);
+ color: var(--theme-color, $black);
+}
+.modal__content h3{
+ margin: 20px 0;
+ text-align: center;
+}
+.delete__action__buttons{
+ display: flex;
+ justify-content: space-around;
+ margin: 40px 0px 20px 0;
+}
+
+.delete__action__buttons div {
+ border: 2px solid var(--theme-color, $black);
+ padding: 5px 20px;
+ cursor: pointer;
+ user-select: none;
+}
+.message__delete__modal__hide{
+ display: none;
+}
+.message__delete__button{
+ background: #ff0000;
+ color: white;
}
\ No newline at end of file
diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb
index 821a338947f61..7a91fe9f47d5e 100644
--- a/app/controllers/messages_controller.rb
+++ b/app/controllers/messages_controller.rb
@@ -1,14 +1,16 @@
class MessagesController < ApplicationController
- before_action :authenticate_user!
+ before_action :set_message, only: %i[destroy]
+ before_action :authenticate_user!, only: %i[create]
def create
@message = Message.new(message_params)
@message.user_id = session_current_user_id
+ @temp_message_id = (0...20).map { ("a".."z").to_a[rand(8)] }.join
authorize @message
if @message.valid?
begin
- message_json = create_pusher_payload(@message)
+ message_json = create_pusher_payload(@message, @temp_message_id)
Pusher.trigger(@message.chat_channel.pusher_channels, "message-created", message_json)
rescue Pusher::Error => e
logger.info "PUSHER ERROR: #{e.message}"
@@ -16,7 +18,32 @@ def create
end
if @message.save
- render json: { status: "success", message: "Message created" }, status: :created
+ render json: { status: "success", message: { temp_id: @temp_message_id, id: @message.id } }, status: :created
+ else
+ render json: {
+ status: "error",
+ message: {
+ chat_channel_id: @message.chat_channel_id,
+ message: @message.errors.full_messages,
+ type: "error"
+ }
+ }, status: :unauthorized
+ end
+ end
+
+ def destroy
+ authorize @message
+
+ if @message.valid?
+ begin
+ Pusher.trigger(@message.chat_channel.pusher_channels, "message-deleted", @message.to_json)
+ rescue Pusher::Error => e
+ logger.info "PUSHER ERROR: #{e.message}"
+ end
+ end
+
+ if @message.destroy
+ render json: { status: "success", message: "Message was deleted" }
else
render json: {
status: "error",
@@ -31,8 +58,9 @@ def create
private
- def create_pusher_payload(new_message)
+ def create_pusher_payload(new_message, temp_id)
{
+ temp_id: temp_id,
user_id: new_message.user.id,
chat_channel_id: new_message.chat_channel.id,
chat_channel_adjusted_slug: new_message.chat_channel.adjusted_slug(current_user, "sender"),
@@ -49,6 +77,11 @@ def message_params
params.require(:message).permit(:message_markdown, :user_id, :chat_channel_id)
end
+ def set_message
+ logger.info "PUSHER ERROR: #{params[:id]}"
+ @message = Message.find(params[:id])
+ end
+
def user_not_authorized
respond_to do |format|
format.json do
diff --git a/app/javascript/chat/__tests__/__snapshots__/chat.test.jsx.snap b/app/javascript/chat/__tests__/__snapshots__/chat.test.jsx.snap
index cd6c4704c444a..22ee18eec3f04 100644
--- a/app/javascript/chat/__tests__/__snapshots__/chat.test.jsx.snap
+++ b/app/javascript/chat/__tests__/__snapshots__/chat.test.jsx.snap
@@ -198,6 +198,40 @@ exports[` should load chat 1`] = `
Scroll to Bottom
+
+
+
+ Are you sure, you want to delete this message ?
+
+
+
+
diff --git a/app/javascript/chat/__tests__/__snapshots__/message.test.jsx.snap b/app/javascript/chat/__tests__/__snapshots__/message.test.jsx.snap
index d92a9e227d405..91f840a043cc4 100644
--- a/app/javascript/chat/__tests__/__snapshots__/message.test.jsx.snap
+++ b/app/javascript/chat/__tests__/__snapshots__/message.test.jsx.snap
@@ -26,25 +26,47 @@ exports[`
should render and test snapshot 1`] = `
class="chatmessage__body"
role="presentation"
>
-
-
- asdf
-
-
-
+
+
+ asdf
+
+
+
+
+
+
+ Delete
+
+
+ Edit
+
+
+
diff --git a/app/javascript/chat/actions.js b/app/javascript/chat/actions.js
index 7e516d8ff658a..99bffbacc39a6 100644
--- a/app/javascript/chat/actions.js
+++ b/app/javascript/chat/actions.js
@@ -178,3 +178,23 @@ export function sendChannelInviteAction(id, action, successCb, failureCb) {
.then(successCb)
.catch(failureCb);
}
+
+export function deleteMessage(messageId, successCb, failureCb) {
+ fetch(`/messages/${messageId}`, {
+ method: 'DELETE',
+ headers: {
+ Accept: 'application/json',
+ 'X-CSRF-Token': window.csrfToken,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ message: {
+ user_id: window.currentUser.id,
+ },
+ }),
+ credentials: 'same-origin',
+ })
+ .then(response => response.json())
+ .then(successCb)
+ .catch(failureCb);
+}
diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx
index 897d0650cac5b..a366ebac67c35 100644
--- a/app/javascript/chat/chat.jsx
+++ b/app/javascript/chat/chat.jsx
@@ -10,6 +10,7 @@ import {
getContent,
getChannelInvites,
sendChannelInviteAction,
+ deleteMessage,
} from './actions';
import { hideMessages, scrollToBottom, setupObserver } from './util';
import Alert from './alert';
@@ -61,6 +62,8 @@ export default class Chat extends Component {
soundOn: true,
videoOn: true,
messageOffset: 0,
+ showDeleteModal: false,
+ messageDeleteId: null,
allMessagesLoaded: false,
currentMessageLocation: 0,
};
@@ -166,6 +169,7 @@ export default class Chat extends Component {
setupPusher(pusherKey, {
channelId: channelName,
messageCreated: this.receiveNewMessage,
+ messageDeleted: this.removeMessage,
channelCleared: this.clearChannel,
redactUserMessages: this.redactUserMessages,
channelError: this.channelError,
@@ -302,10 +306,24 @@ export default class Chat extends Component {
}));
};
+ removeMessage = message => {
+ const { activeChannelId } = this.state;
+ this.setState(prevState => ({
+ messages: {
+ [activeChannelId]: {
+ ...prevState.messages[activeChannelId].filter(
+ oldmessage => oldmessage.id !== message.id,
+ ),
+ },
+ },
+ }));
+ };
+
receiveNewMessage = message => {
const { messages, activeChannelId, scrolled, chatChannels } = this.state;
const receivedChatChannelId = message.chat_channel_id;
let newMessages = [];
+
if (messages[receivedChatChannelId]) {
newMessages = messages[receivedChatChannelId].slice();
newMessages.push(message);
@@ -524,8 +542,25 @@ export default class Chat extends Component {
}
};
+ triggerDeleteMessage = e => {
+ this.setState({ messageDeleteId: e.target.dataset.content });
+ this.setState({ showDeleteModal: true });
+ };
+
handleSuccess = response => {
- if (response.status === 'error') {
+ const { activeChannelId } = this.state;
+ if (response.status === 'success') {
+ if (response.message.temp_id) {
+ this.setState(({ messages }) => {
+ const newMessages = messages;
+ const foundIndex = messages[activeChannelId].findIndex(
+ message => message.temp_id === response.message.temp_id,
+ );
+ newMessages[activeChannelId][foundIndex].id = response.message.id;
+ return { messages: newMessages };
+ });
+ }
+ } else if (response.status === 'error') {
this.receiveNewMessage(response.message);
}
};
@@ -717,6 +752,7 @@ export default class Chat extends Component {
}
return messages[activeChannelId].map(message => (
));
};
@@ -951,6 +988,7 @@ export default class Chat extends Component {
Scroll to Bottom
+ {this.renderDeleteModal()}
@@ -974,6 +1012,61 @@ export default class Chat extends Component {
);
};
+ renderDeleteModal = () => {
+ const { showDeleteModal } = this.state;
+ return (
+
+
+
Are you sure, you want to delete this message ?
+
+
+
{
+ if (e.keyCode === 13) this.handleCloseDeleteModal();
+ }}
+ >
+ {' '}
+ Cancel
+
+
{
+ if (e.keyCode === 13) this.handleMessageDelete();
+ }}
+ >
+ {' '}
+ Delete
+
+
+
+
+ );
+ };
+
+ handleCloseDeleteModal = () => {
+ this.setState({ showDeleteModal: false, messageDeleteId: null });
+ };
+
+ handleMessageDelete = () => {
+ const { messageDeleteId } = this.state;
+ deleteMessage(messageDeleteId);
+ this.setState({ showDeleteModal: false });
+ };
+
renderChannelHeaderInner = () => {
const { activeChannel, activeChannelId } = this.state;
if (activeChannel.channel_type === 'direct') {
diff --git a/app/javascript/chat/message.jsx b/app/javascript/chat/message.jsx
index 4ae3087f87ddf..345bf073b1be2 100644
--- a/app/javascript/chat/message.jsx
+++ b/app/javascript/chat/message.jsx
@@ -4,6 +4,7 @@ import { adjustTimestamp } from './util';
import ErrorMessage from './messages/errorMessage';
const Message = ({
+ id,
user,
userID,
message,
@@ -12,6 +13,7 @@ const Message = ({
timestamp,
profileImageUrl,
onContentTrigger,
+ onDeleteMessageTrigger,
}) => {
const spanStyle = { color };
@@ -52,25 +54,43 @@ const Message = ({
className="chatmessage__body"
onClick={onContentTrigger}
>
-
-
- {user}
-
-
- {timestamp ? (
-
- {`${adjustTimestamp(timestamp)}`}
-
- ) : (
-
- )}
+
+
+
+
+ {user}
+
+
+ {timestamp ? (
+
+ {`${adjustTimestamp(timestamp)}`}
+
+ ) : (
+
+ )}
+
+
+ {
+ if (e.keyCode === 13) onDeleteMessageTrigger();
+ }}
+ >
+ Delete
+
+ Edit
+
+
{messageArea}
@@ -78,6 +98,7 @@ const Message = ({
};
Message.propTypes = {
+ id: PropTypes.number.isRequired,
user: PropTypes.string.isRequired,
userID: PropTypes.number.isRequired,
color: PropTypes.string.isRequired,
@@ -86,6 +107,7 @@ Message.propTypes = {
timestamp: PropTypes.string,
profileImageUrl: PropTypes.string,
onContentTrigger: PropTypes.func.isRequired,
+ onDeleteMessageTrigger: PropTypes.func.isRequired,
};
Message.defaultProps = {
diff --git a/app/javascript/src/utils/getUnopenedChannels.jsx b/app/javascript/src/utils/getUnopenedChannels.jsx
index 312da141ed44f..bca546e7ba7a5 100644
--- a/app/javascript/src/utils/getUnopenedChannels.jsx
+++ b/app/javascript/src/utils/getUnopenedChannels.jsx
@@ -3,16 +3,16 @@ import PropTypes from 'prop-types';
import setupPusher from './pusher';
class UnopenedChannelNotice extends Component {
- static defaultProps = {
- unopenedChannels: undefined,
- pusherKey: undefined,
- };
-
propTypes = {
unopenedChannels: PropTypes.Object,
pusherKey: PropTypes.Object,
};
+ static defaultProps = {
+ unopenedChannels: undefined,
+ pusherKey: undefined,
+ };
+
constructor(props) {
super(props);
const { unopenedChannels } = this.props;
@@ -28,6 +28,7 @@ class UnopenedChannelNotice extends Component {
setupPusher(pusherKey, {
channelId: `private-message-notifications-${window.currentUser.id}`,
messageCreated: this.receiveNewMessage,
+ messageDeleted: this.removeMessage,
});
const component = this;
document.getElementById('connect-link').onclick = () => {
@@ -37,6 +38,8 @@ class UnopenedChannelNotice extends Component {
};
}
+ removeMessage = () => {};
+
receiveNewMessage = e => {
if (window.location.pathname.startsWith('/connect')) {
return;
@@ -118,7 +121,7 @@ class UnopenedChannelNotice extends Component {
padding: '19px 5px 14px',
}}
>
- New Message from
+ New Message from
{' '}
{channels}
diff --git a/app/javascript/src/utils/pusher.js b/app/javascript/src/utils/pusher.js
index 4f905ed26ce88..d1ad57e474e7c 100644
--- a/app/javascript/src/utils/pusher.js
+++ b/app/javascript/src/utils/pusher.js
@@ -19,6 +19,7 @@ export default function setupPusher(key, callbackObjects) {
const channel = pusher.subscribe(callbackObjects.channelId.toString());
channel.bind('message-created', callbackObjects.messageCreated);
+ channel.bind('message-deleted', callbackObjects.messageDeleted);
channel.bind('channel-cleared', callbackObjects.channelCleared);
channel.bind('user-banned', callbackObjects.redactUserMessages);
channel.bind('client-livecode', callbackObjects.liveCoding);
diff --git a/app/policies/message_policy.rb b/app/policies/message_policy.rb
index 3f38f236cc687..ae954e69ce6e2 100644
--- a/app/policies/message_policy.rb
+++ b/app/policies/message_policy.rb
@@ -2,4 +2,14 @@ class MessagePolicy < ApplicationPolicy
def create?
!user_is_banned?
end
+
+ def destroy?
+ user_is_sender?
+ end
+
+ private
+
+ def user_is_sender?
+ record.user_id == user.id
+ end
end
diff --git a/app/views/chat_channels/show.json.jbuilder b/app/views/chat_channels/show.json.jbuilder
index 1c5176ff4d32b..2a22cc35e8ce1 100644
--- a/app/views/chat_channels/show.json.jbuilder
+++ b/app/views/chat_channels/show.json.jbuilder
@@ -1,4 +1,5 @@
json.messages @chat_messages.reverse do |message|
+ json.id message.id
json.user_id message.user_id
json.username message.user.username
json.profile_image_url ProfileImage.new(message.user).get(90)
diff --git a/config/routes.rb b/config/routes.rb
index 66e29b5314c4b..a39f86580eede 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -218,6 +218,7 @@
get "/connect/:slug" => "chat_channels#index"
post "/chat_channels/create_chat" => "chat_channels#create_chat"
post "/chat_channels/block_chat" => "chat_channels#block_chat"
+ delete "/messages/:id" => "messages#destroy"
get "/live/:username" => "twitch_live_streams#show"
post "/pusher/auth" => "pusher#auth"
From e6f8d2922f76d5375d7005290dc78db91872272d Mon Sep 17 00:00:00 2001
From: Sarthak <7lovesharma7@gmail.com>
Date: Mon, 9 Dec 2019 16:58:03 +0530
Subject: [PATCH 02/10] =?UTF-8?q?Minor=20Bug=20=F0=9F=90=9E:=20Show=20mess?=
=?UTF-8?q?age=20action=20only=20for=20current=20user?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- User can delete or edit their own messages
---
app/javascript/chat/message.jsx | 32 ++++++++++++++++++--------------
1 file changed, 18 insertions(+), 14 deletions(-)
diff --git a/app/javascript/chat/message.jsx b/app/javascript/chat/message.jsx
index 345bf073b1be2..08c2638cbe578 100644
--- a/app/javascript/chat/message.jsx
+++ b/app/javascript/chat/message.jsx
@@ -76,20 +76,24 @@ const Message = ({
)}
-
- {
- if (e.keyCode === 13) onDeleteMessageTrigger();
- }}
- >
- Delete
-
- Edit
-
+ {userID === window.currentUser.id ? (
+
+ {
+ if (e.keyCode === 13) onDeleteMessageTrigger();
+ }}
+ >
+ Delete
+
+ Edit
+
+ ) : (
+ ' '
+ )}
{messageArea}
From e796764564c957370c4c83b395929c0a4e2b8c82 Mon Sep 17 00:00:00 2001
From: Sarthak <7lovesharma7@gmail.com>
Date: Mon, 9 Dec 2019 17:10:11 +0530
Subject: [PATCH 03/10] Test cases added
---
app/javascript/chat/chat.jsx | 1 +
app/javascript/chat/message.jsx | 4 +++-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx
index a366ebac67c35..6f70d1f0230f6 100644
--- a/app/javascript/chat/chat.jsx
+++ b/app/javascript/chat/chat.jsx
@@ -752,6 +752,7 @@ export default class Chat extends Component {
}
return messages[activeChannelId].map(message => (
)}
- {userID === window.currentUser.id ? (
+ {userID === currentUserId ? (
Date: Mon, 9 Dec 2019 19:00:30 +0530
Subject: [PATCH 04/10] =?UTF-8?q?Bug=20=F0=9F=90=9E:=20Update=20message=20?=
=?UTF-8?q?id=20for=20receiver?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Message id was not sent to receiver by pusher
---
app/assets/stylesheets/chat.scss | 777 +++++++++++++------------
app/controllers/messages_controller.rb | 13 +-
app/javascript/chat/chat.jsx | 13 +-
3 files changed, 422 insertions(+), 381 deletions(-)
diff --git a/app/assets/stylesheets/chat.scss b/app/assets/stylesheets/chat.scss
index c7685796e9f66..f665c5492fb88 100644
--- a/app/assets/stylesheets/chat.scss
+++ b/app/assets/stylesheets/chat.scss
@@ -3,18 +3,22 @@
@import 'mixins';
// High level classes
-.chat-page-wrapper{
+.chat-page-wrapper {
margin: 85px auto 0px;
width: 96%;
max-width: calc(1440px + 10vw);
}
-.live-chat{
+
+.live-chat {
height: calc(100vh - 80px);
- &.live-chat--iossafari{
+
+ &.live-chat--iossafari {
height: calc(100vh - 190px);
}
+
overflow-y: hidden;
- @media screen and ( min-width: 400px ){
+
+ @media screen and (min-width: 400px) {
height: calc(100vh - 80px);
}
}
@@ -23,24 +27,28 @@
.chat {
display: flex;
height: calc(100vh - 91px);
- &.chat--iossafari{
+
+ &.chat--iossafari {
height: calc(100vh - 201px);
}
+
padding-right: 3px;
- @media screen and ( min-width: 400px ){
+
+ @media screen and (min-width: 400px) {
height: calc(100vh - 91px);
}
}
-.chat--expanded{
+.chat--expanded {
min-width: 500px;
width: calc(100%-3px);
- @media screen and ( min-width: 500px ){
+
+ @media screen and (min-width: 500px) {
min-width: 300px;
}
}
-.chat__notificationsbutton{
+.chat__notificationsbutton {
border: 0px;
font-size: 12px;
padding: 15px 0px;
@@ -51,14 +59,13 @@
border-radius: 3px;
border: 1px solid darken($green, 30%);
box-shadow: 3px 3px 0px darken($green, 30%);
- background: lighten($green,30%);
+ background: lighten($green, 30%);
margin-bottom: 5px;
- &:hover{
- @include themeable(
- background,
- theme-container-background-hover,
- lighten($green, 20%)
- );
+
+ &:hover {
+ @include themeable(background,
+ theme-container-background-hover,
+ lighten($green, 20%));
}
}
@@ -70,33 +77,41 @@
overflow: hidden;
}
-.chat__channels--expanded{
+.chat__channels--expanded {
width: 160px;
min-width: 160px;
- @media screen and ( min-width: 1000px ){
+
+ @media screen and (min-width: 1000px) {
width: 200px;
min-width: 200px;
- .chat__channelstogglebutt{
+
+ .chat__channelstogglebutt {
display: none;
}
}
- @media screen and ( min-width: 1300px ){
+
+ @media screen and (min-width: 1300px) {
width: calc(280px + 3vw);
min-width: calc(280px);
}
- &.chat__channels--placeholder{
+
+ &.chat__channels--placeholder {
.chat__channelstogglebutt--placeholderunexpanded {
display: none;
}
- @media screen and ( max-width: 599px ){
+
+ @media screen and (max-width: 599px) {
width: 45px;
min-width: 45px;
- input{
+
+ input {
display: none;
}
- .chat__channelstogglebutt{
+
+ .chat__channelstogglebutt {
display: none;
}
+
.chat__channelstogglebutt--placeholderunexpanded {
display: block;
width: 100%;
@@ -105,7 +120,7 @@
}
}
-.chat__channelstogglebutt{
+.chat__channelstogglebutt {
width: 26px;
height: 38px;
background: transparent;
@@ -123,27 +138,24 @@
padding: 8px;
font-size: 14px;
-webkit-appearance: none;
- @include themeable(
- background,
- theme-container-background,
- white
- );
- @include themeable(
- color,
- theme-color,
- $black
- );
- @media screen and ( min-width: 1000px ){
+ @include themeable(background,
+ theme-container-background,
+ white);
+ @include themeable(color,
+ theme-color,
+ $black);
+
+ @media screen and (min-width: 1000px) {
width: calc(100% - 13px);
}
}
-.chat__channeltypefilter{
+.chat__channeltypefilter {
padding: 4px 0px 0px;
width: calc(100% - 13px);
}
-.chat__channeltypefilterbutton{
+.chat__channeltypefilterbutton {
background: transparent;
border: 0px;
font-size: 15px;
@@ -153,7 +165,8 @@
width: 100%;
box-sizing: border-box;
border-bottom: 1px solid darken($light-medium-gray, 5%);
- @media screen and ( min-width: 1300px ){
+
+ @media screen and (min-width: 1300px) {
width: 33%;
display: inline-block;
border-top-left-radius: 3px;
@@ -162,48 +175,44 @@
}
.chat__channeltypefilterbutton--active {
- @include themeable(
- color,
- theme-anchor-color,
- darken($sky-blue, 5%)
- );
- @media screen and ( min-width: 1300px ){
+ @include themeable(color,
+ theme-anchor-color,
+ darken($sky-blue, 5%));
+
+ @media screen and (min-width: 1300px) {
border: 1px solid darken($light-medium-gray, 5%);
}
}
.chat__channeltypefilterbutton--inactive {
- @include themeable(
- color,
- theme-secondary-color,
+ @include themeable(color,
+ theme-secondary-color,
lighten($medium-gray, 5%),
);
}
-.chat_chatconfig{
+.chat_chatconfig {
position: absolute;
bottom: 0px;
left: 0px;
right: 6px;
- font-size:12px;
+ font-size: 12px;
padding: 3px 12px;
display: inline-block;
border-radius: 3px;
- @include themeable(
- background,
- theme-background,
- $lightest-gray
- );
+ @include themeable(background,
+ theme-background,
+ $lightest-gray);
border: 1px solid $light-medium-gray;
font-weight: bold;
cursor: default;
}
-.chat_chatconfig--on{
+.chat_chatconfig--on {
color: $green;
}
-.chat_chatconfig--off{
+.chat_chatconfig--off {
color: $red;
}
@@ -215,15 +224,16 @@
height: inherit;
width: calc(100% - 45px);
max-width: calc(100% - 45px);
- @media screen and ( min-width: 1300px ){
+
+ @media screen and (min-width: 1300px) {
min-width: calc(100% - calc(280px + 3vw));
}
}
-.chat__videocall{
+.chat__videocall {
height: calc(75vw - 60px);
width: calc(100% - 60px);
- position:fixed;
+ position: fixed;
background: $black;
left: 0px;
top: 0px;
@@ -231,50 +241,61 @@
cursor: grab;
border-radius: 12px;
z-index: 10000;
- @media screen and ( min-width: 640px ){
+
+ @media screen and (min-width: 640px) {
width: 640px;
height: 480px;
left: 250px;
top: 150px;
}
- &:active{
+
+ &:active {
cursor: grabbing;
}
- video{
+
+ video {
border-radius: 12px;
width: 100%;
height: 100%;
}
- .chat__remotevideoscreen-2{
- video{
+
+ .chat__remotevideoscreen-2 {
+ video {
margin-top: 20%;
- width:45%;
+ width: 45%;
margin-left: 1%;
margin-right: 1%;
}
}
- .chat__remotevideoscreen-3,.chat__remotevideoscreen-4{
- video{
- width:45%;
+
+ .chat__remotevideoscreen-3,
+ .chat__remotevideoscreen-4 {
+ video {
+ width: 45%;
margin-top: 1%;
margin-left: 1%;
margin-right: 1%;
}
}
- .chat__remotevideoscreen-5,.chat__remotevideoscreen-6{
- video{
- width:30%;
+
+ .chat__remotevideoscreen-5,
+ .chat__remotevideoscreen-6 {
+ video {
+ width: 30%;
margin-top: 3%;
margin-left: 1%;
margin-right: 1%;
}
}
- .chat__remotevideoscreen-7,.chat__remotevideoscreen-8{
- video{
- width:24%;
+
+ .chat__remotevideoscreen-7,
+ .chat__remotevideoscreen-8 {
+ video {
+ width: 24%;
}
}
- .chat__localvideoscren{
+
+ .chat__localvideoscren {
height: 120px;
width: 160px;
position: absolute;
@@ -282,56 +303,53 @@
bottom: 15px;
border-radius: 5px;
box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.4);
- video{
+
+ video {
border-radius: 5px
}
}
}
-.chatNonChatView{
+.chatNonChatView {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
padding: 80px 7%;
- z-index:10;
- @include themeable(
- background,
- theme-background,
- white
- );
+ z-index: 10;
+ @include themeable(background,
+ theme-background,
+ white);
}
-.chatNonChatView_exitbutton{
+.chatNonChatView_exitbutton {
position: absolute;
left: 4%;
top: 60px;
- font-size:25px;
+ font-size: 25px;
background: transparent;
- border:0px;
- font-size:1.8em;
- @include themeable(
- color,
- theme-color,
- $black
- );
+ border: 0px;
+ font-size: 1.8em;
+ @include themeable(color,
+ theme-color,
+ $black);
}
-.chatNonChatView_contentblock{
+.chatNonChatView_contentblock {
border: 1px solid $medium-gray;
padding: 20px;
margin-bottom: 10px;
max-width: 900px;
- @include themeable(
- box-shadow,
- theme-container-box-shadow,
- $bold-shadow
- );
- h2{
- margin:5px auto 5px;
+ @include themeable(box-shadow,
+ theme-container-box-shadow,
+ $bold-shadow);
+
+ h2 {
+ margin: 5px auto 5px;
}
- button{
+
+ button {
font-size: 1.6em;
background: white;
border: 4px solid $black;
@@ -341,12 +359,12 @@
}
}
-.chat__videocallexitbutton{
- border: 0px;
+.chat__videocallexitbutton {
+ border: 0px;
font-size: 20px;
color: $medium-gray;
- padding:1px 6px 2px;
- border-radius:6px;
+ padding: 1px 6px 2px;
+ border-radius: 6px;
background: rgba(0, 0, 0, 0.1);
position: absolute;
left: 3px;
@@ -354,16 +372,17 @@
}
.chat__videocallcontrolbutton {
- border: 0px;
+ border: 0px;
font-size: 14px;
color: $light-medium-gray;
- padding:3px 0px 4px;
- border-radius:6px;
+ padding: 3px 0px 4px;
+ border-radius: 6px;
background: rgba(0, 0, 0, 0.7);
position: absolute;
bottom: 10px;
left: 10px;
width: 125px;
+
&.chat__videocallcontrolbutton--videoonoff {
left: 140px;
}
@@ -383,62 +402,50 @@
display: flex;
flex-direction: row;
justify-content: space-between;
- @include themeable(
- border,
- theme-container-border,
- 1px solid $outline-color
- );
- @include themeable(
- box-shadow,
- theme-container-box-shadow,
- $bold-shadow
- );
- @include themeable(
- background,
- theme-container-background,
- white
- );
+ @include themeable(border,
+ theme-container-border,
+ 1px solid $outline-color);
+ @include themeable(box-shadow,
+ theme-container-box-shadow,
+ $bold-shadow);
+ @include themeable(background,
+ theme-container-background,
+ white);
flex-flow: row nowrap;
}
-.activechatchannel__conversation{
- @include themeable(
- border-top,
- theme-container-border,
- 1px solid $outline-color
- );
+.activechatchannel__conversation {
+ @include themeable(border-top,
+ theme-container-border,
+ 1px solid $outline-color);
order: 1;
display: flex;
flex-direction: column;
flex: 0 1 auto;
width: 100%;
min-width: 55%;
- @media screen and ( max-width: 426px ) {
+
+ @media screen and (max-width: 426px) {
overflow-x: hidden;
}
}
-.activechatchannel__header{
- @include themeable(
- border-bottom,
- theme-container-border,
- 1px solid $light-medium-gray
- );
+.activechatchannel__header {
+ @include themeable(border-bottom,
+ theme-container-border,
+ 1px solid $light-medium-gray);
padding: 10px 12px;
font-weight: bold;
- font-size:14px;
- @include themeable(
- background,
- theme-container-accent-background,
- $light-gray
- );
+ font-size: 14px;
+ @include themeable(background,
+ theme-container-accent-background,
+ $light-gray);
position: relative;
+
a {
- @include themeable(
- color,
- theme-color,
- $black
- );
+ @include themeable(color,
+ theme-color,
+ $black);
}
.activechatchannel__channelconfig {
@@ -447,11 +454,9 @@
right: 15px;
top: 10px;
cursor: pointer;
- @include themeable(
- filter,
- theme-social-icon-invert,
- invert(0)
- );
+ @include themeable(filter,
+ theme-social-icon-invert,
+ invert(0));
img {
height: 100%;
@@ -493,21 +498,17 @@
}
.activechatchannel__form {
- @include themeable(
- border-top,
- theme-container-border,
- 1px solid $outline-color
- );
- @include themeable(
- background,
- theme-container-accent-background,
- #ededed
- );
+ @include themeable(border-top,
+ theme-container-border,
+ 1px solid $outline-color);
+ @include themeable(background,
+ theme-container-accent-background,
+ #ededed);
width: 100%;
height: 70px;
}
-.activechatchannel__incomingcall{
+.activechatchannel__incomingcall {
border: 2px solid $green;
box-shadow: 6px 6px 0px $green;
padding: 20px;
@@ -515,11 +516,11 @@
background: lighten($green, 28%);
font-size: 28px;
font-weight: bold;
- cursor:pointer;
+ cursor: pointer;
animation: pulser 0.5s linear infinite;
}
-.activechatchannel__activecontent{
+.activechatchannel__activecontent {
order: 2;
flex: 0 1 auto;
border: 1px solid $outline-color;
@@ -527,110 +528,116 @@
padding: 13px;
min-width: 45%;
-webkit-overflow-scrolling: touch;
- @include themeable(
- background,
- theme-container-accent-background,
- white
- );
+ @include themeable(background,
+ theme-container-accent-background,
+ white);
z-index: 200;
position: relative;
box-sizing: border-box;
overflow-y: auto;
- max-width:96%;
- @media screen and ( max-width: 426px ) {
+ max-width: 96%;
+
+ @media screen and (max-width: 426px) {
padding: 10px;
min-width: 50%;
width: calc(95% - 45px);
position: fixed;
height: calc(100% - 90px);
}
- @media screen and ( min-width: 1000px ){
+
+ @media screen and (min-width: 1000px) {
margin-left: initial;
padding: 18px;
- max-width:480px;
+ max-width: 480px;
}
- @media screen and ( min-width: 1300px ){
+
+ @media screen and (min-width: 1300px) {
margin-left: initial;
padding: 18px;
- max-width:620px;
+ max-width: 620px;
}
}
-.live-chat-wrapper .activechatchannel__activecontent{
+.live-chat-wrapper .activechatchannel__activecontent {
min-width: 310px;
margin-left: -180px;
- max-width:96%;
+ max-width: 96%;
padding: 13px;
}
-.activechatchannel__activecontentexitbutton{
- border: 0px;
+.activechatchannel__activecontentexitbutton {
+ border: 0px;
font-size: 30px;
- @include themeable(
- color,
- theme-container-color,
- $medium-gray
- );
- padding:0px;
+ @include themeable(color,
+ theme-container-color,
+ $medium-gray);
+ padding: 0px;
background: transparent;
position: absolute;
left: 6px;
- top:-3px;
+ top: -3px;
}
-.activechatchannel__activeArticle{
+.activechatchannel__activeArticle {
position: absolute;
- top: 36px; left: 0; right: 0; bottom: 0;
+ top: 36px;
+ left: 0;
+ right: 0;
+ bottom: 0;
border-top: 1px solid $light-medium-gray !important;
}
-.activechatchannel__activeArticleDetails{
+.activechatchannel__activeArticleDetails {
position: absolute;
top: -26px;
right: 2px;
left: 35px;
font-family: $monospace;
- font-size:0.8em;
- a{
- @include themeable(
- color,
- theme-container-color,
- $dark-gray
- );
- }
- .activechatchannel__activeArticleDetailsPath{
+ font-size: 0.8em;
+
+ a {
+ @include themeable(color,
+ theme-container-color,
+ $dark-gray);
+ }
+
+ .activechatchannel__activeArticleDetailsPath {
display: inline-block;
max-width: calc(100% - 15px);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
- img{
+
+ img {
height: 1.1em;
width: 1.1em;
margin-left: 10px;
- vertical-align:2px;
- opacity:0.86;
- &:hover{
- opacity:1;
+ vertical-align: 2px;
+ opacity: 0.86;
+
+ &:hover {
+ opacity: 1;
}
}
}
-.activechatchannel__activecontentuserdetails{
- margin-top:20px;
+.activechatchannel__activecontentuserdetails {
+ margin-top: 20px;
font-family: $monospace;
- font-size:0.9em;
- .key{
+ font-size: 0.9em;
+
+ .key {
color: $medium-gray;
margin-bottom: 3px;
- font-size:0.9em
+ font-size: 0.9em
}
}
.userdetails__blockreport {
margin: 10px 0;
+
button {
background-color: rgb(255, 0, 0);
color: rgb(255, 255, 254);
@@ -643,13 +650,15 @@
border-style: initial;
border-color: initial;
border-image: initial;
+
&:first-child {
margin-left: 0;
}
}
}
-.userdetails__reportabuse, .userdetails__blockmsg {
+.userdetails__reportabuse,
+.userdetails__blockmsg {
a {
background-color: rgb(255, 0, 0);
color: rgb(255, 255, 254);
@@ -663,6 +672,7 @@
border-color: initial;
border-image: initial;
}
+
.no {
background-color: rgb(78, 87, 239);
}
@@ -670,53 +680,58 @@
.chat .activechatchannel__activeArticle .container {
position: absolute;
- top: 0px; left: 0; right: 0; bottom: 30px;
+ top: 0px;
+ left: 0;
+ right: 0;
+ bottom: 30px;
overflow-y: scroll;
border-radius: 0px;
margin-top: 0px !important;
- pre{
- width:97%;
- margin-left:-3%;
- padding-left:4%;
- padding-right:7%;
+
+ pre {
+ width: 97%;
+ margin-left: -3%;
+ padding-left: 4%;
+ padding-right: 7%;
}
}
-.activechatchannel__activeArticleActions{
- position:absolute;
- bottom:0;
- left:0px;
- right:0px;
+.activechatchannel__activeArticleActions {
+ position: absolute;
+ bottom: 0;
+ left: 0px;
+ right: 0px;
padding: 17px;
- @include themeable(
- background,
- theme-container-accent-background,
- white
- );
+ @include themeable(background,
+ theme-container-accent-background,
+ white);
z-index: 20;
border-top: 1px solid $light-medium-gray;
border-bottom: 1px solid $light-medium-gray;
border-right: 1px solid $light-medium-gray;
+
button {
margin-right: 15px;
width: 54px;
- @include themeable(
- background,
- theme-container-background,
- #f2f3f5
- );
+ @include themeable(background,
+ theme-container-background,
+ #f2f3f5);
border-radius: 100px !important;
border-width: 0;
padding: 5px 10px;
+
&.active {
- background-color:$green;
- &.unicorn-reaction-button{
- background-color:$purple;
+ background-color: $green;
+
+ &.unicorn-reaction-button {
+ background-color: $purple;
}
- &.readinglist-reaction-button{
+
+ &.readinglist-reaction-button {
background: lighten($bold-blue, 32%);
}
- }
+ }
+
img {
width: 22px;
height: 22px;
@@ -725,24 +740,24 @@
}
-.activecontent__githubrepo{
-}
+.activecontent__githubrepo {}
-.activecontent__githubrepoheader{
+.activecontent__githubrepoheader {
padding-left: 30px;
padding-bottom: 3px;
min-height: 30px;
- margin-top:-9px;
+ margin-top: -9px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.9em;
}
-.activecontent__githubrepofiles{
+.activecontent__githubrepofiles {
border-bottom: 1px solid $light-medium-gray;
- font-size:0.8em;
+ font-size: 0.8em;
overflow: auto;
+
&.activecontent__githubrepofiles--root {
height: 280px;
max-height: 40vh;
@@ -751,12 +766,13 @@
.activecontent__githubreporeadme {
font-size: 0.8em;
+
img {
max-width: 100%;
}
}
-.activecontent__githubrepofilerow{
+.activecontent__githubrepofilerow {
padding: 4px 2px;
border-top: 1px solid $light-medium-gray;
}
@@ -781,17 +797,16 @@
width: 99%;
}
-.chatchanneltabbutton{
+.chatchanneltabbutton {
width: 93%;
border: none;
background: transparent;
- padding:3px 0px;
+ padding: 3px 0px;
margin-bottom: -5px 0;
- @include themeable(
- color,
- theme-color,
- $black
- );
+ @include themeable(color,
+ theme-color,
+ $black);
+
&:hover {
.chatchanneltab--inactive {
border: 1px solid $outline-color;
@@ -801,7 +816,7 @@
}
.chatchanneltab {
- display:inline-block;
+ display: inline-block;
width: 90%;
margin-bottom: 5px;
background: inherit;
@@ -814,18 +829,17 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- @media screen and ( min-width: 550px ){
+
+ @media screen and (min-width: 550px) {
font-size: 13px;
padding: 8px;
}
}
.chatchanneltab--active {
- @include themeable(
- background,
- theme-container-background,
- white
- );
+ @include themeable(background,
+ theme-container-background,
+ white);
border: 1px solid $dark-purple;
box-shadow: 3px 3px 0px $dark-purple;
}
@@ -840,7 +854,7 @@
animation: small-pulser 0.5s linear infinite;
}
-.chatchanneltabindicator{
+.chatchanneltabindicator {
display: inline-block;
min-height: 0.3em;
min-width: 0.3em;
@@ -853,28 +867,29 @@
vertical-align: calc(-7px - 0.14vw);
border: 2px solid transparent;
- &.chatchanneltabindicatordirectimage{
+ &.chatchanneltabindicatordirectimage {
border: 2px solid $light-medium-gray;
border-radius: 100px;
}
}
- .chatchanneltabgroupicon{
+ .chatchanneltabgroupicon {
display: inline-block;
margin-left: calc(20px + 0.3vw);
}
}
-.chatchanneltabindicator--new{
+.chatchanneltabindicator--new {
background: $yellow;
+
img.chatchanneltabindicatordirectimage {
border: 2px solid $black;
}
}
-.chatchanneltabindicator--phone{
+.chatchanneltabindicator--phone {
background: $green;
padding: 0px 8px;
}
@@ -888,7 +903,7 @@
-webkit-overflow-scrolling: touch;
}
-.chatchannels__channelslistheader{
+.chatchannels__channelslistheader {
background: lighten($purple, 5%);
border: 1px solid darken($purple, 20%);
font-size: 12px;
@@ -899,39 +914,40 @@
border-radius: 3px;
}
-.chatchannels__channelslistfooter{
+.chatchannels__channelslistfooter {
font-size: 12px;
color: $medium-gray;
padding: 10px;
- opacity:0.8;
+ opacity: 0.8;
padding-bottom: 70px;
}
-.chatchannels__config{
+.chatchannels__config {
position: absolute;
bottom: -14px;
left: 0;
right: 0;
- @include themeable(
- background,
- theme-background,
- $lightest-gray
- );
+ @include themeable(background,
+ theme-background,
+ $lightest-gray);
border-top: 1px solid $light-medium-gray;
padding: 8px 10px 20px;
font-weight: bold;
padding-left: 16px;
cursor: pointer;
- img{
- opacity:0.6;
+
+ img {
+ opacity: 0.6;
}
- &:hover{
- .chatchannels__configmenu{
+
+ &:hover {
+ .chatchannels__configmenu {
display: block;
}
}
}
-.chatchannels__configmenu{
+
+.chatchannels__configmenu {
display: none;
position: absolute;
bottom: 42px;
@@ -942,6 +958,7 @@
background: $lightest-gray;
border-top: 1px solid $light-medium-gray;
font-size: 13px;
+
a {
color: $dark-gray;
display: block;
@@ -959,26 +976,22 @@
padding: 3px 10px;
display: flex;
max-width: 100%;
+
&:hover {
- @include themeable(
- background,
- theme-container-background-hover,
- $light-gray
- );
+ @include themeable(background,
+ theme-container-background-hover,
+ $light-gray);
+
.chatmessagebody__username--link {
- @include themeable(
- background,
- theme-container-background-hover,
- transparent
- );
+ @include themeable(background,
+ theme-container-background-hover,
+ transparent);
}
}
}
-.chatmessage__profileimage {
-
-}
+.chatmessage__profileimage {}
.chatmessage__body {
flex-grow: 1;
@@ -993,28 +1006,30 @@
font-size: 11px;
color: darken($light-medium-gray, 6%);
min-width: 85px;
- text-align:right;
+ text-align: right;
padding-left: 4px;
vertical-align: 1px;
}
-.chatmessage__bodytext{
+.chatmessage__bodytext {
margin: 3px 1px 4px;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
- pre{
+
+ pre {
background: #29292e;
}
+
code {
background: #29292e;
- padding:0.1em 0.3em;
- border-radius:2px;
+ padding: 0.1em 0.3em;
+ border-radius: 2px;
color: #eff0f9;
- font-size:0.95em;
- vertical-align:0.05em;
- max-width:100%;
- line-height:1.4em;
+ font-size: 0.95em;
+ vertical-align: 0.05em;
+ max-width: 100%;
+ line-height: 1.4em;
font-family: $monospace;
}
}
@@ -1025,7 +1040,8 @@
margin-right: 6px;
border-radius: 20px;
vertical-align: -3px;
- @media screen and ( min-width: 400px ){
+
+ @media screen and (min-width: 400px) {
height: 31px;
width: 31px;
}
@@ -1036,46 +1052,44 @@
border-radius: 3px;
padding: 0px 2px;
display: inline-block;
+
&:hover {
- @include themeable(
- background,
- theme-container-background-hover,
- transparent
- );
+ @include themeable(background,
+ theme-container-background-hover,
+ transparent);
}
}
-.chatmessagebody__divider {
-
-}
+.chatmessagebody__divider {}
.chatmessagebody__message {
p {
margin-top: 0;
margin-bottom: 9px;
+
&:last-child {
margin-bottom: 0;
}
}
+
blockquote {
border-left: 4px solid $black;
margin-left: 0px;
padding-left: 8px;
}
+
a h1 {
- @include themeable(
- color,
- theme-container-color,
- $black
- );
- @include themeable-important(
- border-color,
- theme-container-color,
- #0a0a0a
- );
+ @include themeable(color,
+ theme-container-color,
+ $black);
+ @include themeable-important(border-color,
+ theme-container-color,
+ #0a0a0a);
}
- img { max-width: 100%; }
+ img {
+ max-width: 100%;
+ }
}
.chatmessagebody__currentuser {
@@ -1099,21 +1113,15 @@
padding: 5px;
resize: none;
flex-grow: 1;
- @include themeable(
- background,
- theme-container-background,
- white
- );
- @include themeable(
- border,
- theme-border,
- 1px solid $outline-color
- );
- @include themeable(
- color,
- theme-color,
- $black
- );
+ @include themeable(background,
+ theme-container-background,
+ white);
+ @include themeable(border,
+ theme-border,
+ 1px solid $outline-color);
+ @include themeable(color,
+ theme-color,
+ $black);
}
.messagecomposer__submit {
@@ -1124,10 +1132,12 @@
font-family: $helvetica-condensed;
color: white;
font-size: 18px;
- @media screen and ( min-width: 500px ){
+
+ @media screen and (min-width: 500px) {
width: 80px;
}
- @media screen and ( min-width: 1500px ){
+
+ @media screen and (min-width: 1500px) {
width: 100px;
}
}
@@ -1136,31 +1146,30 @@
height: 5px;
}
-.chatcodeeditor{
+.chatcodeeditor {
position: absolute;
top: 40px;
left: 0px;
right: 24px;
bottom: 0px;
}
-.chatcodeeditor__header{
+
+.chatcodeeditor__header {
position: absolute;
top: -27px;
left: 35px;
font-family: $monospace;
- @include themeable(
- color,
- theme-color,
- $dark-gray
- );
+ @include themeable(color,
+ theme-color,
+ $dark-gray);
}
-.CodeMirror{
+.CodeMirror {
padding: 12px;
- font-size:0.9em;
+ font-size: 0.9em;
}
-.cursorelement{
+.cursorelement {
border-left: 3px solid $red;
animation: blinker 1s linear infinite;
padding: 0px;
@@ -1192,24 +1201,27 @@
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;
+
+ .title {
+ width: 95%;
+ font-size: 18px;
+
+ h1 {
+ font-size: 28px !important;
}
}
+
.body {
- font-size:20px;
- width:96%;
+ font-size: 20px;
+ width: 96%;
}
}
-.chatchanneljumpback{
+.chatchanneljumpback {
position: relative;
}
-.chatchanneljumpback__messages{
+.chatchanneljumpback__messages {
position: absolute;
bottom: 12px;
right: 8px;
@@ -1224,29 +1236,35 @@
font-weight: bold;
}
-.chatchanneljumpback__hide{
+.chatchanneljumpback__hide {
display: none;
}
-.message__info__actions{
+.message__info__actions {
display: grid;
grid-template-columns: 4fr 1fr;
}
-.message__actions{
+
+.message__actions {
display: none;
justify-content: end;
+ -webkit-justify-content: flex-end;
+
}
+
.message__actions span {
- margin-left: 7px;
- font-size: 13px;
- font-weight: bold;
- cursor: pointer;
+ margin-left: 7px;
+ font-size: 13px;
+ font-weight: bold;
+ cursor: pointer;
}
-.chatmessage:hover .message__actions{
- display: flex;
+
+.chatmessage:hover .message__actions {
+ display: -webkit-flex;
+ display: flex;
}
-.message__delete__modal{
+.message__delete__modal {
position: fixed;
background-color: rgba(255, 255, 255, 0.25);
top: 0;
@@ -1257,38 +1275,43 @@
display: block;
transition: all 0.3s;
}
-.modal__content{
+
+.modal__content {
width: 400px;
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
- transform: translate(-50%, -50%);
+ transform: translate(-50%, -50%);
padding: 1em;
border: 2px solid;
background: var(--theme-background, #f5f6f7);
color: var(--theme-color, $black);
}
-.modal__content h3{
+
+.modal__content h3 {
margin: 20px 0;
text-align: center;
}
-.delete__action__buttons{
- display: flex;
+
+.delete__action__buttons {
+ display: flex !important;
justify-content: space-around;
margin: 40px 0px 20px 0;
}
.delete__action__buttons div {
- border: 2px solid var(--theme-color, $black);
- padding: 5px 20px;
- cursor: pointer;
- user-select: none;
+ border: 2px solid var(--theme-color, $black);
+ padding: 5px 20px;
+ cursor: pointer;
+ user-select: none;
}
-.message__delete__modal__hide{
+
+.message__delete__modal__hide {
display: none;
}
-.message__delete__button{
+
+.message__delete__button {
background: #ff0000;
color: white;
-}
\ No newline at end of file
+}
diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb
index 7a91fe9f47d5e..6556b96a4f077 100644
--- a/app/controllers/messages_controller.rb
+++ b/app/controllers/messages_controller.rb
@@ -8,16 +8,25 @@ def create
@temp_message_id = (0...20).map { ("a".."z").to_a[rand(8)] }.join
authorize @message
+ # sending temp message only to sender
if @message.valid?
begin
message_json = create_pusher_payload(@message, @temp_message_id)
- Pusher.trigger(@message.chat_channel.pusher_channels, "message-created", message_json)
+ Pusher.trigger("private-message-notifications-#{@message.user_id}", "message-created", message_json)
rescue Pusher::Error => e
logger.info "PUSHER ERROR: #{e.message}"
end
end
if @message.save
+ if @message.valid?
+ begin
+ message_json = create_pusher_payload(@message, @temp_message_id)
+ Pusher.trigger(@message.chat_channel.pusher_channels, "message-created", message_json)
+ rescue Pusher::Error => e
+ logger.info "PUSHER ERROR: #{e.message}"
+ end
+ end
render json: { status: "success", message: { temp_id: @temp_message_id, id: @message.id } }, status: :created
else
render json: {
@@ -61,6 +70,7 @@ def destroy
def create_pusher_payload(new_message, temp_id)
{
temp_id: temp_id,
+ id: new_message.id,
user_id: new_message.user.id,
chat_channel_id: new_message.chat_channel.id,
chat_channel_adjusted_slug: new_message.chat_channel.adjusted_slug(current_user, "sender"),
@@ -78,7 +88,6 @@ def message_params
end
def set_message
- logger.info "PUSHER ERROR: #{params[:id]}"
@message = Message.find(params[:id])
end
diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx
index 6f70d1f0230f6..ac0ea020b722d 100644
--- a/app/javascript/chat/chat.jsx
+++ b/app/javascript/chat/chat.jsx
@@ -310,11 +310,11 @@ export default class Chat extends Component {
const { activeChannelId } = this.state;
this.setState(prevState => ({
messages: {
- [activeChannelId]: {
+ [activeChannelId]: [
...prevState.messages[activeChannelId].filter(
oldmessage => oldmessage.id !== message.id,
),
- },
+ ],
},
}));
};
@@ -324,6 +324,15 @@ export default class Chat extends Component {
const receivedChatChannelId = message.chat_channel_id;
let newMessages = [];
+ if (
+ message.temp_id &&
+ messages[activeChannelId].findIndex(
+ oldmessage => oldmessage.temp_id === message.temp_id,
+ ) > -1
+ ) {
+ return;
+ }
+
if (messages[receivedChatChannelId]) {
newMessages = messages[receivedChatChannelId].slice();
newMessages.push(message);
From 8d402cfa9986b5389e8779e1f38de4f2e2022455 Mon Sep 17 00:00:00 2001
From: Sarthak <7lovesharma7@gmail.com>
Date: Mon, 9 Dec 2019 21:49:01 +0530
Subject: [PATCH 05/10] =?UTF-8?q?Refactoring=F0=9F=9B=A0:=20Message=20cont?=
=?UTF-8?q?roller=20refactoring?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/controllers/messages_controller.rb | 34 +++++++++++++-------------
1 file changed, 17 insertions(+), 17 deletions(-)
diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb
index 6556b96a4f077..5771fe511418a 100644
--- a/app/controllers/messages_controller.rb
+++ b/app/controllers/messages_controller.rb
@@ -9,24 +9,9 @@ def create
authorize @message
# sending temp message only to sender
- if @message.valid?
- begin
- message_json = create_pusher_payload(@message, @temp_message_id)
- Pusher.trigger("private-message-notifications-#{@message.user_id}", "message-created", message_json)
- rescue Pusher::Error => e
- logger.info "PUSHER ERROR: #{e.message}"
- end
- end
-
+ pusher_message_created(true)
if @message.save
- if @message.valid?
- begin
- message_json = create_pusher_payload(@message, @temp_message_id)
- Pusher.trigger(@message.chat_channel.pusher_channels, "message-created", message_json)
- rescue Pusher::Error => e
- logger.info "PUSHER ERROR: #{e.message}"
- end
- end
+ pusher_message_created(false)
render json: { status: "success", message: { temp_id: @temp_message_id, id: @message.id } }, status: :created
else
render json: {
@@ -105,4 +90,19 @@ def user_not_authorized
end
end
end
+
+ def pusher_message_created(is_single)
+ return unless @message.valid?
+
+ begin
+ message_json = create_pusher_payload(@message, @temp_message_id)
+ if is_single
+ Pusher.trigger("private-message-notifications-#{@message.user_id}", "message-created", message_json)
+ else
+ Pusher.trigger(@message.chat_channel.pusher_channels, "message-created", message_json)
+ end
+ rescue Pusher::Error => e
+ logger.info "PUSHER ERROR: #{e.message}"
+ end
+ end
end
From a8bd27c299c5372ba837bb2bf2b7384325872bf5 Mon Sep 17 00:00:00 2001
From: Sarthak <7lovesharma7@gmail.com>
Date: Tue, 10 Dec 2019 16:41:59 +0530
Subject: [PATCH 06/10] =?UTF-8?q?Test=20Cases=F0=9F=93=9D=20:=20Specs=20fo?=
=?UTF-8?q?r=20Delete=20message=20added?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
spec/requests/messages_spec.rb | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/spec/requests/messages_spec.rb b/spec/requests/messages_spec.rb
index 19a22bfc2bd04..fab47c959af09 100644
--- a/spec/requests/messages_spec.rb
+++ b/spec/requests/messages_spec.rb
@@ -9,6 +9,7 @@
{
message_markdown: "hi",
user_id: user.id,
+ temp_id: "sd78jdssd",
chat_channel_id: chat_channel.id
}
end
@@ -26,6 +27,7 @@
end
it "returns 201 upon success" do
+ allow(Pusher).to receive(:trigger).and_return(true)
expect(response.status).to eq(201)
end
@@ -46,4 +48,28 @@
end
end
end
+
+ describe "DELETE /messages/:id" do
+ let(:old_message) { create(:message, user_id: user.id) }
+
+ it "requires user to be signed in" do
+ expect { delete "/messages/#{old_message.id}" }.to raise_error(Pundit::NotAuthorizedError)
+ end
+
+ context "when user is signed in" do
+ before do
+ allow(Pusher).to receive(:trigger).and_return(true)
+ sign_in user
+ delete "/messages/#{old_message.id}", params: { message: old_message }
+ end
+
+ it "returns message deleted" do
+ expect(response.body).to include "deleted"
+ end
+
+ it "returns in json" do
+ expect(response.content_type).to eq("application/json")
+ end
+ end
+ end
end
From bb805eda39613c1815c554d75e94bbe868f6351d Mon Sep 17 00:00:00 2001
From: Sarthak <7lovesharma7@gmail.com>
Date: Mon, 16 Dec 2019 22:34:20 +0530
Subject: [PATCH 07/10] =?UTF-8?q?Feature=20=F0=9F=9A=80=20:=20Ability=20to?=
=?UTF-8?q?=20edit=20messages?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.vscode/settings.json | 18 ++-
app/assets/stylesheets/chat.scss | 31 +++++
app/controllers/messages_controller.rb | 29 ++++-
.../__snapshots__/chat.test.jsx.snap | 34 +++---
.../__snapshots__/compose.test.jsx.snap | 34 +++---
.../__snapshots__/message.test.jsx.snap | 8 +-
app/javascript/chat/actions.js | 28 +++++
app/javascript/chat/chat.jsx | 80 +++++++++++++
app/javascript/chat/compose.jsx | 106 ++++++++++++++----
app/javascript/chat/message.jsx | 27 ++++-
.../src/utils/getUnopenedChannels.jsx | 3 +
app/javascript/src/utils/pusher.js | 1 +
app/policies/message_policy.rb | 8 ++
app/views/chat_channels/show.json.jbuilder | 2 +
config/routes.rb | 1 +
...0191215145706_add_edited_at_to_messages.rb | 5 +
db/schema.rb | 3 +-
17 files changed, 359 insertions(+), 59 deletions(-)
create mode 100644 db/migrate/20191215145706_add_edited_at_to_messages.rb
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 7de1b8db04feb..226bd287239fe 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -7,5 +7,21 @@
}
},
"ruby.format": "rubocop",
- "ruby.intellisense": "rubyLocate"
+ "ruby.intellisense": "rubyLocate",
+ "workbench.colorCustomizations": {
+ "activityBar.background": "#ab307e",
+ "activityBar.activeBorder": "#25320e",
+ "activityBar.foreground": "#e7e7e7",
+ "activityBar.inactiveForeground": "#e7e7e799",
+ "activityBarBadge.background": "#25320e",
+ "activityBarBadge.foreground": "#e7e7e7",
+ "titleBar.activeBackground": "#832561",
+ "titleBar.inactiveBackground": "#83256199",
+ "titleBar.activeForeground": "#e7e7e7",
+ "titleBar.inactiveForeground": "#e7e7e799",
+ "statusBar.background": "#832561",
+ "statusBarItem.hoverBackground": "#ab307e",
+ "statusBar.foreground": "#e7e7e7"
+ },
+ "peacock.color": "#832561"
}
diff --git a/app/assets/stylesheets/chat.scss b/app/assets/stylesheets/chat.scss
index f665c5492fb88..2d24038f5d4d7 100644
--- a/app/assets/stylesheets/chat.scss
+++ b/app/assets/stylesheets/chat.scss
@@ -1104,6 +1104,7 @@
display: flex;
height: inherit;
align-items: stretch;
+ position: relative;
}
.messagecomposer__input {
@@ -1315,3 +1316,33 @@
background: #ff0000;
color: white;
}
+
+.messageToBeEdited {
+ position: absolute;
+ display: grid;
+ grid-template-columns: 1fr 40px;
+ width: calc(100% - 14px);
+ top: -53px;
+ background: white;
+ padding: 0px 5px;
+ border: 1px solid var(--theme-top-bar-background, #dbdbdb);
+ border-left: 3px solid #4e57ef;
+ height: 50px;
+ overflow: hidden;
+}
+
+.closeEdit {
+ text-align: right;
+ user-select: none;
+ cursor: pointer;
+ padding: 2px 5px;
+ color: #4e57ef;
+ font-weight: bold;
+}
+
+.editHead {
+ position: absolute;
+ color: #4e57ef;
+ font-size: 14px;
+ font-weight: bold;
+}
diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb
index 5771fe511418a..4421b7b47e2d5 100644
--- a/app/controllers/messages_controller.rb
+++ b/app/controllers/messages_controller.rb
@@ -1,5 +1,5 @@
class MessagesController < ApplicationController
- before_action :set_message, only: %i[destroy]
+ before_action :set_message, only: %i[destroy update]
before_action :authenticate_user!, only: %i[create]
def create
@@ -50,6 +50,31 @@ def destroy
end
end
+ def update
+ authorize @message
+
+ if @message.update(permitted_attributes(@message).merge(edited_at: Time.zone.now))
+ if @message.valid?
+ begin
+ message_json = create_pusher_payload(@message, "")
+ Pusher.trigger(@message.chat_channel.pusher_channels, "message-edited", message_json)
+ rescue Pusher::Error => e
+ logger.info "PUSHER ERROR: #{e.message}"
+ end
+ end
+ render json: { status: "success", message: "Message was edited" }
+ else
+ render json: {
+ status: "error",
+ message: {
+ chat_channel_id: @message.chat_channel_id,
+ message: @message.errors.full_messages,
+ type: "error"
+ }
+ }, status: :unauthorized
+ end
+ end
+
private
def create_pusher_payload(new_message, temp_id)
@@ -62,6 +87,8 @@ def create_pusher_payload(new_message, temp_id)
username: new_message.user.username,
profile_image_url: ProfileImage.new(new_message.user).get(90),
message: new_message.message_html,
+ markdown: new_message.message_markdown,
+ edited_at: new_message.edited_at,
timestamp: Time.current,
color: new_message.preferred_user_color,
reception_method: "pushed"
diff --git a/app/javascript/chat/__tests__/__snapshots__/chat.test.jsx.snap b/app/javascript/chat/__tests__/__snapshots__/chat.test.jsx.snap
index 22ee18eec3f04..3ee97f2fac9bc 100644
--- a/app/javascript/chat/__tests__/__snapshots__/chat.test.jsx.snap
+++ b/app/javascript/chat/__tests__/__snapshots__/chat.test.jsx.snap
@@ -244,23 +244,25 @@ exports[` should load chat 1`] = `
diff --git a/app/javascript/chat/__tests__/__snapshots__/compose.test.jsx.snap b/app/javascript/chat/__tests__/__snapshots__/compose.test.jsx.snap
index 1cb93ca6f21c1..ff544fdd4e021 100644
--- a/app/javascript/chat/__tests__/__snapshots__/compose.test.jsx.snap
+++ b/app/javascript/chat/__tests__/__snapshots__/compose.test.jsx.snap
@@ -1,22 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[` behavior with message should render and test snapshot 1`] = `
-
-
-
`;
diff --git a/app/javascript/chat/__tests__/__snapshots__/message.test.jsx.snap b/app/javascript/chat/__tests__/__snapshots__/message.test.jsx.snap
index 91f840a043cc4..db8d9b527b48f 100644
--- a/app/javascript/chat/__tests__/__snapshots__/message.test.jsx.snap
+++ b/app/javascript/chat/__tests__/__snapshots__/message.test.jsx.snap
@@ -50,7 +50,7 @@ exports[` should render and test snapshot 1`] = `
asdf
-
+
should render and test snapshot 1`] = `
>
Delete
-
+
Edit
diff --git a/app/javascript/chat/actions.js b/app/javascript/chat/actions.js
index 99bffbacc39a6..a8378b4ad5c8f 100644
--- a/app/javascript/chat/actions.js
+++ b/app/javascript/chat/actions.js
@@ -31,6 +31,34 @@ export function sendMessage(activeChannelId, message, successCb, failureCb) {
.catch(failureCb);
}
+export function editMessage(
+ activeChannelId,
+ messageId,
+ message,
+ successCb,
+ failureCb,
+) {
+ fetch(`/messages/${messageId}`, {
+ method: 'PATCH',
+ headers: {
+ Accept: 'application/json',
+ 'X-CSRF-Token': window.csrfToken,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ message: {
+ message_markdown: message,
+ user_id: window.currentUser.id,
+ chat_channel_id: activeChannelId,
+ },
+ }),
+ credentials: 'same-origin',
+ })
+ .then(response => response.json())
+ .then(successCb)
+ .catch(failureCb);
+}
+
export function sendOpen(activeChannelId, successCb, failureCb) {
fetch(`/chat_channels/${activeChannelId}/open`, {
method: 'POST',
diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx
index ac0ea020b722d..103fafe76e91d 100644
--- a/app/javascript/chat/chat.jsx
+++ b/app/javascript/chat/chat.jsx
@@ -11,6 +11,7 @@ import {
getChannelInvites,
sendChannelInviteAction,
deleteMessage,
+ editMessage,
} from './actions';
import { hideMessages, scrollToBottom, setupObserver } from './util';
import Alert from './alert';
@@ -66,6 +67,8 @@ export default class Chat extends Component {
messageDeleteId: null,
allMessagesLoaded: false,
currentMessageLocation: 0,
+ startEditing: false,
+ activeEditMessage: {},
};
}
@@ -170,6 +173,7 @@ export default class Chat extends Component {
channelId: channelName,
messageCreated: this.receiveNewMessage,
messageDeleted: this.removeMessage,
+ messageEdited: this.updateMessage,
channelCleared: this.clearChannel,
redactUserMessages: this.redactUserMessages,
channelError: this.channelError,
@@ -319,6 +323,19 @@ export default class Chat extends Component {
}));
};
+ updateMessage = message => {
+ const { activeChannelId } = this.state;
+
+ this.setState(({ messages }) => {
+ const newMessages = messages;
+ const foundIndex = messages[activeChannelId].findIndex(
+ oldMessage => oldMessage.id === message.id,
+ );
+ newMessages[activeChannelId][foundIndex] = message;
+ return { messages: newMessages };
+ });
+ };
+
receiveNewMessage = message => {
const { messages, activeChannelId, scrolled, chatChannels } = this.state;
const receivedChatChannelId = message.chat_channel_id;
@@ -449,6 +466,35 @@ export default class Chat extends Component {
}
};
+ handleKeyDownEdit = 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.handleMessageSubmitEdit(e.target.value);
+ e.target.value = '';
+ }
+ }
+ };
+
+ handleMessageSubmitEdit = message => {
+ const { activeChannelId, activeEditMessage } = this.state;
+ editMessage(
+ activeChannelId,
+ activeEditMessage.id,
+ message,
+ this.handleSuccess,
+ this.handleFailure,
+ );
+ this.handleEditMessageClose();
+ };
+
handleMessageSubmit = message => {
const { activeChannelId } = this.state;
// should check if user has the privilege
@@ -551,11 +597,30 @@ export default class Chat extends Component {
}
};
+ handleSubmitOnClickEdit = e => {
+ e.preventDefault();
+ const message = document.getElementById('messageform').value;
+ if (message.length > 0) {
+ this.handleMessageSubmitEdit(message);
+ document.getElementById('messageform').value = '';
+ }
+ };
+
triggerDeleteMessage = e => {
this.setState({ messageDeleteId: e.target.dataset.content });
this.setState({ showDeleteModal: true });
};
+ triggerEditMessage = e => {
+ const { messages, activeChannelId } = this.state;
+ this.setState({
+ activeEditMessage: messages[activeChannelId].filter(
+ message => message.id === parseInt(e.target.dataset.content, 10),
+ )[0],
+ });
+ this.setState({ startEditing: true });
+ };
+
handleSuccess = response => {
const { activeChannelId } = this.state;
if (response.status === 'success') {
@@ -768,10 +833,12 @@ export default class Chat extends Component {
profileImageUrl={message.profile_image_url}
message={message.message}
timestamp={showTimestamp ? message.timestamp : null}
+ editedAt={message.edited_at}
color={message.color}
type={message.type}
onContentTrigger={this.triggerActiveContent}
onDeleteMessageTrigger={this.triggerDeleteMessage}
+ onEditMessageTrigger={this.triggerEditMessage}
/>
));
};
@@ -1006,7 +1073,13 @@ export default class Chat extends Component {
@@ -1022,6 +1095,13 @@ export default class Chat extends Component {
);
};
+ handleEditMessageClose = () => {
+ this.setState({
+ startEditing: false,
+ activeEditMessage: { message: '', markdown: '' },
+ });
+ };
+
renderDeleteModal = () => {
const { showDeleteModal } = this.state;
return (
diff --git a/app/javascript/chat/compose.jsx b/app/javascript/chat/compose.jsx
index 00adc7121639f..563ecd37c69ca 100644
--- a/app/javascript/chat/compose.jsx
+++ b/app/javascript/chat/compose.jsx
@@ -4,34 +4,100 @@ import PropTypes from 'prop-types';
export default class Chat extends Component {
static propTypes = {
handleKeyDown: PropTypes.func.isRequired,
+ handleKeyDownEdit: PropTypes.func.isRequired,
handleSubmitOnClick: PropTypes.func.isRequired,
- activeChannelId: PropTypes.number.isRequired,
+ handleSubmitOnClickEdit: PropTypes.func.isRequired,
+ startEditing: PropTypes.bool.isRequired,
+ editMessageHtml: PropTypes.string.isRequired,
+ editMessageMarkdown: PropTypes.string.isRequired,
+ handleEditMessageClose: PropTypes.func.isRequired,
};
- shouldComponentUpdate(nextProps) {
- const { activeChannelId } = this.props;
- return activeChannelId !== nextProps.activeChannelId;
+ constructor(props) {
+ super(props);
+ this.state = {
+ startEditing: false,
+ editMessageMarkdown: null,
+ };
+ }
+
+ componentWillReceiveProps(props) {
+ this.setState({
+ startEditing: props.startEditing,
+ editMessageMarkdown: props.editMessageMarkdown,
+ editMessageHtml: props.editMessageHtml,
+ });
}
render() {
- const { handleSubmitOnClick, handleKeyDown } = this.props;
+ const {
+ handleSubmitOnClick,
+ handleKeyDown,
+ handleSubmitOnClickEdit,
+ handleKeyDownEdit,
+ handleEditMessageClose,
+ } = this.props;
+ const { startEditing, editMessageHtml, editMessageMarkdown } = this.state;
return (
-
-
-
- SEND
-
+
+ {!startEditing ? (
+
+
+
+ SEND
+
+
+ ) : (
+
+
+
+
{
+ if (e.keyCode === 13) handleEditMessageClose();
+ }}
+ >
+ x
+
+
+
+
+ Save
+
+
+ )}
);
}
diff --git a/app/javascript/chat/message.jsx b/app/javascript/chat/message.jsx
index 4385934a68d03..d69eb74308e23 100644
--- a/app/javascript/chat/message.jsx
+++ b/app/javascript/chat/message.jsx
@@ -11,10 +11,12 @@ const Message = ({
message,
color,
type,
+ editedAt,
timestamp,
profileImageUrl,
onContentTrigger,
onDeleteMessageTrigger,
+ onEditMessageTrigger,
}) => {
const spanStyle = { color };
@@ -69,12 +71,21 @@ const Message = ({
{user}
+ {editedAt ? (
+
+ {`${adjustTimestamp(editedAt)}`}
+ (edited)
+
+ ) : (
+ ' '
+ )}
+
{timestamp ? (
{`${adjustTimestamp(timestamp)}`}
) : (
-
+ ' '
)}
{userID === currentUserId ? (
@@ -90,7 +101,17 @@ const Message = ({
>
Delete
- Edit
+ {
+ if (e.keyCode === 13) onEditMessageTrigger();
+ }}
+ >
+ Edit
+
) : (
' '
@@ -111,9 +132,11 @@ Message.propTypes = {
message: PropTypes.string.isRequired,
type: PropTypes.string,
timestamp: PropTypes.string,
+ editedAt: PropTypes.number.isRequired,
profileImageUrl: PropTypes.string,
onContentTrigger: PropTypes.func.isRequired,
onDeleteMessageTrigger: PropTypes.func.isRequired,
+ onEditMessageTrigger: PropTypes.func.isRequired,
};
Message.defaultProps = {
diff --git a/app/javascript/src/utils/getUnopenedChannels.jsx b/app/javascript/src/utils/getUnopenedChannels.jsx
index bca546e7ba7a5..edd0ab573bcef 100644
--- a/app/javascript/src/utils/getUnopenedChannels.jsx
+++ b/app/javascript/src/utils/getUnopenedChannels.jsx
@@ -29,6 +29,7 @@ class UnopenedChannelNotice extends Component {
channelId: `private-message-notifications-${window.currentUser.id}`,
messageCreated: this.receiveNewMessage,
messageDeleted: this.removeMessage,
+ messageEdited: this.updateMessage,
});
const component = this;
document.getElementById('connect-link').onclick = () => {
@@ -40,6 +41,8 @@ class UnopenedChannelNotice extends Component {
removeMessage = () => {};
+ updateMessage = () => {};
+
receiveNewMessage = e => {
if (window.location.pathname.startsWith('/connect')) {
return;
diff --git a/app/javascript/src/utils/pusher.js b/app/javascript/src/utils/pusher.js
index d1ad57e474e7c..7e5f0fd6def57 100644
--- a/app/javascript/src/utils/pusher.js
+++ b/app/javascript/src/utils/pusher.js
@@ -20,6 +20,7 @@ export default function setupPusher(key, callbackObjects) {
const channel = pusher.subscribe(callbackObjects.channelId.toString());
channel.bind('message-created', callbackObjects.messageCreated);
channel.bind('message-deleted', callbackObjects.messageDeleted);
+ channel.bind('message-edited', callbackObjects.messageEdited);
channel.bind('channel-cleared', callbackObjects.channelCleared);
channel.bind('user-banned', callbackObjects.redactUserMessages);
channel.bind('client-livecode', callbackObjects.liveCoding);
diff --git a/app/policies/message_policy.rb b/app/policies/message_policy.rb
index ae954e69ce6e2..243ac959c38d5 100644
--- a/app/policies/message_policy.rb
+++ b/app/policies/message_policy.rb
@@ -7,6 +7,14 @@ def destroy?
user_is_sender?
end
+ def update?
+ destroy?
+ end
+
+ def permitted_attributes_for_update
+ %i[message_markdown]
+ end
+
private
def user_is_sender?
diff --git a/app/views/chat_channels/show.json.jbuilder b/app/views/chat_channels/show.json.jbuilder
index 2a22cc35e8ce1..25a9543f2367d 100644
--- a/app/views/chat_channels/show.json.jbuilder
+++ b/app/views/chat_channels/show.json.jbuilder
@@ -4,6 +4,8 @@ json.messages @chat_messages.reverse do |message|
json.username message.user.username
json.profile_image_url ProfileImage.new(message.user).get(90)
json.message message.message_html
+ json.markdown message.message_markdown
+ json.edited_at message.edited_at
json.timestamp message.created_at
json.color message.preferred_user_color
end
diff --git a/config/routes.rb b/config/routes.rb
index a39f86580eede..568871e12a600 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -219,6 +219,7 @@
post "/chat_channels/create_chat" => "chat_channels#create_chat"
post "/chat_channels/block_chat" => "chat_channels#block_chat"
delete "/messages/:id" => "messages#destroy"
+ patch "/messages/:id" => "messages#update"
get "/live/:username" => "twitch_live_streams#show"
post "/pusher/auth" => "pusher#auth"
diff --git a/db/migrate/20191215145706_add_edited_at_to_messages.rb b/db/migrate/20191215145706_add_edited_at_to_messages.rb
new file mode 100644
index 0000000000000..e7162e7dc0908
--- /dev/null
+++ b/db/migrate/20191215145706_add_edited_at_to_messages.rb
@@ -0,0 +1,5 @@
+class AddEditedAtToMessages < ActiveRecord::Migration[5.2]
+ def change
+ add_column :messages, :edited_at, :datetime
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c4e48f0d6c628..57971603c1e1c 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -12,7 +12,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_12_03_171558) do
+ActiveRecord::Schema.define(version: 2019_12_15_145706) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -527,6 +527,7 @@
create_table "messages", force: :cascade do |t|
t.bigint "chat_channel_id", null: false
t.datetime "created_at", null: false
+ t.datetime "edited_at"
t.string "message_html", null: false
t.string "message_markdown", null: false
t.datetime "updated_at", null: false
From 481f0db2dfed4803c1c41999bb17884ad7488eb4 Mon Sep 17 00:00:00 2001
From: Sarthak <7lovesharma7@gmail.com>
Date: Mon, 16 Dec 2019 23:30:16 +0530
Subject: [PATCH 08/10] =?UTF-8?q?Test=20Cases=F0=9F=93=9D=20:=20Specs=20fo?=
=?UTF-8?q?r=20Edit=20message=20added?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
spec/requests/messages_spec.rb | 32 ++++++++++++++++++++++++++++++++
1 file changed, 32 insertions(+)
diff --git a/spec/requests/messages_spec.rb b/spec/requests/messages_spec.rb
index fab47c959af09..893c62bd9ec20 100644
--- a/spec/requests/messages_spec.rb
+++ b/spec/requests/messages_spec.rb
@@ -72,4 +72,36 @@
end
end
end
+
+ describe "UPDATE /messages/:id" do
+ let(:old_message) { create(:message, user_id: user.id) }
+
+ let(:new_message) do
+ {
+ message_markdown: "hi",
+ user_id: user.id,
+ chat_channel_id: chat_channel.id
+ }
+ end
+
+ it "requires user to be signed in" do
+ expect { patch "/messages/#{old_message.id}" }.to raise_error(Pundit::NotAuthorizedError)
+ end
+
+ context "when user is signed in" do
+ before do
+ allow(Pusher).to receive(:trigger).and_return(true)
+ sign_in user
+ patch "/messages/#{old_message.id}", params: { message: new_message }
+ end
+
+ it "returns message updated" do
+ expect(response.body).to include "edited"
+ end
+
+ it "returns in json" do
+ expect(response.content_type).to eq("application/json")
+ end
+ end
+ end
end
From 56bf806aa0bfe205345dc721c948f23bb5046502 Mon Sep 17 00:00:00 2001
From: Paras Gaur
Date: Sat, 14 Mar 2020 01:53:28 +0530
Subject: [PATCH 09/10] unfurl medium blogs with preview
---
Gemfile | 1 +
Gemfile.lock | 6 +++++-
app/models/message.rb | 40 ++++++++++++++++++++++++++++++++--------
3 files changed, 38 insertions(+), 9 deletions(-)
diff --git a/Gemfile b/Gemfile
index cbf0a9a9b858b..3b3ba445d0742 100644
--- a/Gemfile
+++ b/Gemfile
@@ -102,6 +102,7 @@ gem "uglifier", "~> 4.2" # Uglifier minifies JavaScript files
gem "ulid", "~> 1.2" # Universally Unique Lexicographically Sortable Identifier implementation for Ruby
gem "validate_url", "~> 1.0" # Library for validating urls in Rails
gem "webpacker", "~> 3.5" # Use webpack to manage app-like JavaScript modules in Rails
+gem "opengraph_parser" #Using opengraph_parser to parse metadata from websites.
group :development do
gem "better_errors", "~> 2.6" # Provides a better error page for Rails and other Rack apps
diff --git a/Gemfile.lock b/Gemfile.lock
index 2ec15b6efd5a6..2ff284242c9ae 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -512,6 +512,9 @@ GEM
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
+ opengraph_parser (0.2.3)
+ addressable
+ nokogiri
orm_adapter (0.5.0)
os (1.0.1)
parallel (1.19.1)
@@ -921,6 +924,7 @@ DEPENDENCIES
omniauth (~> 1.9)
omniauth-github (~> 1.3)
omniauth-twitter (~> 1.4)
+ opengraph_parser
parallel_tests (~> 2.31)
pg (~> 1.2)
pry (~> 0.12)
@@ -994,4 +998,4 @@ RUBY VERSION
ruby 2.6.5p114
BUNDLED WITH
- 2.0.2
+ 2.1.4
diff --git a/app/models/message.rb b/app/models/message.rb
index 4aeb6f1f9014a..31e5dfa6b99ad 100644
--- a/app/models/message.rb
+++ b/app/models/message.rb
@@ -93,13 +93,23 @@ def append_rich_links(html)
doc = Nokogiri::HTML(html)
doc.css("a").each do |anchor|
if (article = rich_link_article(anchor))
- html += "
- #{"" if article.main_image.present?}
- #{article.title}
-
#{article.cached_user.name}・#{article.readable_publish_date || 'Draft Post'}
- ".html_safe
+ if article.class == Hash
+ html += "
+ #{"" if article[:image].present?}
+ #{article[:title]}
+ #{article[:description]}
+ ".html_safe
+ else
+ html += "
+ #{"" if article.main_image.present?}
+ #{article.title}
+
#{article.cached_user.name}・#{article.readable_publish_date || 'Draft Post'}
+ ".html_safe
+ end
elsif (tag = rich_link_tag(anchor))
html += "
Date: Sat, 14 Mar 2020 01:55:24 +0530
Subject: [PATCH 10/10] condition fix
---
app/models/message.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/models/message.rb b/app/models/message.rb
index 31e5dfa6b99ad..44cc4158602d2 100644
--- a/app/models/message.rb
+++ b/app/models/message.rb
@@ -163,7 +163,7 @@ def channel_permission
def rich_link_article(link)
website = OpenGraph.new(link["href"]).metadata
- unless website.nil?
+ if website.present?
if website[:site_name][0][:_value] == "Medium"
{
title: website[:title][0][:_value],