/// /// Copyright © 2003-2008 JetBrains s.r.o. /// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder). /// using System; using System.Diagnostics; using System.Text; using System.Xml; using System.IO; using JetBrains.Omea.Base; using JetBrains.Omea.OpenAPI; using JetBrains.Omea.HTML; using JetBrains.Omea.ResourceTools; namespace JetBrains.Omea.Conversations { public class IMConversationsManager: IResourceTextProvider { #region Attributes private const string _Style = ""; private readonly IResourceStore _store; private readonly string _conversationResType; private readonly int _propAccountLink; private readonly int _propFromAccount; private readonly int _propToAccount; private readonly int _propDate; private readonly int _propStartDate; private readonly int _propFrom; private readonly int _propTo; private readonly int _propConversationList; private readonly int _propSubject; private readonly int _propMySelf; private TimeSpan _period; private bool _reverseMode; #endregion Attributes public IMConversationsManager( string resType, string resourceTypeDisplayName, string displayNameMask, TimeSpan period, int propAccountLink, int propFromAccount, int propToAccount, IPlugin ownerPlugin ) { _store = Core.ResourceStore; _propDate = _store.PropTypes.Register( "Date", PropDataType.Date ); _propStartDate = ResourceTypeHelper.UpdatePropTypeRegistration( "StartDate", PropDataType.Date, PropTypeFlags.Normal ); _store.PropTypes.RegisterDisplayName( _propStartDate, "Start Date" ); _propFrom = ResourceTypeHelper.UpdatePropTypeRegistration( "From", PropDataType.Link, PropTypeFlags.DirectedLink ); _propTo = ResourceTypeHelper.UpdatePropTypeRegistration( "To", PropDataType.Link, PropTypeFlags.DirectedLink ); _propConversationList = ResourceTypeHelper.UpdatePropTypeRegistration( "ConversationList", PropDataType.StringList, PropTypeFlags.Internal ); _propSubject = _store.PropTypes.Register( "Subject", PropDataType.String ); _propMySelf = _store.PropTypes.Register( "MySelf", PropDataType.Int, PropTypeFlags.Internal ); _conversationResType = resType; _store.ResourceTypes.Register( resType, resourceTypeDisplayName, displayNameMask, ResourceTypeFlags.Normal, ownerPlugin ); Core.FilterEngine.RegisterRuleApplicableResourceType( resType ); _store.RegisterLinkRestriction( resType, _propFrom, null, 1, 1 ); _store.RegisterLinkRestriction( resType, _propTo, null, 1, 1 ); _store.RegisterLinkRestriction( resType, propFromAccount, null, 1, 1 ); _store.RegisterLinkRestriction( resType, propToAccount, null, 1, 1 ); Core.MessageFormatter.RegisterPreviewTextProvider( resType, new ConversationPreviewBuilder( this ) ); _period = period; _reverseMode = false; _propAccountLink = propAccountLink; _propFromAccount = propFromAccount; _propToAccount = propToAccount; } public TimeSpan ConversationPeriod { get { return _period; } set { _period = value; } } public bool ReverseMode { get { return _reverseMode; } set { _reverseMode = value; } } #region Update Conversation /** * updates existing or creates the new conversation resource * texts should be updated in chronological order (ascending sorting by date) */ public IResource Update( string text, DateTime date, IResource fromAccount, IResource toAccount ) { IResource from = fromAccount.GetLinkProp( _propAccountLink ); if( from == null ) { throw new Exception( "'From' account is not linked with a contact" ); } IResource to = toAccount.GetLinkProp( _propAccountLink ); if( to == null ) { throw new Exception( "'To' account is not linked with a contact" ); } IResourceList lastConversations = _store.FindResourcesInRange( _conversationResType, _propDate, date - _period, date ); IResource convs; for( int i = lastConversations.Count - 1; i >= 0; --i ) { convs = lastConversations[ i ]; IResource convsFrom = convs.GetLinkProp( _propFrom ); IResource convsTo = convs.GetLinkProp( _propTo ); if( ( from == convsFrom && to == convsTo ) || ( from == convsTo && to == convsFrom ) ) { convs.BeginUpdate(); try { UpdateConversationDate( convs, date, from, to, from == convsTo ); AddFragment( convs, text, date, from == convsFrom ); string subject = convs.GetPropText( _propSubject ); if( subject.Length <= 10 && subject.Split( ' ' ).Length < 2 ) { subject = text.Trim().Replace( '\n', ' ' ).Replace( '\r', ' '); if( subject.Length > 64 ) { subject = subject.Remove( 61, subject.Length - 61 ); subject = subject + "..."; } if( subject.Length > 10 || subject.Split( ' ' ).Length > 1 ) { UpdateConversationSubject( convs, subject ); } } } finally { convs.EndUpdate(); } return convs; } } // if not found then create new conversation convs = _store.BeginNewResource( _conversationResType ); try { convs.AddLink( _propFrom, from ); convs.AddLink( _propTo, to ); convs.AddLink( _propFromAccount, fromAccount ); convs.AddLink( _propToAccount, toAccount ); convs.SetProp( _propStartDate, date ); UpdateConversationDate( convs, date, from, to, false ); AddFragment( convs, text, date, true ); string subject = text.Trim().Replace( '\n', ' ').Replace( '\r', ' '); if( subject.Length > 64 ) { subject = subject.Remove( 61, subject.Length - 61 ); subject = subject + "..."; } UpdateConversationSubject( convs, subject ); } finally { convs.EndUpdate(); } return convs; } #endregion Update Conversation #region Convert To HTML /** * creates html representation for a given conversation resource & * string-type property which is used to display accounts */ public string ToHtmlString( IResourceList convs, int propDisplayName ) { StringBuilder htmlBuilder = StringBuilderPool.Alloc(); try { ToHtmlHead( htmlBuilder ); for( int i = 0; i < convs.Count; i++ ) { ToHtmlBody( convs[ i ], propDisplayName, htmlBuilder ); if( i != convs.Count - 1 ) htmlBuilder.Append( "

