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)