feat(messages): add html formatting to messages

This commit allows users to use the trix editor to send
messages with basic formatting and attachements.

* add active storage
* add actiontext
* add richtext field to messages
* add imageprocessing for message attachements
* add html email layout and adjust translations to use html urls
This commit is contained in:
Philipp Rothmann 2023-02-09 17:18:25 +01:00
parent 666e7934a6
commit 8eda266340
27 changed files with 149 additions and 7 deletions

View file

@ -128,3 +128,5 @@ group :test do
end end
gem "importmap-rails", "~> 1.1" gem "importmap-rails", "~> 1.1"
gem "image_processing", "~> 1.12"

View file

@ -255,6 +255,9 @@ GEM
i18n-spec (0.6.0) i18n-spec (0.6.0)
iso iso
ice_cube (0.16.4) ice_cube (0.16.4)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
importmap-rails (1.1.5) importmap-rails (1.1.5)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
@ -318,6 +321,7 @@ GEM
mime-types (3.4.1) mime-types (3.4.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2022.0105) mime-types-data (3.2022.0105)
mini_magick (4.12.0)
mini_mime (1.1.2) mini_mime (1.1.2)
minitest (5.17.0) minitest (5.17.0)
mono_logger (1.1.1) mono_logger (1.1.1)
@ -496,6 +500,8 @@ GEM
ruby-prof (1.4.5) ruby-prof (1.4.5)
ruby-progressbar (1.11.0) ruby-progressbar (1.11.0)
ruby-units (3.0.0) ruby-units (3.0.0)
ruby-vips (2.1.4)
ffi (~> 1.12)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.3.2) rubyzip (2.3.2)
sass-rails (6.0.0) sass-rails (6.0.0)
@ -631,6 +637,7 @@ DEPENDENCIES
i18n-js (~> 3.0.0.rc8) i18n-js (~> 3.0.0.rc8)
i18n-spec i18n-spec
ice_cube ice_cube
image_processing (~> 1.12)
importmap-rails (~> 1.1) importmap-rails (~> 1.1)
inherited_resources inherited_resources
jquery-rails jquery-rails

View file

@ -0,0 +1,31 @@
/*
* Provides a drop-in pointer for the default Trix stylesheet that will format the toolbar and
* the trix-editor content (whether displayed or under editing). Feel free to incorporate this
* inclusion directly in any other asset bundle and remove this file.
*
*= require trix
*/
/*
* We need to override trix.csss image gallery styles to accommodate the
* <action-text-attachment> element we wrap around attachments. Otherwise,
* images in galleries will be squished by the max-width: 33%; rule.
*/
.trix-content .attachment-gallery > action-text-attachment,
.trix-content .attachment-gallery > .attachment {
flex: 1 0 33%;
padding: 0 0.5em;
max-width: 33%;
}
.trix-content .attachment-gallery.attachment-gallery--2 > action-text-attachment,
.trix-content .attachment-gallery.attachment-gallery--2 > .attachment, .trix-content .attachment-gallery.attachment-gallery--4 > action-text-attachment,
.trix-content .attachment-gallery.attachment-gallery--4 > .attachment {
flex-basis: 50%;
max-width: 50%;
}
.trix-content action-text-attachment .attachment {
padding: 0 !important;
max-width: 100% !important;
}

View file

@ -7,4 +7,5 @@
*= require list.unlist *= require list.unlist
*= require list.missing *= require list.missing
*= require recurring_select *= require recurring_select
*= require actiontext
*/ */

View file

@ -1 +1,3 @@
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "trix"
import "@rails/actiontext"

View file

@ -0,0 +1,14 @@
<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
<% if blob.representable? %>
<%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
<% end %>
<figcaption class="attachment__caption">
<% if caption = blob.try(:caption) %>
<%= caption %>
<% else %>
<span class="attachment__name"><%= blob.filename %></span>
<span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
<% end %>
</figcaption>
</figure>

View file

@ -0,0 +1,3 @@
<div class="trix-content">
<%= yield -%>
</div>

View file

@ -0,0 +1,12 @@
= yield
\
%hr
%ul
%li
%a{href: root_url} Foodsoft
- if FoodsoftConfig[:homepage]
%li
%a{href: FoodsoftConfig[:homepage]} Foodcoop
- if FoodsoftConfig[:help_url]
%li
%a{href: FoodsoftConfig[:help_url]}= t '.help'

View file

@ -67,6 +67,8 @@ module Foodsoft
config.autoloader = :zeitwerk config.autoloader = :zeitwerk
config.active_storage.variant_processor = :mini_magick
# Ex:- :default =>'' # Ex:- :default =>''
# CORS for API # CORS for API

View file

@ -1,2 +1,4 @@
# Pin npm packages by running ./bin/importmap # Pin npm packages by running ./bin/importmap
pin "application", preload: true pin "application", preload: true
pin "trix"
pin "@rails/actiontext", to: "actiontext.js"

