testimonials 0.3.0 → 0.4.1

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/README.md +3 -3
  4. data/app/controllers/testimonials/api/testimonials_controller.rb +2 -1
  5. data/app/controllers/testimonials/media_controller.rb +6 -0
  6. data/app/controllers/testimonials/testimonials_controller.rb +23 -2
  7. data/app/models/testimonials/testimonial.rb +5 -0
  8. data/app/views/testimonials/testimonials/show.html.erb +6 -4
  9. data/config/locales/testimonials.ar.yml +4 -2
  10. data/config/locales/testimonials.bg.yml +4 -2
  11. data/config/locales/testimonials.bn.yml +4 -2
  12. data/config/locales/testimonials.de.yml +4 -2
  13. data/config/locales/testimonials.el.yml +4 -2
  14. data/config/locales/testimonials.en.yml +4 -2
  15. data/config/locales/testimonials.es.yml +4 -2
  16. data/config/locales/testimonials.fr.yml +4 -2
  17. data/config/locales/testimonials.hi.yml +4 -2
  18. data/config/locales/testimonials.hr.yml +4 -2
  19. data/config/locales/testimonials.id.yml +4 -2
  20. data/config/locales/testimonials.it.yml +4 -2
  21. data/config/locales/testimonials.ja.yml +4 -2
  22. data/config/locales/testimonials.ko.yml +4 -2
  23. data/config/locales/testimonials.lb.yml +4 -2
  24. data/config/locales/testimonials.nl.yml +4 -2
  25. data/config/locales/testimonials.pl.yml +4 -2
  26. data/config/locales/testimonials.pt.yml +4 -2
  27. data/config/locales/testimonials.ro.yml +4 -2
  28. data/config/locales/testimonials.ru.yml +4 -2
  29. data/config/locales/testimonials.th.yml +4 -2
  30. data/config/locales/testimonials.tr.yml +4 -2
  31. data/config/locales/testimonials.uk.yml +4 -2
  32. data/config/locales/testimonials.ur.yml +4 -2
  33. data/config/locales/testimonials.vi.yml +4 -2
  34. data/config/locales/testimonials.zh-CN.yml +4 -2
  35. data/config/routes.rb +1 -0
  36. data/lib/generators/testimonials/install/templates/initializer.rb +4 -3
  37. data/lib/testimonials/version.rb +1 -1
  38. data/lib/testimonials/widget.js +71 -24
  39. data/lib/testimonials/widget.rb +13 -2
  40. data/lib/testimonials.rb +15 -3
  41. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 775e2f3be386fed634ac1dc381b5557e2ab6de931e0ee70bf31d7dedaa7aa1c5
4
- data.tar.gz: 59115bcc0fcc2b7a882d94acf3bf1052396ef6cffc72c173fbfbc5928bc6f183
3
+ metadata.gz: eec2476402ec7e3a4d2c414cad340aae35a0456db87d65c950d527e983ab7053
4
+ data.tar.gz: a910019755bf56f04cdfc180c6706be9a1b6fe2246c481822a647610a63a53f5
5
5
  SHA512:
6
- metadata.gz: 45d8c4d2c40d674d44a086be802101ba4ff30dde287da19e5f442b4c2f0bd54362221f98fbe2a5d854198778a587537fb2d4536509e4556d0f6d9d5f7ff14f99
7
- data.tar.gz: 1ffbac4c4bc0cd7714545d365de849a4c6914c7ccfe5ea558dd33e6d1e480f5c268798c0b2249d6bf464636a0cd767d3622bbb487050c3125da9d97cbcbc01cc
6
+ metadata.gz: 84e12d4988eddabcba1ba473b7ddb92158674f34051fdf7005aae11171962b686621c81aade5121c33a700363422f1843183d6bb4d81c32123e2a84c757c51ee
7
+ data.tar.gz: 97731aaeffebf8bad9e02e44005a69d1507c99b46badda66735d3c4d5b756dbcb06eda88e10a3716d36ef13a82d9c3f962ac9993f0c2968e8d2bd39f8443b576
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1
4
+
5
+ - Video poster frames: a still is captured from the camera at record time and
6
+ shown via `<video poster>`, so recorded videos display a real thumbnail
7
+ instead of a black box in every browser (Safari won't paint a
8
+ fragmented-MP4 frame from the file itself). Served at `/:id/poster`,
9
+ exposed as `poster_url` in the API.
10
+
11
+ ## 0.4.0
12
+
13
+ - Consent is now an explicit public/private choice instead of a single
14
+ checkbox: "You can use my testimonial publicly…" vs "…only privately in
15
+ your marketing and sales." Public maps to the existing `consent_given`
16
+ (served by the read API); private is stored, admin-visible, and never
17
+ public. No migration. Both choices snapshot their exact wording.
18
+ Localized in all 26 languages. (Locale key `testimonials.consent` was
19
+ replaced by `consent_prompt` / `consent_public` / `consent_private`; if you
20
+ overrode it, update your keys. `config.consent_text` now overrides the
21
+ public line.)
22
+
3
23
  ## 0.3.0
4
24
 
5
25
  - Reverted 0.2.0's API path change: the read API collection is back at
data/README.md CHANGED
@@ -29,9 +29,9 @@ copy-paste examples included.
29
29
  - **NPS built in.** 0–10 with a comment; promoters (9–10) are immediately
30
30
  offered the testimonial form, detractors flow into `on_detractor` (pairs
31
31
  well with [feedback_engine](https://github.com/yshmarov/feedback-engine)).
32
- - **Consent is first-class.** A consent checkbox with a stored text snapshot;
33
- the API only ever serves approved **and** consented records, and never
34
- emails.
32
+ - **Consent is first-class.** Customers choose public or private use in their
33
+ own words, with a stored text snapshot; the read API only ever serves
34
+ approved records marked for **public** use, and never emails.
35
35
  - **26 languages**, including localized best-practice guiding questions.
36
36
 
37
37
  ## How it works
@@ -33,7 +33,8 @@ module Testimonials
33
33
  featured: testimonial.featured?,
34
34
  created_at: testimonial.created_at.iso8601,
35
35
  avatar_url: (testimonial_avatar_url(testimonial) if testimonial.avatar_attached?),
36
- video_url: (testimonial_video_url(testimonial) if testimonial.video_attached?)
36
+ video_url: (testimonial_video_url(testimonial) if testimonial.video_attached?),
37
+ poster_url: (testimonial_poster_url(testimonial) if testimonial.poster_attached?)
37
38
  }
38
39
  end
39
40
  end
@@ -17,6 +17,12 @@ module Testimonials
17
17
  redirect_to main_app.rails_blob_path(@testimonial.video_file, disposition: disposition)
18
18
  end
19
19
 
20
+ def poster
21
+ head :not_found and return unless @testimonial.poster_attached?
22
+
23
+ redirect_to main_app.rails_blob_path(@testimonial.poster, disposition: disposition)
24
+ end
25
+
20
26
  def avatar
21
27
  head :not_found and return unless @testimonial.avatar_attached?
22
28
 
@@ -55,7 +55,10 @@ module Testimonials
55
55
  if testimonial.save
56
56
  # Purge only after a valid save, so a failed update can't strand a
57
57
  # review with its video already gone.
58
- testimonial.video_file.purge if remove_video? && testimonial.video_attached?
58
+ if remove_video? && testimonial.video_attached?
59
+ testimonial.video_file.purge
60
+ testimonial.poster.purge if testimonial.poster_attached?
61
+ end
59
62
  record_submission
60
63
  notify_host(testimonial)
61
64
  head :created
@@ -101,7 +104,7 @@ module Testimonials
101
104
  end
102
105
  testimonial.kind = wants_video?(testimonial) ? 'video' : 'text'
103
106
  testimonial.source = 'widget' unless Testimonial::SOURCES.include?(testimonial.source)
104
- testimonial.consent_text = testimonial.consent_given? ? Testimonials.consent_text : nil
107
+ testimonial.consent_text = Testimonials.consent_text_for(testimonial.consent_given?)
105
108
  testimonial.locale = I18n.locale.to_s
106
109
  testimonial.user_agent = request.user_agent
107
110
  attribute_author(testimonial)
@@ -154,9 +157,27 @@ module Testimonials
154
157
  return error_label(:error_save) unless file.content_type.to_s.start_with?('video/', 'audio/')
155
158
 
156
159
  testimonial.video_file.attach(file)
160
+ attach_poster(testimonial)
157
161
  nil
158
162
  end
159
163
 
164
+ # The poster rides with a new video upload. It's best-effort — a missing
165
+ # or malformed poster never blocks the testimonial. Attaching replaces any
166
+ # previous poster; a new video with no usable poster (e.g. an uploaded
167
+ # file, which we can't frame-grab) drops the stale one so it can't show a
168
+ # thumbnail from the wrong video.
169
+ def attach_poster(testimonial)
170
+ file = params.dig(:testimonial, :poster)
171
+ usable = file.present? && file.content_type.to_s.start_with?('image/') &&
172
+ file.size <= Testimonials.config.max_avatar_size
173
+
174
+ if usable
175
+ testimonial.poster.attach(file)
176
+ elsif testimonial.poster_attached?
177
+ testimonial.poster.purge
178
+ end
179
+ end
180
+
160
181
  def attach_avatar(testimonial)
161
182
  file = params.dig(:testimonial, :avatar)
162
183
  return nil if file.blank?
@@ -11,6 +11,7 @@ module Testimonials
11
11
 
12
12
  if defined?(::ActiveStorage)
13
13
  has_one_attached :video_file
14
+ has_one_attached :poster # a still frame for the video, captured at record time
14
15
  has_one_attached :avatar
15
16
  end
16
17
 
@@ -37,6 +38,10 @@ module Testimonials
37
38
  respond_to?(:video_file) && video_file.attached?
38
39
  end
39
40
 
41
+ def poster_attached?
42
+ respond_to?(:poster) && poster.attached?
43
+ end
44
+
40
45
  def avatar_attached?
41
46
  respond_to?(:avatar) && avatar.attached?
42
47
  end
@@ -17,10 +17,11 @@
17
17
 
18
18
  <div class="card pad" style="margin-bottom: 16px;">
19
19
  <% if @testimonial.video_attached? %>
20
- <%# No #t=0.1 fragment: MediaRecorder webm has no seek index, and
21
- Firefox honors media fragments literally the pending seek stalls
22
- playback right after play. preload="metadata" paints the frame. %>
20
+ <%# The stored poster shows a real frame with no decoding Safari won't
21
+ paint a fragmented-MP4 frame on its own, so preload/fragment tricks
22
+ leave it black. %>
23
23
  <video class="playback" controls preload="metadata"
24
+ poster="<%= testimonial_poster_path(@testimonial) if @testimonial.poster_attached? %>"
24
25
  src="<%= testimonial_video_path(@testimonial) %>"></video>
25
26
  <p class="muted">
26
27
  <%= link_to "⬇ #{t('testimonials.dashboard.download_video', default: 'Download video')}",
@@ -68,7 +69,8 @@
68
69
  <% if @testimonial.consent_given? %>
69
70
  ✓ <span class="muted"><%= @testimonial.consent_text %></span>
70
71
  <% else %>
71
- <span class="muted"><%= t('testimonials.dashboard.no_consent', default: 'No publication consent. Not served by the API.') %></span>
72
+ 🔒 <span class="muted"><%= @testimonial.consent_text.presence ||
73
+ t('testimonials.dashboard.no_consent', default: 'Private use only. Not served by the read API.') %></span>
72
74
  <% end %>
73
75
  </dd>
74
76
  <dt><%= t('testimonials.dashboard.source', default: 'Source') %></dt>
@@ -4,7 +4,9 @@ ar:
4
4
  - "من أنت وكيف تستخدم %{app}؟"
5
5
  - "كيف ساعدك %{app}؟"
6
6
  - "ما أفضل ما في %{app}؟"
7
- consent: "أسمح باستخدام هذه الشهادة عبر وسائل التواصل الاجتماعي وجهود تسويقية أخرى."
7
+ consent_prompt: "أين يمكننا استخدام شهادتك؟"
8
+ consent_public: "يمكنكم استخدام شهادتي علنًا في التسويق والمبيعات."
9
+ consent_private: "يمكنكم استخدام شهادتي في التسويق والمبيعات بشكل خاص فقط."
8
10
  cta: "اترك تقييمًا"
9
11
  update_title: "حدِّث تقييمك"
10
12
  enjoying: "هل يعجبك %{app}؟"
@@ -71,7 +73,7 @@ ar:
71
73
  testimonial: "الشهادة"
72
74
  from: "من"
73
75
  consent: "الموافقة"
74
- no_consent: "لا موافقة على النشر. لا تُقدَّم عبر واجهة البرمجة."
76
+ no_consent: "للاستخدام الخاص فقط. لا تُقدَّم عبر واجهة القراءة البرمجية."
75
77
  received: "تاريخ الاستلام"
76
78
  featured: "مميّز"
77
79
  approve: "اعتماد"
@@ -4,7 +4,9 @@ bg:
4
4
  - "Кой сте вие и как използвате %{app}?"
5
5
  - "Как %{app} ви помогна?"
6
6
  - "Кое е най-хубавото на %{app}?"
7
- consent: "Разрешавам този отзив да се използва в социалните мрежи и други маркетингови дейности."
7
+ consent_prompt: "Къде можем да използваме вашия отзив?"
8
+ consent_public: "Можете да използвате моя отзив публично в маркетинга и продажбите си."
9
+ consent_private: "Можете да използвате моя отзив само частно в маркетинга и продажбите си."
8
10
  cta: "Оставете отзив"
9
11
  update_title: "Обновете отзива си"
10
12
  enjoying: "Харесва ли ви %{app}?"
@@ -71,7 +73,7 @@ bg:
71
73
  testimonial: "Отзив"
72
74
  from: "От"
73
75
  consent: "Съгласие"
74
- no_consent: "Няма съгласие за публикуване. Не се предоставя през API."
76
+ no_consent: "Само за лична употреба. Не се предоставя през API за четене."
75
77
  received: "Получено"
76
78
  featured: "Избрано"
77
79
  approve: "Одобри"
@@ -4,7 +4,9 @@ bn:
4
4
  - "আপনি কে এবং কীভাবে %{app} ব্যবহার করেন?"
5
5
  - "%{app} আপনাকে কীভাবে সাহায্য করেছে?"
6
6
  - "%{app}-এর সবচেয়ে ভালো দিক কী?"
7
- consent: "আমি এই প্রশংসাপত্রটি সোশ্যাল মিডিয়া অন্যান্য মার্কেটিং কাজে ব্যবহারের অনুমতি দিচ্ছি।"
7
+ consent_prompt: "আমরা আপনার প্রশংসাপত্র কোথায় ব্যবহার করতে পারি?"
8
+ consent_public: "আপনি আমার প্রশংসাপত্র আপনার মার্কেটিং ও সেলসে প্রকাশ্যে ব্যবহার করতে পারেন।"
9
+ consent_private: "আপনি আমার প্রশংসাপত্র আপনার মার্কেটিং ও সেলসে শুধু ব্যক্তিগতভাবে ব্যবহার করতে পারেন।"
8
10
  cta: "রিভিউ লিখুন"
9
11
  update_title: "আপনার রিভিউ আপডেট করুন"
10
12
  enjoying: "%{app} ভালো লাগছে?"
@@ -71,7 +73,7 @@ bn:
71
73
  testimonial: "প্রশংসাপত্র"
72
74
  from: "প্রেরক"
73
75
  consent: "সম্মতি"
74
- no_consent: "প্রকাশের সম্মতি নেই। API দিয়ে দেওয়া হয় না।"
76
+ no_consent: "শুধু ব্যক্তিগত ব্যবহারের জন্য। রিড API দিয়ে দেওয়া হয় না।"
75
77
  received: "গৃহীত"
76
78
  featured: "বিশেষ"
77
79
  approve: "অনুমোদন"
@@ -4,7 +4,9 @@ de:
4
4
  - "Wer sind Sie und wie nutzen Sie %{app}?"
5
5
  - "Wie hat %{app} Ihnen geholfen?"
6
6
  - "Was ist das Beste an %{app}?"
7
- consent: "Ich erlaube die Verwendung dieses Erfahrungsberichts in sozialen Medien und für andere Marketingzwecke."
7
+ consent_prompt: "Wo dürfen wir Ihren Erfahrungsbericht verwenden?"
8
+ consent_public: "Sie dürfen meinen Erfahrungsbericht öffentlich in Ihrem Marketing und Vertrieb verwenden."
9
+ consent_private: "Sie dürfen meinen Erfahrungsbericht nur privat in Ihrem Marketing und Vertrieb verwenden."
8
10
  cta: "Bewertung abgeben"
9
11
  update_title: "Ihre Bewertung aktualisieren"
10
12
  enjoying: "Gefällt Ihnen %{app}?"
@@ -71,7 +73,7 @@ de:
71
73
  testimonial: "Erfahrungsbericht"
72
74
  from: "Von"
73
75
  consent: "Einwilligung"
74
- no_consent: "Keine Veröffentlichungs-Einwilligung. Wird nicht über die API ausgeliefert."
76
+ no_consent: "Nur zur privaten Nutzung. Wird nicht über die Lese-API ausgeliefert."
75
77
  received: "Eingegangen"
76
78
  featured: "Hervorgehoben"
77
79
  approve: "Freigeben"
@@ -4,7 +4,9 @@ el:
4
4
  - "Ποιοι είστε και πώς χρησιμοποιείτε το %{app};"
5
5
  - "Πώς σας βοήθησε το %{app};"
6
6
  - "Ποιο είναι το καλύτερο στοιχείο του %{app};"
7
- consent: "Επιτρέπω τη χρήση αυτής της μαρτυρίας στα social media και σε άλλες ενέργειες μάρκετινγκ."
7
+ consent_prompt: "Πού μπορούμε να χρησιμοποιήσουμε τη μαρτυρία σας;"
8
+ consent_public: "Μπορείτε να χρησιμοποιήσετε τη μαρτυρία μου δημόσια στο μάρκετινγκ και τις πωλήσεις σας."
9
+ consent_private: "Μπορείτε να χρησιμοποιήσετε τη μαρτυρία μου μόνο ιδιωτικά στο μάρκετινγκ και τις πωλήσεις σας."
8
10
  cta: "Αφήστε μια κριτική"
9
11
  update_title: "Ενημερώστε την κριτική σας"
10
12
  enjoying: "Σας αρέσει το %{app};"
@@ -71,7 +73,7 @@ el:
71
73
  testimonial: "Μαρτυρία"
72
74
  from: "Από"
73
75
  consent: "Συγκατάθεση"
74
- no_consent: "Χωρίς συγκατάθεση δημοσίευσης. Δεν σερβίρεται από το API."
76
+ no_consent: "Μόνο για ιδιωτική χρήση. Δεν σερβίρεται από το API ανάγνωσης."
75
77
  received: "Ελήφθη"
76
78
  featured: "Προβεβλημένο"
77
79
  approve: "Έγκριση"
@@ -4,7 +4,9 @@ en:
4
4
  - "Who are you, and how do you use %{app}?"
5
5
  - "How has %{app} helped you?"
6
6
  - "What is the best thing about %{app}?"
7
- consent: "I give permission to use this testimonial across social channels and other marketing efforts."
7
+ consent_prompt: "Where can we use your testimonial?"
8
+ consent_public: "You can use my testimonial publicly in your marketing and sales."
9
+ consent_private: "You can only use my testimonial privately in your marketing and sales."
8
10
  cta: "Leave a review"
9
11
  update_title: "Update your review"
10
12
  enjoying: "Enjoying %{app}?"
@@ -71,7 +73,7 @@ en:
71
73
  testimonial: "Testimonial"
72
74
  from: "From"
73
75
  consent: "Consent"
74
- no_consent: "No publication consent. Not served by the API."
76
+ no_consent: "Private use only. Not served by the read API."
75
77
  received: "Received"
76
78
  featured: "Featured"
77
79
  approve: "Approve"
@@ -4,7 +4,9 @@ es:
4
4
  - "¿Quién eres y cómo usas %{app}?"
5
5
  - "¿Cómo te ha ayudado %{app}?"
6
6
  - "¿Qué es lo mejor de %{app}?"
7
- consent: "Doy permiso para usar este testimonio en redes sociales y otras acciones de marketing."
7
+ consent_prompt: "¿Dónde podemos usar tu testimonio?"
8
+ consent_public: "Puedes usar mi testimonio públicamente en tu marketing y ventas."
9
+ consent_private: "Solo puedes usar mi testimonio de forma privada en tu marketing y ventas."
8
10
  cta: "Dejar una reseña"
9
11
  update_title: "Actualizar tu reseña"
10
12
  enjoying: "¿Te gusta %{app}?"
@@ -71,7 +73,7 @@ es:
71
73
  testimonial: "Testimonio"
72
74
  from: "De"
73
75
  consent: "Consentimiento"
74
- no_consent: "Sin consentimiento de publicación. No se sirve por la API."
76
+ no_consent: "Solo para uso privado. No se sirve por la API de lectura."
75
77
  received: "Recibido"
76
78
  featured: "Destacado"
77
79
  approve: "Aprobar"
@@ -4,7 +4,9 @@ fr:
4
4
  - "Qui êtes-vous et comment utilisez-vous %{app} ?"
5
5
  - "Comment %{app} vous a-t-il aidé ?"
6
6
  - "Quelle est la meilleure chose à propos de %{app} ?"
7
- consent: "J'autorise l'utilisation de ce témoignage sur les réseaux sociaux et dans d'autres actions marketing."
7
+ consent_prompt: " pouvons-nous utiliser votre témoignage ?"
8
+ consent_public: "Vous pouvez utiliser mon témoignage publiquement dans votre marketing et vos ventes."
9
+ consent_private: "Vous pouvez utiliser mon témoignage uniquement en privé dans votre marketing et vos ventes."
8
10
  cta: "Laisser un avis"
9
11
  update_title: "Mettre à jour votre avis"
10
12
  enjoying: "Vous aimez %{app} ?"
@@ -71,7 +73,7 @@ fr:
71
73
  testimonial: "Témoignage"
72
74
  from: "De"
73
75
  consent: "Consentement"
74
- no_consent: "Pas de consentement à la publication. Non servi par l'API."
76
+ no_consent: "Usage privé uniquement. Non servi par l'API de lecture."
75
77
  received: "Reçu"
76
78
  featured: "Mis en avant"
77
79
  approve: "Approuver"
@@ -4,7 +4,9 @@ hi:
4
4
  - "आप कौन हैं और %{app} का उपयोग कैसे करते हैं?"
5
5
  - "%{app} ने आपकी कैसे मदद की?"
6
6
  - "%{app} की सबसे अच्छी बात क्या है?"
7
- consent: "मैं इस प्रशंसापत्र को सोशल मीडिया और अन्य मार्केटिंग प्रयासों में उपयोग करने की अनुमति देता/देती हूँ।"
7
+ consent_prompt: "हम आपका प्रशंसापत्र कहाँ उपयोग कर सकते हैं?"
8
+ consent_public: "आप मेरा प्रशंसापत्र अपनी मार्केटिंग और बिक्री में सार्वजनिक रूप से उपयोग कर सकते हैं।"
9
+ consent_private: "आप मेरा प्रशंसापत्र अपनी मार्केटिंग और बिक्री में केवल निजी रूप से उपयोग कर सकते हैं।"
8
10
  cta: "समीक्षा लिखें"
9
11
  update_title: "अपनी समीक्षा अपडेट करें"
10
12
  enjoying: "%{app} पसंद आ रहा है?"
@@ -71,7 +73,7 @@ hi:
71
73
  testimonial: "प्रशंसापत्र"
72
74
  from: "किससे"
73
75
  consent: "सहमति"
74
- no_consent: "प्रकाशन की सहमति नहीं। API से नहीं दिया जाता।"
76
+ no_consent: "केवल निजी उपयोग के लिए। रीड API से नहीं दिया जाता।"
75
77
  received: "प्राप्त"
76
78
  featured: "विशेष"
77
79
  approve: "स्वीकृत करें"
@@ -4,7 +4,9 @@ hr:
4
4
  - "Tko ste i kako koristite %{app}?"
5
5
  - "Kako vam je %{app} pomogao?"
6
6
  - "Što je najbolje kod %{app}?"
7
- consent: "Dajem dopuštenje da se ovaj osvrt koristi na društvenim mrežama i u drugim marketinškim aktivnostima."
7
+ consent_prompt: "Gdje možemo koristiti vaš osvrt?"
8
+ consent_public: "Moj osvrt možete javno koristiti u svom marketingu i prodaji."
9
+ consent_private: "Moj osvrt možete koristiti samo privatno u svom marketingu i prodaji."
8
10
  cta: "Ostavite recenziju"
9
11
  update_title: "Ažurirajte svoju recenziju"
10
12
  enjoying: "Sviđa li vam se %{app}?"
@@ -71,7 +73,7 @@ hr:
71
73
  testimonial: "Osvrt"
72
74
  from: "Od"
73
75
  consent: "Privola"
74
- no_consent: "Nema privole za objavu. Ne poslužuje se putem API-ja."
76
+ no_consent: "Samo za privatnu upotrebu. Ne poslužuje se putem API-ja za čitanje."
75
77
  received: "Zaprimljeno"
76
78
  featured: "Istaknuto"
77
79
  approve: "Odobri"
@@ -4,7 +4,9 @@ id:
4
4
  - "Siapa Anda dan bagaimana Anda menggunakan %{app}?"
5
5
  - "Bagaimana %{app} membantu Anda?"
6
6
  - "Apa hal terbaik dari %{app}?"
7
- consent: "Saya mengizinkan testimoni ini digunakan di media sosial dan upaya pemasaran lainnya."
7
+ consent_prompt: "Di mana kami boleh menggunakan testimoni Anda?"
8
+ consent_public: "Anda boleh menggunakan testimoni saya secara publik dalam pemasaran dan penjualan Anda."
9
+ consent_private: "Anda hanya boleh menggunakan testimoni saya secara pribadi dalam pemasaran dan penjualan Anda."
8
10
  cta: "Tulis ulasan"
9
11
  update_title: "Perbarui ulasan Anda"
10
12
  enjoying: "Suka dengan %{app}?"
@@ -71,7 +73,7 @@ id:
71
73
  testimonial: "Testimoni"
72
74
  from: "Dari"
73
75
  consent: "Persetujuan"
74
- no_consent: "Tanpa persetujuan publikasi. Tidak disajikan lewat API."
76
+ no_consent: "Hanya untuk penggunaan pribadi. Tidak disajikan oleh API baca."
75
77
  received: "Diterima"
76
78
  featured: "Unggulan"
77
79
  approve: "Setujui"
@@ -4,7 +4,9 @@ it:
4
4
  - "Chi sei e come usi %{app}?"
5
5
  - "In che modo %{app} ti ha aiutato?"
6
6
  - "Qual è la cosa migliore di %{app}?"
7
- consent: "Autorizzo l'uso di questa testimonianza sui social e in altre attività di marketing."
7
+ consent_prompt: "Dove possiamo usare la tua testimonianza?"
8
+ consent_public: "Puoi usare la mia testimonianza pubblicamente nel tuo marketing e nelle tue vendite."
9
+ consent_private: "Puoi usare la mia testimonianza solo privatamente nel tuo marketing e nelle tue vendite."
8
10
  cta: "Lascia una recensione"
9
11
  update_title: "Aggiorna la tua recensione"
10
12
  enjoying: "Ti piace %{app}?"
@@ -71,7 +73,7 @@ it:
71
73
  testimonial: "Testimonianza"
72
74
  from: "Da"
73
75
  consent: "Consenso"
74
- no_consent: "Nessun consenso alla pubblicazione. Non servito dall'API."
76
+ no_consent: "Solo per uso privato. Non servito dall'API di lettura."
75
77
  received: "Ricevuto"
76
78
  featured: "In evidenza"
77
79
  approve: "Approva"
@@ -4,7 +4,9 @@ ja:
4
4
  - "あなたはどなたで、%{app}をどのように使っていますか?"
5
5
  - "%{app}はどのように役立ちましたか?"
6
6
  - "%{app}の一番良いところは何ですか?"
7
- consent: "この声をSNSやその他のマーケティング活動で使用することを許可します。"
7
+ consent_prompt: "この声をどこで使ってよいですか?"
8
+ consent_public: "私の声をマーケティングや販売で公開して使ってかまいません。"
9
+ consent_private: "私の声はマーケティングや販売で非公開にのみ使ってください。"
8
10
  cta: "レビューを書く"
9
11
  update_title: "レビューを更新する"
10
12
  enjoying: "%{app}を気に入っていますか?"
@@ -71,7 +73,7 @@ ja:
71
73
  testimonial: "お客様の声"
72
74
  from: "投稿者"
73
75
  consent: "同意"
74
- no_consent: "公開への同意なし。APIでは配信されません。"
76
+ no_consent: "非公開利用のみ。読み取りAPIでは配信されません。"
75
77
  received: "受信日時"
76
78
  featured: "注目"
77
79
  approve: "承認"
@@ -4,7 +4,9 @@ ko:
4
4
  - "당신은 누구이며 %{app}을(를) 어떻게 사용하시나요?"
5
5
  - "%{app}이(가) 어떻게 도움이 되었나요?"
6
6
  - "%{app}의 가장 좋은 점은 무엇인가요?"
7
- consent: " 후기를 소셜 미디어 및 기타 마케팅 활동에 사용하는 것을 허락합니다."
7
+ consent_prompt: "귀하의 후기를 어디에 사용해도 될까요?"
8
+ consent_public: "제 후기를 마케팅과 영업에 공개적으로 사용하셔도 됩니다."
9
+ consent_private: "제 후기를 마케팅과 영업에 비공개로만 사용하셔도 됩니다."
8
10
  cta: "리뷰 남기기"
9
11
  update_title: "리뷰 수정하기"
10
12
  enjoying: "%{app}이(가) 마음에 드시나요?"
@@ -71,7 +73,7 @@ ko:
71
73
  testimonial: "후기"
72
74
  from: "보낸 사람"
73
75
  consent: "동의"
74
- no_consent: "게시 동의 없음. API로 제공되지 않습니다."
76
+ no_consent: "비공개 사용 전용. 읽기 API로 제공되지 않습니다."
75
77
  received: "받은 시각"
76
78
  featured: "추천"
77
79
  approve: "승인"
@@ -4,7 +4,9 @@ lb:
4
4
  - "Wien sidd Dir a wéi benotzt Dir %{app}?"
5
5
  - "Wéi huet %{app} Iech gehollef?"
6
6
  - "Wat ass dat Bescht un %{app}?"
7
- consent: "Ech ginn d'Erlaabnis, dësen Temoignage op soziale Medien an an anere Marketingaktivitéiten ze benotzen."
7
+ consent_prompt: "Wou kënne mir Ären Temoignage benotzen?"
8
+ consent_public: "Dir kënnt mäin Temoignage ëffentlech an Ärem Marketing an Äre Verkeef benotzen."
9
+ consent_private: "Dir kënnt mäin Temoignage nëmme privat an Ärem Marketing an Äre Verkeef benotzen."
8
10
  cta: "Eng Bewäertung hannerloossen"
9
11
  update_title: "Är Bewäertung aktualiséieren"
10
12
  enjoying: "Gefällt Iech %{app}?"
@@ -71,7 +73,7 @@ lb:
71
73
  testimonial: "Temoignage"
72
74
  from: "Vun"
73
75
  consent: "Averständnis"
74
- no_consent: "Keen Averständnis fir d'Publikatioun. Gëtt net iwwer d'API servéiert."
76
+ no_consent: "Nëmme fir privaten Asaz. Gëtt net iwwer d'Lies-API servéiert."
75
77
  received: "Erakritt"
76
78
  featured: "Ervirgehuewen"
77
79
  approve: "Guttheeschen"
@@ -4,7 +4,9 @@ nl:
4
4
  - "Wie ben je en hoe gebruik je %{app}?"
5
5
  - "Hoe heeft %{app} je geholpen?"
6
6
  - "Wat is het beste aan %{app}?"
7
- consent: "Ik geef toestemming om deze testimonial te gebruiken op sociale media en voor andere marketingdoeleinden."
7
+ consent_prompt: "Waar mogen we je testimonial gebruiken?"
8
+ consent_public: "Je mag mijn testimonial openbaar gebruiken in je marketing en verkoop."
9
+ consent_private: "Je mag mijn testimonial alleen privé gebruiken in je marketing en verkoop."
8
10
  cta: "Laat een review achter"
9
11
  update_title: "Werk je review bij"
10
12
  enjoying: "Bevalt %{app} je?"
@@ -71,7 +73,7 @@ nl:
71
73
  testimonial: "Testimonial"
72
74
  from: "Van"
73
75
  consent: "Toestemming"
74
- no_consent: "Geen publicatietoestemming. Niet geserveerd door de API."
76
+ no_consent: "Alleen voor privégebruik. Niet geserveerd door de lees-API."
75
77
  received: "Ontvangen"
76
78
  featured: "Uitgelicht"
77
79
  approve: "Goedkeuren"
@@ -4,7 +4,9 @@ pl:
4
4
  - "Kim jesteś i jak korzystasz z %{app}?"
5
5
  - "Jak %{app} Ci pomogło?"
6
6
  - "Co jest najlepsze w %{app}?"
7
- consent: "Wyrażam zgodę na wykorzystanie tej opinii w mediach społecznościowych i innych działaniach marketingowych."
7
+ consent_prompt: "Gdzie możemy wykorzystać Twoją opinię?"
8
+ consent_public: "Możesz publicznie wykorzystać moją opinię w swoim marketingu i sprzedaży."
9
+ consent_private: "Możesz wykorzystać moją opinię wyłącznie prywatnie w swoim marketingu i sprzedaży."
8
10
  cta: "Zostaw opinię"
9
11
  update_title: "Zaktualizuj swoją opinię"
10
12
  enjoying: "Podoba Ci się %{app}?"
@@ -71,7 +73,7 @@ pl:
71
73
  testimonial: "Opinia"
72
74
  from: "Od"
73
75
  consent: "Zgoda"
74
- no_consent: "Brak zgody na publikację. Nie serwowane przez API."
76
+ no_consent: "Tylko do użytku prywatnego. Nie serwowane przez API odczytu."
75
77
  received: "Otrzymano"
76
78
  featured: "Wyróżnione"
77
79
  approve: "Zatwierdź"
@@ -4,7 +4,9 @@ pt:
4
4
  - "Quem é você e como usa %{app}?"
5
5
  - "Como %{app} ajudou você?"
6
6
  - "Qual é a melhor coisa de %{app}?"
7
- consent: "Autorizo o uso deste depoimento em redes sociais e outras ações de marketing."
7
+ consent_prompt: "Onde podemos usar o seu depoimento?"
8
+ consent_public: "Você pode usar o meu depoimento publicamente no seu marketing e vendas."
9
+ consent_private: "Você só pode usar o meu depoimento de forma privada no seu marketing e vendas."
8
10
  cta: "Deixar uma avaliação"
9
11
  update_title: "Atualizar sua avaliação"
10
12
  enjoying: "Está gostando de %{app}?"
@@ -71,7 +73,7 @@ pt:
71
73
  testimonial: "Depoimento"
72
74
  from: "De"
73
75
  consent: "Consentimento"
74
- no_consent: "Sem consentimento de publicação. Não servido pela API."
76
+ no_consent: "Apenas para uso privado. Não servido pela API de leitura."
75
77
  received: "Recebido"
76
78
  featured: "Destacado"
77
79
  approve: "Aprovar"
@@ -4,7 +4,9 @@ ro:
4
4
  - "Cine sunteți și cum folosiți %{app}?"
5
5
  - "Cum v-a ajutat %{app}?"
6
6
  - "Care este cel mai bun lucru la %{app}?"
7
- consent: "Îmi dau acordul ca acest testimonial să fie folosit pe rețelele sociale și în alte acțiuni de marketing."
7
+ consent_prompt: "Unde putem folosi testimonialul tău?"
8
+ consent_public: "Poți folosi testimonialul meu public în marketingul și vânzările tale."
9
+ consent_private: "Poți folosi testimonialul meu doar privat în marketingul și vânzările tale."
8
10
  cta: "Lasă o recenzie"
9
11
  update_title: "Actualizează-ți recenzia"
10
12
  enjoying: "Vă place %{app}?"
@@ -71,7 +73,7 @@ ro:
71
73
  testimonial: "Testimonial"
72
74
  from: "De la"
73
75
  consent: "Consimțământ"
74
- no_consent: "Fără consimțământ de publicare. Nu este servit de API."
76
+ no_consent: "Doar pentru uz privat. Nu este servit de API-ul de citire."
75
77
  received: "Primit"
76
78
  featured: "Evidențiat"
77
79
  approve: "Aprobă"
@@ -4,7 +4,9 @@ ru:
4
4
  - "Кто вы и как вы используете %{app}?"
5
5
  - "Как %{app} вам помог?"
6
6
  - "Что самое лучшее в %{app}?"
7
- consent: "Я разрешаю использовать этот отзыв в соцсетях и других маркетинговых материалах."
7
+ consent_prompt: "Где мы можем использовать ваш отзыв?"
8
+ consent_public: "Вы можете публично использовать мой отзыв в маркетинге и продажах."
9
+ consent_private: "Вы можете использовать мой отзыв только конфиденциально в маркетинге и продажах."
8
10
  cta: "Оставить отзыв"
9
11
  update_title: "Обновить ваш отзыв"
10
12
  enjoying: "Нравится %{app}?"
@@ -71,7 +73,7 @@ ru:
71
73
  testimonial: "Отзыв"
72
74
  from: "От"
73
75
  consent: "Согласие"
74
- no_consent: "Нет согласия на публикацию. Не отдаётся через API."
76
+ no_consent: "Только для личного использования. Не отдаётся через API чтения."
75
77
  received: "Получено"
76
78
  featured: "Избранное"
77
79
  approve: "Одобрить"
@@ -4,7 +4,9 @@ th:
4
4
  - "คุณคือใคร และใช้ %{app} อย่างไร?"
5
5
  - "%{app} ช่วยคุณอย่างไรบ้าง?"
6
6
  - "สิ่งที่ดีที่สุดของ %{app} คืออะไร?"
7
- consent: "ฉันอนุญาตให้ใช้คำรับรองนี้บนโซเชียลมีเดียและกิจกรรมการตลาดอื่น ๆ"
7
+ consent_prompt: "เราสามารถใช้คำรับรองของคุณได้ที่ไหนบ้าง"
8
+ consent_public: "คุณสามารถใช้คำรับรองของฉันแบบสาธารณะในการตลาดและการขายของคุณได้"
9
+ consent_private: "คุณสามารถใช้คำรับรองของฉันแบบส่วนตัวเท่านั้นในการตลาดและการขายของคุณ"
8
10
  cta: "เขียนรีวิว"
9
11
  update_title: "อัปเดตรีวิวของคุณ"
10
12
  enjoying: "ชอบ %{app} ไหม?"
@@ -71,7 +73,7 @@ th:
71
73
  testimonial: "คำรับรอง"
72
74
  from: "จาก"
73
75
  consent: "ความยินยอม"
74
- no_consent: "ไม่มีความยินยอมให้เผยแพร่ จะไม่ถูกส่งผ่าน API"
76
+ no_consent: "สำหรับใช้งานส่วนตัวเท่านั้น จะไม่ถูกส่งผ่าน API สำหรับอ่าน"
75
77
  received: "ได้รับเมื่อ"
76
78
  featured: "แนะนำ"
77
79
  approve: "อนุมัติ"
@@ -4,7 +4,9 @@ tr:
4
4
  - "Kimsiniz ve %{app} uygulamasını nasıl kullanıyorsunuz?"
5
5
  - "%{app} size nasıl yardımcı oldu?"
6
6
  - "%{app} uygulamasının en iyi yanı nedir?"
7
- consent: "Bu yorumun sosyal medyada ve diğer pazarlama çalışmalarında kullanılmasına izin veriyorum."
7
+ consent_prompt: "Yorumunuzu nerede kullanabiliriz?"
8
+ consent_public: "Yorumumu pazarlama ve satışlarınızda herkese açık şekilde kullanabilirsiniz."
9
+ consent_private: "Yorumumu pazarlama ve satışlarınızda yalnızca özel olarak kullanabilirsiniz."
8
10
  cta: "Yorum bırak"
9
11
  update_title: "Yorumunuzu güncelleyin"
10
12
  enjoying: "%{app} hoşunuza gidiyor mu?"
@@ -71,7 +73,7 @@ tr:
71
73
  testimonial: "Görüş"
72
74
  from: "Kimden"
73
75
  consent: "Onay"
74
- no_consent: "Yayın onayı yok. API üzerinden sunulmaz."
76
+ no_consent: "Yalnızca özel kullanım. Okuma API'si üzerinden sunulmaz."
75
77
  received: "Alındı"
76
78
  featured: "Öne çıkan"
77
79
  approve: "Onayla"
@@ -4,7 +4,9 @@ uk:
4
4
  - "Хто ви і як ви використовуєте %{app}?"
5
5
  - "Як %{app} вам допоміг?"
6
6
  - "Що найкраще в %{app}?"
7
- consent: "Я дозволяю використовувати цей відгук у соцмережах та інших маркетингових матеріалах."
7
+ consent_prompt: "Де ми можемо використати ваш відгук?"
8
+ consent_public: "Ви можете публічно використовувати мій відгук у своєму маркетингу та продажах."
9
+ consent_private: "Ви можете використовувати мій відгук лише приватно у своєму маркетингу та продажах."
8
10
  cta: "Залишити відгук"
9
11
  update_title: "Оновити ваш відгук"
10
12
  enjoying: "Подобається %{app}?"
@@ -71,7 +73,7 @@ uk:
71
73
  testimonial: "Відгук"
72
74
  from: "Від"
73
75
  consent: "Згода"
74
- no_consent: "Немає згоди на публікацію. Не віддається через API."
76
+ no_consent: "Лише для приватного використання. Не віддається через API читання."
75
77
  received: "Отримано"
76
78
  featured: "Обране"
77
79
  approve: "Схвалити"
@@ -4,7 +4,9 @@ ur:
4
4
  - "آپ کون ہیں اور %{app} کیسے استعمال کرتے ہیں؟"
5
5
  - "%{app} نے آپ کی کیسے مدد کی؟"
6
6
  - "%{app} کی سب سے اچھی بات کیا ہے؟"
7
- consent: "میں اس تعریفی کلمات کو سوشل میڈیا اور دیگر مارکیٹنگ سرگرمیوں میں استعمال کرنے کی اجازت دیتا/دیتی ہوں۔"
7
+ consent_prompt: "ہم آپ کے تعریفی کلمات کہاں استعمال کر سکتے ہیں؟"
8
+ consent_public: "آپ میرے تعریفی کلمات اپنی مارکیٹنگ اور سیلز میں عوامی طور پر استعمال کر سکتے ہیں۔"
9
+ consent_private: "آپ میرے تعریفی کلمات اپنی مارکیٹنگ اور سیلز میں صرف نجی طور پر استعمال کر سکتے ہیں۔"
8
10
  cta: "تبصرہ چھوڑیں"
9
11
  update_title: "اپنا تبصرہ اپ ڈیٹ کریں"
10
12
  enjoying: "%{app} پسند آ رہا ہے؟"
@@ -71,7 +73,7 @@ ur:
71
73
  testimonial: "تعریفی کلمات"
72
74
  from: "منجانب"
73
75
  consent: "رضامندی"
74
- no_consent: "اشاعت کی رضامندی نہیں۔ API سے فراہم نہیں کیا جاتا۔"
76
+ no_consent: "صرف نجی استعمال کے لیے۔ پڑھنے والے API سے فراہم نہیں کیا جاتا۔"
75
77
  received: "موصول ہوا"
76
78
  featured: "نمایاں"
77
79
  approve: "منظور کریں"
@@ -4,7 +4,9 @@ vi:
4
4
  - "Bạn là ai và bạn dùng %{app} như thế nào?"
5
5
  - "%{app} đã giúp bạn ra sao?"
6
6
  - "Điều tuyệt nhất ở %{app} là gì?"
7
- consent: "Tôi cho phép sử dụng cảm nhận này trên mạng xã hội và các hoạt động tiếp thị khác."
7
+ consent_prompt: "Chúng tôi thể sử dụng cảm nhận của bạn đâu?"
8
+ consent_public: "Bạn có thể sử dụng cảm nhận của tôi công khai trong hoạt động tiếp thị và bán hàng."
9
+ consent_private: "Bạn chỉ có thể sử dụng cảm nhận của tôi một cách riêng tư trong hoạt động tiếp thị và bán hàng."
8
10
  cta: "Để lại đánh giá"
9
11
  update_title: "Cập nhật đánh giá của bạn"
10
12
  enjoying: "Bạn thích %{app} chứ?"
@@ -71,7 +73,7 @@ vi:
71
73
  testimonial: "Cảm nhận"
72
74
  from: "Từ"
73
75
  consent: "Sự đồng ý"
74
- no_consent: "Chưa đồng ý công bố. Không được cung cấp qua API."
76
+ no_consent: "Chỉ dùng riêng tư. Không được cung cấp qua API đọc."
75
77
  received: "Đã nhận"
76
78
  featured: "Nổi bật"
77
79
  approve: "Duyệt"
@@ -4,7 +4,9 @@ zh-CN:
4
4
  - "您是谁?您如何使用 %{app}?"
5
5
  - "%{app} 是如何帮助您的?"
6
6
  - "%{app} 最棒的地方是什么?"
7
- consent: "我允许在社交媒体和其他营销活动中使用这条评价。"
7
+ consent_prompt: "我们可以在哪里使用您的评价?"
8
+ consent_public: "您可以在营销和销售中公开使用我的评价。"
9
+ consent_private: "您只能在营销和销售中私下使用我的评价。"
8
10
  cta: "写评价"
9
11
  update_title: "更新您的评价"
10
12
  enjoying: "喜欢 %{app} 吗?"
@@ -71,7 +73,7 @@ zh-CN:
71
73
  testimonial: "评价"
72
74
  from: "来自"
73
75
  consent: "授权"
74
- no_consent: "未授权发布。API 不会提供此条。"
76
+ no_consent: "仅供私人使用。读取 API 不会提供此条。"
75
77
  received: "收到时间"
76
78
  featured: "精选"
77
79
  approve: "批准"
data/config/routes.rb CHANGED
@@ -21,6 +21,7 @@ Testimonials::Engine.routes.draw do
21
21
 
22
22
  # Media by testimonial id, gated (admin, author, or public_api + publishable).
23
23
  get ':id/video', to: 'media#video', as: :testimonial_video, constraints: { id: /\d+/ }
24
+ get ':id/poster', to: 'media#poster', as: :testimonial_poster, constraints: { id: /\d+/ }
24
25
  get ':id/avatar', to: 'media#avatar', as: :testimonial_avatar, constraints: { id: /\d+/ }
25
26
 
26
27
  # Flat, human URLs: the mount path IS the resource. /testimonials is the
@@ -46,9 +46,10 @@ Testimonials.configure do |config|
46
46
  # config.reprompt_after = 90.days
47
47
  # config.max_prompts = 3
48
48
 
49
- # Consent line stored verbatim with each submission. nil = localized default:
50
- # "I give permission to use this testimonial across social channels and
51
- # other marketing efforts."
49
+ # The public-use consent line, stored verbatim when a customer picks
50
+ # "use publicly". nil = localized default ("You can use my testimonial
51
+ # publicly in your marketing and sales."). The private-use line is
52
+ # localized only (testimonials.consent_private).
52
53
  # config.consent_text = "..."
53
54
 
54
55
  # The standalone collection page at /testimonials/new — share it with customers
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Testimonials
4
- VERSION = '0.3.0'
4
+ VERSION = '0.4.1'
5
5
  end
@@ -163,6 +163,7 @@
163
163
  if (!overlay) return;
164
164
  stopMedia();
165
165
  if (state && state.playbackUrl) URL.revokeObjectURL(state.playbackUrl);
166
+ if (state && state.posterUrl) URL.revokeObjectURL(state.posterUrl);
166
167
  if (state && state.auto && !state.submitted) postEvent(state.kind, "dismissed");
167
168
  overlay.remove();
168
169
  overlay = null;
@@ -276,6 +277,7 @@
276
277
  state.body = config.existing.body || "";
277
278
  state.consent = !!config.existing.consent;
278
279
  state.existingVideoUrl = config.existing.videoUrl || null;
280
+ state.existingPosterUrl = config.existing.posterUrl || null;
279
281
  }
280
282
 
281
283
  // --- stage 2: the testimonial form ------------------------------------------
@@ -374,21 +376,35 @@
374
376
  return box;
375
377
  }
376
378
 
379
+ // Where may we use this? Public (shown in marketing, served by the read
380
+ // API) or private (kept for internal use only). Public is the default —
381
+ // someone leaving a testimonial usually expects it shown — but private is
382
+ // an equally-visible, graceful choice, not a buried opt-out.
377
383
  function consentField() {
378
- var wrap = el("label", "tml-consent");
379
- var checkbox = document.createElement("input");
380
- checkbox.type = "checkbox";
381
- checkbox.name = "consent";
382
- // Pre-checked when editing a review that already granted consent, and
383
- // kept across stage round trips — an update must never silently revoke
384
- // (or invent) publication consent.
385
- checkbox.checked = !!state.consent;
386
- checkbox.addEventListener("change", function () { state.consent = checkbox.checked; });
387
- wrap.appendChild(checkbox);
388
- wrap.appendChild(el("span", null, config.consent));
384
+ var wrap = el("div", "tml-consent");
385
+ if (config.consent.prompt) wrap.appendChild(el("p", "tml-consent-prompt", config.consent.prompt));
386
+ // state.consent (a bool) survives stage round trips and reflects an
387
+ // edited review's existing choice; undefined on a new review means public.
388
+ wrap.appendChild(consentOption("public", config.consent.public, state.consent !== false));
389
+ wrap.appendChild(consentOption("private", config.consent.private, state.consent === false));
389
390
  return wrap;
390
391
  }
