mirror of https://github.com/zulip/zulip.git
455 lines
15 KiB
JavaScript
455 lines
15 KiB
JavaScript
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.snapshot_message = function () {
|
|
if (!compose_state.composing() || (compose_state.message_content() === "")) {
|
|
// If you aren't in the middle of composing the body of a
|
|
// message, don't try to snapshot.
|
|
return;
|
|
}
|
|
|
|
// Save what we can.
|
|
var message = {
|
|
type: compose_state.get_message_type(),
|
|
content: compose_state.message_content(),
|
|
};
|
|
if (message.type === "private") {
|
|
var recipient = compose_state.recipient();
|
|
message.reply_to = recipient;
|
|
message.private_message_recipient = recipient;
|
|
} else {
|
|
message.stream = compose_state.stream_name();
|
|
message.subject = compose_state.subject();
|
|
}
|
|
return message;
|
|
};
|
|
|
|
function draft_notify() {
|
|
$(".alert-draft").css("display", "inline-block");
|
|
$(".alert-draft").delay(1000).fadeOut(300);
|
|
}
|
|
|
|
exports.update_draft = function () {
|
|
var draft = drafts.snapshot_message();
|
|
var draft_id = $("#compose-textarea").data("draft-id");
|
|
|
|
if (draft_id !== undefined) {
|
|
if (draft !== undefined) {
|
|
draft_model.editDraft(draft_id, draft);
|
|
draft_notify();
|
|
} else {
|
|
draft_model.deleteDraft(draft_id);
|
|
}
|
|
} else {
|
|
if (draft !== undefined) {
|
|
var new_draft_id = draft_model.addDraft(draft);
|
|
$("#compose-textarea").data("draft-id", new_draft_id);
|
|
draft_notify();
|
|
}
|
|
}
|
|
};
|
|
|
|
exports.delete_draft_after_send = function () {
|
|
var draft_id = $("#compose-textarea").data("draft-id");
|
|
if (draft_id) {
|
|
draft_model.deleteDraft(draft_id);
|
|
}
|
|
$("#compose-textarea").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);
|
|
}
|
|
|
|
if (draft.type === "stream") {
|
|
if (draft.stream !== "") {
|
|
narrow.activate(
|
|
[
|
|
{operator: "stream", operand: draft.stream},
|
|
{operator: "topic", operand: draft.subject},
|
|
],
|
|
{trigger: "restore draft"});
|
|
}
|
|
} else {
|
|
if (draft.private_message_recipient !== "") {
|
|
narrow.activate([{operator: "pm-with", operand: draft.private_message_recipient}],
|
|
{trigger: "restore draft"});
|
|
}
|
|
}
|
|
|
|
overlays.close_overlay("drafts");
|
|
compose_fade.clear_compose();
|
|
compose.clear_preview_area();
|
|
|
|
if (draft.type === "stream" && draft.stream === "") {
|
|
draft_copy.subject = "";
|
|
}
|
|
compose_actions.start(draft_copy.type, draft_copy);
|
|
compose_ui.autosize_textarea();
|
|
$("#compose-textarea").data("draft-id", draft_id);
|
|
};
|
|
|
|
var DRAFT_LIFETIME = 30;
|
|
|
|
function remove_old_drafts() {
|
|
var old_date = new Date().setDate(new Date().getDate() - DRAFT_LIFETIME);
|
|
var drafts = draft_model.get();
|
|
_.each(drafts, function (draft, id) {
|
|
if (draft.updatedAt < old_date) {
|
|
draft_model.deleteDraft(id);
|
|
}
|
|
});
|
|
}
|
|
// Exporting for testing purpose
|
|
exports.remove_old_drafts = remove_old_drafts;
|
|
|
|
exports.setup_page = function (callback) {
|
|
function setup_event_handlers() {
|
|
$(".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 = {};
|
|
var data_array = [];
|
|
_.each(data, function (draft, id) {
|
|
data_array.push([id, data[id]]);
|
|
});
|
|
var data_sorted = data_array.sort(function (draft_a,draft_b) {
|
|
return draft_a[1].updatedAt - draft_b[1].updatedAt;
|
|
});
|
|
_.each(data_sorted, function (data_element) {
|
|
var draft = data_element[1];
|
|
var id = data_element[0];
|
|
var formatted;
|
|
var time = new XDate(draft.updatedAt);
|
|
var time_stamp = timerender.render_now(time).time_str;
|
|
if (time_stamp === i18n.t("Today")) {
|
|
time_stamp = timerender.stringify_time(time);
|
|
}
|
|
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(" ");
|
|
var stream = (draft.stream.length > 0 ? draft.stream : space_string);
|
|
var draft_topic = draft.subject.length === 0 ?
|
|
compose.empty_topic_placeholder() : draft.subject;
|
|
|
|
formatted = {
|
|
draft_id: id,
|
|
is_stream: true,
|
|
stream: stream,
|
|
stream_color: stream_data.get_color(draft.stream),
|
|
topic: draft_topic,
|
|
raw_content: draft.content,
|
|
time_stamp: time_stamp,
|
|
};
|
|
} 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,
|
|
time_stamp: time_stamp,
|
|
};
|
|
}
|
|
|
|
try {
|
|
markdown.apply_markdown(formatted);
|
|
} catch (error) {
|
|
// In the unlikely event that there is syntax in the
|
|
// draft content which our markdown processor is
|
|
// unable to process, we delete the draft, so that the
|
|
// drafts overlay can be opened without any errors.
|
|
// We also report the exception to the server so that
|
|
// the bug can be fixed.
|
|
draft_model.deleteDraft(id);
|
|
blueslip.error("Error in rendering draft.", {
|
|
draft_content: draft.content,
|
|
}, error.stack);
|
|
return;
|
|
}
|
|
|
|
drafts[id] = 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,
|
|
draft_lifetime: DRAFT_LIFETIME,
|
|
});
|
|
$('#drafts_table').append(rendered);
|
|
if ($("#drafts_table .draft-row").length > 0) {
|
|
$('#drafts_table .no-drafts').hide();
|
|
}
|
|
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
|
|
setup_event_handlers();
|
|
}
|
|
|
|
remove_old_drafts();
|
|
populate_and_fill();
|
|
};
|
|
|
|
function drafts_initialize_focus(event_name) {
|
|
// If a draft is not focused in draft modal, then focus the last draft
|
|
// if up_arrow is clicked or the first draft if down_arrow is clicked.
|
|
if (event_name !== "up_arrow" && event_name !== "down_arrow" || $(".draft-info-box:focus")[0]) {
|
|
return;
|
|
}
|
|
|
|
var draft_arrow = draft_model.get();
|
|
var draft_id_arrow = Object.getOwnPropertyNames(draft_arrow);
|
|
if (draft_id_arrow.length === 0) { // empty drafts modal
|
|
return;
|
|
}
|
|
|
|
var draft_element;
|
|
if (event_name === "up_arrow") {
|
|
draft_element = document.querySelectorAll('[data-draft-id="' + draft_id_arrow[draft_id_arrow.length - 1] + '"]');
|
|
} else if (event_name === "down_arrow") {
|
|
draft_element = document.querySelectorAll('[data-draft-id="' + draft_id_arrow[0] + '"]');
|
|
}
|
|
var focus_element = draft_element[0].children[0];
|
|
focus_element.focus();
|
|
}
|
|
|
|
function drafts_scroll(next_focus_draft_row) {
|
|
if (next_focus_draft_row[0] === undefined) {
|
|
return;
|
|
}
|
|
if (next_focus_draft_row[0].children[0] === undefined) {
|
|
return;
|
|
}
|
|
next_focus_draft_row[0].children[0].focus();
|
|
|
|
// If focused draft is first draft, scroll to the top.
|
|
if ($(".draft-info-box:first")[0].parentElement === next_focus_draft_row[0]) {
|
|
$(".drafts-list")[0].scrollTop = 0;
|
|
}
|
|
|
|
// If focused draft is the last draft, scroll to the bottom.
|
|
if ($(".draft-info-box:last")[0].parentElement === next_focus_draft_row[0]) {
|
|
$(".drafts-list")[0].scrollTop = $('.drafts-list')[0].scrollHeight - $('.drafts-list').height();
|
|
}
|
|
|
|
// If focused draft is cut off from the top, scroll up halfway in draft modal.
|
|
if (next_focus_draft_row.position().top < 55) {
|
|
// 55 is the minimum distance from the top that will require extra scrolling.
|
|
$(".drafts-list")[0].scrollTop -= $(".drafts-list")[0].clientHeight / 2;
|
|
}
|
|
|
|
// If focused draft is cut off from the bottom, scroll down halfway in draft modal.
|
|
var dist_from_top = next_focus_draft_row.position().top;
|
|
var total_dist = dist_from_top + next_focus_draft_row[0].clientHeight;
|
|
var dist_from_bottom = $(".drafts-container")[0].clientHeight - total_dist;
|
|
if (dist_from_bottom < -4) {
|
|
//-4 is the min dist from the bottom that will require extra scrolling.
|
|
$(".drafts-list")[0].scrollTop += $(".drafts-list")[0].clientHeight / 2;
|
|
}
|
|
}
|
|
|
|
exports.drafts_handle_events = function (e, event_key) {
|
|
var draft_arrow = draft_model.get();
|
|
var draft_id_arrow = Object.getOwnPropertyNames(draft_arrow);
|
|
drafts_initialize_focus(event_key);
|
|
|
|
// This detects up arrow key presses when the draft overlay
|
|
// is open and scrolls through the drafts.
|
|
if (event_key === "up_arrow") {
|
|
var focus_draft_up_row = $(".draft-info-box:focus")[0].parentElement;
|
|
var prev_focus_draft_row = $(focus_draft_up_row).prev();
|
|
drafts_scroll(prev_focus_draft_row);
|
|
}
|
|
|
|
// This detects down arrow key presses when the draft overlay
|
|
// is open and scrolls through the drafts.
|
|
if (event_key === "down_arrow") {
|
|
var focus_draft_down_row = $(".draft-info-box:focus")[0].parentElement;
|
|
var next_focus_draft_row = $(focus_draft_down_row).next();
|
|
drafts_scroll(next_focus_draft_row);
|
|
}
|
|
|
|
var elt = document.activeElement;
|
|
var focused_draft = $(elt.parentElement)[0].getAttribute("data-draft-id");
|
|
// Allows user to delete drafts with backspace
|
|
if (event_key === "backspace" || event_key === "delete") {
|
|
if (elt.parentElement.hasAttribute("data-draft-id")) {
|
|
var focus_draft_back_row = $(elt)[0].parentElement;
|
|
var backnext_focus_draft_row = $(focus_draft_back_row).next();
|
|
var backprev_focus_draft_row = $(focus_draft_back_row).prev();
|
|
var delete_id;
|
|
if (backnext_focus_draft_row[0] !== undefined) {
|
|
delete_id = backnext_focus_draft_row[0].getAttribute("data-draft-id");
|
|
} else if (backprev_focus_draft_row[0] !== undefined) {
|
|
delete_id = backprev_focus_draft_row[0].getAttribute("data-draft-id");
|
|
}
|
|
drafts.draft_model.deleteDraft(focused_draft);
|
|
document.activeElement.parentElement.remove();
|
|
var new_focus_element = document.querySelectorAll('[data-draft-id="' + delete_id + '"]');
|
|
if (new_focus_element[0] !== undefined) {
|
|
new_focus_element[0].children[0].focus();
|
|
}
|
|
if ($("#drafts_table .draft-row").length === 0) {
|
|
$('#drafts_table .no-drafts').show();
|
|
}
|
|
}
|
|
}
|
|
|
|
// This handles when pressing enter while looking at drafts.
|
|
// It restores draft that is focused.
|
|
if (event_key === "enter") {
|
|
if (document.activeElement.parentElement.hasAttribute("data-draft-id")) {
|
|
exports.restore_draft(focused_draft);
|
|
} else {
|
|
var first_draft = draft_id_arrow[draft_id_arrow.length - 1];
|
|
exports.restore_draft(first_draft);
|
|
}
|
|
}
|
|
};
|
|
|
|
exports.launch = function () {
|
|
exports.setup_page(function () {
|
|
overlays.open_overlay({
|
|
name: 'drafts',
|
|
overlay: $('#draft_overlay'),
|
|
on_close: function () {
|
|
hashchange.exit_overlay();
|
|
},
|
|
});
|
|
|
|
var draft_list = drafts.draft_model.get();
|
|
var draft_id_list = Object.getOwnPropertyNames(draft_list);
|
|
if (draft_id_list.length > 0) {
|
|
var last_draft = draft_id_list[draft_id_list.length - 1];
|
|
var last_draft_element = document.querySelectorAll('[data-draft-id="' + last_draft + '"]');
|
|
var focus_element = last_draft_element[0].children[0];
|
|
focus_element.focus();
|
|
$(".drafts-list")[0].scrollTop = $('.drafts-list')[0].scrollHeight - $('.drafts-list').height();
|
|
}
|
|
});
|
|
};
|
|
|
|
exports.initialize = function () {
|
|
window.addEventListener("beforeunload", function () {
|
|
exports.update_draft();
|
|
});
|
|
|
|
$("#compose-textarea").focusout(exports.update_draft);
|
|
};
|
|
|
|
return exports;
|
|
|
|
}());
|
|
if (typeof module !== 'undefined') {
|
|
module.exports = drafts;
|
|
}
|