Frameworks/Adium Framework/Source/AIListContact.m
author Zachary West <zacw@adium.im>
Fri Nov 27 15:50:57 2009 -0500 (2009-11-27)
changeset 2835 1e8c89f99dfe
parent 2834 e7db526620d1
child 3033 0b981d5c2c76
permissions -rw-r--r--
Simplify the "status message" contact/account property into one getter method. Correct error -1728 from AS on contact's status messages. Fixes #13460.

The totally undocumented -1728 error appears to be caused by runtime type disagreeing with event type. "status message" is apparently assumed to only have 1 code, so specifying one for contacts and one for accounts was confusing it when it was going to fetch it.
David@0
     1
/* 
David@0
     2
 * Adium is the legal property of its developers, whose names are listed in the copyright file included
David@0
     3
 * with this source distribution.
David@0
     4
 * 
David@0
     5
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
David@0
     6
 * General Public License as published by the Free Software Foundation; either version 2 of the License,
David@0
     7
 * or (at your option) any later version.
David@0
     8
 * 
David@0
     9
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
David@0
    10
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
David@0
    11
 * Public License for more details.
David@0
    12
 * 
David@0
    13
 * You should have received a copy of the GNU General Public License along with this program; if not,
David@0
    14
 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
David@0
    15
 */
David@0
    16
David@0
    17
#import <Adium/AIContactControllerProtocol.h>
David@0
    18
#import <Adium/AIChatControllerProtocol.h>
David@0
    19
#import <Adium/AIContentControllerProtocol.h>
David@0
    20
#import <Adium/AIStatusControllerProtocol.h>
David@0
    21
#import <Adium/AIContentMessage.h>
David@0
    22
#import <Adium/AIListContact.h>
David@1023
    23
#import <Adium/AIContactList.h>
David@548
    24
#import <Adium/AIListGroup.h>
David@0
    25
#import <Adium/AIMetaContact.h>
David@0
    26
#import <Adium/AIService.h>
David@0
    27
#import <Adium/AIUserIcons.h>
David@0
    28
#import <Adium/ESFileTransfer.h>
David@557
    29
#import <Adium/AIStatus.h>
David@0
    30
#import <Adium/AIHTMLDecoder.h>
David@0
    31
David@0
    32
#import <AIUtilities/AIMutableOwnerArray.h>
David@0
    33
#import <AIUtilities/AIMutableStringAdditions.h>
David@0
    34
catfish@2093
    35
#import <AvailabilityMacros.h>
David@0
    36
David@9
    37
#import "AIAddressBookController.h"
David@0
    38
David@0
    39
#define KEY_BASE_WRITING_DIRECTION		@"Base Writing Direction"
David@0
    40
#define PREF_GROUP_WRITING_DIRECTION	@"Writing Direction"
David@0
    41
David@0
    42
#define CONTACT_SIGN_ON_OR_OFF_PERSISTENCE_DELAY 15
David@0
    43
David@594
    44
@interface AIListObject ()
David@594
    45
- (void)setContainingObject:(AIListObject <AIContainingObject> *)inGroup;
David@594
    46
@end
David@594
    47
David@589
    48
@interface AIListContact ()
David@594
    49
@property (readwrite, nonatomic, assign) AIMetaContact *metaContact;
David@1381
    50
- (void) remoteGroupingChanged;
David@589
    51
@end
David@589
    52
David@0
    53
@implementation AIListContact
David@0
    54
David@0
    55
//Init with an account
David@0
    56
- (id)initWithUID:(NSString *)inUID account:(AIAccount *)inAccount service:(AIService *)inService
David@0
    57
{
David@449
    58
	if ((self = [self initWithUID:inUID service:inService])) {
David@449
    59
		account = [inAccount retain];
David@449
    60
	}
David@0
    61
	
David@108
    62
	return self;
David@0
    63
}
David@0
    64
David@0
    65
//Standard init
David@0
    66
- (id)initWithUID:(NSString *)inUID service:(AIService *)inService
David@0
    67
{
David@449
    68
	if ((self = [super initWithUID:inUID service:inService])) {
David@449
    69
		account = nil;
David@588
    70
		m_remoteGroupNames = [[NSMutableSet alloc] initWithCapacity:1];
David@449
    71
		internalUniqueObjectID = nil;
David@449
    72
	}
David@0
    73
David@0
    74
	return self;
David@0
    75
}
David@0
    76
David@0
    77
- (void)dealloc
David@0
    78
{
David@0
    79
	[account release]; account = nil;
David@588
    80
	[m_remoteGroupNames release]; m_remoteGroupNames = nil;
David@108
    81
	[internalUniqueObjectID release]; internalUniqueObjectID = nil;
David@0
    82
	
David@108
    83
	[super dealloc];
David@0
    84
}
David@0
    85
David@0
    86
//The account that owns this contact
David@108
    87
@synthesize account;
David@0
    88
David@0
    89
/*!
David@0
    90
 * @brief Set the UID of this contact
David@0
    91
 *
David@0
    92
 * The UID for an AIListContact generally shouldn't change... if the contact is actually renamed serverside, however,
David@0
    93
 * it is useful to change the UID without having to change everything else associated with it.
David@0
    94
 */
David@0
    95
- (void)setUID:(NSString *)inUID
David@0
    96
{
David@0
    97
	if (UID != inUID) {
David@0
    98
		[UID release]; UID = [inUID retain];
David@0
    99
		[internalObjectID release]; internalObjectID = nil;
David@0
   100
		[internalUniqueObjectID release]; internalUniqueObjectID = nil;		
David@0
   101
	}
David@0
   102
}
David@0
   103
