Commit e4248968 authored by Augier's avatar Augier Committed by Steffen van Bergerem

Fully port conversations to Backbone and drop inbox.js

parent caf46fdc
......@@ -9,7 +9,7 @@ app.Router = Backbone.Router.extend({
"commented(/)": "stream",
"community_spotlight(/)": "spotlight",
"contacts(/)": "contacts",
"conversations(/)": "conversations",
"conversations(/)(:id)(/)": "conversations",
"followed_tags(/)": "followed_tags",
"getting_started(/)": "gettingStarted",
"help(/)": "help",
......@@ -93,8 +93,11 @@ app.Router = Backbone.Router.extend({
app.page = new app.pages.Contacts({stream: stream});
},
conversations: function() {
app.conversations = new app.views.Conversations();
conversations: function(id) {
app.conversations = app.conversations || new app.views.ConversationsInbox();
if (parseInt("" + id, 10)) {
app.conversations.renderConversation(id);
}
},
/* eslint-disable camelcase */
......
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.views.ConversationsForm = Backbone.View.extend({
el: ".conversations-form-container",
events: {
"keydown textarea#conversation_text" : "keyDown",
"keydown .conversation-message-text": "keyDown",
"submit #conversation-new": "onSubmitNewConversation"
},
initialize: function(opts) {
this.contacts = _.has(opts, "contacts") ? opts.contacts : null;
if(!this.contacts || this.contacts.length === 0) {
this.displayNoContactsMessage();
return;
}
this.prefill = [];
if (_.has(opts, "prefillName") && _.has(opts, "prefillValue")) {
this.prefill = [{name : opts.prefillName,
value : opts.prefillValue}];
this.prefill = [{name: opts.prefillName, value: opts.prefillValue}];
}
this.autocompleteInput = $("#contact_autocomplete");
this.prepareAutocomplete(this.contacts);
},
prepareAutocomplete: function(data){
this.autocompleteInput.autoSuggest(data, {
this.$("#contact-autocomplete").autoSuggest(data, {
selectedItemProp: "name",
searchObjProps: "name",
asHtmlID: "contact_ids",
......@@ -32,20 +28,26 @@ app.views.ConversationsForm = Backbone.View.extend({
startText: '',
emptyText: Diaspora.I18n.t("no_results"),
preFill: this.prefill
}).focus();
$("#contact_ids").attr("aria-labelledby", "toLabel");
},
displayNoContactsMessage: function() {
$("form#new_conversation").replaceWith(
"<div class=\"well text-center\">" + Diaspora.I18n.t("conversation.new.no_contacts") + "</div>"
);
});
$("#contact_ids").attr("aria-labelledby", "toLabel").focus();
},
keyDown : function(evt) {
if(evt.which === Keycodes.ENTER && evt.ctrlKey) {
$(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 magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.views.Conversations = Backbone.View.extend({
el: "#conversations_container",
app.views.ConversationsInbox = Backbone.View.extend({
el: "#conversations-container",
events: {
"keydown textarea#message_text" : "keyDown",
"conversation:loaded" : "setupConversation"
"click .conversation-wrapper": "displayConversation",
"click .new-conversation-btn": "displayNewConversation"
},
initialize: function() {
if($("#conversation_new:visible").length > 0) {
new app.views.ConversationsForm({
el: $("#conversation_new"),
contacts: gon.contacts
});
}
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"});
......@@ -26,13 +50,13 @@ app.views.Conversations = Backbone.View.extend({
var conv = $(".conversation-wrapper .stream-element.selected"),
cBadge = $("#conversations-link .badge");
if(conv.hasClass("unread") ){
if (conv.hasClass("unread")) {
var unreadCount = parseInt(conv.find(".unread-message-count").text(), 10);
if(cBadge.text() !== "") {
cBadge.text().replace(/\d+/, function(num){
if (cBadge.text() !== "") {
cBadge.text().replace(/\d+/, function(num) {
num = parseInt(num, 10) - unreadCount;
if(num > 0) {
if (num > 0) {
cBadge.text(num);
} else {
cBadge.text(0).addClass("hidden");
......@@ -43,16 +67,15 @@ app.views.Conversations = Backbone.View.extend({
conv.find(".unread-message-count").remove();
var pos = $("#first_unread").offset().top - 50;
$("html").animate({scrollTop:pos});
$("html").animate({scrollTop: pos});
} else {
$("html").animate({scrollTop:0});
$("html").animate({scrollTop: 0});
}
},
keyDown : function(evt) {
if(evt.which === Keycodes.ENTER && evt.ctrlKey) {
$(evt.target).parents("form").submit();
}
displayConversation: function(evt) {
var $target = $(evt.target).closest(".conversation-wrapper");
app.router.navigate($target.data("conversation-path"), {trigger: true});
}
});
// @license-end
......
// @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
......@@ -3,7 +3,6 @@
//= require templates
//= require main
//= require fileuploader-custom
//= require inbox
//= require mobile/mobile
//= require jquery.autoSuggest.custom
//= require contact-list
......
......@@ -594,7 +594,8 @@ form#new_user.new_user input.btn {
text-shadow: 1px 1px 20px rgb(126, 240, 77);
}
#conversation_inbox, .notifications {
.conversation-inbox,
.notifications {
div.pagination {
width: 100%;
margin-left: auto;
......
......@@ -24,7 +24,7 @@ class ConversationsController < ApplicationController
gon.contacts = contacts_data
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 }
end
end
......@@ -53,7 +53,7 @@ class ConversationsController < ApplicationController
@response[:success] = false
@response[:message] = I18n.t('conversations.create.fail')
if person_ids.blank?
@response[:message] = I18n.t('conversations.create.no_contact')
@response[:message] = I18n.t("javascripts.conversation.create.no_recipient")
end
end
respond_to do |format|
......@@ -64,7 +64,7 @@ class ConversationsController < ApplicationController
def show
respond_to do |format|
format.html do
redirect_to conversations_path(:conversation_id => params[:id])
redirect_to conversations_path(conversation_id: params[:id])
return
end
......@@ -72,7 +72,6 @@ class ConversationsController < ApplicationController
@first_unread_message_id = @conversation.first_unread_message(current_user).try(:id)
@conversation.set_read(current_user)
format.js
format.json { render :json => @conversation, :status => 200 }
else
redirect_to conversations_path
......@@ -80,6 +79,17 @@ class ConversationsController < ApplicationController
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
if !params[:modal] && !session[:mobile_view] && request.format.html?
redirect_to conversations_path
......
......@@ -6,17 +6,18 @@
.media-left
= owner_image_tag(:thumb_small)
.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
%label#messageLabel.sr-only{for: "message_text"}
= t("conversations.new.message")
%label.sr-only#message-label{for: "response-message-text"}= t("conversations.new.message")
= message.text_area :text,
rows: 5,
rows: 5,
tabindex: 1,
class: "form-control form-group",
aria: {labelledby: "messageLabel"}
id: "response-message-text",
class: "form-control form-group conversation-message-text",
aria: {labelledby: "message-label"}
= message.submit t("conversations.show.reply"),
"data-disable-with" => t("conversations.show.replying"),
class: "btn btn-primary pull-right", tabindex: 2
"data-disable-with" => t("conversations.show.replying"),
:class => "btn btn-primary pull-right",
:tabindex => 2
.clearfix
.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
%label#toLabel{for: "contact_ids"}
= 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
%label#subjectLabel{for: "conversation_subject"}
%label#subject-label{for: "conversation-subject"}
= t(".subject")
= conversation.text_field :subject,
id: "conversation-subject",
class: "input-block-level form-control",
aria: {labelledby: "subjectLabel"}
aria: {labelledby: "subject-label"},
value: "",
placeholder: t("conversations.new.subject_default")
.form-group
%label#messageLabel.sr-only{for: "conversation_text"}
= t(".message")
= text_area_tag "conversation[text]",
"",
rows: 5,
class: "input-block-level form-control",
aria: {labelledby: "messageLabel"}
%label.sr-only#message-label{for: "new-message-text"} = t(".message")
= text_area_tag "conversation[text]", "",
rows: 5,
id: "new-message-text",
class: "conversation-message-text input-block-level form-control",
aria: {labelledby: "message-label"}
.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"
var response = <%= raw @response.to_json %>;
<% if session[:mobile_view] %>
if(response.success) {
window.location.href = "<%= conversations_path(conversation_id: @conversation.id) %>";
}
<% else %>
if(response.success){
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) %>";
} else {
app.flashMessages.error(response.message);
......
- content_for :head do
= javascript_include_tag :inbox
- content_for :page_title do
= t('.conversations_inbox')
= t(".conversations_inbox")
.container-fluid#conversations_container
.container-fluid#conversations-container
.row
.col-md-4
.sidebar#left_pane
.sidebar-header.clearfix#left_pane_header
.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
= t(".inbox")
.conversation-inbox#conversation_inbox
.stream.conversations
.conversation-inbox#conversation-inbox
.conversations-form-container.stream.conversations
- if @visibilities.count > 0
= render partial: "conversations/conversation", collection: @visibilities, as: :visibility
- else
.no-conversations
= t('.no_messages')
= t(".no_messages")
.pagination-container
= will_paginate @visibilities, previous_label: "&laquo;", next_label: "&raquo;", inner_window: 1,
renderer: WillPaginate::ActionView::BootstrapLinkRenderer
.col-md-8
- if @conversation
.stream_container
#conversation_show
.conversations-form-container.stream_container
#conversation-show{class: @conversation ? "" : "hidden"}
- if @conversation
= render 'conversations/show', conversation: @conversation
- else
.stream_container.hidden
#conversation_show
.framed-content.clearfix#conversation_new
#conversation-new{class: @conversation ? "framed-content clearfix hidden" : "framed-content clearfix"}
.new-conversation
%h3.text-center
= t("conversations.index.new_conversation")
= render "conversations/new"
%h3.text-center= t("conversations.index.new_conversation")
- if no_contacts
.well.text-center= t("javascripts.conversation.new.no_contacts")
- else
= render "conversations/new"
......@@ -12,7 +12,7 @@
.stream
%p{ class: "conversation_#{name}" }= msg
#conversation_inbox
.conversation-inbox#conversation-inbox
.stream.conversations
- if @visibilities.count > 0
= render partial: "conversations/conversation", collection: @visibilities, as: :visibility
......
......@@ -5,7 +5,7 @@
:javascript
$(document).ready(function () {
var data = $.parseJSON( "#{escape_javascript(@contacts_json)}" ),
autocompleteInput = $("#contact_autocomplete");
autocompleteInput = $("#contact-autocomplete");
autocompleteInput.autoSuggest(data, {
selectedItemProp: "name",
......
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");
......@@ -27,7 +27,7 @@
id: 'mentionModal'
-if @contact
#new_conversation_pane
.conversations-form-container#new_conversation_pane
= render 'shared/modal',
path: new_conversation_path(:contact_id => @contact.id, name: @contact.person.name, modal: true),
title: t('conversations.index.new_conversation'),
......
......@@ -274,6 +274,7 @@ en:
new_conversation: "New conversation"
no_messages: "No messages"
inbox: "Inbox"
no_contacts: "You need to add some contacts before you can start a conversation"
show:
reply: "Reply"
replying: "Replying..."
......@@ -290,7 +291,6 @@ en:
create:
sent: "Message sent"
fail: "Invalid message"
no_contact: "Hey, you need to add the contact first!"
new_conversation:
fail: "Invalid message"
destroy:
......
......@@ -240,6 +240,8 @@ en:
posts: "Posts"
conversation:
create:
no_recipient: "Hey, you need to add a recipient first!"
new:
no_contacts: "You need to add some contacts before you can start a conversation."
......
......@@ -77,6 +77,7 @@ Diaspora::Application.routes.draw do
resources :conversations, except: %i(edit update destroy) do
resources :messages, only: %i(create)
delete 'visibility' => 'conversation_visibilities#destroy'
get "raw"
end
resources :notifications, :only => [:index, :update] do
......
......@@ -18,10 +18,10 @@ Feature: private conversations
Scenario: send a message
When I sign in as "bob@bob.bob"
And I send a message with subject "Greetings" and text "hello, alice!" to "Alice Awesome"
Then I should see "Greetings" within "#conversation_inbox"
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_show"
Then I should see "Greetings" within "#conversation-inbox"
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-show"
And I should see "Alice Awesome" as a participant
And "Alice Awesome" should be part of active conversation
And I should see "hello, alice!" within ".stream_container"
......@@ -34,8 +34,8 @@ Feature: private conversations
Scenario: send a message using keyboard shortcuts
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
Then I should see "Greetings" within "#conversation_inbox"
And I should see "Greetings" within "#conversation_show"
Then I should see "Greetings" within "#conversation-inbox"
And I should see "Greetings" within "#conversation-show"
And "Alice Awesome" should be part of active conversation
And I should see "hello, alice!" within ".stream_container"
When I reply with "hey, how you doing?" using keyboard shortcuts
......@@ -47,9 +47,9 @@ Feature: private conversations
Scenario: delete a conversation
When I sign in as "bob@bob.bob"
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"
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"
Then I should have 1 unread private message
And I should have 1 email delivery
......
......@@ -14,38 +14,38 @@ end
Then /^I send a message with subject "([^"]*)" and text "([^"]*)" to "([^"]*)"$/ do |subject, text, person|
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 press the first ".as-result-item" within ".as-results")
step %(I fill in "conversation_subject" with "#{subject}")
step %(I fill in "conversation_text" with "#{text}")
step %(I fill in "conversation-subject" with "#{subject}")
step %(I fill in "new-message-text" with "#{text}")
step %(I press "Send")
end
end
Then /^I send a message with subject "([^"]*)" and text "([^"]*)" to "([^"]*)" using keyboard shortcuts$/ do |subject, text, person|
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 press the first ".as-result-item" within ".as-results")
step %(I fill in "conversation_subject" with "#{subject}")
step %(I fill in "conversation_text" with "#{text}")
find("#conversation_text").native.send_key %i(Ctrl Return)
step %(I fill in "conversation-subject" with "#{subject}")
step %(I fill in "new-message-text" with "#{text}")
find("#new-message-text").native.send_key %i(Ctrl Return)
end
end
When /^I reply with "([^"]*)"$/ do |text|
step %(I am on the conversations page)
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")
end
When /^I reply with "([^"]*)" using keyboard shortcuts$/ do |text|
step %(I am on the conversations page)
step %(I press the first ".conversation" within ".conversations")
step %(I fill in "message_text" with "#{text}")
find("#message_text").native.send_key %i(Ctrl Return)
step %(I fill in "response-message-text" with "#{text}")
find("#response-message-text").native.send_key %i(Ctrl Return)
end
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 fill in "contact_autocomplete" with "#{person}")
step %(I press the first ".as-result-item" within ".as-results")
step %(I fill in "conversation_subject" with "#{subject}")
step %(I fill in "conversation_text" with "#{text}")
step %(I fill in "conversation-subject" with "#{subject}")
step %(I fill in "new-message-text" with "#{text}")
step %(I press "Send")
end
......
......@@ -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
post :create, @hash
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
......@@ -300,12 +300,6 @@ describe ConversationsController, :type => :controller do
@conversation = Conversation.create(hash)
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
get :show, :id => @conversation.id, :format => :json
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))
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
......@@ -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() {
it("calls _initializeStreamView", function() {
spyOn(app.router, "_initializeStreamView");
......
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});