Implement persistent drafts functionality

* Created a drafts modal to display/restore/delete drafts
* Created a Draft model to support storing draft data in localstorage
* Removed existing restore-draft functionality
* Added casper and node tests for drafts functionality

Fixes #1717.
This commit is contained in:
Sampriti Panda 2017-02-22 07:04:05 +05:30 committed by Tim Abbott
parent 8b22b94ab1
commit 1929cc5190
18 changed files with 861 additions and 59 deletions

View File

@ -94,7 +94,8 @@
"unread_ui": false, "unread_ui": false,
"user_events": false, "user_events": false,
"Plotly": false, "Plotly": false,
"emoji_codes": false "emoji_codes": false,
"drafts": false
}, },
"rules": { "rules": {
"no-restricted-syntax": 0, "no-restricted-syntax": 0,

View File

@ -0,0 +1,209 @@
var common = require('../casper_lib/common.js').common;
function waitWhileDraftsVisible(then) {
casper.waitFor(function () {
return casper.evaluate(function () {
return $("#draft_overlay").length === 0 ||
$("#draft_overlay").css("opacity") === "0";
});
}, then);
}
function waitUntilDraftsVisible(then) {
casper.waitFor(function () {
return casper.evaluate(function () {
return $("#draft_overlay").length === 1 &&
$("#draft_overlay").css("opacity") === "1";
});
}, then);
}
common.start_and_log_in();
casper.then(function () {
casper.test.info('Drafts page');
casper.waitUntilVisible('.drafts-link', function () {
casper.click('.drafts-link');
});
});
casper.then(function () {
casper.test.assertUrlMatch(/^http:\/\/[^/]+\/#drafts/,
'URL suggests we are on drafts page');
waitUntilDraftsVisible(function () {
casper.test.assertExists('#draft_overlay', 'Drafts page is active');
casper.test.assertSelectorHasText('.no-drafts', 'No Drafts.');
});
});
casper.then(function () {
casper.click('#draft_overlay .exit');
waitWhileDraftsVisible();
});
casper.then(function () {
casper.test.info('Creating Stream Message Draft');
casper.click('body');
casper.page.sendEvent('keypress', "c");
casper.waitUntilVisible('#stream-message', function () {
casper.test.info('Creating Private Message Draft');
casper.fill('form#send_message_form', {
stream: 'all',
subject: 'tests',
content: 'Test Stream Message',
}, false);
casper.click("#compose_close");
});
});
casper.then(function () {
casper.test.info('Creating Private Message Draft');
casper.click('body');
casper.page.sendEvent('keypress', "C");
casper.waitUntilVisible('#private-message', function () {
casper.fill('form#send_message_form', {
recipient: 'cordelia@zulip.com, hamlet@zulip.com',
content: 'Test Private Message',
}, false);
casper.click("#compose_close");
});
});
casper.then(function () {
casper.waitUntilVisible('.drafts-link', function () {
casper.click('.drafts-link');
});
});
casper.then(function () {
waitUntilDraftsVisible(function () {
casper.test.assertElementCount('.draft-row', 2, 'Drafts loaded');
casper.test.assertSelectorHasText('.draft-row .message_header_stream .stream_label', 'all');
casper.test.assertSelectorHasText('.draft-row .message_header_stream .stream_topic', 'tests');
casper.test.assertTextExists('Test Stream Message', 'Stream draft contains message content');
casper.test.assertSelectorHasText('.draft-row .message_header_private_message .stream_label',
'You and Cordelia Lear, King Hamlet');
casper.test.assertTextExists('Test Private Message', 'Private draft contains message content');
});
});
casper.then(function () {
casper.test.info('Restoring Stream Message Draft');
casper.click("#drafts_table .message_row:not(.private-message) .restore-draft");
waitWhileDraftsVisible(function () {
casper.test.assertVisible('#stream-message', 'Stream Message Box Restored');
common.check_form('form#send_message_form', {
stream: 'all',
subject: 'tests',
content: 'Test Stream Message',
}, "Stream message box filled with draft content");
});
});
casper.then(function () {
casper.test.info('Editing Stream Message Draft');
casper.fill('form#send_message_form', {
stream: 'all',
subject: 'tests',
content: 'Updated Stream Message',
}, false);
casper.click("#compose_close");
});
casper.then(function () {
casper.waitUntilVisible('.drafts-link', function () {
casper.click('.drafts-link');
});
});
casper.then(function () {
waitUntilDraftsVisible(function () {
casper.test.assertSelectorHasText('.draft-row .message_header_stream .stream_label', 'all');
casper.test.assertSelectorHasText('.draft-row .message_header_stream .stream_topic', 'tests');
casper.test.assertTextExists('Updated Stream Message', 'Stream draft contains message content');
});
});
casper.then(function () {
casper.test.info('Restoring Private Message Draft');
casper.click("#drafts_table .message_row.private-message .restore-draft");
waitWhileDraftsVisible(function () {
casper.test.assertVisible('#private-message', 'Private Message Box Restored');
common.check_form('form#send_message_form', {
recipient: 'cordelia@zulip.com, hamlet@zulip.com',
content: 'Test Private Message',
}, "Private message box filled with draft content");
});
});
casper.then(function () {
casper.click("#compose_close");
casper.waitUntilVisible('.drafts-link', function () {
casper.click('.drafts-link');
});
});
casper.then(function () {
casper.test.info('Deleting Draft');
casper.click("#drafts_table .message_row.private-message .delete-draft");
casper.test.assertElementCount('.draft-row', 1, 'Draft deleted');
casper.test.assertDoesntExist("#drafts_table .message_row.private-message");
});
casper.then(function () {
casper.test.info('Saving Draft by Reloading');
casper.click('#draft_overlay .exit');
waitWhileDraftsVisible(function () {
casper.click('body');
casper.page.sendEvent('keypress', "C");
casper.waitUntilVisible('#private-message', function () {
casper.fill('form#send_message_form', {
recipient: 'cordelia@zulip.com',
content: 'Test Private Message',
}, false);
casper.reload();
});
});
});
casper.then(function () {
casper.waitUntilVisible('.drafts-link', function () {
casper.click('.drafts-link');
});
waitUntilDraftsVisible(function () {
casper.test.assertElementCount('.draft-row', 2, 'Drafts loaded');
casper.test.assertSelectorHasText('.draft-row .message_header_private_message .stream_label',
'You and Cordelia Lear');
casper.test.assertTextExists('Test Private Message');
});
});
casper.then(function () {
casper.test.info('Deleting Draft after Sending Message');
casper.click("#drafts_table .message_row.private-message .restore-draft");
waitWhileDraftsVisible(function () {
casper.test.assertVisible('#private-message');
casper.click('#compose-send-button');
});
});
casper.then(function () {
// This tests the second drafts link in the compose area
casper.waitUntilVisible('.compose_table .drafts-link', function () {
casper.click('.compose_table .drafts-link');
});
waitUntilDraftsVisible(function () {
casper.test.assertElementCount('.draft-row', 1, 'Drafts loaded');
casper.test.assertDoesntExist("#drafts_table .message_row.private-message");
});
});
common.then_log_out();
casper.run(function () {
casper.test.done();
});

View File

@ -0,0 +1,95 @@
global.stub_out_jquery();
add_dependencies({
localstorage: 'js/localstorage',
drafts: 'js/drafts',
});
var ls_container = {};
set_global('localStorage', {
getItem: function (key) {
return ls_container[key];
},
setItem: function (key, val) {
ls_container[key] = val;
},
removeItem: function (key) {
delete ls_container[key];
},
clear: function () {
ls_container = {};
},
});
function stub_timestamp(model, timestamp, func) {
var original_func = model.getTimestamp;
model.getTimestamp = function () {
return timestamp;
};
func();
model.getTimestamp = original_func;
}
var draft_1 = {
stream: "stream",
subject: "topic",
type: "stream",
content: "Test Stream Message",
};
var draft_2 = {
private_message_recipient: "aaron@zulip.com",
type: "private",
content: "Test Private Message",
};
(function test_draft_model() {
var draft_model = drafts.draft_model;
var ls = localstorage();
localStorage.clear();
(function test_get() {
var expected = { id1: draft_1, id2: draft_2 };
ls.set("drafts", expected);
assert.deepEqual(draft_model.get(), expected);
}());
localStorage.clear();
(function test_get() {
ls.set("drafts", { id1: draft_1 });
assert.deepEqual(draft_model.getDraft("id1"), draft_1);
assert.equal(draft_model.getDraft("id2"), false);
}());
localStorage.clear();
(function test_addDraft() {
stub_timestamp(draft_model, 1, function () {
var expected = draft_1;
expected.updatedAt = 1;
var id = draft_model.addDraft(draft_1);
assert.deepEqual(ls.get("drafts")[id], expected);
});
}());
localStorage.clear();
(function test_editDraft() {
stub_timestamp(draft_model, 2, function () {
ls.set("drafts", { id1: draft_1 } );
var expected = draft_2;
expected.updatedAt = 2;
draft_model.editDraft("id1", draft_2);
assert.deepEqual(ls.get("drafts").id1, expected);
});
}());
localStorage.clear();
(function test_deleteDraft() {
ls.set("drafts", { id1: draft_1 } );
draft_model.deleteDraft("id1");
assert.deepEqual(ls.get("drafts"), {});
}());
}());

View File

@ -393,6 +393,51 @@ function render(template_name, args) {
assert.equal(a.text(), "Narrow to here"); assert.equal(a.text(), "Narrow to here");
}()); }());
(function draft_table_body() {
var args = {
drafts: [
{
draft_id: '1',
is_stream: true,
stream: 'all',
stream_color: '#FF0000', // rgb(255, 0, 0)
topic: 'tests',
content: 'Public draft',
},
{
draft_id: '2',
is_stream: false,
recipients: 'Jordan, Michael',
content: 'Private draft',
},
],
};
var html = '';
html += '<div id="drafts_table">';
html += render('draft_table_body', args);
html += '</div>';
global.write_handlebars_output("draft_table_body", html);
var row_1 = $(html).find(".draft-row[data-draft-id='1']");
assert.equal(row_1.find(".stream_label").text().trim(), "all");
assert.equal(row_1.find(".stream_label").css("background"), "rgb(255, 0, 0)");
assert.equal(row_1.find(".stream_topic").text().trim(), "tests");
assert(!row_1.find(".message_row").hasClass("private-message"));
assert.equal(row_1.find(".messagebox").css("box-shadow"),
"inset 2px 0px 0px 0px #FF0000, -1px 0px 0px 0px #FF0000");
assert.equal(row_1.find(".message_content").text().trim(), "Public draft");
var row_2 = $(html).find(".draft-row[data-draft-id='2']");
assert.equal(row_2.find(".stream_label").text().trim(), "You and Jordan, Michael");
assert(row_2.find(".message_row").hasClass("private-message"));
assert.equal(row_2.find(".messagebox").css("box-shadow"),
"inset 2px 0px 0px 0px #444444, -1px 0px 0px 0px #444444");
assert.equal(row_2.find(".message_content").text().trim(), "Private draft");
}());
(function email_address_hint() { (function email_address_hint() {
var html = render('email_address_hint'); var html = render('email_address_hint');
global.write_handlebars_output("email_address_hint", html); global.write_handlebars_output("email_address_hint", html);

View File

@ -268,6 +268,18 @@ $(function () {
subs.close(); subs.close();
} }
}); });
$("#drafts_table").on("click", ".exit, #draft_overlay", function (e) {
if (meta.focusing) {
meta.focusing = false;
return;
}
if ($(e.target).is(".exit, .exit-sign, #draft_overlay, #draft_overlay > .flex")) {
drafts.close();
}
});
// HOME // HOME
// Capture both the left-sidebar Home click and the tab breadcrumb Home // Capture both the left-sidebar Home click and the tab breadcrumb Home

