Layout and design improvements :

* New way to list messages
* Infinite scrolling for the webmail

see #585
This commit is contained in:
Antoine Nguyen
2014-09-14 14:54:33 +02:00
parent bd92bd6213
commit d8de4363a8
19 changed files with 490 additions and 263 deletions
+1 -1
View File
@@ -89,7 +89,7 @@ def parse_date(value, **kwargs):
ndate = datetime.datetime(*(tmp)[:7])
now = datetime.datetime.now()
if now - ndate > datetime.timedelta(7):
return ndate.strftime("%d.%m.%Y %H:%M")
return ndate.strftime("%d/%m/%Y %H:%M")
return ndate.strftime("%a %H:%M")
except ValueError:
return value
+3 -6
View File
@@ -649,15 +649,12 @@ class IMAPconnector(object):
if not r'\Seen' in data[int(uid)]['FLAGS']:
msg['style'] = 'unseen'
if r'\Answered' in data[int(uid)]['FLAGS']:
msg['img_flags'] = [static_url('pics/answered.png')]
msg['answered'] = True
if r'$Forwarded' in data[int(uid)]['FLAGS']:
if 'img_flags' in msg:
msg['img_flags'] += [static_url('pics/forwarded.png')]
else:
msg['img_flags'] = [static_url('pics/forwarded.png')]
msg['forwarded'] = True
bs = BodyStructure(data[int(uid)]['BODYSTRUCTURE'])
if bs.has_attachments():
msg['img_withatts'] = static_url('pics/attachment.png')
msg['attachments'] = True
result += [msg]
return result
+6 -6
View File
@@ -14,25 +14,25 @@ class WMtable(tables.Table):
styles = "table-hover"
idkey = "imapid"
drag = tables.ImgColumn(
"drag", cssclass="draggable left", width="1%",
"drag", cssclass="draggable left",
defvalue="%spics/grippy.png" % settings.STATIC_URL,
header="<input type='checkbox' name='toggleselect' id='toggleselect' />"
)
selection = tables.SelectionColumn(
"selection", safe=True, width='1%', header=None, sortable=False
"selection", safe=True, header=None, sortable=False
)
flags = tables.ImgColumn("flags", width="3%")
withatts = tables.ImgColumn("withatts", width="2%")
flags = tables.ImgColumn("flags")
withatts = tables.ImgColumn("withatts")
subject = tables.Column(
"subject", label=ugettext_lazy("Subject"), limit=60,
cssclass="openable"
)
from_ = tables.Column(
"from", width="20%", label=ugettext_lazy("From"), limit=30,
"from", label=ugettext_lazy("From"), limit=30,
cssclass="openable"
)
date = tables.Column(
"date", width="15%", label=ugettext_lazy("Date"), cssclass="openable"
"date", label=ugettext_lazy("Date"), cssclass="openable"
)
cols_order = [
@@ -1,10 +1,16 @@
/* MEDIA */
#listing {
position: absolute;
overflow: auto;
top: 60px;
padding: 0;
top: 75px;
bottom: 0;
margin: 0;
overflow: auto;
padding-left: 15px;
padding-right: 15px;
}
td[name=drag], td[name=selection] {
width: 1%;
}
@media (max-width: 400px) {
@@ -21,7 +27,7 @@
@media (max-width: 500px) {
th#date,
td[name="date"],
#menubar .Actions{
#menubar .actions {
display: none;
}
}
@@ -38,7 +44,7 @@
@media (min-width: 768px) {
td[name=from] {
width: 20%;
width: 25%;
}
}
@@ -48,7 +54,7 @@
}
th#date,
td[name="date"]{
max-width: 60px;
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -71,9 +77,6 @@
@media (min-width: 1198px) {
#listing {
padding-left: 15px;
}
}
/* GENERAL */
@@ -82,14 +85,18 @@ body, html {
}
#rightcol {
position: relative;
position: fixed;
top: 51px;
bottom: 0;
}
#folders {
/*position: fixed;
top: 73px;*/
position: absolute;
top: 73px;
left: 15px;
right: 15px;
overflow-x: hidden;
overflow-y: auto;
margin-bottom: 5px;
}
#folders .ui-resizable-handle {
@@ -149,9 +156,19 @@ td[name="date"] {
text-align: right;
}
#left-toolbar {
position: absolute;
width: 100%;
bottom: 0;
}
#quotabox {
padding-top: 15px;
}
#quotabar {
margin: 15px 0 0;
width: 190px;
height: 5px;
margin-bottom: 0;
}
#quotaraw {
@@ -160,7 +177,7 @@ td[name="date"] {
/*
*TOP NAVBAR
* TOP NAVBAR
*/
.navbar-header{
@@ -170,9 +187,26 @@ td[name="date"] {
/*
* Mailboxes list
*/
.email {
border-top: 1px solid #ddd;
padding-top: 10px;
}
div.email:hover {
background-color: #f5f5f5;
}
.openable {
cursor: pointer;
}
.leftcol-heading {
margin-bottom: 15px;
}
#mboxactions {
position: fixed;
padding-left: 30px;
margin: 5px 0;
}
div.clickbox {
@@ -216,9 +250,9 @@ li.droppable li{
padding: 2px 0;
}
#mailheader label {
text-align: left;
}
/* #mailheader label { */
/* text-align: left; */
/* } */
#mailheader .options {
margin-bottom: 8px;
@@ -240,7 +274,7 @@ li.droppable li{
float: left;
}
#list {
list {
position: absolute;
top: 5px;
left: 180px;
@@ -308,16 +342,13 @@ li.droppable li{
* Table
*/
td[class*=openable] {
cursor: pointer;
}
/*
*Action buttons
*/
#menubar {
/*background-color: #fff;*/
position: absolute;
top: 0;
padding-top: 20px;
@@ -20,8 +20,6 @@ Webmail.prototype = {
mboxes_col_width: 200
},
initialize: function(options) {
this.options = $.extend({}, this.defaults, options);
this.rtimer = null;
@@ -38,88 +36,116 @@ Webmail.prototype = {
});
this.record_unseen_messages();
$("#mboxactions").on("shown.bs.dropdown", function() {
var $menu = $("ul", this);
var offset = $menu.offset();
$("#mboxactions").css({
bottom: $("#bottom-bar").outerHeight(true) + "px"
$("body").append($menu);
$menu.show();
$menu.css("position", "absolute");
$menu.css("top", offset.top + "px");
$menu.css("left", offset.left + "px");
$(this).data("original_menu", $menu);
}).on("hide.bs.dropdown", function() {
var $this = $(this);
$this.append($this.data("original_menu"));
$this.data("original_menu").removeAttr("style");
});
/*$("#mboxactions").css({
bottom: $("#bottom-bar").outerHeight(true) + "px"
});*/
/* Responsive -> on resizing folders (leftcol) */
$("#folders").resizable({
start: function(event, ui) {
$('#listing iframe').css('pointer-events','none');
},
handles: "e",
minWidth: parseInt($("#folders").css("width").replace("px", "")),
maxWidth: 400,
stop: function(event, ui) {
$('#listing iframe').css('pointer-events','auto');
}
});
// $("#folders").resizable({
// start: function(event, ui) {
// $('#listing iframe').css('pointer-events','none');
// },
// handles: "e",
// minWidth: $("#folders").width(),
// maxWidth: 400,
// stop: function(event, ui) {
// $('#listing iframe').css('pointer-events','auto');
// }
// });
$("#folders").resizable({
resize: function(event, ui) {
if ($(window).width() > 1198) {
var width = ui.size.width;
/*$("#rightcol").css({
marginLeft: width + 'px'
});*/
$("#folders").css({
width : width + 'px'
});
/*$("#rightcol").css({
paddingRight: width - 110 + 'px'
});
if ($(window).width() < 1800) {
$("#rightcol").css({
paddingRight: width - 110 + 'px'
});
}*/
}
}
});
// $("#folders").resizable({
// resize: function(event, ui) {
// if ($(window).width() > 1198) {
// var width = ui.size.width;
// /*$("#rightcol").css({
// marginLeft: width + 'px'
// });*/
// $("#folders").css({
// width : width + 'px'
// });
// /*$("#rightcol").css({
// paddingRight: width - 110 + 'px'
// });
// if ($(window).width() < 1800) {
// $("#rightcol").css({
// paddingRight: width - 110 + 'px'
// });
// }*/
// }
// }
// });
/* end Responsive */
this.resize();
this.setup_infinite_scroll();
this.init_droppables();
this.register_navcallbacks();
this.listen();
},
register_navcallbacks: function() {
this.navobject.register_callback("listmailbox", $.proxy(this.listmailbox_callback, this));
this.navobject.register_callback("compose", $.proxy(this.compose_callback, this));
this.navobject.register_callback("viewmail", $.proxy(this.viewmail_callback, this));
this.navobject.register_callback(
"listmailbox", $.proxy(this.listmailbox_callback, this));
this.navobject.register_callback(
"compose", $.proxy(this.compose_callback, this));
this.navobject.register_callback(
"viewmail", $.proxy(this.viewmail_callback, this));
this.navobject.register_callback("reply", $.proxy(this.compose_callback, this));
this.navobject.register_callback("replyall", $.proxy(this.compose_callback, this));
this.navobject.register_callback("forward", $.proxy(this.compose_callback, this));
this.navobject.register_callback(
"reply", $.proxy(this.compose_callback, this));
this.navobject.register_callback(
"replyall", $.proxy(this.compose_callback, this));
this.navobject.register_callback(
"forward", $.proxy(this.compose_callback, this));
},
listen: function() {
$(window).resize($.proxy(this.resize, this));
$(document).on("click", "a[name=compose]", $.proxy(this.compose_loader, this));
$(document).on("click", "a[name=totrash]", $.proxy(this.delete_messages, this));
$(document).on("click", "a[name*=mark-]", $.proxy(this.send_mark_request, this));
$(document).on(
"click", "a[name=compose]", $.proxy(this.compose_loader, this));
$(document).on(
"click", "a[name=totrash]", $.proxy(this.delete_messages, this));
$(document).on(
"click", "a[name*=mark-]", $.proxy(this.send_mark_request, this));
$(document).on("click", "a[name=compress]", $.proxy(this.compress, this));
$(document).on("click", "a[name=empty]", $.proxy(this.empty, this));
$(document).on("click", "#bottom-bar a", $.proxy(this.getpage_loader, this));
$(document).on("click", "a[name=loadfolder]", $.proxy(this.listmailbox_loader, this));
$(document).on(
"click", "a[name=loadfolder]", $.proxy(this.listmailbox_loader, this));
$(document).on("click", "a[name=selectfolder]", this.select_parent_mailbox);
$(document).on("click", "div[class*=clickbox]", $.proxy(this.mbox_state_callback, this));
$(document).on(
"click", "div[class*=clickbox]", $.proxy(this.mbox_state_callback, this));
$(document).on("click", "a[name=newmbox]", $.proxy(this.new_mailbox, this));
$(document).on("click", "a[name=editmbox]", $.proxy(this.edit_mbox, this));
$(document).on("click", "a[name=removembox]", $.proxy(this.remove_mbox, this));
$(document).on("click", "td[class*=openable]", $.proxy(this.viewmail_loader, this));
$(document).on("click", "div.openable", $.proxy(this.viewmail_loader, this));
$(document).on("click", "a[name=reply]", $.proxy(this.reply_loader, this));
$(document).on("click", "a[name=replyall]", $.proxy(this.reply_loader, this));
$(document).on("click", "a[name=forward]", $.proxy(this.reply_loader, this));
$(document).on("click", "a[name=delete]", $.proxy(this.delete_message, this));
$(document).on("click", "a[name=activate_links]", $.proxy(function(e) { this.display_mode(e, "1"); }, this));
$(document).on(
"click", "a[name=activate_links]", $.proxy(function(e) { this.display_mode(e, "1"); }, this));
$(document).on("click", "a[name=disable_links]", $.proxy(function(e) { this.display_mode(e, "0"); }, this));
$(document).on("click", "a[name=sendmail]", $.proxy(this.sendmail, this));
@@ -130,6 +156,28 @@ Webmail.prototype = {
}, this));
},
/**
* Setup an "infinite scroll" behaviour for the email list.
*/
setup_infinite_scroll: function() {
var that = this;
$("#listing").infinite_scroll({
url: this.options.listing_url,
calculate_bottom: function($element) {
return $("#emails").height() - $element.height();
},
get_args: function() {
return that.navparams;
},
process_results: function(data) {
var content = $("#emails").html() + data.listing;
$("#emails").html(content);
}
});
},
record_unseen_messages: function() {
var unseen_counters = {};
@@ -144,37 +192,23 @@ Webmail.prototype = {
* Global resize event callback.
*/
resize: function() {
/*$("#mboxes_container").height(
$("#folders").height() - $("#mboxactions").height());*/
var $window = $(window);
var current_action = this.navobject.getparam("action");
$("#folders").height(
$("#left-toolbar").offset().top - $("#folders").offset().top
);
if ($window.width() > 1198) {
$("#folders").css({
/*bottom: $("#bottom-bar").outerHeight(true) + 10,*/
width: this.options.mboxes_col_width + 'px'
});
// $("#folders").css({
// width: this.options.mboxes_col_width + 'px'
// });
}
if (current_action == "listmailbox") {
this.resize_listing("auto");
} else if (current_action == "viewmail") {
this.resize_listing("hidden");
} else if (current_action == "compose") {
if (current_action == "compose") {
this.resize_compose_body();
}
},
/**
* Resize the "listing" div.
*/
resize_listing: function(overflow) {
$("#listing").css("overflow", overflow);
$("#listing").height(
$("#bottom-bar").offset().top -
($("#menubar").offset().top + $("#menubar").outerHeight())
);
},
/**
* Resize the textarea used to write plain/text messages.
*/
@@ -183,13 +217,12 @@ Webmail.prototype = {
if ($container.length) {
var top = $container.offset().top;
var bottom = $("#bottom-bar").offset().top;
$("#body_container").innerHeight(bottom - top - 10);
$("#body_container").innerHeight($(window).height() - top - 10);
}
},
/*
/**
* Callback of the 'compose' action.
*
* It is also shared with other similar actions : reply, forward.
@@ -199,14 +232,14 @@ Webmail.prototype = {
$("#body_container").outerHeight(true));
},
/*
/**
* Simple helper to retrieve the currently selected mailbox.
*/
get_current_mailbox: function() {
return this.navobject.getparam("mbox", "INBOX");
},
/*
/**
* Keep track of interesting navigation parameters in order to
* restore them later.
*/
@@ -276,10 +309,10 @@ Webmail.prototype = {
$("#menubar").html(response.menu);
$("#searchfield").searchbar({navobj: this.navobject});
}
if (response.navbar) {
$("#bottom-bar-right").html(response.navbar);
/*if (response.navbar) {
$("#pagination").html(response.navbar);
$("#pagination-responsive").html(response.navbar);
}
}*/
$("#listing").html(response.listing);
},
@@ -832,22 +865,22 @@ Webmail.prototype = {
* Loader of the 'viewmail' action
*/
viewmail_loader: function(e) {
var $tr = $(e.target).parent();
var $div = $(e.target).parents(".email");
var curmb = this.get_current_mailbox();
e.preventDefault();
if ($tr.hasClass("disabled")) {
if ($div.hasClass("disabled")) {
return;
}
$tr.addClass("disabled");
if ($tr.hasClass("unseen")) {
$div.addClass("disabled");
if ($div.hasClass("unseen")) {
var mb = this.get_current_mailbox();
this.change_unseen_messages(mb, -1);
}
this.navobject.reset().setparams({
action: "viewmail",
mbox: curmb,
mailid: $tr.attr("id")
mailid: $div.attr("id")
}).update();
},
@@ -861,14 +894,14 @@ Webmail.prototype = {
listmailbox_callback: function(resp) {
this.store_nav_params();
this.page_update(resp);
$("#emails").htmltable();
$("#emails").htmltable({
row_selector: "div.email"
});
this.htmltable = $("#emails").data("htmltable");
this.init_draggables();
this.resize_listing("auto");
$("#listing").css("overflow", "auto");
},
add_field: function(e, name) {
e.preventDefault();
$("label[for=id_" + name + "]").parent().show();
@@ -912,7 +945,7 @@ Webmail.prototype = {
*/
viewmail_callback: function(resp) {
this.page_update(resp);
this.resize_listing("hidden");
$("#listing").css("overflow", "hidden");
$("a[name=back]").click($.proxy(function(e) {
e.preventDefault();
this.go_back_to_listing();
@@ -1,17 +1,19 @@
{% load i18n form_tags %}{% load url from future %}
<form class="form-horizontal" method="POST" action="{{ posturl }}" id="composemail">{% csrf_token %}
<div id="compose-container" class="col-lg-12">
<div id="mailheader" class="well">
{% render_field form.to %}
<div id="mailheader">
<div class="form-group">
{% render_field form.to %}
</div>
<div class="options col-lg-offset-4 col-lg-7 col-md-offset-4 col-md-7 col-sm-offset-4 col-sm-7">
{% if not form.cc.value %}<a id="add_cc" href="#" class="glyphicon glyphicon-plus">{% trans "Cc" %}</a>{% endif %} <a id="add_cci" href="#" class="glyphicon glyphicon-plus">{% trans "Cci" %}</a>
</div>
{% if form.cc.value %}{% render_field form.cc %}{% else %}{% render_and_hide_field form.cc %}{% endif %}
{% render_and_hide_field form.cci %}
{% render_field form.subject %}
<div id="attachments" name="{% url 'modoboa.extensions.webmail.views.attachments' %}">
<div>
<img src="{{ STATIC_URL }}pics/attachment.png" />
<div id="attachments" class="row" name="{% url 'modoboa.extensions.webmail.views.attachments' %}">
<div class="col-lg-3">
<span class="glyphicon glyphicon-paperclip"></span>
{% trans "Attachments" %}
</div>
<div id="list">
@@ -0,0 +1,22 @@
{% load i18n %}
{% load webmail_tags %}
<div id="emails">
{% for email in emails %}
<div id="{{ email.imapid }}" class="row email{% if email.style %} {{ email.style }}{% endif %}">
<div class="col-xs-1 col-lg-1 text-center">
<input type="checkbox" />
</div>
<div class="col-xs-9 col-lg-9 openable">
{{ email.from|parse_imap_header:"from" }}
<p class="text-muted">
{{ email.subject|parse_imap_header:"subject" }}
</p>
</div>
<div class="col-xs-2 col-lg-2 text-right">
{{ email.date|parse_imap_header:"date" }}
<p>{% if email.answered %}<span class="glyphicon glyphicon-share-alt"></span>{% endif %}{% if email.attachments %}<span class="glyphicon glyphicon-paperclip"></span>{% endif %}</p>
</div>
</div>
{% endfor %}
</div>
@@ -1,7 +1,4 @@
{% load i18n webmail_tags %}
<!--<div id="mboxes_container">-->
<ul id="folders" class="nav nav-sidebar">
<ul class="nav nav-sidebar">
{% print_mailboxes mboxes selected withunseen %}
</ul>
<!--</div>-->
<div id="mboxactions">{% mboxes_menu %}</div>
@@ -1,4 +1,4 @@
{% extends "fixed.html" %}{% load i18n lib_tags %}{% load url from future %}
{% extends "fixed.html" %}{% load i18n lib_tags webmail_tags %}{% load url from future %}
{% block pagetitle %}Webmail{% endblock %}
{% block html_head %}
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/jquery-ui.css" />
@@ -8,6 +8,7 @@
<script type="text/javascript" src="{{ STATIC_URL }}js/history.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/jquery.htmltable.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/searchbar.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/jquery.infinitescroll.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}webmail/js/webmail.js"></script>
<script type="text/javascript">
var webmail = undefined;
@@ -16,6 +17,7 @@ $(document).ready(function() {
webmail = new Webmail({
poller_interval: {{ refreshrate }},
poller_url: "{% url 'modoboa.extensions.webmail.views.check_unseen_messages' %}",
listing_url: "{% url 'modoboa.extensions.webmail.views.index' %}?action=listmailbox",
move_url: "{% url 'modoboa.extensions.webmail.views.move' %}",
submboxes_url: "{% url 'modoboa.extensions.webmail.views.submailboxes' %}",
delattachment_url: "{% url 'modoboa.extensions.webmail.views.delattachment' %}",
@@ -33,9 +35,40 @@ $(document).ready(function() {
{% block fixed_content %}
<div class="row">
<div id="leftcol" class="col-lg-2 col-md-3 col-sm-3 col-xs-0 sidebar">
<!--<div class="row">-->
{{ mboxes }}
<!--</div>-->
<div class="leftcol-heading text-center">
<a name="compose" class="btn btn-success btn-lg" href="compose">
<span class="glyphicon glyphicon-edit"></span>
{% trans "Compose" %}
</a>
</div>
<div id="folders" class="row">
<div class="col-sm-12 col-md-12 col-lg-12">
{{ mboxes }}
</div>
</div>
<div id="left-toolbar" class="row">
<div class="col-sm-2 col-md-2 col-lg-2">
<div id="mboxactions" class="btn-group dropup">
<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
<span class="glyphicon glyphicon-cog"></span>
</button>
{% mboxes_menu %}
</div>
</div>
{% if quota != -1 %}
<div id="quotabox" class="col-sm-10 col-md-10 col-lg-10">
<!--<label class="pull-left navbar-text">{% trans "Mailbox quota" %}</label>-->
<div id="quotabar" class="progress">
<div class="progress-bar {% progress_color quota %}" role="progressbar" style="width: {{ quota }}%" aria-valuenow="{{ quota }}" aria-valuemin="0" aria-valuemax="100">{{ quota }}%</div>
</div>
</div>
{% endif %}
</div>
</div>
<div id="rightcol" class="col-lg-offset-2 col-lg-10 col-md-offset-3 col-md-9 col-sm-offset-3 col-sm-9 col-xs-12">
<div class="row">
@@ -47,13 +80,4 @@ $(document).ready(function() {
</div>
</div>
{% endblock %}
{% block footerleft %}
{% if quota != -1 %}
<div id="quotabox">
<label class="pull-left navbar-text">{% trans "Mailbox quota" %}</label>
<div id="quotabar" class="pull-left progress">
<div class="{% progress_color quota %}" role="progressbar" style="width: {{ quota }}%" aria-valuenow="{{ quota }}" aria-valuemin="0" aria-valuemax="100">{{ quota }}%</div>
</div>
</div>
{% endif %}
{% endblock %}
@@ -1,3 +1,13 @@
{% extends "common/buttons_list.html" %}{% load i18n %}
{% block extracontent %}{% include "common/email_searchbar.html" with extraopts=extraopts %}
{% block before_menubar %}
<div class="col-xs-5 col-sm-5 col-md-5 col-lg-5">
{% include "common/email_searchbar.html" with extraopts=extraopts %}
</div>
{% endblock %}
{% block menubar_classes %}col-lg-2{% endblock %}
{% block after_menubar %}
<div id="pagination" class="col-lg-2"></div>
{% endblock %}
@@ -4,8 +4,11 @@ from django.template.loader import render_to_string
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
from django.conf import settings
from modoboa.extensions import webmail
from modoboa.extensions.webmail.lib import separate_mailbox
from modoboa.extensions.webmail.lib import (
imapheader, separate_mailbox
)
from modoboa.lib import parameters
register = template.Library()
@@ -15,10 +18,10 @@ register = template.Library()
def viewmail_menu(selection, folder, user, mail_id=None):
entries = [
{"name": "back",
"url": "javascript:history.go(-2);",
"img": "glyphicon glyphicon-arrow-left",
"class": "btn-primary sm-margin-left-back",
"label": _("Back")},
"url": "javascript:history.go(-1);",
"img": "glyphicon glyphicon-arrow-left",
"class": "btn-primary sm-margin-left-back",
"label": _("Back")},
{"name": "reply",
"url": "action=reply&mbox=%s&mailid=%s" % (folder, mail_id),
"img": "glyphicon glyphicon-share",
@@ -78,33 +81,23 @@ def compose_menu(selection, backurl, user):
@register.simple_tag
def listmailbox_menu(selection, folder, user):
entries = [
{"name": "compose",
"url": "compose",
"img": "glyphicon glyphicon-edit",
"label": _("New message"),
"class": "btn btn-default"},
{"name": "totrash",
"label": "",
"class": "btn btn-default",
"img": "glyphicon glyphicon-trash",
"url": reverse("modoboa.extensions.webmail.views.delete"),
},
{"name": "mark",
"label": _("Mark messages"),
"class": "btn btn-default",
"menu": [
{"name": "mark-read",
"label": _("As read"),
"url": reverse(webmail.views.mark, args=[folder]) + "?status=read"},
{"name": "mark-unread",
"label": _("As unread"),
"url": reverse(webmail.views.mark, args=[folder]) + "?status=unread"}
]
},
{"name": "actions",
"label": _("Actions"),
"class": "btn btn-default",
"menu": [
{"name": "mark-read",
"label": _("Mark as read"),
"url": reverse(webmail.views.mark, args=[folder]) + "?status=read"},
{"name": "mark-unread",
"label": _("Mark as unread"),
"url": reverse(webmail.views.mark, args=[folder]) + "?status=unread"},
{"divider": True},
{"name": "compress",
"label": _("Compress folder"),
"url": "compact/%s/" % folder}
@@ -179,7 +172,7 @@ def mboxes_menu():
{"name": "newmbox",
"url": reverse(webmail.views.newfolder),
"img": "glyphicon glyphicon-plus",
"title": _("Create a new mailbox"),
"label": _("Create a new mailbox"),
"modal": True,
"modalcb": "webmail.mboxform_cb",
"closecb": "webmail.mboxform_close",
@@ -187,15 +180,25 @@ def mboxes_menu():
{"name": "editmbox",
"url": reverse(webmail.views.editfolder),
"img": "glyphicon glyphicon-edit",
"title": _("Edit the selected mailbox"),
"label": _("Edit the selected mailbox"),
"class": "btn-default btn-xs"},
{"name": "removembox",
"url": reverse(webmail.views.delfolder),
"img": "glyphicon glyphicon-remove",
"title": _("Remove the selected mailbox"),
"label": _("Remove the selected mailbox"),
"class": "btn-default btn-xs"}
]
return render_to_string('common/buttons_list.html', dict(
entries=entries, css="nav"
return render_to_string('common/menu.html', dict(
entries=entries, css="dropdown-menu"
))
@register.filter
def parse_imap_header(value, header):
"""Simple template tag to display a IMAP header."""
try:
value = getattr(imapheader, "parse_%s" % header)(value)
except AttributeError:
pass
return value
+17 -2
View File
@@ -1,6 +1,12 @@
# coding: utf-8
"""
Webmail extension views.
"""
import os
from rfc6266 import build_header
from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import render
@@ -10,6 +16,7 @@ from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.gzip import gzip_page
from modoboa.lib import parameters
from modoboa.lib.exceptions import ModoboaException, BadRequest
from modoboa.lib.webutils import (
@@ -27,7 +34,7 @@ from .lib import (
ImapEmail, WebmailNavigationParameters, ReplyModifier, ForwardModifier,
get_imapconnector, IMAPconnector, separate_mailbox
)
from templatetags import webmail_tags
from .templatetags import webmail_tags
@login_required
@@ -338,7 +345,15 @@ def listmailbox(request, defmailbox="INBOX", update_session=True):
elems_per_page=int(parameters.get_user(request.user, "MESSAGES_PER_PAGE")),
**request.session["webmail_navparams"]
)
return lst.render(request, navparams.get('page'))
page = lst.paginator.getpage(navparams.get('page'))
if page is not None:
content = _render_to_string(request, "webmail/email_list.html", {
"emails": lst.mbc.fetch(page.id_start, page.id_stop, mbox, nbelems=40)
})
else:
content = ""
return {"listing": content, "length": len(content)}
#return lst.render(request, navparams.get('page'))
def render_compose(request, form, posturl, email=None, insert_signature=False):
+2 -2
View File
@@ -54,11 +54,11 @@
.sidebar {
position: fixed;
top: 51px;
bottom: 51px;
bottom: 0;
left: 0;
z-index: 1000;
display: block;
padding: 20px;
padding: 20px 15px;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
background-color: #f5f5f5;
+44 -39
View File
@@ -26,35 +26,37 @@
},
listen: function() {
$(document)
.off("change", "tbody input[type=checkbox]")
.on("change", "tbody input[type=checkbox]",
this.$element
.off("change", "input[type=checkbox]")
.on("change", "input[type=checkbox]",
$.proxy(this.toggle_select, this));
$(document).on("keydown", "body", $.proxy(this.keydown, this));
$(document).on("keyup", "body", $.proxy(this.keyup, this));
},
/**
* Effective change of a table row's state.
* Effective change of a row's state.
*/
_toggle_select: function($tr) {
if ($tr.hasClass(this.options.tr_selected_class)) {
$tr.children('td[name=selection]').children('input').prop('checked', false);
$tr.removeClass(this.options.tr_selected_class);
if (this.options.tr_unselected_event != undefined) {
this.options.tr_unselected_event($tr);
_toggle_select: function($row) {
var $input = $row.find(this.options.input_selector);
if ($row.hasClass(this.options.row_selected_class)) {
$input.prop('checked', false);
$row.removeClass(this.options.row_selected_class);
if (this.options.row_unselected_event !== undefined) {
this.options.row_unselected_event($row);
}
} else {
$tr.children('td[name=selection]').children('input').prop('checked', true);
$tr.addClass(this.options.tr_selected_class);
if (this.options.tr_selected_event != undefined) {
this.options.tr_selected_event($tr);
$input.prop('checked', true);
$row.addClass(this.options.row_selected_class);
if (this.options.row_selected_event !== undefined) {
this.options.row_selected_event($row);
}
}
},
/**
* Change the selection state of a given table row.
* Change the selection state of a given row.
*
* Holding the shift key between two selections let users
* select all rows between those two locations.
@@ -63,49 +65,50 @@
* an additive selection.
*/
toggle_select: function(e) {
var $tr = $(e.target).parents('tr');
var $row = $(e.target).parents(this.options.row_selector);
if (this.shift_pressed && this.last_selection) {
var start = this.last_selection;
this.clear_selection();
this.last_selection = start;
var itfunc = ($tr.index() >= this.last_selection.index())
? "next" : "prev";
var $curtr = null;
var itfunc = ($row.index() >= this.last_selection.index()) ?
"next" : "prev";
var $cur_row = null;
if (this.prev_dir && itfunc != this.prev_dir) {
$curtr = this.last_selection;
$cur_row = this.last_selection;
} else {
$curtr = this.last_selection;
$cur_row = this.last_selection;
}
for (; $curtr.length; $curtr = $curtr[itfunc]("tr")) {
this._toggle_select($curtr);
if ($tr.attr("id") == $curtr.attr("id")) {
for (; $cur_row.length; $cur_row = $cur_row[itfunc](this.options.row_selector)) {
this._toggle_select($cur_row);
if ($row.attr("id") == $cur_row.attr("id")) {
break;
}
}
this.prev_dir = itfunc;
return;
} else if (!this.ctrl_pressed && (!this.last_selection || !this.last_selection.is($tr))) {
} else if (!this.ctrl_pressed && (!this.last_selection || !this.last_selection.is($row))) {
this.clear_selection();
}
this.last_selection = $tr;
this._toggle_select($tr);
this.last_selection = $row;
this._toggle_select($row);
},
/**
* Check if $row is selected or not.
*/
is_selected: function($row) {
return $row.hasClass(this.options.tr_selected_class);
return $row.hasClass(this.options.row_selected_class);
},
/**
* Return the current selection. (jQuery object)
*/
current_selection: function() {
return $("tr[class*=" + this.options.tr_selected_class + "]");
return this.$element.find("." + this.options.row_selected_class);
},
/**
@@ -113,23 +116,23 @@
*/
clear_selection: function() {
this.last_selection = null;
this.current_selection().removeClass(this.options.tr_selected_class);
$("tbody input[type=checkbox]").prop('checked', false);
if (this.options.tr_unselected_event != undefined) {
this.options.tr_unselected_event();
this.current_selection().removeClass(this.options.row_selected_class);
this.$element.find(this.options.input_selector).prop('checked', false);
if (this.options.row_unselected_event !== undefined) {
this.options.row_unselected_event();
}
},
/**
* Manually select a table row.
* Manually select a row.
*/
select_row: function($row) {
if (!this.last_selection) {
this.last_selection = $row;
}
$row.addClass(this.options.tr_selected_class);
if (this.options.tr_selected_event != undefined) {
this.options.tr_selected_event($row);
$row.addClass(this.options.row_selected_class);
if (this.options.row_selected_event !== undefined) {
this.options.row_selected_event($row);
}
},
@@ -177,6 +180,8 @@
};
$.fn.htmltable.defaults = {
tr_selected_class: 'tr-selected'
row_selector: 'tr',
input_selector: "input[type=checkbox]",
row_selected_class: 'tr-selected'
};
})(jQuery);
})(jQuery);
@@ -0,0 +1,79 @@
/**
* Infinite scroll plugin for jQuery.
*
*/
!function($) {
'use strict';
var InfiniteScroll = function(element, options) {
this.$element = $(element);
this.$data = $(element).data();
this.$options = options;
this.executing = false;
this.end_of_results = false;
this.current_page = 1;
var that = this;
this.$element.scroll(function() {
if (that.$element.scrollTop() >= that.$options.calculate_bottom(that.$element)) {
that.load_more();
}
});
};
InfiniteScroll.prototype = {
constructor: InfiniteScroll,
load_more: function() {
var $this = this;
if ($this.executing || $this.end_of_results) {
return;
}
/*$this.$element.find('.spinner').removeClass('hide');*/
$this.executing = true;
$this.current_page += 1;
var args = $this.$options.get_args();
args.page = $this.current_page;
$.ajax({
contentType: 'application/json; charset=UTF-8',
data: args,
url: $this.$options.url,
type: 'GET',
success: function(data) {
$this.$options.process_results(data);
if (data.length === 0) {
$this.end_of_results = true;
/*$this.$element.find('#end-of-results').removeClass('hide');*/
}
//$this.$element.find('.spinner').addClass('hide');
$this.executing = false;
}
});
}
};
$.fn.infinite_scroll = function (option) {
return this.each(function () {
var $this = $(this),
data = $this.data('infinite-scroll'),
options = $.extend({}, $.fn.infinite_scroll.defaults, typeof option == 'object' && option);
if (!data) {
$this.data('infinite-scroll', (data = new InfiniteScroll(this, options)));
}
if (typeof options == 'string') {
data[options]();
}
});
};
$.fn.infinite_scroll.defaults = {
calculate_bottom: null,
get_data: null,
process_results: null,
url: ''
};
$.fn.infinite_scroll.Constructor = InfiniteScroll;
}(window.jQuery);
+23 -13
View File
@@ -1,18 +1,28 @@
{% load lib_tags %}
<!-- Brand and toggle get grouped for better mobile display -->
<div class="btn-toolbar {{ extraclasses }}">
{% for entry in entries %}
<ul class="nav navbar-nav action-buttons btn-group">{% if entry.menu %}
<a class="btn btn-default dropdown-toggle {{ entry.label }}" data-toggle="dropdown">{% if entry.img %}<i class="{{ entry.img}}"></i>{% endif %}{{ entry.label }} <span class="caret"></span></a>
<ul class="dropdown-menu">{% for sentry in entry.menu %}
<li>{% render_link sentry %}</li>
{% endfor %}</ul>
{% else %}{% render_link entry "btn" %}{% endif %}
</ul>
{% endfor %}
<ul class="nav navbar-nav action-buttons navbar-right">
{% block extracontent %}{% endblock %}
</ul>
<div class="row">
{% block before_menubar %}{% endblock %}
<div class="btn-toolbar {{ extraclasses }}{% block menubar_classes %}col-xs-12 col-sm-12 col-md-12 col-lg-12{% endblock %}">
{% for entry in entries %}
<ul class="nav navbar-nav action-buttons btn-group">
{% if entry.menu %}
<a class="btn btn-default dropdown-toggle {{ entry.label }}" data-toggle="dropdown">{% if entry.img %}<i class="{{ entry.img}}"></i>{% endif %}{{ entry.label }} <span class="caret"></span></a>
<ul class="dropdown-menu">
{% for sentry in entry.menu %}
{% if sentry.divider %}
<li class="divider"></li>
{% else %}
<li>{% render_link sentry %}</li>
{% endif %}
{% endfor %}
</ul>
{% else %}
{% render_link entry "btn" %}
{% endif %}
</ul>
{% endfor %}
</div>
{% block after_menubar %}{% endblock %}
</div>
+13 -13
View File
@@ -1,15 +1,15 @@
{% load i18n %}
<div id="searchbox" class="navbar-form navbar-left">
<div class="input-group">
<div id="searchbox" class="">
<div class="input-group">
<input type="text" class="form-control" name="searchfield" id="searchfield" placeholder="{% trans "Search..." %}" />
<ul class="dropdown-menu pull-right" id="searchparams">
<li><label class="radio"><input type="radio" id="crit_from_addr" name="scriteria" value="from_addr" checked>{% trans "From address" %}</label></li>
<li><label class="radio"><input type="radio" id="crit_subject" name="scriteria" value="subject">{% trans "Subject" %}</label></li>
{% for opt in extraopts %}
<li><label class="radio"><input type="radio" id="crit_{{ opt.name }}" name="scriteria" value="{{ opt.name }}">{{ opt.label }}</label></li>
{% endfor %}
<li><label class="radio"><input type="radio" id="crit_both" name="scriteria" value="both">{% trans "Both" %}</label></li>
</ul>
<a href="#" class="input-group-addon" data-toggle="dropdown"><i class="glyphicon glyphicon-search" id="searchopts"></i></a>
</div>
</div>
<ul class="dropdown-menu pull-right" id="searchparams">
<li><label class="radio"><input type="radio" id="crit_from_addr" name="scriteria" value="from_addr" checked>{% trans "From address" %}</label></li>
<li><label class="radio"><input type="radio" id="crit_subject" name="scriteria" value="subject">{% trans "Subject" %}</label></li>
{% for opt in extraopts %}
<li><label class="radio"><input type="radio" id="crit_{{ opt.name }}" name="scriteria" value="{{ opt.name }}">{{ opt.label }}</label></li>
{% endfor %}
<li><label class="radio"><input type="radio" id="crit_both" name="scriteria" value="both">{% trans "Both" %}</label></li>
</ul>
<a href="#" class="input-group-addon" data-toggle="dropdown"><i class="glyphicon glyphicon-search" id="searchopts"></i></a>
</div>
</div>
+1 -1
View File
@@ -1 +1 @@
<ul class="{{ css }}">{% include "common/menulist.html" %}</ul>
<ul class="{{ css }}" role="menu">{% include "common/menulist.html" %}</ul>
+13 -14
View File
@@ -97,21 +97,20 @@
</div>
</div>
{% block content %}{% endblock %}
<!--<div class="display-up-1198 nav-bottom">-->
<div id="bottom-bar" class="navbar navbar-inverse navbar-fixed-bottom">
<div class="container-fluid">
<div id="bottom-bar-left" class="col-lg-4 col-md-4 col-sm-4 col-xs-0">
{% block footerleft %}{% endblock %}
</div>
<div id="bottom-bar-middle" class="col-lg-4 col-md-4 col-sm-4 col-xs-0 col-xs-offset-0">
<p class="navbar-text"><i>{% trans "Powered by Modoboa" %}</i></p>
{% block footermiddle %}{% endblock %}
</div>
<div id="bottom-bar-right" class="col-lg-4 col-md-4 col-sm-4 col-xs-12">
{% block footerright %}{% endblock %}
</div>
<!--<div id="bottom-bar" class="navbar navbar-inverse navbar-fixed-bottom">
<div class="container-fluid">
<div id="bottom-bar-left" class="col-lg-4 col-md-4 col-sm-4 col-xs-0">
{% block footerleft %}{% endblock %}
</div>
<div id="bottom-bar-middle" class="col-lg-4 col-md-4 col-sm-4 col-xs-0 col-xs-offset-0">
<p class="navbar-text"><i>{% trans "Powered by Modoboa" %}</i></p>
{% block footermiddle %}{% endblock %}
</div>
<div id="bottom-bar-right" class="col-lg-4 col-md-4 col-sm-4 col-xs-12">
{% block footerright %}{% endblock %}
</div>
</div>
<!--</div>-->
</div>-->
</body>
</html>