David@0
   104
//An object ID generated by Adium that is completely unique to this contact.  This ID is generated from the service ID, 
David@0
   105
//UID, and account UID.  Adium will not allow multiple contacts with the same internalUniqueObjectID to be created.
David@0
   106
- (NSString *)internalUniqueObjectID
David@0
   107
{
David@0
   108
	if (!internalUniqueObjectID) {
David@427
   109
		internalUniqueObjectID = [[AIListContact internalUniqueObjectIDForService:self.service
David@108
   110
																		  account:self.account
David@427
   111
																			  UID:self.UID] retain];
David@0
   112
	}
David@0
   113
	return internalUniqueObjectID;
David@0
   114
}
David@0
   115
David@0
   116
//Generate a unique object ID for the passed object
David@0
   117
+ (NSString *)internalUniqueObjectIDForService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
David@0
   118
{
David@108
   119
	return [NSString stringWithFormat:@"%@.%@.%@", inService.serviceClass, inAccount.UID, inUID];
David@0
   120
}
David@0
   121
David@0
   122
David@0
   123
//Remote Grouping ------------------------------------------------------------------------------------------------------
David@0
   124
#pragma mark Remote Grouping
David@1381
   125
David@1381
   126
- (NSSet *) remoteGroupNames
David@0
   127
{
David@1381
   128
	return [[m_remoteGroupNames copy] autorelease];
David@1381
   129
}
David@1381
   130
David@1381
   131
- (void) setRemoteGroupNames:(NSSet *)inGroupNames
David@1381
   132
{
David@1381
   133
	NSParameterAssert(inGroupNames != nil);
David@1381
   134
	[m_remoteGroupNames setSet:inGroupNames];
David@1381
   135
	[self remoteGroupingChanged];
David@893
   136
}
David@705
   137
David@705
   138
- (void) addRemoteGroupName:(NSString *)inName
David@705
   139
{
David@705
   140
	NSParameterAssert(inName != nil);
David@1381
   141
	if ([m_remoteGroupNames containsObject:inName])
David@1381
   142
		return;
David@1381
   143
	
David@1381
   144
	[m_remoteGroupNames addObject:inName];
David@1381
   145
	[self remoteGroupingChanged];
David@0
   146
}
David@0
   147
David@705
   148
- (void) removeRemoteGroupName:(NSString *)inName
David@589
   149
{
David@705
   150
	NSParameterAssert(inName != nil);
David@1381
   151
	if (![m_remoteGroupNames containsObject:inName])
David@1381
   152
		return;
David@1381
   153
	
David@1381
   154
	[m_remoteGroupNames removeObject:inName];
David@1381
   155
	[self remoteGroupingChanged];
David@589
   156
}
David@589
   157
David@1381
   158
- (NSUInteger) countOfRemoteGroupNames
David@893
   159
{
David@1381
   160
	return m_remoteGroupNames.count;
David@0
   161
}
David@0
   162
David@898
   163
- (NSSet *)remoteGroups
David@898
   164
{
David@898
   165
	NSMutableSet *groups = [NSMutableSet set];
David@1381
   166
	for (NSString *remoteGroup in m_remoteGroupNames) {
David@898
   167
		[groups addObject:[adium.contactController groupWithUID:remoteGroup]];
David@898
   168
	}
David@898
   169
	return groups;
David@898
   170
}
David@898
   171
David@1381
   172
- (void) remoteGroupingChanged
David@1381
   173
{
David@1381
   174
	NSUInteger remoteGroupCount = m_remoteGroupNames.count;
David@1381
   175
	if (remoteGroupCount == 0)
David@1381
   176
		[AIUserIcons flushCacheForObject:self];
David@1381
   177
	
David@1381
   178
	[self restoreGrouping];
David@1381
   179
	
David@1381
   180
	if (self.isStranger != (remoteGroupCount == 0)) {
David@1597
   181
		[self setValue:[NSNumber numberWithBool:remoteGroupCount > 0]
David@1381
   182
		   forProperty:@"NotAStranger"
David@1381
   183
				notify:NotifyLater];
David@1381
   184
		[self notifyOfChangedPropertiesSilently:YES];
David@1381
   185
	}
David@1381
   186
}
David@1381
   187
David@1381
   188
//An AIListContact normally groups based on its remoteGroupNames (if it is not within a metaContact). 
David@0
   189
//Restore this grouping.
David@0
   190
- (void)restoreGrouping
David@0
   191
{
David@1381
   192
	if (self.metaContact) {
David@1381
   193
		[self.metaContact updateRemoteGroupingOfContact:self];		
David@1381
   194
		return;
David@1381
   195
	}
David@1381
   196
	
David@1023
   197
	//Create a group for the contact even if contact list groups aren't on,
David@1023
   198
	//otherwise requests for all the contact list groups will return nothing
David@1023
   199
	NSMutableSet *groups = [NSMutableSet set];
David@1381
   200
	for (NSString *remoteGroupName in m_remoteGroupNames) {
David@1023
   201
		AIListGroup *localGroup = [adium.contactController groupWithUID:remoteGroupName];
David@1023
   202
		
David@1023
   203
		if (!adium.contactController.useContactListGroups)
David@1023
   204
			localGroup = adium.contactController.contactList;
zacw@2191
   205
		else if (adium.contactController.useOfflineGroup && !self.online && !self.alwaysVisible)
David@1023
   206
			localGroup = adium.contactController.offlineGroup;
David@1023
   207
		
David@1023
   208
		[groups addObject:localGroup];
David@1023
   209
	}
zacw@2128
   210
	[adium.contactController _moveContactLocally:self fromGroups:self.groups toGroups:groups];
David@0
   211
}
David@0
   212
