From 8ed3f132be48179b1c2cc364855be8340714ab01 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 28 Sep 2019 12:29:42 +0100 Subject: [PATCH] Media timeline --- daemon.py | 73 +++++++++++++++++++++++++++++++++++++++++++++ epicyon-profile.css | 31 +++++++++++++++++++ posts.py | 45 ++++++++++++++++++++++++---- webinterface.py | 46 +++++++++++++++++++++++----- 4 files changed, 183 insertions(+), 12 deletions(-) diff --git a/daemon.py b/daemon.py index 5c822409..7559fc16 100644 --- a/daemon.py +++ b/daemon.py @@ -85,6 +85,7 @@ from webinterface import htmlAbout from webinterface import htmlRemoveSharedItem from webinterface import htmlInboxDMs from webinterface import htmlInboxReplies +from webinterface import htmlInboxMedia from webinterface import htmlUnblockConfirm from webinterface import htmlPersonOptions from webinterface import htmlIndividualPost @@ -1478,6 +1479,7 @@ class PubServer(BaseHTTPRequestHandler): self._404() self.server.GETbusy=False return + # get replies to a post /users/nickname/statuses/number/replies if self.path.endswith('/replies') or '/replies?page=' in self.path: if '/statuses/' in self.path and '/users/' in self.path: @@ -1941,6 +1943,77 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy=False return + # get the media for a given person + if self.path.endswith('/tlmedia') or '/tlmedia?page=' in self.path: + if '/users/' in self.path: + if authorized: + inboxMediaFeed= \ + personBoxJson(self.server.baseDir, \ + self.server.domain, \ + self.server.port, \ + self.path, \ + self.server.httpPrefix, \ + maxPostsInFeed, 'tlmedia', \ + True,self.server.ocapAlways) + if not inboxMediaFeed: + inboxMediaFeed=[] + if self._requestHTTP(): + nickname=self.path.replace('/users/','').replace('/tlmedia','') + pageNumber=1 + if '?page=' in nickname: + pageNumber=nickname.split('?page=')[1] + nickname=nickname.split('?page=')[0] + if pageNumber.isdigit(): + pageNumber=int(pageNumber) + else: + pageNumber=1 + if 'page=' not in self.path: + # if no page was specified then show the first + inboxMediaFeed= \ + personBoxJson(self.server.baseDir, \ + self.server.domain, \ + self.server.port, \ + self.path+'?page=1', \ + self.server.httpPrefix, \ + maxPostsInFeed, 'tlmedia', \ + True,self.server.ocapAlways) + msg=htmlInboxMedia(self.server.translate, \ + pageNumber,maxPostsInFeed, \ + self.server.session, \ + self.server.baseDir, \ + self.server.cachedWebfingers, \ + self.server.personCache, \ + nickname, \ + self.server.domain, \ + self.server.port, \ + inboxMediaFeed, \ + self.server.allowDeletion, \ + self.server.httpPrefix, \ + self.server.projectVersion).encode('utf-8') + self._set_headers('text/html',len(msg),cookie) + self.wfile.write(msg) + else: + # don't need authenticated fetch here because there is + # already the authorization check + msg=json.dumps(inboxMediaFeed).encode('utf-8') + self._set_headers('application/json',len(msg),None) + self.wfile.write(msg) + self.server.GETbusy=False + return + else: + if self.server.debug: + nickname=self.path.replace('/users/','').replace('/tlmedia','') + print('DEBUG: '+nickname+ \ + ' was not authorized to access '+self.path) + if self.path!='/tlmedia': + # not the media inbox + if self.server.debug: + print('DEBUG: GET access to inbox is unauthorized') + self.send_response(405) + self.end_headers() + self.server.GETbusy=False + return + # get outbox feed for a person outboxFeed=personBoxJson(self.server.baseDir,self.server.domain, \ self.server.port,self.path, \ diff --git a/epicyon-profile.css b/epicyon-profile.css index f06240b6..18bdf63c 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -28,6 +28,8 @@ --button-height: 10px; --button-height-padding-mobile: 20px; --button-height-padding: 10px; + --gallery-border: #ccc; + --gallery-hover: #777; } body, html { @@ -760,10 +762,35 @@ input[type=checkbox] transform: translateY(-10%); } +div.gallery { + margin: 5px; + border: 1px solid var(--gallery-border); + float: left; + width: 180px; +} + +div.gallery:hover { + border: 1px solid var(--gallery-hover); +} + +div.gallery img { + width: 100%; + height: auto; +} + +div.imagedesc { + padding: 22px; + text-align: center; +} + @media screen and (min-width: 400px) { body, html { font-size: 22px; } + div.imagedesc { + padding: 22px; + text-align: center; + } .container img { float: left; max-width: 400px; @@ -989,6 +1016,10 @@ input[type=checkbox] body, html { font-size: 35px; } + div.imagedesc { + padding: 35px; + text-align: center; + } .container img { float: left; max-width: 400px; diff --git a/posts.py b/posts.py index 4ae0a90a..d3c1c265 100644 --- a/posts.py +++ b/posts.py @@ -1585,6 +1585,11 @@ def createRepliesTimeline(baseDir: str,nickname: str,domain: str,port: int,httpP return createBoxBase(baseDir,'tlreplies',nickname,domain,port,httpPrefix, \ itemsPerPage,headerOnly,True,ocapAlways,pageNumber) +def createMediaTimeline(baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str, \ + itemsPerPage: int,headerOnly: bool,ocapAlways: bool,pageNumber=None) -> {}: + return createBoxBase(baseDir,'tlmedia',nickname,domain,port,httpPrefix, \ + itemsPerPage,headerOnly,True,ocapAlways,pageNumber) + def createOutbox(baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str, \ itemsPerPage: int,headerOnly: bool,authorized: bool,pageNumber=None) -> {}: return createBoxBase(baseDir,'outbox',nickname,domain,port,httpPrefix, \ @@ -1684,6 +1689,30 @@ def isDM(postJsonObject: {}) -> bool: return False return True +def isImageMedia(postJsonObject: {}) -> bool: + """Returns true if the given post has attached image media + """ + if postJsonObject['type']!='Create': + return False + if not postJsonObject.get('object'): + return False + if not isinstance(postJsonObject['object'], dict): + return False + if postJsonObject['object']['type']!='Note': + return False + if not postJsonObject['object'].get('attachment'): + return False + if not isinstance(postJsonObject['object']['attachment'], list): + return False + if len(postJsonObject['object']['attachment'])==0: + return False + for attach in postJsonObject['object']['attachment']: + if attach.get('mediaType') and attach.get('url'): + mediaType=attach['mediaType'] + if mediaType.startswith('image/'): + return True + return False + def isReply(postJsonObject: {},actor: str) -> bool: """Returns true if the given post is a reply to the given actor """ @@ -1707,15 +1736,16 @@ def createBoxBase(baseDir: str,boxname: str, \ ocapAlways: bool,pageNumber=None) -> {}: """Constructs the box feed for a person with the given nickname """ - if boxname!='inbox' and boxname!='dm' and boxname!='tlreplies' and boxname!='outbox': + if boxname!='inbox' and boxname!='dm' and \ + boxname!='tlreplies' and boxname!='tlmedia' and boxname!='outbox': return None - if boxname!='dm' and boxname!='tlreplies': + if boxname!='dm' and boxname!='tlreplies' and boxname!='tlmedia': boxDir = createPersonDir(nickname,domain,baseDir,boxname) else: # extract DMs or replies from the inbox boxDir = createPersonDir(nickname,domain,baseDir,'inbox') sharedBoxDir=None - if boxname=='inbox' or boxname=='tlreplies': + if boxname=='inbox' or boxname=='tlreplies' or boxname=='tlmedia': sharedBoxDir = createPersonDir('inbox',domain,baseDir,boxname) if port: @@ -1865,7 +1895,7 @@ def createBoxBase(baseDir: str,boxname: str, \ # get the post as json p = json.loads(postStr) - if (boxname!='dm' and boxname!='tlreplies'): + if (boxname!='dm' and boxname!='tlreplies' and boxname!='tlmedia'): isTimelinePost=True else: if boxname=='dm': @@ -1874,6 +1904,9 @@ def createBoxBase(baseDir: str,boxname: str, \ elif boxname=='tlreplies': if isDM(p) or isReply(p,boxActor): isTimelinePost=True + elif boxname=='tlmedia': + if isImageMedia(p): + isTimelinePost=True if isTimelinePost and currPage == pageNumber: # remove any capability so that it's not displayed @@ -2084,7 +2117,9 @@ def sendCapabilitiesUpdate(session,baseDir: str,httpPrefix: str, \ sendThreads,postLog,cachedWebfingers, \ personCache,debug,projectVersion) -def populateRepliesJson(baseDir: str,nickname: str,domain: str,postRepliesFilename: str,authorized: bool,repliesJson: {}) -> None: +def populateRepliesJson(baseDir: str,nickname: str,domain: str, \ + postRepliesFilename: str,authorized: bool, \ + repliesJson: {}) -> None: # populate the items list with replies repliesBoxes=['outbox','inbox'] with open(postRepliesFilename,'r') as repliesFile: diff --git a/webinterface.py b/webinterface.py index a38bde6c..ed417ca5 100644 --- a/webinterface.py +++ b/webinterface.py @@ -1666,6 +1666,7 @@ def individualPostAsHtml(iconsDir: str,translate: {}, \ showDMicon=True titleStr='' + galleryStr='' isAnnounced=False if postJsonObject['type']=='Announce': if postJsonObject.get('object'): @@ -1875,6 +1876,17 @@ def individualPostAsHtml(iconsDir: str,translate: {}, \ attach['url'].endswith('.gif'): if attachmentCtr>0: attachmentStr+='
' + if boxName=='tlmedia': + galleryStr+= \ + '\n' \ attachmentStr+= \ '' \ ''+imageDescription+'\n' @@ -2064,12 +2076,15 @@ def individualPostAsHtml(iconsDir: str,translate: {}, \ contentStr='
'+contentStr+'
' - return \ - '
\n'+ \ - avatarImageInPost+ \ - '

'+titleStr+replyAvatarImageInPost+'

'+ \ - contentStr+footerStr+ \ - '
\n' + if boxName!='tlmedia': + return \ + '
\n'+ \ + avatarImageInPost+ \ + '

'+titleStr+replyAvatarImageInPost+'

'+ \ + contentStr+footerStr+ \ + '
\n' + else: + return galleryStr def isQuestion(postObjectJson: {}) -> bool: """ is the given post a question? @@ -2106,6 +2121,7 @@ def htmlTimeline(translate: {},pageNumber: int, \ inboxButton='button' dmButton='button' repliesButton='button' + mediaButton='button' sentButton='button' moderationButton='button' if boxName=='inbox': @@ -2114,6 +2130,8 @@ def htmlTimeline(translate: {},pageNumber: int, \ dmButton='buttonselected' elif boxName=='tlreplies': repliesButton='buttonselected' + elif boxName=='tlmedia': + mediaButton='buttonselected' elif boxName=='outbox': sentButton='buttonselected' elif boxName=='moderation': @@ -2125,7 +2143,8 @@ def htmlTimeline(translate: {},pageNumber: int, \ showIndividualPostIcons=True followApprovals='' - followRequestsFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/followrequests.txt' + followRequestsFilename= \ + baseDir+'/accounts/'+nickname+'@'+domain+'/followrequests.txt' if os.path.isfile(followRequestsFilename): with open(followRequestsFilename,'r') as f: for line in f: @@ -2160,6 +2179,7 @@ def htmlTimeline(translate: {},pageNumber: int, \ ' ' \ ' ' \ ' ' \ + ' ' \ ' '+ \ moderationButtonStr+newPostButtonStr+ \ ' '+translate['Search and follow']+''+ \ @@ -2250,6 +2270,18 @@ def htmlInboxReplies(translate: {},pageNumber: int,itemsPerPage: int, \ nickname,domain,port,inboxJson,'tlreplies',allowDeletion, \ httpPrefix,projectVersion,False) +def htmlInboxMedia(translate: {},pageNumber: int,itemsPerPage: int, \ + session,baseDir: str,wfRequest: {},personCache: {}, \ + nickname: str,domain: str,port: int,inboxJson: {}, \ + allowDeletion: bool, \ + httpPrefix: str,projectVersion: str) -> str: + """Show the media timeline as html + """ + return htmlTimeline(translate,pageNumber, \ + itemsPerPage,session,baseDir,wfRequest,personCache, \ + nickname,domain,port,inboxJson,'tlmedia',allowDeletion, \ + httpPrefix,projectVersion,False) + def htmlModeration(translate: {},pageNumber: int,itemsPerPage: int, \ session,baseDir: str,wfRequest: {},personCache: {}, \ nickname: str,domain: str,port: int,inboxJson: {}, \