From 1e471655dcc3e16edda3892b6c60ed9a7316a7e6 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Tue, 25 Feb 2025 15:06:57 +0000
Subject: [PATCH] susidns: Add bubble support

---
 .../java/src/net/i2p/addressbook/Daemon.java  | 14 ++++++++--
 apps/routerconsole/java/bundle-messages.sh    |  1 +
 .../src/java/src/i2p/susi/dns/BaseBean.java   | 22 +++++++++++++++
 .../src/i2p/susi/dns/NamingServiceBean.java   | 13 +++++++++
 apps/susidns/src/jsp/addressbook.jsp          | 24 ++++++++++++++--
 apps/susidns/src/jsp/index.jsp                | 12 +++++++-
 apps/susidns/src/themes/dark/susidns.css      | 28 +++++++++++++------
 apps/susidns/src/themes/light/susidns.css     | 26 +++++++++++------
 8 files changed, 117 insertions(+), 23 deletions(-)

diff --git a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java
index 60597a5f9d..1ec1d2f7de 100644
--- a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java
+++ b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java
@@ -42,6 +42,7 @@ import net.i2p.util.OrderedProperties;
 import net.i2p.util.PortMapper;
 import net.i2p.util.SecureDirectory;
 import net.i2p.util.SystemVersion;
+import net.i2p.util.Translate;
 
 /**
  * Main class of addressbook.  Performs updates, and runs the main loop.
@@ -674,12 +675,21 @@ class Daemon {
             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);
+                    int nc = cmgr.getBubbleCount(PortMapper.SVC_SUSIDNS) + nnew;
+                    String msg = ngettext("1 new host", "{0} new hosts", nc);
+                    cmgr.setBubble(PortMapper.SVC_SUSIDNS, nc, msg);
                 }
             }
     }
 
+    /**
+     *  translate (ngettext) from the routerconsole bundle
+     *  @since 0.9.66
+     */
+    private static String ngettext(String s, String p, int n) {
+        return Translate.getString(n, s, p, I2PAppContext.getGlobalContext(), "net.i2p.router.web.messages");
+    }
+
     /** @since 0.9.26 */
     private static void logInner(Log log, String action, String name, AddressBook addressbook) {
         if (log != null) {
diff --git a/apps/routerconsole/java/bundle-messages.sh b/apps/routerconsole/java/bundle-messages.sh
index 19bcba45c5..2383822e7d 100755
--- a/apps/routerconsole/java/bundle-messages.sh
+++ b/apps/routerconsole/java/bundle-messages.sh
@@ -51,6 +51,7 @@ ROUTERFILES="\
    ../../../router/java/src/net/i2p/router/transport/ntcp/EstablishState.java \
    ../../../router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java \
    ../../../router/java/src/net/i2p/router/transport/udp/UDPTransport.java \
+   ../../addressbook/java/src/net/i2p/addressbook/Daemon.java \
 "
 
 # add ../java/ so the refs will work in the po file
diff --git a/apps/susidns/src/java/src/i2p/susi/dns/BaseBean.java b/apps/susidns/src/java/src/i2p/susi/dns/BaseBean.java
index 27f53d32fe..b42b009a25 100644
--- a/apps/susidns/src/java/src/i2p/susi/dns/BaseBean.java
+++ b/apps/susidns/src/java/src/i2p/susi/dns/BaseBean.java
@@ -8,9 +8,11 @@ import java.util.List;
 import java.util.Properties;
 
 import net.i2p.I2PAppContext;
+import net.i2p.app.ClientAppManager;
 import net.i2p.data.DataHelper;
 import net.i2p.util.Log;
 import net.i2p.util.OrderedProperties;
+import net.i2p.util.PortMapper;
 
 /**
  * Holds methods common to several Beans.
@@ -179,6 +181,26 @@ public class BaseBean
         this.method = method;
     }
 
+    /**
+     *  @since 0.9.66
+     */
+    public int getBubbleCount() {
+        ClientAppManager cmgr = _context.clientAppManager();
+        if (cmgr != null)
+            return cmgr.getBubbleCount(PortMapper.SVC_SUSIDNS);
+        return 0;
+    }
+
+    /**
+     *  @since 0.9.66
+     */
+    public String getBubbleText() {
+        ClientAppManager cmgr = _context.clientAppManager();
+        if (cmgr != null)
+            return cmgr.getBubbleText(PortMapper.SVC_SUSIDNS);
+        return null;
+    }
+
     /**
      * Translate
      * @since 0.9.13 moved from subclasses
diff --git a/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java b/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java
index 34a43e3104..8488bdcc9c 100644
--- a/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java
+++ b/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java
@@ -39,12 +39,14 @@ import java.util.Map;
 import java.util.Properties;
 import java.util.SortedMap;
 
+import net.i2p.app.ClientAppManager;
 import net.i2p.client.naming.NamingService;
 import net.i2p.client.naming.SingleFileNamingService;
 import net.i2p.data.DataFormatException;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
 import net.i2p.servlet.RequestWrapper;
+import net.i2p.util.PortMapper;
 
 /**
  *  Talk to the NamingService API instead of modifying the hosts.txt files directly,
@@ -216,6 +218,8 @@ public class NamingServiceBean extends AddressbookBean
 			AddressBean array[] = list.toArray(new AddressBean[list.size()]);
 			if (sortByDate) {
 				Arrays.sort(array, new AddressByDateSorter());
+				if (getBook().equals("router"))
+					clearBubbles();
 			} else if (!(results instanceof SortedMap)) {
 				Arrays.sort(array, sorter);
 			}
@@ -528,6 +532,15 @@ public class NamingServiceBean extends AddressbookBean
 		return rv;
 	}
 
+	/**
+	 *  @since 0.9.66
+	 */
+	private void clearBubbles() {
+		ClientAppManager cmgr = _context.clientAppManager();
+		if (cmgr != null)
+			cmgr.setBubble(PortMapper.SVC_SUSIDNS, 0, null);
+	}
+
 	/**
 	 *  @since 0.9.20
 	 */
diff --git a/apps/susidns/src/jsp/addressbook.jsp b/apps/susidns/src/jsp/addressbook.jsp
index e2d1d52f86..17030ea419 100644
--- a/apps/susidns/src/jsp/addressbook.jsp
+++ b/apps/susidns/src/jsp/addressbook.jsp
@@ -69,7 +69,17 @@
 <a id="overview" href="index"><%=intl._t("Overview")%></a>&nbsp;
 <a class="abook private" href="addressbook?book=private&amp;filter=none"><%=intl._t("Private")%></a>&nbsp;
 <a class="abook local" href="addressbook?book=local&amp;filter=none"><%=intl._t("Local")%></a>&nbsp;
-<a class="abook router" href="addressbook?book=router&amp;filter=none"><%=intl._t("Router")%></a>&nbsp;
+<a class="abook router" href="addressbook?book=router&amp;filter=none"><%=intl._t("Router")%><%
+    int bubbleCount = book.getBubbleCount();
+    String bubbleText = book.getBubbleText();
+    if (bubbleCount > 0) {
+        %><span class="notifbubble" <%
+        if (bubbleText != null) {
+            %> title="<%=bubbleText%>"<%
+        }
+        %>><%=bubbleCount%></span><%
+    }
+%></a>&nbsp;
 <a class="abook published" href="addressbook?book=published&amp;filter=none"><%=intl._t("Published")%></a>&nbsp;
 <a id="subs" href="subscriptions"><%=intl._t("Subscriptions")%></a>&nbsp;
 <a id="config" href="config"><%=intl._t("Configuration")%></a>
@@ -151,7 +161,7 @@ ${book.loadBookMessages}
 <c:if test="${book.notEmpty}">
 <div id="filter">
 <c:if test="${book.hasFilter}">
-<span><%=intl._t("Current filter")%>:
+<span id="filterheader"><%=intl._t("Current filter")%>:
 <%
     String f = book.getFilter();
     if ("latest".equals(f))
@@ -195,7 +205,15 @@ ${book.loadBookMessages}
 <a href="addressbook?filter=0-9&amp;begin=0&amp;end=49">0-9</a>
 <a href="addressbook?filter=xn--&amp;begin=0&amp;end=49"><%=intl._t("other")%></a>
 <% if (!book.getBook().equals("published")) { %>
-   <a href="addressbook?filter=latest&amp;begin=0&amp;end=49"><%=intl._t("latest")%></a>
+   <a href="addressbook?filter=latest&amp;begin=0&amp;end=49"><%=intl._t("latest")%><%
+    if (bubbleCount > 0 && book.getBook().equals("router")) {
+        %><span class="notifbubble" <%
+        if (bubbleText != null) {
+            %> title="<%=bubbleText%>"<%
+        }
+        %>><%=bubbleCount%></span><%
+    }
+    %></a>
 <% } %>
 <a href="addressbook?filter=none&amp;begin=0&amp;end=49"><%=intl._t("all")%></a>
 </p>
diff --git a/apps/susidns/src/jsp/index.jsp b/apps/susidns/src/jsp/index.jsp
index c698518d4c..8e782200c1 100644
--- a/apps/susidns/src/jsp/index.jsp
+++ b/apps/susidns/src/jsp/index.jsp
@@ -56,7 +56,17 @@
 <a id="overview" class="active" href="index"><%=intl._t("Overview")%></a>&nbsp;
 <a class="abook" href="addressbook?book=private&amp;filter=none"><%=intl._t("Private")%></a>&nbsp;
 <a class="abook" href="addressbook?book=local&amp;filter=none"><%=intl._t("Local")%></a>&nbsp;
-<a class="abook" href="addressbook?book=router&amp;filter=none"><%=intl._t("Router")%></a>&nbsp;
+<a class="abook" href="addressbook?book=router&amp;filter=none"><%=intl._t("Router")%><%
+    int bubbleCount = base.getBubbleCount();
+    String bubbleText = base.getBubbleText();
+    if (bubbleCount > 0) {
+        %><span class="notifbubble" <%
+        if (bubbleText != null) {
+            %> title="<%=bubbleText%>"<%
+        }
+        %>><%=bubbleCount%></span><%
+    }
+%></a>&nbsp;
 <a class="abook" href="addressbook?book=published&amp;filter=none"><%=intl._t("Published")%></a>&nbsp;
 <a id="subs" href="subscriptions"><%=intl._t("Subscriptions")%></a>&nbsp;
 <a id="config" href="config"><%=intl._t("Configuration")%></a>
diff --git a/apps/susidns/src/themes/dark/susidns.css b/apps/susidns/src/themes/dark/susidns.css
index 107c0ba5af..38c2814098 100644
--- a/apps/susidns/src/themes/dark/susidns.css
+++ b/apps/susidns/src/themes/dark/susidns.css
@@ -59,8 +59,8 @@ body.iframed {
 /* topnav */
 
 #navi  {
-     margin: -16px auto 30px;
-     padding: 5px 3px;
+     margin: -10px auto 30px;
+     padding: 10px 3px;
      position: sticky;
      top: -1px;
      z-index: 999;
@@ -75,7 +75,7 @@ body.iframed {
 
 .iframed #navi  {
      margin: -6px -15px 30px;
-     padding: 5px 0;
+     padding: 13px 0;
      position: static;
      border-right: none;
      border-left: none;
@@ -183,6 +183,16 @@ body.iframed {
      background: #1F1A24 url(../images/overview.png) 5px center no-repeat !important;
 }
 
+.notifbubble {
+    background-color: #e33;
+    border: 1px solid #e33;
+    border-radius: 9px;
+    color: #fff;
+    margin: -16px 0 0 -4px;
+    padding: 0 4px;
+    position: absolute;
+}
+
 /* end topnav */
 
 hr {
@@ -273,7 +283,7 @@ div#filter + div#search > form {
      box-shadow: inset 3px 3px 3px #000;
 }
 
-#filter span {
+#filterheader {
      display: inline-block;
      border: 1px solid #292929;
      min-width: 300px;
@@ -286,18 +296,18 @@ div#filter + div#search > form {
      background-image: linear-gradient(to bottom, #1F1A24 0%, #222730 100%) !important;
 }
 
-#filter span a, #filter span a:hover {
+#filterheader a, #filterheader a:hover {
      border: none;
      background: none;
      margin: 0 0 0 10px;
      padding: 0;
 }
 
-#filter span a:active {
+#filterheader a:active {
      box-shadow: none;
 }
 
-#filter span b {
+#filterheader b {
      text-transform: uppercase;
      font-size: 10pt;
      margin: 0 0 0 5px;
@@ -1232,7 +1242,7 @@ body, input[type="submit"], input[type="reset"], .fakebutton, input, select, h4,
 }
 
 #navi {
-     padding: 6px 5px !important;
+     padding: 10px 5px !important;
 }
 
 #navi a {
@@ -1240,7 +1250,7 @@ body, input[type="submit"], input[type="reset"], .fakebutton, input, select, h4,
      padding: 4px 7px 5px 25px !important;
 }
 
-h3, #filter a, #filter span {
+h3, #filter a, #filterheader {
      font-size: 11pt !important;
 }
 
diff --git a/apps/susidns/src/themes/light/susidns.css b/apps/susidns/src/themes/light/susidns.css
index 6e41f128e8..3774b81b44 100644
--- a/apps/susidns/src/themes/light/susidns.css
+++ b/apps/susidns/src/themes/light/susidns.css
@@ -63,7 +63,7 @@ object {
 
 #navi {
      margin: -1px 0 0;
-     padding: 5px 3px;
+     padding: 10px 3px;
      text-align: center;
      border: 1px solid #dee2e6;
      border-radius: 2px 2px 0 0;
@@ -77,7 +77,7 @@ object {
 }
 .iframed #navi {
      margin: -11px -11px 10px;
-     padding: 7px 5px 6px;
+     padding: 13px 5px 6px;
      border-radius: 0;
      position: static
 }
@@ -116,6 +116,16 @@ object {
      transition: ease border 0.7s
 }
 
+.notifbubble {
+    background-color: #e33;
+    border: 1px solid #e33;
+    border-radius: 9px;
+    color: #fff;
+    margin: -16px 0 0 -4px;
+    padding: 0 4px;
+    position: absolute;
+}
+
 .invisible {
      visibility: hidden;
      display: none;
@@ -132,7 +142,7 @@ object {
      margin-top: 6px;
 }
 
-h3, h4, th, #filter span {
+h3, h4, th, #filterheader {
      color: #41465f;
 }
 
@@ -649,7 +659,7 @@ textarea[name="config"]:focus, textarea[name="content"]:focus {
      box-shadow: inset 0 0 0 1px #fff, inset 3px 3px 3px #33333f;
 }
 
-#filter span {
+#filterheader {
      display: inline-block;
      text-align: center;
      
@@ -665,14 +675,14 @@ textarea[name="config"]:focus, textarea[name="content"]:focus {
      box-shadow: inset 0 0 0 1px #fff;
 }
 
-#filter span a {
+#filterheader a {
      margin: -8px 1px;
      letter-spacing: normal;
      word-spacing: normal;
      padding: 2px 5px;
 }
 
-#filter span b {
+#filterheader b {
      font-size: 11pt;
      margin: 0 3px 0 0;
      
@@ -1373,7 +1383,7 @@ input[type="checkbox"][disabled]:checked, input[type="radio"][disabled]:checked,
 
 @media screen and (max-width: 680px) {
 #navi {
-     padding: 5px 3px !important;
+     padding: 10px 3px !important;
 }
 
 #navi a {
@@ -1420,7 +1430,7 @@ code, tt, .destaddress {
      margin-top: -40px !important;
 }
 
-#filter span, #filter a {
+#filterheader, #filter a {
      font-size: 11pt !important;
 }
 
-- 
GitLab