blob: 23a9c4bcf617878752b0c33762a224375f9698fc [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"
kkostiukc48e73e2021-04-13 09:20:08 -040033#import "views/FileToSendCollectionItem.h"
Anthony Léonard2382b562017-12-13 15:51:28 -050034#import "delegates/ImageManipulationDelegate.h"
Anthony Léonard6f819752018-01-05 09:53:40 -050035#import "utils.h"
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040036#import "views/NSColor+RingTheme.h"
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040037#import "views/IconButton.h"
kkostiukcf0757f2021-03-26 12:29:47 -040038#import "views/TextViewWithPlaceholder.h"
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040039#import <QuickLook/QuickLook.h>
40#import <Quartz/Quartz.h>
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -040041#import <AVFoundation/AVFoundation.h>
42
43#import "RecordFileVC.h"
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040044
kkostiukc48e73e2021-04-13 09:20:08 -040045@implementation PendingFile
46@end
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040047
kkostiukc48e73e2021-04-13 09:20:08 -040048@interface MessagesVC () <NSTableViewDelegate, NSTableViewDataSource, QLPreviewPanelDataSource, NSTextViewDelegate, NSCollectionViewDataSource> {
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040049
Anthony Léonard2382b562017-12-13 15:51:28 -050050 __unsafe_unretained IBOutlet NSTableView* conversationView;
kkostiukc48e73e2021-04-13 09:20:08 -040051 __unsafe_unretained IBOutlet DraggingDestinationView* draggingDestinationView;
52 __unsafe_unretained IBOutlet NSCollectionView* pendingFilesCollectionView;
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040053 __unsafe_unretained IBOutlet NSView* containerView;
kkostiukcf0757f2021-03-26 12:29:47 -040054 __unsafe_unretained IBOutlet TextViewWithPlaceholder* messageView;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050055 __unsafe_unretained IBOutlet IconButton *sendFileButton;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -040056 __unsafe_unretained IBOutlet IconButton *recordVideoButton;
57 __unsafe_unretained IBOutlet IconButton *recordAudioButton;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050058 __unsafe_unretained IBOutlet NSLayoutConstraint* sendPanelHeight;
kkostiuk26f99fb2021-02-17 15:06:56 -050059 __unsafe_unretained IBOutlet NSLayoutConstraint* messageHeight;
kkostiuk26f99fb2021-02-17 15:06:56 -050060 __unsafe_unretained IBOutlet NSLayoutConstraint* textBottomConstraint;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -040061 IBOutlet NSPopover *recordMessagePopover;
kkostiuk01236c42021-04-28 16:51:14 -040062 NSMenu *messageActionsMenu;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040063
Kateryna Kostiukc867eb92020-03-08 13:15:17 -040064 QString convUid_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050065 lrc::api::ConversationModel* convModel_;
Anthony Léonard2382b562017-12-13 15:51:28 -050066 const lrc::api::conversation::Info* cachedConv_;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -040067 lrc::api::AVModel* avModel;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050068 QMetaObject::Connection newInteractionSignal_;
Anthony Léonard2382b562017-12-13 15:51:28 -050069
70 // Both are needed to invalidate cached conversation as pointer
71 // may not be referencing the same conversation anymore
72 QMetaObject::Connection modelSortedSignal_;
73 QMetaObject::Connection filterChangedSignal_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050074 QMetaObject::Connection interactionStatusUpdatedSignal_;
kkostiuk01236c42021-04-28 16:51:14 -040075 QMetaObject::Connection interactionRemovedSignal_;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -040076 QMetaObject::Connection peerComposingMsgSignal_;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -040077 QMetaObject::Connection lastDisplayedChanged_;
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040078 NSString* previewImage;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050079 NSMutableDictionary *pendingMessagesToSend;
kkostiukc48e73e2021-04-13 09:20:08 -040080 RecordFileVC* recordingController;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040081}
82
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040083@end
84
Anthony Léonardf2bb17d2018-02-15 17:18:09 -050085// Tags for view
86NSInteger const GENERIC_INT_TEXT_TAG = 100;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040087NSInteger const GENERIC_INT_TIME_TAG = 200;
88
89// views size
90CGFloat const GENERIC_CELL_HEIGHT = 60;
91CGFloat const TIME_BOX_HEIGHT = 34;
92CGFloat const MESSAGE_TEXT_PADDING = 10;
93CGFloat const MAX_TRANSFERED_IMAGE_SIZE = 250;
94CGFloat const BUBBLE_HEIGHT_FOR_TRANSFERED_FILE = 87;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -040095CGFloat const DEFAULT_ROW_HEIGHT = 10;
96CGFloat const HEIGHT_FOR_COMPOSING_INDICATOR = 46;
Kateryna Kostiuka1201922020-04-20 11:59:35 -040097CGFloat const HEIGHT_DEFAULT = 34;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050098NSInteger const SEND_PANEL_DEFAULT_HEIGHT = 60;
kkostiuk26f99fb2021-02-17 15:06:56 -050099NSInteger const SEND_PANEL_MAX_HEIGHT = 167;
100NSInteger const SEND_PANEL_BOTTOM_MARGIN = 13;
kkostiuk153c2692021-02-23 21:30:23 -0500101NSInteger MESSAGE_VIEW_DEFAULT_HEIGHT = 17;
kkostiuk26f99fb2021-02-17 15:06:56 -0500102NSInteger const BOTTOM_MARGIN = 8;
103NSInteger const BOTTOM_MARGIN_MIN = 0;
104NSInteger const TOP_MARGIN = 20;
105NSInteger const TOP_MARGIN_MIN = 13;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500106
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400107BOOL peerComposingMessage = false;
108BOOL composingMessage = false;
109
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400110@implementation MessagesVC
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400111
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400112
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400113//MessageBuble type
114typedef NS_ENUM(NSInteger, MessageSequencing) {
115 SINGLE_WITH_TIME = 0,
116 SINGLE_WITHOUT_TIME = 1,
117 FIRST_WITH_TIME = 2,
118 FIRST_WITHOUT_TIME = 3,
119 MIDDLE_IN_SEQUENCE = 5,
120 LAST_IN_SEQUENCE = 6,
121};
122
123- (void)awakeFromNib
124{
125 NSNib *cellNib = [[NSNib alloc] initWithNibNamed:@"MessageCells" bundle:nil];
126 [conversationView registerNib:cellNib forIdentifier:@"LeftIncomingFileView"];
127 [conversationView registerNib:cellNib forIdentifier:@"LeftOngoingFileView"];
128 [conversationView registerNib:cellNib forIdentifier:@"LeftFinishedFileView"];
129 [conversationView registerNib:cellNib forIdentifier:@"RightOngoingFileView"];
130 [conversationView registerNib:cellNib forIdentifier:@"RightFinishedFileView"];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400131 [conversationView registerNib:cellNib forIdentifier:@"PeerComposingMsgView"];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500132 [[conversationView.enclosingScrollView contentView] setCopiesOnScroll:NO];
kkostiuk26f99fb2021-02-17 15:06:56 -0500133 [messageView setFont: [NSFont systemFontOfSize: 14 weight: NSFontWeightLight]];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500134 [conversationView setWantsLayer:YES];
kkostiukc48e73e2021-04-13 09:20:08 -0400135 draggingDestinationView.draggingDestinationDelegate = self;
136}
137
138-(void)callFinished {
139 [self reloadPendingFiles];
140 dispatch_async(dispatch_get_main_queue(), ^{
141 [conversationView scrollToEndOfDocument:nil];
142 [messageView.window makeFirstResponder: messageView];
143 });
144}
145
146+(NSMutableDictionary*)pendingFiles
147{
148 static NSMutableDictionary* files = nil;
149 static dispatch_once_t oncePredicate;
150 dispatch_once(&oncePredicate, ^{
151 files = [[NSMutableDictionary alloc] init];
152 });
153 return files;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400154}
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500155
156- (instancetype)initWithCoder:(NSCoder *)coder
157{
158 self = [super initWithCoder:coder];
159 if (self) {
160 pendingMessagesToSend = [[NSMutableDictionary alloc] init];
161 }
162 return self;
163}
164
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -0400165-(void) setAVModel: (lrc::api::AVModel*) avmodel {
166 avModel = avmodel;
167 if (recordingController == nil) {
168 recordingController = [[RecordFileVC alloc] initWithNibName:@"RecordFileVC" bundle:nil avModel: self->avModel];
169 recordingController.delegate = self;
170 }
171}
172
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500173- (void)setMessage:(NSString *)newValue {
174 _message = [newValue removeEmptyLinesAtBorders];
175}
176
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400177-(void) clearData {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400178 if (!convUid_.isEmpty()) {
kkostiuk26f99fb2021-02-17 15:06:56 -0500179 pendingMessagesToSend[convUid_.toNSString()] = self.message;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500180 }
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400181 cachedConv_ = nil;
182 convUid_ = "";
183 convModel_ = nil;
184
185 QObject::disconnect(modelSortedSignal_);
186 QObject::disconnect(filterChangedSignal_);
187 QObject::disconnect(interactionStatusUpdatedSignal_);
188 QObject::disconnect(newInteractionSignal_);
Kateryna Kostiuk5acaefd2020-03-25 11:14:25 -0400189 QObject::disconnect(peerComposingMsgSignal_);
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400190 QObject::disconnect(lastDisplayedChanged_);
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -0400191 [self closeRecordingView];
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400192}
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400193
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500194-(void) scrollToBottom {
195 CGRect visibleRect = [conversationView enclosingScrollView].contentView.visibleRect;
196 NSRange range = [conversationView rowsInRect:visibleRect];
197 NSIndexSet* visibleIndexes = [NSIndexSet indexSetWithIndexesInRange:range];
198 NSUInteger lastvisibleRow = [visibleIndexes lastIndex];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400199 NSInteger numberOfRows = [conversationView numberOfRows];
200 if ((numberOfRows > 0) &&
kkostiuk01236c42021-04-28 16:51:14 -0400201 lastvisibleRow > (numberOfRows - visibleIndexes.count * 2)) {
kkostiukd83b4742021-03-10 14:29:37 -0500202 [conversationView scrollToEndOfDocument: nil];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500203 }
204}
205
Anthony Léonard2382b562017-12-13 15:51:28 -0500206-(const lrc::api::conversation::Info*) getCurrentConversation
207{
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400208 if (convModel_ == nil || convUid_.isEmpty())
Anthony Léonard2382b562017-12-13 15:51:28 -0500209 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400210
Anthony Léonard2382b562017-12-13 15:51:28 -0500211 if (cachedConv_ != nil)
212 return cachedConv_;
Kateryna Kostiuka7b909c2020-10-19 11:46:26 -0400213 auto convOpt = getConversationFromUid(convUid_, *convModel_);
214 if (convOpt.has_value()) {
kkostiukf81c6372021-01-11 18:51:28 -0500215 lrc::api::conversation::Info& conversation = *convOpt;
Kateryna Kostiuka7b909c2020-10-19 11:46:26 -0400216 cachedConv_ = &conversation;
Kateryna Kostiuk3541ae22020-08-17 12:26:14 -0400217 }
Anthony Léonard2382b562017-12-13 15:51:28 -0500218 return cachedConv_;
219}
220
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400221-(void) reloadConversationForMessage:(uint64_t) uid updateSize:(BOOL) update {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400222 auto* conv = [self getCurrentConversation];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400223 if (conv == nil)
224 return;
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400225 auto it = conv->interactions.find(uid);
226 if (it == conv->interactions.end()) {
227 return;
228 }
229 auto itIndex = distance(conv->interactions.begin(),it);
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400230 if (itIndex >= ([conversationView numberOfRows] - 1) || itIndex >= conv->interactions.size()) {
Kateryna Kostiuka1201922020-04-20 11:59:35 -0400231 return;
232 }
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400233 NSRange rangeToUpdate = NSMakeRange(itIndex, 2);
234 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndexesInRange:rangeToUpdate];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400235 //reload previous message to update bubbleview
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400236 if (itIndex > 0) {
237 auto previousIt = it;
238 previousIt--;
239 auto previousInteraction = previousIt->second;
240 if (previousInteraction.type == lrc::api::interaction::Type::TEXT) {
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400241 NSRange range = NSMakeRange(itIndex - 1, 3);
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400242 indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
243 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400244 }
245 if (update) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400246 NSRange insertRange = NSMakeRange(itIndex, 1);
247 NSIndexSet* insertRangeSet = [NSIndexSet indexSetWithIndexesInRange:insertRange];
248 [conversationView removeRowsAtIndexes:insertRangeSet withAnimation:(NSTableViewAnimationEffectNone)];
249 [conversationView insertRowsAtIndexes:insertRangeSet withAnimation:(NSTableViewAnimationEffectNone)];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400250 }
251 [conversationView reloadDataForRowIndexes: indexSet
252 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400253 [self scrollToBottom];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400254}
255
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400256-(void)setConversationUid:(const QString&)convUid model:(lrc::api::ConversationModel *)model
Anthony Léonard2382b562017-12-13 15:51:28 -0500257{
258 if (convUid_ == convUid && convModel_ == model)
259 return;
260
261 cachedConv_ = nil;
262 convUid_ = convUid;
263 convModel_ = model;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400264 peerComposingMessage = false;
265 composingMessage = false;
Anthony Léonard2382b562017-12-13 15:51:28 -0500266
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500267 // Signal triggered when messages are received or their status updated
268 QObject::disconnect(newInteractionSignal_);
269 QObject::disconnect(interactionStatusUpdatedSignal_);
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400270 QObject::disconnect(peerComposingMsgSignal_);
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400271 QObject::disconnect(lastDisplayedChanged_);
kkostiuk01236c42021-04-28 16:51:14 -0400272 QObject::disconnect(interactionRemovedSignal_);
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400273 lastDisplayedChanged_ =
274 QObject::connect(convModel_,
275 &lrc::api::ConversationModel::displayedInteractionChanged,
276 [self](const QString &uid,
277 const QString &participantURI,
278 const uint64_t &previousUid,
279 const uint64_t &newdUid) {
280 if (uid != convUid_)
281 return;
282 [self reloadConversationForMessage:newdUid updateSize: NO];
283 [self reloadConversationForMessage:previousUid updateSize: NO];
284 });
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400285
286 peerComposingMsgSignal_ = QObject::connect(convModel_,
287 &lrc::api::ConversationModel::composingStatusChanged,
288 [self](const QString &uid,
289 const QString &contactUri,
290 bool isComposing) {
291 if (uid != convUid_)
292 return;
293 bool shouldUpdate = isComposing != peerComposingMessage;
294 if (!shouldUpdate) {
295 return;
296 }
297 // reload and update height for composing indicator
298 peerComposingMessage = isComposing;
299 auto* conv = [self getCurrentConversation];
300 if (conv == nil)
301 return;
302 auto row = [conversationView numberOfRows] - 1;
303 if (row < 0) {
304 return;
305 }
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400306 if(peerComposingMessage) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400307 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:row];
308 [conversationView reloadDataForRowIndexes: indexSet
309 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400310 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400311 [self scrollToBottom];
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400312 } else {
313 //whait for possible incoming message to avoid view jumping
314 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400315 auto row = [conversationView numberOfRows] - 1;
316 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:row];
317 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
318 [conversationView reloadDataForRowIndexes: indexSet
319 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
320 [self scrollToBottom];
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400321 });
322 }
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400323 });
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500324 newInteractionSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newInteraction,
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400325 [self](const QString& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400326 if (uid != convUid_)
327 return;
328 cachedConv_ = nil;
329 peerComposingMessage = false;
330 [conversationView noteNumberOfRowsChanged];
331 [self reloadConversationForMessage:interactionId updateSize: YES];
332 [self scrollToBottom];
333 });
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500334 interactionStatusUpdatedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::interactionStatusUpdated,
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400335 [self](const QString& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400336 if (uid != convUid_)
337 return;
338 cachedConv_ = nil;
339 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
340 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
341 convModel_->refreshFilter();
342 }
343 [self reloadConversationForMessage:interactionId updateSize: interaction.type == lrc::api::interaction::Type::DATA_TRANSFER];
344 [self scrollToBottom];
345 });
kkostiuk01236c42021-04-28 16:51:14 -0400346 interactionRemovedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::interactionRemoved,
347 [self](const QString& uid, uint64_t interactionId) {
348 cachedConv_ = nil;
349 });
350
Anthony Léonard2382b562017-12-13 15:51:28 -0500351
352 // Signals tracking changes in conversation list, we need them as cached conversation can be invalid
353 // after a reordering.
354 QObject::disconnect(modelSortedSignal_);
355 QObject::disconnect(filterChangedSignal_);
Kateryna Kostiuka7b909c2020-10-19 11:46:26 -0400356 modelSortedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::modelChanged,
Anthony Léonard2382b562017-12-13 15:51:28 -0500357 [self](){
358 cachedConv_ = nil;
359 });
360 filterChangedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::filterChanged,
361 [self](){
362 cachedConv_ = nil;
363 });
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400364 if (pendingMessagesToSend[convUid_.toNSString()]) {
kkostiuk26f99fb2021-02-17 15:06:56 -0500365 NSString *mess = pendingMessagesToSend[convUid_.toNSString()];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400366 self.message = pendingMessagesToSend[convUid_.toNSString()];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500367 [self updateSendMessageHeight];
368 } else {
369 self.message = @"";
kkostiuk26f99fb2021-02-17 15:06:56 -0500370 [self resetSendMessagePanelToDefaultSize];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500371 }
kkostiuke6c33db2021-02-24 16:20:50 -0500372 dispatch_async(dispatch_get_main_queue(), ^{
373 [messageView.window makeFirstResponder: messageView];
374 });
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500375 auto* conv = [self getCurrentConversation];
376
377 if (conv == nil)
378 return;
kkostiukcf0757f2021-03-26 12:29:47 -0400379 NSString* name = bestNameForConversation(*conv, *convModel_);
kkostiuke5d4abe2021-04-21 21:13:49 -0400380 [self updatePlaceholder];
kkostiukc48e73e2021-04-13 09:20:08 -0400381 [self reloadPendingFiles];
382 conversationView.alphaValue = 0.0;
383 [conversationView reloadData];
384 [conversationView scrollToEndOfDocument:nil];
385 CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:@"opacity"];
386 fadeIn.fromValue = [NSNumber numberWithFloat:0.0];
387 fadeIn.toValue = [NSNumber numberWithFloat:1.0];
388 fadeIn.duration = 0.4f;
389
390 [conversationView.layer addAnimation:fadeIn forKey:fadeIn.keyPath];
391 conversationView.alphaValue = 1;
392 try {
393 [sendFileButton setEnabled:(convModel_->owner.contactModel->getContact(conv->participants[0]).profileInfo.type != lrc::api::profile::Type::SIP)];
394 } catch (std::out_of_range& e) {
395 NSLog(@"contact out of range");
396 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400397}
398
kkostiuke5d4abe2021-04-21 21:13:49 -0400399-(void)updatePlaceholder {
400 auto* conv = [self getCurrentConversation];
401
402 if (conv == nil)
403 return;
404 NSString* name = bestNameForConversation(*conv, *convModel_);
405 NSString *placeholder = [NSString stringWithFormat:@"%@%@", @"Write to ", name];
406
407 NSFont *fontName = [NSFont systemFontOfSize: 14.0 weight: NSFontWeightRegular];
408 NSColor *color = [NSColor tertiaryLabelColor];
409 NSDictionary *nameAttrs = [NSDictionary dictionaryWithObjectsAndKeys:
410 fontName, NSFontAttributeName,
411 color, NSForegroundColorAttributeName,
412 nil];
413 NSAttributedString* attributedPlaceholder = [[NSAttributedString alloc] initWithString: placeholder attributes:nameAttrs];
414 messageView.placeholderAttributedString = attributedPlaceholder;
415}
416
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400417#pragma mark - configure cells
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400418
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400419-(NSTableCellView*) makeGenericInteractionViewForTableView:(NSTableView*)tableView withText:(NSString*)text andTime:(NSString*) time
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500420{
421 NSTableCellView* result = [tableView makeViewWithIdentifier:@"GenericInteractionView" owner:self];
422 NSTextField* textField = [result viewWithTag:GENERIC_INT_TEXT_TAG];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400423 NSTextField* timeField = [result viewWithTag:GENERIC_INT_TIME_TAG];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500424
425 // TODO: Fix symbol in LRC
Kateryna Kostiuk66406432019-11-09 17:20:34 -0500426 NSString* fixedString = [[text stringByReplacingOccurrencesOfString:@"🕽" withString:@""] stringByReplacingOccurrencesOfString:@"📞" withString:@""];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500427 [textField setStringValue:fixedString];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400428 [timeField setStringValue:time];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500429
430 return result;
431}
432
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400433-(NSTableCellView*) configureViewforTransfer:(lrc::api::interaction::Info)interaction interactionID: (uint64_t) interactionID tableView:(NSTableView*)tableView
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500434{
435 IMTableCellView* result;
436
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400437 auto type = interaction.type;
438 auto status = interaction.status;
439
440 NSString* fileName = @"incoming file";
441
kkostiuk01236c42021-04-28 16:51:14 -0400442 BOOL couldResend = false;
443
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500444 // First, view is created
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400445 if (!interaction.authorUri.isEmpty()) {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500446 switch (status) {
447 case lrc::api::interaction::Status::TRANSFER_CREATED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400448 case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST: {
449 result = [tableView makeViewWithIdentifier:@"LeftIncomingFileView" owner: conversationView];
450 [result.acceptButton setAction:@selector(acceptIncomingFile:)];
451 [result.acceptButton setTarget:self];
452 [result.declineButton setAction:@selector(declineIncomingFile:)];
453 [result.declineButton setTarget:self];
454 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500455 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400456 case lrc::api::interaction::Status::TRANSFER_ONGOING: {
457 result = [tableView makeViewWithIdentifier:@"LeftOngoingFileView" owner:conversationView];
458 [result.progressIndicator startAnimation:conversationView];
459 [result.declineButton setAction:@selector(declineIncomingFile:)];
460 [result.declineButton setTarget:self];
461 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500462 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400463 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400464 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500465 case lrc::api::interaction::Status::TRANSFER_CANCELED:
466 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400467 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
468 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500469 }
Kateryna Kostiuk209a6302019-08-14 16:46:21 -0400470 } else {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400471 NSString* fileName = @"sent file";
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500472 switch (status) {
473 case lrc::api::interaction::Status::TRANSFER_CREATED:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500474 case lrc::api::interaction::Status::TRANSFER_ONGOING:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400475 case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500476 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400477 result = [tableView makeViewWithIdentifier:@"RightOngoingFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500478 [result.progressIndicator startAnimation:nil];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400479 [result.declineButton setAction:@selector(declineIncomingFile:)];
480 [result.declineButton setTarget:self];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500481 break;
482 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400483 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400484 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500485 case lrc::api::interaction::Status::TRANSFER_CANCELED:
486 case lrc::api::interaction::Status::TRANSFER_ERROR:
Olivier Soldanoe521a182018-02-26 16:55:19 -0500487 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400488 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
kkostiuk01236c42021-04-28 16:51:14 -0400489 couldResend = true;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500490 }
491 }
492
493 // Then status label is updated if needed
494 switch (status) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400495 [result.statusLabel setTextColor:[NSColor textColor]];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500496 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400497 [result.statusLabel setTextColor:[NSColor greenSuccessColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500498 [result.statusLabel setStringValue:NSLocalizedString(@"Success", @"File transfer successful label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500499 break;
500 case lrc::api::interaction::Status::TRANSFER_CANCELED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400501 [result.statusLabel setTextColor:[NSColor orangeColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500502 [result.statusLabel setStringValue:NSLocalizedString(@"Canceled", @"File transfer canceled label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500503 break;
504 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400505 [result.statusLabel setTextColor:[NSColor errorTransferColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500506 [result.statusLabel setStringValue:NSLocalizedString(@"Failed", @"File transfer failed label")];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500507 break;
508 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400509 [result.statusLabel setTextColor:[NSColor textColor]];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500510 [result.statusLabel setStringValue:NSLocalizedString(@"Unjoinable", @"File transfer peer unjoinable label")];
511 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500512 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400513 result.transferedImage.image = nil;
Kateryna Kostiukeaf1bc82018-10-12 14:33:50 -0400514 [result.openImagebutton setHidden:YES];
kkostiukea7984a2021-04-21 20:59:21 -0400515 [result.openFileButton setHidden:YES];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400516 [result.msgBackground setHidden:NO];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400517 NSString* name = interaction.body.toNSString();
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400518 if (name.length > 0) {
Kateryna Kostiuk67735232018-05-10 15:05:32 -0400519 fileName = [name lastPathComponent];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400520 }
Kateryna Kostiukd508f752020-11-13 16:26:00 -0500521 NSFont *nameFont = [NSFont systemFontOfSize: 12 weight: NSFontWeightLight];
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400522 NSColor *nameColor = [NSColor textColor];
523 NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
524 paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
525 paragraphStyle.alignment = NSTextAlignmentLeft;
526 NSDictionary *nameAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
527 nameColor,NSForegroundColorAttributeName,
528 paragraphStyle,NSParagraphStyleAttributeName, nil];
529 NSAttributedString* nameAttributedString = [[NSAttributedString alloc] initWithString:fileName attributes:nameAttr];
530 result.transferedFileName.attributedTitle = nameAttributedString;
kkostiuk01236c42021-04-28 16:51:14 -0400531 BOOL fileExists = false;
532 BOOL hasImage = false;
533 NSColor *higlightColor = [NSColor grayColor];
534 NSDictionary *alternativeNametAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
535 higlightColor,NSForegroundColorAttributeName,
536 paragraphStyle,NSParagraphStyleAttributeName, nil];
537 NSAttributedString* alternativeString = [[NSAttributedString alloc] initWithString:fileName attributes:alternativeNametAttr];
538 result.transferedFileName.attributedAlternateTitle = alternativeString;
539 NSString* path = name;
540 NSImage* image = [self getImageForFilePath:name];
541 if (([name rangeOfString:@"/"].location == NSNotFound)) {
542 path = [self getDataTransferPath:interactionID];
543 image = [self getImageForFilePath: path];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400544 }
kkostiuk01236c42021-04-28 16:51:14 -0400545 NSFileManager *fileManager = [[NSFileManager alloc] init];
546 BOOL isDir = false;
547 fileExists = ([fileManager fileExistsAtPath: path isDirectory:&isDir] && !isDir);
548 if(image != nil && status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
549 result.transferedImage.image = image;
550 [result updateImageConstraintWithMax: MAX_TRANSFERED_IMAGE_SIZE];
551 [result.openImagebutton setAction:@selector(preview:)];
552 [result.openImagebutton setTarget:self];
553 [result.openImagebutton setHidden:NO];
554 hasImage = true;
555 } else if (fileExists) {
556 [result.openFileButton setAction:@selector(preview:)];
557 [result.openFileButton setTarget:self];
558 [result.openFileButton setHidden:NO];
559 }
560 result.onRightClick = ^(NSEvent *event) {
561 messageActionsMenu = [[NSMenu alloc] initWithTitle:@""];
562 if (fileExists) {
563 NSMenuItem* menuItem = [self menuItemWithTitle: NSLocalizedString(@"Quick Look", @"Contextual menu for message") action: @selector(quickLook:) keyEquivalent: @"" interactionId: interactionID];
564 [messageActionsMenu insertItem: menuItem atIndex: messageActionsMenu.itemArray.count];
565 menuItem = [self menuItemWithTitle: NSLocalizedString(@"Open", @"Contextual menu for message") action: @selector(openFile:) keyEquivalent: @"" interactionId: interactionID];
566 [messageActionsMenu insertItem: menuItem atIndex: messageActionsMenu.itemArray.count];
567 menuItem = [self menuItemWithTitle: NSLocalizedString(@"Show in Finder", @"Contextual menu for message") action: @selector(showInFinder:) keyEquivalent: @"" interactionId: interactionID];
568 [messageActionsMenu insertItem:menuItem atIndex: messageActionsMenu.itemArray.count];
569 if (couldResend) {
570 NSMenuItem* menuItem = [self menuItemWithTitle: NSLocalizedString(@"Resend", @"Contextual menu for message") action: @selector(resendMessage:) keyEquivalent: @"" interactionId: interactionID];
571 [messageActionsMenu insertItem: menuItem atIndex: messageActionsMenu.itemArray.count];
572 }
573 }
kkostiuk66246aa2021-05-14 14:37:53 -0400574 NSMenuItem* menuItem = [self menuItemWithTitle: NSLocalizedString(@"Delete", @"Contextual menu for message") action: @selector(deleteMessage:) keyEquivalent: @"" interactionId: interactionID];
kkostiuk01236c42021-04-28 16:51:14 -0400575 [messageActionsMenu insertItem: menuItem atIndex: messageActionsMenu.itemArray.count];
576
577 [NSMenu popUpContextMenu:messageActionsMenu withEvent: event forView: result.openFileButton];
578 };
579
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400580 [result setupForInteraction:interactionID];
581 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
582 NSString* timeString = [self timeForMessage: msgTime];
583 result.timeLabel.stringValue = timeString;
584 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
585 if (!isOutgoing) {
Kateryna Kostiukdc720842020-09-10 16:44:02 -0400586 @autoreleasepool {
587 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
588 auto* conv = [self getCurrentConversation];
589 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
590 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400591 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500592 return result;
593}
594
kkostiuk01236c42021-04-28 16:51:14 -0400595-(NSMenuItem*)menuItemWithTitle:(NSString *)title action:(nullable SEL)selector keyEquivalent:(NSString *)charCode interactionId:(uint64) interactionId {
596 NSMenuItem* menuItem = [[NSMenuItem alloc]
597 initWithTitle: title
598 action: selector
599 keyEquivalent: charCode];
600 NSImage *image=[[NSImage alloc]initWithSize:NSMakeSize(1,30)];
601 [menuItem setImage:image];
602 [menuItem setTarget:self];
603 menuItem.representedObject = [NSNumber numberWithUnsignedLongLong: interactionId];
604 return menuItem;
605}
606
607-(NSString*) getFilePath:(uint64) interId {
608 auto it = [self getCurrentConversation]->interactions.find(interId);
609 if (it == [self getCurrentConversation]->interactions.end()) {
610 return @"";
611 }
612 auto& interaction = it->second;
613 NSString* name = interaction.body.toNSString();
614 if (([name rangeOfString:@"/"].location == NSNotFound)) {
615 name = [self getDataTransferPath:interId];
616 }
617 return name;
618}
619- (void)quickLook:(id)sender {
620 NSString* name = [self getFilePath: [[sender representedObject] unsignedLongLongValue]];
621 if (name.length <= 0) {
622 return;
623 }
624 previewImage = name;
625 if ([NSApp keyWindow] != messageView.window) {
626 [NSApp activateIgnoringOtherApps:YES];
627 }
628 [self addToResponderChain];
629 if ([QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]) {
630 [[QLPreviewPanel sharedPreviewPanel] orderOut:nil];
631 } else {
632 dispatch_async(dispatch_get_main_queue(), ^{
633 [[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:self];
634 });
635 }
636}
637- (void)openFile:(id)sender {
638 NSString* name = [self getFilePath: [[sender representedObject] unsignedLongLongValue]];
639 if (name.length <= 0) {
640 return;
641 }
642 NSURL* url = [NSURL fileURLWithPath: name isDirectory: false];
643 [[NSWorkspace sharedWorkspace] openURL: url];
644}
645
646- (void)showInFinder:(id)sender {
647 NSString* name = [self getFilePath: [[sender representedObject] unsignedLongLongValue]];
648 if (name.length <= 0) {
649 return;
650 }
651 [[NSWorkspace sharedWorkspace] selectFile: name inFileViewerRootedAtPath:nil];
652}
653
654- (void)deleteMessage:(id)sender {
655 auto interId = [[sender representedObject] unsignedLongLongValue];
656 auto conv = [self getCurrentConversation];
657 auto it = conv->interactions.find(interId);
658 if (it == conv->interactions.end()) {
659 return;
660 }
661 auto itIndex = distance(conv->interactions.begin(),it);
662 if (itIndex >= ([conversationView numberOfRows] - 1) || itIndex >= conv->interactions.size()) {
663 return;
664 }
665 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex: itIndex];
666 [conversationView removeRowsAtIndexes: indexSet withAnimation: NSTableViewAnimationSlideDown];
667 [conversationView noteNumberOfRowsChanged];
668 convModel_->clearInteractionFromConversation(convUid_, interId);
669}
670
671- (void)resendMessage:(id)sender {
672 auto interId = [[sender representedObject] unsignedLongLongValue];
673 NSString* name = [self getFilePath: interId];
674 if (name.length <= 0) {
675 return;
676 }
677 convModel_->sendFile(convUid_, QString::fromNSString(name), QString::fromNSString(name.lastPathComponent));
678}
679
Anthony Léonard2382b562017-12-13 15:51:28 -0500680#pragma mark - NSTableViewDelegate methods
681- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400682{
683 return YES;
684}
685
Anthony Léonard2382b562017-12-13 15:51:28 -0500686- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400687{
Anthony Léonard2382b562017-12-13 15:51:28 -0500688 return NO;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400689}
690
Anthony Léonard2382b562017-12-13 15:51:28 -0500691- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400692{
Anthony Léonard2382b562017-12-13 15:51:28 -0500693 auto* conv = [self getCurrentConversation];
Anthony Léonard2382b562017-12-13 15:51:28 -0500694 if (conv == nil)
695 return nil;
696
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400697 IMTableCellView* result;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500698 auto it = conv->interactions.begin();
Kateryna Kostiuka1201922020-04-20 11:59:35 -0400699 auto size = [conversationView numberOfRows] - 1;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400700
701 if (row > size || row > conv->interactions.size()) {
Kateryna Kostiuk0c068552020-03-30 09:48:17 -0400702 return [[NSView alloc] init];
703 }
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400704
705 if (row == size) {
Kateryna Kostiukb39ca192020-03-27 14:01:00 -0400706 if (size < 1) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400707 return nil;
Kateryna Kostiukb39ca192020-03-27 14:01:00 -0400708 }
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400709 //last row peer composing view
710 result = [tableView makeViewWithIdentifier:@"PeerComposingMsgView" owner:conversationView];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400711 result.alphaValue = 0;
712 [result animateCompozingIndicator: NO];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400713 CGFloat alpha = peerComposingMessage ? 1 : 0;
714 CGFloat height = peerComposingMessage ? HEIGHT_FOR_COMPOSING_INDICATOR : DEFAULT_ROW_HEIGHT;
715 [result updateHeightConstraints: height];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400716 if (alpha == 1) {
717 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
718 if (peerComposingMessage) {
719 result.alphaValue = alpha;
720 [result animateCompozingIndicator: YES];
721 }
722 });
723 }
724 return result;
725 }
Anthony Léonard2382b562017-12-13 15:51:28 -0500726
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500727 std::advance(it, row);
Anthony Léonard2382b562017-12-13 15:51:28 -0500728
Kateryna Kostiuk0c068552020-03-30 09:48:17 -0400729 if (it == conv->interactions.end()) {
730 return [[NSView alloc] init];
731 }
732
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400733 auto interaction = it->second;
Anthony Léonard2382b562017-12-13 15:51:28 -0500734 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
735
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500736 switch (interaction.type) {
737 case lrc::api::interaction::Type::TEXT:
738 if (isOutgoing) {
739 result = [tableView makeViewWithIdentifier:@"RightMessageView" owner:self];
740 } else {
741 result = [tableView makeViewWithIdentifier:@"LeftMessageView" owner:self];
742 }
743 break;
Kateryna Kostiuk209a6302019-08-14 16:46:21 -0400744 case lrc::api::interaction::Type::DATA_TRANSFER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400745 return [self configureViewforTransfer:interaction interactionID: it->first tableView:tableView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500746 break;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500747 case lrc::api::interaction::Type::CONTACT:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400748 case lrc::api::interaction::Type::CALL: {
749 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
750 NSString* timeString = [self timeForMessage: msgTime];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400751 return [self makeGenericInteractionViewForTableView:tableView withText:interaction.body.toNSString() andTime:timeString];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400752 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500753 default: // If interaction is not of a known type
754 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400755 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400756 MessageSequencing sequence = [self computeSequencingFor:row];
757 BubbleType type = SINGLE;
758 if (sequence == FIRST_WITHOUT_TIME || sequence == FIRST_WITH_TIME) {
759 type = FIRST;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400760 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400761 if (sequence == MIDDLE_IN_SEQUENCE) {
762 type = MIDDLE;
763 }
764 if (sequence == LAST_IN_SEQUENCE) {
765 type = LAST;
766 }
767 result.msgBackground.type = type;
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400768 bool sendingFail = false;
769 [result.messageStatus setHidden:YES];
770 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
771 if (interaction.status == lrc::api::interaction::Status::SENDING) {
772 [result.messageStatus setHidden:NO];
773 [result.sendingMessageIndicator startAnimation:nil];
774 [result.messageFailed setHidden:YES];
Kateryna Kostiuk209a6302019-08-14 16:46:21 -0400775 } else if (interaction.status == lrc::api::interaction::Status::FAILURE) {
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400776 [result.messageStatus setHidden:NO];
777 [result.sendingMessageIndicator setHidden:YES];
778 [result.messageFailed setHidden:NO];
779 sendingFail = true;
780 }
781 }
782 [result setupForInteraction:it->first isFailed: sendingFail];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400783 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400784 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400785 [result.msgBackground setNeedsDisplay:YES];
786 [result setNeedsDisplay:YES];
787 [result.timeBox setNeedsDisplay:YES];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400788
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400789 NSString *text = interaction.body.toNSString();
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400790 text = [text removeEmptyLinesAtBorders];
791
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400792 NSMutableAttributedString* msgAttString =
Kateryna Kostiuk5a41a8d2020-05-13 09:05:56 -0400793 [[NSMutableAttributedString alloc] initWithString:text
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400794 attributes:[self messageAttributes]];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400795
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400796 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400797
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400798 [result updateMessageConstraint:messageSize.width andHeight:messageSize.height timeIsVisible:shouldDisplayTime isTopPadding: shouldApplyPadding];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400799 [[result.msgView textStorage] appendAttributedString:msgAttString];
Anthony Léonard2382b562017-12-13 15:51:28 -0500800
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400801 NSDataDetector *linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
802 NSArray *matches = [linkDetector matchesInString:result.msgView.string options:0 range:NSMakeRange(0, result.msgView.string.length)];
803
804 [result.msgView.textStorage beginEditing];
805
806 for (NSTextCheckingResult *match in matches) {
807 if (!match.URL) continue;
808
809 NSDictionary *linkAttributes = @{
810 NSLinkAttributeName: match.URL,
811 };
812 [result.msgView.textStorage addAttributes:linkAttributes range:match.range];
813 }
814
815 [result.msgView.textStorage endEditing];
816
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400817 if (shouldDisplayTime) {
818 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
819 NSString* timeString = [self timeForMessage: msgTime];
820 result.timeLabel.stringValue = timeString;
Anthony Léonard64e19672018-01-18 16:40:34 -0500821 }
822
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400823 bool shouldDisplayAvatar = (sequence != MIDDLE_IN_SEQUENCE && sequence != FIRST_WITHOUT_TIME
824 && sequence != FIRST_WITH_TIME) ? YES : NO;
825 [result.photoView setHidden:!shouldDisplayAvatar];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400826 BOOL showIndicator = convModel_->isLastDisplayed(convUid_, it->first, conv->participants.front());
827 [result.readIndicator setHidden: !showIndicator];
Kateryna Kostiukdc720842020-09-10 16:44:02 -0400828 @autoreleasepool {
829 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
830 auto image = QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)));
831 [result.readIndicator setImage:image];
832 if (!isOutgoing && shouldDisplayAvatar) {
833 [result.photoView setImage:image];
834 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400835 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400836 return result;
837}
838
Anthony Léonard2382b562017-12-13 15:51:28 -0500839- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400840{
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400841 try {
842 double someWidth = tableView.frame.size.width * 0.7;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400843
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400844 auto* conv = [self getCurrentConversation];
Anthony Léonard2382b562017-12-13 15:51:28 -0500845
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400846 if (conv == nil)
847 return HEIGHT_DEFAULT;
Kateryna Kostiuk0c068552020-03-30 09:48:17 -0400848
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400849 auto size = [conversationView numberOfRows] - 1;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400850
851 if (row > size || row > conv->interactions.size()) {
852 return HEIGHT_DEFAULT;
853 }
854 if (row == size) {
855 return peerComposingMessage ? HEIGHT_FOR_COMPOSING_INDICATOR : DEFAULT_ROW_HEIGHT;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400856 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400857
858 auto it = conv->interactions.begin();
859
860 std::advance(it, row);
861
862 if (it == conv->interactions.end()) {
863 return HEIGHT_DEFAULT;
864 }
865
866 auto interaction = it->second;
867
868 MessageSequencing sequence = [self computeSequencingFor:row];
869
870 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
871
872 if(interaction.type == lrc::api::interaction::Type::DATA_TRANSFER) {
873 if( interaction.status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
874 NSString* name = interaction.body.toNSString();
875 NSImage* image = [self getImageForFilePath:name];
876 if (([name rangeOfString:@"/"].location == NSNotFound)) {
877 image = [self getImageForFilePath:[self getDataTransferPath:it->first]];
878 }
879 if (image != nil) {
880 CGFloat widthScaleFactor = MAX_TRANSFERED_IMAGE_SIZE / image.size.width;
881 CGFloat heightScaleFactor = MAX_TRANSFERED_IMAGE_SIZE / image.size.height;
882 CGFloat heigt = 0;
883 if((widthScaleFactor >= 1) && (heightScaleFactor >= 1)) {
884 heigt = image.size.height;
885 } else {
886 CGFloat scale = MIN(widthScaleFactor, heightScaleFactor);
887 heigt = image.size.height * scale;
888 }
889 return heigt + TIME_BOX_HEIGHT;
890 }
891 }
892 return BUBBLE_HEIGHT_FOR_TRANSFERED_FILE + TIME_BOX_HEIGHT;
893 }
894
895 if(interaction.type == lrc::api::interaction::Type::CONTACT || interaction.type == lrc::api::interaction::Type::CALL)
896 return GENERIC_CELL_HEIGHT;
897
898 NSString *text = interaction.body.toNSString();
899 text = [text removeEmptyLinesAtBorders];
900
901 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
902 CGFloat singleLignMessageHeight = 15;
903
904 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
905
906 if (shouldDisplayTime) {
907 return MAX(messageSize.height + TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2,
908 TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2 + singleLignMessageHeight);
909 }
910 if(shouldApplyPadding) {
911 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2 + 15,
912 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2 + 15);
913 }
914 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2,
915 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2);
916 } catch (std::out_of_range& e) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400917 return DEFAULT_ROW_HEIGHT;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400918 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400919}
920
921#pragma mark - message view parameters
922
923-(NSString *) getDataTransferPath:(uint64_t)interactionId {
924 lrc::api::datatransfer::Info info = {};
925 convModel_->getTransferInfo(interactionId, info);
926 double convertData = static_cast<double>(info.totalSize);
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400927 return info.path.toNSString();
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400928}
929
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400930-(NSImage*) getImageForFilePath: (NSString *) path {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400931 if (path.length <= 0) {return nil;}
932 if (![[NSFileManager defaultManager] fileExistsAtPath: path]) {return nil;}
933 NSImage* transferedImage = [[NSImage alloc] initWithContentsOfFile: path];
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400934 return transferedImage;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400935}
936
937-(CGSize) sizeFor:(NSString *) message maxWidth:(CGFloat) width {
938 CGFloat horizaontalMargin = 6;
Anthony Léonard2382b562017-12-13 15:51:28 -0500939 NSMutableAttributedString* msgAttString =
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400940 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@", message]
Anthony Léonard2382b562017-12-13 15:51:28 -0500941 attributes:[self messageAttributes]];
942
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400943 CGFloat finalWidth = MIN(msgAttString.size.width + horizaontalMargin * 2, width);
944 NSRect frame = NSMakeRect(0, 0, finalWidth, msgAttString.size.height);
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400945 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400946 [[tv textStorage] setAttributedString:msgAttString];
947 [tv sizeToFit];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400948 return tv.frame.size;
949}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400950
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400951-(MessageSequencing) computeSequencingFor:(NSInteger) row {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400952 try {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400953 auto* conv = [self getCurrentConversation];
954 if (row >= conversationView.numberOfRows - 1 || row >= conv->interactions.size()) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400955 return SINGLE_WITHOUT_TIME;
956 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400957 if (conv == nil)
958 return SINGLE_WITHOUT_TIME;
959 auto it = conv->interactions.begin();
960 std::advance(it, row);
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400961 if (it == conv->interactions.end()) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400962 return SINGLE_WITHOUT_TIME;
963 }
964 auto interaction = it->second;
965 if (interaction.type != lrc::api::interaction::Type::TEXT) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400966 return SINGLE_WITH_TIME;
967 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400968 // first message in comversation
969 if (row == 0) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400970 if (it == conv->interactions.end() || conv->interactions.size() < 2) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400971 return SINGLE_WITH_TIME;
972 }
973 auto nextIt = it;
974 nextIt++;
975 if (nextIt == conv->interactions.end()) {
976 return SINGLE_WITH_TIME;
977 }
978 auto nextInteraction = nextIt->second;
979 if ([self sequenceChangedFrom:interaction to: nextInteraction]) {
980 return SINGLE_WITH_TIME;
981 }
982 return FIRST_WITH_TIME;
983 }
984 // last message in comversation
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400985 if (row == conv->interactions.size() - 1) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400986 if(it == conv->interactions.begin()) {
987 return SINGLE_WITH_TIME;
988 }
989 auto previousIt = it;
990 previousIt--;
991 auto previousInteraction = previousIt->second;
992 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
993 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
994 if (!timeChanged && !authorChanged) {
995 return LAST_IN_SEQUENCE;
996 }
997 if (!timeChanged && authorChanged) {
998 return SINGLE_WITHOUT_TIME;
999 }
Kateryna Kostiukcd9397f2020-04-13 09:40:53 -04001000 return SINGLE_WITH_TIME;
1001 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -04001002 // single message in comversation
1003 if(it == conv->interactions.begin() || it == conv->interactions.end()) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001004 return SINGLE_WITH_TIME;
1005 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -04001006 // message in the middle of conversation
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001007 auto previousIt = it;
1008 previousIt--;
1009 auto previousInteraction = previousIt->second;
Kateryna Kostiukbea05222020-05-29 11:20:33 -04001010 auto nextIt = it;
1011 nextIt++;
1012 if (nextIt == conv->interactions.end()) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001013 return SINGLE_WITHOUT_TIME;
1014 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -04001015 auto nextInteraction = nextIt->second;
1016
1017 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
1018 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
1019 bool sequenceWillChange = [self sequenceChangedFrom:interaction to: nextInteraction];
1020 if (previousInteraction.type == lrc::api::interaction::Type::DATA_TRANSFER) {
1021 if(!sequenceWillChange) {
1022 return FIRST_WITH_TIME;
1023 }
1024 return SINGLE_WITH_TIME;
1025 }
1026 if (!sequenceWillChange) {
1027 if (!timeChanged && !authorChanged) {
1028 return MIDDLE_IN_SEQUENCE;
1029 }
1030 if (timeChanged) {
1031 return FIRST_WITH_TIME;
1032 }
1033 return FIRST_WITHOUT_TIME;
1034 } if (!timeChanged && !authorChanged) {
1035 return LAST_IN_SEQUENCE;
1036 } if (timeChanged) {
1037 return SINGLE_WITH_TIME;
1038 }
1039 return SINGLE_WITHOUT_TIME;
1040 } catch (std::out_of_range& e) {
Kateryna Kostiukcd9397f2020-04-13 09:40:53 -04001041 return SINGLE_WITHOUT_TIME;
1042 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001043}
1044
1045-(bool) sequenceChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
1046 return ([self sequenceTimeChangedFrom:firstInteraction to:secondInteraction] || [self sequenceAuthorChangedFrom:firstInteraction to:secondInteraction]);
1047}
1048
1049-(bool) sequenceTimeChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
1050 bool timeChanged = NO;
1051 NSDate* firstMessageTime = [NSDate dateWithTimeIntervalSince1970:firstInteraction.timestamp];
1052 NSDate* secondMessageTime = [NSDate dateWithTimeIntervalSince1970:secondInteraction.timestamp];
1053 bool hourComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitHour];
1054 bool minutComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitMinute];
1055 if(hourComp != NSOrderedSame || minutComp != NSOrderedSame) {
1056 timeChanged = YES;
1057 }
1058 return timeChanged;
1059}
1060
1061-(bool) sequenceAuthorChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
1062 bool authorChanged = YES;
1063 bool isOutgoing = lrc::api::interaction::isOutgoing(firstInteraction);
1064 if ((secondInteraction.type == lrc::api::interaction::Type::TEXT) && (isOutgoing == lrc::api::interaction::isOutgoing(secondInteraction))) {
1065 authorChanged = NO;
1066 }
1067 return authorChanged;
1068}
1069
1070-(NSString *)timeForMessage:(NSDate*) msgTime {
1071 NSDate *today = [NSDate date];
1072 NSDateFormatter *dateFormatter=[[NSDateFormatter alloc] init];
Kateryna Kostiukaf6d5e22018-06-12 15:00:00 -04001073 [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:[[NSLocale currentLocale] localeIdentifier]]];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001074 if ([[NSCalendar currentCalendar] compareDate:today
1075 toDate:msgTime
1076 toUnitGranularity:NSCalendarUnitYear]!= NSOrderedSame) {
Kateryna Kostiuk66406432019-11-09 17:20:34 -05001077 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterLongStyle timeStyle:NSDateFormatterMediumStyle];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001078 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001079 if ([[NSCalendar currentCalendar] compareDate:today
1080 toDate:msgTime
1081 toUnitGranularity:NSCalendarUnitDay]!= NSOrderedSame ||
1082 [[NSCalendar currentCalendar] compareDate:today
1083 toDate:msgTime
1084 toUnitGranularity:NSCalendarUnitMonth]!= NSOrderedSame) {
Kateryna Kostiuk66406432019-11-09 17:20:34 -05001085 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterShortStyle];
1086 }
1087 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterNoStyle timeStyle:NSDateFormatterShortStyle];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04001088}
1089
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001090- (void) updateSendMessageHeight {
kkostiuk26f99fb2021-02-17 15:06:56 -05001091 NSAttributedString *msgAttString = messageView.attributedString;
kkostiuk153c2692021-02-23 21:30:23 -05001092 if (!msgAttString || msgAttString.length == 0) {
1093 [self resetSendMessagePanelToDefaultSize];
kkostiuk26f99fb2021-02-17 15:06:56 -05001094 return;
1095 }
1096 NSRect frame = NSMakeRect(0, 0, messageView.frame.size.width, msgAttString.size.height);
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001097 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
1098 [[tv textStorage] setAttributedString:msgAttString];
1099 [tv sizeToFit];
kkostiuk153c2692021-02-23 21:30:23 -05001100 // check the height of one line and update default line height if it does not match
1101 NSAttributedString *firstLetter = [msgAttString attributedSubstringFromRange:NSMakeRange(0, 1)];
1102 auto lineHeight = firstLetter.size.height;
kkostiuke6c33db2021-02-24 16:20:50 -05001103 // we do not want to update constraints if number of lines does not change. Save difference between actual line height and default height and use it after to check messageHeight.constant
kkostiuk153c2692021-02-23 21:30:23 -05001104 auto accuracy = abs(lineHeight - MESSAGE_VIEW_DEFAULT_HEIGHT);
kkostiuk153c2692021-02-23 21:30:23 -05001105 // top and bottom margins change for single line and multiline. MESSAGE_VIEW_DEFAULT_HEIGHT is the height of one line
kkostiuk26f99fb2021-02-17 15:06:56 -05001106 auto top = tv.frame.size.height > MESSAGE_VIEW_DEFAULT_HEIGHT ? TOP_MARGIN_MIN : TOP_MARGIN;
1107 auto bottom = tv.frame.size.height > MESSAGE_VIEW_DEFAULT_HEIGHT ? BOTTOM_MARGIN_MIN : BOTTOM_MARGIN;
1108 CGFloat heightWithMargins = tv.frame.size.height + top + bottom + SEND_PANEL_BOTTOM_MARGIN;
1109 CGFloat newSendPanelHeight = MIN(SEND_PANEL_MAX_HEIGHT, MAX(SEND_PANEL_DEFAULT_HEIGHT, heightWithMargins));
1110 CGFloat msgHeight = MAX(MESSAGE_VIEW_DEFAULT_HEIGHT, MIN(SEND_PANEL_MAX_HEIGHT - SEND_PANEL_BOTTOM_MARGIN - top, tv.frame.size.height));
kkostiuk153c2692021-02-23 21:30:23 -05001111 if (abs(messageHeight.constant - msgHeight) <= accuracy) {
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001112 return;
1113 }
kkostiuke6c33db2021-02-24 16:20:50 -05001114 if (MESSAGE_VIEW_DEFAULT_HEIGHT != lineHeight) {
1115 MESSAGE_VIEW_DEFAULT_HEIGHT = lineHeight;
1116 }
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001117 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.05 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
1118 [self scrollToBottom];
kkostiuk26f99fb2021-02-17 15:06:56 -05001119 messageHeight.constant = msgHeight;
1120 textBottomConstraint.constant = bottom;
1121 sendPanelHeight.constant = newSendPanelHeight;
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001122 });
1123}
1124
Anthony Léonard2382b562017-12-13 15:51:28 -05001125#pragma mark - NSTableViewDataSource
1126
1127- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
1128{
1129 auto* conv = [self getCurrentConversation];
1130
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -04001131 // return conversation +1 view for composing indicator
Anthony Léonardf2bb17d2018-02-15 17:18:09 -05001132 if (conv)
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -04001133 return conv->interactions.size() + 1;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -05001134 else
1135 return 0;
Anthony Léonard2382b562017-12-13 15:51:28 -05001136}
1137
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04001138#pragma mark - Text formatting
1139
Kateryna Kostiuk33089872017-07-14 16:43:59 -04001140- (NSMutableDictionary*) messageAttributes
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04001141{
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04001142 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
Kateryna Kostiukd508f752020-11-13 16:26:00 -05001143 NSFont *font = [NSFont systemFontOfSize: 12 weight: NSFontWeightLight];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001144 attrs[NSForegroundColorAttributeName] = [NSColor labelColor];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04001145 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
Kateryna Kostiukd508f752020-11-13 16:26:00 -05001146 attrs[NSFontAttributeName] = font;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04001147 return attrs;
1148}
1149
1150- (NSParagraphStyle*) paragraphStyle
1151{
1152 /*
1153 The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
1154 NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
1155 to copy, we use the default one.
1156
1157 The default values supplied by the default NSParagraphStyle are:
1158 Alignment NSNaturalTextAlignment
1159 Tab stops 12 left-aligned tabs, spaced by 28.0 points
1160 Line break mode NSLineBreakByWordWrapping
1161 All others 0.0
1162 */
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001163 NSMutableParagraphStyle* aMutableParagraphStyle =
1164 [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
1165 [aMutableParagraphStyle setHeadIndent:1.0];
1166 [aMutableParagraphStyle setFirstLineHeadIndent:1.0];
1167 return aMutableParagraphStyle;
1168}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04001169
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05001170#pragma mark - Actions
1171
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001172- (void)acceptIncomingFile:(id)sender {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -04001173 auto interId = [(IMTableCellView*)[[[[[[sender superview] superview] superview] superview] superview] superview] interaction];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05001174 auto& inter = [self getCurrentConversation]->interactions.find(interId)->second;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001175 if (convModel_ && !convUid_.isEmpty()) {
Kateryna Kostiuk26405ac2020-05-27 14:25:13 -04001176 convModel_->acceptTransfer(convUid_, interId);
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05001177 }
1178}
1179
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001180- (void)declineIncomingFile:(id)sender {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -04001181 auto inter = [(IMTableCellView*)[[[[[[sender superview] superview] superview] superview] superview] superview] interaction];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001182 if (convModel_ && !convUid_.isEmpty()) {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05001183 convModel_->cancelTransfer(convUid_, inter);
1184 }
1185}
1186
kkostiuk01236c42021-04-28 16:51:14 -04001187- (void)preview:(id)sender {
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001188 uint64_t interId;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -04001189 if ([[[[[[sender superview] superview] superview] superview] superview] isKindOfClass:[IMTableCellView class]]) {
1190 interId = [(IMTableCellView*)[[[[[sender superview] superview] superview] superview] superview] interaction];
1191 } else if ([[[[[sender superview] superview] superview] superview] isKindOfClass:[IMTableCellView class]]) {
1192 interId = [(IMTableCellView*)[[[[sender superview] superview] superview] superview] interaction];
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001193 } else {
1194 return;
1195 }
kkostiuk01236c42021-04-28 16:51:14 -04001196 NSString* name = [self getFilePath: interId];
1197 if (name.length <= 0) {
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001198 return;
1199 }
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001200 previewImage = name;
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001201 [self addToResponderChain];
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001202 if ([QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]) {
1203 [[QLPreviewPanel sharedPreviewPanel] orderOut:nil];
1204 } else {
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001205 dispatch_async(dispatch_get_main_queue(), ^{
1206 [[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:self];
1207 });
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001208 }
1209}
1210
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001211- (IBAction)sendMessage:(id)sender {
1212 NSString* text = self.message;
Kateryna Kostiuka8b6b562019-02-01 13:26:18 -05001213 unichar separatorChar = NSLineSeparatorCharacter;
1214 NSString *separatorString = [NSString stringWithCharacters:&separatorChar length:1];
1215 text = [text stringByReplacingOccurrencesOfString: separatorString withString: @"\n"];
kkostiukc48e73e2021-04-13 09:20:08 -04001216 // send files
1217 NSMutableArray* files = [MessagesVC pendingFiles][convUid_.toNSString()];
1218 for (PendingFile* file : files) {
1219 convModel_->sendFile(convUid_, QString::fromNSString(file.fileUrl.path), QString::fromNSString(file.name));
1220 }
1221 [MessagesVC.pendingFiles removeObjectForKey: convUid_.toNSString()];
1222 [self reloadPendingFiles];
1223
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001224 if (text && text.length > 0) {
1225 auto* conv = [self getCurrentConversation];
Kateryna Kostiuk0c068552020-03-30 09:48:17 -04001226 if (conv == nil)
1227 return;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001228 convModel_->sendMessage(convUid_, QString::fromNSString(text));
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001229 self.message = @"";
kkostiuk26f99fb2021-02-17 15:06:56 -05001230 [self resetSendMessagePanelToDefaultSize];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -04001231 if (composingMessage) {
1232 composingMessage = false;
1233 convModel_->setIsComposing(convUid_, composingMessage);
1234 }
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001235 }
1236}
1237
kkostiuk26f99fb2021-02-17 15:06:56 -05001238-(void) resetSendMessagePanelToDefaultSize {
1239 if(messageHeight.constant != MESSAGE_VIEW_DEFAULT_HEIGHT) {
1240 sendPanelHeight.constant = SEND_PANEL_DEFAULT_HEIGHT;
1241 messageHeight.constant = MESSAGE_VIEW_DEFAULT_HEIGHT;
kkostiuk26f99fb2021-02-17 15:06:56 -05001242 textBottomConstraint.constant = BOTTOM_MARGIN;
1243 [self scrollToBottom];
1244 }
1245}
1246
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001247- (IBAction)openEmojy:(id)sender {
kkostiuk26f99fb2021-02-17 15:06:56 -05001248 [messageView.window makeFirstResponder: messageView];
1249 [NSApp orderFrontCharacterPalette: messageView];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001250}
1251
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001252- (IBAction)startVideoMessage:(id)sender
1253{
1254 [self startRecording:NO];
1255}
1256
1257- (IBAction)startAudioMessage:(id)sender
1258{
1259 [self startRecording:YES];
1260}
1261-(void) startRecording:(BOOL)isAudio {
1262 if (recordingController == nil) {
1263 recordingController = [[RecordFileVC alloc] initWithNibName:@"RecordFileVC" bundle:nil avModel: self->avModel];
1264 recordingController.delegate = self;
1265 }
1266 if(recordMessagePopover != nil)
1267 {
1268 [self closeRecordingView];
1269 return;
1270 }
1271 recordMessagePopover = [[NSPopover alloc] init];
1272 [recordingController prepareRecordingView: isAudio];
1273 [recordMessagePopover setContentSize: recordingController.view.frame.size];
1274 [recordMessagePopover setContentViewController:recordingController];
1275 [recordMessagePopover setAnimates:YES];
1276 NSButton *anchorButton = isAudio ? recordAudioButton : recordVideoButton;
1277 [recordMessagePopover showRelativeToRect: anchorButton.bounds
1278 ofView: anchorButton
1279 preferredEdge: NSMaxYEdge];
1280}
1281
1282-(void) sendFile:(NSString *) name withFilePath:(NSString *) path {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001283 convModel_->sendFile(convUid_, QString::fromNSString(path), QString::fromNSString(name));
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001284}
1285
1286-(void) closeRecordingView {
1287 if(recordMessagePopover != nil) {
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001288 [recordMessagePopover close];
1289 recordMessagePopover = nil;
Kateryna Kostiuka7404812019-10-28 12:24:46 -04001290 recordingController.stopRecordingView;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001291 }
1292}
1293
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001294- (IBAction)sendFile:(id)sender {
1295 NSOpenPanel* filePicker = [NSOpenPanel openPanel];
1296 [filePicker setCanChooseFiles:YES];
1297 [filePicker setCanChooseDirectories:NO];
kkostiukc48e73e2021-04-13 09:20:08 -04001298 [filePicker setAllowsMultipleSelection:YES];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001299
1300 if ([filePicker runModal] == NSFileHandlingPanelOKButton) {
kkostiukc48e73e2021-04-13 09:20:08 -04001301 [self filesDragged: [filePicker URLs]];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001302 }
1303}
1304
1305
kkostiuk26f99fb2021-02-17 15:06:56 -05001306#pragma mark - NSTextViewDelegate
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001307
kkostiuk26f99fb2021-02-17 15:06:56 -05001308- (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector {
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001309 if (commandSelector == @selector(insertNewline:)) {
kkostiukc48e73e2021-04-13 09:20:08 -04001310 if(self.message.length > 0 || [(NSMutableArray*)MessagesVC. pendingFiles[convUid_.toNSString()] count] > 0) {
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001311 [self sendMessage: nil];
kkostiuk26f99fb2021-02-17 15:06:56 -05001312 return YES;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001313 }
kkostiuk26f99fb2021-02-17 15:06:56 -05001314 [self resetSendMessagePanelToDefaultSize];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001315 return YES;
1316 }
1317 return NO;
1318}
1319
kkostiukc48e73e2021-04-13 09:20:08 -04001320-(void)textDidChange:(NSNotification *)notification {
kkostiuk26f99fb2021-02-17 15:06:56 -05001321 [self checkIfComposingMsg];
kkostiukc48e73e2021-04-13 09:20:08 -04001322 self.enableSendButton = self.message.length > 0 || [(NSMutableArray*)MessagesVC. pendingFiles[convUid_.toNSString()] count] > 0;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001323}
1324
kkostiuk26f99fb2021-02-17 15:06:56 -05001325- (void) checkIfComposingMsg {
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -04001326 [self updateSendMessageHeight];
kkostiuk26f99fb2021-02-17 15:06:56 -05001327 BOOL haveText = [messageView.string removeEmptyLinesAtBorders].length != 0;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -04001328 if (haveText != composingMessage) {
1329 composingMessage = haveText;
1330 convModel_->setIsComposing(convUid_, composingMessage);
1331 }
1332}
1333
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001334#pragma mark - QLPreviewPanelDataSource
1335
1336-(void)beginPreviewPanelControl:(QLPreviewPanel *)panel
1337{
1338 panel.dataSource = self;
1339}
1340
1341- (void)endPreviewPanelControl:(QLPreviewPanel *)panel {
1342 panel.dataSource = nil;
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001343 [self removeFromResponderChain];
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001344}
1345
1346-(BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel
1347{
1348 return YES;
1349}
1350
1351- (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel *)panel {
1352 return 1;
1353}
1354
1355- (id <QLPreviewItem>)previewPanel:(QLPreviewPanel *)panel previewItemAtIndex:(NSInteger)index {
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001356 if (previewImage == nil) {
1357 return nil;
1358 }
Kateryna Kostiuk30c6ac22020-05-06 17:42:59 -04001359 try {
1360 return [NSURL fileURLWithPath: previewImage];
1361 } catch (NSException *exception) {
kkostiuk01236c42021-04-28 16:51:14 -04001362 return nil;
Kateryna Kostiuk30c6ac22020-05-06 17:42:59 -04001363 }
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001364}
1365
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001366- (void)addToResponderChain {
1367 if (conversationView.window &&
1368 ![[conversationView.window nextResponder] isEqual:self]) {
1369 NSResponder * aNextResponder = [conversationView.window nextResponder];
1370 [conversationView.window setNextResponder:self];
1371 }
1372}
1373
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001374- (void)removeFromResponderChain {
1375 if (conversationView.window &&
1376 [[conversationView.window nextResponder] isEqual:self]) {
1377 NSResponder * aNextResponder = [conversationView.window nextResponder];
1378 [conversationView.window setNextResponder:[self nextResponder]];
1379 }
1380}
1381
kkostiukc48e73e2021-04-13 09:20:08 -04001382#pragma mark - NSCollectionViewDataSource
1383
1384- (NSInteger)collectionView:(NSCollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
1385 return [(NSMutableArray*)MessagesVC.pendingFiles[convUid_.toNSString()] count];
1386}
1387- (NSCollectionViewItem*)collectionView:(NSCollectionView *)collectionView itemForRepresentedObjectAtIndexPath:(NSIndexPath *)indexPath {
1388 FileToSendCollectionItem* fileCell = [collectionView makeItemWithIdentifier:@"FileToSendCollectionItem" forIndexPath:indexPath];
1389 PendingFile* file = MessagesVC.pendingFiles[convUid_.toNSString()][indexPath.item];
kkostiuke5d4abe2021-04-21 21:13:49 -04001390 fileCell.placeholderPreview.hidden = file.preview != nil;
kkostiukc48e73e2021-04-13 09:20:08 -04001391 fileCell.filePreview.image = file.preview;
1392 fileCell.fileName.stringValue = file.name;
1393 fileCell.fileName.toolTip = file.name;
1394 fileCell.fileSize.stringValue = file.size;
1395 [fileCell.closeButton setAction:@selector(removePendingFile:)];
1396 fileCell.closeButton.tag = indexPath.item;
1397 [fileCell.closeButton setTarget:self];
1398 return fileCell;
1399}
1400
1401#pragma mark - DraggingDestinationDelegate
1402
1403-(void)filesDragged:(NSArray*)urls {
1404 [self prepareFilesToSend: urls];
1405}
1406
1407-(NSString*)convertBytedToString:(double)bytes {
1408 if (bytes <= 1000) {
1409 return [NSString stringWithFormat:@"%.2f%@", bytes, @" B"];
1410 } else if (bytes <= 1e6) {
1411 return [NSString stringWithFormat:@"%.2f%@",(bytes * 1e-3), @" KB"];
1412 } else if (bytes <= 1e9) {
1413 return [NSString stringWithFormat:@"%.2f%@",(bytes * 1e-6), @" MB"];
1414 }
1415 return [NSString stringWithFormat:@"%.2f%@",(bytes * 1e-9), @" GB"];
1416}
1417
1418-(void)prepareFilesToSend:(NSArray*)urls {
1419 NSMutableArray* files = [[NSMutableArray alloc] init];
1420 NSMutableArray* existingFiles = MessagesVC.pendingFiles[convUid_.toNSString()];
1421 [files addObjectsFromArray: existingFiles];
1422 for (NSURL* url : urls) {
1423 NSString* filePath = [url path];
1424 NSImage* preview = [[NSImage alloc] initWithContentsOfFile: filePath];
1425 NSString* name = [url lastPathComponent];
1426 NSData* documentBytes = [[NSData alloc] initWithContentsOfFile: filePath];
1427 PendingFile* file = [[PendingFile alloc] init];
1428 file.name = name;
1429 file.size = [self convertBytedToString: documentBytes.length];
1430 file.preview = preview;
1431 file.fileUrl = url;
1432 [files addObject: file];
1433 }
1434 MessagesVC.pendingFiles[convUid_.toNSString()] = files;
1435 [self reloadPendingFiles];
1436 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
1437 [self scrollToBottom];
1438 });
1439}
1440
1441- (void)removePendingFile:(id)sender {
1442 NSButton* closeButton = (NSButton*)sender;
1443 NSInteger index = closeButton.tag;
1444 if(index < 0) {
1445 return;
1446 }
1447 [MessagesVC.pendingFiles[convUid_.toNSString()] removeObjectAtIndex:index];
1448 [self reloadPendingFiles];
1449}
1450
1451- (void)reloadPendingFiles {
1452 self.hideFilesCollection = [(NSMutableArray*)MessagesVC .pendingFiles[convUid_.toNSString()] count]== 0;
1453 self.enableSendButton = self.message.length > 0 || [(NSMutableArray*)MessagesVC. pendingFiles[convUid_.toNSString()] count] > 0;
1454 [pendingFilesCollectionView reloadData];
1455}
1456
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04001457@end