Fully port conversations to Backbone and drop inbox.js

This commit is contained in:
Augier 2016-07-10 13:53:26 +02:00 committed by Steffen van Bergerem
parent caf46fdcb9
commit e424896822
No known key found for this signature in database
GPG key ID: 315C9787D548DC6B
26 changed files with 513 additions and 268 deletions

View file

@ -9,7 +9,7 @@ app.Router = Backbone.Router.extend({
"commented(/)": "stream", "commented(/)": "stream",
"community_spotlight(/)": "spotlight", "community_spotlight(/)": "spotlight",
"contacts(/)": "contacts", "contacts(/)": "contacts",
"conversations(/)": "conversations", "conversations(/)(:id)(/)": "conversations",
"followed_tags(/)": "followed_tags", "followed_tags(/)": "followed_tags",
"getting_started(/)": "gettingStarted", "getting_started(/)": "gettingStarted",
"help(/)": "help", "help(/)": "help",
@ -93,8 +93,11 @@ app.Router = Backbone.Router.extend({
app.page = new app.pages.Contacts({stream: stream}); app.page = new app.pages.Contacts({stream: stream});
}, },
conversations: function() { conversations: function(id) {
app.conversations = new app.views.Conversations(); app.conversations = app.conversations || new app.views.ConversationsInbox();
if (parseInt("" + id, 10)) {
app.conversations.renderConversation(id);
}
}, },
/* eslint-disable camelcase */ /* eslint-disable camelcase */

View file

@ -1,28 +1,24 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.views.ConversationsForm = Backbone.View.extend({ app.views.ConversationsForm = Backbone.View.extend({
el: ".conversations-form-container",
events: { events: {
"keydown textarea#conversation_text" : "keyDown", "keydown .conversation-message-text": "keyDown",
"submit #conversation-new": "onSubmitNewConversation"
}, },
initialize: function(opts) { initialize: function(opts) {
this.contacts = _.has(opts, "contacts") ? opts.contacts : null; this.contacts = _.has(opts, "contacts") ? opts.contacts : null;
if(!this.contacts || this.contacts.length === 0) {
this.displayNoContactsMessage();
return;
}
this.prefill = []; this.prefill = [];
if (_.has(opts, "prefillName") && _.has(opts, "prefillValue")) { if (_.has(opts, "prefillName") && _.has(opts, "prefillValue")) {
this.prefill = [{name : opts.prefillName, this.prefill = [{name: opts.prefillName, value: opts.prefillValue}];
value : opts.prefillValue}];
} }
this.autocompleteInput = $("#contact_autocomplete");
this.prepareAutocomplete(this.contacts); this.prepareAutocomplete(this.contacts);
}, },
prepareAutocomplete: function(data){ prepareAutocomplete: function(data){
this.autocompleteInput.autoSuggest(data, { this.$("#contact-autocomplete").autoSuggest(data, {
selectedItemProp: "name", selectedItemProp: "name",
searchObjProps: "name", searchObjProps: "name",
asHtmlID: "contact_ids", asHtmlID: "contact_ids",
@ -32,20 +28,26 @@ app.views.ConversationsForm = Backbone.View.extend({
startText: '', startText: '',
emptyText: Diaspora.I18n.t("no_results"), emptyText: Diaspora.I18n.t("no_results"),
preFill: this.prefill preFill: this.prefill
}).focus(); });
$("#contact_ids").attr("aria-labelledby", "toLabel"); $("#contact_ids").attr("aria-labelledby", "toLabel").focus();
},
displayNoContactsMessage: function() {
$("form#new_conversation").replaceWith(
"<div class=\"well text-center\">" + Diaspora.I18n.t("conversation.new.no_contacts") + "</div>"
);
}, },
keyDown : function(evt) { keyDown : function(evt) {
if(evt.which === Keycodes.ENTER && evt.ctrlKey) { if(evt.which === Keycodes.ENTER && evt.ctrlKey) {
$(evt.target).parents("form").submit(); $(evt.target).parents("form").submit();
} }
},
getConversationParticipants: function() {
return this.$("#as-values-contact_ids").val().split(",");
},
onSubmitNewConversation: function(evt) {
evt.preventDefault();
if (this.getConversationParticipants().length === 0) {
evt.stopPropagation();
app.flashMessages.error(Diaspora.I18n.t("conversation.create.no_recipient"));
}
} }
}); });
// @license-end // @license-end

View file

@ -0,0 +1,82 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.views.ConversationsInbox = Backbone.View.extend({
el: "#conversations-container",
events: {
"click .conversation-wrapper": "displayConversation",
"click .new-conversation-btn": "displayNewConversation"
},
initialize: function() {
new app.views.ConversationsForm({contacts: gon.contacts});
this.setupConversation();
},
renderConversation: function(conversationId) {
var self = this;
$.ajax({
url: Routes.conversationRaw(conversationId),
dataType: "html",
success: function(data) {
self.$el.find("#conversation-new").addClass("hidden");
self.$el.find("#conversation-show").removeClass("hidden").html(data);
self.selectConversation(conversationId);
self.setupConversation();
}
});
},
selectConversation: function(conversationId) {
this.$el.find("#conversation-inbox .stream-element").removeClass("selected");
if (conversationId) {
this.$el.find("#conversation-inbox .stream-element[data-guid='" + conversationId + "']").addClass("selected");
}
},
displayNewConversation: function(evt) {
evt.preventDefault();
evt.stopPropagation();
this.$el.find("#conversation-new").removeClass("hidden");
this.$el.find("#conversation-show").addClass("hidden");
this.selectConversation();
app.router.navigate(Routes.conversations());
},
setupConversation: function() {
app.helpers.timeago($(this.el));
$(".control-icons a").tooltip({placement: "bottom"});
var conv = $(".conversation-wrapper .stream-element.selected"),
cBadge = $("#conversations-link .badge");
if (conv.hasClass("unread")) {
var unreadCount = parseInt(conv.find(".unread-message-count").text(), 10);
if (cBadge.text() !== "") {
cBadge.text().replace(/\d+/, function(num) {
num = parseInt(num, 10) - unreadCount;
if (num > 0) {
cBadge.text(num);
} else {
cBadge.text(0).addClass("hidden");
}
});
}
conv.removeClass("unread");
conv.find(".unread-message-count").remove();
var pos = $("#first_unread").offset().top - 50;
$("html").animate({scrollTop: pos});
} else {
$("html").animate({scrollTop: 0});
}
},
displayConversation: function(evt) {
var $target = $(evt.target).closest(".conversation-wrapper");
app.router.navigate($target.data("conversation-path"), {trigger: true});
}
});
// @license-end

View file

@ -1,59 +0,0 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.views.Conversations = Backbone.View.extend({
el: "#conversations_container",
events: {
"keydown textarea#message_text" : "keyDown",
"conversation:loaded" : "setupConversation"
},
initialize: function() {
if($("#conversation_new:visible").length > 0) {
new app.views.ConversationsForm({
el: $("#conversation_new"),
contacts: gon.contacts
});
}
this.setupConversation();
},
setupConversation: function() {
app.helpers.timeago($(this.el));
$(".control-icons a").tooltip({placement: "bottom"});
var conv = $(".conversation-wrapper .stream-element.selected"),
cBadge = $("#conversations-link .badge");
if(conv.hasClass("unread") ){
var unreadCount = parseInt(conv.find(".unread-message-count").text(), 10);
if(cBadge.text() !== "") {
cBadge.text().replace(/\d+/, function(num){
num = parseInt(num, 10) - unreadCount;
if(num > 0) {
cBadge.text(num);
} else {
cBadge.text(0).addClass("hidden");
}
});
}
conv.removeClass("unread");
conv.find(".unread-message-count").remove();
var pos = $("#first_unread").offset().top - 50;
$("html").animate({scrollTop:pos});
} else {
$("html").animate({scrollTop:0});
}
},
keyDown : function(evt) {
if(evt.which === Keycodes.ENTER && evt.ctrlKey) {
$(evt.target).parents("form").submit();
}
}
});
// @license-end

View file

@ -1,21 +0,0 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
$(document).ready(function(){
$(document).on('click', '.conversation-wrapper', function(){
var conversation_path = $(this).data('conversation-path');
$.getScript(conversation_path, function() {
Diaspora.page.directionDetector.updateBinds();
});
history.pushState(null, "", conversation_path);
return false;
});
$(window).bind("popstate", function(){
if (/conversations\/\d+/.test(location.href)) {
$.getScript(location.href, function() {
Diaspora.page.directionDetector.updateBinds();
});
return false;
}
});
});
// @license-end

View file

@ -3,7 +3,6 @@
//= require templates //= require templates
//= require main //= require main
//= require fileuploader-custom //= require fileuploader-custom
//= require inbox
//= require mobile/mobile //= require mobile/mobile
//= require jquery.autoSuggest.custom //= require jquery.autoSuggest.custom
//= require contact-list //= require contact-list

View file

@ -594,7 +594,8 @@ form#new_user.new_user input.btn {
text-shadow: 1px 1px 20px rgb(126, 240, 77); text-shadow: 1px 1px 20px rgb(126, 240, 77);
} }
#conversation_inbox, .notifications { .conversation-inbox,
.notifications {
div.pagination { div.pagination {
width: 100%; width: 100%;
margin-left: auto; margin-left: auto;

View file

@ -24,7 +24,7 @@ class ConversationsController < ApplicationController
gon.contacts = contacts_data gon.contacts = contacts_data
respond_with do |format| respond_with do |format|
format.html format.html { render "index", locals: {no_contacts: current_user.contacts.mutual.empty?} }
format.json { render json: @visibilities.map(&:conversation), status: 200 } format.json { render json: @visibilities.map(&:conversation), status: 200 }
end end
end end
@ -53,7 +53,7 @@ class ConversationsController < ApplicationController
@response[:success] = false @response[:success] = false
@response[:message] = I18n.t('conversations.create.fail') @response[:message] = I18n.t('conversations.create.fail')
if person_ids.blank? if person_ids.blank?
@response[:message] = I18n.t('conversations.create.no_contact') @response[:message] = I18n.t("javascripts.conversation.create.no_recipient")
end end
end end
respond_to do |format| respond_to do |format|
@ -64,7 +64,7 @@ class ConversationsController < ApplicationController
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html do
redirect_to conversations_path(:conversation_id => params[:id]) redirect_to conversations_path(conversation_id: params[:id])
return return
end end
@ -72,7 +72,6 @@ class ConversationsController < ApplicationController
@first_unread_message_id = @conversation.first_unread_message(current_user).try(:id) @first_unread_message_id = @conversation.first_unread_message(current_user).try(:id)
@conversation.set_read(current_user) @conversation.set_read(current_user)
format.js
format.json { render :json => @conversation, :status => 200 } format.json { render :json => @conversation, :status => 200 }
else else
redirect_to conversations_path redirect_to conversations_path
@ -80,6 +79,17 @@ class ConversationsController < ApplicationController
end end
end end
def raw
@conversation = current_user.conversations.where(id: params[:conversation_id]).first
if @conversation
@first_unread_message_id = @conversation.first_unread_message(current_user).try(:id)
@conversation.set_read(current_user)
render partial: "conversations/show", locals: {conversation: @conversation}
else
render nothing: true, status: 404
end
end
def new def new
if !params[:modal] && !session[:mobile_view] && request.format.html? if !params[:modal] && !session[:mobile_view] && request.format.html?
redirect_to conversations_path redirect_to conversations_path

View file

@ -6,17 +6,18 @@
.media-left .media-left
= owner_image_tag(:thumb_small) = owner_image_tag(:thumb_small)
.media-body .media-body
= form_for [conversation, Message.new], html: {class: "control-group"} do |message| = form_for [conversation, Message.new], html: {id: "response-message", class: "control-group"} do |message|
.form-group .form-group
%label#messageLabel.sr-only{for: "message_text"} %label.sr-only#message-label{for: "response-message-text"}= t("conversations.new.message")
= t("conversations.new.message")
= message.text_area :text, = message.text_area :text,
rows: 5, rows: 5,
tabindex: 1, tabindex: 1,
class: "form-control form-group", id: "response-message-text",
aria: {labelledby: "messageLabel"} class: "form-control form-group conversation-message-text",
aria: {labelledby: "message-label"}
= message.submit t("conversations.show.reply"), = message.submit t("conversations.show.reply"),
"data-disable-with" => t("conversations.show.replying"), "data-disable-with" => t("conversations.show.replying"),
class: "btn btn-primary pull-right", tabindex: 2 :class => "btn btn-primary pull-right",
:tabindex => 2
.clearfix .clearfix

View file

@ -1,22 +1,25 @@
.container-fluid .container-fluid
= form_for Conversation.new, html: {class: "form-horizontal form_do_not_clear"}, remote: true do |conversation| = form_for Conversation.new, html: {id: "new-conversation",
class: "new-conversation form-horizontal form-do-not-clear"}, remote: true do |conversation|
.form-group .form-group
%label#toLabel{for: "contact_ids"} %label#toLabel{for: "contact_ids"}
= t(".to") = t(".to")
= text_field_tag "contact_autocomplete", nil, class: "form-control" = text_field_tag "contact_autocomplete", nil, id: "contact-autocomplete", class: "form-control"
.form-group .form-group
%label#subjectLabel{for: "conversation_subject"} %label#subject-label{for: "conversation-subject"}
= t(".subject") = t(".subject")
= conversation.text_field :subject, = conversation.text_field :subject,
id: "conversation-subject",
class: "input-block-level form-control", class: "input-block-level form-control",
aria: {labelledby: "subjectLabel"} aria: {labelledby: "subject-label"},
value: "",
placeholder: t("conversations.new.subject_default")
.form-group .form-group
%label#messageLabel.sr-only{for: "conversation_text"} %label.sr-only#message-label{for: "new-message-text"} = t(".message")
= t(".message") = text_area_tag "conversation[text]", "",
= text_area_tag "conversation[text]", rows: 5,
"", id: "new-message-text",
rows: 5, class: "conversation-message-text input-block-level form-control",
class: "input-block-level form-control", aria: {labelledby: "message-label"}
aria: {labelledby: "messageLabel"}
.form-group .form-group
= conversation.submit t('.send'), 'data-disable-with' => t('.sending'), class: 'btn btn-primary pull-right' = conversation.submit t(".send"), "data-disable-with" => t(".sending"), :class => "btn btn-primary pull-right"

View file

@ -1,10 +1,12 @@
var response = <%= raw @response.to_json %>; var response = <%= raw @response.to_json %>;
<% if session[:mobile_view] %> <% if session[:mobile_view] %>
if(response.success) {
window.location.href = "<%= conversations_path(conversation_id: @conversation.id) %>"; window.location.href = "<%= conversations_path(conversation_id: @conversation.id) %>";
}
<% else %> <% else %>
if(response.success){ if(response.success){
app.flashMessages.success(response.message); app.flashMessages.success(response.message);
$("#new_conversation").removeClass('form_do_not_clear').clearForm(); $("#new-conversation").removeClass('form-do-not-clear').clearForm();
window.location.href = "<%= conversations_path(conversation_id: @conversation.id) %>"; window.location.href = "<%= conversations_path(conversation_id: @conversation.id) %>";
} else { } else {
app.flashMessages.error(response.message); app.flashMessages.error(response.message);

View file

@ -1,41 +1,35 @@
- content_for :head do
= javascript_include_tag :inbox
- content_for :page_title do - content_for :page_title do
= t('.conversations_inbox') = t(".conversations_inbox")
.container-fluid#conversations_container .container-fluid#conversations-container
.row .row
.col-md-4 .col-md-4
.sidebar#left_pane .sidebar#left_pane
.sidebar-header.clearfix#left_pane_header .sidebar-header.clearfix#left_pane_header
.pull-right .pull-right
= link_to t(".new_conversation"), conversations_path, class: "btn btn-default" = link_to t(".new_conversation"), conversations_path, class: "new-conversation-btn btn btn-default"
%h3 %h3
= t(".inbox") = t(".inbox")
.conversation-inbox#conversation_inbox .conversation-inbox#conversation-inbox
.stream.conversations .conversations-form-container.stream.conversations
- if @visibilities.count > 0 - if @visibilities.count > 0
= render partial: "conversations/conversation", collection: @visibilities, as: :visibility = render partial: "conversations/conversation", collection: @visibilities, as: :visibility
- else - else
.no-conversations .no-conversations
= t('.no_messages') = t(".no_messages")
.pagination-container .pagination-container
= will_paginate @visibilities, previous_label: "&laquo;", next_label: "&raquo;", inner_window: 1, = will_paginate @visibilities, previous_label: "&laquo;", next_label: "&raquo;", inner_window: 1,
renderer: WillPaginate::ActionView::BootstrapLinkRenderer renderer: WillPaginate::ActionView::BootstrapLinkRenderer
.col-md-8 .col-md-8
- if @conversation .conversations-form-container.stream_container
.stream_container #conversation-show{class: @conversation ? "" : "hidden"}
#conversation_show - if @conversation
= render 'conversations/show', conversation: @conversation = render 'conversations/show', conversation: @conversation
- else #conversation-new{class: @conversation ? "framed-content clearfix hidden" : "framed-content clearfix"}
.stream_container.hidden
#conversation_show
.framed-content.clearfix#conversation_new
.new-conversation .new-conversation
%h3.text-center %h3.text-center= t("conversations.index.new_conversation")
= t("conversations.index.new_conversation") - if no_contacts
= render "conversations/new" .well.text-center= t("javascripts.conversation.new.no_contacts")
- else
= render "conversations/new"

View file

@ -12,7 +12,7 @@
.stream .stream
%p{ class: "conversation_#{name}" }= msg %p{ class: "conversation_#{name}" }= msg
#conversation_inbox .conversation-inbox#conversation-inbox
.stream.conversations .stream.conversations
- if @visibilities.count > 0 - if @visibilities.count > 0
= render partial: "conversations/conversation", collection: @visibilities, as: :visibility = render partial: "conversations/conversation", collection: @visibilities, as: :visibility

View file

@ -5,7 +5,7 @@
:javascript :javascript
$(document).ready(function () { $(document).ready(function () {
var data = $.parseJSON( "#{escape_javascript(@contacts_json)}" ), var data = $.parseJSON( "#{escape_javascript(@contacts_json)}" ),
autocompleteInput = $("#contact_autocomplete"); autocompleteInput = $("#contact-autocomplete");
autocompleteInput.autoSuggest(data, { autocompleteInput.autoSuggest(data, {
selectedItemProp: "name", selectedItemProp: "name",

View file

@ -1,10 +0,0 @@
if($('.stream_container').hasClass('hidden')){
$('#conversation_new').hide();
$('.stream_container').removeClass('hidden');
}
$('#conversation_show').html("<%= escape_javascript(render('conversations/show', :conversation => @conversation)) %>");
$(".stream-element", "#conversation_inbox").removeClass('selected');
$(".stream-element[data-guid='<%= @conversation.id %>']", "#conversation_inbox").addClass('selected');
$('#conversation_show').trigger("conversation:loaded");

View file

@ -27,7 +27,7 @@
id: 'mentionModal' id: 'mentionModal'
-if @contact -if @contact
#new_conversation_pane .conversations-form-container#new_conversation_pane
= render 'shared/modal', = render 'shared/modal',
path: new_conversation_path(:contact_id => @contact.id, name: @contact.person.name, modal: true), path: new_conversation_path(:contact_id => @contact.id, name: @contact.person.name, modal: true),
title: t('conversations.index.new_conversation'), title: t('conversations.index.new_conversation'),

View file

@ -274,6 +274,7 @@ en:
new_conversation: "New conversation" new_conversation: "New conversation"
no_messages: "No messages" no_messages: "No messages"
inbox: "Inbox" inbox: "Inbox"
no_contacts: "You need to add some contacts before you can start a conversation"
show: show:
reply: "Reply" reply: "Reply"
replying: "Replying..." replying: "Replying..."
@ -290,7 +291,6 @@ en:
create: create:
sent: "Message sent" sent: "Message sent"
fail: "Invalid message" fail: "Invalid message"
no_contact: "Hey, you need to add the contact first!"
new_conversation: new_conversation:
fail: "Invalid message" fail: "Invalid message"
destroy: destroy:

View file

@ -240,6 +240,8 @@ en:
posts: "Posts" posts: "Posts"
conversation: conversation:
create:
no_recipient: "Hey, you need to add a recipient first!"
new: new:
no_contacts: "You need to add some contacts before you can start a conversation." no_contacts: "You need to add some contacts before you can start a conversation."

View file

@ -77,6 +77,7 @@ Diaspora::Application.routes.draw do
resources :conversations, except: %i(edit update destroy) do resources :conversations, except: %i(edit update destroy) do
resources :messages, only: %i(create) resources :messages, only: %i(create)
delete 'visibility' => 'conversation_visibilities#destroy' delete 'visibility' => 'conversation_visibilities#destroy'
get "raw"
end end
resources :notifications, :only => [:index, :update] do resources :notifications, :only => [:index, :update] do

View file

@ -18,10 +18,10 @@ Feature: private conversations
Scenario: send a message Scenario: send a message
When I sign in as "bob@bob.bob" When I sign in as "bob@bob.bob"
And I send a message with subject "Greetings" and text "hello, alice!" to "Alice Awesome" And I send a message with subject "Greetings" and text "hello, alice!" to "Alice Awesome"
Then I should see "Greetings" within "#conversation_inbox" Then I should see "Greetings" within "#conversation-inbox"
And I should see "Greetings" within "#conversation_show" And I should see "Greetings" within "#conversation-show"
And I should see "less than a minute ago" within "#conversation_inbox" And I should see "less than a minute ago" within "#conversation-inbox"
And I should see "less than a minute ago" within "#conversation_show" And I should see "less than a minute ago" within "#conversation-show"
And I should see "Alice Awesome" as a participant And I should see "Alice Awesome" as a participant
And "Alice Awesome" should be part of active conversation And "Alice Awesome" should be part of active conversation
And I should see "hello, alice!" within ".stream_container" And I should see "hello, alice!" within ".stream_container"
@ -34,8 +34,8 @@ Feature: private conversations
Scenario: send a message using keyboard shortcuts Scenario: send a message using keyboard shortcuts
When I sign in as "bob@bob.bob" When I sign in as "bob@bob.bob"
And I send a message with subject "Greetings" and text "hello, alice!" to "Alice Awesome" using keyboard shortcuts And I send a message with subject "Greetings" and text "hello, alice!" to "Alice Awesome" using keyboard shortcuts
Then I should see "Greetings" within "#conversation_inbox" Then I should see "Greetings" within "#conversation-inbox"
And I should see "Greetings" within "#conversation_show" And I should see "Greetings" within "#conversation-show"
And "Alice Awesome" should be part of active conversation And "Alice Awesome" should be part of active conversation
And I should see "hello, alice!" within ".stream_container" And I should see "hello, alice!" within ".stream_container"
When I reply with "hey, how you doing?" using keyboard shortcuts When I reply with "hey, how you doing?" using keyboard shortcuts
@ -47,9 +47,9 @@ Feature: private conversations
Scenario: delete a conversation Scenario: delete a conversation
When I sign in as "bob@bob.bob" When I sign in as "bob@bob.bob"
And I send a message with subject "Greetings" and text "hello, alice!" to "Alice Awesome" And I send a message with subject "Greetings" and text "hello, alice!" to "Alice Awesome"
Then I should see "Greetings" within "#conversation_inbox" Then I should see "Greetings" within "#conversation-inbox"
When I click on selector ".hide_conversation" When I click on selector ".hide_conversation"
Then I should not see "Greetings" within "#conversation_inbox" Then I should not see "Greetings" within "#conversation-inbox"
When I sign in as "alice@alice.alice" When I sign in as "alice@alice.alice"
Then I should have 1 unread private message Then I should have 1 unread private message
And I should have 1 email delivery And I should have 1 email delivery

View file

@ -14,38 +14,38 @@ end
Then /^I send a message with subject "([^"]*)" and text "([^"]*)" to "([^"]*)"$/ do |subject, text, person| Then /^I send a message with subject "([^"]*)" and text "([^"]*)" to "([^"]*)"$/ do |subject, text, person|
step %(I am on the conversations page) step %(I am on the conversations page)
within("#conversation_new", match: :first) do within("#new-conversation", match: :first) do
step %(I fill in "contact_autocomplete" with "#{person}") step %(I fill in "contact_autocomplete" with "#{person}")
step %(I press the first ".as-result-item" within ".as-results") step %(I press the first ".as-result-item" within ".as-results")
step %(I fill in "conversation_subject" with "#{subject}") step %(I fill in "conversation-subject" with "#{subject}")
step %(I fill in "conversation_text" with "#{text}") step %(I fill in "new-message-text" with "#{text}")
step %(I press "Send") step %(I press "Send")
end end
end end
Then /^I send a message with subject "([^"]*)" and text "([^"]*)" to "([^"]*)" using keyboard shortcuts$/ do |subject, text, person| Then /^I send a message with subject "([^"]*)" and text "([^"]*)" to "([^"]*)" using keyboard shortcuts$/ do |subject, text, person|
step %(I am on the conversations page) step %(I am on the conversations page)
within("#conversation_new", match: :first) do within("#new-conversation", match: :first) do
step %(I fill in "contact_autocomplete" with "#{person}") step %(I fill in "contact_autocomplete" with "#{person}")
step %(I press the first ".as-result-item" within ".as-results") step %(I press the first ".as-result-item" within ".as-results")
step %(I fill in "conversation_subject" with "#{subject}") step %(I fill in "conversation-subject" with "#{subject}")
step %(I fill in "conversation_text" with "#{text}") step %(I fill in "new-message-text" with "#{text}")
find("#conversation_text").native.send_key %i(Ctrl Return) find("#new-message-text").native.send_key %i(Ctrl Return)
end end
end end
When /^I reply with "([^"]*)"$/ do |text| When /^I reply with "([^"]*)"$/ do |text|
step %(I am on the conversations page) step %(I am on the conversations page)
step %(I press the first ".conversation" within ".conversations") step %(I press the first ".conversation" within ".conversations")
step %(I fill in "message_text" with "#{text}") step %(I fill in "response-message-text" with "#{text}")
step %(I press "Reply") step %(I press "Reply")
end end
When /^I reply with "([^"]*)" using keyboard shortcuts$/ do |text| When /^I reply with "([^"]*)" using keyboard shortcuts$/ do |text|
step %(I am on the conversations page) step %(I am on the conversations page)
step %(I press the first ".conversation" within ".conversations") step %(I press the first ".conversation" within ".conversations")
step %(I fill in "message_text" with "#{text}") step %(I fill in "response-message-text" with "#{text}")
find("#message_text").native.send_key %i(Ctrl Return) find("#response-message-text").native.send_key %i(Ctrl Return)
end end
Then /^I send a mobile message with subject "([^"]*)" and text "([^"]*)" to "([^"]*)"$/ do |subject, text, person| Then /^I send a mobile message with subject "([^"]*)" and text "([^"]*)" to "([^"]*)"$/ do |subject, text, person|
@ -53,8 +53,8 @@ Then /^I send a mobile message with subject "([^"]*)" and text "([^"]*)" to "([^
step %(I follow "New conversation") step %(I follow "New conversation")
step %(I fill in "contact_autocomplete" with "#{person}") step %(I fill in "contact_autocomplete" with "#{person}")
step %(I press the first ".as-result-item" within ".as-results") step %(I press the first ".as-result-item" within ".as-results")
step %(I fill in "conversation_subject" with "#{subject}") step %(I fill in "conversation-subject" with "#{subject}")
step %(I fill in "conversation_text" with "#{text}") step %(I fill in "new-message-text" with "#{text}")
step %(I press "Send") step %(I press "Send")
end end

View file

@ -259,7 +259,7 @@ describe ConversationsController, :type => :controller do
it 'should set response with success to false and message to fail due to no contact' do it 'should set response with success to false and message to fail due to no contact' do
post :create, @hash post :create, @hash
expect(assigns[:response][:success]).to eq(false) expect(assigns[:response][:success]).to eq(false)
expect(assigns[:response][:message]).to eq(I18n.t('conversations.create.no_contact')) expect(assigns[:response][:message]).to eq(I18n.t("javascripts.conversation.create.no_recipient"))
end end
end end
@ -300,12 +300,6 @@ describe ConversationsController, :type => :controller do
@conversation = Conversation.create(hash) @conversation = Conversation.create(hash)
end end
it 'succeeds with js' do
xhr :get, :show, :id => @conversation.id, :format => :js
expect(response).to be_success
expect(assigns[:conversation]).to eq(@conversation)
end
it 'succeeds with json' do it 'succeeds with json' do
get :show, :id => @conversation.id, :format => :json get :show, :id => @conversation.id, :format => :json
expect(response).to be_success expect(response).to be_success
@ -318,4 +312,26 @@ describe ConversationsController, :type => :controller do
expect(response).to redirect_to(conversations_path(:conversation_id => @conversation.id)) expect(response).to redirect_to(conversations_path(:conversation_id => @conversation.id))
end end
end end
describe "#raw" do
before do
hash = {
author: alice.person,
participant_ids: [alice.contacts.first.person.id, alice.person.id],
subject: "not spam",
messages_attributes: [{author: alice.person, text: "cool stuff"}]
}
@conversation = Conversation.create(hash)
end
it "returns html of conversation" do
get :raw, conversation_id: @conversation.id
expect(response).to render_template(partial: "show", locals: {conversation: @conversation})
end
it "returns 404 when requesting non-existant conversation" do
get :raw, conversation_id: -1
expect(response).to have_http_status(404)
end
end
end end

View file

@ -80,6 +80,30 @@ describe('app.Router', function () {
}); });
}); });
describe("conversations", function() {
beforeEach(function() {
this.router = new app.Router();
});
it("doesn't do anything if no conversation id is passed", function() {
spyOn(app.views.ConversationsInbox.prototype, "renderConversation");
this.router.conversations();
expect(app.views.ConversationsInbox.prototype.renderConversation).not.toHaveBeenCalled();
});
it("doesn't do anything if id is not a readable number", function() {
spyOn(app.views.ConversationsInbox.prototype, "renderConversation");
this.router.conversations("yolo");
expect(app.views.ConversationsInbox.prototype.renderConversation).not.toHaveBeenCalled();
});
it("renders the conversation if id is a readable number", function() {
spyOn(app.views.ConversationsInbox.prototype, "renderConversation");
this.router.conversations("12");
expect(app.views.ConversationsInbox.prototype.renderConversation).toHaveBeenCalledWith("12");
});
});
describe("stream", function() { describe("stream", function() {
it("calls _initializeStreamView", function() { it("calls _initializeStreamView", function() {
spyOn(app.router, "_initializeStreamView"); spyOn(app.router, "_initializeStreamView");

View file

@ -0,0 +1,110 @@
describe("app.views.ConversationsForm", function() {
describe("keyDown", function() {
beforeEach(function() {
this.submitCallback = jasmine.createSpy().and.returnValue(false);
spec.loadFixture("conversations_read");
new app.views.ConversationsForm();
});
context("on new message form", function() {
beforeEach(function() {
$("#conversation-new").removeClass("hidden");
$("#conversation-show").addClass("hidden");
});
it("should submit the form with ctrl+enter", function() {
$("#new-conversation").submit(this.submitCallback);
var e = $.Event("keydown", {which: Keycodes.ENTER, ctrlKey: true});
$("#new-message-text").trigger(e);
expect(this.submitCallback).toHaveBeenCalled();
});
it("shouldn't submit the form without the ctrl key", function() {
$("#new-conversation").submit(this.submitCallback);
var e = $.Event("keydown", {which: Keycodes.ENTER, ctrlKey: false});
$("#new-message-text").trigger(e);
expect(this.submitCallback).not.toHaveBeenCalled();
});
});
context("on response message form", function() {
beforeEach(function() {
$("#conversation-new").addClass("hidden");
$("#conversation-show").removeClass("hidden");
});
it("should submit the form with ctrl+enter", function() {
$("#response-message").submit(this.submitCallback);
var e = $.Event("keydown", {which: Keycodes.ENTER, ctrlKey: true});
$("#response-message-text").trigger(e);
expect(this.submitCallback).toHaveBeenCalled();
});
it("shouldn't submit the form without the ctrl key", function() {
$("#response-message").submit(this.submitCallback);
var e = $.Event("keydown", {which: Keycodes.ENTER, ctrlKey: false});
$("#response-message-text").trigger(e);
expect(this.submitCallback).not.toHaveBeenCalled();
});
});
});
describe("onSubmitNewConversation", function() {
beforeEach(function() {
spec.loadFixture("conversations_read");
$("#conversation-new").removeClass("hidden");
$("#conversation-show").addClass("hidden");
spyOn(app.views.ConversationsForm.prototype, "onSubmitNewConversation").and.callThrough();
this.target = new app.views.ConversationsForm();
});
it("onSubmitNewConversation is called when submitting the conversation form", function() {
spyOn(app.views.ConversationsForm.prototype, "getConversationParticipants").and.returnValue([]);
$("#conversation-new").trigger("submit");
expect(app.views.ConversationsForm.prototype.onSubmitNewConversation).toHaveBeenCalled();
});
it("does not submit a conversation with no recipient", function() {
spyOn(app.views.ConversationsForm.prototype, "getConversationParticipants").and.returnValue([]);
var event = jasmine.createSpyObj("event", ["preventDefault", "stopPropagation"]);
this.target.onSubmitNewConversation(event);
expect(event.preventDefault).toHaveBeenCalled();
expect(event.stopPropagation).toHaveBeenCalled();
});
it("submits a conversation with recipients", function() {
spyOn(app.views.ConversationsForm.prototype, "getConversationParticipants").and.returnValue([1]);
var event = jasmine.createSpyObj("event", ["preventDefault", "stopPropagation"]);
this.target.onSubmitNewConversation(event);
expect(event.preventDefault).toHaveBeenCalled();
expect(event.stopPropagation).not.toHaveBeenCalled();
});
it("flashes an error message when submitting a conversation with no recipient", function() {
spyOn(app.views.FlashMessages.prototype, "error");
spyOn(app.views.ConversationsForm.prototype, "getConversationParticipants").and.returnValue([]);
var event = jasmine.createSpyObj("event", ["preventDefault", "stopPropagation"]);
this.target.onSubmitNewConversation(event);
expect(app.views.FlashMessages.prototype.error)
.toHaveBeenCalledWith(Diaspora.I18n.t("conversation.create.no_recipient"));
});
it("does not flash an error message when submitting a conversation with recipients", function() {
spyOn(app.views.FlashMessages.prototype, "error");
spyOn(app.views.ConversationsForm.prototype, "getConversationParticipants").and.returnValue([1]);
var event = jasmine.createSpyObj("event", ["preventDefault", "stopPropagation"]);
this.target.onSubmitNewConversation(event);
expect(app.views.FlashMessages.prototype.error).not
.toHaveBeenCalledWith(Diaspora.I18n.t("conversation.create.no_recipient"));
});
});
});

View file

@ -0,0 +1,164 @@
describe("app.views.ConversationsInbox", function() {
describe("initialize", function() {
beforeEach(function() {
spec.loadFixture("conversations_read");
$("#conversation-new").removeClass("hidden");
$("#conversation-show").addClass("hidden");
});
it("initializes the conversations form", function() {
spyOn(app.views.ConversationsForm.prototype, "initialize");
new app.views.ConversationsInbox();
expect(app.views.ConversationsForm.prototype.initialize).toHaveBeenCalled();
});
it("call setupConversation", function() {
spyOn(app.views.ConversationsInbox.prototype, "setupConversation");
new app.views.ConversationsInbox();
expect(app.views.ConversationsInbox.prototype.setupConversation).toHaveBeenCalled();
});
});
describe("renderConversation", function() {
beforeEach(function() {
spec.loadFixture("conversations_read");
$("#conversation-new").removeClass("hidden");
$("#conversation-show").addClass("hidden");
var conversations = $("#conversation-inbox .stream-element");
conversations.removeClass("selected");
this.conversationId = conversations.first().data("guid");
this.target = new app.views.ConversationsInbox();
});
it("renders conversation of given id", function() {
spyOn($, "ajax").and.callThrough();
spyOn(app.views.ConversationsInbox.prototype, "selectConversation");
spyOn(app.views.ConversationsInbox.prototype, "setupConversation");
this.target.renderConversation(this.conversationId);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
responseText: "<div id='fake-conversation-content'></div>"
});
expect($.ajax).toHaveBeenCalled();
expect(jasmine.Ajax.requests.mostRecent().url).toBe("/conversations/" + this.conversationId + "/raw");
expect(app.views.ConversationsInbox.prototype.selectConversation).toHaveBeenCalledWith(this.conversationId);
expect(app.views.ConversationsInbox.prototype.setupConversation).toHaveBeenCalled();
expect($("#conversation-new")).toHaveClass("hidden");
expect($("#conversation-show")).not.toHaveClass("hidden");
expect($("#conversation-show #fake-conversation-content").length).toBe(1);
});
});
describe("selectConversation", function() {
beforeEach(function() {
spec.loadFixture("conversations_read");
this.conversationId = $("#conversation-inbox .stream-element").first().data("guid");
this.target = new app.views.ConversationsInbox();
$("#conversation-inbox .stream-element").addClass("selected");
});
it("unselects every conversation if called with no parameters", function() {
expect($("#conversation-inbox .stream-element.selected").length).not.toBe(0);
this.target.selectConversation();
expect($("#conversation-inbox .stream-element.selected").length).toBe(0);
});
it("selects the given conversation", function() {
expect($("#conversation-inbox .stream-element.selected").length).not.toBe(1);
this.target.selectConversation(this.conversationId);
expect($("#conversation-inbox .stream-element.selected").length).toBe(1);
expect($("#conversation-inbox .stream-element.selected").data("guid")).toBe(this.conversationId);
});
});
describe("displayNewConversation", function() {
beforeEach(function() {
spec.loadFixture("conversations_read");
$("#conversation-new").addClass("hidden");
$("#conversation-show").removeClass("hidden");
spyOn(app.views.ConversationsInbox.prototype, "selectConversation");
new app.views.ConversationsInbox();
});
it("displays the new conversation panel", function() {
$(".new-conversation-btn").click();
expect(app.views.ConversationsInbox.prototype.selectConversation).toHaveBeenCalledWith();
expect($("#conversation-new")).not.toHaveClass("hidden");
expect($("#conversation-show")).toHaveClass("hidden");
expect(window.location.pathname).toBe("/conversations");
});
});
describe("setupConversation", function() {
context("for unread conversations", function() {
beforeEach(function() {
spec.loadFixture("conversations_unread");
// select second conversation that is still unread
$(".conversation-wrapper > .conversation.selected").removeClass("selected");
$(".conversation-wrapper > .conversation.unread").addClass("selected");
});
it("removes the unread class from the conversation", function() {
expect($(".conversation-wrapper > .conversation.selected")).toHaveClass("unread");
new app.views.ConversationsInbox();
expect($(".conversation-wrapper > .conversation.selected")).not.toHaveClass("unread");
});
it("removes the unread message counter from the conversation", function() {
expect($(".conversation-wrapper > .conversation.selected .unread-message-count").length).toEqual(1);
new app.views.ConversationsInbox();
expect($(".conversation-wrapper > .conversation.selected .unread-message-count").length).toEqual(0);
});
it("decreases the unread message count in the header", function() {
var badge = "<div id=\"conversations-link\"><div class=\"badge\">3</div></div>";
$("header").append(badge);
expect($("#conversations-link .badge").text().trim()).toEqual("3");
expect($(".conversation-wrapper > .conversation .unread-message-count").text().trim()).toEqual("1");
new app.views.ConversationsInbox();
expect($("#conversations-link .badge").text().trim()).toEqual("2");
});
it("removes the badge in the header if there are no unread messages left", function() {
var badge = "<div id=\"conversations-link\"><div class=\"badge\">1</div></div>";
$("header").append(badge);
expect($("#conversations-link .badge").text().trim()).toEqual("1");
expect($(".conversation-wrapper > .conversation.selected .unread-message-count").text().trim()).toEqual("1");
new app.views.ConversationsInbox();
expect($("#conversations-link .badge").text().trim()).toEqual("0");
expect($("#conversations-link .badge")).toHaveClass("hidden");
});
});
context("for read conversations", function() {
beforeEach(function() {
spec.loadFixture("conversations_read");
});
it("does not change the badge in the header", function() {
var badge = "<div id=\"conversations-link\"><div class=\"badge\">3</div></div>";
$("header").append(badge);
expect($("#conversations-link .badge").text().trim()).toEqual("3");
new app.views.ConversationsInbox();
expect($("#conversations-link .badge").text().trim()).toEqual("3");
});
});
});
describe("displayConversation", function() {
beforeEach(function() {
spyOn(app.router, "navigate");
spec.loadFixture("conversations_read");
new app.views.ConversationsInbox();
});
it("calls app.router.navigate with correct parameters", function() {
var conversationEl = $(".conversation-wrapper").first();
var conversationPath = conversationEl.data("conversation-path");
conversationEl.children().first().click();
expect(app.router.navigate).toHaveBeenCalledWith(conversationPath, {trigger: true});
});
});
});

View file

@ -1,79 +0,0 @@
describe("app.views.Conversations", function(){
describe("setupConversation", function() {
context("for unread conversations", function() {
beforeEach(function() {
spec.loadFixture("conversations_unread");
// select second conversation that is still unread
$(".conversation-wrapper > .conversation.selected").removeClass("selected");
$(".conversation-wrapper > .conversation.unread").addClass("selected");
});
it("removes the unread class from the conversation", function() {
expect($(".conversation-wrapper > .conversation.selected")).toHaveClass("unread");
new app.views.Conversations();
expect($(".conversation-wrapper > .conversation.selected")).not.toHaveClass("unread");
});
it("removes the unread message counter from the conversation", function() {
expect($(".conversation-wrapper > .conversation.selected .unread-message-count").length).toEqual(1);
new app.views.Conversations();
expect($(".conversation-wrapper > .conversation.selected .unread-message-count").length).toEqual(0);
});
it("decreases the unread message count in the header", function() {
var badge = "<div id=\"conversations-link\"><div class=\"badge\">3</div></div>";
$("header").append(badge);
expect($("#conversations-link .badge").text().trim()).toEqual("3");
expect($(".conversation-wrapper > .conversation .unread-message-count").text().trim()).toEqual("1");
new app.views.Conversations();
expect($("#conversations-link .badge").text().trim()).toEqual("2");
});
it("removes the badge in the header if there are no unread messages left", function() {
var badge = "<div id=\"conversations-link\"><div class=\"badge\">1</div></div>";
$("header").append(badge);
expect($("#conversations-link .badge").text().trim()).toEqual("1");
expect($(".conversation-wrapper > .conversation.selected .unread-message-count").text().trim()).toEqual("1");
new app.views.Conversations();
expect($("#conversations-link .badge").text().trim()).toEqual("0");
expect($("#conversations-link .badge")).toHaveClass("hidden");
});
});
context("for read conversations", function() {
beforeEach(function() {
spec.loadFixture("conversations_read");
});
it("does not change the badge in the header", function() {
var badge = "<div id=\"conversations-link\"><div class=\"badge\">3</div></div>";
$("header").append(badge);
expect($("#conversations-link .badge").text().trim()).toEqual("3");
new app.views.Conversations();
expect($("#conversations-link .badge").text().trim()).toEqual("3");
});
});
});
describe("keyDown", function(){
beforeEach(function() {
this.submitCallback = jasmine.createSpy().and.returnValue(false);
spec.loadFixture("conversations_read");
new app.views.Conversations();
});
it("should submit the form with ctrl+enter", function(){
$("form#new_message").submit(this.submitCallback);
var e = $.Event("keydown", { which: Keycodes.ENTER, ctrlKey: true });
$("textarea#message_text").trigger(e);
expect(this.submitCallback).toHaveBeenCalled();
});
it("shouldn't submit the form without the ctrl key", function(){
$("form#new_message").submit(this.submitCallback);
var e = $.Event("keydown", { which: Keycodes.ENTER, ctrlKey: false });
$("textarea#message_text").trigger(e);
expect(this.submitCallback).not.toHaveBeenCalled();
});
});
});