UI/UX: refactor messages view
Change-Id: I2292ddf0d958aadc8171a86f12450ec56af0f6fc
Reviewed-by: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
diff --git a/src/MessagesVC.mm b/src/MessagesVC.mm
index 4892137..7e72aca 100644
--- a/src/MessagesVC.mm
+++ b/src/MessagesVC.mm
@@ -29,9 +29,12 @@
#import "MessagesVC.h"
#import "views/IMTableCellView.h"
#import "views/MessageBubbleView.h"
+#import "views/NSImage+Extensions.h"
#import "INDSequentialTextSelectionManager.h"
#import "delegates/ImageManipulationDelegate.h"
#import "utils.h"
+#import "views/NSColor+RingTheme.h"
+
@interface MessagesVC () <NSTableViewDelegate, NSTableViewDataSource> {
@@ -56,9 +59,37 @@
// Tags for view
NSInteger const GENERIC_INT_TEXT_TAG = 100;
+NSInteger const GENERIC_INT_TIME_TAG = 200;
+
+// views size
+CGFloat const GENERIC_CELL_HEIGHT = 60;
+CGFloat const TIME_BOX_HEIGHT = 34;
+CGFloat const MESSAGE_TEXT_PADDING = 10;
+CGFloat const MAX_TRANSFERED_IMAGE_SIZE = 250;
+CGFloat const BUBBLE_HEIGHT_FOR_TRANSFERED_FILE = 87;
@implementation MessagesVC
+//MessageBuble type
+typedef NS_ENUM(NSInteger, MessageSequencing) {
+ SINGLE_WITH_TIME = 0,
+ SINGLE_WITHOUT_TIME = 1,
+ FIRST_WITH_TIME = 2,
+ FIRST_WITHOUT_TIME = 3,
+ MIDDLE_IN_SEQUENCE = 5,
+ LAST_IN_SEQUENCE = 6,
+};
+
+- (void)awakeFromNib
+{
+ NSNib *cellNib = [[NSNib alloc] initWithNibNamed:@"MessageCells" bundle:nil];
+ [conversationView registerNib:cellNib forIdentifier:@"LeftIncomingFileView"];
+ [conversationView registerNib:cellNib forIdentifier:@"LeftOngoingFileView"];
+ [conversationView registerNib:cellNib forIdentifier:@"LeftFinishedFileView"];
+ [conversationView registerNib:cellNib forIdentifier:@"RightOngoingFileView"];
+ [conversationView registerNib:cellNib forIdentifier:@"RightFinishedFileView"];
+}
+
-(const lrc::api::conversation::Info*) getCurrentConversation
{
if (convModel_ == nil || convUid_.empty())
@@ -67,16 +98,57 @@
if (cachedConv_ != nil)
return cachedConv_;
- auto& convQueue = convModel_->allFilteredConversations();
-
auto it = getConversationFromUid(convUid_, *convModel_);
-
- if (it != convQueue.end())
+ if (it != convModel_->allFilteredConversations().end())
cachedConv_ = &(*it);
return cachedConv_;
}
+-(void) reloadConversationForMessage:(uint64_t) uid shouldUpdateHeight:(bool)update {
+ auto* conv = [self getCurrentConversation];
+
+ if (conv == nil)
+ return;
+ auto it = distance(conv->interactions.begin(),conv->interactions.find(uid));
+ NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:it];
+ //reload previous message to update bubbleview
+ if (it > 0) {
+ NSRange range = NSMakeRange(it - 1, it);
+ indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
+ }
+ if (update) {
+ [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
+ }
+ [conversationView reloadDataForRowIndexes: indexSet
+ columnIndexes:[NSIndexSet indexSetWithIndex:0]];
+ if (update) {
+ [conversationView scrollToEndOfDocument:nil];
+ }
+}
+
+-(void) reloadConversationForMessage:(uint64_t) uid shouldUpdateHeight:(bool)update updateConversation:(bool) updateConversation {
+ auto* conv = [self getCurrentConversation];
+
+ if (conv == nil)
+ return;
+ auto it = distance(conv->interactions.begin(),conv->interactions.find(uid));
+ NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:it];
+ //reload previous message to update bubbleview
+ if (it > 0) {
+ NSRange range = NSMakeRange(it - 1, it);
+ indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
+ }
+ if (update) {
+ [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
+ }
+ [conversationView reloadDataForRowIndexes: indexSet
+ columnIndexes:[NSIndexSet indexSetWithIndex:0]];
+ if (update) {
+ [conversationView scrollToEndOfDocument:nil];
+ }
+}
+
-(void)setConversationUid:(const std::string)convUid model:(lrc::api::ConversationModel *)model
{
if (convUid_ == convUid && convModel_ == model)
@@ -90,20 +162,42 @@
QObject::disconnect(newInteractionSignal_);
QObject::disconnect(interactionStatusUpdatedSignal_);
newInteractionSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newInteraction,
- [self](const std::string& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
- if (uid != convUid_)
- return;
- cachedConv_ = nil;
- [conversationView reloadData];
- [conversationView scrollToEndOfDocument:nil];
- });
+ [self](const std::string& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
+ if (uid != convUid_)
+ return;
+ cachedConv_ = nil;
+ [conversationView noteNumberOfRowsChanged];
+ [self reloadConversationForMessage:interactionId shouldUpdateHeight:YES];
+ });
interactionStatusUpdatedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::interactionStatusUpdated,
[self](const std::string& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
if (uid != convUid_)
return;
cachedConv_ = nil;
- [conversationView reloadData];
- [conversationView scrollToEndOfDocument:nil];
+ bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
+ if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
+ convModel_->refreshFilter();
+ }
+ [self reloadConversationForMessage:interactionId shouldUpdateHeight:YES];
+ //accept incoming transfer
+ if (interaction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER &&
+ (interaction.status == lrc::api::interaction::Status::TRANSFER_AWAITING_HOST ||
+ interaction.status == lrc::api::interaction::Status::TRANSFER_CREATED)) {
+ lrc::api::datatransfer::Info info = {};
+ convModel_->getTransferInfo(interactionId, info);
+ double convertData = static_cast<double>(info.totalSize);
+ NSString* pathUrl = @(info.displayName.c_str());
+
+ NSString* fileExtension = pathUrl.pathExtension;
+
+ CFStringRef utiType = UTTypeCreatePreferredIdentifierForTag(
+ kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtension, NULL);
+
+ bool isImage = UTTypeConformsTo(utiType, kUTTypeImage);
+ if( convertData <= 10485760 && isImage) {
+ [self acceptFile:interactionId];
+ }
+ }
});
// Signals tracking changes in conversation list, we need them as cached conversation can be invalid
@@ -118,84 +212,133 @@
[self](){
cachedConv_ = nil;
});
-
-
-
[conversationView reloadData];
[conversationView scrollToEndOfDocument:nil];
}
--(void)newMessageSent
-{
- [conversationView reloadData];
- [conversationView scrollToEndOfDocument:nil];
-}
+#pragma mark - configure cells
--(NSTableCellView*) makeGenericInteractionViewForTableView:(NSTableView*)tableView withText:(NSString*)text
+-(NSTableCellView*) makeGenericInteractionViewForTableView:(NSTableView*)tableView withText:(NSString*)text andTime:(NSString*) time
{
NSTableCellView* result = [tableView makeViewWithIdentifier:@"GenericInteractionView" owner:self];
NSTextField* textField = [result viewWithTag:GENERIC_INT_TEXT_TAG];
+ NSTextField* timeField = [result viewWithTag:GENERIC_INT_TIME_TAG];
// TODO: Fix symbol in LRC
NSString* fixedString = [text stringByReplacingOccurrencesOfString:@"🕽" withString:@"📞"];
[textField setStringValue:fixedString];
+ [timeField setStringValue:time];
return result;
}
--(IMTableCellView*) makeViewforTransferStatus:(lrc::api::interaction::Status)status type:(lrc::api::interaction::Type)type tableView:(NSTableView*)tableView
+-(NSTableCellView*) configureViewforTransfer:(lrc::api::interaction::Info)interaction interactionID: (uint64_t) interactionID tableView:(NSTableView*)tableView
{
IMTableCellView* result;
+ auto type = interaction.type;
+ auto status = interaction.status;
+
+ NSString* fileName = @"incoming file";
+
// First, view is created
if (type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER) {
switch (status) {
case lrc::api::interaction::Status::TRANSFER_CREATED:
- case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST:
- result = [tableView makeViewWithIdentifier:@"LeftIncomingFileView" owner:self];
- break;
+ case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST: {
+ result = [tableView makeViewWithIdentifier:@"LeftIncomingFileView" owner: conversationView];
+ [result.acceptButton setAction:@selector(acceptIncomingFile:)];
+ [result.acceptButton setTarget:self];
+ [result.declineButton setAction:@selector(declineIncomingFile:)];
+ [result.declineButton setTarget:self];
+ break;}
case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
- case lrc::api::interaction::Status::TRANSFER_ONGOING:
- result = [tableView makeViewWithIdentifier:@"LeftOngoingFileView" owner:self];
- [result.progressIndicator startAnimation:nil];
- break;
+ case lrc::api::interaction::Status::TRANSFER_ONGOING: {
+ result = [tableView makeViewWithIdentifier:@"LeftOngoingFileView" owner:conversationView];
+ [result.progressIndicator startAnimation:conversationView];
+ [result.declineButton setAction:@selector(declineIncomingFile:)];
+ [result.declineButton setTarget:self];
+ break;}
case lrc::api::interaction::Status::TRANSFER_FINISHED:
case lrc::api::interaction::Status::TRANSFER_CANCELED:
case lrc::api::interaction::Status::TRANSFER_ERROR:
- result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:self];
+ result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
+ break;
}
} else if (type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) {
+ NSString* fileName = @"sent file";
switch (status) {
case lrc::api::interaction::Status::TRANSFER_CREATED:
- case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER:
case lrc::api::interaction::Status::TRANSFER_ONGOING:
+ case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER:
case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
- result = [tableView makeViewWithIdentifier:@"RightOngoingFileView" owner:self];
+ result = [tableView makeViewWithIdentifier:@"RightOngoingFileView" owner:conversationView];
[result.progressIndicator startAnimation:nil];
+ [result.declineButton setAction:@selector(declineIncomingFile:)];
+ [result.declineButton setTarget:self];
break;
case lrc::api::interaction::Status::TRANSFER_FINISHED:
case lrc::api::interaction::Status::TRANSFER_CANCELED:
case lrc::api::interaction::Status::TRANSFER_ERROR:
case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
- result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:self];
+ result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
}
}
// Then status label is updated if needed
switch (status) {
+ [result.statusLabel setTextColor:[NSColor textColor]];
case lrc::api::interaction::Status::TRANSFER_FINISHED:
+ [result.statusLabel setTextColor:[NSColor greenColor]];
[result.statusLabel setStringValue:NSLocalizedString(@"Success", @"File transfer successful label")];
break;
case lrc::api::interaction::Status::TRANSFER_CANCELED:
+ [result.statusLabel setTextColor:[NSColor orangeColor]];
[result.statusLabel setStringValue:NSLocalizedString(@"Canceled", @"File transfer canceled label")];
break;
case lrc::api::interaction::Status::TRANSFER_ERROR:
+ [result.statusLabel setTextColor:[NSColor redColor]];
[result.statusLabel setStringValue:NSLocalizedString(@"Failed", @"File transfer failed label")];
break;
case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
+ [result.statusLabel setTextColor:[NSColor textColor]];
[result.statusLabel setStringValue:NSLocalizedString(@"Unjoinable", @"File transfer peer unjoinable label")];
break;
}
+ result.transferedImage.image = nil;
+ [result.msgBackground setHidden:NO];
+ [result invalidateImageConstraints];
+ NSString* name = @(interaction.body.c_str());
+ if (name.length > 0) {
+ if (([name rangeOfString:@"/"].location != NSNotFound)) {
+ NSArray *listItems = [name componentsSeparatedByString:@"/"];
+ NSString* name1 = listItems.lastObject;
+ fileName = name1;
+ } else {
+ fileName = name;
+ }
+ }
+ result.transferedFileName.stringValue = fileName;
+ if (status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
+ NSImage* image = [self getImageForFilePath:name];
+ if (([name rangeOfString:@"/"].location == NSNotFound)) {
+ image = [self getImageForFilePath:[self getDataTransferPath:interactionID]];
+ }
+ if(image != nil) {
+ result.transferedImage.image = [image roundCorners:14];
+ [result updateImageConstraint:image.size.width andHeight:image.size.height];
+ }
+ }
+ [result setupForInteraction:interactionID];
+ NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
+ NSString* timeString = [self timeForMessage: msgTime];
+ result.timeLabel.stringValue = timeString;
+ bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
+ if (!isOutgoing) {
+ auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
+ auto* conv = [self getCurrentConversation];
+ [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
+ }
return result;
}
@@ -212,6 +355,7 @@
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
+
auto* conv = [self getCurrentConversation];
if (conv == nil)
@@ -222,9 +366,7 @@
std::advance(it, row);
IMTableCellView* result;
-
- auto& interaction = it->second;
-
+ auto interaction = it->second;
bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
switch (interaction.type) {
@@ -239,53 +381,70 @@
break;
case lrc::api::interaction::Type::INCOMING_DATA_TRANSFER:
case lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER:
- result = [self makeViewforTransferStatus:interaction.status type:interaction.type tableView:tableView];
+ return [self configureViewforTransfer:interaction interactionID: it->first tableView:tableView];
break;
case lrc::api::interaction::Type::CONTACT:
- case lrc::api::interaction::Type::CALL:
- return [self makeGenericInteractionViewForTableView:tableView withText:@(interaction.body.c_str())];
+ case lrc::api::interaction::Type::CALL: {
+ NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
+ NSString* timeString = [self timeForMessage: msgTime];
+ return [self makeGenericInteractionViewForTableView:tableView withText:@(interaction.body.c_str()) andTime:timeString];
+ }
default: // If interaction is not of a known type
return nil;
}
-
- // check if the message first in incoming or outgoing messages sequence
- Boolean isFirstInSequence = true;
- if (it != conv->interactions.begin()) {
- auto previousIt = it;
- previousIt--;
- auto& previousInteraction = previousIt->second;
- if ((previousInteraction.type == lrc::api::interaction::Type::TEXT
- || previousInteraction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER
- || previousInteraction.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) && (isOutgoing == lrc::api::interaction::isOutgoing(previousInteraction)))
- isFirstInSequence = false;
+ MessageSequencing sequence = [self computeSequencingFor:row];
+ BubbleType type = SINGLE;
+ if (sequence == FIRST_WITHOUT_TIME || sequence == FIRST_WITH_TIME) {
+ type = FIRST;
}
- [result.photoView setHidden:!isFirstInSequence];
- result.msgBackground.needPointer = isFirstInSequence;
+ if (sequence == MIDDLE_IN_SEQUENCE) {
+ type = MIDDLE;
+ }
+ if (sequence == LAST_IN_SEQUENCE) {
+ type = LAST;
+ }
+ result.msgBackground.type = type;
[result setupForInteraction:it->first];
+ bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
+ [result.msgBackground setNeedsDisplay:YES];
+ [result setNeedsDisplay:YES];
+ [result.timeBox setNeedsDisplay:YES];
NSMutableAttributedString* msgAttString =
- [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",@(interaction.body.c_str())]
+ [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@",@(interaction.body.c_str())]
attributes:[self messageAttributes]];
- NSDate *msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
- NSAttributedString* timestampAttrString =
- [[NSAttributedString alloc] initWithString:[NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterMediumStyle]
- attributes:[self timestampAttributes]];
+ CGSize messageSize = [self sizeFor: @(interaction.body.c_str()) maxWidth:tableView.frame.size.width * 0.7];
- CGFloat finalWidth = MAX(msgAttString.size.width, timestampAttrString.size.width);
-
- finalWidth = MIN(finalWidth + 30, tableView.frame.size.width * 0.7);
-
- [msgAttString appendAttributedString:timestampAttrString];
+ [result updateMessageConstraint:messageSize.width andHeight:messageSize.height timeIsVisible:shouldDisplayTime];
[[result.msgView textStorage] appendAttributedString:msgAttString];
[result.msgView checkTextInDocument:nil];
- [result updateWidthConstraint:finalWidth];
- auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
- if (!isOutgoing) {
- [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
+ if (shouldDisplayTime) {
+ NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
+ NSString* timeString = [self timeForMessage: msgTime];
+ result.timeLabel.stringValue = timeString;
}
+ bool shouldDisplayAvatar = (sequence != MIDDLE_IN_SEQUENCE && sequence != FIRST_WITHOUT_TIME
+ && sequence != FIRST_WITH_TIME) ? YES : NO;
+ [result.photoView setHidden:!shouldDisplayAvatar];
+ if (!isOutgoing && shouldDisplayAvatar) {
+ auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
+ [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
+ }
+ [result.messageStatus setHidden:YES];
+ if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
+ if (interaction.status == lrc::api::interaction::Status::SENDING) {
+ [result.messageStatus setHidden:NO];
+ [result.sendingMessageIndicator startAnimation:nil];
+ [result.messageFailed setHidden:YES];
+ } else if (interaction.status == lrc::api::interaction::Status::FAILED) {
+ [result.messageStatus setHidden:NO];
+ [result.sendingMessageIndicator setHidden:YES];
+ [result.messageFailed setHidden:NO];
+ }
+ }
return result;
}
@@ -302,41 +461,195 @@
std::advance(it, row);
- auto& interaction = it->second;
+ auto interaction = it->second;
- if(interaction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER || interaction.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER)
- return 52.0;
+ MessageSequencing sequence = [self computeSequencingFor:row];
+
+ bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
+
+
+ if(interaction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER || interaction.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) {
+
+ if( interaction.status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
+ NSString* name = @(interaction.body.c_str());
+ NSImage* image = [self getImageForFilePath:name];
+ if (([name rangeOfString:@"/"].location == NSNotFound)) {
+ image = [self getImageForFilePath:[self getDataTransferPath:it->first]];
+ }
+ if (image != nil) {
+ return image.size.height + TIME_BOX_HEIGHT;
+ }
+ }
+ return BUBBLE_HEIGHT_FOR_TRANSFERED_FILE + TIME_BOX_HEIGHT;
+ }
if(interaction.type == lrc::api::interaction::Type::CONTACT || interaction.type == lrc::api::interaction::Type::CALL)
- return 27.0;
+ return GENERIC_CELL_HEIGHT;
// TODO Implement interactions other than messages
if(interaction.type != lrc::api::interaction::Type::TEXT) {
return 0;
}
+ CGSize messageSize = [self sizeFor: @(interaction.body.c_str()) maxWidth:tableView.frame.size.width * 0.7];
+ CGFloat singleLignMessageHeight = 15;
+
+ if (shouldDisplayTime) {
+ return MAX(messageSize.height + TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2,
+ TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2 + singleLignMessageHeight);
+ }
+ return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2,
+ singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2);
+}
+
+#pragma mark - message view parameters
+
+-(NSString *) getDataTransferPath:(uint64_t)interactionId {
+ lrc::api::datatransfer::Info info = {};
+ convModel_->getTransferInfo(interactionId, info);
+ double convertData = static_cast<double>(info.totalSize);
+ return @(info.path.c_str());
+}
+
+-(NSImage*) getImageForFilePath: (NSString *) path {
+ if (path.length <= 0) {return nil;}
+ if (![[NSFileManager defaultManager] fileExistsAtPath: path]) {return nil;}
+ NSImage* transferedImage = [[NSImage alloc] initWithContentsOfFile: path];
+ if(transferedImage != nil) {
+ return [transferedImage imageResizeInsideMax: MAX_TRANSFERED_IMAGE_SIZE];
+ }
+ return nil;
+}
+
+-(CGSize) sizeFor:(NSString *) message maxWidth:(CGFloat) width {
+ CGFloat horizaontalMargin = 6;
NSMutableAttributedString* msgAttString =
- [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",@(interaction.body.c_str())]
+ [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@", message]
attributes:[self messageAttributes]];
- NSDate *msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
- NSAttributedString* timestampAttrString =
- [[NSAttributedString alloc] initWithString:[NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterMediumStyle]
- attributes:[self timestampAttributes]];
-
- [msgAttString appendAttributedString:timestampAttrString];
-
- [msgAttString appendAttributedString:timestampAttrString];
-
- NSRect frame = NSMakeRect(0, 0, someWidth, MAXFLOAT);
+ CGFloat finalWidth = MIN(msgAttString.size.width + horizaontalMargin * 2, width);
+ NSRect frame = NSMakeRect(0, 0, finalWidth, msgAttString.size.height);
NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
- [tv setEnabledTextCheckingTypes:NSTextCheckingTypeLink];
- [tv setAutomaticLinkDetectionEnabled:YES];
[[tv textStorage] setAttributedString:msgAttString];
[tv sizeToFit];
+ return tv.frame.size;
+}
- double height = tv.frame.size.height + 10;
- return MAX(height, 50.0f);
+-(MessageSequencing) computeSequencingFor:(NSInteger) row {
+ auto* conv = [self getCurrentConversation];
+ if (conv == nil)
+ return SINGLE_WITHOUT_TIME;
+ auto it = conv->interactions.begin();
+ std::advance(it, row);
+ auto interaction = it->second;
+ if (interaction.type != lrc::api::interaction::Type::TEXT) {
+ return SINGLE_WITH_TIME;
+ }
+ if (row == 0) {
+ if (it == conv->interactions.end()) {
+ return SINGLE_WITH_TIME;
+ }
+ auto nextIt = it;
+ nextIt++;
+ auto nextInteraction = nextIt->second;
+ if ([self sequenceChangedFrom:interaction to: nextInteraction]) {
+ return SINGLE_WITH_TIME;
+ }
+ return FIRST_WITH_TIME;
+ }
+
+ if (row == conversationView.numberOfRows - 1) {
+ if(it == conv->interactions.begin()) {
+ return SINGLE_WITH_TIME;
+ }
+ auto previousIt = it;
+ previousIt--;
+ auto previousInteraction = previousIt->second;
+ bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
+ bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
+ if (!timeChanged && !authorChanged) {
+ return LAST_IN_SEQUENCE;
+ }
+ if (!timeChanged && authorChanged) {
+ return SINGLE_WITHOUT_TIME;
+ }
+ return SINGLE_WITH_TIME;
+ }
+ if(it == conv->interactions.begin() || it == conv->interactions.end()) {
+ return SINGLE_WITH_TIME;
+ }
+ auto previousIt = it;
+ previousIt--;
+ auto previousInteraction = previousIt->second;
+ auto nextIt = it;
+ nextIt++;
+ auto nextInteraction = nextIt->second;
+
+ bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
+ bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
+ bool sequenceWillChange = [self sequenceChangedFrom:interaction to: nextInteraction];
+ if (!sequenceWillChange) {
+ if (!timeChanged && !authorChanged) {
+ return MIDDLE_IN_SEQUENCE;
+ }
+ if (timeChanged) {
+ return FIRST_WITH_TIME;
+ }
+ return FIRST_WITHOUT_TIME;
+ } if (!timeChanged && !authorChanged) {
+ return LAST_IN_SEQUENCE;
+ } if (timeChanged) {
+ return SINGLE_WITH_TIME;
+ }
+ return SINGLE_WITHOUT_TIME;
+}
+
+-(bool) sequenceChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
+ return ([self sequenceTimeChangedFrom:firstInteraction to:secondInteraction] || [self sequenceAuthorChangedFrom:firstInteraction to:secondInteraction]);
+}
+
+-(bool) sequenceTimeChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
+ bool timeChanged = NO;
+ NSDate* firstMessageTime = [NSDate dateWithTimeIntervalSince1970:firstInteraction.timestamp];
+ NSDate* secondMessageTime = [NSDate dateWithTimeIntervalSince1970:secondInteraction.timestamp];
+ bool hourComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitHour];
+ bool minutComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitMinute];
+ if(hourComp != NSOrderedSame || minutComp != NSOrderedSame) {
+ timeChanged = YES;
+ }
+ return timeChanged;
+}
+
+-(bool) sequenceAuthorChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
+ bool authorChanged = YES;
+ bool isOutgoing = lrc::api::interaction::isOutgoing(firstInteraction);
+ if ((secondInteraction.type == lrc::api::interaction::Type::TEXT) && (isOutgoing == lrc::api::interaction::isOutgoing(secondInteraction))) {
+ authorChanged = NO;
+ }
+ return authorChanged;
+}
+
+-(NSString *)timeForMessage:(NSDate*) msgTime {
+ NSDate *today = [NSDate date];
+ NSDateFormatter *dateFormatter=[[NSDateFormatter alloc] init];
+ if ([[NSCalendar currentCalendar] compareDate:today
+ toDate:msgTime
+ toUnitGranularity:NSCalendarUnitYear]!= NSOrderedSame) {
+ return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterMediumStyle];
+ }
+
+ if ([[NSCalendar currentCalendar] compareDate:today
+ toDate:msgTime
+ toUnitGranularity:NSCalendarUnitDay]!= NSOrderedSame ||
+ [[NSCalendar currentCalendar] compareDate:today
+ toDate:msgTime
+ toUnitGranularity:NSCalendarUnitMonth]!= NSOrderedSame) {
+ [dateFormatter setDateFormat:@"MMM dd, HH:mm"];
+ return [dateFormatter stringFromDate:msgTime];
+ }
+
+ [dateFormatter setDateFormat:@"HH:mm"];
+ return [dateFormatter stringFromDate:msgTime];
}
#pragma mark - NSTableViewDataSource
@@ -353,25 +666,11 @@
#pragma mark - Text formatting
-- (NSMutableDictionary*) timestampAttributes
-{
- NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
- attrs[NSForegroundColorAttributeName] = [NSColor grayColor];
- NSFont* systemFont = [NSFont systemFontOfSize:12.0f];
- attrs[NSFontAttributeName] = systemFont;
- attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
-
- return attrs;
-}
-
- (NSMutableDictionary*) messageAttributes
{
NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
- attrs[NSForegroundColorAttributeName] = [NSColor blackColor];
- NSFont* systemFont = [NSFont systemFontOfSize:14.0f];
- attrs[NSFontAttributeName] = systemFont;
+ attrs[NSForegroundColorAttributeName] = [NSColor labelColor];
attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
-
return attrs;
}
@@ -388,22 +687,29 @@
Line break mode NSLineBreakByWordWrapping
All others 0.0
*/
- NSMutableParagraphStyle* aMutableParagraphStyle =
- [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
+ NSMutableParagraphStyle* aMutableParagraphStyle =
+ [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
+ [aMutableParagraphStyle setHeadIndent:1.0];
+ [aMutableParagraphStyle setFirstLineHeadIndent:1.0];
+ return aMutableParagraphStyle;
+}
- // Now adjust our NSMutableParagraphStyle formatting to be whatever we want.
- // The numeric values below are in points (72 points per inch)
- [aMutableParagraphStyle setLineSpacing:1.5];
- [aMutableParagraphStyle setParagraphSpacing:5.0];
- [aMutableParagraphStyle setHeadIndent:5.0];
- [aMutableParagraphStyle setTailIndent:-5.0];
- [aMutableParagraphStyle setFirstLineHeadIndent:5.0];
- return aMutableParagraphStyle;
+- (void)acceptFile:(uint64_t)interactionID {
+ NSURL *downloadsURL = [[NSFileManager defaultManager]
+ URLForDirectory:NSDownloadsDirectory
+ inDomain:NSUserDomainMask appropriateForURL:nil
+ create:YES error:nil];
+ auto& inter = [self getCurrentConversation]->interactions.find(interactionID)->second;
+ if (convModel_ && !convUid_.empty()) {
+ NSURL *bUrl = [downloadsURL URLByAppendingPathComponent:@(inter.body.c_str())];
+ const char* fullPath = [bUrl fileSystemRepresentation];
+ convModel_->acceptTransfer(convUid_, interactionID, fullPath);
+ }
}
#pragma mark - Actions
-- (IBAction)acceptIncomingFile:(id)sender {
+- (void)acceptIncomingFile:(id)sender {
auto interId = [(IMTableCellView*)[[sender superview] superview] interaction];
auto& inter = [self getCurrentConversation]->interactions.find(interId)->second;
if (convModel_ && !convUid_.empty()) {
@@ -417,7 +723,7 @@
}
}
-- (IBAction)declineIncomingFile:(id)sender {
+- (void)declineIncomingFile:(id)sender {
auto inter = [(IMTableCellView*)[[sender superview] superview] interaction];
if (convModel_ && !convUid_.empty()) {
convModel_->cancelTransfer(convUid_, inter);