diff --git a/content.py b/content.py index 1d8334412..bc3db29e6 100644 --- a/content.py +++ b/content.py @@ -788,6 +788,13 @@ def addHtmlTags(baseDir: str, httpPrefix: str, prevWordStr = '' continue elif firstChar == '#': + # remove any endings from the hashtag + hashTagEndings = ('.', ':', ';', '-', '\n') + for ending in hashTagEndings: + if wordStr.endswith(ending): + wordStr = wordStr[:len(wordStr) - 1] + break + if _addHashTags(wordStr, httpPrefix, originalDomain, replaceHashTags, hashtags): prevWordStr = '' diff --git a/daemon.py b/daemon.py index 3e091bebc..d4e9dd30c 100644 --- a/daemon.py +++ b/daemon.py @@ -218,6 +218,7 @@ from utils import loadJson from utils import saveJson from utils import isSuspended from utils import dangerousMarkup +from utils import refreshNewswire from manualapprove import manualDenyFollowRequest from manualapprove import manualApproveFollowRequest from announce import createAnnounce @@ -255,7 +256,6 @@ from newswire import rss2Footer from newswire import loadHashtagCategories from newsdaemon import runNewswireWatchdog from newsdaemon import runNewswireDaemon -from newsdaemon import refreshNewswire from filters import isFiltered from filters import addGlobalFilter from filters import removeGlobalFilter @@ -1948,12 +1948,50 @@ class PubServer(BaseHTTPRequestHandler): if postsToNews == 'on': if os.path.isfile(newswireBlockedFilename): os.remove(newswireBlockedFilename) + refreshNewswire(self.server.baseDir) else: if os.path.isdir(accountDir): noNewswireFile = open(newswireBlockedFilename, "w+") if noNewswireFile: noNewswireFile.write('\n') noNewswireFile.close() + refreshNewswire(self.server.baseDir) + usersPathStr = \ + usersPath + '/' + self.server.defaultTimeline + \ + '?page=' + str(pageNumber) + self._redirect_headers(usersPathStr, cookie, + callingDomain) + self.server.POSTbusy = False + return + + # person options screen, permission to post to featured articles + # See htmlPersonOptions + if '&submitPostToFeatures=' in optionsConfirmParams: + adminNickname = getConfigParam(self.server.baseDir, 'admin') + if (chooserNickname != optionsNickname and + (chooserNickname == adminNickname or + (isModerator(self.server.baseDir, chooserNickname) and + not isModerator(self.server.baseDir, optionsNickname)))): + postsToFeatures = None + if 'postsToFeatures=' in optionsConfirmParams: + postsToFeatures = \ + optionsConfirmParams.split('postsToFeatures=')[1] + if '&' in postsToFeatures: + postsToFeatures = postsToFeatures.split('&')[0] + accountDir = self.server.baseDir + '/accounts/' + \ + optionsNickname + '@' + optionsDomain + featuresBlockedFilename = accountDir + '/.nofeatures' + if postsToFeatures == 'on': + if os.path.isfile(featuresBlockedFilename): + os.remove(featuresBlockedFilename) + refreshNewswire(self.server.baseDir) + else: + if os.path.isdir(accountDir): + noFeaturesFile = open(featuresBlockedFilename, "w+") + if noFeaturesFile: + noFeaturesFile.write('\n') + noFeaturesFile.close() + refreshNewswire(self.server.baseDir) usersPathStr = \ usersPath + '/' + self.server.defaultTimeline + \ '?page=' + str(pageNumber) @@ -5469,7 +5507,9 @@ class PubServer(BaseHTTPRequestHandler): self.server.dormantMonths, backToPath, lockedAccount, - movedTo, alsoKnownAs).encode('utf-8') + movedTo, alsoKnownAs, + self.server.textModeBanner, + self.server.newsInstance).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, callingDomain) @@ -11085,7 +11125,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.translate, self.server.baseDir, self.path, self.server.httpPrefix, - self.server.domainFull).encode('utf-8') + self.server.domainFull, + self.server.textModeBanner).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, callingDomain) self._write(msg) diff --git a/emoji/android.png b/emoji/android.png new file mode 100644 index 000000000..2fb586588 Binary files /dev/null and b/emoji/android.png differ diff --git a/emoji/default_emoji.json b/emoji/default_emoji.json index 9f86c0384..f241708d0 100644 --- a/emoji/default_emoji.json +++ b/emoji/default_emoji.json @@ -1,4 +1,5 @@ { + "android": "android", "popcorn": "1F37F", "1stplacemedal": "1F947", "abbutton": "1F18E", diff --git a/epicyon-calendar.css b/epicyon-calendar.css index 223f3836a..f9d48b826 100644 --- a/epicyon-calendar.css +++ b/epicyon-calendar.css @@ -84,6 +84,14 @@ a:focus { border: 2px solid var(--focus-color); } +.transparent { + color: transparent; + background: transparent; + font-size: 0px; + line-height: 0px; + height: 0px; +} + .calendar__day__header, .calendar__day__cell { border: 2px solid var(--lines-color); diff --git a/epicyon-options.css b/epicyon-options.css index aaff9989b..f7093c1a7 100644 --- a/epicyon-options.css +++ b/epicyon-options.css @@ -98,6 +98,14 @@ a:focus { border: 2px solid var(--focus-color); } +.transparent { + color: transparent; + background: transparent; + font-size: 0px; + line-height: 0px; + height: 0px; +} + .follow { height: 100%; position: relative; diff --git a/newsdaemon.py b/newsdaemon.py index e52932072..a32628428 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -750,15 +750,3 @@ def runNewswireWatchdog(projectVersion: str, httpd) -> None: newswireOriginal.clone(runNewswireDaemon) httpd.thrNewswireDaemon.start() print('Restarting newswire daemon...') - - -def refreshNewswire(baseDir: str) -> None: - """Causes the newswire to be updated. - This creates a file which is then detected by the daemon - """ - refreshFilename = baseDir + '/accounts/.refresh_newswire' - if os.path.isfile(refreshFilename): - return - refreshFile = open(refreshFilename, 'w+') - refreshFile.write('\n') - refreshFile.close() diff --git a/newswire.py b/newswire.py index ccaf983bf..29abb2428 100644 --- a/newswire.py +++ b/newswire.py @@ -7,6 +7,7 @@ __email__ = "bob@freedombone.net" __status__ = "Production" import os +import json import requests from socket import error as SocketError import errno @@ -332,12 +333,14 @@ def _xml2StrToDict(baseDir: str, domain: str, xmlStr: str, result, pubDateStr, title, link, votesStatus, postFilename, - description, moderated, mirrored) + description, moderated, + mirrored) postCtr += 1 if postCtr >= maxPostsPerSource: break if postCtr > 0: - print('Added ' + str(postCtr) + ' rss 2.0 feed items to newswire') + print('Added ' + str(postCtr) + + ' rss 2.0 feed items to newswire') return result @@ -416,12 +419,14 @@ def _xml1StrToDict(baseDir: str, domain: str, xmlStr: str, result, pubDateStr, title, link, votesStatus, postFilename, - description, moderated, mirrored) + description, moderated, + mirrored) postCtr += 1 if postCtr >= maxPostsPerSource: break if postCtr > 0: - print('Added ' + str(postCtr) + ' rss 1.0 feed items to newswire') + print('Added ' + str(postCtr) + + ' rss 1.0 feed items to newswire') return result @@ -488,12 +493,124 @@ def _atomFeedToDict(baseDir: str, domain: str, xmlStr: str, result, pubDateStr, title, link, votesStatus, postFilename, - description, moderated, mirrored) + description, moderated, + mirrored) postCtr += 1 if postCtr >= maxPostsPerSource: break if postCtr > 0: - print('Added ' + str(postCtr) + ' atom feed items to newswire') + print('Added ' + str(postCtr) + + ' atom feed items to newswire') + return result + + +def _jsonFeedV1ToDict(baseDir: str, domain: str, xmlStr: str, + moderated: bool, mirrored: bool, + maxPostsPerSource: int, + maxFeedItemSizeKb: int) -> {}: + """Converts a json feed string to a dictionary + See https://jsonfeed.org/version/1.1 + """ + if '"items"' not in xmlStr: + return {} + try: + feedJson = json.loads(xmlStr) + except BaseException: + return {} + maxBytes = maxFeedItemSizeKb * 1024 + if not feedJson.get('version'): + return {} + if not feedJson['version'].startswith('https://jsonfeed.org/version/1'): + return {} + if not feedJson.get('items'): + return {} + if not isinstance(feedJson['items'], list): + return {} + postCtr = 0 + result = {} + for jsonFeedItem in feedJson['items']: + if not jsonFeedItem: + continue + if not isinstance(jsonFeedItem, dict): + continue + if not jsonFeedItem.get('url'): + continue + if not isinstance(jsonFeedItem['url'], str): + continue + if not jsonFeedItem.get('date_published'): + if not jsonFeedItem.get('date_modified'): + continue + if not jsonFeedItem.get('content_text'): + if not jsonFeedItem.get('content_html'): + continue + if jsonFeedItem.get('content_html'): + if not isinstance(jsonFeedItem['content_html'], str): + continue + title = removeHtml(jsonFeedItem['content_html']) + else: + if not isinstance(jsonFeedItem['content_text'], str): + continue + title = removeHtml(jsonFeedItem['content_text']) + if len(title) > maxBytes: + print('WARN: json feed title is too long') + continue + description = '' + if jsonFeedItem.get('description'): + if not isinstance(jsonFeedItem['description'], str): + continue + description = removeHtml(jsonFeedItem['description']) + if len(description) > maxBytes: + print('WARN: json feed description is too long') + continue + if jsonFeedItem.get('tags'): + if isinstance(jsonFeedItem['tags'], list): + for tagName in jsonFeedItem['tags']: + if not isinstance(tagName, str): + continue + if ' ' in tagName: + continue + if not tagName.startswith('#'): + tagName = '#' + tagName + if tagName not in description: + description += ' ' + tagName + + link = jsonFeedItem['url'] + if '://' not in link: + continue + if len(link) > maxBytes: + print('WARN: json feed link is too long') + continue + itemDomain = link.split('://')[1] + if '/' in itemDomain: + itemDomain = itemDomain.split('/')[0] + if isBlockedDomain(baseDir, itemDomain): + continue + if jsonFeedItem.get('date_published'): + if not isinstance(jsonFeedItem['date_published'], str): + continue + pubDate = jsonFeedItem['date_published'] + else: + if not isinstance(jsonFeedItem['date_modified'], str): + continue + pubDate = jsonFeedItem['date_modified'] + + pubDateStr = parseFeedDate(pubDate) + if pubDateStr: + if _validFeedDate(pubDateStr): + postFilename = '' + votesStatus = [] + _addNewswireDictEntry(baseDir, domain, + result, pubDateStr, + title, link, + votesStatus, postFilename, + description, moderated, + mirrored) + postCtr += 1 + if postCtr >= maxPostsPerSource: + break + if postCtr > 0: + print('Added ' + str(postCtr) + + ' json feed items to newswire') return result @@ -593,6 +710,10 @@ def _xmlStrToDict(baseDir: str, domain: str, xmlStr: str, return _atomFeedToDict(baseDir, domain, xmlStr, moderated, mirrored, maxPostsPerSource, maxFeedItemSizeKb) + elif 'https://jsonfeed.org/version/1' in xmlStr: + return _jsonFeedV1ToDict(baseDir, domain, + xmlStr, moderated, mirrored, + maxPostsPerSource, maxFeedItemSizeKb) return {} @@ -794,7 +915,7 @@ def _addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, locatePost(baseDir, nickname, domain, postUrl, False) if not fullPostFilename: - print('Unable to locate post ' + postUrl) + print('Unable to locate post for newswire ' + postUrl) ctr += 1 if ctr >= maxBlogsPerAccount: break @@ -840,7 +961,7 @@ def _addBlogsToNewswire(baseDir: str, domain: str, newswire: {}, for handle in dirs: if '@' not in handle: continue - if 'inbox@' in handle: + if 'inbox@' in handle or 'news@' in handle: continue nickname = handle.split('@')[0] diff --git a/outbox.py b/outbox.py index e50000386..2c3fda855 100644 --- a/outbox.py +++ b/outbox.py @@ -18,6 +18,7 @@ from utils import getFullDomain from utils import removeIdEnding from utils import getDomainFromActor from utils import dangerousMarkup +from utils import isFeaturedWriter from blocking import isBlockedDomain from blocking import outboxBlock from blocking import outboxUndoBlock @@ -211,14 +212,16 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str, # save all instance blogs to the news actor if postToNickname != 'news' and outboxName == 'tlblogs': if '/' in savedFilename: - savedPostId = savedFilename.split('/')[-1] - blogsDir = baseDir + '/accounts/news@' + domain + '/tlblogs' - if not os.path.isdir(blogsDir): - os.mkdir(blogsDir) - copyfile(savedFilename, blogsDir + '/' + savedPostId) - inboxUpdateIndex('tlblogs', baseDir, - 'news@' + domain, - savedFilename, debug) + if isFeaturedWriter(baseDir, postToNickname, domain): + savedPostId = savedFilename.split('/')[-1] + blogsDir = \ + baseDir + '/accounts/news@' + domain + '/tlblogs' + if not os.path.isdir(blogsDir): + os.mkdir(blogsDir) + copyfile(savedFilename, blogsDir + '/' + savedPostId) + inboxUpdateIndex('tlblogs', baseDir, + 'news@' + domain, + savedFilename, debug) # clear the citations file if it exists citationsFilename = \ diff --git a/person.py b/person.py index 71422ac42..b6bf9baf4 100644 --- a/person.py +++ b/person.py @@ -40,6 +40,7 @@ from utils import loadJson from utils import saveJson from utils import setConfigParam from utils import getConfigParam +from utils import refreshNewswire def generateRSAKey() -> (str, str): @@ -915,6 +916,9 @@ def removeAccount(baseDir: str, nickname: str, os.remove(baseDir + '/wfdeactivated/' + handle + '.json') if os.path.isdir(baseDir + '/sharefilesdeactivated/' + nickname): shutil.rmtree(baseDir + '/sharefilesdeactivated/' + nickname) + + refreshNewswire(baseDir) + return True @@ -944,6 +948,9 @@ def deactivateAccount(baseDir: str, nickname: str, domain: str) -> bool: os.mkdir(deactivatedSharefilesDir) shutil.move(baseDir + '/sharefiles/' + nickname, deactivatedSharefilesDir + '/' + nickname) + + refreshNewswire(baseDir) + return os.path.isdir(deactivatedDir + '/' + nickname + '@' + domain) @@ -970,6 +977,8 @@ def activateAccount(baseDir: str, nickname: str, domain: str) -> None: shutil.move(deactivatedSharefilesDir + '/' + nickname, baseDir + '/sharefiles/' + nickname) + refreshNewswire(baseDir) + def isPersonSnoozed(baseDir: str, nickname: str, domain: str, snoozeActor: str) -> bool: diff --git a/translations/ar.json b/translations/ar.json index b3a408074..605d5b98b 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -367,5 +367,6 @@ "Skip to timeline": "تخطي إلى الجدول الزمني", "Skip to Newswire": "انتقل إلى Newswire", "Skip to Links": "تخطي إلى روابط الويب", - "Publish a blog article": "نشر مقال بلوق" + "Publish a blog article": "نشر مقال بلوق", + "Featured writer": "كاتب متميز" } diff --git a/translations/ca.json b/translations/ca.json index 232ee4300..3c8846bbf 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -367,5 +367,6 @@ "Skip to timeline": "Ves a la cronologia", "Skip to Newswire": "Vés a Newswire", "Skip to Links": "Vés als enllaços web", - "Publish a blog article": "Publicar un article del bloc" + "Publish a blog article": "Publicar un article del bloc", + "Featured writer": "Escriptor destacat" } diff --git a/translations/cy.json b/translations/cy.json index 55b4b95e1..6e45de39d 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -367,5 +367,6 @@ "Skip to timeline": "Neidio i'r llinell amser", "Skip to Newswire": "Neidio i Newswire", "Skip to Links": "Neidio i Dolenni Gwe", - "Publish a blog article": "Cyhoeddi erthygl blog" + "Publish a blog article": "Cyhoeddi erthygl blog", + "Featured writer": "Awdur dan sylw" } diff --git a/translations/de.json b/translations/de.json index 140712ebd..c15cb492f 100644 --- a/translations/de.json +++ b/translations/de.json @@ -367,5 +367,6 @@ "Skip to timeline": "Zur Zeitleiste springen", "Skip to Newswire": "Springe zu Newswire", "Skip to Links": "Springe zu Weblinks", - "Publish a blog article": "Veröffentlichen Sie einen Blog-Artikel" + "Publish a blog article": "Veröffentlichen Sie einen Blog-Artikel", + "Featured writer": "Ausgewählter Schriftsteller" } diff --git a/translations/en.json b/translations/en.json index 77da14052..3fbbfdd34 100644 --- a/translations/en.json +++ b/translations/en.json @@ -367,5 +367,6 @@ "Skip to timeline": "Skip to timeline", "Skip to Newswire": "Skip to Newswire", "Skip to Links": "Skip to Links", - "Publish a blog article": "Publish a blog article" + "Publish a blog article": "Publish a blog article", + "Featured writer": "Featured writer" } diff --git a/translations/es.json b/translations/es.json index cf09d1446..3566d06b0 100644 --- a/translations/es.json +++ b/translations/es.json @@ -367,5 +367,6 @@ "Skip to timeline": "Saltar a la línea de tiempo", "Skip to Newswire": "Saltar a Newswire", "Skip to Links": "Saltar a enlaces web", - "Publish a blog article": "Publica un artículo de blog" + "Publish a blog article": "Publica un artículo de blog", + "Featured writer": "Escritora destacada" } diff --git a/translations/fr.json b/translations/fr.json index ac51b24fa..78c7ff547 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -367,5 +367,6 @@ "Skip to timeline": "Passer à la chronologie", "Skip to Newswire": "Passer à Newswire", "Skip to Links": "Passer aux liens Web", - "Publish a blog article": "Publier un article de blog" + "Publish a blog article": "Publier un article de blog", + "Featured writer": "Écrivain en vedette" } diff --git a/translations/ga.json b/translations/ga.json index 5cff4c525..75f87780c 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -367,5 +367,6 @@ "Skip to timeline": "Scipeáil chuig an amlíne", "Skip to Newswire": "Scipeáil chuig Newswire", "Skip to Links": "Scipeáil chuig Naisc Ghréasáin", - "Publish a blog article": "Foilsigh alt blagála" + "Publish a blog article": "Foilsigh alt blagála", + "Featured writer": "Scríbhneoir mór le rá" } diff --git a/translations/hi.json b/translations/hi.json index a83d8d5a9..acf417f88 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -367,5 +367,6 @@ "Skip to timeline": "टाइमलाइन पर जाएं", "Skip to Newswire": "Newswire पर जाएं", "Skip to Links": "वेब लिंक पर जाएं", - "Publish a blog article": "एक ब्लॉग लेख प्रकाशित करें" + "Publish a blog article": "एक ब्लॉग लेख प्रकाशित करें", + "Featured writer": "फीचर्ड लेखक" } diff --git a/translations/it.json b/translations/it.json index e7082ff98..f2bc1e1ed 100644 --- a/translations/it.json +++ b/translations/it.json @@ -367,5 +367,6 @@ "Skip to timeline": "Passa alla sequenza temporale", "Skip to Newswire": "Passa a Newswire", "Skip to Links": "Passa a collegamenti Web", - "Publish a blog article": "Pubblica un articolo sul blog" + "Publish a blog article": "Pubblica un articolo sul blog", + "Featured writer": "Scrittore in primo piano" } diff --git a/translations/ja.json b/translations/ja.json index 38d6685a4..9f1275544 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -367,5 +367,6 @@ "Skip to timeline": "タイムラインにスキップ", "Skip to Newswire": "Newswireにスキップ", "Skip to Links": "Webリンクにスキップ", - "Publish a blog article": "ブログ記事を公開する" + "Publish a blog article": "ブログ記事を公開する", + "Featured writer": "注目の作家" } diff --git a/translations/oc.json b/translations/oc.json index 7cd70a858..c9e3717a0 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -363,5 +363,6 @@ "Skip to timeline": "Skip to timeline", "Skip to Newswire": "Skip to Newswire", "Skip to Links": "Skip to Links", - "Publish a blog article": "Publish a blog article" + "Publish a blog article": "Publish a blog article", + "Featured writer": "Featured writer" } diff --git a/translations/pt.json b/translations/pt.json index 2abe336bf..b2a29ca2d 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -367,5 +367,6 @@ "Skip to timeline": "Pular para a linha do tempo", "Skip to Newswire": "Pular para Newswire", "Skip to Links": "Pular para links da web", - "Publish a blog article": "Publique um artigo de blog" + "Publish a blog article": "Publique um artigo de blog", + "Featured writer": "Escritor em destaque" } diff --git a/translations/ru.json b/translations/ru.json index b75628208..618c5cbf0 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -367,5 +367,6 @@ "Skip to timeline": "Перейти к временной шкале", "Skip to Newswire": "Перейти к ленте новостей", "Skip to Links": "Перейти к веб-ссылкам", - "Publish a blog article": "Опубликовать статью в блоге" + "Publish a blog article": "Опубликовать статью в блоге", + "Featured writer": "Избранный писатель" } diff --git a/translations/zh.json b/translations/zh.json index ea0dcce39..e7fab5755 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -367,5 +367,6 @@ "Skip to timeline": "跳到时间线", "Skip to Newswire": "跳到新闻专线", "Skip to Links": "跳到网页链接", - "Publish a blog article": "发布博客文章" + "Publish a blog article": "发布博客文章", + "Featured writer": "特色作家" } diff --git a/utils.py b/utils.py index 5a58d45ce..f1247e898 100644 --- a/utils.py +++ b/utils.py @@ -26,6 +26,27 @@ invalidCharacters = ( ) +def isFeaturedWriter(baseDir: str, nickname: str, domain: str) -> bool: + """Is the given account a featured writer, appearing in the features + timeline on news instances? + """ + featuresBlockedFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + '/.nofeatures' + return not os.path.isfile(featuresBlockedFilename) + + +def refreshNewswire(baseDir: str): + """Causes the newswire to be updates after a change to user accounts + """ + refreshNewswireFilename = baseDir + '/accounts/.refresh_newswire' + if os.path.isfile(refreshNewswireFilename): + return + refreshFile = open(refreshNewswireFilename, 'w+') + refreshFile.write('\n') + refreshFile.close() + + def getSHA256(msg: str): """Returns a SHA256 hash of the given string """ diff --git a/webapp_calendar.py b/webapp_calendar.py index 0135cc08e..025614e79 100644 --- a/webapp_calendar.py +++ b/webapp_calendar.py @@ -21,6 +21,8 @@ from happening import getCalendarEvents from webapp_utils import htmlHeaderWithExternalStyle from webapp_utils import htmlFooter from webapp_utils import getAltPath +from webapp_utils import htmlHideFromScreenReader +from webapp_utils import htmlKeyboardNavigation def htmlCalendarDeleteConfirm(cssCache: {}, translate: {}, baseDir: str, @@ -200,7 +202,8 @@ def _htmlCalendarDay(cssCache: {}, translate: {}, def htmlCalendar(cssCache: {}, translate: {}, baseDir: str, path: str, - httpPrefix: str, domainFull: str) -> str: + httpPrefix: str, domainFull: str, + textModeBanner: str) -> str: """Show the calendar for a person """ domain = domainFull @@ -297,8 +300,10 @@ def htmlCalendar(cssCache: {}, translate: {}, instanceTitle = \ getConfigParam(baseDir, 'instanceTitle') - calendarStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle) - calendarStr += '
\n' + headerStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle) + + # the main graphical calendar as a table + calendarStr = '
\n' calendarStr += '\n' - calendarStr += ' \n' - calendarStr += ' \n' - calendarStr += ' \n' - calendarStr += ' \n' - calendarStr += ' \n' - calendarStr += ' \n' calendarStr += '\n' calendarStr += '\n' calendarStr += '\n' + # beginning of the links used for accessibility + navLinks = {} + timelineLinkStr = htmlHideFromScreenReader('🏠') + ' ' + \ + translate['Switch to timeline view'] + navLinks[timelineLinkStr] = calActor + '/inbox' + dayOfMonth = 0 dow = weekDayOfMonthStart(monthNumber, year) for weekOfMonth in range(1, 7): @@ -358,8 +369,15 @@ def htmlCalendar(cssCache: {}, translate: {}, url = calActor + '/calendar?year=' + \ str(year) + '?month=' + \ str(monthNumber) + '?day=' + str(dayOfMonth) - dayLink = '' + \ + dayDescription = monthName + ' ' + str(dayOfMonth) + dayLink = '' + \ str(dayOfMonth) + '' + # accessibility menu links + menuOptionStr = \ + htmlHideFromScreenReader('📅') + ' ' + \ + dayDescription + navLinks[menuOptionStr] = url # there are events for this day if not isToday: calendarStr += \ @@ -387,5 +405,17 @@ def htmlCalendar(cssCache: {}, translate: {}, calendarStr += '\n' calendarStr += '
\n' calendarStr += \ ' ' + \ + calendarStr += '
' + \ translate['Sun'] + '' + \ + calendarStr += ' ' + \ translate['Mon'] + '' + \ + calendarStr += ' ' + \ translate['Tue'] + '' + \ + calendarStr += ' ' + \ translate['Wed'] + '' + \ + calendarStr += ' ' + \ translate['Thu'] + '' + \ + calendarStr += ' ' + \ translate['Fri'] + '' + \ + calendarStr += ' ' + \ translate['Sat'] + '
\n' - calendarStr += htmlFooter() - return calendarStr + + # end of the links used for accessibility + nextMonthStr = \ + htmlHideFromScreenReader('→') + ' ' + translate['Next month'] + navLinks[nextMonthStr] = calActor + '/calendar?year=' + str(nextYear) + \ + '?month=' + str(nextMonthNumber) + prevMonthStr = \ + htmlHideFromScreenReader('←') + ' ' + translate['Previous month'] + navLinks[prevMonthStr] = calActor + '/calendar?year=' + str(prevYear) + \ + '?month=' + str(prevMonthNumber) + screenReaderCal = \ + htmlKeyboardNavigation(textModeBanner, navLinks, monthName) + + return headerStr + screenReaderCal + calendarStr + htmlFooter() diff --git a/webapp_person_options.py b/webapp_person_options.py index 3dc819f7b..75dd21045 100644 --- a/webapp_person_options.py +++ b/webapp_person_options.py @@ -17,6 +17,7 @@ from utils import isDormant from utils import removeHtml from utils import getDomainFromActor from utils import getNicknameFromActor +from utils import isFeaturedWriter from blocking import isBlocked from follow import isFollowerOfPerson from follow import isFollowingActor @@ -24,6 +25,7 @@ from followingCalendar import receivingCalendarEvents from webapp_utils import htmlHeaderWithExternalStyle from webapp_utils import htmlFooter from webapp_utils import getBrokenLinkSubstitute +from webapp_utils import htmlKeyboardNavigation def htmlPersonOptions(defaultTimeline: str, @@ -49,7 +51,9 @@ def htmlPersonOptions(defaultTimeline: str, backToPath: str, lockedAccount: bool, movedTo: str, - alsoKnownAs: []) -> str: + alsoKnownAs: [], + textModeBanner: str, + newsInstance: bool) -> str: """Show options for a person: view/follow/block/report """ optionsDomain, optionsPort = getDomainFromActor(optionsActor) @@ -108,12 +112,13 @@ def htmlPersonOptions(defaultTimeline: str, if donateUrl: donateStr = \ ' \n' instanceTitle = \ getConfigParam(baseDir, 'instanceTitle') optionsStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle) + optionsStr += htmlKeyboardNavigation(textModeBanner, {}) optionsStr += '

\n' optionsStr += '
\n' optionsStr += '
\n' @@ -284,6 +289,24 @@ def htmlPersonOptions(defaultTimeline: str, checkboxStr = checkboxStr.replace(' checked>', '>') optionsStr += checkboxStr + # checkbox for permission to post to featured articles + if newsInstance and optionsDomainFull == domainFull: + adminNickname = getConfigParam(baseDir, 'admin') + if (nickname == adminNickname or + (isModerator(baseDir, nickname) and + not isModerator(baseDir, optionsNickname))): + checkboxStr = \ + ' ' + \ + translate['Featured writer'] + \ + '\n
\n' + if not isFeaturedWriter(baseDir, optionsNickname, + optionsDomain): + checkboxStr = checkboxStr.replace(' checked>', '>') + optionsStr += checkboxStr + optionsStr += optionsLinkStr backPath = '/' if nickname: diff --git a/webapp_post.py b/webapp_post.py index c9505c819..b5646c416 100644 --- a/webapp_post.py +++ b/webapp_post.py @@ -327,6 +327,10 @@ def _getEditIconHtml(baseDir: str, nickname: str, domainFull: str, """ editStr = '' actor = postJsonObject['actor'] + # This should either be a post which you created, + # or it could be generated from the newswire (see + # _addBlogsToNewswire) in which case anyone with + # editor status should be able to alter it if (actor.endswith('/' + domainFull + '/users/' + nickname) or (isEditor(baseDir, nickname) and actor.endswith('/' + domainFull + '/users/news'))): diff --git a/webapp_profile.py b/webapp_profile.py index b4859a6d9..8c7704390 100644 --- a/webapp_profile.py +++ b/webapp_profile.py @@ -1745,6 +1745,10 @@ def _individualFollowAsHtml(translate: {}, if avatarUrl2: avatarUrl = avatarUrl2 if displayName: + displayName = \ + addEmojiToDisplayName(baseDir, httpPrefix, + actorNickname, domain, + displayName, False) titleStr = displayName if dormant: diff --git a/webapp_timeline.py b/webapp_timeline.py index f3707b826..0c0407520 100644 --- a/webapp_timeline.py +++ b/webapp_timeline.py @@ -432,7 +432,7 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, } if moderator: navLinks[menuModeration] = usersPath + '/moderation#modtimeline' - tlStr += htmlKeyboardNavigation(textModeBanner, navLinks, + tlStr += htmlKeyboardNavigation(textModeBanner, navLinks, None, usersPath, translate, followApprovals) # banner and row of buttons diff --git a/webapp_utils.py b/webapp_utils.py index 1511b1408..014b42a19 100644 --- a/webapp_utils.py +++ b/webapp_utils.py @@ -887,6 +887,7 @@ def htmlHideFromScreenReader(htmlStr: str) -> str: def htmlKeyboardNavigation(banner: str, links: {}, + subHeading=None, usersPath=None, translate=None, followApprovals=False) -> str: """Given a set of links return the html for keyboard navigation @@ -896,6 +897,10 @@ def htmlKeyboardNavigation(banner: str, links: {}, if banner: htmlStr += '
' + banner + '

' + if subHeading: + htmlStr += '
' + # show new follower approvals if usersPath and translate and followApprovals: htmlStr += '