David@0
   213
#pragma mark Names
David@0
   214
/*!
David@0
   215
 * @brief Display name
David@0
   216
 *
David@0
   217
 * Display name, drawing first from any externally-provided display name, then falling back to 
David@0
   218
 * the formatted UID.
David@0
   219
 *
David@0
   220
 * A listContact attempts to have the same displayName as its containing contact (potentially its metaContact).
David@108
   221
 * If it is not in a metaContact, its display name is returned by super.displayName
David@0
   222
 */
David@0
   223
- (NSString *)displayName
David@0
   224
{
David@586
   225
	AIMetaContact	*meta = self.metaContact;
David@0
   226
David@586
   227
	NSString *displayName = meta ? meta.displayName : super.displayName;
David@0
   228
David@0
   229
	//If a display name was found, return it; otherwise, return the formattedUID  
David@108
   230
	return displayName ? displayName : self.formattedUID;
David@0
   231
}
David@0
   232
David@0
   233
/*!
David@0
   234
 * @brief Own display name
David@0
   235
 *
David@0
   236
 * Returns the display name without trying to account for a metaContact. Exists for use by AIMetaContact to avoid
David@0
   237
 * infinite recursion by its displayName calling our displayName calling its displayName and so on.
David@0
   238
 */
David@0
   239
- (NSString *)ownDisplayName
David@0
   240
{
David@108
   241
	return super.displayName;
David@0
   242
}
David@0
   243
David@0
   244
/*!
David@0
   245
 * @brief This contact's serverside display name, which is generally specificed by the contact remotely
David@0
   246
 *
David@0
   247
 * @result The serverside display name, or nil if none is set
David@0
   248
 */
David@0
   249
- (NSString *)serversideDisplayName
David@0
   250
{
David@0
   251
	return [self valueForProperty:@"Server Display Name"];	
David@0
   252
}
David@0
   253
David@0
   254
- (void)setServersideAlias:(NSString *)alias 
David@0
   255
				  silently:(BOOL)silent
David@0
   256
{
David@0
   257
	BOOL changes = NO;
David@0
   258
	BOOL displayNameChanges = NO;
David@0
   259
	
David@0
   260
	AILogWithSignature(@"%@ received alias %@", self, alias);
David@0
   261
	
David@0
   262
	//This is the server display name.  Set it as such.
David@0
   263
	if (![alias isEqualToString:[self valueForProperty:@"Server Display Name"]]) {
David@0
   264
		//Set the server display name property as the full display name
David@0
   265
		[self setValue:alias
David@0
   266
					   forProperty:@"Server Display Name"
David@0
   267
					   notify:NotifyLater];
David@0
   268
		
David@0
   269
		changes = YES;
David@0
   270
	}
David@0
   271
David@0
   272
	NSMutableString *cleanedAlias;
David@0
   273
	
David@0
   274
	//Remove any newlines, since we won't want them anywhere below
David@0
   275
	cleanedAlias = [alias mutableCopy];
David@0
   276
	[cleanedAlias convertNewlinesToSlashes];
David@0
   277
David@0
   278
	AIMutableOwnerArray	*displayNameArray = [self displayArrayForKey:@"Display Name"];
David@0
   279
	NSString			*oldDisplayName = [displayNameArray objectValue];
David@0
   280
	
David@0
   281
	//If the mutableOwnerArray's current value isn't identical to this alias, we should set it
David@108
   282
	if (![[displayNameArray objectWithOwner:self.account] isEqualToString:cleanedAlias]) {
David@0
   283
		[displayNameArray setObject:cleanedAlias
David@108
   284
						  withOwner:self.account
David@0
   285
					  priorityLevel:Low_Priority];
David@0
   286
		
David@0
   287
		//If this causes the object value to change, we need to request a manual update of the display name
David@0
   288
		if (oldDisplayName != [displayNameArray objectValue]) {
David@0
   289
			displayNameChanges = YES;
David@0
   290
		}
David@0
   291
	}
David@0
   292
	
David@0
   293
	if (changes) {
David@0
   294
		//Apply any changes
David@0
   295
		[self notifyOfChangedPropertiesSilently:silent];
David@0
   296
	}
David@0
   297
	
David@0
   298
	if (displayNameChanges) {
David@0
   299
		//Request an alias change
David@1109
   300
		[[NSNotificationCenter defaultCenter] postNotificationName:Contact_ApplyDisplayName
David@0
   301
												  object:self
David@0
   302
												userInfo:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES]
David@0
   303
																					 forKey:@"Notify"]];
David@0
   304
	}
David@0
   305
	
David@0
   306
	[cleanedAlias release];
David@0
   307
}
David@0
   308
David@0
   309
/*!
David@0
   310
 * @brief The way this object's name should be spoken
David@0
   311
 *
David@0
   312
 * If not found, the display name is returned.
David@0
   313
 */
David@0
   314
- (NSString *)phoneticName
David@0
   315
{
David@586
   316
	AIMetaContact *meta = self.metaContact;
David@0
   317
	NSString		*phoneticName;
David@0
   318
David@586
   319
	phoneticName = meta ? meta.phoneticName : super.phoneticName;;
David@0
   320
	
David@0
   321
	//If a display name was found, return it; otherwise, return the formattedUID
David@586
   322
	return phoneticName ? phoneticName : self.displayName;
David@0
   323
}
David@0
   324
David@0
   325
/*!
David@0
   326
 * @brief Own phonetic name
David@0
   327
 *
David@0
   328
 * Returns the phonetic name without trying to account for a metaContact. Exists for use by AIMetaContact to avoid
David@0
   329
 * infinite recursion by its phoneticName calling our phoneticName calling its phoneticName and so on.
David@0
   330
 */
