Calvert's murmur

使用 Mailboxer 實作站內信系統

2015-05-20

約 21466 字 / 需 119 分鐘閱讀

Mailboxer 是一個 Rails gem,為 social_stream 框架用來建立社群網路的一部分。它是具備一些通用方法的訊息系統,允許任何 model 擔任傳遞訊息的角色。

使用 Mailboxer,你可以與一或多個收件人建立對話(訊息會被歸納到寄件匣、收件匣及垃圾桶)並透過電子郵件發送通知。它甚至可以在不同 model 間發送訊息和附件!唯一的缺點是缺乏文件,所以我希望這邊文章是有幫助的。

我們要討論的範例程式為:

  • 使用 Devise 做基本身份驗證
  • 使用 Gravatar 讓使用者管理頭像
  • 整合 Mailboxer
  • 使用圖形介面來建立/回覆對話(使用 Bootstrap 樣式及 Chosen jQuery 插件)
  • 顯示資料夾,並允許在它們之間輕鬆切換
  • 允許將對話標示為已讀、放到垃圾桶或還原,也可以清理垃圾桶。
  • 設定電子郵件通知

此範例將使用 Rails 4,但幾乎相同的解決方案也可以實作於 Rails 3.2(Mailboxer 不再支援 3.1 版)。

原始碼可以在 Github 找到。

前置準備

假設我們要建立內部的私人訊息系統提供同事討論不同主旨。這個系統應該允許使用者與無數個收件人建立對話、提供通知系統並允許刪除舊的對話。

建立名為 Synergy 不含預設測試套件的 Rails 應用程式:

$ rails new Synergy -T

在你的 Gemfile 加入以下的 Gem(我會使用 Bootstrap,但你可以使用任何其它的 CSS 框架、自行設計或者跳過美化網站的步驟):

Gemfile
[...] gem 'bootstrap-sass' gem 'kaminari' [...]

執行

$ bundle install
$ rails generate kaminari:views bootstrap3

然後加入 Bootstrap 檔案:

app/assets/stylesheets/application.scss
@import "bootstrap-sprockets"; @import "bootstrap";
app/assets/javascripts/application.js
//= require bootstrap-sprockets

接著調整 layout:

app/views/layouts/application.html.erb
[...] <div class="container"> <% flash.each do |key, value| %> <div class="alert alert-<%= key %>"> <%= value %> </div> <% end %> <div class="page-header"> <h1><%= yield :page_header %></h1> </div> <%= yield %> </div> [...]

讓我們新增 helper 來方便呈現頁面標題:

app/helpers/application_helper.rb
[...] def page_header(text) content_for(:page_header) { text.to_s } end [...]

身份驗證

實作訊息功能之前,我們需要一個 model 來傳遞訊息。建立 User model:

$ rails g model User name:string
$ rake db:migrate

你可以使用任何類型的身份驗證,但我喜歡 Devise。Devise 的基本設定非常簡單,並有大量文件協助您進一步自訂設定。

加入新的 gem:

Gemfile
[...] gem 'devise' [...]

並安裝它:

$ bundle install

現在,我們可以利用 Devise 的產生器來幫我們做一些工作:

$ rails generate devise:install

請務必閱讀安裝後的訊息來完成一些額外的步驟。具體來說,你需要為 development 和 production 調整 config.action_mailer.default_url_options 設定,因為它會被用來發送郵件給使用者(例如,幫助他們恢復遺忘的密碼)。
請注意,電子郵件在 development 時不會發送,除非你在 config/environments/development.rb 設定 config.action_mailer.perform_deliveries = true

這裡有關於如何設定 ActionMailer 的一些範例

當你準備好後,執行以下指令使用 Devise 建立 User model:

$ rails generate devise User
$ rake db:migrate

你可能想要在套用變更前檢查指令產生的遷移檔,並加入更多欄位到資料表(啟用 Confirmable 或 Lockable 模組)。你還需要調整 model 內相應的設定。

最後,執行以下指令來複製 Devise 的 views 到你的專案,以便稍作修改:

$ rails generate devise:views

要能夠讓使用者變更他們的名字,所以新增一個欄位到註冊表單:

app/views/devise/registrations/new.html.erb
[...] <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> [...]

同樣的將此欄位加入 app/views/devise/registrations/edit.html.erb(或者將它重構為 partial),讓使用者可以在註冊時提供名字,並在稍後編輯它。

由於 Rails 4 引進了新的保護機制 Strong Parameters,需要設定允許 :name 參數可以傳遞:

app/controllers/application_controller.rb
[...] before_action :configure_permitted_parameters, if: :devise_controller? protected def configure_permitted_parameters devise_parameter_sanitizer.for(:sign_up) << :name devise_parameter_sanitizer.for(:account_update) << :name end [...]

devise_controller?方法是由 Devise 提供。在這裡,我們允許建立及編輯帳號時傳遞 :name 屬性。如果你忘了這樣做,使用者將無法設定自己的名字。

此時,你也可以修改 view 的樣式。我不會涵蓋這一步驟,因為它不是太難且高度依賴於你的設定(無論你是不是使用 Bootstrap)。如果你決定使用 Bootstrap,Devise 所產生的訊息將不會有樣式。為了解決這個問題,使用 SASS @extend 方法,如下:

app/assets/stylesheets/application.scss
[...] .alert-notice { @extend .alert-success; } .alert-alert { @extend .alert-warning; }

整合 Mailboxer

太好了,我們準備進行主要的任務-整合及設定 Mailboxer。

首先,加入新的 gem:

Gemfile
[...] gem 'mailboxer' [...]

並安裝它:

$ bundle install

產生和套用所有必要的遷移並建立初始化檔案:

$ rails generate mailboxer:install
$ rake db:migrate

查看 config/initializers/mailboxer.rb 檔案看你能修改哪些選項。現在,先維持檔案內的設定,稍後我們會設定寄送電子郵件通知。

需要稍作調整 model 來配置 Mailboxer 的功能:

app/models/user.rb
[...] acts_as_messageable [...]

顯示對話

指南所建議,為 Mailboxer 建立圖形介面的最佳方法是建立兩個 controller:一個用於訊息,一個用於對話。獨立的訊息會被歸類為對話。稍後,你會看到對話可以是不同類型。

首先建立對話 controller:

$ rails generate controller conversations
app/controllers/conversations_controller.rb
class ConversationsController < ApplicationController before_action :authenticate_user! before_action :get_mailbox def index @conversations = @mailbox.inbox.page(params[:page]) end private def get_mailbox @mailbox ||= current_user.mailbox end end

每個使用者都有自己的信箱,反過來說,又分為收件匣、寄件匣及垃圾桶。目前,我們先關注在收件匣就好。

authenticate_user!是 Devise 的一部分。我們只希望通過驗證的使用者存取應用程式,因此它設定為 before_action。如果使用者沒有通過驗證,他將被導向到登入頁面。

如你所見,我也使用了 kaminari 所提供的 page 方法。

新增路由(其它 controller 方法很快就會加入):

config/routes.rb
[...] resources :conversations, only: [:index, :show, :destroy] [...]

並修改 view:

app/views/conversations/index.html.erb
<% page_header "Your Conversations" %> <ul class="list-group"> <%= render partial: 'conversations/conversation', collection: @conversations %> </ul> <%= paginate @conversations %>

page_header 是我們先前建立的 helper 方法。paginate 會顯示分頁控制項(只有超過一頁時會顯示)。

我們必須指定 partial 參數,因為 @conversationsMailboxer::Conversation::ActiveRecord_Relation 的一個實例,因此 Rails 預設會在 mailboxer/conversations 目錄尋找 _conversation

新增 partial:

app/views/conversations/_conversation.html.erb
<li class="list-group-item clearfix"> <%= link_to conversation.subject, conversation_path(conversation) %> </li>

每個對話都有一個主旨和一些將被顯示於頁面上的訊息。

新增選單到 layout:

app/views/layouts/application.html.erb
[...] <nav class="navbar navbar-inverse"> <div class="container"> <div class="navbar-header"> <%= link_to 'Synergy', root_path, class: 'navbar-brand' %> </div> <ul class="nav navbar-nav"> <% if user_signed_in? %> <li><%= link_to 'Edit Profile', edit_user_registration_path %></li> <li><%= link_to 'Your Conversations', conversations_path %></li> <li><%= link_to 'Log Out', destroy_user_session_path, method: :delete %></li> <% else %> <li><%= link_to 'Log In', new_user_session_path %></li> <% end %> </ul> </div> </nav> [...]

user_signed_in? 方法以及大部分的路由都是由 Devise 提供。

接下來是 show 動作:

app/controllers/conversations_controller.rb
class ConversationsController < ApplicationController before_action :authenticate_user! before_action :get_mailbox before_action :get_conversation, except: [:index] def index @conversations = @mailbox.inbox.page(params[:page]) end def show end private def get_mailbox @mailbox ||= current_user.mailbox end def get_conversation @conversation ||= @mailbox.conversations.find(params[:id]) end end

你可能知道,當查詢不到資料時會拋出例外錯誤。這就是我們想要的,但是應該要處理例外錯誤。為了簡單起見,我們使用 rescue_from 方法:

app/controllers/application_controller.rb
[...] rescue_from ActiveRecord::RecordNotFound do flash[:warning] = 'Resource not found.' redirect_back_or root_path end def redirect_back_or(path) redirect_to request.referer || path end [...]

我們只是將使用者導向並顯示警告訊息。如果 referer 欄位沒有設定(例如,使用者安裝了插件清除此欄位),他們會被導向到 root_path

接著,編輯 view:

app/views/conversations/show.html.erb
<% page_header "Conversation" %> <div class="panel panel-default"> <div class="panel-heading"><%= @conversation.subject %></div> <div class="panel-body"> <div class="messages"> <% @conversation.receipts_for(current_user).each do |receipt| %> <% message = receipt.message %> <%= message.sender.name %> says at <%= message.created_at.strftime("%F %T") %> <%= message.body %> <% end %> </div> </div> </div>

我們呈現了每個訊息的寄件者的名字、建立時間及訊息內容。讓我們來修改一下 .messages 容器的樣式,讓它不要變得太高:

app/assets/stylesheets/application.scss
[...] .messages { max-height: 400px; overflow-y: auto; margin-bottom: 1em; margin-top: 1em; }

不錯,一些基本的 view 都已經存在了。然而,我們還缺乏了一些重要的東西:

  • 使用者應該知道在和誰對話
  • 使用者需要可以建立新的對話
  • 使用者需要可以回覆對話
  • 寄件匣和垃圾桶應該顯示在對話頁面
  • 使用者應該能將對話標示為已讀

顯示使用者頭像

雖然這和 Mailboxer 無關,我認為顯示頭像會讓我們的應用程式看起來更漂亮。然而,允許使用者直接上傳頭像到應用程式有點小題大作,讓我們使用 Gravatar 並透過 gravatar_image_tag 整合到 Rails。

加入新的 gem:

Gemfile
[...] gem 'gravatar_image_tag' [...]

並執行

$ bundle install

同樣的,新增 helper 來方便呈現頭像:

app/helpers/application_helper.rb
[...] def gravatar_for(user, size = 30, title = user.name) image_tag gravatar_image_url(user.email, size: size), title: title, class: 'img-rounded' end [...]

建立單獨的 partial 來呈現對話內收件人的頭像(除了目前的使用者):

app/views/conversations/_participants.html.erb
<% conversation.participants.each do |participant| %> <% unless participant == current_user %> <%= gravatar_for participant %> <% end %> <% end %>

編輯以下的 view:

app/views/conversations/show.html.erb
<% page_header "Conversation" %> <p>Chatting with <%= render 'conversations/participants', conversation: @conversation %> </p> <div class="panel panel-default"> <div class="panel-heading"><%= @conversation.subject %></div> <div class="panel-body"> <div class="messages"> <% @conversation.receipts_for(current_user).each do |receipt| %> <div class="media"> <% message = receipt.message %> <div class="media-left"> <%= gravatar_for message.sender, 45, message.sender.name %> </div> <div class="media-body"> <h6 class="media-heading"><%= message.sender.name %> says at <%= message.created_at.strftime("%F %T") %> </h6> <%= message.body %> </div> </div> <% end %> </div> </div> </div>
app/views/conversations/_conversation.html.erb
<li class="list-group-item clearfix"> <%= link_to conversation.subject, conversation_path(conversation) %> <p><%= render 'conversations/participants', conversation: conversation %></p> </li>

顯示對話內最後一則訊息及它的建立時間:

app/views/conversations/_conversation.html.erb
<li class="list-group-item clearfix"> <%= link_to conversation.subject, conversation_path(conversation) %> <p><%= render 'conversations/participants', conversation: conversation %></p> <p> <%= conversation.last_message.body %> <small>(<span class="text-muted"><%= conversation.last_message.created_at.strftime("%F %T") %></span>)</small> </p> </li>

我們已經完成頭像了。是時候讓使用者建立新的對話。


建立對話

建立對話實際上意味著建立帶有主旨的新訊息(雖然這是可選的)。這表示需要新的 controller:

$ rails generate controller messages
app/controllers/messages_controller.rb
class MessagesController < ApplicationController before_action :authenticate_user! def new end def create recipients = User.where(id: params['recipients']) conversation = current_user.send_message(recipients, params[:message][:body], params[:message][:subject]).conversation flash[:success] = "Message has been sent!" redirect_to conversation_path(conversation) end end

在 create 動作內,尋找收件人(存放在 params[‘recipients’] 中)並利用 Mailboxer 的 send_message 方法,傳入收件人、訊息和主旨。稍後,我們將啟用電子郵件通知,以便讓使用者知道收到新訊息了。

現在來編輯 view:

app/views/messages/new.html.erb
<% page_header "Start Conversation" %> <%= form_tag messages_path, method: :post do %> <div class="form-group"> <%= label_tag 'message[subject]', 'Subject' %> <%= text_field_tag 'message[subject]', nil, class: 'form-control', required: true %> </div> <div class="form-group"> <%= label_tag 'message[body]', 'Message' %> <%= text_area_tag 'message[body]', nil, cols: 3, class: 'form-control', required: true %> </div> <div class="form-group"> <%= label_tag 'recipients', 'Choose recipients' %> <%= select_tag 'recipients', recipients_options, multiple: true, class: 'form-control' %> </div> <%= submit_tag 'Send', class: 'btn btn-primary' %> <% end %>

recipients_options 是 helper 方法,我們需要建立它:

app/helpers/messages_helper.rb
module MessagesHelper def recipients_options options_for_select(User.all.map { |user| [user.name, user.id] }) end end

別忘了設定路由:

config/routes.rb
[...] resources :messages, only: [:new, :create] [...]

接著在 conversations#index 頁面顯示「Start conversation」連結:

app/views/conversations/index.html.erb
<% page_header "Your Conversations" %> <p><%= link_to 'Start conversation', new_message_path, class: 'btn btn-lg btn-primary' %></p> [...]

技術上來說,一切都已經就緒可以發佈你的第一則訊息。你可以傳遞給自己或是註冊另一個帳號來模擬有兩個使用者的情況。

然而,選擇收件人不是很方便。目前,已經呈現了基本的選擇欄位,但如果有很多使用者時,要在列表中找到某個人是很麻煩的。我們可以使用 Chosen 來強化這個欄位,它是讓下拉選單更加人性化的一個 jQuery 插件。有個 chosen-rails gem 可以輕鬆的將此插件整合到 Rails 應用程式。

將這個 gem 加到 Gemfile:

Gemfile
[...] gem 'chosen-rails' [...]

我也指定了 sass-railscoffee-rails 的版本,因為有與 application.scss 檔案相關的 bug

Gemfile
[...] gem 'sass-rails', '~> 4.0.5' gem 'coffee-rails', '~> 4.1.0' gem 'jquery-turbolinks' [...]

同時也使用了 jquery-turbolinks gem,當使用 Turbolinks 時可恢復預設的 jQuery page load 事件。

別忘了執行

$ bundle install

然後將 Chosen 加到 application.jsapplication.scss

app/assets/javascripts/application.js
[...] //= require jquery.turbolinks //= require chosen-jquery [...]
app/assets/stylesheets/application.scss
[...] @import "chosen"; [...]

接著將 .chosen-it 類別加到我們的 select 標籤:

app/views/messages/new.html.erb
[...] <div class="form-group"> <%= label_tag 'recipients', 'Choose recipients' %> <%= select_tag 'recipients', recipients_options, multiple: true, class: 'form-control chosen-it' %> </div> [...]

並將所有此類別的元素都裝上 Chosen 的功能:

app/assets/javascripts/messages.coffee
$ -> $('.chosen-it').chosen()

現在重載伺服器,到 conversations/new 頁面,會發現嶄新的 select 標籤。這用起來更加方便,不是嗎?

我們可以更進一步的在 select 標籤內使用者名字前顯示頭像。有個 Chosen 的擴充插件 Image-Select。只要將 ImageSelect.jquery.jsImageSelect.css 放到你的專案並分別在 application.js 和 application.scss 引入。然後,稍微修改 helper 方法:

app/helpers/messages_helper.rb
module MessagesHelper def recipients_options options_for_select(User.all.map { |user| [user.name, user.id, { 'data-img-src' => gravatar_image_url(user.email, size: 50) }] }) end end

接著再重載伺服器並確認成果。非常棒!


回覆對話

現在,使用者可以建立對話,但是沒有辦法回覆!為了解決這個問題,我們需要另一個表單及 controller 方法,以及新的路由:

app/views/conversations/show.html.erb
[...] <%= form_tag reply_conversation_path(@conversation), method: :post do %> <div class="form-group"> <%= text_area_tag 'body', nil, cols: 3, class: 'form-control', placeholder: 'Type something...', required: true %> </div> <%= submit_tag "Send Message", class: 'btn btn-primary' %> <% end %>

註:你也可以加入另一個文字欄位讓使用者新增主旨。

app/controllers/conversations_controller.rb
[...] def reply current_user.reply_to_conversation(@conversation, params[:body]) flash[:success] = 'Reply sent' redirect_to conversation_path(@conversation) end [...]

Mailboxer 的 reply_to_conversation 方法讓回覆對話變得很輕鬆。它接受對話的回覆訊息、主旨(可選)及些許其他的參數。需要注意的是,如果對話被移動到垃圾桶(我們將在稍後處理),預設會被復原。可以去看看原始碼取得更多資訊。

config/routes.rb
[...] resources :conversations, only: [:index, :show, :destroy] do member do post :reply end end [...]

很好,基本的聊天系統已可啟動並執行!


實作寄件匣及垃圾桶

目前,我們只有顯示使用者的收件匣。然而,顯示寄件匣與垃圾桶是個好主意。

也許要決定該顯示哪個資料夾最簡單的方式是使用 GET 參數,所以讓我們來調整 controller:

app/controllers/conversations_controller.rb
[...] before_action :get_box, only: [:inbox] def index if @box.eql? "inbox" @conversations = @mailbox.inbox elsif @box.eql? "sent" @conversations = @mailbox.sentbox else @conversations = @mailbox.trash end @conversations = @conversations.page(params[:page]) end [...] private [...] def get_box if params[:box].blank? or !["inbox", "sent", "trash"].include?(params[:box]) params[:box] = 'inbox' end @box = params[:box] end [...]

新的私有方法 get_box 用來取得所需的資料夾。

在 view 的地方,如果你使用的是 Bootstrap,我建議使用垂直導覽列來呈現資料夾。此外,目前所在的資料夾應該被高亮。建立 helper 方法來處理這個:

app/helpers/conversations_helper.rb
module ConversationsHelper def mailbox_section(title, current_box, opts = {}) opts[:class] = opts.fetch(:class, '') opts[:class] += ' active' if title.downcase == current_box content_tag :li, link_to(title.capitalize, conversations_path(box: title.downcase)), opts end end

這個方法需要連結的標題(也會被用於 GET 的參數)、目前開啟的資料夾及要直接傳遞給 content_tag 方法的 hash 格式選項。然後檢查 opts 是否已有類別的屬性。沒有的話就設為空字串,並在目前的資料夾的類別附加 active 類別。

修改 view:

