Comment
user:references, table:references, record_id:integer parent:references rely:references content:texthas_many :children and has_many: replies through parent/reply_id. cascade_reply_ids which find all comment IDs that need to be deleted. Code
    1. use data-turbolinks=false to make anchor works without reloading pages
    2. use js.erb file to make operating comments AJAX without reloading pages
# db/migrate/20210227120803_create_comments.rb
class CreateComments < ActiveRecord::Migration[6.0]
  def change
    create_table :comments do |t|
      t.references :user, null: false, foreign_key: true
      t.references :table, null: false, foreign_key: true
      t.integer :record_id, null: true
      # For nested comment, parent links to the top-level comment, reply links to its reply
      # parent comment
      t.references :parent, null: true, foreign_key: {to_table: "comments"}
      # reply comment
      t.references :reply,  null: true, foreign_key: {to_table: "comments"}
      t.text :content, null: false, default: ""
      t.timestamps
    end
    add_index :comments, [:table_id, :record_id]
    add_index :comments, [:table_id, :record_id, :parent_id]
  end
end
# app/models/comment.rb
class Comment < ApplicationRecord
  default_scope { order('id DESC') }
  belongs_to :user
  belongs_to :table
  belongs_to :parent, class_name: "Comment", optional: true
  belongs_to :reply, class_name: "Comment", optional: true
  has_many   :children, class_name: 'Comment', foreign_key: :parent_id, dependent: :destroy
  has_many   :replies, class_name: 'Comment', foreign_key: :reply_id, dependent: :destroy
  # delete_all has better performance if many records. InvalidForeignKey error
  #has_many   :children, class_name: 'Comment', foreign_key: :parent_id, dependent: :delete_all
  #has_many   :replies, class_name: 'Comment', foreign_key: :reply_id, dependent: :delete_all
  validates_presence_of :content
  def cascade_reply_ids
    # top-level comment
    return self.children.pluck(:id) unless self.parent_id?
    # non-top-level comment
    ids = self.replies.pluck(:id); nids=ids.clone
    while nids.any?
      id = nids.pop
      nn = Comment.where(table: self.table, record_id: self.record_id, parent_id:self.parent_id, reply_id: id).pluck(:id)
      ids += nn;
      nids.unshift(*nn)
    end
    return ids;
  end
end
# app/views/comments/_comment_section.html.erb
<div class="comment-container mt-5 small">
  <% if logged_in? %>
    <div class="new-comment" id="new-reply">
      <%= render "comments/new_comment", comment: comment %>
    </div>
  <% end %>
  <div class="comments" id="children">
    <%= render comments %>
  </div>
</div>
# app/views/comments/_comment.html.erb
<% comment_id = "comment-#{comment.id}" %>
<% reply_container_id = "reply-container-#{comment.id}" %>
<% new_reply_id = "new-reply-#{comment.id}" %>
<% children_id = "children-#{comment.id}" %>
<div class="card mx-4 my-1" id="<%=comment_id%>">
  <div class="card-body">
    <div class="card-title">
      <h6><%= link_to comment.user.name, f_user_path(comment.user) %></h6>
    </div>
    <% if (comment.parent_id && comment.reply_id && (comment.parent_id!=comment.reply_id)) %>
      <div class='markdown-body'>
        <% rc = "> @#{comment.reply.user.name} \n >> #{comment.reply.content.truncate(100)}" %>
        <a href="#comment-<%=comment.reply.id%>" data-turbolinks="false">
          <%= markdown(rc) %>
        </a>
      </div>
    <% end %>
    <div class='markdown-body'>
      <%= markdown(comment.content)%>
    </div>
    <% tstyle = fmt_sh_style(comment.children.any?) %>
    <% cid = "children-num-#{comment.id}" %>
    <% cnum = comment.children.count %>
    <% tstr = t_cat([("<div id='#{cid}' style='#{tstyle}'>"+cnum.to_s+"</div>"),t('reply'),">"]).html_safe %>
    <div class="d-flex flex-row justify-content-between mt-2">
      <div>
        <%= link_to tstr, "javascript:void(0)", class: "click-toggle-id", "toggle-id": reply_container_id%>
        <%= fmt_time(comment.created_at) %>
      </div>
      <% if logged_in? %>
        <% if current_user==comment.user  || admin? %>
          <%= link_to fa_destroy_icon, comment_path(comment), class: "#{lh_btn_cls} btn-sm", method: :delete, remote: true, data: { confirm: t('destroy_confirm') } %>
        <% end %>
      <% end %>
    </div>
    <div class="comment-reply-container" id="<%=reply_container_id%>" style="<%=fmt_hide_style%>">
      <div class="new-comment" id="<%=new_reply_id%>">
        <% if logged_in? %>
          <%= render "comments/new_comment", comment: Comment.new(table: comment.table, record_id: comment.record_id, parent_id: comment.parent_id || comment.id, reply_id: comment.id) %>
        <% end %>
      </div>
      <div class="comment-children" id="<%=children_id%>" %>
        <%= render comment.children if comment.children.any? %>
      </div>
    </div>
  </div>
</div>
# app/views/comments/_new_comment.html.erb
<%= form_with(model: comment, remote: true) do |form| %>
  <% if comment.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(comment.errors.count, "error") %> prohibited this comment from being saved:</h2>
      <ul>
        <% comment.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>
  <div class="field form-group row justify-content-between">
    <%= form.hidden_field :table_id %>
    <%= form.hidden_field :record_id %>
    <%= form.hidden_field :parent_id %>
    <%= form.hidden_field :reply_id %>
    <div class="col-md-10 col-xs-12">
      <%= form.text_area :content, rows: 1, class:"form-control", placeholder: t("comment"), required: true %>
    </div>
    <div class="col-md-2 col-xs-12 text-center">
      <%= form.submit t("submit"), class: "btn btn-secondary form-control"%>
    </div>
  </div>
<% end %>
# app/views/comments/create.js.erb
<% if @comment.parent_id.present? %>
  // Clear form
  $('.comment-container #new-reply-<%=@comment.reply_id%> form')[0].reset()
  <% if @comment.reply_id!=@comment.parent_id %>
    $('.comment-container #reply-container-<%=@comment.reply_id%>').hide()
  <% end %>
  // Add comment
  $('.comment-container #children-<%=@comment.parent_id%>').prepend('<%= escape_javascript(render(@comment)) %>')
  // Update children number
  $('.comment-container #children-num-<%=@comment.parent_id%>').attr('style','display: inline-block;')
  <% cnum = @comment.parent.children.count %>
  $('.comment-container #children-num-<%=@comment.parent_id%>').text(<%=cnum%>)
<% else %>
  $('.comment-container #new-reply form')[0].reset()
  $('.comment-container #children').prepend('<%= escape_javascript(render(@comment)) %>')
<% end %>
# app/views/comments/destroy.js.erb
// Remove comments
$('.comment-container #comment-<%=@comment.id%>').remove()
<% if @comment.parent_id.present? %>
  // Remove comments's replies
  <% @reply_ids.each do |id| %>
    $('.comment-container #comment-<%=id%>').remove()
  <% end %>
  // Update children number
  <% cnum = @comment.parent.children.count %>
  // Set reply num
  $('.comment-container #children-num-<%=@comment.parent_id%>').text(<%=cnum%>)
  // Hide if no reply
  <% if cnum <=0 %>
    $('.comment-container #children-num-<%=@comment.parent_id%>').attr('style','display: none;')
  <% end %>
<% end %>