-
Notifications
You must be signed in to change notification settings - Fork 0
/
TimelineWindow.py
1163 lines (1041 loc) · 46.5 KB
/
TimelineWindow.py
1
"""Macstodon - a Mastodon client for classic Mac OSMIT LicenseCopyright (c) 2022-2024 Scott Small and ContributorsPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associateddocumentation files (the "Software"), to deal in the Software without restriction, including without limitation therights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permitpersons to whom the Software is furnished to do so, subject to the following conditions:The above copyright notice and this permission notice shall be included in all copies or substantial portions of theSoftware.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THEWARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS ORCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OROTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."""# ############### Python Imports # ##############import Listsimport Qdimport reimport stringimport urllibimport Wfrom EasyDialogs import AskString# ########### My Imports# ##########from MacstodonConstants import VERSIONfrom MacstodonHelpers import attachmentsDialog, buildTimelinePicker, cleanUpUnicode, dprint, handleRequest, \ ImageWidget, LinkExtractor, linksDialog, okDialog, okCancelDialog, speak, TitledEditText, TimelineList# ############ Application# ###########class TimelineWindow(W.Window): def __init__(self): """ Initializes the TimelineWindow class. """ # Set window size. Default to 600x400 which fits nicely in a 640x480 display. # However if you're on a compact Mac that is 512x342, we need to make it smaller. screenbounds = Qd.qd.screenBits.bounds if screenbounds[2] <= 600 and screenbounds[3] <= 400: bounds = (0, 20, 512, 342) else: bounds = (600, 400) self.defaulttext = "Click on a toot or notification in one of the above lists..." self.timelines = { "col1": [], "col2": [], "col3": [] } self.timelinecol1 = None self.timelinecol2 = None self.timelinecol3 = None w = W.Window.__init__(self, bounds, "Macstodon %s - Timeline" % VERSION, minsize=(512, 342)) self.setupwidgets() # ######################### # Window Handling Functions # ######################### def setupwidgets(self): """ Defines the Timeline window """ self.panes = W.HorizontalPanes((8, 8, -8, -20), (0.6, 0.4)) self.panes.tlpanes = W.VerticalPanes(None, (0.33, 0.34, 0.33)) self.panes.tlpanes.col1 = TimelineList( None, "Loading...", self.timelines["col1"], pickerItems=buildTimelinePicker(self.parent), btnCallback=self.refreshCol1Callback, callback=self.col1ClickCallback, pickerCallback=self.timelinePicker1Callback, flags=Lists.lOnlyOne ) self.panes.tlpanes.col2 = TimelineList( None, "Loading...", self.timelines["col2"], pickerItems=buildTimelinePicker(self.parent), btnCallback=self.refreshCol2Callback, callback=self.col2ClickCallback, pickerCallback=self.timelinePicker2Callback, flags=Lists.lOnlyOne ) self.panes.tlpanes.col3 = TimelineList( None, "Loading...", self.timelines["col3"], pickerItems=buildTimelinePicker(self.parent), btnCallback=self.refreshCol3Callback, callback=self.col3ClickCallback, pickerCallback=self.timelinePicker3Callback, flags=Lists.lOnlyOne ) self.panes.tootgroup = W.Group(None) self.panes.tootgroup.toottxt = TitledEditText( (56, 0, -24, -20), title="", text=self.defaulttext, readonly=1, vscroll=1 ) # Links, Attachment, and Speech buttons self.panes.tootgroup.links = ImageWidget( (-20, 52, 16, 16), pixmap=self.parent.pctLnkDis, callback=self.linksCallback ) self.panes.tootgroup.attch = ImageWidget( (-20, 70, 16, 16), pixmap=self.parent.pctAtcDis, callback=self.attachmentsCallback ) self.panes.tootgroup.speak = ImageWidget( (-20, 88, 16, 16), pixmap=self.parent.pctSpcDis, callback=self.speakCallback ) # Avatar, reply/boost/favourite/bookmark buttons self.panes.tootgroup.authorimg = ImageWidget((0, 0, 24, 24), callback=self.avatarClickCallback) self.panes.tootgroup.boosterimg = ImageWidget((24, 24, 24, 24), callback=self.boosterClickCallback) self.panes.tootgroup.reply = ImageWidget( (4, 52, 16, 16), pixmap=self.parent.pctRplDis, callback=self.replyCallback ) self.panes.tootgroup.rpnum = W.TextBox((24, 55, 28, 16), "") self.panes.tootgroup.favrt = ImageWidget( (4, 70, 16, 16), pixmap=self.parent.pctFvtDis, callback=self.favouriteCallback ) self.panes.tootgroup.fvnum = W.TextBox((24, 73, 28, 16), "") self.panes.tootgroup.boost = ImageWidget( (4, 88, 16, 16), pixmap=self.parent.pctBstDis, callback=self.boostCallback ) self.panes.tootgroup.bonum = W.TextBox((24, 91, 28, 16), "") self.panes.tootgroup.bmark = ImageWidget( (4, 106, 16, 16), pixmap=self.parent.pctBkmDis, callback=self.bookmarkCallback ) self.panes.tootgroup.logoutbutton = W.Button((56, -15, 80, 0), "Logout", self.close) self.panes.tootgroup.prefsbutton = W.Button((146, -15, 80, 0), "Preferences", self.prefsCallback) self.panes.tootgroup.profilebutton = W.Button((-170, -15, 80, 0), "Find User", self.profileCallback) self.panes.tootgroup.tootbutton = W.Button((-80, -15, 80, 0), "Post Toot", self.tootCallback) def open(self): """ When the timeline window is opened, populate the timelines. """ app = self.parent prefs = app.getprefs() W.Window.open(self) self.timelinecol1 = prefs.timelinecol1 self.timelinecol2 = prefs.timelinecol2 self.timelinecol3 = prefs.timelinecol3 # Set timeline titles try: int(self.timelinecol1) for list in app.lists: if list["id"] == self.timelinecol1: self.panes.tlpanes.col1.setTitle(list["title"]) except ValueError: self.panes.tlpanes.col1.setTitle(string.capitalize(self.timelinecol1)) try: int(self.timelinecol2) for list in app.lists: if list["id"] == self.timelinecol2: self.panes.tlpanes.col2.setTitle(list["title"]) except ValueError: self.panes.tlpanes.col2.setTitle(string.capitalize(self.timelinecol2)) try: int(self.timelinecol3) for list in app.lists: if list["id"] == self.timelinecol3: self.panes.tlpanes.col3.setTitle(list["title"]) except ValueError: self.panes.tlpanes.col3.setTitle(string.capitalize(self.timelinecol3)) # Load initial toots initial_toots = int(prefs.toots_to_load_startup) if initial_toots: self.refreshCol1Callback(initial_toots) self.refreshCol2Callback(initial_toots) self.refreshCol3Callback(initial_toots) def close(self): """ When the timeline window is closed, return to the login window. """ for window in self.parent.profilewindows.values(): window.close() self.parent.currentuser = None self.parent.loginwindow = self.parent.LoginWindow() self.parent.loginwindow.open() W.Window.close(self) # ####################### # Menu Handling Functions # ####################### def can_logout(self, menuitem): """ Enable the Logout menu item when the Timeline window is open and active. """ return 1 def domenu_logout(self, *args): """ Log out when the Logout menu item is selected. """ self.close() def can_prefs(self, menuitem): """ Enable the Preferences menu item when the Timeline window is open and active. """ return 1 def domenu_prefs(self, *args): """ Open the Preferences window when the Preferences menu item is selected. """ win = self.parent.PrefsWindow() win.open() # ################## # Callback Functions # ################## def profileCallback(self): """ Prompt the user for a username when the Find User button is clicked """ acctname = AskString( "Please enter a user and domain name to view their profile.", "[email protected]" ) if acctname: req_data = { "acct": acctname } path = "/api/v1/accounts/lookup?%s" % urllib.urlencode(req_data) data = handleRequest(self.parent, path, title="Looking up profile...") if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when querying user ID:\r\r %s" % data['error_description']) elif data.get("error") is not None: okDialog("Server error when querying user ID:\r\r %s" % data['error']) else: win = self.parent.ProfileWindow(data) win.open() def prefsCallback(self): """ Open the Preferences window when the Preferences button is clicked. """ win = self.parent.PrefsWindow() win.open() def avatarClickCallback(self): """ Run when clicking on a user's avatar. Opens the user's profile. """ toot, _, _, _ = self.getSelectedToot(resolve_boosts=1) if toot: win = self.parent.ProfileWindow(toot["account"]) win.open() else: okDialog("Please select a toot, then click on the author's avatar to view their profile.") def boosterClickCallback(self): toot, _, _, _ = self.getSelectedToot(resolve_boosts=0) if toot: win = self.parent.ProfileWindow(toot["account"]) win.open() else: okDialog("Please select a toot, then click on the author's avatar to view their profile.") def col1ClickCallback(self): """ Run when the user clicks somewhere in the first timeline """ self.panes.tlpanes.col2.setselection([-1]) self.panes.tlpanes.col3.setselection([-1]) if self.timelinecol1 == "notifications": self.notificationClickCallback("col1") else: self.timelineClickCallback("col1") def col2ClickCallback(self): """ Run when the user clicks somewhere in the second timeline """ self.panes.tlpanes.col1.setselection([-1]) self.panes.tlpanes.col3.setselection([-1]) if self.timelinecol2 == "notifications": self.notificationClickCallback("col2") else: self.timelineClickCallback("col2") def col3ClickCallback(self): """ Run when the user clicks somewhere in the third timeline """ self.panes.tlpanes.col1.setselection([-1]) self.panes.tlpanes.col2.setselection([-1]) if self.timelinecol3 == "notifications": self.notificationClickCallback("col3") else: self.timelineClickCallback("col3") def timelineClickCallback(self, name): """ Run when the user clicks somewhere in the named timeline """ if name == "col1": list = self.panes.tlpanes.col1 elif name == "col2": list = self.panes.tlpanes.col2 elif name == "col3": list = self.panes.tlpanes.col3 selected = list.getselection() if len(selected) < 1: self.panes.tootgroup.authorimg.clearImage() self.panes.tootgroup.boosterimg.clearImage() self.panes.tootgroup.reply.setImage(self.parent.pctRplDis) self.panes.tootgroup.favrt.setImage(self.parent.pctFvtDis) self.panes.tootgroup.boost.setImage(self.parent.pctBstDis) self.panes.tootgroup.bmark.setImage(self.parent.pctBkmDis) self.panes.tootgroup.links.setImage(self.parent.pctLnkDis) self.panes.tootgroup.attch.setImage(self.parent.pctAtcDis) self.panes.tootgroup.speak.setImage(self.parent.pctSpcDis) self.panes.tootgroup.fvnum.set("") self.panes.tootgroup.bonum.set("") self.panes.tootgroup.rpnum.set("") self.panes.tootgroup.toottxt.setTitle("") self.panes.tootgroup.toottxt.set(self.defaulttext) return else: index = selected[0] toot = self.timelines[name][index] self.formatAndDisplayToot(toot) def notificationClickCallback(self, name): """ Run when the user clicks somewhere in the notification timeline """ if name == "col1": list = self.panes.tlpanes.col1 elif name == "col2": list = self.panes.tlpanes.col2 elif name == "col3": list = self.panes.tlpanes.col3 selected = list.getselection() if len(selected) < 1: self.panes.tootgroup.authorimg.clearImage() self.panes.tootgroup.boosterimg.clearImage() self.panes.tootgroup.reply.setImage(self.parent.pctRplDis) self.panes.tootgroup.favrt.setImage(self.parent.pctFvtDis) self.panes.tootgroup.boost.setImage(self.parent.pctBstDis) self.panes.tootgroup.bmark.setImage(self.parent.pctBkmDis) self.panes.tootgroup.links.setImage(self.parent.pctLnkDis) self.panes.tootgroup.attch.setImage(self.parent.pctAtcDis) self.panes.tootgroup.speak.setImage(self.parent.pctSpcDis) self.panes.tootgroup.fvnum.set("") self.panes.tootgroup.bonum.set("") self.panes.tootgroup.rpnum.set("") self.panes.tootgroup.toottxt.setTitle("") self.panes.tootgroup.toottxt.set(self.defaulttext) return else: index = selected[0] notification = self.timelines[name][index] if notification["type"] in ["favourite", "reblog", "status", "mention", "poll", "update"]: toot = notification["status"] self.formatAndDisplayToot(toot) elif notification["type"] == "admin.report": okDialog("Sorry, displaying the notification type '%s' is not supported yet" % notification["type"]) self.panes.tootgroup.authorimg.clearImage() self.panes.tootgroup.boosterimg.clearImage() self.panes.tootgroup.reply.setImage(self.parent.pctRplDis) self.panes.tootgroup.favrt.setImage(self.parent.pctFvtDis) self.panes.tootgroup.boost.setImage(self.parent.pctBstDis) self.panes.tootgroup.bmark.setImage(self.parent.pctBkmDis) self.panes.tootgroup.links.setImage(self.parent.pctLnkDis) self.panes.tootgroup.attch.setImage(self.parent.pctAtcDis) self.panes.tootgroup.speak.setImage(self.parent.pctSpcDis) self.panes.tootgroup.fvnum.set("") self.panes.tootgroup.bonum.set("") self.panes.tootgroup.rpnum.set("") self.panes.tootgroup.toottxt.setTitle("") self.panes.tootgroup.toottxt.set(self.defaulttext) else: win = self.parent.ProfileWindow(notification["account"]) win.open() self.panes.tootgroup.authorimg.clearImage() self.panes.tootgroup.boosterimg.clearImage() self.panes.tootgroup.reply.setImage(self.parent.pctRplDis) self.panes.tootgroup.favrt.setImage(self.parent.pctFvtDis) self.panes.tootgroup.boost.setImage(self.parent.pctBstDis) self.panes.tootgroup.bmark.setImage(self.parent.pctBkmDis) self.panes.tootgroup.links.setImage(self.parent.pctLnkDis) self.panes.tootgroup.attch.setImage(self.parent.pctAtcDis) self.panes.tootgroup.speak.setImage(self.parent.pctSpcDis) self.panes.tootgroup.fvnum.set("") self.panes.tootgroup.bonum.set("") self.panes.tootgroup.rpnum.set("") self.panes.tootgroup.toottxt.setTitle("") self.panes.tootgroup.toottxt.set(self.defaulttext) def refreshCol1Callback(self, limit=None): """ Run when the user clicks the Refresh button above the first column timeline """ self.updateTimeline(self.timelinecol1, "col1", limit) if self.timelinecol1 == "notifications": listitems = self.formatNotificationsForList("col1") self.panes.tlpanes.col1.set(listitems) else: self.panes.tlpanes.col1.set(self.formatTimelineForList("col1")) def refreshCol2Callback(self, limit=None): """ Run when the user clicks the Refresh button above the second column timeline """ self.updateTimeline(self.timelinecol2, "col2", limit) if self.timelinecol2 == "notifications": listitems = self.formatNotificationsForList("col2") self.panes.tlpanes.col2.set(listitems) else: self.panes.tlpanes.col2.set(self.formatTimelineForList("col2")) def refreshCol3Callback(self, limit=None): """ Run when the user clicks the Refresh button above the third column timeline """ self.updateTimeline(self.timelinecol3, "col3", limit) if self.timelinecol3 == "notifications": listitems = self.formatNotificationsForList("col3") self.panes.tlpanes.col3.set(listitems) else: self.panes.tlpanes.col3.set(self.formatTimelineForList("col3")) def timelinePicker1Callback(self, item): """ Run when the user selects a timeline for column 1 """ app = self.parent prefs = app.getprefs() if item == "__hashtag": hashtag = AskString( "Please type the name of the hashtag to build a timeline for." ) if hashtag is not None: if hashtag[0] == "#": item = hashtag else: item = "#%s" % hashtag else: return self.panes.tootgroup.authorimg.clearImage() self.panes.tootgroup.boosterimg.clearImage() self.panes.tootgroup.reply.setImage(self.parent.pctRplDis) self.panes.tootgroup.favrt.setImage(self.parent.pctFvtDis) self.panes.tootgroup.boost.setImage(self.parent.pctBstDis) self.panes.tootgroup.bmark.setImage(self.parent.pctBkmDis) self.panes.tootgroup.links.setImage(self.parent.pctLnkDis) self.panes.tootgroup.attch.setImage(self.parent.pctAtcDis) self.panes.tootgroup.speak.setImage(self.parent.pctSpcDis) self.panes.tootgroup.fvnum.set("") self.panes.tootgroup.bonum.set("") self.panes.tootgroup.rpnum.set("") self.panes.tootgroup.toottxt.setTitle("") self.panes.tootgroup.toottxt.set(self.defaulttext) self.timelinecol1 = item try: int(self.timelinecol1) for list in app.lists: if list["id"] == self.timelinecol1: self.panes.tlpanes.col1.setTitle(list["title"]) except ValueError: self.panes.tlpanes.col1.setTitle(string.capitalize(self.timelinecol1)) self.timelines["col1"] = [] initial_toots = int(prefs.toots_to_load_startup) if initial_toots: self.refreshCol1Callback(initial_toots) else: self.panes.tlpanes.col1.set([]) def timelinePicker2Callback(self, item): """ Run when the user selects a timeline for column 2 """ app = self.parent prefs = app.getprefs() if item == "__hashtag": hashtag = AskString( "Please type the name of the hashtag to build a timeline for." ) if hashtag is not None: if hashtag[0] == "#": item = hashtag else: item = "#%s" % hashtag else: return self.panes.tootgroup.authorimg.clearImage() self.panes.tootgroup.boosterimg.clearImage() self.panes.tootgroup.reply.setImage(self.parent.pctRplDis) self.panes.tootgroup.favrt.setImage(self.parent.pctFvtDis) self.panes.tootgroup.boost.setImage(self.parent.pctBstDis) self.panes.tootgroup.bmark.setImage(self.parent.pctBkmDis) self.panes.tootgroup.links.setImage(self.parent.pctLnkDis) self.panes.tootgroup.attch.setImage(self.parent.pctAtcDis) self.panes.tootgroup.speak.setImage(self.parent.pctSpcDis) self.panes.tootgroup.fvnum.set("") self.panes.tootgroup.bonum.set("") self.panes.tootgroup.rpnum.set("") self.panes.tootgroup.toottxt.setTitle("") self.panes.tootgroup.toottxt.set(self.defaulttext) self.timelinecol2 = item try: int(self.timelinecol2) for list in app.lists: if list["id"] == self.timelinecol2: self.panes.tlpanes.col2.setTitle(list["title"]) except ValueError: self.panes.tlpanes.col2.setTitle(string.capitalize(self.timelinecol2)) self.timelines["col2"] = [] initial_toots = int(prefs.toots_to_load_startup) if initial_toots: self.refreshCol2Callback(initial_toots) else: self.panes.tlpanes.col2.set([]) def timelinePicker3Callback(self, item): """ Run when the user selects a timeline for column 3 """ app = self.parent prefs = app.getprefs() if item == "__hashtag": hashtag = AskString( "Please type the name of the hashtag to build a timeline for." ) if hashtag is not None: if hashtag[0] == "#": item = hashtag else: item = "#%s" % hashtag else: return self.panes.tootgroup.authorimg.clearImage() self.panes.tootgroup.boosterimg.clearImage() self.panes.tootgroup.reply.setImage(self.parent.pctRplDis) self.panes.tootgroup.favrt.setImage(self.parent.pctFvtDis) self.panes.tootgroup.boost.setImage(self.parent.pctBstDis) self.panes.tootgroup.bmark.setImage(self.parent.pctBkmDis) self.panes.tootgroup.links.setImage(self.parent.pctLnkDis) self.panes.tootgroup.attch.setImage(self.parent.pctAtcDis) self.panes.tootgroup.speak.setImage(self.parent.pctSpcDis) self.panes.tootgroup.fvnum.set("") self.panes.tootgroup.bonum.set("") self.panes.tootgroup.rpnum.set("") self.panes.tootgroup.toottxt.setTitle("") self.panes.tootgroup.toottxt.set(self.defaulttext) self.timelinecol3 = item try: int(self.timelinecol3) for list in app.lists: if list["id"] == self.timelinecol3: self.panes.tlpanes.col3.setTitle(list["title"]) except ValueError: self.panes.tlpanes.col3.setTitle(string.capitalize(self.timelinecol3)) self.timelines["col3"] = [] initial_toots = int(prefs.toots_to_load_startup) if initial_toots: self.refreshCol3Callback(initial_toots) else: self.panes.tlpanes.col3.set([]) def tootCallback(self): """ Run when the user clicks the "Post Toot" button from the timeline window. It opens up the toot window. """ self.parent.tootwindow = self.parent.TootWindow() self.parent.tootwindow.open() def replyCallback(self): """ Run when the user clicks the "Reply" button from the timeline window. It opens up the toot window, passing the currently selected toot as a parameter. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: self.parent.tootwindow = self.parent.TootWindow(replyTo=toot) self.parent.tootwindow.open() else: okDialog("Please select a toot first.") def boostCallback(self): """ Boosts a toot. Removes the boost if the toot was already boosted. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: if toot["reblogged"]: # already boosted, undo it title = "Removing boost..." action = "unreblog" req_data = {} else: # not boosted yet title = "Boosting..." action = "reblog" visibility = toot["visibility"] if visibility == "limited" or visibility == "direct": visibility = "public" req_data = { "visibility": visibility } path = "/api/v1/statuses/%s/%s" % (toot["id"], action) data = handleRequest(self.parent, path, req_data, use_token=1, title=title) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error_description'])) elif data.get("error") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error'])) else: if action == "reblog": if origToot: if origToot.get("reblog"): dprint("overwriting boosted toot") timeline[index]["reblog"] = data["reblog"] else: dprint("overwriting notification") timeline[index]["status"] = data["reblog"] else: dprint("overwriting normal toot") timeline[index] = data["reblog"] self.panes.tootgroup.boost.setImage(self.parent.pctBstClr) else: if origToot: if origToot.get("reblog"): if origToot["account"]["id"] != self.parent.currentuser["id"]: dprint("overwriting boosted toot by another user") timeline[index]["reblog"] = data else: dprint("own boosted toot, need to remove from timeline") del timeline[index] self.panes.tlpanes.timeline.set(self.formatTimelineForList()) else: dprint("overwriting notification") timeline[index]["status"] = data else: dprint("overwriting normal toot") timeline[index] = data self.panes.tootgroup.boost.setImage(self.parent.pctBstBnW) okDialog("Toot %sged successfully!" % action) else: okDialog("Please select a toot first.") def favouriteCallback(self): """ Favourites a toot. Removes the favourite if the toot was already favourited. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: if toot["favourited"]: # already favourited, undo it title = "Removing favourite..." action = "unfavourite" else: # not favourited yet title = "Favouriting..." action = "favourite" path = "/api/v1/statuses/%s/%s" % (toot["id"], action) data = handleRequest(self.parent, path, {}, use_token=1, title=title) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action[:-1], data['error_description'])) elif data.get("error") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action[:-1], data['error'])) else: if origToot: if origToot.get("reblog"): dprint("overwriting boosted toot") timeline[index]["reblog"] = data else: dprint("overwriting notification") timeline[index]["status"] = data else: dprint("overwriting normal toot") timeline[index] = data okDialog("Toot %sd successfully!" % action) if action == "favourite": self.panes.tootgroup.favrt.setImage(self.parent.pctFvtClr) else: self.panes.tootgroup.favrt.setImage(self.parent.pctFvtBnW) else: okDialog("Please select a toot first.") def bookmarkCallback(self): """ Bookmarks a toot. Removes the bookmark if the toot was already bookmarked. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: if toot["bookmarked"]: # already bookmarked, undo it title = "Removing bookmark..." action = "unbookmark" else: # not bookmarked yet title = "Bookmarking..." action = "bookmark" path = "/api/v1/statuses/%s/%s" % (toot["id"], action) data = handleRequest(self.parent, path, {}, use_token=1, title=title) if not data: # handleRequest failed and should have popped an error dialog return if data.get("error_description") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error_description'])) elif data.get("error") is not None: okDialog("Server error when %sing toot:\r\r %s" % (action, data['error'])) else: if origToot: if origToot.get("reblog"): dprint("overwriting boosted toot") timeline[index]["reblog"] = data else: dprint("overwriting notification") timeline[index]["status"] = data else: dprint("overwriting normal toot") timeline[index] = data okDialog("Toot %sed successfully!" % action) if action == "bookmark": self.panes.tootgroup.bmark.setImage(self.parent.pctBkmClr) else: self.panes.tootgroup.bmark.setImage(self.parent.pctBkmBnW) else: okDialog("Please select a toot first.") def linksCallback(self): """ Displays a dialog containing the links in the toot and allows the user to open them. """ toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: content = toot["content"] # Replace HTML linebreak tags with actual linebreaks content = cleanUpUnicode(content) # Extract links le = LinkExtractor() le.feed(content) le.close() linksDialog(le) else: okDialog("Please select a toot first.") def attachmentsCallback(self): toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: attachmentsDialog(toot["media_attachments"]) else: okDialog("Please select a toot first.") def speakCallback(self): toot, origToot, timeline, index = self.getSelectedToot(resolve_boosts=1) if toot: content = toot["content"] # Replace HTML linebreak tags with actual linebreaks content = cleanUpUnicode(content) content = string.replace(content, "<br>", "\r") content = string.replace(content, "<br/>", "\r") content = string.replace(content, "<br />", "\r") content = string.replace(content, "<p>", "") content = string.replace(content, "</p>", "\r\r") # Strip all other HTML tags content = re.sub('<[^<]+?>', '', content) # Hashtags content = string.replace(content, "#", "hashtag") # HACK HACK HACK # make certain words sound better content = string.lower(content) content = string.replace(content, "macstodon", "macstodawn") content = string.replace(content, "mastodon", "mastodawn") speak(content) else: okDialog("Please select a toot first.") # #################### # Formatting Functions # #################### def formatAndDisplayToot(self, toot): """ Formats a toot for display and displays it in the bottom third """ prefs = self.parent.getprefs() # clear existing toot self.panes.tootgroup.authorimg.clearImage() self.panes.tootgroup.boosterimg.clearImage() self.panes.tootgroup.reply.setImage(self.parent.pctRplDis) self.panes.tootgroup.favrt.setImage(self.parent.pctFvtDis) self.panes.tootgroup.boost.setImage(self.parent.pctBstDis) self.panes.tootgroup.bmark.setImage(self.parent.pctBkmDis) self.panes.tootgroup.links.setImage(self.parent.pctLnkDis) self.panes.tootgroup.attch.setImage(self.parent.pctAtcDis) self.panes.tootgroup.speak.setImage(self.parent.pctSpcDis) self.panes.tootgroup.fvnum.set("") self.panes.tootgroup.bonum.set("") self.panes.tootgroup.rpnum.set("") self.panes.tootgroup.toottxt.setTitle("") self.panes.tootgroup.toottxt.set("Loading toot...") display_name = toot["account"]["display_name"] or toot["account"]["username"] display_name = cleanUpUnicode(display_name) if toot["reblog"]: self.panes.tootgroup.authorimg.resize(24,24) if prefs.show_avatars: image = self.parent.imagehandler.getImageFromURL(toot["reblog"]["account"]["avatar"], "account") bimage = self.parent.imagehandler.getImageFromURL(toot["account"]["avatar"], "account") else: image = None bimage = None reblog_display_name = toot["reblog"]["account"]["display_name"] or toot["reblog"]["account"]["username"] reblog_display_name = cleanUpUnicode(reblog_display_name) title = "%s boosted %s (@%s)" % (display_name, reblog_display_name, toot["reblog"]["account"]["acct"]) content = toot["reblog"]["content"] sensitive = toot["reblog"]["sensitive"] spoiler_text = toot["reblog"]["spoiler_text"] favourites_count = toot["reblog"]["favourites_count"] reblogs_count = toot["reblog"]["reblogs_count"] replies_count = toot["reblog"]["replies_count"] favourited = toot["reblog"]["favourited"] reblogged = toot["reblog"]["reblogged"] bookmarked = toot["reblog"]["bookmarked"] else: self.panes.tootgroup.authorimg.resize(48,48) if prefs.show_avatars: image = self.parent.imagehandler.getImageFromURL(toot["account"]["avatar"], "account") else: image = None bimage = None title = "%s (@%s)" % (display_name, toot["account"]["acct"]) content = toot["content"] sensitive = toot["sensitive"] spoiler_text = toot["spoiler_text"] favourites_count = toot["favourites_count"] reblogs_count = toot["reblogs_count"] replies_count = toot["replies_count"] favourited = toot["favourited"] reblogged = toot["reblogged"] bookmarked = toot["bookmarked"] # Check for CW if sensitive: cwText = "This toot has a content warning. " \ "Press OK to view or Cancel to not view.\r\r%s" try: okCancelDialog(cwText % spoiler_text) except KeyboardInterrupt: self.panes.tootgroup.toottxt.set(self.defaulttext) return # Replace HTML linebreak tags with actual linebreaks content = cleanUpUnicode(content) content = string.replace(content, "<br>", "\r") content = string.replace(content, "<br/>", "\r") content = string.replace(content, "<br />", "\r") content = string.replace(content, "<p>", "") content = string.replace(content, "</p>", "\r\r") # Extract links le = LinkExtractor() le.feed(content) le.close() # Strip all other HTML tags content = re.sub('<[^<]+?>', '', content) # Render content into UI if image: self.panes.tootgroup.authorimg.setImage(image) if bimage: self.panes.tootgroup.boosterimg.setImage(bimage) self.panes.tootgroup.reply.setImage(self.parent.pctRplBnW) if favourited: self.panes.tootgroup.favrt.setImage(self.parent.pctFvtClr) else: self.panes.tootgroup.favrt.setImage(self.parent.pctFvtBnW) if reblogged: self.panes.tootgroup.boost.setImage(self.parent.pctBstClr) else: self.panes.tootgroup.boost.setImage(self.parent.pctBstBnW) if bookmarked: self.panes.tootgroup.bmark.setImage(self.parent.pctBkmClr) else: self.panes.tootgroup.bmark.setImage(self.parent.pctBkmBnW) self.panes.tootgroup.links.setImage(self.parent.pctLnkBnW) self.panes.tootgroup.attch.setImage(self.parent.pctAtcBnW) self.panes.tootgroup.speak.setImage(self.parent.pctSpcBnW) self.panes.tootgroup.toottxt.setTitle(title) self.panes.tootgroup.toottxt.set(content) self.panes.tootgroup.fvnum.set(str(favourites_count)) self.panes.tootgroup.bonum.set(str(reblogs_count)) self.panes.tootgroup.rpnum.set(str(replies_count)) def formatTimelineForList(self, name): """ Formats toots for display in a timeline list """ listitems = [] for toot in self.timelines[name]: if toot["reblog"]: if toot["reblog"]["sensitive"]: content = toot["reblog"]["spoiler_text"] else: content = toot["reblog"]["content"] else: if toot["sensitive"]: content = toot["spoiler_text"] else: content = toot["content"] content = cleanUpUnicode(content) # Replace linebreaks with spaces content = string.replace(content, "<br>", " ") content = string.replace(content, "<br/>", " ") content = string.replace(content, "<br />", " ") content = string.replace(content, "<p>", "") content = string.replace(content, "</p>", " ") # Strip all other HTML tags content = re.sub('<[^<]+?>', '', content) display_name = toot["account"]["display_name"] or toot["account"]["username"] display_name = cleanUpUnicode(display_name) if toot["reblog"]: reblog_display_name = toot["reblog"]["account"]["display_name"] or toot["reblog"]["account"]["username"] reblog_display_name = cleanUpUnicode(reblog_display_name) listitem = "%s boosted %s\r%s" % (display_name, reblog_display_name, content) else: listitem = "%s\r%s" % (display_name, content) listitems.append(listitem) return listitems def formatNotificationsForList(self, name): """ Formats notifications for display in a list """ listitems = [] for notification in self.timelines[name]: display_name = notification["account"]["display_name"] or notification["account"]["username"] display_name = cleanUpUnicode(display_name) if notification["type"] == "mention": listitem = "%s mentioned you in their toot" % display_name elif notification["type"] == "status": listitem = "%s posted a toot" % display_name elif notification["type"] == "reblog": listitem = "%s boosted your toot" % display_name elif notification["type"] == "follow": listitem = "%s followed you" % display_name elif notification["type"] == "follow_request": listitem = "%s requested to follow you" % display_name elif notification["type"] == "favourite": listitem = "%s favourited your toot" % display_name elif notification["type"] == "poll": listitem = "%s's poll has ended" % display_name elif notification["type"] == "update": listitem = "%s updated their toot" % display_name elif notification["type"] == "admin.sign_up": listitem = "%s signed up" % display_name elif notification["type"] == "admin.report": listitem = "%s filed a report" % display_name else: # unknown type, ignore it, but print to console if debugging dprint("Unknown notification type: %s" % notification["type"]) listitems.append(listitem) return listitems # ################ # Helper Functions # ################ def getSelectedToot(self, resolve_boosts=0): """ Returns the selected toot, the containing toot (if boost or notification), the timeline to which the toot belongs, and the index of the toot in the timeline. """ homeTimeline = self.panes.tlpanes.col1 localTimeline = self.panes.tlpanes.col2 notificationsTimeline = self.panes.tlpanes.col3 homeSelected = homeTimeline.getselection() localSelected = localTimeline.getselection() notificationsSelected = notificationsTimeline.getselection() if len(homeSelected) > 0: index = homeSelected[0] toot = self.timelines["col1"][index] timeline = self.timelines["col1"] elif len(localSelected) > 0: index = localSelected[0] toot = self.timelines["col2"][index] timeline = self.timelines["col2"] elif len(notificationsSelected) > 0: index = notificationsSelected[0] toot = self.timelines["col3"][index] timeline = self.timelines["col3"] else: return None, None, None, None if toot.get("reblog") and resolve_boosts: return toot["reblog"], toot, timeline, index elif toot.get("status"): return toot["status"], toot, timeline, index else: return toot, None, timeline, index def updateTimeline(self, name, col_name, limit=None): """ Pulls a timeline from the server and updates the global dicts """ params = {} app = self.parent prefs = app.getprefs() if limit: # If a limit was explicitly set in the call, use that params["limit"] = limit else: # Otherwise, use the refresh limit from the prefs if one was set refresh_toots = int(prefs.toots_to_load_refresh) if refresh_toots: params["limit"] = refresh_toots if len(self.timelines[col_name]) > 0: params["min_id"] = self.timelines[col_name][0]["id"] try: int(name) path = "/api/v1/timelines/list/%s" % name except ValueError: if name == "home": path = "/api/v1/timelines/home" elif name == "local": path = "/api/v1/timelines/public" params["local"] = "true" elif name == "federated": path = "/api/v1/timelines/public" params["remote"] = "true" elif name == "notifications": path = "/api/v1/notifications" elif name == "bookmarks": path = "/api/v1/bookmarks" elif name == "favourites": path = "/api/v1/favourites" elif name == "mentions": path = "/api/v1/timelines/direct" elif name[0] == "#": path = "/api/v1/timelines/tag/%s" % name[1:] encoded_params = urllib.urlencode(params) if encoded_params: path = path + "?" + encoded_params data = handleRequest(self.parent, path, use_token=1, title="Updating timeline...") if not data: # handleRequest failed and should have popped an error dialog return # if data is a list, it worked if type(data) == type([]): for i in range(len(data)-1, -1, -1): self.timelines[col_name].insert(0, data[i]) self.timelines[col_name] = self.timelines[col_name][:int(prefs.toots_per_timeline)] # if data is a dict, it failed elif type(data) == type({}) and data.get("error") is not None: okDialog("Server error when refreshing %s timeline:\r\r %s" % (name, data['error'])) # i don't think this is reachable, but just in case... else: okDialog("Server error when refreshing %s timeline. Unable to determine data type." % name)