🚀Feature/ability to edit messages (#5139) [deploy]
* Feature 🚀 : Ability to delete messages in chat channels - Sending message ID to frontend - Deleting Message - Use pusher to delete message realtime * Minor Bug 🐞: Show message action only for current user - User can delete or edit their own messages * Test cases added * Bug 🐞: Update message id for receiver Message id was not sent to receiver by pusher * Refactoring🛠: Message controller refactoring * Test Cases📝 : Specs for Delete message added * Feature 🚀 : Ability to edit messages * Test Cases📝 : Specs for Edit message added * Refactoring🔩: Refactoring for edit messages * Refactoring🔩: Refactoring:2 for edit messages * Refactoring🔩: Refactoring:2 for edit messages * Test Cases📝 : Test Cases for edit message added * Minor Bug 🐞: Show message action only for current user - User can delete or edit their own messages * Test cases added * Bug 🐞: Update message id for receiver Message id was not sent to receiver by pusher * Feature 🚀 : Ability to edit messages * Test Cases📝 : Specs for Edit message added * Refactoring🔩: Refactoring for edit messages * Refactoring🔩: Refactoring:2 for edit messages * Refactoring🔩: Refactoring:2 for edit messages * Bug Fix 🐞: Added space between edited and timestamp Co-authored-by: Ben Halpern <bendhalpern@gmail.com>
This commit is contained in:
parent
07fd2281cd
commit
b602b7b91f
18 changed files with 393 additions and 52 deletions
18
.vscode/settings.json
vendored
18
.vscode/settings.json
vendored
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1104,6 +1104,7 @@
|
|||
display: flex;
|
||||
height: inherit;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.messagecomposer__input {
|
||||
|
|
@ -1249,6 +1250,7 @@
|
|||
display: none;
|
||||
justify-content: end;
|
||||
-webkit-justify-content: flex-end;
|
||||
|
||||
}
|
||||
|
||||
.message__actions span {
|
||||
|
|
@ -1256,7 +1258,6 @@
|
|||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
color: var(--theme-secondary-color, $medium-gray);
|
||||
}
|
||||
|
||||
.chatmessage:hover .message__actions {
|
||||
|
|
@ -1287,7 +1288,6 @@
|
|||
border: 2px solid;
|
||||
background: var(--theme-background, #f5f6f7);
|
||||
color: var(--theme-color, $black);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.modal__content h3 {
|
||||
|
|
@ -1306,7 +1306,6 @@
|
|||
padding: 5px 20px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.message__delete__modal__hide {
|
||||
|
|
@ -1316,4 +1315,34 @@
|
|||
.message__delete__button {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 @@ class MessagesController < ApplicationController
|
|||
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 @@ class MessagesController < ApplicationController
|
|||
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"
|
||||
|
|
|
|||
|
|
@ -244,23 +244,25 @@ exports[`<Chat /> should load chat 1`] = `
|
|||
<div
|
||||
class="activechatchannel__form"
|
||||
>
|
||||
<div
|
||||
class="messagecomposer"
|
||||
>
|
||||
<textarea
|
||||
class="messagecomposer__input"
|
||||
id="messageform"
|
||||
maxLength="1000"
|
||||
onKeyDown={[Function]}
|
||||
placeholder="Message goes here"
|
||||
/>
|
||||
<button
|
||||
class="messagecomposer__submit"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
<div>
|
||||
<div
|
||||
class="messagecomposer"
|
||||
>
|
||||
SEND
|
||||
</button>
|
||||
<textarea
|
||||
class="messagecomposer__input"
|
||||
id="messageform"
|
||||
maxLength="1000"
|
||||
onKeyDown={[Function]}
|
||||
placeholder="Message goes here"
|
||||
/>
|
||||
<button
|
||||
class="messagecomposer__submit"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
SEND
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,24 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Compose /> behavior with message should render and test snapshot 1`] = `
|
||||
<div
|
||||
class="messagecomposer"
|
||||
>
|
||||
<textarea
|
||||
class="messagecomposer__input"
|
||||
id="messageform"
|
||||
maxLength="1000"
|
||||
onKeyDown={[Function]}
|
||||
placeholder="Message goes here"
|
||||
/>
|
||||
<button
|
||||
class="messagecomposer__submit"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
<div>
|
||||
<div
|
||||
class="messagecomposer"
|
||||
>
|
||||
SEND
|
||||
</button>
|
||||
<textarea
|
||||
class="messagecomposer__input"
|
||||
id="messageform"
|
||||
maxLength="1000"
|
||||
onKeyDown={[Function]}
|
||||
placeholder="Message goes here"
|
||||
/>
|
||||
<button
|
||||
class="messagecomposer__submit"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
SEND
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ exports[`<Message /> should render and test snapshot 1`] = `
|
|||
asdf
|
||||
</a>
|
||||
</span>
|
||||
<span />
|
||||
|
||||
</div>
|
||||
<div
|
||||
class="message__actions"
|
||||
|
|
@ -62,6 +62,13 @@ exports[`<Message /> should render and test snapshot 1`] = `
|
|||
>
|
||||
Delete
|
||||
</span>
|
||||
<span
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -31,6 +31,28 @@ export function sendMessage(activeChannelId, message, successCb, failureCb) {
|
|||
.catch(failureCb);
|
||||
}
|
||||
|
||||
export function editMessage(editedMessage, successCb, failureCb) {
|
||||
fetch(`/messages/${editedMessage.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-CSRF-Token': window.csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: {
|
||||
message_markdown: editedMessage.message,
|
||||
user_id: window.currentUser.id,
|
||||
chat_channel_id: editedMessage.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',
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
getChannelInvites,
|
||||
sendChannelInviteAction,
|
||||
deleteMessage,
|
||||
editMessage,
|
||||
} from './actions';
|
||||
import { hideMessages, scrollToBottom, setupObserver } from './util';
|
||||
import Alert from './alert';
|
||||
|
|
@ -67,6 +68,8 @@ export default class Chat extends Component {
|
|||
messageDeleteId: null,
|
||||
allMessagesLoaded: false,
|
||||
currentMessageLocation: 0,
|
||||
startEditing: false,
|
||||
activeEditMessage: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +92,9 @@ export default class Chat extends Component {
|
|||
channel => `open-channel-${channel.chat_channel_id}`,
|
||||
);
|
||||
setupObserver(this.observerCallback);
|
||||
if (!window.currentUser) {
|
||||
window.currentUser = JSON.parse(document.body.dataset.user);
|
||||
}
|
||||
this.subscribePusher(
|
||||
`private-message-notifications-${currentUserId}`,
|
||||
);
|
||||
|
|
@ -171,6 +177,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,
|
||||
|
|
@ -320,6 +327,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;
|
||||
|
|
@ -333,6 +353,15 @@ export default class Chat extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
@ -449,6 +478,34 @@ 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;
|
||||
const editedMessage = {
|
||||
activeChannelId,
|
||||
id: activeEditMessage.id,
|
||||
message,
|
||||
};
|
||||
editMessage(editedMessage, this.handleSuccess, this.handleFailure);
|
||||
this.handleEditMessageClose();
|
||||
};
|
||||
|
||||
handleMessageSubmit = message => {
|
||||
const { activeChannelId } = this.state;
|
||||
// should check if user has the privilege
|
||||
|
|
@ -551,11 +608,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') {
|
||||
|
|
@ -762,17 +838,19 @@ export default class Chat extends Component {
|
|||
}
|
||||
return messages[activeChannelId].map(message => (
|
||||
<Message
|
||||
currentUserId={currentUserId}
|
||||
currentUserId={window.currentUser.id}
|
||||
id={message.id}
|
||||
user={message.username}
|
||||
userID={message.user_id}
|
||||
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}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
|
@ -1008,7 +1086,13 @@ export default class Chat extends Component {
|
|||
<Compose
|
||||
handleSubmitOnClick={this.handleSubmitOnClick}
|
||||
handleKeyDown={this.handleKeyDown}
|
||||
handleSubmitOnClickEdit={this.handleSubmitOnClickEdit}
|
||||
handleKeyDownEdit={this.handleKeyDownEdit}
|
||||
activeChannelId={state.activeChannelId}
|
||||
startEditing={state.startEditing}
|
||||
editMessageHtml={state.activeEditMessage.message}
|
||||
editMessageMarkdown={state.activeEditMessage.markdown}
|
||||
handleEditMessageClose={this.handleEditMessageClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1024,6 +1108,13 @@ export default class Chat extends Component {
|
|||
);
|
||||
};
|
||||
|
||||
handleEditMessageClose = () => {
|
||||
this.setState({
|
||||
startEditing: false,
|
||||
activeEditMessage: { message: '', markdown: '' },
|
||||
});
|
||||
};
|
||||
|
||||
renderDeleteModal = () => {
|
||||
const { showDeleteModal } = this.state;
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -4,35 +4,103 @@ 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 = {
|
||||
editMessageMarkdown: null,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { handleSubmitOnClick, handleKeyDown } = this.props;
|
||||
componentWillReceiveProps(props) {
|
||||
this.setState({
|
||||
editMessageMarkdown: props.editMessageMarkdown,
|
||||
editMessageHtml: props.editMessageHtml,
|
||||
});
|
||||
}
|
||||
|
||||
messageCompose = () => {
|
||||
const {
|
||||
handleSubmitOnClickEdit,
|
||||
handleKeyDownEdit,
|
||||
handleEditMessageClose,
|
||||
} = this.props;
|
||||
const { editMessageHtml, editMessageMarkdown } = this.state;
|
||||
return (
|
||||
<div className="messagecomposer">
|
||||
<div className="messageToBeEdited">
|
||||
<div className="message">
|
||||
<span className="editHead">Edit Message</span>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: editMessageHtml,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="closeEdit"
|
||||
role="button"
|
||||
onClick={handleEditMessageClose}
|
||||
tabIndex="0"
|
||||
onKeyUp={e => {
|
||||
if (e.keyCode === 13) handleEditMessageClose();
|
||||
}}
|
||||
>
|
||||
x
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
className="messagecomposer__input"
|
||||
id="messageform"
|
||||
placeholder="Message goes here"
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyDown={handleKeyDownEdit}
|
||||
maxLength="1000"
|
||||
value={editMessageMarkdown}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="messagecomposer__submit"
|
||||
onClick={handleSubmitOnClick}
|
||||
onClick={handleSubmitOnClickEdit}
|
||||
>
|
||||
SEND
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { handleSubmitOnClick, handleKeyDown, startEditing } = this.props;
|
||||
return (
|
||||
<div>
|
||||
{!startEditing ? (
|
||||
<div className="messagecomposer">
|
||||
<textarea
|
||||
className="messagecomposer__input"
|
||||
id="messageform"
|
||||
placeholder="Message goes here"
|
||||
onKeyDown={handleKeyDown}
|
||||
maxLength="1000"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="messagecomposer__submit"
|
||||
onClick={handleSubmitOnClick}
|
||||
>
|
||||
SEND
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
this.messageCompose()
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
</a>
|
||||
</span>
|
||||
{timestamp ? (
|
||||
{editedAt ? (
|
||||
<span className="chatmessage__timestamp edited_message">
|
||||
{`${adjustTimestamp(editedAt)}`}
|
||||
<i> (edited)</i>
|
||||
</span>
|
||||
) : (
|
||||
' '
|
||||
)}
|
||||
|
||||
{timestamp && !editedAt ? (
|
||||
<span className="chatmessage__timestamp">
|
||||
{`${adjustTimestamp(timestamp)}`}
|
||||
</span>
|
||||
) : (
|
||||
<span />
|
||||
' '
|
||||
)}
|
||||
</div>
|
||||
{userID === currentUserId ? (
|
||||
|
|
@ -90,6 +101,17 @@ const Message = ({
|
|||
>
|
||||
Delete
|
||||
</span>
|
||||
<span
|
||||
role="button"
|
||||
data-content={id}
|
||||
onClick={onEditMessageTrigger}
|
||||
tabIndex="0"
|
||||
onKeyUp={e => {
|
||||
if (e.keyCode === 13) onEditMessageTrigger();
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
' '
|
||||
|
|
@ -110,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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,14 @@ class MessagePolicy < ApplicationPolicy
|
|||
user_is_sender?
|
||||
end
|
||||
|
||||
def update?
|
||||
destroy?
|
||||
end
|
||||
|
||||
def permitted_attributes_for_update
|
||||
%i[message_markdown]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_is_sender?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -224,6 +224,7 @@ Rails.application.routes.draw do
|
|||
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"
|
||||
|
|
|
|||
5
db/migrate/20191215145706_add_edited_at_to_messages.rb
Normal file
5
db/migrate/20191215145706_add_edited_at_to_messages.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class AddEditedAtToMessages < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :messages, :edited_at, :datetime
|
||||
end
|
||||
end
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2019_12_10_144342) 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"
|
||||
|
||||
|
|
@ -529,6 +529,7 @@ ActiveRecord::Schema.define(version: 2019_12_10_144342) do
|
|||
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
|
||||
|
|
|
|||
|
|
@ -72,4 +72,36 @@ RSpec.describe "Messages", type: :request do
|
|||
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue