Merging in multithread branch now that it is stable enough

This commit is contained in:
Anna 2010-05-12 11:41:14 -04:00
commit 0c280eeb4d
5 changed files with 453 additions and 185 deletions

6
TODO
View File

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

263
apithreads.py Normal file
View File

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

View File

@ -221,7 +221,7 @@
<property name="use_underline">True</property>
<property name="relief">GTK_RELIEF_NORMAL</property>
<property name="focus_on_click">True</property>
<signal name="clicked" handler="update_windows_callback" last_modification_time="Mon, 12 Apr 2010 18:33:51 GMT"/>
<signal name="clicked" handler="update_window_callback" last_modification_time="Thu, 06 May 2010 15:25:15 GMT"/>
</widget>
<packing>
<property name="padding">0</property>

View File

@ -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):
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()
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():
with self.api.lock:
try:
self.api.DestroyFriendship(user_name)
current_pane.set_following(self.check_following(user_name))
except HTTPError,URLError:
self.update_status_bar('Failed to unfollow user.')
return
current_pane.set_following(False)
else:
with self.api.lock:
try:
self.api.CreateFriendship(user_name)
current_pane.set_following(self.check_following(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()

View File

@ -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):
with self.data_lock:
return self.following
def get_verified(self):
with self.data_lock:
return self.verified
def set_following(self, 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)