From 252a94abc71f903a64141850182f5ea756ac4580 Mon Sep 17 00:00:00 2001 From: Andreas Traczyk Date: Fri, 20 Apr 2018 16:36:20 -0400 Subject: [PATCH] UI/UX: refactor smartlist Change-Id: Ibfd5154757908ebd85f4b0060da00a7c608a0e56 Reviewed-by: Kateryna Kostiuk --- CMakeLists.txt | 5 +- data/dark/ic_action_block.png | Bin 0 -> 967 bytes src/ConversationVC.mm | 14 + src/MessagesVC.h | 1 + src/MessagesVC.mm | 13 +- src/NSString+Extensions.h | 27 ++ src/NSString+Extensions.mm | 77 ++++ src/SmartViewVC.mm | 472 ++++++++++++++------- src/delegates/ImageManipulationDelegate.mm | 12 +- src/utils.h | 33 +- src/views/IconButton.h | 2 +- src/views/IconButton.mm | 9 +- src/views/RoundedTextField.h | 5 + src/views/RoundedTextField.mm | 7 +- ui/Base.lproj/Conversation.xib | 77 ++-- ui/Base.lproj/RingWindow.strings | 4 +- ui/Base.lproj/RingWindow.xib | 232 ++++++---- 17 files changed, 707 insertions(+), 283 deletions(-) create mode 100644 data/dark/ic_action_block.png create mode 100644 src/NSString+Extensions.h create mode 100644 src/NSString+Extensions.mm diff --git a/CMakeLists.txt b/CMakeLists.txt index 902723eb..8f3a947a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -227,7 +227,9 @@ SET(ringclient_OTHERS src/delegates/ImageManipulationDelegate.h src/AccountSelectionManager.h src/AccountSelectionManager.mm - src/utils.h) + src/utils.h + src/NSString+Extensions.h + src/NSString+Extensions.mm) SET(ringclient_XIBS @@ -275,6 +277,7 @@ SET_SOURCE_FILES_PROPERTIES(${myApp_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) SET(ring_ICONS +${CMAKE_CURRENT_SOURCE_DIR}/data/dark/ic_action_block.png ${CMAKE_CURRENT_SOURCE_DIR}/data/dark/ic_attachment.png ${CMAKE_CURRENT_SOURCE_DIR}/data/default_avatar_overlay.png ${CMAKE_CURRENT_SOURCE_DIR}/data/symbol_name.png diff --git a/data/dark/ic_action_block.png b/data/dark/ic_action_block.png new file mode 100644 index 0000000000000000000000000000000000000000..03204492ffccc8583b82f9681e27f4dd1a8fcfc6 GIT binary patch literal 967 zcmV;&133JNP)KizX`65QC`^S3p2Op2?tAZEaWUUnWpd7WU*7#OhtQ!zhfkI= zCz)V|RW=A|(IRAnRc4rAloB72Iu}@`MQje5JmWl{;hkX~@E@^dL@Y3bH;(h1I72LR z4Cj2#Z^Subj(s@f8~!EE8QYBEgdTo2JmMeb7-y6|_EMmWfc*?}hC946RNSCIPI_4) z*?5cV^aE!4nIt4x`G|lV)c7;e@^8i|VndMzn~C}t)W}G0LZ``PN~9s+3QXsoBU2dHumt47#-iIKj^x_G+mi>7Kus5*Xgc7eee7Tj&)+T zsbkGuy;u(4j*MTiJoU01;eVzWv4m-SJkJ3 zwB>=il}nCwAmEMqybX0Vel?uy0Gw4LT#X&MpuR$j9(16@JNrL+S)^X~q607`e*XkB zl-20o?@0#+M8kuSM1gj7-QW*rN{2io8o0nGtE@9(AB0GH+pIRiur2 z)PWI^yMmDok=Ex?2l_~HrM;+)E6-_&#E{{6UBZcgiEbyoU zRk>t#n|i-T9XKF2wcW8F_NW7= delegate; diff --git a/src/MessagesVC.mm b/src/MessagesVC.mm index a05b8c5e..17b451b4 100644 --- a/src/MessagesVC.mm +++ b/src/MessagesVC.mm @@ -1,3 +1,4 @@ + /* * Copyright (C) 2015-2018 Savoir-faire Linux Inc. * Author: Kateryna Kostiuk @@ -89,6 +90,16 @@ typedef NS_ENUM(NSInteger, MessageSequencing) { [conversationView registerNib:cellNib forIdentifier:@"RightOngoingFileView"]; [conversationView registerNib:cellNib forIdentifier:@"RightFinishedFileView"]; } +-(void) clearData { + cachedConv_ = nil; + convUid_ = ""; + convModel_ = nil; + + QObject::disconnect(modelSortedSignal_); + QObject::disconnect(filterChangedSignal_); + QObject::disconnect(interactionStatusUpdatedSignal_); + QObject::disconnect(newInteractionSignal_); +} -(const lrc::api::conversation::Info*) getCurrentConversation { @@ -390,8 +401,6 @@ typedef NS_ENUM(NSInteger, MessageSequencing) { } else { result = [tableView makeViewWithIdentifier:@"LeftMessageView" owner:self]; } - if (interaction.status == lrc::api::interaction::Status::UNREAD) - convModel_->setInteractionRead(convUid_, it->first); break; case lrc::api::interaction::Type::INCOMING_DATA_TRANSFER: case lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER: diff --git a/src/NSString+Extensions.h b/src/NSString+Extensions.h new file mode 100644 index 00000000..7cd75068 --- /dev/null +++ b/src/NSString+Extensions.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2018 Savoir-faire Linux Inc. + * Author: Kateryna Kostiuk + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#import + +@interface NSString (Extensions) + +- (NSString *) removeAllNewLinesAtTheEnd; +- (NSString *) removeEmptyLinesAtBorders; + +@end diff --git a/src/NSString+Extensions.mm b/src/NSString+Extensions.mm new file mode 100644 index 00000000..d843ee22 --- /dev/null +++ b/src/NSString+Extensions.mm @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2018 Savoir-faire Linux Inc. + * Author: Kateryna Kostiuk + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#import "NSString+Extensions.h" + +@implementation NSString (Extensions) + +- (NSString *) removeAllNewLinesAtTheEnd { + NSString *result = self; + while ([result endedByEmptyLine]) { + result = [result removeLastWhiteSpaceAndNewLineCharacter]; + } + return result; +} + +- (NSString *) removeAllNewLinesAtBegining { + NSString *result = self; + while ([result startByEmptyLine]) { + result = [result removeFirstWhiteSpaceAndNewLineCharacter]; + } + return result; +} + +- (NSString *) removeEmptyLinesAtBorders { + NSString *result = self; + result = [result removeAllNewLinesAtBegining]; + result = [result removeAllNewLinesAtTheEnd]; + return result; +} + +-(bool)endedByEmptyLine { + if ([self length] < 1) { + return false; + } + unichar last = [self characterAtIndex:[self length] - 1]; + return [[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:last]; +} + +- (bool)startByEmptyLine { + if ([self length] < 1) { + return false; + } + unichar first = [self characterAtIndex:0]; + return [[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:first]; +} + +- (NSString *) removeLastWhiteSpaceAndNewLineCharacter { + if ([self endedByEmptyLine]) { + return [self substringToIndex:[self length]-1]; + } + return self; +} + +- (NSString *) removeFirstWhiteSpaceAndNewLineCharacter { + if ([self startByEmptyLine]) { + return [self substringFromIndex:1]; + } + return self; +} + +@end diff --git a/src/SmartViewVC.mm b/src/SmartViewVC.mm index 547cb183..8ee6565e 100755 --- a/src/SmartViewVC.mm +++ b/src/SmartViewVC.mm @@ -21,6 +21,9 @@ #import "SmartViewVC.h" +//std +#import + //Qt #import #import @@ -54,11 +57,12 @@ __unsafe_unretained IBOutlet RingTableView* smartView; __unsafe_unretained IBOutlet NSSearchField* searchField; __strong IBOutlet NSSegmentedControl *listTypeSelector; + __strong IBOutlet NSLayoutConstraint *listTypeSelectorHeight; bool selectorIsPresent; - QMetaObject::Connection modelSortedConnection_, modelUpdatedConnection_, filterChangedConnection_, newConversationConnection_, conversationRemovedConnection_, interactionStatusUpdatedConnection_, conversationClearedConnection; + QMetaObject::Connection modelSortedConnection_, modelUpdatedConnection_, filterChangedConnection_, newConversationConnection_, conversationRemovedConnection_, newInteractionConnection_, interactionStatusUpdatedConnection_, conversationClearedConnection; - lrc::api::ConversationModel* model_; + lrc::api::ConversationModel* convModel_; std::string selectedUid_; lrc::api::profile::Type currentFilterType; @@ -74,12 +78,16 @@ // Tags for views NSInteger const IMAGE_TAG = 100; NSInteger const DISPLAYNAME_TAG = 200; -NSInteger const DETAILS_TAG = 300; -NSInteger const CALL_BUTTON_TAG = 400; -NSInteger const TXT_BUTTON_TAG = 500; -NSInteger const CANCEL_BUTTON_TAG = 600; -NSInteger const RING_ID_LABEL = 700; -NSInteger const PRESENCE_TAG = 800; +NSInteger const NOTIFICATONS_TAG = 300; +NSInteger const RING_ID_LABEL = 400; +NSInteger const PRESENCE_TAG = 500; +NSInteger const TOTALMSGS_TAG = 600; +NSInteger const TOTALINVITES_TAG = 700; +NSInteger const DATE_TAG = 800; +NSInteger const SNIPPET_TAG = 900; +NSInteger const ADD_BUTTON_TAG = 1000; +NSInteger const REFUSE_BUTTON_TAG = 1100; +NSInteger const BLOCK_BUTTON_TAG = 1200; // Segment indices for smartlist selector NSInteger const CONVERSATION_SEG = 0; @@ -109,6 +117,9 @@ NSInteger const REQUEST_SEG = 1; currentFilterType = lrc::api::profile::Type::RING; selectorIsPresent = true; + + smartView.selectionHighlightStyle = NSTableViewSelectionHighlightStyleNone; + } - (void)placeCall:(id)sender @@ -120,57 +131,71 @@ NSInteger const REQUEST_SEG = 1; row = [smartView selectedRow]; else return; - if (model_ == nil) + if (convModel_ == nil) + return; + + auto conv = convModel_->filteredConversation(row); + convModel_->placeCall(conv.uid); +} + +-(void) reloadSelectorNotifications +{ + NSTextField* totalMsgsCount = [self.view viewWithTag:TOTALMSGS_TAG]; + NSTextField* totalInvites = [self.view viewWithTag:TOTALINVITES_TAG]; + + if (!selectorIsPresent) { + [totalMsgsCount setHidden:true]; + [totalInvites setHidden:true]; return; + } - auto conv = model_->filteredConversation(row); - model_->placeCall(conv.uid); + auto ringConversations = convModel_->getFilteredConversations(lrc::api::profile::Type::RING); + int totalUnreadMessages = 0; + std::for_each(ringConversations.begin(), ringConversations.end(), + [&totalUnreadMessages, self] (const auto& conversation) { + totalUnreadMessages += convModel_->getNumberOfUnreadMessagesFor(conversation.uid); + }); + [totalMsgsCount setHidden:(totalUnreadMessages == 0)]; + [totalMsgsCount setIntValue:totalUnreadMessages]; + + auto totalRequests = [self chosenAccount].contactModel->pendingRequestCount(); + [totalInvites setHidden:(totalRequests == 0)]; + [totalInvites setIntValue:totalRequests]; } -(void) reloadData { NSLog(@"reload"); [smartView deselectAll:nil]; - if (model_ == nil) + if (convModel_ == nil) return; - if (!model_->owner.contactModel->hasPendingRequests()) { + [self reloadSelectorNotifications]; + + if (!convModel_->owner.contactModel->hasPendingRequests()) { if (currentFilterType == lrc::api::profile::Type::PENDING) { [self selectConversationList]; } if (selectorIsPresent) { - [listTypeSelector removeFromSuperview]; + listTypeSelectorHeight.constant = 0.0; + [listTypeSelector setHidden:YES]; selectorIsPresent = false; } } else { if (!selectorIsPresent) { - // First we restore the selector with selection on "Conversations" - [self.view addSubview:listTypeSelector]; [listTypeSelector setSelected:YES forSegment:CONVERSATION_SEG]; - - // Then constraints are recreated (as these are lost when calling removeFromSuperview) - [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[searchField]-8-[listTypeSelector]" - options:0 - metrics:nil - views:NSDictionaryOfVariableBindings(searchField, listTypeSelector)]]; - [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[listTypeSelector]-8-[tabbar]" - options:0 - metrics:nil - views:NSDictionaryOfVariableBindings(listTypeSelector, tabbar)]]; - [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-20-[listTypeSelector]-20-|" - options:0 - metrics:nil - views:NSDictionaryOfVariableBindings(listTypeSelector)]]; + listTypeSelectorHeight.constant = 18.0; + [listTypeSelector setHidden:NO]; selectorIsPresent = true; } } [smartView reloadData]; - if (!selectedUid_.empty() && model_ != nil) { - auto it = getConversationFromUid(selectedUid_, *model_); - if (it != model_->allFilteredConversations().end()) { - NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:(it - model_->allFilteredConversations().begin())]; + if (!selectedUid_.empty() && convModel_ != nil) { + auto it = getConversationFromUid(selectedUid_, *convModel_); + if (it != convModel_->allFilteredConversations().end()) { + NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:(it - convModel_->allFilteredConversations().begin())]; [smartView selectRowIndexes:indexSet byExtendingSelection:NO]; } } @@ -180,69 +205,80 @@ NSInteger const REQUEST_SEG = 1; -(void) reloadConversationWithUid:(NSString *)uid { - if (model_ != nil) { - auto it = getConversationFromUid(std::string([uid UTF8String]), *model_); - if (it != model_->allFilteredConversations().end()) { - NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:(it - model_->allFilteredConversations().begin())]; - NSLog(@"reloadConversationWithUid: %@", uid); - [smartView reloadDataForRowIndexes:indexSet - columnIndexes:[NSIndexSet indexSetWithIndex:0]]; - } + if (convModel_ == nil) { + return; + } + + auto it = getConversationFromUid(std::string([uid UTF8String]), *convModel_); + if (it != convModel_->allFilteredConversations().end()) { + NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:(it - convModel_->allFilteredConversations().begin())]; + NSLog(@"reloadConversationWithUid: %@", uid); + [smartView reloadDataForRowIndexes:indexSet + columnIndexes:[NSIndexSet indexSetWithIndex:0]]; } } - (BOOL)setConversationModel:(lrc::api::ConversationModel *)conversationModel { - if (model_ != conversationModel) { - model_ = conversationModel; - selectedUid_.clear(); // Clear selected conversation as the selected account is being changed - QObject::disconnect(modelSortedConnection_); - QObject::disconnect(modelUpdatedConnection_); - QObject::disconnect(filterChangedConnection_); - QObject::disconnect(newConversationConnection_); - QObject::disconnect(conversationRemovedConnection_); - QObject::disconnect(interactionStatusUpdatedConnection_); - QObject::disconnect(conversationClearedConnection); - [self reloadData]; - if (model_ != nil) { - modelSortedConnection_ = QObject::connect(model_, &lrc::api::ConversationModel::modelSorted, - [self] (){ - [self reloadData]; - }); - modelUpdatedConnection_ = QObject::connect(model_, &lrc::api::ConversationModel::conversationUpdated, - [self] (const std::string& uid){ - [self reloadConversationWithUid: [NSString stringWithUTF8String:uid.c_str()]]; - }); - filterChangedConnection_ = QObject::connect(model_, &lrc::api::ConversationModel::filterChanged, + if (convModel_ == conversationModel) { + return false; + } + + convModel_ = conversationModel; + selectedUid_.clear(); // Clear selected conversation as the selected account is being changed + QObject::disconnect(modelSortedConnection_); + QObject::disconnect(modelUpdatedConnection_); + QObject::disconnect(filterChangedConnection_); + QObject::disconnect(newConversationConnection_); + QObject::disconnect(conversationRemovedConnection_); + QObject::disconnect(conversationClearedConnection); + QObject::disconnect(interactionStatusUpdatedConnection_); + QObject::disconnect(newInteractionConnection_); + [self reloadData]; + if (convModel_ != nil) { + modelSortedConnection_ = QObject::connect(convModel_, &lrc::api::ConversationModel::modelSorted, [self] (){ [self reloadData]; }); - newConversationConnection_ = QObject::connect(model_, &lrc::api::ConversationModel::newConversation, - [self] (const std::string& uid) { - [self reloadData]; - [self updateConversationForNewContact:[NSString stringWithUTF8String:uid.c_str()]]; - }); - conversationRemovedConnection_ = QObject::connect(model_, &lrc::api::ConversationModel::conversationRemoved, - [self] (){ - [self reloadData]; - }); - conversationClearedConnection = QObject::connect(model_, &lrc::api::ConversationModel::conversationCleared, - [self] (const std::string& id){ - [self deselect]; - [delegate listTypeChanged]; - }); - interactionStatusUpdatedConnection_ = QObject::connect(model_, &lrc::api::ConversationModel::interactionStatusUpdated, - [self] (const std::string& convUid) { - if (convUid != selectedUid_) - return; - [self reloadConversationWithUid: [NSString stringWithUTF8String:convUid.c_str()]]; - }); - model_->setFilter(""); // Reset the filter - } - [searchField setStringValue:@""]; - return YES; + modelUpdatedConnection_ = QObject::connect(convModel_, &lrc::api::ConversationModel::conversationUpdated, + [self] (const std::string& convUid){ + [self reloadConversationWithUid: [NSString stringWithUTF8String:convUid.c_str()]]; + }); + filterChangedConnection_ = QObject::connect(convModel_, &lrc::api::ConversationModel::filterChanged, + [self] (){ + [self reloadData]; + }); + newConversationConnection_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newConversation, + [self] (const std::string& convUid) { + [self reloadData]; + [self updateConversationForNewContact:[NSString stringWithUTF8String:convUid.c_str()]]; + }); + conversationRemovedConnection_ = QObject::connect(convModel_, &lrc::api::ConversationModel::conversationRemoved, + [self] (){ + [delegate listTypeChanged]; + [self reloadData]; + }); + conversationClearedConnection = QObject::connect(convModel_, &lrc::api::ConversationModel::conversationCleared, + [self] (const std::string& convUid){ + [self deselect]; + [delegate listTypeChanged]; + }); + interactionStatusUpdatedConnection_ = QObject::connect(convModel_, &lrc::api::ConversationModel::interactionStatusUpdated, + [self] (const std::string& convUid) { + if (convUid != selectedUid_) + return; + [self reloadConversationWithUid: [NSString stringWithUTF8String:convUid.c_str()]]; + }); + newInteractionConnection_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newInteraction, + [self](const std::string& convUid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){ + if (convUid == selectedUid_) { + convModel_->clearUnreadInteractions(convUid); + } + }); + convModel_->setFilter(""); // Reset the filter } - return NO; + [searchField setStringValue:@""]; + return true; } -(void)selectConversation:(const lrc::api::conversation::Info&)conv model:(lrc::api::ConversationModel*)model; @@ -253,13 +289,15 @@ NSInteger const REQUEST_SEG = 1; [self setConversationModel:model]; - if (model_ != nil) { - auto it = getConversationFromUid(selectedUid_, *model_); - if (it != model_->allFilteredConversations().end()) { - NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:(it - model_->allFilteredConversations().begin())]; - [smartView selectRowIndexes:indexSet byExtendingSelection:NO]; - selectedUid_ = uid; - } + if (convModel_ == nil) { + return; + } + + auto it = getConversationFromUid(selectedUid_, *convModel_); + if (it != convModel_->allFilteredConversations().end()) { + NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:(it - convModel_->allFilteredConversations().begin())]; + [smartView selectRowIndexes:indexSet byExtendingSelection:NO]; + selectedUid_ = uid; } } @@ -270,7 +308,7 @@ NSInteger const REQUEST_SEG = 1; } -(void) clearConversationModel { - model_ = nil; + convModel_ = nil; [self deselect]; [smartView reloadData]; if (selectorIsPresent) { @@ -281,18 +319,19 @@ NSInteger const REQUEST_SEG = 1; - (IBAction) listTypeChanged:(id)sender { + selectedUid_.clear(); NSInteger selectedItem = [sender selectedSegment]; switch (selectedItem) { case CONVERSATION_SEG: if (currentFilterType != lrc::api::profile::Type::RING) { - model_->setFilter(lrc::api::profile::Type::RING); + convModel_->setFilter(lrc::api::profile::Type::RING); [delegate listTypeChanged]; currentFilterType = lrc::api::profile::Type::RING; } break; case REQUEST_SEG: if (currentFilterType != lrc::api::profile::Type::PENDING) { - model_->setFilter(lrc::api::profile::Type::PENDING); + convModel_->setFilter(lrc::api::profile::Type::PENDING); [delegate listTypeChanged]; currentFilterType = lrc::api::profile::Type::PENDING; } @@ -311,8 +350,8 @@ NSInteger const REQUEST_SEG = 1; // Do not invert order of the next two lines or stack overflow // may happen on -(void) reloadData call if filter is currently set to PENDING currentFilterType = lrc::api::profile::Type::RING; - model_->setFilter(lrc::api::profile::Type::RING); - model_->setFilter(""); + convModel_->setFilter(lrc::api::profile::Type::RING); + convModel_->setFilter(""); } -(void) selectPendingList @@ -322,8 +361,8 @@ NSInteger const REQUEST_SEG = 1; [listTypeSelector setSelectedSegment:REQUEST_SEG]; currentFilterType = lrc::api::profile::Type::PENDING; - model_->setFilter(lrc::api::profile::Type::PENDING); - model_->setFilter(""); + convModel_->setFilter(lrc::api::profile::Type::PENDING); + convModel_->setFilter(""); } #pragma mark - NSTableViewDelegate methods @@ -342,15 +381,26 @@ NSInteger const REQUEST_SEG = 1; { NSInteger row = [notification.object selectedRow]; + [smartView enumerateAvailableRowViewsUsingBlock:^(NSTableRowView *rowView, NSInteger row){ + NSTableRowView* cellRowView = [smartView rowViewAtRow:row makeIfNecessary:NO]; + if(rowView.selected){ + cellRowView.backgroundColor = [NSColor controlColor]; + }else{ + cellRowView.backgroundColor = [NSColor whiteColor]; + } + }]; + if (row == -1) return; - if (model_ == nil) + if (convModel_ == nil) return; - auto uid = model_->filteredConversation(row).uid; + auto uid = convModel_->filteredConversation(row).uid; if (selectedUid_ != uid) { selectedUid_ = uid; - model_->selectConversation(uid); + convModel_->selectConversation(uid); + convModel_->clearUnreadInteractions(uid); + [self reloadSelectorNotifications]; } } @@ -361,56 +411,105 @@ NSInteger const REQUEST_SEG = 1; - (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { - if (model_ == nil) + if (convModel_ == nil) return nil; - auto conversation = model_->filteredConversation(row); + auto conversation = convModel_->filteredConversation(row); NSTableCellView* result; result = [tableView makeViewWithIdentifier:@"MainCell" owner:tableView]; -// NSTextField* details = [result viewWithTag:DETAILS_TAG]; - - NSMutableArray* controls = [NSMutableArray arrayWithObject:[result viewWithTag:CALL_BUTTON_TAG]]; - [((ContextualTableCellView*) result) setContextualsControls:controls]; - [((ContextualTableCellView*) result) setShouldBlurParentView:YES]; - - // if (auto call = RecentModel::instance().getActiveCall(qIdx)) { - // [details setStringValue:call->roleData((int)Ring::Role::FormattedState).toString().toNSString()]; - // [((ContextualTableCellView*) result) setActiveState:YES]; - // } else { - // [details setStringValue:qIdx.data((int)Ring::Role::FormattedLastUsed).toString().toNSString()]; - // [((ContextualTableCellView*) result) setActiveState:NO]; - // } - NSTextField* unreadCount = [result viewWithTag:TXT_BUTTON_TAG]; + NSTextField* unreadCount = [result viewWithTag:NOTIFICATONS_TAG]; [unreadCount setHidden:(conversation.unreadMessages == 0)]; [unreadCount setIntValue:conversation.unreadMessages]; - NSTextField* displayName = [result viewWithTag:DISPLAYNAME_TAG]; - NSString* displayNameString = bestNameForConversation(conversation, *model_); - NSString* displayIDString = bestIDForConversation(conversation, *model_); + NSTextField* displayRingID = [result viewWithTag:RING_ID_LABEL]; + NSString* displayNameString = bestNameForConversation(conversation, *convModel_); + NSString* displayIDString = bestIDForConversation(conversation, *convModel_); if(displayNameString.length == 0 || [displayNameString isEqualToString:displayIDString]) { - NSTextField* displayRingID = [result viewWithTag:RING_ID_LABEL]; [displayName setStringValue:displayIDString]; [displayRingID setHidden:YES]; } else { - NSTextField* displayRingID = [result viewWithTag:RING_ID_LABEL]; [displayName setStringValue:displayNameString]; [displayRingID setStringValue:displayIDString]; [displayRingID setHidden:NO]; } - NSImageView* photoView = [result viewWithTag:IMAGE_TAG]; + NSImageView* photoView = [result viewWithTag:IMAGE_TAG]; auto& imageManip = reinterpret_cast(GlobalInstances::pixmapManipulator()); - [photoView setImage:QtMac::toNSImage(qvariant_cast(imageManip.conversationPhoto(conversation, model_->owner)))]; + [photoView setImage:QtMac::toNSImage(qvariant_cast(imageManip.conversationPhoto(conversation, convModel_->owner)))]; NSView* presenceView = [result viewWithTag:PRESENCE_TAG]; - if (model_->owner.contactModel->getContact(conversation.participants[0]).isPresent) { + auto contact = convModel_->owner.contactModel->getContact(conversation.participants[0]); + if (contact.isPresent) { [presenceView setHidden:NO]; } else { [presenceView setHidden:YES]; } + NSTextField* lastInteractionDate = [result viewWithTag:DATE_TAG]; + NSTextField* interactionSnippet = [result viewWithTag:SNIPPET_TAG]; + NSButton* addContactButton = [result viewWithTag:ADD_BUTTON_TAG]; + NSButton* refuseContactButton = [result viewWithTag:REFUSE_BUTTON_TAG]; + NSButton* blockContactButton = [result viewWithTag:BLOCK_BUTTON_TAG]; + [addContactButton setHidden:YES]; + [refuseContactButton setHidden:YES]; + [blockContactButton setHidden:YES]; + + if (profileType(conversation, *convModel_) == lrc::api::profile::Type::PENDING) { + [lastInteractionDate setHidden:true]; + [interactionSnippet setHidden:true]; + [addContactButton setHidden:NO]; + [refuseContactButton setHidden:NO]; + [blockContactButton setHidden:NO]; + [addContactButton setAction:@selector(acceptInvitation:)]; + [addContactButton setTarget:self]; + [refuseContactButton setAction:@selector(refuseInvitation:)]; + [refuseContactButton setTarget:self]; + [blockContactButton setAction:@selector(blockPendingContact:)]; + [blockContactButton setTarget:self]; + return result; + } + + [lastInteractionDate setHidden:false]; + + [interactionSnippet setHidden:false]; + + auto lastUid = conversation.lastMessageUid; + if (conversation.interactions.find(lastUid) != conversation.interactions.end()) { + // last interaction snippet + std::string lastInteractionSnippet = conversation.interactions[lastUid].body; + std::stringstream ss(lastInteractionSnippet); + std::getline(ss, lastInteractionSnippet); + NSString* lastInteractionSnippetFixedString = [[NSString stringWithUTF8String:lastInteractionSnippet.c_str()] + stringByReplacingOccurrencesOfString:@"🕽" withString:@""]; + lastInteractionSnippetFixedString = [lastInteractionSnippetFixedString stringByReplacingOccurrencesOfString:@"📞" withString:@""]; + if (conversation.interactions[lastUid].type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER + || conversation.interactions[lastUid].type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER) { + if (([lastInteractionSnippetFixedString rangeOfString:@"/"].location != NSNotFound)) { + NSArray *listItems = [lastInteractionSnippetFixedString componentsSeparatedByString:@"/"]; + NSString* name = listItems.lastObject; + lastInteractionSnippetFixedString = name; + } + } + [interactionSnippet setStringValue:lastInteractionSnippetFixedString]; + + // last interaction date/time + std::time_t lastInteractionTimestamp = conversation.interactions[lastUid].timestamp; + std::time_t now = std::time(nullptr); + char interactionDay[64]; + char nowDay[64]; + std::strftime(interactionDay, sizeof(interactionDay), "%D", std::localtime(&lastInteractionTimestamp)); + std::strftime(nowDay, sizeof(nowDay), "%D", std::localtime(&now)); + if (std::string(interactionDay) == std::string(nowDay)) { + char interactionTime[64]; + std::strftime(interactionTime, sizeof(interactionTime), "%R", std::localtime(&lastInteractionTimestamp)); + [lastInteractionDate setStringValue:[NSString stringWithUTF8String:interactionTime]]; + } else { + [lastInteractionDate setStringValue:[NSString stringWithUTF8String:interactionDay]]; + } + } + return result; } @@ -423,9 +522,8 @@ NSInteger const REQUEST_SEG = 1; - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { - if (tableView == smartView) { - if (model_ != nullptr) - return model_->allFilteredConversations().size(); + if (tableView == smartView && convModel_ != nullptr) { + return convModel_->allFilteredConversations().size(); } return 0; @@ -442,16 +540,16 @@ NSInteger const REQUEST_SEG = 1; if (row == -1) return; - if (model_ == nil) + if (convModel_ == nil) return; - auto conv = model_->filteredConversation(row); + auto conv = convModel_->filteredConversation(row); auto& callId = conv.callId; if (callId.empty()) return; - auto* callModel = model_->owner.callModel.get(); + auto* callModel = convModel_->owner.callModel.get(); callModel->hangUp(callId); } @@ -468,14 +566,16 @@ NSInteger const REQUEST_SEG = 1; - (void) processSearchFieldInput { - if (model_ == nil) - return; - model_->setFilter(std::string([[searchField stringValue] UTF8String])); + if (convModel_ == nil) { + return; + } + + convModel_->setFilter(std::string([[searchField stringValue] UTF8String])); } -(const lrc::api::account::Info&) chosenAccount { - return model_->owner; + return convModel_->owner; } - (void) clearSearchField @@ -485,18 +585,18 @@ NSInteger const REQUEST_SEG = 1; } -(void)updateConversationForNewContact:(NSString *)uId { - if (model_ == nil) { + if (convModel_ == nil) { return; } [self clearSearchField]; auto uid = std::string([uId UTF8String]); - auto it = getConversationFromUid(uid, *model_); - if (it != model_->allFilteredConversations().end()) { + auto it = getConversationFromUid(uid, *convModel_); + if (it != convModel_->allFilteredConversations().end()) { @try { - auto contact = model_->owner.contactModel->getContact(it->participants[0]); + auto contact = convModel_->owner.contactModel->getContact(it->participants[0]); if (!contact.profileInfo.uri.empty() && contact.profileInfo.uri.compare(selectedUid_) == 0) { selectedUid_ = uid; - model_->selectConversation(uid); + convModel_->selectConversation(uid); } } @catch (NSException *exception) { return; @@ -523,25 +623,25 @@ NSInteger const REQUEST_SEG = 1; if (commandSelector != @selector(insertNewline:) || [[searchField stringValue] isEqual:@""]) { return NO; } - if (model_ == nil) { + if (convModel_ == nil) { [self displayErrorModalWithTitle:NSLocalizedString(@"No account available", @"Displayed as RingID when no accounts are available for selection") WithMessage:NSLocalizedString(@"Navigate to preferences to create a new account", @"Allert message when no accounts are available")]; return NO; } - if (model_->allFilteredConversations().size() <= 0) { + if (convModel_->allFilteredConversations().size() <= 0) { return YES; } - auto model = model_->filteredConversation(0); + auto model = convModel_->filteredConversation(0); auto uid = model.uid; if (selectedUid_ == uid) { return YES; } @try { - auto contact = model_->owner.contactModel->getContact(model.participants[0]); + auto contact = convModel_->owner.contactModel->getContact(model.participants[0]); if ((contact.profileInfo.uri.empty() && contact.profileInfo.type != lrc::api::profile::Type::SIP) || contact.profileInfo.type == lrc::api::profile::Type::INVALID) { return YES; } selectedUid_ = uid; - model_->selectConversation(uid); + convModel_->selectConversation(uid); [self.view.window makeFirstResponder: smartView]; return YES; } @catch (NSException *exception) { @@ -581,24 +681,24 @@ NSInteger const REQUEST_SEG = 1; { if ([smartView selectedRow] == -1) return; - if (model_ == nil) + if (convModel_ == nil) return ; - auto uid = model_->filteredConversation([smartView selectedRow]).uid; - model_->makePermanent(uid); + auto uid = convModel_->filteredConversation([smartView selectedRow]).uid; + convModel_->makePermanent(uid); } #pragma mark - ContextMenuDelegate - (NSMenu*) contextualMenuForRow:(int) index { - if (model_ == nil) + if (convModel_ == nil) return nil; - auto conversation = model_->filteredConversation(NSInteger(index)); + auto conversation = convModel_->filteredConversation(NSInteger(index)); @try { - auto contact = model_->owner.contactModel->getContact(conversation.participants[0]); + auto contact = convModel_->owner.contactModel->getContact(conversation.participants[0]); if (contact.profileInfo.type == lrc::api::profile::Type::INVALID) { return nil; } @@ -671,7 +771,7 @@ NSInteger const REQUEST_SEG = 1; } NSString * convUId = (NSString*)menuObject; std::string conversationID = std::string([convUId UTF8String]); - model_->makePermanent(conversationID); + convModel_->makePermanent(conversationID); } - (void) blockContact: (NSMenuItem* ) item { @@ -681,8 +781,8 @@ NSInteger const REQUEST_SEG = 1; } NSString * convUId = (NSString*)menuObject; std::string conversationID = std::string([convUId UTF8String]); - model_->clearHistory(conversationID); - model_->removeConversation(conversationID, true); + //convModel_->clearHistory(conversationID); + convModel_->removeConversation(conversationID, true); } - (void) audioCall: (NSMenuItem* ) item { @@ -692,7 +792,7 @@ NSInteger const REQUEST_SEG = 1; } NSString * convUId = (NSString*)menuObject; std::string conversationID = std::string([convUId UTF8String]); - model_->placeAudioOnlyCall(conversationID); + convModel_->placeAudioOnlyCall(conversationID); } @@ -703,7 +803,7 @@ NSInteger const REQUEST_SEG = 1; } NSString * convUId = (NSString*)menuObject; std::string conversationID = std::string([convUId UTF8String]); - model_->placeCall(conversationID); + convModel_->placeCall(conversationID); } - (void) clearConversation:(NSMenuItem* ) item { @@ -713,7 +813,57 @@ NSInteger const REQUEST_SEG = 1; } NSString * convUId = (NSString*)menuObject; std::string conversationID = std::string([convUId UTF8String]); - model_->clearHistory(conversationID); + convModel_->clearHistory(conversationID); +} + +- (void)acceptInvitation:(id)sender { + NSInteger row = [smartView rowForView:sender]; + + if (row == -1) + return; + if (convModel_ == nil) + return; + + auto conv = convModel_->filteredConversation(row); + auto& convID = conv.Info::uid; + + if (convID.empty()) + return; + convModel_->makePermanent(convID); +} + +- (void)refuseInvitation:(id)sender { + NSInteger row = [smartView rowForView:sender]; + + if (row == -1) + return; + if (convModel_ == nil) + return; + + auto conv = convModel_->filteredConversation(row); + auto& convID = conv.Info::uid; + + if (convID.empty()) + return; + convModel_->removeConversation(convID); +} + +- (void)blockPendingContact:(id)sender { + NSInteger row = [smartView rowForView:sender]; + + if (row == -1) + return; + if (convModel_ == nil) + return; + + auto conv = convModel_->filteredConversation(row); + auto& convID = conv.Info::uid; + + if (convID.empty()) + return; + convModel_->removeConversation(convID, true); + [self deselect]; + [delegate listTypeChanged]; } @end diff --git a/src/delegates/ImageManipulationDelegate.mm b/src/delegates/ImageManipulationDelegate.mm index f2014b6f..9420c653 100644 --- a/src/delegates/ImageManipulationDelegate.mm +++ b/src/delegates/ImageManipulationDelegate.mm @@ -277,13 +277,23 @@ namespace Interfaces { return pxm; } else { char color = contact.profileInfo.uri.at(0); + contact.profileInfo.alias.erase(std::remove(contact.profileInfo.alias.begin(), contact.profileInfo.alias.end(), '\n'), contact.profileInfo.alias.end()); + contact.profileInfo.alias.erase(std::remove(contact.profileInfo.alias.begin(), contact.profileInfo.alias.end(), ' '), contact.profileInfo.alias.end()); + contact.profileInfo.alias.erase(std::remove(contact.profileInfo.alias.begin(), contact.profileInfo.alias.end(), '\r'), contact.profileInfo.alias.end()); if (!contact.profileInfo.alias.empty()) { return drawDefaultUserPixmap(size, color, std::toupper(contact.profileInfo.alias.at(0))); } else if((contact.profileInfo.type == lrc::api::profile::Type::RING || contact.profileInfo.type == lrc::api::profile::Type::PENDING) && !contact.registeredName.empty()) { - return drawDefaultUserPixmap(size, color, std::toupper(contact.registeredName.at(0))); + contact.registeredName.erase(std::remove(contact.registeredName.begin(), contact.registeredName.end(), '\n'), contact.registeredName.end()); + contact.registeredName.erase(std::remove(contact.registeredName.begin(), contact.registeredName.end(), ' '), contact.registeredName.end()); + contact.registeredName.erase(std::remove(contact.registeredName.begin(), contact.registeredName.end(), '\r'), contact.registeredName.end()); + if(!contact.registeredName.empty()) { + return drawDefaultUserPixmap(size, color, std::toupper(contact.registeredName.at(0))); + } else { + return drawDefaultUserPixmapUriOnly(size, color); + } } else { return drawDefaultUserPixmapUriOnly(size, color); } diff --git a/src/utils.h b/src/utils.h index e6ba40f5..1c971a64 100755 --- a/src/utils.h +++ b/src/utils.h @@ -18,6 +18,7 @@ */ #import +#import "NSString+Extensions.h" #import #import #import @@ -28,19 +29,39 @@ static inline NSString* bestIDForConversation(const lrc::api::conversation::Info& conv, const lrc::api::ConversationModel& model) { auto contact = model.owner.contactModel->getContact(conv.participants[0]); - if (!contact.registeredName.empty()) - return @(contact.registeredName.c_str()); + if (!contact.registeredName.empty()) { + contact.registeredName.erase(std::remove(contact.registeredName.begin(), contact.registeredName.end(), '\n'), contact.registeredName.end()); + contact.registeredName.erase(std::remove(contact.registeredName.begin(), contact.registeredName.end(), '\r'), contact.registeredName.end()); + return [@(contact.registeredName.c_str()) removeEmptyLinesAtBorders]; + } else - return @(contact.profileInfo.uri.c_str()); + return [@(contact.profileInfo.uri.c_str()) removeEmptyLinesAtBorders]; } static inline NSString* bestNameForConversation(const lrc::api::conversation::Info& conv, const lrc::api::ConversationModel& model) { auto contact = model.owner.contactModel->getContact(conv.participants[0]); - if (!contact.profileInfo.alias.empty()) - return @(contact.profileInfo.alias.c_str()); - else + if (contact.profileInfo.alias.empty()) { + return bestIDForConversation(conv, model); + } + auto alias = contact.profileInfo.alias; + alias.erase(std::remove(alias.begin(), alias.end(), '\n'), alias.end()); + alias.erase(std::remove(alias.begin(), alias.end(), '\r'), alias.end()); + if(alias.length() == 0) { return bestIDForConversation(conv, model); + } + return @(alias.c_str()); +} + +static inline lrc::api::profile::Type profileType(const lrc::api::conversation::Info& conv, const lrc::api::ConversationModel& model) +{ + @try { + auto contact = model.owner.contactModel->getContact(conv.participants[0]); + return contact.profileInfo.type; + } + @catch (NSException *exception) { + lrc::api::profile::Type::INVALID; + } } /** diff --git a/src/views/IconButton.h b/src/views/IconButton.h index 66b53506..cd71176b 100644 --- a/src/views/IconButton.h +++ b/src/views/IconButton.h @@ -52,7 +52,7 @@ /* * Padding - * default value : 5.0 + * default value : 8.0 */ @property CGFloat imageInsets; diff --git a/src/views/IconButton.mm b/src/views/IconButton.mm index 96c3271c..a211fc1a 100644 --- a/src/views/IconButton.mm +++ b/src/views/IconButton.mm @@ -103,12 +103,9 @@ //// Group { //// Oval Drawing - NSBezierPath* ovalPath = [NSBezierPath bezierPathWithRoundedRect: - NSMakeRect(NSMinX(group) + floor(NSWidth(group) * 0.00000 + 0.5), - NSMinY(group) + floor(NSHeight(group) * 0.00000 + 0.5), - floor(NSWidth(group) * 1.00000 + 0.5) - floor(NSWidth(group) * 0.00000 + 0.5), - floor(NSHeight(group) * 1.00000 + 0.5) - floor(NSHeight(group) * 0.00000 + 0.5)) - xRadius:[self.cornerRadius floatValue] yRadius:[self.cornerRadius floatValue]]; + NSBezierPath* ovalPath = [NSBezierPath bezierPathWithRoundedRect:dirtyRect + xRadius:[self.cornerRadius floatValue] + yRadius:[self.cornerRadius floatValue]]; [backgroundColor setFill]; [ovalPath fill]; diff --git a/src/views/RoundedTextField.h b/src/views/RoundedTextField.h index dafbc35e..4f9e4eee 100644 --- a/src/views/RoundedTextField.h +++ b/src/views/RoundedTextField.h @@ -32,6 +32,11 @@ */ @property (nonatomic, strong) NSColor* borderColor; +/* + * default value : [NSNumber numberWithDouble:1.0]; + */ +@property (nonatomic, strong) NSNumber* borderThickness; + /* * default value : (self.frame) / 2; */ diff --git a/src/views/RoundedTextField.mm b/src/views/RoundedTextField.mm index e557b6f3..719d8f2f 100644 --- a/src/views/RoundedTextField.mm +++ b/src/views/RoundedTextField.mm @@ -34,6 +34,10 @@ self.borderColor = [self.bgColor darkenColorByValue:0.1]; } + if(!self.borderThickness) { + self.borderThickness = [NSNumber numberWithDouble:1.0]; + } + self.backgroundColor = [NSColor controlColor]; } @@ -42,6 +46,7 @@ NSColor* backgroundColor = self.bgColor; NSColor* borderColor = self.borderColor; + CGFloat borderThickness = [self.borderThickness floatValue]; NSRect group = NSMakeRect(NSMinX(dirtyRect) + floor(NSWidth(dirtyRect) * 0.03333) + 0.5, NSMinY(dirtyRect) + floor(NSHeight(dirtyRect) * 0.03333) + 0.5, @@ -57,7 +62,7 @@ [backgroundColor setFill]; [ovalPath fill]; [borderColor setStroke]; - [ovalPath setLineWidth: 1.0]; + [ovalPath setLineWidth: borderThickness]; [ovalPath stroke]; NSDictionary *att = nil; diff --git a/ui/Base.lproj/Conversation.xib b/ui/Base.lproj/Conversation.xib index 6cfcfe83..1fc0eabc 100644 --- a/ui/Base.lproj/Conversation.xib +++ b/ui/Base.lproj/Conversation.xib @@ -329,11 +329,11 @@ - + - + @@ -379,13 +379,13 @@ - + - - - + + + + + - - + + - + + - + - - - - - + + + + + + + + + + + @@ -199,28 +267,6 @@ - @@ -243,13 +289,9 @@ - - - - @@ -291,13 +333,13 @@ - + - - + + @@ -306,7 +348,7 @@ - + @@ -315,20 +357,64 @@ + + + - + - + + + + + - - + @@ -512,6 +598,7 @@ + @@ -527,8 +614,9 @@ + - + -- GitLab