View File

@ -15,8 +15,6 @@ var user_acknowledged_all_everyone;
exports.all_everyone_warn_threshold = 15; exports.all_everyone_warn_threshold = 15;
var message_snapshot;
var uploads_domain = document.location.protocol + '//' + document.location.host; var uploads_domain = document.location.protocol + '//' + document.location.host;
var uploads_path = '/user_uploads'; var uploads_path = '/user_uploads';
var uploads_re = new RegExp("\\]\\(" + uploads_domain + "(" + uploads_path + "[^\\)]+)\\)", 'g'); var uploads_re = new RegExp("\\]\\(" + uploads_domain + "(" + uploads_path + "[^\\)]+)\\)", 'g');
@ -116,11 +114,11 @@ function clear_invites() {
} }
function clear_box() { function clear_box() {
exports.snapshot_message();
clear_invites(); clear_invites();
clear_all_everyone_warnings(); clear_all_everyone_warnings();
user_acknowledged_all_everyone = undefined; user_acknowledged_all_everyone = undefined;
$("#compose").find('input[type=text], textarea').val(''); $("#compose").find('input[type=text], textarea').val('');
$("#new_message_content").removeData("draft-id");
exports.autosize_textarea(); exports.autosize_textarea();
$("#send-status").hide(0); $("#send-status").hide(0);
} }
@ -131,9 +129,6 @@ function clear_preview_area() {
$("#preview_message_area").hide(); $("#preview_message_area").hide();
$("#preview_content").empty(); $("#preview_content").empty();
$("#markdown_preview").show(); $("#markdown_preview").show();
if (message_snapshot !== undefined) {
$('#restore-draft').show();
}
} }
function hide_box() { function hide_box() {
@ -307,9 +302,6 @@ exports.cancel = function () {
notifications.clear_compose_notifications(); notifications.clear_compose_notifications();
abort_xhr(); abort_xhr();
is_composing_message = false; is_composing_message = false;
if (message_snapshot !== undefined) {
$('#restore-draft').show();
}
$(document).trigger($.Event('compose_canceled.zulip')); $(document).trigger($.Event('compose_canceled.zulip'));
}; };
@ -355,40 +347,10 @@ exports.snapshot_message = function (message) {
} }
if (message !== undefined) { if (message !== undefined) {
message_snapshot = _.extend({}, message); return _.extend({}, message);
} else {
// Save what we can.
message_snapshot = create_message_object();
}
};
function clear_message_snapshot() {
$("#restore-draft").hide();
message_snapshot = undefined;
}
exports.restore_message = function () {
if (!message_snapshot) {
return;
}
var snapshot_copy = _.extend({}, message_snapshot);
if ((snapshot_copy.type === "stream" &&
snapshot_copy.stream.length > 0 &&
snapshot_copy.subject.length > 0) ||
(snapshot_copy.type === "private" &&
snapshot_copy.reply_to.length > 0)) {
snapshot_copy = _.extend({replying_to_message: snapshot_copy},
snapshot_copy);
}
clear_message_snapshot();
compose_fade.clear_compose();
compose.start(snapshot_copy.type, snapshot_copy);
exports.autosize_textarea();
if (snapshot_copy.content !== undefined &&
util.is_all_or_everyone_mentioned(snapshot_copy.content)) {
show_all_everyone_warnings();
} }
// Save what we can.
return create_message_object();
}; };
function compose_error(error_text, bad_input) { function compose_error(error_text, bad_input) {
@ -525,9 +487,9 @@ function process_send_time(message_id, start_time, locally_echoed) {
function clear_compose_box() { function clear_compose_box() {
$("#new_message_content").val('').focus(); $("#new_message_content").val('').focus();
drafts.delete_draft_after_send();
exports.autosize_textarea(); exports.autosize_textarea();
$("#send-status").hide(0); $("#send-status").hide(0);
clear_message_snapshot();
$("#compose-send-button").removeAttr('disabled'); $("#compose-send-button").removeAttr('disabled');
$("#sending-indicator").hide(); $("#sending-indicator").hide();
resize.resize_bottom_whitespace(); resize.resize_bottom_whitespace();
@ -622,7 +584,7 @@ exports.respond_to_message = function (opts) {
var msg_type; var msg_type;
// Before initiating a reply to a message, if there's an // Before initiating a reply to a message, if there's an
// in-progress composition, snapshot it. // in-progress composition, snapshot it.
compose.snapshot_message(); drafts.update_draft();
message = current_msg_list.selected_message(); message = current_msg_list.selected_message();
@ -1043,7 +1005,6 @@ $(function () {
var message = $("#new_message_content").val(); var message = $("#new_message_content").val();
$("#new_message_content").hide(); $("#new_message_content").hide();
$("#markdown_preview").hide(); $("#markdown_preview").hide();
$("#restore-draft").hide();
$("#undo_markdown_preview").show(); $("#undo_markdown_preview").show();
$("#preview_message_area").show(); $("#preview_message_area").show();

230
static/js/drafts.js Normal file
View File

@ -0,0 +1,230 @@
var drafts = (function () {
var exports = {};
var draft_model = (function () {
var exports = {};
// the key that the drafts are stored under.
var KEY = "drafts";
var ls = localstorage();
ls.version = 1;
function getTimestamp() {
return new Date().getTime();
}
function get() {
return ls.get(KEY) || {};
}
exports.get = get;
exports.getDraft = function (id) {
return get()[id] || false;
};
function save(drafts) {
ls.set(KEY, drafts);
}
exports.addDraft = function (draft) {
var drafts = get();
// use the base16 of the current time + a random string to reduce
// collisions to essentially zero.
var id = getTimestamp().toString(16) + "-" + Math.random().toString(16).split(/\./).pop();
draft.updatedAt = getTimestamp();
drafts[id] = draft;
save(drafts);
return id;
};
exports.editDraft = function (id, draft) {
var drafts = get();
if (drafts[id]) {
draft.updatedAt = getTimestamp();
drafts[id] = draft;
save(drafts);
}
};
exports.deleteDraft = function (id) {
var drafts = get();
delete drafts[id];
save(drafts);
};
return exports;
}());
exports.draft_model = draft_model;
exports.update_draft = function () {
var draft = compose.snapshot_message();
var draft_id = $("#new_message_content").data("draft-id");
if (draft_id !== undefined) {
if (draft !== undefined) {
draft_model.editDraft(draft_id, draft);
} else {
draft_model.deleteDraft(draft_id);
}
} else {
if (draft !== undefined) {
var new_draft_id = draft_model.addDraft(draft);
$("#new_message_content").data("draft-id", new_draft_id);
}
}
};
exports.delete_draft_after_send = function () {
var draft_id = $("#new_message_content").data("draft-id");
if (draft_id) {
draft_model.deleteDraft(draft_id);
}
$("#new_message_content").removeData("draft-id");
};
exports.restore_draft = function (draft_id) {
var draft = draft_model.getDraft(draft_id);
if (!draft) {
return;
}
var draft_copy = _.extend({}, draft);
if ((draft_copy.type === "stream" &&
draft_copy.stream.length > 0 &&
draft_copy.subject.length > 0) ||
(draft_copy.type === "private" &&
draft_copy.reply_to.length > 0)) {
draft_copy = _.extend({replying_to_message: draft_copy},
draft_copy);
}
exports.close();
compose_fade.clear_compose();
if (draft.type === "stream" && draft.stream === "") {
draft_copy.subject = "";
}
compose.start(draft_copy.type, draft_copy);
compose.autosize_textarea();
$("#new_message_content").data("draft-id", draft_id);
};
exports.setup_page = function (callback) {
function setup_event_handlers() {
$(".draft_controls .restore-draft").on("click", function (e) {
e.stopPropagation();
var draft_row = $(this).closest(".draft-row");
var draft_id = draft_row.data("draft-id");
exports.restore_draft(draft_id);
});
$(".draft_controls .delete-draft").on("click", function () {
var draft_row = $(this).closest(".draft-row");
var draft_id = draft_row.data("draft-id");
exports.draft_model.deleteDraft(draft_id);
draft_row.remove();
if ($("#drafts_table .draft-row").length === 0) {
$('#drafts_table .no-drafts').show();
}
});
}
function format_drafts(data) {
var drafts = _.mapObject(data, function (draft, id) {
var formatted;
if (draft.type === "stream") {
// In case there is no stream for the draft, we need a
// single space char for proper rendering of the stream label
var space_string = new Handlebars.SafeString("&nbsp;");
var stream = (draft.stream.length > 0 ? draft.stream : space_string);
formatted = {
draft_id: id,
is_stream: true,
stream: stream,
stream_color: stream_data.get_color(draft.stream),
topic: draft.subject,
raw_content: draft.content,
};
echo.apply_markdown(formatted);
} else {
var emails = util.extract_pm_recipients(draft.private_message_recipient);
var recipients = _.map(emails, function (email) {
email = email.trim();
var person = people.get_by_email(email);
if (person !== undefined) {
return person.full_name;
}
return email;
}).join(', ');
formatted = {
draft_id: id,
is_stream: false,
recipients: recipients,
raw_content: draft.content,
};
echo.apply_markdown(formatted);
}
return formatted;
});
return drafts;
}
function _populate_and_fill() {
$('#drafts_table').empty();
var drafts = format_drafts(draft_model.get());
var rendered = templates.render('draft_table_body', { drafts: drafts });
$('#drafts_table').append(rendered);
if ($("#drafts_table .draft-row").length > 0) {
$('#drafts_table .no-drafts').hide();
}
if (callback) {
callback();
}
setup_event_handlers();
}
function populate_and_fill() {
i18n.ensure_i18n(function () {
_populate_and_fill();
});
}
populate_and_fill();
};
exports.launch = function () {
exports.setup_page(function () {
$("#draft_overlay").addClass("show");
});
};
exports.close = function () {
hashchange.exit_settings();
$("#draft_overlay").removeClass("show");
};
$(function () {
window.addEventListener("beforeunload", function () {
exports.update_draft();
});
$("#new_message_content").focusout(exports.update_draft);
});
return exports;
}());
if (typeof module !== 'undefined') {
module.exports = drafts;
}

View File

@ -184,6 +184,9 @@ function do_hashchange(from_reload) {
case "#subscriptions": case "#subscriptions":
ui.change_tab_to("#subscriptions"); ui.change_tab_to("#subscriptions");
break; break;
case "#drafts":
ui.change_tab_to("#drafts");
break;
case "#administration": case "#administration":
ui.change_tab_to("#administration"); ui.change_tab_to("#administration");
break; break;
@ -243,7 +246,7 @@ var get_hash_group = (function () {
function should_ignore(hash) { function should_ignore(hash) {
// an array of hashes to ignore (eg. ["subscriptions", "settings", "administration"]). // an array of hashes to ignore (eg. ["subscriptions", "settings", "administration"]).
var ignore_list = ["subscriptions", "settings", "administration"]; var ignore_list = ["subscriptions", "drafts", "settings", "administration"];
var main_hash = get_main_hash(hash); var main_hash = get_main_hash(hash);
return (ignore_list.indexOf(main_hash) > -1); return (ignore_list.indexOf(main_hash) > -1);
@ -275,6 +278,8 @@ function hashchanged(from_reload, e) {
if (base === "subscriptions") { if (base === "subscriptions") {
subs.launch(); subs.launch();
} else if (base === "drafts") {
drafts.launch();
} else if (/settings|administration/.test(base)) { } else if (/settings|administration/.test(base)) {
settings.setup_page(); settings.setup_page();
admin.setup_page(); admin.setup_page();

View File

@ -211,6 +211,9 @@ function process_hotkey(e) {
} else if ($("#subscription_overlay").hasClass("show")) { } else if ($("#subscription_overlay").hasClass("show")) {
subs.close(); subs.close();
return true; return true;
} else if ($("#draft_overlay").hasClass("show")) {
drafts.close();
return true;
} else if ($(".informational-overlays").hasClass("show")) { } else if ($(".informational-overlays").hasClass("show")) {
ui.hide_info_overlay(); ui.hide_info_overlay();
return true; return true;

View File

@ -11,6 +11,12 @@
width: 10px; width: 10px;
} }
#compose_controls .drafts-link {
position: absolute;
left: 10px;
bottom: 5px;
}
.new_message_button { .new_message_button {
padding-top: 1.1em; padding-top: 1.1em;
} }
@ -355,14 +361,7 @@ input.recipient_box {
display: none; display: none;
} }
#sending-indicator { .compose_table .drafts-link {
float: left;
font-weight: bold;
display: none;
}
#restore-draft {
display: none;
position: relative; position: relative;
margin-right: 1em; margin-right: 1em;
margin-left: 5px; margin-left: 5px;
@ -370,11 +369,13 @@ input.recipient_box {
} }
#sending-indicator { #sending-indicator {
padding-top: 2px; float: left;
font-weight: bold;
display: none;
} }
#restore-draft:hover { #sending-indicator {
cursor: pointer; padding-top: 2px;
} }
#compose a.message-control-button { #compose a.message-control-button {

166
static/styles/drafts.css Normal file
View File

@ -0,0 +1,166 @@
.drafts {
margin-top: 55px;
padding-left: 15px;
-webkit-font-smoothing: antialiased;
}
.drafts-container {
position: relative;
height: 95%;
background-color: #fff;
border-radius: 4px;
padding: 0px;
width: 60%;
overflow: hidden;
max-width: 1200px;
max-height: 1000px;
}
.drafts-header {
padding: 20px;
margin-bottom: 10px;
text-align: center;
text-transform: uppercase;
font-weight: 700;
font-size: 1.25em;
border-bottom: 1px solid #ddd;
}
.drafts-container .exit {
font-weight: 400;
position: absolute;
top: 10px;
right: 10px;
color: #AAA;
cursor: pointer;
}
.drafts-container .exit-sign {
position: relative;
top: 3px;
margin-left: 3px;
font-size: 1.7em;
font-weight: 300;
cursor: pointer;
}
.drafts-list {
overflow: auto;
height: calc(100% - 90px);
width: 100%;
}
.drafts-list .no-drafts {
display: block;
margin-top: calc(45vh - 30px - 1.5em);
text-align: center;
font-size: 1.5em;
color: #aaa;
pointer-events: none;
-webkit-font-smoothing: antialiased;
}
.draft-row {
padding: 5px 10px;
}
.draft-row.active {
background-color: #eee;
}
.draft-row > div {
display: inline-block;
vertical-align: top;
}
.draft-row .draft-info-box {
width: 100%;
background: #f1f1f1;
border-bottom: 1px solid #e2e2e2;
border-top: 1px solid #e2e2e2;
margin-bottom: 10px;
}
.draft-info-box .messagebox {
cursor: auto;
}
.draft-info-box .message_content {
line-height: 1;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 0px;
}
.draft-row .draft-info-box .draft_controls {
display: inline-block;
position: absolute;
top: 5px;
right: -55px;
}
.draft_controls .restore-draft {
cursor: pointer;
margin-right: 5px;
color: #037C2C;
opacity: 0.7;
}
.draft_controls .restore-draft:hover {
opacity: 1;
}
.draft_controls .delete-draft {
cursor: pointer;
margin-left: 5px;
color: #D8000C;
opacity: 0.7;
}
.draft_controls .delete-draft:hover {
opacity: 1;
}
#draft_overlay {
pointer-events: none;
opacity: 0;
position: fixed;
top: 0px;
left: 0px;
width: 100vw;
height: 100vh;
background-color: rgba(0,0,0,0.8);
overflow: auto;
z-index: 102;
transition: all 0.3s ease;
}
#draft_overlay.show {
display: block;
pointer-events: all;
opacity: 1;
}
#draft_overlay .drafts-container {
-webkit-transform: scale(0.5);
transform: scale(0.5);
transition: all 0.3s ease;
}
#draft_overlay.show .drafts-container {
-webkit-transform: scale(1);
transform: scale(1);
}
@media (max-width: 1130px) {
.drafts-container {
max-width: 60%;
}
}
@media (max-width: 700px) {
#draft_overlay .drafts-container {
height: 95%;
}
}

View File

@ -0,0 +1,42 @@
<div class="draft-row" data-draft-id="{{draft_id}}">
<div class="draft-info-box">
{{#if is_stream}}
<div class="message_header message_header_stream">
<div class="message-header-contents">
<div class="message_label_clickable stream_label"
style="background: {{stream_color}}; border-left-color: {{stream_color}};">
{{stream}}
</div>
<span class="stream_topic">
<div class="message_label_clickable narrows_by_subject">
{{topic}}
</div>
</span>
</div>
</div>
{{else}}
<div class="message_header message_header_private_message dark_background">
<div class="message-header-contents">
<div class="message_label_clickable stream_label">
{{#tr this}}You and __recipients__{{/tr}}
</div>
</div>
</div>
{{/if}}
<div class="message_row{{^is_stream}} private-message{{/is_stream}}">
<div class="messagebox"
style="box-shadow: inset 2px 0px 0px 0px {{#if is_stream}}{{stream_color}}{{else}}#444444{{/if}}, -1px 0px 0px 0px {{#if is_stream}}{{stream_color}}{{else}}#444444{{/if}};">
<div class="messagebox-content">
<div class="message_top_line">
<div class="draft_controls">
<i class="icon-vector-large icon-vector-pencil restore-draft" data-toggle="tooltip" title="{{t 'Restore Draft' }}"></i>
<i class="icon-vector-large icon-vector-trash delete-draft" data-toggle="tooltip" title="{{t 'Delete Draft' }}"></i>
</div>
</div>
<div class="message_content">{{{content}}}</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,21 @@
<div id="draft_overlay" class="new-style" data-overlay="drafts">
<div class="flex">
<div class="drafts-container">
<div class="drafts-header">
{{t 'Drafts' }}
<div class="exit">
<span class="exit-sign">&times;</span>
</div>
</div>
<div class="drafts-list">
<div class="no-drafts">
{{t 'No Drafts.'}}
</div>
{{#each drafts}}
{{partial "draft"}}
{{/each}}
</div>
</div>
</div>
</div>

View File

@ -3,6 +3,7 @@
</div> </div>
<div id="compose-container"> <div id="compose-container">
<div id="compose_controls" class="compose-content"> <div id="compose_controls" class="compose-content">
<a class="drafts-link" href="#drafts">{{ _('Drafts') }}</a>
<div id="compose_buttons"> <div id="compose_buttons">
<span class="new_message_button"> <span class="new_message_button">
<button type="button" class="btn btn-default btn-large compose_stream_button" <button type="button" class="btn btn-default btn-large compose_stream_button"
@ -87,7 +88,7 @@
<a class="message-control-button icon-vector-font" title="{{ _('Formatting') }}" data-overlay-trigger="markdown-help"></a> <a class="message-control-button icon-vector-font" title="{{ _('Formatting') }}" data-overlay-trigger="markdown-help"></a>
<a id="undo_markdown_preview" class="icon-vector-edit" style="display:none;" title="{{ _('Write') }}"></a> <a id="undo_markdown_preview" class="icon-vector-edit" style="display:none;" title="{{ _('Write') }}"></a>
<a id="markdown_preview" class="icon-vector-eye-open" title="{{ _('Preview') }}"></a> <a id="markdown_preview" class="icon-vector-eye-open" title="{{ _('Preview') }}"></a>
<a id="restore-draft" onclick="compose.restore_message();">{{ _('Restore draft') }}</a> <a class="drafts-link" href="#drafts">{{ _('Drafts') }}</a>
<span id="sending-indicator">{{ _('Sending...') }}</span> <span id="sending-indicator">{{ _('Sending...') }}</span>
<div id="send_controls"> <div id="send_controls">
<label id="enter-sends-label" class="compose_checkbox_label" for="enter_sends">{{ _('Press Enter to send') }}&nbsp;</label> <label id="enter-sends-label" class="compose_checkbox_label" for="enter_sends">{{ _('Press Enter to send') }}&nbsp;</label>

View File

@ -0,0 +1,5 @@
{# List of drafts for an user. #}
<div class="drafts">
<div id="drafts_table">
</div>
</div>

View File

@ -122,6 +122,7 @@ var page_params = {{ page_params }};
</div><!--/right sidebar--> </div><!--/right sidebar-->
{% include "zerver/image-overlay.html" %} {% include "zerver/image-overlay.html" %}
{% include "zerver/subscriptions.html" %} {% include "zerver/subscriptions.html" %}
{% include "zerver/drafts.html" %}
</div><!--/row--> </div><!--/row-->
<div class="informational-overlays new-style"> <div class="informational-overlays new-style">
<div class="overlay-content"> <div class="overlay-content">

View File

@ -63,6 +63,7 @@ class TemplateTestCase(ZulipTestCase):
logged_in = [ logged_in = [
'analytics/stats.html', 'analytics/stats.html',
'zerver/drafts.html',
'zerver/home.html', 'zerver/home.html',
'zerver/invite_user.html', 'zerver/invite_user.html',
'zerver/keyboard_shortcuts.html', 'zerver/keyboard_shortcuts.html',

View File

@ -684,6 +684,7 @@ PIPELINE = {
'styles/zulip.css', 'styles/zulip.css',
'styles/settings.css', 'styles/settings.css',
'styles/subscriptions.css', 'styles/subscriptions.css',
'styles/drafts.css',
'styles/informational-overlays.css', 'styles/informational-overlays.css',
'styles/compose.css', 'styles/compose.css',
'styles/reactions.css', 'styles/reactions.css',
@ -706,6 +707,7 @@ PIPELINE = {
'styles/zulip.css', 'styles/zulip.css',
'styles/settings.css', 'styles/settings.css',
'styles/subscriptions.css', 'styles/subscriptions.css',
'styles/drafts.css',
'styles/informational-overlays.css', 'styles/informational-overlays.css',
'styles/compose.css', 'styles/compose.css',
'styles/reactions.css', 'styles/reactions.css',
@ -793,6 +795,7 @@ JS_SPECS = {
'js/dict.js', 'js/dict.js',
'js/components.js', 'js/components.js',
'js/localstorage.js', 'js/localstorage.js',
'js/drafts.js',
'js/channel.js', 'js/channel.js',
'js/setup.js', 'js/setup.js',
'js/unread_ui.js', 'js/unread_ui.js',