Railsチュートリアル　返信機能を実装

ゴールデンウイーク中もずっとRailsチュートリアルをこなして、約１か月程かかってようやく完走しました。大変だった・・・。

完走後に拡張機能を自身で実装する章があり、5つほどある内の1つ
返信機能を実装してRailsチュートリアルは区切りをつけることにしました。

[14.4.1 サンプルアプリケーションの機能を拡張する](https://railstutorial.jp/chapters/following_users?version=6.0#sec-guide_to_extensions)


#返信機能
実装にあたり以下をパクらせてもらいました。

■参考サイト
https://stackoverflow.com/questions/57555708/rails-6-how-to-add-jquery-ui-through-webpacker
https://blog.office-aship.info/rails6-webpacker-jquery-ui/


##仕様
- 自分と返信相手と自分をフォローしている人にのみ見える
- 「＠XXXスペース」が入力されたら、返信と判断する　※XXXは一意のユーザー識別キー
- 一意のユーザー識別キーはユーザーの`名前` + `_` + `ID`　とする。この時、空白は`_`に置換する
- 一意のユーザー識別キーの入力はサジェストで支援する

`microposts`テーブルに返信先の一意のユーザー識別キーを保持する列を追加します。列の型は`string`にします。

本当はユーザーの名前が変更されてしまった時に何も処理せずにリンクが切れないように
`integer`にしたかったのですが、そうなるとmicropost登録時に一意のキーから`id`を求めに行く処理が
`where replace(replace((name || '_' || id),' ','_'), '　', '_') = 'Example_User_1'`
のようにDB依存になってしまうので辞めました。(replace関数がないDBがある)

一番スマートにやるには、さらに`users`テーブルに一意のキー用の列を設ければ良さそうですが
今回は`microposts`テーブルの拡張だけでどうにかやろうと思います。

「＠XXXスペース」で判断する都合上、姓と名の間のスペースは`_`に置き換えることにします。
この置き換えた値をユーザーに入力させるのは難しいので、サジェストで支援します。

##実装

###新規列追加
`users`テーブルに新規列を追加します。

```migrationファイル
class AddReplyColumnToMicropost < ActiveRecord::Migration[6.0]
  def change
    add_column :microposts, :in_reply_to, :string
    add_index :microposts, :in_reply_to 
  end
end
```

###jQuery UI導入

```
yarn add jquery-ui
```

```config/webpack/environment.js
const { environment } = require("@rails/webpacker");

const webpack = require("webpack");
environment.plugins.prepend(
  "Provide",
  new webpack.ProvidePlugin({
    $: "jquery/src/jquery",
    jQuery: "jquery/src/jquery",
    Popper: ['popper.js', 'default']
  })
);

environment.toWebpackConfig().merge({
  resolve: {
    alias: {
      'jquery': 'jquery/src/jquery'
    }
  }
});

module.exports = environment;
```
以下は効いてないかも・・・

```app/javascript/packs/application.js
require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
require("jquery");
require("jquery-ui/ui/widgets/autocomplete");
import "bootstrap";
```

`app/views/layouts/_head.html.erb`

```erb
<head>
    <title><%= full_title(yield(:title)) %></title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag    'application', media: 'all',
                               'data-turbolinks-track': 'reload' %>
    <%= stylesheet_link_tag '//ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/le-frog/jquery-ui.min.css' %>                               
    <%= javascript_pack_tag 'application',
                               'data-turbolinks-track': 'reload' %>
</head>
```
`<%= stylesheet_link_tag '//ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/le-frog/jquery-ui.min.css' %> `の行がないとスタイルが適用されませんでした。

###ルーター

```config/routes.rb
Rails.application.routes.draw do

  get 'password_resets/new'
  get 'password_resets/edit'
  get 'sessions/new'
  root 'static_pages#home'
  get  '/help',    to: 'static_pages#help', as: 'help'
  get  '/about',   to: 'static_pages#about'
  get  '/contact', to: 'static_pages#contact'
  get  '/signup',  to: 'users#new'
  get  '/login',   to: 'sessions#new'
  post  '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users do
    member do
      get :following, :followers
    end
    collection do
      get :auto_complete
    end
  end
  resources :users
  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
  resources :microposts,          only: [:create, :destroy]
  resources :relationships,       only: [:create, :destroy]
end
```
以下が増えただけですね

```rb
    collection do
      get :auto_complete
    end
```

###フォーム
ほぼ丸パクリです。
`app/views/shared/_micropost_form.html.erb`


```erb
<%= form_with(model: @micropost, local: true) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost...", id: "micropost_content" %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
  <span class="image">
    <%= f.file_field :image, accept: "image/jpeg,image/gif,image/png" %>
  </span>
<% end %>

<script type="text/javascript">
  $("[name='micropost[image]']").bind("change", function() {
    var size_in_megabytes = this.files[0].size/1024/1024;
    if (size_in_megabytes > 5) {
      alert("Maximum file size is 5MB. Please choose a smaller file.");
      $("[name='micropost[image]']").val("");
    }
  });
  $('#micropost_content').autocomplete({
      source: "/users/auto_complete.json",
      delay: 300,
      minLength: 2,
      focus: function(event, ui) {
        $("#micropost_content").val(ui.item.value);
        return false;
      },
      select: function(event, ui) {
        $('#micropost_content').val(ui.item.value);
        return false;
      }
    }).data("ui-autocomplete")._renderItem = function(ul, item) {
      return $("<li>").attr("data-value", item.value).data("ui-autocomplete-item", item).append("<a>" + item.caption + "</a>").appendTo(ul);
    };
</script>
```

`<%= f.text_area :content, placeholder: "Compose new micropost...", id: "micropost_content" %>`の行と
`$('#micropost_content').autocomplete({`の行から下が変更箇所です。


###モデル
一意のキーで検索しています。


```user.rb
    def feed
        following_ids = "SELECT followed_id FROM relationships WHERE follower_id = :user_id"
        Micropost.where("user_id IN (#{following_ids})
                     OR user_id = :user_id OR in_reply_to = :reply_user", {user_id: id, reply_user: unique_user_id })
    end

    def unique_user_id
        "#{name.gsub(/\s/, "_")}_#{id}"
    end
```

```micropost.rb
  before_save   :extract_reply_user
・
・
・
  private
    def extract_reply_user
        if reply_user = content.match(/(@[^\s]+)\s/)
            # 存在チェックはしていない・・・
            self.in_reply_to = reply_user[0][1..-2]
        end     
    end
```
正規表現で取得します。

###コントローラー
`app/controllers/users_controller.rb`

```rb
  def update
    before_unique_user_id = @user.unique_user_id
    if @user.update(user_params)
      # マイクロポストのreplyも更新する
      microposts = Micropost.where(in_reply_to: before_unique_user_id)
      @user.reload
      microposts.update_all(in_reply_to: @user.unique_user_id)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end
```
もしユーザーの名称が変更されてもリンクが追随されるように、ユーザー更新時はmicropostsのreply列を更新します。

#GitHub
https://github.com/GodPhwng/sample_app

汚いコミット履歴がいっぱいあります・・・