app/views/conversations/index.html.erb
<% page_header "Your Conversations" %> <p><%= link_to 'Start conversation', new_message_path, class: 'btn btn-lg btn-primary' %></p> <div class="row"> <div class="col-sm-3"> <ul class="nav nav-pills nav-stacked"> <%= mailbox_section 'inbox', @box %> <%= mailbox_section 'sent', @box %> <%= mailbox_section 'trash', @box %> </ul> </div> <div class="col-sm-9"> <ul class="list-group"> <%= render partial: 'conversations/conversation', collection: @conversations %> </ul> <%= paginate @conversations %> </div> </div>

下一步是在每個尚未丟棄到垃圾桶的對話加入「Move to trash」按鈕。對於丟棄在垃圾桶的對話,應該顯示「Restore」按鈕。

app/views/conversations/_conversation.html.erb
<li class="list-group-item clearfix"> <%= link_to conversation.subject, conversation_path(conversation) %> <div class="btn-group-vertical pull-right"> <% if conversation.is_trashed?(current_user) %> <%= link_to 'Restore', restore_conversation_path(conversation), class: 'btn btn-xs btn-info', method: :post %> <% else %> <%= link_to 'Move to trash', conversation_path(conversation), class: 'btn btn-xs btn-danger', method: :delete, data: { confirm: 'Are you sure?' } %> <% end %> </div> <p><%= render 'conversations/participants', conversation: conversation %></p> <p> <%= conversation.last_message.body %> <small>(<span class="text-muted"><%= conversation.last_message.created_at.strftime("%F %T") %></span>)</small> </p> </li>

增加相應的方法:

app/controllers/conversations_controller.rb
[...] def destroy @conversation.move_to_trash(current_user) flash[:success] = 'The conversation was moved to trash.' redirect_to conversations_path end def restore @conversation.untrash(current_user) flash[:success] = 'The conversation was restored.' redirect_to conversations_path end [...]

move_to_trashuntrash 是由 Mailboxer 提供的兩個方法,從命名就能知道用途。

修改路由:

config/routes.rb
[...] resources :conversations, only: [:index, :show, :destroy] do member do post :reply post :restore end end [...]

那麼「Empty trash」按鈕呢?很簡單:

app/views/conversations/index.html.erb
[...] <div class="col-sm-9"> <% if @box == 'trash' %> <p><%= link_to 'Empty trash', empty_trash_conversations_path, class: 'btn btn-danger', method: :delete, data: { confirm: 'Are you sure?' } %></p> <% end %> <ul class="list-group"> <%= render partial: 'conversations/conversation', collection: @conversations %> </ul> <%= paginate @conversations %> </div> [...]

以及相應的方法:

app/controllers/conversations_controller.rb
[...] before_action :get_conversation, except: [:index, :empty_trash] [...] def empty_trash @mailbox.trash.each do |conversation| conversation.receipts_for(current_user).update_all(deleted: true) end flash[:success] = 'Your trash was cleaned!' redirect_to conversations_path end [...]

並增加路由:

config/routes.rb
[...] resources :conversations, only: [:index, :show, :destroy] do member do post :reply post :restore end collection do delete :empty_trash end end [...]

將對話標示為已讀

讓我們允許使用者將對話標示為已讀。為了實作它,我們需要另外的方法、路由及按鈕:

app/views/conversations/_conversation.html.erb
[...] <div class="btn-group-vertical pull-right"> <% if conversation.is_trashed?(current_user) %> <%= link_to 'Restore', restore_conversation_path(conversation), class: 'btn btn-xs btn-info', method: :post %> <% else %> <%= link_to 'Move to trash', conversation_path(conversation), class: 'btn btn-xs btn-danger', method: :delete, data: { confirm: 'Are you sure?' } %> <% if conversation.is_unread?(current_user) %> <%= link_to 'Mark as read', mark_as_read_conversation_path(conversation), class: 'btn btn-xs btn-info', method: :post %> <% end %> <% end %> </div> [...]

這邊使用的 is_unread? 方法需要指定使用者。還有另一個相反的方法 is_read?

app/controllers/conversations_controller.rb
[...] def mark_as_read @conversation.mark_as_read(current_user) flash[:success] = 'The conversation was marked as read.' redirect_to conversations_path end [...]

最後,修改路由:

config/routes.rb
[...] resources :conversations, only: [:index, :show, :destroy] do member do post :reply post :restore post :mark_as_read end collection do delete :empty_trash end end [...]

大功告成!

註:你還可以優化 show 動作,讓對話被開啟時自動標示為已讀!


電子郵件通知

記住,Mailboxer 可以在使用者收到訊息時寄送電子郵件通知。此功能在 initializer 中啟用:

config/initializers/mailboxer.rb
Mailboxer.setup do |config| #Configures if you application uses or not email sending for Notifications and Messages config.uses_emails = true #Configures the default from for emails sent for Messages and Notifications config.default_from = "no-reply@mailboxer.com" #Configures the methods needed by mailboxer config.email_method = :mailboxer_email config.name_method = :name [...] end

config.email_methodconfig.name_method 告訴 Mailboxer 如何分別取得電子郵件和名字。name 已經存在於我們的 User model,但是沒有 mailboxer_email。你可以嘗試改變這個值為 Devise 所提供的 email 方法,但是這會導致一個錯誤,因為 Mailboxer 會傳遞參數給它,包含收到的訊息。所以有兩個選擇,一個是重新定義此方法,或是建立一個新的。我會選擇第二個選項:

app/models/user.rb
[...] def mailboxer_email(object) email end [...]

電子郵件通知已經啟用了(請確定有依照先前的指示設定 ActionMailer。另外,不要忘記,電子郵件在 development 時預設是不會發送的。)


補充:如何新增按鈕來寄送訊息給指定使用者

這個功能可以很容易地完成!指定的使用者應該從「Start conversation」頁面上的下拉選單自動選取。我認為,最好的方式就是使用 GET 參數帶入指定的使用者。修改 MessagesController 如下:

app/controllers/messages_controller.rb
[...] def new @chosen_recipient = User.find_by(id: params[:to].to_i) if params[:to] end [...]

現在,@chosen_recipient 會有使用者的資料或是 nil

接著修改 view:

app/views/messages/new.html.erb
[...] <div class="form-group"> <%= label_tag 'recipients', 'Choose recipients' %> <%= select_tag 'recipients', recipients_options(@chosen_recipient), multiple: true, class: 'form-control chosen-it' %> </div> [...]

我們傳遞了 @chosen_recipient 給 helper 方法。

app/helpers/messages_helper.rb
[...] def recipients_options(chosen_recipient = nil) options_for_select(User.all.map { |user| [user.name, user.id, { 'data-img-src' => gravatar_image_url(user.email, size: 50) }] }, chosen_recipient.nil? ? nil : chosen_recipient.id) end [...]

這是更新後的 recipients_options helper 方法。將預設選取的選項帶入 options_for_select 的第二個參數即可!

基本上,這樣就完成了!為了示範如何運作,新增一個使用者清單頁面並於每個使用者後方加上「Send message」按鈕。

config/routes.rb
[...] resources :users, only: [:index] [...]

建立使用者 controller:

$ rails generate controller users
app/controllers/users_controller.rb
class UsersController < ApplicationController def index @users = User.order('created_at DESC').page(params[:page]) end end
app/views/users/index.html.erb
<% page_header "Users" %> <ul> <% @users.each do |user| %> <li> <strong><%= user.name %></strong> <% unless current_user == user %> <%= link_to 'Send message', new_message_path(to: user.id), class: 'btn btn-default btn-sm' %> <% end %> </li> <% end %> </ul> <%= paginate @users %>

調整 layout 加入使用者清單頁面連結:

app/views/layouts/application.html.erb
[...] <li><%= link_to 'Users', users_path %></li> [...]

到此,這個功能就完成囉!


結論

呼!討論了相當多,對吧?我們探討了 Mailboxer 的基本功能,包含了訊息、不同類型的對話、管理對話以及設定電子郵件通知。我們也整合了 Devise 到此應用程式並利用 Gravatar 讓頁面看起來更美觀。

希望本文對你有幫助。順帶一提,你可能會對 Mailboxer 維基上的這個頁面以及介紹 Mailboxer 基本功能的應用程式範例有興趣。

資料來源:Messaging with Rails and Mailboxer