1444 lines
53 KiB
Python
Executable File
1444 lines
53 KiB
Python
Executable File
import HydrusConstants as HC
|
|
import ClientConstants as CC
|
|
import ClientConstantsMessages
|
|
import ClientGUICommon
|
|
import ClientGUIDialogs
|
|
import ClientGUIMedia
|
|
import cStringIO
|
|
import hashlib
|
|
import HydrusMessageHandling
|
|
import os
|
|
import random
|
|
import threading
|
|
import traceback
|
|
import webbrowser
|
|
import wx
|
|
import wx.html
|
|
import wx.richtext
|
|
import wx.lib.scrolledpanel
|
|
import yaml
|
|
from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
|
|
from wx.lib.mixins.listctrl import ColumnSorterMixin
|
|
|
|
# Sizer Flags
|
|
|
|
FLAGS_NONE = wx.SizerFlags( 0 )
|
|
|
|
FLAGS_SMALL_INDENT = wx.SizerFlags( 0 ).Border( wx.ALL, 2 )
|
|
|
|
FLAGS_EXPAND_PERPENDICULAR = wx.SizerFlags( 0 ).Border( wx.ALL, 2 ).Expand()
|
|
FLAGS_EXPAND_BOTH_WAYS = wx.SizerFlags( 2 ).Border( wx.ALL, 2 ).Expand()
|
|
|
|
FLAGS_EXPAND_SIZER_PERPENDICULAR = wx.SizerFlags( 0 ).Expand()
|
|
FLAGS_EXPAND_SIZER_BOTH_WAYS = wx.SizerFlags( 2 ).Expand()
|
|
|
|
FLAGS_BUTTON_SIZERS = wx.SizerFlags( 0 ).Align( wx.ALIGN_RIGHT )
|
|
FLAGS_LONE_BUTTON = wx.SizerFlags( 0 ).Border( wx.ALL, 2 ).Align( wx.ALIGN_RIGHT )
|
|
|
|
FLAGS_MIXED = wx.SizerFlags( 0 ).Border( wx.ALL, 2 ).Align( wx.ALIGN_CENTER_VERTICAL )
|
|
|
|
class ConversationsListCtrl( wx.ListCtrl, ListCtrlAutoWidthMixin, ColumnSorterMixin ):
|
|
|
|
def __init__( self, parent, page_key, identity, conversations ):
|
|
|
|
wx.ListCtrl.__init__( self, parent, style = wx.LC_REPORT | wx.LC_SINGLE_SEL )
|
|
ListCtrlAutoWidthMixin.__init__( self )
|
|
ColumnSorterMixin.__init__( self, 8 )
|
|
|
|
self._options = HC.app.Read( 'options' )
|
|
|
|
self._page_key = page_key
|
|
self._identity = identity
|
|
|
|
image_list = wx.ImageList( 16, 16, True, 2 )
|
|
|
|
image_list.Add( CC.GlobalBMPs.transparent_bmp )
|
|
image_list.Add( CC.GlobalBMPs.inbox_bmp )
|
|
|
|
self.AssignImageList( image_list, wx.IMAGE_LIST_SMALL )
|
|
|
|
self.InsertColumn( 0, 'inbox', width = 30 )
|
|
self.InsertColumn( 1, 'subject' )
|
|
self.InsertColumn( 2, 'creator', width = 90 )
|
|
self.InsertColumn( 3, 'to', width = 100 )
|
|
self.InsertColumn( 4, 'messages', width = 60 )
|
|
self.InsertColumn( 5, 'unread', width = 60 )
|
|
self.InsertColumn( 6, 'created', width = 130 )
|
|
self.InsertColumn( 7, 'updated', width = 130 )
|
|
|
|
self.setResizeColumn( 2 ) # subject
|
|
|
|
self._SetConversations( conversations )
|
|
|
|
self.Bind( wx.EVT_LIST_ITEM_SELECTED, self.EventSelected )
|
|
self.Bind( wx.EVT_LIST_ITEM_DESELECTED, self.EventSelected )
|
|
self.Bind( wx.EVT_LIST_ITEM_RIGHT_CLICK, self.EventShowMenu )
|
|
self.Bind( wx.EVT_MENU, self.EventMenu )
|
|
|
|
self.RefreshAcceleratorTable()
|
|
|
|
HC.pubsub.sub( self, 'SetConversations', 'set_conversations' )
|
|
HC.pubsub.sub( self, 'ArchiveConversation', 'archive_conversation_gui' )
|
|
HC.pubsub.sub( self, 'InboxConversation', 'inbox_conversation_gui' )
|
|
HC.pubsub.sub( self, 'DeleteConversation', 'delete_conversation_gui' )
|
|
HC.pubsub.sub( self, 'UpdateMessageStatuses', 'message_statuses_gui' )
|
|
HC.pubsub.sub( self, 'RefreshAcceleratorTable', 'options_updated' )
|
|
|
|
|
|
def RefreshAcceleratorTable( self ):
|
|
|
|
entries = [
|
|
( wx.ACCEL_NORMAL, wx.WXK_DELETE, CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'delete' ) ),
|
|
( wx.ACCEL_NORMAL, wx.WXK_NUMPAD_DELETE, CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'delete' ) )
|
|
]
|
|
|
|
for ( modifier, key_dict ) in self._options[ 'shortcuts' ].items(): entries.extend( [ ( modifier, key, CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( action ) ) for ( key, action ) in key_dict.items() ] )
|
|
|
|
self.SetAcceleratorTable( wx.AcceleratorTable( entries ) )
|
|
|
|
|
|
def _GetIndexFromConversationKey( self, conversation_key ):
|
|
|
|
for i in range( self.GetItemCount() ):
|
|
|
|
data_index = self.GetItemData( i )
|
|
|
|
conversation = self._data_indices_to_conversations[ data_index ]
|
|
|
|
if conversation.GetConversationKey() == conversation_key: return i
|
|
|
|
|
|
return None
|
|
|
|
|
|
def _GetPrettyStatus( self ):
|
|
|
|
if len( self._conversations ) == 1: return '1 conversation'
|
|
else: return HC.u( len( self._conversations ) ) + ' conversations'
|
|
|
|
|
|
def _SetConversations( self, conversations ):
|
|
|
|
self._conversations = list( conversations )
|
|
|
|
self.DeleteAllItems()
|
|
|
|
self.itemDataMap = {}
|
|
self._data_indices_to_conversations = {}
|
|
|
|
i = 0
|
|
|
|
cmp_conversations = lambda c1, c2: cmp( c1.GetUpdated(), c2.GetUpdated() )
|
|
|
|
self._conversations.sort( cmp = cmp_conversations, reverse = True ) # order by newest change first
|
|
|
|
for conversation in self._conversations:
|
|
|
|
( conversation_key, inbox, subject, name_from, participants, message_count, unread_count, created, updated ) = conversation.GetListCtrlTuple()
|
|
|
|
if created is None:
|
|
|
|
created_string = ''
|
|
updated_string = ''
|
|
|
|
else:
|
|
|
|
created_string = HC.ConvertTimestampToHumanPrettyTime( created )
|
|
updated_string = HC.ConvertTimestampToHumanPrettyTime( updated )
|
|
|
|
|
|
self.Append( ( '', subject, name_from, ', '.join( [ contact.GetName() for contact in participants if contact.GetName() != name_from ] ), HC.u( message_count ), HC.u( unread_count ), created_string, updated_string ) )
|
|
|
|
data_index = i
|
|
|
|
self.SetItemData( i, data_index )
|
|
|
|
if inbox: self.SetItemImage( i, 1 ) # inbox
|
|
else: self.SetItemImage( i, 0 ) # transparent
|
|
|
|
self.itemDataMap[ data_index ] = ( inbox, subject, name_from, len( participants ), message_count, unread_count, created, updated )
|
|
|
|
self._data_indices_to_conversations[ data_index ] = conversation
|
|
|
|
i += 1
|
|
|
|
|
|
HC.pubsub.pub( 'conversation_focus', self._page_key, None )
|
|
HC.pubsub.pub( 'new_page_status', self._page_key, self._GetPrettyStatus() )
|
|
|
|
|
|
def _UpdateConversationItem( self, conversation_key ):
|
|
|
|
selection = self._GetIndexFromConversationKey( conversation_key )
|
|
|
|
if selection is not None:
|
|
|
|
conversation = self._data_indices_to_conversations[ self.GetItemData( selection ) ]
|
|
|
|
( conversation_key, inbox, subject, name_from, participants, message_count, unread_count, created, updated ) = conversation.GetListCtrlTuple()
|
|
|
|
selection = self._GetIndexFromConversationKey( conversation_key )
|
|
|
|
data_index = self.GetItemData( selection )
|
|
|
|
self.itemDataMap[ data_index ] = ( inbox, subject, name_from, len( participants ), message_count, unread_count, created, updated )
|
|
|
|
if inbox: self.SetItemImage( selection, 1 )
|
|
else: self.SetItemImage( selection, 0 )
|
|
|
|
self.SetStringItem( selection, 4, HC.u( message_count ) )
|
|
self.SetStringItem( selection, 5, HC.u( unread_count ) )
|
|
|
|
if created is None:
|
|
|
|
created_string = ''
|
|
updated_string = ''
|
|
|
|
else:
|
|
|
|
created_string = HC.ConvertTimestampToHumanPrettyTime( created )
|
|
|
|
updated_string = HC.ConvertTimestampToHumanPrettyTime( updated )
|
|
|
|
|
|
self.SetStringItem( selection, 6, created_string )
|
|
self.SetStringItem( selection, 7, updated_string )
|
|
|
|
|
|
|
|
def ArchiveConversation( self, conversation_key ): self._UpdateConversationItem( conversation_key )
|
|
|
|
def DeleteConversation( self, conversation_key ):
|
|
|
|
selection = self._GetIndexFromConversationKey( conversation_key )
|
|
|
|
if selection is not None:
|
|
|
|
conversation = self._data_indices_to_conversations[ self.GetItemData( selection ) ]
|
|
|
|
self._conversations.remove( conversation )
|
|
|
|
self.DeleteItem( selection )
|
|
|
|
|
|
|
|
def EventMenu( self, event ):
|
|
|
|
action = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetAction( event.GetId() )
|
|
|
|
if action is not None:
|
|
|
|
try:
|
|
|
|
( command, data ) = action
|
|
|
|
selection = self.GetFirstSelected()
|
|
|
|
conversation = self._data_indices_to_conversations[ self.GetItemData( selection ) ]
|
|
|
|
conversation_key = conversation.GetConversationKey()
|
|
|
|
identity_contact_key = self._identity.GetContactKey()
|
|
|
|
if command == 'archive': HC.app.Write( 'archive_conversation', conversation_key )
|
|
elif command == 'inbox': HC.app.Write( 'inbox_conversation', conversation_key )
|
|
elif command == 'read':
|
|
|
|
message_keys = conversation.GetMessageKeysWithDestination( ( self._identity, 'sent' ) )
|
|
|
|
for message_key in message_keys: HC.app.Write( 'message_statuses', message_key, [ ( identity_contact_key, 'read' ) ] )
|
|
|
|
elif command == 'unread':
|
|
|
|
message_keys = conversation.GetMessageKeysWithDestination( ( self._identity, 'read' ) )
|
|
|
|
for message_key in message_keys: HC.app.Write( 'message_statuses', message_key, [ ( identity_contact_key, 'sent' ) ] )
|
|
|
|
elif command == 'delete':
|
|
|
|
with ClientGUIDialogs.DialogYesNo( self, 'Are you sure you want to delete this conversation?' ) as dlg:
|
|
|
|
if dlg.ShowModal() == wx.ID_YES: HC.app.Write( 'delete_conversation', conversation_key )
|
|
|
|
|
|
else: event.Skip()
|
|
|
|
except Exception as e:
|
|
|
|
wx.MessageBox( HC.u( e ) )
|
|
wx.MessageBox( traceback.format_exc() )
|
|
|
|
|
|
|
|
def EventSelected( self, event ):
|
|
|
|
selection = self.GetFirstSelected()
|
|
|
|
if selection == wx.NOT_FOUND: HC.pubsub.pub( 'conversation_focus', self._page_key, None )
|
|
else: HC.pubsub.pub( 'conversation_focus', self._page_key, self._data_indices_to_conversations[ self.GetItemData( selection ) ] )
|
|
|
|
|
|
def EventShowMenu( self, event ):
|
|
|
|
conversation = self._data_indices_to_conversations[ self.GetItemData( event.GetIndex() ) ]
|
|
|
|
menu = wx.Menu()
|
|
|
|
if conversation.IsInbox(): menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'archive' ), 'archive' )
|
|
else: menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'inbox' ), 'return to inbox' )
|
|
|
|
if conversation.HasUnread(): menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'read' ), 'set all as read' )
|
|
if conversation.HasRead(): menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'unread' ), 'set all as unread' )
|
|
|
|
menu.AppendSeparator()
|
|
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'delete' ), 'delete' )
|
|
|
|
self.PopupMenu( menu )
|
|
|
|
menu.Destroy()
|
|
|
|
|
|
def GetListCtrl( self ): return self
|
|
|
|
def InboxConversation( self, conversation_key ): self._UpdateConversationItem( conversation_key )
|
|
|
|
def SetConversations( self, page_key, conversations ):
|
|
|
|
if page_key == self._page_key:
|
|
|
|
try: self._SetConversations( conversations )
|
|
except:
|
|
|
|
wx.MessageBox( traceback.format_exc() )
|
|
|
|
|
|
|
|
|
|
def UpdateMessageStatuses( self, message_key, updates ):
|
|
|
|
for conversation in self._data_indices_to_conversations.values():
|
|
|
|
if conversation.HasMessageKey( message_key ):
|
|
|
|
conversation_key = conversation.GetConversationKey()
|
|
|
|
self._UpdateConversationItem( conversation_key )
|
|
|
|
|
|
|
|
|
|
class ConversationPanel( wx.Panel ):
|
|
|
|
def __init__( self, parent, page_key, identity, conversation ):
|
|
|
|
wx.Panel.__init__( self, parent, style = wx.SIMPLE_BORDER )
|
|
|
|
self.SetBackgroundColour( wx.WHITE )
|
|
|
|
self._identity = identity
|
|
self._page_key = page_key
|
|
self._conversation = conversation
|
|
|
|
self._vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
self._message_keys_to_message_panels = {}
|
|
self._draft_keys_to_draft_panels = {}
|
|
|
|
self._scrolling_messages_window = wx.lib.scrolledpanel.ScrolledPanel( self )
|
|
self._scrolling_messages_window.SetupScrolling()
|
|
self._scrolling_messages_window.SetScrollRate( 0, 50 )
|
|
|
|
self._window_vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
self._messages_vbox = wx.BoxSizer( wx.VERTICAL )
|
|
self._drafts_vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
self._window_vbox.AddF( self._messages_vbox, FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
self._window_vbox.AddF( self._drafts_vbox, FLAGS_EXPAND_SIZER_PERPENDICULAR )
|
|
|
|
self._DrawConversation()
|
|
|
|
self.SetSizer( self._vbox )
|
|
|
|
HC.pubsub.sub( self, 'DeleteDraft', 'delete_draft_gui' )
|
|
HC.pubsub.sub( self, 'NewMessage', 'new_message' )
|
|
|
|
|
|
def _DrawConversation( self ):
|
|
|
|
# fix it so this stuff is reusable?
|
|
|
|
self._messages_vbox.DeleteWindows()
|
|
self._drafts_vbox.DeleteWindows()
|
|
|
|
self._convo_frame = wx.Panel( self )
|
|
|
|
convo_vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
subject_static_text = wx.StaticText( self._convo_frame, label = self._conversation.GetSubject() )
|
|
|
|
f = subject_static_text.GetFont()
|
|
|
|
f.SetWeight( wx.BOLD )
|
|
|
|
subject_static_text.SetFont( f )
|
|
|
|
convo_vbox.AddF( subject_static_text, FLAGS_EXPAND_PERPENDICULAR )
|
|
convo_vbox.AddF( wx.StaticText( self._convo_frame, label = ', '.join( contact.GetName() for contact in self._conversation.GetParticipants() ) ), FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self._convo_frame.SetSizer( convo_vbox )
|
|
|
|
# archive_all
|
|
# set all as read
|
|
# delete all button, eventually
|
|
|
|
( messages, drafts ) = self._conversation.GetMessages()
|
|
|
|
for message in messages:
|
|
|
|
message_panel = MessagePanel( self._scrolling_messages_window, message, self._identity )
|
|
|
|
self._message_keys_to_message_panels[ message.GetMessageKey() ] = message_panel
|
|
|
|
self._messages_vbox.AddF( message_panel, FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
|
|
for draft in drafts:
|
|
|
|
draft_panel = DraftPanel( self._scrolling_messages_window, draft )
|
|
|
|
self._draft_keys_to_draft_panels[ draft.GetDraftKey() ] = draft_panel
|
|
|
|
self._drafts_vbox.AddF( draft_panel, FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
|
|
self._reply_button = wx.Button( self._scrolling_messages_window, label = 'reply' )
|
|
self._reply_button.Bind( wx.EVT_BUTTON, self.EventReply )
|
|
self._reply_button.Disable()
|
|
|
|
if len( messages ) > 0 and self._conversation.GetStartedBy().GetName() != 'Anonymous': self._reply_button.Enable()
|
|
|
|
if self._conversation.GetStartedBy() == self._identity: self._reply_button.Enable()
|
|
|
|
self._window_vbox.AddF( self._reply_button, FLAGS_LONE_BUTTON )
|
|
|
|
self._scrolling_messages_window.SetSizer( self._window_vbox )
|
|
|
|
self._vbox.AddF( self._convo_frame, FLAGS_EXPAND_PERPENDICULAR )
|
|
self._vbox.AddF( self._scrolling_messages_window, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self.SetSizer( self._vbox )
|
|
|
|
|
|
def DeleteDraft( self, draft_key ):
|
|
|
|
if draft_key in self._draft_keys_to_draft_panels:
|
|
|
|
draft_panel = self._draft_keys_to_draft_panels[ draft_key ]
|
|
|
|
del self._draft_keys_to_draft_panels[ draft_key ]
|
|
|
|
self._drafts_vbox.Detach( draft_panel )
|
|
|
|
draft_panel.Destroy()
|
|
|
|
self._scrolling_messages_window.FitInside()
|
|
|
|
|
|
|
|
def EventReply( self, event ):
|
|
|
|
draft_key = os.urandom( 32 )
|
|
conversation_key = self._conversation.GetConversationKey()
|
|
subject = self._conversation.GetSubject()
|
|
contact_from = self._identity
|
|
participants = self._conversation.GetParticipants()
|
|
contact_names_to = [ contact.GetName() for contact in participants if contact is not None and contact.GetName() != 'Anonymous' and contact != contact_from ]
|
|
recipients_visible = True
|
|
body = ''
|
|
attachment_hashes = []
|
|
|
|
draft = ClientConstantsMessages.DraftMessage( draft_key, conversation_key, subject, contact_from, contact_names_to, recipients_visible, body, attachment_hashes, is_new = True )
|
|
|
|
self._conversation.AddDraft( draft )
|
|
|
|
draft_panel = DraftPanel( self._scrolling_messages_window, draft )
|
|
|
|
self._draft_keys_to_draft_panels[ draft_key ] = draft_panel
|
|
|
|
self._drafts_vbox.AddF( draft_panel, FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self._scrolling_messages_window.FitInside()
|
|
|
|
|
|
def NewMessage( self, conversation_key, message ):
|
|
|
|
if self._conversation is not None and conversation_key == self._conversation.GetConversationKey():
|
|
|
|
message_key = message.GetMessageKey()
|
|
|
|
if message_key not in self._message_keys_to_message_panels: # if not already here!
|
|
|
|
message_panel = MessagePanel( self._scrolling_messages_window, message, self._identity )
|
|
|
|
self._message_keys_to_message_panels[ message_key ] = message_panel
|
|
|
|
self._messages_vbox.AddF( message_panel, FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self._conversation.AddMessage( message )
|
|
|
|
self._scrolling_messages_window.FitInside()
|
|
|
|
|
|
|
|
|
|
class ConversationSplitter( wx.SplitterWindow ):
|
|
|
|
def __init__( self, parent, page_key, identity, conversations = [] ):
|
|
|
|
wx.SplitterWindow.__init__( self, parent )
|
|
|
|
self._page_key = page_key
|
|
self._identity = identity
|
|
self._conversations = conversations
|
|
|
|
self.SetMinimumPaneSize( 180 )
|
|
self.SetSashGravity( 0.0 )
|
|
|
|
self._InitConversationsPanel()
|
|
self._InitConversationPanel()
|
|
|
|
wx.CallAfter( self.SplitHorizontally, self._conversations_panel, self._conversation_panel, 180 )
|
|
wx.CallAfter( self._conversation_panel.Refresh )
|
|
|
|
HC.pubsub.sub( self, 'SetConversationFocus', 'conversation_focus' )
|
|
|
|
|
|
def _InitConversationsPanel( self ): self._conversations_panel = ConversationsListCtrl( self, self._page_key, self._identity, self._conversations )
|
|
|
|
def _InitConversationPanel( self ): self._conversation_panel = wx.Window( self )
|
|
|
|
def SetConversationFocus( self, page_key, conversation ):
|
|
|
|
if page_key == self._page_key:
|
|
|
|
with wx.FrozenWindow( self ):
|
|
|
|
if conversation is None: new_panel = wx.Window( self )
|
|
else: new_panel = ConversationPanel( self, self._page_key, self._identity, conversation )
|
|
|
|
self.ReplaceWindow( self._conversation_panel, new_panel )
|
|
|
|
self._conversation_panel.Destroy()
|
|
|
|
self._conversation_panel = new_panel
|
|
|
|
|
|
|
|
|
|
class DestinationPanel( wx.Panel ):
|
|
|
|
def __init__( self, parent, message_key, contact, status, identity ):
|
|
|
|
wx.Panel.__init__( self, parent )
|
|
|
|
self.SetBackgroundColour( CC.COLOUR_MESSAGE )
|
|
|
|
self._message_key = message_key
|
|
self._contact = contact
|
|
self._contact_key = contact.GetContactKey()
|
|
self._identity = identity
|
|
self._status = status
|
|
|
|
name = contact.GetName()
|
|
|
|
name_static_text = wx.StaticText( self, label = name )
|
|
|
|
if self._contact == self._identity:
|
|
|
|
f = name_static_text.GetFont()
|
|
|
|
f.SetWeight( wx.BOLD )
|
|
|
|
name_static_text.SetFont( f )
|
|
|
|
if self._status == 'sent': self._status = 'unread'
|
|
|
|
|
|
self._status_panel = self._CreateStatusPanel()
|
|
|
|
self._hbox = wx.BoxSizer( wx.HORIZONTAL )
|
|
|
|
self._hbox.AddF( name_static_text, FLAGS_MIXED )
|
|
self._hbox.AddF( self._status_panel, FLAGS_MIXED )
|
|
|
|
self.SetSizer( self._hbox )
|
|
|
|
self.Bind( wx.EVT_MENU, self.EventMenu )
|
|
|
|
|
|
def _CreateStatusPanel( self ):
|
|
|
|
if self._status == 'failed':
|
|
|
|
status_text = wx.StaticText( self, label = self._status )
|
|
|
|
status_text.SetForegroundColour( ( 128, 0, 0 ) )
|
|
|
|
status_text.SetCursor( wx.StockCursor( wx.CURSOR_HAND ) )
|
|
|
|
status_text.Bind( wx.EVT_LEFT_DOWN, self.EventRetryMenu )
|
|
|
|
elif self._status == 'unread':
|
|
|
|
status_text = wx.StaticText( self, label = self._status )
|
|
|
|
status_text.SetForegroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_HIGHLIGHT ) )
|
|
|
|
status_text.SetCursor( wx.StockCursor( wx.CURSOR_HAND ) )
|
|
|
|
status_text.Bind( wx.EVT_LEFT_DOWN, self.EventReadMenu )
|
|
|
|
elif self._status == 'read':
|
|
|
|
status_text = wx.StaticText( self, label = self._status )
|
|
|
|
status_text.SetForegroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_HIGHLIGHT ) )
|
|
|
|
status_text.SetCursor( wx.StockCursor( wx.CURSOR_HAND ) )
|
|
|
|
status_text.Bind( wx.EVT_LEFT_DOWN, self.EventUnreadMenu )
|
|
|
|
else:
|
|
|
|
status_text = wx.StaticText( self, label = self._status )
|
|
|
|
status_text.SetForegroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_HIGHLIGHT ) )
|
|
|
|
|
|
return status_text
|
|
|
|
|
|
def EventMenu( self, event ):
|
|
|
|
action = CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetAction( event.GetId() )
|
|
|
|
if action is not None:
|
|
|
|
try:
|
|
|
|
( command, data ) = action
|
|
|
|
if command in ( 'retry', 'read', 'unread' ):
|
|
|
|
if command == 'retry': status = 'pending'
|
|
elif command == 'read': status = 'read'
|
|
elif command == 'unread': status = 'sent'
|
|
|
|
my_message_depot = HC.app.Read( 'service', self._identity )
|
|
|
|
connection = my_message_depot.GetConnection()
|
|
|
|
my_public_key = self._identity.GetPublicKey()
|
|
my_contact_key = self._identity.GetContactKey()
|
|
|
|
contacts_contact_key = self._contact.GetContactKey()
|
|
|
|
status_updates = []
|
|
|
|
status_key = hashlib.sha256( contacts_contact_key + self._message_key ).digest()
|
|
|
|
packaged_status = HydrusMessageHandling.PackageStatusForDelivery( ( self._message_key, contacts_contact_key, status ), my_public_key )
|
|
|
|
status_updates = ( ( status_key, packaged_status ), )
|
|
|
|
connection.Post( 'message_statuses', contact_key = my_contact_key, statuses = status_updates )
|
|
|
|
HC.app.Write( 'message_statuses', self._message_key, [ ( self._contact_key, status ) ] )
|
|
|
|
else: event.Skip()
|
|
|
|
except Exception as e:
|
|
|
|
wx.MessageBox( 'Could not contact your message depot, so could not update status!' )
|
|
|
|
wx.MessageBox( HC.u( e ) )
|
|
wx.MessageBox( traceback.format_exc() )
|
|
|
|
|
|
|
|
|
|
def EventReadMenu( self, event ):
|
|
|
|
menu = wx.Menu()
|
|
|
|
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'read' ), 'read' )
|
|
|
|
self.PopupMenu( menu )
|
|
|
|
menu.Destroy()
|
|
|
|
|
|
def EventRetryMenu( self, event ):
|
|
|
|
menu = wx.Menu()
|
|
|
|
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'retry' ), 'retry' )
|
|
|
|
self.PopupMenu( menu )
|
|
|
|
menu.Destroy()
|
|
|
|
|
|
def EventUnreadMenu( self, event ):
|
|
|
|
menu = wx.Menu()
|
|
|
|
menu.Append( CC.MENU_EVENT_ID_TO_ACTION_CACHE.GetId( 'unread' ), 'unread' )
|
|
|
|
self.PopupMenu( menu )
|
|
|
|
menu.Destroy()
|
|
|
|
|
|
def SetStatus( self, status ):
|
|
|
|
if self._contact == self._identity and status == 'sent': status = 'unread'
|
|
|
|
self._status = status
|
|
|
|
new_status_panel = self._CreateStatusPanel()
|
|
|
|
self._hbox.Replace( self._status_panel, new_status_panel )
|
|
|
|
self._status_panel.Destroy()
|
|
|
|
self._status_panel = new_status_panel
|
|
|
|
|
|
class DestinationsPanel( wx.Panel ):
|
|
|
|
def __init__( self, parent, message_key, destinations, identity ):
|
|
|
|
wx.Panel.__init__( self, parent )
|
|
|
|
self.SetBackgroundColour( CC.COLOUR_MESSAGE )
|
|
|
|
self._message_key = message_key
|
|
self._my_panels = {}
|
|
|
|
vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
for ( contact, status ) in destinations:
|
|
|
|
destination_panel = DestinationPanel( self, message_key, contact, status, identity )
|
|
|
|
vbox.AddF( destination_panel, FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
self._my_panels[ contact.GetContactKey() ] = destination_panel
|
|
|
|
|
|
self.SetSizer( vbox )
|
|
|
|
HC.pubsub.sub( self, 'UpdateMessageStatuses', 'message_statuses_gui' )
|
|
|
|
|
|
def UpdateMessageStatuses( self, message_key, updates ):
|
|
|
|
if message_key == self._message_key:
|
|
|
|
with wx.FrozenWindow( self ):
|
|
|
|
for ( contact_key, status ) in updates:
|
|
|
|
if contact_key in self._my_panels: self._my_panels[ contact_key ].SetStatus( status )
|
|
|
|
|
|
# doing replace on the destpanels' tricky sizer is a huge pain, hence the size event
|
|
# has to be postevent, not processevent
|
|
wx.PostEvent( self.GetParent(), wx.SizeEvent() )
|
|
|
|
|
|
|
|
|
|
# A whole bunch of this is cribbed from/inspired by the excellent rtc example in the wxPython Demo
|
|
class DraftBodyPanel( wx.Panel ):
|
|
|
|
ID_BOLD = 0
|
|
ID_ITALIC = 1
|
|
ID_UNDERLINE = 2
|
|
|
|
ID_ALIGN_LEFT = 3
|
|
ID_ALIGN_CENTER = 4
|
|
ID_ALIGN_RIGHT = 5
|
|
ID_ALIGN_JUSTIFY = 6 # rtc doesn't yet support this, sadly
|
|
|
|
ID_INDENT_LESS = 7
|
|
ID_INDENT_MORE = 8
|
|
|
|
ID_FONT = 9
|
|
ID_FONT_COLOUR = 10
|
|
|
|
ID_LINK = 11
|
|
ID_LINK_BREAK = 12
|
|
|
|
def __init__( self, parent, xml ):
|
|
|
|
wx.Panel.__init__( self, parent )
|
|
|
|
self._CreateToolBar()
|
|
|
|
self._CreateRTC( xml )
|
|
|
|
vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
vbox.AddF( self._toolbar, FLAGS_EXPAND_PERPENDICULAR )
|
|
vbox.AddF( self._rtc, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
self.SetSizer( vbox )
|
|
|
|
self.SetAcceleratorTable( wx.AcceleratorTable( [
|
|
( wx.ACCEL_CMD, ord( 'b' ), self.ID_BOLD ),
|
|
( wx.ACCEL_CMD, ord( 'i' ), self.ID_ITALIC ),
|
|
( wx.ACCEL_CMD, ord( 'u' ), self.ID_UNDERLINE )
|
|
] ) )
|
|
|
|
self.Bind( wx.EVT_TOOL, self.EventToolBar )
|
|
|
|
self.Bind( wx.EVT_UPDATE_UI, self.EventUpdateUI )
|
|
|
|
|
|
def _CreateToolBar( self ):
|
|
|
|
self._toolbar = wx.ToolBar( self )
|
|
|
|
self._toolbar.SetToolBitmapSize( ( 16, 16 ) )
|
|
|
|
self._toolbar.AddCheckTool( self.ID_BOLD, CC.GlobalBMPs.bold_bmp )
|
|
self._toolbar.AddCheckTool( self.ID_ITALIC, CC.GlobalBMPs.italic_bmp )
|
|
self._toolbar.AddCheckTool( self.ID_UNDERLINE, CC.GlobalBMPs.underline_bmp )
|
|
|
|
self._toolbar.AddSeparator()
|
|
|
|
self._toolbar.AddRadioTool( self.ID_ALIGN_LEFT, CC.GlobalBMPs.align_left_bmp )
|
|
self._toolbar.AddRadioTool( self.ID_ALIGN_CENTER, CC.GlobalBMPs.align_center_bmp )
|
|
self._toolbar.AddRadioTool( self.ID_ALIGN_RIGHT, CC.GlobalBMPs.align_right_bmp )
|
|
|
|
self._toolbar.AddSeparator()
|
|
|
|
self._toolbar.AddLabelTool( self.ID_INDENT_LESS, 'indent less', CC.GlobalBMPs.indent_less_bmp )
|
|
self._toolbar.AddLabelTool( self.ID_INDENT_MORE, 'indent more', CC.GlobalBMPs.indent_more_bmp )
|
|
|
|
self._toolbar.AddSeparator()
|
|
|
|
self._toolbar.AddLabelTool( self.ID_FONT, 'font', CC.GlobalBMPs.font_bmp )
|
|
self._toolbar.AddLabelTool( self.ID_FONT_COLOUR, 'font colour', CC.GlobalBMPs.colour_bmp, shortHelp = 'font colour' )
|
|
|
|
# font background
|
|
# message background?
|
|
|
|
self._toolbar.AddSeparator()
|
|
|
|
self._toolbar.AddLabelTool( self.ID_LINK, 'link', CC.GlobalBMPs.link_bmp )
|
|
self._toolbar.AddLabelTool( self.ID_LINK_BREAK, 'break link', CC.GlobalBMPs.link_break_bmp )
|
|
|
|
self._toolbar.Realize()
|
|
|
|
|
|
def _CreateRTC( self, xml ):
|
|
|
|
self._rtc = wx.richtext.RichTextCtrl( self, size = ( -1, 300 ), style = wx.WANTS_CHARS | wx.richtext.RE_MULTILINE )
|
|
|
|
if len( xml ) > 0:
|
|
|
|
xml_handler = wx.richtext.RichTextXMLHandler()
|
|
|
|
stream = cStringIO.StringIO( xml )
|
|
|
|
xml_handler.LoadStream( self._rtc.GetBuffer(), stream )
|
|
|
|
|
|
|
|
def EventUpdateUI( self, event ):
|
|
|
|
self._toolbar.ToggleTool( self.ID_BOLD, self._rtc.IsSelectionBold() )
|
|
self._toolbar.ToggleTool( self.ID_ITALIC, self._rtc.IsSelectionItalics() )
|
|
self._toolbar.ToggleTool( self.ID_UNDERLINE, self._rtc.IsSelectionUnderlined() )
|
|
|
|
if self._rtc.IsSelectionAligned( wx.TEXT_ALIGNMENT_LEFT ): self._toolbar.ToggleTool( self.ID_ALIGN_LEFT, True )
|
|
elif self._rtc.IsSelectionAligned( wx.TEXT_ALIGNMENT_CENTER ): self._toolbar.ToggleTool( self.ID_ALIGN_CENTER, True )
|
|
elif self._rtc.IsSelectionAligned( wx.TEXT_ALIGNMENT_RIGHT ): self._toolbar.ToggleTool( self.ID_ALIGN_RIGHT, True )
|
|
|
|
event.Skip()
|
|
|
|
|
|
def EventToolBar( self, event ):
|
|
|
|
id = event.GetId()
|
|
|
|
if id == self.ID_BOLD: self._rtc.ApplyBoldToSelection()
|
|
elif id == self.ID_ITALIC: self._rtc.ApplyItalicToSelection()
|
|
elif id == self.ID_UNDERLINE: self._rtc.ApplyUnderlineToSelection()
|
|
elif id == self.ID_ALIGN_LEFT: self._rtc.ApplyAlignmentToSelection( wx.TEXT_ALIGNMENT_LEFT )
|
|
elif id == self.ID_ALIGN_CENTER: self._rtc.ApplyAlignmentToSelection( wx.TEXT_ALIGNMENT_CENTRE )
|
|
elif id == self.ID_ALIGN_RIGHT: self._rtc.ApplyAlignmentToSelection( wx.TEXT_ALIGNMENT_RIGHT )
|
|
elif id == self.ID_INDENT_LESS:
|
|
|
|
text_attribute = wx.TEXTAttrEx()
|
|
|
|
text_attribute.SetFlags( wx.TEXT_ATTR_LEFT_INDENT )
|
|
|
|
ip = self._rtc.GetInsertionPoint()
|
|
|
|
if self._rtc.GetStyle( ip, text_attribute ): # this copies the current style into text_attribute, returning true if successful
|
|
|
|
if self._rtc.HasSelection(): selection_range = self._rtc.GetSelectionRange()
|
|
else: selection_range = wx.richtext.RichTextRange( ip, ip )
|
|
|
|
if text_attribute.GetLeftIndent() >= 100:
|
|
|
|
text_attribute.SetLeftIndent( text_attribute.GetLeftIndent() - 100 )
|
|
text_attribute.SetFlags( wx.TEXT_ATTR_LEFT_INDENT )
|
|
|
|
self._rtc.SetStyle( selection_range, text_attribute )
|
|
|
|
|
|
|
|
elif id == self.ID_INDENT_MORE:
|
|
|
|
text_attribute = wx.richtext.TextAttrEx()
|
|
|
|
text_attribute.SetFlags( wx.TEXT_ATTR_LEFT_INDENT )
|
|
|
|
ip = self._rtc.GetInsertionPoint()
|
|
|
|
if self._rtc.GetStyle( ip, text_attribute ): # this copies the current style into text_attribute, returning true if successful
|
|
|
|
if self._rtc.HasSelection(): selection_range = self._rtc.GetSelectionRange()
|
|
else: selection_range = wx.richtext.RichTextRange( ip, ip )
|
|
|
|
text_attribute.SetLeftIndent( text_attribute.GetLeftIndent() + 100 )
|
|
text_attribute.SetFlags( wx.TEXT_ATTR_LEFT_INDENT )
|
|
|
|
self._rtc.SetStyle( selection_range, text_attribute )
|
|
|
|
|
|
elif id == self.ID_FONT:
|
|
|
|
font_data = wx.FontData()
|
|
font_data.EnableEffects( False )
|
|
|
|
text_attribute = wx.richtext.TextAttrEx()
|
|
text_attribute.SetFlags( wx.TEXT_ATTR_FONT )
|
|
|
|
if self._rtc.GetStyle( self._rtc.GetInsertionPoint(), text_attribute ): font_data.SetInitialFont( text_attribute.GetFont() )
|
|
|
|
with wx.FontDialog( self, font_data ) as dlg:
|
|
|
|
if dlg.ShowModal() == wx.ID_OK:
|
|
|
|
font_data = dlg.GetFontData()
|
|
|
|
font = font_data.GetChosenFont()
|
|
|
|
if not self._rtc.HasSelection(): self._rtc.BeginFont( font )
|
|
else:
|
|
|
|
selection_range = self._rtc.GetSelectionRange()
|
|
|
|
text_attribute.SetFlags( wx.TEXT_ATTR_FONT )
|
|
text_attribute.SetFont( font )
|
|
|
|
self._rtc.SetStyle( selection_range, text_attribute )
|
|
|
|
|
|
|
|
|
|
elif id == self.ID_FONT_COLOUR:
|
|
|
|
colour_data = wx.ColourData()
|
|
|
|
text_attribute = wx.richtext.TextAttrEx()
|
|
text_attribute.SetFlags( wx.TEXT_ATTR_TEXT_COLOUR )
|
|
|
|
if self._rtc.GetStyle( self._rtc.GetInsertionPoint(), text_attribute ): colour_data.SetColour( text_attribute.GetTextColour() )
|
|
|
|
with wx.ColourDialog( self, colour_data ) as dlg:
|
|
|
|
if dlg.ShowModal() == wx.ID_OK:
|
|
|
|
colour_data = dlg.GetColourData()
|
|
colour = colour_data.GetColour()
|
|
|
|
if colour:
|
|
|
|
if not self._rtc.HasSelection(): self._rtc.BeginTextColour( colour )
|
|
else:
|
|
|
|
selection_range = self._rtc.GetSelectionRange()
|
|
|
|
text_attribute.SetFlags( wx.TEXT_ATTR_TEXT_COLOUR )
|
|
text_attribute.SetTextColour( colour )
|
|
|
|
self._rtc.SetStyle( selection_range, text_attribute )
|
|
|
|
|
|
|
|
|
|
|
|
elif id == self.ID_LINK:
|
|
|
|
text_attribute = wx.richtext.TextAttrEx()
|
|
|
|
text_attribute.SetFlags( wx.TEXT_ATTR_URL )
|
|
|
|
ip = self._rtc.GetInsertionPoint()
|
|
|
|
self._rtc.GetStyle( self._rtc.GetInsertionPoint(), text_attribute )
|
|
|
|
if text_attribute.HasURL(): initial_url = text_attribute.GetURL()
|
|
else: initial_url = 'http://'
|
|
|
|
with wx.TextEntryDialog( self, 'Enter url', defaultValue = initial_url ) as dlg:
|
|
|
|
if dlg.ShowModal() == wx.ID_OK:
|
|
|
|
url = dlg.GetValue()
|
|
|
|
if self._rtc.HasSelection(): selection_range = self._rtc.GetSelectionRange()
|
|
else: selection_range = wx.richtext.RichTextRange( ip, ip )
|
|
|
|
text_attribute.SetFlags( wx.TEXT_ATTR_TEXT_COLOUR )
|
|
text_attribute.SetTextColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_HIGHLIGHT ) )
|
|
|
|
text_attribute.SetFontUnderlined( True )
|
|
|
|
text_attribute.SetURL( url )
|
|
|
|
self._rtc.SetStyle( selection_range, text_attribute )
|
|
|
|
|
|
|
|
elif id == self.ID_LINK_BREAK:
|
|
|
|
if self._rtc.HasSelection(): selection_range = self._rtc.GetSelectionRange()
|
|
else: selection_range = wx.richtext.RichTextRange( ip, ip )
|
|
|
|
text_attribute = wx.richtext.TextAttrEx()
|
|
|
|
text_attribute.SetFlags( wx.TEXT_ATTR_TEXT_COLOUR )
|
|
text_attribute.SetTextColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_WINDOWTEXT ) )
|
|
|
|
text_attribute.SetFontUnderlined( False )
|
|
|
|
self._rtc.SetStyle( selection_range, text_attribute )
|
|
|
|
text_attribute = wx.richtext.TextAttrEx()
|
|
|
|
text_attribute.SetFlags( wx.TEXT_ATTR_URL )
|
|
|
|
self._rtc.SetStyleEx( selection_range, text_attribute, wx.richtext.RICHTEXT_SETSTYLE_REMOVE )
|
|
|
|
|
|
|
|
def GetXMLHTML( self ):
|
|
|
|
xml_handler = wx.richtext.RichTextXMLHandler()
|
|
|
|
stream = cStringIO.StringIO()
|
|
|
|
xml_handler.SaveStream( self._rtc.GetBuffer(), stream )
|
|
|
|
stream.seek( 0 )
|
|
|
|
xml = stream.read()
|
|
|
|
html_handler = wx.richtext.RichTextHTMLHandler()
|
|
html_handler.SetFlags( wx.richtext.RICHTEXT_HANDLER_SAVE_IMAGES_TO_MEMORY )
|
|
html_handler.SetFontSizeMapping( [7,9,11,12,14,22,100] )
|
|
|
|
stream = cStringIO.StringIO()
|
|
|
|
html_handler.SaveStream( self._rtc.GetBuffer(), stream )
|
|
|
|
stream.seek( 0 )
|
|
|
|
html = stream.read()
|
|
|
|
return yaml.safe_dump( ( xml, html ) )
|
|
|
|
|
|
|
|
class DraftPanel( wx.Panel ):
|
|
|
|
def __init__( self, parent, draft_message ):
|
|
|
|
wx.Panel.__init__( self, parent )
|
|
|
|
self.SetBackgroundColour( CC.COLOUR_MESSAGE )
|
|
|
|
self._compose_key = os.urandom( 32 )
|
|
|
|
self._draft_message = draft_message
|
|
|
|
( self._draft_key, self._conversation_key, subject, self._contact_from, contacts_to, recipients_visible, body, attachment_hashes ) = self._draft_message.GetInfo()
|
|
|
|
is_new = self._draft_message.IsNew()
|
|
|
|
self._from = wx.StaticText( self, label = self._contact_from.GetName() )
|
|
|
|
if not self._draft_message.IsReply():
|
|
|
|
self._to_panel = ClientGUICommon.StaticBox( self, 'to' )
|
|
|
|
self._recipients_list = wx.ListCtrl( self._to_panel, style = wx.LC_LIST | wx.LC_NO_HEADER | wx.LC_SINGLE_SEL )
|
|
self._recipients_list.InsertColumn( 0, 'contacts' )
|
|
for name in contacts_to: self._recipients_list.Append( ( name, ) )
|
|
self._recipients_list.Bind( wx.EVT_LIST_ITEM_ACTIVATED, self.EventRemove )
|
|
|
|
self._new_recipient = ClientGUICommon.AutoCompleteDropdownContacts( self._to_panel, self._compose_key, self._contact_from )
|
|
|
|
self._recipients_visible = wx.CheckBox( self._to_panel )
|
|
self._recipients_visible.SetValue( recipients_visible )
|
|
self._recipients_visible.Bind( wx.EVT_CHECKBOX, self.EventChanged )
|
|
|
|
self._subject_panel = ClientGUICommon.StaticBox( self, 'subject' )
|
|
|
|
self._subject = wx.TextCtrl( self._subject_panel, value = subject )
|
|
self._subject.Bind( wx.EVT_KEY_DOWN, self.EventChanged )
|
|
|
|
|
|
if body == '': xml = ''
|
|
else: ( xml, html ) = yaml.safe_load( body )
|
|
|
|
self._body = DraftBodyPanel( self, xml )
|
|
self.Bind( wx.richtext.EVT_RICHTEXT_STYLE_CHANGED, self.EventChanged )
|
|
self.Bind( wx.richtext.EVT_RICHTEXT_CHARACTER, self.EventChanged )
|
|
self.Bind( wx.richtext.EVT_RICHTEXT_RETURN, self.EventChanged )
|
|
self.Bind( wx.richtext.EVT_RICHTEXT_DELETE, self.EventChanged )
|
|
|
|
self._attachments = wx.TextCtrl( self, value = os.linesep.join( [ hash.encode( 'hex' ) for hash in attachment_hashes ] ), style = wx.TE_MULTILINE )
|
|
self._attachments.Bind( wx.EVT_KEY_DOWN, self.EventChanged )
|
|
# do thumbnails later! for now, do a listbox or whatever
|
|
|
|
self._send = wx.Button( self, label = 'send' )
|
|
self._send.Bind( wx.EVT_BUTTON, self.EventSend )
|
|
self._send.SetForegroundColour( ( 0, 128, 0 ) )
|
|
if len( contacts_to ) == 0: self._send.Disable()
|
|
|
|
self._delete_draft = wx.Button( self, label = 'delete' )
|
|
self._delete_draft.Bind( wx.EVT_BUTTON, self.EventDeleteDraft )
|
|
self._delete_draft.SetForegroundColour( ( 128, 0, 0 ) )
|
|
|
|
self._save_draft = wx.Button( self, label = 'save' )
|
|
self._save_draft.Bind( wx.EVT_BUTTON, self.EventSaveDraft )
|
|
|
|
if is_new:
|
|
|
|
self._draft_changed = True
|
|
self._delete_draft.SetLabel( 'discard' )
|
|
|
|
else:
|
|
|
|
self._draft_changed = False
|
|
self._save_draft.SetLabel( 'saved' )
|
|
self._save_draft.Disable()
|
|
|
|
|
|
vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
vbox.AddF( self._from, FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
if not self._draft_message.IsReply():
|
|
|
|
recipients_hbox = wx.BoxSizer( wx.HORIZONTAL )
|
|
|
|
recipients_hbox.AddF( wx.StaticText( self._to_panel, label = 'recipients can see each other' ), FLAGS_MIXED )
|
|
recipients_hbox.AddF( self._recipients_visible, FLAGS_MIXED )
|
|
|
|
self._to_panel.AddF( self._recipients_list, FLAGS_EXPAND_PERPENDICULAR )
|
|
self._to_panel.AddF( self._new_recipient, FLAGS_LONE_BUTTON )
|
|
self._to_panel.AddF( recipients_hbox, FLAGS_BUTTON_SIZERS )
|
|
|
|
self._subject_panel.AddF( self._subject, FLAGS_EXPAND_BOTH_WAYS )
|
|
|
|
vbox.AddF( self._to_panel, FLAGS_EXPAND_PERPENDICULAR )
|
|
vbox.AddF( self._subject_panel, FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
|
|
vbox.AddF( self._body, FLAGS_EXPAND_BOTH_WAYS )
|
|
#vbox.AddF( wx.StaticText( self, label = 'attachment hashes:' ), FLAGS_MIXED )
|
|
#vbox.AddF( self._attachments, FLAGS_EXPAND_PERPENDICULAR )
|
|
self._attachments.Hide()
|
|
button_hbox = wx.BoxSizer( wx.HORIZONTAL )
|
|
button_hbox.AddF( self._send, FLAGS_MIXED )
|
|
button_hbox.AddF( self._delete_draft, FLAGS_MIXED )
|
|
button_hbox.AddF( self._save_draft, FLAGS_MIXED )
|
|
|
|
vbox.AddF( button_hbox, FLAGS_BUTTON_SIZERS )
|
|
|
|
self.SetSizer( vbox )
|
|
|
|
HC.pubsub.sub( self, 'AddContact', 'add_contact' )
|
|
HC.pubsub.sub( self, 'DraftSaved', 'draft_saved' )
|
|
|
|
if not self._draft_message.IsReply(): wx.CallAfter( self._new_recipient.SetFocus )
|
|
|
|
|
|
def _GetDraftMessage( self ):
|
|
|
|
( self._draft_key, self._conversation_key, subject, self._contact_from, contacts_to, recipients_visible, body, attachment_hashes ) = self._draft_message.GetInfo()
|
|
|
|
if not self._draft_message.IsReply():
|
|
|
|
subject = self._subject.GetValue()
|
|
contacts_to = [ self._recipients_list.GetItemText( i ) for i in range( self._recipients_list.GetItemCount() ) ]
|
|
recipients_visible = self._recipients_visible.GetValue()
|
|
|
|
|
|
body = self._body.GetXMLHTML()
|
|
|
|
try:
|
|
|
|
raw_attachments = self._attachments.GetValue()
|
|
|
|
attachment_hashes = [ hash.decode( 'hex' ) for hash in raw_attachments.split( os.linesep ) if hash != '' ]
|
|
|
|
except:
|
|
|
|
attachment_hashes = []
|
|
|
|
wx.MessageBox( 'Could not parse attachments!' )
|
|
|
|
|
|
return ClientConstantsMessages.DraftMessage( self._draft_key, self._conversation_key, subject, self._contact_from, contacts_to, recipients_visible, body, attachment_hashes )
|
|
|
|
|
|
def AddContact( self, compose_key, name ):
|
|
|
|
if compose_key == self._compose_key:
|
|
|
|
index = self._recipients_list.FindItem( -1, name )
|
|
|
|
if index == -1: self._recipients_list.Append( ( name, ) )
|
|
else: self._recipients_list.DeleteItem( index )
|
|
|
|
self.EventChanged( None )
|
|
|
|
|
|
|
|
def DraftSaved( self, draft_key, draft_message ):
|
|
|
|
if draft_key == self._draft_key:
|
|
|
|
self._draft_changed = False
|
|
|
|
self._save_draft.SetLabel( 'saved' )
|
|
self._save_draft.Disable()
|
|
|
|
self._delete_draft.SetLabel( 'delete' )
|
|
|
|
self._draft_message.Saved()
|
|
|
|
|
|
|
|
def EventChanged( self, event ):
|
|
|
|
if not self._draft_changed:
|
|
|
|
self._draft_changed = True
|
|
|
|
self._send.Enable()
|
|
self._save_draft.Enable()
|
|
|
|
|
|
if event is not None: event.Skip()
|
|
|
|
|
|
def EventDeleteDraft( self, event ): HC.app.Write( 'delete_draft', self._draft_key )
|
|
|
|
def EventSend( self, event ):
|
|
|
|
draft_message = self._GetDraftMessage()
|
|
|
|
transport_messages = HC.app.Read( 'transport_messages_from_draft', draft_message )
|
|
|
|
if self._contact_from.GetName() != 'Anonymous':
|
|
|
|
try:
|
|
|
|
my_message_depot = HC.app.Read( 'service', self._contact_from )
|
|
|
|
connection = my_message_depot.GetConnection()
|
|
|
|
my_public_key = self._contact_from.GetPublicKey()
|
|
my_contact_key = self._contact_from.GetContactKey()
|
|
|
|
for transport_message in transport_messages:
|
|
|
|
packaged_message = HydrusMessageHandling.PackageMessageForDelivery( transport_message, my_public_key )
|
|
|
|
connection.Post( 'message', contact_key = my_contact_key, message = packaged_message )
|
|
|
|
message_key = transport_message.GetMessageKey()
|
|
|
|
status_updates = []
|
|
|
|
for contact_to in transport_message.GetContactsTo():
|
|
|
|
contact_to_key = contact_to.GetContactKey()
|
|
|
|
status_key = hashlib.sha256( contact_to_key + message_key ).digest()
|
|
|
|
status = HydrusMessageHandling.PackageStatusForDelivery( ( message_key, contact_to_key, 'pending' ), my_public_key )
|
|
|
|
status_updates.append( ( status_key, status ) )
|
|
|
|
|
|
connection.Post( 'message_statuses', contact_key = my_contact_key, statuses = status_updates )
|
|
|
|
|
|
except:
|
|
|
|
message = 'The hydrus client could not connect to your message depot, so the message could not be sent!'
|
|
|
|
wx.MessageBox( message )
|
|
HC.pubsub.pub( 'message', HC.Message( HC.MESSAGE_TYPE_ERROR, Exception( message ) ) )
|
|
print( traceback.format_exc() )
|
|
|
|
return
|
|
|
|
|
|
|
|
for transport_message in transport_messages: HC.app.Write( 'message', transport_message, forced_status = 'pending' )
|
|
|
|
draft_key = draft_message.GetDraftKey()
|
|
|
|
HC.app.Write( 'delete_draft', draft_key )
|
|
|
|
|
|
def EventSaveDraft( self, event ):
|
|
|
|
draft_message = self._GetDraftMessage()
|
|
|
|
HC.app.Write( 'draft_message', draft_message )
|
|
|
|
|
|
def EventRemove( self, event ):
|
|
|
|
selection = self._recipients_list.GetFirstSelected()
|
|
|
|
if selection != wx.NOT_FOUND:
|
|
|
|
self._recipients_list.DeleteItem( selection )
|
|
|
|
self.EventChanged( None )
|
|
|
|
|
|
|
|
def GetConversationKey( self ): return self._conversation_key
|
|
|
|
def GetDraftKey( self ): return self._draft_key
|
|
|
|
class MessageHTML( wx.html.HtmlWindow ):
|
|
|
|
def __init__( self, *args, **kwargs ):
|
|
|
|
kwargs[ 'style' ] = wx.html.HW_SCROLLBAR_NEVER
|
|
|
|
wx.html.HtmlWindow.__init__( self, *args, **kwargs )
|
|
|
|
self.Bind( wx.EVT_MOUSEWHEEL, self.EventScroll )
|
|
|
|
self.SetRelatedFrame( wx.GetTopLevelParent( self ), '%s' )
|
|
self.SetRelatedStatusBar( 0 )
|
|
|
|
|
|
def EventScroll( self, event ):
|
|
|
|
sw = self.GetParent().GetParent()
|
|
|
|
sw.GetEventHandler().ProcessEvent( event )
|
|
|
|
|
|
def GetClientSize( self ): return self.GetSize()
|
|
|
|
def OnLinkClicked( self, link ): webbrowser.open( link.GetHref() )
|
|
|
|
def OnOpeningURL( self, type, url, redirect ): return wx.html.HTML_BLOCK
|
|
|
|
class MessagePanel( wx.Panel ):
|
|
|
|
def __init__( self, parent, message, identity ):
|
|
|
|
wx.Panel.__init__( self, parent )
|
|
|
|
self.SetBackgroundColour( CC.COLOUR_MESSAGE )
|
|
|
|
self._message = message
|
|
self._identity = identity
|
|
|
|
vbox = wx.BoxSizer( wx.VERTICAL )
|
|
|
|
contact_from = self._message.GetContactFrom()
|
|
|
|
if contact_from is None: name = 'Anonymous'
|
|
else: name = self._message.GetContactFrom().GetName()
|
|
|
|
#vbox.AddF( wx.StaticText( self, label = name + ', ' + HC.ConvertTimestampToPrettyAgo( self._message.GetTimestamp() ) ), FLAGS_EXPAND_PERPENDICULAR )
|
|
vbox.AddF( ClientGUICommon.AnimatedStaticTextTimestamp( self, name + ', ', HC.ConvertTimestampToPrettyAgo, self._message.GetTimestamp(), '' ), FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
body = self._message.GetBody()
|
|
|
|
display_body = not ( body is None or body == '' )
|
|
|
|
if display_body:
|
|
|
|
self._body_panel = wx.Panel( self )
|
|
|
|
wx.CallAfter( self.SetBody, body )
|
|
|
|
else: self._body_panel = wx.StaticText( self, label = 'no body' )
|
|
|
|
self._message_key = self._message.GetMessageKey()
|
|
destinations = self._message.GetDestinations()
|
|
|
|
self._destinations_panel = DestinationsPanel( self, self._message_key, destinations, identity )
|
|
|
|
self._hbox = wx.BoxSizer( wx.HORIZONTAL )
|
|
|
|
self._hbox.AddF( self._body_panel, FLAGS_EXPAND_BOTH_WAYS )
|
|
self._hbox.AddF( self._destinations_panel, FLAGS_EXPAND_PERPENDICULAR )
|
|
|
|
vbox.AddF( self._hbox, FLAGS_EXPAND_SIZER_BOTH_WAYS )
|
|
|
|
# vbox.AddF( some kind of attachment window! )
|
|
|
|
self.SetSizer( vbox )
|
|
|
|
|
|
def SetBody( self, body ):
|
|
|
|
with wx.FrozenWindow( self ):
|
|
|
|
( width, height ) = self._body_panel.GetClientSize()
|
|
|
|
body_panel = MessageHTML( self, size = ( width, -1 ) )
|
|
body_panel.SetPage( body )
|
|
|
|
internal = body_panel.GetInternalRepresentation()
|
|
|
|
body_panel.SetSize( ( -1, internal.GetHeight() ) )
|
|
|
|
self._hbox.Replace( self._body_panel, body_panel )
|
|
|
|
self._body_panel.Destroy()
|
|
|
|
self._body_panel = body_panel
|
|
|
|
|
|
self.Layout()
|
|
self.GetParent().FitInside()
|
|
|
|
|
|
|