blob: 7e72acab18c46817bc773680bfbaba5e8ab8c329 [file] [log] [blame]
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04001/*
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05002 * Copyright (C) 2015-2018 Savoir-faire Linux Inc.
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04003 * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
Anthony Léonard2382b562017-12-13 15:51:28 -05004 * Anthony Léonard <anthony.leonard@savoirfairelinux.com>
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04005 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 */
20
21#import <QItemSelectionModel>
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040022#import <QPixmap>
23#import <QtMacExtras/qmacfunctions.h>
24
Anthony Léonard2382b562017-12-13 15:51:28 -050025// LRC
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040026#import <globalinstances.h>
Anthony Léonard2382b562017-12-13 15:51:28 -050027#import <api/interaction.h>
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040028
29#import "MessagesVC.h"
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040030#import "views/IMTableCellView.h"
31#import "views/MessageBubbleView.h"
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040032#import "views/NSImage+Extensions.h"
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040033#import "INDSequentialTextSelectionManager.h"
Anthony Léonard2382b562017-12-13 15:51:28 -050034#import "delegates/ImageManipulationDelegate.h"
Anthony Léonard6f819752018-01-05 09:53:40 -050035#import "utils.h"
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040036#import "views/NSColor+RingTheme.h"
37
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040038
Anthony Léonard2382b562017-12-13 15:51:28 -050039@interface MessagesVC () <NSTableViewDelegate, NSTableViewDataSource> {
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040040
Anthony Léonard2382b562017-12-13 15:51:28 -050041 __unsafe_unretained IBOutlet NSTableView* conversationView;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040042
Anthony Léonard2382b562017-12-13 15:51:28 -050043 std::string convUid_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050044 lrc::api::ConversationModel* convModel_;
Anthony Léonard2382b562017-12-13 15:51:28 -050045 const lrc::api::conversation::Info* cachedConv_;
46
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050047 QMetaObject::Connection newInteractionSignal_;
Anthony Léonard2382b562017-12-13 15:51:28 -050048
49 // Both are needed to invalidate cached conversation as pointer
50 // may not be referencing the same conversation anymore
51 QMetaObject::Connection modelSortedSignal_;
52 QMetaObject::Connection filterChangedSignal_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050053 QMetaObject::Connection interactionStatusUpdatedSignal_;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040054}
55
56@property (nonatomic, strong, readonly) INDSequentialTextSelectionManager* selectionManager;
57
58@end
59
Anthony Léonardf2bb17d2018-02-15 17:18:09 -050060// Tags for view
61NSInteger const GENERIC_INT_TEXT_TAG = 100;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040062NSInteger const GENERIC_INT_TIME_TAG = 200;
63
64// views size
65CGFloat const GENERIC_CELL_HEIGHT = 60;
66CGFloat const TIME_BOX_HEIGHT = 34;
67CGFloat const MESSAGE_TEXT_PADDING = 10;
68CGFloat const MAX_TRANSFERED_IMAGE_SIZE = 250;
69CGFloat const BUBBLE_HEIGHT_FOR_TRANSFERED_FILE = 87;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -050070
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040071@implementation MessagesVC
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040072
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040073//MessageBuble type
74typedef NS_ENUM(NSInteger, MessageSequencing) {
75 SINGLE_WITH_TIME = 0,
76 SINGLE_WITHOUT_TIME = 1,
77 FIRST_WITH_TIME = 2,
78 FIRST_WITHOUT_TIME = 3,
79 MIDDLE_IN_SEQUENCE = 5,
80 LAST_IN_SEQUENCE = 6,
81};
82
83- (void)awakeFromNib
84{
85 NSNib *cellNib = [[NSNib alloc] initWithNibNamed:@"MessageCells" bundle:nil];
86 [conversationView registerNib:cellNib forIdentifier:@"LeftIncomingFileView"];
87 [conversationView registerNib:cellNib forIdentifier:@"LeftOngoingFileView"];
88 [conversationView registerNib:cellNib forIdentifier:@"LeftFinishedFileView"];
89 [conversationView registerNib:cellNib forIdentifier:@"RightOngoingFileView"];
90 [conversationView registerNib:cellNib forIdentifier:@"RightFinishedFileView"];
91}
92
Anthony Léonard2382b562017-12-13 15:51:28 -050093-(const lrc::api::conversation::Info*) getCurrentConversation
94{
95 if (convModel_ == nil || convUid_.empty())
96 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040097
Anthony Léonard2382b562017-12-13 15:51:28 -050098 if (cachedConv_ != nil)
99 return cachedConv_;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400100
Anthony Léonard6f819752018-01-05 09:53:40 -0500101 auto it = getConversationFromUid(convUid_, *convModel_);
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400102 if (it != convModel_->allFilteredConversations().end())
Anthony Léonard2382b562017-12-13 15:51:28 -0500103 cachedConv_ = &(*it);
104
105 return cachedConv_;
106}
107
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400108-(void) reloadConversationForMessage:(uint64_t) uid shouldUpdateHeight:(bool)update {
109 auto* conv = [self getCurrentConversation];
110
111 if (conv == nil)
112 return;
113 auto it = distance(conv->interactions.begin(),conv->interactions.find(uid));
114 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:it];
115 //reload previous message to update bubbleview
116 if (it > 0) {
117 NSRange range = NSMakeRange(it - 1, it);
118 indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
119 }
120 if (update) {
121 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
122 }
123 [conversationView reloadDataForRowIndexes: indexSet
124 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
125 if (update) {
126 [conversationView scrollToEndOfDocument:nil];
127 }
128}
129
130-(void) reloadConversationForMessage:(uint64_t) uid shouldUpdateHeight:(bool)update updateConversation:(bool) updateConversation {
131 auto* conv = [self getCurrentConversation];
132
133 if (conv == nil)
134 return;
135 auto it = distance(conv->interactions.begin(),conv->interactions.find(uid));
136 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:it];
137 //reload previous message to update bubbleview
138 if (it > 0) {
139 NSRange range = NSMakeRange(it - 1, it);
140 indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
141 }
142 if (update) {
143 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
144 }
145 [conversationView reloadDataForRowIndexes: indexSet
146 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
147 if (update) {
148 [conversationView scrollToEndOfDocument:nil];
149 }
150}
151
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500152-(void)setConversationUid:(const std::string)convUid model:(lrc::api::ConversationModel *)model
Anthony Léonard2382b562017-12-13 15:51:28 -0500153{
154 if (convUid_ == convUid && convModel_ == model)
155 return;
156
157 cachedConv_ = nil;
158 convUid_ = convUid;
159 convModel_ = model;
160
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500161 // Signal triggered when messages are received or their status updated
162 QObject::disconnect(newInteractionSignal_);
163 QObject::disconnect(interactionStatusUpdatedSignal_);
164 newInteractionSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newInteraction,
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400165 [self](const std::string& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
166 if (uid != convUid_)
167 return;
168 cachedConv_ = nil;
169 [conversationView noteNumberOfRowsChanged];
170 [self reloadConversationForMessage:interactionId shouldUpdateHeight:YES];
171 });
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500172 interactionStatusUpdatedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::interactionStatusUpdated,
173 [self](const std::string& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
174 if (uid != convUid_)
175 return;
176 cachedConv_ = nil;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400177 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
178 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
179 convModel_->refreshFilter();
180 }
181 [self reloadConversationForMessage:interactionId shouldUpdateHeight:YES];
182 //accept incoming transfer
183 if (interaction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER &&
184 (interaction.status == lrc::api::interaction::Status::TRANSFER_AWAITING_HOST ||
185 interaction.status == lrc::api::interaction::Status::TRANSFER_CREATED)) {
186 lrc::api::datatransfer::Info info = {};
187 convModel_->getTransferInfo(interactionId, info);
188 double convertData = static_cast<double>(info.totalSize);
189 NSString* pathUrl = @(info.displayName.c_str());
190
191 NSString* fileExtension = pathUrl.pathExtension;
192
193 CFStringRef utiType = UTTypeCreatePreferredIdentifierForTag(
194 kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtension, NULL);
195
196 bool isImage = UTTypeConformsTo(utiType, kUTTypeImage);
197 if( convertData <= 10485760 && isImage) {
198 [self acceptFile:interactionId];
199 }
200 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500201 });
Anthony Léonard2382b562017-12-13 15:51:28 -0500202
203 // Signals tracking changes in conversation list, we need them as cached conversation can be invalid
204 // after a reordering.
205 QObject::disconnect(modelSortedSignal_);
206 QObject::disconnect(filterChangedSignal_);
207 modelSortedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::modelSorted,
208 [self](){
209 cachedConv_ = nil;
210 });
211 filterChangedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::filterChanged,
212 [self](){
213 cachedConv_ = nil;
214 });
Anthony Léonard2382b562017-12-13 15:51:28 -0500215 [conversationView reloadData];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400216 [conversationView scrollToEndOfDocument:nil];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400217}
218
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400219#pragma mark - configure cells
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400220
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400221-(NSTableCellView*) makeGenericInteractionViewForTableView:(NSTableView*)tableView withText:(NSString*)text andTime:(NSString*) time
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500222{
223 NSTableCellView* result = [tableView makeViewWithIdentifier:@"GenericInteractionView" owner:self];
224 NSTextField* textField = [result viewWithTag:GENERIC_INT_TEXT_TAG];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400225 NSTextField* timeField = [result viewWithTag:GENERIC_INT_TIME_TAG];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500226
227 // TODO: Fix symbol in LRC
228 NSString* fixedString = [text stringByReplacingOccurrencesOfString:@"🕽" withString:@"📞"];
229 [textField setStringValue:fixedString];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400230 [timeField setStringValue:time];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500231
232 return result;
233}
234
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400235-(NSTableCellView*) configureViewforTransfer:(lrc::api::interaction::Info)interaction interactionID: (uint64_t) interactionID tableView:(NSTableView*)tableView
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500236{
237 IMTableCellView* result;
238
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400239 auto type = interaction.type;
240 auto status = interaction.status;
241
242 NSString* fileName = @"incoming file";
243
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500244 // First, view is created
245 if (type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER) {
246 switch (status) {
247 case lrc::api::interaction::Status::TRANSFER_CREATED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400248 case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST: {
249 result = [tableView makeViewWithIdentifier:@"LeftIncomingFileView" owner: conversationView];
250 [result.acceptButton setAction:@selector(acceptIncomingFile:)];
251 [result.acceptButton setTarget:self];
252 [result.declineButton setAction:@selector(declineIncomingFile:)];
253 [result.declineButton setTarget:self];
254 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500255 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400256 case lrc::api::interaction::Status::TRANSFER_ONGOING: {
257 result = [tableView makeViewWithIdentifier:@"LeftOngoingFileView" owner:conversationView];
258 [result.progressIndicator startAnimation:conversationView];
259 [result.declineButton setAction:@selector(declineIncomingFile:)];
260 [result.declineButton setTarget:self];
261 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500262 case lrc::api::interaction::Status::TRANSFER_FINISHED:
263 case lrc::api::interaction::Status::TRANSFER_CANCELED:
264 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400265 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
266 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500267 }
268 } else if (type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400269 NSString* fileName = @"sent file";
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500270 switch (status) {
271 case lrc::api::interaction::Status::TRANSFER_CREATED:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500272 case lrc::api::interaction::Status::TRANSFER_ONGOING:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400273 case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500274 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400275 result = [tableView makeViewWithIdentifier:@"RightOngoingFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500276 [result.progressIndicator startAnimation:nil];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400277 [result.declineButton setAction:@selector(declineIncomingFile:)];
278 [result.declineButton setTarget:self];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500279 break;
280 case lrc::api::interaction::Status::TRANSFER_FINISHED:
281 case lrc::api::interaction::Status::TRANSFER_CANCELED:
282 case lrc::api::interaction::Status::TRANSFER_ERROR:
Olivier Soldanoe521a182018-02-26 16:55:19 -0500283 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400284 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500285 }
286 }
287
288 // Then status label is updated if needed
289 switch (status) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400290 [result.statusLabel setTextColor:[NSColor textColor]];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500291 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400292 [result.statusLabel setTextColor:[NSColor greenColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500293 [result.statusLabel setStringValue:NSLocalizedString(@"Success", @"File transfer successful label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500294 break;
295 case lrc::api::interaction::Status::TRANSFER_CANCELED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400296 [result.statusLabel setTextColor:[NSColor orangeColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500297 [result.statusLabel setStringValue:NSLocalizedString(@"Canceled", @"File transfer canceled label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500298 break;
299 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400300 [result.statusLabel setTextColor:[NSColor redColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500301 [result.statusLabel setStringValue:NSLocalizedString(@"Failed", @"File transfer failed label")];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500302 break;
303 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400304 [result.statusLabel setTextColor:[NSColor textColor]];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500305 [result.statusLabel setStringValue:NSLocalizedString(@"Unjoinable", @"File transfer peer unjoinable label")];
306 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500307 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400308 result.transferedImage.image = nil;
309 [result.msgBackground setHidden:NO];
310 [result invalidateImageConstraints];
311 NSString* name = @(interaction.body.c_str());
312 if (name.length > 0) {
313 if (([name rangeOfString:@"/"].location != NSNotFound)) {
314 NSArray *listItems = [name componentsSeparatedByString:@"/"];
315 NSString* name1 = listItems.lastObject;
316 fileName = name1;
317 } else {
318 fileName = name;
319 }
320 }
321 result.transferedFileName.stringValue = fileName;
322 if (status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
323 NSImage* image = [self getImageForFilePath:name];
324 if (([name rangeOfString:@"/"].location == NSNotFound)) {
325 image = [self getImageForFilePath:[self getDataTransferPath:interactionID]];
326 }
327 if(image != nil) {
328 result.transferedImage.image = [image roundCorners:14];
329 [result updateImageConstraint:image.size.width andHeight:image.size.height];
330 }
331 }
332 [result setupForInteraction:interactionID];
333 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
334 NSString* timeString = [self timeForMessage: msgTime];
335 result.timeLabel.stringValue = timeString;
336 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
337 if (!isOutgoing) {
338 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
339 auto* conv = [self getCurrentConversation];
340 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
341 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500342 return result;
343}
344
Anthony Léonard2382b562017-12-13 15:51:28 -0500345#pragma mark - NSTableViewDelegate methods
346- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400347{
348 return YES;
349}
350
Anthony Léonard2382b562017-12-13 15:51:28 -0500351- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400352{
Anthony Léonard2382b562017-12-13 15:51:28 -0500353 return NO;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400354}
355
Anthony Léonard2382b562017-12-13 15:51:28 -0500356- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400357{
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400358
Anthony Léonard2382b562017-12-13 15:51:28 -0500359 auto* conv = [self getCurrentConversation];
360
361 if (conv == nil)
362 return nil;
363
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500364 auto it = conv->interactions.begin();
Anthony Léonard2382b562017-12-13 15:51:28 -0500365
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500366 std::advance(it, row);
Anthony Léonard2382b562017-12-13 15:51:28 -0500367
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400368 IMTableCellView* result;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400369 auto interaction = it->second;
Anthony Léonard2382b562017-12-13 15:51:28 -0500370 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
371
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500372 switch (interaction.type) {
373 case lrc::api::interaction::Type::TEXT:
374 if (isOutgoing) {
375 result = [tableView makeViewWithIdentifier:@"RightMessageView" owner:self];
376 } else {
377 result = [tableView makeViewWithIdentifier:@"LeftMessageView" owner:self];
378 }
Anthony Léonarddd85dee2018-02-19 14:34:35 -0500379 if (interaction.status == lrc::api::interaction::Status::UNREAD)
380 convModel_->setInteractionRead(convUid_, it->first);
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500381 break;
382 case lrc::api::interaction::Type::INCOMING_DATA_TRANSFER:
383 case lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400384 return [self configureViewforTransfer:interaction interactionID: it->first tableView:tableView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500385 break;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500386 case lrc::api::interaction::Type::CONTACT:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400387 case lrc::api::interaction::Type::CALL: {
388 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
389 NSString* timeString = [self timeForMessage: msgTime];
390 return [self makeGenericInteractionViewForTableView:tableView withText:@(interaction.body.c_str()) andTime:timeString];
391 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500392 default: // If interaction is not of a known type
393 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400394 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400395 MessageSequencing sequence = [self computeSequencingFor:row];
396 BubbleType type = SINGLE;
397 if (sequence == FIRST_WITHOUT_TIME || sequence == FIRST_WITH_TIME) {
398 type = FIRST;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400399 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400400 if (sequence == MIDDLE_IN_SEQUENCE) {
401 type = MIDDLE;
402 }
403 if (sequence == LAST_IN_SEQUENCE) {
404 type = LAST;
405 }
406 result.msgBackground.type = type;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500407 [result setupForInteraction:it->first];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400408 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
409 [result.msgBackground setNeedsDisplay:YES];
410 [result setNeedsDisplay:YES];
411 [result.timeBox setNeedsDisplay:YES];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400412
413 NSMutableAttributedString* msgAttString =
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400414 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@",@(interaction.body.c_str())]
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400415 attributes:[self messageAttributes]];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400416
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400417 CGSize messageSize = [self sizeFor: @(interaction.body.c_str()) maxWidth:tableView.frame.size.width * 0.7];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400418
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400419 [result updateMessageConstraint:messageSize.width andHeight:messageSize.height timeIsVisible:shouldDisplayTime];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400420 [[result.msgView textStorage] appendAttributedString:msgAttString];
421 [result.msgView checkTextInDocument:nil];
Anthony Léonard2382b562017-12-13 15:51:28 -0500422
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400423 if (shouldDisplayTime) {
424 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
425 NSString* timeString = [self timeForMessage: msgTime];
426 result.timeLabel.stringValue = timeString;
Anthony Léonard64e19672018-01-18 16:40:34 -0500427 }
428
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400429 bool shouldDisplayAvatar = (sequence != MIDDLE_IN_SEQUENCE && sequence != FIRST_WITHOUT_TIME
430 && sequence != FIRST_WITH_TIME) ? YES : NO;
431 [result.photoView setHidden:!shouldDisplayAvatar];
432 if (!isOutgoing && shouldDisplayAvatar) {
433 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
434 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
435 }
436 [result.messageStatus setHidden:YES];
437 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
438 if (interaction.status == lrc::api::interaction::Status::SENDING) {
439 [result.messageStatus setHidden:NO];
440 [result.sendingMessageIndicator startAnimation:nil];
441 [result.messageFailed setHidden:YES];
442 } else if (interaction.status == lrc::api::interaction::Status::FAILED) {
443 [result.messageStatus setHidden:NO];
444 [result.sendingMessageIndicator setHidden:YES];
445 [result.messageFailed setHidden:NO];
446 }
447 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400448 return result;
449}
450
Anthony Léonard2382b562017-12-13 15:51:28 -0500451- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400452{
Anthony Léonard2382b562017-12-13 15:51:28 -0500453 double someWidth = tableView.frame.size.width * 0.7;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400454
Anthony Léonard2382b562017-12-13 15:51:28 -0500455 auto* conv = [self getCurrentConversation];
456
457 if (conv == nil)
458 return 0;
459
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500460 auto it = conv->interactions.begin();
Anthony Léonard2382b562017-12-13 15:51:28 -0500461
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500462 std::advance(it, row);
Anthony Léonard2382b562017-12-13 15:51:28 -0500463
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400464 auto interaction = it->second;
Anthony Léonard2382b562017-12-13 15:51:28 -0500465
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400466 MessageSequencing sequence = [self computeSequencingFor:row];
467
468 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
469
470
471 if(interaction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER || interaction.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) {
472
473 if( interaction.status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
474 NSString* name = @(interaction.body.c_str());
475 NSImage* image = [self getImageForFilePath:name];
476 if (([name rangeOfString:@"/"].location == NSNotFound)) {
477 image = [self getImageForFilePath:[self getDataTransferPath:it->first]];
478 }
479 if (image != nil) {
480 return image.size.height + TIME_BOX_HEIGHT;
481 }
482 }
483 return BUBBLE_HEIGHT_FOR_TRANSFERED_FILE + TIME_BOX_HEIGHT;
484 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500485
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500486 if(interaction.type == lrc::api::interaction::Type::CONTACT || interaction.type == lrc::api::interaction::Type::CALL)
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400487 return GENERIC_CELL_HEIGHT;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500488
Anthony Léonard2382b562017-12-13 15:51:28 -0500489 // TODO Implement interactions other than messages
490 if(interaction.type != lrc::api::interaction::Type::TEXT) {
491 return 0;
492 }
493
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400494 CGSize messageSize = [self sizeFor: @(interaction.body.c_str()) maxWidth:tableView.frame.size.width * 0.7];
495 CGFloat singleLignMessageHeight = 15;
496
497 if (shouldDisplayTime) {
498 return MAX(messageSize.height + TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2,
499 TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2 + singleLignMessageHeight);
500 }
501 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2,
502 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2);
503}
504
505#pragma mark - message view parameters
506
507-(NSString *) getDataTransferPath:(uint64_t)interactionId {
508 lrc::api::datatransfer::Info info = {};
509 convModel_->getTransferInfo(interactionId, info);
510 double convertData = static_cast<double>(info.totalSize);
511 return @(info.path.c_str());
512}
513
514-(NSImage*) getImageForFilePath: (NSString *) path {
515 if (path.length <= 0) {return nil;}
516 if (![[NSFileManager defaultManager] fileExistsAtPath: path]) {return nil;}
517 NSImage* transferedImage = [[NSImage alloc] initWithContentsOfFile: path];
518 if(transferedImage != nil) {
519 return [transferedImage imageResizeInsideMax: MAX_TRANSFERED_IMAGE_SIZE];
520 }
521 return nil;
522}
523
524-(CGSize) sizeFor:(NSString *) message maxWidth:(CGFloat) width {
525 CGFloat horizaontalMargin = 6;
Anthony Léonard2382b562017-12-13 15:51:28 -0500526 NSMutableAttributedString* msgAttString =
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400527 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@", message]
Anthony Léonard2382b562017-12-13 15:51:28 -0500528 attributes:[self messageAttributes]];
529
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400530 CGFloat finalWidth = MIN(msgAttString.size.width + horizaontalMargin * 2, width);
531 NSRect frame = NSMakeRect(0, 0, finalWidth, msgAttString.size.height);
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400532 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400533 [[tv textStorage] setAttributedString:msgAttString];
534 [tv sizeToFit];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400535 return tv.frame.size;
536}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400537
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400538-(MessageSequencing) computeSequencingFor:(NSInteger) row {
539 auto* conv = [self getCurrentConversation];
540 if (conv == nil)
541 return SINGLE_WITHOUT_TIME;
542 auto it = conv->interactions.begin();
543 std::advance(it, row);
544 auto interaction = it->second;
545 if (interaction.type != lrc::api::interaction::Type::TEXT) {
546 return SINGLE_WITH_TIME;
547 }
548 if (row == 0) {
549 if (it == conv->interactions.end()) {
550 return SINGLE_WITH_TIME;
551 }
552 auto nextIt = it;
553 nextIt++;
554 auto nextInteraction = nextIt->second;
555 if ([self sequenceChangedFrom:interaction to: nextInteraction]) {
556 return SINGLE_WITH_TIME;
557 }
558 return FIRST_WITH_TIME;
559 }
560
561 if (row == conversationView.numberOfRows - 1) {
562 if(it == conv->interactions.begin()) {
563 return SINGLE_WITH_TIME;
564 }
565 auto previousIt = it;
566 previousIt--;
567 auto previousInteraction = previousIt->second;
568 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
569 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
570 if (!timeChanged && !authorChanged) {
571 return LAST_IN_SEQUENCE;
572 }
573 if (!timeChanged && authorChanged) {
574 return SINGLE_WITHOUT_TIME;
575 }
576 return SINGLE_WITH_TIME;
577 }
578 if(it == conv->interactions.begin() || it == conv->interactions.end()) {
579 return SINGLE_WITH_TIME;
580 }
581 auto previousIt = it;
582 previousIt--;
583 auto previousInteraction = previousIt->second;
584 auto nextIt = it;
585 nextIt++;
586 auto nextInteraction = nextIt->second;
587
588 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
589 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
590 bool sequenceWillChange = [self sequenceChangedFrom:interaction to: nextInteraction];
591 if (!sequenceWillChange) {
592 if (!timeChanged && !authorChanged) {
593 return MIDDLE_IN_SEQUENCE;
594 }
595 if (timeChanged) {
596 return FIRST_WITH_TIME;
597 }
598 return FIRST_WITHOUT_TIME;
599 } if (!timeChanged && !authorChanged) {
600 return LAST_IN_SEQUENCE;
601 } if (timeChanged) {
602 return SINGLE_WITH_TIME;
603 }
604 return SINGLE_WITHOUT_TIME;
605}
606
607-(bool) sequenceChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
608 return ([self sequenceTimeChangedFrom:firstInteraction to:secondInteraction] || [self sequenceAuthorChangedFrom:firstInteraction to:secondInteraction]);
609}
610
611-(bool) sequenceTimeChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
612 bool timeChanged = NO;
613 NSDate* firstMessageTime = [NSDate dateWithTimeIntervalSince1970:firstInteraction.timestamp];
614 NSDate* secondMessageTime = [NSDate dateWithTimeIntervalSince1970:secondInteraction.timestamp];
615 bool hourComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitHour];
616 bool minutComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitMinute];
617 if(hourComp != NSOrderedSame || minutComp != NSOrderedSame) {
618 timeChanged = YES;
619 }
620 return timeChanged;
621}
622
623-(bool) sequenceAuthorChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
624 bool authorChanged = YES;
625 bool isOutgoing = lrc::api::interaction::isOutgoing(firstInteraction);
626 if ((secondInteraction.type == lrc::api::interaction::Type::TEXT) && (isOutgoing == lrc::api::interaction::isOutgoing(secondInteraction))) {
627 authorChanged = NO;
628 }
629 return authorChanged;
630}
631
632-(NSString *)timeForMessage:(NSDate*) msgTime {
633 NSDate *today = [NSDate date];
634 NSDateFormatter *dateFormatter=[[NSDateFormatter alloc] init];
635 if ([[NSCalendar currentCalendar] compareDate:today
636 toDate:msgTime
637 toUnitGranularity:NSCalendarUnitYear]!= NSOrderedSame) {
638 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterMediumStyle];
639 }
640
641 if ([[NSCalendar currentCalendar] compareDate:today
642 toDate:msgTime
643 toUnitGranularity:NSCalendarUnitDay]!= NSOrderedSame ||
644 [[NSCalendar currentCalendar] compareDate:today
645 toDate:msgTime
646 toUnitGranularity:NSCalendarUnitMonth]!= NSOrderedSame) {
647 [dateFormatter setDateFormat:@"MMM dd, HH:mm"];
648 return [dateFormatter stringFromDate:msgTime];
649 }
650
651 [dateFormatter setDateFormat:@"HH:mm"];
652 return [dateFormatter stringFromDate:msgTime];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400653}
654
Anthony Léonard2382b562017-12-13 15:51:28 -0500655#pragma mark - NSTableViewDataSource
656
657- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
658{
659 auto* conv = [self getCurrentConversation];
660
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500661 if (conv)
662 return conv->interactions.size();
663 else
664 return 0;
Anthony Léonard2382b562017-12-13 15:51:28 -0500665}
666
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400667#pragma mark - Text formatting
668
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400669- (NSMutableDictionary*) messageAttributes
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400670{
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400671 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400672 attrs[NSForegroundColorAttributeName] = [NSColor labelColor];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400673 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400674 return attrs;
675}
676
677- (NSParagraphStyle*) paragraphStyle
678{
679 /*
680 The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
681 NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
682 to copy, we use the default one.
683
684 The default values supplied by the default NSParagraphStyle are:
685 Alignment NSNaturalTextAlignment
686 Tab stops 12 left-aligned tabs, spaced by 28.0 points
687 Line break mode NSLineBreakByWordWrapping
688 All others 0.0
689 */
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400690 NSMutableParagraphStyle* aMutableParagraphStyle =
691 [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
692 [aMutableParagraphStyle setHeadIndent:1.0];
693 [aMutableParagraphStyle setFirstLineHeadIndent:1.0];
694 return aMutableParagraphStyle;
695}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400696
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400697- (void)acceptFile:(uint64_t)interactionID {
698 NSURL *downloadsURL = [[NSFileManager defaultManager]
699 URLForDirectory:NSDownloadsDirectory
700 inDomain:NSUserDomainMask appropriateForURL:nil
701 create:YES error:nil];
702 auto& inter = [self getCurrentConversation]->interactions.find(interactionID)->second;
703 if (convModel_ && !convUid_.empty()) {
704 NSURL *bUrl = [downloadsURL URLByAppendingPathComponent:@(inter.body.c_str())];
705 const char* fullPath = [bUrl fileSystemRepresentation];
706 convModel_->acceptTransfer(convUid_, interactionID, fullPath);
707 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400708}
709
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500710#pragma mark - Actions
711
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400712- (void)acceptIncomingFile:(id)sender {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500713 auto interId = [(IMTableCellView*)[[sender superview] superview] interaction];
714 auto& inter = [self getCurrentConversation]->interactions.find(interId)->second;
715 if (convModel_ && !convUid_.empty()) {
716 NSSavePanel* filePicker = [NSSavePanel savePanel];
717 [filePicker setNameFieldStringValue:@(inter.body.c_str())];
718
719 if ([filePicker runModal] == NSFileHandlingPanelOKButton) {
720 const char* fullPath = [[filePicker URL] fileSystemRepresentation];
721 convModel_->acceptTransfer(convUid_, interId, fullPath);
722 }
723 }
724}
725
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400726- (void)declineIncomingFile:(id)sender {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500727 auto inter = [(IMTableCellView*)[[sender superview] superview] interaction];
728 if (convModel_ && !convUid_.empty()) {
729 convModel_->cancelTransfer(convUid_, inter);
730 }
731}
732
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400733@end