/* Copyright 2009 Palm, Inc.  All rights reserved. */

var ChatviewAssistant = Class.create({
	initialize: function(chatThreadId, params) {
		Mojo.Log.info("++++++ChatviewAssistant::initialize");
		Mojo.Log.info("ChatView %j", params)
		this.appController = Mojo.Controller.getAppController();
		this.Messaging = this.appController.Messaging;
		this.chatData = {};
		this.chatThreadId = chatThreadId;
		this.focusWindow = false;
		this.launchToAddress;
		this.clearListBadgeFn;
		if (params) {
			if (params.focusWindow) 
				this.focusWindow = true;
			if (params.contactPointId) 
				this.launchToContactPointId = params.contactPointId;
			if (params.clearListBadgeFn) 
				this.clearListBadgeFn = params.clearListBadgeFn;
			if (params.address) 
				this.launchToAddress = params.address;
			if (params.selectIMTransport) 
				this.selectIMTransport = true;
				
		}
		this.initialLoadComplete = false;
		this.listReset = false;
		this.hasSetChatToReadAndSetDraftText = false;
		this.isWindowFocused = true;
		this.isScreenOn = true;
		this.isScreenLocked = false;
		this.isSceneActive = false;
		this.bottomBeforeDeactivate = null;
		this.fixScrollPositionOnActivate = false;
		this.avatarHash = {}; // key = chatAddress, value = /path/to/avatar
		this.contactAvatar;
		this.chatDisplayName;
		this.transports = [];
		this.invitations = [];
		this.selectedTransport = {};
		this.IMAddressSubscriptions = [];
		this.attachment;
		this.messageTextElement;
		this.hasLoadedChatList = false;
		this.attemptedToFixChatWithBadPersonId = false;
		this.chatListSubscriberId = null;
		this.MAX_BOTTOM_HEIGHT_FOR_SNAP = 300;
		this.chatHasTransientStatusMessage = false;
		this.TRANSIENT_MESSAGES = {};
		this.TRANSIENT_MESSAGES[this.Messaging.Availability.AVAILABLE] = {
			template: new Template($L("#{chatAddress} is available")),
			isTransientFn: ChatFlags.isTransientAvailable
		};
		this.TRANSIENT_MESSAGES[this.Messaging.Availability.BUSY] = {
			template: new Template($L("#{chatAddress} is busy")),
			isTransientFn: ChatFlags.isTransientBusy
		};
		this.TRANSIENT_MESSAGES[this.Messaging.Availability.OFFLINE] = {
			template: new Template($L("#{chatAddress} is offline")),
			isTransientFn: ChatFlags.isTransientOffline
		};
		this.messageTemplatePath = 'chatview/message/';
		
		this.appMenuAddPictureItem = {
			label: $L('Attach file...'),
			command: 'addPicture',
			disabled: false
		};

		this.appMenuBlockUsernameItem = {
			label: $L('Block this person'),
			command: 'blockUsername'
		};

		this.appMenuAttrs = {
			omitDefaultItems: true
		};

		// Add "Copy All" to the standard edit menu.
		var chatEditItems = {
			label: $L('Edit'),
			items: [
				Mojo.Menu.cutItem,
				Mojo.Menu.copyItem,
				{label: $L('Copy All'), command: 'copyall'},
				Mojo.Menu.pasteItem
			]
		};

		this.appMenuModel = {
			visible: true,
			label: $L('Chat view menu'),
			items: [
				chatEditItems,
				this.appMenuAddPictureItem,
				Mojo.Menu.helpItem
			]
		};
		
		this.newMessageFieldLoseFocus = function(event) {
			if (this.messageTextElement !== undefined) {
				this.messageTextElement.focus();
			}
		}.bind(this);
		
		this.listLoadedTimerFn = this._debouncedListLoadedHelper.bind(this);
		this.mmsImageOnLoad = this.mmsImageOnLoad.bind(this);
		this.mmsImageOnError = this.mmsImageOnError.bind(this);
		
		this.handleWindowResize = this.handleWindowResize.bindAsEventListener(this);
		this.handleMessageTap = this.handleMessageTap.bindAsEventListener(this);
		this.handleMessageDelete = this.handleMessageDelete.bindAsEventListener(this);
		this.renderTransportPickerPopup = this.renderTransportPickerPopup.bindAsEventListener(this);
		this.considerForSend = this.considerForSend.bind(this);
		this.attachFilePicker = this.attachFilePicker.bind(this);
		this.handleCancelAttachment = this.resetTextBox.bind(this, false);
		this.handleTextAreaChange = this.handleTextAreaChange.bind(this);
		this.handleTextAreaKeyDown = this.handleTextAreaKeyDown.bind(this);
		this.handleTextAreaKeyUp = this.handleTextAreaKeyUp.bind(this);
		this.handleScreenStateChange = this.handleScreenStateChange.bind(this);
		this.handleStageFocus = this.handleStageFocus.bind(this);
		this.handleStageBlur = this.handleStageBlur.bind(this);

		this.appController.notificationAssistant.clearNotificationsForChat(this.chatThreadId);
	},
	
	setup: function() {
		Mojo.Log.info("++++++ChatviewAssistant::setup");
		this.controller.setupWidget(Mojo.Menu.appMenu, this.appMenuAttrs, this.appMenuModel);
		
		var messageTextWidgetAttributes = {
			modelProperty: 'value',
			hintText: $L('Enter message here...'),
			multiline: true,
			focus: false,
			enterSubmits: true,
			textFieldName: "messageTextElement",
			className: " ",
			changeOnKeyPress: true
		};
		this.messageTextWidgetModel = {};
		this.controller.setupWidget('messageTextWidget', messageTextWidgetAttributes, this.messageTextWidgetModel);
		
		this.chatListAttr = {
			itemTemplate: 'chatview/chatList-row',
			listTemplate: 'chatview/chatList-container',
			itemsCallback: this.chatListItemsCallback.bind(this),
			dividerTemplate: 'chatview/separator',
			dividerFunction: this.chatListDividerFunction.bind(this),
			onItemRendered: this.itemRendered.bind(this),
			swipeToDelete: true,
			uniquenessProperty: 'messageId'
		};
		this.chatListModel = {}; // This is used to reset the list by calling modelChanged
		this.controller.setupWidget('chatlist', this.chatListAttr, this.chatListModel);
	},
	
	ready: function() {
		//Mojo.Timing.resume("ChatviewAssistant#ready");
		Mojo.Log.info("++++++ChatviewAssistant::ready");
		this.chatListWidget = this.controller.get('chatlist');
		this.actionButton = this.controller.get('ActionButton');
		this.sendButton = this.controller.get('SendButton');
		this.attachButton = this.controller.get('AttachButton');
		this.transportPicker = this.controller.get('msg-transport-picker');
		this.currentTransportDownArrow = this.controller.get('currentTransportDownArrow');
		this.cancelAttachment = this.controller.get('cancelAttachment');
		this.chatViewHeaderClickTarget = this.controller.get('chatViewHeaderClickTarget');
		this.chatViewRecipientName = this.controller.get('chatViewRecipientName');
		
		this.messageTextWidget = this.controller.get('messageTextWidget');
		this.messageTextElement = this.messageTextWidget.querySelector('[name=messageTextElement]');
		
		//this.chatListWidget.observe("click", this.onClickHandler);
		
		this.chatListWidget.observe(Mojo.Event.listTap, this.handleMessageTap);
		this.chatListWidget.observe(Mojo.Event.listDelete, this.handleMessageDelete);
		this.sendButton.observe(Mojo.Event.tap, this.considerForSend);
		this.attachButton.observe(Mojo.Event.tap, this.attachFilePicker);
		
		this.cancelAttachment.observe(Mojo.Event.tap, this.handleCancelAttachment);
	
		this.messageTextElement.observe('keydown', this.handleTextAreaKeyDown);
		this.messageTextElement.observe('keyup', this.handleTextAreaKeyUp);
		this.messageTextWidget.observe(Mojo.Event.propertyChange, this.handleTextAreaChange);

		// observe keydown/keyup on the text field WIDGET to handle height changes due to text forcing it to resize
		this.fixListScrollPositionOnLoad = this._fixListScrollPositionOnLoad.bind(this, this.messageTextWidget, this.chatListWidget);
		this.handleTextFieldResize = this._fixListScrollPositionWhenTextFieldResize.bind(this, this.messageTextWidget, this.chatListWidget);
		this.messageTextWidget.observe('keydown', this.handleTextFieldResize);
		this.messageTextWidget.observe('keyup', this.handleTextFieldResize);
		
		var charCounterParams = {
			segmentCounter: {
				segmentCountContainer: this.controller.get('segmentCounterContainer'),
				segmentCountElement: this.controller.get('segmentCounter'),
				setTextFieldValueFn: this.setTextFieldValue.bind(this)
			}
		};

		this.characterCounter = new CharacterCounter(this.controller, this.Messaging.messagingPrefs.platformType, this.messageTextElement, charCounterParams);
		MessagingMojoService.getChatDetails(this.controller, this.chatThreadId, this.renderChatViewHeader.bind(this));
		this.controller.window.addEventListener('resize', this.handleWindowResize, false);
		
		this.controller.document.addEventListener(Mojo.Event.stageActivate, this.handleStageFocus, false);
		this.controller.document.addEventListener(Mojo.Event.stageDeactivate, this.handleStageBlur, false);
		
		this.Messaging.DisplayState.observe(this.handleScreenStateChange);
		MessagingMojoService.getLockStatus(this.controller, this.handleLockScreenStateChange.bind(this));
		
		this.updateChatList = this.updateChatList.bind(this, this.chatListWidget);
		//Mojo.Timing.pause("ChatviewAssistant#ready");
	},
	
	handleScreenStateChange: function(isScreenOn) {
		Mojo.Log.info("[CV] ***** handle screen state change:", isScreenOn);
		this.isScreenOn = isScreenOn;
		this.handleClearNotifications();
	},
	
	handleLockScreenStateChange: function(result) {
		Mojo.Log.info("[CV] ****** getLockStatus: %j", result);
		if (result && result.locked) {
			this.isScreenLocked = true;
		} else {
			this.isScreenLocked = false;
		}
		this.handleClearNotifications();
	},
	
	handleClearNotifications: function() {
		if (this.isWindowFocused && this.isSceneActive && this.isScreenOn && !this.isScreenLocked) {
			Mojo.Log.info("[CV] ****** handleClearNotifications ---- calling handleStageFocus()");
			this.handleStageFocus();
		}
	},

	cleanup: function() {
		this.chatViewHeaderClickTarget.stopObserving(Mojo.Event.tap, this.handleHeaderTap);
		this.chatListWidget.stopObserving(Mojo.Event.listTap, this.handleMessageTap);
		this.sendButton.stopObserving(Mojo.Event.tap, this.considerForSend);
		this.attachButton.stopObserving(Mojo.Event.tap, this.attachFilePicker);
		this.transportPicker.stopObserving(Mojo.Event.tap, this.renderTransportPickerPopup);
		this.cancelAttachment.stopObserving(Mojo.Event.tap, this.handleCancelAttachment);
		this.messageTextWidget.stopObserving('keydown', this.handleTextFieldResize);
		this.messageTextWidget.stopObserving('keyup', this.handleTextFieldResize);
		this.messageTextWidget.stopObserving(Mojo.Event.propertyChange, this.handleTextAreaChange);
		this.messageTextElement.stopObserving('keydown', this.handleTextAreaKeyDown);
		this.messageTextElement.stopObserving('keyup', this.handleTextAreaKeyUp);
		this.Messaging.DisplayState.stopObserving(this.handleScreenStateChange);
		this.controller.document.removeEventListener(Mojo.Event.stageActivate, this.handleStageFocus, false);
		this.controller.document.removeEventListener(Mojo.Event.stageDeactivate, this.handleStageBlur, false);
		this.controller.window.removeEventListener('resize', this.handleWindowResize, false);
		this.characterCounter.destroy();
		if (!this.hasSetChatToReadAndSetDraftText) {
			this.setChatToReadAndSetDraftText();
		}
		
		if (this.clearListBadgeFn) {
			this.clearListBadgeFn();
		}
	},
	
	setChatToReadAndSetDraftText: function() {
		this.hasSetChatToReadAndSetDraftText = true;
		MessagingMojoService.setChatToReadAndSetDraftText(this.controller, this.chatThreadId, this.messageTextElement.value, this.chatHasTransientStatusMessage);
	},
	
	aboutToActivate: function(afterAboutToActivateFn) {
		this.afterAboutToActivateFn = afterAboutToActivateFn;
		// We need to revealBottom() when a scene is popped off from on top of us and we received a 
		// new message while the chat was hidden
		// this.fixScrollPositionOnActivate will be false when the scene initially loads
		if (this.fixScrollPositionOnActivate && this.bottomBeforeDeactivate < this.MAX_BOTTOM_HEIGHT_FOR_SNAP) {
			this.revealBottomHack();
			this.fixScrollPositionOnActivate = false;
		}
		
		if (this.hasInitialBottomWindow && this.afterAboutToActivateFn) {
			this.afterAboutToActivateFn();
		}
		
	},
	
	activate: function() {
		this.isSceneActive = true;
		this.bottomBeforeDeactivate = null;
		// clear any dashboard notifications for this chat if they exist
		this.messageTextElement.observe('blur', this.newMessageFieldLoseFocus);
		this.newMessageFieldLoseFocus();
		if (this.focusWindow) {
			this.focusWindow = false;
			this.controller.stageController.activate();
		}
		this.handleClearNotifications();
	},
	
	deactivate: function() {
		this.isSceneActive = false;
		this.bottomBeforeDeactivate = this.getCurrentBottom();
		this.messageTextElement.stopObserving('blur', this.newMessageFieldLoseFocus);
	},
	
	handleWindowResize: function(event) {
		if (!this.isSceneActive) {
			this.fixScrollPositionOnActivate = true;
		} else if (this.getCurrentBottom() < this.MAX_BOTTOM_HEIGHT_FOR_SNAP) {
			this.revealBottomHack();
		}
	},
	
	handleStageFocus: function() {
		this.isWindowFocused = true;
		this.appController.notificationAssistant.clearNotificationsForChat(this.chatThreadId);
	},
	
	handleStageBlur: function() {
		this.isWindowFocused = false;
		
		// If the return key was being held down, then we have an artificial tap highlight on the send button.
		// Remove it, since we won't get the keyup if the stage is deactivated.
		this.sendButton.removeClassName('selected');
	},
	
	handleCommand: function(event) {
		if (event.type === Mojo.Event.command) {
			if (event.command === 'addPicture') {
				this.attachFilePicker();
				event.stop();
			} else if (event.command === 'blockUsername') {
				this.blockUsername();
				event.stop();
			} else if (event.command === 'copyall') {
				this.copyConversationToClipboard();
			}
		} else if (event.type === Mojo.Event.back) {
			event.stop();
			this.popAndRevealListView();
			return;
		}
	},
	
	popAndRevealListView: function() {
		// on scene unload, mark all new messages in this chat as read
		if (!MessagingUtils.sceneExistsInStack(this.controller.stageController, 'listview')) {
			this.setChatToReadAndSetDraftText();
			this.appController.assistant.getListChunk(this.controller, this.handleListChunkOnPop.bind(this));
		} else {
			this.controller.stageController.popScenesTo('listview');
		}
	},
	
	handleListChunkOnPop: function(forceListView, result) {
		this.controller.stageController.popScenesTo('listview');
		this.controller.stageController.pushScene('listview',{forceListView:forceListView, listDataChunk:result});
	},
	
	considerForNotification: function(data) {
		Mojo.Log.info("[CV] ****** chatview considerForNotification");
		// always put the notification through if the screen is off
		// if we are in the chat and the screen is on, do not display a banner + dashboard, just play a notification sound 
		if (this.isScreenOn && data.chatThreadId == this.chatThreadId) {
			Mojo.Log.info("[CV] ****** chatview considerForNotification --- screen is on!");
			if (data.notificationType == this.Messaging.notificationTypes.newMessage) {
				data = {playSoundOnly:true};
			} else if (data.notificationType == this.Messaging.notificationTypes.sendFailure) {
				data = {}; // wipe out the notification because we are in the chat
			}
		}
		return data;
	},
	
	/**
	 * This is called when the app is open, the chatview is the top card in the stack, and
	 * another app is attempting to launch messaging to a specific chat
	 * @param {Object} chatThreadId
	 * @param {Object} contactPointId
	 */
	considerForChatLaunch: function(chatThreadId, contactPointId, address, serviceName) {
		if (chatThreadId == this.chatThreadId) {
			// The chat is already open but could be set to a different transport
			// Re-render the transport picker if a contactPointId is specified
			if (contactPointId) 
				this.renderTransportPicker({contactPointId:contactPointId,address:address,serviceName:serviceName});
			return false;
		} else {
			return true;
		}
	},
	
	// TODO: Cleanup this method and separate some of the logic
	renderChatViewHeader: function(response) {
		//Mojo.Timing.resume("ChatviewAssistant#renderChatViewHeader");
		// If the event == deleted, then pop out of the chat
		if (response.event && response.event === "deleted") {
			this.popAndRevealListView();
		}
		
		// keep a reference to the chat data.  We need the flags for the transient status message logic
		this.chatData = response.record;
		
		// set the contactAvatar - this is used for SMS/MMS messages
		this.contactAvatar = response.record.pictureLocation || response.record.imAvatarLocation || null;
		
		response.record.displayName = MessagingUtils.getContactDisplayName(response.record).strip();
		if (!response.record.serviceName && response.record.displayName) {
			response.record.displayName = MessagingUtils.formatPhoneNumber(response.record.displayName);
		}
		if (!this.chatDisplayName || this.chatDisplayName != response.record.displayName) {
			this.chatViewRecipientName.update(response.record.displayName.escapeHTML());
			this.chatDisplayName = response.record.displayName;
		}
		
		// keep track of the unread count so we can determine if the reason that the chat header
		// data changed is because of a new message.  This is the only case where we should change
		// the currently selected transport
		var lastUnreadCount;
		if (this.lastUnreadCount === undefined) 
			lastUnreadCount = response.record.unreadCount;
		else 
			lastUnreadCount = this.lastUnreadCount;
		this.lastUnreadCount = response.record.unreadCount;
		this.lastTotalCount = response.record.totalCount;
		
		if (this.pickerIsBuilt) {
			// if the picker is built and the chat address changes because of a new incoming message, set the selected 
			// transport to the incoming account. This happens when you are on a different transport than an incoming message
			if (lastUnreadCount != response.record.unreadCount && this.selectedTransport.chatAddress && this.selectedTransport.chatAddress != response.record.chatAddress) {
				this.findAndSetSelectedTransport(this.transports, response.record.serviceName, response.record.chatAddress, response.record.availability);
			}
			return; // do not re-render the picker if it is already built!
		}
		this.pickerIsBuilt = true;
		
		// if there are transient chat flags set, then we failed to clear them when the chat was last popped
		// indicate that we have transient messages in the chat so we can clear them on pop
		if (ChatFlags.isTransient(response.record.flags)) {
			this.chatHasTransientStatusMessage = true;
		}
		
		if (response.record.draftText) {
			this.setTextFieldValue(response.record.draftText);
		}
		
		// If this async request comes back after the list had loaded, fix the scroll position because
		// we may have just changed the hieght of the text box by setting the draft text
		if (this.initialLoadComplete) {
			this.fixListScrollPositionOnLoad();
		} else {
			this.fixListScrollPositionOnLoad(true);
		}
		
		if (response.record.name.substring(0, 3) === 'pID') {
			this.personId = response.record.name.substring(3);
		}
		var transportPickerParams = {
			serviceName: response.record.serviceName,
			chatAddress: response.record.chatAddress,
			availability: response.record.availability,
			contactPointId: this.launchToContactPointId,
			address: this.launchToAddress
		};
		this.renderTransportPicker(transportPickerParams);
		
		this.handleHeaderTap = this.handleHeaderTap.bind(this, response.record.serviceName, response.record.chatAddress);
		this.chatViewHeaderClickTarget.observe(Mojo.Event.tap, this.handleHeaderTap);
		
		// The chat details will tell us if this chat has any invitations
		if (ChatFlags.isInvitation(response.record.flags)) {
			// Retrieve all invitations for this chat - typically this will be just one
			// FIXME: should we subscribe to this?  Currently if a new invitation comes in
			//        while the chatview is open, the user will have to back out to the listview
			//        and come back into the chat or click on the invitation in the listview
			MessagingMojoService.getInvitationsFromChat(this.controller, this.chatThreadId, function(result) {
				this.invitations = result.list;
				this.renderInvitationUI();
			}.bind(this));
		}

		this.updateBlockUsernameMenu();
		//Mojo.Timing.pause("ChatviewAssistant#renderChatViewHeader");
	},
	
	handleHeaderTap: function(serviceName, chatAddress, event) {
		// If this chat is not associated with a contact, launch contacts with add UI
		if (!this.personId) {
			this.pushContactDetails(serviceName, chatAddress, null);
		} else { // we have a personId
			// If we were unable to retrieve any transports for this chat, 
			// make sure that the personId still exists.  Sometimes we will have a bogus
			// personId when contacts get clipped and a person is deleted. The messaging
			// data model stores the person ids
			if (this.transports.length === 0) {
				MessagingMojoService.reverseLookup(
											this.controller, 
											chatAddress, 
											this.handleContactsReverseLookup.bind(this, serviceName, chatAddress), 
											this.handleContactsReverseLookup.bind(this, serviceName, chatAddress));
			} else {
				this.pushContactDetails(null, null, this.personId);
			}
		}
	},
	
	handleContactsReverseLookup: function(serviceName, chatAddress, result) {
		if (result.record && result.record.id) {
			this.pushContactDetails(null, null, result.record.id);
		} else {
			this.pushContactDetails(serviceName, chatAddress, null);
		}
	},
	
	pushContactDetails: function(serviceName, chatAddress, personId) {
		var params = {};
		if (personId) {
			params = {personId:personId};
		} else if (!serviceName && chatAddress && chatAddress.indexOf("@") > -1) {
			params = {
				contact: {
					emailAddresses:[{value: chatAddress}]
				}
			};
		} else if (serviceName && chatAddress) {
			params = {
				contact: {
					imNames:[{value:chatAddress, serviceName:serviceName}]
				}
			};
		} else {
			params = {
				contact: {
					phoneNumbers:[{value:chatAddress}]
				}
			};
		}
		Pseudocard.pushPseudocard(this.controller.stageController, params);
	},
	
	// The only time that the transport picker will be re-rendered is when a new invitation is accepted
	renderTransportPicker: function(params) {
		if (!params) 
			params = {};
			
		// personId indicates this is in our contact table. Otherwise it is an unknown contact
		if (this.personId) {
			MessagingMojoService.getTransportTypes(this.controller, this.personId, this.retrieveTransportData.bind(this, params));
		} else {
			var number = parseInt(params.chatAddress);
			var data = {
				count: 1,
				list: [{
					value: params.chatAddress,
					type: isNaN(number) ? "IM" : "PHONE",
					serviceName: params.serviceName,
					label:3,
					availability: isNaN(number) ? params.availability : undefined
				}]
			};
			this.retrieveTransportData(params, data);
		}
		this.transportPicker.stopObserving();
		this.transportPicker.observe(Mojo.Event.tap, this.renderTransportPickerPopup);
	},
	
	/*
	 * This goes through all the contact points to render the transport picker
	 * Contact points are the recipients gmail accounts, aol accounts, sms number, etc
	 */
	retrieveTransportData: function(transportParams, data) {
		var fixSuspiciousChat = false;
		var params = Object.extend({serviceName:'',chatAddress:'',contactPointId:'', address:''},transportParams);
		
		var transportsPhone = [];
		var transportsIM = [];
		
		// build a temporary hash of IM types that we support
		var myAccountTypes = this.Messaging.IMAccounts.getMyAccountTypesHash();
		
		// preformat data + subscribe to updates for all IM addresses + setup avatarHash
		for (var i = 0; i < data.list.length; i++) {
			var transport = data.list[i];
			if (!transport.displayName) 
				transport.displayName = transport.value;
			if (transport.serviceName) 
				transport.serviceName = transport.serviceName.toLowerCase();
			
			transport.availabilityClass = this.Messaging.Availability.getPopupAvailabilityClass(transport.availability);
			transport.transportIcon = this.Messaging.transports.getTransportByName(transport.serviceName).largeIcon;
			transport.transportIconClass = this.Messaging.transports.getTransportByName(transport.serviceName).className;
			// Found an IM address
			if (transport.type === "IM" || transport.serviceName) {
				if (transport.serviceName) {
					this.avatarHash[transport.value] = transport.avatarLocation;
					// only add the transport if we have an account of that type
					if (myAccountTypes[transport.serviceName]) {
						transport.displayName = transport.value;
						transportsIM.push(transport);
					}
				}
			} else {
				transport.value = MessagingUtils.cleanPhoneNumber(transport.value);
				transport.displayName = MessagingUtils.formatPhoneNumber(transport.value);
				transport.secondaryLabel = Mojo.PatternMatching.addContactLabelFormatting('PHONE', transport.label, "", transport.serviceName);
				transportsPhone.push(transport);
			}
			
		}
		this.transports = transportsPhone.concat(transportsIM); // make sure phone numbers come first for the transport picker
		this.findAndSetSelectedTransport(this.transports, params.serviceName, params.chatAddress, params.availability, params.contactPointId, params.address);
		
		// we are in this method because we have a personId.
		// if we do not have any results from calling listContactPoints then something is wrong
		// hit the service to do a reverse lookup and fix the chat data
		if (!data.list.length && !this.attemptedToFixChatWithBadPersonId) {
			this.attemptedToFixChatWithBadPersonId = true; // this is also referenced when we observe the transports in the list callback
			fixSuspiciousChat = true;
		}
		
		// load the chatlist if we have not already done this
		this.initChatList();

		if (fixSuspiciousChat) {
			MessagingMojoService.fixChatWithBadPersonId(this.controller, this.chatThreadId, this.personId, params.serviceName, params.chatAddress, this.handleFixChatWithBadPersonId.bind(this, transportParams));
		}
	},
	
	handleFixChatWithBadPersonId: function(transportParams, result) {
		// There will be a newPersonId if the reverse lookup was successful and we updated the chat details
		// If there is no newPersonId then that means the chat has no transports.  The chat is clearly messed up, 
		// so we will not be observing any IMAddress changes.  We should never get into this state.
		if (result.newPersonId) {
			this.personId = result.newPersonId;
			// re-render the transport picker for the new personId
			this.renderTransportPicker(transportParams);
		}
	},
	
	observeTransportIMAddresses: function(transports) {
		// cancel any subscriptions to IM Addresses -- we will already
		// have subscriptions if the transport picker has already been rendered
		for (var i = 0; i < this.IMAddressSubscriptions.length; i++) {
			this.controller.cancelServiceRequest(this.IMAddressSubscriptions[i]);
		}
		this.IMAddressSubscriptions.clear();
		
		// Observe IM addresses that have legit presence values.  IM addresses that are not
		// on your buddy list will have NO_PRESENCE
		for (var i = 0; i < transports.length; i++) {
			var transport = transports[i];
			if (transport.serviceName && transport.availability != this.Messaging.Availability.NO_PRESENCE) {
				this.IMAddressSubscriptions.push(MessagingMojoService.observeIMAddress(this.controller, transport.value, transport.serviceName, this.handleIMAddressChange.bind(this)));
			}
		}
	},
	
	handleIMAddressChange: function(data) {
		// Update the presence for the address
		for (var i = 0; i < this.transports.length; i++) {
			var transport = this.transports[i];
			if (!data.record.serviceName && data.record.value) {
				data.record.value = MessagingUtils.cleanPhoneNumber(data.record.value);
			}
			if (transport.value == data.record.value && transport.serviceName == data.record.serviceName) {
				var previousAvailability = this.selectedTransport.availability;
				// current transport just went from online to offline
				if (this.selectedTransport.contactPointId == transport.id) {
					// FIXME: should this update more than just availability?
					this.selectedTransport.availability = data.record.availability;
					this.setCurrentTransportAvailabilityIcon(this.selectedTransport.availability);
				}
				transport.availability = data.record.availability;
				// add chat bubbles to the list for the user going offline/online
				if (this.selectedTransport.contactPointId == transport.id && transport.serviceName && this.TRANSIENT_MESSAGES[transport.availability]) {
					// availability has changed & we have not already inserted a transient message for the new availability + address
					var transientMessageExists = this.TRANSIENT_MESSAGES[transport.availability].isTransientFn(this.chatData.flags);
					if (previousAvailability != transport.availability &&
					(!transientMessageExists || (transientMessageExists && transport.chatAddress != this.chatData.statusAddress))) {

						var message = this.TRANSIENT_MESSAGES[transport.availability].template.evaluate({chatAddress:transport.value});
						// special case for offline. Make sure we did not get logged out
						if (transport.availability == this.Messaging.Availability.OFFLINE) {
							var account = this.Messaging.IMAccounts.getAccountFromAccountTagOrServiceName(data.record.accountTag, data.record.serviceName);
							if (account && account.availability == this.Messaging.Availability.OFFLINE) {
								message = $L("You are offline");
							}
						}

						this.addStatusMessageToChat(message, transport.availability);
					}

					// Whenever the buddy's custom message changes, show it in the chat list. 
					// NOTE: if this ever needs to persist, maybe move it to BaseIMTransport.updateChatPresence
					if (data.record.customMessage && this.chatData.customMessage != data.record.customMessage) {
						this.chatData.customMessage = data.record.customMessage;
						this.addStatusMessageToChat(this.chatData.customMessage, this.Messaging.Availability.AVAILABLE);
					}
				}
			}
		}
		
		// determine if the avatar hash has changed
		if (this.avatarHash[data.record.value] && this.avatarHash[data.record.value] != data.record.avatarLocation) {
			//Mojo.Log.info("*** Calling invalidateItems because the avatar hash has changed!");
			// update the avatarHash
			this.avatarHash[data.record.value] = data.record.avatarLocation;
			this.chatListWidget.mojo.invalidateItems(0); // invalidate the list items so it will reload the window
		}
	},
	
	renderTransportPickerPopup: function(e) {
		// don't render the transport picker popup if there is only one transport
		if (this.transports.length < 1) 
			return;
		
		var listItems = [];
		var foundPhone = false;
		listItems.push({
			label: $L("Send message to..."),
			command: ' ', // Do NOT Localize
			disabled: true
		});
		
		for (var i = 0; i < this.transports.length; i++) {
			var transport = this.transports[i];
			// only render one divider for multiple phone numbers.  All IM accounts
			// get a divider.
			if ((!foundPhone && !transport.serviceName) || transport.serviceName) {
				foundPhone = true;
				listItems.push({
					label: this.Messaging.transports.getTransportByName(transport.serviceName).displayName.toUpperCase()
				});
			}
			
			// TODO: When we have multiple accounts of the same type, append the account username
			var rowParams = {
				label: transport.displayName,
				secondaryLabel: transport.secondaryLabel,
				command: "transport" + transport.id // concat a string with the contactPointId just incase the id was ever null
				// to avoid having this transport be rendered as a divider by mistake
			};
			
			// For phone numbers, show the quick-dial icon
			if (!transport.serviceName) {
				rowParams.showQuickDialIcon = "show-icon";
				rowParams.quickDialNumber = ChatviewAssistant.quickDialIdPrefix + transport.value;
			}

			if (transport.availability != undefined) {
				rowParams.secondaryIcon = this.Messaging.Availability.getPopupAvailabilityClass(transport.availability);
			}
			
			listItems.push(rowParams);
		}
		
		this.controller.popupSubmenu({
			onChoose: this.handleTransportSelection.bind(this),
			toggleCmd: "transport" + this.selectedTransport.contactPointId, // set this to the current transport
			placeNear: e.target,
			//manualPlacement:true,
			itemTemplate: 'chatview/popup-item-transport',
			items: listItems
		});
	},
	
	handleTransportSelection: function(selectedValue, tapTarget) {
		if (!selectedValue) 
			return;
		
		if (tapTarget && tapTarget.id.startsWith(ChatviewAssistant.quickDialIdPrefix)) {
			try {
				// The phone number is the substring after the quick dial prefix.
				var phoneNumber = tapTarget.id.substr(ChatviewAssistant.quickDialIdPrefix.length);
				if (phoneNumber.length > 0) {
					this.controller.serviceRequest("palm://com.palm.applicationManager", {
						method: 'open',
						parameters: {
							id: 'com.palm.app.phone',
							params: {number:phoneNumber}
						}
					});
				}
			} catch (e) {
				Mojo.Log.logException(e);
			}
		} else {
			// iterate through transports and find the selected one
			var that = this;
			this.transports.each(function(transport) {
				if ("transport" + transport.id == selectedValue) {
					that.setSelectedTransport(transport.id, transport.value, transport.serviceName, transport.accountTag, transport.availability);
					throw $break;
				}
			});
		}

		this.messageTextElement.focus();
	},
	
	setSelectedTransportFromId: function(transportId) {
		var that = this;
		this.transports.each(function(transport) {
			if (transport.id == transportId) {
				that.setSelectedTransport(transport.id, transport.value, transport.serviceName, transport.accountTag, transport.availability);
				throw $break;
			}
		});
	},
	
	// TODO: this could be further improved to choose an online transport over one that is offline
	//       when no exact matches are found
	findAndSetSelectedTransport: function(data, serviceName, chatAddress, availability, contactPointId, address) {
		var selectedTransport;
		if (data.length > 0) {
			if (contactPointId) { // simple search if we have the contact point id
				var transport = undefined;
				var found = false;
				for (var i = 0; i < data.length; i++) {
					transport = data[i];
					if (transport.id == contactPointId) {
						selectedTransport = transport;
						found = true;
					}
				}
				
				if ((!found) && (address)) {
					Mojo.Log.info("contactId not found, rerun looking for matching phone number address " + address);
					for (var i = 0; i < data.length; i++) {
						transport = data[i];
						Mojo.Log.info("value " + transport.value);
						if (transport.value == address) {
							Mojo.Log.info("found here");
							selectedTransport = transport;
						}
					}
				}
			} else {
				serviceName = Object.isString(serviceName) ? serviceName.toLowerCase() : "";
				chatAddress = Object.isString(chatAddress) ? chatAddress.toLowerCase() : "";
				// selected transport is a phone number
				if (!serviceName) {
					// Special case where the chatview was launched from the buddy list, but the latest
					// chat over a phone number. HI wants this to switch to a IM transport so choose the
					// IM transport with the best avaibility
					var found = false;
					if (this.selectIMTransport) {
						this.selectIMTransport = undefined;
						var transportMatch = { availability:999 };
						for (var i = 0; i < data.length; i++) {
							var transport = data[i];
							Mojo.Log.info("%j", transport);
							if (transport.type === "IM" && transport.availability < transportMatch.availability) {
								transportMatch = transport;
								selectedTransport = transport;
								found = true;
							}
						}
					}
					
					if (found === false) {
						// grab only phone numbers
						var numbers = [];
						data.each(function(transport) {
							if (!transport.serviceName) 
								numbers.push(transport);
						});
						var bestMatches = MessagingUtils.findBestPhoneNumberMatches(chatAddress, numbers, 'value');
						if (bestMatches.length > 0) 
							selectedTransport = bestMatches[0];
					}
				} else { // transport is IM
					data.each(function(transport) {
						var transportServiceName = transport.serviceName ? transport.serviceName.toLowerCase() : "";
						var transportChatAddress = transport.value ? transport.value.toLowerCase() : "";
						if (transportServiceName == serviceName && transportChatAddress == chatAddress) 
							selectedTransport = transport;
					});
				}
			}
			
			// if there was no match, use a transport of the same type
			// else: just use the first transport as a last resort
			if (!selectedTransport && data.length > 0) {
				var foundMatch = false;
				var backupChoice;
				for (var i = 0; i < data.length && !foundMatch; i++) {
					var transport = data[i];
					// match if both are phone numbers (serviceName == null) or match where the serviceNames are the same (same IM transport)
					if ((!serviceName && !transport.serviceName) || (serviceName && transport.serviceName && serviceName == transport.serviceName)) {
						selectedTransport = transport;
						foundMatch = true;
					}
					// backup match: both are an IM transport.  We will use this if we cannot match another IM transport with the same serviceName
					if (serviceName && transport.serviceName) 
						backupChoice = transport;
				}
	
				if (!selectedTransport) {
					if (backupChoice) {
						selectedTransport = backupChoice; // use the backup if we have one
					} else {
						selectedTransport = data[0]; // only other option is to use the first transport in the array
					}
				}
				Mojo.Log.warn('Unable to match transport that the chat was launched with, choosing the best one.');
			}
		} else {
			// This chat is clearly messed up because we didn't find any transports use the chat address from the chat data
			selectedTransport = {
				id: 0, // we did not match a transport, so we don't have an id
				value: chatAddress,
				serviceName: serviceName,
				availability: this.Messaging.Availability.NO_PRESENCE, // set the availability to no presence because this will never update if we set it to the availability on the chat
				accountTag: null
			};
		}
		this.setSelectedTransport(selectedTransport.id, selectedTransport.value, selectedTransport.serviceName, selectedTransport.accountTag, selectedTransport.availability);
	},
	
	// This is a simple setter.  No data validation will happen here.
	setSelectedTransport: function(id, value, serviceName, accountTag, availability) {
		this.selectedTransport.contactPointId = id;
		this.selectedTransport.chatAddress = value;
		this.selectedTransport.serviceName = serviceName;
		this.selectedTransport.accountTag = accountTag;
		this.selectedTransport.availability = availability;
		this.setCurrentTransportAvailabilityIcon(availability);
		this.controller.get('currentTransport').update(this.Messaging.transports.getTransportByName(serviceName).secondaryDisplayName);
		
		if (!this.hasTransportBeenSet) {
			this.hasTransportBeenSet = true;
			
			this.launchToContactPointId = null; // this is only used at launch to initially set the selected transport
			// Put a transient message into the chat If the current transport is IM & it is different than the aggregate chat availability
			if (serviceName && availability && this.TRANSIENT_MESSAGES[availability] && (this.chatData.availability != availability)) {
				var message = this.TRANSIENT_MESSAGES[availability].template.evaluate({chatAddress:value});
				// special case for offline. Make sure we did not get logged out
				if (availability == this.Messaging.Availability.OFFLINE) {
					var account = this.Messaging.IMAccounts.getAccountFromAccountTagOrServiceName(accountTag, serviceName);
					if (account && account.availability == this.Messaging.Availability.OFFLINE) {
						message = $L("You are offline");
					}
				}
				this.addStatusMessageToChat(message, availability);
			}
		}
		
		if (!this.selectedTransport.serviceName) { // undefined means we are using the phone
			// show the attach paper clip
			this.showAttachButton();
		} else {
			this.hideAttachButton();
			// if the attachment state is enabled, disable it
			if (this.attachment) 
				this.resetTextBox(false);
		}
		this.setCharacterCounterMaxLength();
	},
	
	setCurrentTransportAvailabilityIcon: function(availability) {
		var classStr = "";
		if (availability != undefined) 
			classStr = this.Messaging.Availability.getNormalAvailabilityClass(availability);
		else 
			classStr = this.Messaging.Availability.getNormalAvailabilityClass(this.Messaging.Availability.NO_PRESENCE);
		
		this.controller.get('presenceicon').className = 'chatview-header-status status-' + classStr;
	},
	
	setCharacterCounterMaxLength: function() {
		// set the max size for the textbox based on the transport
		var params = {
			maxPartLength: 1000,
			maxParts: 1,
			headerSize: 0,
			encodings: ""
		};
		
		if (!this.attachment && !this.selectedTransport.serviceName) {
			if (this.selectedTransport.chatAddress.indexOf("@") > -1) { // SMS to email
				params.maxPartLength = (this.Messaging.carrierSettings.smsMessageSizeLimit - (this.selectedTransport.chatAddress.length + 1));
				params.maxParts = 1;
				params.headerSize = 0;
			} else { // SMS
				params.maxPartLength = this.Messaging.carrierSettings.smsMessageSizeLimit;
				params.maxParts = this.Messaging.carrierSettings.smsMaxPartsPerMessage;
				params.headerSize = this.Messaging.carrierSettings.smsHeaderSizeLimit;
			}
			
			params.encodings = this.Messaging.carrierSettings.smsEncoding;
		}
		
		this.characterCounter.setSizeParams(params);
	},
	
	/***************************
	 * CHATVIEW LIST
	 ***************************/
	initChatList: function() {
		if (!this.hasInitChatList) {
			this.hasInitChatList = true;
			// only do this once when the chat is initially loaded
			//MessagingMojoService.getChatCount(this.controller, this.chatThreadId, this.handleSetInitialListLength.bind(this, this.chatListWidget), this.listReset);
			this.handleSetInitialListLength(this.chatListWidget, { count: this.lastTotalCount });
		} else {
			// something is changed (most likely the transport picker is being re-rendered)
			// trash our subscriptions to observe the IM addresses & create them again
			this.observeTransportIMAddresses(this.transports);
		}
		this.transportPicker.show();
		if (this.transports.length < 1) 
			this.currentTransportDownArrow.hide();
		else 
			this.currentTransportDownArrow.show();
	},
	
	handleSetInitialListLength: function(listWidget, data) {
		this.hasLoadedChatList = true;
		listWidget.mojo.setLength(data.count);
		this.revealBottomHack();
		this.listInitialized = true;
		if (data.count == 0) 
			this.controller.modelChanged(this.chatListModel);
		else 
			listWidget.mojo.invalidateItems(0);
	},
	
	chatListDividerFunction: function(item) {
		if (!item) {
			return;
		}
		// return a unique string for the divider
		return item.dividerText;
	},
	
	chatListItemsCallback: function(listWidget, offset, limit) {
		Mojo.Log.info("+++++chatListItemsCallback");
		if (this.listInitialized) {
			//var range = this.chatListWidget.mojo.getLoadedItemRange();
			//console.log("***@@@ Current window: " + range.offset + ", " + range.limit + "   Requesting offset: " + offset + ", limit:" + limit);
			MessagingMojoService.getChatNew(this.controller, this.chatThreadId, this.updateChatList, this.listReset, this.chatListSubscriberId, listWidget.mojo.maxLoadedItems(), offset, limit);
			this.listReset = false;
		}
	},
	
	updateChatList: function(listWidget, stuff) {
		//Mojo.Timing.resume("ChatviewAssistant#updateChatList");
		if (stuff.subscriberId) 
			this.chatListSubscriberId = stuff.subscriberId;
		
		// we have an update event
		if (stuff.updated) {
			this.handleChatListUpdateEvent(listWidget, stuff);
			return;
		}
		this.preFormatChatList(listWidget, stuff);
		
		var offset = 0;
		if (stuff.offset) 
			offset = stuff.offset;
			
		// analyze the current bottom value before we change the list
		var doRevealBottom = false;
		
		// if the scene is deactivated because another scene is on top, set fixScrollPositionOnActivate
		// to indicate that we should check if we need to reveal bottom in aboutToActivate() 
		if (!this.isSceneActive) {
			this.fixScrollPositionOnActivate = true;
		} else if (this.getCurrentBottom() < this.MAX_BOTTOM_HEIGHT_FOR_SNAP) {
			doRevealBottom = true;
		}

		//Mojo.Timing.resume("ChatviewAssistant#noticeUpdatedItems");
		listWidget.mojo.noticeUpdatedItems(offset, stuff.list);
		//Mojo.Timing.pause("ChatviewAssistant#noticeUpdatedItems");
		
		if (!this.hasInitialBottomWindow || doRevealBottom) {
			if (!this.hasInitialBottomWindow && !this.attemptedToFixChatWithBadPersonId) {
				// observe transport IM addresses after we have loaded the list
				// delay this .5 seconds just in case the list is still fixing itself up
				this.observeTransportIMAddresses.bind(this).delay(0.5, this.transports);
			}
			this.hasInitialBottomWindow = true;
			this.revealBottomHack();
		}
		
		if (this.afterAboutToActivateFn) {
			this.afterAboutToActivateFn();
			this.afterAboutToActivateFn = null;
		}
		
		if (stuff.list) {
			// iterate over the attachments starting from the end of the list
			// this will load the bottom image and lock our position there while
			// all of the MMS content above loads
			for (var i = stuff.list.length - 1; i >= 0; i--) {
				var msg = stuff.list[i];
				if (ChatFlags.hasAttachment(msg.flags)) {
					MessagingMojoService.getMMSParts(this.controller, msg.messageId, this.buildMMSParts.bind(this, msg.messageId, msg.flags, (offset + i)));
				}
			}
		}
		//Mojo.Timing.pause("ChatviewAssistant#updateChatList");
	},
	
	handleChatListUpdateEvent: function(listWidget, update) {
		Mojo.Log.info("+++++handleChatListUpdateEvent ");
		// ID updates do not require issuing a count query
		// look up the item in the list and invalidate it
		if (update.descriptions && update.descriptions.idUpdate) {
			var loadedItemRange = listWidget.mojo.getLoadedItemRange();
			var listItems = listWidget.mojo.getItems(loadedItemRange.offset, loadedItemRange.limit);
			var foundMatch = false;
			for (var i = 0; i < listItems.length && !foundMatch; i++) {
				if (listItems[i].messageId == update.descriptions.idUpdate) {
					listWidget.mojo.invalidateItems(loadedItemRange.offset + i, 1);
					foundMatch = true;
				}
			}
		} else {
			MessagingMojoService.getChatCount(this.controller, this.chatThreadId, this.handleChatListUpdateEventAfterCountQuery.bind(this, listWidget, update), this.listReset);
		}
	},
	
	handleChatListUpdateEventAfterCountQuery: function(listWidget, update, data) {
		//Mojo.Timing.resume("ChatviewAssistant#handleChatListUpdateEventAfterCountQuery");
		var newDataSize = data.count;
		if (update.descriptions && (update.descriptions.outgoingMsg || update.descriptions.incomingMsg)) {
			var insertLimit = listWidget.mojo.getLength();
			// If the size hasn't changed, invalidate the widget since it isn't clear what to add or remove
			if (newDataSize == insertLimit) {
				listWidget.mojo.invalidateItems(0);
			} else {
				var sizeDelta = newDataSize - insertLimit;
				// get the new data ourselves instead of setting the list length
				// calling noticeUpdatedItems() in our subscribed callback will set the length properly
				MessagingMojoService.getChatNew(this.controller, this.chatThreadId, null, this.listReset, this.chatListSubscriberId, listWidget.mojo.maxLoadedItems(), insertLimit, sizeDelta);
			}

		} else if (update.descriptions && update.descriptions.msgDeleted) {
			// do nothing.  The list will remove the item so we can just ignore this event
			// We already reset the list when we sent the delete request to the service
		} else {
			// update the count and reload the window
			var currentListSize = listWidget.mojo.getLength();
			
			// This should only happen during weird events like chats getting merged
			// This will not look pretty but it should rarely happen
			if (newDataSize != currentListSize) {
				// setLength will change the bottom position.  We need to analyze
				// the position before we setLength, and keep it snapped to the bottom
				var doRevealBottom = false;
				if (this.getCurrentBottom() < this.MAX_BOTTOM_HEIGHT_FOR_SNAP) 
					doRevealBottom = true;
				
				listWidget.mojo.setLength(newDataSize);
				
				if (doRevealBottom) {
					this.revealBottomHack();
				}
			} else {
				listWidget.mojo.invalidateItems(0);
			}
		}
		//Mojo.Timing.pause("ChatviewAssistant#handleChatListUpdateEventAfterCountQuery");
	},

	/**
	 * We need to instantiate audio objects after they have been rendered
	 * This method is called every time a new item is added to the DOM
	 * We keep a cache of audio objects so that we don't lose the
	 * play state of the audio object.
	 *
	 * @param {Object} listWidget
	 * @param {Object} itemModel
	 * @param {Object} itemNode
	 */
	itemRendered: function(listWidget, itemModel, itemNode) {
		//Mojo.Timing.resume("ChatviewAssistant#itemRendered");
		// Whether to show the "mms delayed download" UI 
		if (itemModel.status == this.Messaging.MessageStatus.RETRIEVING_CONTENT) {
			Mojo.Log.info("itemRendered setup spinner")
			this.controller.get('download-button_' + itemModel.messageId).mojo.activate();
		} else if (itemModel.status == this.Messaging.MessageStatus.SUCCESS || itemModel.status == this.Messaging.MessageStatus.DELAYED_DELIVERY) {
			var dlbutton = this.controller.get('download-button_' + itemModel.messageId);
			if (dlbutton !== undefined) {
				dlbutton.mojo.deactivate();
			}
		}

		if (itemModel.hasImageAttachment) {
			var imageContainer = this.controller.get('HACKImage' + itemModel.messageId);
		
			for (var i = 0; i < itemModel.imageData.length; i++) {
				var imageData = itemModel.imageData[i];
				var imageObject = new Image();

				// inject the image into the dom after it has been loaded so we can fix the scroll position
				imageObject.onload = this.mmsImageOnLoad.curry(imageObject, imageContainer, itemModel, itemNode);
				imageObject.onerror = this.mmsImageOnError.curry(imageObject);

				imageObject.className = "MMSImageObject";
				imageObject.setAttribute("originalSrc", imageData.originalImagePath);
				imageObject.src = imageData.resizedImagePath;

				// append file name/size info below the image
				//var imageFooterContent = Mojo.View.render({object:imageData, template:this.messageTemplatePath + 'mms-image'});
				//imageContainer.insert(imageFooterContent);
			}
		}
		
		if (itemModel.hasAudioAttachment) {
			var audioElementCache = this.getAudioElementCacheInstance();
			if (audioElementCache.hasElement(itemModel.messageId)) {
				this.controller.get(itemNode).replace(audioElementCache.get(itemModel.messageId));
			} else {
				var partHTML = Mojo.View.render({
					collection: itemModel.audioData,
					template: this.messageTemplatePath + 'mms-audio'
				});
				var audioElement = this.controller.get('HACKAudio' + itemModel.messageId);
				if (audioElement) 
					audioElement.update(partHTML);
				else 
					Mojo.Log.info("ERROR: Unable to locate audio object");
					
				for (var i = 0; i < itemModel.audioData.length; i++) {
					var audio = itemModel.audioData[i];
					// calling extendElement will tie the audio object to this scene.  When the scene is trashed,
					// any audio objects will get GCed
					audioElement = this.controller.get(audio.elementId);
					var audioTag = AudioTag.extendElement(audioElement, this.controller, audio.path);
					audioTag.autoplay = false;
					
					audioElement.observe('error', this.hackHandleAudioError);
					
				};
				audioElementCache.add(itemModel.messageId, itemNode);
			}
		}
		
		// Images are a special case because we adjust the scroll position in the image onload callback
		if (itemModel.hasAttachmentData && !itemModel.hasImageAttachment) {
			var currentBottom = this.getCurrentBottom();
			if (currentBottom < this.MAX_BOTTOM_HEIGHT_FOR_SNAP) {
				this.debouncedRevealBottom();
			}
		}
		
		// If we have delayed elements on this item, now is the time to render them ... deferred
		var delayed = itemModel._delayed;
		if (delayed)
		{
			(function()
			{
				Mojo.Log.info("********** RUNNING DELAYED TEXT INDEXER ***************");
				var val = delayed.summary;
				if (val)
				{
					var nval = PalmSystem.runTextIndexer(val);
					if (nval != val)
					{
						itemModel.summary = nval;
						itemNode.querySelector(".messageSummary").innerHTML = nval;
					}
				}
				val = delayed.messageText;
				if (val)
				{
					var nval = PalmSystem.runTextIndexer(val);
					if (nval != val)
					{
						itemModel.messageText = nval;
						itemNode.querySelector(".messageText").innerHTML = nval;
					}
				}
				val = delayed.callbackNumber;
				if (val)
				{
					var nval = PalmSystem.runTextIndexer(val);
					if (nval != val)
					{
						itemModel.callbackNumber = nval;
						itemModel.callbackNumberText = new Template($L("Call back #: #{number}")).evaluate(
						{
							number: MessagingUtils.formatPhoneNumber(nval)
						});
						itemNode.querySelector(".callbackNumber").innerHTML = itemModel.callbackNumberText;
					}
				}
			}).defer();
		}
		
		this.debouncedListLoaded();
		//Mojo.Timing.pause("ChatviewAssistant#itemRendered");
	},
	
	// Since the audio error event does not bubble up, we cannot currently listen for this
	// event on this list.  To avoid memory leaks we will use this unbound function to look up
	// the current scene and pop up the alert dialog for the error
	hackHandleAudioError: function(error) {
		var stageController = Mojo.Controller.getAppController().getStageController(MainStageName);
		if (stageController) {
			var sceneStack = stageController.getScenes();
			for (var i = 0; i < sceneStack.length; i++) {
				if (sceneStack[i].sceneName === "chatview") {
					var f = sceneStack[i].assistant.handleAudioError;
					if (f) {
						f.apply(sceneStack[i].assistant, [error]);
					}
				}
			}
		}
	},
	
	handleAudioError: function(error) {
		this.controller.showAlertDialog({
			onChoose: function() {
			},
			title: $L("Error"),
			message: $L("There was an error attempting to play the selected audio attachment"),
			preventCancel: false,
			choices: [{
				label: $L('Dismiss'),
				value: 'Dismiss'
			}]
		});
	},
	
	mmsImageOnLoad: function(imageObject, imageContainer, itemModel, itemNode) {
		
		imageObject.onload = null;
		imageObject.onerror = null;
		
		// Detect duplicate images and remove them.
		for (var child = imageContainer.firstChild; child; child = child.nextSibling)
		{
			if (child.src == imageObject.src)
			{
				return;
			}
		}
		
		var referenceItem = this.getOnScreenItem();
		var referenceItemOffset;

		if (referenceItem) {
			referenceItemOffset = Mojo.View.viewportOffset(referenceItem).top;
		}
		
		imageContainer.appendChild(imageObject);
		
		if (referenceItem) {
			var delta = referenceItemOffset - Mojo.View.viewportOffset(referenceItem).top;
			if (delta) {
				this.controller.sceneScroller.mojo.adjustBy(0, delta);
			} else {
				// special case for last item in the list:
				// - sometimes injecting an image will not affect the position of the item being watched.
				// - This is only a problem for the last item in the list since we know that we are not
				//   watching an element below it
				// - This special case only applies if the last item is visible on screen
				
				var nodeTop = Mojo.View.viewportOffset(itemNode).top;
				var isNodeVisible = (nodeTop >= 0 && nodeTop <= this.controller.window.innerHeight);
				
				if (isNodeVisible) {
					var lastListItem = this.chatListWidget.mojo.getItems(this.chatListWidget.mojo.getLength() - 1, 1)[0];
					if (lastListItem && lastListItem.messageId == itemModel.messageId) {
						this.controller.sceneScroller.mojo.adjustBy(0, -(itemNode.offsetHeight));
					}
				}
			}
		}
	},
	
	mmsImageOnError: function(imageObject) {
		if (!imageObject.hasError) { // set a property on the image object to indicate we have already encountered an error
			imageObject.hasError = true; // this prevents an infinite loop if brokenImage.png were to be missing
			// loading the brokenImage will fire the onload callback and will clean up the onload/onerror observers
			var originalSrc = imageObject.getAttribute('originalSrc');
			imageObject.src = originalSrc || "images/brokenImage.png";
		} else {
			// this will only get called if the original image is not able to be loaded
			imageObject.src = "images/brokenImage.png";
			imageObject.onload = null;
			imageObject.onerror = null;
		}
	},
	
	debouncedListLoaded: function() {
		if (!this.initialLoadComplete) {
			if (this.listLoadedTimer) {
				clearTimeout(this.listLoadedTimer);
			}
			this.listLoadedTimer = setTimeout(this.listLoadedTimerFn, 1000);
		}
	},
	
	_debouncedListLoadedHelper: function() {
		this.fixListScrollPositionOnLoad();
		this.initialLoadComplete = true;
	},
	
	
	debouncedRevealBottom: function() {
		var now = Date.now();
		var lastForcedReveal = this.lastForcedReveal || 0;
		
		if (now - lastForcedReveal > 600) {
			this.revealBottomHack();
			this.lastForcedReveal = now;
		}
		
		// delay revealing bottom so attachments have time to load
		if (this.revealerTimer) {
			clearTimeout(this.revealerTimer);
		}
		this.revealerTimer = setTimeout(function() {
			this.revealBottomHack();
			this.lastForcedReveal = Date.now();
		}.bind(this), 200);
	},
	
	getCurrentBottom: function() {
		if (!this.controller) {
			return;
		}
		var scrollState = this.controller.sceneScroller.mojo.getScrollPosition().top;
		var sceneHeight = this.controller.sceneElement.getHeight();
		var scrollerHeight = this.controller.sceneScroller.mojo.scrollerSize().height;
		var bottom = Math.abs(-scrollState + scrollerHeight - sceneHeight);
		return bottom;
	},
	
	preFormatChatList: function(listWidget, stuff) {
		//Mojo.Timing.resume("ChatviewAssistant#preFormatChatList");
		var needToInstantiateButtons = false;
		var today = Date.now();
		var d = new Date();
		var prevAddress = "";
		for (var i = 0; i < stuff.list.length; i++) {
			var delayed = {};
			var msg = stuff.list[i];
			if (msg.summary && !ChatFlags.isTransient(msg.flags)) {
				msg.summary = msg.summary.escapeHTML();
				//msg.summary = PalmSystem.runTextIndexer(msg.summary);
				delayed.summary = msg.summary;
			}
			// Need to do setupWidget of the button here since activityButton only works before the
			// button's setup() function is called.
			if (msg.status == this.Messaging.MessageStatus.DELAYED_DELIVERY || msg.status == this.Messaging.MessageStatus.RETRIEVING_CONTENT) {
				msg.label = $L("Tap to download MMS");
				this.controller.setupWidget('download-button_' + msg.messageId, {type:Mojo.Widget.activityButton}, msg);
				needToInstantiateButtons = true;
			}

			if (msg.messageText && !ChatFlags.isTransient(msg.flags)) {
				// Keep a copy of the (safe) original text since smileys get converted to img tags
				msg.textForCopying = msg.messageText;
				// escape the text we sent since user may have added html markup.
				// Inbound messages are sanitized in the messaging service (to allow <u>, <i>, etc)
				if (ChatFlags.isOutgoing(msg.flags)) {
					msg.messageText = msg.messageText.escapeHTML();
				}
				//msg.messageText = PalmSystem.runTextIndexer(msg.messageText);
				delayed.messageText = msg.messageText;
			}
			
			var transport = this.Messaging.transports.getTransportByName(msg.messageServiceName);
			msg.transportClass = transport.className;
			//msg.transportLabel = transport.secondaryDisplayName;
				
			d.setTime(msg.deviceTimeStamp);
			if (msg.fromAddress !== undefined) {
				prevAddress = MessagingUtils.formatPhoneNumber(msg.fromAddress);
				msg.dividerAddress = prevAddress; // this used to use msg.timeStamp - changed 2008-11-05	
			}
			msg.niceDate = BucketDateFormatter.getDateBucket(d, true, true);

			// divider hack - round to the nearest 5 minutes
			// we have to do this because the list divider implementation does not allow a divider
			// to have knowledge about other dividers
			var deviceTimeStampMinutes = d.getMinutes();
			d.setMinutes((deviceTimeStampMinutes - (deviceTimeStampMinutes % 5)));
			d.setSeconds(0);
			// Do this to prevent the divider from showing up if an address isn't available. 
			if (prevAddress) {
				this.dividerAddress = prevAddress;
			}
			msg.dividerText = msg.transportClass + BucketDateFormatter.getDateBucket(d, true, true) + this.dividerAddress;
	
			// if the INCOMING message was sent longer than 10 minutes before it arrived on device,
			// add sent date to indicate that this delay happened
			if (!ChatFlags.isOutgoing(msg.flags) && msg.deviceTimeStamp && msg.timeStamp && (msg.deviceTimeStamp - msg.timeStamp) > (600000)) {
				d.setTime(msg.timeStamp);
				var sentTimeStamp = d;
				
				// Only show the message sent time if it is less than two days old
				// from when it arrived on device
				if (today - msg.timeStamp <= 172800000) { //172800000 = (1000*60*60*24*2)
					msg.timeStampDeltaMsg = new Template($L("Message Sent: #{dateTime}")).evaluate({dateTime:Mojo.Format.formatDate(sentTimeStamp,'short')});
				}
			}

			if (msg.callbackNumber) {
				//msg.callbackNumber = PalmSystem.runTextIndexer(msg.callbackNumber);
				delayed.callbackNumber = msg.callbackNumber;
				var template = new Template($L("Call back #: #{number}"));
				msg.callbackNumberText = template.evaluate({
					number: MessagingUtils.formatPhoneNumber(msg.callbackNumber)
				});
			}
			
			msg.statusClass = this.Messaging.MessageStatus.getStatusClass(msg.status);
			
			// 0 = normal priority
			// 1 = interactive
			// 2 = urgent
			// 3 = emergency
			// 4 = low priority
			if (msg.priority && (msg.priority == 2 || msg.priority == 3)) 
				msg.messageTextClass = "priority"
			else 
				msg.messageTextClass = msg.statusClass;
			
			// If we have a failed SMS with no summary or mesage, then display a canned error message
			if (msg.messageType) {
				var messageTypeLowerCase = msg.messageType.toLowerCase();
				if (messageTypeLowerCase === "sms" && (msg.status == this.Messaging.MessageStatus.FAILED || msg.status == this.Messaging.MessageStatus.UNDELIVERABLE) && msg.summary.length === 0 && msg.messageText.length === 0) {
					msg.messageText = $L("This message has an error.");
				}
				
				if (messageTypeLowerCase === "mms" && msg.status == this.Messaging.MessageStatus.FAILED && !ChatFlags.isOutgoing(msg.flags)) {
					msg.messageText = $L("Failed to download msg");
				}
			}
			var tmpClass = [];
			
			if (ChatFlags.isOutgoing(msg.flags)) {
				tmpClass.push("my-chat");
				// Disabling MY avatar because we are not doing this for 1.0  2008-11-05
				//msg.avatar = this.myAvatar || 'images/my-avatar.png';
			} else {
				tmpClass.push("their-chat");
				msg.avatar = this.avatarHash[msg.fromAddress] || this.contactAvatar || "images/list-view-default-avatar.png";
			}
			msg.imAvatarOverlay = "images/list-view-avatar-frame.png";
			
			// isTransient is set on messages that we added to the chat when the account
			// on the current transport goes online/offline
			if (ChatFlags.isTransient(msg.flags)) {
				tmpClass.push("status-message");
			}
			
			if (msg.status == this.Messaging.MessageStatus.FAILED || msg.status == this.Messaging.MessageStatus.UNDELIVERABLE) {
				tmpClass.push("error");
				msg.errorContent = Mojo.View.render({object:msg, template:this.messageTemplatePath + 'message-error'});
			}
			msg.css_class = tmpClass.join(" ");
			
			// render parts of the message from templates so we don't have lots of hidden divs that impact perf
			msg.messageContents = '';
			if (msg.timeStampDeltaMsg) {
				msg.messageContents += Mojo.View.render({object:msg, template:this.messageTemplatePath + 'delayed-message-timestamp'});
			}
			if (msg.summary) {
				msg.messageContents += Mojo.View.render({object:msg, template:this.messageTemplatePath + 'message-summary'});
			}
			msg.messageContents += Mojo.View.render({object:msg, template:this.messageTemplatePath + 'message-text'});
			if (msg.callbackNumberText) {
				msg.messageContents += Mojo.View.render({object:msg, template:this.messageTemplatePath + 'callback-number'});
			}
			
			// If we have delayed items, attach them to the msg for later processing
			for (var k in delayed)
			{
				msg._delayed = delayed;
				break;
			}
		}
		
		// Only take the hit of instantiateChildWidgets if a download button had to be setup. 
		if (needToInstantiateButtons === true) {
			this.controller.instantiateChildWidgets(listWidget);
		}
		
		//Mojo.Timing.pause("ChatviewAssistant#preFormatChatList");
	},
	
	buildMMSParts: function(id, flags, offset, parts) {
		var audioData = [];
		var imageData = [];
		var partCount = 0;
		var partId = id + "_" + partCount;
			
		var items = this.chatListWidget.mojo.getItems(offset, 1);
		var item = items[0];
		if (!item) {
			return;
		}
		// Keep a copy of the (safe) original text since smileys get converted to img tags
		var addNewline = false;
		item.textForCopying = "";

		var referenceItem = this.getOnScreenItem(id);
		var referenceItemOffsetBeforeUpdate;
		var referenceItemOffsetAfterUpdate;

		var mmsContent = '';

		for (var i = 0; i < parts.list.length; i++) {
			var part = parts.list[i];
			//console.log("PART: " + Object.toJSON(part) + " PART ID: " + partId);

			if (part.size && part.size > 0) { // convert the byte value into KB
				var size = Math.round((part.size / 1024) * 10) / 10;
				var sizeUnit = "K";
				// if more than 999 KB then convert to MB to save screen space
				if (size >= 1000) {
					size = Math.round((part.size / (1024 * 1024)) * 10) / 10;
					sizeUnit = "M";
				}

				part.size = new Template($L("#{size}#{sizeUnit}")).evaluate({
					size: size,
					sizeUnit: sizeUnit
				});
			} else {
				part.size = '';
			}
			
			if (!part.mimeType) {
				part.mimeType = '';
			}
			
			if (part.mimeType.substring(0, 5) == 'image') {
				item.hasImageAttachment = true;
				var imagePart = {
					elementId: "mmsImage_" + partId,
					resizedImagePath: MessagingUtils.extractfs(part.uri,{width:240,height:300,crop:3}),
					originalImagePath: part.uri
				};
				// if outgoing, hide the size + display name.
				if (ChatFlags.isOutgoing(flags)) {
					imagePart.size = '';
					imagePart.displayName = '';
				} else {
					imagePart.size = part.size;
					imagePart.displayName = part.displayName;
				}
				imageData.push(imagePart);
			} else if (part.mimeType.substring(0, 5) == 'audio') {
				// queue up the audio file to be loaded after the content has been drawn in our itemRendered callback
				item.hasAudioAttachment = true;
				audioData.push({
					elementId: "mmsAudio_" + partId,
					path: MessagingUtils.systemPathToLocalURL(part.uri),
					displayName: part.displayName,
					size: part.size
				});
			} else if (part.mimeType.substring(0, 5) == 'video') {
				part.fileInfo = part.displayName;
				part.uri = MessagingUtils.systemPathToLocalURL(part.uri);
				part.mmsTypeClass = 'mms-video';
				mmsContent += Mojo.View.render({object:part, template:this.messageTemplatePath + 'mms-video'});
			} else if (part.mimeType == "text/plain" && !part.uri) {
				if (part.partText) {
					if (part.partText && part.partText.length > 0) {
						if (addNewline) {
							item.textForCopying += "\n";
						}
						addNewline = true;
						item.textForCopying += part.partText;
					}
					part.MMSMessageText = part.partText.escapeHTML();
					part.MMSMessageText = PalmSystem.runTextIndexer(part.MMSMessageText);
				}
				mmsContent += Mojo.View.render({object:part, template:this.messageTemplatePath + 'mms-text'});
			} else if (part.mimeType.substring(0, 4) == 'text') { // if text but not plain (vcard/vcal)
				var textType = part.mimeType.substring(5).toLowerCase();
				if (textType == 'x-vcalendar') 
					part.mmsTypeClass = "mms-vcal";
				else if (textType == 'x-vcard') 
					part.mmsTypeClass = "mms-vcard";
				mmsContent += Mojo.View.render({object:part, template:this.messageTemplatePath + 'mms-generic'});
			} else if (part.mimeType != "application/smil") { // filter out the smil
				// This is an attachment that we don't handle -> just display the name
				mmsContent += Mojo.View.render({object:part, template:this.messageTemplatePath + 'mms-generic'});
			}
			partId = id + "_" + (++partCount);
		}

		item.hasAttachmentData = true; // data for attachments have been put on the item model
		// Special cases for attachments that are handled in the itemRendered callback
		if (item.hasImageAttachment) {
			mmsContent += Mojo.View.render({object:{messageId:id}, template:this.messageTemplatePath + 'mms-image-container'});
		}
		if (item.hasAudioAttachment) {
			mmsContent += Mojo.View.render({object:{messageId:id}, template:this.messageTemplatePath + 'mms-audio-container'});
		}
		
		// store the image/audio data that will be used in the itemRendered callback
		item.imageData = imageData;
		item.audioData = audioData;
		item.mmsAttachments = mmsContent; // inject all the attachment content that we just built

		// Adjust the scroll position if it changes after calling noticeUpdatedItems
		if (referenceItem) {
			referenceItemOffsetBeforeUpdate = Mojo.View.viewportOffset(referenceItem).top;
		}

		this.chatListWidget.mojo.noticeUpdatedItems(offset, [item]);

		if (referenceItem) {
			referenceItemOffsetAfterUpdate = Mojo.View.viewportOffset(referenceItem).top;
			if (referenceItemOffsetBeforeUpdate != referenceItemOffsetAfterUpdate) {
				this.controller.sceneScroller.mojo.adjustBy(0, (referenceItemOffsetBeforeUpdate - referenceItemOffsetAfterUpdate));
			}
		}
	},
	
	getOnScreenItem: function(excludeId) {
		var loadedItems = this.chatListWidget.querySelectorAll(".chat-entry");
		var windowHeight = this.controller.window.innerHeight;
		var node;
		var closestOffset;
		var bestNode;
		var topOffset;
		if (loadedItems) {
			for (var i = loadedItems.length - 1; i >= 0; i--) {
				node = loadedItems[i];
				var messageId = node.getAttribute('messageId');
				if (node) {
					topOffset = Mojo.View.viewportOffset(node).top;
					if (topOffset >= 0 && topOffset <= windowHeight && (!excludeId || excludeId != messageId)) {
						return node;
					} else {
						var adjustedOffset;
						if (topOffset < 0) {
							adjustedOffset = Math.abs(topOffset);
						} else if (topOffset > this.controller.window.innerHeight) {
							adjustedOffset = (topOffset - windowHeight);
						}
						if (!closestOffset || closestOffset > adjustedOffset) {
							closestOffset = adjustedOffset;
							bestNode = node;
						}
						
					}
				}
			}
		}
		return bestNode;
	},
	
	handleMessageDelete: function(event) {
		this.listReset = true;
		MessagingMojoService.deleteMessage(this.controller, event.item.messageId);
	},
	
	handleMessageDeleteFromContextMenu: function(messageId, index) {
		this.controller.showAlertDialog({
			onChoose: function(value) {
				if (value === 'delete') {
					this.chatListWidget.mojo.noticeRemovedItems(index, 1);
			
					this.listReset = true;
					MessagingMojoService.deleteMessage(this.controller, messageId);
				}
			},
			title: $L("Delete Message"),
			message: $L("Are you sure you want to delete this message?"),
			choices: [
				{label:$L('Delete'),  value:'delete', type:'negative'},
				{label: $L("Cancel"), value: 'cancel'}
			]
		});
		
	},

	/**
	 * handle context menu selection after tap on SMS/IM message
	 * 
	 * @param {Object} text
	 * @param {Object} messageId
	 * @param {Object} index
	 * @param {Object} value
	 */
	handleContextMenuSelectSmsIm: function(text, messageId, index, value) {
		Mojo.Log.info("+++++handleContextMenuSelectSmsIm");
		
		Mojo.Log.info("value " + value);
		
		if (value === 'forward-cmd') {
			var composeParams = {
				messageText: text,
				focusWindow: true,
				launchedAsForward: true
			};
				
			this.controller.stageController.pushScene("compose", composeParams);
		} else if (value === 'forward-as-email-cmd') {
			this.forwardAsEmail({text: text});
		} else if (value === 'copy-cmd') {
			Mojo.Log.info("text to copy : " + text);
			this.addTextToClipboard(text);
		} else if (value === 'delete-cmd') {
			this.handleMessageDeleteFromContextMenu(messageId, index);
		}
	},
	
	/**
	 * context menu handler for tap on MMS in chat list
	 * 
	 * @param {Object} paramsIn
	 * @param {Object} value
	 */
	handleContextMenuSelectMms: function(paramsIn, value) {
		Mojo.Log.info("+++++handleContextMenuSelectMms");
		
		if (value === 'forward-cmd') {
			var composeParams = {
				messageId: paramsIn.messageId,
				chatBalloonDiv: paramsIn.html,
				focusWindow: true
			};

			this.controller.stageController.pushScene("mmsforward", composeParams);
		} else if (value === 'forward-as-email-cmd') {
			var composeParams = {
				summary: paramsIn.summary,
				text: paramsIn.copyText,
				attachments: [{uri:paramsIn.filePath, displayName:paramsIn.fileName}]
			};
			this.forwardAsEmail(composeParams);
		} else if (value === 'view-image-cmd') {
			this.controller.stageController.pushScene('imageview', paramsIn.filePath);
		} else if (value === 'play-video-cmd') {
			var args = {
				appId: "com.palm.app.videoplayer",
				name: "nowplaying"
			};
			var videoPlayParams = {
				target: paramsIn.filePath,
				title: paramsIn.fileName
			};
			this.controller.stageController.pushScene(args, videoPlayParams);
			return;
		} else if (value === 'add-contact-cmd') {
			MessagingMojoService.launchFile(this.controller, 'text/x-vcard', 'file://' + paramsIn.filePath);
			return;
		} else if (value === 'add-calendar-cmd') {
			MessagingMojoService.launchFile(this.controller, 'text/x-vcalendar', 'file://' + paramsIn.filePath);
			return;
		} else if (value === 'delete-cmd') {
			this.handleMessageDeleteFromContextMenu(paramsIn.messageId, paramsIn.index);
		} else if (value === 'copy-text-cmd') {
			var text;
			if (paramsIn.summary && paramsIn.summary.length > 0) {
				text = paramsIn.summary + "\n" + paramsIn.copyText;
			} else {
				text = paramsIn.copyText;
			}
			this.addTextToClipboard(text);
		}
	},
	
	/**
	 * handler for message tap within chat list
	 *
	 * @param {Object} event
	 */
	handleMessageTap: function(event) {
		//Mojo.Timing.reportTiming("ChatviewAssistant", "ChatviewAssistant");
		//Mojo.Timing.reset("ChatviewAssistant");		
		/*
		 * Special handling for delayed MMS status. First tap starts download.
		 * If the request is in progress, then ignore the tap
		 */
		var status = event.item.status;
		if (status == this.Messaging.MessageStatus.DELAYED_DELIVERY) {
			event.item.label = $L("Downloading...");
			event.item.status = this.Messaging.MessageStatus.RETRIEVING_CONTENT;
			this.controller.get('download-button_' + event.item.messageId).mojo.activate();
			this.controller.get('download-button_' + event.item.messageId).className = 'active';
			this.controller.modelChanged(event.item);

			var failHandler = function(item, error) {
				Mojo.Log.error("failHandler %i %j", item.messageId, error)
				item.status = this.Messaging.MessageStatus.DELAYED_DELIVERY
				this.controller.get('download-button_' + event.item.messageId).mojo.deactivate();
				item.label = $L("Download MMS");
				this.controller.modelChanged(item);
			}.bind(this, event.item);
			MessagingMojoService.downloadDelayedMessage(this.controller, event.item.messageId, Mojo.doNothing, failHandler);
			return;
		} else if (status == this.Messaging.MessageStatus.RETRIEVING_CONTENT) {
			Mojo.Log.info("TODO: handleMessageTap -- in the process of retrieving mms");
			return;
		}

		/*
		 * see if they tapped a link, phone number, email etc.
		 */
		var eventTarget = this.controller.get(event.originalEvent.target);
		if ((eventTarget.tagName === "A") &&
		(eventTarget.href !== undefined)) {
			Mojo.Log.info("link tapped no context menu");
			return;
		}
		
		/*
		 * look to see if they tapped error target first
		 */
		if (eventTarget.hasClassName('error')) {
			Mojo.Log.info("hit error target")
			MessagingUtils.simpleListClick(this.controller.get(event.originalEvent.target), "chatRow", function(targetRow) {
				var messageData = {
					errorCode: event.item.statusErrorCode,
					status: event.item.status,
					messageId: event.item.messageId,
					flags: event.item.flags,
					messageType: event.item.messageType
				};
			
				MessagingMojoService.getMessageErrorInfo(this.controller, messageData.messageId, messageData.flags, this.handleMessageErrorPopup.bind(this, messageData));
		
			}.bind(this), false);
			return;
		}
		
		/*
		 * Action determined on where where in the chat row they tapped
		 */
		var chatRowTarget = MessagingUtils.getClassUpChain(eventTarget, 'chatRow');
		if (chatRowTarget) {
			Mojo.Log.info("got chat row tap");
			
			var messageId = event.item.messageId;
			var messageType = event.item.messageType;
			
			Mojo.Log.info("messageId " + messageId);
			Mojo.Log.info("messageType " + messageType);
			Mojo.Log.info("index " + event.index);
			
			var composeParams;
			if ((messageType === "SMS") || (messageType === "IM")) {
				this.controller.popupSubmenu({
					onChoose: this.handleContextMenuSelectSmsIm.bind(this, event.item.textForCopying, messageId, event.index),
					placeNear: eventTarget.target,
					items: [
						{label: $L('Forward'), command: 'forward-cmd'},
						{label: $L('Forward Via Email'), command: 'forward-as-email-cmd'},
						{label: $L('Copy Text'), command: 'copy-cmd'},
						{label: $L('Delete'), command: 'delete-cmd'}
					]
				});

			} else if (messageType === "MMS") {
			
				Mojo.Log.info("++++++MMS Text content was " + eventTarget.textContent);
				
				/*
				 * chat-balloon-wrapper contains the visible div of the MMS which 
				 * is the right level to look for text to copy, or send on to the
				 * forward scene to show the MMS
				 */
				var chatBalloonWrapper = MessagingUtils.getClassUpChain(eventTarget, 'chat-balloon-wrapper');
				if (chatBalloonWrapper == null) {
					chatBalloonWrapper = this.searchDownForChatBalloonWrapper(chatRowTarget);
				}
				
				if (chatBalloonWrapper == null) {
					Mojo.Log.error("unable to find chat-baloon-wrapper");
					return;
				}
				
				var params = {
					html: chatBalloonWrapper.innerHTML,
					copyText: event.item.textForCopying,
					summary: event.item.summary,
					messageId: messageId,
					index: event.index
				};
				
				/*
				 * Check for the type of element the user tapped one by one and show the
				 * appropriate context menu
				 */
				var mmsTarget = MessagingUtils.getClassUpChain(eventTarget, 'MMSImageObject');
				if (mmsTarget) {
					params.filePath = mmsTarget.getAttribute('originalSrc');
					
					this.controller.popupSubmenu({
						onChoose: this.handleContextMenuSelectMms.bind(this, params),
						placeNear: mmsTarget,
						items: [
							{label: $L('View'), command: 'view-image-cmd'},
							{label: $L('Forward'), command: 'forward-cmd'},
							{label: $L('Forward Via Email'), command: 'forward-as-email-cmd'},
							{label: $L('Copy Text'), command: 'copy-text-cmd'},
							{label: $L('Delete'), command: 'delete-cmd'}
						]
					});
					
					return;
				}
				
				mmsTarget = MessagingUtils.getClassUpChain(eventTarget, 'mms-video');
				if (mmsTarget) {

					params.filePath = mmsTarget.getAttribute('filePath');
					params.fileName = mmsTarget.getAttribute('fileInfo');
					
					this.controller.popupSubmenu({
						onChoose: this.handleContextMenuSelectMms.bind(this, params),
						placeNear: mmsTarget,
						items: [
							{label: $L('Play'), command: 'play-video-cmd'},
							{label: $L('Forward'), command: 'forward-cmd'},
							{label: $L('Forward Via Email'), command: 'forward-as-email-cmd'},
							{label: $L('Copy Text'), command: 'copy-text-cmd'},
							{label: $L('Delete'), command: 'delete-cmd'}
						]
					});
					return;
				}

				mmsTarget = MessagingUtils.getClassUpChain(eventTarget, 'mms-vcard');
				if (mmsTarget) {

					params.filePath = mmsTarget.getAttribute('filePath');
					
					this.controller.popupSubmenu({
						onChoose: this.handleContextMenuSelectMms.bind(this, params),
						placeNear: mmsTarget,
						items: [
							{label: $L('Add to Contacts'), command: 'add-contact-cmd'},
							{label: $L('Forward'), command: 'forward-cmd'},
							{label: $L('Forward Via Email'), command: 'forward-as-email-cmd'},
							{label: $L('Copy Text'), command: 'copy-text-cmd'},
							{label: $L('Delete'), command: 'delete-cmd'}
						]
					});
					return;
				}
				
				/*
				 mmsTarget = MessagingUtils.getClassUpChain(eventTarget,'mms-vcal');
				 if (mmsTarget){
				 
				 params.filePath = mmsTarget.getAttribute('filePath');
				 
				 this.controller.popupSubmenu({
				 onChoose:  this.handleContextMenuSelectMms.bind(this, params),
				 placeNear: mmsTarget,
				 items: [
				 {label: $L('Add to Calendar'), command: 'add-calendar-cmd'},
				 {label: $L('Forward'), command: 'forward-cmd'},
				 {label: $L('Forward Via Email'), command: 'forward-as-email-cmd'},
				 {label: $L('Copy Text'), command: 'copy-text-cmd'},
				 {label: $L('Delete'), command: 'delete-cmd'},
				 ]
				 });
				 return;
				 }*/
				
				mmsTarget = MessagingUtils.getClassUpChain(eventTarget, 'mms-audio-container');
				if (mmsTarget) {
					Mojo.Log.info("audio target, ignore");
					/*
					 * Audio object plays inline and I cant really pop a contect menu up
					 */
					return;
				}
				
				/*
				 * default behavior
				 */

				// this is kinda hacktastic, but it will do for now. The whole attachment handling needs to
				// be made more generic at some point. For now ignore multiple attachments and just use the
				// first attachment in the bubble. 
				var mmsAttachmentElem = chatBalloonWrapper.down('.MMSImageObject');
				if (mmsAttachmentElem !== undefined) {
					params.filePath = mmsAttachmentElem.getAttribute('originalSrc');
				} else {
					mmsAttachmentElem = chatBalloonWrapper.down('.mms-video');
					if (mmsAttachmentElem !== undefined) {
						params.filePath = mmsAttachmentElem.getAttribute('filePath');
						params.fileName = mmsAttachmentElem.getAttribute('fileInfo');
					} else {
						mmsAttachmentElem = chatBalloonWrapper.down('.mms-vcard');
						if (mmsAttachmentElem !== undefined) {
							params.filePath = mmsAttachmentElem.getAttribute('filePath');
						} else {
							mmsAttachmentElem = chatBalloonWrapper.down('.mms-vcal');
							if (mmsAttachmentElem !== undefined) {
								params.filePath = mmsAttachmentElem.getAttribute('filePath');
							}
						}
					}
				}
				this.controller.popupSubmenu({
					onChoose: this.handleContextMenuSelectMms.bind(this, params),
					placeNear: eventTarget.target,
					items: [
						{label: $L('Forward'), command: 'forward-cmd'},
						{label: $L('Forward Via Email'), command: 'forward-as-email-cmd'},
						{label: $L('Copy Text'), command: 'copy-text-cmd'},
						{label: $L('Delete'), command: 'delete-cmd'}
					]
				});
			}
		}
	},
	
	/**
	 * Search out the chat-balloon-wrapper element starting at the chat-row
	 * 
	 * @param {Object} chatRowTarget
	 */
	searchDownForChatBalloonWrapper: function(chatRowTarget) {
		Mojo.Log.info("tap above level of chat-balloon-wrapper, look inside");
		
		for (var i = 0; i < chatRowTarget.children.length; i++) {
			var palmRowWrapper = chatRowTarget.children.item(i);
					
			if (palmRowWrapper.hasClassName('palm-row-wrapper')) {
				for (var j = 0; j < palmRowWrapper.children.length; j++) {
					var chatBalloon = palmRowWrapper.children.item(j);
					if (chatBalloon.hasClassName('chat-balloon')) {
						for (var k = 0; k < chatBalloon.children.length; k++) {
							var chatBalloonWrapper = chatBalloon.children.item(k);
							if (chatBalloonWrapper.hasClassName('chat-balloon-wrapper')) {
								Mojo.Log.info("found chat-balloon-wrapper");
								return chatBalloonWrapper;
							}
						}
					}
				}
			}
		}
		
		return null;
	},
	

	handleMessageErrorPopup: function(messageData, messageErrorData) {
		var that = this;
		var dialogChoices = [];
		var retryTransport = null;
		var title = undefined;
		
		Mojo.Log.error("*** ERROR CODE: " + messageData.errorCode + " status: " + messageData.status);
		
		// MMS ERRORS
		if (messageData.status != this.Messaging.MessageStatus.SUCCESS && messageData.errorCode && messageData.errorCode > 0) {
			
			if (ChatFlags.isOutgoing(messageData.flags)) {
				title = $L("Unable To Send Message");
			} else {
				title = $L("Unable to Download Message");
			}
			
			// provide a retry option for failed incoming MMS messages
			if (messageData.status == this.Messaging.MessageStatus.FAILED && !ChatFlags.isOutgoing(messageData.flags) && messageData.messageType === "MMS") {
				dialogChoices.push({
					label: $L('Retry message fetch'),
					value: 'retry'
				});
			} else if (messageData.status == this.Messaging.MessageStatus.FAILED && ChatFlags.isOutgoing(messageData.flags) && messageData.messageType === "MMS") {
				
				if (messageErrorData && messageErrorData.list) {
					for (var i = 0; i < messageErrorData.list.length; i++) {
						var recipient = messageErrorData.list[i];
						var transport = that.getTransportFromPhoneNumber(recipient.address);
						if (transport) {
							retryTransport = transport;
						}
					}
					if (retryTransport) {
						dialogChoices.push({
							label: $L('Send again'),
							value: 'sendagain'
						});
					}
				}
			}
			
			dialogChoices.push({
				label: $L('Ok'),
				value: 'Close'
			});

			this.controller.showAlertDialog({
				onChoose: function(messageId, choice) {
					if ( (choice === 'retry') || 
							(choice === 'sendagain') ){
						MessagingMojoService.reFetch(this.controller, messageId);
					}
				}.bind(this, messageData.messageId),
				title: title,
				message: CONSTANTS.ERROR[messageData.errorCode],
				preventCancel: false,
				choices: dialogChoices
			});
		
		// SMS Errors
		} else if ((!messageData.errorCode || messageData.errorCode == 0) && (messageData.status == this.Messaging.MessageStatus.FAILED || messageData.status == this.Messaging.MessageStatus.UNDELIVERABLE)) {
			var failure = [];
			var success = [];
			var pending = [];
			var params = {
				hasFailureClass: "hide",
				hasSuccessClass: "hide",
				hasPendingClass: "hide"
			};
	
			if (messageErrorData.list && messageErrorData.list.length > 0) {
				messageErrorData.list.each(function(recipient) {
					var recipientName = MessagingUtils.generateNameString(recipient);
					var recipientErrorCode = (recipient.errorCode) ? recipient.errorCode.toLowerCase() : "";
					if (recipientErrorCode == "success") {
						success.push(recipientName);
					} else if (recipientErrorCode == "pending") {
						pending.push(recipientName);
						var transport = that.getTransportFromPhoneNumber(recipient.address);
						if (transport) {
							retryTransport = transport;
						}
					} else {
						var transport = that.getTransportFromPhoneNumber(recipient.address);
						if (transport) {
							retryTransport = transport;
						}
						
						var causeCodeStr = "";
						var tmp = "#{recipientName}";
						var recipientError = that.Messaging.Error.getMessageErrorFromCode(recipientErrorCode);
						if (recipient.causeCode && recipient.causeCode > 0) 
							causeCodeStr = new Template($L("(code: #{causeCode})")).evaluate({causeCode: recipient.causeCode});
						if (recipientError) 
							tmp += " #{recipientError}";
						if (causeCodeStr) 
							tmp += " #{causeCodeStr}";
	
						if (recipientErrorCode == "fdnrestricted") {
							tmp += $L(" (FDN Restricted)");
						}
						
						failure.push(new Template($L(tmp)).evaluate({
							recipientName: recipientName,
							recipientError: recipientError,
							causeCodeStr: causeCodeStr
						}));
					}
				});
				if (failure.length > 0) {
					params.hasFailureClass = "";
					params.failureRecipients = new Template($L("Message failed to: #{failure}")).evaluate({
						failure: failure.join(", ")
					});
				}
				if (success.length > 0) {
					params.hasSuccessClass = "";
					params.successRecipients = new Template($L("Message successfully sent to: #{success}")).evaluate({
						success: success.join(", ")
					});
				}
				if (pending.length > 0) {
					params.hasPendingClass = "";
					params.pendingRecipients = new Template($L("Message will be sent to: #{pending}")).evaluate({
						pending: pending.join(", ")
					});
				}
			}
		
			var errorContent = Mojo.View.render({
				object: params,
				template: 'chatview/message-error'
			});
	
			if (retryTransport && messageData.status == this.Messaging.MessageStatus.FAILED) {
				dialogChoices.push({
					label: $L('Send again'),
					value: 'sendagain'
				});
			}
			dialogChoices.push({
				label: $L('Ok'),
				value: 'Close'
			});
	
			this.controller.showAlertDialog({
				onChoose: function(choice) {
					if (choice === 'sendagain') {
						var params = {
							chatThreadId: this.chatThreadId,
							recipient: retryTransport.value,
							messageText: "", // will be set in handleGetMessageDetails
							contactPointId: retryTransport.contactPointId
						};
						MessagingMojoService.getMessageDetails(this.controller, messageData.messageId, this.handleErrorGetMessageDetailsForSendAgain.bind(this, params));
					}
				}.bind(this),
				message: errorContent,
				allowHTMLMessage: true,
				preventCancel: false,
				choices: dialogChoices
			});
		}
	},
	
	
	// Called for SMS messages
	handleErrorGetMessageDetailsForSendAgain: function(params, result) {
		if (result && result.record && result.record.messageText) {
			params.messageText = result.record.messageText;
		}
		this.sendMessageHelper(this.controller, params);
		this.revealBottomHack(); // snap to the bottom when the user sends a message
	},
	
	// Called for MMS messages
	handleErrorGetMMSPartsForSendAgain: function(params, parts) {
		params.summary = '';
		for (var i = 0; i < parts.list.length; i++) {
			var part = parts.list[i];
			if (part.mimeType) {
				if (part.mimeType && part.mimeType == "text/plain" && !part.uri) {
					if (part.partText) {
						params.messageText = part.partText;
					}
				} else if (part.mimeType.substring(0, 5) == 'image') {
					if (part.uri) {
						params.attachment = part.uri;
					}
				}
			}
		}
		this.sendMessageHelper(this.controller, params);
		this.revealBottomHack(); // snap to the bottom when the user sends a message	
	},
	
	// iterate over our array of transports and look for one whose address matches
	getTransportFromPhoneNumber: function(phoneNumber) {
		for (var i = 0; i < this.transports.length; i++) {
			var transport = this.transports[i];
			if (!transport.serviceName && transport.value == phoneNumber) {
				return transport;
			}
		}
		return null;
	},
	
	setTextFieldValue: function(value) {
		if (value != undefined) {
			this.messageTextWidgetModel.value = value;
			this.controller.modelChanged(this.messageTextWidgetModel);
			// make sure the right action button is showing if there is any text in the textbox
			this.handleTextAreaChange();
		}
	},
	
	handleTextAreaKeyDown: function(event) {
		// Check for return keys, and put a fake highlight on the send button.
		// It will be cleared on key up.
		if (event && Mojo.Char.isEnterKey(event.keyCode)) {
			this.sendButton.addClassName('selected');			
		}
	},

	handleTextAreaKeyUp: function(event) {
		if (event && Mojo.Char.isEnterKey(event.keyCode)) {
			this.considerForSend();
			Event.stop(event);
			
			// Remove highlight that was added on keydown.
			this.sendButton.removeClassName('selected');
		}
	},

	handleTextAreaChange: function() {
		if (this.messageTextElement && this.messageTextElement.value == '') {
			this.actionButton.removeClassName('textentry');
			this.actionButton.addClassName('empty');
		} else {
			this.actionButton.addClassName('textentry');
			this.actionButton.removeClassName('empty');
		}
		
		this.characterCounter.deferredResetCount();
	},
	
	// This method is called when the send button is clicked or the enter key is fired in the textbox
	// This is our fallback logic when attempting to send to an offline user
	considerForSend: function(forceSendIfOffline) {
		// Make sure that forceSendIfOffline is really a boolean in the falsy case
		// This will sometimes be an object because we pass this method directly when we observe
		// Mojo.Event.tap on the send button
		if (forceSendIfOffline && forceSendIfOffline != true) 
			forceSendIfOffline = false;
		
		// prevent blank messages from going through
		var message = this.messageTextElement.value.strip();
		if ((!this.attachment && message.length > 0) || this.attachment) {
			// if the current message is longer than the max size, alert the user and block the send action
			if (!this.characterCounter.isValidLength()) {
				this.controller.showAlertDialog({
					onChoose: function(){},
					title: $L("Message length error"),
					message: $L("The message that you have entered is larger than the allowed size."),
					preventCancel: false,
					choices: [{
						label: $L('Ok'),
						value: 'Close'
					}]
				});
				return;
			}
			
			// If the current transport is IM + it has gone offline + you are not offline, display a dialog to choose another transport
			// we will also give the user an option to force the send
			var account = this.Messaging.IMAccounts.getAccountFromAccountTagOrServiceName(this.selectedTransport.accountTag, this.selectedTransport.serviceName);
			if (!forceSendIfOffline && this.selectedTransport.serviceName &&
				this.selectedTransport.availability == this.Messaging.Availability.OFFLINE && 
				account && account.availability != this.Messaging.Availability.OFFLINE) {
					
				this.controller.showAlertDialog({
					onChoose: function(choice) {
						if (choice === 'SendAnyway') {
							this.considerForSend(true);
						}
					}.bind(this),
					title: $L("Recipient is offline"),
					message: $L("#{chatAddress} is offline. What would you like to do?").interpolate(this.selectedTransport),
					preventCancel: false,
					choices: [{
						label: $L('Send Anyway'),
						value: 'SendAnyway'
					},
					{
						label: $L('Cancel'),
						value: 'Cancel'
					}]
				});
			} else {
				var segments = this.characterCounter.getSegmentData();
				if (segments.segmentCount) {
					for (var x = 0; x < segments.segmentCount; x++) {
						this.sendMessage(segments.segments[x]);
					}
					this.resetTextBox(true);
					this.revealBottomHack(); // snap to the bottom when the user sends a message
				} else {
					Mojo.Log.warn("Segment count is zero or not set."); // This should not happen
				}
			}
		}
	},
	
	sendMessage: function(message) {
		var recipient = this.selectedTransport.chatAddress;
		
		// Edge cases:
		// 1. If the user has a partial number stored in contacts, and we are using the chatAddress to send, the moment that they 
		//    send or receive on a different transport in the same chat, we will have lost the phone number that was stored in the chat address
		//    switching back to the phone number that was being used previously will fail to send 
		// 2. If the transport picker contains a valid short code that happens to be a subset of a different phone number in the transport picker
		//    Then it is possible for us to send using the chatAddress when we should really use what is in the transport picker
		if (!this.selectedTransport.serviceName && !this.chatData.serviceName && this.chatData.chatAddress) {
			var chatNum = MessagingUtils.cleanPhoneNumber(this.chatData.chatAddress);
			var transportNum = MessagingUtils.cleanPhoneNumber(recipient);
			if (transportNum.length >= 7 && chatNum.length > transportNum.length) {
				var isMatch = true;
				for (var i = 1; i <= transportNum.length && isMatch; i++) {
					if (transportNum[transportNum.length - i] != chatNum[chatNum.length - i]) {
						isMatch = false;
					}
				}
				if (isMatch) {
					recipient = this.chatData.chatAddress;
				}
			}
		}
		
		var params = {
			chatThreadId: this.chatThreadId,
			recipient: recipient,
			messageText: message
		};
		if (this.selectedTransport.contactPointId) 
			params.contactPointId = this.selectedTransport.contactPointId;
		if (this.attachment) { // sending an attachment
		
			Mojo.Log.logProperties("this.attachment", this.attachment);
			params.attachment = this.attachment;
			var beginning = this.attachment.substr(0, 5);
			Mojo.Log.info("attachment in " + beginning);
			params.transcodeParams = this.transcodeParams;
			if (this.attachmentIsTmpFile){
				/*
				 * delete this attach
				 */
				Mojo.Log.info("setting attachment to be deleted on finish");
				params.deleteAttachmentWhenDone = true;
			}
			//params.summary = $L('New picture message');
			params.summary = '';
		} else { // sending a normal message 
			if (this.selectedTransport.serviceName) 
				params.serviceName = this.selectedTransport.serviceName;
		}
		this.sendMessageHelper(this.controller, params);
	},
	
	addStatusMessageToChat: function(message, transientMsgType) {
		this.chatHasTransientStatusMessage = true;
		var params = {
			chatThreadId: this.chatThreadId,
			transientMsgType: transientMsgType,
			messageText: message,
			recipient: this.selectedTransport.chatAddress,
			serviceName: this.selectedTransport.serviceName
		};
		this.sendMessageHelper(this.controller, params);
	},
	
	sendMessageHelper: function(controller, params) {
		MessagingMojoService.sendMessageFromChat(this.controller, params);
	},
	
	resetTextBox: function(resetValue) {
		if (resetValue === true) {
			this.setTextFieldValue("");
			this.characterCounter.resetCount();
		}
		if (this.messageTextElement.value.length === 0) {
			this.actionButton.addClassName('empty');
			this.actionButton.removeClassName('textentry');
		}
		this.controller.get('attachmentContainer').hide();
		this.controller.get('attachmentImage').src = "";
		this.attachment = null;
		this.setCharacterCounterMaxLength();
		this.handleTextFieldResize();
	},
	
	_fixListScrollPositionWhenTextFieldResize: function(messageTextWidget, listElement, event) {
		var height = messageTextWidget.offsetHeight + 6;
		if (messageTextWidget && (!this.textFieldHeight || this.textFieldHeight != height)) {
			if (!this.textFieldHeight) {
				this.textFieldHeight = height;
			}
			listElement.setStyle({"padding-bottom": height + "px"});
			this.controller.sceneScroller.mojo.adjustBy(0, (this.textFieldHeight - height));
			this.textFieldHeight = height;
		}
	},
	
	_fixListScrollPositionOnLoad: function(messageTextWidget, listElement, setPaddingOnly) {
		if (!messageTextWidget) {
			return;
		}
		var height = messageTextWidget.offsetHeight + 6;
		if (!this.textFieldHeight) {
			this.textFieldHeight = height;
		}
		
		var doRevealBottom = false;
		if (!setPaddingOnly && this.getCurrentBottom() < this.MAX_BOTTOM_HEIGHT_FOR_SNAP) {
			doRevealBottom = true;
		}

		listElement.setStyle({"padding-bottom": height + "px"});
		
		if (doRevealBottom) {
			this.revealBottomHack();
		}
	},
	
	attachFilePicker: function(event) {

		Mojo.Log.info("++++++ maxMessageAttachmentSize " + this.Messaging.carrierSettings.mmscMessageSizeLimit);
		var params = {
			actionType: 'attach',
			kinds: ['image','video'], // TODO: integrate other media types
			defaultKind: 'image',
			maxVideoSize: this.Messaging.carrierSettings.mmscMessageSizeLimit,
			tempVideoFilePath: '/media/internal/.attachments/mms/mmsVideo2',
			onSelect: this.handleFilePickerSelect.bind(this)
		};
		
		Mojo.FilePicker.pickFile(params, this.controller.stageController);
	},
	
	handleFilePickerSelect: function(file) {
		
		//"crossApp1262900390599", "callbackParams": [ { "select": { "fullPath": "\/media\/internal\/DCIM\/100PALM\/CIMG0002.mp4", "iconPath": "\/var\/luna\/data\/extractfs\/media\/internal\/DCIM\/100PALM\/CIMG0002.mp4:621790:17423:75:64:3", "size": "639213", "isTmpVideoFile": false } } ] }] 
		
		this.attachFile(file.fullPath, file.iconPath, file.isTmpVideoFile, file.attachmentType, file.transcodeParams);
	},
	
	attachFile: function(imagePath, displayPath, attachmentIsTempFile, attachmentType, transcodeParams) {
		if (imagePath) {
			var lowerCaseImagePath = imagePath.toLocaleLowerCase();
			if (attachmentType == "video") {
				if ((!displayPath) || (displayPath.length === 0) ){
					Mojo.Log.info("Using Generic video attachment icon as none specified by caller");	
					displayPath = Mojo.appPath + "images/compose-attachment-video.png";
				}
			} else if ( (lowerCaseImagePath.endsWith(".jpg")) ||
						((lowerCaseImagePath.endsWith(".jpeg"))) ||
						((lowerCaseImagePath.endsWith(".png"))) ){
					//always use extractfs for images, even if iconPath came in		
					Mojo.Log.info("attached a JPG/PNG image");
					displayPath = MessagingUtils.extractfs(imagePath,{width:200,height:100,crop:3});
			}else{
				if (!displayPath){
					if (imagePath.endsWith('.vcf')){
						Mojo.Log.info("VCard attachment");
						displayPath = Mojo.appPath + "images/compose-attachment-vcard.png";
					}else{
						Mojo.Log.info("Generic attached");
						displayPath = Mojo.appPath + "images/compose-attachment-generic.png";
					}
					
				}
			}
			this.controller.get('attachmentContainer').show();
			this.actionButton.addClassName('textentry');
			this.actionButton.removeClassName('empty');
			this.controller.get('attachmentImage').src = displayPath;
			this.attachment = imagePath;
			this.transcodeParams = transcodeParams;
			this.attachmentIsTmpFile = attachmentIsTempFile;
			this.setCharacterCounterMaxLength();
		}
	},
	
	/** 
	 * Render the invitation UI if we have at least one invitation
	 * @param callbackIfempty - if there are no invitations then execute
	 *                          this callback if it has been provided
	 *                          else render the full chatview UI
	 */
	renderInvitationUI: function(callbackIfempty) {
		if (this.invitations.length > 0) {
			var invitationHash = this.invitations.pop();
			invitationHash.fromText = new Template($L("From: #{address}")).evaluate({
				address: invitationHash.address
			});
			
			// hide chatview elements that we don't want to see &
			// show the invitations UI
			this.transportPicker.hide();
			var footerInvitationContent = Mojo.View.render({
				object: invitationHash,
				template: "chatview/invitation-footer"
			});
			this.controller.get('footerInvitation').update(footerInvitationContent);
			this.controller.get('footerInvitation').show();
			this.controller.get('footer').hide(); // hide the chatview footer
			// observe the accept/ignore buttons
			this.acceptInvitation = this.controller.get('acceptInvitation');
			this.rejectInvitation = this.controller.get('rejectInvitation');
			/* 
			//This needs to be cleaned up when we start supporting invitations
			this.acceptInvitation.observe(Mojo.Event.tap, this.handleInvitationAccept.bind(this).curry(invitationHash, this.handleAcceptInvitation.bind(this)));
			this.rejectInvitation.observe(Mojo.Event.tap, this.handleRejectInvitation.bind(this).curry(invitationHash));
			*/
			return;
		}
		
		this.controller.get('footer').show();
		this.controller.get('footerInvitation').hide();
		this.transportPicker.show();
		
		// Do this after hiding/showing elements in case the view has been
		// popped in the callbackIfempty()
		if (callbackIfempty) {
			callbackIfempty();
		}
	},
	
	handleInvitationAccept: function(invitationHash, callback, event) {
		var listItems = [{label:$L('Add buddy to...'), command:"", disabled:true}, {label:$L('Groups')}];
		// Retrieve groups that this invitation can be accepted into
		MessagingMojoService.getGroupsForInvitation(this.controller, function(result) {
			result.list.each(function(group) {
				listItems.push({
					label: group.groupName,
					command: group.id
				});
			});
			
			this.controller.popupSubmenu({
				onChoose: callback.bind(this).curry(invitationHash),
				placeNear: event.target,
				items: listItems
			});
		}.bind(this));
	},
	
	handleAcceptInvitation: function(invitationHash, groupId) {
		MessagingMojoService.acceptBuddy(this.controller, this.chatThreadId, invitationHash.id, invitationHash.address, groupId, function(result) {
			if (result.personId) {
				this.personId = result.personId;
				// if we have more invitations for this chat, render another invitation footer
				this.renderInvitationUI(function() {
					var params = {
						serviceName: this.selectedTransport.serviceName,
						chatAddress: this.selectedTransport.chatAddress,
						availability: this.selectedTransport.availability
					}
					this.renderTransportPicker(params);
				}.bind(this));
			}
		}.bind(this));
	},
	
	forwardAsEmail: function(params) {
		this.controller.serviceRequest('palm://com.palm.applicationManager', {
			method: 'open',
			parameters: {
				id: 'com.palm.app.email',
				params: params
			}
		});
	},
	
	addTextToClipboard: function(text) {
		if (text) {
			// Temporarily unassign the text element to prevent it from automatically
			// refocusing on blur, then do the copy-to-clipboard.
			var temp = this.messageTextElement;
			this.messageTextElement = undefined;
			// Note html tags are stripped from the text since the clipboard only supports plain text
			this.controller.stageController.setClipboard(text.stripTags(), false);
			this.messageTextElement = temp;
			this.messageTextElement.focus();
		}
	},
	
	copyConversationToClipboard: function() {
		var range = this.chatListWidget.mojo.getLoadedItemRange();
		var list = this.chatListWidget.mojo.getItems(range.offset, range.limit);
		if (list.length > 0) {
			var text = list.pluck('messageText').join("\n\n");
			this.addTextToClipboard(text);
		}
	},
	
	blockUsername: function() {
		var onChooseHandler = function(value) {
			if (value === 'block') {
				// If this chat was blocked, pop out to the listview
				var onSuccessFunc = this.popAndRevealListView.bind(this);
				MessagingMojoService.blockBuddy(this.controller, this.chatThreadId, true, onSuccessFunc);
			}
		}.bind(this);

		this.controller.showAlertDialog({
			onChoose: onChooseHandler,
			title: $L('Block this person'),
			message: $L("Are you sure you want to block this person?"),
			preventCancel: false,
			choices: [
				{label: $L('Block'), value: 'block', type: 'negative'},
				{label: $L('Cancel'), value: 'cancel', type: 'secondary'}
			]
		});
	},
	
	handleRejectInvitation: function(invitationHash, event) {
		MessagingMojoService.rejectBuddy(this.controller, this.chatThreadId, invitationHash.id, invitationHash.address, function(result) {
			this.renderInvitationUI(function() {
				if (result.chatIsEmpty) { // if the chat was deleted, pop out to the listview
					this.popAndRevealListView();
				}
			}.bind(this));
		}.bind(this));
	},
	
	updateBlockUsernameMenu: function() {
		// personId is only set if the chat is with someone in the user's contacts
		var hideBlockMenu = (this.chatData === undefined || this.chatData.type !== "IM" || this.personId !== undefined);
		var blockItemIndex = this.appMenuModel.items.indexOf(this.appMenuBlockUsernameItem);
		if (hideBlockMenu) {
			// -1 means it isn't in the list so it doesn't need to be removed
			if (blockItemIndex !== -1) {
				this.appMenuModel.items.splice(blockItemIndex, 1);
				this.controller.modelChanged(this.appMenuModel);
			}
		} else {
			if (blockItemIndex === -1) {
				this.appMenuModel.items.splice(this.appMenuModel.items.length - 1, 0, this.appMenuBlockUsernameItem);
				this.controller.modelChanged(this.appMenuModel);
			}
		}
	},
	
	showAttachButton: function() {
		this.controller.get('AttachButton').show();
		this.appMenuAddPictureItem.disabled = false;
		this.controller.modelChanged(this.appMenuAddPictureItem);
	},
	
	hideAttachButton: function() {
		this.controller.get('AttachButton').hide();
		this.appMenuAddPictureItem.disabled = true;
		this.controller.modelChanged(this.appMenuAddPictureItem);
	},
	
	getAudioElementCacheInstance: function() {
		if (!this.audioElementCache) {
			this.audioElementCache = function() {
				var elemHash = {};
				var elemArr = [];
				var maxSize = 10;
				
				return {
					get: function(id) {
						if (elemHash[id]) 
							return elemHash[id];
					},
					add: function(id, element) {
						elemHash[id] = element;
						elemArr.push(id);
						
						// remove the oldest element
						if (elemArr.length === maxSize) {
							delete elemHash[elemArr[0]];
							elemArr.splice(0, 1);
						}
					},
					hasElement: function(id) {
						if (id && elemHash[id]) 
							return true;
						return false;
					}
				}
			}();
		}
		return this.audioElementCache;
	},
	
	revealBottomHack: function() {
		//Mojo.Timing.resume("ChatviewAssistant#revealBottomHack");
		// do this twice because calling revealBottom() once sometimes only gets you close to the bottom
		this.controller.sceneScroller.mojo.revealBottom();
		this.controller.sceneScroller.mojo.revealBottom();
		//Mojo.Timing.pause("ChatviewAssistant#revealBottomHack");
	}
});

ChatviewAssistant.quickDialIdPrefix = "quick-dial-";
