livechat 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/CHANGELOG.md +11 -0
- data/MIT-LICENSE +20 -0
- data/README.md +160 -0
- data/Rakefile +11 -0
- data/app/controllers/livechat/application_controller.rb +76 -0
- data/app/controllers/livechat/conversations_controller.rb +57 -0
- data/app/controllers/livechat/messages_controller.rb +21 -0
- data/app/controllers/livechat/visitor_controller.rb +108 -0
- data/app/controllers/livechat/widgets_controller.rb +36 -0
- data/app/helpers/livechat/widget_helper.rb +37 -0
- data/app/mailers/livechat/mailer.rb +51 -0
- data/app/models/livechat/application_record.rb +7 -0
- data/app/models/livechat/conversation.rb +108 -0
- data/app/models/livechat/message.rb +53 -0
- data/app/views/layouts/livechat/application.html.erb +97 -0
- data/app/views/livechat/conversations/index.html.erb +62 -0
- data/app/views/livechat/conversations/show.html.erb +73 -0
- data/app/views/livechat/mailer/new_agent_reply.text.erb +7 -0
- data/app/views/livechat/mailer/new_visitor_message.text.erb +7 -0
- data/config/locales/livechat.ar.yml +44 -0
- data/config/locales/livechat.bg.yml +44 -0
- data/config/locales/livechat.bn.yml +44 -0
- data/config/locales/livechat.de.yml +44 -0
- data/config/locales/livechat.el.yml +44 -0
- data/config/locales/livechat.en.yml +44 -0
- data/config/locales/livechat.es.yml +44 -0
- data/config/locales/livechat.fr.yml +44 -0
- data/config/locales/livechat.hi.yml +44 -0
- data/config/locales/livechat.hr.yml +44 -0
- data/config/locales/livechat.id.yml +44 -0
- data/config/locales/livechat.it.yml +44 -0
- data/config/locales/livechat.ja.yml +44 -0
- data/config/locales/livechat.ko.yml +44 -0
- data/config/locales/livechat.lb.yml +44 -0
- data/config/locales/livechat.nl.yml +44 -0
- data/config/locales/livechat.pl.yml +44 -0
- data/config/locales/livechat.pt.yml +44 -0
- data/config/locales/livechat.ro.yml +44 -0
- data/config/locales/livechat.ru.yml +44 -0
- data/config/locales/livechat.th.yml +44 -0
- data/config/locales/livechat.tr.yml +44 -0
- data/config/locales/livechat.uk.yml +44 -0
- data/config/locales/livechat.ur.yml +44 -0
- data/config/locales/livechat.vi.yml +44 -0
- data/config/locales/livechat.zh-CN.yml +44 -0
- data/config/routes.rb +27 -0
- data/lib/generators/livechat/install/install_generator.rb +42 -0
- data/lib/generators/livechat/install/templates/create_livechat_tables.rb.tt +36 -0
- data/lib/generators/livechat/install/templates/initializer.rb +59 -0
- data/lib/livechat/configuration.rb +103 -0
- data/lib/livechat/dashboard.js +58 -0
- data/lib/livechat/engine.rb +13 -0
- data/lib/livechat/notifications.rb +41 -0
- data/lib/livechat/version.rb +5 -0
- data/lib/livechat/widget.js +563 -0
- data/lib/livechat/widget.rb +115 -0
- data/lib/livechat.rb +56 -0
- metadata +125 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
ru:
|
|
2
|
+
livechat:
|
|
3
|
+
launcher: "Напишите нам"
|
|
4
|
+
greeting: "Здравствуйте! Чем можем помочь?"
|
|
5
|
+
reply_time: "Обычно мы отвечаем в течение нескольких часов."
|
|
6
|
+
placeholder: "Напишите сообщение…"
|
|
7
|
+
send: "Отправить"
|
|
8
|
+
close: "Закрыть"
|
|
9
|
+
you: "Вы"
|
|
10
|
+
team: "Поддержка"
|
|
11
|
+
email_prompt: "Получите копию нашего ответа по электронной почте:"
|
|
12
|
+
email_placeholder: "you@example.com"
|
|
13
|
+
email_save: "Сохранить"
|
|
14
|
+
email_saved: "Мы также ответим по электронной почте."
|
|
15
|
+
event_resolved: "Диалог завершён"
|
|
16
|
+
event_reopened: "Диалог возобновлён"
|
|
17
|
+
error_send: "Не удалось отправить. Пожалуйста, попробуйте ещё раз."
|
|
18
|
+
error_rate_limited: "Слишком много сообщений. Подождите немного и попробуйте ещё раз."
|
|
19
|
+
unread_aria: "непрочитанные сообщения"
|
|
20
|
+
statuses:
|
|
21
|
+
open: "Открыт"
|
|
22
|
+
resolved: "Завершён"
|
|
23
|
+
events:
|
|
24
|
+
resolved: "%{agent} завершил(а) диалог"
|
|
25
|
+
reopened: "%{agent} возобновил(а) диалог"
|
|
26
|
+
dashboard:
|
|
27
|
+
title: "Чат"
|
|
28
|
+
empty: "Пока здесь пусто. Когда посетитель напишет, диалог появится на этой странице."
|
|
29
|
+
visitor: "Посетитель"
|
|
30
|
+
last_message: "Последнее сообщение"
|
|
31
|
+
updated: "Обновлено"
|
|
32
|
+
back: "Все диалоги"
|
|
33
|
+
resolve: "Завершить"
|
|
34
|
+
reopen: "Возобновить"
|
|
35
|
+
reply_placeholder: "Напишите ответ…"
|
|
36
|
+
send: "Отправить"
|
|
37
|
+
new_messages: "Новые сообщения — обновите"
|
|
38
|
+
newer: "Новее"
|
|
39
|
+
older: "Старше"
|
|
40
|
+
mail:
|
|
41
|
+
visitor_subject: "%{name} написал(а) вам в %{app}"
|
|
42
|
+
reply_subject: "%{name} ответил(а) вам в %{app}"
|
|
43
|
+
answer: "Ответить"
|
|
44
|
+
continue: "Продолжить диалог"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
th:
|
|
2
|
+
livechat:
|
|
3
|
+
launcher: "แชทกับเรา"
|
|
4
|
+
greeting: "สวัสดี! เราช่วยอะไรคุณได้บ้าง"
|
|
5
|
+
reply_time: "โดยปกติเราจะตอบกลับภายในไม่กี่ชั่วโมง"
|
|
6
|
+
placeholder: "เขียนข้อความ…"
|
|
7
|
+
send: "ส่ง"
|
|
8
|
+
close: "ปิด"
|
|
9
|
+
you: "คุณ"
|
|
10
|
+
team: "ฝ่ายสนับสนุน"
|
|
11
|
+
email_prompt: "รับสำเนาคำตอบของเราทางอีเมล:"
|
|
12
|
+
email_placeholder: "you@example.com"
|
|
13
|
+
email_save: "บันทึก"
|
|
14
|
+
email_saved: "เราจะตอบกลับทางอีเมลด้วย"
|
|
15
|
+
event_resolved: "การสนทนาได้รับการแก้ไขแล้ว"
|
|
16
|
+
event_reopened: "เปิดการสนทนาอีกครั้งแล้ว"
|
|
17
|
+
error_send: "ส่งไม่สำเร็จ โปรดลองอีกครั้ง"
|
|
18
|
+
error_rate_limited: "ข้อความมากเกินไป โปรดรอสักครู่แล้วลองอีกครั้ง"
|
|
19
|
+
unread_aria: "ข้อความที่ยังไม่ได้อ่าน"
|
|
20
|
+
statuses:
|
|
21
|
+
open: "เปิดอยู่"
|
|
22
|
+
resolved: "แก้ไขแล้ว"
|
|
23
|
+
events:
|
|
24
|
+
resolved: "%{agent} แก้ไขการสนทนาแล้ว"
|
|
25
|
+
reopened: "%{agent} เปิดการสนทนาอีกครั้ง"
|
|
26
|
+
dashboard:
|
|
27
|
+
title: "แชท"
|
|
28
|
+
empty: "ยังไม่มีอะไรที่นี่ เมื่อผู้เยี่ยมชมส่งข้อความ การสนทนาจะปรากฏบนหน้านี้"
|
|
29
|
+
visitor: "ผู้เยี่ยมชม"
|
|
30
|
+
last_message: "ข้อความล่าสุด"
|
|
31
|
+
updated: "อัปเดตเมื่อ"
|
|
32
|
+
back: "การสนทนาทั้งหมด"
|
|
33
|
+
resolve: "แก้ไขเสร็จสิ้น"
|
|
34
|
+
reopen: "เปิดอีกครั้ง"
|
|
35
|
+
reply_placeholder: "เขียนคำตอบของคุณ…"
|
|
36
|
+
send: "ส่ง"
|
|
37
|
+
new_messages: "มีข้อความใหม่ — รีเฟรช"
|
|
38
|
+
newer: "ใหม่กว่า"
|
|
39
|
+
older: "เก่ากว่า"
|
|
40
|
+
mail:
|
|
41
|
+
visitor_subject: "%{name} เขียนถึงคุณบน %{app}"
|
|
42
|
+
reply_subject: "%{name} ตอบกลับคุณบน %{app}"
|
|
43
|
+
answer: "ตอบกลับ"
|
|
44
|
+
continue: "สนทนาต่อ"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
tr:
|
|
2
|
+
livechat:
|
|
3
|
+
launcher: "Bizimle sohbet edin"
|
|
4
|
+
greeting: "Merhaba! Size nasıl yardımcı olabiliriz?"
|
|
5
|
+
reply_time: "Genellikle birkaç saat içinde yanıtlıyoruz."
|
|
6
|
+
placeholder: "Bir mesaj yazın…"
|
|
7
|
+
send: "Gönder"
|
|
8
|
+
close: "Kapat"
|
|
9
|
+
you: "Siz"
|
|
10
|
+
team: "Destek"
|
|
11
|
+
email_prompt: "Yanıtımızın bir kopyasını e-postayla alın:"
|
|
12
|
+
email_placeholder: "siz@ornek.com"
|
|
13
|
+
email_save: "Kaydet"
|
|
14
|
+
email_saved: "Ayrıca e-postayla da yanıtlayacağız."
|
|
15
|
+
event_resolved: "Görüşme çözüldü"
|
|
16
|
+
event_reopened: "Görüşme yeniden açıldı"
|
|
17
|
+
error_send: "Gönderilemedi. Lütfen tekrar deneyin."
|
|
18
|
+
error_rate_limited: "Çok fazla mesaj. Lütfen biraz bekleyip tekrar deneyin."
|
|
19
|
+
unread_aria: "okunmamış mesaj"
|
|
20
|
+
statuses:
|
|
21
|
+
open: "Açık"
|
|
22
|
+
resolved: "Çözüldü"
|
|
23
|
+
events:
|
|
24
|
+
resolved: "%{agent} görüşmeyi çözdü"
|
|
25
|
+
reopened: "%{agent} görüşmeyi yeniden açtı"
|
|
26
|
+
dashboard:
|
|
27
|
+
title: "Sohbet"
|
|
28
|
+
empty: "Henüz burada bir şey yok. Bir ziyaretçi yazdığında görüşme bu sayfada görünür."
|
|
29
|
+
visitor: "Ziyaretçi"
|
|
30
|
+
last_message: "Son mesaj"
|
|
31
|
+
updated: "Güncellendi"
|
|
32
|
+
back: "Tüm görüşmeler"
|
|
33
|
+
resolve: "Çöz"
|
|
34
|
+
reopen: "Yeniden aç"
|
|
35
|
+
reply_placeholder: "Yanıtınızı yazın…"
|
|
36
|
+
send: "Gönder"
|
|
37
|
+
new_messages: "Yeni mesajlar — yenileyin"
|
|
38
|
+
newer: "Daha yeni"
|
|
39
|
+
older: "Daha eski"
|
|
40
|
+
mail:
|
|
41
|
+
visitor_subject: "%{name}, %{app} üzerinden size yazdı"
|
|
42
|
+
reply_subject: "%{name}, %{app} üzerinden size yanıt verdi"
|
|
43
|
+
answer: "Yanıtla"
|
|
44
|
+
continue: "Görüşmeye devam et"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
uk:
|
|
2
|
+
livechat:
|
|
3
|
+
launcher: "Напишіть нам"
|
|
4
|
+
greeting: "Вітаємо! Чим можемо допомогти?"
|
|
5
|
+
reply_time: "Зазвичай ми відповідаємо протягом кількох годин."
|
|
6
|
+
placeholder: "Напишіть повідомлення…"
|
|
7
|
+
send: "Надіслати"
|
|
8
|
+
close: "Закрити"
|
|
9
|
+
you: "Ви"
|
|
10
|
+
team: "Підтримка"
|
|
11
|
+
email_prompt: "Отримайте копію нашої відповіді електронною поштою:"
|
|
12
|
+
email_placeholder: "you@example.com"
|
|
13
|
+
email_save: "Зберегти"
|
|
14
|
+
email_saved: "Ми також відповімо електронною поштою."
|
|
15
|
+
event_resolved: "Розмову завершено"
|
|
16
|
+
event_reopened: "Розмову відновлено"
|
|
17
|
+
error_send: "Не вдалося надіслати. Будь ласка, спробуйте ще раз."
|
|
18
|
+
error_rate_limited: "Забагато повідомлень. Зачекайте трохи та спробуйте ще раз."
|
|
19
|
+
unread_aria: "непрочитані повідомлення"
|
|
20
|
+
statuses:
|
|
21
|
+
open: "Відкрита"
|
|
22
|
+
resolved: "Завершена"
|
|
23
|
+
events:
|
|
24
|
+
resolved: "%{agent} завершив(ла) розмову"
|
|
25
|
+
reopened: "%{agent} відновив(ла) розмову"
|
|
26
|
+
dashboard:
|
|
27
|
+
title: "Чат"
|
|
28
|
+
empty: "Поки що тут порожньо. Коли відвідувач напише, розмова з'явиться на цій сторінці."
|
|
29
|
+
visitor: "Відвідувач"
|
|
30
|
+
last_message: "Останнє повідомлення"
|
|
31
|
+
updated: "Оновлено"
|
|
32
|
+
back: "Усі розмови"
|
|
33
|
+
resolve: "Завершити"
|
|
34
|
+
reopen: "Відновити"
|
|
35
|
+
reply_placeholder: "Напишіть відповідь…"
|
|
36
|
+
send: "Надіслати"
|
|
37
|
+
new_messages: "Нові повідомлення — оновіть"
|
|
38
|
+
newer: "Новіші"
|
|
39
|
+
older: "Старіші"
|
|
40
|
+
mail:
|
|
41
|
+
visitor_subject: "%{name} написав(ла) вам у %{app}"
|
|
42
|
+
reply_subject: "%{name} відповів(ла) вам у %{app}"
|
|
43
|
+
answer: "Відповісти"
|
|
44
|
+
continue: "Продовжити розмову"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
ur:
|
|
2
|
+
livechat:
|
|
3
|
+
launcher: "ہم سے چیٹ کریں"
|
|
4
|
+
greeting: "ہیلو! ہم آپ کی کیا مدد کر سکتے ہیں؟"
|
|
5
|
+
reply_time: "ہم عام طور پر چند گھنٹوں میں جواب دیتے ہیں۔"
|
|
6
|
+
placeholder: "پیغام لکھیں…"
|
|
7
|
+
send: "بھیجیں"
|
|
8
|
+
close: "بند کریں"
|
|
9
|
+
you: "آپ"
|
|
10
|
+
team: "سپورٹ"
|
|
11
|
+
email_prompt: "ہمارے جواب کی ایک نقل ای میل پر حاصل کریں:"
|
|
12
|
+
email_placeholder: "you@example.com"
|
|
13
|
+
email_save: "محفوظ کریں"
|
|
14
|
+
email_saved: "ہم ای میل پر بھی جواب دیں گے۔"
|
|
15
|
+
event_resolved: "گفتگو حل ہو گئی"
|
|
16
|
+
event_reopened: "گفتگو دوبارہ کھول دی گئی"
|
|
17
|
+
error_send: "بھیجا نہیں جا سکا۔ براہ کرم دوبارہ کوشش کریں۔"
|
|
18
|
+
error_rate_limited: "بہت زیادہ پیغامات۔ براہ کرم تھوڑا انتظار کریں اور دوبارہ کوشش کریں۔"
|
|
19
|
+
unread_aria: "غیر پڑھے گئے پیغامات"
|
|
20
|
+
statuses:
|
|
21
|
+
open: "کھلی"
|
|
22
|
+
resolved: "حل شدہ"
|
|
23
|
+
events:
|
|
24
|
+
resolved: "%{agent} نے گفتگو حل کر دی"
|
|
25
|
+
reopened: "%{agent} نے گفتگو دوبارہ کھول دی"
|
|
26
|
+
dashboard:
|
|
27
|
+
title: "چیٹ"
|
|
28
|
+
empty: "یہاں ابھی کچھ نہیں ہے۔ جب کوئی وزیٹر لکھے گا تو گفتگو اس صفحے پر نظر آئے گی۔"
|
|
29
|
+
visitor: "وزیٹر"
|
|
30
|
+
last_message: "آخری پیغام"
|
|
31
|
+
updated: "اپ ڈیٹ"
|
|
32
|
+
back: "تمام گفتگوئیں"
|
|
33
|
+
resolve: "حل کریں"
|
|
34
|
+
reopen: "دوبارہ کھولیں"
|
|
35
|
+
reply_placeholder: "اپنا جواب لکھیں…"
|
|
36
|
+
send: "بھیجیں"
|
|
37
|
+
new_messages: "نئے پیغامات — ریفریش کریں"
|
|
38
|
+
newer: "نئے"
|
|
39
|
+
older: "پرانے"
|
|
40
|
+
mail:
|
|
41
|
+
visitor_subject: "%{name} نے آپ کو %{app} پر لکھا"
|
|
42
|
+
reply_subject: "%{name} نے آپ کو %{app} پر جواب دیا"
|
|
43
|
+
answer: "جواب دیں"
|
|
44
|
+
continue: "گفتگو جاری رکھیں"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
vi:
|
|
2
|
+
livechat:
|
|
3
|
+
launcher: "Trò chuyện với chúng tôi"
|
|
4
|
+
greeting: "Xin chào! Chúng tôi có thể giúp gì cho bạn?"
|
|
5
|
+
reply_time: "Chúng tôi thường trả lời trong vòng vài giờ."
|
|
6
|
+
placeholder: "Viết tin nhắn…"
|
|
7
|
+
send: "Gửi"
|
|
8
|
+
close: "Đóng"
|
|
9
|
+
you: "Bạn"
|
|
10
|
+
team: "Hỗ trợ"
|
|
11
|
+
email_prompt: "Nhận bản sao câu trả lời của chúng tôi qua email:"
|
|
12
|
+
email_placeholder: "you@example.com"
|
|
13
|
+
email_save: "Lưu"
|
|
14
|
+
email_saved: "Chúng tôi cũng sẽ trả lời qua email."
|
|
15
|
+
event_resolved: "Cuộc trò chuyện đã được giải quyết"
|
|
16
|
+
event_reopened: "Cuộc trò chuyện đã được mở lại"
|
|
17
|
+
error_send: "Không thể gửi. Vui lòng thử lại."
|
|
18
|
+
error_rate_limited: "Quá nhiều tin nhắn. Vui lòng đợi một lát rồi thử lại."
|
|
19
|
+
unread_aria: "tin nhắn chưa đọc"
|
|
20
|
+
statuses:
|
|
21
|
+
open: "Đang mở"
|
|
22
|
+
resolved: "Đã giải quyết"
|
|
23
|
+
events:
|
|
24
|
+
resolved: "%{agent} đã giải quyết cuộc trò chuyện"
|
|
25
|
+
reopened: "%{agent} đã mở lại cuộc trò chuyện"
|
|
26
|
+
dashboard:
|
|
27
|
+
title: "Trò chuyện"
|
|
28
|
+
empty: "Chưa có gì ở đây. Khi khách truy cập nhắn tin, cuộc trò chuyện sẽ xuất hiện trên trang này."
|
|
29
|
+
visitor: "Khách truy cập"
|
|
30
|
+
last_message: "Tin nhắn cuối"
|
|
31
|
+
updated: "Đã cập nhật"
|
|
32
|
+
back: "Tất cả cuộc trò chuyện"
|
|
33
|
+
resolve: "Giải quyết"
|
|
34
|
+
reopen: "Mở lại"
|
|
35
|
+
reply_placeholder: "Viết câu trả lời của bạn…"
|
|
36
|
+
send: "Gửi"
|
|
37
|
+
new_messages: "Tin nhắn mới — làm mới"
|
|
38
|
+
newer: "Mới hơn"
|
|
39
|
+
older: "Cũ hơn"
|
|
40
|
+
mail:
|
|
41
|
+
visitor_subject: "%{name} đã nhắn tin cho bạn trên %{app}"
|
|
42
|
+
reply_subject: "%{name} đã trả lời bạn trên %{app}"
|
|
43
|
+
answer: "Trả lời"
|
|
44
|
+
continue: "Tiếp tục cuộc trò chuyện"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"zh-CN":
|
|
2
|
+
livechat:
|
|
3
|
+
launcher: "与我们聊天"
|
|
4
|
+
greeting: "您好!有什么可以帮您?"
|
|
5
|
+
reply_time: "我们通常会在几小时内回复。"
|
|
6
|
+
placeholder: "输入消息…"
|
|
7
|
+
send: "发送"
|
|
8
|
+
close: "关闭"
|
|
9
|
+
you: "您"
|
|
10
|
+
team: "客服"
|
|
11
|
+
email_prompt: "通过邮箱接收我们回复的副本:"
|
|
12
|
+
email_placeholder: "you@example.com"
|
|
13
|
+
email_save: "保存"
|
|
14
|
+
email_saved: "我们也会通过邮件回复您。"
|
|
15
|
+
event_resolved: "对话已解决"
|
|
16
|
+
event_reopened: "对话已重新打开"
|
|
17
|
+
error_send: "发送失败,请重试。"
|
|
18
|
+
error_rate_limited: "消息过多,请稍等片刻再重试。"
|
|
19
|
+
unread_aria: "条未读消息"
|
|
20
|
+
statuses:
|
|
21
|
+
open: "进行中"
|
|
22
|
+
resolved: "已解决"
|
|
23
|
+
events:
|
|
24
|
+
resolved: "%{agent} 已解决该对话"
|
|
25
|
+
reopened: "%{agent} 重新打开了该对话"
|
|
26
|
+
dashboard:
|
|
27
|
+
title: "聊天"
|
|
28
|
+
empty: "这里还没有内容。当访客发来消息时,对话会显示在此页面。"
|
|
29
|
+
visitor: "访客"
|
|
30
|
+
last_message: "最后一条消息"
|
|
31
|
+
updated: "更新时间"
|
|
32
|
+
back: "所有对话"
|
|
33
|
+
resolve: "解决"
|
|
34
|
+
reopen: "重新打开"
|
|
35
|
+
reply_placeholder: "输入您的回复…"
|
|
36
|
+
send: "发送"
|
|
37
|
+
new_messages: "有新消息 — 请刷新"
|
|
38
|
+
newer: "较新"
|
|
39
|
+
older: "较旧"
|
|
40
|
+
mail:
|
|
41
|
+
visitor_subject: "%{name} 在 %{app} 上给您留言"
|
|
42
|
+
reply_subject: "%{name} 在 %{app} 上回复了您"
|
|
43
|
+
answer: "回复"
|
|
44
|
+
continue: "继续对话"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Livechat::Engine.routes.draw do
|
|
4
|
+
get 'widget.js', to: 'widgets#show', as: :widget
|
|
5
|
+
get 'dashboard.js', to: 'widgets#dashboard', as: :dashboard_script
|
|
6
|
+
|
|
7
|
+
# The widget's API. Everything is scoped to the requesting visitor —
|
|
8
|
+
# signed-in id or guest cookie — never to an enumerable conversation id.
|
|
9
|
+
scope :widget, as: :widget do
|
|
10
|
+
get 'conversation', to: 'visitor#show'
|
|
11
|
+
post 'messages', to: 'visitor#create'
|
|
12
|
+
post 'email', to: 'visitor#email'
|
|
13
|
+
post 'read', to: 'visitor#read'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The inbox. Flat, human URLs: the mount path IS the conversation list.
|
|
17
|
+
resources :conversations, path: '', only: %i[index show], constraints: { id: /\d+/ } do
|
|
18
|
+
member do
|
|
19
|
+
get :poll
|
|
20
|
+
post :resolve
|
|
21
|
+
post :reopen
|
|
22
|
+
end
|
|
23
|
+
resources :messages, only: :create
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
root to: 'conversations#index'
|
|
27
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/active_record'
|
|
5
|
+
|
|
6
|
+
module Livechat
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path('templates', __dir__)
|
|
12
|
+
|
|
13
|
+
desc 'Installs livechat: config initializer, migration, and engine mount.'
|
|
14
|
+
|
|
15
|
+
def create_initializer
|
|
16
|
+
copy_file 'initializer.rb', 'config/initializers/livechat.rb'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create_migration_file
|
|
20
|
+
migration_template 'create_livechat_tables.rb.tt',
|
|
21
|
+
'db/migrate/create_livechat_tables.rb'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def mount_engine
|
|
25
|
+
route %(mount Livechat::Engine => "/livechat")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def post_install
|
|
29
|
+
say "\nlivechat installed. Run `rails db:migrate`, then add", :green
|
|
30
|
+
say '`<%= livechat_tag %>` before </body> in your layout.'
|
|
31
|
+
say 'Answer visitors at /livechat (development only until you set config.authorize_agent).'
|
|
32
|
+
say "Set config.mailer_from + config.agent_emails to hear about new messages by email.\n"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def migration_version
|
|
38
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateLivechatTables < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
def change
|
|
5
|
+
create_table :livechat_conversations do |t|
|
|
6
|
+
t.string :visitor_token
|
|
7
|
+
t.string :visitor_id
|
|
8
|
+
t.string :visitor_label
|
|
9
|
+
t.string :visitor_email
|
|
10
|
+
t.string :status, null: false, default: 'open'
|
|
11
|
+
t.string :last_message_preview
|
|
12
|
+
t.datetime :last_activity_at
|
|
13
|
+
t.string :page_url
|
|
14
|
+
t.string :locale
|
|
15
|
+
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
add_index :livechat_conversations, :visitor_token
|
|
19
|
+
add_index :livechat_conversations, :visitor_id
|
|
20
|
+
add_index :livechat_conversations, :status
|
|
21
|
+
add_index :livechat_conversations, :last_activity_at
|
|
22
|
+
|
|
23
|
+
create_table :livechat_messages do |t|
|
|
24
|
+
t.references :conversation, null: false
|
|
25
|
+
t.string :author_type, null: false
|
|
26
|
+
t.string :agent_id
|
|
27
|
+
t.string :agent_label
|
|
28
|
+
t.text :body
|
|
29
|
+
t.string :event
|
|
30
|
+
t.datetime :read_at
|
|
31
|
+
|
|
32
|
+
t.timestamps
|
|
33
|
+
end
|
|
34
|
+
add_index :livechat_messages, %i[conversation_id id]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Livechat.configure do |config|
|
|
4
|
+
# Shown in the widget header and in notification emails. Defaults to your
|
|
5
|
+
# Rails application name.
|
|
6
|
+
# config.app_name = "My App"
|
|
7
|
+
|
|
8
|
+
# Who sees the widget and can write. Return false to hide and reject for
|
|
9
|
+
# this request. Defaults to everyone.
|
|
10
|
+
# config.enabled = ->(request) { true }
|
|
11
|
+
|
|
12
|
+
# Who can answer at the mount path. Defaults to development only — override
|
|
13
|
+
# before deploying.
|
|
14
|
+
# config.authorize_agent = ->(request) { request.env["warden"]&.user&.admin? }
|
|
15
|
+
|
|
16
|
+
# Resolve the current user (optional). Return an object responding to #id,
|
|
17
|
+
# or nil. Signed-in visitors keep one conversation across devices; guests
|
|
18
|
+
# are tracked with a cookie. The same user is the agent when replying.
|
|
19
|
+
# config.current_user = ->(request) { request.env["warden"]&.user }
|
|
20
|
+
|
|
21
|
+
# How visitors appear in the inbox.
|
|
22
|
+
# config.visitor_label = ->(user) { user.name.presence || user.email }
|
|
23
|
+
|
|
24
|
+
# How agents are attributed on their replies.
|
|
25
|
+
# config.agent_label = ->(user) { user.name.presence || user.email }
|
|
26
|
+
|
|
27
|
+
# What visitors see as the sender. Default: the agent_label unchanged.
|
|
28
|
+
# Return a first name, or a constant to keep the team anonymous.
|
|
29
|
+
# config.agent_display_name = ->(label) { label.split.first }
|
|
30
|
+
|
|
31
|
+
# Widget copy. nil = localized defaults.
|
|
32
|
+
# config.greeting = "Hi! How can we help?"
|
|
33
|
+
# config.reply_time_text = "We usually reply within a few hours."
|
|
34
|
+
# config.launcher_label = "Chat with us"
|
|
35
|
+
|
|
36
|
+
# Brand color (hex) for the launcher, header, bubbles and send button.
|
|
37
|
+
# Text flips black/white automatically for contrast. nil = built-in blue.
|
|
38
|
+
# config.accent_color = "#7c3aed"
|
|
39
|
+
|
|
40
|
+
# The floating bubble. Set false and open the widget from your own
|
|
41
|
+
# elements carrying data-livechat-open, or window.Livechat.open().
|
|
42
|
+
# config.show_launcher = true
|
|
43
|
+
|
|
44
|
+
# Email the team when a visitor writes and nobody has read it — one email
|
|
45
|
+
# per unread stretch, not one per message. Both settings are required.
|
|
46
|
+
# config.mailer_from = "chat@example.com"
|
|
47
|
+
# config.agent_emails = ["support@example.com"]
|
|
48
|
+
# config.agent_emails = -> { User.where(admin: true).pluck(:email) }
|
|
49
|
+
|
|
50
|
+
# Called with each saved Livechat::Message — notify Slack, push…
|
|
51
|
+
# config.on_visitor_message = ->(message) {}
|
|
52
|
+
# config.on_agent_message = ->(message) {}
|
|
53
|
+
|
|
54
|
+
# Per-IP throttle for the widget endpoints (Rails 7.2+; ignored on 7.1).
|
|
55
|
+
# config.rate_limit = { to: 30, within: 1.minute }
|
|
56
|
+
|
|
57
|
+
# Keep in sync with the `mount` in config/routes.rb.
|
|
58
|
+
# config.mount_path = "/livechat"
|
|
59
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Livechat
|
|
4
|
+
# Host-tunable settings. Everything has a safe default, so a fresh install
|
|
5
|
+
# works with zero configuration; the hooks below let an app decide who can
|
|
6
|
+
# chat, who answers, and how everyone is named.
|
|
7
|
+
class Configuration
|
|
8
|
+
# Shown in the widget header and in notification emails. nil resolves to
|
|
9
|
+
# the Rails application name.
|
|
10
|
+
attr_accessor :app_name
|
|
11
|
+
|
|
12
|
+
# Per-request gate for the widget and its endpoints. Return false to hide
|
|
13
|
+
# the widget and reject writes for this request.
|
|
14
|
+
attr_accessor :enabled
|
|
15
|
+
|
|
16
|
+
# Per-request gate for the inbox. Defaults to development only — override
|
|
17
|
+
# it before deploying, e.g. with an admin check. Independent of `enabled`,
|
|
18
|
+
# so your team can answer from production even where the widget is off.
|
|
19
|
+
attr_accessor :authorize_agent
|
|
20
|
+
|
|
21
|
+
# Resolve the current user (optional). Return an object responding to
|
|
22
|
+
# #id, or nil. Receives the request. Signed-in users keep one conversation
|
|
23
|
+
# across devices; guests are tracked with a cookie.
|
|
24
|
+
attr_accessor :current_user
|
|
25
|
+
|
|
26
|
+
# Turn a resolved user into the visitor name shown in the inbox.
|
|
27
|
+
attr_accessor :visitor_label
|
|
28
|
+
|
|
29
|
+
# Turn a resolved user into the attribution stored on an agent's message.
|
|
30
|
+
# Every reply carries this, so a thread with several teammates stays
|
|
31
|
+
# legible.
|
|
32
|
+
attr_accessor :agent_label
|
|
33
|
+
|
|
34
|
+
# What visitors see as the sender of an agent message. Receives the
|
|
35
|
+
# stored agent_label; return it unchanged (default), a first name, or a
|
|
36
|
+
# constant like "Support team" to keep agents anonymous.
|
|
37
|
+
attr_accessor :agent_display_name
|
|
38
|
+
|
|
39
|
+
# Widget copy. nil uses the localized defaults. reply_time_text is the
|
|
40
|
+
# honest line under the greeting — "We usually reply within a few hours."
|
|
41
|
+
attr_accessor :greeting, :reply_time_text, :launcher_label
|
|
42
|
+
|
|
43
|
+
# Brand color for the widget (launcher, header, visitor bubbles, send
|
|
44
|
+
# button) as a hex value, e.g. "#7c3aed". The widget picks black or white
|
|
45
|
+
# text automatically for contrast. nil keeps the built-in blue.
|
|
46
|
+
attr_accessor :accent_color
|
|
47
|
+
|
|
48
|
+
# The floating launcher bubble. Set false to open the widget only from
|
|
49
|
+
# your own elements carrying `data-livechat-open`.
|
|
50
|
+
attr_accessor :show_launcher
|
|
51
|
+
|
|
52
|
+
# Email addresses to notify when a visitor writes and nobody has read it —
|
|
53
|
+
# an array, or a callable returning one. nil disables the built-in email.
|
|
54
|
+
attr_accessor :agent_emails
|
|
55
|
+
|
|
56
|
+
# From-address for the built-in notification emails. Required for any
|
|
57
|
+
# email to be sent.
|
|
58
|
+
attr_accessor :mailer_from
|
|
59
|
+
|
|
60
|
+
# Called with each saved Livechat::Message — wire up Slack, Noticed,
|
|
61
|
+
# push… Runs inline after save; keep it fast or hand off to a job.
|
|
62
|
+
attr_accessor :on_visitor_message, :on_agent_message
|
|
63
|
+
|
|
64
|
+
# Per-IP throttle for the public endpoints, as keyword arguments for
|
|
65
|
+
# Rails' rate limiter (Rails 7.2+; ignored on 7.1). nil disables it.
|
|
66
|
+
attr_accessor :rate_limit
|
|
67
|
+
|
|
68
|
+
# Where the engine is mounted. The widget calls paths under it, so keep
|
|
69
|
+
# this in sync with the `mount` line in your routes.
|
|
70
|
+
attr_accessor :mount_path
|
|
71
|
+
|
|
72
|
+
def initialize
|
|
73
|
+
@app_name = nil
|
|
74
|
+
@enabled = ->(_request) { true }
|
|
75
|
+
@authorize_agent = ->(_request) { Rails.env.development? }
|
|
76
|
+
@current_user = ->(_request) {}
|
|
77
|
+
@visitor_label = ->(user) { user.try(:name).presence || user.try(:email).presence || user.to_s }
|
|
78
|
+
@agent_label = ->(user) { user.try(:name).presence || user.try(:email).presence || user.to_s }
|
|
79
|
+
@agent_display_name = ->(label) { label }
|
|
80
|
+
@greeting = nil
|
|
81
|
+
@reply_time_text = nil
|
|
82
|
+
@launcher_label = nil
|
|
83
|
+
@accent_color = nil
|
|
84
|
+
@show_launcher = true
|
|
85
|
+
@agent_emails = nil
|
|
86
|
+
@mailer_from = nil
|
|
87
|
+
@on_visitor_message = ->(_message) {}
|
|
88
|
+
@on_agent_message = ->(_message) {}
|
|
89
|
+
@rate_limit = { to: 30, within: 60 }
|
|
90
|
+
@mount_path = '/livechat'
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def widget_endpoint = "#{mount_path.chomp('/')}/widget"
|
|
94
|
+
|
|
95
|
+
def agent_email_list
|
|
96
|
+
list = agent_emails
|
|
97
|
+
list = list.call if list.respond_to?(:call)
|
|
98
|
+
Array(list).compact_blank
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def emails_enabled? = mailer_from.present?
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* livechat dashboard — the little JavaScript the inbox needs, served as a
|
|
3
|
+
* same-origin script so it works under strict CSPs (no inline handlers).
|
|
4
|
+
*
|
|
5
|
+
* On a conversation page it polls for new messages every few seconds. If the
|
|
6
|
+
* reply box is empty the page just reloads to show them; if the agent is
|
|
7
|
+
* mid-sentence it shows a "New messages" link instead — a reload must never
|
|
8
|
+
* eat a half-written reply. Cmd/Ctrl+Enter sends.
|
|
9
|
+
*/
|
|
10
|
+
(function () {
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
var POLL_MS = 5000;
|
|
14
|
+
|
|
15
|
+
function ready(fn) {
|
|
16
|
+
if (document.readyState === "loading") {
|
|
17
|
+
document.addEventListener("DOMContentLoaded", fn);
|
|
18
|
+
} else {
|
|
19
|
+
fn();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
ready(function () {
|
|
24
|
+
var thread = document.getElementById("thread");
|
|
25
|
+
if (!thread) return;
|
|
26
|
+
|
|
27
|
+
thread.scrollTop = thread.scrollHeight;
|
|
28
|
+
|
|
29
|
+
var replyBox = document.querySelector(".reply textarea");
|
|
30
|
+
var banner = document.getElementById("new-messages");
|
|
31
|
+
var latest = parseInt(thread.getAttribute("data-latest"), 10) || 0;
|
|
32
|
+
var url = thread.getAttribute("data-poll-url");
|
|
33
|
+
|
|
34
|
+
if (replyBox) {
|
|
35
|
+
replyBox.addEventListener("keydown", function (event) {
|
|
36
|
+
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
replyBox.form.requestSubmit();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setInterval(function () {
|
|
44
|
+
if (document.hidden) return;
|
|
45
|
+
fetch(url, { headers: { Accept: "application/json" }, credentials: "same-origin" })
|
|
46
|
+
.then(function (response) { return response.ok ? response.json() : null; })
|
|
47
|
+
.then(function (data) {
|
|
48
|
+
if (!data || data.latest <= latest) return;
|
|
49
|
+
if (!replyBox || replyBox.value.trim() === "") {
|
|
50
|
+
window.location.reload();
|
|
51
|
+
} else if (banner) {
|
|
52
|
+
banner.hidden = false;
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
.catch(function () { /* transient network error — next tick retries */ });
|
|
56
|
+
}, POLL_MS);
|
|
57
|
+
});
|
|
58
|
+
})();
|