diff --git a/app/assets/stylesheets/chat.scss b/app/assets/stylesheets/chat.scss index da4cae04f..4dc9f7740 100644 --- a/app/assets/stylesheets/chat.scss +++ b/app/assets/stylesheets/chat.scss @@ -1287,3 +1287,22 @@ .active__message__list { background: var(--theme-container-accent-background, #f5f6f7); } +.chatmessage__action { + &:hover { + background: none; + } + + .chatmessage__bodytext { + margin: 0; + font-size: 12px; + font-style: italic; + color: var(--card-color-tertiary); + a { + text-decoration: none; + color: var(--card-color-tertiary); + &:hover { + text-decoration: underline; + } + } + } +} diff --git a/app/controllers/chat_channel_memberships_controller.rb b/app/controllers/chat_channel_memberships_controller.rb index fcb664783..6f80921c8 100644 --- a/app/controllers/chat_channel_memberships_controller.rb +++ b/app/controllers/chat_channel_memberships_controller.rb @@ -1,5 +1,6 @@ class ChatChannelMembershipsController < ApplicationController after_action :verify_authorized + include MessagesHelper def index skip_authorization @@ -46,6 +47,7 @@ class ChatChannelMembershipsController < ApplicationController @chat_channel_membership.destroy flash[:settings_notice] = "Invitation removed." else + send_chat_action_message("@#{current_user.username} removed @#{@chat_channel_membership.user.username} from #{@chat_channel_membership.channel_name}", current_user, @chat_channel_membership.chat_channel_id, "removed_from_channel") @chat_channel_membership.update(status: "removed_from_channel") flash[:settings_notice] = "Removed #{@chat_channel_membership.user.name}" end @@ -69,6 +71,7 @@ class ChatChannelMembershipsController < ApplicationController @chat_channel_membership = ChatChannelMembership.find(params[:id]) authorize @chat_channel_membership channel_name = @chat_channel_membership.chat_channel.channel_name + send_chat_action_message("@#{current_user.username} left #{@chat_channel_membership.channel_name}", current_user, @chat_channel_membership.chat_channel_id, "left_channel") @chat_channel_membership.update(status: "left_channel") @chat_channels_memberships = [] flash[:settings_notice] = "You have left the channel #{channel_name}. It may take a moment to be removed from your list." @@ -85,6 +88,7 @@ class ChatChannelMembershipsController < ApplicationController if permitted_params[:user_action] == "accept" @chat_channel_membership.update(status: "active") channel_name = @chat_channel_membership.chat_channel.channel_name + send_chat_action_message("@#{current_user.username} joined #{@chat_channel_membership.channel_name}", current_user, @chat_channel_membership.chat_channel_id, "joined") flash[:settings_notice] = "Invitation to #{channel_name} accepted. It may take a moment to show up in your list." else @chat_channel_membership.update(status: "rejected") @@ -92,4 +96,10 @@ class ChatChannelMembershipsController < ApplicationController end redirect_to chat_channel_memberships_path end + + def send_chat_action_message(message, user, channel_id, action) + temp_message_id = (0...20).map { ("a".."z").to_a[rand(8)] }.join + message = Message.create("message_markdown" => message, "user_id" => user.id, "chat_channel_id" => channel_id, "chat_action" => action) + pusher_message_created(false, message, temp_message_id) + end end diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb index f8ffb2612..02645ad9f 100644 --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -1,6 +1,7 @@ class MessagesController < ApplicationController before_action :set_message, only: %i[destroy update] before_action :authenticate_user!, only: %i[create] + include MessagesHelper def create @message = Message.new(message_params) @@ -9,9 +10,9 @@ class MessagesController < ApplicationController authorize @message # sending temp message only to sender - pusher_message_created(true) + pusher_message_created(true, @message, @temp_message_id) if @message.save - pusher_message_created(false) + pusher_message_created(false, @message, @temp_message_id) notify_users(@message.chat_channel.channel_users_ids, "all") notify_users(@mentioned_users_id, "mention") render json: { status: "success", message: { temp_id: @temp_message_id, id: @message.id } }, status: :created @@ -79,30 +80,6 @@ class MessagesController < ApplicationController private - def create_pusher_payload(new_message, temp_id) - payload = { - 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"), - channel_type: new_message.chat_channel.channel_type, - username: new_message.user.username, - profile_image_url: ProfileImage.new(new_message.user).get(width: 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" - } - - if new_message.chat_channel.group? - payload[:chat_channel_adjusted_slug] = new_message.chat_channel.adjusted_slug - end - payload.to_json - end - def message_params @mentioned_users_id = params[:message][:mentioned_users_id] params.require(:message).permit(:message_markdown, :user_id, :chat_channel_id) @@ -127,21 +104,6 @@ class MessagesController < ApplicationController 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 - def notify_users(user_ids, type) return unless user_ids diff --git a/app/helpers/messages_helper.rb b/app/helpers/messages_helper.rb new file mode 100644 index 000000000..2e74b942c --- /dev/null +++ b/app/helpers/messages_helper.rb @@ -0,0 +1,41 @@ +module MessagesHelper + def create_pusher_payload(new_message, temp_id) + payload = { + 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"), + channel_type: new_message.chat_channel.channel_type, + username: new_message.user.username, + profile_image_url: ProfileImage.new(new_message.user).get(width: 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", + action: new_message.chat_action + } + + if new_message.chat_channel.group? + payload[:chat_channel_adjusted_slug] = new_message.chat_channel.adjusted_slug + end + payload.to_json + end + + def pusher_message_created(is_single, message, temp_message_id) + 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 diff --git a/app/javascript/chat/actionMessage.jsx b/app/javascript/chat/actionMessage.jsx new file mode 100644 index 000000000..a54f92a89 --- /dev/null +++ b/app/javascript/chat/actionMessage.jsx @@ -0,0 +1,90 @@ +import { h } from 'preact'; +import PropTypes from 'prop-types'; +import { adjustTimestamp } from './util'; + +const ActionMessage = ({ + user, + message, + color, + timestamp, + profileImageUrl, + onContentTrigger, +}) => { + const spanStyle = { color }; + + const messageArea = ( + + ); + + return ( +
+
+ + {`${user} + +
+
+
+
+ + + {user} + + + + {`${adjustTimestamp(timestamp)}`} + +
+
+
{messageArea}
+
+
+ ); +}; + +ActionMessage.propTypes = { + user: PropTypes.string.isRequired, + color: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + timestamp: PropTypes.string, + profileImageUrl: PropTypes.string, + onContentTrigger: PropTypes.func.isRequired, +}; + +ActionMessage.defaultProps = { + profileImageUrl: '', + timestamp: null, +}; + +export default ActionMessage; diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx index dd106bf0f..ea64df045 100644 --- a/app/javascript/chat/chat.jsx +++ b/app/javascript/chat/chat.jsx @@ -19,6 +19,7 @@ import Alert from './alert'; import Channels from './channels'; import Compose from './compose'; import Message from './message'; +import ActionMessage from './actionMessage'; import Content from './content'; import Video from './video'; @@ -957,23 +958,34 @@ export default class Chat extends Component { ); } } - return messages[activeChannelId].map((message) => ( - - )); + + return messages[activeChannelId].map((message) => + message.action ? ( + + ) : ( + + ), + ); }; triggerChannelFilter = (e) => { diff --git a/app/views/chat_channels/show.json.jbuilder b/app/views/chat_channels/show.json.jbuilder index 0facda140..463a8d053 100644 --- a/app/views/chat_channels/show.json.jbuilder +++ b/app/views/chat_channels/show.json.jbuilder @@ -8,6 +8,7 @@ json.messages @chat_messages.reverse do |message| json.edited_at message.edited_at json.timestamp message.created_at json.color message.preferred_user_color + json.action message.chat_action end json.key_format! camelize: :lower diff --git a/db/migrate/20200405103927_add_column_chat_action_to_messages.rb b/db/migrate/20200405103927_add_column_chat_action_to_messages.rb new file mode 100644 index 000000000..75a8af112 --- /dev/null +++ b/db/migrate/20200405103927_add_column_chat_action_to_messages.rb @@ -0,0 +1,5 @@ +class AddColumnChatActionToMessages < ActiveRecord::Migration[5.2] + def change + add_column :messages, :chat_action, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 1d2e63ba3..5ea2943cd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,6 +12,7 @@ ActiveRecord::Schema.define(version: 2020_04_07_091449) do + # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -307,6 +308,7 @@ ActiveRecord::Schema.define(version: 2020_04_07_091449) do t.string "slug" t.string "status", default: "active" t.datetime "updated_at", null: false + t.index ["slug"], name: "index_chat_channels_on_slug", unique: true end create_table "classified_listings", force: :cascade do |t| @@ -578,6 +580,7 @@ ActiveRecord::Schema.define(version: 2020_04_07_091449) do end create_table "messages", force: :cascade do |t| + t.string "chat_action" t.bigint "chat_channel_id", null: false t.datetime "created_at", null: false t.datetime "edited_at" diff --git a/spec/requests/chat_channel_memberships_spec.rb b/spec/requests/chat_channel_memberships_spec.rb index 0cb75cb8b..d40272504 100644 --- a/spec/requests/chat_channel_memberships_spec.rb +++ b/spec/requests/chat_channel_memberships_spec.rb @@ -186,6 +186,7 @@ RSpec.describe "ChatChannelMemberships", type: :request do context "when second user accept invitation" do it "sets chat channl membership status to rejected" do + allow(Pusher).to receive(:trigger).and_return(true) membership = ChatChannelMembership.last sign_in second_user put "/chat_channel_memberships/#{membership.id}", params: { @@ -200,6 +201,7 @@ RSpec.describe "ChatChannelMemberships", type: :request do context "when second user rejects invitation" do it "sets chat channl membership status to rejected" do + allow(Pusher).to receive(:trigger).and_return(true) membership = ChatChannelMembership.last sign_in second_user put "/chat_channel_memberships/#{membership.id}", params: { @@ -238,6 +240,7 @@ RSpec.describe "ChatChannelMemberships", type: :request do describe "DELETE /chat_channel_memberships/:id" do context "when user is logged in" do it "leaves chat channel" do + allow(Pusher).to receive(:trigger).and_return(true) chat_channel.add_users([second_user]) membership = ChatChannelMembership.last sign_in second_user @@ -265,6 +268,7 @@ RSpec.describe "ChatChannelMemberships", type: :request do context "when user is super admin" do it "removes member from channel" do + allow(Pusher).to receive(:trigger).and_return(true) user.add_role(:super_admin) membership = chat_channel.chat_channel_memberships.where(user_id: user.id).last removed_channel_membership = ChatChannelMembership.last @@ -279,6 +283,7 @@ RSpec.describe "ChatChannelMemberships", type: :request do context "when user is moderator of channel" do it "removes member from channel" do + allow(Pusher).to receive(:trigger).and_return(true) membership = chat_channel.chat_channel_memberships.where(user_id: user.id).last membership.update(role: "mod") removed_channel_membership = ChatChannelMembership.last