Merge branch 'susimail-search-v2' into 'master'

Susimail: Add search

See merge request i2p-hackers/i2p.i2p!189
This commit is contained in:
zzz
2024-04-25 10:55:06 +00:00
5 changed files with 375 additions and 55 deletions

View File

@ -40,7 +40,7 @@ function addClickHandler1(elem)
function addClickHandler2(elem)
{
elem.addEventListener("click", function() {
var form = document.forms[0];
var form = document.forms[3];
form.delete.disabled = false;
form.markall.disabled = true;
form.clearselection.disabled = false;
@ -57,7 +57,7 @@ function addClickHandler2(elem)
function addClickHandler3(elem)
{
elem.addEventListener("click", function() {
var form = document.forms[0];
var form = document.forms[3];
form.delete.disabled = true;
form.markall.disabled = false;
form.clearselection.disabled = true;
@ -82,7 +82,7 @@ function deleteboxclicked() {
var hasOne = false;
var hasAll = true;
var hasNone = true;
var form = document.forms[0];
var form = document.forms[3];
for(i = 0; i < form.elements.length; i++) {
var elem = form.elements[i];
if (elem.type == 'checkbox') {

View File

@ -61,12 +61,28 @@ public class Folder<O extends Object> {
UP;
}
/**
* @since 0.9.63
*/
public interface Selector<O> {
/**
* @return non-null
*/
public String getSelectionKey();
/**
* @return true if selected
*/
public boolean select(O element);
}
private int pages, pageSize, currentPage;
private O[] elements;
private final Map<String, Comparator<O>> sorter;
private SortOrder sortingDirection;
private Comparator<O> currentSorter;
private String currentSortID;
private Selector<O> currentSelector;
private List<O> selected;
public Folder()
{
@ -109,10 +125,18 @@ public class Folder<O extends Object> {
/**
* Returns the number of pages in the folder.
* Minimum of 1 even if empty.
* If the selector is non-null, this will be the number of pages in the selected results.
*
* @return Returns the number of pages.
*/
public synchronized int getPages() {
return pages;
if (currentSelector == null || elements == null)
return pages;
int ps = getPageSize();
int rv = selected.size() / ps;
if (rv * ps < elements.length)
rv++;
return rv;
}
/**
@ -177,6 +201,7 @@ public class Folder<O extends Object> {
if (elements.length > 0) {
this.elements = elements;
sort();
select();
} else {
this.elements = null;
}
@ -285,6 +310,27 @@ public class Folder<O extends Object> {
return list.iterator();
}
/**
* Returns an iterator containing the elements on the current page.
* This iterator is over a copy of the current page, and so
* is thread safe w.r.t. other operations on this folder,
* but will not reflect subsequent changes, and iter.remove()
* will not change the folder.
*
* @return Iterator containing the elements on the current page.
* @since 0.9.63
*/
public synchronized Iterator<O> currentPageSelectorIterator()
{
if (currentSelector == null)
return currentPageIterator();
int pageSize = getPageSize();
int offset = ( currentPage - 1 ) * pageSize;
if (selected == null || offset > selected.size())
return Collections.<O>emptyList().iterator();
return selected.subList(offset, Math.min(selected.size(), offset + pageSize)).iterator();
}
/**
* Turns folder to next page.
*/
@ -370,27 +416,54 @@ public class Folder<O extends Object> {
public synchronized SortOrder getCurrentSortingDirection() {
return sortingDirection;
}
/**
* Returns the element on the current page on the given position.
* Warning, this does not do the actual selection, this is done in the iterator.
* Resets page to 1 if selector changed.
*
* @param x Position of the element on the current page.
* @return Element on the current page on the given position.
* @param selector may be null
* @since 0.9.63
*/
/**** unused, we now fetch by UIDL, not position
public synchronized O getElementAtPosXonCurrentPage( int x )
{
O result = null;
if( elements != null ) {
int pageSize = getPageSize();
int offset = ( currentPage - 1 ) * pageSize;
offset += x;
if( offset >= 0 && offset < elements.length )
result = elements[offset];
public synchronized void setSelector(Selector<O> selector) {
if ((currentSelector != null && selector == null) ||
(currentSelector == null && selector != null) ||
(currentSelector != null && selector != null &&
!currentSelector.getSelectionKey().equals(selector.getSelectionKey()))) {
currentPage = 1;
}
currentSelector = selector;
if (selector != null)
select();
else
selected = null;
}
/**
* @return current selector or null
* @since 0.9.63
*/
public synchronized Selector<O> getCurrentSelector() {
return currentSelector;
}
/**
* Select and cache results
* @since 0.9.63
*/
private synchronized void select() {
if (selected == null)
selected = new ArrayList<O>();
else
selected.clear();
if (elements == null || currentSelector == null)
return;
int sz = getSize();
for (int i = 0; i < sz; i++) {
if (currentSelector.select(elements[i])) {
selected.add(elements[i]);
}
}
return result;
}
****/
/**
* Returns the first element of the sorted folder.
@ -467,6 +540,47 @@ public class Folder<O extends Object> {
}
return result;
}
/**
* Retrieves the next element in the sorted array.
*
* @param element
* @return The next element
*/
public synchronized O getNextSelectedElement(O element)
{
if (currentSelector == null)
return getNextElement(element);
O result = null;
int i = selected.indexOf(element);
if (i != -1 && selected != null) {
i++;
if (i < selected.size())
result = selected.get(i);
}
return result;
}
/**
* Retrieves the previous element in the sorted array.
*
* @param element
* @return The previous element
*/
public synchronized O getPreviousSelectedElement(O element)
{
if (currentSelector == null)
return getPreviousElement(element);
O result = null;
int i = selected.indexOf(element);
if (i != -1 && selected != null) {
i--;
if (i >= 0 && i < selected.size())
result = selected.get(i);
}
return result;
}
/**
* Retrieves element at index i.
*

View File

@ -147,6 +147,7 @@ public class WebMail extends HttpServlet
private static final String NEXT_PAGE_NUM = "nextpagenum";
private static final String CURRENT_SORT = "currentsort";
private static final String CURRENT_FOLDER = "folder";
private static final String CURRENT_SEARCH = "currentsearch";
private static final String NEW_FOLDER = "newfolder";
private static final String DRAFT_EXISTS = "draftexists";
private static final String DEBUG_STATE = "currentstate";
@ -169,6 +170,7 @@ public class WebMail extends HttpServlet
private static final String REALLYDELETE = "really_delete";
private static final String MOVE_TO = "moveto";
private static final String SWITCH_TO = "switchto";
private static final String SEARCH = "s";
// also a GET param
private static final String SHOW = "show";
private static final String DOWNLOAD = "download";
@ -1193,7 +1195,8 @@ public class WebMail extends HttpServlet
buttonPressed( request, CLEAR ) ||
buttonPressed( request, INVERT ) ||
buttonPressed( request, SORT ) ||
buttonPressed( request, REFRESH )) {
buttonPressed( request, REFRESH ) ||
buttonPressed( request, SEARCH )) {
state = State.LIST;
} else if (buttonPressed(request, PREV) ||
buttonPressed(request, NEXT) ||
@ -1221,6 +1224,9 @@ public class WebMail extends HttpServlet
} else if (buttonPressed(request, DRAFT_ATTACHMENT)) {
// GET params
state = State.NEW;
} else if (buttonPressed(request, SEARCH)) {
// GET params for XHR
return State.LIST;
}
/*
@ -2614,15 +2620,31 @@ public class WebMail extends HttpServlet
}
out.println("<script src=\"/susimail/js/notifications.js?" + CoreVersion.VERSION + "\" type=\"text/javascript\"></script>");
out.print("</head>\n<body>");
String nonce = state == State.AUTH ? LOGIN_NONCE :
Long.toString(ctx.random().nextLong());
sessionObject.addNonce(nonce);
out.println(
"<div class=\"page\" id=\"page\">" +
"<form method=\"POST\" enctype=\"multipart/form-data\" action=\"" + myself + "\" accept-charset=\"UTF-8\">\n" +
"<input type=\"hidden\" name=\"" + SUSI_NONCE + "\" value=\"" + nonce + "\">\n" +
// we use this to know if the user thought he was logged in at the time
"<input type=\"hidden\" name=\"" + DEBUG_STATE + "\" value=\"" + state + "\">");
out.println("<div class=\"page\" id=\"page\">");
if (state != State.LIST) {
// For all states except LIST, we have one big form for the whole page.
// LIST has several forms, we will output them in showFolder().
String nonce = state == State.AUTH ? LOGIN_NONCE :
Long.toString(ctx.random().nextLong());
sessionObject.addNonce(nonce);
out.println("<form method=\"POST\" enctype=\"multipart/form-data\" action=\"" + myself + "\" accept-charset=\"UTF-8\">\n" +
"<input type=\"hidden\" name=\"" + SUSI_NONCE + "\" value=\"" + nonce + "\">\n" +
// we use this to know if the user thought he was logged in at the time
"<input type=\"hidden\" name=\"" + DEBUG_STATE + "\" value=\"" + state + "\">");
if (state != State.AUTH && state != State.CONFIG && state != State.LOADING) {
// maintain the search param when changing pages or folders or
// going to message view and back
String search = request.getParameter(CURRENT_SEARCH);
if (search == null || search.length() == 0) {
Folder.Selector selector = mc.getFolder().getCurrentSelector();
if (selector != null)
search = selector.getSelectionKey();
}
if (search != null && search.length() > 0)
out.println("<input type=\"hidden\" name=\"" + CURRENT_SEARCH + "\" value=\"" + DataHelper.escapeHTML(search) + "\">\n");
}
}
if (state == State.NEW) {
String newUIDL = request.getParameter(NEW_UIDL);
if (newUIDL == null || newUIDL.length() <= 0)
@ -2646,8 +2668,6 @@ public class WebMail extends HttpServlet
}
out.println("<input type=\"hidden\" name=\"" + CUR_PAGE + "\" value=\"" + page + "\">");
}
}
if (state == State.SHOW || state == State.NEW || state == State.LIST) {
// Save sort order in case it changes later
String curSort = folder.getCurrentSortBy();
SortOrder curOrder = folder.getCurrentSortingDirection();
@ -2698,6 +2718,7 @@ public class WebMail extends HttpServlet
}
out.println("</div>" );
}
/*
* now write body
*/
@ -2735,7 +2756,8 @@ public class WebMail extends HttpServlet
else if( state == State.CONFIG )
showConfig(out, folder);
out.println("</form>\n");
if (state != State.LIST)
out.println("</form>\n");
out.println("<div class=\"footer\"><p class=\"footer\">\n" +
"<img class=\"footer\" src=\"" + myself + "themes/images/susimail.png\" alt=\"susimail\">\n" +
@ -3484,18 +3506,9 @@ public class WebMail extends HttpServlet
*/
private static void showFolder( PrintWriter out, SessionObject sessionObject, MailCache mc, RequestWrapper request )
{
out.println("<div class=\"topbuttons\">");
out.println( button( NEW, _t("New") ) + spacer);
// In theory, these are valid and will apply to the first checked message,
// but that's not obvious and did it work?
//button( REPLY, _t("Reply") ) +
//button( REPLYALL, _t("Reply All") ) +
//button( FORWARD, _t("Forward") ) + spacer +
//button( DELETE, _t("Delete") ) + spacer +
String folderName = mc.getFolderName();
String floc;
if (folderName.equals(DIR_FOLDER)) {
out.println((sessionObject.isFetching ? button2(REFRESH, _t("Check Mail")) : button(REFRESH, _t("Check Mail"))) + spacer);
floc = "";
} else if (folderName.equals(DIR_DRAFTS)) {
floc = "";
@ -3504,11 +3517,75 @@ public class WebMail extends HttpServlet
}
boolean isSpamFolder = folderName.equals(DIR_SPAM);
boolean showToColumn = folderName.equals(DIR_DRAFTS) || folderName.equals(DIR_SENT);
//if (Config.hasConfigFile())
// out.println(button( RELOAD, _t("Reload Config") ) + spacer);
out.println(button( LOGOUT, _t("Logout") ));
int page = 1;
// For all states except LIST, we have one big form for the whole page.
// Here, for LIST, we set up 4-5 forms
// to deal with html rules and have a search box that works right.
// 1: new/checkmail/logout, inside topbuttons div
// 2: search, inside topbuttons div
// 3: change folder and page buttons, inside topbuttons div, surrounds pagenav table
// 4: mail delete boxes and bottombuttons div, surrounds mailbox table
// 5: change folder and page buttons, inside 2nd topbuttons div, surrounds pagenav table,
// only shown if more than 30 mails on a page
//
// Create the common form header
I2PAppContext ctx = I2PAppContext.getGlobalContext();
String nonce = Long.toString(ctx.random().nextLong());
sessionObject.addNonce(nonce);
// for all but search
String form = "<form method=\"POST\" enctype=\"multipart/form-data\" action=\"" + myself + "\" accept-charset=\"UTF-8\">\n";
StringBuilder fbf = new StringBuilder(256);
fbf.append("<input type=\"hidden\" name=\"").append(SUSI_NONCE).append("\" value=\"").append(nonce).append("\">\n")
.append("<input type=\"hidden\" name=\"").append(DEBUG_STATE).append("\" value=\"").append(State.LIST).append("\">\n");
Folder<String> folder = mc.getFolder();
String curSort = folder.getCurrentSortBy();
SortOrder curOrder = folder.getCurrentSortingDirection();
// UP is reverse sort. DOWN is normal sort.
String fullSort = curOrder == SortOrder.UP ? '-' + curSort : curSort;
fbf.append("<input type=\"hidden\" name=\"").append(CURRENT_SORT).append("\" value=\"").append(fullSort).append("\">\n")
.append("<input type=\"hidden\" name=\"").append(CURRENT_FOLDER).append("\" value=\"").append(folderName).append("\">\n");
String cursearch = request.getParameter(CURRENT_SEARCH);
String search = request.getParameter(SEARCH);
if (cursearch != null && cursearch.length() > 0) {
if (search == null) {
// we came from somewhere else, set search to cursearch, will set selector below
search = cursearch;
} else {
// we came from here, search wins, will set selector below
}
}
if (search != null && search.length() > 0) {
fbf.append("<input type=\"hidden\" name=\"").append(CURRENT_SEARCH).append("\" value=\"").append(DataHelper.escapeHTML(search)).append("\">\n");
Folder.Selector olds = folder.getCurrentSelector();
if (olds == null || !olds.getSelectionKey().equals(search)) {
folder.setSelector(new SearchSelector(mc, search));
}
} else if ((search == null || search.length() == 0) && folder.getCurrentSelector() != null) {
folder.setSelector(null);
}
String hidden = fbf.toString();
out.println("<div class=\"topbuttons\">");
// form 1
out.print(form);
out.print(hidden);
out.println( button( NEW, _t("New") ) + spacer);
if (folderName.equals(DIR_FOLDER))
out.println((sessionObject.isFetching ? button2(REFRESH, _t("Check Mail")) : button(REFRESH, _t("Check Mail"))) + spacer);
out.println(button( LOGOUT, _t("Logout") ));
out.println("</form>");
if (folder.getSize() > 1 || (search != null && search.length() > 0)) {
// form 2
out.println("<form class=\"search\" id = \"search\" action=\"" + myself + "\" method=\"POST\">");
out.print(hidden);
out.write("<input type=\"text\" name=\"" + SEARCH + "\" size=\"20\" class=\"search\" id=\"searchbox\"");
if (search != null)
out.write(" value=\"" + DataHelper.escapeHTML(search) + '"');
out.println("><a class=\"cancel\" id=\"searchcancel\" href=\"" + myself + "\"></a>");
out.println("</form>");
}
int page = 1;
if (folder.getPages() > 1) {
String sp = request.getParameter(CUR_PAGE);
if (sp != null) {
@ -3518,13 +3595,18 @@ public class WebMail extends HttpServlet
}
folder.setCurrentPage(page);
}
// form 3
out.print(form);
out.print(hidden);
showPageButtons(out, sessionObject.user, folderName, page, folder.getPages(), true);
out.println("</form>");
out.println("</div>");
String curSort = folder.getCurrentSortBy();
SortOrder curOrder = folder.getCurrentSortingDirection();
out.println("<table id=\"mailbox\" cellspacing=\"0\" cellpadding=\"5\">\n" +
"<tr><td colspan=\"9\"><hr></td></tr>\n<tr><th title=\"" + _t("Mark for deletion") + "\">&nbsp;</th>" +
// form 4
out.print(form);
out.print(hidden);
out.println("<table id=\"mailbox\" cellspacing=\"0\" cellpadding=\"5\">\n");
out.println("<tr><td colspan=\"9\"><hr></td></tr>\n<tr><th title=\"" + _t("Mark for deletion") + "\">&nbsp;</th>" +
thSpacer + "<th>" + sortHeader(SORT_SENDER, showToColumn ? _t("To") : _t("From"), sessionObject.imgPath, curSort, curOrder, page, folderName) + "</th>" +
thSpacer + "<th>" + sortHeader(SORT_SUBJECT, _t("Subject"), sessionObject.imgPath, curSort, curOrder, page, folderName) + "</th>" +
thSpacer + "<th>" + sortHeader(SORT_DATE, _t("Date"), sessionObject.imgPath, curSort, curOrder, page, folderName) +
@ -3532,7 +3614,7 @@ public class WebMail extends HttpServlet
thSpacer + "<th>" + sortHeader(SORT_SIZE, _t("Size"), sessionObject.imgPath, curSort, curOrder, page, folderName) + "</th></tr>" );
int bg = 0;
int i = 0;
for (Iterator<String> it = folder.currentPageIterator(); it != null && it.hasNext(); ) {
for (Iterator<String> it = folder.currentPageSelectorIterator(); it != null && it.hasNext(); ) {
String uidl = it.next();
Mail mail = mc.getMail(uidl, MailCache.FetchMode.HEADER_CACHE_ONLY);
if (mail == null || !mail.hasHeader()) {
@ -3644,14 +3726,82 @@ public class WebMail extends HttpServlet
out.print(button(CONFIGURE, _t("Settings")));
out.println("</td></tr>");
out.println( "</table>");
if (folder.getPages() > 1 && i > 30) {
out.println("</form>");
int ps = folder.getPageSize();
if (ps > 30 && (folder.getCurrentPage() - 1) * ps < folder.getSize() - 30) {
// show the buttons again if page is big
out.println("<div class=\"topbuttons\">");
// form 5
out.print(form);
out.print(hidden);
showPageButtons(out, sessionObject.user, folderName, page, folder.getPages(), false);
out.println("</form>");
out.println("</div>");
}
}
/**
* Folder callback to search mails for matching terms.
* Only subject and sender (or recipients for drafts).
* Mail bodies are not in-memory and would be very slow.
*
* @since 0.9.63
*/
private static class SearchSelector implements Folder.Selector<String> {
private final String key;
private final MailCache mc;
private final String[] terms;
private final boolean isDrafts;
/**
* @param search non-null, non-empty, and %-encoded, will be decoded here
*/
public SearchSelector(MailCache cache, String search) {
mc = cache;
isDrafts = mc.getFolderName().equals(DIR_DRAFTS);
key = search;
terms = DataHelper.split(search, " ");
// decode
for (int i = 0; i < terms.length; i++) {
terms[i] = terms[i].toLowerCase(Locale.US);
}
}
public String getSelectionKey() {
return key;
}
public boolean select(String uidl) {
Mail mail = mc.getMail(uidl, MailCache.FetchMode.HEADER_CACHE_ONLY);
if (mail == null)
return false;
String subj = mail.subject.toLowerCase(Locale.US);
String sender = isDrafts ? null : mail.sender;
if (sender != null)
sender = sender.toLowerCase(Locale.US);
String[] to = isDrafts ? mail.to : null;
for (String term : terms) {
if (subj.contains(term))
return true;
if (sender != null && sender.contains(term))
return true;
if (to != null) {
for (int i = 0; i < to.length; i++) {
if (to[i].toLowerCase(Locale.US).contains(term))
return true;
}
}
}
return false;
}
@Override
public String toString() {
return "Search selector for '" + key + "'";
}
}
/**
* Folder selector, then, if pages greater than 1:
* first prev next last
@ -3794,7 +3944,11 @@ public class WebMail extends HttpServlet
out.println("<div id=\"messagenav\">");
Folder<String> folder = mc.getFolder();
if (hasHeader) {
String uidl = folder.getPreviousElement(showUIDL);
String uidl;
if (folder.getCurrentSelector() != null)
uidl = folder.getPreviousSelectedElement(showUIDL);
else
uidl = folder.getPreviousElement(showUIDL);
String text = _t("Previous");
if (uidl == null || folder.isFirstElement(showUIDL)) {
out.println(button2(PREV, text));
@ -3809,7 +3963,11 @@ public class WebMail extends HttpServlet
out.println("<input type=\"hidden\" name=\"" + CUR_PAGE + "\" value=\"" + page + "\">");
out.println(button( LIST, _t("Back to Folder") ) + spacer);
if (hasHeader) {
String uidl = folder.getNextElement(showUIDL);
String uidl;
if (folder.getCurrentSelector() != null)
uidl = folder.getNextSelectedElement(showUIDL);
else
uidl = folder.getNextElement(showUIDL);
String text = _t("Next");
if (uidl == null || folder.isLastElement(showUIDL)) {
out.println(button2(NEXT, text));

View File

@ -244,6 +244,30 @@ div.topbuttons br {
border-radius: 15px;
}
#searchbox {
background: #f8f8ff url(/themes/console/images/buttons/search.png) 7px center no-repeat !important;
margin: 2px 4px 2px 24px !important;
padding: 4px 32px 4px 32px !important;
color: #47475f;
}
#searchbox:focus, #searchbox:active {
color: #19191f;
}
#searchcancel {
background: url(../images/delete.png) 0px center no-repeat;
margin: 9px 4px 2px 2px;
padding: 6px 12px;
color: transparent;
border: none;
float: left;
}
#searchcancel.disabled {
display: none;
}
table#pagenav {
float: right;
width: 200px;

View File

@ -959,7 +959,7 @@ hr {
}
#composemail table {
width: auto;
width: 90%;
margin: auto;
}
@ -1187,6 +1187,30 @@ h3#config {
float: none !important;
}
#searchbox {
background: #f8f8ff url(/themes/console/images/buttons/search.png) 7px center no-repeat !important;
margin: 2px 4px 2px 24px !important;
padding: 4px 32px 4px 32px !important;
color: #47475f;
}
#searchbox:focus, #searchbox:active {
color: #19191f;
}
#searchcancel {
background: url(../images/delete.png) 0px center no-repeat;
margin: 9px 4px 2px 2px;
padding: 6px 12px;
color: transparent;
border: none;
float: left;
}
#searchcancel.disabled {
display: none;
}
input.moveto {
float: left;
margin-left: 14px;