blob: 90360b57f52b5a35d2072f4dc883d00b5d4766f2 [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
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"
Anthony Léonard2382b562017-12-13 15:51:28 -050033#import "delegates/ImageManipulationDelegate.h"
Anthony Léonard6f819752018-01-05 09:53:40 -050034#import "utils.h"
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040035#import "views/NSColor+RingTheme.h"
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040036#import "views/IconButton.h"
37#import <QuickLook/QuickLook.h>
38#import <Quartz/Quartz.h>
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040039
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040040
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040041@interface MessagesVC () <NSTableViewDelegate, NSTableViewDataSource, QLPreviewPanelDataSource> {
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040042
Anthony Léonard2382b562017-12-13 15:51:28 -050043 __unsafe_unretained IBOutlet NSTableView* conversationView;
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040044 __unsafe_unretained IBOutlet NSView* containerView;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050045 __unsafe_unretained IBOutlet NSTextField* messageField;
46 __unsafe_unretained IBOutlet IconButton *sendFileButton;
47 __unsafe_unretained IBOutlet NSLayoutConstraint* sendPanelHeight;
48 __unsafe_unretained IBOutlet NSLayoutConstraint* messagesBottomMargin;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040049
Anthony Léonard2382b562017-12-13 15:51:28 -050050 std::string convUid_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050051 lrc::api::ConversationModel* convModel_;
Anthony Léonard2382b562017-12-13 15:51:28 -050052 const lrc::api::conversation::Info* cachedConv_;
53
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050054 QMetaObject::Connection newInteractionSignal_;
Anthony Léonard2382b562017-12-13 15:51:28 -050055
56 // Both are needed to invalidate cached conversation as pointer
57 // may not be referencing the same conversation anymore
58 QMetaObject::Connection modelSortedSignal_;
59 QMetaObject::Connection filterChangedSignal_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050060 QMetaObject::Connection interactionStatusUpdatedSignal_;
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040061 NSString* previewImage;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050062 NSMutableDictionary *pendingMessagesToSend;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040063}
64
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040065
66@end
67
Anthony Léonardf2bb17d2018-02-15 17:18:09 -050068// Tags for view
69NSInteger const GENERIC_INT_TEXT_TAG = 100;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040070NSInteger const GENERIC_INT_TIME_TAG = 200;
71
72// views size
73CGFloat const GENERIC_CELL_HEIGHT = 60;
74CGFloat const TIME_BOX_HEIGHT = 34;
75CGFloat const MESSAGE_TEXT_PADDING = 10;
76CGFloat const MAX_TRANSFERED_IMAGE_SIZE = 250;
77CGFloat const BUBBLE_HEIGHT_FOR_TRANSFERED_FILE = 87;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050078NSInteger const MEESAGE_MARGIN = 21;
79NSInteger const SEND_PANEL_DEFAULT_HEIGHT = 60;
80NSInteger const SEND_PANEL_MAX_HEIGHT = 120;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -050081
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040082@implementation MessagesVC
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040083
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040084
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040085//MessageBuble type
86typedef NS_ENUM(NSInteger, MessageSequencing) {
87 SINGLE_WITH_TIME = 0,
88 SINGLE_WITHOUT_TIME = 1,
89 FIRST_WITH_TIME = 2,
90 FIRST_WITHOUT_TIME = 3,
91 MIDDLE_IN_SEQUENCE = 5,
92 LAST_IN_SEQUENCE = 6,
93};
94
95- (void)awakeFromNib
96{
97 NSNib *cellNib = [[NSNib alloc] initWithNibNamed:@"MessageCells" bundle:nil];
98 [conversationView registerNib:cellNib forIdentifier:@"LeftIncomingFileView"];
99 [conversationView registerNib:cellNib forIdentifier:@"LeftOngoingFileView"];
100 [conversationView registerNib:cellNib forIdentifier:@"LeftFinishedFileView"];
101 [conversationView registerNib:cellNib forIdentifier:@"RightOngoingFileView"];
102 [conversationView registerNib:cellNib forIdentifier:@"RightFinishedFileView"];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500103 [[conversationView.enclosingScrollView contentView] setCopiesOnScroll:NO];
104 [messageField setFocusRingType:NSFocusRingTypeNone];
105 [conversationView setWantsLayer:YES];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400106}
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500107
108- (instancetype)initWithCoder:(NSCoder *)coder
109{
110 self = [super initWithCoder:coder];
111 if (self) {
112 pendingMessagesToSend = [[NSMutableDictionary alloc] init];
113 }
114 return self;
115}
116
117- (void)setMessage:(NSString *)newValue {
118 _message = [newValue removeEmptyLinesAtBorders];
119}
120
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400121-(void) clearData {
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500122 if (!convUid_.empty()) {
123 pendingMessagesToSend[@(convUid_.c_str())] = messageField.stringValue;
124 }
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400125 cachedConv_ = nil;
126 convUid_ = "";
127 convModel_ = nil;
128
129 QObject::disconnect(modelSortedSignal_);
130 QObject::disconnect(filterChangedSignal_);
131 QObject::disconnect(interactionStatusUpdatedSignal_);
132 QObject::disconnect(newInteractionSignal_);
133}
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400134
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500135-(void) scrollToBottom {
136 CGRect visibleRect = [conversationView enclosingScrollView].contentView.visibleRect;
137 NSRange range = [conversationView rowsInRect:visibleRect];
138 NSIndexSet* visibleIndexes = [NSIndexSet indexSetWithIndexesInRange:range];
139 NSUInteger lastvisibleRow = [visibleIndexes lastIndex];
140 if (([conversationView numberOfRows] > 0) &&
141 lastvisibleRow == ([conversationView numberOfRows] -1)) {
142 [conversationView scrollToEndOfDocument:nil];
143 }
144}
145
Anthony Léonard2382b562017-12-13 15:51:28 -0500146-(const lrc::api::conversation::Info*) getCurrentConversation
147{
148 if (convModel_ == nil || convUid_.empty())
149 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400150
Anthony Léonard2382b562017-12-13 15:51:28 -0500151 if (cachedConv_ != nil)
152 return cachedConv_;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400153
Anthony Léonard6f819752018-01-05 09:53:40 -0500154 auto it = getConversationFromUid(convUid_, *convModel_);
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400155 if (it != convModel_->allFilteredConversations().end())
Anthony Léonard2382b562017-12-13 15:51:28 -0500156 cachedConv_ = &(*it);
157
158 return cachedConv_;
159}
160
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400161-(void) reloadConversationForMessage:(uint64_t) uid shouldUpdateHeight:(bool)update {
162 auto* conv = [self getCurrentConversation];
163
164 if (conv == nil)
165 return;
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400166 auto it = conv->interactions.find(uid);
167 if (it == conv->interactions.end()) {
168 return;
169 }
170 auto itIndex = distance(conv->interactions.begin(),it);
171 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:itIndex];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400172 //reload previous message to update bubbleview
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400173 if (itIndex > 0) {
174 auto previousIt = it;
175 previousIt--;
176 auto previousInteraction = previousIt->second;
177 if (previousInteraction.type == lrc::api::interaction::Type::TEXT) {
178 NSRange range = NSMakeRange(itIndex - 1, 2);
179 indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
180 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400181 }
182 if (update) {
183 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
184 }
185 [conversationView reloadDataForRowIndexes: indexSet
186 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400187 CGRect visibleRect = [conversationView enclosingScrollView].contentView.visibleRect;
188 NSRange range = [conversationView rowsInRect:visibleRect];
189 NSIndexSet* visibleIndexes = [NSIndexSet indexSetWithIndexesInRange:range];
190 NSUInteger lastvisibleRow = [visibleIndexes lastIndex];
191 if (([conversationView numberOfRows] > 0) &&
192 lastvisibleRow == ([conversationView numberOfRows] -1)) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400193 [conversationView scrollToEndOfDocument:nil];
194 }
195}
196
197-(void) reloadConversationForMessage:(uint64_t) uid shouldUpdateHeight:(bool)update updateConversation:(bool) updateConversation {
198 auto* conv = [self getCurrentConversation];
199
200 if (conv == nil)
201 return;
202 auto it = distance(conv->interactions.begin(),conv->interactions.find(uid));
203 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:it];
204 //reload previous message to update bubbleview
205 if (it > 0) {
206 NSRange range = NSMakeRange(it - 1, it);
207 indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
208 }
209 if (update) {
210 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
211 }
212 [conversationView reloadDataForRowIndexes: indexSet
213 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
214 if (update) {
215 [conversationView scrollToEndOfDocument:nil];
216 }
217}
218
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500219-(void)setConversationUid:(const std::string)convUid model:(lrc::api::ConversationModel *)model
Anthony Léonard2382b562017-12-13 15:51:28 -0500220{
221 if (convUid_ == convUid && convModel_ == model)
222 return;
223
224 cachedConv_ = nil;
225 convUid_ = convUid;
226 convModel_ = model;
227
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500228 // Signal triggered when messages are received or their status updated
229 QObject::disconnect(newInteractionSignal_);
230 QObject::disconnect(interactionStatusUpdatedSignal_);
231 newInteractionSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newInteraction,
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400232 [self](const std::string& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
233 if (uid != convUid_)
234 return;
235 cachedConv_ = nil;
236 [conversationView noteNumberOfRowsChanged];
237 [self reloadConversationForMessage:interactionId shouldUpdateHeight:YES];
238 });
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500239 interactionStatusUpdatedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::interactionStatusUpdated,
240 [self](const std::string& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
241 if (uid != convUid_)
242 return;
243 cachedConv_ = nil;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400244 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
245 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
246 convModel_->refreshFilter();
247 }
248 [self reloadConversationForMessage:interactionId shouldUpdateHeight:YES];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500249 });
Anthony Léonard2382b562017-12-13 15:51:28 -0500250
251 // Signals tracking changes in conversation list, we need them as cached conversation can be invalid
252 // after a reordering.
253 QObject::disconnect(modelSortedSignal_);
254 QObject::disconnect(filterChangedSignal_);
255 modelSortedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::modelSorted,
256 [self](){
257 cachedConv_ = nil;
258 });
259 filterChangedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::filterChanged,
260 [self](){
261 cachedConv_ = nil;
262 });
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500263 if (pendingMessagesToSend[@(convUid_.c_str())]) {
264 self.message = pendingMessagesToSend[@(convUid_.c_str())];
265 [self updateSendMessageHeight];
266 } else {
267 self.message = @"";
268 if(messagesBottomMargin.constant != SEND_PANEL_DEFAULT_HEIGHT) {
269 sendPanelHeight.constant = SEND_PANEL_DEFAULT_HEIGHT;
270 messagesBottomMargin.constant = SEND_PANEL_DEFAULT_HEIGHT;
271 [self scrollToBottom];
272 }
273 }
274 conversationView.alphaValue = 0.0;
Anthony Léonard2382b562017-12-13 15:51:28 -0500275 [conversationView reloadData];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400276 [conversationView scrollToEndOfDocument:nil];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500277 CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:@"opacity"];
278 fadeIn.fromValue = [NSNumber numberWithFloat:0.0];
279 fadeIn.toValue = [NSNumber numberWithFloat:1.0];
280 fadeIn.duration = 0.4f;
281
282 [conversationView.layer addAnimation:fadeIn forKey:fadeIn.keyPath];
283 conversationView.alphaValue = 1;
284 auto* conv = [self getCurrentConversation];
285
286 if (conv == nil)
287 return;
288 [sendFileButton setEnabled:(convModel_->owner.contactModel->getContact(conv->participants[0]).profileInfo.type != lrc::api::profile::Type::SIP)];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400289}
290
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400291#pragma mark - configure cells
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400292
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400293-(NSTableCellView*) makeGenericInteractionViewForTableView:(NSTableView*)tableView withText:(NSString*)text andTime:(NSString*) time
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500294{
295 NSTableCellView* result = [tableView makeViewWithIdentifier:@"GenericInteractionView" owner:self];
296 NSTextField* textField = [result viewWithTag:GENERIC_INT_TEXT_TAG];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400297 NSTextField* timeField = [result viewWithTag:GENERIC_INT_TIME_TAG];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500298
299 // TODO: Fix symbol in LRC
300 NSString* fixedString = [text stringByReplacingOccurrencesOfString:@"🕽" withString:@"📞"];
301 [textField setStringValue:fixedString];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400302 [timeField setStringValue:time];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500303
304 return result;
305}
306
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400307-(NSTableCellView*) configureViewforTransfer:(lrc::api::interaction::Info)interaction interactionID: (uint64_t) interactionID tableView:(NSTableView*)tableView
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500308{
309 IMTableCellView* result;
310
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400311 auto type = interaction.type;
312 auto status = interaction.status;
313
314 NSString* fileName = @"incoming file";
315
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500316 // First, view is created
317 if (type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER) {
318 switch (status) {
319 case lrc::api::interaction::Status::TRANSFER_CREATED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400320 case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST: {
321 result = [tableView makeViewWithIdentifier:@"LeftIncomingFileView" owner: conversationView];
322 [result.acceptButton setAction:@selector(acceptIncomingFile:)];
323 [result.acceptButton setTarget:self];
324 [result.declineButton setAction:@selector(declineIncomingFile:)];
325 [result.declineButton setTarget:self];
326 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500327 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400328 case lrc::api::interaction::Status::TRANSFER_ONGOING: {
329 result = [tableView makeViewWithIdentifier:@"LeftOngoingFileView" owner:conversationView];
330 [result.progressIndicator startAnimation:conversationView];
331 [result.declineButton setAction:@selector(declineIncomingFile:)];
332 [result.declineButton setTarget:self];
333 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500334 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400335 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
336 [result.transferedFileName setAction:@selector(imagePreview:)];
337 [result.transferedFileName setTarget:self];
338 [result.transferedFileName.cell setHighlightsBy:NSContentsCellMask];
339 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500340 case lrc::api::interaction::Status::TRANSFER_CANCELED:
341 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400342 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
343 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500344 }
345 } else if (type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400346 NSString* fileName = @"sent file";
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500347 switch (status) {
348 case lrc::api::interaction::Status::TRANSFER_CREATED:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500349 case lrc::api::interaction::Status::TRANSFER_ONGOING:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400350 case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500351 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400352 result = [tableView makeViewWithIdentifier:@"RightOngoingFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500353 [result.progressIndicator startAnimation:nil];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400354 [result.declineButton setAction:@selector(declineIncomingFile:)];
355 [result.declineButton setTarget:self];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500356 break;
357 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400358 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
359 [result.transferedFileName setAction:@selector(imagePreview:)];
360 [result.transferedFileName setTarget:self];
361 [result.transferedFileName.cell setHighlightsBy:NSContentsCellMask];
362 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500363 case lrc::api::interaction::Status::TRANSFER_CANCELED:
364 case lrc::api::interaction::Status::TRANSFER_ERROR:
Olivier Soldanoe521a182018-02-26 16:55:19 -0500365 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400366 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500367 }
368 }
369
370 // Then status label is updated if needed
371 switch (status) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400372 [result.statusLabel setTextColor:[NSColor textColor]];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500373 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400374 [result.statusLabel setTextColor:[NSColor greenSuccessColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500375 [result.statusLabel setStringValue:NSLocalizedString(@"Success", @"File transfer successful label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500376 break;
377 case lrc::api::interaction::Status::TRANSFER_CANCELED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400378 [result.statusLabel setTextColor:[NSColor orangeColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500379 [result.statusLabel setStringValue:NSLocalizedString(@"Canceled", @"File transfer canceled label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500380 break;
381 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400382 [result.statusLabel setTextColor:[NSColor errorTransferColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500383 [result.statusLabel setStringValue:NSLocalizedString(@"Failed", @"File transfer failed label")];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500384 break;
385 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400386 [result.statusLabel setTextColor:[NSColor textColor]];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500387 [result.statusLabel setStringValue:NSLocalizedString(@"Unjoinable", @"File transfer peer unjoinable label")];
388 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500389 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400390 result.transferedImage.image = nil;
Kateryna Kostiukeaf1bc82018-10-12 14:33:50 -0400391 [result.openImagebutton setHidden:YES];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400392 [result.msgBackground setHidden:NO];
393 [result invalidateImageConstraints];
394 NSString* name = @(interaction.body.c_str());
395 if (name.length > 0) {
Kateryna Kostiuk67735232018-05-10 15:05:32 -0400396 fileName = [name lastPathComponent];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400397 }
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400398 NSFont *nameFont = [NSFont userFontOfSize:14.0];
399 NSColor *nameColor = [NSColor textColor];
400 NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
401 paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
402 paragraphStyle.alignment = NSTextAlignmentLeft;
403 NSDictionary *nameAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
404 nameColor,NSForegroundColorAttributeName,
405 paragraphStyle,NSParagraphStyleAttributeName, nil];
406 NSAttributedString* nameAttributedString = [[NSAttributedString alloc] initWithString:fileName attributes:nameAttr];
407 result.transferedFileName.attributedTitle = nameAttributedString;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400408 if (status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400409 NSColor *higlightColor = [NSColor grayColor];
410 NSDictionary *alternativeNametAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
411 higlightColor,NSForegroundColorAttributeName,
412 paragraphStyle,NSParagraphStyleAttributeName, nil];
413 NSAttributedString* alternativeString = [[NSAttributedString alloc] initWithString:fileName attributes:alternativeNametAttr];
414 result.transferedFileName.attributedAlternateTitle = alternativeString;
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400415 NSImage* image = [self getImageForFilePath:name];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400416 if (([name rangeOfString:@"/"].location == NSNotFound)) {
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400417 image = [self getImageForFilePath:[self getDataTransferPath:interactionID]];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400418 }
419 if(image != nil) {
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400420 result.transferedImage.image = image;
421 [result updateImageConstraintWithMax: MAX_TRANSFERED_IMAGE_SIZE];
422 [result.openImagebutton setAction:@selector(imagePreview:)];
423 [result.openImagebutton setTarget:self];
Kateryna Kostiukeaf1bc82018-10-12 14:33:50 -0400424 [result.openImagebutton setHidden:NO];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400425 }
426 }
427 [result setupForInteraction:interactionID];
428 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
429 NSString* timeString = [self timeForMessage: msgTime];
430 result.timeLabel.stringValue = timeString;
431 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
432 if (!isOutgoing) {
433 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
434 auto* conv = [self getCurrentConversation];
435 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
436 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500437 return result;
438}
439
Anthony Léonard2382b562017-12-13 15:51:28 -0500440#pragma mark - NSTableViewDelegate methods
441- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400442{
443 return YES;
444}
445
Anthony Léonard2382b562017-12-13 15:51:28 -0500446- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400447{
Anthony Léonard2382b562017-12-13 15:51:28 -0500448 return NO;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400449}
450
Anthony Léonard2382b562017-12-13 15:51:28 -0500451- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400452{
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400453
Anthony Léonard2382b562017-12-13 15:51:28 -0500454 auto* conv = [self getCurrentConversation];
455
456 if (conv == nil)
457 return nil;
458
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500459 auto it = conv->interactions.begin();
Anthony Léonard2382b562017-12-13 15:51:28 -0500460
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500461 std::advance(it, row);
Anthony Léonard2382b562017-12-13 15:51:28 -0500462
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400463 IMTableCellView* result;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400464 auto interaction = it->second;
Anthony Léonard2382b562017-12-13 15:51:28 -0500465 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
466
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500467 switch (interaction.type) {
468 case lrc::api::interaction::Type::TEXT:
469 if (isOutgoing) {
470 result = [tableView makeViewWithIdentifier:@"RightMessageView" owner:self];
471 } else {
472 result = [tableView makeViewWithIdentifier:@"LeftMessageView" owner:self];
473 }
474 break;
475 case lrc::api::interaction::Type::INCOMING_DATA_TRANSFER:
476 case lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400477 return [self configureViewforTransfer:interaction interactionID: it->first tableView:tableView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500478 break;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500479 case lrc::api::interaction::Type::CONTACT:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400480 case lrc::api::interaction::Type::CALL: {
481 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
482 NSString* timeString = [self timeForMessage: msgTime];
483 return [self makeGenericInteractionViewForTableView:tableView withText:@(interaction.body.c_str()) andTime:timeString];
484 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500485 default: // If interaction is not of a known type
486 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400487 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400488 MessageSequencing sequence = [self computeSequencingFor:row];
489 BubbleType type = SINGLE;
490 if (sequence == FIRST_WITHOUT_TIME || sequence == FIRST_WITH_TIME) {
491 type = FIRST;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400492 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400493 if (sequence == MIDDLE_IN_SEQUENCE) {
494 type = MIDDLE;
495 }
496 if (sequence == LAST_IN_SEQUENCE) {
497 type = LAST;
498 }
499 result.msgBackground.type = type;
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400500 bool sendingFail = false;
501 [result.messageStatus setHidden:YES];
502 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
503 if (interaction.status == lrc::api::interaction::Status::SENDING) {
504 [result.messageStatus setHidden:NO];
505 [result.sendingMessageIndicator startAnimation:nil];
506 [result.messageFailed setHidden:YES];
507 } else if (interaction.status == lrc::api::interaction::Status::FAILED) {
508 [result.messageStatus setHidden:NO];
509 [result.sendingMessageIndicator setHidden:YES];
510 [result.messageFailed setHidden:NO];
511 sendingFail = true;
512 }
513 }
514 [result setupForInteraction:it->first isFailed: sendingFail];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400515 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400516 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400517 [result.msgBackground setNeedsDisplay:YES];
518 [result setNeedsDisplay:YES];
519 [result.timeBox setNeedsDisplay:YES];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400520
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400521 NSString *text = @(interaction.body.c_str());
522 text = [text removeEmptyLinesAtBorders];
523
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400524 NSMutableAttributedString* msgAttString =
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400525 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:text]
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400526 attributes:[self messageAttributes]];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400527
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400528 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400529
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400530 [result updateMessageConstraint:messageSize.width andHeight:messageSize.height timeIsVisible:shouldDisplayTime isTopPadding: shouldApplyPadding];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400531 [[result.msgView textStorage] appendAttributedString:msgAttString];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500532 // [result.msgView checkTextInDocument:nil];
Anthony Léonard2382b562017-12-13 15:51:28 -0500533
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400534 NSDataDetector *linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
535 NSArray *matches = [linkDetector matchesInString:result.msgView.string options:0 range:NSMakeRange(0, result.msgView.string.length)];
536
537 [result.msgView.textStorage beginEditing];
538
539 for (NSTextCheckingResult *match in matches) {
540 if (!match.URL) continue;
541
542 NSDictionary *linkAttributes = @{
543 NSLinkAttributeName: match.URL,
544 };
545 [result.msgView.textStorage addAttributes:linkAttributes range:match.range];
546 }
547
548 [result.msgView.textStorage endEditing];
549
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400550 if (shouldDisplayTime) {
551 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
552 NSString* timeString = [self timeForMessage: msgTime];
553 result.timeLabel.stringValue = timeString;
Anthony Léonard64e19672018-01-18 16:40:34 -0500554 }
555
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400556 bool shouldDisplayAvatar = (sequence != MIDDLE_IN_SEQUENCE && sequence != FIRST_WITHOUT_TIME
557 && sequence != FIRST_WITH_TIME) ? YES : NO;
558 [result.photoView setHidden:!shouldDisplayAvatar];
559 if (!isOutgoing && shouldDisplayAvatar) {
560 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
561 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
562 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400563 return result;
564}
565
Anthony Léonard2382b562017-12-13 15:51:28 -0500566- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400567{
Anthony Léonard2382b562017-12-13 15:51:28 -0500568 double someWidth = tableView.frame.size.width * 0.7;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400569
Anthony Léonard2382b562017-12-13 15:51:28 -0500570 auto* conv = [self getCurrentConversation];
571
572 if (conv == nil)
573 return 0;
574
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500575 auto it = conv->interactions.begin();
Anthony Léonard2382b562017-12-13 15:51:28 -0500576
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500577 std::advance(it, row);
Anthony Léonard2382b562017-12-13 15:51:28 -0500578
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400579 auto interaction = it->second;
Anthony Léonard2382b562017-12-13 15:51:28 -0500580
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400581 MessageSequencing sequence = [self computeSequencingFor:row];
582
583 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
584
585
586 if(interaction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER || interaction.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) {
587
588 if( interaction.status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
589 NSString* name = @(interaction.body.c_str());
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400590 NSImage* image = [self getImageForFilePath:name];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400591 if (([name rangeOfString:@"/"].location == NSNotFound)) {
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400592 image = [self getImageForFilePath:[self getDataTransferPath:it->first]];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400593 }
594 if (image != nil) {
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400595 CGFloat widthScaleFactor = MAX_TRANSFERED_IMAGE_SIZE / image.size.width;
596 CGFloat heightScaleFactor = MAX_TRANSFERED_IMAGE_SIZE / image.size.height;
597 CGFloat heigt = 0;
598 if((widthScaleFactor >= 1) && (heightScaleFactor >= 1)) {
599 heigt = image.size.height;
600 } else {
601 CGFloat scale = MIN(widthScaleFactor, heightScaleFactor);
602 heigt = image.size.height * scale;
603 }
604 return heigt + TIME_BOX_HEIGHT;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400605 }
606 }
607 return BUBBLE_HEIGHT_FOR_TRANSFERED_FILE + TIME_BOX_HEIGHT;
608 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500609
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500610 if(interaction.type == lrc::api::interaction::Type::CONTACT || interaction.type == lrc::api::interaction::Type::CALL)
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400611 return GENERIC_CELL_HEIGHT;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500612
Anthony Léonard2382b562017-12-13 15:51:28 -0500613 // TODO Implement interactions other than messages
614 if(interaction.type != lrc::api::interaction::Type::TEXT) {
615 return 0;
616 }
617
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400618 NSString *text = @(interaction.body.c_str());
619 text = [text removeEmptyLinesAtBorders];
620
621 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400622 CGFloat singleLignMessageHeight = 15;
623
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400624 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
625
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400626 if (shouldDisplayTime) {
627 return MAX(messageSize.height + TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2,
628 TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2 + singleLignMessageHeight);
629 }
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400630 if(shouldApplyPadding) {
631 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2 + 15,
632 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2 + 15);
633 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400634 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2,
635 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2);
636}
637
638#pragma mark - message view parameters
639
640-(NSString *) getDataTransferPath:(uint64_t)interactionId {
641 lrc::api::datatransfer::Info info = {};
642 convModel_->getTransferInfo(interactionId, info);
643 double convertData = static_cast<double>(info.totalSize);
644 return @(info.path.c_str());
645}
646
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400647-(NSImage*) getImageForFilePath: (NSString *) path {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400648 if (path.length <= 0) {return nil;}
649 if (![[NSFileManager defaultManager] fileExistsAtPath: path]) {return nil;}
650 NSImage* transferedImage = [[NSImage alloc] initWithContentsOfFile: path];
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400651 return transferedImage;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400652}
653
654-(CGSize) sizeFor:(NSString *) message maxWidth:(CGFloat) width {
655 CGFloat horizaontalMargin = 6;
Anthony Léonard2382b562017-12-13 15:51:28 -0500656 NSMutableAttributedString* msgAttString =
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400657 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@", message]
Anthony Léonard2382b562017-12-13 15:51:28 -0500658 attributes:[self messageAttributes]];
659
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400660 CGFloat finalWidth = MIN(msgAttString.size.width + horizaontalMargin * 2, width);
661 NSRect frame = NSMakeRect(0, 0, finalWidth, msgAttString.size.height);
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400662 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400663 [[tv textStorage] setAttributedString:msgAttString];
664 [tv sizeToFit];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400665 return tv.frame.size;
666}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400667
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400668-(MessageSequencing) computeSequencingFor:(NSInteger) row {
669 auto* conv = [self getCurrentConversation];
670 if (conv == nil)
671 return SINGLE_WITHOUT_TIME;
672 auto it = conv->interactions.begin();
673 std::advance(it, row);
674 auto interaction = it->second;
675 if (interaction.type != lrc::api::interaction::Type::TEXT) {
676 return SINGLE_WITH_TIME;
677 }
678 if (row == 0) {
679 if (it == conv->interactions.end()) {
680 return SINGLE_WITH_TIME;
681 }
682 auto nextIt = it;
683 nextIt++;
684 auto nextInteraction = nextIt->second;
685 if ([self sequenceChangedFrom:interaction to: nextInteraction]) {
686 return SINGLE_WITH_TIME;
687 }
688 return FIRST_WITH_TIME;
689 }
690
691 if (row == conversationView.numberOfRows - 1) {
692 if(it == conv->interactions.begin()) {
693 return SINGLE_WITH_TIME;
694 }
695 auto previousIt = it;
696 previousIt--;
697 auto previousInteraction = previousIt->second;
698 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
699 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
700 if (!timeChanged && !authorChanged) {
701 return LAST_IN_SEQUENCE;
702 }
703 if (!timeChanged && authorChanged) {
704 return SINGLE_WITHOUT_TIME;
705 }
706 return SINGLE_WITH_TIME;
707 }
708 if(it == conv->interactions.begin() || it == conv->interactions.end()) {
709 return SINGLE_WITH_TIME;
710 }
711 auto previousIt = it;
712 previousIt--;
713 auto previousInteraction = previousIt->second;
714 auto nextIt = it;
715 nextIt++;
716 auto nextInteraction = nextIt->second;
717
718 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
719 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
720 bool sequenceWillChange = [self sequenceChangedFrom:interaction to: nextInteraction];
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400721 if (previousInteraction.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER ||
722 previousInteraction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER) {
723 if(!sequenceWillChange) {
724 return FIRST_WITH_TIME;
725 }
726 return SINGLE_WITH_TIME;
727 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400728 if (!sequenceWillChange) {
729 if (!timeChanged && !authorChanged) {
730 return MIDDLE_IN_SEQUENCE;
731 }
732 if (timeChanged) {
733 return FIRST_WITH_TIME;
734 }
735 return FIRST_WITHOUT_TIME;
736 } if (!timeChanged && !authorChanged) {
737 return LAST_IN_SEQUENCE;
738 } if (timeChanged) {
739 return SINGLE_WITH_TIME;
740 }
741 return SINGLE_WITHOUT_TIME;
742}
743
744-(bool) sequenceChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
745 return ([self sequenceTimeChangedFrom:firstInteraction to:secondInteraction] || [self sequenceAuthorChangedFrom:firstInteraction to:secondInteraction]);
746}
747
748-(bool) sequenceTimeChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
749 bool timeChanged = NO;
750 NSDate* firstMessageTime = [NSDate dateWithTimeIntervalSince1970:firstInteraction.timestamp];
751 NSDate* secondMessageTime = [NSDate dateWithTimeIntervalSince1970:secondInteraction.timestamp];
752 bool hourComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitHour];
753 bool minutComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitMinute];
754 if(hourComp != NSOrderedSame || minutComp != NSOrderedSame) {
755 timeChanged = YES;
756 }
757 return timeChanged;
758}
759
760-(bool) sequenceAuthorChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
761 bool authorChanged = YES;
762 bool isOutgoing = lrc::api::interaction::isOutgoing(firstInteraction);
763 if ((secondInteraction.type == lrc::api::interaction::Type::TEXT) && (isOutgoing == lrc::api::interaction::isOutgoing(secondInteraction))) {
764 authorChanged = NO;
765 }
766 return authorChanged;
767}
768
769-(NSString *)timeForMessage:(NSDate*) msgTime {
770 NSDate *today = [NSDate date];
771 NSDateFormatter *dateFormatter=[[NSDateFormatter alloc] init];
Kateryna Kostiukaf6d5e22018-06-12 15:00:00 -0400772 [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:[[NSLocale currentLocale] localeIdentifier]]];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400773 if ([[NSCalendar currentCalendar] compareDate:today
774 toDate:msgTime
775 toUnitGranularity:NSCalendarUnitYear]!= NSOrderedSame) {
776 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterMediumStyle];
777 }
778
779 if ([[NSCalendar currentCalendar] compareDate:today
780 toDate:msgTime
781 toUnitGranularity:NSCalendarUnitDay]!= NSOrderedSame ||
782 [[NSCalendar currentCalendar] compareDate:today
783 toDate:msgTime
784 toUnitGranularity:NSCalendarUnitMonth]!= NSOrderedSame) {
785 [dateFormatter setDateFormat:@"MMM dd, HH:mm"];
786 return [dateFormatter stringFromDate:msgTime];
787 }
788
789 [dateFormatter setDateFormat:@"HH:mm"];
790 return [dateFormatter stringFromDate:msgTime];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400791}
792
Anthony Léonard2382b562017-12-13 15:51:28 -0500793#pragma mark - NSTableViewDataSource
794
795- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
796{
797 auto* conv = [self getCurrentConversation];
798
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500799 if (conv)
800 return conv->interactions.size();
801 else
802 return 0;
Anthony Léonard2382b562017-12-13 15:51:28 -0500803}
804
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400805#pragma mark - Text formatting
806
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400807- (NSMutableDictionary*) messageAttributes
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400808{
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400809 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400810 attrs[NSForegroundColorAttributeName] = [NSColor labelColor];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400811 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400812 return attrs;
813}
814
815- (NSParagraphStyle*) paragraphStyle
816{
817 /*
818 The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
819 NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
820 to copy, we use the default one.
821
822 The default values supplied by the default NSParagraphStyle are:
823 Alignment NSNaturalTextAlignment
824 Tab stops 12 left-aligned tabs, spaced by 28.0 points
825 Line break mode NSLineBreakByWordWrapping
826 All others 0.0
827 */
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400828 NSMutableParagraphStyle* aMutableParagraphStyle =
829 [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
830 [aMutableParagraphStyle setHeadIndent:1.0];
831 [aMutableParagraphStyle setFirstLineHeadIndent:1.0];
832 return aMutableParagraphStyle;
833}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400834
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500835#pragma mark - Actions
836
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400837- (void)acceptIncomingFile:(id)sender {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500838 auto interId = [(IMTableCellView*)[[sender superview] superview] interaction];
839 auto& inter = [self getCurrentConversation]->interactions.find(interId)->second;
840 if (convModel_ && !convUid_.empty()) {
841 NSSavePanel* filePicker = [NSSavePanel savePanel];
842 [filePicker setNameFieldStringValue:@(inter.body.c_str())];
843
844 if ([filePicker runModal] == NSFileHandlingPanelOKButton) {
845 const char* fullPath = [[filePicker URL] fileSystemRepresentation];
846 convModel_->acceptTransfer(convUid_, interId, fullPath);
847 }
848 }
849}
850
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400851- (void)declineIncomingFile:(id)sender {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500852 auto inter = [(IMTableCellView*)[[sender superview] superview] interaction];
853 if (convModel_ && !convUid_.empty()) {
854 convModel_->cancelTransfer(convUid_, inter);
855 }
856}
857
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400858- (void)imagePreview:(id)sender {
859 uint64_t interId;
860 if ([[sender superview] isKindOfClass:[IMTableCellView class]]) {
861 interId = [(IMTableCellView*)[sender superview] interaction];
862 } else if ([[[sender superview] superview] isKindOfClass:[IMTableCellView class]]) {
863 interId = [(IMTableCellView*)[[sender superview] superview] interaction];
864 } else {
865 return;
866 }
867 auto it = [self getCurrentConversation]->interactions.find(interId);
868 if (it == [self getCurrentConversation]->interactions.end()) {
869 return;
870 }
871 auto& interaction = it->second;
872 NSString* name = @(interaction.body.c_str());
873 if (([name rangeOfString:@"/"].location == NSNotFound)) {
874 name = [self getDataTransferPath:interId];
875 }
876 previewImage = name;
877 if ([QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]) {
878 [[QLPreviewPanel sharedPreviewPanel] orderOut:nil];
879 } else {
880 [[QLPreviewPanel sharedPreviewPanel] updateController];
881 [QLPreviewPanel sharedPreviewPanel].dataSource = self;
882 [[QLPreviewPanel sharedPreviewPanel] setAnimationBehavior:NSWindowAnimationBehaviorDocumentWindow];
883 [[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:nil];
884 }
885}
886
887- (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel *)panel {
888 return 1;
889}
890
891- (id <QLPreviewItem>)previewPanel:(QLPreviewPanel *)panel previewItemAtIndex:(NSInteger)index {
892 return [NSURL fileURLWithPath:previewImage];
893}
894
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500895- (void) updateSendMessageHeight {
896 NSAttributedString *msgAttString = messageField.attributedStringValue;
897 NSRect frame = NSMakeRect(0, 0, messageField.frame.size.width, msgAttString.size.height);
898 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
899 [[tv textStorage] setAttributedString:msgAttString];
900 [tv sizeToFit];
901 CGFloat height = tv.frame.size.height + MEESAGE_MARGIN * 2;
902 CGFloat newHeight = MIN(SEND_PANEL_MAX_HEIGHT, MAX(SEND_PANEL_DEFAULT_HEIGHT, height));
903 if(messagesBottomMargin.constant == newHeight) {
904 return;
905 }
906 messagesBottomMargin.constant = newHeight;
907 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.05 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
908 [self scrollToBottom];
909 sendPanelHeight.constant = newHeight;
910 });
911}
912
913- (IBAction)sendMessage:(id)sender {
914 NSString* text = self.message;
915 if (text && text.length > 0) {
916 auto* conv = [self getCurrentConversation];
917 convModel_->sendMessage(convUid_, std::string([text UTF8String]));
918 self.message = @"";
919 if(sendPanelHeight.constant != SEND_PANEL_DEFAULT_HEIGHT) {
920 sendPanelHeight.constant = SEND_PANEL_DEFAULT_HEIGHT;
921 messagesBottomMargin.constant = SEND_PANEL_DEFAULT_HEIGHT;
922 [self scrollToBottom];
923 }
924 }
925}
926
927- (IBAction)openEmojy:(id)sender {
928 [messageField.window makeFirstResponder: messageField];
929 [[messageField currentEditor] moveToEndOfLine:nil];
930 [NSApp orderFrontCharacterPalette: messageField];
931}
932
933- (IBAction)sendFile:(id)sender {
934 NSOpenPanel* filePicker = [NSOpenPanel openPanel];
935 [filePicker setCanChooseFiles:YES];
936 [filePicker setCanChooseDirectories:NO];
937 [filePicker setAllowsMultipleSelection:NO];
938
939 if ([filePicker runModal] == NSFileHandlingPanelOKButton) {
940 if ([[filePicker URLs] count] == 1) {
941 NSURL* url = [[filePicker URLs] objectAtIndex:0];
942 const char* fullPath = [url fileSystemRepresentation];
943 NSString* fileName = [url lastPathComponent];
944 if (convModel_) {
945 auto* conv = [self getCurrentConversation];
946 convModel_->sendFile(convUid_, std::string(fullPath), std::string([fileName UTF8String]));
947 }
948 }
949 }
950}
951
952
953#pragma mark - NSTextFieldDelegate
954
955- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector
956{
957 if (commandSelector == @selector(insertNewline:)) {
958 if(self.message.length > 0) {
959 [self sendMessage: nil];
960 } else if(messagesBottomMargin.constant != SEND_PANEL_DEFAULT_HEIGHT) {
961 sendPanelHeight.constant = SEND_PANEL_DEFAULT_HEIGHT;
962 messagesBottomMargin.constant = SEND_PANEL_DEFAULT_HEIGHT;
963 [self scrollToBottom];
964 }
965 return YES;
966 }
967 return NO;
968}
969
970- (void)controlTextDidChange:(NSNotification *)aNotification {
971 [self updateSendMessageHeight];
972}
973
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400974@end