Emojis from Scratch

Episode #227 by Teacher's Avatar David Kimura

Summary

In this episode, we look at attaching emojis to our comments model, allowing them to fill in some emotional cues.
rails ruby 19:35

Resources

Summary

# Terminal
rails g scaffold comments user:belongs_to
rails g model emote user:belongs_to comment:belongs_to emoji
rails action_text:install
rails db:migrate

# models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :comments, dependent: :destroy
  has_many :emotes, dependent: :destroy
  # has_many :emoted_comments, through: :emotes, class_name: "Comment"
end

# models/emote.rb
class Emote < ApplicationRecord
  belongs_to :user
  belongs_to :comment
end

# models/emoji.rb
class Emoji
  # [{key: emoji.png, text: Emoji}, {}]
  # Emoji.all
  def self.all
    self.new.all
  end

  def all
    list_of_emojis
  end

  private

  def list_of_emojis
    Dir.children(emojis_path).map { |file| emoji_hash(file) }
  end

  def emojis_path
    Rails.root.join('app', 'assets', 'images', 'emojis')
  end

  def emoji_hash(file)
    { key: file, text: humanized(file) }
  end

  def humanized(file)
    basename(file).humanize
  end

  def basename(file)
    File.basename(file, File.extname(file))
  end
end

# models/comment.rb
class Comment < ApplicationRecord
  belongs_to :user
  has_rich_text :content

  has_many :emotes, dependent: :destroy
  # has_many :emoters, through: :emotes, class_name: "User"

  def emotes_size(key)
    self.emotes.select { |e| e.emoji == key }.size
  end
end

# views/comments/_form.html.erb
<%= form_with(model: comment, local: true) do |form| %>
  <div class="field">
    <%= form.rich_text_area :content %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

# views/comments/index.html.erb
<% @comments.each do |comment| %>
  <div class='comment'>
    <%= comment.content %>
    <em class='meta'>
      <%= comment.user.name %> |
      <%= comment.created_at %> |
      <%= link_to 'Destroy', comment, method: :delete, data: { confirm: 'Are you sure?' } %>
    </em>
    <div class='emojis'>
      <% Emoji.all.each do |emoji| %>
        <% size = comment.emotes_size(emoji[:key]) %>
        <%= link_to comment_emote_path(comment, emote: emoji[:key]), class: "emoji #{size.zero? ? 'emoji-gray' : ''}" do %>
          <%= image_tag File.join('emojis', emoji[:key]), size: '25x25', title: emoji[:text] %>
          <%= content_tag :span, size, class: 'count' %>
        <% end %>
      <% end %>
    </div>
  </div>
<% end %>

<%= render 'form', comment: Comment.new %>

# controllers/comments_controller.rb
class CommentsController < ApplicationController
  before_action :set_comment, only: :destroy

  def index
    @comments = Comment.includes(:emotes).all
  end

  def create
    @comment = current_user.comments.new(comment_params)

    if @comment.save
      redirect_to comments_path, notice: 'Comment was successfully created.'
    else
      render :new
    end
  end

  def destroy
    @comment.destroy
    redirect_to comments_url, notice: 'Comment was successfully destroyed.'
  end

  private

  def set_comment
    @comment = current_user.comments.find(params[:id])
  end

  def comment_params
    params.require(:comment).permit(:content)
  end
end

# controllers/emotes_controller.rb
class EmotesController < ApplicationController
  def show
    comment = Comment.find_by(id: params[:comment_id])
    emote = current_user.emotes.find_or_initialize_by(comment: comment, emoji: params[:emote])
    if emote.new_record?
      emote.save
    else
      emote.destroy
    end
    redirect_to root_path
  end
end

# routes.rb
  resources :comments do
    resource :emote, only: :show
  end

# db/migrate/20200202022753_create_emotes.rb
class CreateEmotes < ActiveRecord::Migration[6.0]
  def change
    create_table :emotes do |t|
      t.belongs_to :user, null: false, foreign_key: true
      t.belongs_to :comment, null: false, foreign_key: true
      t.string :emoji, null: false

      t.timestamps
    end
  end
end

# comments.scss
.comment {
  border: 1px dashed gray;
  margin-bottom: 10px;
  padding: 10px;
}

.comment .meta,
.comment .meta a {
  color: gray;
}

.emojis {
  margin-top: 10px;
}

.emoji img {
  padding: 5px;
}

.emoji.emoji-gray img {
  filter: grayscale(100%);
}

.emoji.emoji-gray img:hover {
  filter: grayscale(0%);
}

.emoji {
  position: relative;
}

.emoji .count {
  position: absolute;
  top: 10;
  left: 0;
  padding: 3px;
  background-color: #269ba5;
  text-decoration: none;
  color: #ffffff;
  border-radius: 20px;
  font-size: 10px;
}