10d24a04f2cc45af8484f29d35899bec2e53db46
jcasper
  Fri May 27 16:29:24 2016 -0700
Initial commit of public session listing CGI hgPublicSessions and supporting changes to hgSession. refs #15312

diff --git src/hg/hgSession/hgSession.c src/hg/hgSession/hgSession.c
index c25f4d8..604ae52 100644
--- src/hg/hgSession/hgSession.c
+++ src/hg/hgSession/hgSession.c
@@ -11,31 +11,32 @@
 #include "net.h"
 #include "textOut.h"
 #include "hCommon.h"
 #include "hui.h"
 #include "cart.h"
 #include "jsHelper.h"
 #include "web.h"
 #include "hdb.h"
 #include "ra.h"
 #include "wikiLink.h"
 #include "customTrack.h"
 #include "customFactory.h"
 #include "udc.h"
 #include "hgSession.h"
 #include "hubConnect.h"
-
+#include "hgConfig.h"
+#include "sessionThumbnail.h"
 
 void usage()
 /* Explain usage and exit. */
 {
 errAbort(
   "hgSession - Interface with wiki login and do session saving/loading.\n"
   "usage:\n"
   "    hgSession <various CGI settings>\n"
   );
 }
 
 /* Global variables. */
 struct cart *cart;
 char *excludeVars[] = {"Submit", "submit", NULL};
 struct slName *existingSessionNames = NULL;
@@ -49,30 +50,41 @@
 size_t len = strlen(encStr);
 char *decStr = needMem(len+1);
 cgiDecode(encStr, decStr, len);
 return decStr;
 }
 
 
 void welcomeUser(char *wikiUserName)
 /* Tell the user they are not logged in to the wiki or other login
  * system and tell them how to do so. */
 {
 char *wikiHost = wikiLinkHost();
 
 cartWebStart(cart, NULL, "Welcome %s", wikiUserName);
 jsInit();
+
+/* Includes for the jquery datatables plugin. Clashes a bit with the jquery included
+ * by cartWebStart, unfortunately.  Should resolve this ultimately (ideally by solving
+ * the issues preventing us from upgrading the global jquery include) */
+printf ("<link rel=\"stylesheet\" type=\"text/css\" "
+        "href=\"https://cdn.datatables.net/1.10.12/css/jquery.dataTables.min.css\">\n");
+printf ("<script type=\"text/javascript\" "
+        "src=\"https://code.jquery.com/jquery-1.12.3.min.js\"></script>\n");
+printf ("<script type=\"text/javascript\" charset=\"utf8\" "
+        "src=\"https://cdn.datatables.net/1.10.12/js/jquery.dataTables.min.js\"></script>\n");
+
 if (loginSystemEnabled()) /* Using the new hgLogin CGI for login? */
     {
     printf("<h4 style=\"margin: 0pt 0pt 7px;\">Your Account Information</h4>"
         "<ul style=\"list-style: none outside none; margin: 0pt; padding: 0pt;\">"
         "<li>Username:  %s</li>",wikiUserName);
     printf("<li><A HREF=\"%s\">Change password</A></li></ul>",
         wikiLinkChangePasswordUrl(cartSessionId(cart)));
     printf("<p><A HREF=\"%s\">Sign out</A></p>",
         wikiLinkUserLogoutUrl(cartSessionId(cart)));
     }
 else
     {
     printf("If you are not %s (on the wiki at "
         "<A HREF=\"http://%s/\" TARGET=_BLANK>%s</A>) "
         "and would like to sign out or change identity, \n",
@@ -254,83 +266,122 @@
 return val;
 }
 
 void showExistingSessions(char *userName)
 /* Print out a table with buttons for sharing/unsharing/loading/deleting
  * previously saved sessions. */
 {
 struct sqlConnection *conn = hConnectCentral();
 struct sqlResult *sr = NULL;
 char **row = NULL;
 char query[512];
 boolean foundAny = FALSE;
 char *encUserName = cgiEncodeFull(userName);
 boolean gotSettings = (sqlFieldIndex(conn, namedSessionTable, "settings") >= 0);
 
+printf ("<script type=\"text/javascript\">"
+        "$(document).ready(function () {\n"
+        "    $('#sessionTable').DataTable({\"columnDefs\": [{\"orderable\":false, \"targets\":[0,3,4,5,6,7,8]}],\n"
+        "                                       \"order\":[1,'asc']\n"
+        "                                 });\n"
+        "} );\n"
+        "</script>\n");
+
 printf("<H3>My Sessions</H3>\n");
-printf("<TABLE BORDERWIDTH=0>\n");
+printf("<div style=\"max-width:1024px\">");
+printf("<table id=\"sessionTable\" class=\"display compact\" borderwidth=0>\n");
+printf("<thead><tr>");
+printf("<TH><TD><B>session name (click to edit description)</B></TD><TD><B>created on</B></TD>"
+       "<TD align=center><B>use this&nbsp;<BR>session&nbsp;</B></TD>"
+       "<TD align=center><B>delete this&nbsp;<BR>session&nbsp;</B></TD><TD align=center><B>share with&nbsp;<BR>others?&nbsp;</B></TD>"
+       "<td align-center><b>post in&nbsp;<br><a href=\"../cgi-bin/hgPublicSessions?%s\">public listing</a>?</b></td>"
+       "<TD align=center><B>link to<BR>session</B></TD><TD align=center><B>send to<BR>mail</B></TD></TH>",
+       cartSidUrlString(cart));
+printf("</tr></thead>");
+printf("<tbody>\n");
+
+if (gotSettings)
+    sqlSafef(query, sizeof(query), "SELECT sessionName, shared, firstUse, settings from %s "
+        "WHERE userName = '%s' ORDER BY sessionName;",
+        namedSessionTable, encUserName);
+else
     sqlSafef(query, sizeof(query), "SELECT sessionName, shared, firstUse from %s "
         "WHERE userName = '%s' ORDER BY sessionName;",
         namedSessionTable, encUserName);
 sr = sqlGetResult(conn, query);
-printf("<TH><TD><B>session name</B></TD><TD><B>created on</B></TD><TD align=center><B>use this&nbsp;<BR>session&nbsp;</B></TD>"
-       "<TD align=center><B>delete this&nbsp;<BR>session&nbsp;</B></TD><TD align=center><B>share with&nbsp;<BR>others?&nbsp;</B></TD><TD align=center><B>link to<BR>session</B></TD>"
-       "<TD align=center><B>send to<BR>mail</B></TD></TH>");
+
 while ((row = sqlNextRow(sr)) != NULL)
     {
     char *encSessionName = row[0];
     char *sessionName = cgiDecodeClone(encSessionName);
     char *link = NULL;
-    boolean shared = atoi(row[1]);
+    int shared = atoi(row[1]);
     char *firstUse = row[2];
     char buf[512];
+    boolean inGallery = FALSE;
+
+    if (shared >=2)
+        inGallery = TRUE;
+
     printf("<TR><TD>&nbsp;&nbsp;</TD><TD>");
     if (gotSettings)
 	    printf("<A HREF=\"%s?%s&%s=%s\">",
 	       hgSessionName(), cartSidUrlString(cart), hgsDoSessionDetail, encSessionName);
     htmlTextOut(sessionName);
-    char *spacePt = strchr(firstUse, ' ');
-    if (spacePt != NULL) *spacePt = '\0';
     if (gotSettings)
 	    printf("</A>");
+
+    char *spacePt = strchr(firstUse, ' ');
+    if (spacePt != NULL) *spacePt = '\0';
         printf("&nbsp;&nbsp;</TD>"
-	   "<TD>%s&nbsp;&nbsp;</TD><TD align=center>", firstUse);
+	        "<TD><nobr>%s<nobr>&nbsp;&nbsp;</TD><TD align=center>", firstUse);
+
     safef(buf, sizeof(buf), "%s%s", hgsLoadPrefix, encSessionName);
     cgiMakeButton(buf, "use");
     printf("</TD><TD align=center>");
     safef(buf, sizeof(buf), "%s%s", hgsDeletePrefix, encSessionName);
     char command[512];
     safef(command, sizeof(command), confirmDeleteFormat, sessionName);
     cgiMakeOnClickSubmitButton(command, buf, "delete");
+
     printf("</TD><TD align=center>");
     safef(buf, sizeof(buf), "%s%s", hgsSharePrefix, encSessionName);
-    cgiMakeCheckBoxJS(buf, shared, "onchange=\"document.mainForm.submit();\"");
+    cgiMakeCheckBoxJS(buf, shared>0, "onchange=\"document.mainForm.submit();\"");
+
+    printf("</TD><TD align=center>");
+    safef(buf, sizeof(buf), "%s%s", hgsGalleryPrefix, encSessionName);
+    cgiMakeCheckBoxFourWay(buf, inGallery, shared>0, NULL, NULL,
+        "onchange=\"document.mainForm.submit();\"");
+        
     link = getSessionLink(encUserName, encSessionName);
     printf("</TD><TD align=center>%s</TD>\n", link);
     freez(&link);
     link = getSessionEmailLink(encUserName, encSessionName);
     printf("<TD align=center>%s</TD></TR>", link);
     freez(&link);
     foundAny = TRUE;
     struct slName *sn = slNameNew(sessionName);
     slAddHead(&existingSessionNames, sn);
     }
 if (!foundAny)
     printf("<TR><TD>&nbsp;&nbsp;&nbsp;</TD><TD>(none)</TD>"
 	   "<TD colspan=5></TD></TR>\n");
-printf("<TR><TD colspan=7></TD></TR>\n");
+
+printf("</tbody>\n");
+
 printf("</TABLE>\n");
+printf("</div>\n");
 printf("<P></P>\n");
 sqlFreeResult(&sr);
 hDisconnectCentral(&conn);
 }
 
 void showOtherUserOptions()
 /* Print out inputs for loading another user's saved session. */
 {
 printf("<TABLE BORDERWIDTH=0>\n");
 printf("<TR><TD colspan=2>"
        "Use settings from another user's saved session:</TD></TR>\n"
        "<TR><TD>&nbsp;&nbsp;&nbsp;</TD><TD>user: \n");
 cgiMakeOnKeypressTextVar(hgsOtherUserName,
 			 cartUsualString(cart, hgsOtherUserName, ""),
 			 20, "return noSubmitOnEnter(event);");
@@ -498,31 +549,38 @@
 webNewSection("Sharing Sessions");
 printf("There are several ways to share saved sessions with others.\n");
 printf("<UL>\n");
 if (userName != NULL)
     {
     printf("<LI>Each previously saved named session appears with "
 	   "Browser and Email links.  "
            "The Email link invokes your email tool with a message "
            "containing the Genome Browser link. The Email link can "
            "be bookmarked in your web browser and/or shared with "
            "others. If you right-click and copy the Browser link, "
            "it will be the same as the Email link. However, if you "
            "click the Browser link it will take you to the Genome "
            "Browser and become a uniquely identified URL once the "
            "session loads, so that resulting link is not advised "
-           "for sharing.</LI>\n");
+           "for sharing.</LI>\n"
+       "<li>Each previously saved named session also appears with "
+           "a checkbox to add the session to our "
+           "<a href=\"../cgi-bin/hgPublicSessions?%s\">Public Sessions</a> "
+           "listing. Adding a session to this listing allows other "
+           "browser users to view the description and a thumbnail "
+           "image of your session, and to load the session if they "
+           "are interested.</li>\n", cartSidUrlString(cart));
     }
 else if (loginSystemEnabled() || wikiLinkEnabled())
     {
      printf("<LI>If you <A HREF=\"%s\">sign in</A>, you will be able " 
             " to save named sessions which will be displayed with "
             " Browser and Email links.</LI>\n",
             wikiLinkUserLoginUrl(cartSessionId(cart)));
     }
 dyStringPrintf(dyUrl, "http%s://%s%s", cgiAppendSForHttps(), cgiServerNamePort(), cgiScriptName());
 
 printf("<LI>If you have saved your settings to a local file, you can send "
        "email to others with the file as an attachment and direct them to "
        "<A HREF=\"%s\">%s</A> .</LI>\n",
        dyUrl->string, dyUrl->string);
 dyStringPrintf(dyUrl, "?hgS_doLoadUrl=submit&hgS_loadUrlName=");
@@ -576,30 +634,33 @@
     else
 	showSessionControls(NULL, FALSE, FALSE);
     showLinkingTemplates(NULL);
     }
 cartWebEnd();
 }
 
 
 void cleanHgSessionFromCart(struct cart *cart)
 /* Remove hgSession action variables that should not stay in the cart. */
 {
 char varName[256];
 safef(varName, sizeof(varName), "%s%s", cgiBooleanShadowPrefix(), hgsSharePrefix);
 cartRemovePrefix(cart, varName);
 cartRemovePrefix(cart, hgsSharePrefix);
+safef(varName, sizeof(varName), "%s%s", cgiBooleanShadowPrefix(), hgsGalleryPrefix);
+cartRemovePrefix(cart, varName);
+cartRemovePrefix(cart, hgsGalleryPrefix);
 cartRemovePrefix(cart, hgsLoadPrefix);
 cartRemovePrefix(cart, hgsLoadLocalFileName);
 cartRemovePrefix(cart, hgsDeletePrefix);
 cartRemovePrefix(cart, hgsDo);
 cartRemove(cart, hgsOldSessionName);
 cartRemove(cart, hgsCancel);
 }
 
 static void outIfNotPresent(struct cart *cart, struct dyString *dy, char *track, int tdbVis)
 /* Output default trackDb visibility if it's not mentioned in the cart. */
 {
 char *cartVis = cartOptionalString(cart, track);
 if (cartVis == NULL)
     {
     if (dy)
@@ -723,81 +784,183 @@
 	  htmlEncode(sessionName), (shareSession ? "may" : "may not"),
 	  getSessionLink(encUserName, encSessionName),
 	  getSessionEmailLink(encUserName, encSessionName));
     cartCheckForCustomTracks(cart, dyMessage);
     }
 else
     dyStringPrintf(dyMessage,
 	  "Sorry, required table %s does not exist yet in the central "
 	  "database (%s).  Please ask a developer to create it using "
 	  "kent/src/hg/lib/namedSessionDb.sql .",
 	  namedSessionTable, sqlGetDatabase(conn));
 hDisconnectCentral(&conn);
 return dyStringCannibalize(&dyMessage);
 }
 
+void doGalleryAdd(char *encUserName, char *encSessionName, struct sqlConnection *conn)
+/* Create a thumbnail image for the gallery.  Leaks memory from a generated filename string,
+ * plus a couple of dyStrings.  Returns without determining if the image creation
+ * succeeded. */
+{
+char query[4096];
+char **row;
+struct sqlResult *sr;
+sqlSafef(query, sizeof(query),
+    "select m.idx, n.firstUse from gbMembers m join namedSessionDb n on m.userName = n.userName "
+    "where m.userName = \"%s\" and n.sessionName = \"%s\"",
+    encUserName, encSessionName);
+sr = sqlGetResult(conn, query);
+row = sqlNextRow(sr);
+if (row == NULL)
+    errAbort("cannot add session to gallery; user %s, session %s",
+        encUserName, encSessionName);
+
+char *destFile = sessionThumbnailFilePath(row[0], encSessionName, row[1]);
+if (destFile != NULL)
+    {
+    struct dyString *hgTracksUrl = dyStringNew(0);
+    addSessionLink(hgTracksUrl, encUserName, encSessionName, FALSE);
+    struct dyString *renderUrl =
+        dyStringSub(hgTracksUrl->string, "cgi-bin/hgTracks", "cgi-bin/hgRenderTracks");
+    dyStringAppend(renderUrl, "&pix=640");
+    char *renderCmd[] = {"wget", "-q", "-O", "-", renderUrl->string, NULL};
+    char *convertCmd[] = {"convert", "-", "-resize", "320", "-crop", "320x240+0+0", destFile, NULL};
+    char **cmdsImg[] = {renderCmd, convertCmd, NULL};
+    pipelineOpen(cmdsImg, pipelineWrite, "/dev/null", NULL);
+    }
+sqlFreeResult(&sr);
+}
+
+
+void doGalleryRemove(char *encUserName, char *encSessionName, struct sqlConnection *conn)
+/* Unlink thumbnail image for the gallery.  Leaks memory from a generated filename string. */
+{
+char query[4096];
+char **row;
+struct sqlResult *sr;
+sqlSafef(query, sizeof(query),
+    "select m.idx, n.firstUse from gbMembers m join namedSessionDb n on m.userName = n.userName "
+    "where m.userName = \"%s\" and n.sessionName = \"%s\"",
+    encUserName, encSessionName);
+sr = sqlGetResult(conn, query);
+row = sqlNextRow(sr);
+if (row == NULL)
+    errAbort("cannot remove session from gallery; user %s, session %s",
+        encUserName, encSessionName);
+
+char *filePath = sessionThumbnailFilePath(row[0], encSessionName, row[1]);
+if (filePath != NULL)
+    unlink(filePath);
+sqlFreeResult(&sr);
+}
 
 char *doUpdateSessions(char *userName)
 /* Look for cart variables matching prefixes for sharing/unsharing,
  * loading or deleting a previously saved session.
  * Return a message confirming what we did, or NULL if no such variables
  * were in the cart. */
 {
 if (userName == NULL)
     return NULL;
 struct dyString *dyMessage = dyStringNew(1024);
 struct hashEl *cartHelList = NULL, *hel = NULL;
 struct sqlConnection *conn = hConnectCentral();
 char *encUserName = cgiEncodeFull(userName);
 boolean didSomething = FALSE;
 char query[512];
 
+cartHelList = cartFindPrefix(cart, hgsGalleryPrefix);
+if (cartHelList != NULL)
+    {
+    struct hash *galleryHash = hashNew(0);
+    char **row;
+    struct sqlResult *sr;
+    sqlSafef(query, sizeof(query),
+	  "select sessionName,shared from %s where userName = '%s'",
+	  namedSessionTable, encUserName);
+    sr = sqlGetResult(conn, query);
+    while ((row = sqlNextRow(sr)) != NULL)
+	    hashAddInt(galleryHash, row[0], atoi(row[1]));
+    sqlFreeResult(&sr);
+    for (hel = cartHelList;  hel != NULL;  hel = hel->next)
+	{
+	char *encSessionName = hel->name + strlen(hgsGalleryPrefix);
+	char *sessionName = cgiDecodeClone(encSessionName);
+	boolean inGallery = hashIntVal(galleryHash, encSessionName) >= 2 ? TRUE : FALSE;
+	boolean newGallery  = cartUsualInt(cart, hel->name, 0) > 0 ? TRUE : FALSE;
+
+	if (newGallery != inGallery)
+	    {
+	    sqlSafef(query, sizeof(query), "UPDATE %s SET shared = %d "
+		  "WHERE userName = '%s' AND sessionName = '%s';",
+		  namedSessionTable, newGallery == TRUE ? 2 : 1, encUserName, encSessionName);
+	    sqlUpdate(conn, query);
+	    sessionTouchLastUse(conn, encUserName, encSessionName);
+	    dyStringPrintf(dyMessage,
+			   "Marked session <B>%s</B> as %s.<BR>\n",
+			   htmlEncode(sessionName),
+			   (newGallery == TRUE ? "added to gallery" : "removed from public listing"));
+        if (newGallery == FALSE)
+            doGalleryRemove(encUserName, encSessionName, conn);
+        if (newGallery == TRUE)
+            doGalleryAdd(encUserName, encSessionName, conn);
+	    didSomething = TRUE;
+	    }
+	}
+    hashFree(&galleryHash);
+    }
+
 cartHelList = cartFindPrefix(cart, hgsSharePrefix);
 if (cartHelList != NULL)
     {
     struct hash *sharedHash = hashNew(0);
     char **row;
     struct sqlResult *sr;
     sqlSafef(query, sizeof(query),
 	  "select sessionName,shared from %s where userName = '%s'",
 	  namedSessionTable, encUserName);
     sr = sqlGetResult(conn, query);
     while ((row = sqlNextRow(sr)) != NULL)
 	hashAddInt(sharedHash, row[0], atoi(row[1]));
     sqlFreeResult(&sr);
     for (hel = cartHelList;  hel != NULL;  hel = hel->next)
 	{
 	char *encSessionName = hel->name + strlen(hgsSharePrefix);
 	char *sessionName = cgiDecodeClone(encSessionName);
-	boolean alreadyShared = hashIntVal(sharedHash, encSessionName);
-	boolean shared  = cartUsualBoolean(cart, hel->name, TRUE);
-	if (shared != alreadyShared)
+	boolean alreadyShared = hashIntVal(sharedHash, encSessionName) > 0 ? TRUE : FALSE;
+    boolean inGallery = hashIntVal(sharedHash, encSessionName) >= 2 ? TRUE : FALSE;
+	boolean newShared = cartUsualInt(cart, hel->name, 1) ? TRUE : FALSE;
+
+	if (newShared != alreadyShared)
 	    {
 	    sqlSafef(query, sizeof(query), "UPDATE %s SET shared = %d "
 		  "WHERE userName = '%s' AND sessionName = '%s';",
-		  namedSessionTable, shared, encUserName, encSessionName);
+		  namedSessionTable, newShared, encUserName, encSessionName);
 	    sqlUpdate(conn, query);
 	    sessionTouchLastUse(conn, encUserName, encSessionName);
 	    dyStringPrintf(dyMessage,
 			   "Marked session <B>%s</B> as %s.<BR>\n",
 			   htmlEncode(sessionName),
-			   (shared ? "shared" : "unshared"));
+			   (newShared == TRUE ? "shared" : "unshared"));
+        if (newShared == FALSE && inGallery == TRUE)
+            doGalleryRemove(encUserName, encSessionName, conn);
 	    didSomething = TRUE;
 	    }
 	}
     hashFree(&sharedHash);
     }
+
 hel = cartFindPrefix(cart, hgsLoadPrefix);
 if (hel != NULL)
     {
     char *encSessionName = hel->name + strlen(hgsLoadPrefix);
     char *sessionName = cgiDecodeClone(encSessionName);
     char wildStr[256];
     safef(wildStr, sizeof(wildStr), "%s*", hgsLoadPrefix);
     dyStringPrintf(dyMessage,
 		   "Loaded settings from session <B>%s</B>. %s %s<BR>\n",
 		   htmlEncode(sessionName),
 		   getSessionLink(encUserName, encSessionName),
 		   getSessionEmailLink(encUserName, encSessionName));
     cartLoadUserSession(conn, userName, sessionName, cart, NULL, wildStr);
     cartHideDefaultTracks(cart);
     hubConnectLoadHubs(cart);
@@ -806,30 +969,31 @@
     didSomething = TRUE;
     }
 
 cartHelList = cartFindPrefix(cart, hgsDeletePrefix);
 for (hel = cartHelList;  hel != NULL;  hel = hel->next)
     {
     char *encSessionName = hel->name + strlen(hgsDeletePrefix);
     char *sessionName = cgiDecodeClone(encSessionName);
     sqlSafef(query, sizeof(query), "DELETE FROM %s "
 	  "WHERE userName = '%s' AND sessionName = '%s';",
 	  namedSessionTable, encUserName, encSessionName);
     sqlUpdate(conn, query);
     dyStringPrintf(dyMessage,
 		   "Deleted session <B>%s</B>.<BR>\n",
 		   htmlEncode(sessionName));
+    doGalleryRemove(encUserName, encSessionName, conn);
     didSomething = TRUE;
     }
 
 hDisconnectCentral(&conn);
 if (didSomething)
     return(dyStringCannibalize(&dyMessage));
 else
     {
     dyStringFree(&dyMessage);
     return NULL;
     }
 }
 
 char *doOtherUser(char *actionVar)
 /* Load settings from another user's named session.
@@ -974,70 +1138,90 @@
 char query[512];
 webPushErrHandlersCartDb(cart, cartUsualString(cart, "db", NULL));
 boolean gotSettings = (sqlFieldIndex(conn, namedSessionTable, "settings") >= 0);
 
 if (gotSettings)
     sqlSafef(query, sizeof(query), "SELECT shared, firstUse, settings from %s "
 	  "WHERE userName = '%s' AND sessionName = '%s'",
           namedSessionTable, encUserName, encSessionName);
 else
     sqlSafef(query, sizeof(query), "SELECT shared, firstUse from %s "
 	  "WHERE userName = '%s' AND sessionName = '%s'",
           namedSessionTable, encUserName, encSessionName);
 sr = sqlGetResult(conn, query);
 if ((row = sqlNextRow(sr)) != NULL)
     {
-    boolean shared = atoi(row[0]);
+    int shared = atoi(row[0]);
     char *firstUse = row[1];
     char *settings = NULL;
     if (gotSettings)
 	settings = row[2];
     char *description = getSetting(settings, "description");
     if (description == NULL) description = "";
 
     dyStringPrintf(dyMessage, "<A HREF=\"../goldenPath/help/hgSessionHelp.html#Details\" "
 		   "TARGET=_BLANK>Session Details Help</A><P/>\n");
 
-#define highlightAccChanges "{ var b = document.getElementById('" hgsDoSessionChange "'); " \
-                            "  if (b) { b.style.background = '#ff9999'; } }"
+#define highlightAccChanges " var b = document.getElementById('" hgsDoSessionChange "'); " \
+                            "  if (b) { b.style.background = '#ff9999'; }"
+
+#define toggleGalleryDisable \
+                            "  var c = document.getElementById('detailsSharedCheckbox'); " \
+                            "  var d = document.getElementById('detailsGalleryCheckbox'); " \
+                            "  if (c.checked)" \
+                            "    {d.disabled = false;} " \
+                            "  else" \
+                            "    {d.disabled = true; " \
+                            "     d.checked = false; }"
 
     dyStringPrintf(dyMessage, "<B>%s</B><P>\n"
 		   "<FORM ACTION=\"%s\" NAME=\"detailForm\" METHOD=GET>\n"
 		   "<INPUT TYPE=HIDDEN NAME=\"%s\" VALUE=%s>"
 		   "<INPUT TYPE=HIDDEN NAME=\"%s\" VALUE=\"%s\">"
 		   "Session Name: "
 		   "<INPUT TYPE=TEXT NAME=\"%s\" SIZE=%d VALUE=\"%s\" "
-		   "onChange=\"%s\" onKeypress=\"%s\">\n",
+		   "onChange=\"{%s}\" onKeypress=\"{%s}\">\n",
 		   sessionName, hgSessionName(),
 		   cartSessionVarName(cart), cartSessionId(cart), hgsOldSessionName, sessionName,
 		   hgsNewSessionName, 32, sessionName, highlightAccChanges, highlightAccChanges);
     dyStringPrintf(dyMessage,
 		   "&nbsp;&nbsp;<INPUT TYPE=SUBMIT NAME=\"%s%s\" VALUE=\"use\">"
 		   "&nbsp;&nbsp;<INPUT TYPE=SUBMIT NAME=\"%s%s\" VALUE=\"delete\" "
 		   "onClick=\"" confirmDeleteFormat "\">"
 		   "&nbsp;&nbsp;<INPUT TYPE=SUBMIT ID=\"%s\" NAME=\"%s\" VALUE=\"accept changes\">"
 		   "&nbsp;&nbsp;<INPUT TYPE=SUBMIT NAME=\"%s\" VALUE=\"cancel\"> "
 		   "<BR>\n",
 		   hgsLoadPrefix, encSessionName, hgsDeletePrefix, encSessionName,
 		   sessionName, hgsDoSessionChange, hgsDoSessionChange, hgsCancel);
     dyStringPrintf(dyMessage,
 		   "Share with others? <INPUT TYPE=CHECKBOX NAME=\"%s%s\"%s VALUE=on "
-		   "onChange=\"%s\" onClick=\"%s\">\n"
+		   "onChange=\"{%s %s}\" onClick=\"{%s %s}\" id=\"detailsSharedCheckbox\">\n"
 		   "<INPUT TYPE=HIDDEN NAME=\"%s%s%s\" VALUE=0><BR>\n",
-		   hgsSharePrefix, encSessionName, (shared ? " CHECKED" : ""),
-		   highlightAccChanges, highlightAccChanges,
+		   hgsSharePrefix, encSessionName, (shared>0 ? " CHECKED" : ""),
+		   highlightAccChanges, toggleGalleryDisable, highlightAccChanges, toggleGalleryDisable,
 		   cgiBooleanShadowPrefix(), hgsSharePrefix, encSessionName);
+
+    dyStringPrintf(dyMessage,
+		   "List in Public Sessions? <INPUT TYPE=CHECKBOX NAME=\"%s%s\"%s VALUE=on "
+		   "onChange=\"{%s}\" onClick=\"{%s}\" id=\"detailsGalleryCheckbox\">\n"
+		   "<INPUT TYPE=HIDDEN NAME=\"%s%s%s\" VALUE=0><BR>\n",
+		   hgsGalleryPrefix, encSessionName, (shared>=2 ? " CHECKED" : ""),
+		   highlightAccChanges, highlightAccChanges,
+		   cgiBooleanShadowPrefix(), hgsGalleryPrefix, encSessionName);
+
+    /* Set initial disabled state of the gallery checkbox */
+    dyStringPrintf(dyMessage, "\n<script>\n%s\n</script>\n", toggleGalleryDisable);
     dyStringPrintf(dyMessage,
 		   "Created on %s.<BR>\n", firstUse);
     if (gotSettings)
 	{
 	description = replaceChars(description, "\\\\", "\\__ESC__");
 	description = replaceChars(description, "\\r", "\r");
 	description = replaceChars(description, "\\n", "\n");
 	description = replaceChars(description, "\\__ESC__", "\\");
 	dyStringPrintf(dyMessage,
 		       "Description:<BR>\n"
 		       "<TEXTAREA NAME=\"%s\" ROWS=%d COLS=%d "
 		       "onChange=\"%s\" onKeypress=\"%s\">%s</TEXTAREA><BR>\n",
 		       hgsNewSessionDescription, 5, 80,
 		       highlightAccChanges, highlightAccChanges, description);
 	}
@@ -1066,31 +1250,31 @@
 char *doSessionChange(char *userName, char *oldSessionName)
 /* Process changes to session from session details page. */
 {
 if (userName == NULL)
     return "Unable to make changes to session.  Please log in again.";
 struct dyString *dyMessage = dyStringNew(1024);
 webPushErrHandlersCartDb(cart, cartUsualString(cart, "db", NULL));
 char *sessionName = oldSessionName;
 char *encSessionName = cgiEncodeFull(sessionName);
 char *encOldSessionName = encSessionName;
 char *encUserName = cgiEncodeFull(userName);
 struct sqlConnection *conn = hConnectCentral();
 struct sqlResult *sr = NULL;
 char **row = NULL;
 char query[512];
-boolean shared = TRUE;
+int shared = 1;
 char *settings = NULL;
 boolean gotSettings = (sqlFieldIndex(conn, namedSessionTable, "settings") >= 0);
 
 if (gotSettings)
     sqlSafef(query, sizeof(query), "SELECT shared, settings from %s "
 	  "WHERE userName = '%s' AND sessionName = '%s'",
           namedSessionTable, encUserName, encSessionName);
 else
     sqlSafef(query, sizeof(query), "SELECT shared from %s "
 	  "WHERE userName = '%s' AND sessionName = '%s'",
           namedSessionTable, encUserName, encSessionName);
 sr = sqlGetResult(conn, query);
 if ((row = sqlNextRow(sr)) != NULL)
     {
     shared = atoi(row[0]);
@@ -1103,49 +1287,67 @@
 
 char *newName = cartOptionalString(cart, hgsNewSessionName);
 if (isNotEmpty(newName) && !sameString(sessionName, newName))
     {
     char *encNewName = cgiEncodeFull(newName);
     sqlSafef(query, sizeof(query),
 	  "UPDATE %s set sessionName = '%s' WHERE userName = '%s' AND sessionName = '%s';",
 	  namedSessionTable, encNewName, encUserName, encSessionName);
 	sqlUpdate(conn, query);
     dyStringPrintf(dyMessage, "Changed session name from %s to <B>%s</B>.\n",
 		   sessionName, newName);
     sessionName = newName;
     encSessionName = encNewName;
     renamePrefixedCartVar(hgsLoadPrefix, encOldSessionName, encNewName);
     renamePrefixedCartVar(hgsDeletePrefix, encOldSessionName, encNewName);
+    if (shared >= 2)
+        {
+        doGalleryRemove(encUserName, encSessionName, conn);
+        doGalleryAdd(encUserName, encNewName, conn);
         }
-char varName[256];
-safef(varName, sizeof(varName), hgsSharePrefix "%s", encOldSessionName);
-if (cgiBooleanDefined(varName))
+    }
+
+char sharedVarName[256];
+char galleryVarName[256];
+safef(sharedVarName, sizeof(sharedVarName), hgsSharePrefix "%s", encOldSessionName);
+safef(galleryVarName, sizeof(galleryVarName), hgsGalleryPrefix "%s", encOldSessionName);
+if (cgiBooleanDefined(sharedVarName) || cgiBooleanDefined(galleryVarName))
     {
-    boolean newShared = cartBoolean(cart, varName);
+    int newShared = shared;
+    if (cgiBooleanDefined(sharedVarName))
+        newShared = cartBoolean(cart, sharedVarName) ? 1 : 0;
+    if (cgiBooleanDefined(galleryVarName))
+        newShared = cartBoolean(cart, galleryVarName) ? 2 : newShared;
     if (newShared != shared)
         {
         sqlSafef(query, sizeof(query),
             "UPDATE %s set shared = %d WHERE userName = '%s' AND sessionName = '%s';",
             namedSessionTable, newShared, encUserName, encSessionName);
         sqlUpdate(conn, query);
         dyStringPrintf(dyMessage, "Marked session <B>%s</B> as %s.<BR>\n",
-		       htmlEncode(sessionName), (newShared ? "shared" : "unshared"));
-
-	}
-    cartRemove(cart, varName);
+            htmlEncode(sessionName), (newShared>0 ? newShared>=2 ? "shared in public listing" : "shared, but not in public listing" : "unshared"));
+        if (shared >= 2 && newShared < 2)
+            doGalleryRemove(encUserName, encSessionName, conn);
+        if (shared < 2 && newShared >= 2)
+            doGalleryAdd(encUserName, encSessionName, conn);
+        }
+    cartRemove(cart, sharedVarName);
+    cartRemove(cart, galleryVarName);
     char shadowVarName[512];
-    safef(shadowVarName, sizeof(shadowVarName), "%s%s", cgiBooleanShadowPrefix(), varName);
+    safef(shadowVarName, sizeof(shadowVarName), "%s%s", cgiBooleanShadowPrefix(), sharedVarName);
+    cartRemove(cart, shadowVarName);
+    safef(shadowVarName, sizeof(shadowVarName), "%s%s", cgiBooleanShadowPrefix(), galleryVarName);
     cartRemove(cart, shadowVarName);
     }
 if (gotSettings)
     {
     struct hash *settingsHash = raFromString(settings);
     char *description = hashFindVal(settingsHash, "description");
     char *newDescription = cartOptionalString(cart, hgsNewSessionDescription);
     if (newDescription != NULL)
 	{
 	// newline escaping of \n is needed for ra syntax.
         // not sure why \r and \ are being escaped, but it may be too late to change
         // since there are probably records in the database that way now.
 	newDescription = replaceChars(newDescription, "\\", "\\\\");
 	newDescription = replaceChars(newDescription, "\r", "\\r");
 	newDescription = replaceChars(newDescription, "\n", "\\n");