" ); } ToHtmlEnd( htmlBuilder ); return htmlBuilder.ToString(); } finally { StringBuilderPool.Dispose( htmlBuilder ); } } public string ToHtmlString( IResource convs, int propDisplayName ) { StringBuilder htmlBuilder = StringBuilderPool.Alloc(); try { ToHtmlHead( htmlBuilder ); ToHtmlBody( convs, propDisplayName, htmlBuilder ); ToHtmlEnd( htmlBuilder ); Trace.WriteLine( htmlBuilder.ToString() ); return htmlBuilder.ToString(); } finally { StringBuilderPool.Dispose( htmlBuilder ); } } private static void ToHtmlHead( StringBuilder htmlBuilder ) { htmlBuilder.Append( "" ).Append( _Style ).Append( "\n" ); } private static void ToHtmlEnd( StringBuilder htmlBuilder ) { htmlBuilder.Append(""); } private void ToHtmlBody( IResource convs, int propDisplayName, StringBuilder htmlBuilder ) { string cachedFrom = string.Empty; string fromName, toName; ExtractNames( convs, propDisplayName, out fromName, out toName ); htmlBuilder.Append(""); DateTime lastMsgDate = convs.GetDateProp( _propDate ); XmlTextReader reader = new XmlTextReader( new StringReader( ToString( convs ) ) ); while( reader.Read() ) { if ( reader.NodeType == XmlNodeType.Element && reader.LocalName == "fragment" ) { if( reader.MoveToAttribute( "date" ) ) { DateTime newMsgDate = new DateTime( Convert.ToInt64( reader.Value ) ); if( newMsgDate.Day != lastMsgDate.Day ) { ToHtmlDate( newMsgDate, htmlBuilder ); } lastMsgDate = newMsgDate; } bool incoming = reader.MoveToAttribute( "incoming" ) && reader.Value.ToLower() == "true"; ToHtmlLeadingCols( incoming, fromName, toName, lastMsgDate, ref cachedFrom, htmlBuilder ); if( reader.MoveToAttribute( "body" ) ) { ToHtmlPhrase( reader.ReadInnerXml(), htmlBuilder ); } htmlBuilder.Append(""); reader.MoveToElement(); } } htmlBuilder.Append("
"); } private static void ToHtmlDate( DateTime date, StringBuilder htmlBuilder ) { htmlBuilder.Append("
"); htmlBuilder.Append( date.ToLongDateString()); htmlBuilder.Append("
 
