Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save secretpray/2494534bc0917b5d88797e8af2626a20 to your computer and use it in GitHub Desktop.
Save secretpray/2494534bc0917b5d88797e8af2626a20 to your computer and use it in GitHub Desktop.
Create an avatar from initials or by importing from oauth
  1. Gemfile
gem 'rmagick'

and

bundle
  1. app/jobs/avatar_creation_job.rb
class AvatarCreationJob < ApplicationJob
  queue_as :default

  def perform(user_id, size = 100, avatar_url = nil)
    Users::AvatarService.call(user_id, size, avatar_url)
  end
end

PS Check if the Services path is included in the Ruby on Rails autoloader For example

config.autoload_paths += %W(#{config.root}/app/services)

3.1) app/services/application_service.rb

class ApplicationService
  def call
    raise NotImplementedError
  end

  class << self
    def call(*)
      new(*).call
    end
  end
end

3.2) app/services/users/avatar_service.rb

class Users::AvatarService < ApplicationService
  include ActionView::RecordIdentifier

  attr_reader :user, :size, :avatar_url

  TAILWIND_COLOR_CODES = { 'blue' => '3490dc', 'green' => '38c172',
                           'red' => 'e3342f', 'yellow' => 'ffed4a',
                           'indigo' => '6574cd', 'purple' => '9561e2',
                           'pink' => 'f66d9b', 'teal' => '4dc0b5',
                           'cyan' => '6cb2eb', 'gray' => '6b7280',
                           'amber' => 'f6993f', 'lime' => '84cc16',
                           'emerald' => '10b981', 'rose' => 'ef4444',
                           'fuchsia' => 'd946ef', 'violet' => '8b5cf6',
                           'light-blue' => '60a5fa', 'warm-gray' => '6b7280' }.freeze

  def initialize(user_id, size, avatar_url)
    @user = User.find_by(id: user_id)
    @size = size
    @avatar_url = avatar_url
  end

  def call
    return if skip_avatar_processing?

    process_avatar
    update_navbar_avatar(user)
  end

  private

  def skip_avatar_processing?
    user.avatar.attached? && avatar_url.present? && user_metadata_matches?(user, avatar_url)
  end

  def process_avatar
    if avatar_url.present? && remote_avatar_exists?(avatar_url)
      save_remote_avatar(user, avatar_url, size)
    else
      generate_and_cache_avatar(user, size)
    end
  end

  def remote_avatar_exists?(url)
    uri = URI.parse(url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = (uri.scheme == 'https')

    response = http.request_head(uri.path)
    response.is_a?(Net::HTTPSuccess)
  rescue StandardError
    false
  end

  def user_metadata_matches?(user, avatar_url)
    metadata = user.avatar.blob.metadata || {}
    metadata_source = metadata.dig('avatar_source', 'source')
    metadata_original_url = metadata.dig('avatar_source', 'original_url')
    return false if metadata_source.blank? || metadata_original_url.blank?

    metadata_source == 'remote' && metadata_original_url == avatar_url
  end

  def save_remote_avatar(user, avatar_url, size)
    image_data = Net::HTTP.get(URI.parse(avatar_url))
    user.avatar.attach(io: StringIO.new(image_data), filename: "avatar_#{user.id}.png", content_type: 'image/png')
    update_metadata(user, source: 'remote', original_url: avatar_url)
  end

  def generate_and_cache_avatar(user, size)
    avatar_data = {
      initials: user.initials,
      bgColor: random_color
    }

    image_data = generate_avatar_image(avatar_data, size)
    user.avatar.attach(io: StringIO.new(image_data), filename: "avatar_#{user.id}.png", content_type: 'image/png')
    update_metadata(user, source: 'generated')
  end

  def generate_avatar_image(data, size)
    canvas = Magick::Image.new(size.to_i, size.to_i) do |options|
      options.background_color = data[:bgColor]
    end

    draw = Magick::Draw.new
    draw.annotate(canvas, 0, 0, 0, 0, data[:initials]) do |options|
      options.font_family = 'Arial'
      options.pointsize = size.to_i / 3
      options.fill = 'white'
      options.gravity = Magick::CenterGravity
    end

    canvas.format = 'PNG'
    canvas.to_blob
  end

  def random_color
    "##{TAILWIND_COLOR_CODES.values.sample}"
  end

  def update_metadata(user, source_info)
    metadata = user.avatar.blob.metadata || {}
    metadata['avatar_source'] = source_info
    user.avatar.blob.update(metadata:)
  end

  def update_navbar_avatar(user)
    sleep 0.1 # wait for avatar to be attached
    Turbo::StreamsChannel.broadcast_update_to([user, :service_info],
                                              target: dom_id(user, :desktop_avatar),
                                              partial: 'shared/user_avatar',
                                              locals: { user: })

  end
end
  1. app/models/user.rb
class User < ApplicationRecord

  def self.from_omniauth(omniauth_params)
    provider = omniauth_params.provider
    uid = omniauth_params.uid
    user = User.find_or_initialize_by(provider:, uid:)
    user.email = omniauth_params.info.email
    user.name = omniauth_params.info.name.presence || omniauth_params.info.nickname
    perform_avatar_creation(user, omniauth_params.info.image)
    user.tap(&:save)
  end

  has_one_attached :avatar

  def initials
    name_initials || email_initials || 'N'
  end

  def self.perform_avatar_creation(user, avatar_url = nil)
    user.image = avatar_url if avatar_url.present?
    AvatarCreationJob.perform_later(user.id, 100, avatar_url)
  end

  private

  def name_initials
    return if name.blank?

    name.split.map { _1[0].capitalize }.join
  end

  def email_initials
    return if email.blank?

    email.split('@').first[0].capitalize
  end
end
  1. app/views/layouts/application.html.erb
  <body class="flex flex-col min-h-screen">
    <%= turbo_stream_from [current_user, :service_info] %>
  1. app/views/shared/_navbar.html.erb
<div id='<%= dom_id(current_user, :desktop_avatar) %>'>  
  <%= render 'shared/user_avatar', user: current_user %>
</div>
  1. app/views/shared/_user_avatar.html.erb
<% if user&.avatar.attached? %>
  <%= image_tag url_for(user.avatar),
                class: 'h-8 w-8 rounded-full',
                data: { id: user.id }, 
                alt: user.email %>
<% end %>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment