diff --git a/epicyon-profile.css b/epicyon-profile.css index 94545124d..caef0e4d0 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -1,7 +1,9 @@ @charset "UTF-8"; :root { + --header-bg-color: #282c37; --main-bg-color: #282c37; + --post-bg-color: #282c37; --column-left-color: #282c37; --link-bg-color: #282c37; --dropdown-fg-color: #dddddd; @@ -46,6 +48,8 @@ --time-vertical-align: 4px; --time-vertical-align-mobile: 25px; --publish-button-text: #FFFFFF; + --button-margin: 5px; + --button-left-margin: none; --button-text: #FFFFFF; --button-selected-text: #FFFFFF; --publish-button-background: #999; @@ -62,7 +66,7 @@ --button-height: 10px; --button-height-padding-mobile: 20px; --button-height-padding: 10px; - --image-corners: 10%, + --image-corners: 10%; --gallery-border: #ccc; --gallery-hover: #777; --gallery-text-color: #ccc; @@ -70,6 +74,7 @@ --gallery-font-size-mobile: 35px; --button-corner-radius: 15px; --timeline-border-radius: 30px; + --timeline-posts-background-color: #282c37; --icons-side: right; --title-color: #999; --focus-color: white; @@ -83,6 +88,8 @@ --column-left-width: 10vw; --column-center-width: 80vw; --column-right-width: 10vw; + --column-left-mobile-margin: 2%; + --column-left-top-margin: 0; --column-left-header-style: uppercase; --column-left-header-background: #555; --column-left-header-color: #fff; @@ -129,13 +136,16 @@ --post-separator-margin-top: 0; --post-separator-margin-bottom: 0; --post-separator-width: 95%; + --separator-width-left: 95%; + --separator-width-right: 95%; --post-separator-height: 1px; --header-vertical-offset: 0; --profile-background-height: 25vw; --profile-text-align: left; --verticals-width: 0; --italic-font-style: italic; - --header-font: 'Bedstead'; + --header-font: 'Arial, Helvetica, sans-serif'; + --button-bottom-margin: 10px; } @font-face { @@ -166,13 +176,14 @@ body, html { .leftColIcons { width: 100%; - background-color: var(--main-bg-color); + background-color: var(--column-left-color); float: right; display: block; padding-bottom: var(--column-left-icons-margin); } .postSeparatorImage img { + background-color: var(--post-bg-color); padding-top: var(--post-separator-margin-top); padding-bottom: var(--post-separator-margin-bottom); width: var(--post-separator-width); @@ -180,6 +191,24 @@ body, html { display: block; } +.postSeparatorImageLeft img { + background-color: var(--column-left-color); + padding-top: var(--post-separator-margin-top); + padding-bottom: var(--post-separator-margin-bottom); + width: var(--separator-width-left); + height: var(--post-separator-height); + display: block; +} + +.postSeparatorImageRight img { + background-color: var(--column-left-color); + padding-top: var(--post-separator-margin-top); + padding-bottom: var(--post-separator-margin-bottom); + width: var(--separator-width-right); + height: var(--post-separator-height); + display: block; +} + .headericons { display: inline-block; float: right; @@ -217,6 +246,10 @@ blockquote p { margin: -4px 5px; } +.imageAnchor { + font-family: var(--header-font); +} + .imageAnchor:focus img{ border: 2px solid var(--focus-color); } @@ -378,6 +411,10 @@ a:focus { width: 10%; } +.timeline-posts { + background-color: var(--timeline-posts-background-color); +} + .container img.timelineicon:hover { filter: brightness(var(--icon-brightness-change)); } @@ -530,6 +567,10 @@ a:focus { background: var(--link-bg-color); } +.announceOrReply { + font-family: var(--header-font); +} + .container img.announceOrReply { float: none; width: 30px; @@ -932,15 +973,16 @@ div.container { } .containerHeader { border: var(--border-width-header) solid var(--border-color); - background-color: var(--main-bg-color); + background-color: var(--header-bg-color); border-radius: var(--timeline-border-radius); padding: var(--header-button-padding); margin: var(--vertical-between-posts-header); transform: translateY(var(--header-vertical-offset)); + margin-bottom: var(--button-bottom-margin); } .container { border: var(--border-width) solid var(--border-color); - background-color: var(--main-bg-color); + background-color: var(--post-bg-color); border-radius: var(--timeline-border-radius); padding-left: var(--container-padding); padding-right: var(--container-padding); @@ -954,7 +996,6 @@ div.container { font-family: var(--header-font); font-size: var(--column-left-header-size); text-transform: var(--column-left-header-style); - padding: 4px; border: none; } .newswireItem { @@ -999,13 +1040,14 @@ div.container { display: none; } .timeline-banner { + vertical-align: top; object-fit: cover; - width: 98vw; + width: 100%; max-height: var(--banner-height); } .timeline { border: 0; - width: 98vw; + width: 100%; } .col-left a:link { background: var(--column-left-color); @@ -1018,6 +1060,7 @@ div.container { width: var(--column-left-width); } .col-left { + margin-top: var(--column-left-top-margin); border: var(--column-left-border-width) solid var(--column-left-border-color); color: var(--column-left-fg-color); font-size: var(--font-size-links); @@ -1252,7 +1295,7 @@ div.container { font-family: var(--header-font); padding: var(--button-height-padding); width: 10%; - margin: 5px; + margin: var(--button-margin); min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; @@ -1260,6 +1303,8 @@ div.container { border-bottom: none; border-left: var(--tab-border-width) solid var(--tab-border-color); border-right: var(--tab-border-width) solid var(--tab-border-color); + margin-bottom: var(--button-bottom-margin); + margin-left: var(--button-left-margin); } .buttonDesktop { border-radius: var(--button-corner-radius); @@ -1278,6 +1323,8 @@ div.container { border-bottom: none; border-left: var(--tab-border-width) solid var(--tab-border-color); border-right: var(--tab-border-width) solid var(--tab-border-color); + margin-bottom: var(--button-bottom-margin); + margin-left: var(--button-left-margin); } .publishbtn { border-radius: var(--button-corner-radius); @@ -1322,7 +1369,7 @@ div.container { font-family: var(--header-font); padding: var(--button-height-padding); width: 10%; - margin: 5px; + margin: var(--button-margin); min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; @@ -1330,6 +1377,8 @@ div.container { border-bottom: none; border-left: var(--tab-border-width) solid var(--tab-border-color); border-right: var(--tab-border-width) solid var(--tab-border-color); + margin-bottom: var(--button-bottom-margin); + margin-left: var(--button-left-margin); } .buttonselectedhighlighted { border-radius: var(--button-corner-radius); @@ -1381,6 +1430,7 @@ div.container { } .pageicon { width: 4%; + background-color: var(--post-bg-color); } .time-right { float: var(--icons-side); @@ -1564,15 +1614,16 @@ div.container { } .containerHeader { border: var(--border-width-header) solid var(--border-color); - background-color: var(--main-bg-color); + background-color: var(--header-bg-color); border-radius: var(--timeline-border-radius); padding: var(--header-button-padding); margin: var(--vertical-between-posts-header); transform: translateY(0%); + margin-bottom: var(--button-bottom-margin); } .container { border: var(--border-width) solid var(--border-color); - background-color: var(--main-bg-color); + background-color: var(--post-bg-color); border-radius: var(--timeline-border-radius); padding-left: var(--container-padding); padding-right: var(--container-padding); @@ -1586,7 +1637,6 @@ div.container { font-family: var(--header-font); font-size: var(--column-left-header-size-mobile); text-transform: var(--column-left-header-style); - padding: 4px; border: none; } .leftColEditImage { @@ -1657,6 +1707,7 @@ div.container { display: inline; } .timeline-banner { + vertical-align: top; object-fit: cover; width: 98vw; max-height: var(--banner-height-mobile); @@ -1671,6 +1722,9 @@ div.container { display: none; width: 0%; } + .col-left-mobile { + margin-left: var(--column-left-mobile-margin); + } .col-left { float: left; width: 0%; @@ -1679,6 +1733,10 @@ div.container { .col-center { width: 100vw; } + .col-right-mobile { + margin-left: var(--column-left-mobile-margin); + margin-right: var(--column-left-mobile-margin); + } .col-right { float: right; width: 0%; @@ -1848,11 +1906,12 @@ div.container { min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; - margin: 5px; + margin: var(--button-margin); border-top: var(--tab-border-width) solid var(--tab-border-color); border-bottom: none; border-left: var(--tab-border-width) solid var(--tab-border-color); border-right: var(--tab-border-width) solid var(--tab-border-color); + margin-bottom: var(--button-bottom-margin); } .frontPageMobileButtons{ display: block; @@ -1881,11 +1940,12 @@ div.container { min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; - margin: 5px; + margin: var(--button-margin); border-top: var(--tab-border-width) solid var(--tab-border-color); border-bottom: none; border-left: var(--tab-border-width) solid var(--tab-border-color); border-right: var(--tab-border-width) solid var(--tab-border-color); + margin-bottom: var(--button-bottom-margin); } .buttonDesktop { background: transparent; @@ -1910,7 +1970,7 @@ div.container { min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; - margin: 15px; + margin: 15px 0px; } .buttonhighlighted { border-radius: var(--button-corner-radius); @@ -1940,11 +2000,12 @@ div.container { min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; - margin: 5px; + margin: var(--button-margin); border-top: var(--tab-border-width) solid var(--tab-border-color); border-bottom: none; border-left: var(--tab-border-width) solid var(--tab-border-color); border-right: var(--tab-border-width) solid var(--tab-border-color); + margin-bottom: var(--button-bottom-margin); } .buttonselectedhighlighted { border-radius: var(--button-corner-radius); @@ -1996,6 +2057,7 @@ div.container { } .pageicon { width: 14%; + background-color: var(--post-bg-color); } .time-right { float: var(--icons-side); diff --git a/epicyon-search.css b/epicyon-search.css index 0b04e0458..16ae3fb4b 100644 --- a/epicyon-search.css +++ b/epicyon-search.css @@ -35,6 +35,8 @@ --focus-color: white; --search-banner-height: 30vh; --search-banner-height-mobile: 20vh; + --title-color: #999; + --header-font: 'Arial, Helvetica, sans-serif'; } @font-face { @@ -62,6 +64,11 @@ body, html { min-width: 600px; } +h1 { + font-family: var(--header-font); + color: var(--title-color); +} + a, u { color: var(--main-fg-color); } @@ -90,6 +97,13 @@ a:focus { border: 2px solid var(--focus-color); } +.domainHistogramLeft { + float: right; +} +.domainHistogramRight { + float: left; +} + .follow { background-image: url("follow-background.jpg"); background-size: cover; @@ -192,6 +206,10 @@ input[type=text] { } @media screen and (min-width: 400px) { + .domainHistogram { + border: 0; + font-size: var(--hashtag-size1); + } .timeline-banner { object-fit: cover; width: 98vw; @@ -244,6 +262,10 @@ input[type=text] { } @media screen and (max-width: 1000px) { + .domainHistogram { + border: 0; + font-size: var(--hashtag-size2); + } .timeline-banner { object-fit: cover; width: 98vw; diff --git a/pyjsonld.py b/pyjsonld.py index a6da7f28f..f0f55b41b 100644 --- a/pyjsonld.py +++ b/pyjsonld.py @@ -4474,6 +4474,8 @@ class JsonLdError(Exception): self.causeTrace = traceback.extract_tb(*sys.exc_info()[2:]) def __str__(self): + if not hasattr(self, 'message'): + return 'Unknown exception' rval = repr(self.message) rval += '\nType: ' + self.type if self.code: diff --git a/theme/blue/theme.json b/theme/blue/theme.json index e47b82426..8772270dc 100644 --- a/theme/blue/theme.json +++ b/theme/blue/theme.json @@ -17,6 +17,9 @@ "gallery-font-size": "35px", "gallery-font-size-mobile": "55px", "main-bg-color": "#002365", + "post-bg-color": "#002365", + "timeline-posts-background-color": "#002365", + "header-bg-color": "#002365", "column-left-color": "#002365", "text-entry-background": "#002365", "link-bg-color": "#002365", diff --git a/theme/hacker/theme.json b/theme/hacker/theme.json index 14adc6f5f..e7681d9b4 100644 --- a/theme/hacker/theme.json +++ b/theme/hacker/theme.json @@ -6,6 +6,9 @@ "publish-button-at-top": "False", "focus-color": "green", "main-bg-color": "black", + "post-bg-color": "black", + "timeline-posts-background-color": "black", + "header-bg-color": "black", "column-left-color": "black", "link-bg-color": "black", "main-bg-color-dm": "#0b0a0a", diff --git a/theme/henge/theme.json b/theme/henge/theme.json index 7d5bae9f6..9ea96f4a3 100644 --- a/theme/henge/theme.json +++ b/theme/henge/theme.json @@ -14,6 +14,9 @@ "font-size4": "24px", "font-size5": "22px", "main-bg-color": "#383335", + "post-bg-color": "#383335", + "timeline-posts-background-color": "#383335", + "header-bg-color": "#383335", "column-left-color": "#383335", "text-entry-background": "#383335", "link-bg-color": "#383335", diff --git a/theme/indymediaclassic/theme.json b/theme/indymediaclassic/theme.json index 04cf161fc..932129063 100644 --- a/theme/indymediaclassic/theme.json +++ b/theme/indymediaclassic/theme.json @@ -27,6 +27,9 @@ "font-size4": "24px", "font-size5": "22px", "main-bg-color": "black", + "post-bg-color": "black", + "timeline-posts-background-color": "black", + "header-bg-color": "black", "column-left-header-color": "#fff", "column-left-header-background": "#555", "column-left-header-size": "20px", diff --git a/theme/indymediamodern/icons/separator_right.png b/theme/indymediamodern/icons/separator_right.png new file mode 100644 index 000000000..f3b5f0799 Binary files /dev/null and b/theme/indymediamodern/icons/separator_right.png differ diff --git a/theme/indymediamodern/left_col_image.png b/theme/indymediamodern/left_col_image.png new file mode 100644 index 000000000..fdf06cfd5 Binary files /dev/null and b/theme/indymediamodern/left_col_image.png differ diff --git a/theme/indymediamodern/theme.json b/theme/indymediamodern/theme.json index 3c754d4af..478243e49 100644 --- a/theme/indymediamodern/theme.json +++ b/theme/indymediamodern/theme.json @@ -1,6 +1,10 @@ { - "verticals-width": "3px", + "button-bottom-margin": "0", + "header-vertical-offset": "10px", + "header-bg-color": "#efefef", + "verticals-width": "0px", "newswire-publish-icon": "False", + "line-spacing-newswire": "150%", "full-width-timeline-buttons": "False", "icons-as-buttons": "True", "rss-icon-at-top": "False", @@ -13,8 +17,8 @@ "font-size-calendar-cell": "2rem", "calendar-horizontal-padding": "20%", "time-vertical-align": "10px", - "header-vertical-offset": "-10%", - "publish-button-vertical-offset": "0", + "publish-button-vertical-offset": "15px", + "vertical-between-posts": "0", "vertical-between-posts-header": "0 0", "header-button-padding": "0 0", "containericons-horizontal-spacing": "0%", @@ -31,44 +35,49 @@ "font-size-dropdown-header": "30px", "post-separator-margin-top": "1%", "post-separator-margin-bottom": "1%", - "post-separator-width": "95%", + "post-separator-width": "96%", + "separator-width-left": "98%", + "separator-width-right": "99%", "post-separator-height": "1px", + "column-left-top-margin": "10px", "column-left-border-width": "0px", "column-right-border-width": "0px", + "column-left-mobile-margin": "2%", "column-left-border-color": "black", "column-left-header-color": "black", - "column-left-header-background": "white", + "column-left-header-background": "#efefef", "column-left-header-style": "none", "search-banner-height": "15vh", "search-banner-height-mobile": "10vh", "container-button-padding": "0px", "container-button-margin": "0px", "column-left-icon-size": "15%", - "column-right-icon-size": "15%", + "column-right-icon-size": "8%", + "button-margin": "2px", "button-height-padding": "5px", "icon-brightness-change": "70%", "border-width": "0px", "border-width-header": "0px", - "tab-border-width": "3px", + "tab-border-width": "0px", "tab-border-color": "grey", "button-corner-radius": "0px", "login-button-color": "#25408f", "login-button-fg-color": "white", "column-left-width": "10vw", - "column-center-width": "80vw", - "column-right-width": "10vw", + "column-center-width": "70vw", + "column-right-width": "20vw", "column-right-fg-color": "#25408f", "column-right-fg-color-voted-on": "red", "newswire-item-moderated-color": "red", "newswire-date-moderated-color": "red", "newswire-date-color": "grey", "timeline-border-radius": "0px", - "button-background": "#767674", - "button-background-hover": "#555", - "button-text-hover": "white", + "button-background": "#dedede", + "button-background-hover": "white", + "button-text-hover": "black", "button-selected": "white", "button-selected-text": "black", - "button-text": "white", + "button-text": "black", "hashtag-fg-color": "white", "publish-button-background": "#25408f", "publish-button-text": "white", @@ -77,10 +86,12 @@ "font-size-button-mobile": "26px", "font-size-publish-button": "14px", "rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)", - "column-left-color": "white", - "main-bg-color": "white", + "column-left-color": "#efefef", + "main-bg-color": "#efefef", + "post-bg-color": "white", + "timeline-posts-background-color": "white", "main-bg-color-dm": "white", - "link-bg-color": "white", + "link-bg-color": "transparent", "main-bg-color-reply": "white", "main-bg-color-report": "white", "main-header-color-roles": "#ebebf0", diff --git a/theme/lcd/theme.json b/theme/lcd/theme.json index d17864ec3..66da23085 100644 --- a/theme/lcd/theme.json +++ b/theme/lcd/theme.json @@ -8,6 +8,9 @@ "column-left-header-background": "#9fb42b", "column-left-header-color": "#33390d", "main-bg-color": "#9fb42b", + "post-bg-color": "#9fb42b", + "timeline-posts-background-color": "#9fb42b", + "header-bg-color": "#9fb42b", "column-left-color": "#33390d", "column-left-fg-color": "#9fb42b", "link-bg-color": "#33390d", diff --git a/theme/light/theme.json b/theme/light/theme.json index 76fe70ba2..829094cae 100644 --- a/theme/light/theme.json +++ b/theme/light/theme.json @@ -22,6 +22,9 @@ "rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)", "column-left-color": "#e6ebf0", "main-bg-color": "#e6ebf0", + "post-bg-color": "#e6ebf0", + "timeline-posts-background-color": "#e6ebf0", + "header-bg-color": "#e6ebf0", "main-bg-color-dm": "#e3dbf0", "link-bg-color": "#e6ebf0", "main-bg-color-reply": "#e0dbf0", diff --git a/theme/night/theme.json b/theme/night/theme.json index 32b61aa98..7e1ca228a 100644 --- a/theme/night/theme.json +++ b/theme/night/theme.json @@ -20,6 +20,9 @@ "font-size4": "24px", "font-size5": "22px", "main-bg-color": "#0f0d10", + "post-bg-color": "#0f0d10", + "timeline-posts-background-color": "#0f0d10", + "header-bg-color": "#0f0d10", "column-left-color": "#0f0d10", "text-entry-background": "#0f0d10", "link-bg-color": "#0f0d10", diff --git a/theme/purple/theme.json b/theme/purple/theme.json index 8e3c6456c..62368f6c1 100644 --- a/theme/purple/theme.json +++ b/theme/purple/theme.json @@ -13,6 +13,9 @@ "font-size4": "24px", "font-size5": "22px", "main-bg-color": "#1f152d", + "post-bg-color": "#1f152d", + "timeline-posts-background-color": "#1f152d", + "header-bg-color": "#1f152d", "column-left-color": "#1f152d", "link-bg-color": "#1f152d", "main-bg-color-reply": "#1a142d", diff --git a/theme/rc3/icons/favicon.ico b/theme/rc3/icons/favicon.ico index f9c76d599..3f32cf578 100644 Binary files a/theme/rc3/icons/favicon.ico and b/theme/rc3/icons/favicon.ico differ diff --git a/theme/rc3/icons/links.png b/theme/rc3/icons/links.png index a22fd6eec..ea303bbd4 100644 Binary files a/theme/rc3/icons/links.png and b/theme/rc3/icons/links.png differ diff --git a/theme/rc3/icons/newswire.png b/theme/rc3/icons/newswire.png index 23c3a5149..5a314630e 100644 Binary files a/theme/rc3/icons/newswire.png and b/theme/rc3/icons/newswire.png differ diff --git a/theme/rc3/icons/pagedown.png b/theme/rc3/icons/pagedown.png index 79363393c..e11ca1a0c 100644 Binary files a/theme/rc3/icons/pagedown.png and b/theme/rc3/icons/pagedown.png differ diff --git a/theme/rc3/icons/pageup.png b/theme/rc3/icons/pageup.png index 0b7c335a2..43e2e37b5 100644 Binary files a/theme/rc3/icons/pageup.png and b/theme/rc3/icons/pageup.png differ diff --git a/theme/rc3/icons/person.png b/theme/rc3/icons/person.png index 87b3357c2..5e5d3edd1 100644 Binary files a/theme/rc3/icons/person.png and b/theme/rc3/icons/person.png differ diff --git a/theme/rc3/icons/prev.png b/theme/rc3/icons/prev.png index 66d80630f..0d8a2cb5f 100644 Binary files a/theme/rc3/icons/prev.png and b/theme/rc3/icons/prev.png differ diff --git a/theme/rc3/icons/scope_public.png b/theme/rc3/icons/scope_public.png index 7cb6928d0..5f17a6cf5 100644 Binary files a/theme/rc3/icons/scope_public.png and b/theme/rc3/icons/scope_public.png differ diff --git a/theme/rc3/icons/search.png b/theme/rc3/icons/search.png index 05b14c073..9e1458fe7 100644 Binary files a/theme/rc3/icons/search.png and b/theme/rc3/icons/search.png differ diff --git a/theme/rc3/icons/vote.png b/theme/rc3/icons/vote.png index c810092b0..9080991b6 100644 Binary files a/theme/rc3/icons/vote.png and b/theme/rc3/icons/vote.png differ diff --git a/theme/rc3/theme.json b/theme/rc3/theme.json index 08edd84e0..a62a09517 100644 --- a/theme/rc3/theme.json +++ b/theme/rc3/theme.json @@ -41,6 +41,9 @@ "font-size5": "12px", "font-size-likes": "10px", "main-bg-color": "#100e23", + "post-bg-color": "#100e23", + "timeline-posts-background-color": "#100e23", + "header-bg-color": "#100e23", "column-left-color": "#0f0d10", "text-entry-background": "#0f0d10", "link-bg-color": "#0f0d10", diff --git a/theme/solidaric/theme.json b/theme/solidaric/theme.json index 8e59a2b3c..6ad6b0011 100644 --- a/theme/solidaric/theme.json +++ b/theme/solidaric/theme.json @@ -29,9 +29,12 @@ "font-size5": "22px", "rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)", "main-bg-color": "white", + "post-bg-color": "white", + "timeline-posts-background-color": "white", + "header-bg-color": "#ddd", "column-left-color": "white", "main-bg-color-dm": "white", - "link-bg-color": "white", + "link-bg-color": "transparent", "main-bg-color-reply": "white", "main-bg-color-report": "white", "main-header-color-roles": "#ebebf0", diff --git a/theme/starlight/theme.json b/theme/starlight/theme.json index 33eb6a095..71b1c1795 100644 --- a/theme/starlight/theme.json +++ b/theme/starlight/theme.json @@ -16,6 +16,9 @@ "font-size4": "24px", "font-size5": "22px", "main-bg-color": "#0f0d10", + "post-bg-color": "#0f0d10", + "timeline-posts-background-color": "#0f0d10", + "header-bg-color": "#0f0d10", "column-left-color": "#0f0d10", "text-entry-background": "#0f0d10", "link-bg-color": "#0f0d10", diff --git a/theme/zen/theme.json b/theme/zen/theme.json index c33845735..50524edd3 100644 --- a/theme/zen/theme.json +++ b/theme/zen/theme.json @@ -8,6 +8,9 @@ "banner-height-mobile": "10vh", "newswire-date-color": "yellow", "main-bg-color": "#5c4e41", + "post-bg-color": "#5c4e41", + "timeline-posts-background-color": "#5c4e41", + "header-bg-color": "#5c4e41", "column-left-color": "#5c4e41", "text-entry-background": "#5c4e41", "link-bg-color": "#5c4e41", diff --git a/translations/ar.json b/translations/ar.json index 3d19e6d50..8f8b54018 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -329,5 +329,6 @@ "Choose newswire items referenced in your article": "اختر العناصر الإخبارية المشار إليها في مقالتك", "RSS feed for your blog": "تغذية RSS لمدونتك", "Create a new shared item": "إنشاء عنصر مشترك جديد", - "Rc3": "Rc3" + "Rc3": "Rc3", + "Hashtag origins": "أصول الهاشتاق" } diff --git a/translations/ca.json b/translations/ca.json index 741887da8..0d4859f51 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -329,5 +329,6 @@ "Choose newswire items referenced in your article": "Trieu articles de newswire als quals faci referència el vostre article", "RSS feed for your blog": "Feed RSS del vostre bloc", "Create a new shared item": "Creeu un element compartit nou", - "Rc3": "Rc3" + "Rc3": "Rc3", + "Hashtag origins": "Orígens de hashtag" } diff --git a/translations/cy.json b/translations/cy.json index 8f16d5969..0ff025d0d 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -329,5 +329,6 @@ "Choose newswire items referenced in your article": "Dewiswch eitemau newyddion y cyfeirir atynt yn eich erthygl", "RSS feed for your blog": "Porthiant RSS ar gyfer eich blog", "Create a new shared item": "Creu eitem newydd a rennir", - "Rc3": "Rc3" + "Rc3": "Rc3", + "Hashtag origins": "Gwreiddiau Hashtag" } diff --git a/translations/de.json b/translations/de.json index e0bb78ea0..d8072410f 100644 --- a/translations/de.json +++ b/translations/de.json @@ -329,5 +329,6 @@ "Choose newswire items referenced in your article": "Wählen Sie Newswire-Artikel aus, auf die in Ihrem Artikel verwiesen wird", "RSS feed for your blog": "RSS-Feed für Ihr Blog", "Create a new shared item": "Erstellen Sie ein neues freigegebenes Element", - "Rc3": "Rc3" + "Rc3": "Rc3", + "Hashtag origins": "Hashtag-Ursprünge" } diff --git a/translations/en.json b/translations/en.json index 74ffd65d0..aac880269 100644 --- a/translations/en.json +++ b/translations/en.json @@ -329,5 +329,6 @@ "Choose newswire items referenced in your article": "Choose newswire items referenced in your article", "RSS feed for your blog": "RSS feed for your blog", "Create a new shared item": "Create a new shared item", - "Rc3": "Rc3" + "Rc3": "Rc3", + "Hashtag origins": "Hashtag origins" } diff --git a/translations/es.json b/translations/es.json index 427ce582e..9bedd3634 100644 --- a/translations/es.json +++ b/translations/es.json @@ -329,5 +329,6 @@ "Choose newswire items referenced in your article": "Elija elementos de Newswire a los que se hace referencia en su artículo", "RSS feed for your blog": "Fuente RSS para tu blog", "Create a new shared item": "Crea un nuevo elemento compartido", - "Rc3": "Rc3" + "Rc3": "Rc3", + "Hashtag origins": "Orígenes del hashtag" } diff --git a/translations/fr.json b/translations/fr.json index d9e10c0af..f6751c253 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -329,5 +329,6 @@ "Choose newswire items referenced in your article": "Choisissez les éléments de fil d'actualité référencés dans votre article", "RSS feed for your blog": "Flux RSS pour votre blog", "Create a new shared item": "Créer un nouvel élément partagé", - "Rc3": "Rc3" + "Rc3": "Rc3", + "Hashtag origins": "Origines des hashtags" } diff --git a/translations/ga.json b/translations/ga.json index cbfe07da5..96610433c 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -329,5 +329,6 @@ "Choose newswire items referenced in your article": "Roghnaigh míreanna sreanga nuachta dá dtagraítear i d’alt", "RSS feed for your blog": "Fotha RSS do do bhlag", "Create a new shared item": "Cruthaigh mír nua roinnte", - "Rc3": "Rc3" + "Rc3": "Rc3", + "Hashtag origins": "Bunús Hashtag" } diff --git a/translations/hi.json b/translations/hi.json index 8875da63c..fff80d94d 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -329,5 +329,6 @@ "Choose newswire items referenced in your article": "अपने लेख में संदर्भित newswire आइटम चुनें", "RSS feed for your blog": "RSS आपके ब्लॉग के लिए फ़ीड करता है", "Create a new shared item": "एक नया साझा आइटम बनाएं", - "Rc3": "Rc3" + "Rc3": "Rc3", + "Hashtag origins": "हैशटैग की उत्पत्ति" } diff --git a/translations/it.json b/translations/it.json index e72c14aa7..799fca7b3 100644 --- a/translations/it.json +++ b/translations/it.json @@ -329,5 +329,6 @@ "Choose newswire items referenced in your article": "Scegli gli articoli del newswire a cui fa riferimento il tuo articolo", "RSS feed for your blog": "Feed RSS per il tuo blog", "Create a new shared item": "Crea un nuovo elemento condiviso", - "Rc3": "Rc3" + "Rc3": "Rc3", + "Hashtag origins": "Origini hashtag" } diff --git a/translations/ja.json b/translations/ja.json index 1d0881313..f26fe9c0d 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -329,5 +329,6 @@ "Choose newswire items referenced in your article": "あなたの記事で参照されているニュースワイヤーアイテムを選択してください", "RSS feed for your blog": "ブログのRSSフィード", "Create a new shared item": "新しい共有アイテムを作成する", - "Rc3": "Rc3" + "Rc3": "Rc3", + "Hashtag origins": "ハッシュタグの起源" } diff --git a/translations/oc.json b/translations/oc.json index 3e954109b..6e1ae44d5 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -325,5 +325,6 @@ "Choose newswire items referenced in your article": "Choose newswire items referenced in your article", "RSS feed for your blog": "RSS feed for your blog", "Create a new shared item": "Create a new shared item", - "Rc3": "Rc3" + "Rc3": "Rc3", + "Hashtag origins": "Hashtag origins" } diff --git a/translations/pt.json b/translations/pt.json index fc9c721a1..6f8a999ac 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -329,5 +329,6 @@ "Choose newswire items referenced in your article": "Escolha os itens de notícias mencionados em seu artigo", "RSS feed for your blog": "Feed RSS para o seu blog", "Create a new shared item": "Crie um novo item compartilhado", - "Rc3": "Rc3" + "Rc3": "Rc3", + "Hashtag origins": "Origens de hashtag" } diff --git a/translations/ru.json b/translations/ru.json index e8a39c26b..f736bb5ef 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -329,5 +329,6 @@ "Choose newswire items referenced in your article": "Выберите элементы ленты новостей, на которые есть ссылки в вашей статье", "RSS feed for your blog": "RSS-канал для вашего блога", "Create a new shared item": "Создать новый общий элемент", - "Rc3": "Rc3" + "Rc3": "Rc3", + "Hashtag origins": "Происхождение хэштегов" } diff --git a/translations/zh.json b/translations/zh.json index d0e26f6fc..b49452236 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -329,5 +329,6 @@ "Choose newswire items referenced in your article": "选择文章中引用的新闻专栏文章", "RSS feed for your blog": "您博客的RSS供稿", "Create a new shared item": "创建一个新的共享项目", - "Rc3": "Rc3" + "Rc3": "Rc3", + "Hashtag origins": "标签起源" } diff --git a/webapp_column_left.py b/webapp_column_left.py index 66e1f3d38..b72c282da 100644 --- a/webapp_column_left.py +++ b/webapp_column_left.py @@ -227,6 +227,7 @@ def htmlLinksMobile(cssCache: {}, baseDir: str, '\n' + htmlStr += '
\n' htmlStr += '
' + \ headerButtonsFrontScreen(translate, nickname, 'links', authorized, @@ -237,6 +238,10 @@ def htmlLinksMobile(cssCache: {}, baseDir: str, iconsPath, editor, False, timelinePath, rssIconAtTop, False, False) + + # end of col-left-mobile + htmlStr += '
\n' + htmlStr += '\n' + htmlFooter() return htmlStr diff --git a/webapp_column_right.py b/webapp_column_right.py index 1095b3b78..dff002dd4 100644 --- a/webapp_column_right.py +++ b/webapp_column_right.py @@ -10,6 +10,7 @@ import os from datetime import datetime from shutil import copyfile from content import removeLongWords +from utils import removeHtml from utils import locatePost from utils import loadJson from utils import getConfigParam @@ -205,6 +206,14 @@ def htmlNewswire(baseDir: str, newswire: {}, nickname: str, moderator: bool, separatorStr = htmlPostSeparator(baseDir, 'right') htmlStr = '' for dateStr, item in newswire.items(): + item[0] = removeHtml(item[0]).strip() + if not item[0]: + continue + # remove any CDATA + if 'CDATA[' in item[0]: + item[0] = item[0].split('CDATA[')[1] + if ']' in item[0]: + item[0] = item[0].split(']')[0] publishedDate = \ datetime.strptime(dateStr, "%Y-%m-%d %H:%M:%S%z") dateShown = publishedDate.strftime("%Y-%m-%d %H:%M") @@ -346,6 +355,14 @@ def htmlCitations(baseDir: str, nickname: str, domain: str, if newswire: ctr = 0 for dateStr, item in newswire.items(): + item[0] = removeHtml(item[0]).strip() + if not item[0]: + continue + # remove any CDATA + if 'CDATA[' in item[0]: + item[0] = item[0].split('CDATA[')[1] + if ']' in item[0]: + item[0] = item[0].split(']')[0] # should this checkbox be selected? selectedStr = '' if dateStr in citationsSelected: @@ -416,6 +433,8 @@ def htmlNewswireMobile(cssCache: {}, baseDir: str, nickname: str, '\n' + htmlStr += '
\n' + htmlStr += '
' + \ headerButtonsFrontScreen(translate, nickname, 'newswire', authorized, @@ -428,6 +447,9 @@ def htmlNewswireMobile(cssCache: {}, baseDir: str, nickname: str, False, timelinePath, showPublishButton, showPublishAsIcon, rssIconAtTop, False, authorized, False) + # end of col-right-mobile + htmlStr += '' + htmlStr += htmlFooter() return htmlStr diff --git a/webapp_hashtagswarm.py b/webapp_hashtagswarm.py new file mode 100644 index 000000000..6cf95974f --- /dev/null +++ b/webapp_hashtagswarm.py @@ -0,0 +1,143 @@ +__filename__ = "webapp_hashtagswarm.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + +import os +from blocking import isBlockedHashtag +from datetime import datetime + + +def getHashtagDomainMax(domainHistogram: {}) -> str: + """Returns the domain with the maximum number of hashtags + """ + maxCount = 1 + maxDomain = None + for domain, count in domainHistogram.items(): + if count > maxCount: + maxDomain = domain + maxCount = count + return maxDomain + + +def getHashtagDomainHistogram(domainHistogram: {}, translate: {}) -> str: + """Returns the html for a histogram of domains + from which hashtags are coming + """ + totalCount = 0 + for domain, count in domainHistogram.items(): + totalCount += count + if totalCount == 0: + return '' + + htmlStr = '' + histogramHeaderStr = '

\n' + histogramHeaderStr += '

' + translate['Hashtag origins'] + '

\n' + histogramHeaderStr += ' \n' + histogramHeaderStr += ' \n' + histogramHeaderStr += ' \n' + histogramHeaderStr += ' \n' + histogramHeaderStr += ' \n' + histogramHeaderStr += ' \n' + histogramHeaderStr += ' \n' + + leftColStr = '' + rightColStr = '' + + for i in range(len(domainHistogram)): + domain = getHashtagDomainMax(domainHistogram) + if not domain: + break + percent = int(domainHistogram[domain] * 100 / totalCount) + if histogramHeaderStr: + htmlStr += histogramHeaderStr + histogramHeaderStr = None + leftColStr += str(percent) + '%
' + rightColStr += domain + '
' + del domainHistogram[domain] + + if htmlStr: + htmlStr += ' \n' + htmlStr += ' \n' + htmlStr += ' \n' + htmlStr += ' \n' + htmlStr += '
' + leftColStr + '' + rightColStr + '
\n' + htmlStr += '
\n' + + return htmlStr + + +def htmlHashTagSwarm(baseDir: str, actor: str, translate: {}) -> str: + """Returns a tag swarm of today's hashtags + """ + currTime = datetime.utcnow() + daysSinceEpoch = (currTime - datetime(1970, 1, 1)).days + daysSinceEpochStr = str(daysSinceEpoch) + ' ' + tagSwarm = [] + domainHistogram = {} + + for subdir, dirs, files in os.walk(baseDir + '/tags'): + for f in files: + tagsFilename = os.path.join(baseDir + '/tags', f) + if not os.path.isfile(tagsFilename): + continue + # get last modified datetime + modTimesinceEpoc = os.path.getmtime(tagsFilename) + lastModifiedDate = datetime.fromtimestamp(modTimesinceEpoc) + fileDaysSinceEpoch = (lastModifiedDate - datetime(1970, 1, 1)).days + # check if the file was last modified today + if fileDaysSinceEpoch != daysSinceEpoch: + continue + + hashTagName = f.split('.')[0] + if isBlockedHashtag(baseDir, hashTagName): + continue + if daysSinceEpochStr not in open(tagsFilename).read(): + continue + with open(tagsFilename, 'r') as tagsFile: + while True: + line = tagsFile.readline() + if not line: + break + elif ' ' not in line: + break + sections = line.split(' ') + if len(sections) != 3: + break + postDaysSinceEpochStr = sections[0] + if not postDaysSinceEpochStr.isdigit(): + break + postDaysSinceEpoch = int(postDaysSinceEpochStr) + if postDaysSinceEpoch < daysSinceEpoch - 1: + break + else: + postUrl = sections[2] + if '##' not in postUrl: + break + postDomain = postUrl.split('##')[1] + if '#' in postDomain: + postDomain = postDomain.split('#')[0] + if domainHistogram.get(postDomain): + domainHistogram[postDomain] = \ + domainHistogram[postDomain] + 1 + else: + domainHistogram[postDomain] = 1 + tagSwarm.append(hashTagName) + break + + if not tagSwarm: + return '' + tagSwarm.sort() + tagSwarmStr = '' + ctr = 0 + for tagName in tagSwarm: + tagSwarmStr += \ + '' + tagName + '\n' + ctr += 1 + tagSwarmHtml = tagSwarmStr.strip() + '\n' + tagSwarmHtml += getHashtagDomainHistogram(domainHistogram, translate) + return tagSwarmHtml diff --git a/webapp_headerbuttons.py b/webapp_headerbuttons.py new file mode 100644 index 000000000..9fea8bd2b --- /dev/null +++ b/webapp_headerbuttons.py @@ -0,0 +1,330 @@ +__filename__ = "webapp_headerbuttons.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + + +import time +from datetime import datetime +from happening import todaysEventsCheck +from happening import thisWeeksEventsCheck +from webapp_utils import htmlHighlightLabel + + +def headerButtonsTimeline(defaultTimeline: str, + boxName: str, + pageNumber: int, + translate: {}, + usersPath: str, + mediaButton: str, + blogsButton: str, + newsButton: str, + inboxButton: str, + dmButton: str, + newDM: str, + repliesButton: str, + newReply: str, + minimal: bool, + sentButton: str, + sharesButtonStr: str, + bookmarksButtonStr: str, + eventsButtonStr: str, + moderationButtonStr: str, + newPostButtonStr: str, + baseDir: str, + nickname: str, domain: str, + iconsPath: str, + timelineStartTime, + newCalendarEvent: bool, + calendarPath: str, + calendarImage: str, + followApprovals: str, + iconsAsButtons: bool) -> str: + """Returns the header at the top of the timeline, containing + buttons for inbox, outbox, search, calendar, etc + """ + # start of the button header with inbox, outbox, etc + tlStr = '
\n' + # first button + if defaultTimeline == 'tlmedia': + tlStr += \ + '' + elif defaultTimeline == 'tlblogs': + tlStr += \ + '' + elif defaultTimeline == 'tlnews': + tlStr += \ + '' + else: + tlStr += \ + '' + + # if this is a news instance and we are viewing the news timeline + newsHeader = False + if defaultTimeline == 'tlnews' and boxName == 'tlnews': + newsHeader = True + + if not newsHeader: + tlStr += \ + '' + + tlStr += \ + '' + + # typically the media button + if defaultTimeline != 'tlmedia': + if not minimal and not newsHeader: + tlStr += \ + '' + else: + if not minimal: + tlStr += \ + '' + + isFeaturesTimeline = \ + defaultTimeline == 'tlnews' and boxName == 'tlnews' + + if not isFeaturesTimeline: + # typically the blogs button + # but may change if this is a blogging oriented instance + if defaultTimeline != 'tlblogs': + if not minimal and not isFeaturesTimeline: + titleStr = translate['Blogs'] + if defaultTimeline == 'tlnews': + titleStr = translate['Article'] + tlStr += \ + '' + else: + if not minimal: + tlStr += \ + '' + + # typically the news button + # but may change if this is a news oriented instance + if defaultTimeline != 'tlnews': + tlStr += \ + '' + else: + if not newsHeader: + tlStr += \ + '' + + # show todays events buttons on the first inbox page + happeningStr = '' + if boxName == 'inbox' and pageNumber == 1: + if todaysEventsCheck(baseDir, nickname, domain): + now = datetime.now() + + # happening today button + if not iconsAsButtons: + happeningStr += \ + '' + \ + '' + else: + happeningStr += \ + '' + \ + '' + + # happening this week button + if thisWeeksEventsCheck(baseDir, nickname, domain): + if not iconsAsButtons: + happeningStr += \ + '' + else: + happeningStr += \ + '' + else: + # happening this week button + if thisWeeksEventsCheck(baseDir, nickname, domain): + if not iconsAsButtons: + happeningStr += \ + '' + else: + happeningStr += \ + '' + + if not newsHeader: + # button for the outbox + tlStr += \ + '' + + # add other buttons + tlStr += \ + sharesButtonStr + bookmarksButtonStr + eventsButtonStr + \ + moderationButtonStr + happeningStr + newPostButtonStr + + if not newsHeader: + if not iconsAsButtons: + # the search icon + tlStr += \ + '| ' + \
+                translate['Search and follow'] + \
+                '' + else: + # the search button + tlStr += \ + '' + + # benchmark 5 + timeDiff = int((time.time() - timelineStartTime) * 1000) + if timeDiff > 100: + print('TIMELINE TIMING ' + boxName + ' 5 = ' + str(timeDiff)) + + # the calendar button + if not isFeaturesTimeline: + calendarAltText = translate['Calendar'] + if newCalendarEvent: + # indicate that the calendar icon is highlighted + calendarAltText = '*' + calendarAltText + '*' + if not iconsAsButtons: + tlStr += \ + ' | ' + calendarAltText + \
+                '\n' + else: + tlStr += \ + '' + + if not newsHeader: + # the show/hide button, for a simpler header appearance + if not iconsAsButtons: + tlStr += \ + ' | ' + translate['Show/Hide Buttons'] + \
+                '\n' + else: + tlStr += \ + '' + + if newsHeader: + tlStr += \ + '' + \ + '' + + # the newswire button to show right column links + if not iconsAsButtons: + tlStr += \ + '' + \ + '| ' + translate['News'] + \
+            '' + else: + # NOTE: deliberately no \n at end of line + tlStr += \ + '' + + # the links button to show left column links + if not iconsAsButtons: + tlStr += \ + '' + \ + '| ' + translate['Edit Links'] + \
+            '' + else: + # NOTE: deliberately no \n at end of line + tlStr += \ + '' + + if newsHeader: + tlStr += \ + '' + \ + '' + + if not newsHeader: + tlStr += followApprovals + + if not iconsAsButtons: + # end of headericons div + tlStr += '
' + + # end of the button header with inbox, outbox, etc + tlStr += '
\n' + return tlStr diff --git a/webapp_post.py b/webapp_post.py index 002f89c6e..2b67010e6 100644 --- a/webapp_post.py +++ b/webapp_post.py @@ -758,7 +758,8 @@ def individualPostAsHtml(allowDownloads: bool, iconsPath + '/repeat_inactive.png" ' + \ 'class="announceOrReply"/>\n' + \ ' ' + \ + postJsonObject['object']['id'] + '" ' + \ + 'class="announceOrReply">' + \ announceDisplayName + '\n' # show avatar of person replied to announceActor = \ @@ -804,7 +805,8 @@ def individualPostAsHtml(allowDownloads: bool, '/repeat_inactive.png" ' + \ 'class="announceOrReply"/>\n' + \ ' @' + \ + postJsonObject['object']['id'] + '" ' + \ + 'class="announceOrReply">@' + \ announceNickname + '@' + \ announceDomain + '\n' else: @@ -816,7 +818,7 @@ def individualPostAsHtml(allowDownloads: bool, 'class="announceOrReply"/>\n' + \ ' @unattributed\n' + '" class="announceOrReply">@unattributed\n' else: titleStr += \ ' ' + \ @@ -826,7 +828,8 @@ def individualPostAsHtml(allowDownloads: bool, '/repeat_inactive.png" ' + \ 'class="announceOrReply"/>\n' + \ ' @unattributed\n' + postJsonObject['object']['id'] + '" ' + \ + 'class="announceOrReply">@unattributed\n' else: if postJsonObject['object'].get('inReplyTo'): containerClassIcons = 'containericons darker' @@ -892,7 +895,8 @@ def individualPostAsHtml(allowDownloads: bool, 'class="announceOrReply"/>\n' + \ ' ' + \ '' + replyDisplayName + '\n' + '" class="announceOrReply">' + \ + replyDisplayName + '\n' # benchmark 13.7 if not allowDownloads: @@ -953,7 +957,8 @@ def individualPostAsHtml(allowDownloads: bool, iconsPath + '/reply.png" ' + \ 'class="announceOrReply"/>\n' + \ ' @' + \ + inReplyTo + '" ' + \ + 'class="announceOrReply">@' + \ replyNickname + '@' + \ replyDomain + '\n' else: @@ -967,7 +972,7 @@ def individualPostAsHtml(allowDownloads: bool, '/reply.png" class="announceOrReply"/>\n' + \ ' @unknown\n' + '" class="announceOrReply">@unknown\n' else: postDomain = \ postJsonObject['object']['inReplyTo'] @@ -986,7 +991,8 @@ def individualPostAsHtml(allowDownloads: bool, 'class="announceOrReply"/>\n' + \ ' ' + postDomain + '\n' + '" class="announceOrReply">' + \ + postDomain + '\n' # benchmark 14 if not allowDownloads: diff --git a/webapp_profile.py b/webapp_profile.py index 84dbc57e7..fb491036e 100644 --- a/webapp_profile.py +++ b/webapp_profile.py @@ -614,9 +614,6 @@ def htmlProfile(rssIconAtTop: bool, if isSystemAccount(nickname): bannerFile, bannerFilename = \ getBannerFile(baseDir, nickname, domain) - # profileStyle = \ - # profileStyle.replace('banner.png', - # '/users/' + nickname + '/' + bannerFile) licenseStr = \ '' + \ @@ -733,7 +730,7 @@ def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int, showPublishedDateOnly, False, False, False, True, False) if postStr: - profileStr += separatorStr + postStr + profileStr += postStr + separatorStr ctr += 1 if ctr >= maxItems: break diff --git a/webapp_search.py b/webapp_search.py index db4b04525..d3418d404 100644 --- a/webapp_search.py +++ b/webapp_search.py @@ -28,7 +28,7 @@ from webapp_utils import htmlFooter from webapp_utils import getSearchBannerFile from webapp_utils import htmlPostSeparator from webapp_post import individualPostAsHtml -from blocking import isBlockedHashtag +from webapp_hashtagswarm import htmlHashTagSwarm def htmlSearchEmoji(cssCache: {}, translate: {}, @@ -372,7 +372,7 @@ def htmlSearch(cssCache: {}, translate: {}, 'name="submitSearch">' + translate['Submit'] + '\n' followStr += ' \n' followStr += '

' + \ - htmlHashTagSwarm(baseDir, actor) + '

\n' + htmlHashTagSwarm(baseDir, actor, translate) + '

\n' followStr += ' \n' followStr += ' \n' followStr += '\n' @@ -380,79 +380,221 @@ def htmlSearch(cssCache: {}, translate: {}, return followStr -def htmlHashTagSwarm(baseDir: str, actor: str) -> str: - """Returns a tag swarm of today's hashtags +def htmlSkillsSearch(cssCache: {}, translate: {}, baseDir: str, + httpPrefix: str, + skillsearch: str, instanceOnly: bool, + postsPerPage: int) -> str: + """Show a page containing search results for a skill """ - currTime = datetime.utcnow() - daysSinceEpoch = (currTime - datetime(1970, 1, 1)).days - daysSinceEpochStr = str(daysSinceEpoch) + ' ' - tagSwarm = [] + if skillsearch.startswith('*'): + skillsearch = skillsearch[1:].strip() - for subdir, dirs, files in os.walk(baseDir + '/tags'): + skillsearch = skillsearch.lower().strip('\n').strip('\r') + + results = [] + # search instance accounts + for subdir, dirs, files in os.walk(baseDir + '/accounts/'): for f in files: - tagsFilename = os.path.join(baseDir + '/tags', f) - if not os.path.isfile(tagsFilename): + if not f.endswith('.json'): continue - # get last modified datetime - modTimesinceEpoc = os.path.getmtime(tagsFilename) - lastModifiedDate = datetime.fromtimestamp(modTimesinceEpoc) - fileDaysSinceEpoch = (lastModifiedDate - datetime(1970, 1, 1)).days - # check if the file was last modified today - if fileDaysSinceEpoch != daysSinceEpoch: + if '@' not in f: continue + if f.startswith('inbox@'): + continue + actorFilename = os.path.join(subdir, f) + actorJson = loadJson(actorFilename) + if actorJson: + if actorJson.get('id') and \ + actorJson.get('skills') and \ + actorJson.get('name') and \ + actorJson.get('icon'): + actor = actorJson['id'] + for skillName, skillLevel in actorJson['skills'].items(): + skillName = skillName.lower() + if not (skillName in skillsearch or + skillsearch in skillName): + continue + skillLevelStr = str(skillLevel) + if skillLevel < 100: + skillLevelStr = '0' + skillLevelStr + if skillLevel < 10: + skillLevelStr = '0' + skillLevelStr + indexStr = \ + skillLevelStr + ';' + actor + ';' + \ + actorJson['name'] + \ + ';' + actorJson['icon']['url'] + if indexStr not in results: + results.append(indexStr) + if not instanceOnly: + # search actor cache + for subdir, dirs, files in os.walk(baseDir + '/cache/actors/'): + for f in files: + if not f.endswith('.json'): + continue + if '@' not in f: + continue + if f.startswith('inbox@'): + continue + actorFilename = os.path.join(subdir, f) + cachedActorJson = loadJson(actorFilename) + if cachedActorJson: + if cachedActorJson.get('actor'): + actorJson = cachedActorJson['actor'] + if actorJson.get('id') and \ + actorJson.get('skills') and \ + actorJson.get('name') and \ + actorJson.get('icon'): + actor = actorJson['id'] + for skillName, skillLevel in \ + actorJson['skills'].items(): + skillName = skillName.lower() + if not (skillName in skillsearch or + skillsearch in skillName): + continue + skillLevelStr = str(skillLevel) + if skillLevel < 100: + skillLevelStr = '0' + skillLevelStr + if skillLevel < 10: + skillLevelStr = '0' + skillLevelStr + indexStr = \ + skillLevelStr + ';' + actor + ';' + \ + actorJson['name'] + \ + ';' + actorJson['icon']['url'] + if indexStr not in results: + results.append(indexStr) - hashTagName = f.split('.')[0] - if isBlockedHashtag(baseDir, hashTagName): - continue - if daysSinceEpochStr not in open(tagsFilename).read(): - continue - with open(tagsFilename, 'r') as tagsFile: - line = tagsFile.readline() - lineCtr = 1 - tagCtr = 0 - maxLineCtr = 1 - while line: - if ' ' not in line: - line = tagsFile.readline() - lineCtr += 1 - # don't read too many lines - if lineCtr >= maxLineCtr: - break - continue - postDaysSinceEpochStr = line.split(' ')[0] - if not postDaysSinceEpochStr.isdigit(): - line = tagsFile.readline() - lineCtr += 1 - # don't read too many lines - if lineCtr >= maxLineCtr: - break - continue - postDaysSinceEpoch = int(postDaysSinceEpochStr) - if postDaysSinceEpoch < daysSinceEpoch: - break - if postDaysSinceEpoch == daysSinceEpoch: - if tagCtr == 0: - tagSwarm.append(hashTagName) - tagCtr += 1 + results.sort(reverse=True) - line = tagsFile.readline() - lineCtr += 1 - # don't read too many lines - if lineCtr >= maxLineCtr: - break + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' - if not tagSwarm: - return '' - tagSwarm.sort() - tagSwarmStr = '' - ctr = 0 - for tagName in tagSwarm: - tagSwarmStr += \ - '
' + tagName + '\n' - ctr += 1 - tagSwarmHtml = tagSwarmStr.strip() + '\n' - return tagSwarmHtml + skillSearchForm = htmlHeaderWithExternalStyle(cssFilename) + skillSearchForm += \ + '

' + translate['Skills search'] + ': ' + \ + skillsearch + '

' + + if len(results) == 0: + skillSearchForm += \ + '
' + translate['No results'] + \ + '
' + else: + skillSearchForm += '
' + ctr = 0 + for skillMatch in results: + skillMatchFields = skillMatch.split(';') + if len(skillMatchFields) != 4: + continue + actor = skillMatchFields[1] + actorName = skillMatchFields[2] + avatarUrl = skillMatchFields[3] + skillSearchForm += \ + '
' + skillSearchForm += \ + '' + actorName + \ + '
' + ctr += 1 + if ctr >= postsPerPage: + break + skillSearchForm += '
' + skillSearchForm += htmlFooter() + return skillSearchForm + + +def htmlHistorySearch(cssCache: {}, translate: {}, baseDir: str, + httpPrefix: str, + nickname: str, domain: str, + historysearch: str, + postsPerPage: int, pageNumber: int, + projectVersion: str, + recentPostsCache: {}, + maxRecentPosts: int, + session, + wfRequest, + personCache: {}, + port: int, + YTReplacementDomain: str, + showPublishedDateOnly: bool) -> str: + """Show a page containing search results for your post history + """ + if historysearch.startswith('!'): + historysearch = historysearch[1:].strip() + + historysearch = historysearch.lower().strip('\n').strip('\r') + + boxFilenames = \ + searchBoxPosts(baseDir, nickname, domain, + historysearch, postsPerPage) + + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + historySearchForm = \ + htmlHeaderWithExternalStyle(cssFilename) + + # add the page title + historySearchForm += \ + '

' + translate['Your Posts'] + '

' + + if len(boxFilenames) == 0: + historySearchForm += \ + '
' + translate['No results'] + \ + '
' + return historySearchForm + + iconsPath = getIconsWebPath(baseDir) + separatorStr = htmlPostSeparator(baseDir, None) + + # ensure that the page number is in bounds + if not pageNumber: + pageNumber = 1 + elif pageNumber < 1: + pageNumber = 1 + + # get the start end end within the index file + startIndex = int((pageNumber - 1) * postsPerPage) + endIndex = startIndex + postsPerPage + noOfBoxFilenames = len(boxFilenames) + if endIndex >= noOfBoxFilenames and noOfBoxFilenames > 0: + endIndex = noOfBoxFilenames - 1 + + index = startIndex + while index <= endIndex: + postFilename = boxFilenames[index] + if not postFilename: + index += 1 + continue + postJsonObject = loadJson(postFilename) + if not postJsonObject: + index += 1 + continue + showIndividualPostIcons = True + allowDeletion = False + postStr = \ + individualPostAsHtml(True, recentPostsCache, + maxRecentPosts, + iconsPath, translate, None, + baseDir, session, wfRequest, + personCache, + nickname, domain, port, + postJsonObject, + None, True, allowDeletion, + httpPrefix, projectVersion, + 'search', + YTReplacementDomain, + showPublishedDateOnly, + showIndividualPostIcons, + showIndividualPostIcons, + False, False, False) + if postStr: + historySearchForm += separatorStr + postStr + index += 1 + + historySearchForm += htmlFooter() + return historySearchForm def htmlHashtagSearch(cssCache: {}, @@ -711,220 +853,3 @@ def rssHashtagSearch(nickname: str, domain: str, port: int, break return hashtagFeed + rss2TagFooter() - - -def htmlSkillsSearch(cssCache: {}, translate: {}, baseDir: str, - httpPrefix: str, - skillsearch: str, instanceOnly: bool, - postsPerPage: int) -> str: - """Show a page containing search results for a skill - """ - if skillsearch.startswith('*'): - skillsearch = skillsearch[1:].strip() - - skillsearch = skillsearch.lower().strip('\n').strip('\r') - - results = [] - # search instance accounts - for subdir, dirs, files in os.walk(baseDir + '/accounts/'): - for f in files: - if not f.endswith('.json'): - continue - if '@' not in f: - continue - if f.startswith('inbox@'): - continue - actorFilename = os.path.join(subdir, f) - actorJson = loadJson(actorFilename) - if actorJson: - if actorJson.get('id') and \ - actorJson.get('skills') and \ - actorJson.get('name') and \ - actorJson.get('icon'): - actor = actorJson['id'] - for skillName, skillLevel in actorJson['skills'].items(): - skillName = skillName.lower() - if not (skillName in skillsearch or - skillsearch in skillName): - continue - skillLevelStr = str(skillLevel) - if skillLevel < 100: - skillLevelStr = '0' + skillLevelStr - if skillLevel < 10: - skillLevelStr = '0' + skillLevelStr - indexStr = \ - skillLevelStr + ';' + actor + ';' + \ - actorJson['name'] + \ - ';' + actorJson['icon']['url'] - if indexStr not in results: - results.append(indexStr) - if not instanceOnly: - # search actor cache - for subdir, dirs, files in os.walk(baseDir + '/cache/actors/'): - for f in files: - if not f.endswith('.json'): - continue - if '@' not in f: - continue - if f.startswith('inbox@'): - continue - actorFilename = os.path.join(subdir, f) - cachedActorJson = loadJson(actorFilename) - if cachedActorJson: - if cachedActorJson.get('actor'): - actorJson = cachedActorJson['actor'] - if actorJson.get('id') and \ - actorJson.get('skills') and \ - actorJson.get('name') and \ - actorJson.get('icon'): - actor = actorJson['id'] - for skillName, skillLevel in \ - actorJson['skills'].items(): - skillName = skillName.lower() - if not (skillName in skillsearch or - skillsearch in skillName): - continue - skillLevelStr = str(skillLevel) - if skillLevel < 100: - skillLevelStr = '0' + skillLevelStr - if skillLevel < 10: - skillLevelStr = '0' + skillLevelStr - indexStr = \ - skillLevelStr + ';' + actor + ';' + \ - actorJson['name'] + \ - ';' + actorJson['icon']['url'] - if indexStr not in results: - results.append(indexStr) - - results.sort(reverse=True) - - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - skillSearchForm = htmlHeaderWithExternalStyle(cssFilename) - skillSearchForm += \ - '

' + translate['Skills search'] + ': ' + \ - skillsearch + '

' - - if len(results) == 0: - skillSearchForm += \ - '
' + translate['No results'] + \ - '
' - else: - skillSearchForm += '
' - ctr = 0 - for skillMatch in results: - skillMatchFields = skillMatch.split(';') - if len(skillMatchFields) != 4: - continue - actor = skillMatchFields[1] - actorName = skillMatchFields[2] - avatarUrl = skillMatchFields[3] - skillSearchForm += \ - '
' - skillSearchForm += \ - '' + actorName + \ - '
' - ctr += 1 - if ctr >= postsPerPage: - break - skillSearchForm += '
' - skillSearchForm += htmlFooter() - return skillSearchForm - - -def htmlHistorySearch(cssCache: {}, translate: {}, baseDir: str, - httpPrefix: str, - nickname: str, domain: str, - historysearch: str, - postsPerPage: int, pageNumber: int, - projectVersion: str, - recentPostsCache: {}, - maxRecentPosts: int, - session, - wfRequest, - personCache: {}, - port: int, - YTReplacementDomain: str, - showPublishedDateOnly: bool) -> str: - """Show a page containing search results for your post history - """ - if historysearch.startswith('!'): - historysearch = historysearch[1:].strip() - - historysearch = historysearch.lower().strip('\n').strip('\r') - - boxFilenames = \ - searchBoxPosts(baseDir, nickname, domain, - historysearch, postsPerPage) - - cssFilename = baseDir + '/epicyon-profile.css' - if os.path.isfile(baseDir + '/epicyon.css'): - cssFilename = baseDir + '/epicyon.css' - - historySearchForm = \ - htmlHeaderWithExternalStyle(cssFilename) - - # add the page title - historySearchForm += \ - '

' + translate['Your Posts'] + '

' - - if len(boxFilenames) == 0: - historySearchForm += \ - '
' + translate['No results'] + \ - '
' - return historySearchForm - - iconsPath = getIconsWebPath(baseDir) - separatorStr = htmlPostSeparator(baseDir, None) - - # ensure that the page number is in bounds - if not pageNumber: - pageNumber = 1 - elif pageNumber < 1: - pageNumber = 1 - - # get the start end end within the index file - startIndex = int((pageNumber - 1) * postsPerPage) - endIndex = startIndex + postsPerPage - noOfBoxFilenames = len(boxFilenames) - if endIndex >= noOfBoxFilenames and noOfBoxFilenames > 0: - endIndex = noOfBoxFilenames - 1 - - index = startIndex - while index <= endIndex: - postFilename = boxFilenames[index] - if not postFilename: - index += 1 - continue - postJsonObject = loadJson(postFilename) - if not postJsonObject: - index += 1 - continue - showIndividualPostIcons = True - allowDeletion = False - postStr = \ - individualPostAsHtml(True, recentPostsCache, - maxRecentPosts, - iconsPath, translate, None, - baseDir, session, wfRequest, - personCache, - nickname, domain, port, - postJsonObject, - None, True, allowDeletion, - httpPrefix, projectVersion, - 'search', - YTReplacementDomain, - showPublishedDateOnly, - showIndividualPostIcons, - showIndividualPostIcons, - False, False, False) - if postStr: - historySearchForm += separatorStr + postStr - index += 1 - - historySearchForm += htmlFooter() - return historySearchForm diff --git a/webapp_timeline.py b/webapp_timeline.py index 05a60a33e..617fee84f 100644 --- a/webapp_timeline.py +++ b/webapp_timeline.py @@ -7,23 +7,22 @@ __email__ = "bob@freedombone.net" __status__ = "Production" import os -from datetime import datetime import time from utils import removeIdEnding from follow import followerApprovalActive from person import isPersonSnoozed -from happening import todaysEventsCheck -from happening import thisWeeksEventsCheck from webapp_utils import getIconsWebPath from webapp_utils import htmlPostSeparator from webapp_utils import getBannerFile from webapp_utils import htmlHeaderWithExternalStyle from webapp_utils import htmlFooter from webapp_utils import sharesTimelineJson +from webapp_utils import htmlHighlightLabel from webapp_post import preparePostFromHtmlCache from webapp_post import individualPostAsHtml from webapp_column_left import getLeftColumnContent from webapp_column_right import getRightColumnContent +from webapp_headerbuttons import headerButtonsTimeline from posts import isModerator from posts import isEditor @@ -427,6 +426,8 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, calendarImage, followApprovals, iconsAsButtons) + tlStr += '
\n' + # second row of buttons for moderator actions if moderator and boxName == 'moderation': tlStr += \ @@ -580,9 +581,9 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, if currTlStr: itemCtr += 1 + tlStr += currTlStr if separatorStr: tlStr += separatorStr - tlStr += currTlStr if boxName == 'tlmedia': tlStr += '
\n' @@ -598,6 +599,9 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, translate['Page down'] + '">\n' + \ ' \n' + # end of timeline-posts + tlStr += ' \n' + # end of column-center tlStr += ' \n' @@ -694,9 +698,10 @@ def htmlSharesTimeline(translate: {}, pageNumber: int, itemsPerPage: int, showRemoveButton = False if item['actor'] == actor: showRemoveButton = True - timelineStr += separatorStr + \ + timelineStr += \ htmlIndividualShare(actor, item, translate, showContactButton, showRemoveButton) + timelineStr += separatorStr if not lastPage: iconsPath = getIconsWebPath(baseDir) @@ -712,332 +717,6 @@ def htmlSharesTimeline(translate: {}, pageNumber: int, itemsPerPage: int, return timelineStr -def htmlHighlightLabel(label: str, highlight: bool) -> str: - """If the give text should be highlighted then return - the appropriate markup. - This is so that in shell browsers, like lynx, it's possible - to see if the replies or DM button are highlighted. - """ - if not highlight: - return label - return '*' + str(label) + '*' - - -def headerButtonsTimeline(defaultTimeline: str, - boxName: str, - pageNumber: int, - translate: {}, - usersPath: str, - mediaButton: str, - blogsButton: str, - newsButton: str, - inboxButton: str, - dmButton: str, - newDM: str, - repliesButton: str, - newReply: str, - minimal: bool, - sentButton: str, - sharesButtonStr: str, - bookmarksButtonStr: str, - eventsButtonStr: str, - moderationButtonStr: str, - newPostButtonStr: str, - baseDir: str, - nickname: str, domain: str, - iconsPath: str, - timelineStartTime, - newCalendarEvent: bool, - calendarPath: str, - calendarImage: str, - followApprovals: str, - iconsAsButtons: bool) -> str: - """Returns the header at the top of the timeline, containing - buttons for inbox, outbox, search, calendar, etc - """ - # start of the button header with inbox, outbox, etc - tlStr = '
\n' - # first button - if defaultTimeline == 'tlmedia': - tlStr += \ - '' - elif defaultTimeline == 'tlblogs': - tlStr += \ - '' - elif defaultTimeline == 'tlnews': - tlStr += \ - '' - else: - tlStr += \ - '' - - # if this is a news instance and we are viewing the news timeline - newsHeader = False - if defaultTimeline == 'tlnews' and boxName == 'tlnews': - newsHeader = True - - if not newsHeader: - tlStr += \ - '' - - tlStr += \ - '' - - # typically the media button - if defaultTimeline != 'tlmedia': - if not minimal and not newsHeader: - tlStr += \ - '' - else: - if not minimal: - tlStr += \ - '' - - isFeaturesTimeline = \ - defaultTimeline == 'tlnews' and boxName == 'tlnews' - - if not isFeaturesTimeline: - # typically the blogs button - # but may change if this is a blogging oriented instance - if defaultTimeline != 'tlblogs': - if not minimal and not isFeaturesTimeline: - titleStr = translate['Blogs'] - if defaultTimeline == 'tlnews': - titleStr = translate['Article'] - tlStr += \ - '' - else: - if not minimal: - tlStr += \ - '' - - # typically the news button - # but may change if this is a news oriented instance - if defaultTimeline != 'tlnews': - tlStr += \ - '' - else: - if not newsHeader: - tlStr += \ - '' - - # show todays events buttons on the first inbox page - happeningStr = '' - if boxName == 'inbox' and pageNumber == 1: - if todaysEventsCheck(baseDir, nickname, domain): - now = datetime.now() - - # happening today button - if not iconsAsButtons: - happeningStr += \ - '' + \ - '' - else: - happeningStr += \ - '' + \ - '' - - # happening this week button - if thisWeeksEventsCheck(baseDir, nickname, domain): - if not iconsAsButtons: - happeningStr += \ - '' - else: - happeningStr += \ - '' - else: - # happening this week button - if thisWeeksEventsCheck(baseDir, nickname, domain): - if not iconsAsButtons: - happeningStr += \ - '' - else: - happeningStr += \ - '' - - if not newsHeader: - # button for the outbox - tlStr += \ - '' - - # add other buttons - tlStr += \ - sharesButtonStr + bookmarksButtonStr + eventsButtonStr + \ - moderationButtonStr + happeningStr + newPostButtonStr - - if not newsHeader: - if not iconsAsButtons: - # the search icon - tlStr += \ - '| ' + \
-                translate['Search and follow'] + \
-                '' - else: - # the search button - tlStr += \ - '' - - # benchmark 5 - timeDiff = int((time.time() - timelineStartTime) * 1000) - if timeDiff > 100: - print('TIMELINE TIMING ' + boxName + ' 5 = ' + str(timeDiff)) - - # the calendar button - if not isFeaturesTimeline: - calendarAltText = translate['Calendar'] - if newCalendarEvent: - # indicate that the calendar icon is highlighted - calendarAltText = '*' + calendarAltText + '*' - if not iconsAsButtons: - tlStr += \ - ' | ' + calendarAltText + \
-                '\n' - else: - tlStr += \ - '' - - if not newsHeader: - # the show/hide button, for a simpler header appearance - if not iconsAsButtons: - tlStr += \ - ' | ' + translate['Show/Hide Buttons'] + \
-                '\n' - else: - tlStr += \ - '' - - if newsHeader: - tlStr += \ - '' + \ - '' - - # the newswire button to show right column links - if not iconsAsButtons: - tlStr += \ - '' + \ - '| ' + translate['News'] + \
-            '' - else: - # NOTE: deliberately no \n at end of line - tlStr += \ - '' - - # the links button to show left column links - if not iconsAsButtons: - tlStr += \ - '' + \ - '| ' + translate['Edit Links'] + \
-            '' - else: - # NOTE: deliberately no \n at end of line - tlStr += \ - '' - - if newsHeader: - tlStr += \ - '' + \ - '' - - if not iconsAsButtons: - # end of headericons div - tlStr += '
' - - if not newsHeader: - tlStr += followApprovals - # end of the button header with inbox, outbox, etc - tlStr += ' \n' - return tlStr - - def htmlShares(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, diff --git a/webapp_utils.py b/webapp_utils.py index 8b3c8bc6a..6a18f06c4 100644 --- a/webapp_utils.py +++ b/webapp_utils.py @@ -728,13 +728,15 @@ def htmlPostSeparator(baseDir: str, column: str) -> str: iconsPath = getIconsWebPath(baseDir) theme = getConfigParam(baseDir, 'theme') filename = 'separator.png' + separatorClass = "postSeparatorImage" if column: + separatorClass = "postSeparatorImage" + column.title() filename = 'separator_' + column + '.png' separatorImageFilename = baseDir + '/theme/' + theme + '/icons/' + filename separatorStr = '' if os.path.isfile(separatorImageFilename): separatorStr = \ - '
' + \ + '
' + \ '' + \ '
\n' return separatorStr @@ -808,3 +810,14 @@ def headerButtonsFrontScreen(translate: {}, headerStr + \ '
\n' return headerStr + + +def htmlHighlightLabel(label: str, highlight: bool) -> str: + """If the given text should be highlighted then return + the appropriate markup. + This is so that in shell browsers, like lynx, it's possible + to see if the replies or DM button are highlighted. + """ + if not highlight: + return label + return '*' + str(label) + '*'