Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main

merge-requests/30/head
Bob Mottram 2020-11-19 22:21:50 +00:00
commit c0ed411c8c
52 changed files with 945 additions and 677 deletions

View File

@ -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);

View File

@ -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;

View File

@ -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:

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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": "أصول الهاشتاق"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -329,5 +329,6 @@
"Choose newswire items referenced in your article": "Roghnaigh míreanna sreanga nuachta dá dtagraítear i dalt",
"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"
}

View File

@ -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": "हैशटैग की उत्पत्ति"
}

View File

@ -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"
}

View File

@ -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": "ハッシュタグの起源"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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": "Происхождение хэштегов"
}

View File

@ -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": "标签起源"
}

View File

@ -227,6 +227,7 @@ def htmlLinksMobile(cssCache: {}, baseDir: str,
'<img loading="lazy" class="timeline-banner" ' + \
'src="/users/' + nickname + '/' + bannerFile + '" /></a>\n'
htmlStr += '<div class="col-left-mobile">\n'
htmlStr += '<center>' + \
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 += '</div>\n'
htmlStr += '</div>\n' + htmlFooter()
return htmlStr

View File

@ -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,
'<img loading="lazy" class="timeline-banner" ' + \
'src="/users/' + nickname + '/' + bannerFile + '" /></a>\n'
htmlStr += '<div class="col-right-mobile">\n'
htmlStr += '<center>' + \
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 += '</div\n>'
htmlStr += htmlFooter()
return htmlStr

View File

@ -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 = '<br><br><center>\n'
histogramHeaderStr += ' <h1>' + translate['Hashtag origins'] + '</h1>\n'
histogramHeaderStr += ' <table class="domainHistogram">\n'
histogramHeaderStr += ' <colgroup>\n'
histogramHeaderStr += ' <col span="1" class="domainHistogramLeft">\n'
histogramHeaderStr += ' <col span="1" class="domainHistogramRight">\n'
histogramHeaderStr += ' </colgroup>\n'
histogramHeaderStr += ' <tbody>\n'
histogramHeaderStr += ' <tr>\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) + '%<br>'
rightColStr += domain + '<br>'
del domainHistogram[domain]
if htmlStr:
htmlStr += ' <td>' + leftColStr + '</td>\n'
htmlStr += ' <td>' + rightColStr + '</td>\n'
htmlStr += ' </tr>\n'
htmlStr += ' </tbody>\n'
htmlStr += ' </table>\n'
htmlStr += '</center>\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 += \
'<a href="' + actor + '/tags/' + tagName + \
'" class="hashtagswarm">' + tagName + '</a>\n'
ctr += 1
tagSwarmHtml = tagSwarmStr.strip() + '\n'
tagSwarmHtml += getHashtagDomainHistogram(domainHistogram, translate)
return tagSwarmHtml

View File

@ -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 = '<div class="containerHeader">\n'
# first button
if defaultTimeline == 'tlmedia':
tlStr += \
'<a href="' + usersPath + \
'/tlmedia"><button class="' + \
mediaButton + '"><span>' + translate['Media'] + \
'</span></button></a>'
elif defaultTimeline == 'tlblogs':
tlStr += \
'<a href="' + usersPath + \
'/tlblogs"><button class="' + \
blogsButton + '"><span>' + translate['Blogs'] + \
'</span></button></a>'
elif defaultTimeline == 'tlnews':
tlStr += \
'<a href="' + usersPath + \
'/tlnews"><button class="' + \
newsButton + '"><span>' + translate['Features'] + \
'</span></button></a>'
else:
tlStr += \
'<a href="' + usersPath + \
'/inbox"><button class="' + \
inboxButton + '"><span>' + \
translate['Inbox'] + '</span></button></a>'
# 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 += \
'<a href="' + usersPath + \
'/dm"><button class="' + dmButton + \
'"><span>' + htmlHighlightLabel(translate['DM'], newDM) + \
'</span></button></a>'
tlStr += \
'<a href="' + usersPath + '/tlreplies"><button class="' + \
repliesButton + '"><span>' + \
htmlHighlightLabel(translate['Replies'], newReply) + \
'</span></button></a>'
# typically the media button
if defaultTimeline != 'tlmedia':
if not minimal and not newsHeader:
tlStr += \
'<a href="' + usersPath + \
'/tlmedia"><button class="' + \
mediaButton + '"><span>' + translate['Media'] + \
'</span></button></a>'
else:
if not minimal:
tlStr += \
'<a href="' + usersPath + \
'/inbox"><button class="' + \
inboxButton+'"><span>' + translate['Inbox'] + \
'</span></button></a>'
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 += \
'<a href="' + usersPath + \
'/tlblogs"><button class="' + \
blogsButton + '"><span>' + titleStr + \
'</span></button></a>'
else:
if not minimal:
tlStr += \
'<a href="' + usersPath + \
'/inbox"><button class="' + \
inboxButton + '"><span>' + translate['Inbox'] + \
'</span></button></a>'
# typically the news button
# but may change if this is a news oriented instance
if defaultTimeline != 'tlnews':
tlStr += \
'<a href="' + usersPath + \
'/tlnews"><button class="' + \
newsButton + '"><span>' + translate['News'] + \
'</span></button></a>'
else:
if not newsHeader:
tlStr += \
'<a href="' + usersPath + \
'/inbox"><button class="' + \
inboxButton + '"><span>' + translate['Inbox'] + \
'</span></button></a>'
# 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 += \
'<a href="' + usersPath + '/calendar?year=' + \
str(now.year) + '?month=' + str(now.month) + \
'?day=' + str(now.day) + '">' + \
'<button class="buttonevent">' + \
translate['Happening Today'] + '</button></a>'
else:
happeningStr += \
'<a href="' + usersPath + '/calendar?year=' + \
str(now.year) + '?month=' + str(now.month) + \
'?day=' + str(now.day) + '">' + \
'<button class="button">' + \
translate['Happening Today'] + '</button></a>'
# happening this week button
if thisWeeksEventsCheck(baseDir, nickname, domain):
if not iconsAsButtons:
happeningStr += \
'<a href="' + usersPath + \
'/calendar"><button class="buttonevent">' + \
translate['Happening This Week'] + '</button></a>'
else:
happeningStr += \
'<a href="' + usersPath + \
'/calendar"><button class="button">' + \
translate['Happening This Week'] + '</button></a>'
else:
# happening this week button
if thisWeeksEventsCheck(baseDir, nickname, domain):
if not iconsAsButtons:
happeningStr += \
'<a href="' + usersPath + \
'/calendar"><button class="buttonevent">' + \
translate['Happening This Week'] + '</button></a>'
else:
happeningStr += \
'<a href="' + usersPath + \
'/calendar"><button class="button">' + \
translate['Happening This Week'] + '</button></a>'
if not newsHeader:
# button for the outbox
tlStr += \
'<a href="' + usersPath + \
'/outbox"><button class="' + \
sentButton + '"><span>' + translate['Outbox'] + \
'</span></button></a>'
# add other buttons
tlStr += \
sharesButtonStr + bookmarksButtonStr + eventsButtonStr + \
moderationButtonStr + happeningStr + newPostButtonStr
if not newsHeader:
if not iconsAsButtons:
# the search icon
tlStr += \
'<a class="imageAnchor" href="' + usersPath + \
'/search"><img loading="lazy" src="/' + \
iconsPath + '/search.png" title="' + \
translate['Search and follow'] + '" alt="| ' + \
translate['Search and follow'] + \
'" class="timelineicon"/></a>'
else:
# the search button
tlStr += \
'<a href="' + usersPath + \
'/search"><button class="button">' + \
'<span>' + translate['Search'] + \
'</span></button></a>'
# 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 += \
' <a class="imageAnchor" href="' + \
usersPath + calendarPath + \
'"><img loading="lazy" src="/' + iconsPath + '/' + \
calendarImage + '" title="' + translate['Calendar'] + \
'" alt="| ' + calendarAltText + \
'" class="timelineicon"/></a>\n'
else:
tlStr += \
'<a href="' + usersPath + calendarPath + \
'"><button class="button">' + \
'<span>' + translate['Calendar'] + \
'</span></button></a>'
if not newsHeader:
# the show/hide button, for a simpler header appearance
if not iconsAsButtons:
tlStr += \
' <a class="imageAnchor" href="' + \
usersPath + '/minimal' + \
'"><img loading="lazy" src="/' + iconsPath + \
'/showhide.png" title="' + translate['Show/Hide Buttons'] + \
'" alt="| ' + translate['Show/Hide Buttons'] + \
'" class="timelineicon"/></a>\n'
else:
tlStr += \
'<a href="' + usersPath + '/minimal' + \
'"><button class="button">' + \
'<span>' + translate['Show/Hide Buttons'] + \
'</span></button></a>'
if newsHeader:
tlStr += \
'<a href="' + usersPath + '/inbox">' + \
'<button class="button">' + \
'<span>' + translate['User'] + '</span></button></a>'
# the newswire button to show right column links
if not iconsAsButtons:
tlStr += \
'<a class="imageAnchorMobile" href="' + \
usersPath + '/newswiremobile">' + \
'<img loading="lazy" src="/' + iconsPath + \
'/newswire.png" title="' + translate['News'] + \
'" alt="| ' + translate['News'] + \
'" class="timelineicon"/></a>'
else:
# NOTE: deliberately no \n at end of line
tlStr += \
'<a href="' + \
usersPath + '/newswiremobile' + \
'"><button class="buttonMobile">' + \
'<span>' + translate['Newswire'] + \
'</span></button></a>'
# the links button to show left column links
if not iconsAsButtons:
tlStr += \
'<a class="imageAnchorMobile" href="' + \
usersPath + '/linksmobile">' + \
'<img loading="lazy" src="/' + iconsPath + \
'/links.png" title="' + translate['Edit Links'] + \
'" alt="| ' + translate['Edit Links'] + \
'" class="timelineicon"/></a>'
else:
# NOTE: deliberately no \n at end of line
tlStr += \
'<a href="' + \
usersPath + '/linksmobile' + \
'"><button class="buttonMobile">' + \
'<span>' + translate['Links'] + \
'</span></button></a>'
if newsHeader:
tlStr += \
'<a href="' + usersPath + '/editprofile">' + \
'<button class="buttonDesktop">' + \
'<span>' + translate['Settings'] + '</span></button></a>'
if not newsHeader:
tlStr += followApprovals
if not iconsAsButtons:
# end of headericons div
tlStr += '</div>'
# end of the button header with inbox, outbox, etc
tlStr += ' </div>\n'
return tlStr

View File

@ -758,7 +758,8 @@ def individualPostAsHtml(allowDownloads: bool,
iconsPath + '/repeat_inactive.png" ' + \
'class="announceOrReply"/>\n' + \
' <a href="' + \
postJsonObject['object']['id'] + '">' + \
postJsonObject['object']['id'] + '" ' + \
'class="announceOrReply">' + \
announceDisplayName + '</a>\n'
# show avatar of person replied to
announceActor = \
@ -804,7 +805,8 @@ def individualPostAsHtml(allowDownloads: bool,
'/repeat_inactive.png" ' + \
'class="announceOrReply"/>\n' + \
' <a href="' + \
postJsonObject['object']['id'] + '">@' + \
postJsonObject['object']['id'] + '" ' + \
'class="announceOrReply">@' + \
announceNickname + '@' + \
announceDomain + '</a>\n'
else:
@ -816,7 +818,7 @@ def individualPostAsHtml(allowDownloads: bool,
'class="announceOrReply"/>\n' + \
' <a href="' + \
postJsonObject['object']['id'] + \
'">@unattributed</a>\n'
'" class="announceOrReply">@unattributed</a>\n'
else:
titleStr += \
' ' + \
@ -826,7 +828,8 @@ def individualPostAsHtml(allowDownloads: bool,
'/repeat_inactive.png" ' + \
'class="announceOrReply"/>\n' + \
' <a href="' + \
postJsonObject['object']['id'] + '">@unattributed</a>\n'
postJsonObject['object']['id'] + '" ' + \
'class="announceOrReply">@unattributed</a>\n'
else:
if postJsonObject['object'].get('inReplyTo'):
containerClassIcons = 'containericons darker'
@ -892,7 +895,8 @@ def individualPostAsHtml(allowDownloads: bool,
'class="announceOrReply"/>\n' + \
' ' + \
'<a href="' + inReplyTo + \
'">' + replyDisplayName + '</a>\n'
'" class="announceOrReply">' + \
replyDisplayName + '</a>\n'
# benchmark 13.7
if not allowDownloads:
@ -953,7 +957,8 @@ def individualPostAsHtml(allowDownloads: bool,
iconsPath + '/reply.png" ' + \
'class="announceOrReply"/>\n' + \
' <a href="' + \
inReplyTo + '">@' + \
inReplyTo + '" ' + \
'class="announceOrReply">@' + \
replyNickname + '@' + \
replyDomain + '</a>\n'
else:
@ -967,7 +972,7 @@ def individualPostAsHtml(allowDownloads: bool,
'/reply.png" class="announceOrReply"/>\n' + \
' <a href="' + \
postJsonObject['object']['inReplyTo'] + \
'">@unknown</a>\n'
'" class="announceOrReply">@unknown</a>\n'
else:
postDomain = \
postJsonObject['object']['inReplyTo']
@ -986,7 +991,8 @@ def individualPostAsHtml(allowDownloads: bool,
'class="announceOrReply"/>\n' + \
' <a href="' + \
postJsonObject['object']['inReplyTo'] + \
'">' + postDomain + '</a>\n'
'" class="announceOrReply">' + \
postDomain + '</a>\n'
# benchmark 14
if not allowDownloads:

View File

@ -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 = \
'<a href="https://gitlab.com/bashrc2/epicyon">' + \
@ -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

View File

@ -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'] + '</button>\n'
followStr += ' </form>\n'
followStr += ' <p class="hashtagswarm">' + \
htmlHashTagSwarm(baseDir, actor) + '</p>\n'
htmlHashTagSwarm(baseDir, actor, translate) + '</p>\n'
followStr += ' </center>\n'
followStr += ' </div>\n'
followStr += '</div>\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 += \
'<a href="' + actor + '/tags/' + tagName + \
'" class="hashtagswarm">' + tagName + '</a>\n'
ctr += 1
tagSwarmHtml = tagSwarmStr.strip() + '\n'
return tagSwarmHtml
skillSearchForm = htmlHeaderWithExternalStyle(cssFilename)
skillSearchForm += \
'<center><h1>' + translate['Skills search'] + ': ' + \
skillsearch + '</h1></center>'
if len(results) == 0:
skillSearchForm += \
'<center><h5>' + translate['No results'] + \
'</h5></center>'
else:
skillSearchForm += '<center>'
ctr = 0
for skillMatch in results:
skillMatchFields = skillMatch.split(';')
if len(skillMatchFields) != 4:
continue
actor = skillMatchFields[1]
actorName = skillMatchFields[2]
avatarUrl = skillMatchFields[3]
skillSearchForm += \
'<div class="search-result""><a href="' + \
actor + '/skills">'
skillSearchForm += \
'<img loading="lazy" src="' + avatarUrl + \
'"/><span class="search-result-text">' + actorName + \
'</span></a></div>'
ctr += 1
if ctr >= postsPerPage:
break
skillSearchForm += '</center>'
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 += \
'<center><h1>' + translate['Your Posts'] + '</h1></center>'
if len(boxFilenames) == 0:
historySearchForm += \
'<center><h5>' + translate['No results'] + \
'</h5></center>'
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 += \
'<center><h1>' + translate['Skills search'] + ': ' + \
skillsearch + '</h1></center>'
if len(results) == 0:
skillSearchForm += \
'<center><h5>' + translate['No results'] + \
'</h5></center>'
else:
skillSearchForm += '<center>'
ctr = 0
for skillMatch in results:
skillMatchFields = skillMatch.split(';')
if len(skillMatchFields) != 4:
continue
actor = skillMatchFields[1]
actorName = skillMatchFields[2]
avatarUrl = skillMatchFields[3]
skillSearchForm += \
'<div class="search-result""><a href="' + \
actor + '/skills">'
skillSearchForm += \
'<img loading="lazy" src="' + avatarUrl + \
'"/><span class="search-result-text">' + actorName + \
'</span></a></div>'
ctr += 1
if ctr >= postsPerPage:
break
skillSearchForm += '</center>'
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 += \
'<center><h1>' + translate['Your Posts'] + '</h1></center>'
if len(boxFilenames) == 0:
historySearchForm += \
'<center><h5>' + translate['No results'] + \
'</h5></center>'
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

View File

@ -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 += ' <div class="timeline-posts">\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 += '</div>\n'
@ -598,6 +599,9 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str,
translate['Page down'] + '"></a>\n' + \
' </center>\n'
# end of timeline-posts
tlStr += ' </div>\n'
# end of column-center
tlStr += ' </td>\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 = '<div class="containerHeader">\n'
# first button
if defaultTimeline == 'tlmedia':
tlStr += \
'<a href="' + usersPath + \
'/tlmedia"><button class="' + \
mediaButton + '"><span>' + translate['Media'] + \
'</span></button></a>'
elif defaultTimeline == 'tlblogs':
tlStr += \
'<a href="' + usersPath + \
'/tlblogs"><button class="' + \
blogsButton + '"><span>' + translate['Blogs'] + \
'</span></button></a>'
elif defaultTimeline == 'tlnews':
tlStr += \
'<a href="' + usersPath + \
'/tlnews"><button class="' + \
newsButton + '"><span>' + translate['Features'] + \
'</span></button></a>'
else:
tlStr += \
'<a href="' + usersPath + \
'/inbox"><button class="' + \
inboxButton + '"><span>' + \
translate['Inbox'] + '</span></button></a>'
# 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 += \
'<a href="' + usersPath + \
'/dm"><button class="' + dmButton + \
'"><span>' + htmlHighlightLabel(translate['DM'], newDM) + \
'</span></button></a>'
tlStr += \
'<a href="' + usersPath + '/tlreplies"><button class="' + \
repliesButton + '"><span>' + \
htmlHighlightLabel(translate['Replies'], newReply) + \
'</span></button></a>'
# typically the media button
if defaultTimeline != 'tlmedia':
if not minimal and not newsHeader:
tlStr += \
'<a href="' + usersPath + \
'/tlmedia"><button class="' + \
mediaButton + '"><span>' + translate['Media'] + \
'</span></button></a>'
else:
if not minimal:
tlStr += \
'<a href="' + usersPath + \
'/inbox"><button class="' + \
inboxButton+'"><span>' + translate['Inbox'] + \
'</span></button></a>'
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 += \
'<a href="' + usersPath + \
'/tlblogs"><button class="' + \
blogsButton + '"><span>' + titleStr + \
'</span></button></a>'
else:
if not minimal:
tlStr += \
'<a href="' + usersPath + \
'/inbox"><button class="' + \
inboxButton + '"><span>' + translate['Inbox'] + \
'</span></button></a>'
# typically the news button
# but may change if this is a news oriented instance
if defaultTimeline != 'tlnews':
tlStr += \
'<a href="' + usersPath + \
'/tlnews"><button class="' + \
newsButton + '"><span>' + translate['News'] + \
'</span></button></a>'
else:
if not newsHeader:
tlStr += \
'<a href="' + usersPath + \
'/inbox"><button class="' + \
inboxButton + '"><span>' + translate['Inbox'] + \
'</span></button></a>'
# 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 += \
'<a href="' + usersPath + '/calendar?year=' + \
str(now.year) + '?month=' + str(now.month) + \
'?day=' + str(now.day) + '">' + \
'<button class="buttonevent">' + \
translate['Happening Today'] + '</button></a>'
else:
happeningStr += \
'<a href="' + usersPath + '/calendar?year=' + \
str(now.year) + '?month=' + str(now.month) + \
'?day=' + str(now.day) + '">' + \
'<button class="button">' + \
translate['Happening Today'] + '</button></a>'
# happening this week button
if thisWeeksEventsCheck(baseDir, nickname, domain):
if not iconsAsButtons:
happeningStr += \
'<a href="' + usersPath + \
'/calendar"><button class="buttonevent">' + \
translate['Happening This Week'] + '</button></a>'
else:
happeningStr += \
'<a href="' + usersPath + \
'/calendar"><button class="button">' + \
translate['Happening This Week'] + '</button></a>'
else:
# happening this week button
if thisWeeksEventsCheck(baseDir, nickname, domain):
if not iconsAsButtons:
happeningStr += \
'<a href="' + usersPath + \
'/calendar"><button class="buttonevent">' + \
translate['Happening This Week'] + '</button></a>'
else:
happeningStr += \
'<a href="' + usersPath + \
'/calendar"><button class="button">' + \
translate['Happening This Week'] + '</button></a>'
if not newsHeader:
# button for the outbox
tlStr += \
'<a href="' + usersPath + \
'/outbox"><button class="' + \
sentButton + '"><span>' + translate['Outbox'] + \
'</span></button></a>'
# add other buttons
tlStr += \
sharesButtonStr + bookmarksButtonStr + eventsButtonStr + \
moderationButtonStr + happeningStr + newPostButtonStr
if not newsHeader:
if not iconsAsButtons:
# the search icon
tlStr += \
'<a class="imageAnchor" href="' + usersPath + \
'/search"><img loading="lazy" src="/' + \
iconsPath + '/search.png" title="' + \
translate['Search and follow'] + '" alt="| ' + \
translate['Search and follow'] + \
'" class="timelineicon"/></a>'
else:
# the search button
tlStr += \
'<a href="' + usersPath + \
'/search"><button class="button">' + \
'<span>' + translate['Search'] + \
'</span></button></a>'
# 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 += \
' <a class="imageAnchor" href="' + \
usersPath + calendarPath + \
'"><img loading="lazy" src="/' + iconsPath + '/' + \
calendarImage + '" title="' + translate['Calendar'] + \
'" alt="| ' + calendarAltText + \
'" class="timelineicon"/></a>\n'
else:
tlStr += \
'<a href="' + usersPath + calendarPath + \
'"><button class="button">' + \
'<span>' + translate['Calendar'] + \
'</span></button></a>'
if not newsHeader:
# the show/hide button, for a simpler header appearance
if not iconsAsButtons:
tlStr += \
' <a class="imageAnchor" href="' + \
usersPath + '/minimal' + \
'"><img loading="lazy" src="/' + iconsPath + \
'/showhide.png" title="' + translate['Show/Hide Buttons'] + \
'" alt="| ' + translate['Show/Hide Buttons'] + \
'" class="timelineicon"/></a>\n'
else:
tlStr += \
'<a href="' + usersPath + '/minimal' + \
'"><button class="button">' + \
'<span>' + translate['Show/Hide Buttons'] + \
'</span></button></a>'
if newsHeader:
tlStr += \
'<a href="' + usersPath + '/inbox">' + \
'<button class="button">' + \
'<span>' + translate['User'] + '</span></button></a>'
# the newswire button to show right column links
if not iconsAsButtons:
tlStr += \
'<a class="imageAnchorMobile" href="' + \
usersPath + '/newswiremobile">' + \
'<img loading="lazy" src="/' + iconsPath + \
'/newswire.png" title="' + translate['News'] + \
'" alt="| ' + translate['News'] + \
'" class="timelineicon"/></a>'
else:
# NOTE: deliberately no \n at end of line
tlStr += \
'<a href="' + \
usersPath + '/newswiremobile' + \
'"><button class="buttonMobile">' + \
'<span>' + translate['Newswire'] + \
'</span></button></a>'
# the links button to show left column links
if not iconsAsButtons:
tlStr += \
'<a class="imageAnchorMobile" href="' + \
usersPath + '/linksmobile">' + \
'<img loading="lazy" src="/' + iconsPath + \
'/links.png" title="' + translate['Edit Links'] + \
'" alt="| ' + translate['Edit Links'] + \
'" class="timelineicon"/></a>'
else:
# NOTE: deliberately no \n at end of line
tlStr += \
'<a href="' + \
usersPath + '/linksmobile' + \
'"><button class="buttonMobile">' + \
'<span>' + translate['Links'] + \
'</span></button></a>'
if newsHeader:
tlStr += \
'<a href="' + usersPath + '/editprofile">' + \
'<button class="buttonDesktop">' + \
'<span>' + translate['Settings'] + '</span></button></a>'
if not iconsAsButtons:
# end of headericons div
tlStr += '</div>'
if not newsHeader:
tlStr += followApprovals
# end of the button header with inbox, outbox, etc
tlStr += ' </div>\n'
return tlStr
def htmlShares(cssCache: {}, defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,

View File

@ -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 = \
'<div class="postSeparatorImage"><center>' + \
'<div class="' + separatorClass + '"><center>' + \
'<img src="/' + iconsPath + '/' + filename + '"/>' + \
'</center></div>\n'
return separatorStr
@ -808,3 +810,14 @@ def headerButtonsFrontScreen(translate: {},
headerStr + \
' </div>\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) + '*'