"); } private static void ToHtmlLeadingCols( bool incoming, string fromName, string toName, DateTime date, ref string cachedName, StringBuilder htmlBuilder ) { string name = ( incoming ) ? fromName : toName; if( name == cachedName ) name = " "; else cachedName = name; htmlBuilder.Append("" : "outgoing\">" ); htmlBuilder.Append(""); htmlBuilder.Append( name ); htmlBuilder.Append(""); htmlBuilder.Append(""); htmlBuilder.Append( date.ToLongTimeString() ); htmlBuilder.Append("  "); } private static void ToHtmlPhrase( string body, StringBuilder htmlBuilder ) { htmlBuilder.Append(""); // convert all urls to active weblinks htmlBuilder.Append( HtmlTools.ConvertLinks( body.Replace( "\n", "
" ) ) ); htmlBuilder.Append(""); } private void ExtractNames( IResource convs, int propNameId, out string fromName, out string toName ) { IResource fromAcct = convs.GetLinkProp( _propFromAccount ); fromName = fromAcct.GetPropText( propNameId ); if ( fromName.Length == 0 ) { fromName = fromAcct.DisplayName; } IResource toAcct = convs.GetLinkProp( _propToAccount ); toName = toAcct.GetPropText( propNameId ); if ( toName.Length == 0 ) { toName = toAcct.DisplayName; } } #endregion Convert To HTML public bool ProcessResourceText( IResource convs, IResourceTextConsumer consumer ) { // index conversation as sequence of fragments, each one is a message try { XmlTextReader reader = new XmlTextReader( new StringReader( ToString( convs ) ) ); while( reader.Read() ) { if ( reader.NodeType == XmlNodeType.Element ) { if( reader.MoveToAttribute( "body" ) ) { consumer.AddDocumentFragment( convs.Id, reader.Value ); } reader.MoveToElement(); } } } catch( XmlException ) { // nothing to do with this :( } return true; } public string ToString( int convsID ) { return ToString( _store.LoadResource( convsID ) ); } public string ToString( IResource convs ) { StringBuilder bodyBuilder = StringBuilderPool.Alloc(); try { bodyBuilder.Append( "" ); IStringList convsList = convs.GetStringListProp( _propConversationList ); if( !_reverseMode ) { for( int i = 0; i < convsList.Count; ++i ) { bodyBuilder.Append( convsList[ i ] ); } } else { for( int i = convsList.Count - 1; i >= 0; --i ) { bodyBuilder.Append( convsList[ i ] ); } } bodyBuilder.Append( "" ); convsList.Dispose(); return bodyBuilder.ToString(); } finally { StringBuilderPool.Dispose( bodyBuilder ); } } private void UpdateConversationDate( IResource convs, DateTime date, IResource from, IResource to, bool updateLastCorrespondDate ) { convs.SetProp( _propDate, date ); if( updateLastCorrespondDate ) { IResource correspondent = ( from.GetIntProp( _propMySelf ) == 0 ) ? from : to; if( correspondent.GetDateProp( Core.ContactManager.Props.LastCorrespondenceDate ) < date ) { correspondent.SetProp( Core.ContactManager.Props.LastCorrespondenceDate, date ); } } } private void UpdateConversationSubject( IResource convs, string subject ) { try { convs.SetProp( _propSubject, subject ); } catch( ArgumentException ) { // ignore garbage } } private void AddFragment( IResource convs, string text, DateTime date, bool incoming ) { StringBuilder bodyBuilder = StringBuilderPool.Alloc(); try { XmlTextWriter xmlWriter = new XmlTextWriter( new StringWriter( bodyBuilder ) ); try { xmlWriter.WriteStartElement( "fragment" ); xmlWriter.WriteAttributeString( "body", text ); xmlWriter.WriteAttributeString( "date", date.Ticks.ToString() ); xmlWriter.WriteAttributeString( "incoming", incoming.ToString() ); xmlWriter.WriteEndElement(); xmlWriter.Flush(); convs.GetStringListProp( _propConversationList ).Add( bodyBuilder.ToString() ); } catch( ArgumentException ) { // ignore garbage } finally { xmlWriter.Close(); } } finally { StringBuilderPool.Dispose( bodyBuilder ); } } private class ConversationPreviewBuilder: IPreviewTextProvider { private readonly IMConversationsManager _manager; public ConversationPreviewBuilder( IMConversationsManager manager ) { _manager = manager; } public string GetPreviewText( IResource res, int lines ) { StringBuilder builder = StringBuilderPool.Alloc(); try { builder.Append( "" ); IStringList convsList = res.GetStringListProp( _manager._propConversationList ); if( !_manager._reverseMode ) { for( int i = 0; i < convsList.Count && lines-- > 0; ++i ) { builder.Append( convsList[ i ] ); } } else { for( int i = convsList.Count - 1; i >= 0 && lines-- > 0; --i ) { builder.Append( convsList[ i ] ); } } convsList.Dispose(); builder.Append( "" ); XmlTextReader reader = new XmlTextReader( new StringReader( builder.ToString() ) ); builder.Length = 0; while( reader.Read() ) { if ( reader.NodeType == XmlNodeType.Element ) { if( reader.MoveToAttribute( "body" ) ) { builder.AppendFormat( "{0}\r\n", reader.Value ); } reader.MoveToElement(); } } return builder.ToString(); } finally { StringBuilderPool.Dispose( builder ); } } } } }