feat: Standardize rich editor across all channels (#12600)
# Pull Request Template ## Description This PR includes, 1. **Channel-specific formatting and menu options** for the rich reply editor. 2. **Removal of the plain reply editor** and full **standardization** on the rich reply editor across all channels. 3. **Fix for multiple canned responses insertion:** * **Before:** The plain editor only allowed inserting canned responses at the beginning of a message, making it impossible to combine multiple canned responses in a single reply. This caused inconsistent behavior across the app. * **Solution:** Replaced the plain reply editor with the rich (ProseMirror) editor to ensure a unified experience. Agents can now insert multiple canned responses at any cursor position. 4. **Floating editor menu** for the reply box to improve accessibility and overall user experience. 5. **New Strikethrough formatting option** added to the editor menu. --- **Editor repo PR**: https://github.com/chatwoot/prosemirror-schema/pull/36 Fixes https://github.com/chatwoot/chatwoot/issues/12517, [CW-5924](https://linear.app/chatwoot/issue/CW-5924/standardize-the-editor), [CW-5679](https://linear.app/chatwoot/issue/CW-5679/allow-inserting-multiple-canned-responses-in-a-single-message) ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ### Screenshot **Dark** <img width="850" height="345" alt="image" src="https://github.com/user-attachments/assets/47748e6c-380f-44a3-9e3b-c27e0c830bd0" /> **Light** <img width="850" height="345" alt="image" src="https://github.com/user-attachments/assets/6746cf32-bf63-4280-a5bd-bbd42c3cbe84" /> ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Pranav <pranav@chatwoot.com> Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>
This commit is contained in:
64
app/services/messages/markdown_renderer_service.rb
Normal file
64
app/services/messages/markdown_renderer_service.rb
Normal file
@@ -0,0 +1,64 @@
|
||||
class Messages::MarkdownRendererService
|
||||
CHANNEL_RENDERERS = {
|
||||
'Channel::Email' => :render_html,
|
||||
'Channel::WebWidget' => :render_html,
|
||||
'Channel::Telegram' => :render_telegram_html,
|
||||
'Channel::Whatsapp' => :render_whatsapp,
|
||||
'Channel::FacebookPage' => :render_instagram,
|
||||
'Channel::Instagram' => :render_instagram,
|
||||
'Channel::Line' => :render_line,
|
||||
'Channel::TwitterProfile' => :render_plain_text,
|
||||
'Channel::Sms' => :render_plain_text,
|
||||
'Channel::TwilioSms' => :render_plain_text
|
||||
}.freeze
|
||||
|
||||
def initialize(content, channel_type)
|
||||
@content = content
|
||||
@channel_type = channel_type
|
||||
end
|
||||
|
||||
def render
|
||||
return @content if @content.blank?
|
||||
|
||||
renderer_method = CHANNEL_RENDERERS[@channel_type]
|
||||
renderer_method ? send(renderer_method) : @content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def commonmarker_doc
|
||||
@commonmarker_doc ||= CommonMarker.render_doc(@content, [:DEFAULT, :STRIKETHROUGH_DOUBLE_TILDE])
|
||||
end
|
||||
|
||||
def render_html
|
||||
markdown_renderer = BaseMarkdownRenderer.new
|
||||
doc = CommonMarker.render_doc(@content, :DEFAULT, [:strikethrough])
|
||||
markdown_renderer.render(doc)
|
||||
end
|
||||
|
||||
def render_telegram_html
|
||||
renderer = Messages::MarkdownRenderers::TelegramRenderer.new
|
||||
doc = CommonMarker.render_doc(@content, [:STRIKETHROUGH_DOUBLE_TILDE], [:strikethrough])
|
||||
renderer.render(doc).gsub(/\n+\z/, '')
|
||||
end
|
||||
|
||||
def render_whatsapp
|
||||
renderer = Messages::MarkdownRenderers::WhatsAppRenderer.new
|
||||
renderer.render(commonmarker_doc).gsub(/\n+\z/, '')
|
||||
end
|
||||
|
||||
def render_instagram
|
||||
renderer = Messages::MarkdownRenderers::InstagramRenderer.new
|
||||
renderer.render(commonmarker_doc).gsub(/\n+\z/, '')
|
||||
end
|
||||
|
||||
def render_line
|
||||
renderer = Messages::MarkdownRenderers::LineRenderer.new
|
||||
renderer.render(commonmarker_doc).gsub(/\n+\z/, '')
|
||||
end
|
||||
|
||||
def render_plain_text
|
||||
renderer = Messages::MarkdownRenderers::PlainTextRenderer.new
|
||||
renderer.render(commonmarker_doc).gsub(/\n+\z/, '')
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
class Messages::MarkdownRenderers::BaseMarkdownRenderer < CommonMarker::Renderer
|
||||
def document(_node)
|
||||
out(:children)
|
||||
end
|
||||
|
||||
def paragraph(_node)
|
||||
out(:children)
|
||||
cr
|
||||
end
|
||||
|
||||
def text(node)
|
||||
out(node.string_content)
|
||||
end
|
||||
|
||||
def softbreak(_node)
|
||||
out(' ')
|
||||
end
|
||||
|
||||
def linebreak(_node)
|
||||
out("\n")
|
||||
end
|
||||
|
||||
def strikethrough(_node)
|
||||
out('<del>')
|
||||
out(:children)
|
||||
out('</del>')
|
||||
end
|
||||
|
||||
def method_missing(method_name, node = nil, *args, **kwargs, &)
|
||||
return super unless node.is_a?(CommonMarker::Node)
|
||||
|
||||
out(:children)
|
||||
cr unless %i[text softbreak linebreak].include?(node.type)
|
||||
end
|
||||
|
||||
def respond_to_missing?(_method_name, _include_private = false)
|
||||
true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
class Messages::MarkdownRenderers::InstagramRenderer < Messages::MarkdownRenderers::BaseMarkdownRenderer
|
||||
def initialize
|
||||
super
|
||||
@list_item_number = 0
|
||||
end
|
||||
|
||||
def strong(_node)
|
||||
out('*', :children, '*')
|
||||
end
|
||||
|
||||
def emph(_node)
|
||||
out('_', :children, '_')
|
||||
end
|
||||
|
||||
def code(node)
|
||||
out(node.string_content)
|
||||
end
|
||||
|
||||
def link(node)
|
||||
out(node.url)
|
||||
end
|
||||
|
||||
def list(node)
|
||||
@list_type = node.list_type
|
||||
@list_item_number = @list_type == :ordered_list ? node.list_start : 0
|
||||
out(:children)
|
||||
cr
|
||||
end
|
||||
|
||||
def list_item(_node)
|
||||
if @list_type == :ordered_list
|
||||
out("#{@list_item_number}. ", :children)
|
||||
@list_item_number += 1
|
||||
else
|
||||
out('- ', :children)
|
||||
end
|
||||
cr
|
||||
end
|
||||
|
||||
def blockquote(_node)
|
||||
out(:children)
|
||||
cr
|
||||
end
|
||||
end
|
||||
36
app/services/messages/markdown_renderers/line_renderer.rb
Normal file
36
app/services/messages/markdown_renderers/line_renderer.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
class Messages::MarkdownRenderers::LineRenderer < Messages::MarkdownRenderers::BaseMarkdownRenderer
|
||||
def strong(_node)
|
||||
out(' *', :children, '* ')
|
||||
end
|
||||
|
||||
def emph(_node)
|
||||
out(' _', :children, '_ ')
|
||||
end
|
||||
|
||||
def code(node)
|
||||
out(' `', node.string_content, '` ')
|
||||
end
|
||||
|
||||
def link(node)
|
||||
out(node.url)
|
||||
end
|
||||
|
||||
def list(_node)
|
||||
out(:children)
|
||||
cr
|
||||
end
|
||||
|
||||
def list_item(_node)
|
||||
out(:children)
|
||||
cr
|
||||
end
|
||||
|
||||
def code_block(node)
|
||||
out(' ```', "\n", node.string_content, '``` ', "\n")
|
||||
end
|
||||
|
||||
def blockquote(_node)
|
||||
out(:children)
|
||||
cr
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
class Messages::MarkdownRenderers::PlainTextRenderer < Messages::MarkdownRenderers::BaseMarkdownRenderer
|
||||
def initialize
|
||||
super
|
||||
@list_item_number = 0
|
||||
end
|
||||
|
||||
def link(node)
|
||||
out(:children)
|
||||
out(' ', node.url) if node.url.present?
|
||||
end
|
||||
|
||||
def strong(_node)
|
||||
out(:children)
|
||||
end
|
||||
|
||||
def emph(_node)
|
||||
out(:children)
|
||||
end
|
||||
|
||||
def code(node)
|
||||
out(node.string_content)
|
||||
end
|
||||
|
||||
def list(node)
|
||||
@list_type = node.list_type
|
||||
@list_item_number = @list_type == :ordered_list ? node.list_start : 0
|
||||
out(:children)
|
||||
cr
|
||||
end
|
||||
|
||||
def list_item(_node)
|
||||
if @list_type == :ordered_list
|
||||
out("#{@list_item_number}. ", :children)
|
||||
@list_item_number += 1
|
||||
else
|
||||
out('- ', :children)
|
||||
end
|
||||
cr
|
||||
end
|
||||
|
||||
def blockquote(_node)
|
||||
out(:children)
|
||||
cr
|
||||
end
|
||||
|
||||
def code_block(node)
|
||||
out(node.string_content, "\n")
|
||||
end
|
||||
|
||||
def header(_node)
|
||||
out(:children)
|
||||
cr
|
||||
end
|
||||
|
||||
def thematic_break(_node)
|
||||
out("\n")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,60 @@
|
||||
class Messages::MarkdownRenderers::TelegramRenderer < Messages::MarkdownRenderers::BaseMarkdownRenderer
|
||||
def initialize
|
||||
super
|
||||
@list_item_number = 0
|
||||
end
|
||||
|
||||
def strong(_node)
|
||||
out('<strong>', :children, '</strong>')
|
||||
end
|
||||
|
||||
def emph(_node)
|
||||
out('<em>', :children, '</em>')
|
||||
end
|
||||
|
||||
def code(node)
|
||||
out('<code>', node.string_content, '</code>')
|
||||
end
|
||||
|
||||
def link(node)
|
||||
out('<a href="', node.url, '">', :children, '</a>')
|
||||
end
|
||||
|
||||
def strikethrough(_node)
|
||||
out('<del>', :children, '</del>')
|
||||
end
|
||||
|
||||
def blockquote(_node)
|
||||
out('<blockquote>', :children, '</blockquote>')
|
||||
end
|
||||
|
||||
def code_block(node)
|
||||
out('<pre>', node.string_content, '</pre>')
|
||||
end
|
||||
|
||||
def list(node)
|
||||
@list_type = node.list_type
|
||||
@list_item_number = @list_type == :ordered_list ? node.list_start : 0
|
||||
out(:children)
|
||||
cr
|
||||
end
|
||||
|
||||
def list_item(_node)
|
||||
if @list_type == :ordered_list
|
||||
out("#{@list_item_number}. ", :children)
|
||||
@list_item_number += 1
|
||||
else
|
||||
out('• ', :children)
|
||||
end
|
||||
cr
|
||||
end
|
||||
|
||||
def header(_node)
|
||||
out('<strong>', :children, '</strong>')
|
||||
cr
|
||||
end
|
||||
|
||||
def softbreak(_node)
|
||||
out("\n")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
class Messages::MarkdownRenderers::WhatsAppRenderer < Messages::MarkdownRenderers::BaseMarkdownRenderer
|
||||
def strong(_node)
|
||||
out('*', :children, '*')
|
||||
end
|
||||
|
||||
def emph(_node)
|
||||
out('_', :children, '_')
|
||||
end
|
||||
|
||||
def code(node)
|
||||
out('`', node.string_content, '`')
|
||||
end
|
||||
|
||||
def link(node)
|
||||
out(node.url)
|
||||
end
|
||||
|
||||
def list(_node)
|
||||
out(:children)
|
||||
cr
|
||||
end
|
||||
|
||||
def list_item(_node)
|
||||
out('- ', :children)
|
||||
cr
|
||||
end
|
||||
|
||||
def blockquote(_node)
|
||||
out('> ', :children)
|
||||
cr
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user