blob: 9258027879b71f450446c9e921a3185175c98242 [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 Kostiukfbe1b2f2019-10-07 17:32:26 -040039#import <AVFoundation/AVFoundation.h>
40
41#import "RecordFileVC.h"
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040042
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040043
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040044@interface MessagesVC () <NSTableViewDelegate, NSTableViewDataSource, QLPreviewPanelDataSource> {
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040045
Anthony Léonard2382b562017-12-13 15:51:28 -050046 __unsafe_unretained IBOutlet NSTableView* conversationView;
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040047 __unsafe_unretained IBOutlet NSView* containerView;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050048 __unsafe_unretained IBOutlet NSTextField* messageField;
49 __unsafe_unretained IBOutlet IconButton *sendFileButton;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -040050 __unsafe_unretained IBOutlet IconButton *recordVideoButton;
51 __unsafe_unretained IBOutlet IconButton *recordAudioButton;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050052 __unsafe_unretained IBOutlet NSLayoutConstraint* sendPanelHeight;
53 __unsafe_unretained IBOutlet NSLayoutConstraint* messagesBottomMargin;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -040054 IBOutlet NSPopover *recordMessagePopover;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040055
Kateryna Kostiukc867eb92020-03-08 13:15:17 -040056 QString convUid_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050057 lrc::api::ConversationModel* convModel_;
Anthony Léonard2382b562017-12-13 15:51:28 -050058 const lrc::api::conversation::Info* cachedConv_;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -040059 lrc::api::AVModel* avModel;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050060 QMetaObject::Connection newInteractionSignal_;
Anthony Léonard2382b562017-12-13 15:51:28 -050061
62 // Both are needed to invalidate cached conversation as pointer
63 // may not be referencing the same conversation anymore
64 QMetaObject::Connection modelSortedSignal_;
65 QMetaObject::Connection filterChangedSignal_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050066 QMetaObject::Connection interactionStatusUpdatedSignal_;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -040067 QMetaObject::Connection peerComposingMsgSignal_;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -040068 QMetaObject::Connection lastDisplayedChanged_;
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040069 NSString* previewImage;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050070 NSMutableDictionary *pendingMessagesToSend;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -040071 RecordFileVC * recordingController;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040072}
73
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040074@end
75
Anthony Léonardf2bb17d2018-02-15 17:18:09 -050076// Tags for view
77NSInteger const GENERIC_INT_TEXT_TAG = 100;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040078NSInteger const GENERIC_INT_TIME_TAG = 200;
79
80// views size
81CGFloat const GENERIC_CELL_HEIGHT = 60;
82CGFloat const TIME_BOX_HEIGHT = 34;
83CGFloat const MESSAGE_TEXT_PADDING = 10;
84CGFloat const MAX_TRANSFERED_IMAGE_SIZE = 250;
85CGFloat const BUBBLE_HEIGHT_FOR_TRANSFERED_FILE = 87;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -040086CGFloat const DEFAULT_ROW_HEIGHT = 10;
87CGFloat const HEIGHT_FOR_COMPOSING_INDICATOR = 46;
Kateryna Kostiuka1201922020-04-20 11:59:35 -040088CGFloat const HEIGHT_DEFAULT = 34;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050089NSInteger const MEESAGE_MARGIN = 21;
90NSInteger const SEND_PANEL_DEFAULT_HEIGHT = 60;
91NSInteger const SEND_PANEL_MAX_HEIGHT = 120;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -050092
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -040093BOOL peerComposingMessage = false;
94BOOL composingMessage = false;
95
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040096@implementation MessagesVC
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040097
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040098
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040099//MessageBuble type
100typedef NS_ENUM(NSInteger, MessageSequencing) {
101 SINGLE_WITH_TIME = 0,
102 SINGLE_WITHOUT_TIME = 1,
103 FIRST_WITH_TIME = 2,
104 FIRST_WITHOUT_TIME = 3,
105 MIDDLE_IN_SEQUENCE = 5,
106 LAST_IN_SEQUENCE = 6,
107};
108
109- (void)awakeFromNib
110{
111 NSNib *cellNib = [[NSNib alloc] initWithNibNamed:@"MessageCells" bundle:nil];
112 [conversationView registerNib:cellNib forIdentifier:@"LeftIncomingFileView"];
113 [conversationView registerNib:cellNib forIdentifier:@"LeftOngoingFileView"];
114 [conversationView registerNib:cellNib forIdentifier:@"LeftFinishedFileView"];
115 [conversationView registerNib:cellNib forIdentifier:@"RightOngoingFileView"];
116 [conversationView registerNib:cellNib forIdentifier:@"RightFinishedFileView"];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400117 [conversationView registerNib:cellNib forIdentifier:@"PeerComposingMsgView"];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500118 [[conversationView.enclosingScrollView contentView] setCopiesOnScroll:NO];
119 [messageField setFocusRingType:NSFocusRingTypeNone];
120 [conversationView setWantsLayer:YES];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400121}
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500122
123- (instancetype)initWithCoder:(NSCoder *)coder
124{
125 self = [super initWithCoder:coder];
126 if (self) {
127 pendingMessagesToSend = [[NSMutableDictionary alloc] init];
128 }
129 return self;
130}
131
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -0400132-(void) setAVModel: (lrc::api::AVModel*) avmodel {
133 avModel = avmodel;
134 if (recordingController == nil) {
135 recordingController = [[RecordFileVC alloc] initWithNibName:@"RecordFileVC" bundle:nil avModel: self->avModel];
136 recordingController.delegate = self;
137 }
138}
139
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500140- (void)setMessage:(NSString *)newValue {
141 _message = [newValue removeEmptyLinesAtBorders];
142}
143
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400144-(void) clearData {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400145 if (!convUid_.isEmpty()) {
146 pendingMessagesToSend[convUid_.toNSString()] = messageField.stringValue;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500147 }
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400148 cachedConv_ = nil;
149 convUid_ = "";
150 convModel_ = nil;
151
152 QObject::disconnect(modelSortedSignal_);
153 QObject::disconnect(filterChangedSignal_);
154 QObject::disconnect(interactionStatusUpdatedSignal_);
155 QObject::disconnect(newInteractionSignal_);
Kateryna Kostiuk5acaefd2020-03-25 11:14:25 -0400156 QObject::disconnect(peerComposingMsgSignal_);
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400157 QObject::disconnect(lastDisplayedChanged_);
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -0400158 [self closeRecordingView];
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400159}
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400160
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500161-(void) scrollToBottom {
162 CGRect visibleRect = [conversationView enclosingScrollView].contentView.visibleRect;
163 NSRange range = [conversationView rowsInRect:visibleRect];
164 NSIndexSet* visibleIndexes = [NSIndexSet indexSetWithIndexesInRange:range];
165 NSUInteger lastvisibleRow = [visibleIndexes lastIndex];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400166 NSInteger numberOfRows = [conversationView numberOfRows];
167 if ((numberOfRows > 0) &&
168 lastvisibleRow > (numberOfRows - 5)) {
169 [conversationView scrollRowToVisible:numberOfRows - 1];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500170 }
171}
172
Anthony Léonard2382b562017-12-13 15:51:28 -0500173-(const lrc::api::conversation::Info*) getCurrentConversation
174{
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400175 if (convModel_ == nil || convUid_.isEmpty())
Anthony Léonard2382b562017-12-13 15:51:28 -0500176 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400177
Anthony Léonard2382b562017-12-13 15:51:28 -0500178 if (cachedConv_ != nil)
179 return cachedConv_;
Kateryna Kostiuk146cb9e2020-08-11 14:29:26 -0400180 auto it = getConversationFromUid(convUid_, *convModel_, true);
181 if (conversationExists(it, *convModel_, true))
Anthony Léonard2382b562017-12-13 15:51:28 -0500182 cachedConv_ = &(*it);
183
184 return cachedConv_;
185}
186
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400187-(void) reloadConversationForMessage:(uint64_t) uid updateSize:(BOOL) update {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400188 auto* conv = [self getCurrentConversation];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400189 if (conv == nil)
190 return;
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400191 auto it = conv->interactions.find(uid);
192 if (it == conv->interactions.end()) {
193 return;
194 }
195 auto itIndex = distance(conv->interactions.begin(),it);
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400196 if (itIndex >= ([conversationView numberOfRows] - 1) || itIndex >= conv->interactions.size()) {
Kateryna Kostiuka1201922020-04-20 11:59:35 -0400197 return;
198 }
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400199 NSRange rangeToUpdate = NSMakeRange(itIndex, 2);
200 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndexesInRange:rangeToUpdate];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400201 //reload previous message to update bubbleview
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400202 if (itIndex > 0) {
203 auto previousIt = it;
204 previousIt--;
205 auto previousInteraction = previousIt->second;
206 if (previousInteraction.type == lrc::api::interaction::Type::TEXT) {
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400207 NSRange range = NSMakeRange(itIndex - 1, 3);
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400208 indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
209 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400210 }
211 if (update) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400212 NSRange insertRange = NSMakeRange(itIndex, 1);
213 NSIndexSet* insertRangeSet = [NSIndexSet indexSetWithIndexesInRange:insertRange];
214 [conversationView removeRowsAtIndexes:insertRangeSet withAnimation:(NSTableViewAnimationEffectNone)];
215 [conversationView insertRowsAtIndexes:insertRangeSet withAnimation:(NSTableViewAnimationEffectNone)];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400216 }
217 [conversationView reloadDataForRowIndexes: indexSet
218 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400219 [self scrollToBottom];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400220}
221
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400222-(void)setConversationUid:(const QString&)convUid model:(lrc::api::ConversationModel *)model
Anthony Léonard2382b562017-12-13 15:51:28 -0500223{
224 if (convUid_ == convUid && convModel_ == model)
225 return;
226
227 cachedConv_ = nil;
228 convUid_ = convUid;
229 convModel_ = model;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400230 peerComposingMessage = false;
231 composingMessage = false;
Anthony Léonard2382b562017-12-13 15:51:28 -0500232
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500233 // Signal triggered when messages are received or their status updated
234 QObject::disconnect(newInteractionSignal_);
235 QObject::disconnect(interactionStatusUpdatedSignal_);
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400236 QObject::disconnect(peerComposingMsgSignal_);
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400237 QObject::disconnect(lastDisplayedChanged_);
238 lastDisplayedChanged_ =
239 QObject::connect(convModel_,
240 &lrc::api::ConversationModel::displayedInteractionChanged,
241 [self](const QString &uid,
242 const QString &participantURI,
243 const uint64_t &previousUid,
244 const uint64_t &newdUid) {
245 if (uid != convUid_)
246 return;
247 [self reloadConversationForMessage:newdUid updateSize: NO];
248 [self reloadConversationForMessage:previousUid updateSize: NO];
249 });
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400250
251 peerComposingMsgSignal_ = QObject::connect(convModel_,
252 &lrc::api::ConversationModel::composingStatusChanged,
253 [self](const QString &uid,
254 const QString &contactUri,
255 bool isComposing) {
256 if (uid != convUid_)
257 return;
258 bool shouldUpdate = isComposing != peerComposingMessage;
259 if (!shouldUpdate) {
260 return;
261 }
262 // reload and update height for composing indicator
263 peerComposingMessage = isComposing;
264 auto* conv = [self getCurrentConversation];
265 if (conv == nil)
266 return;
267 auto row = [conversationView numberOfRows] - 1;
268 if (row < 0) {
269 return;
270 }
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400271 if(peerComposingMessage) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400272 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:row];
273 [conversationView reloadDataForRowIndexes: indexSet
274 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400275 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400276 [self scrollToBottom];
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400277 } else {
278 //whait for possible incoming message to avoid view jumping
279 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400280 auto row = [conversationView numberOfRows] - 1;
281 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:row];
282 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
283 [conversationView reloadDataForRowIndexes: indexSet
284 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
285 [self scrollToBottom];
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400286 });
287 }
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400288 });
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500289 newInteractionSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newInteraction,
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400290 [self](const QString& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400291 if (uid != convUid_)
292 return;
293 cachedConv_ = nil;
294 peerComposingMessage = false;
295 [conversationView noteNumberOfRowsChanged];
296 [self reloadConversationForMessage:interactionId updateSize: YES];
297 [self scrollToBottom];
298 });
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500299 interactionStatusUpdatedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::interactionStatusUpdated,
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400300 [self](const QString& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400301 if (uid != convUid_)
302 return;
303 cachedConv_ = nil;
304 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
305 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
306 convModel_->refreshFilter();
307 }
308 [self reloadConversationForMessage:interactionId updateSize: interaction.type == lrc::api::interaction::Type::DATA_TRANSFER];
309 [self scrollToBottom];
310 });
Anthony Léonard2382b562017-12-13 15:51:28 -0500311
312 // Signals tracking changes in conversation list, we need them as cached conversation can be invalid
313 // after a reordering.
314 QObject::disconnect(modelSortedSignal_);
315 QObject::disconnect(filterChangedSignal_);
316 modelSortedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::modelSorted,
317 [self](){
318 cachedConv_ = nil;
319 });
320 filterChangedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::filterChanged,
321 [self](){
322 cachedConv_ = nil;
323 });
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400324 if (pendingMessagesToSend[convUid_.toNSString()]) {
325 self.message = pendingMessagesToSend[convUid_.toNSString()];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500326 [self updateSendMessageHeight];
327 } else {
328 self.message = @"";
329 if(messagesBottomMargin.constant != SEND_PANEL_DEFAULT_HEIGHT) {
330 sendPanelHeight.constant = SEND_PANEL_DEFAULT_HEIGHT;
331 messagesBottomMargin.constant = SEND_PANEL_DEFAULT_HEIGHT;
332 [self scrollToBottom];
333 }
334 }
335 conversationView.alphaValue = 0.0;
Anthony Léonard2382b562017-12-13 15:51:28 -0500336 [conversationView reloadData];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400337 [conversationView scrollToEndOfDocument:nil];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500338 CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:@"opacity"];
339 fadeIn.fromValue = [NSNumber numberWithFloat:0.0];
340 fadeIn.toValue = [NSNumber numberWithFloat:1.0];
341 fadeIn.duration = 0.4f;
342
343 [conversationView.layer addAnimation:fadeIn forKey:fadeIn.keyPath];
344 conversationView.alphaValue = 1;
345 auto* conv = [self getCurrentConversation];
346
347 if (conv == nil)
348 return;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400349 try {
350 [sendFileButton setEnabled:(convModel_->owner.contactModel->getContact(conv->participants[0]).profileInfo.type != lrc::api::profile::Type::SIP)];
351 } catch (std::out_of_range& e) {
352 NSLog(@"contact out of range");
353 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400354}
355
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400356#pragma mark - configure cells
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400357
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400358-(NSTableCellView*) makeGenericInteractionViewForTableView:(NSTableView*)tableView withText:(NSString*)text andTime:(NSString*) time
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500359{
360 NSTableCellView* result = [tableView makeViewWithIdentifier:@"GenericInteractionView" owner:self];
361 NSTextField* textField = [result viewWithTag:GENERIC_INT_TEXT_TAG];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400362 NSTextField* timeField = [result viewWithTag:GENERIC_INT_TIME_TAG];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500363
364 // TODO: Fix symbol in LRC
Kateryna Kostiuk66406432019-11-09 17:20:34 -0500365 NSString* fixedString = [[text stringByReplacingOccurrencesOfString:@"🕽" withString:@""] stringByReplacingOccurrencesOfString:@"📞" withString:@""];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500366 [textField setStringValue:fixedString];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400367 [timeField setStringValue:time];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500368
369 return result;
370}
371
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400372-(NSTableCellView*) configureViewforTransfer:(lrc::api::interaction::Info)interaction interactionID: (uint64_t) interactionID tableView:(NSTableView*)tableView
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500373{
374 IMTableCellView* result;
375
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400376 auto type = interaction.type;
377 auto status = interaction.status;
378
379 NSString* fileName = @"incoming file";
380
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500381 // First, view is created
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400382 if (!interaction.authorUri.isEmpty()) {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500383 switch (status) {
384 case lrc::api::interaction::Status::TRANSFER_CREATED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400385 case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST: {
386 result = [tableView makeViewWithIdentifier:@"LeftIncomingFileView" owner: conversationView];
387 [result.acceptButton setAction:@selector(acceptIncomingFile:)];
388 [result.acceptButton setTarget:self];
389 [result.declineButton setAction:@selector(declineIncomingFile:)];
390 [result.declineButton setTarget:self];
391 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500392 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400393 case lrc::api::interaction::Status::TRANSFER_ONGOING: {
394 result = [tableView makeViewWithIdentifier:@"LeftOngoingFileView" owner:conversationView];
395 [result.progressIndicator startAnimation:conversationView];
396 [result.declineButton setAction:@selector(declineIncomingFile:)];
397 [result.declineButton setTarget:self];
398 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500399 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400400 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
401 [result.transferedFileName setAction:@selector(imagePreview:)];
402 [result.transferedFileName setTarget:self];
403 [result.transferedFileName.cell setHighlightsBy:NSContentsCellMask];
404 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500405 case lrc::api::interaction::Status::TRANSFER_CANCELED:
406 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400407 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
408 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500409 }
Kateryna Kostiuk209a6302019-08-14 16:46:21 -0400410 } else {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400411 NSString* fileName = @"sent file";
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500412 switch (status) {
413 case lrc::api::interaction::Status::TRANSFER_CREATED:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500414 case lrc::api::interaction::Status::TRANSFER_ONGOING:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400415 case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500416 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400417 result = [tableView makeViewWithIdentifier:@"RightOngoingFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500418 [result.progressIndicator startAnimation:nil];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400419 [result.declineButton setAction:@selector(declineIncomingFile:)];
420 [result.declineButton setTarget:self];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500421 break;
422 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400423 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
424 [result.transferedFileName setAction:@selector(imagePreview:)];
425 [result.transferedFileName setTarget:self];
426 [result.transferedFileName.cell setHighlightsBy:NSContentsCellMask];
427 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500428 case lrc::api::interaction::Status::TRANSFER_CANCELED:
429 case lrc::api::interaction::Status::TRANSFER_ERROR:
Olivier Soldanoe521a182018-02-26 16:55:19 -0500430 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400431 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500432 }
433 }
434
435 // Then status label is updated if needed
436 switch (status) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400437 [result.statusLabel setTextColor:[NSColor textColor]];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500438 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400439 [result.statusLabel setTextColor:[NSColor greenSuccessColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500440 [result.statusLabel setStringValue:NSLocalizedString(@"Success", @"File transfer successful label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500441 break;
442 case lrc::api::interaction::Status::TRANSFER_CANCELED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400443 [result.statusLabel setTextColor:[NSColor orangeColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500444 [result.statusLabel setStringValue:NSLocalizedString(@"Canceled", @"File transfer canceled label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500445 break;
446 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400447 [result.statusLabel setTextColor:[NSColor errorTransferColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500448 [result.statusLabel setStringValue:NSLocalizedString(@"Failed", @"File transfer failed label")];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500449 break;
450 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400451 [result.statusLabel setTextColor:[NSColor textColor]];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500452 [result.statusLabel setStringValue:NSLocalizedString(@"Unjoinable", @"File transfer peer unjoinable label")];
453 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500454 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400455 result.transferedImage.image = nil;
Kateryna Kostiukeaf1bc82018-10-12 14:33:50 -0400456 [result.openImagebutton setHidden:YES];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400457 [result.msgBackground setHidden:NO];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400458 NSString* name = interaction.body.toNSString();
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400459 if (name.length > 0) {
Kateryna Kostiuk67735232018-05-10 15:05:32 -0400460 fileName = [name lastPathComponent];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400461 }
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400462 NSFont *nameFont = [NSFont userFontOfSize:14.0];
463 NSColor *nameColor = [NSColor textColor];
464 NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
465 paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
466 paragraphStyle.alignment = NSTextAlignmentLeft;
467 NSDictionary *nameAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
468 nameColor,NSForegroundColorAttributeName,
469 paragraphStyle,NSParagraphStyleAttributeName, nil];
470 NSAttributedString* nameAttributedString = [[NSAttributedString alloc] initWithString:fileName attributes:nameAttr];
471 result.transferedFileName.attributedTitle = nameAttributedString;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400472 if (status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400473 NSColor *higlightColor = [NSColor grayColor];
474 NSDictionary *alternativeNametAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
475 higlightColor,NSForegroundColorAttributeName,
476 paragraphStyle,NSParagraphStyleAttributeName, nil];
477 NSAttributedString* alternativeString = [[NSAttributedString alloc] initWithString:fileName attributes:alternativeNametAttr];
478 result.transferedFileName.attributedAlternateTitle = alternativeString;
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400479 NSImage* image = [self getImageForFilePath:name];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400480 if (([name rangeOfString:@"/"].location == NSNotFound)) {
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400481 image = [self getImageForFilePath:[self getDataTransferPath:interactionID]];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400482 }
483 if(image != nil) {
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400484 result.transferedImage.image = image;
485 [result updateImageConstraintWithMax: MAX_TRANSFERED_IMAGE_SIZE];
486 [result.openImagebutton setAction:@selector(imagePreview:)];
487 [result.openImagebutton setTarget:self];
Kateryna Kostiukeaf1bc82018-10-12 14:33:50 -0400488 [result.openImagebutton setHidden:NO];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400489 }
490 }
491 [result setupForInteraction:interactionID];
492 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
493 NSString* timeString = [self timeForMessage: msgTime];
494 result.timeLabel.stringValue = timeString;
495 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
496 if (!isOutgoing) {
497 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
498 auto* conv = [self getCurrentConversation];
499 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
500 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500501 return result;
502}
503
Anthony Léonard2382b562017-12-13 15:51:28 -0500504#pragma mark - NSTableViewDelegate methods
505- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400506{
507 return YES;
508}
509
Anthony Léonard2382b562017-12-13 15:51:28 -0500510- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400511{
Anthony Léonard2382b562017-12-13 15:51:28 -0500512 return NO;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400513}
514
Anthony Léonard2382b562017-12-13 15:51:28 -0500515- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400516{
Anthony Léonard2382b562017-12-13 15:51:28 -0500517 auto* conv = [self getCurrentConversation];
Anthony Léonard2382b562017-12-13 15:51:28 -0500518 if (conv == nil)
519 return nil;
520
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400521 IMTableCellView* result;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500522 auto it = conv->interactions.begin();
Kateryna Kostiuka1201922020-04-20 11:59:35 -0400523 auto size = [conversationView numberOfRows] - 1;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400524
525 if (row > size || row > conv->interactions.size()) {
Kateryna Kostiuk0c068552020-03-30 09:48:17 -0400526 return [[NSView alloc] init];
527 }
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400528
529 if (row == size) {
Kateryna Kostiukb39ca192020-03-27 14:01:00 -0400530 if (size < 1) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400531 return nil;
Kateryna Kostiukb39ca192020-03-27 14:01:00 -0400532 }
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400533 //last row peer composing view
534 result = [tableView makeViewWithIdentifier:@"PeerComposingMsgView" owner:conversationView];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400535 result.alphaValue = 0;
536 [result animateCompozingIndicator: NO];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400537 CGFloat alpha = peerComposingMessage ? 1 : 0;
538 CGFloat height = peerComposingMessage ? HEIGHT_FOR_COMPOSING_INDICATOR : DEFAULT_ROW_HEIGHT;
539 [result updateHeightConstraints: height];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400540 if (alpha == 1) {
541 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
542 if (peerComposingMessage) {
543 result.alphaValue = alpha;
544 [result animateCompozingIndicator: YES];
545 }
546 });
547 }
548 return result;
549 }
Anthony Léonard2382b562017-12-13 15:51:28 -0500550
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500551 std::advance(it, row);
Anthony Léonard2382b562017-12-13 15:51:28 -0500552
Kateryna Kostiuk0c068552020-03-30 09:48:17 -0400553 if (it == conv->interactions.end()) {
554 return [[NSView alloc] init];
555 }
556
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400557 auto interaction = it->second;
Anthony Léonard2382b562017-12-13 15:51:28 -0500558 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
559
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500560 switch (interaction.type) {
561 case lrc::api::interaction::Type::TEXT:
562 if (isOutgoing) {
563 result = [tableView makeViewWithIdentifier:@"RightMessageView" owner:self];
564 } else {
565 result = [tableView makeViewWithIdentifier:@"LeftMessageView" owner:self];
566 }
567 break;
Kateryna Kostiuk209a6302019-08-14 16:46:21 -0400568 case lrc::api::interaction::Type::DATA_TRANSFER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400569 return [self configureViewforTransfer:interaction interactionID: it->first tableView:tableView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500570 break;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500571 case lrc::api::interaction::Type::CONTACT:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400572 case lrc::api::interaction::Type::CALL: {
573 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
574 NSString* timeString = [self timeForMessage: msgTime];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400575 return [self makeGenericInteractionViewForTableView:tableView withText:interaction.body.toNSString() andTime:timeString];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400576 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500577 default: // If interaction is not of a known type
578 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400579 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400580 MessageSequencing sequence = [self computeSequencingFor:row];
581 BubbleType type = SINGLE;
582 if (sequence == FIRST_WITHOUT_TIME || sequence == FIRST_WITH_TIME) {
583 type = FIRST;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400584 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400585 if (sequence == MIDDLE_IN_SEQUENCE) {
586 type = MIDDLE;
587 }
588 if (sequence == LAST_IN_SEQUENCE) {
589 type = LAST;
590 }
591 result.msgBackground.type = type;
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400592 bool sendingFail = false;
593 [result.messageStatus setHidden:YES];
594 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
595 if (interaction.status == lrc::api::interaction::Status::SENDING) {
596 [result.messageStatus setHidden:NO];
597 [result.sendingMessageIndicator startAnimation:nil];
598 [result.messageFailed setHidden:YES];
Kateryna Kostiuk209a6302019-08-14 16:46:21 -0400599 } else if (interaction.status == lrc::api::interaction::Status::FAILURE) {
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400600 [result.messageStatus setHidden:NO];
601 [result.sendingMessageIndicator setHidden:YES];
602 [result.messageFailed setHidden:NO];
603 sendingFail = true;
604 }
605 }
606 [result setupForInteraction:it->first isFailed: sendingFail];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400607 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400608 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400609 [result.msgBackground setNeedsDisplay:YES];
610 [result setNeedsDisplay:YES];
611 [result.timeBox setNeedsDisplay:YES];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400612
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400613 NSString *text = interaction.body.toNSString();
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400614 text = [text removeEmptyLinesAtBorders];
615
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400616 NSMutableAttributedString* msgAttString =
Kateryna Kostiuk5a41a8d2020-05-13 09:05:56 -0400617 [[NSMutableAttributedString alloc] initWithString:text
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400618 attributes:[self messageAttributes]];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400619
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400620 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400621
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400622 [result updateMessageConstraint:messageSize.width andHeight:messageSize.height timeIsVisible:shouldDisplayTime isTopPadding: shouldApplyPadding];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400623 [[result.msgView textStorage] appendAttributedString:msgAttString];
Anthony Léonard2382b562017-12-13 15:51:28 -0500624
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400625 NSDataDetector *linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
626 NSArray *matches = [linkDetector matchesInString:result.msgView.string options:0 range:NSMakeRange(0, result.msgView.string.length)];
627
628 [result.msgView.textStorage beginEditing];
629
630 for (NSTextCheckingResult *match in matches) {
631 if (!match.URL) continue;
632
633 NSDictionary *linkAttributes = @{
634 NSLinkAttributeName: match.URL,
635 };
636 [result.msgView.textStorage addAttributes:linkAttributes range:match.range];
637 }
638
639 [result.msgView.textStorage endEditing];
640
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400641 if (shouldDisplayTime) {
642 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
643 NSString* timeString = [self timeForMessage: msgTime];
644 result.timeLabel.stringValue = timeString;
Anthony Léonard64e19672018-01-18 16:40:34 -0500645 }
646
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400647 bool shouldDisplayAvatar = (sequence != MIDDLE_IN_SEQUENCE && sequence != FIRST_WITHOUT_TIME
648 && sequence != FIRST_WITH_TIME) ? YES : NO;
649 [result.photoView setHidden:!shouldDisplayAvatar];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400650 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
651 auto image = QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)));
652 BOOL showIndicator = convModel_->isLastDisplayed(convUid_, it->first, conv->participants.front());
653 [result.readIndicator setHidden: !showIndicator];
654 [result.readIndicator setImage:image];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400655 if (!isOutgoing && shouldDisplayAvatar) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400656 [result.photoView setImage:image];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400657 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400658 return result;
659}
660
Anthony Léonard2382b562017-12-13 15:51:28 -0500661- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400662{
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400663 try {
664 double someWidth = tableView.frame.size.width * 0.7;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400665
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400666 auto* conv = [self getCurrentConversation];
Anthony Léonard2382b562017-12-13 15:51:28 -0500667
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400668 if (conv == nil)
669 return HEIGHT_DEFAULT;
Kateryna Kostiuk0c068552020-03-30 09:48:17 -0400670
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400671 auto size = [conversationView numberOfRows] - 1;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400672
673 if (row > size || row > conv->interactions.size()) {
674 return HEIGHT_DEFAULT;
675 }
676 if (row == size) {
677 return peerComposingMessage ? HEIGHT_FOR_COMPOSING_INDICATOR : DEFAULT_ROW_HEIGHT;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400678 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400679
680 auto it = conv->interactions.begin();
681
682 std::advance(it, row);
683
684 if (it == conv->interactions.end()) {
685 return HEIGHT_DEFAULT;
686 }
687
688 auto interaction = it->second;
689
690 MessageSequencing sequence = [self computeSequencingFor:row];
691
692 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
693
694 if(interaction.type == lrc::api::interaction::Type::DATA_TRANSFER) {
695 if( interaction.status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
696 NSString* name = interaction.body.toNSString();
697 NSImage* image = [self getImageForFilePath:name];
698 if (([name rangeOfString:@"/"].location == NSNotFound)) {
699 image = [self getImageForFilePath:[self getDataTransferPath:it->first]];
700 }
701 if (image != nil) {
702 CGFloat widthScaleFactor = MAX_TRANSFERED_IMAGE_SIZE / image.size.width;
703 CGFloat heightScaleFactor = MAX_TRANSFERED_IMAGE_SIZE / image.size.height;
704 CGFloat heigt = 0;
705 if((widthScaleFactor >= 1) && (heightScaleFactor >= 1)) {
706 heigt = image.size.height;
707 } else {
708 CGFloat scale = MIN(widthScaleFactor, heightScaleFactor);
709 heigt = image.size.height * scale;
710 }
711 return heigt + TIME_BOX_HEIGHT;
712 }
713 }
714 return BUBBLE_HEIGHT_FOR_TRANSFERED_FILE + TIME_BOX_HEIGHT;
715 }
716
717 if(interaction.type == lrc::api::interaction::Type::CONTACT || interaction.type == lrc::api::interaction::Type::CALL)
718 return GENERIC_CELL_HEIGHT;
719
720 NSString *text = interaction.body.toNSString();
721 text = [text removeEmptyLinesAtBorders];
722
723 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
724 CGFloat singleLignMessageHeight = 15;
725
726 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
727
728 if (shouldDisplayTime) {
729 return MAX(messageSize.height + TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2,
730 TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2 + singleLignMessageHeight);
731 }
732 if(shouldApplyPadding) {
733 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2 + 15,
734 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2 + 15);
735 }
736 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2,
737 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2);
738 } catch (std::out_of_range& e) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400739 return DEFAULT_ROW_HEIGHT;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400740 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400741}
742
743#pragma mark - message view parameters
744
745-(NSString *) getDataTransferPath:(uint64_t)interactionId {
746 lrc::api::datatransfer::Info info = {};
747 convModel_->getTransferInfo(interactionId, info);
748 double convertData = static_cast<double>(info.totalSize);
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400749 return info.path.toNSString();
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400750}
751
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400752-(NSImage*) getImageForFilePath: (NSString *) path {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400753 if (path.length <= 0) {return nil;}
754 if (![[NSFileManager defaultManager] fileExistsAtPath: path]) {return nil;}
755 NSImage* transferedImage = [[NSImage alloc] initWithContentsOfFile: path];
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400756 return transferedImage;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400757}
758
759-(CGSize) sizeFor:(NSString *) message maxWidth:(CGFloat) width {
760 CGFloat horizaontalMargin = 6;
Anthony Léonard2382b562017-12-13 15:51:28 -0500761 NSMutableAttributedString* msgAttString =
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400762 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@", message]
Anthony Léonard2382b562017-12-13 15:51:28 -0500763 attributes:[self messageAttributes]];
764
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400765 CGFloat finalWidth = MIN(msgAttString.size.width + horizaontalMargin * 2, width);
766 NSRect frame = NSMakeRect(0, 0, finalWidth, msgAttString.size.height);
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400767 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400768 [[tv textStorage] setAttributedString:msgAttString];
769 [tv sizeToFit];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400770 return tv.frame.size;
771}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400772
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400773-(MessageSequencing) computeSequencingFor:(NSInteger) row {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400774 try {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400775 auto* conv = [self getCurrentConversation];
776 if (row >= conversationView.numberOfRows - 1 || row >= conv->interactions.size()) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400777 return SINGLE_WITHOUT_TIME;
778 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400779 if (conv == nil)
780 return SINGLE_WITHOUT_TIME;
781 auto it = conv->interactions.begin();
782 std::advance(it, row);
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400783 if (it == conv->interactions.end()) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400784 return SINGLE_WITHOUT_TIME;
785 }
786 auto interaction = it->second;
787 if (interaction.type != lrc::api::interaction::Type::TEXT) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400788 return SINGLE_WITH_TIME;
789 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400790 // first message in comversation
791 if (row == 0) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400792 if (it == conv->interactions.end() || conv->interactions.size() < 2) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400793 return SINGLE_WITH_TIME;
794 }
795 auto nextIt = it;
796 nextIt++;
797 if (nextIt == conv->interactions.end()) {
798 return SINGLE_WITH_TIME;
799 }
800 auto nextInteraction = nextIt->second;
801 if ([self sequenceChangedFrom:interaction to: nextInteraction]) {
802 return SINGLE_WITH_TIME;
803 }
804 return FIRST_WITH_TIME;
805 }
806 // last message in comversation
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400807 if (row == conv->interactions.size() - 1) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400808 if(it == conv->interactions.begin()) {
809 return SINGLE_WITH_TIME;
810 }
811 auto previousIt = it;
812 previousIt--;
813 auto previousInteraction = previousIt->second;
814 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
815 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
816 if (!timeChanged && !authorChanged) {
817 return LAST_IN_SEQUENCE;
818 }
819 if (!timeChanged && authorChanged) {
820 return SINGLE_WITHOUT_TIME;
821 }
Kateryna Kostiukcd9397f2020-04-13 09:40:53 -0400822 return SINGLE_WITH_TIME;
823 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400824 // single message in comversation
825 if(it == conv->interactions.begin() || it == conv->interactions.end()) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400826 return SINGLE_WITH_TIME;
827 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400828 // message in the middle of conversation
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400829 auto previousIt = it;
830 previousIt--;
831 auto previousInteraction = previousIt->second;
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400832 auto nextIt = it;
833 nextIt++;
834 if (nextIt == conv->interactions.end()) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400835 return SINGLE_WITHOUT_TIME;
836 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400837 auto nextInteraction = nextIt->second;
838
839 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
840 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
841 bool sequenceWillChange = [self sequenceChangedFrom:interaction to: nextInteraction];
842 if (previousInteraction.type == lrc::api::interaction::Type::DATA_TRANSFER) {
843 if(!sequenceWillChange) {
844 return FIRST_WITH_TIME;
845 }
846 return SINGLE_WITH_TIME;
847 }
848 if (!sequenceWillChange) {
849 if (!timeChanged && !authorChanged) {
850 return MIDDLE_IN_SEQUENCE;
851 }
852 if (timeChanged) {
853 return FIRST_WITH_TIME;
854 }
855 return FIRST_WITHOUT_TIME;
856 } if (!timeChanged && !authorChanged) {
857 return LAST_IN_SEQUENCE;
858 } if (timeChanged) {
859 return SINGLE_WITH_TIME;
860 }
861 return SINGLE_WITHOUT_TIME;
862 } catch (std::out_of_range& e) {
Kateryna Kostiukcd9397f2020-04-13 09:40:53 -0400863 return SINGLE_WITHOUT_TIME;
864 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400865}
866
867-(bool) sequenceChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
868 return ([self sequenceTimeChangedFrom:firstInteraction to:secondInteraction] || [self sequenceAuthorChangedFrom:firstInteraction to:secondInteraction]);
869}
870
871-(bool) sequenceTimeChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
872 bool timeChanged = NO;
873 NSDate* firstMessageTime = [NSDate dateWithTimeIntervalSince1970:firstInteraction.timestamp];
874 NSDate* secondMessageTime = [NSDate dateWithTimeIntervalSince1970:secondInteraction.timestamp];
875 bool hourComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitHour];
876 bool minutComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitMinute];
877 if(hourComp != NSOrderedSame || minutComp != NSOrderedSame) {
878 timeChanged = YES;
879 }
880 return timeChanged;
881}
882
883-(bool) sequenceAuthorChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
884 bool authorChanged = YES;
885 bool isOutgoing = lrc::api::interaction::isOutgoing(firstInteraction);
886 if ((secondInteraction.type == lrc::api::interaction::Type::TEXT) && (isOutgoing == lrc::api::interaction::isOutgoing(secondInteraction))) {
887 authorChanged = NO;
888 }
889 return authorChanged;
890}
891
892-(NSString *)timeForMessage:(NSDate*) msgTime {
893 NSDate *today = [NSDate date];
894 NSDateFormatter *dateFormatter=[[NSDateFormatter alloc] init];
Kateryna Kostiukaf6d5e22018-06-12 15:00:00 -0400895 [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:[[NSLocale currentLocale] localeIdentifier]]];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400896 if ([[NSCalendar currentCalendar] compareDate:today
897 toDate:msgTime
898 toUnitGranularity:NSCalendarUnitYear]!= NSOrderedSame) {
Kateryna Kostiuk66406432019-11-09 17:20:34 -0500899 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterLongStyle timeStyle:NSDateFormatterMediumStyle];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400900 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400901 if ([[NSCalendar currentCalendar] compareDate:today
902 toDate:msgTime
903 toUnitGranularity:NSCalendarUnitDay]!= NSOrderedSame ||
904 [[NSCalendar currentCalendar] compareDate:today
905 toDate:msgTime
906 toUnitGranularity:NSCalendarUnitMonth]!= NSOrderedSame) {
Kateryna Kostiuk66406432019-11-09 17:20:34 -0500907 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterShortStyle];
908 }
909 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterNoStyle timeStyle:NSDateFormatterShortStyle];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400910}
911
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400912- (void) updateSendMessageHeight {
913 NSAttributedString *msgAttString = messageField.attributedStringValue;
914 NSRect frame = NSMakeRect(0, 0, messageField.frame.size.width, msgAttString.size.height);
915 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
916 [[tv textStorage] setAttributedString:msgAttString];
917 [tv sizeToFit];
918 CGFloat height = tv.frame.size.height + MEESAGE_MARGIN * 2;
919 CGFloat newHeight = MIN(SEND_PANEL_MAX_HEIGHT, MAX(SEND_PANEL_DEFAULT_HEIGHT, height));
920 if(messagesBottomMargin.constant == newHeight) {
921 return;
922 }
923 messagesBottomMargin.constant = newHeight;
924 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.05 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
925 [self scrollToBottom];
926 sendPanelHeight.constant = newHeight;
927 });
928}
929
Anthony Léonard2382b562017-12-13 15:51:28 -0500930#pragma mark - NSTableViewDataSource
931
932- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
933{
934 auto* conv = [self getCurrentConversation];
935
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400936 // return conversation +1 view for composing indicator
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500937 if (conv)
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400938 return conv->interactions.size() + 1;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500939 else
940 return 0;
Anthony Léonard2382b562017-12-13 15:51:28 -0500941}
942
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400943#pragma mark - Text formatting
944
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400945- (NSMutableDictionary*) messageAttributes
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400946{
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400947 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400948 attrs[NSForegroundColorAttributeName] = [NSColor labelColor];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400949 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400950 return attrs;
951}
952
953- (NSParagraphStyle*) paragraphStyle
954{
955 /*
956 The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
957 NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
958 to copy, we use the default one.
959
960 The default values supplied by the default NSParagraphStyle are:
961 Alignment NSNaturalTextAlignment
962 Tab stops 12 left-aligned tabs, spaced by 28.0 points
963 Line break mode NSLineBreakByWordWrapping
964 All others 0.0
965 */
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400966 NSMutableParagraphStyle* aMutableParagraphStyle =
967 [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
968 [aMutableParagraphStyle setHeadIndent:1.0];
969 [aMutableParagraphStyle setFirstLineHeadIndent:1.0];
970 return aMutableParagraphStyle;
971}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400972
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500973#pragma mark - Actions
974
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400975- (void)acceptIncomingFile:(id)sender {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400976 auto interId = [(IMTableCellView*)[[[[[[sender superview] superview] superview] superview] superview] superview] interaction];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500977 auto& inter = [self getCurrentConversation]->interactions.find(interId)->second;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400978 if (convModel_ && !convUid_.isEmpty()) {
Kateryna Kostiuk26405ac2020-05-27 14:25:13 -0400979 convModel_->acceptTransfer(convUid_, interId);
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500980 }
981}
982
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400983- (void)declineIncomingFile:(id)sender {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400984 auto inter = [(IMTableCellView*)[[[[[[sender superview] superview] superview] superview] superview] superview] interaction];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400985 if (convModel_ && !convUid_.isEmpty()) {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500986 convModel_->cancelTransfer(convUid_, inter);
987 }
988}
989
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400990- (void)imagePreview:(id)sender {
991 uint64_t interId;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400992 if ([[[[[[sender superview] superview] superview] superview] superview] isKindOfClass:[IMTableCellView class]]) {
993 interId = [(IMTableCellView*)[[[[[sender superview] superview] superview] superview] superview] interaction];
994 } else if ([[[[[sender superview] superview] superview] superview] isKindOfClass:[IMTableCellView class]]) {
995 interId = [(IMTableCellView*)[[[[sender superview] superview] superview] superview] interaction];
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400996 } else {
997 return;
998 }
999 auto it = [self getCurrentConversation]->interactions.find(interId);
1000 if (it == [self getCurrentConversation]->interactions.end()) {
1001 return;
1002 }
1003 auto& interaction = it->second;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001004 NSString* name = interaction.body.toNSString();
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001005 if (([name rangeOfString:@"/"].location == NSNotFound)) {
1006 name = [self getDataTransferPath:interId];
1007 }
1008 previewImage = name;
Kateryna Kostiuk30c6ac22020-05-06 17:42:59 -04001009 if (!previewImage || previewImage.length <= 0) {
1010 return;
1011 }
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001012 [self addToResponderChain];
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001013 if ([QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]) {
1014 [[QLPreviewPanel sharedPreviewPanel] orderOut:nil];
1015 } else {
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001016 dispatch_async(dispatch_get_main_queue(), ^{
1017 [[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:self];
1018 });
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001019 }
1020}
1021
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001022- (IBAction)sendMessage:(id)sender {
1023 NSString* text = self.message;
Kateryna Kostiuka8b6b562019-02-01 13:26:18 -05001024 unichar separatorChar = NSLineSeparatorCharacter;
1025 NSString *separatorString = [NSString stringWithCharacters:&separatorChar length:1];
1026 text = [text stringByReplacingOccurrencesOfString: separatorString withString: @"\n"];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001027 if (text && text.length > 0) {
1028 auto* conv = [self getCurrentConversation];
Kateryna Kostiuk0c068552020-03-30 09:48:17 -04001029 if (conv == nil)
1030 return;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001031 convModel_->sendMessage(convUid_, QString::fromNSString(text));
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001032 self.message = @"";
1033 if(sendPanelHeight.constant != SEND_PANEL_DEFAULT_HEIGHT) {
1034 sendPanelHeight.constant = SEND_PANEL_DEFAULT_HEIGHT;
1035 messagesBottomMargin.constant = SEND_PANEL_DEFAULT_HEIGHT;
1036 [self scrollToBottom];
1037 }
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -04001038 if (composingMessage) {
1039 composingMessage = false;
1040 convModel_->setIsComposing(convUid_, composingMessage);
1041 }
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001042 }
1043}
1044
1045- (IBAction)openEmojy:(id)sender {
1046 [messageField.window makeFirstResponder: messageField];
1047 [[messageField currentEditor] moveToEndOfLine:nil];
1048 [NSApp orderFrontCharacterPalette: messageField];
1049}
1050
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001051- (IBAction)startVideoMessage:(id)sender
1052{
1053 [self startRecording:NO];
1054}
1055
1056- (IBAction)startAudioMessage:(id)sender
1057{
1058 [self startRecording:YES];
1059}
1060-(void) startRecording:(BOOL)isAudio {
1061 if (recordingController == nil) {
1062 recordingController = [[RecordFileVC alloc] initWithNibName:@"RecordFileVC" bundle:nil avModel: self->avModel];
1063 recordingController.delegate = self;
1064 }
1065 if(recordMessagePopover != nil)
1066 {
1067 [self closeRecordingView];
1068 return;
1069 }
1070 recordMessagePopover = [[NSPopover alloc] init];
1071 [recordingController prepareRecordingView: isAudio];
1072 [recordMessagePopover setContentSize: recordingController.view.frame.size];
1073 [recordMessagePopover setContentViewController:recordingController];
1074 [recordMessagePopover setAnimates:YES];
1075 NSButton *anchorButton = isAudio ? recordAudioButton : recordVideoButton;
1076 [recordMessagePopover showRelativeToRect: anchorButton.bounds
1077 ofView: anchorButton
1078 preferredEdge: NSMaxYEdge];
1079}
1080
1081-(void) sendFile:(NSString *) name withFilePath:(NSString *) path {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001082 convModel_->sendFile(convUid_, QString::fromNSString(path), QString::fromNSString(name));
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001083}
1084
1085-(void) closeRecordingView {
1086 if(recordMessagePopover != nil) {
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001087 [recordMessagePopover close];
1088 recordMessagePopover = nil;
Kateryna Kostiuka7404812019-10-28 12:24:46 -04001089 recordingController.stopRecordingView;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001090 }
1091}
1092
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001093- (IBAction)sendFile:(id)sender {
1094 NSOpenPanel* filePicker = [NSOpenPanel openPanel];
1095 [filePicker setCanChooseFiles:YES];
1096 [filePicker setCanChooseDirectories:NO];
1097 [filePicker setAllowsMultipleSelection:NO];
1098
1099 if ([filePicker runModal] == NSFileHandlingPanelOKButton) {
1100 if ([[filePicker URLs] count] == 1) {
1101 NSURL* url = [[filePicker URLs] objectAtIndex:0];
1102 const char* fullPath = [url fileSystemRepresentation];
1103 NSString* fileName = [url lastPathComponent];
1104 if (convModel_) {
1105 auto* conv = [self getCurrentConversation];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001106 convModel_->sendFile(convUid_, QString::fromStdString(fullPath), QString::fromNSString(fileName));
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001107 }
1108 }
1109 }
1110}
1111
1112
1113#pragma mark - NSTextFieldDelegate
1114
1115- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector
1116{
1117 if (commandSelector == @selector(insertNewline:)) {
1118 if(self.message.length > 0) {
1119 [self sendMessage: nil];
1120 } else if(messagesBottomMargin.constant != SEND_PANEL_DEFAULT_HEIGHT) {
1121 sendPanelHeight.constant = SEND_PANEL_DEFAULT_HEIGHT;
1122 messagesBottomMargin.constant = SEND_PANEL_DEFAULT_HEIGHT;
1123 [self scrollToBottom];
1124 }
1125 return YES;
1126 }
1127 return NO;
1128}
1129
1130- (void)controlTextDidChange:(NSNotification *)aNotification {
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -04001131 [self checkIfcomposingMsg];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001132 [self updateSendMessageHeight];
1133}
1134
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -04001135- (void) checkIfcomposingMsg {
1136 [self updateSendMessageHeight];
1137 BOOL haveText = [messageField.stringValue removeEmptyLinesAtBorders].length != 0;
1138 if (haveText != composingMessage) {
1139 composingMessage = haveText;
1140 convModel_->setIsComposing(convUid_, composingMessage);
1141 }
1142}
1143
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001144#pragma mark - QLPreviewPanelDataSource
1145
1146-(void)beginPreviewPanelControl:(QLPreviewPanel *)panel
1147{
1148 panel.dataSource = self;
1149}
1150
1151- (void)endPreviewPanelControl:(QLPreviewPanel *)panel {
1152 panel.dataSource = nil;
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001153 [self removeFromResponderChain];
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001154}
1155
1156-(BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel
1157{
1158 return YES;
1159}
1160
1161- (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel *)panel {
1162 return 1;
1163}
1164
1165- (id <QLPreviewItem>)previewPanel:(QLPreviewPanel *)panel previewItemAtIndex:(NSInteger)index {
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001166 if (previewImage == nil) {
1167 return nil;
1168 }
Kateryna Kostiuk30c6ac22020-05-06 17:42:59 -04001169 try {
1170 return [NSURL fileURLWithPath: previewImage];
1171 } catch (NSException *exception) {
1172 nil;
1173 }
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001174}
1175
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001176- (void)addToResponderChain {
1177 if (conversationView.window &&
1178 ![[conversationView.window nextResponder] isEqual:self]) {
1179 NSResponder * aNextResponder = [conversationView.window nextResponder];
1180 [conversationView.window setNextResponder:self];
1181 }
1182}
1183
1184
1185- (void)removeFromResponderChain {
1186 if (conversationView.window &&
1187 [[conversationView.window nextResponder] isEqual:self]) {
1188 NSResponder * aNextResponder = [conversationView.window nextResponder];
1189 [conversationView.window setNextResponder:[self nextResponder]];
1190 }
1191}
1192
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04001193@end