David@0
   331
- (NSString *)ownPhoneticName
David@0
   332
{
David@108
   333
	return super.phoneticName;
David@0
   334
}
David@0
   335
David@0
   336
#pragma mark Properties
David@0
   337
David@0
   338
/*!
David@0
   339
 * @brief Set online
David@0
   340
 */
David@0
   341
- (void)setOnline:(BOOL)online notify:(NotifyTiming)notify silently:(BOOL)silent
David@0
   342
{
David@837
   343
	if (online != self.online) {
David@1597
   344
		[self setValue:[NSNumber numberWithBool:online]
catfish@2483
   345
					   forProperty:@"Online"
catfish@2483
   346
					   notify:notify];
David@0
   347
		
David@0
   348
		if (!silent) {
David@0
   349
			[self setValue:[NSNumber numberWithBool:YES] 
David@0
   350
						   forProperty:(online ? @"Signed On" : @"Signed Off")
David@0
   351
						   notify:notify];
David@0
   352
			[self setValue:nil 
David@0
   353
						   forProperty:(online ? @"Signed Off" : @"Signed On")
David@0
   354
						   notify:notify];
David@0
   355
			[self setValue:nil
David@0
   356
						   forProperty:(online ? @"Signed On" : @"Signed Off")
David@0
   357
					   afterDelay:CONTACT_SIGN_ON_OR_OFF_PERSISTENCE_DELAY];
David@0
   358
		}
David@0
   359
		
David@0
   360
		if (online) {
David@0
   361
			if (notify == NotifyNow) {
David@0
   362
				[self notifyOfChangedPropertiesSilently:silent];
David@0
   363
			}
David@0
   364
			
David@0
   365
		} else {
David@0
   366
			//Will always notify
David@1594
   367
			[self.account removePropertyValuesFromContact:self
David@0
   368
												  silently:silent];	
David@0
   369
		}
David@0
   370
	}
David@0
   371
}
David@0
   372
David@0
   373
/*!
David@0
   374
 * @brief Set the sign on date
David@0
   375
 */
David@0
   376
- (void)setSignonDate:(NSDate *)signonDate notify:(NotifyTiming)notify
David@0
   377
{
David@0
   378
	[self setValue:signonDate
David@0
   379
				   forProperty:@"Signon Date"
David@0
   380
				   notify:notify];
David@0
   381
}
David@0
   382
/*!
David@0
   383
 * @brief Date this contact signed on, if available
David@0
   384
 */
David@0
   385
- (NSDate *)signonDate
David@0
   386
{
David@0
   387
	return [self valueForProperty:@"Signon Date"];
David@0
   388
}
David@0
   389
David@0
   390
/*!
David@0
   391
 * @brief Set the idle state
David@0
   392
 *
David@0
   393
 * @param isIdle YES if the contact is idle
David@0
   394
 * @param idleSinceDate The date this contact went idle. Only relevant if isIdle is YES
David@0
   395
 * @param notify The NotifyTiming
David@0
   396
 */
David@0
   397
- (void)setIdle:(BOOL)isIdle sinceDate:(NSDate *)idleSinceDate notify:(NotifyTiming)notify
David@0
   398
{
David@0
   399
	if (isIdle) {
David@0
   400
		if (idleSinceDate) {
David@0
   401
			[self setValue:idleSinceDate
David@0
   402
						   forProperty:@"IdleSince"
David@0
   403
						   notify:NotifyLater];
David@0
   404
		} else {
David@0
   405
			//No idleSinceDate means we are Idle but don't know how long, so set to -1
David@0
   406
			[self setValue:[NSNumber numberWithInt:-1]
David@0
   407
						   forProperty:@"Idle"
David@0
   408
						   notify:NotifyLater];
David@0
   409
		}
David@0
   410
	} else {
David@0
   411
		[self setValue:nil
David@0
   412
					   forProperty:@"IdleSince"
David@0
   413
					   notify:NotifyLater];
David@0
   414
		[self setValue:nil
David@0
   415
					   forProperty:@"Idle"
David@0
   416
					   notify:NotifyLater];
David@0
   417
	}
David@0
   418
	
David@0
   419
	/* @"Idle", for a contact with an IdleSince date, will be changing every minute.  @"IsIdle" provides observers a way
David@0
   420
	* to perform an action when the contact becomes/comes back from idle, regardless of whether an IdleSince is available,
David@0
   421
	* without having to do that action every minute for other contacts.
David@0
   422
	*/
David@1597
   423
	[self setValue:[NSNumber numberWithBool:isIdle]
David@0
   424
				   forProperty:@"IsIdle"
David@0
   425
				   notify:NotifyLater];
David@0
   426
	
David@0
   427
	//Apply any changes
David@0
   428
	if (notify == NotifyNow) {
David@0
   429
		[self notifyOfChangedPropertiesSilently:NO];
David@0
   430
	}
David@0
   431
}
David@0
   432
David@0
   433
- (void)setServersideIconData:(NSData *)iconData notify:(NotifyTiming)notify
David@0
   434
{
David@0
   435
	[AIUserIcons setServersideIconData:iconData forObject:self notify:notify];
David@0
   436
}
David@0
   437
David@0
   438
/*!
David@0
   439
 * @brief Set the warning level
David@0
   440
 *
David@0
   441
 * @param warningLevel The warning level, an integer between 0 and 100
David@0
   442
 * @param notify The NotifyTiming
David@0
   443
 */
David@0
   444
- (void)setWarningLevel:(int)warningLevel notify:(NotifyTiming)notify
David@0
   445
{
David@1596
   446
	if (warningLevel != self.warningLevel) {
David@0
   447
		[self setValue:[NSNumber numberWithInt:warningLevel]
David@0
   448
					   forProperty:@"Warning"
David@0
   449
					   notify:notify];
David@0
   450
	}
David@0
   451
}
David@0
   452
David@0
   453
/*!
David@0
   454
 * @brief Warning level
David@0
   455
 *
David@0
   456
 * @result The warning level, an integer between 0 and 100
David@0
   457
 */
David@393
   458
- (NSInteger)warningLevel
David@0
   459
{
David@0
   460
	return [self integerValueForProperty:@"Warning"];
David@0
   461
}
David@0
   462
David@0
   463
/*!
David@0
   464
 * @brief Set the profile array
David@0
   465
 */
David@0
   466
- (void)setProfileArray:(NSArray *)array notify:(NotifyTiming)notify
David@0
   467
{
David@0
   468
	[self setValue:array
David@0
   469
	   forProperty:@"ProfileArray"
David@0
   470
			notify:notify];
David@0
   471
}
David@0
   472
David@0
   473
/*!
David@0
   474
 * @brief The profile array
David@0
   475
 */
David@0
   476
- (NSArray *)profileArray
David@0
   477
{
David@0
   478
	return [self valueForProperty:@"ProfileArray"];	
David@0
   479
}
David@0
   480
David@0
   481
/*!
David@0
   482
 * @brief Set the profile
David@0
   483
 */
David@0
   484
- (void)setProfile:(NSAttributedString *)profile notify:(NotifyTiming)notify
David@0
   485
{
David@0
   486
	[self setValue:profile
David@0
   487
				   forProperty:@"TextProfile" 
David@0
   488
				   notify:notify];
David@0
   489
}
David@0
   490
David@0
   491
/*!
David@0
   492
 * @brief Profile
David@0
   493
 */
David@0
   494
- (NSAttributedString *)profile
David@0
   495
{
David@0
   496
	return [self valueForProperty:@"TextProfile"];
David@0
   497
}
David@0
   498
David@0
   499
/*!
David@0
   500
 * @brief Is this contact a stranger?
David@0
   501
 * 
David@0
   502
 * A listContact is a stranger if it has a nil remoteGroupName
David@0
   503
 */
David@0
   504
- (BOOL)isStranger
David@0
   505
{
David@393
   506
	return ![self boolValueForProperty:@"NotAStranger"];
David@0
   507
}
David@0
   508
David@0
   509
/*!
David@0
   510
 * @brief If this contact intentionally on the contact list?
David@0
   511
 */
David@0
   512
- (BOOL)isIntentionallyNotAStranger
David@0
   513
{
David@108
   514
	return !self.isStranger && [self.account isContactIntentionallyListed:self];
David@0
   515
}
David@0
   516
David@0
   517
/*!
David@0
   518
 * @brief Is this object connected via a mobile device?
David@0
   519
 */
David@0
   520
- (BOOL)isMobile
David@0
   521
{
David@393
   522
	return [self boolValueForProperty:@"IsMobile"];
David@0
   523
}
David@0
   524
David@0
   525
/*!
David@0
   526
 * @brief Set if this contact is mobile
David@0
   527
 */
David@0
   528
- (void)setIsMobile:(BOOL)isMobile notify:(NotifyTiming)notify
David@0
   529
{
David@1596
   530
	[self setValue:[NSNumber numberWithBool:isMobile]
David@0
   531
				   forProperty:@"IsMobile"
David@0
   532
				   notify:notify];
David@0
   533
}
David@0
   534
David@0
   535
/*!
David@0
   536
 * @brief Is this contact blocked?
David@0
   537
 *
David@0
   538
 * @result A boolean indicating if the contact is blocked or not
David@0
   539
 */
David@0
   540
- (BOOL)isBlocked
David@0
   541
{
David@393
   542
	return [self boolValueForProperty:KEY_IS_BLOCKED];
David@0
   543
}
David@0
   544
David@0
   545
- (void)setIsBlocked:(BOOL)yesOrNo updateList:(BOOL)addToPrivacyLists
David@0
   546
{
David@0
   547
	[self setIsOnPrivacyList:yesOrNo updateList:addToPrivacyLists privacyType:AIPrivacyTypeDeny];
David@0
   548
}
David@0
   549
David@0
   550
- (void)setIsAllowed:(BOOL)yesOrNo updateList:(BOOL)addToPrivacyLists
David@0
   551
{
David@0
   552
	[self setIsOnPrivacyList:yesOrNo updateList:addToPrivacyLists privacyType:AIPrivacyTypePermit];
David@0
   553
}
David@0
   554
David@0
   555
/*!
David@0
   556
 * @brief Set if this contact is on the privacy list
David@0
   557
 */
David@1596
   558
