blob: 44e9033b28c04a3b7c98d6a4767dbd820d412da5 [file] [log] [blame]
Andreas Traczyk252a94a2018-04-20 16:36:20 -04001
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04002/*
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05003 * Copyright (C) 2015-2018 Savoir-faire Linux Inc.
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04004 * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
Anthony Léonard2382b562017-12-13 15:51:28 -05005 * Anthony Léonard <anthony.leonard@savoirfairelinux.com>
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04006 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 */
21
22#import <QItemSelectionModel>
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040023#import <QPixmap>
24#import <QtMacExtras/qmacfunctions.h>
25
Anthony Léonard2382b562017-12-13 15:51:28 -050026// LRC
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040027#import <globalinstances.h>
Anthony Léonard2382b562017-12-13 15:51:28 -050028#import <api/interaction.h>
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040029
30#import "MessagesVC.h"
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040031#import "views/IMTableCellView.h"
32#import "views/MessageBubbleView.h"
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040033#import "views/NSImage+Extensions.h"
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040034#import "INDSequentialTextSelectionManager.h"
Anthony Léonard2382b562017-12-13 15:51:28 -050035#import "delegates/ImageManipulationDelegate.h"
Anthony Léonard6f819752018-01-05 09:53:40 -050036#import "utils.h"
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040037#import "views/NSColor+RingTheme.h"
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040038#import "views/IconButton.h"
39#import <QuickLook/QuickLook.h>
40#import <Quartz/Quartz.h>
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040041
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040042
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040043@interface MessagesVC () <NSTableViewDelegate, NSTableViewDataSource, QLPreviewPanelDataSource> {
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040044
Anthony Léonard2382b562017-12-13 15:51:28 -050045 __unsafe_unretained IBOutlet NSTableView* conversationView;
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040046 __unsafe_unretained IBOutlet NSView* containerView;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040047
Anthony Léonard2382b562017-12-13 15:51:28 -050048 std::string convUid_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050049 lrc::api::ConversationModel* convModel_;
Anthony Léonard2382b562017-12-13 15:51:28 -050050 const lrc::api::conversation::Info* cachedConv_;
51
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050052 QMetaObject::Connection newInteractionSignal_;
Anthony Léonard2382b562017-12-13 15:51:28 -050053
54 // Both are needed to invalidate cached conversation as pointer
55 // may not be referencing the same conversation anymore
56 QMetaObject::Connection modelSortedSignal_;
57 QMetaObject::Connection filterChangedSignal_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050058 QMetaObject::Connection interactionStatusUpdatedSignal_;
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040059 NSString* previewImage;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040060}
61
62@property (nonatomic, strong, readonly) INDSequentialTextSelectionManager* selectionManager;
63
64@end
65
Anthony Léonardf2bb17d2018-02-15 17:18:09 -050066// Tags for view
67NSInteger const GENERIC_INT_TEXT_TAG = 100;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040068NSInteger const GENERIC_INT_TIME_TAG = 200;
69
70// views size
71CGFloat const GENERIC_CELL_HEIGHT = 60;
72CGFloat const TIME_BOX_HEIGHT = 34;
73CGFloat const MESSAGE_TEXT_PADDING = 10;
74CGFloat const MAX_TRANSFERED_IMAGE_SIZE = 250;
75CGFloat const BUBBLE_HEIGHT_FOR_TRANSFERED_FILE = 87;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -050076
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040077@implementation MessagesVC
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040078
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040079
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040080//MessageBuble type
81typedef NS_ENUM(NSInteger, MessageSequencing) {
82 SINGLE_WITH_TIME = 0,
83 SINGLE_WITHOUT_TIME = 1,
84 FIRST_WITH_TIME = 2,
85 FIRST_WITHOUT_TIME = 3,
86 MIDDLE_IN_SEQUENCE = 5,
87 LAST_IN_SEQUENCE = 6,
88};
89
90- (void)awakeFromNib
91{
92 NSNib *cellNib = [[NSNib alloc] initWithNibNamed:@"MessageCells" bundle:nil];
93 [conversationView registerNib:cellNib forIdentifier:@"LeftIncomingFileView"];
94 [conversationView registerNib:cellNib forIdentifier:@"LeftOngoingFileView"];
95 [conversationView registerNib:cellNib forIdentifier:@"LeftFinishedFileView"];
96 [conversationView registerNib:cellNib forIdentifier:@"RightOngoingFileView"];
97 [conversationView registerNib:cellNib forIdentifier:@"RightFinishedFileView"];
98}
Andreas Traczyk252a94a2018-04-20 16:36:20 -040099-(void) clearData {
100 cachedConv_ = nil;
101 convUid_ = "";
102 convModel_ = nil;
103
104 QObject::disconnect(modelSortedSignal_);
105 QObject::disconnect(filterChangedSignal_);
106 QObject::disconnect(interactionStatusUpdatedSignal_);
107 QObject::disconnect(newInteractionSignal_);
108}
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400109
Anthony Léonard2382b562017-12-13 15:51:28 -0500110-(const lrc::api::conversation::Info*) getCurrentConversation
111{
112 if (convModel_ == nil || convUid_.empty())
113 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400114
Anthony Léonard2382b562017-12-13 15:51:28 -0500115 if (cachedConv_ != nil)
116 return cachedConv_;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400117
Anthony Léonard6f819752018-01-05 09:53:40 -0500118 auto it = getConversationFromUid(convUid_, *convModel_);
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400119 if (it != convModel_->allFilteredConversations().end())
Anthony Léonard2382b562017-12-13 15:51:28 -0500120 cachedConv_ = &(*it);
121
122 return cachedConv_;
123}
124
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400125-(void) reloadConversationForMessage:(uint64_t) uid shouldUpdateHeight:(bool)update {
126 auto* conv = [self getCurrentConversation];
127
128 if (conv == nil)
129 return;
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400130 auto it = conv->interactions.find(uid);
131 if (it == conv->interactions.end()) {
132 return;
133 }
134 auto itIndex = distance(conv->interactions.begin(),it);
135 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:itIndex];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400136 //reload previous message to update bubbleview
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400137 if (itIndex > 0) {
138 auto previousIt = it;
139 previousIt--;
140 auto previousInteraction = previousIt->second;
141 if (previousInteraction.type == lrc::api::interaction::Type::TEXT) {
142 NSRange range = NSMakeRange(itIndex - 1, 2);
143 indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
144 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400145 }
146 if (update) {
147 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
148 }
149 [conversationView reloadDataForRowIndexes: indexSet
150 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400151 CGRect visibleRect = [conversationView enclosingScrollView].contentView.visibleRect;
152 NSRange range = [conversationView rowsInRect:visibleRect];
153 NSIndexSet* visibleIndexes = [NSIndexSet indexSetWithIndexesInRange:range];
154 NSUInteger lastvisibleRow = [visibleIndexes lastIndex];
155 if (([conversationView numberOfRows] > 0) &&
156 lastvisibleRow == ([conversationView numberOfRows] -1)) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400157 [conversationView scrollToEndOfDocument:nil];
158 }
159}
160
161-(void) reloadConversationForMessage:(uint64_t) uid shouldUpdateHeight:(bool)update updateConversation:(bool) updateConversation {
162 auto* conv = [self getCurrentConversation];
163
164 if (conv == nil)
165 return;
166 auto it = distance(conv->interactions.begin(),conv->interactions.find(uid));
167 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:it];
168 //reload previous message to update bubbleview
169 if (it > 0) {
170 NSRange range = NSMakeRange(it - 1, it);
171 indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
172 }
173 if (update) {
174 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
175 }
176 [conversationView reloadDataForRowIndexes: indexSet
177 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
178 if (update) {
179 [conversationView scrollToEndOfDocument:nil];
180 }
181}
182
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500183-(void)setConversationUid:(const std::string)convUid model:(lrc::api::ConversationModel *)model
Anthony Léonard2382b562017-12-13 15:51:28 -0500184{
185 if (convUid_ == convUid && convModel_ == model)
186 return;
187
188 cachedConv_ = nil;
189 convUid_ = convUid;
190 convModel_ = model;
191
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500192 // Signal triggered when messages are received or their status updated
193 QObject::disconnect(newInteractionSignal_);
194 QObject::disconnect(interactionStatusUpdatedSignal_);
195 newInteractionSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newInteraction,
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400196 [self](const std::string& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
197 if (uid != convUid_)
198 return;
199 cachedConv_ = nil;
200 [conversationView noteNumberOfRowsChanged];
201 [self reloadConversationForMessage:interactionId shouldUpdateHeight:YES];
202 });
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500203 interactionStatusUpdatedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::interactionStatusUpdated,
204 [self](const std::string& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
205 if (uid != convUid_)
206 return;
207 cachedConv_ = nil;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400208 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
209 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
210 convModel_->refreshFilter();
211 }
212 [self reloadConversationForMessage:interactionId shouldUpdateHeight:YES];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500213 });
Anthony Léonard2382b562017-12-13 15:51:28 -0500214
215 // Signals tracking changes in conversation list, we need them as cached conversation can be invalid
216 // after a reordering.
217 QObject::disconnect(modelSortedSignal_);
218 QObject::disconnect(filterChangedSignal_);
219 modelSortedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::modelSorted,
220 [self](){
221 cachedConv_ = nil;
222 });
223 filterChangedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::filterChanged,
224 [self](){
225 cachedConv_ = nil;
226 });
Anthony Léonard2382b562017-12-13 15:51:28 -0500227 [conversationView reloadData];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400228 [conversationView scrollToEndOfDocument:nil];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400229}
230
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400231#pragma mark - configure cells
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400232
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400233-(NSTableCellView*) makeGenericInteractionViewForTableView:(NSTableView*)tableView withText:(NSString*)text andTime:(NSString*) time
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500234{
235 NSTableCellView* result = [tableView makeViewWithIdentifier:@"GenericInteractionView" owner:self];
236 NSTextField* textField = [result viewWithTag:GENERIC_INT_TEXT_TAG];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400237 NSTextField* timeField = [result viewWithTag:GENERIC_INT_TIME_TAG];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500238
239 // TODO: Fix symbol in LRC
240 NSString* fixedString = [text stringByReplacingOccurrencesOfString:@"🕽" withString:@"📞"];
241 [textField setStringValue:fixedString];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400242 [timeField setStringValue:time];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500243
244 return result;
245}
246
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400247-(NSTableCellView*) configureViewforTransfer:(lrc::api::interaction::Info)interaction interactionID: (uint64_t) interactionID tableView:(NSTableView*)tableView
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500248{
249 IMTableCellView* result;
250
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400251 auto type = interaction.type;
252 auto status = interaction.status;
253
254 NSString* fileName = @"incoming file";
255
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500256 // First, view is created
257 if (type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER) {
258 switch (status) {
259 case lrc::api::interaction::Status::TRANSFER_CREATED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400260 case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST: {
261 result = [tableView makeViewWithIdentifier:@"LeftIncomingFileView" owner: conversationView];
262 [result.acceptButton setAction:@selector(acceptIncomingFile:)];
263 [result.acceptButton setTarget:self];
264 [result.declineButton setAction:@selector(declineIncomingFile:)];
265 [result.declineButton setTarget:self];
266 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500267 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400268 case lrc::api::interaction::Status::TRANSFER_ONGOING: {
269 result = [tableView makeViewWithIdentifier:@"LeftOngoingFileView" owner:conversationView];
270 [result.progressIndicator startAnimation:conversationView];
271 [result.declineButton setAction:@selector(declineIncomingFile:)];
272 [result.declineButton setTarget:self];
273 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500274 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400275 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
276 [result.transferedFileName setAction:@selector(imagePreview:)];
277 [result.transferedFileName setTarget:self];
278 [result.transferedFileName.cell setHighlightsBy:NSContentsCellMask];
279 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500280 case lrc::api::interaction::Status::TRANSFER_CANCELED:
281 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400282 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
283 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500284 }
285 } else if (type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400286 NSString* fileName = @"sent file";
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500287 switch (status) {
288 case lrc::api::interaction::Status::TRANSFER_CREATED:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500289 case lrc::api::interaction::Status::TRANSFER_ONGOING:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400290 case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500291 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400292 result = [tableView makeViewWithIdentifier:@"RightOngoingFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500293 [result.progressIndicator startAnimation:nil];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400294 [result.declineButton setAction:@selector(declineIncomingFile:)];
295 [result.declineButton setTarget:self];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500296 break;
297 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400298 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
299 [result.transferedFileName setAction:@selector(imagePreview:)];
300 [result.transferedFileName setTarget:self];
301 [result.transferedFileName.cell setHighlightsBy:NSContentsCellMask];
302 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500303 case lrc::api::interaction::Status::TRANSFER_CANCELED:
304 case lrc::api::interaction::Status::TRANSFER_ERROR:
Olivier Soldanoe521a182018-02-26 16:55:19 -0500305 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400306 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500307 }
308 }
309
310 // Then status label is updated if needed
311 switch (status) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400312 [result.statusLabel setTextColor:[NSColor textColor]];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500313 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400314 [result.statusLabel setTextColor:[NSColor greenSuccessColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500315 [result.statusLabel setStringValue:NSLocalizedString(@"Success", @"File transfer successful label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500316 break;
317 case lrc::api::interaction::Status::TRANSFER_CANCELED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400318 [result.statusLabel setTextColor:[NSColor orangeColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500319 [result.statusLabel setStringValue:NSLocalizedString(@"Canceled", @"File transfer canceled label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500320 break;
321 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400322 [result.statusLabel setTextColor:[NSColor errorTransferColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500323 [result.statusLabel setStringValue:NSLocalizedString(@"Failed", @"File transfer failed label")];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500324 break;
325 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400326 [result.statusLabel setTextColor:[NSColor textColor]];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500327 [result.statusLabel setStringValue:NSLocalizedString(@"Unjoinable", @"File transfer peer unjoinable label")];
328 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500329 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400330 result.transferedImage.image = nil;
331 [result.msgBackground setHidden:NO];
332 [result invalidateImageConstraints];
333 NSString* name = @(interaction.body.c_str());
334 if (name.length > 0) {
Kateryna Kostiuk67735232018-05-10 15:05:32 -0400335 fileName = [name lastPathComponent];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400336 }
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400337 NSFont *nameFont = [NSFont userFontOfSize:14.0];
338 NSColor *nameColor = [NSColor textColor];
339 NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
340 paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
341 paragraphStyle.alignment = NSTextAlignmentLeft;
342 NSDictionary *nameAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
343 nameColor,NSForegroundColorAttributeName,
344 paragraphStyle,NSParagraphStyleAttributeName, nil];
345 NSAttributedString* nameAttributedString = [[NSAttributedString alloc] initWithString:fileName attributes:nameAttr];
346 result.transferedFileName.attributedTitle = nameAttributedString;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400347 if (status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400348 NSColor *higlightColor = [NSColor grayColor];
349 NSDictionary *alternativeNametAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
350 higlightColor,NSForegroundColorAttributeName,
351 paragraphStyle,NSParagraphStyleAttributeName, nil];
352 NSAttributedString* alternativeString = [[NSAttributedString alloc] initWithString:fileName attributes:alternativeNametAttr];
353 result.transferedFileName.attributedAlternateTitle = alternativeString;
354 NSImage* image = [self getImageForFilePath:name size:MAX_TRANSFERED_IMAGE_SIZE];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400355 if (([name rangeOfString:@"/"].location == NSNotFound)) {
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400356 image = [self getImageForFilePath:[self getDataTransferPath:interactionID] size:MAX_TRANSFERED_IMAGE_SIZE];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400357 }
358 if(image != nil) {
359 result.transferedImage.image = [image roundCorners:14];
360 [result updateImageConstraint:image.size.width andHeight:image.size.height];
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400361 [result.transferedImage setAction:@selector(imagePreview:)];
362 [result.transferedImage setTarget:self];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400363 }
364 }
365 [result setupForInteraction:interactionID];
366 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
367 NSString* timeString = [self timeForMessage: msgTime];
368 result.timeLabel.stringValue = timeString;
369 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
370 if (!isOutgoing) {
371 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
372 auto* conv = [self getCurrentConversation];
373 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
374 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500375 return result;
376}
377
Anthony Léonard2382b562017-12-13 15:51:28 -0500378#pragma mark - NSTableViewDelegate methods
379- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400380{
381 return YES;
382}
383
Anthony Léonard2382b562017-12-13 15:51:28 -0500384- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400385{
Anthony Léonard2382b562017-12-13 15:51:28 -0500386 return NO;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400387}
388
Anthony Léonard2382b562017-12-13 15:51:28 -0500389- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400390{
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400391
Anthony Léonard2382b562017-12-13 15:51:28 -0500392 auto* conv = [self getCurrentConversation];
393
394 if (conv == nil)
395 return nil;
396
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500397 auto it = conv->interactions.begin();
Anthony Léonard2382b562017-12-13 15:51:28 -0500398
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500399 std::advance(it, row);
Anthony Léonard2382b562017-12-13 15:51:28 -0500400
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400401 IMTableCellView* result;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400402 auto interaction = it->second;
Anthony Léonard2382b562017-12-13 15:51:28 -0500403 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
404
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500405 switch (interaction.type) {
406 case lrc::api::interaction::Type::TEXT:
407 if (isOutgoing) {
408 result = [tableView makeViewWithIdentifier:@"RightMessageView" owner:self];
409 } else {
410 result = [tableView makeViewWithIdentifier:@"LeftMessageView" owner:self];
411 }
412 break;
413 case lrc::api::interaction::Type::INCOMING_DATA_TRANSFER:
414 case lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400415 return [self configureViewforTransfer:interaction interactionID: it->first tableView:tableView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500416 break;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500417 case lrc::api::interaction::Type::CONTACT:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400418 case lrc::api::interaction::Type::CALL: {
419 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
420 NSString* timeString = [self timeForMessage: msgTime];
421 return [self makeGenericInteractionViewForTableView:tableView withText:@(interaction.body.c_str()) andTime:timeString];
422 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500423 default: // If interaction is not of a known type
424 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400425 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400426 MessageSequencing sequence = [self computeSequencingFor:row];
427 BubbleType type = SINGLE;
428 if (sequence == FIRST_WITHOUT_TIME || sequence == FIRST_WITH_TIME) {
429 type = FIRST;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400430 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400431 if (sequence == MIDDLE_IN_SEQUENCE) {
432 type = MIDDLE;
433 }
434 if (sequence == LAST_IN_SEQUENCE) {
435 type = LAST;
436 }
437 result.msgBackground.type = type;
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400438 bool sendingFail = false;
439 [result.messageStatus setHidden:YES];
440 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
441 if (interaction.status == lrc::api::interaction::Status::SENDING) {
442 [result.messageStatus setHidden:NO];
443 [result.sendingMessageIndicator startAnimation:nil];
444 [result.messageFailed setHidden:YES];
445 } else if (interaction.status == lrc::api::interaction::Status::FAILED) {
446 [result.messageStatus setHidden:NO];
447 [result.sendingMessageIndicator setHidden:YES];
448 [result.messageFailed setHidden:NO];
449 sendingFail = true;
450 }
451 }
452 [result setupForInteraction:it->first isFailed: sendingFail];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400453 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400454 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400455 [result.msgBackground setNeedsDisplay:YES];
456 [result setNeedsDisplay:YES];
457 [result.timeBox setNeedsDisplay:YES];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400458
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400459 NSString *text = @(interaction.body.c_str());
460 text = [text removeEmptyLinesAtBorders];
461
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400462 NSMutableAttributedString* msgAttString =
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400463 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:text]
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400464 attributes:[self messageAttributes]];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400465
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400466 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400467
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400468 [result updateMessageConstraint:messageSize.width andHeight:messageSize.height timeIsVisible:shouldDisplayTime isTopPadding: shouldApplyPadding];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400469 [[result.msgView textStorage] appendAttributedString:msgAttString];
470 [result.msgView checkTextInDocument:nil];
Anthony Léonard2382b562017-12-13 15:51:28 -0500471
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400472 NSDataDetector *linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
473 NSArray *matches = [linkDetector matchesInString:result.msgView.string options:0 range:NSMakeRange(0, result.msgView.string.length)];
474
475 [result.msgView.textStorage beginEditing];
476
477 for (NSTextCheckingResult *match in matches) {
478 if (!match.URL) continue;
479
480 NSDictionary *linkAttributes = @{
481 NSLinkAttributeName: match.URL,
482 };
483 [result.msgView.textStorage addAttributes:linkAttributes range:match.range];
484 }
485
486 [result.msgView.textStorage endEditing];
487
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400488 if (shouldDisplayTime) {
489 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
490 NSString* timeString = [self timeForMessage: msgTime];
491 result.timeLabel.stringValue = timeString;
Anthony Léonard64e19672018-01-18 16:40:34 -0500492 }
493
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400494 bool shouldDisplayAvatar = (sequence != MIDDLE_IN_SEQUENCE && sequence != FIRST_WITHOUT_TIME
495 && sequence != FIRST_WITH_TIME) ? YES : NO;
496 [result.photoView setHidden:!shouldDisplayAvatar];
497 if (!isOutgoing && shouldDisplayAvatar) {
498 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
499 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
500 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400501 return result;
502}
503
Anthony Léonard2382b562017-12-13 15:51:28 -0500504- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400505{
Anthony Léonard2382b562017-12-13 15:51:28 -0500506 double someWidth = tableView.frame.size.width * 0.7;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400507
Anthony Léonard2382b562017-12-13 15:51:28 -0500508 auto* conv = [self getCurrentConversation];
509
510 if (conv == nil)
511 return 0;
512
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500513 auto it = conv->interactions.begin();
Anthony Léonard2382b562017-12-13 15:51:28 -0500514
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500515 std::advance(it, row);
Anthony Léonard2382b562017-12-13 15:51:28 -0500516
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400517 auto interaction = it->second;
Anthony Léonard2382b562017-12-13 15:51:28 -0500518
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400519 MessageSequencing sequence = [self computeSequencingFor:row];
520
521 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
522
523
524 if(interaction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER || interaction.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) {
525
526 if( interaction.status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
527 NSString* name = @(interaction.body.c_str());
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400528 NSImage* image = [self getImageForFilePath:name size:MAX_TRANSFERED_IMAGE_SIZE];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400529 if (([name rangeOfString:@"/"].location == NSNotFound)) {
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400530 image = [self getImageForFilePath:[self getDataTransferPath:it->first] size:MAX_TRANSFERED_IMAGE_SIZE];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400531 }
532 if (image != nil) {
533 return image.size.height + TIME_BOX_HEIGHT;
534 }
535 }
536 return BUBBLE_HEIGHT_FOR_TRANSFERED_FILE + TIME_BOX_HEIGHT;
537 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500538
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500539 if(interaction.type == lrc::api::interaction::Type::CONTACT || interaction.type == lrc::api::interaction::Type::CALL)
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400540 return GENERIC_CELL_HEIGHT;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500541
Anthony Léonard2382b562017-12-13 15:51:28 -0500542 // TODO Implement interactions other than messages
543 if(interaction.type != lrc::api::interaction::Type::TEXT) {
544 return 0;
545 }
546
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400547 NSString *text = @(interaction.body.c_str());
548 text = [text removeEmptyLinesAtBorders];
549
550 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400551 CGFloat singleLignMessageHeight = 15;
552
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400553 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
554
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400555 if (shouldDisplayTime) {
556 return MAX(messageSize.height + TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2,
557 TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2 + singleLignMessageHeight);
558 }
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400559 if(shouldApplyPadding) {
560 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2 + 15,
561 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2 + 15);
562 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400563 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2,
564 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2);
565}
566
567#pragma mark - message view parameters
568
569-(NSString *) getDataTransferPath:(uint64_t)interactionId {
570 lrc::api::datatransfer::Info info = {};
571 convModel_->getTransferInfo(interactionId, info);
572 double convertData = static_cast<double>(info.totalSize);
573 return @(info.path.c_str());
574}
575
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400576-(NSImage*) getImageForFilePath: (NSString *) path size:(CGFloat)size {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400577 if (path.length <= 0) {return nil;}
578 if (![[NSFileManager defaultManager] fileExistsAtPath: path]) {return nil;}
579 NSImage* transferedImage = [[NSImage alloc] initWithContentsOfFile: path];
580 if(transferedImage != nil) {
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400581 return [transferedImage imageResizeInsideMax: size];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400582 }
583 return nil;
584}
585
586-(CGSize) sizeFor:(NSString *) message maxWidth:(CGFloat) width {
587 CGFloat horizaontalMargin = 6;
Anthony Léonard2382b562017-12-13 15:51:28 -0500588 NSMutableAttributedString* msgAttString =
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400589 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@", message]
Anthony Léonard2382b562017-12-13 15:51:28 -0500590 attributes:[self messageAttributes]];
591
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400592 CGFloat finalWidth = MIN(msgAttString.size.width + horizaontalMargin * 2, width);
593 NSRect frame = NSMakeRect(0, 0, finalWidth, msgAttString.size.height);
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400594 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400595 [[tv textStorage] setAttributedString:msgAttString];
596 [tv sizeToFit];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400597 return tv.frame.size;
598}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400599
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400600-(MessageSequencing) computeSequencingFor:(NSInteger) row {
601 auto* conv = [self getCurrentConversation];
602 if (conv == nil)
603 return SINGLE_WITHOUT_TIME;
604 auto it = conv->interactions.begin();
605 std::advance(it, row);
606 auto interaction = it->second;
607 if (interaction.type != lrc::api::interaction::Type::TEXT) {
608 return SINGLE_WITH_TIME;
609 }
610 if (row == 0) {
611 if (it == conv->interactions.end()) {
612 return SINGLE_WITH_TIME;
613 }
614 auto nextIt = it;
615 nextIt++;
616 auto nextInteraction = nextIt->second;
617 if ([self sequenceChangedFrom:interaction to: nextInteraction]) {
618 return SINGLE_WITH_TIME;
619 }
620 return FIRST_WITH_TIME;
621 }
622
623 if (row == conversationView.numberOfRows - 1) {
624 if(it == conv->interactions.begin()) {
625 return SINGLE_WITH_TIME;
626 }
627 auto previousIt = it;
628 previousIt--;
629 auto previousInteraction = previousIt->second;
630 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
631 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
632 if (!timeChanged && !authorChanged) {
633 return LAST_IN_SEQUENCE;
634 }
635 if (!timeChanged && authorChanged) {
636 return SINGLE_WITHOUT_TIME;
637 }
638 return SINGLE_WITH_TIME;
639 }
640 if(it == conv->interactions.begin() || it == conv->interactions.end()) {
641 return SINGLE_WITH_TIME;
642 }
643 auto previousIt = it;
644 previousIt--;
645 auto previousInteraction = previousIt->second;
646 auto nextIt = it;
647 nextIt++;
648 auto nextInteraction = nextIt->second;
649
650 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
651 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
652 bool sequenceWillChange = [self sequenceChangedFrom:interaction to: nextInteraction];
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400653 if (previousInteraction.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER ||
654 previousInteraction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER) {
655 if(!sequenceWillChange) {
656 return FIRST_WITH_TIME;
657 }
658 return SINGLE_WITH_TIME;
659 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400660 if (!sequenceWillChange) {
661 if (!timeChanged && !authorChanged) {
662 return MIDDLE_IN_SEQUENCE;
663 }
664 if (timeChanged) {
665 return FIRST_WITH_TIME;
666 }
667 return FIRST_WITHOUT_TIME;
668 } if (!timeChanged && !authorChanged) {
669 return LAST_IN_SEQUENCE;
670 } if (timeChanged) {
671 return SINGLE_WITH_TIME;
672 }
673 return SINGLE_WITHOUT_TIME;
674}
675
676-(bool) sequenceChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
677 return ([self sequenceTimeChangedFrom:firstInteraction to:secondInteraction] || [self sequenceAuthorChangedFrom:firstInteraction to:secondInteraction]);
678}
679
680-(bool) sequenceTimeChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
681 bool timeChanged = NO;
682 NSDate* firstMessageTime = [NSDate dateWithTimeIntervalSince1970:firstInteraction.timestamp];
683 NSDate* secondMessageTime = [NSDate dateWithTimeIntervalSince1970:secondInteraction.timestamp];
684 bool hourComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitHour];
685 bool minutComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitMinute];
686 if(hourComp != NSOrderedSame || minutComp != NSOrderedSame) {
687 timeChanged = YES;
688 }
689 return timeChanged;
690}
691
692-(bool) sequenceAuthorChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
693 bool authorChanged = YES;
694 bool isOutgoing = lrc::api::interaction::isOutgoing(firstInteraction);
695 if ((secondInteraction.type == lrc::api::interaction::Type::TEXT) && (isOutgoing == lrc::api::interaction::isOutgoing(secondInteraction))) {
696 authorChanged = NO;
697 }
698 return authorChanged;
699}
700
701-(NSString *)timeForMessage:(NSDate*) msgTime {
702 NSDate *today = [NSDate date];
703 NSDateFormatter *dateFormatter=[[NSDateFormatter alloc] init];
Kateryna Kostiukaf6d5e22018-06-12 15:00:00 -0400704 [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:[[NSLocale currentLocale] localeIdentifier]]];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400705 if ([[NSCalendar currentCalendar] compareDate:today
706 toDate:msgTime
707 toUnitGranularity:NSCalendarUnitYear]!= NSOrderedSame) {
708 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterMediumStyle];
709 }
710
711 if ([[NSCalendar currentCalendar] compareDate:today
712 toDate:msgTime
713 toUnitGranularity:NSCalendarUnitDay]!= NSOrderedSame ||
714 [[NSCalendar currentCalendar] compareDate:today
715 toDate:msgTime
716 toUnitGranularity:NSCalendarUnitMonth]!= NSOrderedSame) {
717 [dateFormatter setDateFormat:@"MMM dd, HH:mm"];
718 return [dateFormatter stringFromDate:msgTime];
719 }
720
721 [dateFormatter setDateFormat:@"HH:mm"];
722 return [dateFormatter stringFromDate:msgTime];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400723}
724
Anthony Léonard2382b562017-12-13 15:51:28 -0500725#pragma mark - NSTableViewDataSource
726
727- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
728{
729 auto* conv = [self getCurrentConversation];
730
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500731 if (conv)
732 return conv->interactions.size();
733 else
734 return 0;
Anthony Léonard2382b562017-12-13 15:51:28 -0500735}
736
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400737#pragma mark - Text formatting
738
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400739- (NSMutableDictionary*) messageAttributes
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400740{
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400741 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400742 attrs[NSForegroundColorAttributeName] = [NSColor labelColor];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400743 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400744 return attrs;
745}
746
747- (NSParagraphStyle*) paragraphStyle
748{
749 /*
750 The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
751 NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
752 to copy, we use the default one.
753
754 The default values supplied by the default NSParagraphStyle are:
755 Alignment NSNaturalTextAlignment
756 Tab stops 12 left-aligned tabs, spaced by 28.0 points
757 Line break mode NSLineBreakByWordWrapping
758 All others 0.0
759 */
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400760 NSMutableParagraphStyle* aMutableParagraphStyle =
761 [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
762 [aMutableParagraphStyle setHeadIndent:1.0];
763 [aMutableParagraphStyle setFirstLineHeadIndent:1.0];
764 return aMutableParagraphStyle;
765}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400766
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500767#pragma mark - Actions
768
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400769- (void)acceptIncomingFile:(id)sender {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500770 auto interId = [(IMTableCellView*)[[sender superview] superview] interaction];
771 auto& inter = [self getCurrentConversation]->interactions.find(interId)->second;
772 if (convModel_ && !convUid_.empty()) {
773 NSSavePanel* filePicker = [NSSavePanel savePanel];
774 [filePicker setNameFieldStringValue:@(inter.body.c_str())];
775
776 if ([filePicker runModal] == NSFileHandlingPanelOKButton) {
777 const char* fullPath = [[filePicker URL] fileSystemRepresentation];
778 convModel_->acceptTransfer(convUid_, interId, fullPath);
779 }
780 }
781}
782
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400783- (void)declineIncomingFile:(id)sender {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500784 auto inter = [(IMTableCellView*)[[sender superview] superview] interaction];
785 if (convModel_ && !convUid_.empty()) {
786 convModel_->cancelTransfer(convUid_, inter);
787 }
788}
789
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400790- (void)imagePreview:(id)sender {
791 uint64_t interId;
792 if ([[sender superview] isKindOfClass:[IMTableCellView class]]) {
793 interId = [(IMTableCellView*)[sender superview] interaction];
794 } else if ([[[sender superview] superview] isKindOfClass:[IMTableCellView class]]) {
795 interId = [(IMTableCellView*)[[sender superview] superview] interaction];
796 } else {
797 return;
798 }
799 auto it = [self getCurrentConversation]->interactions.find(interId);
800 if (it == [self getCurrentConversation]->interactions.end()) {
801 return;
802 }
803 auto& interaction = it->second;
804 NSString* name = @(interaction.body.c_str());
805 if (([name rangeOfString:@"/"].location == NSNotFound)) {
806 name = [self getDataTransferPath:interId];
807 }
808 previewImage = name;
809 if ([QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]) {
810 [[QLPreviewPanel sharedPreviewPanel] orderOut:nil];
811 } else {
812 [[QLPreviewPanel sharedPreviewPanel] updateController];
813 [QLPreviewPanel sharedPreviewPanel].dataSource = self;
814 [[QLPreviewPanel sharedPreviewPanel] setAnimationBehavior:NSWindowAnimationBehaviorDocumentWindow];
815 [[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:nil];
816 }
817}
818
819- (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel *)panel {
820 return 1;
821}
822
823- (id <QLPreviewItem>)previewPanel:(QLPreviewPanel *)panel previewItemAtIndex:(NSInteger)index {
824 return [NSURL fileURLWithPath:previewImage];
825}
826
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400827@end