View file

@ -1221,6 +1221,7 @@ de:
footer_2_foodsoft: 'Foodsoft: %{url}' footer_2_foodsoft: 'Foodsoft: %{url}'
footer_3_homepage: 'Foodcoop: %{url}' footer_3_homepage: 'Foodcoop: %{url}'
footer_4_help: 'Hilfe: %{url}' footer_4_help: 'Hilfe: %{url}'
help: 'Hilfe'
foodsoft: Foodsoft foodsoft: Foodsoft
footer: footer:
revision: Revision %{revision} revision: Revision %{revision}

View file

@ -1224,6 +1224,7 @@ en:
footer_2_foodsoft: 'Foodsoft: %{url}' footer_2_foodsoft: 'Foodsoft: %{url}'
footer_3_homepage: 'Foodcoop: %{url}' footer_3_homepage: 'Foodcoop: %{url}'
footer_4_help: 'Help: %{url}' footer_4_help: 'Help: %{url}'
help: 'Help'
foodsoft: Foodsoft foodsoft: Foodsoft
footer: footer:
revision: revision %{revision} revision: revision %{revision}

View file

@ -1082,6 +1082,7 @@ es:
layouts: layouts:
email: email:
footer_4_help: 'Ayuda: %{url}' footer_4_help: 'Ayuda: %{url}'
help: 'Ayuda'
footer: footer:
revision: revisión %{revision} revision: revisión %{revision}
header: header:

View file

@ -834,6 +834,7 @@ fr:
email: email:
footer_3_homepage: 'Boufcoop: %{url}' footer_3_homepage: 'Boufcoop: %{url}'
footer_4_help: 'Aide: %{url}' footer_4_help: 'Aide: %{url}'
help: 'Aide'
footer: footer:
revision: révision %{revision} revision: révision %{revision}
header: header:

View file

@ -1194,6 +1194,7 @@ nl:
footer_2_foodsoft: 'Foodsoft: %{url}' footer_2_foodsoft: 'Foodsoft: %{url}'
footer_3_homepage: 'Foodcoop: %{url}' footer_3_homepage: 'Foodcoop: %{url}'
footer_4_help: 'Help: %{url}' footer_4_help: 'Help: %{url}'
help: 'Help'
foodsoft: Foodsoft foodsoft: Foodsoft
footer: footer:
revision: revisie %{revision} revision: revisie %{revision}

View file

@ -0,0 +1,26 @@
# This migration comes from action_text (originally 20180528164100)
class CreateActionTextTables < ActiveRecord::Migration[6.0]
def change
# Use Active Record's configured type for primary and foreign keys
primary_key_type, foreign_key_type = primary_and_foreign_key_types
create_table :action_text_rich_texts, id: primary_key_type do |t|
t.string :name, null: false
t.text :body, size: :long
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
t.timestamps
t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
end
end
private
def primary_and_foreign_key_types
config = Rails.configuration.generators
setting = config.options[config.orm][:primary_key_type]
primary_key_type = setting || :primary_key
foreign_key_type = setting || :bigint
[primary_key_type, foreign_key_type]
end
end

View file

@ -10,7 +10,17 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_01_06_144440) do ActiveRecord::Schema[7.0].define(version: 2023_02_09_105256) do
create_table "action_text_rich_texts", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
t.string "name", null: false
t.text "body", size: :long
t.string "record_type", null: false
t.bigint "record_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
end
create_table "active_storage_attachments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| create_table "active_storage_attachments", id: :integer, charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "record_type", null: false t.string "record_type", null: false

View file

@ -20,8 +20,8 @@ class MessagesController < ApplicationController
@message.group_id = original_message.group_id @message.group_id = original_message.group_id
@message.private = original_message.private @message.private = original_message.private
@message.subject = I18n.t('messages.model.reply_subject', :subject => original_message.subject) @message.subject = I18n.t('messages.model.reply_subject', :subject => original_message.subject)
@message.body = I18n.t('messages.model.reply_header', :user => original_message.sender.display, :when => I18n.l(original_message.created_at, :format => :short)) + "\n" @message.body = I18n.t('messages.model.reply_header', :user => original_message.sender.display, :when => I18n.l(original_message.created_at, :format => :short)) + "\n" \
original_message.body.each_line { |l| @message.body += I18n.t('messages.model.reply_indent', :line => l) } + "<blockquote>" + original_message.body.to_trix_html + "</blockquote>"
else else
redirect_to new_message_url, alert: I18n.t('messages.new.error_private') redirect_to new_message_url, alert: I18n.t('messages.new.error_private')
end end

View file