391
392
 
393
+ function consentOption(value, label, checked) {
394
+ var opt = el("label", "tml-consent-option");
395
+ var radio = document.createElement("input");
396
+ radio.type = "radio";
397
+ radio.name = "consent";
398
+ radio.value = value;
399
+ radio.checked = checked;
400
+ radio.addEventListener("change", function () {
401
+ if (radio.checked) state.consent = value === "public";
402
+ });
403
+ opt.appendChild(radio);
404
+ opt.appendChild(el("span", null, label));
405
+ return opt;
406
+ }
407
+
392
408
  // --- video ------------------------------------------------------------------
393
409
 
394
410
  function videoControl(form) {
@@ -409,12 +425,9 @@
409
425
  playback.controls = true;
410
426
  playback.playsInline = true;
411
427
  playback.preload = "metadata";
412
- // No #t=0.1 fragment here either (see the review-stage comment):
413
- // MediaRecorder webm has no seek index, and Firefox honors the
414
- // fragment literally — the pending seek stalls playback a moment
415
- // after play. preload="metadata" already paints the first frame in
416
- // Chrome and Firefox; iOS shows the controls on a blank frame, which
417
- // beats a player that pauses itself.
428
+ // The stored poster shows a real frame with no decoding — the reliable
429
+ // fix for Safari, which won't paint a fragmented-MP4 frame on its own.
430
+ if (state.existingPosterUrl) playback.poster = state.existingPosterUrl;
418
431
  playback.src = state.existingVideoUrl;
419
432
  wrap.appendChild(playback);
420
433
  wrap.appendChild(attachedVideoChip(form, wrap, playback, function () {
@@ -537,7 +550,7 @@
537
550
  runCountdown(countdown, 3, function () {
538
551
  start.hidden = true;
539
552
  stop.hidden = false;
540
- beginRecording(timer, stop);
553
+ beginRecording(preview, timer, stop);
541
554
  });
542
555
  });
543
556
 
@@ -568,7 +581,7 @@
568
581
  }, 800);
