turbo_chat 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +741 -0
- data/app/assets/config/chat_gem_manifest.js +6 -0
- data/app/assets/javascripts/chat_gem/application.js +4 -0
- data/app/assets/javascripts/chat_gem/lifecycle_events.js +93 -0
- data/app/assets/javascripts/chat_gem/messages.js +442 -0
- data/app/assets/javascripts/chat_gem/realtime.js +398 -0
- data/app/assets/javascripts/chat_gem/shared.js +488 -0
- data/app/assets/stylesheets/chat_gem/application.css +741 -0
- data/app/controllers/chat_gem/application_controller.rb +41 -0
- data/app/controllers/chat_gem/chat_memberships_controller.rb +81 -0
- data/app/controllers/chat_gem/chat_messages_controller.rb +144 -0
- data/app/controllers/chat_gem/chats_controller/event_payload_support.rb +58 -0
- data/app/controllers/chat_gem/chats_controller/invitation_support.rb +31 -0
- data/app/controllers/chat_gem/chats_controller.rb +125 -0
- data/app/helpers/chat_gem/application_helper/config_support.rb +41 -0
- data/app/helpers/chat_gem/application_helper/mention_support/entry_builder.rb +55 -0
- data/app/helpers/chat_gem/application_helper/mention_support/permission_support.rb +28 -0
- data/app/helpers/chat_gem/application_helper/mention_support/token_builder.rb +49 -0
- data/app/helpers/chat_gem/application_helper/mention_support.rb +80 -0
- data/app/helpers/chat_gem/application_helper/message_rendering.rb +165 -0
- data/app/helpers/chat_gem/application_helper/participant_support.rb +81 -0
- data/app/helpers/chat_gem/application_helper.rb +12 -0
- data/app/models/chat_gem/application_record.rb +5 -0
- data/app/models/chat_gem/chat.rb +127 -0
- data/app/models/chat_gem/chat_membership.rb +136 -0
- data/app/models/chat_gem/chat_message/blocked_words_moderation.rb +120 -0
- data/app/models/chat_gem/chat_message/body_length_validation.rb +20 -0
- data/app/models/chat_gem/chat_message/broadcasting.rb +61 -0
- data/app/models/chat_gem/chat_message/formatting.rb +81 -0
- data/app/models/chat_gem/chat_message/mention_validation.rb +85 -0
- data/app/models/chat_gem/chat_message/signals.rb +61 -0
- data/app/models/chat_gem/chat_message.rb +40 -0
- data/app/views/chat_gem/chat_messages/_chat_message.html.erb +1 -0
- data/app/views/chat_gem/chat_messages/_form.html.erb +22 -0
- data/app/views/chat_gem/chat_messages/_message.html.erb +83 -0
- data/app/views/chat_gem/chat_messages/_signal.html.erb +3 -0
- data/app/views/chat_gem/chat_messages/_signals.html.erb +24 -0
- data/app/views/chat_gem/chat_messages/index.html.erb +1 -0
- data/app/views/chat_gem/chats/index.html.erb +51 -0
- data/app/views/chat_gem/chats/new.html.erb +13 -0
- data/app/views/chat_gem/chats/show.html.erb +95 -0
- data/app/views/layouts/chat_gem/application.html.erb +20 -0
- data/config/routes.rb +16 -0
- data/db/migrate/20260215000000_create_chat_gem_chats.rb +8 -0
- data/db/migrate/20260215000001_create_chat_gem_chat_memberships.rb +19 -0
- data/db/migrate/20260215000002_create_chat_gem_chat_messages.rb +14 -0
- data/db/migrate/20260218000011_add_closed_at_to_chat_gem_chats.rb +6 -0
- data/db/migrate/20260218000012_add_custom_role_key_to_chat_memberships.rb +6 -0
- data/db/migrate/20260218000013_add_invitation_accepted_to_chat_gem_chat_memberships.rb +5 -0
- data/lib/chat_gem/configuration.rb +242 -0
- data/lib/chat_gem/engine.rb +29 -0
- data/lib/chat_gem/model_extensions/chat_participant.rb +45 -0
- data/lib/chat_gem/moderation.rb +194 -0
- data/lib/chat_gem/permission.rb +193 -0
- data/lib/chat_gem/signals.rb +26 -0
- data/lib/chat_gem/version.rb +3 -0
- data/lib/chat_gem.rb +24 -0
- data/lib/generators/chat_gem/install/install_generator.rb +18 -0
- data/lib/generators/chat_gem/install/templates/chat_gem.rb +36 -0
- data/lib/generators/turbo_chat/install/install_generator.rb +18 -0
- data/lib/generators/turbo_chat/install/templates/turbo_chat.rb +36 -0
- data/lib/tasks/chat_gem_tasks.rake +1 -0
- data/lib/tasks/turbo_chat_tasks.rake +10 -0
- data/lib/turbo_chat/version.rb +5 -0
- data/lib/turbo_chat.rb +24 -0
- data/turbo_chat.gemspec +31 -0
- metadata +155 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module ChatGem
|
|
2
|
+
class ApplicationController < ::ApplicationController
|
|
3
|
+
layout "chat_gem/application"
|
|
4
|
+
|
|
5
|
+
helper_method :current_chat_participant
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def current_chat_participant
|
|
10
|
+
method_name = :chat_current_participant
|
|
11
|
+
raise NotImplementedError, "Define ##{method_name} in your host application controller" unless respond_to?(method_name, true)
|
|
12
|
+
|
|
13
|
+
participant = send(method_name)
|
|
14
|
+
return participant if participant.nil? || participant.respond_to?(:active_chat_memberships)
|
|
15
|
+
|
|
16
|
+
raise ArgumentError, "##{method_name} must return a model that uses `acts_as_chat_participant`"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def permission_for(chat = nil)
|
|
20
|
+
ChatGem.configuration.permission_adapter.new(current_chat_participant, chat)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def authorize_create_chat!
|
|
24
|
+
return if permission_for.can_create_chat?
|
|
25
|
+
|
|
26
|
+
head :forbidden
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def authorize_view_chat!(chat)
|
|
30
|
+
return if permission_for(chat).can_view_chat?
|
|
31
|
+
|
|
32
|
+
head :forbidden
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def authorize_post_message!(chat)
|
|
36
|
+
return if permission_for(chat).can_post_message?
|
|
37
|
+
|
|
38
|
+
head :forbidden
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module ChatGem
|
|
2
|
+
class ChatMembershipsController < ApplicationController
|
|
3
|
+
include ChatGem::ChatsController::EventPayloadSupport
|
|
4
|
+
|
|
5
|
+
before_action :set_chat
|
|
6
|
+
before_action -> { authorize_view_chat!(@chat) }
|
|
7
|
+
before_action :authorize_invite_member!
|
|
8
|
+
|
|
9
|
+
def create
|
|
10
|
+
participant = invite_participant
|
|
11
|
+
membership = @chat.chat_memberships.find_or_initialize_by(participant: participant)
|
|
12
|
+
pending_invitation_attributes = invitation_pending_attributes
|
|
13
|
+
|
|
14
|
+
if membership.persisted?
|
|
15
|
+
membership.assign_attributes(
|
|
16
|
+
removed_at: nil,
|
|
17
|
+
muted: false,
|
|
18
|
+
timed_out_until: nil
|
|
19
|
+
)
|
|
20
|
+
membership.assign_attributes(pending_invitation_attributes)
|
|
21
|
+
else
|
|
22
|
+
membership.assign_attributes(role: :member)
|
|
23
|
+
membership.assign_attributes(pending_invitation_attributes)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
membership.save!
|
|
27
|
+
set_chat_lifecycle_event(action: :invited, chat: @chat, membership: membership)
|
|
28
|
+
redirect_to chat_path(@chat), notice: "Participant invited"
|
|
29
|
+
rescue ActiveRecord::RecordNotFound
|
|
30
|
+
redirect_to chat_path(@chat), alert: "Participant not found"
|
|
31
|
+
rescue NameError, ArgumentError
|
|
32
|
+
redirect_to chat_path(@chat), alert: "Invalid participant type"
|
|
33
|
+
rescue ActiveRecord::RecordInvalid => error
|
|
34
|
+
redirect_to chat_path(@chat), alert: error.record.errors.full_messages.to_sentence
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def set_chat
|
|
40
|
+
@chat = ChatGem::Chat.find(params[:chat_id])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def authorize_invite_member!
|
|
44
|
+
permission = permission_for(@chat)
|
|
45
|
+
return if permission.respond_to?(:can_invite_member?) && permission.can_invite_member?
|
|
46
|
+
|
|
47
|
+
head :forbidden
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def invite_params
|
|
51
|
+
params.require(:chat_membership).permit(:participant_type, :participant_id)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def invite_participant
|
|
55
|
+
participant_type = invite_params.fetch(:participant_type).to_s
|
|
56
|
+
participant_id = invite_params.fetch(:participant_id).to_s
|
|
57
|
+
raise ArgumentError if participant_type.blank? || participant_id.blank?
|
|
58
|
+
|
|
59
|
+
participant_class = participant_type.safe_constantize
|
|
60
|
+
raise NameError if participant_class.nil?
|
|
61
|
+
raise ArgumentError unless participant_class < ActiveRecord::Base
|
|
62
|
+
raise ArgumentError unless participant_class.method_defined?(:active_chat_memberships)
|
|
63
|
+
raise ArgumentError unless invite_type_allowed?(participant_class)
|
|
64
|
+
|
|
65
|
+
participant_class.find(participant_id)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def invite_type_allowed?(participant_class)
|
|
69
|
+
inviter = current_chat_participant
|
|
70
|
+
return false if inviter.nil?
|
|
71
|
+
|
|
72
|
+
participant_class.base_class.name == inviter.class.base_class.name
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def invitation_pending_attributes
|
|
76
|
+
return {} unless ChatGem::ChatMembership.invitation_tracking_supported?
|
|
77
|
+
|
|
78
|
+
{ invitation_accepted: false }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
module ChatGem
|
|
2
|
+
class ChatMessagesController < ApplicationController
|
|
3
|
+
before_action :set_chat
|
|
4
|
+
before_action -> { authorize_view_chat!(@chat) }, only: :index
|
|
5
|
+
before_action -> { authorize_post_message!(@chat) }, only: :create
|
|
6
|
+
before_action :set_chat_message, only: :update
|
|
7
|
+
before_action :authorize_edit_chat_message!, only: :update
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
@chat_messages = @chat.visible_messages
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create
|
|
14
|
+
return respond_to_clear_signal_request if clear_signal_request?
|
|
15
|
+
|
|
16
|
+
build_chat_message
|
|
17
|
+
|
|
18
|
+
if @chat_message.save
|
|
19
|
+
respond_to_chat_message_create_success
|
|
20
|
+
else
|
|
21
|
+
respond_to_chat_message_create_failure
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def update
|
|
26
|
+
if @chat_message.update(edit_chat_message_params)
|
|
27
|
+
return render_chat_message_update if turbo_stream_request?
|
|
28
|
+
|
|
29
|
+
redirect_to chat_path(@chat), notice: "Message updated"
|
|
30
|
+
else
|
|
31
|
+
return render_chat_message_update(force_edit_open: true, status: :unprocessable_entity) if turbo_stream_request?
|
|
32
|
+
|
|
33
|
+
redirect_to chat_path(@chat), alert: @chat_message.errors.full_messages.to_sentence
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def set_chat
|
|
40
|
+
@chat = ChatGem::Chat.find(params[:chat_id])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def chat_message_params
|
|
44
|
+
params.require(:chat_message).permit(:body, :kind, :signal_type)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def edit_chat_message_params
|
|
48
|
+
params.require(:chat_message).permit(:body)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def signal_request?
|
|
52
|
+
params.dig(:chat_message, :kind).to_s == "signal"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def clear_signal_request?
|
|
56
|
+
return false unless signal_request?
|
|
57
|
+
|
|
58
|
+
ActiveModel::Type::Boolean.new.cast(params.dig(:chat_message, :clear))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_chat_message
|
|
62
|
+
@chat_message = @chat.chat_messages.build(chat_message_params)
|
|
63
|
+
@chat_message.participant = current_chat_participant
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def respond_to_clear_signal_request
|
|
67
|
+
ChatGem::ChatMessage.clear_signals!(chat: @chat, participant: current_chat_participant)
|
|
68
|
+
respond_to do |format|
|
|
69
|
+
format.turbo_stream { render_signals_update }
|
|
70
|
+
format.html { redirect_to chat_path(@chat) }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def respond_to_chat_message_create_success
|
|
75
|
+
respond_to do |format|
|
|
76
|
+
format.turbo_stream do
|
|
77
|
+
signal_request? ? render_signals_update : head(:ok)
|
|
78
|
+
end
|
|
79
|
+
format.html { redirect_to chat_path(@chat) }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def respond_to_chat_message_create_failure
|
|
84
|
+
@chat_messages = @chat.visible_messages
|
|
85
|
+
@chat_permission = permission_for(@chat)
|
|
86
|
+
@can_post_message = @chat_permission.can_post_message?
|
|
87
|
+
respond_to do |format|
|
|
88
|
+
format.turbo_stream { render "chat_gem/chats/show", status: :unprocessable_entity }
|
|
89
|
+
format.html { render "chat_gem/chats/show", status: :unprocessable_entity }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def render_signals_update
|
|
94
|
+
render turbo_stream: turbo_stream.update(
|
|
95
|
+
view_context.dom_id(@chat, :signals),
|
|
96
|
+
partial: "chat_gem/chat_messages/signals",
|
|
97
|
+
locals: { chat: @chat }
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def set_chat_message
|
|
102
|
+
@chat_message = @chat.chat_messages.messages_only.find(params[:id])
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def authorize_edit_chat_message!
|
|
106
|
+
chat_permission = permission_for(@chat)
|
|
107
|
+
can_edit = if chat_permission.respond_to?(:can_edit_message?)
|
|
108
|
+
chat_permission.can_edit_message?(@chat_message)
|
|
109
|
+
else
|
|
110
|
+
can_post = if chat_permission.respond_to?(:can_post_message?)
|
|
111
|
+
chat_permission.can_post_message?
|
|
112
|
+
elsif chat_permission.respond_to?(:can_view_chat?)
|
|
113
|
+
chat_permission.can_view_chat?
|
|
114
|
+
else
|
|
115
|
+
false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
can_post &&
|
|
119
|
+
@chat_message.participant_type.to_s == current_chat_participant.class.base_class.name &&
|
|
120
|
+
@chat_message.participant_id.to_s == current_chat_participant.id.to_s
|
|
121
|
+
end
|
|
122
|
+
return if can_edit
|
|
123
|
+
|
|
124
|
+
respond_to do |format|
|
|
125
|
+
format.html { redirect_to chat_path(@chat), alert: "Not allowed to edit this message" }
|
|
126
|
+
format.any { head :forbidden }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def render_chat_message_update(force_edit_open: false, status: :ok)
|
|
131
|
+
locals = { chat_message: @chat_message }
|
|
132
|
+
locals[:force_edit_open] = true if force_edit_open
|
|
133
|
+
render turbo_stream: turbo_stream.replace(
|
|
134
|
+
view_context.dom_id(@chat_message),
|
|
135
|
+
partial: "chat_gem/chat_messages/message",
|
|
136
|
+
locals: locals
|
|
137
|
+
), status: status
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def turbo_stream_request?
|
|
141
|
+
request.headers["Accept"].to_s.include?("turbo-stream")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module ChatGem
|
|
2
|
+
class ChatsController < ApplicationController
|
|
3
|
+
module EventPayloadSupport
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
def invitation_accepted_payload
|
|
7
|
+
payload = flash[:chat_gem_invitation_accepted]
|
|
8
|
+
return nil unless payload.respond_to?(:to_h)
|
|
9
|
+
|
|
10
|
+
symbolized_payload = payload.to_h.symbolize_keys
|
|
11
|
+
chat_id = symbolized_payload[:chatId].presence || symbolized_payload[:chat_id].presence
|
|
12
|
+
return nil if chat_id.blank?
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
chatId: chat_id.to_s,
|
|
16
|
+
chatTitle: symbolized_payload[:chatTitle].presence || symbolized_payload[:chat_title].presence,
|
|
17
|
+
chatMembershipId: symbolized_payload[:chatMembershipId].presence || symbolized_payload[:chat_membership_id].presence
|
|
18
|
+
}.compact
|
|
19
|
+
rescue StandardError
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def chat_lifecycle_event_payload
|
|
24
|
+
payload = flash[:chat_gem_chat_lifecycle_event]
|
|
25
|
+
return nil unless payload.respond_to?(:to_h)
|
|
26
|
+
|
|
27
|
+
symbolized_payload = payload.to_h.symbolize_keys
|
|
28
|
+
event_name = symbolized_payload[:eventName].presence || symbolized_payload[:event_name].presence
|
|
29
|
+
return nil if event_name.blank?
|
|
30
|
+
|
|
31
|
+
chat_id = symbolized_payload[:chatId].presence || symbolized_payload[:chat_id].presence
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
eventName: event_name.to_s,
|
|
35
|
+
action: symbolized_payload[:action].presence,
|
|
36
|
+
chatId: chat_id.to_s.presence,
|
|
37
|
+
chatTitle: symbolized_payload[:chatTitle].presence || symbolized_payload[:chat_title].presence,
|
|
38
|
+
chatMembershipId: symbolized_payload[:chatMembershipId].presence || symbolized_payload[:chat_membership_id].presence
|
|
39
|
+
}.compact
|
|
40
|
+
rescue StandardError
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def set_chat_lifecycle_event(action:, chat:, membership: nil)
|
|
45
|
+
return if action.blank? || chat.nil?
|
|
46
|
+
|
|
47
|
+
action_key = action.to_s
|
|
48
|
+
flash[:chat_gem_chat_lifecycle_event] = {
|
|
49
|
+
eventName: "chat-gem:chat-#{action_key}",
|
|
50
|
+
action: action_key,
|
|
51
|
+
chatId: chat.id.to_s,
|
|
52
|
+
chatTitle: chat.title,
|
|
53
|
+
chatMembershipId: membership&.id&.to_s
|
|
54
|
+
}.compact
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module ChatGem
|
|
2
|
+
class ChatsController < ApplicationController
|
|
3
|
+
module InvitationSupport
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
def build_invitable_participants
|
|
7
|
+
participant_type = current_chat_participant.class.base_class.name
|
|
8
|
+
participant_class = participant_type.safe_constantize
|
|
9
|
+
return [] if participant_class.nil?
|
|
10
|
+
return [] unless participant_class < ActiveRecord::Base
|
|
11
|
+
return [] unless participant_class.method_defined?(:active_chat_memberships)
|
|
12
|
+
|
|
13
|
+
current_member_ids = @chat.chat_memberships.where(removed_at: nil, participant_type: participant_type).pluck(:participant_id)
|
|
14
|
+
participant_class.where.not(id: current_member_ids).order(id: :asc).limit(100).to_a
|
|
15
|
+
rescue StandardError
|
|
16
|
+
[]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def pending_invitations_for(participant)
|
|
20
|
+
return ChatGem::ChatMembership.none if participant.nil?
|
|
21
|
+
return ChatGem::ChatMembership.none unless ChatGem::ChatMembership.invitation_tracking_supported?
|
|
22
|
+
|
|
23
|
+
ChatGem::ChatMembership
|
|
24
|
+
.pending
|
|
25
|
+
.where(participant: participant)
|
|
26
|
+
.includes(:chat)
|
|
27
|
+
.order(created_at: :desc, id: :desc)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
module ChatGem
|
|
2
|
+
class ChatsController < ApplicationController
|
|
3
|
+
include ChatGem::ChatsController::InvitationSupport
|
|
4
|
+
include ChatGem::ChatsController::EventPayloadSupport
|
|
5
|
+
|
|
6
|
+
before_action :authorize_create_chat!, only: %i[new create]
|
|
7
|
+
before_action :set_chat, only: %i[show accept decline leave close reopen]
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
participant = current_chat_participant
|
|
11
|
+
@chats = ChatGem::Chat.for_participant(participant).order(created_at: :desc, id: :desc)
|
|
12
|
+
@pending_invitations = pending_invitations_for(participant)
|
|
13
|
+
@invitation_accepted_event = invitation_accepted_payload
|
|
14
|
+
@chat_lifecycle_event = chat_lifecycle_event_payload
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def accept
|
|
18
|
+
participant = current_chat_participant
|
|
19
|
+
return head :forbidden if participant.nil?
|
|
20
|
+
return redirect_to(chats_path, alert: "Run latest chat_gem migrations to accept invitations", status: :see_other) unless ChatGem::ChatMembership.invitation_tracking_supported?
|
|
21
|
+
|
|
22
|
+
membership = @chat.chat_memberships.pending.find_by(participant: participant)
|
|
23
|
+
return redirect_to(chats_path, alert: "Invitation not found", status: :see_other) if membership.nil?
|
|
24
|
+
|
|
25
|
+
membership.accept_invitation!
|
|
26
|
+
flash[:chat_gem_invitation_accepted] = {
|
|
27
|
+
chatId: @chat.id,
|
|
28
|
+
chatTitle: @chat.title,
|
|
29
|
+
chatMembershipId: membership.id
|
|
30
|
+
}
|
|
31
|
+
set_chat_lifecycle_event(action: :joined, chat: @chat, membership: membership)
|
|
32
|
+
redirect_to chats_path, notice: "Invitation accepted", status: :see_other
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def decline
|
|
36
|
+
participant = current_chat_participant
|
|
37
|
+
return head :forbidden if participant.nil?
|
|
38
|
+
return redirect_to(chats_path, alert: "Run latest chat_gem migrations to decline invitations", status: :see_other) unless ChatGem::ChatMembership.invitation_tracking_supported?
|
|
39
|
+
|
|
40
|
+
membership = @chat.chat_memberships.pending.find_by(participant: participant)
|
|
41
|
+
return redirect_to(chats_path, alert: "Invitation not found", status: :see_other) if membership.nil?
|
|
42
|
+
|
|
43
|
+
membership.update!(removed_at: Time.current, muted: false, timed_out_until: nil, invitation_accepted: false)
|
|
44
|
+
set_chat_lifecycle_event(action: :declined, chat: @chat, membership: membership)
|
|
45
|
+
redirect_to chats_path, notice: "Invitation declined", status: :see_other
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def new
|
|
49
|
+
@chat = ChatGem::Chat.new
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def create
|
|
53
|
+
@chat = ChatGem::Chat.new(chat_params)
|
|
54
|
+
|
|
55
|
+
if @chat.save
|
|
56
|
+
membership = @chat.chat_memberships.create!(participant: current_chat_participant, role: :admin)
|
|
57
|
+
set_chat_lifecycle_event(action: :joined, chat: @chat, membership: membership)
|
|
58
|
+
redirect_to chat_path(@chat), notice: "Chat created"
|
|
59
|
+
else
|
|
60
|
+
render :new, status: :unprocessable_entity
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def show
|
|
65
|
+
authorize_view_chat!(@chat)
|
|
66
|
+
return if performed?
|
|
67
|
+
|
|
68
|
+
@chat_lifecycle_event = chat_lifecycle_event_payload
|
|
69
|
+
@chat_permission = permission_for(@chat)
|
|
70
|
+
@chat_messages = @chat.visible_messages
|
|
71
|
+
@can_post_message = @chat_permission.can_post_message?
|
|
72
|
+
@can_invite_member = @chat_permission.respond_to?(:can_invite_member?) && @chat_permission.can_invite_member?
|
|
73
|
+
@can_close_chat = @chat_permission.can_close_chat?
|
|
74
|
+
@can_reopen_chat = @chat_permission.can_reopen_chat?
|
|
75
|
+
@can_edit_own_messages = if @chat_permission.respond_to?(:can_edit_message?)
|
|
76
|
+
@chat_permission.can_edit_message?
|
|
77
|
+
else
|
|
78
|
+
@chat_permission.can_post_message?
|
|
79
|
+
end
|
|
80
|
+
@chat_message = @chat.chat_messages.build if @can_post_message
|
|
81
|
+
@invitable_participants = build_invitable_participants if @can_invite_member
|
|
82
|
+
@invite_participant_type = current_chat_participant.class.base_class.name if @can_invite_member
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def leave
|
|
86
|
+
authorize_view_chat!(@chat)
|
|
87
|
+
return if performed?
|
|
88
|
+
|
|
89
|
+
membership = @chat.chat_memberships.active.find_by(participant: current_chat_participant)
|
|
90
|
+
return redirect_to(chats_path, alert: "You are no longer a participant in this chat", status: :see_other) if membership.nil?
|
|
91
|
+
|
|
92
|
+
membership.update!(removed_at: Time.current, muted: false, timed_out_until: nil)
|
|
93
|
+
set_chat_lifecycle_event(action: :left, chat: @chat, membership: membership)
|
|
94
|
+
redirect_to chats_path, notice: "You left the chat", status: :see_other
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def close
|
|
98
|
+
chat_permission = permission_for(@chat)
|
|
99
|
+
return head :forbidden unless chat_permission.can_close_chat?
|
|
100
|
+
|
|
101
|
+
@chat.close!
|
|
102
|
+
set_chat_lifecycle_event(action: :closed, chat: @chat)
|
|
103
|
+
redirect_to chat_path(@chat), notice: "Chat closed", status: :see_other
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def reopen
|
|
107
|
+
chat_permission = permission_for(@chat)
|
|
108
|
+
return head :forbidden unless chat_permission.can_reopen_chat?
|
|
109
|
+
|
|
110
|
+
@chat.reopen!
|
|
111
|
+
set_chat_lifecycle_event(action: :reopened, chat: @chat)
|
|
112
|
+
redirect_to chat_path(@chat), notice: "Chat reopened", status: :see_other
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def set_chat
|
|
118
|
+
@chat = ChatGem::Chat.find(params[:id])
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def chat_params
|
|
122
|
+
params.require(:chat).permit(:title)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module ChatGem
|
|
2
|
+
module ApplicationHelper
|
|
3
|
+
module ConfigSupport
|
|
4
|
+
def chat_mention_filter_exclude_self?
|
|
5
|
+
chat_config_boolean(:mention_filter_exclude_self, default: true)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def chat_mention_filter_hide_roles?
|
|
9
|
+
chat_config_boolean(:mention_filter_hide_roles, default: true)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def chat_emit_mention_events?
|
|
13
|
+
chat_config_boolean(:emit_mention_events, default: false)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def chat_emit_invitation_events?
|
|
17
|
+
chat_config_boolean(:emit_invitation_events, default: false)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def chat_emit_chat_lifecycle_events?
|
|
21
|
+
chat_config_boolean(:emit_chat_lifecycle_events, default: false)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def chat_config_value(method_name, default: nil)
|
|
27
|
+
configuration = ChatGem.configuration
|
|
28
|
+
return default unless configuration.respond_to?(method_name)
|
|
29
|
+
|
|
30
|
+
configuration.public_send(method_name)
|
|
31
|
+
rescue StandardError
|
|
32
|
+
default
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def chat_config_boolean(method_name, default:)
|
|
36
|
+
value = chat_config_value(method_name, default: default)
|
|
37
|
+
ActiveModel::Type::Boolean.new.cast(value)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module ChatGem
|
|
2
|
+
module ApplicationHelper
|
|
3
|
+
module MentionSupport
|
|
4
|
+
module EntryBuilder
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def chat_member_mention_entries(chat)
|
|
8
|
+
taken_tokens = {}
|
|
9
|
+
|
|
10
|
+
active_chat_memberships(chat).each_with_object([]) do |membership, entries|
|
|
11
|
+
participant = membership.participant
|
|
12
|
+
next if participant.nil?
|
|
13
|
+
|
|
14
|
+
identifier = normalized_mention_identifier(participant_mention_base_identifier(participant))
|
|
15
|
+
identifier = fallback_mention_identifier(participant) if identifier.blank?
|
|
16
|
+
token = unique_mention_token(identifier, taken_tokens)
|
|
17
|
+
|
|
18
|
+
entries << {
|
|
19
|
+
token: token,
|
|
20
|
+
label: chat_participant_name(participant),
|
|
21
|
+
participant: participant,
|
|
22
|
+
participant_type: participant.class.base_class.name,
|
|
23
|
+
participant_id: participant.id
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def chat_role_mention_entries(chat)
|
|
29
|
+
role_tokens = {}
|
|
30
|
+
|
|
31
|
+
active_chat_memberships(chat).each_with_object([]) do |membership, entries|
|
|
32
|
+
role_key = membership.effective_role_key.to_s.strip
|
|
33
|
+
next if role_key.blank?
|
|
34
|
+
|
|
35
|
+
role_token = "@#{role_key.upcase}"
|
|
36
|
+
next if role_tokens[role_token]
|
|
37
|
+
|
|
38
|
+
role_tokens[role_token] = true
|
|
39
|
+
entries << {
|
|
40
|
+
token: role_token,
|
|
41
|
+
label: "#{membership.effective_role_name} role",
|
|
42
|
+
kind: "role"
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def active_chat_memberships(chat)
|
|
48
|
+
return [] unless chat.respond_to?(:chat_memberships)
|
|
49
|
+
|
|
50
|
+
chat.chat_memberships.active.includes(:participant).order(:id)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module ChatGem
|
|
2
|
+
module ApplicationHelper
|
|
3
|
+
module MentionSupport
|
|
4
|
+
module PermissionSupport
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def mention_permission_for(chat)
|
|
8
|
+
return nil unless respond_to?(:current_chat_participant, true)
|
|
9
|
+
|
|
10
|
+
participant = current_chat_participant
|
|
11
|
+
return nil if participant.nil?
|
|
12
|
+
|
|
13
|
+
ChatGem.configuration.permission_adapter.new(participant, chat)
|
|
14
|
+
rescue StandardError
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def mention_permission_allows?(permission, method_name)
|
|
19
|
+
return true unless permission.respond_to?(method_name)
|
|
20
|
+
|
|
21
|
+
permission.public_send(method_name)
|
|
22
|
+
rescue StandardError
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module ChatGem
|
|
2
|
+
module ApplicationHelper
|
|
3
|
+
module MentionSupport
|
|
4
|
+
module TokenBuilder
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def participant_mention_base_identifier(participant)
|
|
8
|
+
return participant.username if participant.respond_to?(:username) && participant.username.present?
|
|
9
|
+
if participant.respond_to?(:email) && participant.email.present?
|
|
10
|
+
return participant.email.to_s.split("@").first
|
|
11
|
+
end
|
|
12
|
+
return participant.name if participant.respond_to?(:name) && participant.name.present?
|
|
13
|
+
|
|
14
|
+
participant.to_s
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def fallback_mention_identifier(participant)
|
|
18
|
+
participant_id = participant.respond_to?(:id) ? participant.id : nil
|
|
19
|
+
return "member_#{participant_id}" if participant_id.present?
|
|
20
|
+
|
|
21
|
+
"member"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def normalized_mention_identifier(value)
|
|
25
|
+
slug = I18n.transliterate(value.to_s)
|
|
26
|
+
slug = slug.downcase.gsub(/[^a-z0-9_]+/, "_").gsub(/\A_+|_+\z/, "").squeeze("_")
|
|
27
|
+
slug = "member_#{slug}" if slug.match?(/\A\d/)
|
|
28
|
+
slug.presence
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def unique_mention_token(identifier, taken_tokens)
|
|
32
|
+
base = normalized_mention_identifier(identifier) || "member"
|
|
33
|
+
token = "@#{base}"
|
|
34
|
+
return taken_tokens[token] = token unless taken_tokens.key?(token)
|
|
35
|
+
|
|
36
|
+
suffix = 2
|
|
37
|
+
loop do
|
|
38
|
+
candidate = "@#{base}_#{suffix}"
|
|
39
|
+
unless taken_tokens.key?(candidate)
|
|
40
|
+
taken_tokens[candidate] = candidate
|
|
41
|
+
return candidate
|
|
42
|
+
end
|
|
43
|
+
suffix += 1
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|