diff --git a/TODO b/TODO index b2af7c6..083311f 100644 --- a/TODO +++ b/TODO @@ -9,11 +9,11 @@ features: * Status bar icon * Support viewing a list of friends and unfollowing them * Support creating new lists and adding users to it (as well as removing users and deleting lists) - +* Need a new way to post failed tweet retrieval to the status bar bugs: * Direct Messages have no names, only screen names (may not be fixable without considerable tweets to python-twitter) -* Apparently, reply button isn't functioning, and some posts look like they are - in reply to someone when the twitter website says they are not +* Can't always kill tabs - open tab list not always correctly populated? +* Follow/Unfollow and Verified don't get set on current tab when it first appears... need a signal to come back from the TweetPane. diff --git a/apithreads.py b/apithreads.py new file mode 100644 index 0000000..0c59618 --- /dev/null +++ b/apithreads.py @@ -0,0 +1,263 @@ +# Python module that handles calls to the twitter API as a separate thread + +import re +import gtk, gobject +from threading import Thread,RLock +from twitter import Api +from urllib2 import HTTPError,URLError + +class CustomApi(Api): + ''' + This is a Twitter API with an RLock for multi-threaded access + Also included is logic for processing the list names when the object is + instantiated + ''' + + def __init__(self, username, password): + Api.__init__(self, username, password) + self.lock = RLock() + self.sig_proxy = SigProxy() + + self.username = username + + thread = GetUserLists(api=self) + thread.sig_proxy.connect('lists-ready', self.on_lists_ready) + thread.start() + + + def on_lists_ready(self, widget, lists, ignored): + list_names = [] + for l in lists['lists']: + list_names.append(l.name) + list_names.sort() + self.sig_proxy.emit('lists-ready', self.username, list_names) + +# End class CustomApi + + +class ApiThread(Thread): + def __init__(self, api): + Thread.__init__(self) + self.api = api + self.setDaemon(True) + +# End class ApiThread + + + +class GetTweets(ApiThread): + def __init__(self, api, list_name, pane, num_entries, username): + ApiThread.__init__(self, api) + self.list_name = list_name + self.pane = pane + self.num_entries = num_entries + self.username = username + + + def run(self): + with self.api.lock: + try: + # username/Home entries need to load the appropriate Home feed + if self.list_name == self.username + '/Home': + statuses = self.api.GetHomeTimeline(count=self.num_entries) + + # For @username, check if it is one of our usernames, or + # just needs to be searched on + elif self.list_name == '@' + self.username: + statuses = self.api.GetMentions(count=self.num_entries) + elif re.match('@', self.list_name): + statuses = results_to_statuses(self.api.Search(self.list_name, rpp=self.num_entries)) + + # Direct Messages should match like /Home, above + elif self.list_name == self.username + '/Direct Messages': + statuses = dms_to_statuses(self.api.GetDirectMessages()) + + # User lookups go straight to the user + elif re.match(r'user: ', self.list_name): + statuses = self.api.GetUserTimeline(re.sub(r'^user: ', r'', self.list_name), count=self.num_entries) + + # Lists load the appropriate list from the appropriate account + elif re.match(r'list: ', self.list_name): + real_list = re.sub(r'list: .*/(.*)', r'\1', self.list_name) + statuses = self.api.GetListStatuses(real_list, per_page=self.num_entries) + + # Everything else is a straight search + else: + statuses = results_to_statuses(self.api.Search(self.list_name, rpp=self.num_entries)) + + except (HTTPError, URLError): + statuses = None + + gtk.gdk.threads_enter() + try: + self.pane.update_window(statuses) + finally: + gtk.gdk.threads_leave() + + +### End class GetTweets + + + +class GetSingleTweet(ApiThread): + def __init__(self, api, pane, single_tweet): + ApiThread.__init__(self, api) + self.pane = pane + self.single_tweet = single_tweet + + + def run(self): + statuses = [] + with self.api.lock: + try: + statuses.append(self.api.GetStatus(self.single_tweet)) + except (HTTPError, URLError): + statuses = None + + gtk.gdk.threads_enter() + try: + self.pane.update_window(statuses) + finally: + gtk.gdk.threads_leave() + + +### End class GetSingleTweet + + + +class GetFollowing(ApiThread): + def __init__(self, api, pane, user): + ApiThread.__init__(self, api) + self.pane = pane + self.user = user + + + def run(self): + screen_name = re.sub('user: ', '', self.user) + + try: + with self.api.lock: + relationship = self.api.ShowFriendships(target_screen_name=screen_name) + following = relationship.source.following + except (HTTPError, URLError): + following = false + + self.pane.set_following(following) + +### End class GetFollowing + + + +class GetVerified(ApiThread): + def __init__(self, api, pane, user): + ApiThread.__init__(self, api) + self.pane = pane + self.user = user + + + def run(self): + screen_name = re.sub('user: ', '', self.user) + + try: + with self.api.lock: + user = self.api.GetUser(screen_name) + verified = user.verified + except (HTTPError, URLError): + verified = false + + self.pane.set_verified(verified) + +### End class GetVerified + + +class GetUserLists(ApiThread): + def __init__(self, api): + ApiThread.__init__(self, api) + self.sig_proxy = SigProxy() + + + def run(self): + lists = [] + done = False + + while not done: + done = True + try: + with self.api.lock: + lists = self.api.GetUserLists() + except (HTTPError, URLError): + done = False + + self.sig_proxy.emit('lists-ready', lists, None) + +### End class GetUserLists + + + +class SigProxy(gtk.Alignment): + + def __init__(self): + gtk.Alignment.__init__(self) + +# End class SigProxy + +gobject.signal_new("lists-ready", SigProxy, + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,gobject.TYPE_PYOBJECT)) + + +# We use these classes to emulate a Status object when we need +# one to be built out of something else. +class Status(): + def __init__(self): + self.user = User() + self.id = None + self.created_at = None + self.in_reply_to_screen_name = None + self.in_reply_to_status_id = None + + +class User(): + def __init__(self): + self.screen_name = None + self.name = None + + +# To keep things simple elsewhere and improve code reuse +# we'll build a list of home-cooked Status objects out of results. +# Why is this even necessary? +# Why can't we have more consistency out of the Twitter API? +def results_to_statuses(results): + statuses = [] + for result in results.results: + status = Status() + status.id = result.id + status.user = User() + status.user.screen_name = result.from_user + status.user.name = "" + status.in_reply_to_screen_name = result.to_user + # The Twitter Search API has different timestamps than the + # REST API... balls + # fixme: + # Gotta be a cleaner way to do this, but I can't think of it + # right now + created_at = re.sub(',', '', result.created_at) + created_split = re.split(' ', created_at) + status.created_at = created_split[0] + ' ' + created_split[2] + ' ' + created_split[1] + ' ' + created_split[4] + ' ' + created_split[5] + ' ' + created_split[3] + status.text = result.text + statuses.append(status) + return statuses + + +def dms_to_statuses(direct_messages): + statuses = [] + for dm in direct_messages: + status = Status() + status.id = dm.id + status.user = User() + status.user.screen_name = dm.sender_screen_name + status.user.name = dm.sender.name + status.created_at = dm.created_at + status.text = dm.text + statuses.append(status) + return statuses diff --git a/default.glade b/default.glade index 65666b1..ece8e70 100644 --- a/default.glade +++ b/default.glade @@ -221,7 +221,7 @@ True GTK_RELIEF_NORMAL True - + 0 diff --git a/mytwitter.py b/mytwitter.py index 126d8ab..decfd2b 100755 --- a/mytwitter.py +++ b/mytwitter.py @@ -3,10 +3,10 @@ # Custom twitter client... mostly for learning Python import sys, ConfigParser, os, re, optparse, shelve -import twitter import gtk, gtk.glade, gobject -from urllib2 import HTTPError +from urllib2 import HTTPError,URLError from twitterwidgets import TweetPane +import apithreads class MyTwitter(): @@ -42,11 +42,16 @@ class MyTwitter(): print "Error: You must define at least one [account] section in " + config_file sys.exit(1) + # Init the glade stuff here, so we don't have a race condition with + # the lists-ready signal + self.init_user_interface('./default.glade') + self.accounts = {} for item in config.sections(): if (re.match(r'account', item)): username = config.get(item, 'username') - self.accounts[username] = twitter.Api(username=username, password=config.get(item, 'password')) + self.accounts[username] = apithreads.CustomApi(username=username, password=config.get(item, 'password')) + self.accounts[username].sig_proxy.connect('lists-ready', self.on_lists_ready) self.username = self.accounts.keys()[0] self.api = self.accounts[self.username] @@ -68,8 +73,7 @@ class MyTwitter(): self.reply_id = None - # Load up all the GUI stuff - self.init_user_interface('./default.glade') + # Load up all the programmatic GUI stuff self.init_widgets() @@ -77,8 +81,6 @@ class MyTwitter(): self.widget_tree=gtk.glade.XML(path_to_skin, "window") self.widget_tree.signal_autoconnect(self) - - def init_widgets(self): # Get widgets from glade self.tweet_notebook = self.widget_tree.get_widget('tweet_notebook') self.view_menu = self.widget_tree.get_widget('view_menu') @@ -91,6 +93,8 @@ class MyTwitter(): self.verified_label = self.widget_tree.get_widget('verified_label') self.account_select = self.widget_tree.get_widget('account_select') + + def init_widgets(self): self.context_id = self.status_bar.get_context_id('message') # Manual tweaks to the glade UI, to overcome its limitations @@ -105,93 +109,82 @@ class MyTwitter(): # Add the tabs from last session to the notebook page_num = self.db['active_page'] for tab, single_tweet in self.db['open_tabs']: - self.add_to_notebook(tab, single_tweet) + self.add_to_notebook(tab, single_tweet, update=False) self.tweet_notebook.set_current_page(page_num) - # Put Home, @user, Direct Messages, and lists in the View menu for - # each user - for username in self.accounts.keys(): - outer_menu_item = gtk.MenuItem(username) - self.view_menu.append(outer_menu_item) - new_menu = gtk.Menu() - outer_menu_item.set_submenu(new_menu) - - lists = self.accounts[username].GetUserLists() - list_names = [] - for l in lists['lists']: - list_names.append(l.name) - list_names.sort() - list_names.insert(0, 'Home') - list_names.insert(1, '@' + username) - list_names.insert(2, 'Direct Messages') - for l in list_names: - menu_item = gtk.MenuItem(l) - new_menu.append(menu_item) - menu_item.connect('activate', self.on_view_selected, username, l) - menu_item.show() - outer_menu_item.show() + self.update_windows() # Timer to update periodically gobject.timeout_add(self.refresh_time * 1000, self.update_windows) + # Spawns a thread for each pane, which updates that pane. def update_windows(self): for i in range(0, self.tweet_notebook.get_n_pages()): pane = self.tweet_notebook.get_nth_page(i) - list_name = pane.get_list_name() - - # Single tweets should never be updated here - if pane.get_single_tweet() is not None: - continue - - # username/Home entries need to load the appropriate Home feed - elif re.search(r'/Home', list_name): - account = self.accounts[re.sub(r'/Home', r'', list_name)] - statuses = account.GetHomeTimeline(count=self.num_entries) - - # For @username, we need to check if it is one of our usernames, or - # just needs to be searched on - elif re.match('@', list_name): - if self.accounts.has_key(re.sub('@', '', list_name)): - account = self.accounts[re.sub(r'@', r'', list_name)] - statuses = account.GetMentions(count=self.num_entries) - else: - statuses = self.results_to_statuses(self.api.Search(list_name, rpp=self.num_entries)) - - # Direct Messages should match like /Home, above - elif re.search(r'/Direct Messages', list_name): - account = self.accounts[re.sub(r'/Direct Messages', r'', list_name)] - statuses = self.dms_to_statuses(account.GetDirectMessages()) - - # User lookups go straight to the user - elif re.match(r'user: ', list_name): - statuses = self.api.GetUserTimeline(re.sub(r'^user: ', r'', list_name), count=self.num_entries) - - # Lists load the appropriate list from the appropriate account - elif re.match(r'list: ', list_name): - username = re.sub(r'list: (.*)/.*', r'\1', list_name) - real_list = re.sub(r'list: .*/(.*)', r'\1', list_name) - account = self.accounts[username] - statuses = account.GetListStatuses(real_list, per_page=self.num_entries) - - # Everything else is a straight search - else: - statuses = self.results_to_statuses(self.api.Search(list_name, rpp=self.num_entries)) - - pane.update_window(statuses) + self.update_single_window(pane) # We have to return true, so the timeout_add event will keep happening return True - def update_windows_callback(self, widget): - self.update_windows() + def update_single_window(self, pane): + list_name = pane.get_list_name() + + # Single tweets should never be updated here + if pane.get_single_tweet() is not None: + return + + # Determine username and appropriate account to use + found = False + + username = re.sub('/Home', '', list_name) + if self.accounts.has_key(username): + account = self.accounts[username] + found = True + + if not found: + username = re.sub('@', '', list_name) + if self.accounts.has_key(username): + account = self.accounts[username] + found = True + + if not found: + username = re.sub('/Direct Messages', '', list_name) + if self.accounts.has_key(username): + account = self.accounts[username] + found = True + + if not found: + account = self.api + username = self.username + + apithreads.GetTweets(api=account, + list_name=list_name, + pane=pane, + num_entries=self.num_entries, + username=username + ).start() + + + def update_window_callback(self, widget): + pane = self.tweet_notebook.get_nth_page(self.tweet_notebook.get_current_page()) + self.update_single_window(pane) def update_status(self): + reply_id = self.reply_id text = self.update_entry.get_text() self.update_entry.set_text("") - self.api.PostUpdate(text, in_reply_to_status_id=self.reply_id) + + with self.api.lock: + try: + self.api.PostUpdate(text, in_reply_to_status_id=reply_id) + except HTTPError,URLError: + self.update_status_bar('Failed to post tweet') + self.reply_id = None + return + self.reply_id = None self.update_status_bar('Tweet Posted') @@ -207,7 +200,7 @@ class MyTwitter(): self.update_count.set_label(new_count) # If reply_id is set, unset it if we have removed the @ symbol - if self.reply_id is not None and not re.match('@', self.update_entry.get_text()): + if self.reply_id and not re.match('@', self.update_entry.get_text()): self.reply_id = None @@ -227,7 +220,11 @@ class MyTwitter(): def on_retweet(self, widget, data): - self.api.PostRetweet(data['id']) + with self.api.lock: + try: + self.api.PostRetweet(data['id']) + except HTTPError,URLError: + self.update_status_bar('Failed to retweet') def on_reply_to(self, widget, data): @@ -265,7 +262,7 @@ class MyTwitter(): self.remove_view(name, single_tweet) - def add_to_notebook(self, name, single_tweet=None): + def add_to_notebook(self, name, single_tweet=None, update=True): # If it already exists, don't add it, just switch to it for i in range(self.tweet_notebook.get_n_pages()): pane = self.tweet_notebook.get_nth_page(i) @@ -280,21 +277,21 @@ class MyTwitter(): self.tweet_notebook.set_current_page(i) return - # We check for the name so that the special case of - # the first run is handled... + # Add the pane to the persistent database of open panes if (name, single_tweet) not in self.db['open_tabs']: ot = self.db['open_tabs'] ot.append((name,single_tweet)) self.db['open_tabs'] = ot is_user = False - following = False - verified = False if re.match('user:', name): is_user = True - following = self.check_following(name) - verified = self.check_verified(name) - new_pane = TweetPane(name, num_entries=self.num_entries, single_tweet=single_tweet, is_user=is_user, following=following, verified=verified) + new_pane = TweetPane(name, num_entries=self.num_entries, single_tweet=single_tweet, is_user=is_user) + + if is_user: + apithreads.GetFollowing(api=self.api, pane=new_pane, user=name).start() + apithreads.GetVerified(api=self.api, pane=new_pane, user=name).start() + self.tweet_notebook.append_page_menu(new_pane, new_pane.get_tab_label(), gtk.Label(name)) self.tweet_notebook.set_tab_reorderable(new_pane, True) new_pane.get_tab_label().connect('close-clicked', self.remove_view_callback, name, single_tweet) @@ -302,24 +299,25 @@ class MyTwitter(): new_pane.connect('tweet-retweet', self.on_retweet) new_pane.connect('tweet-in-reply-to', self.on_reply_to) new_pane.connect('show-user', self.show_user_callback) + new_pane.connect('following-set', self.update_follow_button) + new_pane.connect('verified-set', self.update_verified_label) # Special logic for single tweet pane if single_tweet is not None: - try: - statuses = [] - statuses.append(self.api.GetStatus(single_tweet)) - new_pane.update_window(statuses) - except HTTPError: - self.tweet_notebook.remove_page(-1) - self.update_status_bar('Error retrieving tweet id: ' + str(single_tweet)) - return + apithreads.GetSingleTweet(api=self.api, + pane=new_pane, + single_tweet=single_tweet).start() - self.update_windows() - self.tweet_notebook.set_current_page(-1) # switch to the new pane + if update: + self.update_windows() + self.tweet_notebook.set_current_page(-1) # switch to the new pane def on_tab_change(self, event, page, page_num): + last_page = self.db['active_page'] self.db['active_page'] = page_num + + # Now get the new page, and set everything up pane = self.tweet_notebook.get_nth_page(page_num) pane.set_tweets_read() self.update_follow_button(pane) @@ -327,10 +325,8 @@ class MyTwitter(): self.at_button.show() else: self.at_button.hide() - if pane.get_verified(): - self.verified_label.show() - else: - self.verified_label.hide() + + self.update_verified_label(pane) def on_tabs_reordered(self, widget, child, page_num): @@ -362,35 +358,26 @@ class MyTwitter(): current_pane = self.tweet_notebook.get_nth_page(self.tweet_notebook.get_current_page()) user_name = re.sub('^user: ', '', current_pane.get_list_name()) if current_pane.get_following(): - self.api.DestroyFriendship(user_name) - current_pane.set_following(self.check_following(user_name)) + with self.api.lock: + try: + self.api.DestroyFriendship(user_name) + except HTTPError,URLError: + self.update_status_bar('Failed to unfollow user.') + return + current_pane.set_following(False) else: - self.api.CreateFriendship(user_name) - current_pane.set_following(self.check_following(user_name)) + with self.api.lock: + try: + self.api.CreateFriendship(user_name) + except HTTPError,URLError: + self.update_status_bar('Failed to follow user.') + return + current_pane.set_following(True) self.update_follow_button(current_pane) - - - # Name is the name of a pane, with the 'user: ' in place - def check_following(self, name): - screen_name = re.sub('user: ', '', name) - try: - relationship = self.api.ShowFriendships(target_screen_name=screen_name) - except HTTPError: - return False - return relationship.source.following - - - # Name is the name of a pane, with the 'user: ' in place - def check_verified(self, name): - screen_name = re.sub('user: ', '', name) - try: - user = self.api.GetUser(screen_name) - except HTTPError: - return False - return user.verified + # pane should be the currently active pane def update_follow_button(self, pane): if not pane.get_is_user(): self.following_button.set_label('') @@ -403,6 +390,13 @@ class MyTwitter(): self.following_button.show() + def update_verified_label(self, pane): + if pane.get_verified(): + self.verified_label.show() + else: + self.verified_label.hide() + + def show_user(self, name): self.add_to_notebook('user: ' + name) @@ -441,68 +435,38 @@ class MyTwitter(): self.api = self.accounts[self.username] - # To keep things simple elsewhere and improve code reuse - # we'll build a list of home-cooked Status objects out of results. - # Why is this even necessary? - # Why can't we have more consistency out of the Twitter API? - def results_to_statuses(self, results): - statuses = [] - for result in results.results: - status = Status() - status.id = result.id - status.user = User() - status.user.screen_name = result.from_user - status.user.name = "" - status.in_reply_to_screen_name = result.to_user - # The Twitter Search API has different timestamps than the - # REST API... balls - # fixme: - # Gotta be a cleaner way to do this, but I can't think of it - # right now - created_at = re.sub(',', '', result.created_at) - created_split = re.split(' ', created_at) - status.created_at = created_split[0] + ' ' + created_split[2] + ' ' + created_split[1] + ' ' + created_split[4] + ' ' + created_split[5] + ' ' + created_split[3] - status.text = result.text - statuses.append(status) - return statuses + def on_lists_ready(self, widget, username, list_names): + # Setup the new sub-menu + outer_menu_item = gtk.MenuItem(username) + self.view_menu.append(outer_menu_item) + new_menu = gtk.Menu() + outer_menu_item.set_submenu(new_menu) + # Insert the default list items + list_names.insert(0, 'Home') + list_names.insert(1, '@' + username) + list_names.insert(2, 'Direct Messages') - def dms_to_statuses(self, direct_messages): - statuses = [] - for dm in direct_messages: - status = Status() - status.id = dm.id - status.user = User() - status.user.screen_name = dm.sender_screen_name - status.user.name = dm.sender.name - status.created_at = dm.created_at - status.text = dm.text - statuses.append(status) - return statuses + # Add the items to the submenu, connect handler + for l in list_names: + menu_item = gtk.MenuItem(l) + new_menu.append(menu_item) + menu_item.connect('activate', self.on_view_selected, username, l) + menu_item.show() + + outer_menu_item.show() ### end class MyTwitter -# We use these classes to emulate a Status object when we need -# one to be built out of something else. -class Status(): - def __init__(self): - self.user = User() - self.id = None - self.created_at = None - self.in_reply_to_screen_name = None - self.in_reply_to_status_id = None - -class User(): - def __init__(self): - self.screen_name = None - self.name = None - - # main parser = optparse.OptionParser() parser.add_option('-c' ,'--config', dest="filename", default="~/.mytwitter.conf") (options, args) = parser.parse_args() my_twitter = MyTwitter(options.filename) + +gtk.gdk.threads_init() +gtk.gdk.threads_enter() gtk.main() +gtk.gdk.threads_leave() diff --git a/twitterwidgets.py b/twitterwidgets.py index 2077dd8..a043a1a 100644 --- a/twitterwidgets.py +++ b/twitterwidgets.py @@ -1,6 +1,8 @@ import re import datetime, dateutil.tz import gtk, gobject +from threading import RLock + class TweetPane(gtk.ScrolledWindow): ''' @@ -13,9 +15,11 @@ class TweetPane(gtk.ScrolledWindow): It also gets some data from its parent, including num_entries ''' - def __init__(self, list_name, num_entries=20, single_tweet=None, is_user=False, following=False, verified=False): + def __init__(self, list_name, num_entries=20, single_tweet=None, is_user=False): gtk.ScrolledWindow.__init__(self) + self.data_lock = RLock() + self.updated_once = False self.list_name = list_name @@ -26,11 +30,13 @@ class TweetPane(gtk.ScrolledWindow): self.num_entries = 1 self.is_user = is_user - self.following = following - self.verified = verified + self.following = False + self.verified = False self.tab_label = CloseTabLabel(self.list_name) + self.message = gtk.Label('Loading...') + # These handle determining which tweets are unread self.last_tweet_read = None self.latest_tweet = None @@ -48,6 +54,8 @@ class TweetPane(gtk.ScrolledWindow): viewport = gtk.Viewport() # Build us some labels... + tweet_box.pack_start(self.message) + for i in range(0, self.num_entries): self.tweets.append(TweetBox()) tweet_box.pack_start(self.tweets[i], expand=False) @@ -68,8 +76,20 @@ class TweetPane(gtk.ScrolledWindow): self.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) self.show_all() + for tweet in self.tweets: + tweet.hide() + def update_window(self, statuses): + if statuses is None: + self.message.set_label('An error occurred while fetching data') + self.message.show() + for i in range(0, self.num_entries): + self.tweets[i].hide() + return + + self.message.hide() + if self.updated_once is False: self.updated_once = True @@ -90,8 +110,13 @@ class TweetPane(gtk.ScrolledWindow): self.num_new_tweets += 1 read = False self.tweets[i].set_status(statuses[i], read) + self.tweets[i].show() else: self.tweets[i].clear_status() + self.tweets[i].hide() + + if len(statuses) == 0: + self.message.set_label('There is no data to display') try: self.latest_tweet = statuses[0].id @@ -152,15 +177,25 @@ class TweetPane(gtk.ScrolledWindow): def get_following(self): - return self.following + with self.data_lock: + return self.following def get_verified(self): - return self.verified + with self.data_lock: + return self.verified def set_following(self, following): - self.following = following + with self.data_lock: + self.following = following + self.emit("following-set") + + + def set_verified(self, verified): + with self.data_lock: + self.verified = verified + self.emit("verified-set") def get_is_user(self): @@ -183,6 +218,12 @@ gobject.signal_new("tweet-in-reply-to", TweetPane, gobject.signal_new("show-user", TweetPane, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) +gobject.signal_new("following-set", TweetPane, + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, ()) +gobject.signal_new("verified-set", TweetPane, + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, ()) @@ -291,7 +332,7 @@ class TweetBox(gtk.VBox): self.text.set_markup(new_text) # If this is in reply to something, set appropriate label - if self.in_reply_to_screen_name: + if self.in_reply_to_screen_name and self.in_reply_to_id: self.reply_to_button.set_label('in reply to ' + self.in_reply_to_screen_name)