diff --git a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java index 2eaa54881196d6aa13615ed613bc1bd8679f41b4..60597a5f9d4f494e1fa8eedaedac0bc729fd3cba 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java @@ -32,12 +32,14 @@ import java.util.Properties; import java.util.Set; import net.i2p.I2PAppContext; +import net.i2p.app.ClientAppManager; import net.i2p.client.naming.HostTxtEntry; import net.i2p.client.naming.NamingService; import net.i2p.client.naming.SingleFileNamingService; import net.i2p.data.DataFormatException; import net.i2p.data.Destination; import net.i2p.util.OrderedProperties; +import net.i2p.util.PortMapper; import net.i2p.util.SecureDirectory; import net.i2p.util.SystemVersion; @@ -669,6 +671,13 @@ class Daemon { invalid + " invalid, " + conflict + " conflicts"); } + if (nnew > 0) { + ClientAppManager cmgr = I2PAppContext.getGlobalContext().clientAppManager(); + if (cmgr != null) { + int nc = cmgr.getBubbleCount(PortMapper.SVC_SUSIDNS); + cmgr.setBubble(PortMapper.SVC_SUSIDNS, nc + nnew, null); + } + } } /** @since 0.9.26 */ diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index 0027f8ed080f0c9e10d85a4003a7ff5159898b9f..d34c3426c37a9b61933804ecbe8b832225f957eb 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -45,6 +45,7 @@ import net.i2p.util.FileUtil; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; import net.i2p.util.OrderedProperties; +import net.i2p.util.PortMapper; import net.i2p.util.SecureDirectory; import net.i2p.util.SecureFileOutputStream; import net.i2p.util.SimpleTimer; @@ -501,7 +502,10 @@ public class SnarkManager implements CompleteListener, ClientApp, DisconnectList /** @since 0.9 */ public void clearMessages() { - _messages.clear(); + _messages.clear(); + ClientAppManager cmgr = _context.clientAppManager(); + if (cmgr != null) + cmgr.setBubble(PortMapper.SVC_I2PSNARK, 0, null); } /** @@ -509,7 +513,10 @@ public class SnarkManager implements CompleteListener, ClientApp, DisconnectList * @since 0.9.33 */ public void clearMessages(int id) { - _messages.clearThrough(id); + _messages.clearThrough(id); + ClientAppManager cmgr = _context.clientAppManager(); + if (cmgr != null) + cmgr.setBubble(PortMapper.SVC_I2PSNARK, 0, null); } /** @@ -2876,6 +2883,7 @@ public class SnarkManager implements CompleteListener, ClientApp, DisconnectList NotificationService ns = (NotificationService) cmgr.getRegisteredApp("desktopgui"); if (ns != null) ns.notify("I2PSnark", null, priority, _t("I2PSnark"), message, path); + cmgr.addBubble(PortMapper.SVC_I2PSNARK, message); } if (!_context.isRouterContext()) System.out.println(message); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java index b9e012d7e8513bb6358f3686a8d13f6360a14df4..0f9d69c5caa0bc23f7e35afe46d9ab45935994da 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java @@ -297,6 +297,7 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { else msg = "Offline signature for tunnel " + name + " alternate destination expired " + DataHelper.formatTime(exp); _log.log(Log.CRIT, msg); + TunnelController.addBubble(getTunnel().getContext(), msg); throw new IllegalArgumentException(msg); } if (remaining < 60*24*60*60*1000L) { @@ -346,6 +347,7 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { else msg = "Offline signature for tunnel " + name + " expired " + DataHelper.formatTime(exp); _log.log(Log.CRIT, msg); + TunnelController.addBubble(getTunnel().getContext(), msg); throw new IllegalArgumentException(msg); } if (remaining < 60*24*60*60*1000L) { @@ -394,6 +396,7 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { msg += ", giving up"; this.l.log(msg); _log.log(Log.CRIT, msg, ise); + TunnelController.addBubble(getTunnel().getContext(), msg); throw new IllegalArgumentException(msg, ise); } try { Thread.sleep(RETRY_DELAY); } catch (InterruptedException ie) {} @@ -686,6 +689,7 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { String s = "Error accepting - KILLING THE TUNNEL SERVER"; _log.log(Log.CRIT, s, ipe); l.log(s + ": " + ipe); + TunnelController.addBubble(getTunnel().getContext(), s); // Tell TunnelController so it will change state TunnelController tc = getTunnel().getController(); if (tc != null) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java index 0b9855368dc82102fcb193c645bfed850bd5b78a..75b1630c2fa2db4e8df56750603a93aea687cd20 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java @@ -14,6 +14,7 @@ import java.util.Set; import net.i2p.I2PAppContext; import net.i2p.I2PException; +import net.i2p.app.ClientAppManager; import net.i2p.client.I2PClient; import net.i2p.client.I2PClientFactory; import net.i2p.client.I2PSession; @@ -34,6 +35,7 @@ import net.i2p.i2ptunnel.socks.I2PSOCKSTunnel; import net.i2p.util.FileUtil; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; +import net.i2p.util.PortMapper; import net.i2p.util.RandomSource; import net.i2p.util.SecureFile; import net.i2p.util.SecureFileOutputStream; @@ -429,8 +431,10 @@ public class TunnelController implements Logging { try { doStartTunnel(); } catch (RuntimeException e) { - _log.error("Error starting the tunnel " + getName(), e); - log("Error starting the tunnel " + getName() + ": " + e.getMessage()); + String msg = "Error starting the tunnel " + getName(); + _log.error(msg, e); + addBubble(msg); + log(msg + ": " + e.getMessage()); // if we don't acquire() then the release() in stopTunnel() won't work acquire(); stopTunnel(); @@ -1398,6 +1402,25 @@ public class TunnelController implements Logging { return rv; } + /** + * @param msg may be null + * @since 0.9.66 + */ + private void addBubble(String msg) { + addBubble(_tunnel.getContext(), msg); + } + + /** + * @param msg may be null + * @since 0.9.66 + */ + static void addBubble(I2PAppContext ctx, String msg) { + ClientAppManager cmgr = ctx.clientAppManager(); + if (cmgr != null) { + cmgr.addBubble(PortMapper.SVC_I2PTUNNEL, msg); + } + } + /** * @since 0.9.15 */ @@ -1477,6 +1500,7 @@ public class TunnelController implements Logging { msg = "Offline signature in private key file " + f + " for tunnel expired " + DataHelper.formatTime(exp) + ", stopping the tunnel!"; _log.log(Log.CRIT, msg); _tunnel.log(msg); + addBubble(msg); stopTunnel(); return; } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/HomeHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/HomeHelper.java index 34f09b7d6e0c2675b970e05dff61b03e3adda1a4..49499ab0c9579abb7b254ebbc884e6b903293a56 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/HomeHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/HomeHelper.java @@ -8,6 +8,7 @@ import java.util.Set; import java.util.TreeSet; import net.i2p.CoreVersion; +import net.i2p.app.ClientAppManager; import net.i2p.data.DataHelper; import net.i2p.router.RouterContext; import net.i2p.router.web.App; @@ -388,7 +389,9 @@ Steps for the devs after approval at a meeting: StringBuilder buf = new StringBuilder(1024); buf.append("<div class=\"appgroup\">"); PortMapper pm = _context.portMapper(); + ClientAppManager cmgr = _context.clientAppManager(); for (App app : apps) { + String svc = null; String url; if (app.name.equals(website) && app.url.equals("http://127.0.0.1:7658/")) { // fixup I2P Site link @@ -399,27 +402,27 @@ Steps for the devs after approval at a meeting: url = app.url; // check for disabled webapps and other things if (url.equals("/dns")) { - if (!pm.isRegistered(PortMapper.SVC_SUSIDNS)) - continue; + svc = PortMapper.SVC_SUSIDNS; } else if (url.equals("/webmail")) { - if (!pm.isRegistered(PortMapper.SVC_SUSIMAIL)) - continue; + svc = PortMapper.SVC_SUSIMAIL; } else if (url.equals("/torrents")) { - if (!pm.isRegistered(PortMapper.SVC_I2PSNARK)) - continue; + svc = PortMapper.SVC_I2PSNARK; } else if (url.equals("/i2ptunnelmgr")) { - if (!pm.isRegistered(PortMapper.SVC_I2PTUNNEL)) - continue; + svc = PortMapper.SVC_I2PTUNNEL; // need both webapp and TCG, but we aren't refreshing // the icons, so let's not do this //ClientAppManager cmgr = _context.clientAppManager(); //if (cmgr != null && cmgr.getRegisteredApp("i2ptunnel") == null) // continue; + } else if (url.equals("/logs")) { + svc = PortMapper.SVC_LOGS; } else if (url.equals("/configplugins")) { if (!PluginStarter.pluginsEnabled(_context)) continue; } } + if (svc != null && !pm.isRegistered(svc)) + continue; // If an image isn't in a /themes or /images directory, it comes from a plugin. // tag it thus. String plugin = ""; @@ -432,8 +435,21 @@ Steps for the devs after approval at a meeting: "<a href=\"").append(url).append("\" tabindex=\"-1\">" + "<img alt=\"\" title=\"").append(app.desc).append("\" src=\"").append(app.icon) // version the icons because they may change - .append(app.icon.contains("?") ? "&" : "?").append(CoreVersion.VERSION).append("\"></a>" + - "</div>\n" + + .append(app.icon.contains("?") ? "&" : "?").append(CoreVersion.VERSION).append("\">"); + // notification bubbles + if (svc != null && cmgr != null) { + int nc = cmgr.getBubbleCount(svc); + if (nc > 0) { + buf.append("<span class=\"notifbubble\" "); + String ns = cmgr.getBubbleText(svc); + if (ns != null) + buf.append(" title=\"").append(DataHelper.escapeHTML(ns)).append("\" "); + buf.append('>'); + buf.append(nc); + buf.append("</span>"); + } + } + buf.append("</a></div>\n" + "<table><tr><td>" + "<div class=\"applabel\">" + "<a href=\"").append(url).append("\" title=\"").append(app.desc).append("\">").append(app.name).append("</a>" + diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/LogsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/LogsHelper.java index c86168841d5e70b0bc92e8cac33a60ceced88a9b..f63e3c8cee8c7a61eb5223a2946c2c8b5ff9aca8 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/LogsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/LogsHelper.java @@ -12,12 +12,14 @@ import java.util.concurrent.ArrayBlockingQueue; import java.util.jar.Attributes; import net.i2p.I2PAppContext; +import net.i2p.app.ClientAppManager; import net.i2p.crypto.SigType; import net.i2p.data.DataHelper; import net.i2p.router.web.ConfigServiceHandler; import net.i2p.router.web.CSSHelper; import net.i2p.router.web.HelperBase; import net.i2p.router.web.RouterConsoleRunner; +import net.i2p.util.PortMapper; import net.i2p.util.Translate; import net.i2p.util.UIMessages; @@ -88,7 +90,13 @@ public class LogsHelper extends HelperBase { * */ public String getCriticalLogs() { - return formatMessages(_context.logManager().getBuffer().getMostRecentCriticalMessages()); + List<String> msgs = _context.logManager().getBuffer().getMostRecentCriticalMessages(); + if (!msgs.isEmpty()) { + ClientAppManager cmgr = _context.clientAppManager(); + if (cmgr != null) + cmgr.setBubble(PortMapper.SVC_LOGS, 0, null); + } + return formatMessages(msgs); } /** diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/SummaryBarRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/SummaryBarRenderer.java index 523c77ecd08dcc702f059696a08dd7e0b717db66..9d2da1f2dbf8f264c1e9614a81f402da55f23dfb 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/SummaryBarRenderer.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/SummaryBarRenderer.java @@ -226,8 +226,9 @@ class SummaryBarRenderer { "<a href=\"/webmail\" target=\"_top\" title=\"") .append(_t("Anonymous webmail client")) .append("\">") - .append(nbsp(tx)) - .append("</a></td></tr>\n"); + .append(nbsp(tx)); + addBubble(rbuf, PortMapper.SVC_SUSIMAIL); + rbuf.append("</a></td></tr>\n"); svcs.put(tx, rbuf.toString()); } @@ -250,8 +251,9 @@ class SummaryBarRenderer { "<a href=\"/torrents\" target=\"_top\" title=\"") .append(_t("Built-in anonymous BitTorrent Client")) .append("\">") - .append(nbsp(tx)) - .append("</a></td></tr>\n"); + .append(nbsp(tx)); + addBubble(rbuf, PortMapper.SVC_I2PSNARK); + rbuf.append("</a></td></tr>\n"); svcs.put(tx, rbuf.toString()); } @@ -299,6 +301,25 @@ class SummaryBarRenderer { } } + /** + * @since 0.9.66 + */ + private void addBubble(StringBuilder buf, String svc) { + ClientAppManager cmgr = _context.clientAppManager(); + if (cmgr == null) + return; + int nc = cmgr.getBubbleCount(svc); + if (nc <= 0) + return; + buf.append(" <span class=\"notifcount\" "); + String ns = cmgr.getBubbleText(svc); + if (ns != null) + buf.append(" title=\"").append(DataHelper.escapeHTML(ns)).append("\" "); + buf.append('>'); + buf.append(nc); + buf.append("</span>"); + } + /** * @return null if none * @since 0.9.43 split out from above, used by HomeHelper, fixed for IPv6 @@ -349,8 +370,9 @@ class SummaryBarRenderer { rbuf.append("<a href=\"/dns\" target=\"_top\" title=\"") .append(_t("Manage your I2P hosts file here (I2P domain name resolution)")) .append("\">") - .append(nbsp(tx)) - .append("</a>\n"); + .append(nbsp(tx)); + addBubble(rbuf, PortMapper.SVC_SUSIDNS); + rbuf.append("</a>\n"); svcs.put(tx, rbuf.toString()); } @@ -360,8 +382,9 @@ class SummaryBarRenderer { rbuf.append("<a href=\"/i2ptunnelmgr\" target=\"_top\" title=\"") .append(_t("Local Tunnels")) .append("\">") - .append(nbsp(tx)) - .append("</a>\n"); + .append(nbsp(tx)); + addBubble(rbuf, PortMapper.SVC_I2PTUNNEL); + rbuf.append("</a>\n"); svcs.put(tx, rbuf.toString()); } @@ -434,8 +457,9 @@ class SummaryBarRenderer { rbuf.append("<a href=\"/logs\" target=\"_top\" title=\"") .append(_t("Health Report")) .append("\">") - .append(nbsp(tx)) - .append("</a>\n"); + .append(nbsp(tx)); + addBubble(rbuf, PortMapper.SVC_LOGS); + rbuf.append("</a>\n"); svcs.put(tx, rbuf.toString()); tx = _t("NetDB"); diff --git a/apps/routerconsole/jsp/themes/console/dark/console.css b/apps/routerconsole/jsp/themes/console/dark/console.css index 4e3da837032ecfb9a3c496888027a36e659e4aac..07e777c36f56d8ee0309b04f5799a4f4a0bd9dba 100644 --- a/apps/routerconsole/jsp/themes/console/dark/console.css +++ b/apps/routerconsole/jsp/themes/console/dark/console.css @@ -635,6 +635,25 @@ p:empty+.sb_notice { margin-top: 9px; } +.notifcount { + background-color: #e33; + border: 1px solid #e33; + border-radius: 9px; + color: #fff; + margin: 0 6px 0 3px; + padding: 0 4px; +} + +.notifbubble { + background-color: #e33; + border: 1px solid #e33; + border-radius: 9px; + color: #fff; + margin: -6px 0 0 -70px; + padding: 0 4px; + position: absolute; +} + .routersummary tr { background-image: none !important; background-color: transparent !important; diff --git a/apps/routerconsole/jsp/themes/console/light/console.css b/apps/routerconsole/jsp/themes/console/light/console.css index 523280ecbb541ab1632742ac93b8ce74c6840232..a4b87e5fbb3a24a25db1ee677dce42d2e7db45ce 100644 --- a/apps/routerconsole/jsp/themes/console/light/console.css +++ b/apps/routerconsole/jsp/themes/console/light/console.css @@ -779,6 +779,24 @@ p:empty+.sb_notice { background-color: #e9ecef; } +.notifcount { + background-color: #e33; + border: 1px solid #e33; + border-radius: 9px; + color: #fff; + margin: 0 6px 0 3px; + padding: 0 4px; +} + +.notifbubble { + background-color: #e33; + border: 1px solid #e33; + border-radius: 9px; + color: #fff; + margin: -6px 0 0 -70px; + padding: 0 4px; + position: absolute; +} /* end webapp navigation */ diff --git a/apps/susimail/src/src/i2p/susi/webmail/MailCache.java b/apps/susimail/src/src/i2p/susi/webmail/MailCache.java index 5bcf9c877a3dd0a405a89d8ebd007f561f9eed05..9f288a4750ab11ecb6ba07699c538e2700eb2d43 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/MailCache.java +++ b/apps/susimail/src/src/i2p/susi/webmail/MailCache.java @@ -52,6 +52,7 @@ import net.i2p.data.Base64; import net.i2p.util.FileUtil; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; +import net.i2p.util.PortMapper; /** * There's one of these for each Folder. @@ -540,16 +541,19 @@ class MailCache { } } if (newMail > 0) { - // DTG popup + String msg = ngettext("{0} new message", "{0} new messages", newMail); + if (newMail == 1 && additionalMsg != null) + msg += additionalMsg; ClientAppManager cmgr = _context.clientAppManager(); if (cmgr != null) { NotificationService ns = (NotificationService) cmgr.getRegisteredApp("desktopgui"); if (ns != null) { - String msg = ngettext("{0} new message", "{0} new messages", newMail); - if (newMail == 1 && additionalMsg != null) - msg += additionalMsg; + // DTG popup ns.notify("SusiMail", null, Log.INFO, _t("Email"), msg, "/susimail/"); } + // Console sidebar + int nc = cmgr.getBubbleCount(PortMapper.SVC_SUSIMAIL); + cmgr.setBubble(PortMapper.SVC_SUSIMAIL, nc + newMail, msg); } } } diff --git a/apps/susimail/src/src/i2p/susi/webmail/WebMail.java b/apps/susimail/src/src/i2p/susi/webmail/WebMail.java index 356d1fc31a8cbdadb7bcae47a9ffe4d0a5f817f3..bb0951bfd708b5a3cc7b3221d04ba53b4c1acf2d 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/WebMail.java +++ b/apps/susimail/src/src/i2p/susi/webmail/WebMail.java @@ -89,6 +89,7 @@ import javax.servlet.http.HttpSessionBindingListener; import net.i2p.CoreVersion; import net.i2p.I2PAppContext; +import net.i2p.app.ClientAppManager; import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.servlet.RequestWrapper; @@ -96,6 +97,7 @@ import net.i2p.servlet.util.ServletUtil; import net.i2p.servlet.util.WriterOutputStream; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; +import net.i2p.util.PortMapper; import net.i2p.util.RFC822Date; import net.i2p.util.SecureFileOutputStream; import net.i2p.util.Translate; @@ -4122,7 +4124,21 @@ public class WebMail extends HttpServlet "<tr><td colspan=\"2\" align=\"center\"><hr></td></tr>" + "</table></td></tr>\n" ); if( mail.hasPart()) { - mail.setNew(false); + if (mail.isNew()) { + mail.setNew(false); + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + ClientAppManager cmgr = ctx.clientAppManager(); + if (cmgr != null) { + int nc = cmgr.getBubbleCount(PortMapper.SVC_SUSIMAIL); + if (nc > 0) { + nc--; + String msg = ngettext("{0} new message", "{0} new messages", nc); + cmgr.setBubble(PortMapper.SVC_SUSIMAIL, --nc, msg); + } else { + cmgr.setBubble(PortMapper.SVC_SUSIMAIL, 0, null); + } + } + } showPart(out, mail.getPart(), 0, SHOW_HTML, allowHTML); } else { diff --git a/core/java/src/net/i2p/app/ClientAppManager.java b/core/java/src/net/i2p/app/ClientAppManager.java index 0830b7390cc16aed6f896b4191da6a1d04cad46a..626842e8b0105f5dc855705d15f81236e9508e51 100644 --- a/core/java/src/net/i2p/app/ClientAppManager.java +++ b/core/java/src/net/i2p/app/ClientAppManager.java @@ -45,4 +45,32 @@ public interface ClientAppManager { * @return client app or null */ public ClientApp getRegisteredApp(String name); + + /** + * Bubble count + * @since 0.9.66 + */ + public int getBubbleCount(String svc); + + /** + * Bubble message, translated, not HTML escaped + * @return null if none + * @since 0.9.66 + */ + public String getBubbleText(String svc); + + /** + * Update notifications for service + * @param count 0 to clear + * @param text translated, not HTML escaped, null if none + * @since 0.9.66 + */ + public void setBubble(String svc, int count, String text); + + /** + * Increment the count and set the text + * @param text translated, not HTML escaped, null if none + * @since 0.9.66 + */ + public void addBubble(String svc, String text); } diff --git a/core/java/src/net/i2p/app/ClientAppManagerImpl.java b/core/java/src/net/i2p/app/ClientAppManagerImpl.java index 1c7342c42aea65b2f91f2e92779bc24ec56b6b6e..565896393866a65b557e121bb68fc6c7ac4b48df 100644 --- a/core/java/src/net/i2p/app/ClientAppManagerImpl.java +++ b/core/java/src/net/i2p/app/ClientAppManagerImpl.java @@ -64,4 +64,32 @@ public class ClientAppManagerImpl implements ClientAppManager { public ClientApp getRegisteredApp(String name) { return _registered.get(name); } + + /** + * @return 0 always, see RouterAppManager override + * @since 0.9.66 + */ + public int getBubbleCount(String svc) { + return 0; + } + + /** + * @return null always, see RouterAppManager override + * @since 0.9.66 + */ + public String getBubbleText(String svc) { + return null; + } + + /** + * Does nothing, see RouterAppManager override + * @since 0.9.66 + */ + public void setBubble(String svc, int count, String text) {} + + /** + * Does nothing, see RouterAppManager override + * @since 0.9.66 + */ + public synchronized void addBubble(String svc, String text) {} } diff --git a/core/java/src/net/i2p/util/LogManager.java b/core/java/src/net/i2p/util/LogManager.java index 76493b65ba1766234ff36b778bd5527762824f6e..5eb0db888c04617e9200a8052042aa71ea9f6003 100644 --- a/core/java/src/net/i2p/util/LogManager.java +++ b/core/java/src/net/i2p/util/LogManager.java @@ -166,6 +166,9 @@ public class LogManager implements Flushable { if (context.isRouterContext()) { // FIXME don't start thread in constructor startLogWriter(); + // for bubbles, host/port may be wrong + PortMapper pm = context.portMapper(); + pm.register(PortMapper.SVC_LOGS, "127.0.0.1", 7657); } else { // Only in App Context. // In Router Context, the router has its own shutdown hook, diff --git a/core/java/src/net/i2p/util/LogWriter.java b/core/java/src/net/i2p/util/LogWriter.java index 450c51a34591696e52e1d1902a4816d5e485defd..d103d4e887369ed6f3e36cd61d98060c1f64d9bd 100644 --- a/core/java/src/net/i2p/util/LogWriter.java +++ b/core/java/src/net/i2p/util/LogWriter.java @@ -228,6 +228,10 @@ abstract class LogWriter implements Runnable { String tname = Translate.getString(name, _manager.getContext(), ROUTER_BUNDLE_NAME); ns.notify(name, null, priority, tname, msg, null); } + if (priority >= Log.CRIT) { + // Console sidebar + cmgr.addBubble(PortMapper.SVC_LOGS, msg); + } } } } diff --git a/core/java/src/net/i2p/util/PortMapper.java b/core/java/src/net/i2p/util/PortMapper.java index 3553c9b5ae166ab0c384d25210438541ca990fb7..0694aa0ff72924daf22b3331330257b1f32c433f 100644 --- a/core/java/src/net/i2p/util/PortMapper.java +++ b/core/java/src/net/i2p/util/PortMapper.java @@ -91,6 +91,11 @@ public class PortMapper { * @since 0.9.39 */ public static final String SVC_JSONRPC = "jsonrpc"; + /** + * For bubbles + * @since 0.9.66 + */ + public static final String SVC_LOGS = "logs"; /** @since 0.9.34 */ public static final int DEFAULT_CONSOLE_PORT = 7657; diff --git a/router/java/src/net/i2p/router/startup/RouterAppManager.java b/router/java/src/net/i2p/router/startup/RouterAppManager.java index ec3636f796ae5710afaae32e7e18c38b23c75b79..561a9f1ad1ed50145bbfe0d5b31d977b79c9de0e 100644 --- a/router/java/src/net/i2p/router/startup/RouterAppManager.java +++ b/router/java/src/net/i2p/router/startup/RouterAppManager.java @@ -16,6 +16,7 @@ import net.i2p.app.*; import static net.i2p.app.ClientAppState.*; import net.i2p.router.RouterContext; import net.i2p.util.Log; +import net.i2p.util.SystemVersion; /** * Notify the router of events, and provide methods for @@ -30,12 +31,14 @@ public class RouterAppManager extends ClientAppManagerImpl { // client to args // this assumes clients do not override equals() private final ConcurrentHashMap<ClientApp, String[]> _clients; + private final ConcurrentHashMap<String, Bubble> _bubbles; public RouterAppManager(RouterContext ctx) { super(ctx); _context = ctx; _log = ctx.logManager().getLog(RouterAppManager.class); _clients = new ConcurrentHashMap<ClientApp, String[]>(16); + _bubbles = new ConcurrentHashMap<String, Bubble>(4); ctx.addShutdownTask(new Shutdown()); } @@ -192,6 +195,74 @@ public class RouterAppManager extends ClientAppManagerImpl { _log.info("Client " + app.getDisplayName() + " UNREGISTERED AS " + app.getName()); } super.unregister(app); + _bubbles.remove(app.getName()); + } + + /** + * Bubbles + * @since 0.9.66 + */ + private static class Bubble { + public final int cnt; + public final String text; + + public Bubble(int count, String msg) { + cnt = count; + text = msg; + } + } + + /** + * Bubble count + * @since 0.9.66 + */ + @Override + public int getBubbleCount(String svc) { + Bubble n = _bubbles.get(svc); + if (n == null) + return 0; + return n.cnt; + } + + /** + * Bubble message, translated, not HTML escaped + * @return null if none + * @since 0.9.66 + */ + @Override + public String getBubbleText(String svc) { + Bubble n = _bubbles.get(svc); + if (n == null) + return null; + return n.text; + } + + /** + * Update notifications for service + * @param count 0 to clear + * @param text translated, not HTML escaped, null if none + * @since 0.9.66 + */ + @Override + public void setBubble(String svc, int count, String text) { + if (SystemVersion.isAndroid()) + return; + if (count == 0 && text == null) { + _bubbles.remove(svc); + } else { + Bubble n = new Bubble(count, text); + _bubbles.put(svc, n); + } + } + + /** + * Increment the count and set the text + * @param text translated, not HTML escaped, null if none + * @since 0.9.66 + */ + @Override + public synchronized void addBubble(String svc, String text) { + setBubble(svc, getBubbleCount(svc) + 1, text); } /// end ClientAppManager interface