- (void)setIsOnPrivacyList:(BOOL)shouldBeBlocked updateList:(BOOL)addToPrivacyLists privacyType:(AIPrivacyType)privType
David@0
   559
{
David@1596
   560
	if (addToPrivacyLists) {		//caller of this method wants to actually block or unblock the contact, rather than just update the property
David@0
   561
		
David@1596
   562
		if (![self.account conformsToProtocol:@protocol(AIAccount_Privacy)]) {
David@1596
   563
			NSLog(@"Privacy is not supported on contacts for the account: %@", self.account);
David@1596
   564
			return;
David@0
   565
		}
David@1596
   566
		
David@1596
   567
		id<AIAccount_Privacy> contactAccount = (id<AIAccount_Privacy>)self.account;
David@1596
   568
		
David@1596
   569
		BOOL isBlocked = [[contactAccount listObjectsOnPrivacyList:privType] containsObject:self];
David@1596
   570
		
David@1596
   571
		if (shouldBeBlocked == isBlocked)
David@1596
   572
			return;
David@1596
   573
		
David@1596
   574
		BOOL	result = NO;
David@1596
   575
David@1596
   576
		if (shouldBeBlocked)
David@1596
   577
			result = [contactAccount addListObject:self toPrivacyList:privType];
David@1596
   578
		else
David@1596
   579
			result = [contactAccount removeListObject:self fromPrivacyList:privType];
David@1596
   580
		
David@1596
   581
		//Don't update the property if we didn't change anything
David@1596
   582
		if (!result)
David@1596
   583
			return;
David@1596
   584
	} 
David@1596
   585
David@1596
   586
	[self setValue:[NSNumber numberWithBool:((privType == AIPrivacyTypeDeny) == shouldBeBlocked)]
David@1596
   587
				   forProperty:KEY_IS_BLOCKED
David@1596
   588
				   notify:NotifyNow];
David@0
   589
}
David@0
   590
David@738
   591
- (AIEncryptedChatPreference)encryptedChatPreferences {
David@738
   592
	AIEncryptedChatPreference	pref = EncryptedChat_Default;
David@738
   593
	
David@738
   594
	//Get the contact's preference (or metacontact's)
David@1596
   595
	NSNumber *prefNumber = [self.parentContact preferenceForKey:KEY_ENCRYPTED_CHAT_PREFERENCE group:GROUP_ENCRYPTION];
David@738
   596
	
David@738
   597
	//If that turned up nothing, check all the groups it's in
zacw@2181
   598
	if (!prefNumber || [prefNumber integerValue] == EncryptedChat_Default) {
David@738
   599
		for (AIListGroup *group in self.parentContact.groups)
David@738
   600
		{
David@740
   601
			if ((prefNumber = [group preferenceForKey:KEY_ENCRYPTED_CHAT_PREFERENCE group:GROUP_ENCRYPTION]))
David@1596
   602
				break;
David@738
   603
		}	
David@738
   604
	}
David@738
   605
	
David@738
   606
	//If that turned up nothing, check global prefs
David@1596
   607
	if (!prefNumber)
David@738
   608
		prefNumber = [adium.preferenceController preferenceForKey:KEY_ENCRYPTED_CHAT_PREFERENCE group:GROUP_ENCRYPTION];
David@738
   609
	
David@738
   610
	//If no contact preference or the contact is set to use the default, use the account preference
David@738
   611
	if (!prefNumber || ([prefNumber integerValue] == EncryptedChat_Default)) {
David@738
   612
		prefNumber = [self.account preferenceForKey:KEY_ENCRYPTED_CHAT_PREFERENCE
David@738
   613
					  group:GROUP_ENCRYPTION];		
David@738
   614
	}
David@738
   615
	
David@738
   616
	if (prefNumber)
David@738
   617
		pref = [prefNumber integerValue];
David@738
   618
	
David@738
   619
	return pref;
David@738
   620
}
David@738
   621
zacw@2191
   622
- (void)setAlwaysVisible:(BOOL)inVisible
zacw@2191
   623
{
zacw@2191
   624
	[super setAlwaysVisible:inVisible];
zacw@2191
   625
	
zacw@2191
   626
	[self restoreGrouping];
zacw@2191
   627
}
zacw@2191
   628
zacw@2180
   629
- (BOOL)alwaysVisible
zacw@2180
   630
{
zacw@2180
   631
	if (self.metaContact) {
zacw@2180
   632
		return self.metaContact.alwaysVisible;
zacw@2180
   633
	}
zacw@2180
   634
	
zacw@2180
   635
	return [super alwaysVisible];
zacw@2180
   636
}
zacw@2180
   637
David@0
   638
#pragma mark Status
David@0
   639
David@0
   640
/*!
David@0
   641
* @brief Determine the status message to be displayed in the contact list
David@0
   642
 *
David@0
   643
 * Look at the contact's status message.
David@0
   644
 * Failing that, look for a statusName, which might be something like "DND" or "Free for Chat"
David@0
   645
 * and look up the localized description of it.
David@0
   646
 */
David@0
   647
- (NSAttributedString *)contactListStatusMessage
David@0
   648
{
David@837
   649
	NSAttributedString	*contactListStatusMessage = self.statusMessage;
David@0
   650
David@0
   651
	if (!contactListStatusMessage) {
David@1596
   652
		NSString			*statusName = self.statusName;
David@0
   653
		
David@1596
   654
		if (statusName) {
David@1596
   655
			NSString *descriptionOfStatus = [adium.statusController localizedDescriptionForStatusName:statusName
David@1596
   656
											 statusType:self.statusType];
David@0
   657
			
David@1596
   658
			if (descriptionOfStatus)
David@0
   659
				contactListStatusMessage = [[[NSAttributedString alloc] initWithString:descriptionOfStatus] autorelease];			
David@0
   660
		}
David@0
   661
	}
David@0
   662
David@0
   663
	return contactListStatusMessage;	
David@0
   664
}
David@0
   665
David@0
   666
/*!
David@0
   667
 * @brief Are sounds for this contact muted?
David@0
   668
 */
David@0
   669
- (BOOL)soundsAreMuted
David@0
   670
{
David@837
   671
	return [self.account.statusState mutesSound];
David@0
   672
}
David@0
   673
David@0
   674
#pragma mark Parents
David@0
   675
David@0
   676