569
582
  }
570
583
 
571
- function beginRecording(timer, stop) {
584
+ function beginRecording(preview, timer, stop) {
572
585
  media.chunks = [];
573
586
  var recorder;
574
587
  try {
@@ -584,6 +597,12 @@
584
597
  // A teardown (Cancel, close) also stops the recorder — only a real
585
598
  // Finish, where this recorder is still the active one, goes to review.
586
599
  if (media.recorder !== recorder) return;
600
+ // Grab a poster frame from the still-live preview stream *before*
601
+ // showStage tears the camera down. Captured from the camera, never
602
+ // from the recorded blob — seeking Safari's own fragmented MP4 to
603
+ // paint a frame is exactly what fails (black or stall), so we store a
604
+ // real image and hand it to <video poster>, which needs no decoding.
605
+ capturePoster(preview);
587
606
  var type = recorder.mimeType || "video/webm";
588
607
  var blob = new Blob(media.chunks, { type: type.split(";")[0] });
589
608
  showStage(reviewStage(blob));
@@ -654,9 +673,13 @@
654
673
  state.playbackUrl = URL.createObjectURL(blob);
655
674
  // No #t=0.1 here, on purpose: seeking a blob of Safari's own fragmented
656
675
  // MP4 recording can stall the video track while audio keeps playing.
657
- // The fragment-thumbnail trick is only for server-served files; a
658
- // just-recorded clip gets played immediately anyway.
676
+ // The captured poster (below) shows a frame without any decoding.
659
677
  playback.src = state.playbackUrl;
678
+ if (state.posterBlob) {
679
+ if (state.posterUrl) URL.revokeObjectURL(state.posterUrl);
680
+ state.posterUrl = URL.createObjectURL(state.posterBlob);
681
+ playback.poster = state.posterUrl;
682
+ }
660
683
  stage.appendChild(playback);
661
684
 
662
685
  var error = el("p", "tml-error");
@@ -708,6 +731,24 @@
708
731
  media = { stream: null, recorder: null, chunks: [], timer: null };
709
732
  }
710
733
 
734
+ // Draw the current preview frame to a canvas *now* (synchronous, while the
735
+ // stream is live) and encode it as a JPEG poster. The canvas keeps the
736
+ // pixels, so the async toBlob is safe even after the camera stops.
737
+ function capturePoster(preview) {
738
+ try {
739
+ var w = preview.videoWidth;
740
+ var h = preview.videoHeight;
741
+ if (!w || !h) return;
742
+ var canvas = document.createElement("canvas");
743
+ canvas.width = w;
744
+ canvas.height = h;
745
+ canvas.getContext("2d").drawImage(preview, 0, 0, w, h);
746
+ canvas.toBlob(function (blob) {
747
+ if (state && blob) state.posterBlob = blob;
748
+ }, "image/jpeg", 0.8);
749
+ } catch (e) { /* poster is best-effort; a black frame is not fatal */ }
750
+ }
751
+
711
752
  // --- NPS ----------------------------------------------------------------------
712
753
 
713
754
  function npsStage() {
@@ -829,7 +870,8 @@
829
870
  var data = new FormData();
830
871
  data.append("testimonial[body]", body);
831
872
  if (state.rating) data.append("testimonial[rating]", state.rating);
832
- data.append("testimonial[consent_given]", form.querySelector("input[name=consent]").checked ? "1" : "0");
873
+ data.append("testimonial[consent_given]",
874
+ form.querySelector("input[name=consent]:checked").value === "public" ? "1" : "0");
833
875
  data.append("testimonial[page_url]", window.location.href);
834
876
  data.append("testimonial[source]", state.source);
835
877
  ["name", "email", "title_company"].forEach(function (key) {
@@ -843,6 +885,9 @@
843
885
  if (state.videoBlob) {
844
886
  var extension = (state.videoBlob.type || "video/webm").indexOf("mp4") > -1 ? "mp4" : "webm";
845
887
  data.append("testimonial[video_file]", state.videoBlob, "testimonial." + extension);
888
+ // The poster frame captured at record time, so the stored video shows
889
+ // a real thumbnail instead of a black box in every browser.
890
+ if (state.posterBlob) data.append("testimonial[poster]", state.posterBlob, "poster.jpg");
846
891
  } else if (state.removeExistingVideo) {
847
892
  data.append("testimonial[remove_video]", "1");
848
893
  }
@@ -1012,8 +1057,10 @@
1012
1057
  "{width:100%;box-sizing:border-box;padding:8px;border:1px solid #d1d5db;border-radius:8px;",
1013
1058
  "background:inherit;color:inherit;font:inherit}",
1014
1059
  ".tml-stage textarea{resize:vertical}",
1015
- ".tml-consent{display:flex;gap:8px;align-items:flex-start;margin-bottom:12px;font-size:13px;cursor:pointer}",
1016
- ".tml-consent input{margin-top:3px}",
1060
+ ".tml-consent{margin-bottom:12px;font-size:13px}",
1061
+ ".tml-consent-prompt{margin:0 0 6px;font-weight:600}",
1062
+ ".tml-consent-option{display:flex;gap:8px;align-items:flex-start;margin-bottom:6px;cursor:pointer}",
1063
+ ".tml-consent-option input{margin-top:3px}",
1017
1064
  ".tml-error{color:#dc2626;margin:0 0 12px}",
1018
1065
  ".tml-hint{color:#6b7280;font-size:13px;margin:8px 0}",
1019
1066
  ".tml-actions{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap}",
@@ -74,7 +74,11 @@ module Testimonials
74
74
  autoOpen: auto_open&.to_s,
75
75
  mode: mode.to_s,
76
76
  questions: Testimonials.questions,
77
- consent: Testimonials.consent_text,
77
+ consent: {
78
+ prompt: t(:consent_prompt, 'Where can we use your testimonial?'),
79
+ public: Testimonials.consent_text,
80
+ private: Testimonials.consent_text_private
81
+ },
78
82
  # The signed-in user's current review, if any: the widget opens it
79
83
  # pre-filled for editing instead of starting a second one — rating,
80
84
  # text, consent state, and a playable URL for an attached video.
@@ -82,7 +86,8 @@ module Testimonials
82
86
  rating: existing.rating,
83
87
  body: existing.body,
84
88
  consent: existing.consent_given ? true : false,
85
- videoUrl: existing_video_url(existing)
89
+ videoUrl: existing_video_url(existing),
90
+ posterUrl: existing_poster_url(existing)
86
91
  },
87
92
  video: {
88
93
  enabled: config.video_enabled?,
@@ -108,6 +113,12 @@ module Testimonials
108
113
  "#{Testimonials.config.mount_path.chomp('/')}/#{existing.id}/video"
109
114
  end
110
115
 
116
+ def existing_poster_url(existing)
117
+ return unless existing.poster_attached?
118
+
119
+ "#{Testimonials.config.mount_path.chomp('/')}/#{existing.id}/poster"
120
+ end
121
+
111
122
  # Every user-facing string in the widget, resolved through Rails I18n so
112
123
  # the form follows the app's current locale. Each lookup carries an
113
124
  # English default, so the widget stays fully worded even when a key is
data/lib/testimonials.rb CHANGED
@@ -49,11 +49,23 @@ module Testimonials
49
49
  Array(list).map { |q| q.to_s.gsub('%{app}', app_name) }
50
50
  end
51
51
 
52
+ # The public-use consent line, stored verbatim when a customer allows
53
+ # their testimonial to be shown publicly. Override with config.consent_text.
52
54
  def consent_text
53
55
  config.consent_text.presence ||
54
- I18n.t('testimonials.consent',
55
- default: 'I give permission to use this testimonial across social channels ' \
56
- 'and other marketing efforts.')
56
+ I18n.t('testimonials.consent_public',
57
+ default: 'You can use my testimonial publicly in your marketing and sales.')
58
+ end
59
+
60
+ # The private-use line, stored when a customer allows only internal use.
61
+ def consent_text_private
62
+ I18n.t('testimonials.consent_private',
63
+ default: 'You can only use my testimonial privately in your marketing and sales.')
64
+ end
65
+
66
+ # The exact line the customer agreed to, given their public/private choice.
67
+ def consent_text_for(public_use)
68
+ public_use ? consent_text : consent_text_private
57
69
  end
58
70
 
59
71
  private
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: testimonials
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov