implement private messages

This commit is contained in:
joshua stein 2012-07-03 20:48:01 -05:00
parent e47a054e75
commit fc1c474fb3
18 changed files with 504 additions and 115 deletions

View file

@ -569,8 +569,6 @@ table.data th {
background-color: #eaeaea;
border-bottom: 1px solid #cacaca;
border-top: 1px solid #cacaca;
padding: 2px;
padding-left: 3px;
text-align: left;
}
table.data th img {
@ -583,10 +581,13 @@ table.data th.r, table.data td.r {
padding-right: 3px;
}
table.data th,
table.data td {
padding-left: 3px;
padding-top: 4px;
padding-bottom: 3px;
padding: 0.25em 0.5em;
}
table.data tr.bold td {
font-weight: bold;
}
table.thread td {
@ -600,11 +601,13 @@ table.thread td img {
vertical-align: middle;
}
table.data tr.row0 td {
table.data tr.row0 td,
table.data.zebra tr:nth-child(even) td {
background-color: #f8f8f8;
border-bottom: 1px solid #eaeaea;
}
table.data tr.row1 td {
table.data tr.row1 td,
table.data.zebra tr:nth-child(odd) td {
background-color: #f5f5f5;
border-bottom: 1px solid #eaeaea;
}
@ -642,6 +645,16 @@ table.data tr.void td, table.data tr.void td a {
font-weight: bold;
font-size: 11pt;
}
.box .sublegend {
font-size: 10pt;
font-weight: normal;
font-style: italic;
color: gray;
}
.box .legend.right {
float: right;
}
.box .boxtitle {
background-color: #f0f0f0;
border-bottom: 1px solid #cacaca;

View file

@ -1,2 +1,73 @@
class MessagesController < ApplicationController
before_filter :require_logged_in_user
before_filter :find_message, :only => [ :show, :destroy, :keep_as_new ]
def index
@new_message = Message.new
end
def create
@new_message = Message.new(params[:message])
@new_message.author_user_id = @user.id
if @new_message.save
flash.now[:success] = "Your message has been sent to " <<
@new_message.recipient.username
@new_message = Message.new
end
render :action => "index"
end
def show
@new_message = Message.new
@new_message.recipient_username = (@message.author_user_id == @user.id ?
@message.recipient.username : @message.author.username)
if @message.recipient_user_id == @user.id
@message.has_been_read = true
@message.save
end
if @message.subject.match(/^re:/i)
@new_message.subject = @message.subject
else
@new_message.subject = "Re: #{@message.subject}"
end
end
def destroy
if @message.author_user_id == @user.id
@message.deleted_by_author = true
end
if @message.recipient_user_id == @user.id
@message.deleted_by_recipient = true
end
@message.save
flash[:success] = "Deleted message"
return redirect_to "/messages"
end
def keep_as_new
@message.has_been_read = false
@message.save
return redirect_to "/messages"
end
private
def find_message
if @message = Message.find_by_short_id(params[:message_id ] || params[:id])
if !(@message.author_user_id == @user.id ||
@message.recipient_user_id == @user.id)
flash[:error] = "Could not find message"
redirect_to "/messages"
return false
end
end
end
end

View file

@ -0,0 +1,12 @@
class EmailMessage < ActionMailer::Base
default :from => "nobody@lobste.rs"
def notify(message, user)
@message = message
@user = user
mail(:to => user.email, :from => "Lobsters <nobody@lobste.rs>",
:subject => "[Lobsters] Private Message from " <<
"#{message.author.username}: #{message.subject}")
end
end

View file

@ -12,7 +12,7 @@ class Comment < ActiveRecord::Base
:indent_level, :highlighted
before_create :assign_short_id_and_upvote
after_create :assign_votes, :mark_submitter, :email_reply
after_create :assign_votes, :mark_submitter, :deliver_reply_notifications
after_destroy :unassign_votes
MAX_EDIT_MINS = 45
@ -32,12 +32,14 @@ class Comment < ActiveRecord::Base
end
def assign_short_id_and_upvote
(1...10).each do |tries|
if tries == 10
10.times do |try|
if try == 10
raise "too many hash collisions"
end
if !Comment.find_by_short_id(self.short_id = Utils.random_str(6))
self.short_id = Utils.random_str(6)
if !Comment.find_by_short_id(self.short_id)
break
end
end
@ -56,14 +58,14 @@ class Comment < ActiveRecord::Base
Keystore.increment_value_for("user:#{self.user_id}:comments_posted")
end
def email_reply
def deliver_reply_notifications
begin
if self.parent_comment_id && u = self.parent_comment.try(:user)
if u.email_replies?
EmailReply.reply(self, u).deliver
end
if u.pushover_replies?
if u.pushover_replies? && u.pushover_user_key.present?
Pushover.push(u.pushover_user_key, u.pushover_device, {
:title => "Lobsters reply from #{self.user.username} on " <<
"#{self.story.title}",

93
app/models/message.rb Normal file
View file

@ -0,0 +1,93 @@
class Message < ActiveRecord::Base
belongs_to :recipient,
:class_name => "User",
:foreign_key => "recipient_user_id"
belongs_to :author,
:class_name => "User",
:foreign_key => "author_user_id"
validates_presence_of :recipient
validates_presence_of :author
attr_accessor :recipient_username
attr_accessible :recipient_username, :subject, :body
validates_length_of :subject, :in => 1..150
validates_length_of :body, :maximum => (64 * 1024)
before_create :assign_short_id
after_create :deliver_reply_notifications
after_save :check_for_both_deleted
after_save :update_unread_counts
def assign_short_id
10.times do |try|
if try == 10
raise "too many hash collisions"
end
self.short_id = Utils.random_str(6)
if !Message.find_by_short_id(self.short_id)
break
end
end
end
def check_for_both_deleted
if self.deleted_by_author && self.deleted_by_recipient
self.destroy
end
end
def update_unread_counts
self.recipient.update_unread_message_count!
end
def deliver_reply_notifications
begin
if self.recipient.email_messages?
EmailMessage.notify(self, self.recipient).deliver
end
if self.recipient.pushover_messages? &&
self.recipient.pushover_user_key.present?
Pushover.push(self.recipient.pushover_user_key,
self.recipient.pushover_device, {
:title => "Lobsters message from #{self.author.username}: " <<
"#{self.subject}",
:message => self.plaintext_body,
:url => self.url,
:url_title => "Reply to #{self.author.username}",
})
end
rescue
end
end
def recipient_username=(username)
self.recipient_user_id = nil
if u = User.find_by_username(username)
self.recipient_user_id = u.id
@recipient_username = username
else
errors.add(:recipient_username, "is not a valid user")
end
end
def linkified_body
RDiscount.new(self.body.to_s, :smart, :autolink, :safelink,
:filter_html).to_html
end
def plaintext_body
# TODO: linkify then strip tags and convert entities back
self.body.to_s
end
def url
Rails.application.routes.url_helpers.root_url + "messages/#{self.short_id}"
end
end

View file

@ -18,10 +18,9 @@ class Story < ActiveRecord::Base
attr_accessor :vote, :story_type, :already_posted_story, :fetched_content
attr_accessor :new_tags, :tags_to_add, :tags_to_delete
after_save :deal_with_tags
before_create :assign_short_id
after_create :mark_submitter
after_save :deal_with_tags
validate do
if self.url.present?
@ -47,12 +46,13 @@ class Story < ActiveRecord::Base
end
def assign_short_id
(1...10).each do |tries|
if tries == 10
10.times do |try|
if try == 10
raise "too many hash collisions"
end
self.short_id = Utils.random_str(6)
if !Story.find_by_short_id(self.short_id)
break
end

View file

@ -1,6 +1,13 @@
class User < ActiveRecord::Base
has_many :stories,
:include => :user
has_many :comments
has_many :authored_messages,
:class_name => "Message",
:foreign_key => "author_user_id"
has_many :received_messages,
:class_name => "Message",
:foreign_key => "recipient_user_id"
has_secure_password
validates_format_of :username, :with => /\A[A-Za-z0-9][A-Za-z0-9_-]*\Z/
@ -13,7 +20,7 @@ class User < ActiveRecord::Base
attr_accessible :username, :email, :password, :password_confirmation,
:about, :email_replies, :pushover_replies, :pushover_user_key,
:pushover_device
:pushover_device, :email_messages, :pushover_messages
before_save :check_session_token
@ -24,8 +31,13 @@ class User < ActiveRecord::Base
end
def unread_message_count
0
#Message.where(:recipient_user_id => self.id, :has_been_read => 0).count
Keystore.value_for("user:#{self.id}:unread_messages").to_i
end
def update_unread_message_count!
Keystore.put("user:#{self.id}:unread_messages",
Message.where(:recipient_user_id => self.id,
:has_been_read => false).count)
end
def karma

View file

@ -0,0 +1,3 @@
<%= word_wrap(@comment.plaintext_body, :line_width => 72).gsub(/\n/, "\n ") %>
Reply at <%= @message.url %>

View file

@ -2,6 +2,4 @@
<%= word_wrap(@comment.plaintext_comment, :line_width => 72).gsub(/\n/, "\n ") %>
You can view this reply at:
<%= @comment.url %>
Continue this discussion at <%= @comment.url %>

View file

@ -1,16 +1,15 @@
<div id="header">
<div id="headerright" class="<%= @user ? "loggedin" : "" %>">
<% if @user %>
<a href="/settings"><%= @user.username %> (<%= @user.karma %>)</a>
<% if false %>
<% if (count = @user.unread_message_count) > 0 %>
<a href="/messages"><%= count %> New Message<%= count == 1 ? "" : "s"
%></a>
<% else %>
<a href="/messages">Messages</a>
<% end %>
<% end %>
<a href="/settings"><%= @user.username %> (<%= @user.karma %>)</a>
<%= link_to "Logout", { :controller => "login", :action => "logout" },
{ :confirm => "Are you sure you want to logout?",
"method" => "post" } %>

View file

@ -0,0 +1,58 @@
<div class="box wide">
<div class="legend">
Private Messages
</div>
<% if @user.received_messages.any? %>
<table class="data zebra" width="100%" cellspacing=0>
<tr>
<th width="15%">From</th>
<th width="20%">Sent</th>
<th width="60%">Subject</th>
</tr>
<% @user.received_messages.each do |message| %>
<tr class="<%= message.has_been_read? ? "" : "bold" %>">
<td><a href="/u/<%= message.author.username %>"><%=
message.author.username %></a></td>
<td><%= time_ago_in_words(message.created_at) %> ago</td>
<td><a href="/messages/<%= message.short_id %>"><%= message.subject
%></a></td>
</tr>
<% end %>
</table>
<% else %>
<p>
You do not have any private messages.
</p>
<% end %>
<br>
<div class="legend">
Compose Message
</div>
<%= form_for @new_message, :method => :post do |f| %>
<%= error_messages_for @new_message %>
<div class="boxline">
<%= f.label :recipient_username, "To:", :class => "required" %>
<%= f.text_field :recipient_username, :size => 20 %>
</div>
<div class="boxline">
<%= f.label :subject, "Subject:", :class => "required" %>
<%= f.text_field :subject, :style => "width: 500px;" %>
</div>
<div class="boxline">
<%= f.label :body, "Message:", :class => "required" %>
<%= f.text_area :body, :style => "width: 500px;", :rows => 5 %>
</div>
<div class="boxline">
<p></p>
<%= submit_tag "Send Message" %>
</div>
<% end %>
</div>

View file

@ -0,0 +1,66 @@
<div class="box wide">
<div class="legend" style="float: right;">
<a href="/messages">Back to Messages</a>
</div>
<div class="legend">
<%= @message.subject %>
<div class="sublegend">
Sent from <a href="/u/<%= @message.author.username %>"><%=
@message.author.username %></a>
to
<a href="/u/<%= @message.recipient.username %>"><%=
@message.recipient.username %></a>
<%= time_ago_in_words(@message.created_at) %> ago
</div>
</div>
<div class="boxline">
<%= raw @message.linkified_body %>
</div>
<br>
<div class="boxline">
<div style="float: left;">
<%= form_tag message_url(@message.short_id), :method => :delete do %>
<%= submit_tag "Delete Message" %>
<% end %>
</div>
<div style="float: left; padding-left: 1em;">
<%= form_tag message_url(@message.short_id) + "/keep_as_new",
:method => :post do %>
<%= submit_tag "Keep As New" %>
<% end %>
</div>
</div>
<div style="clear: both;"></div>
<br>
<div class="legend">
Compose Reply To <%= @new_message.recipient_username %>
</div>
<%= form_for @new_message, :method => :post do |f| %>
<%= f.hidden_field :recipient_username %>
<%= error_messages_for @new_message %>
<div class="boxline">
<%= f.text_field :subject, :style => "width: 500px;" %>
</div>
<div class="boxline">
<%= f.text_area :body, :style => "width: 500px;", :rows => 5 %>
</div>
<div class="boxline">
<p></p>
<%= submit_tag "Send Message" %>
</div>
<% end %>
</div>

View file

@ -1,4 +1,116 @@
<div class="box wide">
<div class="legend right">
<a href="/u/<%= @user.username %>">View Profile</a>
</div>
<div class="legend">
Account Settings
</div>
<%= form_for @edit_user, :url => settings_url, :method => :post do |f| %>
<%= error_messages_for f.object %>
<div class="boxline">
<%= f.label :username, "Username:", :class => "required" %>
<%= f.text_field :username, :size => 15 %>
<span class="hint">
<tt>[A-Za-z0-9][A-Za-z0-9_-]*</tt>
</span>
</div>
<div class="boxline">
<%= f.label :password, "New Password:", :class => "required" %>
<%= f.password_field :password, :size => 40 %>
</div>
<div class="boxline">
<%= f.label :password_confirmation, "Confirm Password:",
:class => "required" %>
<%= f.password_field :password_confirmation, :size => 40 %>
</div>
<div class="boxline">
<%= f.label :email, "E-mail Address:", :class => "required" %>
<%= f.text_field :email, :size => 40 %>
</div>
<div class="boxline">
<%= f.label :pushover_user_key,
raw("<a href=\"https://pushover.net/\">Pushover</a> User Key:"),
:class => "required" %>
<%= f.text_field :pushover_user_key, :size => 40 %>
</div>
<div class="boxline">
<%= f.label :pushover_device, "Pushover Device:",
:class => "required" %>
<%= f.text_field :pushover_device, :placeholder => "optional",
:size => 15 %>
</div>
<div class="boxline">
<%= f.label :about, "About:", :class => "required" %>
<%= f.text_area :about, :size => "100x5" %>
</div>
<div class="box">
<div class="boxline markdown_help_toggler" style="margin-left: 9em;">
<div class="markdown_help_label">
<span class="fakea">Limited Markdown formatting available</span>
</div>
<div style="clear: both;"></div>
<%= render :partial => "global/markdownhelp" %>
</div>
</div>
<div class="legend">
Comment Reply Notification Settings
</div>
<div class="boxline">
<%= f.label :email_replies, "Receive E-mail:", :class => "required" %>
<%= f.check_box :email_replies %>
</div>
<div class="boxline">
<%= f.label :pushover_replies,
raw("Receive <a href=\"https://pushover.net/\">Pushover</a> Alert:"),
:class => "required" %>
<%= f.check_box :pushover_replies %>
<span class="hint">
Requires user key entered above
</span>
</div>
<br>
<div class="legend">
Private Message Notification Settings
</div>
<div class="boxline">
<%= f.label :email_messages, "Receive E-mail:", :class => "required" %>
<%= f.check_box :email_messages %>
</div>
<div class="boxline">
<%= f.label :pushover_messages,
raw("Receive <a href=\"https://pushover.net/\">Pushover</a> Alert:"),
:class => "required" %>
<%= f.check_box :pushover_replies %>
<span class="hint">
Requires user key entered above
</span>
</div>
<br>
<%= f.submit "Save All Settings" %>
<% end %>
<br>
<br>
<div class="legend">
Invite a New User
</div>
@ -19,86 +131,4 @@
<%= submit_tag "Send Invitation" %>
</div>
<% end %>
<br>
<div class="legend">
Account Settings (<a href="/u/<%= @user.username %>">View Profile</a>)
</div>
<%= form_for @edit_user, :url => settings_url, :method => :post do |f| %>
<%= error_messages_for f.object %>
<div class="boxline">
<%= f.label :username, "Username:", :class => "required" %>
<%= f.text_field :username %>
<span class="hint">
<tt>[A-Za-z0-9][A-Za-z0-9_-]*</tt>
</span>
</div>
<div class="boxline">
<%= f.label :email, "E-mail Address:", :class => "required" %>
<%= f.text_field :email %>
</div>
<div class="boxline">
<%= f.label :password, "New Password:", :class => "required" %>
<%= f.password_field :password %>
</div>
<div class="boxline">
<%= f.label :password_confirmation, "Confirm Password:",
:class => "required" %>
<%= f.password_field :password_confirmation %>
</div>
<div class="boxline">
<%= f.label :about, "About:", :class => "required" %>
<%= f.text_area :about, :size => "100x5" %>
</div>
<div class="box">
<div class="boxline markdown_help_toggler" style="margin-left: 9em;">
<div class="markdown_help_label">
<span class="fakea">Limited Markdown formatting available</span>
</div>
<div style="clear: both;"></div>
<%= render :partial => "global/markdownhelp" %>
</div>
</div>
<div class="legend">
Reply Notification Settings
</div>
<div class="boxline">
<%= f.label :email_replies, "E-mail:", :class => "required" %>
<%= f.check_box :email_replies %>
</div>
<div class="boxline">
<%= f.label :pushover_replies, raw("<a href=\"https://pushover.net/\">" +
"Pushover</a>:"), :class => "required" %>
<%= f.check_box :pushover_replies %>
</div>
<div class="boxline">
<%= f.label :pushover_user_key, "Pushover User Key:",
:class => "required" %>
<%= f.text_field :pushover_user_key, :size => 40 %>
</div>
<div class="boxline">
<%= f.label :pushover_device, "Pushover Device:",
:class => "required" %>
<%= f.text_field :pushover_device, :placeholder => "optional",
:size => 15 %>
</div>
<br>
<%= f.submit "Save All Settings" %>
<% end %>
</div>

View file

@ -27,7 +27,7 @@ module Lobsters
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
# config.time_zone = 'Central Time (US & Canada)'
config.time_zone = 'Central Time (US & Canada)'
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]

View file

@ -38,6 +38,10 @@ Lobsters::Application.routes.draw do
post "/comments/:story_id" => "comments#create"
post "/comments/preview/:story_id" => "comments#preview"
resources :messages do
post "keep_as_new"
end
get "/s/:id/:title/comments/:comment_short_id" => "stories#show_comment"
get "/s/:id/(:title)" => "stories#show"
get "/u/:id" => "users#show"

View file

@ -0,0 +1,11 @@
class FixUpMessages < ActiveRecord::Migration
def up
rename_column :messages, :random_hash, :short_id
add_column :messages, :deleted_by_author, :boolean, :default => false
add_column :messages, :deleted_by_recipient, :boolean, :default => false
end
def down
end
end

View file

@ -0,0 +1,13 @@
class PmNotificationOptions < ActiveRecord::Migration
def up
change_table :messages do |t|
t.change :has_been_read, :boolean, :default => false
end
add_column :users, :email_messages, :boolean, :default => true
add_column :users, :pushover_messages, :boolean, :default => true
end
def down
end
end

View file

@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20120703184957) do
ActiveRecord::Schema.define(:version => 20120704013019) do
create_table "comments", :force => true do |t|
t.datetime "created_at", :null => false
@ -52,13 +52,15 @@ ActiveRecord::Schema.define(:version => 20120703184957) do
t.datetime "created_at"
t.integer "author_user_id"
t.integer "recipient_user_id"
t.integer "has_been_read", :limit => 1, :default => 0
t.string "subject", :limit => 100
t.boolean "has_been_read", :default => false
t.string "subject", :limit => 100
t.text "body"
t.string "random_hash", :limit => 30
t.string "short_id", :limit => 30
t.boolean "deleted_by_author", :default => false
t.boolean "deleted_by_recipient", :default => false
end
add_index "messages", ["random_hash"], :name => "random_hash", :unique => true
add_index "messages", ["short_id"], :name => "random_hash", :unique => true
create_table "stories", :force => true do |t|
t.datetime "created_at"
@ -106,6 +108,8 @@ ActiveRecord::Schema.define(:version => 20120703184957) do
t.boolean "pushover_replies", :default => false
t.string "pushover_user_key"
t.string "pushover_device"
t.boolean "email_messages", :default => true
t.boolean "pushover_messages", :default => true
end
add_index "users", ["session_token"], :name => "session_hash", :unique => true