/*!
David@0
   677
 * @brief This object's parent AIListContact
David@0
   678
 *
David@0
   679
 * The parent AIListContact is the appropriate place to apply preferences specific to this contact so that such
David@0
   680
 * preferences are also applied to other AIListContacts in the same meta contact, if necessary.
David@0
   681
 *
David@0
   682
 * @result Either this contact or some more-encompassing contact which ultimately contains it.
David@0
   683
 */
David@586
   684
- (AIListContact *)parentContact
David@586
   685
{
David@1596
   686
	return self.metaContact ?: self;
David@586
   687
}
David@0
   688
David@0
   689
- (BOOL)containsObject:(AIListObject*)object
David@0
   690
{
David@0
   691
    return NO;
David@0
   692
}
David@0
   693
David@594
   694
- (NSSet *) containingObjects {
David@594
   695
	if (metaContact)
David@600
   696
		return [NSSet setWithObject:metaContact];
David@594
   697
	return super.containingObjects;
David@586
   698
}
David@586
   699
David@0
   700
/*!
David@0
   701
 * @brief Can this object be part of a metacontact?
David@0
   702
 */
David@0
   703
- (BOOL)canJoinMetaContacts
David@0
   704
{
David@0
   705
	return YES;
David@0
   706
}
David@0
   707
David@586
   708
- (AIMetaContact *)metaContact
David@586
   709
{
David@594
   710
	return metaContact;
David@594
   711
}
David@594
   712
David@594
   713
- (void) setMetaContact:(AIMetaContact *)meta
David@594
   714
{
David@705
   715
	metaContact = meta;
David@894
   716
	[m_groups removeAllObjects];
David@586
   717
}
David@586
   718
David@893
   719
- (BOOL) existsServerside
David@893
   720
{
David@893
   721
	return YES;
David@893
   722
}
David@893
   723
zacw@2131
   724
- (void)removeFromGroup:(AIListObject <AIContainingObject> *)group
David@853
   725
{
zacw@2131
   726
	if (self.account.online) {
zacw@2322
   727
		if (group == adium.contactController.contactList
zacw@2322
   728
			|| group == adium.contactController.offlineGroup) {
zacw@2307
   729
			[self.account removeContacts:[NSArray arrayWithObject:self]
zacw@2307
   730
							  fromGroups:[self.remoteGroups allObjects]];	
zacw@2307
   731
		} else {			
zacw@2307
   732
			[self.account removeContacts:[NSArray arrayWithObject:self]
zacw@2307
   733
							  fromGroups:[NSArray arrayWithObject:group]];	
zacw@2307
   734
		}
zacw@2131
   735
	}
David@853
   736
}
David@853
   737
David@0
   738
#pragma mark Equality
David@0
   739
/*
David@0
   740
- (BOOL)isEqual:(id)anObject
David@0
   741
{
David@0
   742
	return ([anObject isMemberOfClass:[self class]] &&
David@0
   743
			[[(AIListContact *)anObject internalUniqueObjectID] isEqualToString:[self internalUniqueObjectID]]);
David@0
   744
}
David@0
   745
*/
David@0
   746
//AppleScript ----------------------------------------------------------------------------------------------------------
David@0
   747
#pragma mark AppleScript
David@0
   748
David@0
   749
- (id)sendScriptCommand:(NSScriptCommand *)command {
David@0
   750
	NSDictionary	*evaluatedArguments = [command evaluatedArguments];
David@108
   751
	NSString			*message = [evaluatedArguments objectForKey:@"message"];
David@0
   752
	AIAccount		*targetAccount = [evaluatedArguments objectForKey:@"account"];
David@108
   753
	NSString			*filePath = [evaluatedArguments objectForKey:@"filePath"];
David@0
   754
	
David@899
   755
	AIListContact   *targetMessagingContact = self;
David@0
   756
	AIListContact   *targetFileTransferContact = nil;
David@0
   757
David@0
   758
	if (targetAccount) {
David@899
   759
		if (self.account != account)
David@902
   760
			targetMessagingContact = [adium.contactController contactWithService:self.service account:account UID:self.UID];
David@899
   761
David@0
   762
		targetFileTransferContact = targetMessagingContact;
David@0
   763
	}
David@0
   764
	
David@0
   765
	//Send any message we were told to send
David@0
   766
	if (message && [message length]) {
David@0
   767
		AIChat			*chat;
David@0
   768
		BOOL			autoreply = [[evaluatedArguments objectForKey:@"autoreply"] boolValue];
David@0
   769
		
David@0
   770
		//Make sure we know where we are sending the message - if we don't have a target yet, find the best contact for
David@0
   771
		//sending CONTENT_MESSAGE_TYPE.
David@0
   772
		if (!targetMessagingContact) {
David@0
   773
			//Get the target contact.  This could be the same contact, an identical contact on another account, 
David@0
   774
			//or a subcontact (if we're talking about a metaContact, for example)
David@89
   775
			targetMessagingContact = [adium.contactController preferredContactForContentType:CONTENT_MESSAGE_TYPE
David@0
   776
																				forListContact:self];
David@108
   777
			targetAccount = targetMessagingContact.account;	
David@0
   778
		}
David@0
   779
		
David@0
   780
		if (targetMessagingContact) {
David@95
   781
			chat = [adium.chatController openChatWithContact:targetMessagingContact
David@0
   782
											onPreferredAccount:NO];
David@0
   783
			
David@0
   784
			//Take the string and turn it into an attributed string (in case we were passed HTML)
David@0
   785
			NSAttributedString  *attributedMessage = [AIHTMLDecoder decodeHTML:message];
David@0
   786
			AIContentMessage	*messageContent;
David@0
   787
			messageContent = [AIContentMessage messageInChat:chat
David@0
   788
												  withSource:targetAccount
David@0
   789
												 destination:targetMessagingContact
David@0
   790
														date:nil
David@0
   791
													 message:attributedMessage
David@0
   792
												   autoreply:autoreply];
David@0
   793
			
David@95
   794
			[adium.contentController sendContentObject:messageContent];
David@0
   795
		} else {
David@0
   796
			AILogWithSignature(@"No contact available to receive a message to %@", self);
David@0
   797
		}
David@0
   798
	}
David@0
   799
	
David@0
   800
	//Send any file we were told to send
David@0
   801
	if (filePath && [filePath length]) {
David@0
   802
		//Make sure we know where we are sending the file - if we don't have a target yet, find the best contact for
David@0
   803
		//sending CONTENT_FILE_TRANSFER_TYPE.
David@0
   804
		if (!targetFileTransferContact) {
David@0
   805
			//Get the target contact.  This could be the same contact, an identical contact on another account, 
David@0
   806
			//or a subcontact (if we're talking about a metaContact, for example)
David@89
   807
			targetFileTransferContact = [adium.contactController preferredContactForContentType:CONTENT_FILE_TRANSFER_TYPE
David@0
   808
																				   forListContact:self];
David@0
   809
		}
David@0
   810
		
David@0
   811
		if (targetFileTransferContact) {
David@100
   812
			[adium.fileTransferController sendFile:filePath toListContact:targetFileTransferContact];
David@0
   813
		} else {
David@0
   814
			AILogWithSignature(@"No contact available to receive files to %@", self);
David@0
   815
			NSBeep();
David@0
   816
		}
David@0
   817
	}
David@0
   818
		
David@0
   819
	return nil;
David@0
   820
}
David@0
   821
