Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Continued work on MUC

  • Loading branch information...
commit 9bc1b169b67100e95c71342f32e17c562789fe20 1 parent d422e28
@robbiehanson authored
Showing with 1,969 additions and 190 deletions.
  1. +2 −0  Extensions/CoreDataStorage/XMPPCoreDataStorage.m
  2. +2 −0  Extensions/CoreDataStorage/XMPPCoreDataStorageProtected.h
  3. +3 −0  Extensions/Roster/CoreDataStorage/XMPPRosterCoreDataStorage.m
  4. +8 −0 Extensions/XEP-0045/CoreDataStorage/XMPPRoom.xcdatamodeld/.xccurrentversion
  5. +35 −0 Extensions/XEP-0045/CoreDataStorage/XMPPRoom.xcdatamodeld/XMPPRoom.xcdatamodel/contents
  6. +84 −0 Extensions/XEP-0045/CoreDataStorage/XMPPRoomCoreDataStorage.h
  7. +475 −0 Extensions/XEP-0045/CoreDataStorage/XMPPRoomCoreDataStorage.m
  8. +38 −0 Extensions/XEP-0045/CoreDataStorage/XMPPRoomMessageCoreDataStorageObject.h
  9. +148 −0 Extensions/XEP-0045/CoreDataStorage/XMPPRoomMessageCoreDataStorageObject.m
  10. +37 −0 Extensions/XEP-0045/CoreDataStorage/XMPPRoomOccupantCoreDataStorageObject.h
  11. +233 −0 Extensions/XEP-0045/CoreDataStorage/XMPPRoomOccupantCoreDataStorageObject.m
  12. +167 −38 Extensions/XEP-0045/MemoryStorage/XMPPRoomMemoryStorage.m
  13. +18 −2 Extensions/XEP-0045/MemoryStorage/XMPPRoomMessageMemoryStorage.h
  14. +56 −18 Extensions/XEP-0045/MemoryStorage/XMPPRoomMessageMemoryStorage.m
  15. +16 −6 Extensions/XEP-0045/MemoryStorage/XMPPRoomOccupantMemoryStorage.h
  16. +5 −0 Extensions/XEP-0045/MemoryStorage/XMPPRoomOccupantMemoryStorage.m
  17. +12 −0 Extensions/XEP-0045/XMPPMUC.h
  18. +1 −8 Extensions/XEP-0045/XMPPMUC.m
  19. +10 −5 Extensions/XEP-0045/XMPPRoom.h
  20. +3 −3 Extensions/XEP-0045/XMPPRoom.m
  21. +48 −8 Extensions/XEP-0045/XMPPRoomMessage.h
  22. +45 −6 Extensions/XEP-0045/XMPPRoomOccupant.h
  23. +1 −1  Utilities/XMPPIDTracker.h
  24. +286 −22 Xcode/DesktopXMPP/English.lproj/MucWindow.xib
  25. +13 −2 Xcode/DesktopXMPP/MucController.h
  26. +59 −28 Xcode/DesktopXMPP/MucController.m
  27. +1 −0  Xcode/DesktopXMPP/XMPPFramework.h
  28. +48 −15 Xcode/DesktopXMPP/XMPPStream.xcodeproj/project.pbxproj
  29. +3 −7 Xcode/iPhoneXMPP/Classes/RootViewController.m
  30. +0 −1  Xcode/iPhoneXMPP/Classes/SettingsViewController.m
  31. +6 −1 Xcode/iPhoneXMPP/Classes/XMPPFramework.h
  32. +15 −16 Xcode/iPhoneXMPP/Classes/iPhoneXMPPAppDelegate.m
  33. +91 −3 Xcode/iPhoneXMPP/iPhoneXMPP.xcodeproj/project.pbxproj
View
2  Extensions/CoreDataStorage/XMPPCoreDataStorage.m
@@ -225,6 +225,7 @@ - (id)initWithDatabaseFilename:(NSString *)aDatabaseFileName
}
[self commonInit];
+ NSAssert(storageQueue != NULL, @"Subclass forgot to invoke [super commonInit]");
}
return self;
}
@@ -234,6 +235,7 @@ - (id)initWithInMemoryStore
if ((self = [super init]))
{
[self commonInit];
+ NSAssert(storageQueue != NULL, @"Subclass forgot to invoke [super commonInit]");
}
return self;
}
View
2  Extensions/CoreDataStorage/XMPPCoreDataStorageProtected.h
@@ -14,6 +14,8 @@
/**
* If your subclass needs to do anything for init, it can do so easily by overriding this method.
* All public init methods will invoke this method at the end of their implementation.
+ *
+ * Important: If overriden you must invoke [super commonInit] at some point.
**/
- (void)commonInit;
View
3  Extensions/Roster/CoreDataStorage/XMPPRosterCoreDataStorage.m
@@ -44,6 +44,9 @@ + (XMPPRosterCoreDataStorage *)sharedInstance
- (void)commonInit
{
+ XMPPLogTrace();
+ [super commonInit];
+
// This method is invoked by all public init methods of the superclass
rosterPopulationSet = [[NSMutableSet alloc] init];
View
8 Extensions/XEP-0045/CoreDataStorage/XMPPRoom.xcdatamodeld/.xccurrentversion
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>_XCCurrentVersionName</key>
+ <string>XMPPRoom.xcdatamodel</string>
+</dict>
+</plist>
View
35 Extensions/XEP-0045/CoreDataStorage/XMPPRoom.xcdatamodeld/XMPPRoom.xcdatamodel/contents
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<model name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="875" systemVersion="11C74" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
+ <entity name="XMPPRoomMessageCoreDataStorageObject" representedClassName="XMPPRoomMessageCoreDataStorageObject" syncable="YES">
+ <attribute name="body" optional="YES" attributeType="String" indexed="YES" syncable="YES"/>
+ <attribute name="fromMe" attributeType="Boolean" defaultValueString="NO" syncable="YES"/>
+ <attribute name="jid" optional="YES" transient="YES" syncable="YES"/>
+ <attribute name="jidStr" optional="YES" attributeType="String" syncable="YES"/>
+ <attribute name="localTimestamp" attributeType="Date" indexed="YES" syncable="YES"/>
+ <attribute name="message" optional="YES" transient="YES" syncable="YES"/>
+ <attribute name="messageStr" optional="YES" attributeType="String" syncable="YES"/>
+ <attribute name="nickname" optional="YES" attributeType="String" syncable="YES"/>
+ <attribute name="remoteTimestamp" optional="YES" attributeType="Date" indexed="YES" syncable="YES"/>
+ <attribute name="roomJID" optional="YES" transient="YES" syncable="YES"/>
+ <attribute name="roomJIDStr" attributeType="String" indexed="YES" syncable="YES"/>
+ <attribute name="streamBareJidStr" attributeType="String" indexed="YES" syncable="YES"/>
+ </entity>
+ <entity name="XMPPRoomOccupantCoreDataStorageObject" representedClassName="XMPPRoomOccupantCoreDataStorageObject" syncable="YES">
+ <attribute name="affiliation" optional="YES" attributeType="String" syncable="YES"/>
+ <attribute name="jid" optional="YES" transient="YES" syncable="YES"/>
+ <attribute name="jidStr" attributeType="String" indexed="YES" syncable="YES"/>
+ <attribute name="nickname" optional="YES" attributeType="String" indexed="YES" syncable="YES"/>
+ <attribute name="presence" optional="YES" transient="YES" syncable="YES"/>
+ <attribute name="presenceStr" optional="YES" attributeType="String" syncable="YES"/>
+ <attribute name="realJID" optional="YES" transient="YES" syncable="YES"/>
+ <attribute name="realJIDStr" optional="YES" attributeType="String" syncable="YES"/>
+ <attribute name="role" optional="YES" attributeType="String" syncable="YES"/>
+ <attribute name="roomJID" optional="YES" transient="YES" syncable="YES"/>
+ <attribute name="roomJIDStr" attributeType="String" indexed="YES" syncable="YES"/>
+ <attribute name="streamBareJidStr" attributeType="String" indexed="YES" syncable="YES"/>
+ </entity>
+ <elements>
+ <element name="XMPPRoomMessageCoreDataStorageObject" positionX="160" positionY="192" width="128" height="225"/>
+ <element name="XMPPRoomOccupantCoreDataStorageObject" positionX="160" positionY="192" width="128" height="225"/>
+ </elements>
+</model>
View
84 Extensions/XEP-0045/CoreDataStorage/XMPPRoomCoreDataStorage.h
@@ -0,0 +1,84 @@
+#import <Foundation/Foundation.h>
+#import <CoreData/CoreData.h>
+
+#import "XMPP.h"
+#import "XMPPRoom.h"
+#import "XMPPRoomMessageCoreDataStorageObject.h"
+#import "XMPPRoomOccupantCoreDataStorageObject.h"
+#import "XMPPCoreDataStorage.h"
+
+
+@interface XMPPRoomCoreDataStorage : XMPPCoreDataStorage <XMPPRoomStorage>
+
+/**
+ * Convenience method to get an instance with the default database name.
+ *
+ * IMPORTANT:
+ * You are NOT required to use the sharedInstance.
+ *
+ * If your application makes extensive use of MUC, and you use a sharedInstance of this class,
+ * then all of your MUC rooms share the same database store. You might get better performance if you create
+ * multiple instances of this class instead (using different database filenames), as this way you can have
+ * concurrent writes to multiple databases.
+**/
++ (XMPPRoomCoreDataStorage *)sharedInstance;
+
+
+/* Inherited from XMPPCoreDataStorage
+ * Please see the XMPPCoreDataStorage header file for extensive documentation.
+
+- (id)initWithDatabaseFilename:(NSString *)databaseFileName;
+- (id)initWithInMemoryStore;
+
+@property (readonly) NSString *databaseFileName;
+
+@property (readwrite) NSUInteger saveThreshold;
+
+@property (readonly) NSManagedObjectModel *managedObjectModel;
+@property (readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;
+
+*/
+
+/**
+ * It is likely you don't want the message history to persist forever.
+ * Doing so would allow the database to grow infinitely large over time.
+ *
+ * The maxMessageAge property provides a way to specify how old a message can get
+ * before it should get deleted from the database.
+ *
+ * The deleteInterval specifies how often to sweep for old messages.
+ * Since deleting is an expensive operation (disk io) it is done on a fixed interval.
+ *
+ * You can optionally disable the maxMessageAge by setting it to zero (or a negative value).
+ * If you disable the maxMessageAge then old messages are not deleted.
+ *
+ * You can optionally disable the deleteInterval by setting it to zero (or a negative value).
+ *
+ * The default maxAge is 7 days.
+ * The default deleteInterval is 5 minutes.
+**/
+@property (assign, readwrite) NSTimeInterval maxMessageAge;
+@property (assign, readwrite) NSTimeInterval deleteInterval;
+
+/**
+ * You may optionally prevent old message deletion for particular rooms.
+**/
+- (void)pauseOldMessageDeletionForRoom:(XMPPJID *)roomJID;
+- (void)resumeOldMessageDeletionForRoom:(XMPPJID *)roomJID;
+
+/**
+ * Returns the timestamp of the most recent message stored in the database for the given room.
+ * This may be used when requesting the message history from the server,
+ * to prevent redownloading messages you already have.
+ *
+ * Keep in mind that the
+**/
+- (NSDate *)mostRecentMessageTimestampForRoom:(XMPPJID *)roomJID;
+- (NSDate *)mostRecentMessageTimestampForRoom:(XMPPJID *)roomJID stream:(XMPPStream *)stream;
+
+/**
+ * Returns the occupant for the given jid.
+**/
+- (XMPPRoomOccupantCoreDataStorageObject *)occupantForJID:(XMPPJID *)jid inContext:(NSManagedObjectContext *)moc;
+
+@end
View
475 Extensions/XEP-0045/CoreDataStorage/XMPPRoomCoreDataStorage.m
@@ -0,0 +1,475 @@
+#import "XMPPRoomCoreDataStorage.h"
+#import "XMPPCoreDataStorageProtected.h"
+#import "XMPPElement+Delay.h"
+#import "XMPPLogging.h"
+
+#if ! __has_feature(objc_arc)
+#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
+#endif
+
+// Log levels: off, error, warn, info, verbose
+#if DEBUG
+ static const int xmppLogLevel = XMPP_LOG_LEVEL_VERBOSE | XMPP_LOG_FLAG_TRACE;
+#else
+ static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
+#endif
+
+#define AssertPrivateQueue() \
+ NSAssert(dispatch_get_current_queue() == storageQueue, @"Private method: MUST run on storageQueue");
+
+@interface XMPPRoomCoreDataStorage ()
+{
+ /* Inherited from XMPPCoreDataStorage
+
+ NSString *databaseFileName;
+ NSUInteger saveThreshold;
+
+ dispatch_queue_t storageQueue;
+
+ */
+
+ NSTimeInterval maxMessageAge;
+ NSTimeInterval deleteInterval;
+
+ NSMutableSet *pausedMessageDeletion;
+}
+
+- (void)_clearAllOccupantsFromRoom:(XMPPJID *)roomJID;
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+@implementation XMPPRoomCoreDataStorage
+
+static XMPPRoomCoreDataStorage *sharedInstance;
+
++ (XMPPRoomCoreDataStorage *)sharedInstance
+{
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+
+ sharedInstance = [[XMPPRoomCoreDataStorage alloc] initWithDatabaseFilename:nil];
+ });
+
+ return sharedInstance;
+}
+
+- (void)commonInit
+{
+ XMPPLogTrace();
+ [super commonInit];
+
+ // This method is invoked by all public init methods of the superclass
+
+ maxMessageAge = (60 * 60 * 24 * 7); // 7 days
+ deleteInterval = (60 * 5); // 5 days
+
+ pausedMessageDeletion = [[NSMutableSet alloc] init];
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Configuration
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (NSTimeInterval)maxMessageAge
+{
+ __block NSTimeInterval result = 0;
+
+ dispatch_block_t block = ^{
+ result = maxMessageAge;
+ };
+
+ if (dispatch_get_current_queue() == storageQueue)
+ block();
+ else
+ dispatch_sync(storageQueue, block);
+
+ return result;
+}
+
+- (void)setMaxMessageAge:(NSTimeInterval)age
+{
+ dispatch_block_t block = ^{
+ maxMessageAge = age;
+ };
+
+ if (dispatch_get_current_queue() == storageQueue)
+ block();
+ else
+ dispatch_async(storageQueue, block);
+}
+
+- (NSTimeInterval)deleteInterval
+{
+ __block NSTimeInterval result = 0;
+
+ dispatch_block_t block = ^{
+ result = deleteInterval;
+ };
+
+ if (dispatch_get_current_queue() == storageQueue)
+ block();
+ else
+ dispatch_sync(storageQueue, block);
+
+ return result;
+}
+
+- (void)setDeleteInterval:(NSTimeInterval)interval
+{
+ dispatch_block_t block = ^{
+ deleteInterval = interval;
+ };
+
+ if (dispatch_get_current_queue() == storageQueue)
+ block();
+ else
+ dispatch_async(storageQueue, block);
+}
+
+- (void)pauseOldMessageDeletionForRoom:(XMPPJID *)roomJID
+{
+ dispatch_block_t block = ^{ @autoreleasepool {
+
+ [pausedMessageDeletion addObject:[roomJID bareJID]];
+ }};
+
+ if (dispatch_get_current_queue() == storageQueue)
+ block();
+ else
+ dispatch_async(storageQueue, block);
+}
+
+- (void)resumeOldMessageDeletionForRoom:(XMPPJID *)roomJID
+{
+ dispatch_block_t block = ^{ @autoreleasepool {
+
+ [pausedMessageDeletion removeObject:[roomJID bareJID]];
+ }};
+
+ if (dispatch_get_current_queue() == storageQueue)
+ block();
+ else
+ dispatch_async(storageQueue, block);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Overrides
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (void)didCreateManagedObjectContext
+{
+ XMPPLogTrace();
+
+ [self _clearAllOccupantsFromRoom:nil];
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Private API
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (NSEntityDescription *)messageEntity:(NSManagedObjectContext *)moc
+{
+ return [NSEntityDescription entityForName:@"XMPPRoomMessageCoreDataStorageObject" inManagedObjectContext:moc];
+}
+
+- (NSEntityDescription *)occupantEntity:(NSManagedObjectContext *)moc
+{
+ return [NSEntityDescription entityForName:@"XMPPRoomOccupantCoreDataStorageObject" inManagedObjectContext:moc];
+}
+
+- (void)_clearAllOccupantsFromRoom:(XMPPJID *)roomJID
+{
+ XMPPLogTrace();
+ AssertPrivateQueue();
+
+ NSManagedObjectContext *moc = [self managedObjectContext];
+ NSEntityDescription *entity = [self occupantEntity:moc];
+
+ NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
+ [fetchRequest setEntity:entity];
+ [fetchRequest setFetchBatchSize:saveThreshold];
+
+ if (roomJID)
+ {
+ NSPredicate *predicate;
+ predicate = [NSPredicate predicateWithFormat:@"roomJIDStr == %@", [roomJID bare]];
+
+ [fetchRequest setPredicate:predicate];
+ }
+
+ NSArray *allOccupants = [moc executeFetchRequest:fetchRequest error:nil];
+
+ NSUInteger unsavedCount = [self numberOfUnsavedChanges];
+
+ for (XMPPRoomOccupantCoreDataStorageObject *occupant in allOccupants)
+ {
+ [moc deleteObject:occupant];
+
+ if (++unsavedCount >= saveThreshold)
+ {
+ [self save];
+ unsavedCount = 0;
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Public API
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (XMPPRoomOccupantCoreDataStorageObject *)occupantForJID:(XMPPJID *)jid inContext:(NSManagedObjectContext *)moc
+{
+ if (jid == nil) return nil;
+
+ NSEntityDescription *entity = [self occupantEntity:moc];
+
+ NSPredicate *predicate = [NSPredicate predicateWithFormat:@"jidStr == %@", jid];
+
+ NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
+ [fetchRequest setEntity:entity];
+ [fetchRequest setPredicate:predicate];
+ [fetchRequest setFetchBatchSize:1];
+
+ return [[moc executeFetchRequest:fetchRequest error:nil] lastObject];
+}
+
+- (NSDate *)mostRecentMessageTimestampForRoom:(XMPPJID *)roomJID
+{
+ return [self mostRecentMessageTimestampForRoom:roomJID stream:nil];
+}
+
+- (NSDate *)mostRecentMessageTimestampForRoom:(XMPPJID *)roomJID stream:(XMPPStream *)stream
+{
+ // Todo...
+ return nil;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark XMPPRoomStorage Protocol
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (void)handlePresence:(XMPPPresence *)presence room:(XMPPRoom *)room
+{
+ XMPPLogTrace();
+
+ XMPPJID *roomJID = room.roomJID;
+ XMPPJID *presenceJID = [presence from];
+ XMPPStream *xmppStream = room.xmppStream;
+
+ NSString *role = nil;
+ NSString *affiliation = nil;
+ XMPPJID *realJID = nil;
+
+ NSXMLElement *x = [presence elementForName:@"x" xmlns:@"http://jabber.org/protocol/muc#user"];
+ NSXMLElement *item = [x elementForName:@"item"];
+ if (item)
+ {
+ role = [[item attributeStringValueForName:@"role"] lowercaseString];
+ affiliation = [[item attributeStringValueForName:@"affiliation"] lowercaseString];
+
+ NSString *realJIDStr = [item attributeStringValueForName:@"jid"];
+ if (realJIDStr)
+ {
+ realJID = [XMPPJID jidWithString:realJIDStr];
+ }
+ }
+
+ [self scheduleBlock:^{
+
+ NSManagedObjectContext *moc = [self managedObjectContext];
+ NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare];
+
+ // Is occupant already in database?
+
+ NSEntityDescription *entity = [self occupantEntity:moc];
+
+ NSString *predicateFormat = @"jidStr == %@ AND streamBareJidStr == %@";
+ NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateFormat, presenceJID, streamBareJidStr];
+
+ NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
+ [fetchRequest setEntity:entity];
+ [fetchRequest setPredicate:predicate];
+ [fetchRequest setFetchLimit:1];
+
+ NSError *error = nil;
+ XMPPRoomOccupantCoreDataStorageObject *occupant;
+
+ occupant = [[moc executeFetchRequest:fetchRequest error:&error] lastObject];
+
+ if (error)
+ {
+ XMPPLogWarn(@"%@: %@ - fetch error: %@", THIS_FILE, THIS_METHOD, error);
+ return;
+ }
+
+ // Is occupant available or unavailable?
+
+ if ([[presence type] isEqualToString:@"unavailable"])
+ {
+ // Remove occupant record from database
+
+ if (occupant)
+ {
+ [moc deleteObject:occupant];
+ }
+ }
+ else
+ {
+ // Updated existing occupant, or add new occupant to database.
+
+ if (occupant == nil)
+ {
+ occupant = [NSEntityDescription insertNewObjectForEntityForName:@"XMPPRoomOccupantCoreDataStorageObject"
+ inManagedObjectContext:moc];
+ }
+
+ occupant.presence = presence;
+ occupant.roomJID = roomJID;
+ occupant.jid = presenceJID;
+ occupant.nickname = [presenceJID resource];
+ occupant.role = role;
+ occupant.affiliation = affiliation;
+ occupant.realJID = realJID;
+ occupant.streamBareJidStr = streamBareJidStr;
+ }
+ }];
+}
+
+- (void)handleOutgoingMessage:(XMPPMessage *)message room:(XMPPRoom *)room
+{
+ XMPPLogTrace();
+
+ XMPPJID *roomJID = room.roomJID;
+ XMPPJID *messageJID = room.myRoomJID;
+ XMPPStream *xmppStream = room.xmppStream;
+
+ NSDate *localTimestamp = [[NSDate alloc] init];
+
+ NSString *messageBody = [[message elementForName:@"body"] stringValue];
+
+ [self scheduleBlock:^{
+
+ NSManagedObjectContext *moc = [self managedObjectContext];
+ NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare];
+
+ XMPPRoomMessageCoreDataStorageObject *roomMessage;
+ roomMessage = [NSEntityDescription insertNewObjectForEntityForName:@"XMPPRoomMessageCoreDataStorageObject"
+ inManagedObjectContext:moc];
+
+ roomMessage.message = message;
+ roomMessage.roomJID = roomJID;
+ roomMessage.jid = messageJID;
+ roomMessage.nickname = [messageJID resource];
+ roomMessage.body = messageBody;
+ roomMessage.localTimestamp = localTimestamp;
+ roomMessage.isFromMe = YES;
+ roomMessage.streamBareJidStr = streamBareJidStr;
+ }];
+}
+
+- (void)handleIncomingMessage:(XMPPMessage *)message room:(XMPPRoom *)room
+{
+ XMPPLogTrace();
+
+ XMPPJID *roomJID = room.roomJID;
+ XMPPJID *messageJID = [message from];
+
+ if ([roomJID isEqualToJID:messageJID])
+ {
+ // Ignore - we already stored message in handleOutgoingMessage:room:
+ return;
+ }
+
+ XMPPStream *xmppStream = room.xmppStream;
+
+ NSDate *localTimestamp = [[NSDate alloc] init];
+ NSDate *remoteTimestamp = [message delayedDeliveryDate];
+
+ NSString *messageBody = [[message elementForName:@"body"] stringValue];
+
+ [self scheduleBlock:^{
+
+ NSManagedObjectContext *moc = [self managedObjectContext];
+ NSString *streamBareJidStr = [[self myJIDForXMPPStream:xmppStream] bare];
+
+ if (remoteTimestamp)
+ {
+ // Does this message already exist in the database?
+ // How can we tell if two XMPPRoomMessages are the same?
+ //
+ // 1. Same streamBareJidStr
+ // 2. Same jid
+ // 3. Same text
+ // 4. Approximately the same timestamps
+ //
+ // This is actually a rather difficult question.
+ // What if the same user sends the exact same message multiple times?
+ //
+ // If we first received the message while already in the room, it won't contain a remoteTimestamp.
+ // Returning to the room later and downloading the discussion history will return the same message,
+ // this time with a remote timestamp.
+ //
+ // So if the message doesn't have a remoteTimestamp,
+ // but it's localTimestamp is approximately the same as the remoteTimestamp,
+ // then this is enough evidence to consider the messages the same.
+
+ NSEntityDescription *entity = [self messageEntity:moc];
+
+ // Note: Predicate order matters. Most unique key should be first, least unique should be last.
+
+ NSDate *minLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval:-60];
+ NSDate *maxLocalTimestamp = [remoteTimestamp dateByAddingTimeInterval: 60];
+
+ NSString *predicateFormat = @" body == %@ "
+ @"AND jidStr == %@ "
+ @"AND streamBareJidStr == %@ "
+ @"AND "
+ @"("
+ @" (remoteTimestamp == %@) "
+ @" OR (remoteTimestamp == NIL && localTimestamp BETWEEN {%@, %@})"
+ @")";
+ NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateFormat,
+ messageBody, messageJID, streamBareJidStr,
+ remoteTimestamp, minLocalTimestamp, maxLocalTimestamp];
+
+ NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
+ [fetchRequest setEntity:entity];
+ [fetchRequest setPredicate:predicate];
+ [fetchRequest setFetchLimit:1];
+
+ NSError *error = nil;
+ NSArray *results = [moc executeFetchRequest:fetchRequest error:&error];
+
+ if ([results count] > 0)
+ {
+ XMPPLogVerbose(@"%@: %@ - Duplicate message", THIS_FILE, THIS_METHOD);
+ return;
+ }
+ else if (error)
+ {
+ XMPPLogError(@"%@: %@ - Fetch error: %@", THIS_FILE, THIS_METHOD, error);
+ return;
+ }
+ }
+
+ XMPPRoomMessageCoreDataStorageObject *roomMessage;
+ roomMessage = [NSEntityDescription insertNewObjectForEntityForName:@"XMPPRoomMessageCoreDataStorageObject"
+ inManagedObjectContext:moc];
+
+ roomMessage.message = message;
+ roomMessage.roomJID = roomJID;
+ roomMessage.jid = messageJID;
+ roomMessage.nickname = [messageJID resource];
+ roomMessage.body = messageBody;
+ roomMessage.localTimestamp = localTimestamp;
+ roomMessage.remoteTimestamp = remoteTimestamp;
+ roomMessage.isFromMe = NO;
+ roomMessage.streamBareJidStr = streamBareJidStr;
+ }];
+}
+
+@end
View
38 Extensions/XEP-0045/CoreDataStorage/XMPPRoomMessageCoreDataStorageObject.h
@@ -0,0 +1,38 @@
+#import <Foundation/Foundation.h>
+#import <CoreData/CoreData.h>
+
+#import "XMPP.h"
+#import "XMPPRoom.h"
+
+
+@interface XMPPRoomMessageCoreDataStorageObject : NSManagedObject <XMPPRoomMessage>
+
+/**
+ * The properties below are documented in the XMPPRoomMessage protocol.
+**/
+
+@property (nonatomic, retain) XMPPMessage * message; // Transient (proper type, not on disk)
+@property (nonatomic, retain) NSString * messageStr; // Shadow (binary data, written to disk)
+
+@property (nonatomic, strong) XMPPJID * roomJID; // Transient (proper type, not on disk)
+@property (nonatomic, strong) NSString * roomJIDStr; // Shadow (binary data, written to disk)
+
+@property (nonatomic, retain) XMPPJID * jid; // Transient (proper type, not on disk)
+@property (nonatomic, retain) NSString * jidStr; // Shadow (binary data, written to disk)
+
+@property (nonatomic, retain) NSString * nickname;
+@property (nonatomic, retain) NSString * body;
+
+@property (nonatomic, retain) NSDate * localTimestamp;
+@property (nonatomic, strong) NSDate * remoteTimestamp;
+
+@property (nonatomic, assign) BOOL isFromMe;
+@property (nonatomic, strong) NSNumber * fromMe;
+
+/**
+ * If a single instance of XMPPRoomCoreDataStorage is shared between multiple xmppStream's,
+ * this may be needed to distinguish between the streams.
+**/
+@property (nonatomic, strong) NSString *streamBareJidStr;
+
+@end
View
148 Extensions/XEP-0045/CoreDataStorage/XMPPRoomMessageCoreDataStorageObject.m
@@ -0,0 +1,148 @@
+#import "XMPPRoomMessageCoreDataStorageObject.h"
+
+#if ! __has_feature(objc_arc)
+#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
+#endif
+
+
+@interface XMPPRoomMessageCoreDataStorageObject ()
+
+@property(nonatomic,strong) XMPPJID * primitiveRoomJID;
+@property(nonatomic,strong) NSString * primitiveRoomJIDStr;
+
+@property(nonatomic,strong) XMPPJID * primitiveJid;
+@property(nonatomic,strong) NSString * primitiveJidStr;
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+@implementation XMPPRoomMessageCoreDataStorageObject
+
+@dynamic message;
+@dynamic messageStr;
+
+@dynamic roomJID, primitiveRoomJID;
+@dynamic roomJIDStr, primitiveRoomJIDStr;
+
+@dynamic jid, primitiveJid;
+@dynamic jidStr, primitiveJidStr;
+
+@dynamic nickname;
+@dynamic body;
+@dynamic localTimestamp;
+@dynamic remoteTimestamp;
+@dynamic isFromMe;
+@dynamic fromMe;
+
+@dynamic streamBareJidStr;
+
+#pragma mark Transient roomJID
+
+- (XMPPJID *)roomJID
+{
+ // Create and cache on demand
+
+ [self willAccessValueForKey:@"roomJID"];
+ XMPPJID *tmp = self.primitiveRoomJID;
+ [self didAccessValueForKey:@"roomJID"];
+
+ if (tmp == nil)
+ {
+ NSString *roomJIDStr = self.roomJIDStr;
+ if (roomJIDStr)
+ {
+ tmp = [XMPPJID jidWithString:roomJIDStr];
+ self.primitiveRoomJID = tmp;
+ }
+ }
+
+ return tmp;
+}
+
+- (void)setRoomJID:(XMPPJID *)roomJID
+{
+ [self willChangeValueForKey:@"roomJID"];
+ [self willChangeValueForKey:@"roomJIDStr"];
+
+ self.primitiveRoomJID = roomJID;
+ self.primitiveRoomJIDStr = [roomJID full];
+
+ [self didChangeValueForKey:@"roomJID"];
+ [self didChangeValueForKey:@"roomJIDStr"];
+}
+
+- (void)setRoomJIDStr:(NSString *)roomJIDStr
+{
+ [self willChangeValueForKey:@"roomJID"];
+ [self willChangeValueForKey:@"roomJIDStr"];
+
+ self.primitiveRoomJID = [XMPPJID jidWithString:roomJIDStr];
+ self.primitiveRoomJIDStr = roomJIDStr;
+
+ [self didChangeValueForKey:@"roomJID"];
+ [self didChangeValueForKey:@"roomJIDStr"];
+}
+
+#pragma mark Transient jid
+
+- (XMPPJID *)jid
+{
+ // Create and cache on demand
+
+ [self willAccessValueForKey:@"jid"];
+ XMPPJID *tmp = self.primitiveJid;
+ [self didAccessValueForKey:@"jid"];
+
+ if (tmp == nil)
+ {
+ NSString *jidStr = self.jidStr;
+ if (jidStr)
+ {
+ tmp = [XMPPJID jidWithString:jidStr];
+ self.primitiveJid = tmp;
+ }
+ }
+
+ return tmp;
+}
+
+- (void)setJid:(XMPPJID *)jid
+{
+ [self willChangeValueForKey:@"jid"];
+ [self willChangeValueForKey:@"jidStr"];
+
+ self.primitiveJid = jid;
+ self.primitiveJidStr = [jid full];
+
+ [self didChangeValueForKey:@"jid"];
+ [self didChangeValueForKey:@"jidStr"];
+}
+
+- (void)setJidStr:(NSString *)jidStr
+{
+ [self willChangeValueForKey:@"jid"];
+ [self willChangeValueForKey:@"jidStr"];
+
+ self.primitiveJid = [XMPPJID jidWithString:jidStr];
+ self.primitiveJidStr = jidStr;
+
+ [self didChangeValueForKey:@"jid"];
+ [self didChangeValueForKey:@"jidStr"];
+}
+
+#pragma mark Scalar
+
+- (BOOL)isFromMe
+{
+ return [[self fromMe] boolValue];
+}
+
+- (void)setIsFromMe:(BOOL)value
+{
+ self.fromMe = [NSNumber numberWithBool:value];
+}
+
+@end
View
37 Extensions/XEP-0045/CoreDataStorage/XMPPRoomOccupantCoreDataStorageObject.h
@@ -0,0 +1,37 @@
+#import <Foundation/Foundation.h>
+#import <CoreData/CoreData.h>
+
+#import "XMPP.h"
+#import "XMPPRoom.h"
+
+
+@interface XMPPRoomOccupantCoreDataStorageObject : NSManagedObject <XMPPRoomOccupant>
+
+/**
+ * The properties below are documented in the XMPPRoomOccupant protocol.
+**/
+
+@property (nonatomic, strong) XMPPPresence * presence; // Transient (proper type, not on disk)
+@property (nonatomic, strong) NSString * presenceStr; // Shadow (binary data, written to disk)
+
+@property (nonatomic, strong) XMPPJID * roomJID; // Transient (proper type, not on disk)
+@property (nonatomic, strong) NSString * roomJIDStr; // Shadow (binary data, written to disk)
+
+@property (nonatomic, strong) XMPPJID * jid; // Transient (proper type, not on disk)
+@property (nonatomic, strong) NSString * jidStr; // Shadow (binary data, written to disk)
+
+@property (nonatomic, strong) NSString * nickname;
+
+@property (nonatomic, strong) NSString * role;
+@property (nonatomic, strong) NSString * affiliation;
+
+@property (nonatomic, strong) XMPPJID * realJID; // Transient (proper type, not on disk)
+@property (nonatomic, strong) NSString * realJIDStr; // Shadow (binary data, written to disk)
+
+/**
+ * If a single instance of XMPPRoomCoreDataStorage is shared between multiple xmppStream's,
+ * this may be needed to distinguish between the streams.
+**/
+@property (nonatomic, strong) NSString * streamBareJidStr;
+
+@end
View
233 Extensions/XEP-0045/CoreDataStorage/XMPPRoomOccupantCoreDataStorageObject.m
@@ -0,0 +1,233 @@
+#import "XMPPRoomOccupantCoreDataStorageObject.h"
+
+#if ! __has_feature(objc_arc)
+#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
+#endif
+
+
+@interface XMPPRoomOccupantCoreDataStorageObject ()
+
+@property(nonatomic,strong) XMPPPresence * primitivePresence;
+@property(nonatomic,strong) NSString * primitivePresenceStr;
+
+@property(nonatomic,strong) XMPPJID * primitiveRoomJID;
+@property(nonatomic,strong) NSString * primitiveRoomJIDStr;
+
+@property(nonatomic,strong) XMPPJID * primitiveJid;
+@property(nonatomic,strong) NSString * primitiveJidStr;
+
+@property(nonatomic,strong) XMPPJID * primitiveRealJID;
+@property(nonatomic,strong) NSString * primitiveRealJIDStr;
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+@implementation XMPPRoomOccupantCoreDataStorageObject
+
+@dynamic presence, primitivePresence;
+@dynamic presenceStr, primitivePresenceStr;
+@dynamic roomJID, primitiveRoomJID;
+@dynamic roomJIDStr, primitiveRoomJIDStr;
+@dynamic jid, primitiveJid;
+@dynamic jidStr, primitiveJidStr;
+@dynamic nickname;
+@dynamic role;
+@dynamic affiliation;
+@dynamic realJID, primitiveRealJID;
+@dynamic realJIDStr, primitiveRealJIDStr;
+@dynamic streamBareJidStr;
+
+#pragma mark Transient presence
+
+- (XMPPPresence *)presence
+{
+ // Create and cache on demand
+
+ [self willAccessValueForKey:@"presence"];
+ XMPPPresence *presence = self.primitivePresence;
+ [self didAccessValueForKey:@"presence"];
+
+ if (presence == nil)
+ {
+ NSString *presenceStr = self.presenceStr;
+ if (presenceStr)
+ {
+ NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:presenceStr error:nil];
+ presence = [XMPPPresence presenceFromElement:element];
+ self.primitivePresence = presence;
+ }
+ }
+
+ return presence;
+}
+
+- (void)setPresence:(XMPPPresence *)newPresence
+{
+ [self willChangeValueForKey:@"presence"];
+ [self willChangeValueForKey:@"presenceStr"];
+
+ self.primitivePresence = newPresence;
+ self.primitivePresenceStr = [newPresence compactXMLString];
+
+ [self didChangeValueForKey:@"presence"];
+ [self didChangeValueForKey:@"presenceStr"];
+}
+
+- (void)setPresenceStr:(NSString *)presenceStr
+{
+ [self willChangeValueForKey:@"presence"];
+ [self willChangeValueForKey:@"presenceStr"];
+
+ NSXMLElement *element = [[NSXMLElement alloc] initWithXMLString:presenceStr error:nil];
+ self.primitivePresence = [XMPPPresence presenceFromElement:element];
+ self.primitivePresenceStr = presenceStr;
+
+ [self didChangeValueForKey:@"presence"];
+ [self didChangeValueForKey:@"presenceStr"];
+}
+
+#pragma mark Transient roomJID
+
+- (XMPPJID *)roomJID
+{
+ // Create and cache on demand
+
+ [self willAccessValueForKey:@"roomJID"];
+ XMPPJID *tmp = self.primitiveRoomJID;
+ [self didAccessValueForKey:@"roomJID"];
+
+ if (tmp == nil)
+ {
+ NSString *roomJIDStr = self.roomJIDStr;
+ if (roomJIDStr)
+ {
+ tmp = [XMPPJID jidWithString:roomJIDStr];
+ self.primitiveRoomJID = tmp;
+ }
+ }
+
+ return tmp;
+}
+
+- (void)setRoomJID:(XMPPJID *)roomJID
+{
+ [self willChangeValueForKey:@"roomJID"];
+ [self willChangeValueForKey:@"roomJIDStr"];
+
+ self.primitiveRoomJID = roomJID;
+ self.primitiveRoomJIDStr = [roomJID full];
+
+ [self didChangeValueForKey:@"roomJID"];
+ [self didChangeValueForKey:@"roomJIDStr"];
+}
+
+- (void)setRoomJIDStr:(NSString *)roomJIDStr
+{
+ [self willChangeValueForKey:@"roomJID"];
+ [self willChangeValueForKey:@"roomJIDStr"];
+
+ self.primitiveRoomJID = [XMPPJID jidWithString:roomJIDStr];
+ self.primitiveRoomJIDStr = roomJIDStr;
+
+ [self didChangeValueForKey:@"roomJID"];
+ [self didChangeValueForKey:@"roomJIDStr"];
+}
+
+#pragma mark Transient jid
+
+- (XMPPJID *)jid
+{
+ // Create and cache on demand
+
+ [self willAccessValueForKey:@"jid"];
+ XMPPJID *tmp = self.primitiveJid;
+ [self didAccessValueForKey:@"jid"];
+
+ if (tmp == nil)
+ {
+ NSString *jidStr = self.jidStr;
+ if (jidStr)
+ {
+ tmp = [XMPPJID jidWithString:jidStr];
+ self.primitiveJid = tmp;
+ }
+ }
+
+ return tmp;
+}
+
+- (void)setJid:(XMPPJID *)jid
+{
+ [self willChangeValueForKey:@"jid"];
+ [self willChangeValueForKey:@"jidStr"];
+
+ self.primitiveJid = jid;
+ self.primitiveJidStr = [jid full];
+
+ [self didChangeValueForKey:@"jid"];
+ [self didChangeValueForKey:@"jidStr"];
+}
+
+- (void)setJidStr:(NSString *)jidStr
+{
+ [self willChangeValueForKey:@"jid"];
+ [self willChangeValueForKey:@"jidStr"];
+
+ self.primitiveJid = [XMPPJID jidWithString:jidStr];
+ self.primitiveJidStr = jidStr;
+
+ [self didChangeValueForKey:@"jid"];
+ [self didChangeValueForKey:@"jidStr"];
+}
+
+#pragma mark Transient realJID
+
+- (XMPPJID *)realJID
+{
+ // Create and cache on demand
+
+ [self willAccessValueForKey:@"realJID"];
+ XMPPJID *tmp = self.primitiveRealJID;
+ [self didAccessValueForKey:@"realJID"];
+
+ if (tmp == nil)
+ {
+ NSString *realJIDStr = self.realJIDStr;
+ if (realJIDStr)
+ {
+ tmp = [XMPPJID jidWithString:realJIDStr];
+ self.primitiveRealJID = tmp;
+ }
+ }
+
+ return tmp;
+}
+
+- (void)setRealJID:(XMPPJID *)realJID
+{
+ [self willChangeValueForKey:@"realJID"];
+ [self willChangeValueForKey:@"realJIDStr"];
+
+ self.primitiveRealJID = realJID;
+ self.primitiveRealJIDStr = [realJID full];
+
+ [self didChangeValueForKey:@"realJID"];
+ [self didChangeValueForKey:@"realJIDStr"];
+}
+
+- (void)setRealJIDStr:(NSString *)realJIDStr
+{
+ [self willChangeValueForKey:@"realJID"];
+ [self willChangeValueForKey:@"realJIDStr"];
+
+ self.primitiveRealJID = [XMPPJID jidWithString:realJIDStr];
+ self.primitiveRealJIDStr = realJIDStr;
+
+ [self didChangeValueForKey:@"realJID"];
+ [self didChangeValueForKey:@"realJIDStr"];
+}
+
+@end
View
205 Extensions/XEP-0045/MemoryStorage/XMPPRoomMemoryStorage.m
@@ -53,8 +53,6 @@ - (id)init
messageClass = [XMPPRoomMessageMemoryStorage class];
occupantClass = [XMPPRoomOccupantMemoryStorage class];
-
-
}
return self;
}
@@ -120,26 +118,6 @@ - (dispatch_queue_t)parentQueue
return result;
}
-- (void)setMessageSortSelector:(SEL)messageSortSel
-{
- dispatch_block_t block = ^{ @autoreleasepool {
-
-
- }};
-
- dispatch_queue_t pq = self.parentQueue;
-
- if (pq == NULL || dispatch_get_current_queue() == pq)
- block();
- else
- dispatch_async(pq, block);
-}
-
-- (void)setOccupantSortSelector:(SEL)occupantSortSel
-{
-
-}
-
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Internal API
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -183,6 +161,8 @@ - (NSUInteger)insertMessage:(XMPPRoomMessageMemoryStorage *)message
NSComparisonResult cmp = [message compare:currentMessage];
if (cmp == NSOrderedAscending)
{
+ // message < currentMessage
+
if (mid == min)
break;
else
@@ -190,6 +170,8 @@ - (NSUInteger)insertMessage:(XMPPRoomMessageMemoryStorage *)message
}
else // Descending || Same
{
+ // message >= currentMessage
+
if (mid == max) {
mid++;
break;
@@ -250,6 +232,8 @@ - (NSUInteger)insertOccupant:(XMPPRoomOccupantMemoryStorage *)occupant
NSComparisonResult cmp = [occupant compare:currentOccupant];
if (cmp == NSOrderedAscending)
{
+ // occupant < currentOccupant
+
if (mid == min)
break;
else
@@ -257,6 +241,8 @@ - (NSUInteger)insertOccupant:(XMPPRoomOccupantMemoryStorage *)occupant
}
else // Descending || Same
{
+ // occupant >= currentOccupant
+
if (mid == max) {
mid++;
break;
@@ -406,7 +392,7 @@ - (NSArray *)resortOccupants
#pragma mark XMPPRoomStorage Protocol
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-- (void)handlePresence:(XMPPPresence *)presence xmppStream:(XMPPStream *)xmppStream
+- (void)handlePresence:(XMPPPresence *)presence room:(XMPPRoom *)room
{
XMPPLogTrace();
AssertParentQueue();
@@ -482,44 +468,187 @@ - (void)handlePresence:(XMPPPresence *)presence xmppStream:(XMPPStream *)xmppStr
}
}
-- (void)handleMessage:(XMPPMessage *)message xmppStream:(XMPPStream *)xmppStream
+- (void)addMessage:(XMPPRoomMessageMemoryStorage *)roomMsg
{
- XMPPRoomMessageMemoryStorage *roomMessage = [[self.messageClass alloc] initWithMessage:message];
- NSUInteger index = [self insertMessage:roomMessage];
+ NSUInteger index = [self insertMessage:roomMsg];
- XMPPRoomOccupantMemoryStorage *occupant = [occupantsDict objectForKey:[message from]];
+ XMPPRoomOccupantMemoryStorage *occupant = [occupantsDict objectForKey:[roomMsg jid]];
- XMPPRoomMessageMemoryStorage *roomMessageCopy = [roomMessage copy];
+ XMPPRoomMessageMemoryStorage *roomMsgCopy = [roomMsg copy];
XMPPRoomOccupantMemoryStorage *occupantCopy = [occupant copy];
NSArray *messagesCopy = [messages copy];
[[self multicastDelegate] xmppRoomMemoryStorage:self
- didReceiveMessage:roomMessageCopy
+ didReceiveMessage:roomMsgCopy
fromOccupant:occupantCopy
atIndex:index
inArray:messagesCopy];
}
-- (void)handleOutgoingMessage:(XMPPMessage *)message xmppStream:(XMPPStream *)xmppStream
+- (void)handleIncomingMessage:(XMPPMessage *)message room:(XMPPRoom *)room
{
XMPPLogTrace();
AssertParentQueue();
- [self handleMessage:message xmppStream:xmppStream];
+ XMPPJID *msgJID = [message from];
+
+ if ([room.myRoomJID isEqualToJID:msgJID])
+ {
+ // Ignore - we already stored message in handleOutgoingMessage:room:
+ return;
+ }
+
+ XMPPRoomMessageMemoryStorage *roomMessage = [[self.messageClass alloc] initWithIncomingMessage:message];
+
+ if (roomMessage.remoteTimestamp && ([messages count] > 0))
+ {
+ // Does this message already exist in the messages array?
+ // How can we tell if two XMPPRoomMessages are the same?
+ //
+ // 1. Same jid
+ // 2. Same text
+ // 3. Same remoteTimestamp
+ // 4. Approximately the same localTimestamps (if existing message doesn't have set remoteTimestamp)
+ //
+ // This is actually a rather difficult question.
+ // What if the same user sends the exact same message multiple times?
+ //
+ // If we first received the message while already in the room, it won't contain a remoteTimestamp.
+ // Returning to the room later and downloading the discussion history will return the same message,
+ // this time with a remote timestamp.
+ //
+ // So if the message doesn't have a remoteTimestamp,
+ // but it's localTimestamp is approximately the same as the remoteTimestamp,
+ // then this is enough evidence to consider the messages the same.
+
+ // Algorithm overview:
+ //
+ // Since the clock of the client and server may be out of sync,
+ // a localTimestamp and remoteTimestamp may be off by several seconds.
+ // So we're going to search a range of messages, bounded by a min and max localTimestamp.
+ //
+ // We find the first message that has a localTimestamp >= minLocalTimestamp.
+ // We then search from there to the first message that has a localTimestamp > maxLocalTimestamp.
+ //
+ // This represents our range of messages to search.
+ // Then we can simply iterate over these messages to see if any have the same jid and text.
+
+ NSDate *minLocalTimestamp = [roomMessage.remoteTimestamp dateByAddingTimeInterval:-60];
+ NSDate *maxLocalTimestamp = [roomMessage.remoteTimestamp dateByAddingTimeInterval: 60];
+
+ // Use binary search to locate first message with localTimestamp >= minLocalTimestamp.
+
+ NSInteger mid;
+ NSInteger min = 0;
+ NSInteger max = [messages count] - 1;
+
+ while (YES)
+ {
+ mid = (min + max) / 2;
+ XMPPRoomMessageMemoryStorage *currentMessage = [messages objectAtIndex:mid];
+
+ NSComparisonResult cmp = [minLocalTimestamp compare:[currentMessage localTimestamp]];
+ if (cmp == NSOrderedAscending)
+ {
+ // minLocalTimestamp < currentMessage.localTimestamp
+
+ if (mid == min)
+ break;
+ else
+ max = mid - 1;
+ }
+ else // Descending || Same
+ {
+ // minLocalTimestamp >= currentMessage.localTimestamp
+
+ if (mid == max) {
+ mid++;
+ break;
+ }
+ else {
+ min = mid + 1;
+ }
+ }
+ }
+
+ // The 'mid' variable now points to the index of the first message in the sorted messages array
+ // that has a localTimestamp >= minLocalTimestamp.
+ //
+ // Now we're going to find the first message in the sorted messages array
+ // that has a localTimestamp <= maxLocalTimestamp.
+
+ NSRange range = (NSRange){ .location = mid, .length = 0 };
+
+ NSInteger index;
+ for (index = range.location; index < [messages count]; index++)
+ {
+ XMPPRoomMessageMemoryStorage *currentMessage = [messages objectAtIndex:index];
+
+ NSComparisonResult cmp = [maxLocalTimestamp compare:[currentMessage localTimestamp]];
+ if (cmp == NSOrderedAscending)
+ {
+ // maxLocalTimestamp < currentMessage.localTimestamp
+ range.length++;
+ }
+ else
+ {
+ // maxLocalTimestamp >= currentMessage.localTimestamp
+ break;
+ }
+ }
+
+ // Now search our range to see if the message already exists
+
+ for (index = range.location; index < range.length; index++)
+ {
+ XMPPRoomMessageMemoryStorage *currentMessage = [messages objectAtIndex:mid];
+
+ if ([currentMessage.jid isEqualToJID:roomMessage.jid])
+ {
+ if ([currentMessage.body isEqualToString:roomMessage.body])
+ {
+ if (currentMessage.remoteTimestamp)
+ {
+ if ([currentMessage.remoteTimestamp isEqualToDate:roomMessage.remoteTimestamp])
+ {
+ // 1. jid matches
+ // 2. body matches
+ // 3. remoteTimestamp matches
+ //
+ // Incoming message already exists in the array.
+
+ return;
+ }
+ }
+ else
+ {
+ // 1. jid matches
+ // 2. body matches
+ // 3. existing message in array doesn't have set remoteTimestamp
+ // 4. existing message has approximately the same localTimestamp
+ //
+ // Incoming message already exists in the array.
+
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ [self addMessage:roomMessage];
+ XMPPLogError(@"messages: %@", messages);
}
-- (void)handleIncomingMessage:(XMPPMessage *)message xmppStream:(XMPPStream *)xmppStream
+- (void)handleOutgoingMessage:(XMPPMessage *)message room:(XMPPRoom *)room
{
XMPPLogTrace();
AssertParentQueue();
- if ([parent.myRoomJID isEqualToJID:[message from]])
- {
- // Ignore - we already stored message in handleOutgoingMessage:xmppStream:
- return;
- }
+ XMPPJID *msgJID = room.myRoomJID;
- [self handleMessage:message xmppStream:xmppStream];
+ XMPPRoomMessageMemoryStorage *roomMsg = [[self.messageClass alloc] initWithOutgoingMessage:message jid:msgJID];
+ [self addMessage:roomMsg];
}
@end
View
20 Extensions/XEP-0045/MemoryStorage/XMPPRoomMessageMemoryStorage.h
@@ -4,15 +4,31 @@
@interface XMPPRoomMessageMemoryStorage : NSObject <XMPPRoomMessage, NSCopying, NSCoding>
-- (id)initWithMessage:(XMPPMessage *)message;
+- (id)initWithIncomingMessage:(XMPPMessage *)message;
+- (id)initWithOutgoingMessage:(XMPPMessage *)message jid:(XMPPJID *)myRoomJID;
+
+/**
+ * The properties below are documented in the XMPPRoomMessage protocol.
+**/
@property (nonatomic, readonly) XMPPMessage *message;
+@property (nonatomic, readonly) XMPPJID * roomJID;
+
@property (nonatomic, readonly) XMPPJID * jid;
@property (nonatomic, readonly) NSString * nickname;
-@property (nonatomic, readonly) NSDate * timestamp;
+@property (nonatomic, readonly) NSDate * localTimestamp;
+@property (nonatomic, readonly) NSDate * remoteTimestamp;
+
+@property (nonatomic, readonly) BOOL isFromMe;
+/**
+ * Compares two messages based on the localTimestamp.
+ *
+ * This method provides the ordering used by XMPPRoomMemoryStorage.
+ * Subclasses may override this method to provide an alternative sorting mechanism.
+**/
- (NSComparisonResult)compare:(XMPPRoomMessageMemoryStorage *)another;
@end
View
74 Extensions/XEP-0045/MemoryStorage/XMPPRoomMessageMemoryStorage.m
@@ -6,18 +6,38 @@
@implementation XMPPRoomMessageMemoryStorage
{
XMPPMessage *message;
- NSDate *timestamp;
+ XMPPJID *jid;
+ NSDate *localTimestamp;
+ NSDate *remoteTimestamp;
+ BOOL isFromMe;
}
-- (id)initWithMessage:(XMPPMessage *)inMessage
+- (id)initWithIncomingMessage:(XMPPMessage *)inMessage
{
if ((self = [super init]))
{
message = inMessage;
+ jid = [inMessage from];
+ isFromMe = NO;
- timestamp = [inMessage delayedDeliveryDate];
- if (timestamp == nil)
- timestamp = [[NSDate alloc] init];
+ remoteTimestamp = [inMessage delayedDeliveryDate];
+ if (remoteTimestamp)
+ localTimestamp = remoteTimestamp;
+ else
+ localTimestamp = [[NSDate alloc] init];
+ }
+ return self;
+}
+
+- (id)initWithOutgoingMessage:(XMPPMessage *)inMessage jid:(XMPPJID *)myRoomJID
+{
+ if ((self = [super init]))
+ {
+ message = inMessage;
+ jid = myRoomJID;
+ isFromMe = YES;
+
+ localTimestamp = [[NSDate alloc] init];
}
return self;
}
@@ -43,13 +63,19 @@ - (id)initWithCoder:(NSCoder *)coder
{
if ([coder allowsKeyedCoding])
{
- message = [coder decodeObjectForKey:@"message"];
- timestamp = [coder decodeObjectForKey:@"timestamp"];
+ message = [coder decodeObjectForKey:@"message"];
+ jid = [coder decodeObjectForKey:@"jid"];
+ localTimestamp = [coder decodeObjectForKey:@"localTimestamp"];
+ remoteTimestamp = [coder decodeObjectForKey:@"remoteTimestamp"];
+ isFromMe = [coder decodeBoolForKey:@"isFromMe"];
}
else
{
- message = [coder decodeObject];
- timestamp = [coder decodeObject];
+ message = [coder decodeObject];
+ jid = [coder decodeObject];
+ localTimestamp = [coder decodeObject];
+ remoteTimestamp = [coder decodeObject];
+ isFromMe = [[coder decodeObject] boolValue];
}
}
return self;
@@ -59,13 +85,19 @@ - (void)encodeWithCoder:(NSCoder *)coder
{
if ([coder allowsKeyedCoding])
{
- [coder encodeObject:message forKey:@"message"];
- [coder encodeObject:timestamp forKey:@"timestamp"];
+ [coder encodeObject:message forKey:@"message"];
+ [coder encodeObject:jid forKey:@"jid"];
+ [coder encodeObject:localTimestamp forKey:@"timestamp"];
+ [coder encodeObject:remoteTimestamp forKey:@"remoteTimestamp"];
+ [coder encodeBool:isFromMe forKey:@"isFromMe"];
}
else
{
[coder encodeObject:message];
- [coder encodeObject:timestamp];
+ [coder encodeObject:jid];
+ [coder encodeObject:localTimestamp];
+ [coder encodeObject:remoteTimestamp];
+ [coder encodeObject:[NSNumber numberWithBool:isFromMe]];
}
}
@@ -80,7 +112,10 @@ - (id)copyWithZone:(NSZone *)zone
XMPPRoomMessageMemoryStorage *deepCopy = (XMPPRoomMessageMemoryStorage *)[[[self class] alloc] init];
deepCopy->message = [message copy];
- deepCopy->timestamp = [timestamp copy];
+ deepCopy->jid = [jid copy];
+ deepCopy->localTimestamp = [localTimestamp copy];
+ deepCopy->remoteTimestamp = [remoteTimestamp copy];
+ deepCopy->isFromMe = isFromMe;
return deepCopy;
}
@@ -90,16 +125,19 @@ - (id)copyWithZone:(NSZone *)zone
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@synthesize message;
-@synthesize timestamp;
+@synthesize jid;
+@synthesize localTimestamp;
+@synthesize remoteTimestamp;
+@synthesize isFromMe;
-- (XMPPJID *)jid
+- (XMPPJID *)roomJID
{
- return [message from];
+ return [jid bareJID];
}
- (NSString *)nickname
{
- return [[message from] resource];
+ return [jid resource];
}
- (NSString *)body
@@ -113,7 +151,7 @@ - (NSString *)body
- (NSComparisonResult)compare:(XMPPRoomMessageMemoryStorage *)another
{
- return [timestamp compare:[another timestamp]];
+ return [localTimestamp compare:[another localTimestamp]];
}
@end
View
22 Extensions/XEP-0045/MemoryStorage/XMPPRoomOccupantMemoryStorage.h
@@ -6,18 +6,28 @@
@interface XMPPRoomOccupantMemoryStorage : NSObject <XMPPRoomOccupant, NSCopying, NSCoding>
- (id)initWithPresence:(XMPPPresence *)presence;
+- (void)updateWithPresence:(XMPPPresence *)presence;
+
+/**
+ * The properties below are documented in the XMPPRoomOccupant protocol.
+**/
@property (readonly) XMPPPresence *presence;
-@property (readonly) XMPPJID * jid; // [presence from]
-@property (readonly) NSString * nickname; // [[presence from] nickname]
+@property (readonly) XMPPJID * jid;
+@property (readonly) XMPPJID * roomJID;
+@property (readonly) NSString * nickname;
@property (readonly) NSString * role;
@property (readonly) NSString * affiliation;
-@property (readonly) XMPPJID * realJID; // Only available in non-anonymous rooms
-
-- (void)updateWithPresence:(XMPPPresence *)presence;
-
+@property (readonly) XMPPJID * realJID;
+
+/**
+ * Compares two occupants based on the nickname.
+ *
+ * This method provides the ordering used by XMPPRoomMemoryStorage.
+ * Subclasses may override this method to provide an alternative sorting mechanism.
+**/
- (NSComparisonResult)compare:(XMPPRoomOccupantMemoryStorage *)another;
@end
View
5 Extensions/XEP-0045/MemoryStorage/XMPPRoomOccupantMemoryStorage.m
@@ -94,6 +94,11 @@ - (void)updateWithPresence:(XMPPPresence *)inPresence
#pragma mark Properties
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+- (XMPPJID *)roomJID
+{
+ return [jid bareJID];
+}
+
- (XMPPJID *)jid
{
return jid;
View
12 Extensions/XEP-0045/XMPPMUC.h
@@ -4,6 +4,18 @@
#define _XMPP_MUC_H
+/**
+ * The XMPPMUC module, combined with XMPPRoom and associated storage classes,
+ * provides an implementation of XEP-0045 Multi-User Chat.
+ *
+ * The bulk of the code resides in XMPPRoom, which handles the xmpp technical details
+ * such as surrounding joining/leaving a room, sending/receiveing messages, etc.
+ *
+ * The XMPPMUC class provides 2 general (but important) tasks relating to MUC.
+ * First, it integrates with XMPPCapabilities (if included) to properly advertise support for MUC.
+ * Second, it monitors active XMPPRoom instances on the xmppStream,
+ * and provides an efficient query to see if a presence or message element is targeted at a room.
+**/
@interface XMPPMUC : XMPPModule
{
View
9 Extensions/XEP-0045/XMPPMUC.m
@@ -20,7 +20,6 @@ - (BOOL)activate:(XMPPStream *)aXmppStream
#ifdef _XMPP_CAPABILITIES_H
[xmppStream autoAddDelegate:self delegateQueue:moduleQueue toModulesOfClass:[XMPPCapabilities class]];
#endif
-
return YES;
}
@@ -104,23 +103,17 @@ - (void)xmppStream:(XMPPStream *)sender willUnregisterModule:(id)module
**/
- (void)xmppCapabilities:(XMPPCapabilities *)sender collectingMyCapabilities:(NSXMLElement *)query
{
- // This method is invoked on the moduleQueue.
+ // This method is invoked on our moduleQueue.
// <query xmlns="http://jabber.org/protocol/disco#info">
// ...
- // <identity category='client' type='pc'/>
// <feature var='http://jabber.org/protocol/muc'/>
// ...
// </query>
- NSXMLElement *identity = [NSXMLElement elementWithName:@"identity"];
- [identity addAttributeWithName:@"category" stringValue:@"client"];
- [identity addAttributeWithName:@"type" stringValue:@"facebook"];
-
NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"];
[feature addAttributeWithName:@"var" stringValue:@"http://jabber.org/protocol/muc"];
- [query addChild:identity];
[query addChild:feature];
}
#endif
View
15 Extensions/XEP-0045/XMPPRoom.h
@@ -52,9 +52,9 @@
@property (readonly) id <XMPPRoomStorage> xmppRoomStorage;
-@property (readonly) XMPPJID * roomJID; // E.g. xmppDevs@muc.deusty.com
+@property (readonly) XMPPJID * roomJID; // E.g. xmpp-development@conference.deusty.com
-@property (readonly) XMPPJID * myRoomJID; // E.g. xmppDevs@muc.deusty.com/robbiehanson
+@property (readonly) XMPPJID * myRoomJID; // E.g. xmpp-development@conference.deusty.com/robbiehanson
@property (readonly) NSString * myNickname; // E.g. robbiehanson
@property (readonly) NSString *roomSubject;
@@ -185,13 +185,18 @@
* Updates and returns the occupant for the given presence element.
* If the presence type is "available", and the occupant doesn't already exist, then one should be created.
**/
-- (void)handlePresence:(XMPPPresence *)presence xmppStream:(XMPPStream *)xmppStream;
+- (void)handlePresence:(XMPPPresence *)presence room:(XMPPRoom *)room;
/**
* Stores or otherwise handles the given message element.
**/
-- (void)handleOutgoingMessage:(XMPPMessage *)message xmppStream:(XMPPStream *)xmppStream;
-- (void)handleIncomingMessage:(XMPPMessage *)message xmppStream:(XMPPStream *)xmppStream;
+- (void)handleIncomingMessage:(XMPPMessage *)message room:(XMPPRoom *)room;
+- (void)handleOutgoingMessage:(XMPPMessage *)message room:(XMPPRoom *)room;
+
+@optional
+
+- (void)handleDidJoinRoom:(XMPPJID *)roomJID withNickname:(NSString *)nickname;
+- (void)handleDidLeaveRoom:(XMPPJID *)roomJID;
@end
View
6 Extensions/XEP-0045/XMPPRoom.m
@@ -888,7 +888,7 @@ - (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)prese
XMPPLogTrace();
- [xmppRoomStorage handlePresence:presence xmppStream:sender];
+ [xmppRoomStorage handlePresence:presence room:self];
// My presence:
//
@@ -1038,7 +1038,7 @@ - (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message
if (isChatMessage)
{
- [xmppRoomStorage handleIncomingMessage:message xmppStream:sender];
+ [xmppRoomStorage handleIncomingMessage:message room:self];
[multicastDelegate xmppRoom:self didReceiveMessage:message fromOccupant:from];
}
else
@@ -1074,7 +1074,7 @@ - (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message
if (isChatMessage)
{
- [xmppRoomStorage handleOutgoingMessage:message xmppStream:sender];
+ [xmppRoomStorage handleOutgoingMessage:message room:self];
}
}
View
56 Extensions/XEP-0045/XMPPRoomMessage.h
@@ -6,13 +6,53 @@
@protocol XMPPRoomMessage <NSObject>
-@property (nonatomic, readonly) XMPPMessage * message;
-
-@property (nonatomic, readonly) XMPPJID * jid; // [message from]
-@property (nonatomic, readonly) NSString * nickname; // [[message from] resource]
-
-@property (nonatomic, readonly) NSString * body;
-
-@property (nonatomic, readonly) NSDate * timestamp;
+/**
+ * The raw message that was sent / received.
+**/
+- (XMPPMessage *)message;
+
+/**
+ * The JID of the MUC room.
+**/
+- (XMPPJID *)roomJID;
+
+/**
+ * Who sent the message.
+ * A typical MUC room jid is of the form "room_name@conference.domain.tld/some_nickname".
+**/
+- (XMPPJID *)jid;
+
+/**
+ * The nickname of the user who sent the message.
+ * This is a convenience method for [jid resource].
+**/
+- (NSString *)nickname;
+
+/**
+ * Convenience method to access the body of the message.
+**/
+- (NSString *)body;
+
+/**
+ * When the message was sent / received (as recorded by us).
+ *
+ * If the message was originally sent by us, the localTimestamp is recorded automatically.
+ * If the message was received, the server may have included a delayed delivery date timestamp.
+ * This is the case when first joining a room, and downloading the discussion history.
+ * In such a case, the localTimestamp will be a reflection of the serverTimestamp.
+**/
+- (NSDate *)localTimestamp;
+
+/**
+ * When the message was sent / received (as recorded by the server).
+ *
+ * Only set when the server includes a delayedDelivery timestamp within the message.
+**/
+- (NSDate *)remoteTimestamp;
+
+/**
+ * Whether or not the message was sent by us.
+**/
+- (BOOL)isFromMe;
@end
View
51 Extensions/XEP-0045/XMPPRoomOccupant.h
@@ -6,13 +6,52 @@
@protocol XMPPRoomOccupant <NSObject>
-@property (readonly) XMPPPresence *presence;
+/**
+ * Most recent presence message from occupant.
+**/
+- (XMPPPresence *)presence;
-@property (readonly) XMPPJID * jid; // [presence from]
-@property (readonly) NSString * nickname; // [[presence from] nickname]
+/**
+ * The MUC room the occupant is associated with.
+**/
+- (XMPPJID *)roomJID;
-@property (readonly) NSString * role;
-@property (readonly) NSString * affiliation;
-@property (readonly) XMPPJID * realJID; // Only available in non-anonymous rooms
+/**
+ * The JID of the occupant as reported by the room.
+ * A typical MUC room will use JIDs of the form: "room_name@conference.domain.tl/some_nickname".
+**/
+- (XMPPJID *)jid;
+
+/**
+ * The nickname of the user.
+ * In other words, the resource portion of the occupants JID.
+**/
+- (NSString *)nickname;
+
+/**
+ * The 'role' and 'affiliation' of the occupant within the MUC room.
+ *
+ * From XEP-0045, Section 5 - Roles and Affiliations:
+ *
+ * There are two dimensions along which we can measure a user's connection with or position in a room.
+ * One is the user's long-lived affiliation with a room -- e.g., a user's status as an owner or an outcast.
+ * The other is a user's role while an occupant of a room -- e.g., an occupant's position as a moderator with the
+ * ability to kick visitors and participants. These two dimensions are distinct from each other, since an affiliation
+ * lasts across visits, while a role lasts only for the duration of a visit. In addition, there is no one-to-one
+ * correspondence between roles and affiliations; for example, someone who is not affiliated with a room may be
+ * a (temporary) moderator, and a member may be a participant or a visitor in a moderated room.
+ *
+ * For more information, please see XEP-0045.
+**/
+- (NSString *)role;
+- (NSString *)affiliation;
+
+/**
+ * If the MUC room is non-anonymous, the real JID of the user will be broadcast.
+ *
+ * An anonymous room uses JID's of the form: "room_name@conference.domain.tld/some_nickname".
+ * A non-anonymous room also includes the occupants real full JID in the presence broadcast.
+**/
+- (XMPPJID *)realJID;
@end
View
2  Utilities/XMPPIDTracker.h
@@ -59,7 +59,7 @@
*
* // Same xmppStream:didReceiveIQ: as example 1
*
- * ---- EXAMPLE 2 - ADVANCED TRACKING ----
+ * ---- EXAMPLE 3 - ADVANCED TRACKING ----
*
* @interface PingTrackingInfo : XMPPBasicTrackingInfo
* ...
View
308 Xcode/DesktopXMPP/English.lproj/MucWindow.xib
@@ -18,6 +18,7 @@
<string>NSScrollView</string>
<string>NSTextFieldCell</string>
<string>NSTableView</string>
+ <string>NSTableCellView</string>
<string>NSCustomView</string>
<string>NSCustomObject</string>
<string>NSView</string>
@@ -80,33 +81,36 @@
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSTableView" id="576258445">
<reference key="NSNextResponder" ref="677239835"/>
- <int key="NSvFlags">256</int>
- <string key="NSFrameSize">{308, 291}</string>
+ <int key="NSvFlags">4352</int>
+ <string key="NSFrameSize">{307, 291}</string>
<reference key="NSSuperview" ref="677239835"/>
<reference key="NSWindow"/>
- <reference key="NSNextKeyView" ref="485121053"/>
+ <reference key="NSNextKeyView" ref="91255239"/>
<string key="NSReuseIdentifierKey">_NS:1828</string>
<bool key="NSEnabled">YES</bool>
<object class="NSTableHeaderView" key="NSHeaderView" id="145726341">
<reference key="NSNextResponder" ref="897407837"/>
<int key="NSvFlags">256</int>
- <string key="NSFrameSize">{308, 17}</string>
+ <string key="NSFrameSize">{307, 17}</string>
<reference key="NSSuperview" ref="897407837"/>
<reference key="NSWindow"/>
- <reference key="NSNextKeyView" ref="677239835"/>
+ <reference key="NSNextKeyView" ref="340243070"/>
<string key="NSReuseIdentifierKey">_NS:1830</string>
<reference key="NSTableView" ref="576258445"/>
</object>
- <object class="_NSCornerView" key="NSCornerView">
- <nil key="NSNextResponder"/>
+ <object class="_NSCornerView" key="NSCornerView" id="340243070">
+ <reference key="NSNextResponder" ref="1026667943"/>
<int key="NSvFlags">-2147483392</int>
<string key="NSFrame">{{224, 0}, {16, 17}}</string>
+ <reference key="NSSuperview" ref="1026667943"/>
+ <reference key="NSWindow"/>
+ <reference key="NSNextKeyView" ref="677239835"/>
<string key="NSReuseIdentifierKey">_NS:1833</string>
</object>
<object class="NSMutableArray" key="NSTableColumns">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSTableColumn" id="444884069">
- <double key="NSWidth">305</double>
+ <double key="NSWidth">304</double>
<double key="NSMinWidth">40</double>
<double key="NSMaxWidth">1000</double>
<object class="NSTableHeaderCell" key="NSHeaderCell">
@@ -179,7 +183,7 @@
<bytes key="NSWhite">MC41AA</bytes>
</object>
</object>
- <double key="NSRowHeight">17</double>
+ <double key="NSRowHeight">49</double>
<int key="NSTvFlags">314572800</int>
<reference key="NSDelegate"/>
<reference key="NSDataSource"/>
@@ -215,13 +219,12 @@
</object>
<object class="NSScroller" id="260800109">
<reference key="NSNextResponder" ref="1026667943"/>
- <int key="NSvFlags">256</int>
+ <int key="NSvFlags">-2147483392</int>
<string key="NSFrame">{{1, 293}, {307, 15}}</string>
<reference key="NSSuperview" ref="1026667943"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView" ref="446738694"/>
<string key="NSReuseIdentifierKey">_NS:1847</string>
- <bool key="NSEnabled">YES</bool>
<int key="NSsFlags">1</int>
<reference key="NSTarget" ref="1026667943"/>
<string key="NSAction">_doScroller:</string>
@@ -243,18 +246,20 @@
<reference key="NSBGColor" ref="73785671"/>
<int key="NScvFlags">4</int>
</object>
+ <reference ref="340243070"/>
</object>
<string key="NSFrame">{{0, 69}, {309, 309}}</string>
<reference key="NSSuperview" ref="830867103"/>
<reference key="NSWindow"/>
<reference key="NSNextKeyView" ref="897407837"/>
<string key="NSReuseIdentifierKey">_NS:1824</string>
- <int key="NSsFlags">133810</int>
+ <int key="NSsFlags">133682</int>
<reference key="NSVScroller" ref="485121053"/>
<reference key="NSHScroller" ref="260800109"/>
<reference key="NSContentView" ref="677239835"/>
<reference key="NSHeaderClipView" ref="897407837"/>
- <bytes key="NSScrollAmts">QSAAAEEgAABBmAAAQZgAAA</bytes>
+ <reference key="NSCornerView" ref="340243070"/>
+ <bytes key="NSScrollAmts">QSAAAEEgAABCTAAAQkwAAA</bytes>
</object>
<object class="NSTextField" id="255430935">
<reference key="NSNextResponder" ref="830867103"/>
@@ -272,7 +277,7 @@
<reference key="NSSupport" ref="26"/>
<string key="NSCellIdentifier">_NS:3944</string>
<reference key="NSControlView" ref="255430935"/>
- <object class="NSColor" key="NSBackgroundColor">
+ <object class="NSColor" key="NSBackgroundColor" id="3505051">
<int key="NSColorSpace">6</int>
<string key="NSCatalogName">System</string>
<string key="NSColorName">controlColor</string>
@@ -345,7 +350,7 @@
<string key="NSFrameSize">{159, 361}</string>
<reference key="NSSuperview" ref="112552427"/>
<reference key="NSWindow"/>
- <reference key="NSNextKeyView" ref="805601027"/>
+ <reference key="NSNextKeyView" ref="454185919"/>
<string key="NSReuseIdentifierKey">_NS:1828</string>
<bool key="NSEnabled">YES</bool>
<object class="NSTableHeaderView" key="NSHeaderView" id="185271513">
@@ -354,13 +359,16 @@
<string key="NSFrameSize">{159, 17}</string>
<reference key="NSSuperview" ref="993256109"/>
<reference key="NSWindow"/>
- <reference key="NSNextKeyView" ref="112552427"/>
+ <reference key="NSNextKeyView" ref="887630968"/>
<reference key="NSTableView" ref="504926578"/>
</object>
- <object class="_NSCornerView" key="NSCornerView">
- <nil key="NSNextResponder"/>
+ <object class="_NSCornerView" key="NSCornerView" id="887630968">
+ <reference key="NSNextResponder" ref="893783524"/>
<int key="NSvFlags">-2147483392</int>
<string key="NSFrame">{{224, 0}, {16, 17}}</string>
+ <reference key="NSSuperview" ref="893783524"/>
+ <reference key="NSWindow"/>
+ <reference key="NSNextKeyView" ref="112552427"/>
<string key="NSReuseIdentifierKey">_NS:1833</string>
</object>
<object class="NSMutableArray" key="NSTableColumns">
@@ -398,7 +406,7 @@
<double key="NSIntercellSpacingHeight">2</double>
<reference key="NSBackgroundColor" ref="903088274"/>
<reference key="NSGridColor" ref="556764159"/>
- <double key="NSRowHeight">17</double>
+ <double key="NSRowHeight">26</double>
<int key="NSTvFlags">314572800</int>
<reference key="NSDelegate"/>
<reference key="NSDataSource"/>
@@ -459,6 +467,7 @@
<reference key="NSBGColor" ref="73785671"/>
<int key="NScvFlags">4</int>
</object>
+ <reference ref="887630968"/>
</object>
<string key="NSFrame">{{0, -1}, {161, 379}}</string>
<reference key="NSSuperview" ref="170361387"/>
@@ -470,7 +479,8 @@
<reference key="NSHScroller" ref="508157546"/>
<reference key="NSContentView" ref="112552427"/>
<reference key="NSHeaderClipView" ref="993256109"/>
- <bytes key="NSScrollAmts">QSAAAEEgAABBmAAAQZgAAA</bytes>
+ <reference key="NSCornerView" ref="887630968"/>
+ <bytes key="NSScrollAmts">QSAAAEEgAABB4AAAQeAAAA</bytes>
</object>
</object>
<string key="NSFrame">{{319, 0}, {161, 378}}</string>
@@ -496,8 +506,9 @@
<reference key="NSWindow"/>
<reference key="NSNextKeyView" ref="1061351068"/>
</object>
- <string key="NSScreenRect">{{0, 0}, {1440, 878}}</string>
+ <string key="NSScreenRect">{{0, 0}, {2560, 1418}}</string>
<string key="NSMaxSize">{10000000000000, 10000000000000}</string>
+ <string key="NSFrameAutosaveName">MUCWindow</string>
<bool key="NSWindowIsRestorable">YES</bool>
</object>
</object>
@@ -576,6 +587,116 @@
</object>
<int key="connectionID">56</int>
</object>
+ <object class="IBConnectionRecord">
+ <object class="IBOutletConnection" key="connection">
+ <string key="label">nicknameField</string>
+ <object class="NSTableCellView" key="source" id="91255239">
+ <nil key="NSNextResponder"/>
+ <int key="NSvFlags">274</int>
+ <object class="NSMutableArray" key="NSSubviews">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="NSTextField" id="993173166">
+ <reference key="NSNextResponder" ref="91255239"/>
+ <int key="NSvFlags">266</int>
+ <string key="NSFrame">{{5, 32}, {294, 17}}</string>
+ <reference key="NSSuperview" ref="91255239"/>
+ <reference key="NSNextKeyView" ref="313404528"/>
+ <bool key="NSEnabled">YES</bool>
+ <object class="NSTextFieldCell" key="NSCell" id="598332454">
+ <int key="NSCellFlags">67239488</int>
+ <int key="NSCellFlags2">272631872</int>
+ <string key="NSContents">Nickname</string>
+ <object class="NSFont" key="NSSupport" id="430374447">
+ <string key="NSName">LucidaGrande</string>
+ <double key="NSSize">12</double>
+ <int key="NSfFlags">16</int>
+ </object>
+ <reference key="NSControlView" ref="993173166"/>
+ <reference key="NSBackgroundColor" ref="3505051"/>
+ <object class="NSColor" key="NSTextColor">
+ <int key="NSColorSpace">1</int>
+ <bytes key="NSRGB">MC4zOTIxNTY4NjI3IDAuMzkyMTU2ODYyNyAwLjM5MjE1Njg2MjcAA</bytes>
+ </object>
+ </object>
+ </object>
+ <object class="NSTextField" id="313404528">
+ <reference key="NSNextResponder" ref="91255239"/>
+ <int key="NSvFlags">274</int>
+ <string key="NSFrame">{{5, 12}, {294, 17}}</string>
+ <reference key="NSSuperview" ref="91255239"/>
+ <reference key="NSNextKeyView" ref="485121053"/>
+ <string key="NSReuseIdentifierKey">_NS:3944</string>
+ <bool key="NSEnabled">YES</bool>
+ <object class="NSTextFieldCell" key="NSCell" id="1058745891">
+ <int key="NSCellFlags">67239424</int>
+ <int key="NSCellFlags2">272629760</int>
+ <string key="NSContents">Message</string>
+ <object class="NSFont" key="NSSupport">
+ <string key="NSName">LucidaGrande</string>
+ <double key="NSSize">13</double>
+ <int key="NSfFlags">16</int>
+ </object>
+ <string key="NSCellIdentifier">_NS:3944</string>
+ <reference key="NSControlView" ref="313404528"/>
+ <reference key="NSBackgroundColor" ref="3505051"/>
+ <object class="NSColor" key="NSTextColor">
+ <int key="NSColorSpace">1</int>
+ <bytes key="NSRGB">MC4wNTg4MjM1Mjk0MSAwLjA1ODgyMzUyOTQxIDAuMDU4ODIzNTI5NDEAA</bytes>
+ </object>
+ </object>
+ </object>
+ </object>
+ <string key="NSFrame">{{1, 1}, {304, 49}}</string>
+ <reference key="NSNextKeyView" ref="993173166"/>
+ </object>
+ <reference key="destination" ref="993173166"/>
+ </object>
+ <int key="connectionID">75</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBOutletConnection" key="connection">
+ <string key="label">messageField</string>
+ <reference key="source" ref="91255239"/>
+ <reference key="destination" ref="313404528"/>
+ </object>
+ <int key="connectionID">78</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBOutletConnection" key="connection">
+ <string key="label">textField</string>
+ <object class="NSTableCellView" key="source" id="454185919">
+ <nil key="NSNextResponder"/>
+ <int key="NSvFlags">274</int>
+ <object class="NSMutableArray" key="NSSubviews">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="NSTextField" id="419562295">
+ <reference key="NSNextResponder" ref="454185919"/>
+ <int key="NSvFlags">266</int>
+ <string key="NSFrame">{{3, 5}, {150, 17}}</string>
+ <reference key="NSSuperview" ref="454185919"/>
+ <reference key="NSNextKeyView" ref="805601027"/>
+ <bool key="NSEnabled">YES</bool>
+ <object class="NSTextFieldCell" key="NSCell" id="370384794">
+ <int key="NSCellFlags">67239488</int>
+ <int key="NSCellFlags2">272631808</int>
+ <string key="NSContents">Table View Cell</string>
+ <reference key="NSSupport" ref="430374447"/>
+ <reference key="NSControlView" ref="419562295"/>
+ <reference key="NSBackgroundColor" ref="3505051"/>
+ <object class="NSColor" key="NSTextColor">
+ <int key="NSColorSpace">1</int>
+ <bytes key="NSRGB">MC4wNTg4MjM1Mjk0MSAwLjA1ODgyMzUyOTQxIDAuMDU4ODIzNTI5NDEAA</bytes>
+ </object>
+ </object>
+ </object>
+ </object>
+ <string key="NSFrame">{{1, 1}, {156, 26}}</string>
+ <reference key="NSNextKeyView" ref="419562295"/>
+ </object>
+ <reference key="destination" ref="419562295"/>
+ </object>
+ <int key="connectionID">73</int>
+ </object>
</object>
<object class="IBMutableOrderedSet" key="objectRecords">
<object class="NSArray" key="orderedObjects">
@@ -691,6 +812,7 @@
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="841386059"/>
<