@ -5,7 +5,7 @@ module MessagesHelper
body = "" body = ""
else else
subject = message.subject subject = message.subject
body = truncate(message.body, :length => length - subject.length) body = truncate(message.body.to_plain_text, :length => length - subject.length)
end end
"<b>#{link_to(h(subject), message)}</b> <span style='color:grey'>#{h(body)}</span>".html_safe "<b>#{link_to(h(subject), message)}</b> <span style='color:grey'>#{h(body)}</span>".html_safe
end end

View file

@ -22,6 +22,8 @@ class Message < ApplicationRecord
validates_presence_of :message_recipients, :subject, :body validates_presence_of :message_recipients, :subject, :body
validates_length_of :subject, :in => 1..255 validates_length_of :subject, :in => 1..255
has_rich_text :body
after_initialize do after_initialize do
@recipients_ids ||= [] @recipients_ids ||= []
@send_method ||= 'recipients' @send_method ||= 'recipients'

View file

@ -110,7 +110,7 @@
= f.input :recipient_tokens, :input_html => { 'data-pre' => User.where(id: @message.recipients_ids).map(&:token_attributes).to_json } = f.input :recipient_tokens, :input_html => { 'data-pre' => User.where(id: @message.recipients_ids).map(&:token_attributes).to_json }
= f.input :private, inline_label: t('.hint_private') = f.input :private, inline_label: t('.hint_private')
= f.input :subject, input_html: {class: 'input-xxlarge'} = f.input :subject, input_html: {class: 'input-xxlarge'}
= f.input :body, input_html: {class: 'input-xxlarge', rows: 13} = f.rich_text_area :body, input_html: {class: 'input-xxlarge', rows: 13}
.form-actions .form-actions
= f.submit class: 'btn btn-primary' = f.submit class: 'btn btn-primary'
= link_to t('ui.or_cancel'), :back = link_to t('ui.or_cancel'), :back

View file

@ -33,7 +33,7 @@
- if @message.can_toggle_private?(current_user) - if @message.can_toggle_private?(current_user)
= link_to t('.change_visibility'), toggle_private_message_path(@message), method: :post, class: 'btn btn-mini' = link_to t('.change_visibility'), toggle_private_message_path(@message), method: :post, class: 'btn btn-mini'
%hr/ %hr/
%p= simple_format(h(@message.body)) .trix-content= @message.body
%hr/ %hr/
%p %p
= link_to t('.reply'), new_message_path(:message => {:reply_to => @message.id}), class: 'btn' = link_to t('.reply'), new_message_path(:message => {:reply_to => @message.id}), class: 'btn'

View file

@ -0,0 +1,11 @@
= raw @message.body
%hr
%ul
- if @message.group
%li= t '.footer_group', group: @message.group.name
%li
%a{href: new_message_url('message[reply_to]' => @message.id)}= t '.reply'
%li
%a{href: message_url(@message)}= t '.see_message_online'
%li
%a{href: my_profile_url}= t '.messaging_options'

View file

@ -138,6 +138,9 @@ de:
Antworten: %{reply_url} Antworten: %{reply_url}
Nachricht online einsehen: %{msg_url} Nachricht online einsehen: %{msg_url}
Nachrichten-Einstellungen: %{profile_url} Nachrichten-Einstellungen: %{profile_url}
reply: Antworten
see_message_online: Nachricht online einsehen
messaging_options: Nachrichten-Einstellungen
footer_group: | footer_group: |
Gesendet an Gruppe: %{group} Gesendet an Gruppe: %{group}
navigation: navigation:

View file

@ -140,6 +140,9 @@ en:
Reply: %{reply_url} Reply: %{reply_url}
See message online: %{msg_url} See message online: %{msg_url}
Messaging options: %{profile_url} Messaging options: %{profile_url}
reply: Reply
see_message_online: See message online
messaging_options: Messaging options
footer_group: | footer_group: |
Sent to group: %{group} Sent to group: %{group}
navigation: navigation:

View file

@ -67,6 +67,9 @@ fr:
Répondre: %{reply_url} Répondre: %{reply_url}
Afficher ce message dans ton navigateur: %{msg_url} Afficher ce message dans ton navigateur: %{msg_url}
Préférences des messages: %{profile_url} Préférences des messages: %{profile_url}
reply: Répondre
see_message_online: Afficher ce message dans ton navigateur
messaging_options: Préférences des messages
simple_form: simple_form:
labels: labels:
settings: settings:

View file

@ -140,6 +140,9 @@ nl:
Antwoorden: %{reply_url} Antwoorden: %{reply_url}
Bericht online lezen: %{msg_url} Bericht online lezen: %{msg_url}
Berichtinstellingen: %{profile_url} Berichtinstellingen: %{profile_url}
reply: Antwoorden
see_message_online: Bericht online lezen
messaging_options: Berichtinstellingen
footer_group: | footer_group: |
Verzenden aan groep: %{group} Verzenden aan groep: %{group}
navigation: navigation: