blob: b11deb438475db0012e36ca3071855e1fb0c914b [file] [log] [blame]
Andreas Traczyk252a94a2018-04-20 16:36:20 -04001
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04002/*
Sébastien Blin029ffa82019-01-02 17:43:48 -05003 * Copyright (C) 2015-2019 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_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050053 QMetaObject::Connection newInteractionSignal_;
Anthony Léonard2382b562017-12-13 15:51:28 -050054
55 // Both are needed to invalidate cached conversation as pointer
56 // may not be referencing the same conversation anymore
57 QMetaObject::Connection modelSortedSignal_;
58 QMetaObject::Connection filterChangedSignal_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050059 QMetaObject::Connection interactionStatusUpdatedSignal_;
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040060 NSString* previewImage;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050061 NSMutableDictionary *pendingMessagesToSend;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040062}
63
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040064
65@end
66
Anthony Léonardf2bb17d2018-02-15 17:18:09 -050067// Tags for view
68NSInteger const GENERIC_INT_TEXT_TAG = 100;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040069NSInteger const GENERIC_INT_TIME_TAG = 200;
70
71// views size
72CGFloat const GENERIC_CELL_HEIGHT = 60;
73CGFloat const TIME_BOX_HEIGHT = 34;
74CGFloat const MESSAGE_TEXT_PADDING = 10;
75CGFloat const MAX_TRANSFERED_IMAGE_SIZE = 250;
76CGFloat const BUBBLE_HEIGHT_FOR_TRANSFERED_FILE = 87;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050077NSInteger const MEESAGE_MARGIN = 21;
78NSInteger const SEND_PANEL_DEFAULT_HEIGHT = 60;
79NSInteger const SEND_PANEL_MAX_HEIGHT = 120;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -050080
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040081@implementation MessagesVC
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040082
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040083
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040084//MessageBuble type
85typedef NS_ENUM(NSInteger, MessageSequencing) {
86 SINGLE_WITH_TIME = 0,
87 SINGLE_WITHOUT_TIME = 1,
88 FIRST_WITH_TIME = 2,
89 FIRST_WITHOUT_TIME = 3,
90 MIDDLE_IN_SEQUENCE = 5,
91 LAST_IN_SEQUENCE = 6,
92};
93
94- (void)awakeFromNib
95{
96 NSNib *cellNib = [[NSNib alloc] initWithNibNamed:@"MessageCells" bundle:nil];
97 [conversationView registerNib:cellNib forIdentifier:@"LeftIncomingFileView"];
98 [conversationView registerNib:cellNib forIdentifier:@"LeftOngoingFileView"];
99 [conversationView registerNib:cellNib forIdentifier:@"LeftFinishedFileView"];
100 [conversationView registerNib:cellNib forIdentifier:@"RightOngoingFileView"];
101 [conversationView registerNib:cellNib forIdentifier:@"RightFinishedFileView"];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500102 [[conversationView.enclosingScrollView contentView] setCopiesOnScroll:NO];
103 [messageField setFocusRingType:NSFocusRingTypeNone];
104 [conversationView setWantsLayer:YES];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400105}
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500106
107- (instancetype)initWithCoder:(NSCoder *)coder
108{
109 self = [super initWithCoder:coder];
110 if (self) {
111 pendingMessagesToSend = [[NSMutableDictionary alloc] init];
112 }
113 return self;
114}
115
116- (void)setMessage:(NSString *)newValue {
117 _message = [newValue removeEmptyLinesAtBorders];
118}
119
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400120-(void) clearData {
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500121 if (!convUid_.empty()) {
122 pendingMessagesToSend[@(convUid_.c_str())] = messageField.stringValue;
123 }
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400124 cachedConv_ = nil;
125 convUid_ = "";
126 convModel_ = nil;
127
128 QObject::disconnect(modelSortedSignal_);
129 QObject::disconnect(filterChangedSignal_);
130 QObject::disconnect(interactionStatusUpdatedSignal_);
131 QObject::disconnect(newInteractionSignal_);
132}
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400133
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500134-(void) scrollToBottom {
135 CGRect visibleRect = [conversationView enclosingScrollView].contentView.visibleRect;
136 NSRange range = [conversationView rowsInRect:visibleRect];
137 NSIndexSet* visibleIndexes = [NSIndexSet indexSetWithIndexesInRange:range];
138 NSUInteger lastvisibleRow = [visibleIndexes lastIndex];
139 if (([conversationView numberOfRows] > 0) &&
140 lastvisibleRow == ([conversationView numberOfRows] -1)) {
141 [conversationView scrollToEndOfDocument:nil];
142 }
143}
144
Anthony Léonard2382b562017-12-13 15:51:28 -0500145-(const lrc::api::conversation::Info*) getCurrentConversation
146{
147 if (convModel_ == nil || convUid_.empty())
148 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400149
Anthony Léonard2382b562017-12-13 15:51:28 -0500150 if (cachedConv_ != nil)
151 return cachedConv_;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400152
Anthony Léonard6f819752018-01-05 09:53:40 -0500153 auto it = getConversationFromUid(convUid_, *convModel_);
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400154 if (it != convModel_->allFilteredConversations().end())
Anthony Léonard2382b562017-12-13 15:51:28 -0500155 cachedConv_ = &(*it);
156
157 return cachedConv_;
158}
159
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400160-(void) reloadConversationForMessage:(uint64_t) uid shouldUpdateHeight:(bool)update {
161 auto* conv = [self getCurrentConversation];
162
163 if (conv == nil)
164 return;
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400165 auto it = conv->interactions.find(uid);
166 if (it == conv->interactions.end()) {
167 return;
168 }
169 auto itIndex = distance(conv->interactions.begin(),it);
170 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:itIndex];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400171 //reload previous message to update bubbleview
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400172 if (itIndex > 0) {
173 auto previousIt = it;
174 previousIt--;
175 auto previousInteraction = previousIt->second;
176 if (previousInteraction.type == lrc::api::interaction::Type::TEXT) {
177 NSRange range = NSMakeRange(itIndex - 1, 2);
178 indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
179 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400180 }
181 if (update) {
182 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
183 }
184 [conversationView reloadDataForRowIndexes: indexSet
185 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400186 CGRect visibleRect = [conversationView enclosingScrollView].contentView.visibleRect;
187 NSRange range = [conversationView rowsInRect:visibleRect];
188 NSIndexSet* visibleIndexes = [NSIndexSet indexSetWithIndexesInRange:range];
189 NSUInteger lastvisibleRow = [visibleIndexes lastIndex];
190 if (([conversationView numberOfRows] > 0) &&
191 lastvisibleRow == ([conversationView numberOfRows] -1)) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400192 [conversationView scrollToEndOfDocument:nil];
193 }
194}
195
196-(void) reloadConversationForMessage:(uint64_t) uid shouldUpdateHeight:(bool)update updateConversation:(bool) updateConversation {
197 auto* conv = [self getCurrentConversation];
198
199 if (conv == nil)
200 return;
201 auto it = distance(conv->interactions.begin(),conv->interactions.find(uid));
202 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:it];
203 //reload previous message to update bubbleview
204 if (it > 0) {
205 NSRange range = NSMakeRange(it - 1, it);
206 indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
207 }
208 if (update) {
209 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
210 }
211 [conversationView reloadDataForRowIndexes: indexSet
212 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
213 if (update) {
214 [conversationView scrollToEndOfDocument:nil];
215 }
216}
217
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500218-(void)setConversationUid:(const std::string)convUid model:(lrc::api::ConversationModel *)model
Anthony Léonard2382b562017-12-13 15:51:28 -0500219{
220 if (convUid_ == convUid && convModel_ == model)
221 return;
222
223 cachedConv_ = nil;
224 convUid_ = convUid;
225 convModel_ = model;
226
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500227 // Signal triggered when messages are received or their status updated
228 QObject::disconnect(newInteractionSignal_);
229 QObject::disconnect(interactionStatusUpdatedSignal_);
230 newInteractionSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newInteraction,
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400231 [self](const std::string& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
232 if (uid != convUid_)
233 return;
234 cachedConv_ = nil;
235 [conversationView noteNumberOfRowsChanged];
236 [self reloadConversationForMessage:interactionId shouldUpdateHeight:YES];
237 });
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500238 interactionStatusUpdatedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::interactionStatusUpdated,
239 [self](const std::string& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
240 if (uid != convUid_)
241 return;
242 cachedConv_ = nil;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400243 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
244 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
245 convModel_->refreshFilter();
246 }
247 [self reloadConversationForMessage:interactionId shouldUpdateHeight:YES];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500248 });
Anthony Léonard2382b562017-12-13 15:51:28 -0500249
250 // Signals tracking changes in conversation list, we need them as cached conversation can be invalid
251 // after a reordering.
252 QObject::disconnect(modelSortedSignal_);
253 QObject::disconnect(filterChangedSignal_);
254 modelSortedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::modelSorted,
255 [self](){
256 cachedConv_ = nil;
257 });
258 filterChangedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::filterChanged,
259 [self](){
260 cachedConv_ = nil;
261 });
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500262 if (pendingMessagesToSend[@(convUid_.c_str())]) {
263 self.message = pendingMessagesToSend[@(convUid_.c_str())];
264 [self updateSendMessageHeight];
265 } else {
266 self.message = @"";
267 if(messagesBottomMargin.constant != SEND_PANEL_DEFAULT_HEIGHT) {
268 sendPanelHeight.constant = SEND_PANEL_DEFAULT_HEIGHT;
269 messagesBottomMargin.constant = SEND_PANEL_DEFAULT_HEIGHT;
270 [self scrollToBottom];
271 }
272 }
273 conversationView.alphaValue = 0.0;
Anthony Léonard2382b562017-12-13 15:51:28 -0500274 [conversationView reloadData];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400275 [conversationView scrollToEndOfDocument:nil];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500276 CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:@"opacity"];
277 fadeIn.fromValue = [NSNumber numberWithFloat:0.0];
278 fadeIn.toValue = [NSNumber numberWithFloat:1.0];
279 fadeIn.duration = 0.4f;
280
281 [conversationView.layer addAnimation:fadeIn forKey:fadeIn.keyPath];
282 conversationView.alphaValue = 1;
283 auto* conv = [self getCurrentConversation];
284
285 if (conv == nil)
286 return;
287 [sendFileButton setEnabled:(convModel_->owner.contactModel->getContact(conv->participants[0]).profileInfo.type != lrc::api::profile::Type::SIP)];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400288}
289
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400290#pragma mark - configure cells
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400291
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400292-(NSTableCellView*) makeGenericInteractionViewForTableView:(NSTableView*)tableView withText:(NSString*)text andTime:(NSString*) time
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500293{
294 NSTableCellView* result = [tableView makeViewWithIdentifier:@"GenericInteractionView" owner:self];
295 NSTextField* textField = [result viewWithTag:GENERIC_INT_TEXT_TAG];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400296 NSTextField* timeField = [result viewWithTag:GENERIC_INT_TIME_TAG];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500297
298 // TODO: Fix symbol in LRC
299 NSString* fixedString = [text stringByReplacingOccurrencesOfString:@"🕽" withString:@"📞"];
300 [textField setStringValue:fixedString];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400301 [timeField setStringValue:time];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500302
303 return result;
304}
305
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400306-(NSTableCellView*) configureViewforTransfer:(lrc::api::interaction::Info)interaction interactionID: (uint64_t) interactionID tableView:(NSTableView*)tableView
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500307{
308 IMTableCellView* result;
309
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400310 auto type = interaction.type;
311 auto status = interaction.status;
312
313 NSString* fileName = @"incoming file";
314
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500315 // First, view is created
316 if (type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER) {
317 switch (status) {
318 case lrc::api::interaction::Status::TRANSFER_CREATED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400319 case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST: {
320 result = [tableView makeViewWithIdentifier:@"LeftIncomingFileView" owner: conversationView];
321 [result.acceptButton setAction:@selector(acceptIncomingFile:)];
322 [result.acceptButton setTarget:self];
323 [result.declineButton setAction:@selector(declineIncomingFile:)];
324 [result.declineButton setTarget:self];
325 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500326 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400327 case lrc::api::interaction::Status::TRANSFER_ONGOING: {
328 result = [tableView makeViewWithIdentifier:@"LeftOngoingFileView" owner:conversationView];
329 [result.progressIndicator startAnimation:conversationView];
330 [result.declineButton setAction:@selector(declineIncomingFile:)];
331 [result.declineButton setTarget:self];
332 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500333 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400334 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
335 [result.transferedFileName setAction:@selector(imagePreview:)];
336 [result.transferedFileName setTarget:self];
337 [result.transferedFileName.cell setHighlightsBy:NSContentsCellMask];
338 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500339 case lrc::api::interaction::Status::TRANSFER_CANCELED:
340 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400341 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
342 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500343 }
344 } else if (type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400345 NSString* fileName = @"sent file";
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500346 switch (status) {
347 case lrc::api::interaction::Status::TRANSFER_CREATED:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500348 case lrc::api::interaction::Status::TRANSFER_ONGOING:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400349 case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500350 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400351 result = [tableView makeViewWithIdentifier:@"RightOngoingFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500352 [result.progressIndicator startAnimation:nil];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400353 [result.declineButton setAction:@selector(declineIncomingFile:)];
354 [result.declineButton setTarget:self];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500355 break;
356 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400357 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
358 [result.transferedFileName setAction:@selector(imagePreview:)];
359 [result.transferedFileName setTarget:self];
360 [result.transferedFileName.cell setHighlightsBy:NSContentsCellMask];
361 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500362 case lrc::api::interaction::Status::TRANSFER_CANCELED:
363 case lrc::api::interaction::Status::TRANSFER_ERROR:
Olivier Soldanoe521a182018-02-26 16:55:19 -0500364 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400365 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500366 }
367 }
368
369 // Then status label is updated if needed
370 switch (status) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400371 [result.statusLabel setTextColor:[NSColor textColor]];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500372 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400373 [result.statusLabel setTextColor:[NSColor greenSuccessColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500374 [result.statusLabel setStringValue:NSLocalizedString(@"Success", @"File transfer successful label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500375 break;
376 case lrc::api::interaction::Status::TRANSFER_CANCELED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400377 [result.statusLabel setTextColor:[NSColor orangeColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500378 [result.statusLabel setStringValue:NSLocalizedString(@"Canceled", @"File transfer canceled label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500379 break;
380 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400381 [result.statusLabel setTextColor:[NSColor errorTransferColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500382 [result.statusLabel setStringValue:NSLocalizedString(@"Failed", @"File transfer failed label")];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500383 break;
384 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400385 [result.statusLabel setTextColor:[NSColor textColor]];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500386 [result.statusLabel setStringValue:NSLocalizedString(@"Unjoinable", @"File transfer peer unjoinable label")];
387 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500388 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400389 result.transferedImage.image = nil;
Kateryna Kostiukeaf1bc82018-10-12 14:33:50 -0400390 [result.openImagebutton setHidden:YES];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400391 [result.msgBackground setHidden:NO];
392 [result invalidateImageConstraints];
393 NSString* name = @(interaction.body.c_str());
394 if (name.length > 0) {
Kateryna Kostiuk67735232018-05-10 15:05:32 -0400395 fileName = [name lastPathComponent];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400396 }
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400397 NSFont *nameFont = [NSFont userFontOfSize:14.0];
398 NSColor *nameColor = [NSColor textColor];
399 NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
400 paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
401 paragraphStyle.alignment = NSTextAlignmentLeft;
402 NSDictionary *nameAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
403 nameColor,NSForegroundColorAttributeName,
404 paragraphStyle,NSParagraphStyleAttributeName, nil];
405 NSAttributedString* nameAttributedString = [[NSAttributedString alloc] initWithString:fileName attributes:nameAttr];
406 result.transferedFileName.attributedTitle = nameAttributedString;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400407 if (status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400408 NSColor *higlightColor = [NSColor grayColor];
409 NSDictionary *alternativeNametAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
410 higlightColor,NSForegroundColorAttributeName,
411 paragraphStyle,NSParagraphStyleAttributeName, nil];
412 NSAttributedString* alternativeString = [[NSAttributedString alloc] initWithString:fileName attributes:alternativeNametAttr];
413 result.transferedFileName.attributedAlternateTitle = alternativeString;
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400414 NSImage* image = [self getImageForFilePath:name];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400415 if (([name rangeOfString:@"/"].location == NSNotFound)) {
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400416 image = [self getImageForFilePath:[self getDataTransferPath:interactionID]];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400417 }
418 if(image != nil) {
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400419 result.transferedImage.image = image;
420 [result updateImageConstraintWithMax: MAX_TRANSFERED_IMAGE_SIZE];
421 [result.openImagebutton setAction:@selector(imagePreview:)];
422 [result.openImagebutton setTarget:self];
Kateryna Kostiukeaf1bc82018-10-12 14:33:50 -0400423 [result.openImagebutton setHidden:NO];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400424 }
425 }
426 [result setupForInteraction:interactionID];
427 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
428 NSString* timeString = [self timeForMessage: msgTime];
429 result.timeLabel.stringValue = timeString;
430 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
431 if (!isOutgoing) {
432 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
433 auto* conv = [self getCurrentConversation];
434 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
435 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500436 return result;
437}
438
Anthony Léonard2382b562017-12-13 15:51:28 -0500439#pragma mark - NSTableViewDelegate methods
440- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400441{
442 return YES;
443}
444
Anthony Léonard2382b562017-12-13 15:51:28 -0500445- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400446{
Anthony Léonard2382b562017-12-13 15:51:28 -0500447 return NO;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400448}
449
Anthony Léonard2382b562017-12-13 15:51:28 -0500450- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400451{
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400452
Anthony Léonard2382b562017-12-13 15:51:28 -0500453 auto* conv = [self getCurrentConversation];
454
455 if (conv == nil)
456 return nil;
457
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500458 auto it = conv->interactions.begin();
Anthony Léonard2382b562017-12-13 15:51:28 -0500459
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500460 std::advance(it, row);
Anthony Léonard2382b562017-12-13 15:51:28 -0500461
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400462 IMTableCellView* result;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400463 auto interaction = it->second;
Anthony Léonard2382b562017-12-13 15:51:28 -0500464 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
465
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500466 switch (interaction.type) {
467 case lrc::api::interaction::Type::TEXT:
468 if (isOutgoing) {
469 result = [tableView makeViewWithIdentifier:@"RightMessageView" owner:self];
470 } else {
471 result = [tableView makeViewWithIdentifier:@"LeftMessageView" owner:self];
472 }
473 break;
474 case lrc::api::interaction::Type::INCOMING_DATA_TRANSFER:
475 case lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400476 return [self configureViewforTransfer:interaction interactionID: it->first tableView:tableView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500477 break;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500478 case lrc::api::interaction::Type::CONTACT:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400479 case lrc::api::interaction::Type::CALL: {
480 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
481 NSString* timeString = [self timeForMessage: msgTime];
482 return [self makeGenericInteractionViewForTableView:tableView withText:@(interaction.body.c_str()) andTime:timeString];
483 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500484 default: // If interaction is not of a known type
485 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400486 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400487 MessageSequencing sequence = [self computeSequencingFor:row];
488 BubbleType type = SINGLE;
489 if (sequence == FIRST_WITHOUT_TIME || sequence == FIRST_WITH_TIME) {
490 type = FIRST;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400491 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400492 if (sequence == MIDDLE_IN_SEQUENCE) {
493 type = MIDDLE;
494 }
495 if (sequence == LAST_IN_SEQUENCE) {
496 type = LAST;
497 }
498 result.msgBackground.type = type;
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400499 bool sendingFail = false;
500 [result.messageStatus setHidden:YES];
501 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
502 if (interaction.status == lrc::api::interaction::Status::SENDING) {
503 [result.messageStatus setHidden:NO];
504 [result.sendingMessageIndicator startAnimation:nil];
505 [result.messageFailed setHidden:YES];
506 } else if (interaction.status == lrc::api::interaction::Status::FAILED) {
507 [result.messageStatus setHidden:NO];
508 [result.sendingMessageIndicator setHidden:YES];
509 [result.messageFailed setHidden:NO];
510 sendingFail = true;
511 }
512 }
513 [result setupForInteraction:it->first isFailed: sendingFail];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400514 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400515 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400516 [result.msgBackground setNeedsDisplay:YES];
517 [result setNeedsDisplay:YES];
518 [result.timeBox setNeedsDisplay:YES];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400519
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400520 NSString *text = @(interaction.body.c_str());
521 text = [text removeEmptyLinesAtBorders];
522
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400523 NSMutableAttributedString* msgAttString =
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400524 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:text]
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400525 attributes:[self messageAttributes]];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400526
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400527 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400528
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400529 [result updateMessageConstraint:messageSize.width andHeight:messageSize.height timeIsVisible:shouldDisplayTime isTopPadding: shouldApplyPadding];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400530 [[result.msgView textStorage] appendAttributedString:msgAttString];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500531 // [result.msgView checkTextInDocument:nil];
Anthony Léonard2382b562017-12-13 15:51:28 -0500532
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400533 NSDataDetector *linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
534 NSArray *matches = [linkDetector matchesInString:result.msgView.string options:0 range:NSMakeRange(0, result.msgView.string.length)];
535
536 [result.msgView.textStorage beginEditing];
537
538 for (NSTextCheckingResult *match in matches) {
539 if (!match.URL) continue;
540
541 NSDictionary *linkAttributes = @{
542 NSLinkAttributeName: match.URL,
543 };
544 [result.msgView.textStorage addAttributes:linkAttributes range:match.range];
545 }
546
547 [result.msgView.textStorage endEditing];
548
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400549 if (shouldDisplayTime) {
550 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
551 NSString* timeString = [self timeForMessage: msgTime];
552 result.timeLabel.stringValue = timeString;
Anthony Léonard64e19672018-01-18 16:40:34 -0500553 }
554
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400555 bool shouldDisplayAvatar = (sequence != MIDDLE_IN_SEQUENCE && sequence != FIRST_WITHOUT_TIME
556 && sequence != FIRST_WITH_TIME) ? YES : NO;
557 [result.photoView setHidden:!shouldDisplayAvatar];
558 if (!isOutgoing && shouldDisplayAvatar) {
559 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
560 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
561 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400562 return result;
563}
564
Anthony Léonard2382b562017-12-13 15:51:28 -0500565- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400566{
Anthony Léonard2382b562017-12-13 15:51:28 -0500567 double someWidth = tableView.frame.size.width * 0.7;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400568
Anthony Léonard2382b562017-12-13 15:51:28 -0500569 auto* conv = [self getCurrentConversation];
570
571 if (conv == nil)
572 return 0;
573
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500574 auto it = conv->interactions.begin();
Anthony Léonard2382b562017-12-13 15:51:28 -0500575
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500576 std::advance(it, row);
Anthony Léonard2382b562017-12-13 15:51:28 -0500577
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400578 auto interaction = it->second;
Anthony Léonard2382b562017-12-13 15:51:28 -0500579
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400580 MessageSequencing sequence = [self computeSequencingFor:row];
581
582 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
583
584
585 if(interaction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER || interaction.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) {
586
587 if( interaction.status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
588 NSString* name = @(interaction.body.c_str());
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400589 NSImage* image = [self getImageForFilePath:name];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400590 if (([name rangeOfString:@"/"].location == NSNotFound)) {
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400591 image = [self getImageForFilePath:[self getDataTransferPath:it->first]];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400592 }
593 if (image != nil) {
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400594 CGFloat widthScaleFactor = MAX_TRANSFERED_IMAGE_SIZE / image.size.width;
595 CGFloat heightScaleFactor = MAX_TRANSFERED_IMAGE_SIZE / image.size.height;
596 CGFloat heigt = 0;
597 if((widthScaleFactor >= 1) && (heightScaleFactor >= 1)) {
598 heigt = image.size.height;
599 } else {
600 CGFloat scale = MIN(widthScaleFactor, heightScaleFactor);
601 heigt = image.size.height * scale;
602 }
603 return heigt + TIME_BOX_HEIGHT;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400604 }
605 }
606 return BUBBLE_HEIGHT_FOR_TRANSFERED_FILE + TIME_BOX_HEIGHT;
607 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500608
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500609 if(interaction.type == lrc::api::interaction::Type::CONTACT || interaction.type == lrc::api::interaction::Type::CALL)
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400610 return GENERIC_CELL_HEIGHT;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500611
Anthony Léonard2382b562017-12-13 15:51:28 -0500612 // TODO Implement interactions other than messages
613 if(interaction.type != lrc::api::interaction::Type::TEXT) {
614 return 0;
615 }
616
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400617 NSString *text = @(interaction.body.c_str());
618 text = [text removeEmptyLinesAtBorders];
619
620 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400621 CGFloat singleLignMessageHeight = 15;
622
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400623 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
624
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400625 if (shouldDisplayTime) {
626 return MAX(messageSize.height + TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2,
627 TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2 + singleLignMessageHeight);
628 }
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400629 if(shouldApplyPadding) {
630 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2 + 15,
631 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2 + 15);
632 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400633 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2,
634 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2);
635}
636
637#pragma mark - message view parameters
638
639-(NSString *) getDataTransferPath:(uint64_t)interactionId {
640 lrc::api::datatransfer::Info info = {};
641 convModel_->getTransferInfo(interactionId, info);
642 double convertData = static_cast<double>(info.totalSize);
643 return @(info.path.c_str());
644}
645
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400646-(NSImage*) getImageForFilePath: (NSString *) path {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400647 if (path.length <= 0) {return nil;}
648 if (![[NSFileManager defaultManager] fileExistsAtPath: path]) {return nil;}
649 NSImage* transferedImage = [[NSImage alloc] initWithContentsOfFile: path];
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400650 return transferedImage;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400651}
652
653-(CGSize) sizeFor:(NSString *) message maxWidth:(CGFloat) width {
654 CGFloat horizaontalMargin = 6;
Anthony Léonard2382b562017-12-13 15:51:28 -0500655 NSMutableAttributedString* msgAttString =
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400656 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@", message]
Anthony Léonard2382b562017-12-13 15:51:28 -0500657 attributes:[self messageAttributes]];
658
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400659 CGFloat finalWidth = MIN(msgAttString.size.width + horizaontalMargin * 2, width);
660 NSRect frame = NSMakeRect(0, 0, finalWidth, msgAttString.size.height);
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400661 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400662 [[tv textStorage] setAttributedString:msgAttString];
663 [tv sizeToFit];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400664 return tv.frame.size;
665}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400666
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400667-(MessageSequencing) computeSequencingFor:(NSInteger) row {
668 auto* conv = [self getCurrentConversation];
669 if (conv == nil)
670 return SINGLE_WITHOUT_TIME;
671 auto it = conv->interactions.begin();
672 std::advance(it, row);
673 auto interaction = it->second;
674 if (interaction.type != lrc::api::interaction::Type::TEXT) {
675 return SINGLE_WITH_TIME;
676 }
677 if (row == 0) {
678 if (it == conv->interactions.end()) {
679 return SINGLE_WITH_TIME;
680 }
681 auto nextIt = it;
682 nextIt++;
683 auto nextInteraction = nextIt->second;
684 if ([self sequenceChangedFrom:interaction to: nextInteraction]) {
685 return SINGLE_WITH_TIME;
686 }
687 return FIRST_WITH_TIME;
688 }
689
690 if (row == conversationView.numberOfRows - 1) {
691 if(it == conv->interactions.begin()) {
692 return SINGLE_WITH_TIME;
693 }
694 auto previousIt = it;
695 previousIt--;
696 auto previousInteraction = previousIt->second;
697 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
698 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
699 if (!timeChanged && !authorChanged) {
700 return LAST_IN_SEQUENCE;
701 }
702 if (!timeChanged && authorChanged) {
703 return SINGLE_WITHOUT_TIME;
704 }
705 return SINGLE_WITH_TIME;
706 }
707 if(it == conv->interactions.begin() || it == conv->interactions.end()) {
708 return SINGLE_WITH_TIME;
709 }
710 auto previousIt = it;
711 previousIt--;
712 auto previousInteraction = previousIt->second;
713 auto nextIt = it;
714 nextIt++;
715 auto nextInteraction = nextIt->second;
716
717 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
718 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
719 bool sequenceWillChange = [self sequenceChangedFrom:interaction to: nextInteraction];
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400720 if (previousInteraction.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER ||
721 previousInteraction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER) {
722 if(!sequenceWillChange) {
723 return FIRST_WITH_TIME;
724 }
725 return SINGLE_WITH_TIME;
726 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400727 if (!sequenceWillChange) {
728 if (!timeChanged && !authorChanged) {
729 return MIDDLE_IN_SEQUENCE;
730 }
731 if (timeChanged) {
732 return FIRST_WITH_TIME;
733 }
734 return FIRST_WITHOUT_TIME;
735 } if (!timeChanged && !authorChanged) {
736 return LAST_IN_SEQUENCE;
737 } if (timeChanged) {
738 return SINGLE_WITH_TIME;
739 }
740 return SINGLE_WITHOUT_TIME;
741}
742
743-(bool) sequenceChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
744 return ([self sequenceTimeChangedFrom:firstInteraction to:secondInteraction] || [self sequenceAuthorChangedFrom:firstInteraction to:secondInteraction]);
745}
746
747-(bool) sequenceTimeChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
748 bool timeChanged = NO;
749 NSDate* firstMessageTime = [NSDate dateWithTimeIntervalSince1970:firstInteraction.timestamp];
750 NSDate* secondMessageTime = [NSDate dateWithTimeIntervalSince1970:secondInteraction.timestamp];
751 bool hourComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitHour];
752 bool minutComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitMinute];
753 if(hourComp != NSOrderedSame || minutComp != NSOrderedSame) {
754 timeChanged = YES;
755 }
756 return timeChanged;
757}
758
759-(bool) sequenceAuthorChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
760 bool authorChanged = YES;
761 bool isOutgoing = lrc::api::interaction::isOutgoing(firstInteraction);
762 if ((secondInteraction.type == lrc::api::interaction::Type::TEXT) && (isOutgoing == lrc::api::interaction::isOutgoing(secondInteraction))) {
763 authorChanged = NO;
764 }
765 return authorChanged;
766}
767
768-(NSString *)timeForMessage:(NSDate*) msgTime {
769 NSDate *today = [NSDate date];
770 NSDateFormatter *dateFormatter=[[NSDateFormatter alloc] init];
Kateryna Kostiukaf6d5e22018-06-12 15:00:00 -0400771 [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:[[NSLocale currentLocale] localeIdentifier]]];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400772 if ([[NSCalendar currentCalendar] compareDate:today
773 toDate:msgTime
774 toUnitGranularity:NSCalendarUnitYear]!= NSOrderedSame) {
775 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterMediumStyle];
776 }
777
778 if ([[NSCalendar currentCalendar] compareDate:today
779 toDate:msgTime
780 toUnitGranularity:NSCalendarUnitDay]!= NSOrderedSame ||
781 [[NSCalendar currentCalendar] compareDate:today
782 toDate:msgTime
783 toUnitGranularity:NSCalendarUnitMonth]!= NSOrderedSame) {
784 [dateFormatter setDateFormat:@"MMM dd, HH:mm"];
785 return [dateFormatter stringFromDate:msgTime];
786 }
787
788 [dateFormatter setDateFormat:@"HH:mm"];
789 return [dateFormatter stringFromDate:msgTime];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400790}
791
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400792- (void) updateSendMessageHeight {
793 NSAttributedString *msgAttString = messageField.attributedStringValue;
794 NSRect frame = NSMakeRect(0, 0, messageField.frame.size.width, msgAttString.size.height);
795 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
796 [[tv textStorage] setAttributedString:msgAttString];
797 [tv sizeToFit];
798 CGFloat height = tv.frame.size.height + MEESAGE_MARGIN * 2;
799 CGFloat newHeight = MIN(SEND_PANEL_MAX_HEIGHT, MAX(SEND_PANEL_DEFAULT_HEIGHT, height));
800 if(messagesBottomMargin.constant == newHeight) {
801 return;
802 }
803 messagesBottomMargin.constant = newHeight;
804 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.05 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
805 [self scrollToBottom];
806 sendPanelHeight.constant = newHeight;
807 });
808}
809
Anthony Léonard2382b562017-12-13 15:51:28 -0500810#pragma mark - NSTableViewDataSource
811
812- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
813{
814 auto* conv = [self getCurrentConversation];
815
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500816 if (conv)
817 return conv->interactions.size();
818 else
819 return 0;
Anthony Léonard2382b562017-12-13 15:51:28 -0500820}
821
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400822#pragma mark - Text formatting
823
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400824- (NSMutableDictionary*) messageAttributes
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400825{
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400826 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400827 attrs[NSForegroundColorAttributeName] = [NSColor labelColor];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400828 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400829 return attrs;
830}
831
832- (NSParagraphStyle*) paragraphStyle
833{
834 /*
835 The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
836 NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
837 to copy, we use the default one.
838
839 The default values supplied by the default NSParagraphStyle are:
840 Alignment NSNaturalTextAlignment
841 Tab stops 12 left-aligned tabs, spaced by 28.0 points
842 Line break mode NSLineBreakByWordWrapping
843 All others 0.0
844 */
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400845 NSMutableParagraphStyle* aMutableParagraphStyle =
846 [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
847 [aMutableParagraphStyle setHeadIndent:1.0];
848 [aMutableParagraphStyle setFirstLineHeadIndent:1.0];
849 return aMutableParagraphStyle;
850}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400851
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500852#pragma mark - Actions
853
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400854- (void)acceptIncomingFile:(id)sender {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500855 auto interId = [(IMTableCellView*)[[sender superview] superview] interaction];
856 auto& inter = [self getCurrentConversation]->interactions.find(interId)->second;
857 if (convModel_ && !convUid_.empty()) {
858 NSSavePanel* filePicker = [NSSavePanel savePanel];
859 [filePicker setNameFieldStringValue:@(inter.body.c_str())];
860
861 if ([filePicker runModal] == NSFileHandlingPanelOKButton) {
862 const char* fullPath = [[filePicker URL] fileSystemRepresentation];
863 convModel_->acceptTransfer(convUid_, interId, fullPath);
864 }
865 }
866}
867
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400868- (void)declineIncomingFile:(id)sender {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500869 auto inter = [(IMTableCellView*)[[sender superview] superview] interaction];
870 if (convModel_ && !convUid_.empty()) {
871 convModel_->cancelTransfer(convUid_, inter);
872 }
873}
874
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400875- (void)imagePreview:(id)sender {
876 uint64_t interId;
877 if ([[sender superview] isKindOfClass:[IMTableCellView class]]) {
878 interId = [(IMTableCellView*)[sender superview] interaction];
879 } else if ([[[sender superview] superview] isKindOfClass:[IMTableCellView class]]) {
880 interId = [(IMTableCellView*)[[sender superview] superview] interaction];
881 } else {
882 return;
883 }
884 auto it = [self getCurrentConversation]->interactions.find(interId);
885 if (it == [self getCurrentConversation]->interactions.end()) {
886 return;
887 }
888 auto& interaction = it->second;
889 NSString* name = @(interaction.body.c_str());
890 if (([name rangeOfString:@"/"].location == NSNotFound)) {
891 name = [self getDataTransferPath:interId];
892 }
893 previewImage = name;
894 if ([QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]) {
895 [[QLPreviewPanel sharedPreviewPanel] orderOut:nil];
896 } else {
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400897 dispatch_async(dispatch_get_main_queue(), ^{
898 [[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:self];
899 });
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400900 }
901}
902
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500903- (IBAction)sendMessage:(id)sender {
904 NSString* text = self.message;
905 if (text && text.length > 0) {
906 auto* conv = [self getCurrentConversation];
907 convModel_->sendMessage(convUid_, std::string([text UTF8String]));
908 self.message = @"";
909 if(sendPanelHeight.constant != SEND_PANEL_DEFAULT_HEIGHT) {
910 sendPanelHeight.constant = SEND_PANEL_DEFAULT_HEIGHT;
911 messagesBottomMargin.constant = SEND_PANEL_DEFAULT_HEIGHT;
912 [self scrollToBottom];
913 }
914 }
915}
916
917- (IBAction)openEmojy:(id)sender {
918 [messageField.window makeFirstResponder: messageField];
919 [[messageField currentEditor] moveToEndOfLine:nil];
920 [NSApp orderFrontCharacterPalette: messageField];
921}
922
923- (IBAction)sendFile:(id)sender {
924 NSOpenPanel* filePicker = [NSOpenPanel openPanel];
925 [filePicker setCanChooseFiles:YES];
926 [filePicker setCanChooseDirectories:NO];
927 [filePicker setAllowsMultipleSelection:NO];
928
929 if ([filePicker runModal] == NSFileHandlingPanelOKButton) {
930 if ([[filePicker URLs] count] == 1) {
931 NSURL* url = [[filePicker URLs] objectAtIndex:0];
932 const char* fullPath = [url fileSystemRepresentation];
933 NSString* fileName = [url lastPathComponent];
934 if (convModel_) {
935 auto* conv = [self getCurrentConversation];
936 convModel_->sendFile(convUid_, std::string(fullPath), std::string([fileName UTF8String]));
937 }
938 }
939 }
940}
941
942
943#pragma mark - NSTextFieldDelegate
944
945- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector
946{
947 if (commandSelector == @selector(insertNewline:)) {
948 if(self.message.length > 0) {
949 [self sendMessage: nil];
950 } else if(messagesBottomMargin.constant != SEND_PANEL_DEFAULT_HEIGHT) {
951 sendPanelHeight.constant = SEND_PANEL_DEFAULT_HEIGHT;
952 messagesBottomMargin.constant = SEND_PANEL_DEFAULT_HEIGHT;
953 [self scrollToBottom];
954 }
955 return YES;
956 }
957 return NO;
958}
959
960- (void)controlTextDidChange:(NSNotification *)aNotification {
961 [self updateSendMessageHeight];
962}
963
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400964#pragma mark - QLPreviewPanelDataSource
965
966-(void)beginPreviewPanelControl:(QLPreviewPanel *)panel
967{
968 panel.dataSource = self;
969}
970
971- (void)endPreviewPanelControl:(QLPreviewPanel *)panel {
972 panel.dataSource = nil;
973}
974
975-(BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel
976{
977 return YES;
978}
979
980- (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel *)panel {
981 return 1;
982}
983
984- (id <QLPreviewItem>)previewPanel:(QLPreviewPanel *)panel previewItemAtIndex:(NSInteger)index {
985 return [NSURL fileURLWithPath:previewImage];
986}
987
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400988@end