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,95 @@
|
|
|
1
|
+
<section class="chat-shell <%= "chat-shell--closed" if @chat.closed? %>"
|
|
2
|
+
data-chat-emit-chat-lifecycle-events="<%= chat_emit_chat_lifecycle_events? %>"
|
|
3
|
+
data-chat-lifecycle-event="<%= json_escape(@chat_lifecycle_event.to_json) if @chat_lifecycle_event.present? %>">
|
|
4
|
+
<% current_participant = current_chat_participant %>
|
|
5
|
+
<% current_participant_type = current_participant.class.base_class.name %>
|
|
6
|
+
<% current_participant_id = current_participant.id %>
|
|
7
|
+
<% self_mention_tokens = chat_self_mention_tokens(chat: @chat, participant: current_participant) %>
|
|
8
|
+
<% self_role_mention_token = chat_self_role_mention_token(chat: @chat, participant: current_participant) %>
|
|
9
|
+
<% mention_container_style = chat_mentions_container_inline_style %>
|
|
10
|
+
<header class="chat-header">
|
|
11
|
+
<div class="chat-header-copy">
|
|
12
|
+
<p class="chat-header-kicker"><%= @chat.closed? ? "Conversation closed" : "Conversation active" %></p>
|
|
13
|
+
<div class="chat-header-title-row">
|
|
14
|
+
<h1><%= @chat.title %></h1>
|
|
15
|
+
<span class="chat-status <%= @chat.closed? ? "chat-status--closed" : "chat-status--open" %>">
|
|
16
|
+
<%= @chat.closed? ? "Closed" : "Open" %>
|
|
17
|
+
</span>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="chat-header-actions">
|
|
21
|
+
<% if @chat.opened? && @can_close_chat %>
|
|
22
|
+
<%= button_to "Close",
|
|
23
|
+
close_chat_path(@chat),
|
|
24
|
+
method: :patch,
|
|
25
|
+
form_class: "chat-inline-form",
|
|
26
|
+
class: "chat-btn chat-btn--danger",
|
|
27
|
+
data: { turbo_confirm: "Close this chat?" } %>
|
|
28
|
+
<% elsif @chat.closed? && @can_reopen_chat %>
|
|
29
|
+
<%= button_to "Reopen",
|
|
30
|
+
reopen_chat_path(@chat),
|
|
31
|
+
method: :patch,
|
|
32
|
+
form_class: "chat-inline-form",
|
|
33
|
+
class: "chat-btn chat-btn--success" %>
|
|
34
|
+
<% end %>
|
|
35
|
+
|
|
36
|
+
<%= button_to "Leave",
|
|
37
|
+
leave_chat_path(@chat),
|
|
38
|
+
method: :patch,
|
|
39
|
+
form_class: "chat-inline-form",
|
|
40
|
+
class: "chat-btn chat-btn--danger",
|
|
41
|
+
data: { turbo_confirm: "Leave this chat?" } %>
|
|
42
|
+
<%= link_to "Back", chats_path, class: "chat-btn chat-btn--ghost" %>
|
|
43
|
+
</div>
|
|
44
|
+
</header>
|
|
45
|
+
|
|
46
|
+
<% if @can_invite_member %>
|
|
47
|
+
<div class="chat-invite">
|
|
48
|
+
<% if @invitable_participants.present? %>
|
|
49
|
+
<%= form_with url: chat_chat_memberships_path(@chat), scope: :chat_membership, class: "chat-form chat-form--invite" do |f| %>
|
|
50
|
+
<%= f.hidden_field :participant_type, value: @invite_participant_type %>
|
|
51
|
+
<%= f.select :participant_id,
|
|
52
|
+
options_for_select(@invitable_participants.map { |participant| ["#{chat_participant_name(participant)} (##{participant.id})", participant.id] }),
|
|
53
|
+
{ prompt: "Invite participant" },
|
|
54
|
+
required: true %>
|
|
55
|
+
<%= f.submit "Invite", class: "chat-btn" %>
|
|
56
|
+
<% end %>
|
|
57
|
+
<% else %>
|
|
58
|
+
<p class="chat-hint">No eligible participants to invite.</p>
|
|
59
|
+
<% end %>
|
|
60
|
+
</div>
|
|
61
|
+
<% end %>
|
|
62
|
+
|
|
63
|
+
<%= turbo_stream_from @chat, :messages %>
|
|
64
|
+
|
|
65
|
+
<section class="chat-window">
|
|
66
|
+
<div id="<%= dom_id(@chat, :messages) %>"
|
|
67
|
+
class="chat-messages"
|
|
68
|
+
data-chat-id="<%= @chat.id %>"
|
|
69
|
+
data-chat-self-participant-type="<%= current_participant_type %>"
|
|
70
|
+
data-chat-self-participant-id="<%= current_participant_id %>"
|
|
71
|
+
data-chat-self-mention-tokens="<%= json_escape(self_mention_tokens.to_json) %>"
|
|
72
|
+
data-chat-self-role-mention-token="<%= self_role_mention_token %>"
|
|
73
|
+
data-chat-mention-filter-exclude-self="<%= chat_mention_filter_exclude_self? %>"
|
|
74
|
+
data-chat-mention-filter-hide-roles="<%= chat_mention_filter_hide_roles? %>"
|
|
75
|
+
data-chat-emit-mention-events="<%= chat_emit_mention_events? %>"
|
|
76
|
+
data-chat-can-edit-own-messages="<%= @can_edit_own_messages %>"
|
|
77
|
+
<%= %(style="#{mention_container_style}") if mention_container_style.present? %>>
|
|
78
|
+
<%= render @chat_messages %>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div id="<%= dom_id(@chat, :signals) %>"
|
|
82
|
+
class="chat-signals"
|
|
83
|
+
data-chat-show-self-signals="<%= ChatGem.configuration.show_self_signals %>"
|
|
84
|
+
data-chat-self-participant-type="<%= current_participant_type %>"
|
|
85
|
+
data-chat-self-participant-id="<%= current_participant_id %>">
|
|
86
|
+
<%= render "chat_gem/chat_messages/signals", chat: @chat %>
|
|
87
|
+
</div>
|
|
88
|
+
</section>
|
|
89
|
+
|
|
90
|
+
<% if @can_post_message %>
|
|
91
|
+
<%= render "chat_gem/chat_messages/form", chat: @chat, chat_message: @chat_message, chat_permission: @chat_permission %>
|
|
92
|
+
<% elsif @chat.closed? %>
|
|
93
|
+
<p class="chat-banner chat-banner--muted">This chat is closed.</p>
|
|
94
|
+
<% end %>
|
|
95
|
+
</section>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>ChatGem</title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
<%= action_cable_meta_tag %>
|
|
8
|
+
<% if respond_to?(:turbo_include_tags) %>
|
|
9
|
+
<%= turbo_include_tags %>
|
|
10
|
+
<% end %>
|
|
11
|
+
<%= stylesheet_link_tag "chat_gem/application", "data-turbo-track": "reload" %>
|
|
12
|
+
<%= javascript_include_tag "chat_gem/shared", "data-turbo-track": "reload", defer: true %>
|
|
13
|
+
<%= javascript_include_tag "chat_gem/messages", "data-turbo-track": "reload", defer: true %>
|
|
14
|
+
<%= javascript_include_tag "chat_gem/realtime", "data-turbo-track": "reload", defer: true %>
|
|
15
|
+
<%= javascript_include_tag "chat_gem/lifecycle_events", "data-turbo-track": "reload", defer: true %>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<%= yield %>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
ChatGem::Engine.routes.draw do
|
|
2
|
+
root to: "chats#index"
|
|
3
|
+
|
|
4
|
+
resources :chats, only: %i[index show new create] do
|
|
5
|
+
member do
|
|
6
|
+
patch :accept
|
|
7
|
+
patch :decline
|
|
8
|
+
patch :leave
|
|
9
|
+
patch :close
|
|
10
|
+
patch :reopen
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
resources :chat_memberships, only: :create
|
|
14
|
+
resources :chat_messages, only: %i[index create update]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class CreateChatGemChatMemberships < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :chat_gem_chat_memberships do |t|
|
|
4
|
+
t.references :chat, null: false, foreign_key: { to_table: :chat_gem_chats }
|
|
5
|
+
t.references :participant, polymorphic: true, null: false
|
|
6
|
+
t.integer :role, null: false, default: 0
|
|
7
|
+
t.boolean :muted, null: false, default: false
|
|
8
|
+
t.datetime :timed_out_until
|
|
9
|
+
t.datetime :removed_at
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :chat_gem_chat_memberships,
|
|
14
|
+
%i[chat_id participant_type participant_id],
|
|
15
|
+
unique: true,
|
|
16
|
+
where: "removed_at IS NULL",
|
|
17
|
+
name: "index_chat_gem_memberships_on_chat_participant_active"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class CreateChatGemChatMessages < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :chat_gem_chat_messages do |t|
|
|
4
|
+
t.references :chat, null: false, foreign_key: { to_table: :chat_gem_chats }
|
|
5
|
+
t.references :participant, polymorphic: true, null: false
|
|
6
|
+
t.text :body
|
|
7
|
+
t.integer :kind, null: false, default: 0
|
|
8
|
+
t.integer :signal_type
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
add_index :chat_gem_chat_messages, %i[chat_id created_at id], name: "index_chat_gem_messages_order"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
module ChatGem
|
|
2
|
+
class Configuration
|
|
3
|
+
DEFAULT_ROLE_DEFINITIONS = {
|
|
4
|
+
"member" => {
|
|
5
|
+
name: "Member",
|
|
6
|
+
rank: 0,
|
|
7
|
+
permissions: %i[view_chat post_message mention_member]
|
|
8
|
+
},
|
|
9
|
+
"moderator" => {
|
|
10
|
+
name: "Moderator",
|
|
11
|
+
rank: 1,
|
|
12
|
+
permissions: %i[view_chat post_message mention_member mention_all mention_role invite_member mute_member timeout_member ban_member delete_message]
|
|
13
|
+
},
|
|
14
|
+
"admin" => {
|
|
15
|
+
name: "Admin",
|
|
16
|
+
rank: 2,
|
|
17
|
+
permissions: %i[view_chat post_message mention_member mention_all mention_role invite_member mute_member timeout_member ban_member delete_message close_chat reopen_chat]
|
|
18
|
+
}
|
|
19
|
+
}.freeze
|
|
20
|
+
DEFAULT_MESSAGE_HTML_TAGS = %w[a b br code em i li ol p pre strong ul].freeze
|
|
21
|
+
DEFAULT_MESSAGE_HTML_ATTRIBUTES = %w[href target rel class].freeze
|
|
22
|
+
DEFAULT_EMOJI_ALIASES = {
|
|
23
|
+
"smile" => "😄",
|
|
24
|
+
"grin" => "😁",
|
|
25
|
+
"laughing" => "😆",
|
|
26
|
+
"blush" => "😊",
|
|
27
|
+
"wink" => "😉",
|
|
28
|
+
"heart" => "❤️",
|
|
29
|
+
"thumbsup" => "👍",
|
|
30
|
+
"+1" => "👍",
|
|
31
|
+
"thumbsdown" => "👎",
|
|
32
|
+
"-1" => "👎",
|
|
33
|
+
"fire" => "🔥",
|
|
34
|
+
"rocket" => "🚀",
|
|
35
|
+
"thinking" => "🤔",
|
|
36
|
+
"tada" => "🎉",
|
|
37
|
+
"wave" => "👋",
|
|
38
|
+
"eyes" => "👀"
|
|
39
|
+
}.freeze
|
|
40
|
+
DEFAULT_BLOCKED_WORDS_SCRAMBLE_CHARS = %w[# * / ( = ) ! 8].freeze
|
|
41
|
+
DEFAULT_BLOCKED_WORDS_ACTION = "reject".freeze
|
|
42
|
+
|
|
43
|
+
attr_accessor :permission_adapter,
|
|
44
|
+
:max_chat_participants,
|
|
45
|
+
:max_message_length,
|
|
46
|
+
:message_history_limit,
|
|
47
|
+
:enable_mentions,
|
|
48
|
+
:mention_filter_exclude_self,
|
|
49
|
+
:mention_filter_hide_roles,
|
|
50
|
+
:enable_emoji_aliases,
|
|
51
|
+
:emoji_aliases,
|
|
52
|
+
:blocked_words,
|
|
53
|
+
:blocked_words_action,
|
|
54
|
+
:blocked_words_scramble_chars,
|
|
55
|
+
:mention_mark_hex_color,
|
|
56
|
+
:mention_highlight_hex_color,
|
|
57
|
+
:own_message_hex_color,
|
|
58
|
+
:other_message_hex_color,
|
|
59
|
+
:role_message_hex_colors,
|
|
60
|
+
:show_timestamp,
|
|
61
|
+
:show_role,
|
|
62
|
+
:active_chat_window,
|
|
63
|
+
:emit_typing_events,
|
|
64
|
+
:emit_message_events,
|
|
65
|
+
:emit_mention_events,
|
|
66
|
+
:emit_invitation_events,
|
|
67
|
+
:emit_chat_lifecycle_events,
|
|
68
|
+
:emit_moderation_events,
|
|
69
|
+
:emit_blocked_words_events,
|
|
70
|
+
:show_self_signals,
|
|
71
|
+
:replace_signals_on_message_submit,
|
|
72
|
+
:message_css_class_resolver,
|
|
73
|
+
:render_message_html,
|
|
74
|
+
:message_html_tags,
|
|
75
|
+
:message_html_attributes,
|
|
76
|
+
:timestamp_formatter,
|
|
77
|
+
:role_formatter
|
|
78
|
+
|
|
79
|
+
def initialize
|
|
80
|
+
@permission_adapter = ChatGem::Permission
|
|
81
|
+
@max_chat_participants = 10
|
|
82
|
+
@max_message_length = 1000
|
|
83
|
+
@message_history_limit = 200
|
|
84
|
+
@enable_mentions = true
|
|
85
|
+
@mention_filter_exclude_self = true
|
|
86
|
+
@mention_filter_hide_roles = true
|
|
87
|
+
@enable_emoji_aliases = true
|
|
88
|
+
@emoji_aliases = DEFAULT_EMOJI_ALIASES.dup
|
|
89
|
+
@blocked_words = []
|
|
90
|
+
@blocked_words_action = DEFAULT_BLOCKED_WORDS_ACTION
|
|
91
|
+
@blocked_words_scramble_chars = DEFAULT_BLOCKED_WORDS_SCRAMBLE_CHARS.dup
|
|
92
|
+
@mention_mark_hex_color = nil
|
|
93
|
+
@mention_highlight_hex_color = nil
|
|
94
|
+
@own_message_hex_color = nil
|
|
95
|
+
@other_message_hex_color = nil
|
|
96
|
+
@role_message_hex_colors = {}
|
|
97
|
+
@show_timestamp = true
|
|
98
|
+
@show_role = false
|
|
99
|
+
@active_chat_window = 5.minutes
|
|
100
|
+
@emit_typing_events = false
|
|
101
|
+
@emit_message_events = false
|
|
102
|
+
@emit_mention_events = false
|
|
103
|
+
@emit_invitation_events = false
|
|
104
|
+
@emit_chat_lifecycle_events = false
|
|
105
|
+
@emit_moderation_events = false
|
|
106
|
+
@emit_blocked_words_events = false
|
|
107
|
+
@show_self_signals = false
|
|
108
|
+
@replace_signals_on_message_submit = false
|
|
109
|
+
@message_css_class_resolver = nil
|
|
110
|
+
@render_message_html = false
|
|
111
|
+
@message_html_tags = DEFAULT_MESSAGE_HTML_TAGS.dup
|
|
112
|
+
@message_html_attributes = DEFAULT_MESSAGE_HTML_ATTRIBUTES.dup
|
|
113
|
+
@additional_roles = {}
|
|
114
|
+
@timestamp_formatter = lambda { |timestamp, _chat_message = nil|
|
|
115
|
+
I18n.l(timestamp.in_time_zone, format: :long)
|
|
116
|
+
}
|
|
117
|
+
@role_formatter = lambda { |role, _chat_message = nil|
|
|
118
|
+
role.to_s.humanize
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def add_role(key, name:, permissions:, rank: 0)
|
|
123
|
+
normalized_key = normalize_role_key(key)
|
|
124
|
+
raise ArgumentError, "Role key cannot be blank" if normalized_key.blank?
|
|
125
|
+
raise ArgumentError, "Role #{normalized_key} is reserved" if DEFAULT_ROLE_DEFINITIONS.key?(normalized_key)
|
|
126
|
+
|
|
127
|
+
role_name = name.to_s.strip
|
|
128
|
+
raise ArgumentError, "Role name cannot be blank" if role_name.blank?
|
|
129
|
+
|
|
130
|
+
@additional_roles[normalized_key] = {
|
|
131
|
+
name: role_name,
|
|
132
|
+
rank: rank.to_i,
|
|
133
|
+
permissions: Array(permissions).map { |permission| permission.to_sym }.uniq
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def remove_role(key)
|
|
138
|
+
@additional_roles.delete(normalize_role_key(key))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def clear_additional_roles!
|
|
142
|
+
@additional_roles = {}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def additional_roles
|
|
146
|
+
@additional_roles.dup
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def role_definitions
|
|
150
|
+
DEFAULT_ROLE_DEFINITIONS.merge(@additional_roles)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def role_definition(key)
|
|
154
|
+
role_definitions[normalize_role_key(key)]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def add_emoji_alias(name, value)
|
|
158
|
+
key = normalize_emoji_alias_key(name)
|
|
159
|
+
raise ArgumentError, "Emoji alias cannot be blank" if key.blank?
|
|
160
|
+
|
|
161
|
+
normalized_value = value.to_s.strip
|
|
162
|
+
raise ArgumentError, "Emoji alias value cannot be blank" if normalized_value.blank?
|
|
163
|
+
|
|
164
|
+
@emoji_aliases = effective_emoji_aliases.merge(key => normalized_value)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def remove_emoji_alias(name)
|
|
168
|
+
key = normalize_emoji_alias_key(name)
|
|
169
|
+
return if key.blank?
|
|
170
|
+
|
|
171
|
+
@emoji_aliases = effective_emoji_aliases.except(key)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def clear_emoji_aliases!
|
|
175
|
+
@emoji_aliases = {}
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def reset_emoji_aliases!
|
|
179
|
+
@emoji_aliases = DEFAULT_EMOJI_ALIASES.dup
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def effective_emoji_aliases
|
|
183
|
+
source = emoji_aliases.is_a?(Hash) ? emoji_aliases : {}
|
|
184
|
+
|
|
185
|
+
source.each_with_object({}) do |(key, value), aliases|
|
|
186
|
+
normalized_key = normalize_emoji_alias_key(key)
|
|
187
|
+
normalized_value = value.to_s.strip
|
|
188
|
+
next if normalized_key.blank? || normalized_value.blank?
|
|
189
|
+
|
|
190
|
+
aliases[normalized_key] = normalized_value
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def effective_blocked_words
|
|
195
|
+
source = blocked_words
|
|
196
|
+
return [] unless source.respond_to?(:each)
|
|
197
|
+
|
|
198
|
+
source.each_with_object([]) do |value, words|
|
|
199
|
+
normalized_word = normalize_blocked_word(value)
|
|
200
|
+
next if normalized_word.blank?
|
|
201
|
+
next if words.include?(normalized_word)
|
|
202
|
+
|
|
203
|
+
words << normalized_word
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def effective_blocked_words_action
|
|
208
|
+
action = blocked_words_action.to_s.strip.downcase
|
|
209
|
+
return DEFAULT_BLOCKED_WORDS_ACTION if action.blank?
|
|
210
|
+
|
|
211
|
+
%w[reject scramble].include?(action) ? action : DEFAULT_BLOCKED_WORDS_ACTION
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def effective_blocked_words_scramble_chars
|
|
215
|
+
source = blocked_words_scramble_chars
|
|
216
|
+
return DEFAULT_BLOCKED_WORDS_SCRAMBLE_CHARS.dup unless source.respond_to?(:each)
|
|
217
|
+
|
|
218
|
+
chars = source.each_with_object([]) do |value, result|
|
|
219
|
+
normalized_char = value.to_s
|
|
220
|
+
next if normalized_char.blank?
|
|
221
|
+
|
|
222
|
+
result << normalized_char
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
chars.presence || DEFAULT_BLOCKED_WORDS_SCRAMBLE_CHARS.dup
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
private
|
|
229
|
+
|
|
230
|
+
def normalize_role_key(key)
|
|
231
|
+
key.to_s.strip
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def normalize_emoji_alias_key(key)
|
|
235
|
+
key.to_s.strip.downcase
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def normalize_blocked_word(word)
|
|
239
|
+
word.to_s.strip.downcase
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module ChatGem
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
isolate_namespace ChatGem
|
|
4
|
+
|
|
5
|
+
initializer "chat_gem.helpers" do
|
|
6
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
7
|
+
helper ChatGem::ApplicationHelper
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
ActiveSupport.on_load(:action_view) do
|
|
11
|
+
include ChatGem::ApplicationHelper
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
initializer "chat_gem.assets.precompile" do |app|
|
|
16
|
+
next unless app.config.respond_to?(:assets)
|
|
17
|
+
next unless app.config.assets.respond_to?(:precompile)
|
|
18
|
+
|
|
19
|
+
app.config.assets.precompile += %w[
|
|
20
|
+
chat_gem/application.css
|
|
21
|
+
chat_gem/application.js
|
|
22
|
+
chat_gem/shared.js
|
|
23
|
+
chat_gem/messages.js
|
|
24
|
+
chat_gem/realtime.js
|
|
25
|
+
chat_gem/lifecycle_events.js
|
|
26
|
+
]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module ChatGem
|
|
2
|
+
module ModelExtensions
|
|
3
|
+
module ChatParticipant
|
|
4
|
+
module ClassMethods
|
|
5
|
+
def acts_as_chat_participant
|
|
6
|
+
return if reflect_on_association(:chat_memberships).present?
|
|
7
|
+
|
|
8
|
+
has_many :chat_memberships,
|
|
9
|
+
-> { order(created_at: :asc, id: :asc) },
|
|
10
|
+
as: :participant,
|
|
11
|
+
class_name: "ChatGem::ChatMembership",
|
|
12
|
+
dependent: :destroy
|
|
13
|
+
|
|
14
|
+
has_many :chat_messages,
|
|
15
|
+
-> { order(created_at: :asc, id: :asc) },
|
|
16
|
+
as: :participant,
|
|
17
|
+
class_name: "ChatGem::ChatMessage",
|
|
18
|
+
dependent: :destroy
|
|
19
|
+
|
|
20
|
+
has_many :chats,
|
|
21
|
+
-> { distinct },
|
|
22
|
+
through: :chat_memberships,
|
|
23
|
+
source: :chat,
|
|
24
|
+
class_name: "ChatGem::Chat"
|
|
25
|
+
|
|
26
|
+
include ChatGem::ModelExtensions::ChatParticipant::InstanceMethods
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
module InstanceMethods
|
|
31
|
+
def active_chat_memberships
|
|
32
|
+
chat_memberships.active
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def joined_chat?(chat)
|
|
36
|
+
active_chat_memberships.exists?(chat_id: chat.id)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
ActiveSupport.on_load(:active_record) do
|
|
44
|
+
extend ChatGem::ModelExtensions::ChatParticipant::ClassMethods
|
|
45
|
+
end
|