David@0
   822
//Writing Direction ----------------------------------------------------------------------------------------------------------
David@0
   823
#pragma mark Writing Direction
David@0
   824
David@0
   825
- (NSWritingDirection)defaultBaseWritingDirection
David@0
   826
{
David@0
   827
	static NSWritingDirection defaultBaseWritingDirection;
David@0
   828
	static BOOL determinedDefaultBaseWritingDirection = NO;
David@0
   829
	
David@0
   830
	if (!determinedDefaultBaseWritingDirection) {
David@0
   831
		/* Use  the default writing direction of the language of the user's locale (and not the language
David@0
   832
		 * of the active localization). By that, we assume most users are mostly talking to their local friends.
David@0
   833
		 */
David@0
   834
		NSString	*lang = [[NSLocale currentLocale] objectForKey:NSLocaleLanguageCode];		
David@0
   835
		defaultBaseWritingDirection = [NSParagraphStyle defaultWritingDirectionForLanguage:lang];
David@0
   836
		determinedDefaultBaseWritingDirection = YES;
David@0
   837
	}
David@0
   838
	
David@0
   839
	return defaultBaseWritingDirection;
David@0
   840
}
David@0
   841
David@0
   842
- (NSWritingDirection)baseWritingDirection {
David@740
   843
	NSNumber	*dir = [self preferenceForKey:KEY_BASE_WRITING_DIRECTION group:PREF_GROUP_WRITING_DIRECTION];
David@0
   844
David@0
   845
	return (dir ? [dir intValue] : [self defaultBaseWritingDirection]);
David@0
   846
}
David@0
   847
David@0
   848
- (void)setBaseWritingDirection:(NSWritingDirection)direction {
David@0
   849
	[self setPreference:[NSNumber numberWithInt:direction]
David@0
   850
				 forKey:KEY_BASE_WRITING_DIRECTION
David@0
   851
				  group:PREF_GROUP_WRITING_DIRECTION];
David@0
   852
}
David@0
   853
David@0
   854
#pragma mark Address Book
David@0
   855
- (ABPerson *)addressBookPerson
David@0
   856
{
David@108
   857
	return [AIAddressBookController personForListObject:self.parentContact];	
David@0
   858
}
David@0
   859
- (void)setAddressBookPerson:(ABPerson *)inPerson
David@0
   860
{
David@108
   861
	[self.parentContact setPreference:[inPerson uniqueId]
David@0
   862
								 forKey:KEY_AB_UNIQUE_ID
David@0
   863
								  group:PREF_GROUP_ADDRESSBOOK];
David@0
   864
}
David@0
   865
David@0
   866
#pragma mark Applescript
David@0
   867
David@0
   868
- (NSScriptObjectSpecifier *)objectSpecifier
David@0
   869
{
David@1596
   870
	NSScriptObjectSpecifier *containerRef = self.account.objectSpecifier;
David@0
   871
	return [[[NSNameSpecifier allocWithZone:[self zone]]
David@0
   872
		initWithContainerClassDescription:[containerRef keyClassDescription]
David@427
   873
		containerSpecifier:containerRef key:@"contacts" name:self.UID] autorelease];
David@0
   874
}
David@0
   875
zacw@2835
   876
zacw@2834
   877
- (NSArray *)groupsAsArray
zacw@2834
   878
{
zacw@2834
   879
	return self.groups.allObjects;
zacw@2834
   880
}
zacw@2834
   881
David@0
   882
- (BOOL)scriptingBlocked
David@0
   883
{
David@0
   884
	return [self isBlocked];
David@0
   885
}
David@0
   886
- (void)setScriptingBlocked:(BOOL)b
David@0
   887
{
David@0
   888
	[self setIsBlocked:b updateList:YES];
David@0
   889
}
David@0
   890
David@894
   891
@dynamic containingObject;
David@894
   892
David@0
   893
@end