Frameworks/Adium Framework/Source/AIListBookmark.m
author Zachary West <zacw@adium.im>
Wed Oct 28 20:48:18 2009 -0400 (2009-10-28)
changeset 2675 f4ba67614e13
parent 2360 7d004b148df5
child 3243 3a23c59ac2ac
permissions -rw-r--r--
Examine any open chats when a bookmark is initialized to set its defaults. Fixes #12771.

This was occurring since the "saved chats" were opened/created before the AIListBookmark.
     1 //
     2 //  AIListBookmark.m
     3 //  Adium
     4 //
     5 //  Created by Erik Beerepoot on 19/07/07.
     6 //  Copyright 2007 Adium Team. All rights reserved.
     7 //
     8 
     9 #import "AIListBookmark.h"
    10 #import <Adium/AIAccount.h>
    11 #import <Adium/AIListGroup.h>
    12 #import <Adium/AIAccountControllerProtocol.h>
    13 #import <Adium/AIInterfaceControllerProtocol.h>
    14 #import <Adium/AIChatControllerProtocol.h>
    15 #import <Adium/AIContactControllerProtocol.h>
    16 #import <Adium/AIUserIcons.h>
    17 #import <Adium/AIService.h>
    18 #import <Adium/AIChat.h>
    19 #import <Adium/AIContactList.h>
    20 #import <AIUtilities/AIAttributedStringAdditions.h>
    21 
    22 #define	KEY_CONTAINING_OBJECT_UID	@"ContainingObjectUID"
    23 #define	OBJECT_STATUS_CACHE			@"Object Status Cache"
    24 
    25 #define KEY_ACCOUNT_INTERNAL_ID		@"AccountInternalObjectID"
    26 
    27 @interface AIListBookmark ()
    28 - (BOOL)chatIsOurs:(AIChat *)chat;
    29 - (AIChat *)openChatWithoutActivating;
    30 - (void)restoreGrouping;
    31 
    32 - (void)claimChatIfOurs:(AIChat *)chat;
    33 @end
    34 
    35 @implementation AIListBookmark
    36 
    37 @synthesize name, password, chatCreationDictionary;
    38 
    39 - (id)initWithUID:(NSString *)inUID
    40 		  account:(AIAccount *)inAccount
    41 		  service:(AIService *)inService
    42 	   dictionary:(NSDictionary *)inChatCreationDictionary
    43 			 name:(NSString *)inName
    44 {
    45 	if ((self = [super initWithUID:inUID
    46 						   account:inAccount
    47 						   service:inService])) {
    48 		chatCreationDictionary = [inChatCreationDictionary copy];
    49 		name = [inName copy];
    50 		
    51 		[adium.chatController registerChatObserver:self];
    52 		
    53 		[self.account addObserver:self
    54 					   forKeyPath:@"Online"
    55 						  options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial)
    56 						  context:NULL];
    57 		
    58 		[[NSNotificationCenter defaultCenter] addObserver:self
    59 									 selector:@selector(chatDidOpen:) 
    60 										 name:Chat_DidOpen
    61 									   object:nil];
    62 		
    63 		// Scan all open chats to claim them, if we loaded after they were available.
    64 		for (AIChat *chat in adium.interfaceController.openChats) {
    65 			[self claimChatIfOurs:chat];
    66 		}
    67 		
    68 		AILog(@"Created %@", self);
    69 		
    70 	}
    71 	
    72 	return self;
    73 }
    74 
    75 -(id)initWithChat:(AIChat *)inChat
    76 {
    77 	if ((self = [self initWithUID:[NSString stringWithFormat:@"Bookmark:%@", inChat.uniqueChatID]
    78 						  account:inChat.account
    79 						  service:inChat.account.service
    80 					   dictionary:inChat.chatCreationDictionary
    81 							 name:inChat.name])) {
    82 		[self setDisplayName:inChat.displayName];
    83 	}
    84 	
    85 	return self;
    86 }
    87 
    88 - (id)initWithCoder:(NSCoder *)decoder
    89 {
    90 	AIAccount *myAccount = [adium.accountController accountWithInternalObjectID:[decoder decodeObjectForKey:KEY_ACCOUNT_INTERNAL_ID]];
    91 	
    92 	if (!myAccount) {
    93 		[self release];
    94 		return nil;
    95 	}
    96 	
    97 	if ((self = [self initWithUID:[decoder decodeObjectForKey:@"UID"]
    98 						  account:myAccount
    99 						  service:[adium.accountController firstServiceWithServiceID:[decoder decodeObjectForKey:@"ServiceID"]]
   100 					   dictionary:[decoder decodeObjectForKey:@"chatCreationDictionary"]
   101 							 name:[decoder decodeObjectForKey:@"name"]])) {
   102 		[self restoreGrouping];
   103 	}
   104 	return self;
   105 }
   106 
   107 
   108 - (void)encodeWithCoder:(NSCoder *)encoder
   109 {
   110 	[encoder encodeObject:self.UID forKey:@"UID"];
   111 	[encoder encodeObject:self.account.internalObjectID forKey:KEY_ACCOUNT_INTERNAL_ID];
   112 	[encoder encodeObject:self.service.serviceID forKey:@"ServiceID"];
   113 	[encoder encodeObject:self.chatCreationDictionary forKey:@"chatCreationDictionary"];
   114 	[encoder encodeObject:name forKey:@"name"];
   115 }
   116 
   117 - (void)dealloc
   118 {
   119 	[name release]; name = nil;
   120 	[chatCreationDictionary release]; chatCreationDictionary = nil;
   121 	[password release]; password = nil;
   122 	
   123 	[[NSNotificationCenter defaultCenter] removeObserver:self];
   124 	[adium.chatController unregisterChatObserver:self];
   125 	[self.account removeObserver:self forKeyPath:@"Online"];
   126 
   127 	[super dealloc];
   128 }
   129 
   130 /*!
   131  * @brief Remove ourself
   132  *
   133  * We've been asked to be removed. Ask the contact controller to do so.
   134  */
   135 - (void)removeFromGroup:(AIListObject <AIContainingObject> *)group
   136 {
   137 	[adium.contactController removeBookmark:self];
   138 }
   139 
   140 /*!
   141  * @brief Our formatted UID
   142  *
   143  * If we're in an active chat, returns the name of the chat; otherwise, our UID.
   144  */
   145 - (NSString *)formattedUID
   146 {
   147 	AIChat *chat = [adium.chatController existingChatWithName:[self name]
   148 													onAccount:self.account];
   149 	
   150 	if ([self chatIsOurs:chat]) {
   151 		return chat.name;
   152 	} else {
   153 		return self.name;
   154 	}
   155 }
   156 
   157 - (BOOL) existsServerside
   158 {
   159 	return NO; //TODO: protocols where this can be yes, like XMPP
   160 }
   161 
   162 /*!
   163  * @brief Internal ID for this object
   164  *
   165  * An object ID generated by Adium that is shared by all objects which are, to most intents and purposes, identical to
   166  * this object.  Ths ID is composed of the service ID and UID, so any object with identical services and object IDs
   167  * will have the same value here.
   168  */
   169 - (NSString *)internalObjectID
   170 {
   171 	if (!internalObjectID) {
   172 		NSAssert(self.account != nil, @"Null list bookmark account - make sure you didn't try to touch the internalObjectID before it was loaded.");
   173 		
   174 		// We're not like any other bookmarks by the same name.
   175 		internalObjectID = [[NSString stringWithFormat:@"%@.%@.%@", self.service.serviceID, self.UID, self.account.UID] retain];
   176 	}
   177 	
   178 	return internalObjectID;
   179 }
   180 
   181 /*!
   182  * @brief Set our display name
   183  *
   184  * Update the display name of our chat if our display name changes.
   185  */
   186 - (void)setDisplayName:(NSString *)inDisplayName
   187 {
   188 	[super setDisplayName:inDisplayName];
   189 	
   190 	AIChat *chat = [adium.chatController existingChatWithName:[self name]
   191 					onAccount:self.account];
   192 	
   193 	if ([self chatIsOurs:chat]) {
   194 		chat.displayName = self.displayName;
   195 	}
   196 }
   197 
   198 /*!
   199  * @brief For a newly created bookmark, set the group that -restoreGrouping will move us to. This is saved, so has no use on existing bookmarks
   200  */
   201 - (void)setInitialGroup:(AIListGroup *)inGroup
   202 {
   203 	[self setPreference:inGroup.UID
   204 				 forKey:KEY_CONTAINING_OBJECT_UID
   205 				  group:OBJECT_STATUS_CACHE];	
   206 }
   207 
   208 /*!
   209  * @brief Add a containing group
   210  *
   211  * When adding a containing group, save the group's UID so that we can rejoin the group next time.
   212  */
   213 - (void)addContainingGroup:(AIListGroup *)inGroup
   214 {
   215 	[super addContainingGroup:inGroup];
   216 	
   217 	NSString *groupUID = inGroup.UID;
   218 	NSString *savedGroupUID = [self preferenceForKey:KEY_CONTAINING_OBJECT_UID group:OBJECT_STATUS_CACHE];
   219 	
   220 	if((!savedGroupUID || ![groupUID isEqualToString:savedGroupUID]) &&
   221 		(inGroup != adium.contactController.contactList)) {
   222 		// We either don't have a group, or this is a new, non-root-list group. Set our preference.
   223 		
   224 		[self setPreference:groupUID
   225 					 forKey:KEY_CONTAINING_OBJECT_UID
   226 					  group:OBJECT_STATUS_CACHE];
   227 	}
   228 }
   229 
   230 /*!
   231  * @brief Restore grouping
   232  *
   233  * When asked to restore grouping, move ourselves to the appropriate AIListGroup:
   234  * - The root contact list if contact list groups are disabled, or
   235  * - The last saved group. If the last saved group is missing for some reason, we move to "Bookmarks".
   236  */
   237 - (void)restoreGrouping
   238 {
   239 	NSSet *targetGroup = nil;
   240 	// In reality, it's extremely unlikely the saved group would be lost.
   241 	NSString *savedGroupUID = [self preferenceForKey:KEY_CONTAINING_OBJECT_UID group:OBJECT_STATUS_CACHE] ?: AILocalizedString(@"Bookmarks", nil);
   242 
   243 	if (adium.contactController.useContactListGroups) {
   244 		targetGroup = [NSSet setWithObject:[adium.contactController groupWithUID:savedGroupUID]];
   245 	} else {
   246 		targetGroup = [NSSet setWithObject:adium.contactController.contactList];
   247 	}
   248 
   249 	[adium.contactController moveContact:self fromGroups:self.groups intoGroups:targetGroup];
   250 }
   251 
   252 /*!
   253  * @brief Open our chat
   254  *
   255  * @return A chat for the bookmark
   256  *
   257  * This is called when we are double-clicked in the contact list.
   258  * Either find or create a chat appropriately, and activate it.
   259  */
   260 - (AIChat *)openChat
   261 {
   262 	AIChat *chat = [self openChatWithoutActivating];
   263 	
   264 	if(!chat.isOpen) {
   265 		[adium.interfaceController openChat:chat];
   266 	}
   267 	
   268 	[adium.interfaceController setActiveChat:chat];
   269 	
   270 	return chat;
   271 }
   272 
   273 /*!
   274  * @brief Open our chat without activating it
   275  *
   276  * This is called when joining automatically on connect, and within the
   277  * method which opens on double click.
   278  */
   279 - (AIChat *)openChatWithoutActivating
   280 {
   281 	AIChat *chat = [adium.chatController existingChatWithName:self.name
   282 					onAccount:self.account];
   283 	
   284 	if (![self chatIsOurs:chat]) {
   285 		//Open a new group chat (bookmarked chat)
   286 		chat = [adium.chatController chatWithName:self.name
   287 				identifier:NULL 
   288 				onAccount:self.account 
   289 				chatCreationInfo:self.chatCreationDictionary];
   290 	}
   291 	
   292 	return chat;
   293 }
   294 
   295 /*!
   296  * @brief A chat opened
   297  *
   298  * If this chat is our representation, set it up appropriately with our settings.
   299  */
   300 - (void)chatDidOpen:(NSNotification *)notification
   301 {
   302 	AIChat *chat = [notification object];
   303 
   304 	[self claimChatIfOurs:chat];
   305 }
   306 
   307 /*!
   308  * @brief Claim a chat
   309  *
   310  * Has no effect if the chat is not ours.
   311  *
   312  * Establishes any defaults we wish for our chats to have. Called when they are created.
   313  */
   314 - (void)claimChatIfOurs:(AIChat *)chat
   315 {
   316 	if ([self chatIsOurs:chat]) {
   317 		chat.displayName = self.displayName;
   318 	}
   319 }
   320 
   321 /*!
   322  * @brief Can this object be part of a metacontact?
   323  *
   324  * Bookmarks cannot join meta contacts.
   325  */
   326 - (BOOL)canJoinMetaContacts
   327 {
   328 	return NO;
   329 }
   330 
   331 /*!
   332  * @brief Is this chat ours?
   333  *
   334  * If the chat's name, account, and creation dictionary matches ours, it should be considered ours.
   335  */
   336 - (BOOL)chatIsOurs:(AIChat *)chat
   337 {
   338 	return (chat &&
   339 			[chat.name isEqualToString:[self.account.service normalizeChatName:self.name]] &&
   340 			chat.account == self.account &&
   341 			((!chat.chatCreationDictionary && !self.chatCreationDictionary) ||
   342 			 ([chat.chatCreationDictionary isEqualToDictionary:self.chatCreationDictionary])));
   343 }
   344 
   345 #pragma mark -
   346 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
   347 {
   348 	if ([keyPath isEqualToString:@"Online"] && object == self.account) {
   349 		// If an account is just initially signing on, a -setOnline:notify:silently will still broadcast an event for the contact.
   350 		// The initial delay an account (usually) sets is done after they're set as online, so these bookmarks would always fire.
   351 		// Thus, we have to use the secondary, silent notification so that the online gets propogated without the events.
   352 		[self setOnline:self.account.online notify:NotifyLater silently:YES];
   353 		[self notifyOfChangedPropertiesSilently:YES];
   354 		
   355 		if (self.account.online && [[self preferenceForKey:KEY_AUTO_JOIN group:GROUP_LIST_BOOKMARK] boolValue]) {
   356 			[self openChatWithoutActivating];
   357 		}
   358 	}
   359 }
   360 
   361 - (NSSet *)updateChat:(AIChat *)inChat keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
   362 {
   363 	if ([self chatIsOurs:inChat] && ([inModifiedKeys containsObject:KEY_UNVIEWED_CONTENT] || [inModifiedKeys containsObject:KEY_UNVIEWED_MENTION])) {
   364 		NSString *statusMessage = nil;
   365 		
   366 		if (inChat.unviewedMentionCount) {
   367 			// We contain mentions; display both this and the content count.
   368 			if (inChat.unviewedMentionCount > 1) {
   369 				statusMessage = [NSString stringWithFormat:AILocalizedString(@"%d mentions, %d messages", "Status message for a bookmark (>1 mention, >1 messages)"),
   370 								 inChat.unviewedMentionCount, inChat.unviewedContentCount];
   371 			} else if (inChat.unviewedContentCount > 1) {
   372 				statusMessage = [NSString stringWithFormat:AILocalizedString(@"1 mention, %d messages", "Status message for a bookmark (1 mention, >1 messages)"),
   373 								 inChat.unviewedContentCount];
   374 			} else {
   375 				statusMessage = AILocalizedString(@"1 mention, 1 message", "Status message for a bookmark (1 mention, 1 message)");
   376 			}
   377 		} else if (inChat.unviewedContentCount) {
   378 			// We don't contain mentions; display the content count.
   379 			if (inChat.unviewedContentCount > 1) {
   380 				statusMessage = [NSString stringWithFormat:AILocalizedString(@"%d messages", "Status message for a bookmark (>1 messages)"),
   381 								 inChat.unviewedContentCount];
   382 			} else {
   383 				statusMessage = AILocalizedString(@"1 message", "Status message for a bookmark (1 message)");
   384 			}
   385 		}
   386 
   387 		if (statusMessage) {
   388 			[self setStatusMessage:[NSAttributedString stringWithString:statusMessage] notify:NotifyNow];
   389 		} else {
   390 			[self setStatusMessage:nil notify:NotifyNow];
   391 		}
   392 	}
   393 	
   394 	return nil;
   395 }
   396 
   397 #pragma mark -
   398 - (NSString *)description
   399 {
   400 	return [NSString stringWithFormat:@"<%@:%x %@ - %@ on %@>",NSStringFromClass([self class]), self, self.formattedUID, [self chatCreationDictionary], self.account];
   401 }
   402 
   403 @end